DeepSeek Harness Gemini 工具调用 400:thought_signature 缺失怎么修

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessGeminithought_signature工具调用400 报错
DeepSeek Harness 经 OpenAI 兼容端点调 Gemini 工具时第二轮报 400,提示缺失 thought_signature。根因是 pi-ai 建工具调用块时不读该字段。本文给出本地补丁回放、升级对齐偏移、改用原生适配器三条路径。

DeepSeek Harness 的签名在第一次响应里就送到了,只是适配器建块时没接住,回放时自然发不回去。 这类 400 与密钥、模型名、配额都无关,报错只出现在「带工具调用的第二轮」,第一轮永远是成功的。下面按数据流定位,再给三条处理路径(来源)。

DeepSeek Harness 报错原文与触发条件

只要一轮里发生过工具调用,下一轮就会 400;把工具去掉或换模型,同一会话立刻恢复正常,这就是最省事的判别方法。

Function call is missing a thought_signature in functionCall parts. This is required
for tools to work correctly, and missing thought_signature may lead to degraded model
performance. Additional data, function call `default_api:get_weather`, position 2.
  1. 用 Google 的 OpenAI 兼容端点(https://generativelanguage.googleapis.com/v1beta/openai/)配置一个模型。预期能正常对话。
  2. 提一个必须调用工具的问题(例如查天气),确认第一轮工具调用成功、结果也回来了。预期界面显示工具执行完成。
  3. 让模型基于工具结果继续回答。预期这一轮返回 HTTP 400,报错里点名某个 function call 的 position。
  4. 换成不带工具的纯对话再试一次。预期正常返回,说明只有工具回放路径有问题。
  5. 如果第 3 步报的是 400 status code (no body) 而不是上面那段文字,先看本文最后一节,那是另一个错误体形状问题。
  6. 注意措辞:报错说的是「缺失」而不是「无效」,所以不必怀疑密钥或项目权限。

签名丢在哪一步:DeepSeek Harness 四段链路逐段排除

链路是「响应带签名 → 建块时不读 → 持久化里没有 → 回放请求缺字段」,只有第一段是 Google 的行为,后三段都在本地。

  1. 抓一次原始响应,看 tool_calls 里是否有 extra_content.google.thought_signature。预期能看到一长串 base64 风格字符串,说明签名确实到达了。
  2. 查这个签名有没有落到工具调用块上。社区核对发布包的结果是:openai-completions 的建块函数里搜不到 extra_content 与 thought_signature,字段在第一次接触时就被丢掉了(据讨论)。
  3. 查 harness 侧的持久化与回放。讨论里确认 thoughtSignature 本来就在工具调用块上有定义,消息帧会保存它,切模型时也会被清理,所以本地并没有二次丢字段。
  4. 得出结论:修的地方只有一处——适配器建块与回放转换。不要在 Agent 循环或 UI 上找。如果 400 换成工具参数 schema 被拒,那是另一个问题,见 DeepSeek Harness 工具参数 schema 被拒排查。

路径一:给 DeepSeek Harness 打本地 patch 捕获并回放

这是最短路径,两处改动即可闭环:建块时把签名存进块的 thoughtSignature,回放时再写回 extra_content。 装 DSH插件 不会自动修好这层字段丢失,补丁必须落在 pi-ai 的适配器上。

  1. 在项目里启用 patch 机制(例如 pnpm 的 patchedDependencies),把 @earendil-works/pi-ai 钉到与补丁匹配的版本。预期安装日志里出现该包的 patch 记录。
  2. 在 dist/api/openai-completions.js 的建块函数里,读完参数字符串后补一段捕获逻辑:
js
const extraThoughtSignature = toolCall.extra_content?.google?.thought_signature;
if (extraThoughtSignature && !block.thoughtSignature) {
  block.thoughtSignature = extraThoughtSignature;
}

预期块对象上出现 thoughtSignature 字段,且值就是响应里那串签名。

  1. 在回放转换消息的地方,把签名写回工具调用项,预期下一个请求的 tool_calls[] 里重新出现 extra_content.google。
  2. 重跑第 2 步的复现流程,预期第二轮不再 400、模型能吃到工具结果。
  3. 故意改坏签名再试一次。预期报错从 400 status code (no body) 变成明确的 Function call is missing a thought_signature——这需要同时打上错误体解包补丁,否则只解决数据丢失,不解决报错可读性。
  4. 记录补丁对应的包版本,升级依赖前必须重新生成。预期任何一次依赖解析变化都能被这份记录发现。
  5. 把补丁文件与锁文件一起提交前,先跑 git apply --check 确认补丁能干净套用,预期输出无 rejected hunk。
  6. 如果只想验证签名链路、暂时不想改代码,可以先在回放请求外挂一个日志,打印工具调用块的 thoughtSignature 是否存在——预期第一轮有值、第二轮仍为空,就确认问题在捕获端。

路径二:升级 DeepSeek Harness 的 pi-ai 并核对补丁偏移

升级是长期解法,但不能盲升:补丁按行号生成,版本一变偏移就变,必须先确认三处改动区域还在原位。

  1. 查当前解析到的 pi-ai 版本。预期仓库声明是 ^1.1.0 这种范围,实际安装版本可能已经更高。
  2. 在目标版本里搜索 ensureToolCallBlock 与 convertMessages 两个函数,确认它们仍存在且结构未大改。预期函数名不变。
  3. 用 git apply --check 对目标版本的发布包试打补丁。预期要么干净通过,要么明确报偏移失败,不要用 --fuzz 硬糊过去。
  4. 如果偏移失败,按新行号重做 hunk,只移动上下文,不改逻辑。预期补丁再次通过 --check。
  5. 升级后跑一轮真实的工具调用两轮流程验收,预期签名往返正常、块索引与工具 id 都没错位。
  6. 把补丁文件与锁文件一起提交,避免别人解析到新版本时补丁静默失效。

路径三:改用 DeepSeek Harness 的原生 Gemini 适配器

如果你不需要 OpenAI 兼容层,原生适配器能绕开 extra_content 这一整套私有字段约定,代价是请求映射与工具 schema 全换一套。

  1. 在模型配置里为 Gemini 单独建一个 provider,选用原生 Gemini 协议而不是 openai-completions。预期设置页能列出该模型。
  2. 用新 provider 新建一个会话,先跑纯对话,再跑一次工具调用。预期两轮都通。
  3. 检查历史回放:新会话里工具调用块应带签名,第二轮不报错。预期行为与路径一一致。
  4. 老会话不要直接切过去续跑,先复制出关键上下文重建。预期避免旧块缺签名导致同样的 400。
  5. 确认工具 schema 在原生协议下的映射没有丢字段(尤其是嵌套对象与枚举)。预期工具调用参数与原来一致。
  6. 保留旧 provider 的配置项,先别删。预期出问题时能一键切回并按路径一打补丁。

DeepSeek Harness 修复后的验证与落地边界

验收只看一件事:同一个会话连续两轮工具调用都不报签名错,且第三轮仍能正常续聊。

  1. 第一轮发起工具调用,确认响应里带签名。
  2. 第二轮确认不 400,且模型正确引用工具结果。
  3. 第三轮再发起一次工具调用,确认签名在多次往返后仍被保留。
  4. 导出会话,确认工具调用块里能看到签名元数据,而不是空字段。
  5. 再换一个工具(不同参数结构)跑一遍,预期签名行为一致——签名与工具名无关,若这个工具失败,说明补丁捕获位置不对。

落地前还要记住的边界

  1. 错误体是单元素数组 [{error:{...}}] 时,OpenAI SDK 会读不到 error,整轮塌成 400 status code (no body)。这和签名丢失是两个上游问题,别混在同一个补丁里判断。
  2. 讨论里的补丁是针对发布 tarball 的,不是源码仓库补丁,升级后要重新对。
  3. 切模型时签名会被清理,这是设计行为,别为了「保住签名」去改这段逻辑。
  4. 原生 Gemini 适配器与 OpenAI 兼容端点的工具 schema 映射不完全等价,切换后要重测参数。
  5. 以上行号与字段行为均为社区核对发布包所得,官方未确认修复,按临时方案对待。
  6. 签名是不透明字符串,不要截断、转义或做长度校验,原样存取即可。
  7. 补丁只解决本项目安装;团队里其他人重新 install 时会按锁文件重放补丁,锁文件没更新就等于没修。

常见问题

Gemini 工具调用第二轮报 400,提示缺少 thought_signature,是什么原因?

DeepSeek Harness 的 400 来自签名回放缺失:签名在第一次响应里就到了,但适配器建工具调用块时没有读 extra_content.google.thought_signature,块里没有地方存它,回放时也就发不回去。Google 在第二轮校签失败,直接返回 400。

给 DeepSeek Harness 的 pi-ai 打本地补丁让 signature 往返,需要改几处?

DeepSeek Harness 的本地补丁要改两处:建工具调用块时把 extra_content.google.thought_signature 存进块的 thoughtSignature,回放转换消息时再把它写回 extra_content。块字段本身已被持久化和跨模型清理,不需要新增序列化逻辑。

为什么升级 DeepSeek Harness 的 pi-ai 之前要先核对补丁偏移?

DeepSeek Harness 仓库声明的是 ^1.1.0,而 npm 上已经到 1.3.0 一类的新版本。补丁是按 1.1.0 的文件行号生成的,打到新版本上要么被拒绝,要么错位套用到别的代码段。

换用 DeepSeek Harness 的原生 Gemini 适配器能绕过 thought_signature 问题吗?

DeepSeek Harness 的原生 Gemini 适配器能绕开 OpenAI 兼容层对 extra_content 的处理,但会换一套请求与工具 schema 的映射,历史会话里的工具调用块需要重新验证。它适合新会话或可重建的会话,不适合直接续跑老会话。

相关术语

thought_signature
thought_signature 是 Gemini 在工具调用响应里附带的不透明签名,要求客户端在下一轮把同一签名的调用原样回传。缺签名时 Google 会拒绝该轮请求并返回 400。— https://github.com/deepseek-ai/deepseek-harness/discussions/10210
extra_content.google
extra_content.google 是 OpenAI 兼容响应里承载 Google 私有元数据的字段命名空间。thought_signature 就放在这一层,OpenAI 官方 SDK 与多数兼容适配器不会主动解析它。— https://github.com/deepseek-ai/deepseek-harness/discussions/10210
openai-completions 适配器
openai-completions 适配器是 pi-ai 中对接 OpenAI Chat Completions 协议的实现,负责把流式 delta 转成内容块并把历史块转回请求消息。字段在这一层被丢弃,下游无从恢复。— https://github.com/deepseek-ai/deepseek-harness/discussions/10210

来源