DeepSeek-R1 终端输出满屏 `<think>` 乱码?一行正则修复 Hermes 过滤 Bug
接入 DeepSeek-R1 后的灾难现场:满屏 think 标签闪烁
满怀期待地看着官方 README 里那句牛逼哄哄的“支持所有兼容 OpenAI 格式的模型”,你兴冲冲地改了 config.yaml,把底层的 LLM 引擎换成了当下最火的 DeepSeek-R1 或者 Qwen3 系列。
你点了一根烟,敲下回车,以为接下来迎接你的是如同原生 IDE 插件般丝滑的智能体多轮交互。别做梦了。
当大模型开始输出的那一瞬间,迎面扑来的是一场 UI 灾难。你的终端并没有优雅地流式打印最终答案,而是像呕吐一样,疯狂地往外喷射带有 <think> 和 </think> 包裹的思维链(Chain of Thought)原文本。原本整洁的 TUI 界面瞬间被几千字的内心戏撑爆,光标四处乱闪,排版稀烂,如果你开了多线程 Agent,甚至会看到各个子 Agent 的 think 标签 串词混流。
你跑去翻 GitHub Issues 才发现,只要是带有推理过程(Reasoning)的国内大模型,无一例外全在这个坑里翻了车。
报错现象总结: 在 Hermes-Agent 中接入 DeepSeek-R1、Qwen3 或 MiniMax 等具有深度思考能力的大模型时,由于底层渲染函数
_clean_for_display无法正确拦截处于流式输出传输中(Mid-stream)的未闭合<think>标签,导致思维链的冗余文本直接绕过过滤机制,写入delta.content,最终完全污染终端 UI 的渲染区域。
官方文档对此避重就轻,这就导致了只要你不用 Claude,你的终端基本就在裸奔。问题究竟出在哪?今天我们直接扒开源码,看看这个低级 Bug 是怎么产生的。
扒开 _clean_for_display 源码:为什么官方的流式正则过滤会彻底失效?
要搞清楚为什么屏幕上会飙乱码,我们得先认清大模型 API 流式输出(SSE, Server-Sent Events)在底层协议上的割裂。
官方在适配 Claude 时,由于 Anthropic 的 API 极其规范,思维链的数据是走专属通道的(通过 delta.thinking_delta 字段传输)。Hermes-Agent 的底层拦截器会把这些数据优雅地交给 reasoning_callback 去处理,根本不会混入最终呈现给用户的 content 里。
但绝大多数兼容 OpenAI 格式的推理模型(如 DeepSeek-R1),简单粗暴到了极点——它们直接把带有 <think>...</think> 的推理过程,硬生生地塞进了标准文本输出的 delta.content 字段里!
为了对比更直观,我们可以看下主流大模型在 Hermes 下的底层链路差异:
| 接入的模型类型 | 推理数据传输字段 | Hermes-Agent 底层处理逻辑 | 终端实际表现 |
|---|---|---|---|
| Claude 3.5 Sonnet | delta.thinking_delta |
走 reasoning_callback 独立渲染 |
完美、纯净、无乱码 |
| DeepSeek-R1 | delta.content |
丢给 _clean_for_display 擦屁股 |
❌ <think> 原码满屏飞 |
| Qwen3-Max / MiniMax | delta.content |
丢给 _clean_for_display 擦屁股 |
❌ <think> 原码满屏飞 |
官方为了给这些“不守规矩”的模型擦屁股,在终端渲染层搞了一个名叫 _clean_for_display 的过滤函数。初衷是好的,想用正则表达式把这些标签洗掉。但写这段代码的老哥,显然在处理流式(Streaming)数据时缺乏基本的工程常识。
官方原生的正则逻辑大概是这样的:查找 <think> 开头,</think> 结尾的完整字符串,然后将其替换为空。
这里的致命漏洞在于:流式输出是一块一块(chunk)吐出来的!
我们推演一下这个灾难性的时序逻辑:
- Chunk 1 到达:包含
<think>我需要先分析一下这个问题...。注意,此时</think>闭合标签根本还没发送过来。 - 正则匹配失败:
_clean_for_display拿着匹配完整闭合标签的正则去扫 Chunk 1,发现没有</think>,于是判定这是“合法正文文本”。 - 脏数据上屏:这半截带着
<think>的残缺内容,直接绕过防线,被无情地写入了 UI 缓冲区。 - Chunk N 到达:包含最后的
</think>。此时再去过滤已经毫无意义,因为前面的垃圾思维链字符早就打印在你的屏幕上了。
处理流式数据的正则,只防“闭合块”而不防“未闭合的半截块”,纯属掩耳盗铃。
手撕 Hermes 虚拟环境:兼顾完整与未闭合 think 标签的临时修法
病因找到了:流式数据截断导致正则失效。那我们要做的,就是对 _clean_for_display 函数进行降维重构。我们必须让它具备处理**“完整块(Complete blocks)”、“流传输中的未闭合块(Incomplete blocks)”以及“孤儿闭合标签”**的三重拦截能力。
如果你头铁,想自己动手丰衣足食,你需要经历以下繁琐的抢修过程:
首先,钻进深不见底的 Python 虚拟环境。如果你是用官方推荐的 uv 工具链安装的,去找类似 venv/lib/python3.11/site-packages/... 下的终端渲染脚本,死磕并定位到 _clean_for_display 函数,将它原有的弱智正则替换为以下三段式暴力清洗逻辑:
import re
def _clean_for_display(cleaned: str) -> str:
# 阶段 1:先干掉已经完整闭合的块(处理速度最快,防范非流式或极快吐出的短块)
cleaned = re.sub(
r'<(think|thinking|reasoning|thought|REASONING_SCRATCHPAD)\b[^>]*>.*?</\1>',
'', cleaned, flags=re.DOTALL | re.IGNORECASE,
)
# 阶段 2:核心修复!干掉还在流中、只有开头没有结尾的未闭合块(Mid-stream)
# 注意:这里使用贪婪匹配到末尾,只要碰到 <think> 起手,后面的流数据全部静默吞噬
cleaned = re.sub(
r'<(think|thinking|reasoning|thought|REASONING_SCRATCHPAD)\b[^>]*>.*',
'', cleaned, flags=re.DOTALL | re.IGNORECASE,
)
# 阶段 3:兜底清理上一轮或网络抖动遗留的孤儿闭合标签
cleaned = re.sub(
r'</(think|thinking|reasoning|thought|REASONING_SCRATCHPAD)>\s*',
'', cleaned, flags=re.IGNORECASE,
)
return cleaned
这看起来简单对吧?但实操起来极其恶心。改完源码后,你需要保存、清空缓存、重启守护进程。万一你缩进敲错了一个空格,整个 Agent 的终端模块会直接抛栈然后挂起。
更痛的是,一旦 Hermes-Agent 官方发布个次要版本更新,你执行了 git pull 和重新构建,这段手敲的代码又会被原封不动地覆盖掉。在国内网络环境下,如果你稍微搞乱了依赖树,试图重新 uv pip install,又得面对茫茫多的 Github Raw 域名 Timeout 和 PyPI 连接重置。
拒绝无效折腾:直接获取重构版 display_cleaner.py 覆盖包
手动改虚拟环境源码这种事,既反直觉又极难维护。为了修一个本来就不该存在的输出 Bug,搭进去一个原本可以愉快 Coding 的下午,性价比极低。
为了拯救各位开发者的头发,我已经把这段针对 DeepSeek、Qwen3 等国内推理模型量身定制优化的正则清洗逻辑,直接做成了一个免编译的底层补丁包。
你不需要去翻找虚拟环境的隐藏路径,也不需要懂正在疯狂变化的正则贪婪匹配逻辑。
👉 来 GitCode 极速下载已修复正则过滤逻辑的 [display_cleaner.py 覆盖包] (https://gitcode.com/GitHub_Trending/he/hermes-agent)
替换操作像呼吸一样简单:
- 访问 GitCode 仓库,拉取打包好的
display_cleaner.py(https://gitcode.com/GitHub_Trending/he/hermes-agent) - 将文件直接扔进你的项目对应目录,覆盖原文件。
- 重新拉起 Hermes-Agent。
完成覆盖后,立刻去终端随便丢给 DeepSeek-R1 一个极其烧脑的逻辑题。你会发现,那些原本能刷满三屏的 think 标签 和推理过程被完美地静默吞噬在了内存层,屏幕上只剩下纯净、优雅、极速流式输出的最终答案。
拿去用,少踩坑,把精力留给真正有价值的业务逻辑编排上。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0139- DDeepSeek-V4-ProDeepSeek-V4-Pro(总参数 1.6 万亿,激活 49B)面向复杂推理和高级编程任务,在代码竞赛、数学推理、Agent 工作流等场景表现优异,性能接近国际前沿闭源模型。Python00
GLM-5.1GLM-5.1是智谱迄今最智能的旗舰模型,也是目前全球最强的开源模型。GLM-5.1大大提高了代码能力,在完成长程任务方面提升尤为显著。和此前分钟级交互的模型不同,它能够在一次任务中独立、持续工作超过8小时,期间自主规划、执行、自我进化,最终交付完整的工程级成果。Jinja00
MiniCPM-V-4.6这是 MiniCPM-V 系列有史以来效率与性能平衡最佳的模型。它以仅 1.3B 的参数规模,实现了性能与效率的双重突破,在全球同尺寸模型中登顶,全面超越了阿里 Qwen3.5-0.8B 与谷歌 Gemma4-E2B-it。Jinja00
MiniMax-M2.7MiniMax-M2.7 是我们首个深度参与自身进化过程的模型。M2.7 具备构建复杂智能体应用框架的能力,能够借助智能体团队、复杂技能以及动态工具搜索,完成高度精细的生产力任务。Python00
MusicFreeDesktop插件化、定制化、无广告的免费音乐播放器TypeScript00