跳到主要内容

一条聊天请求的生命周期:从 HTTP 入口到流式出口

30 秒导读: 你在 Open WebUI 聊天框敲下一句话,后端要经历"装配上下文 → 选后端发出去 → 处理回来的流"三步才把答案吐回你屏幕。这一章把这条主干端到端串一遍,建立一张骨架心智模型;工具、记忆、RAG、pipe 的内部各自有专章,这里只当黑盒。

本章是全组的"地图总纲"。读完你应该能对着源码说清:一个 HTTP 请求进来后,数据在哪几个函数之间流转、哪个开关决定了后面走哪条路、答案是怎么一块一块推到前端的。细节下钻见 02 入口装配03 工具系统04 智能体循环05 插件框架


1. 这是什么(零基础也能懂)

一句话: Open WebUI 的后端把"OpenAI 兼容的聊天接口"包了一层——你以为是直连模型,其实中间插了一整条管线,负责在发给模型前塞进各种上下文、在收到模型回复后做各种加工。

一个最小的真实请求长这样: 前端(或任何脚本)POST 一份 OpenAI 风格的 payload 到 /api/chat/completions:

{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "帮我查一下今天的天气"}],
"stream": true,
"chat_id": "abc123",
"features": {"web_search": true},
"params": {"function_calling": "native"}
}

注意这份 payload 里有一堆标准 OpenAI 没有的字段:chat_idfeaturesparams.function_callingtool_idsfiles……它们正是 Open WebUI 管线的"控制旋钮"。入口的第一件事,就是把这些旋钮从 payload 里摘出来,收进一个叫 metadata 的盒子——业务逻辑全程读这个盒子,而不再碰原始 payload。

一句话直觉: 把这条管线想成机场安检+登机口:

你的请求(行李)


[安检装配] ── 塞进记忆/联网结果/RAG/工具清单 ← inlet


[登机口路由] ── 按机型送上不同航司的飞机(OpenAI/Ollama/pipe) ← 发给后端


[到达处理] ── 拆包、加工、一段一段广播给接机的人 ← outlet

本节不碰代码,记住三段就够:装配 → 路由 → 处理


2. 顶层全景(它大概怎么转)

2.1 一张主干图

先看总流向。从上往下读,是一条请求的时间顺序;右侧标了真实函数与文件。

HTTP POST /api/chat/completions


┌─────────────────────────────────────────────┐
│ chat_completion() main.py:1666 │ ① 入口:校验模型/权限
│ ├─ 组装 metadata main.py:1773 │ 摘旋钮、落库占位消息
│ └─ 内嵌 process_chat() main.py:2021 │
└───────────────┬─────────────────────────────┘
│ 按 model 数量分叉
┌───────┴────────┐
▼ ▼
多模型:每模型 单模型/直连:
起一个后台 task 同步跑一遍
main.py:2139 main.py:2202
└───────┬────────┘
▼ (每个 task 内部都是同一条三段式管线)
┌─────────────────────────────────────────────┐
│ ② process_chat_payload() middleware.py:2311 │ inlet 装配
│ 塞入记忆/联网/RAG/图像/工具 │ → 见 02、03 章
└───────────────┬─────────────────────────────┘

┌─────────────────────────────────────────────┐
│ ③ generate_chat_completion() chat.py:152 │ 后端路由
│ 按 pipe / ollama / openai / arena 分派 │ → 见 05 章
└───────────────┬─────────────────────────────┘

┌─────────────────────────────────────────────┐
│ ④ process_chat_response() middleware.py:5259│ outlet 处理
│ 分叉:streaming vs 非流式 │ → 见 04 章
└───────────────┬─────────────────────────────┘

SSE 流 / JSON ── event_emitter 实时推给前端

2.2 主干部件一句话职责

部件干什么在哪
chat_completionHTTP 入口:校验、组装 metadata、落库、决定同步还是起后台任务main.py:1666
process_chat(内嵌)把三段式管线串起来的协程,一个模型一份main.py:2021
process_chat_payload① inlet:把"特性"变成实际的消息/工具/上下文middleware.py:2311
generate_chat_completion② 路由:按模型类型把请求发给正确的后端utils/chat.py:152
process_chat_response③ outlet:处理返回,分流 streaming / 非流式middleware.py:5259
get_event_emitter造一个"广播器":把中间状态推给前端并落库socket/main.py:894

注意别混淆:main.py 里也有个叫 generate_chat_completion 的名字(是 chat_completion 的别名,main.py:2210),那是HTTP 入口;真正做后端路由的是 utils/chat.py:152 的同名函数。process_chat 里通过 chat_completion_handler 这个别名调用它(在 main.py:560 import 时改的名)。

2.3 主线走一遍(高层)

  1. 请求进 chat_completion(main.py:1666):没加载模型先加载,查权限,把 payload 里的控制旋钮摘进 metadata,给新会话/新消息在数据库里建占位。
  2. 定义内嵌协程 process_chat(main.py:2021),它顺序执行三段:装配 → 路由 → 处理(main.py:2023/2025/2044)。
  3. 分叉:多模型对比场景下,每个模型起一个后台任务跑各自的 process_chat(main.py:2139);老式/直连单模型则同步跑一遍(main.py:2202)。
  4. 管线跑起来后,一切中间状态(状态条、正文增量、引用)都通过 event_emitter 走 WebSocket 实时推给前端,HTTP 响应本身只是 SSE 流或最终 JSON。

3. 核心原理(逐段拆解)

3.1 入口:metadata 是怎么组装出来的

它解决的小问题: OpenAI 的 payload 只认 model/messages/stream 这几样;Open WebUI 需要额外携带 chat_id、启用了哪些特性、挂了哪些工具、走哪种函数调用模式……怎么带、又不污染发给模型的 payload?

思路: 入口把这些"额外字段"从 form_datapop / get 出来,统一装进一个 metadata 字典;之后管线读 metadata,而"要发给模型的干净 payload"仍是 form_data

真实组装在 main.py:1773(chat_completion 内):

# 节选,main.py:1773 附近 —— 真实源码
metadata = {
'user_id': user.id,
'chat_id': form_data.pop('chat_id', None) or '',
'session_id': form_data.pop('session_id', None),
'filter_ids': form_data.pop('filter_ids', []),
'tool_ids': form_data.get('tool_ids', None),
'files': form_data.get('files', None),
'features': form_data.get('features', {}),
'model': model,
'direct': model_item.get('direct', False),
'params': { ... 'function_calling': ... }, # 见 3.2
}

引完点一句:注意 session_idpop(带走,不发给模型),而 features/tool_idsget(留副本给下游看)——这个 pop/get 的细微区分,决定了哪些字段会"泄漏"进发给模型的 payload。

入口还顺手做了两件"副作用"活,但它们不在主干上,知道即可:

副作用干什么位置
落库占位新会话建 chat、给每条 assistant 消息写空占位(content:'', done:False)main.py:1849main.py:1992
权限闸校验模型访问、channel 会话的成员/写权限main.py:1691main.py:1811

3.2 贯穿全程的开关:function_calling == 'native'

为什么单独讲它: 这是整条管线上影响面最大的一个布尔开关。它在入口被算出来、写进 metadata['params'],然后在装配段、出口段被反复查询,决定工具到底怎么调。

来源(入口算): 请求级或模型级任一处设了 native 就是 native,否则 default(main.py:1792):

# 节选,main.py:1792 —— 真实源码
'function_calling': (
'native'
if (
form_data.get('params', {}).get('function_calling') == 'native'
or model_info_params.get('function_calling') == 'native'
)
else 'default'
),

两种模式的区别(白话):

模式谁来决定调哪个工具管线怎么走
native模型自己(标准 OpenAI tools 字段 + tool_calls)装配段把工具塞进 form_data['tools'],交给模型;出口段跑迭代循环
defaultOpen WebUI 先跑一个"选工具"的辅助请求装配段就地调 chat_completion_tools_handler,把结果拼进 prompt

贯穿证据: 同一个开关在装配段被查了很多次——native 时注入内置工具(middleware.py:2846)、把工具挂成 form_data['tools'](middleware.py:2869);非 native 时改走 chat_completion_tools_handler(middleware.py:2879);连折叠文件、注入 RAG 上下文的分支也看它(如 middleware.py:2474)。工具的细节看 03 章,迭代循环看 04 章——这里只要记住:这个开关是那两章共同的总闸。

3.3 三段式管线:装配 → 路由 → 处理

这是本章的心脏。process_chat(main.py:2021)本体极短,就是把三段顺序拼起来:

# 节选,main.py:2023 —— 真实源码(process_chat 内)
form_data, metadata, events = await process_chat_payload(request, form_data, user, metadata, model) # ①
response = await chat_completion_handler(request, form_data, user) # ②
ctx = await build_chat_response_context(request, form_data, user, model, metadata, tasks, events) # ③ 准备上下文
return await process_chat_response(response, ctx) # ③ 处理

① 装配(inlet)—— process_chat_payload(middleware.py:2311)

把"用户勾了哪些特性"翻译成"模型真能看到/能用的东西":记忆、联网结果、RAG 文档、图像、工具清单,全在这一段塞进 form_data['messages']form_data['tools']。它开头一行注释就写明了内部顺序:

Pipeline Inlet → Filter Inlet → Chat Memory → Chat Web Search
→ Chat Image Generation → Chat Code Interpreter → (Default) Tools → Chat Files

返回三样:加工后的 form_data、更新过的 metadata、一个 events 列表(要顺带推给前端的额外事件,如引用来源)(middleware.py:2939)。这一整段的展开是 02 章 的主题,本章当黑盒。

② 路由 —— generate_chat_completion(utils/chat.py:152)

装配好的干净请求要发给"正确的后端"。这个函数就是分派器,按模型属性依次判断(utils/chat.py 尾部):

┌─ request.state.direct 且是直连模型?
│ └→ generate_direct_chat_completion (chat.py:196 → :50)
model 属性判断 ────────┤
(chat.py:274-299) ├─ model.get('pipe') ? → 自定义函数模型 generate_function_chat_completion (05 章)
├─ owned_by == 'ollama'? → Ollama 后端 (转成 ollama payload)
└─ 否则 → OpenAI 后端 generate_openai_chat_completion

补一条:owned_by == 'arena'(模型竞技场)在更早就被处理——装配段 process_chat_payload 会先随机挑一个子模型顶上(middleware.py:2323),这里只是兜底再挑一次(utils/chat.py:218)。真实分派片段:

# 节选,utils/chat.py:274 —— 真实源码
if model.get('pipe'):
return await generate_function_chat_completion(request, form_data, user=user, models=models)
if model.get('owned_by') == 'ollama':
... # 转 ollama payload,再包成 OpenAI 兼容的流
else:
return await generate_openai_chat_completion(request=request, form_data=form_data, user=user)

"直连"是什么? 当请求带 model_item.direct=True(用户在浏览器里配的私有连接),后端自己不发 HTTP,而是通过 WebSocket 回调到用户那个浏览器会话去发请求——generate_direct_chat_completion(utils/chat.py:50)发一条 request:chat:completion 事件,等前端把结果推回来。所以它硬性要求有活跃 WebSocket,后台任务里用不了(utils/chat.py:65)。

③ 处理(outlet)—— process_chat_response(middleware.py:5259)

拿到后端返回后做加工、落库、推前端。它先靠 build_chat_response_context(middleware.py:2960)打包一个 ctx(把 request/model/metadata/events + 两个事件通道塞进一个字典),再进 process_chat_response 分叉。分叉规则见 3.5。

3.4 event_emitter / event_caller:把中间状态推给前端

它解决的小问题: 管线跑几秒甚至几十秒,用户不能干等黑屏。需要一条旁路通道,在 HTTP 响应之外把"正在联网""检索到 3 篇文档""正文又多了一段"实时推过去。

两个通道,别搞混:

通道方向需要什么干什么
event_emitter后端 → 前端(广播)user_id+chat_id+message_iduser:{id} 房间广播事件并落库;后台任务也能用
event_caller后端 → 某个浏览器会话(点对点+等回值)额外要 session_id直连工具、pyodide 代码解释器回调客户端执行

两者由 get_event_emitter_and_caller(middleware.py:2942)按 metadata 里有没有对应字段条件构造——没有 message_id 就返回 None(如纯 API 调用没有前端可推):

# 节选,middleware.py:2949 —— 真实源码
if metadata.get('chat_id') and metadata.get('message_id'):
event_emitter = await get_event_emitter(metadata)
if metadata.get('session_id') and metadata.get('chat_id') and metadata.get('message_id'):
event_caller = await get_event_call(metadata)

event_emitter 的实现(socket/main.py:894)干两件事:sio.emit 广播 + 按事件类型(message/replace/status/source…)把内容增量写回数据库。所以就算前端刷新页面,已推出的内容也不会丢——"推给前端"和"落库"是同一次调用完成的

3.5 出口分叉:streaming vs 非流式

为什么会分叉: 后端可能返回一个(StreamingResponse,stream:true),也可能返回一坨完整 JSON(非流式)。两者加工方式完全不同,所以 process_chat_response 第一件事就是判类型分派:

# 节选,middleware.py:5261 —— 真实源码
if not isinstance(response, StreamingResponse):
return await non_streaming_chat_response_handler(response, ctx) # 非流式:middleware.py:3424
...
return await streaming_chat_response_handler(response, ctx) # 流式:middleware.py:3571

两条分支的差异:

非流式 non_streaming_...:3424流式 streaming_...:3571
输入一份完整 JSON/字典一个 SSE 异步迭代器
加工choices[0].message.content,一次性 chat:completion 事件推出并落库起一个后台任务逐块消费,边解析特殊标签(reasoning/code)边增量推 message 事件
工具循环无(单轮)native 工具调用在这里迭代(→ 04 章)
返回给 HTTP加工后的 JSON一个新的 StreamingResponse

流式分支明显更重:它在 middleware.py:3585 重新组了一份 extra_params(带上 event_emitter/event_caller),然后在后台协程里跑标签检测与工具迭代。这块是 04 章 的正题,本章只标出"分叉在这里、两条路各自去哪"。


4. 分叉的另一面:多模型 vs 单模型

process_chat 定义完后,chat_completion 还有最后一个分叉——同一条请求要跑几个模型?

  • session_id 且有 chat_id(多模型对比) → 对 message_ids 里每个模型起一个后台 task 跑独立的 process_chat,各带自己的 message_id,立刻返回一组 task_ids(main.py:2139-2201)。只有第一个模型跑标题/标签生成,其余只跑后续任务(main.py:2166)。
  • 否则(老式/直连,单模型) → 直接 await process_chat(...) 同步跑完再返回(main.py:2202)。
chat_completion 尾部 (main.py:2140)

session_id+chat_id?│
┌───────────────┴───────────────┐
是 否
▼ ▼
for 每个模型:create_task( metadata['message_id']=第一个
process_chat) → task_ids return await process_chat(...)
立即返回 {task_ids} (:2197) 同步返回结果 (:2205)

这解释了为什么大多数聊天是"火了就走"的异步模式: 后台任务跑管线,前端不靠 HTTP 响应拿正文,而是靠 3.4 那条 event_emitter 通道边跑边收。HTTP 那条 return {task_ids} 只是告诉前端"任务已受理"。

顺带一提:/api/chat/completed(main.py:2278)如今是遗留端点——outlet 过滤器已内联进上面的管线,它只为老集成保留兼容。


5. 巧妙之处(值得带走)

  • 一个 metadata 盒子贯穿全程。 入口一次性把所有非标准控制字段收进 metadata(main.py:1773),下游只读它、不碰原始 payload——既保证发给模型的 payload 干净,又让"这次请求的意图"有单一事实源。
  • function_calling 开关只算一次、查很多次。 在入口算好写进 metadata(main.py:1792),装配段和出口段反复查(middleware.py:2846/2869)。开关集中、行为分散,是控制这类多分支管线的干净做法。
  • "推前端"与"落库"合一。 event_emitter 一次调用既广播又增量写库(socket/main.py:914),天然抗刷新、抗断连,不用前端事后回捞。
  • 管线三段用同一份签名串联。 process_chat 本体只有三四行(main.py:2023),复杂度全下沉到三个函数里——想读懂主干,盯这三行就够;想深入,顺着函数名跳。

6. 边界与局限

  • 直连模型离不开 WebSocket。 generate_direct_chat_completion 要求活跃会话,后台任务(标题/标签生成)里调直连模型会直接抛异常(utils/chat.py:65)。
  • 纯 API 调用没有前端可推。 没有 message_id/session_idevent_emitter/event_callerNone(middleware.py:2949),中间状态无处广播,只能靠最终 HTTP 响应;错误也走 HTTP 400 而非 WebSocket 事件(main.py:2090)。
  • 本章是黑盒视角。 装配段内部(记忆/联网/RAG 顺序)、工具的四类来源、native 迭代循环、pipe/filter 机制都没有展开——分别是 02/03/04/05 章的正题。

7. 代码地图(导航索引)

主题文件路径符号名
HTTP 入口backend/open_webui/main.py:1666chat_completion
metadata 组装backend/open_webui/main.py:1773chat_completion(内联 dict)
function_calling 开关backend/open_webui/main.py:1792metadata['params']['function_calling']
三段式串联backend/open_webui/main.py:2021process_chat
多模型 fan-outbackend/open_webui/main.py:2139chat_completion(create_task 循环)
遗留 outlet 端点backend/open_webui/main.py:2278chat_completed
① inlet 装配backend/open_webui/utils/middleware.py:2311process_chat_payload
② 后端路由backend/open_webui/utils/chat.py:152generate_chat_completion
直连回调路由backend/open_webui/utils/chat.py:50generate_direct_chat_completion
事件通道构造backend/open_webui/utils/middleware.py:2942get_event_emitter_and_caller
出口上下文打包backend/open_webui/utils/middleware.py:2960build_chat_response_context
③ outlet 分叉backend/open_webui/utils/middleware.py:5259process_chat_response
非流式处理backend/open_webui/utils/middleware.py:3424non_streaming_chat_response_handler
流式处理backend/open_webui/utils/middleware.py:3571streaming_chat_response_handler
广播器(推+落库)backend/open_webui/socket/main.py:894get_event_emitter
点对点回调器backend/open_webui/socket/main.py:1014get_event_call

下一步: 想看装配段怎么把"特性"变成能力 → 02 入口装配;工具从哪来、两种调用模式 → 03 工具系统;native 循环怎么迭代 → 04 智能体循环;pipe/filter 插件 → 05 插件框架