DeepSeek Harness 调 Gemini 每轮 400?$defs loaderExpression 排查

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessGeminiHTTP 400$defscordis_inspect_query
Gemini 调 cordis_inspect_query 后每轮 400,报 #/$defs/loaderExpression 不是 display_name:工具把带 $defs 的 schema 原样写进历史,重放时被当成多模态引用。给出内联 $ref、剥离 schema、改查询返回三条路径与中毒会话处理。

工具调用本身是成功的,坏的是它写进历史的那段 JSON:cordis_inspect_query 返回的配置 schema 带 $defs,下一轮作为 function_response 重放时,Gemini 把 $ref 当成了对多模态附件 display_name 的引用,找不到同名附件就返回 400,而且此后每轮都失败。 据讨论,报告者在桌面端复现,社区成员核对了 tool-cordis 的源码与返回结构(讨论原文)。

DeepSeek Harness 调 Gemini 的 400 从哪一轮开始

关键判据是「工具成功、下一轮开始 400、换会话恢复」,三条同时出现就基本可以锁定历史污染。 复现与观察顺序:

  1. 把模型切到 Gemini,预期请求走原生 function_response 结构化回传路径,而不是纯文本工具结果。
  2. 让 Agent 调用 cordis_inspect_query,查询 Config.listConfigs 这类会返回带 loaderExpression 引用 schema 的项,预期返回对象里出现 $defs。
  3. 观察这一轮,预期工具调用成功,没有任何报错。
  4. 继续下一轮对话,预期收到:
400: {
  "message": "The referenced name `#/$defs/loaderExpression` in function_response.response does not match to a display_name in the function_response.parts.",
  "type": "upstream_error",
  "code": 400
}
  1. 在同一会话里换模型或重试,预期仍然 400;新建一个会话,预期恢复正常。

DeepSeek Harness 为什么把 $ref 当成 display_name

Gemini 会扫描 function_response.response 里的 $ref,把它当作附件名字去 parts 里找;#/$defs/loaderExpression 是文档内指针,不是附件名,所以必然匹配失败。 社区核对到的来源是 packages/extensions/tool-cordis/src/config.ts:117:

ts
return toJson({
  ...listed,
  acceptsMissing,
  limitations,
  schema: {
    $schema: 'https://json-schema.org/draft/2020-12/schema',
    $defs: { ...definitions, loaderExpression: LOADER_EXPRESSION_SCHEMA },
    ...schema,
  },
})

拆开看是三步:

  1. 工具把投影出的完整 schema 作为结果返回,$defs 里同时含配置投影器的 definitions 和 loaderExpression。
  2. 结果被写进会话历史,此时没有任何校验,因为它是合法 JSON。
  3. 下一轮按 function_response 重放时 $ref 原样出现,Gemini 按附件名解析失败抛 400;历史只增不减,所以错误永久存在。

LOADER_EXPRESSION_SCHEMA 来自 @deepseek-ai/dsh-app-boot,但内联时不能只处理它——definitions 里也可能有引用。它和 Gemini thought signature 400 的触发点不同:那一条坏在签名回传,这一条坏在工具结果里的 $ref。

DSH插件修法:内联、剥离还是改契约

三条路都能让下一轮不再 400,差别在于改的是返回数据、工具契约,还是只绕开会话历史。 如果你把这个查询封装进自己的 DSH插件,改法同样适用;个人排障优先内联止血,团队再考虑改契约。

路径一:toJson 前把 $ref 内联

这是止血最快、语义最不变的做法:把 $ref 换成它指向的对象,输出体积略增,Gemini 拿到的仍是完整 schema。

  1. 在 toJson() 调用之前,对 schema 做一次深度遍历,预期每个节点都会被访问到。
  2. 遇到 $ref 且值以 #/$defs/ 开头时,取出 $defs 里对应的定义替换当前节点,并设置递归上限,预期引用被替换且递归不会因自引用死循环。
  3. 内联完成后删掉顶层 $defs,预期返回对象里不再出现 #/ 开头的 $ref。
  4. 用同一轮查询重跑,预期下一轮请求不再 400。

示意(不是仓库原代码):

ts
const deref = (node: any, defs: any): any =>
  Array.isArray(node) ? node.map((n) => deref(n, defs))
  : node?.$ref?.startsWith('#/$defs/') ? deref(defs[node.$ref.slice(8)], defs)
  : node && typeof node === 'object'
    ? Object.fromEntries(Object.entries(node).map(([k, v]) => [k, deref(v, defs)]))
    : node

路径二:工具结果剥离 schema 或序列化成字符串

如果消费方并不需要结构化读取 schema,最省事的办法是让它别以对象形式进入 function_response。

  1. 确认哪些调用方真的会读 schema 字段,预期你会拿到一份需要结构化访问的调用方清单。
  2. 把 schema 从返回对象里移出,或改成 JSON.stringify(schema) 后的字符串,同时保留 $schema 版本与摘要字段,预期结果里不再有嵌套的 schema 对象。
  3. 重跑查询,预期返回仍是合法 JSON,但 function_response.response 里没有嵌套的 $ref。
  4. 检查依赖 response.schema.$defs 的调用方,预期需要同步改成解析字符串。

代价是结构化能力下降:需要按字段取值的消费方得自己再解析一次。

路径三:改查询返回,不再带 $defs

把 schema 从「数据」挪回「工具描述」是更正确的方向,但它改的是契约,不是一行代码。

  1. 让 cordis_inspect_query 只返回配置项摘要与一个可用于后续查询的句柄,预期结果里不再带 schema。
  2. 把 schema 放到工具定义或单独的类型查询接口里,按需获取,预期 schema 有独立入口可取。
  3. 更新所有调用方,预期不再从结果里读 schema。
  4. 用 Gemini 重跑完整流程,预期历史里不再出现带 $defs 的工具结果,其他 provider 读取摘要与句柄的路径也正常。

这套改法的正确性最高,但上线成本也最高,适合作为后续整理而不是当天止血。

DeepSeek Harness 已经中毒的会话怎么处理

没有官方恢复手段,坏结果已经在持久历史里,重试不会把它移走。 处理顺序:

  1. 新建一个会话继续工作,预期不再 400。
  2. 如果旧会话里有必须保留的内容,导出文本后手工搬到新会话,不要复制整段工具结果,预期新会话里只留下你选中的文本。
  3. 不要手工编辑 session.v4.jsonl.zstd,预期的压缩格式与 seq 校验都可能因此损坏,代价高于收益。
  4. 如果使用会话回滚能力,回滚到调用 cordis_inspect_query 之前的位置,预期恢复可用。

DeepSeek Harness 修复后的复测与边界

修完只测「这一轮不报错」不够,要测「历史里再也不会出现带 $ref 的结果」。 步骤:

  1. 在 Gemini 下连续调用两次 cordis_inspect_query,预期两轮都成功。
  2. 检查会话日志里该工具结果的 JSON,预期不含 #/ 开头的 $ref。
  3. 用不带 $defs 的普通查询做对照,预期行为与修复前一致;再换一个非 Gemini 的 provider 跑同一查询,预期工具结果仍能被正常读取。

边界只有一条:这套修复只对「原生 function_response 结构化回传」敏感。如果哪天结果改成纯文本塞入,问题会消失,但结构化能力也会一起丢掉。

常见问题

DeepSeek Harness 调 Gemini 后每轮 400,$defs 报错是什么原因?

DeepSeek Harness 里的 cordis_inspect_query 把一份带 $defs 的 JSON Schema 当作工具结果写进了会话历史,下一轮以 function_response 重放时,Gemini 把 $ref 当成对多模态附件 display_name 的引用去查找,找不到就返回 400。历史只增不减,所以之后每轮都失败。

DeepSeek Harness 中把 $ref 内联和删掉 schema,有什么区别?

DeepSeek Harness 里内联 $ref 会保留完整结构,只是不再有引用,消费方读到的仍是同一份 schema。删掉 schema 会让返回变成摘要或句柄,语义信息减少,所有依赖该字段的调用方都要改。

DeepSeek Harness 里已经 400 的会话还能恢复吗,只能新建会话?

DeepSeek Harness 没有官方恢复手段,坏结果已经写进持久历史,重试与新模型都不会把它移走。只能新建会话或回滚到中毒前的历史;手工编辑会话文件风险很高,不建议。

相关术语

JSON Schema $defs
`$defs` 是 JSON Schema 2020-12 中存放可复用子 schema 的容器,其他位置通过 `$ref` 指向它。它出现在数据里时只是一段普通 JSON,但特定 API 可能给它附加额外语义。— https://json-schema.org/draft/2020-12/json-schema-core
function_response
`function_response` 是 Gemini 原生工具调用协议中回传工具执行结果的结构,与 `function_call` 配对。它的 `response` 字段是结构化对象,会被服务端按协议规则解析。— https://ai.google.dev/gemini-api/docs/function-calling
display_name
`display_name` 是 Gemini 多模态 parts 中给附件起的名字,供模型或服务端引用。协议允许在 `function_response` 里按名字引用附件,这也解释了 `$ref` 为什么会被误认。— https://ai.google.dev/gemini-api/docs/function-calling

来源