Open WebUI 为何要禁止把 reasoning 重新注入提示词?

👁 110 次阅读

Open WebUI 为何要禁止把 reasoning 重新注入提示词?

来源:https://github.com/open-webui/open-webui/issues/23339

现象描述

Open WebUI 在重建聊天历史时,会把上一轮 assistant 输出中的 reasoning / thinking 内容重新注入下一轮的 messages。触发路径在 backend/open_webui/utils/middleware.py 的 process_messages_with_output(),它调用 convert_output_to_messages(..., raw=True);而 backend/open_webui/utils/misc.py 中的同名函数在 raw=True 时会把 reasoning 以 think 标签的形式追加进消息。 由此产生的典型症状包括:
  • 部分 provider 与模型在历史里看到 think 标签后开始模仿和放大,生成多重、嵌套、变体的 thinking 标签。
  • 前端的 reasoning 解析与渲染被破坏:thinking 块泄漏进正文、块重复出现、markdown 结构破损。
  • 提示词体积随轮次持续膨胀,token 成本上升。
  • 敏感 tool 数据被重新注入上下文。
据 issue 讨论,Gemma 4、DeepSeek reasoning、Qwen reasoning 等模型均有用户报告同类问题。该 issue 创建于 2026-04-02,截至 2026-09-07 仍为 open,无标签,44 条评论,15 个 reaction,官方尚未合入统一方案,也无对应 PR。

背景原因

UI 显示与模型上下文被耦合在一起

ShirasawaSama 在 2026-04-02 指出,问题的根源是 UI 显示的内容与模型实际看到的内容紧耦合。上游的小改动,或 provider 泄漏 think 标签,都会污染 prompt、增加 token 成本,并把敏感 tool 数据重新注入。他建议把两件事拆开:
  • Include reasoning in LLM context(on / off)
  • Include tool payloads in LLM context(none / summary / full)
即 UI 渲染决策与 LLM context 决策应当是两套独立开关。

上游模型对多轮历史的明确要求

visig9 在 2026-04-04 引用 Gemma 4 官方文档(huggingface.co/google/gemma-4-E2B-it 的 Multi-Turn Conversations 一节):多轮对话中,历史模型输出只应包含最终回复,上一轮的 thoughts 不得在下一轮用户输入开始前加入。这与当前 Open WebUI 的行为直接冲突。

注入方式本身也值得商榷

ShirasawaSama 在 2026-04-08 提出关键取向:不要用 think 标签把 reasoning 注入 assistant 输出,应尽量按 LLM 原样返回数据。Classic298 同意对注入方式的讨论,但认为当前是否返回给 API 的行为可以保持不变。

反向约束:某些场景反而需要 reinject

itsHenry35 在 2026-04-26 指出反向约束:deepseek reasoning 开启并使用 native tool calling 时反而需要 reinject,否则会报错。Classic298 在 2026-05-26 进一步引用 MiMo 文档说明,reasoning_content 必须在包含 tool call 的后续轮次完整返回,否则会出现指令跟随能力下降与幻觉增加,并称 DeepSeek 与 Kimi K2.5 有等效要求。这意味着「默认关闭、按模型 opt-in」是比「全局关闭」更合理的设计。

排查或量化方法

量化实测:payload 从 12 KB 涨到 58 KB

gerpervaz 在 2026-05-26 给出量化实测:Open WebUI 接 Qwen reasoning 模型走 vLLM,多轮时上一轮 assistant 的 think 块被序列化回 messages。抓取的 WebUI → vLLM 日志显示:
状态首个请求 payload几轮后 payload
未启用剥离 Filter约 12 KB约 58 KB
启用剥离 reasoning 块的 Filter约 12 KB稳定在约 12-18 KB
他的结论是 Filter 技术上可行但脆弱:需要发现 Functions / Filters、编写 Python、绑定到正确模型、用请求级日志验证、并跨 Open WebUI 版本维护。

检查 llama.cpp 连接配置

yrro 在 2026-04-22 发现一个配置项:Admin Panel → Settings → Connection → 对应 llama.cpp 连接 → Provider 设为 llama.cpp 后,thinking 内容会以独立的 reasoning_content 字段发送,而不是渲染成 content 里的 think 标签。这是不改代码即可缓解的一条路径。

理解 filter 覆盖不到的位置

Classic298 在 2026-05-26 补充的机制说明很关键:
– filter 除非使用 stream() hook,否则不在 tool call loop 中运行。
– inlet 只在 process_chat_payload 顶部触发一次。
– tool call loop 内部的 generate_chat_completion 不会重跑 filters,因此按迭代构建的 messages(convert_output_to_messages(output, raw=True, reasoning_format=get_reasoning_format(model)))绕过所有 filter hook。
– 真正控制 loop 内 reasoning 形态的是 backend/open_webui/utils/middleware.py 的 get_reasoning_format:ollama 走 content 内的 think 标签,llama.cpp 走顶层 reasoning_content 字段,其他取值下 reasoning 整体丢弃。
– 跨轮 history rebuild 在 middleware.py 第 2385 行执行,该调用在任何 inlet hook 之前,所以按请求改写 model['provider'] 触及不到它。

完整解决方案

方案一:临时补丁(最小改动)

ShirasawaSama 在 2026-04-09 给出的补丁基于 commit 9bd84258d09eefe7bf975878fb0e31a5dadfe0f8:在 misc.py 第 233-242 行 elif item_type == 'reasoning': 与 if raw: 之间插入一条 pass 语句。
elif item_type == 'reasoning':
    pass
if raw:
    # 原有把 reasoning 追加为 think 标签的逻辑
    ...
注意 8bit-coder 在 2026-04-13 反馈:加了 pass 仍然有问题;在容器里直接改文件后 down 再 up -d 会还原改动。Classic298 建议改用自定义 Dockerfile 或启动时的替换脚本。

方案二:剥离 think 标签的 Filter

yrro 在 2026-04-22 给出一个 filter,从 assistant messages 中剥离 think 标签,地址为 gist.github.com/yrro/b0f2765ea55ae3414e06b319dd07ae8e。JoeEnderman 在 2026-05-04 确认该 filter 生效,Gemma 4 26b a4b 恢复正常。Classic298 也在 2026-05-26 说明自己使用 filter,并用一段小代码排除 claude 模型。

方案三:monkey-patch get_reasoning_format

Classic298 在 2026-05-26 发布了一个用户态 filter,用于在轮内与跨轮都保留 reasoning_content:机制是 monkey-patch get_reasoning_format,让任何非 ollama 模型返回 reasoning_content 而不是 None,从而命中所有调用点,包括 in-turn tool-call loop 的三次 rebuild(middleware.py 的 4788 / 4793 / 5015 行)与跨轮 history rebuild(middleware.py 的 2385 行)。安装地址是 openwebui.com/posts/bd34c85d-8b61-40c3-854a-9eaa843a9205。
该 patch 的注意事项:
– 模块全局生效,禁用 filter 不会撤销,只有容器重启才生效。
– excluded_model_ids 这个 valve 可以让列出的模型保留原行为。

方案四:三条缓解思路

tq1008 在 2026-06-12 给出三条思路:
– 存入历史前剥离 think 块(需要改 convert_output_to_messages)。
– 构建下一轮 messages 时使用 raw=False。
– 模型级变通:某些 DeepSeek API provider 在服务端分离 thinking 与 content,choices[0].message.content 只返回最终答案,路由到这类网关后前端完全看不到 think 标签。

方案五:客户端清洗

0DukePan 在 2026-09-07 以 history drift 这个名称描述同一失效模式并 +1,认同两条代码路径(misc.py 的 convert_output_to_messages(..., raw=True),由 middleware.py 的 process_messages_with_output() 调用)是放置默认关闭开关的正确位置;他给出的客户端变通是 unswallow 库的 sanitizeHistory()。

参数或对比表格

方案改动位置覆盖 tool call loop跨版本可维护性备注
插入 pass 补丁misc.py 233-242 行是低,容器重建即还原需自定义 Dockerfile
剥离 think 的 FilterFunctions / Filters否(除非用 stream() hook)中JoeEnderman 确认对 Gemma 4 生效
monkey-patch get_reasoning_format模块全局是中禁用 filter 不撤销,需重启容器
Provider 设为 llama.cppAdmin Panel 连接配置是高后端需接受顶层 reasoning_content
路由到服务端分离的网关部署层是高仅部分 DeepSeek API provider 可用
客户端 sanitizeHistory()客户端不适用中unswallow 库

常见错误排查

  • 加了 pass 补丁但问题依旧:8bit-coder 反馈该情况。可能原因是容器 down 再 up -d 后文件被还原,需改用自定义 Dockerfile 或启动替换脚本。
  • Filter 对 tool call 场景无效:Classic298 说明 filter 除非使用 stream() hook,否则不在 tool call loop 中运行;loop 内部的 generate_chat_completion 不会重跑 filters。
  • 改写 model['provider'] 无法影响跨轮历史:跨轮 history rebuild 在 middleware.py 第 2385 行执行,早于任何 inlet hook。
  • 禁用 monkey-patch filter 后行为未恢复:该 patch 是模块全局的,禁用 filter 不会撤销,只有容器重启才生效。
  • DeepSeek reasoning + native tool calling 报错:itsHenry35 指出该场景反而需要 reinject,不能一律剥离。

实践建议

  • 优先尝试把 llama.cpp 连接的 Provider 设为 llama.cpp,让 thinking 走顶层 reasoning_content 字段,这是不改代码的路径。
  • 若必须用 Filter,注意其覆盖边界:跨轮 history 与 in-turn tool call loop 是两个不同调用点,普通 inlet 只能触及前者。
  • 对 DeepSeek reasoning、Kimi K2.5、MiMo 等有「reasoning_content 必须完整返回」要求的模型,保留 opt-in 通道,不要全局关闭。
  • 监控 WebUI → 后端的请求 payload 体积,若随轮次线性增长,基本可确认 reasoning 被重复注入。
  • 该 issue 至今 open,无标签、无维护者确认的方案、无对应 PR;上述 filter 与 monkey-patch 均为社区自维护实现,升级前需重新验证。

FAQ

Q:这个问题的官方修复进展如何? A:issue #23339 当前为 open,无标签,无维护者确认的方案,无对应 PR。社区给出的 filter 与 monkey-patch 均为自维护实现。 Q:为什么不能直接全局关闭 reasoning 注入? A:据 issue 讨论,DeepSeek reasoning 开启并使用 native tool calling 时需要 reinject,否则会报错;MiMo 文档也要求 reasoning_content 在包含 tool call 的后续轮次完整返回。因此诉求是默认关闭、按模型 opt-in。 Q:issue 中建议的开关命名是什么? A:建议命名是 reinject_reasoning 或 include_reasoning_in_history,默认 OFF。OFF 时保留 tool call 结构但不把 reasoning 加回 messages;ON 时保持现状。 Q:Filter 为什么在 tool call 场景下不生效? A:filter 除非使用 stream() hook,否则不在 tool call loop 中运行;inlet 只在 process_chat_payload 顶部触发一次,loop 内部按迭代构建的 messages 绕过所有 filter hook。 Q:有没有不改代码的缓解方式? A:有两条。一是把对应 llama.cpp 连接的 Provider 设为 llama.cpp,thinking 会以独立 reasoning_content 字段发送;二是路由到在服务端分离 thinking 与 content 的 DeepSeek API provider 网关。

订阅 AI 百事知

每周精选 AI 工具评测、行业资讯与使用教程,第一时间送达。

🔗 相关阅读(你可能还想看)

📄 Gemini Omni 1.2 Flash 发布:视频降价30%、原生15秒,单条成本只贵5分钱
谷歌 11 月 5 日开放 Gemini Omni 1.2 Flash:720p 视频每秒 0.07 美元,比上代降 3
📄 GPT-6 Cyber 发布:121 万美元,948 条通往 root 的路
OpenAI 发布 GPT-6 Cyber:在 Linux 内核上跑出 948 个本地提权利用,整轮 API 成本 12
📄 Anthropic冲刺IPO:170亿营收、8950亿算力承诺、首次在招股书披露递归自我改进风险
📄 Anthropic IPO招股书曝光:单季营收170亿美元背后的8954亿美元算力账单
📄 阿里千问接入 Apple Intelligence:中国版 iPhone 的 AI 时刻来了
📄 苹果AI中国落地靴子落地:千问接入Apple Intelligence,11月4日开测
📄 苹果官宣:Apple Intelligence 国行版落地,阿里千问深度接入 Siri 与系统写作工具
苹果官宣 Apple Intelligence 国行版 11 月 4 日开测、12 月随 iOS 27.3 正式上线。阿
📄 Anthropic 递交 S-1 启动 IPO:Claude 母公司估值剑指 2 万亿美元
📄 Cerebras 公开万亿参数模型部署方案:59 块晶圆跑 3T 参数,推理 2000 tokens/秒
🤖 想直接和 AI 对话?
免费体验 DeepSeek V4 等顶级模型,无需注册,随问随答
🎯 立即体验 AI 工具

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

🤖 嘟嘟客服
滚动至顶部
🤖 嘟嘟客服
🤖 嘟嘟客服
';i.value='';m.innerHTML+='
...
';fetch('/dify-chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,user:'w'+Date.now()})}).then(function(r){return r.json()}).then(function(d){var a=d.answer||'暂时无法回复';m.removeChild(m.lastChild);m.innerHTML+='
'+a+'
'}).catch(function(){m.removeChild(m.lastChild);m.innerHTML+='
网络异常
'})">发送