数据截至 (上游 commit 01f4282f1ffe)
入口装配:把"特性"变成智能体能力(记忆 / 联网 / RAG / 图像)
30 秒导读: 用户在 Open WebUI 里勾选了"联网搜索""引用知识库""生成图片"这些开关,模型本身并不知道这些开关。本章讲的就是那个"翻译层"——
process_chat_payload这条 inlet 管线,如何在请求真正发给模型前,把每一个被开启的"特性(feature)"跑一遍、拿到结果、塞进消息里,让一个只会读文字的模型看起来"有了记忆、能上网、会查资料、能画图"。全章围绕一个核心设计张力:当函数调用是native时,这些"强制注入"几乎全部关闭,改由模型自己决定要不要调工具。
本章属于 Open WebUI 聊天编排系列的第 2 章。上游是 01 请求生命周期(HTTP 入口如何走到这里),下游是 03 工具系统 与 04 智能体循环(工具 schema 与调用循环)。inlet filter 的实现细节在 05 插件框架。全景与阅读地图见 index。
1. 先建立直觉:模型缺什么,这一层补什么
一个大语言模型,本质上只会做一件事:读一段文字,续写一段文字。它没有记忆、不能上网、不知道你上传的 PDF 里写了什么、也画不出图。
Open WebUI 的聊天界面却让它看起来什么都会。秘密不在模型,而在发给模型之前那一刻:系统偷偷把"你需要的外部信息"提前查好,拼进这次对话的消息里。模型读到的,已经是一份"作弊小抄"。
- 开了记忆(memory) → 系统先去向量库里捞出跟你这句话相关的历史片段,拼成一段
User Context:塞进系统提示。 - 开了联网(web_search) → 系统先真的去搜一遍,把网页抓成文档,走 RAG 变成
<source>引用塞进去。 - 上传了文件 / 引用了知识库(RAG) → 系统先检索出最相关的几段,同样塞进去。
- 开了图像生成(image_generation) → 系统直接把图生成好,再塞一句"图已经生成好了,请告诉用户"给模型。
这个"提前查好、拼进消息"的动作,就是本章的主角:inlet 装配管线。它把界面上的开关(feature),变成模型能感知的上下文(context)。
一句话类比: 模型是一位闭卷考试的学霸,inlet 管线是考前帮它把小抄写进卷子背面的助教。助教写什么、写多少,取决于你勾了哪些开关——但如果这位学霸自己有"举手要资料"的能力(native 函数调用),助教就不再硬塞小抄,而是让它自己举手。
2. 顶层全景:一条请求怎么被装配
装配的全部逻辑集中在一个函数:process_chat_payload(backend/open_webui/utils/middleware.py:2248)。它接收原始 form_data(用户消息 + 各种开关),返回三样东西:装配好的 form_data、更新后的 metadata、以及要额外发给前端的 events。
函数开头有一行注释,直接写出了这条流水线的顺序(middleware.py:2253):
Pipeline Inlet → Filter Inlet → Chat Memory → Chat Web Search → Chat Image Generation → Chat Code Interpreter → Chat Tools Function Calling → Chat Files
怎么读下面这张图: 从上到下是执行顺序,请求像流水线上的零件依次经过每一站,每一站可能往消息里"加料"。菱形是那个贯穿全程的判断——是不是 native 函数调用。
form_data (messages + features + tools + files)
│
┌────────────▼─────────────┐
│ apply_params_to_form_data │ 参数落进 payload(temperature 等)
│ (:2068 / 调用 :2349) │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ process_messages_with_ │ 把历史 assistant.output 展开成
│ output (:2192) │ OpenAI 风格的 tool_calls+结果
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ pipeline inlet filter │ 外部 pipeline / filter 函数
│ + process_filter_functions│ 先跑(细节见 05 章)
└────────────┬─────────────┘
│
features = pop('features') ← 界面开关在这里被取出
│
╔═════════════════▼═══════════════════════╗
║ function_calling == 'native' ? ║ ◀── 全章核心的岔路口
╚═══════┬═════════════════════════┬═══════╝
非 native │ │ native
(强制注入) │ │ (让位给工具)
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ memory → 系统提示 │ │ 全部跳过:memory/web/ │
│ web_search → files+RAG │ │ image/RAG 都不注入 │
│ image_generation → 系统提示│ │ 改成把它们做成 builtin │
│ code_interpreter → 提示 │ │ tools,由模型自己调 │
│ RAG files → <source> 注入 │ │ (→ 03/04 章) │
└───────────┬──────────────┘ └───────────┬──────────────┘
└───────────┬──────────────────┘
▼
apply_source_context_to_messages (:957) ← 把检索到的 sources
merge_system_messages / strip 空块 拼成 RAG 模板注入
▼
form_data, metadata, events → 交给出口(01/04 章)
各站职责一句话:
| 站点 | 干什么 | 位置 |
|---|---|---|
apply_params_to_form_data | 把 params(temperature、system 等)拍平进 payload | middleware.py:1947 |
process_messages_with_output | 历史里带工具调用的 assistant 消息还原成标准格式 | middleware.py:2076 |
| filter inlet | 跑外部 filter 函数(可改写 payload) | middleware.py:2514(→05) |
add_memory_context | 查记忆,拼分段记忆清单进系统提示 | middleware.py:2541(实现在 utils/memory.py:290) |
chat_web_search_handler | 搜网页,结果转成 files 走 RAG | middleware.py:1359 |
chat_image_generation_handler | 直接生成/编辑图片,回填状态提示 | middleware.py:1626 |
chat_completion_files_handler | 对文件/知识库做检索,产出 sources | middleware.py:1827 |
apply_source_context_to_messages | 把 sources 按 RAG 模板注入消息 | middleware.py:833 |
add_file_context | (仅 native)把附件文件 URL 标记进用户消息 | middleware.py:1570 |
3. 核心张力:native 时为什么"什么都不注入"
这是本章最重要、也最容易看漏的设计。理解它,你才能读懂后面每一个 handler 外面那圈 if。
3.1 两种函数调用模式,决定"谁来取上下文"
Open WebUI 支持两种让模型用工具的方式(详见 03 工具系统):
- 非 native:模型 API 本身不支持工具调用,或用户选择了 Open WebUI 自己的模拟方案。此时"取外部信息"这件事必须由后端提前做完——因为模型没有"举手要资料"的能力,后端只能把资料硬塞进消息里。
- native:模型 API 原生支持 function calling(如 OpenAI tools、Anthropic tool use)。此时更好的做法是把 memory / web_search / RAG / 画图都做成工具交给模型,让模型在对话中自己判断要不要调、调哪个、用什么参数。
3.2 岔路口长什么样
在 process_chat_payload 里,凡是"强制注入"的 handler,外面都套着同一句判断——判据已从早期的 != 'native' 翻转为显式 == 'legacy' 才强灌(native 成为默认,middleware.py:2552-2573):
# 示意,非源码 —— 展示 4 个 feature 共用的同一道闸门
if 'memory' in features and features['memory'] and await Config.get('memories.system_context.enable'):
form_data = await add_memory_context(...) # 记忆由配置开关控制,与 legacy/native 无关
if 'web_search' in features and features['web_search']:
if metadata.get('params', {}).get('function_calling') == 'legacy':
form_data = await chat_web_search_handler(...) # native 默认跳过
if 'image_generation' in features and features['image_generation']:
if metadata.get('params', {}).get('function_calling') == 'legacy':
form_data = await chat_image_generation_handler(...)
if 'code_interpreter' in features and features['code_interpreter']:
if metadata.get('params', {}).get('function_calling') == 'legacy':
# 注入 XML-tag 提示;native 分支改为注入 builtin tool
...
真源码里这几段一字排开:web_search / image_generation / code_interpreter 三个 handler 都套 metadata.get('params', {}).get('function_calling') == 'legacy',只有显式 legacy 才硬灌;记忆例外,它由 memories.system_context.enable 配置单独控制。