模型"想过"的东西不再丢:跨轮次、重载、压缩都保留推理

你有没有遇到过这种场面:一个跑了半小时的排查会话,你用 --resume 把它接回来,结果模型像失了忆——前面自己推导出的结论、刚排除掉的两个方向,全都不认了,你得重新给它讲一遍背景。或者更气人的是,你对着同一个错误问第三遍,它每次给出的诊断都不一样。
多数人把这归咎于“模型记性差”。但其中一个很具体的原因是:模型上一轮想过的东西,Hermes 没有全部带回去。 推理内容(thinking / reasoning)在很多 provider 上是要在下一轮原样回传的,而 Hermes 以前只对少数几条路线做了这件事——其余路线上,那些内容在离开模型的那一刻就被丢掉了,压缩和会话分支还会主动删掉更早轮次的推理。
这个窗口里合入的两处改动把回放从“例外”改成了默认。
为什么推理内容必须回传
先把概念说清楚,不然后面全是黑话。推理模型在正式给出答案之前,会先产出一段“思考”:可能是一段可读的文字,也可能是一段加密或带签名的 blob(不同 provider 叫法不同:reasoning_content、reasoning_details、带签名的 thinking 块、Gemini 的 thoughtSignature 等等)。
这些内容有几个特点:
- 有些 provider 会把它当作对话历史的一部分:下一轮请求里不带回上一轮的思考,模型的表现会明显退化,甚至在带工具调用时直接返回 400。
- 有些 provider 把它加密或签名,你必须原样回传,自己拼一遍是无效的。
- 有些 provider 的请求结构严格到多一个字段就报错,对这些路线必须一个字节都不多给。
所以“回放”这件事从来不是“全带上”这么简单,它是按路线分发:每条路线知道自己读什么、不读什么。
以前是什么样
改动前的行为可以概括为“只有少数几条路线上心”:
reasoning_content只回传给 DeepSeek、Kimi、MiMo、Ollama Cloud 这一小撮。reasoning_details只回传给 openrouter.ai。- Claude 的带签名 thinking 在 Bedrock、Vertex、Foundry 上被直接丢掉。
- Gemini 文本轮次的
thoughtSignature从来没被捕获过。 - 压缩(compaction)会把更早轮次的推理从规范历史里删掉。
后果就是前面那两种体验:resume 之后模型“重新猜”,以及压缩之后会话质量突然掉一截。
现在怎么做:一张路线表 + 一个私有载体
一张路线表。 现在每个路线映射到它读得懂的载体字段(message_sanitization 里的推理载体路由表):
| 路线 | 拿到的字段 |
|---|---|
| OpenRouter、Kilo、Vercel AI Gateway、Nous Portal | 三种全给:reasoning_content、reasoning、reasoning_details(签名/加密块原样) |
| Novita、MiniMax、腾讯 | reasoning_content + reasoning_details |
| 本地推理服务 | reasoning_content + reasoning |
| 其余绝大多数 | reasoning_content |
| 严格 schema(Mistral、Groq、Cerebras、SambaNova、Hunyuan) | 一个都不给 |
| 必须回显的几家(DeepSeek、Kimi、MiMo 等) | 保留一个 " " 占位符,只为挡住“工具轮没有存过推理”时的 400,它本身不是推理内容 |
另外还顺手带上了各家自己的“别清掉思考”开关:GLM 的 clear_thinking: false、DashScope 的 preserve_thinking、Kimi 的 keep: "all"。
一个私有载体记录。 Gemini 的 thoughtSignature 和 Copilot 的 reasoning_text / reasoning_opaque / thought_signature 这类字段不属于标准结构,以前没地方放。现在它们会被写成一条私有记录 hermes.native_assistant(实现在 agent/reasoning_carriers.py,流式响应由 StreamCarriers 逐块累积),并且只有它自己的那条路线会收到这条记录——别的路线看不到,也就不会被陌生字段打 400。因为它落在会话里,所以网关轮次和 --resume 之后依然在。
Bedrock Converse 单独处理。 它对 Claude 主动请求 thinking,并维护一份按模型区分的回放策略(签名 / 剥离 / 不签名);推理内容以 Anthropic 形状的 reasoning_details 落盘,所以从 state.db 重新载入的那一轮也能重放。这正是 v0.21.7 发版说明里那句“带签名的思考在 hermes chat --resume 之后以及网关下一轮里都能正确重放”的来源。
被拒绝怎么办:记下来,只重试一次
再周全的路线表也会过时——provider 今天收的字段,明天可能就不收了。所以这次还加了一条兜底:
如果一次请求返回 4xx,并且错误信息里点出了某个被回放的字段(只认“未知字段”这一种措辞),Hermes 会把这条结论按 (provider, 主机, 模型) 记下来,持久化进会话的模型配置——所以 --resume 之后这条记忆还在——然后去掉那个字段重试一次。
三条约束让这个机制不会变成隐患:
- “必须回显”的那几家永远不会记下
reasoning_content,因为那个字段是必需的。 - 历史永不修改:只是请求里不带,落盘的历史保持原样。
- 只重试一次,不会在失败里打转。
压缩和分支不再吃掉推理
另一半改动在保留策略上:
- 压缩(
compress()和挽救通道)不再从保留的轮次里删掉推理文本或加密项。只有比新检查点更旧的检查点会被丢掉。 - CLI 的
/branch会把reasoning_content一起复制过去——分出去的支线才有完整的前因。 - 预检估算和压缩尾部的遍历对“不透明载体”按同一套规则计价,所以压缩不会因为算不清而反复触发。
对你的实际影响
- 会话接回来更像原来那个人。 长排查会话用
--resume、或者跨网关轮次继续时,此前推导的结论不会莫名其妙消失。 - 压缩之后质量更稳。 上下文被压缩后,模型仍然看得到自己此前的思考,而不是只剩结论。
- Bedrock / Vertex / Foundry 上的 Claude 用户。 带签名 thinking 不再被丢弃,恢复的会话也不再被整体拒绝。
- Gemini 和 Copilot 用户。 之前从未被捕获的签名/不透明字段现在会跟着对话走。
排障时看哪里
# 会话里有没有落载体记录
hermes sessions show <session-id> | head -40
# 观察一次请求带了哪些推理字段(调试日志)
hermes logs --follow | grep -i reasoning
# 会话状态库的位置,用来确认 reasoning 是否随轮次保留
ls -l "$HERMES_HOME/state.db"
如果你看到某个路线反复报“未知字段”然后重试,那是兜底机制在工作,通常在下一轮就恢复正常。如果某条路线一直报同样的错,值得去提个 issue——路线表需要更新了。
想理解推理相关的其余旋钮,可以看我们之前写的 推理强度指南;会话恢复的完整操作入口在 会话恢复指南;压缩机制的背景在 上下文压缩瘦身指南。