数据截至 (上游 commit 01f4282f1ffe)
智能体循环与出口处理:原生工具调用如何迭代
30 秒导读: 前面几章把"能力"装好了(记忆 / 联网 / RAG / 工具),这一章讲它们怎么真正跑起来。 当模型开始流式吐字,出口管线一边把碎片攒成结构化的
output列表,一边盯着有没有tool_calls; 一旦有,就逐个执行工具 → 把结果当成tool消息塞回对话 → 再调用一次模型,这样一圈一圈转, 直到模型不再要工具、只给最终答复为止。这个"转圈"就是 Open WebUI 的智能体内核。
本章聚焦原生工具调用(native function calling) 的迭代循环。工具从哪来、非原生的选择器模式, 见 工具系统;一条请求怎么走到这里,见 请求生命周期。
1. 这章讲什么(先建立直觉)
先厘清一个常见误解:模型自己不会执行任何工具。它只会在流里说一句"我想调用 search_web,参数是 {...}"。
真正把这句话落地执行、拿到结果、再喂回去让它继续想的,是宿主程序——在 Open WebUI 里,就是这一章讲的出口管线。
所以"智能体"不是一个魔法组件,而是一个朴素的循环:
模型流式输出 ──► 攒成 output 列表 ──► 有 tool_calls 吗?
│
┌───── 有 ──┴── 没有 ─────┐
▼ ▼
逐个执行工具 收尾:标完成
│ 跑 outlet filter
结果转成 tool 消息 跑后台任务(标题/标签)
│ │
再调用一次模型 ◄──(回到顶部) 结束
这一圈里有四件核心的事,本章按顺序讲:
| 步骤 | 干什么 | 主要函数 |
|---|---|---|
| ① 累积 | 把流式 delta 碎片攒成 output 列表 + tool_calls | stream_body_handler(middleware.py:4170) |
| ② 执行 | 逐个跑工具、把结果规整成字符串 | process_tool_result(middleware.py:871) |
| ③ 回灌 | 结果作为 tool 消息塞回,再调用模型 | convert_output_to_messages + generate_chat_completion(middleware.py:5239、4877) |
| ④ 收尾 | 标完成、跑 outlet filter、跑后台后处理 | outlet_filter_handler(3274)、background_tasks_handler(3060) |
全部逻辑集中在一个文件:backend/open_webui/utils/middleware.py。入口是 process_chat_response。
2. 出口全景:两个入口 + 一个循环
出口只有一个门:process_chat_response(response, ctx)(middleware.py:5656)。它先分流:
process_chat_response (5259)
│
├─ 非 StreamingResponse ─────► non_streaming_chat_response_handler (3424)
│
├─ 不是 event-stream/ndjson ─► 原样返回(非标准响应)
│
└─ 流式响应 ─────────────────► streaming_chat_response_handler (3571) ← 本章主角
本章只讲流式路径,因为智能体循环几乎全在 streaming_chat_response_handler 里(middleware.py:3750)。
它内部又套了一个 response_handler(3608),作为后台任务运行;真正逐行读流的是嵌套函数
stream_body_handler(3921)。
关键结构:stream_body_handler 只负责"读完一次响应",把这次响应里的文本、推理、工具调用都攒好。
外层用一个 while tool_calls(middleware.py:4916)反复:每转一圈就执行一批工具、再调用模型、再喂给
stream_body_handler 读下一次响应。"读一次响应"和"转一圈"是两个不同的循环层级,别混。
response_handler (3608)
└─ stream_body_handler(第 1 次响应) ← 读初始流,攒 output + tool_calls
│
└─ while tool_calls: ← 智能体循环(4534)
执行这一批工具
结果 → function_call_output 塞进 output
convert_output_to_messages → new_form_data
generate_chat_completion(...) ← 再调用模型(4877)
stream_body_handler(下一次响应) ← 读新流,可能又攒出 tool_calls
循环直到 tool_calls 空 或 触顶
│
└─ 收尾:标完成 → outlet_filter_handler → background_tasks_handler
3. 累积:从流式 delta 到 output 列表
3.1 output 列表是什么
Open WebUI 内部用一个与 OpenAI Responses API 对齐的 output 列表表示一条助手消息的全部内容,
而不是一根扁平字符串。列表里每个元素是一个"item",有几种类型:
| item 类型 | 代表什么 |
|---|---|
message | 一段助手可见文本(content 里是 output_text 片段) |
reasoning | 一段思维链(<think> 之类) |
function_call | 模型发起的一次工具调用(名字 + 参数) |
function_call_output | 该工具的执行结果(回灌用) |
open_webui:code_interpreter | 代码解释器块 |
之所以用结构化列表,是因为一条助手消息里可能穿插着"想一段 → 调个工具 → 再说一段 → 再调工具",
扁平字符串表达不了这种交错。output 列表按时间顺序把它们一节一节码起来。