DeepSeek Harness 切回 deepseek-official 必现 400:thinking 缺失怎么修
同一个会话,用别的模型跑了几轮,再切回 deepseek-official,从此每条消息都返回 400,服务端只说一句 content[].thinking ... must be passed back。 新建会话完全正常,旧会话怎么重试都没用。这不是模型坏了,而是历史里有一段「有工具调用、没有思考块」的消息——DeepSeek Harness(DSH)的序列化器不会替它补,服务端就拒绝整段历史。
DeepSeek Harness 先确认是历史缺块,不是请求写错
一句话结论:新建会话正常、旧会话必失败,且报错点名 thinking / passed back,就是历史缺块;如果新会话也失败,先查模型配置和密钥。 再做三个快速核对:
- 在旧会话里只发一句「你好」;预期仍 400,说明与当前任务无关。
- 把会话切到另一个不要求 thinking 回传的 provider;预期能成功,基本锁定是适配器要求差异。
- 打开调试日志,找请求体里最后一条带
tool_use的 assistant 消息;预期同一轮没有thinking或reasoning字段。
据讨论里的源码核对,llm-deepseek/src/serialize.ts:24-37 仅当持久消息已有 reasoning 块时才输出 thinking;本地 precheck :131-146 只校验 tool 配对,不看 thinking,所以问题漏到服务端才暴露(#10077)。模型切换的常规配置可以对照 模型 provider 配置。
历史是怎么被 DSH 混合路由投毒的
一句话结论:某几轮在关闭思考的模型上生成,持久消息里没有 reasoning;切回 deepseek-official 后,序列化器按「没有就不补」处理,服务端要求「有 tool_use 就必须有 thinking」,冲突就此产生。
触发链通常是这样:
- 会话先在 provider A 上跑,A 不支持或关闭了思考,assistant 消息只有 tool_use。
- 切到 deepseek-official,历史被原样带过去。
- 序列化器看到这些轮次没有 reasoning,不输出 thinking。
- 服务端检查到 tool_use 缺少配套 thinking,返回 400。
- 之后的每条消息都带着这段历史,所以每次都失败。
压缩不是必要条件,但压缩后更容易丢 reasoning,属于加重因素。
DeepSeek Harness thinking 缺失的三种修法
一句话结论:要保留旧会话就装 guard 插件,能改代码就补序列化器,旧会话价值不高就迁到新会话。
修法一:用 thinking-roundtrip-guard 做合成或降级
这是不改核心、可配置粒度最细的做法,插件提供 synthesize、degrade、observe 三档,适合生产环境先恢复。
- 安装
@argszero/dsh-thinking-roundtrip-guard;预期状态为 active。 - 先切到
observe模式,重放旧会话;预期日志里列出缺失 thinking 的轮次。 - 改成
synthesize模式,让插件为缺失轮次补一个最小 thinking 块;预期下一轮请求不再 400。 - 再发一条消息;预期仍 200,如果模型行为异常,改用
degrade模式让该轮不走 thinking 回传。 - 观察几轮后再决定长期保留哪一档;预期没有新的 400,回答质量可接受。
三档的选择:observe 只记录不修改,用来确认范围;synthesize 补占位 thinking,兼容性最好,但占位内容不参与真实推理;degrade 把该轮降级为普通消息,可能改变模型对历史的理解。
修法二:改 serialize.ts,在输出 tool_use 前补块
根治点在序列化器,让「带 tool_use 的轮次」在缺少 reasoning 时主动合成一个空 thinking,而不是静默跳过。 适合维护 fork 或提交上游。
- 在
serialize.ts输出 assistant 消息前,检查该轮是否包含 tool_use;预期能识别出所有工具调用轮次。 - 如果有 tool_use 且没有 reasoning,插入一个最小 thinking 块(空文本或一句占位);预期每个工具轮次都带 thinking。
- 确保插入位置在 tool_use 之前,顺序符合服务端要求;预期请求能通过校验。
- 补测试:有 reasoning、无 reasoning、混合三种历史各一条;预期三种都能正确序列化。
- 重新构建,用一个跨 provider 的旧会话验证;预期不再需要插件。
合成块会进入后续请求,可能轻微影响模型判断,占位内容越短越好;如果服务端校验的是 thinking 签名而不是存在性,合成可能仍被拒;改动在适配器层,升级后可能被覆盖。
修法三:新建会话,迁移关键上下文
不想装插件也不想改代码时,把旧会话的结论迁到新会话,绕开被投毒的历史。 代价是丢掉工具调用链。
- 在旧会话里导出最近几轮的关键结论、文件路径和待办;预期工作状态以文字保留。
- 新建会话,把背景粘进去,明确当前模型是 deepseek-official;预期没有继承旧工具轮次。
- 先发一条纯文本消息,再发一条带工具的请求;预期两次都返回 200。
- 把旧会话归档;预期以后不会误点进去继续失败。
DSH 插件修复后的验证与避坑
一句话结论:一次 200 不算修好,必须在旧会话、provider 往返和新建会话三条路径上都不再 400。 验证步骤:
- 用一个「先 A 模型、后 deepseek-official」的旧会话发三条消息;预期都不再 400。
- 检查请求体,确认每个带 tool_use 的轮次都有 thinking;预期没有缺口。
- 切回 A 模型再切回来;预期历史没有被二次破坏。
- 新建会话做一次对照;预期修复没有影响正常请求。
避坑要点:
- 不要在 400 后反复重发,历史不变,结果不会变。
- 压缩前先确认 thinking 是否会被保留,否则修好一次又会被压缩打回原形。
- 合成 thinking 不等于恢复原始推理,它只解决协议兼容,不恢复模型当时的思路。
- 讨论里没有官方修复版本,0.2.1-rc.2 仍可复现;升级后以 release notes 为准,别把某个 DSH插件 的旧行为当成已修。
DeepSeek Harness thinking 回传常见问题
Q:DeepSeek Harness 切回 deepseek-official 就 400,新建会话却正常,为什么? A:DeepSeek Harness 切回 deepseek-official 后 400,是因为旧会话有些轮次在关闭思考的模型上生成,历史没有 thinking 块。deepseek-official 要求工具调用轮次把 thinking 原样带回,本地序列化只在历史已有 reasoning 时才输出,于是服务端判定缺块并 400;新建会话没有这段历史。
Q:DeepSeek Harness 的 thinking 块为什么会丢,是压缩把 reasoning 删了吗? A:DeepSeek Harness 的 thinking 块丢失不一定是压缩,更常见的是混合路由:某一轮用不支持或关闭了思考的 provider 生成,持久消息里本来就没有 reasoning。回到 deepseek-official 时序列化器不会凭空合成,历史就变成「有 tool_use、没有 thinking」。
Q:不想放弃旧会话,DeepSeek Harness 里怎么把缺失的 thinking 补回去? A:DeepSeek Harness 的旧会话可以装 thinking-roundtrip-guard 插件,在 llm/stream 层对缺失轮次做 synthesize 或 degrade;也可以改 serialize.ts,在输出 tool_use 前检查同轮是否有 reasoning。两种做法都要先备份会话,并用一条短消息验证不再 400。
Q:DeepSeek Harness 的 400 是本地报的还是服务端返回的,为什么本地没拦住? A:DeepSeek Harness 的这类 400 来自服务端,本地 precheck 主要校验 tool_use 与 tool_result 的配对,不看 thinking 是否齐全。要提前发现,需要在发送前对「带 tool_use 且 provider 要求 thinking 回传」的历史做一次显式校验。
常见问题
DeepSeek Harness 切回 deepseek-official 后 400,是因为旧会话有些轮次在关闭思考的模型上生成,历史没有 thinking 块。deepseek-official 要求工具调用轮次把 thinking 原样带回,本地序列化只在历史已有 reasoning 时才输出,于是服务端判定缺块并 400;新建会话没有这段历史。
DeepSeek Harness 的 thinking 块丢失不一定是压缩,更常见的是混合路由:某一轮用不支持或关闭了思考的 provider 生成,持久消息里本来就没有 reasoning。回到 deepseek-official 时序列化器不会凭空合成,历史就变成「有 tool_use、没有 thinking」。
DeepSeek Harness 的旧会话可以装 thinking-roundtrip-guard 插件,在 llm/stream 层对缺失轮次做 synthesize 或 degrade;也可以改 serialize.ts,在输出 tool_use 前检查同轮是否有 reasoning。两种做法都要先备份会话,并用一条短消息验证不再 400。
DeepSeek Harness 的这类 400 来自服务端,本地 precheck 主要校验 tool_use 与 tool_result 的配对,不看 thinking 是否齐全。要提前发现,需要在发送前对「带 tool_use 且 provider 要求 thinking 回传」的历史做一次显式校验。
相关术语
- thinking 块
- thinking 块是 DeepSeek 模型返回的思考内容,在工具调用轮次里需要随历史一起回传。服务端用它保持推理连续性,缺失时会拒绝请求。— https://github.com/deepseek-ai/deepseek-harness/discussions/10077
- serialize.ts
- serialize.ts 是 llm-deepseek 适配器把持久消息转换成请求体的模块,负责决定哪些 reasoning、tool_use、tool_result 进入本次请求。它只在历史已有 reasoning 时输出 thinking,不会主动合成。— https://github.com/deepseek-ai/deepseek-harness/discussions/10077
- 混合路由
- 混合路由是同一会话在不同 provider 或模型之间切换的做法。不同模型对 thinking 回传的要求不一致,切换后历史消息的字段完整性容易出问题。— https://github.com/deepseek-ai/deepseek-harness/discussions/10077
来源
- #10077 — 混合 provider 会话切回 deepseek-official 后必现 400:tool_use 历史缺 thinking 块· deepseek-ai(GitHub Discussions)
- DeepSeek Harness 官方仓库· deepseek-ai(GitHub)