跳到主要内容

入口装配:把"特性"变成智能体能力(记忆 / 联网 / 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:2311)。它接收原始 form_data(用户消息 + 各种开关),返回三样东西:装配好的 form_data、更新后的 metadata、以及要额外发给前端的 events

函数开头有一行注释,直接写出了这条流水线的顺序(middleware.py:2316):

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_dataparams(temperature、system 等)拍平进 payloadmiddleware.py:2068
process_messages_with_output历史里带工具调用的 assistant 消息还原成标准格式middleware.py:2192
filter inlet跑外部 filter 函数(可改写 payload)middleware.py:2539(→05)
chat_memory_handler查记忆,拼 User Context: 进系统提示middleware.py:1459
chat_web_search_handler搜网页,结果转成 files 走 RAGmiddleware.py:1494
chat_image_generation_handler直接生成/编辑图片,回填状态提示middleware.py:1756
chat_completion_files_handler对文件/知识库做检索,产出 sourcesmiddleware.py:1957
apply_source_context_to_messagessources 按 RAG 模板注入消息middleware.py:957
add_file_context(仅 native)把附件文件 URL 标记进用户消息middleware.py:1704

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,外面都套着同一句判断(middleware.py:2564-2586):

# 示意,非源码 —— 展示 4 个 feature 共用的同一道闸门
if 'memory' in features and features['memory']:
if metadata.get('params', {}).get('function_calling') != 'native':
form_data = await chat_memory_handler(...) # 非 native 才硬塞

if 'web_search' in features and features['web_search']:
if metadata.get('params', {}).get('function_calling') != 'native':
form_data = await chat_web_search_handler(...) # native 跳过

if 'image_generation' in features and features['image_generation']:
if metadata.get('params', {}).get('function_calling') != 'native':
form_data = await chat_image_generation_handler(...)

if 'code_interpreter' in features and features['code_interpreter']:
if metadata.get('params', {}).get('function_calling') != 'native':
# 注入 XML-tag 提示;native 分支改为注入 builtin tool
...

真源码里这四段一字排开,判断完全一致:metadata.get('params', {}).get('function_calling') != 'native'。只有不等于 native 时才调用对应 handler。

3.3 同一条逻辑贯穿更多地方

这道闸门不止管这 4 个 feature。同一个 != 'native' 判断在装配的其它环节反复出现,共同构成"native 时让位"的一致策略:

环节非 native(强制注入)native(让位)位置
文件夹知识把文件夹 files 并进 form_data['files'] 走 RAG只写进 metadata['folder_knowledge'],交给 builtin 工具去读middleware.py:2474
模型绑定知识库把 knowledge 项 append 进 files 走 RAG整段跳过middleware.py:2488
memory / web_search / image / code见上 3.2跳过,做成工具middleware.py:2566-2584
builtin 工具注入不注入add_file_context + get_builtin_tools 一起挂上middleware.py:2846-2862

一句话记住: 非 native = 后端替模型把上下文"喂到嘴边";native = 后端把这些能力"做成按钮",让模型自己按。本章讲的是前者(喂到嘴边)那条路;后者(做成工具)的机制交给 03/04


4. 逐个 handler:每种"特性"怎么变成上下文

下面四节都只发生在非 native 分支里。每个 handler 的签名一致——async def handler(request, form_data, extra_params, user)——拿到 form_data、加料、再返回 form_data

4.1 记忆:chat_memory_handler

要解决的小问题: 让模型"记得"用户以前告诉过它的偏好/事实。

思路: 拿用户最新这句话去向量库里做相似检索,取回最相关的 k=3 条记忆,格式化成一段带日期的清单,append 进系统消息

真实实现(middleware.py:1459 chat_memory_handler):检索用 query_memory(...),请求体是 QueryMemoryForm(content=最后一条用户消息, k=3)(:1461-1470)。拿到结果后逐条拼成 序号. [日期] 内容(:1478-1485),最后:

# 示意,非源码 —— 记忆以「追加到系统提示」的方式注入
form_data['messages'] = add_or_update_system_message(
f'User Context:\n{user_context}\n',
form_data['messages'],
append=True, # append=True:接在已有系统消息后面,而非替换
)

add_or_update_system_message(utils/misc.py:459)的语义值得记住:如果消息列表第 0 条已是 system 就在其上追加/更新,否则在最前面插入一条新的 system 消息。本章后面很多注入都走这个函数。

关键细节: 检索失败(异常)时 results = None(:1471-1473),此时 user_context 为空,仍会注入一句空的 User Context:——不报错,只是没内容。

4.2 联网搜索:chat_web_search_handler

要解决的小问题: 让模型能回答"今天/最新"的问题。

思路(三步):

  1. 生成查询词 —— 不直接拿用户原话去搜,而是先让任务模型把对话浓缩成检索 query(generate_queries(...),middleware.py:1512)。返回是一段可能含 JSON 的文本,代码用"找最后一对花括号"的土办法抠出 {"queries": [...]}(:1538-1547)。抠不出来就退化成"整段响应当一条 query"(:1548-1549);再失败就直接用用户原话(:1554-1556)。
  2. 执行搜索 —— process_web_search(SearchForm(queries=...))(:1588)。
  3. 结果不直接进消息,而是变成 files —— 这是最巧的一步。搜索结果被包装成 type: 'web_search' 的文件项 append 进 form_data['files'](:1594-1621),并不在这里注入正文。真正的检索+注入留给后面的 chat_completion_files_handler(§4.3)统一处理。

为什么绕这一圈? 因为"网页内容"和"上传的 PDF"到了后面是同一种东西——都是待检索的文档源。web_search 只负责"把网页抓成文件",复用统一的 RAG 通道。

观察点: 全程通过 event_emitter 向前端发 status 事件("Searching the web""Searched N sites"),这就是你在界面上看到的那行进度提示(:1496-1505:1623-1634)。

4.3 RAG 文件检索:chat_completion_files_handler

要解决的小问题: 上传的文件/引用的知识库/上一步抓来的网页,内容可能很长,不能整篇塞给模型——要先检索出最相关的几段

思路:metadata['files'] 里的每个源做向量检索,产出结构化的 sources(带文档正文 + 元数据 + 来源信息)。

真实实现(middleware.py:1957 chat_completion_files_handler):

  • 全文模式旁路 —— 若所有文件都标了 context == 'full',则跳过生成查询词,直接整篇进上下文(:1965all_full_context)。否则同样先 generate_queries(... type='retrieval') 生成检索词(:1970-1994)。
  • 核心检索 —— get_sources_from_items(...)(:2014),把 embedding、rerank、hybrid BM25、top-k、相关度阈值等 RAG 参数一次性传进去(:2018-2032)。
  • 返回 —— 函数返回 (body, {'sources': sources})(:2065)。注意:它自己不改消息,只产出 sources;把 sources 拼进消息是后面 §5.1 的活。

调用点在 middleware.py:2889-2892,受模型能力开关 file_context(默认 True)控制;拿到的 sources 累加进外层的 sources 列表。

4.4 图像生成:chat_image_generation_handler

要解决的小问题: 用户说"帮我画一只猫",模型自己画不出来。

思路: 这是四个 handler 里最特殊的一个——它不检索、不给模型资料,而是直接把活干完,然后给模型留一句"话术"。

真实实现(middleware.py:1756 chat_image_generation_handler),分两条路:

  • 有输入图 + 开了 ENABLE_IMAGE_EDIT → 走编辑:image_edits(...)(:1798)。输入图从历史消息里捞(get_images_from_messages,:1666),最多取前 2 组(:1787-1791)。
  • 否则 → 走生成:可选先用 generate_image_prompt 把用户话润色成更好的绘图 prompt(:1857),再 image_generations(...)(:1896)。

不管哪条路,成功后做两件事:

# 示意,非源码 —— 图先经事件推给前端,再给模型一句"交代"
await __event_emitter__({'type': 'files', 'data': {'files': [...图片url...]}}) # 前端直接显示图
system_message_content = (
'<context>The requested image has been created ... '
'Let the user know ...</context>' # 只给模型一句话
)

图片本身通过 files 事件直接推给前端显示(:1815-1828:1913-1926),模型收到的只是一句 <context>...</context> 系统提示告诉它"图已经生成好了,请告诉用户"(:1830:1928)。失败时也注入一句包含错误信息的 <context>,让模型向用户解释(:1851:1949)。最后用 add_or_update_system_message 注入(:1951-1952)。

为什么这么设计? 模型是文本模型,给它图片二进制没用。它真正需要的只是"知道图已经画好了",好在回复里衔接一句"这是你要的图"。真正的图靠事件通道走前端。


5. 装配的收尾:把 sources 变成消息,以及参数/图片处理

前面的 handler 有的直接改了消息(memory、image),有的只产出了 sources(web_search 经 RAG、files)。装配的尾段负责把这些 sources 真正拼进消息,并做几项清理。

5.1 sources → 消息:RAG 模板注入

chat_completion_files_handler 攒下的 sources 在这里落地(middleware.py:2905-2906):

# 示意,非源码
if sources and prompt:
form_data['messages'] = await apply_source_context_to_messages(
request, form_data['messages'], sources, prompt
)

apply_source_context_to_messages(middleware.py:957)做两件事:

  1. <source> 标签 —— 调 get_source_context(sources, ...)(middleware.py:931),把每条来源渲染成 <source id="1" name="..." resource-type="...">正文</source>(:947-953)。id 是自增的引用编号(:941-942),这样模型才能在回答里写 [1][2] 这样的行内引用。
  2. 套 RAG 模板并注入 —— 用 rag_template(RAG_TEMPLATE, context, user_message)(utils/task.py:259)把上下文塞进模板;默认模板 DEFAULT_RAG_TEMPLATE(config.py:1420)是一段指示模型"用 context 回答、按 [id] 标注引用"的系统级说明。

注入落点由一个环境开关决定(middleware.py:981-992):

RAG_SYSTEM_CONTEXT(env.py:361)注入到哪用哪个函数
True系统消息(append)add_or_update_system_message(..., append=True)
False(默认)最后一条用户消息(前置)add_or_update_user_message(..., append=False)

默认把 RAG 上下文放在用户消息而非系统消息里,是为了让检索到的资料贴着问题、不干扰系统人设。

装配还会在注入前先把 sources 快照进 metadata(middleware.py:2902 metadata['sources'] = sources[:]),这样后面 native 工具循环能"回退到 RAG 注入之前"的原始消息(注释见 :2896-2898)。有名字/ID 的 source 还会作为 {'sources': ...} 事件发给前端做引用角标(:2909-2916)。

5.2 参数落地:apply_params_to_form_data

装配一开始就调了它(middleware.py:2349)。作用是把 form_data['params'](temperature、top_p、systemfunction_calling 等)拍平form_data 顶层,让下游 provider 直接读到。

几个要点(middleware.py:2068):

  • 有一组 open_webui_params(stream_responsefunction_callingsystem 等,:2072-2078)是 Open WebUI 自己用的元参数,拍平前会从 params 里删掉,不往下游 provider 传(:2080-2082)。
  • Ollama 模型特殊:所有 params 塞进 form_data['options'](:2098-2100);其它模型逐个铺进顶层(:2102-2105)。
  • custom_params 支持字符串形式的 JSON,会尝试解析后 deep_update 合并(:2084-2096)。

5.3 视觉与图片:参数如何配合模型能力

模型能不能"看图",决定了图片以什么形态进入消息。

  • 历史图片还原成 image_url 部件 —— 从 DB 重建消息时,user 消息里的图片文件被拼成 {'type': 'image_url', 'image_url': {'url': ...}} 追加到 content(middleware.py:2377-2396)。
  • get_image_urls(middleware.py:1683)—— 出口侧处理模型产出的图片(delta 里的 image_url 项);若是 data:image/png;base64 就落盘换成 URL(:1696-1697)。这条更多服务生成回路,但同属"图片如何在消息里流转"的一环。
  • native 下的附件 —— add_file_context(middleware.py:1704)只在 native 分支跑(:2849),把用户消息里非 data: 的附件渲染成 <attached_files><file .../></attached_files> 前置到 content(:1744-1751),让 builtin 工具知道有哪些文件可读。

一个真实的坑(#21878): add_file_context 配对历史消息时,只按 user 角色配对(:1732-1735),不能用位置 zip。因为 process_messages_with_output 会把带工具调用的 assistant 消息展开成多条,payload 的消息列表比 DB 存的长,位置对齐会错位,导致后面的图片丢掉文件上下文(注释见 :1726-1731)。

5.4 历史消息重整:process_messages_with_output

装配早期就调它(middleware.py:2404)。它解决的是"历史里的工具调用怎么还原给模型"。

存库的 assistant 消息带一个结构化的 output 字段(工具调用 + 结果),前端会把它压平成纯文本。process_messages_with_output(middleware.py:2192)把带 output 的 assistant 消息用 convert_output_to_messages(...) 还原成标准 OpenAI 格式(tool_calls + tool 结果消息),并把 output 字段剥掉不让模型看到(:2205-2218)。reasoning_format 按 provider 决定(Ollama 用 <think> 标签、llama.cpp 用 reasoning_content,见 get_reasoning_format :2175)。


6. 边界与坑

  • native 时本章大半失效。 如果你在读代码发现"记忆/联网/RAG 没注入",第一件事是查 function_calling 是不是 native——那不是 bug,是设计(§3)。这些能力此时以工具形态存在(→03)。
  • web_search 不自己注入正文。 它只把网页变成 files,真正的检索注入在 chat_completion_files_handler + apply_source_context_to_messages。追问"搜到的内容去哪了"要顺着这条链看。
  • 图像 handler 是"副作用型"。 它不给模型资料,只做图 + 发事件 + 留一句话术。别指望模型"看到"自己生成的图。
  • 查询词抽取靠 rfind('{')... memory 之外的 web_search / RAG / image prompt 都用"找最后一对花括号 + json.loads"抠 JSON(如 :1538-1547),模型输出格式跑偏时会静默退化成"整段当 query"或"用户原话",不报错但检索质量下降。
  • 注入落点受多个开关影响。 RAG 进系统还是用户消息看 RAG_SYSTEM_CONTEXT;file_context 能力、builtin_tools 能力、terminal 能力都可被模型 meta 关掉(:2887:2843:2794)。行为不符预期先查这些能力位。
  • 空内容防御。 装配尾段有多处清理:strip_empty_content_blocks(:2933,防 Gemini/Claude 拒收空 text 块)、merge_system_messages(:2937,合并多条 system 防严格模板报错)。这些是"为了让各家 provider 都能吃下"而存在的兼容层。

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

主题文件路径符号名
装配主入口backend/open_webui/utils/middleware.pyprocess_chat_payload
native 岔路口(4 feature)backend/open_webui/utils/middleware.pyprocess_chat_payload(function_calling != 'native' 判断,~:2564-2586)
记忆注入backend/open_webui/utils/middleware.pychat_memory_handler
联网搜索backend/open_webui/utils/middleware.pychat_web_search_handler
RAG 文件检索backend/open_webui/utils/middleware.pychat_completion_files_handler
图像生成/编辑backend/open_webui/utils/middleware.pychat_image_generation_handler
sources → 消息注入backend/open_webui/utils/middleware.pyapply_source_context_to_messages
<source> 标签渲染backend/open_webui/utils/middleware.pyget_source_context
历史工具调用还原backend/open_webui/utils/middleware.pyprocess_messages_with_output
参数拍平backend/open_webui/utils/middleware.pyapply_params_to_form_data
native 附件标记backend/open_webui/utils/middleware.pyadd_file_context
模型产出图片处理backend/open_webui/utils/middleware.pyget_image_urls
消息注入辅助backend/open_webui/utils/misc.pyadd_or_update_system_message / add_or_update_user_message
RAG 模板渲染backend/open_webui/utils/task.pyrag_template
RAG 默认模板backend/open_webui/config.pyDEFAULT_RAG_TEMPLATE
RAG 注入落点开关backend/open_webui/env.pyRAG_SYSTEM_CONTEXT

接着读: 工具从哪来、native/非 native 两种调用模式的完整对比 → 03 工具系统;native 工具如何迭代调用、出口如何处理 → 04 智能体循环;inlet filter 的插件机制 → 05 插件框架