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 标签 和推理过程被完美地静默吞噬在了内存层,屏幕上只剩下纯净、优雅、极速流式输出的最终答案。
拿去用,少踩坑,把精力留给真正有价值的业务逻辑编排上。
GLM-5.1GLM-5.1是智谱迄今最智能的旗舰模型,也是目前全球最强的开源模型。GLM-5.1大大提高了代码能力,在完成长程任务方面提升尤为显著。和此前分钟级交互的模型不同,它能够在一次任务中独立、持续工作超过8小时,期间自主规划、执行、自我进化,最终交付完整的工程级成果。Jinja00
MiniMax-M2.7MiniMax-M2.7 是我们首个深度参与自身进化过程的模型。M2.7 具备构建复杂智能体应用框架的能力,能够借助智能体团队、复杂技能以及动态工具搜索,完成高度精细的生产力任务。Python00- QQwen3.5-397B-A17BQwen3.5 实现了重大飞跃,整合了多模态学习、架构效率、强化学习规模以及全球可访问性等方面的突破性进展,旨在为开发者和企业赋予前所未有的能力与效率。Jinja00
HY-Embodied-0.5这是一套专为现实世界具身智能打造的基础模型。该系列模型采用创新的混合Transformer(Mixture-of-Transformers, MoT) 架构,通过潜在令牌实现模态特异性计算,显著提升了细粒度感知能力。Jinja00
LongCat-AudioDiT-1BLongCat-AudioDiT 是一款基于扩散模型的文本转语音(TTS)模型,代表了当前该领域的最高水平(SOTA),它直接在波形潜空间中进行操作。00
ERNIE-ImageERNIE-Image 是由百度 ERNIE-Image 团队开发的开源文本到图像生成模型。它基于单流扩散 Transformer(DiT)构建,并配备了轻量级的提示增强器,可将用户的简短输入扩展为更丰富的结构化描述。凭借仅 80 亿的 DiT 参数,它在开源文本到图像模型中达到了最先进的性能。该模型的设计不仅追求强大的视觉质量,还注重实际生成场景中的可控性,在这些场景中,准确的内容呈现与美观同等重要。特别是,ERNIE-Image 在复杂指令遵循、文本渲染和结构化图像生成方面表现出色,使其非常适合商业海报、漫画、多格布局以及其他需要兼具视觉质量和精确控制的内容创作任务。它还支持广泛的视觉风格,包括写实摄影、设计导向图像以及更多风格化的美学输出。Jinja00