DeepSeek Harness 调 Gemini 每轮 400?$defs loaderExpression 排查
工具调用本身是成功的,坏的是它写进历史的那段 JSON:cordis_inspect_query 返回的配置 schema 带 $defs,下一轮作为 function_response 重放时,Gemini 把 $ref 当成了对多模态附件 display_name 的引用,找不到同名附件就返回 400,而且此后每轮都失败。 据讨论,报告者在桌面端复现,社区成员核对了 tool-cordis 的源码与返回结构(讨论原文)。
DeepSeek Harness 调 Gemini 的 400 从哪一轮开始
关键判据是「工具成功、下一轮开始 400、换会话恢复」,三条同时出现就基本可以锁定历史污染。 复现与观察顺序:
- 把模型切到 Gemini,预期请求走原生
function_response结构化回传路径,而不是纯文本工具结果。 - 让 Agent 调用
cordis_inspect_query,查询Config.listConfigs这类会返回带loaderExpression引用 schema 的项,预期返回对象里出现$defs。 - 观察这一轮,预期工具调用成功,没有任何报错。
- 继续下一轮对话,预期收到:
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
}
- 在同一会话里换模型或重试,预期仍然 400;新建一个会话,预期恢复正常。
DeepSeek Harness 为什么把 $ref 当成 display_name
Gemini 会扫描 function_response.response 里的 $ref,把它当作附件名字去 parts 里找;#/$defs/loaderExpression 是文档内指针,不是附件名,所以必然匹配失败。 社区核对到的来源是 packages/extensions/tool-cordis/src/config.ts:117:
return toJson({
...listed,
acceptsMissing,
limitations,
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$defs: { ...definitions, loaderExpression: LOADER_EXPRESSION_SCHEMA },
...schema,
},
})
拆开看是三步:
- 工具把投影出的完整 schema 作为结果返回,
$defs里同时含配置投影器的definitions和loaderExpression。 - 结果被写进会话历史,此时没有任何校验,因为它是合法 JSON。
- 下一轮按
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。
- 在
toJson()调用之前,对schema做一次深度遍历,预期每个节点都会被访问到。 - 遇到
$ref且值以#/$defs/开头时,取出$defs里对应的定义替换当前节点,并设置递归上限,预期引用被替换且递归不会因自引用死循环。 - 内联完成后删掉顶层
$defs,预期返回对象里不再出现#/开头的$ref。 - 用同一轮查询重跑,预期下一轮请求不再 400。
示意(不是仓库原代码):
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。
- 确认哪些调用方真的会读
schema字段,预期你会拿到一份需要结构化访问的调用方清单。 - 把
schema从返回对象里移出,或改成JSON.stringify(schema)后的字符串,同时保留$schema版本与摘要字段,预期结果里不再有嵌套的 schema 对象。 - 重跑查询,预期返回仍是合法 JSON,但
function_response.response里没有嵌套的$ref。 - 检查依赖
response.schema.$defs的调用方,预期需要同步改成解析字符串。
代价是结构化能力下降:需要按字段取值的消费方得自己再解析一次。
路径三:改查询返回,不再带 $defs
把 schema 从「数据」挪回「工具描述」是更正确的方向,但它改的是契约,不是一行代码。
- 让
cordis_inspect_query只返回配置项摘要与一个可用于后续查询的句柄,预期结果里不再带 schema。 - 把 schema 放到工具定义或单独的类型查询接口里,按需获取,预期 schema 有独立入口可取。
- 更新所有调用方,预期不再从结果里读
schema。 - 用 Gemini 重跑完整流程,预期历史里不再出现带
$defs的工具结果,其他 provider 读取摘要与句柄的路径也正常。
这套改法的正确性最高,但上线成本也最高,适合作为后续整理而不是当天止血。
DeepSeek Harness 已经中毒的会话怎么处理
没有官方恢复手段,坏结果已经在持久历史里,重试不会把它移走。 处理顺序:
- 新建一个会话继续工作,预期不再 400。
- 如果旧会话里有必须保留的内容,导出文本后手工搬到新会话,不要复制整段工具结果,预期新会话里只留下你选中的文本。
- 不要手工编辑
session.v4.jsonl.zstd,预期的压缩格式与 seq 校验都可能因此损坏,代价高于收益。 - 如果使用会话回滚能力,回滚到调用
cordis_inspect_query之前的位置,预期恢复可用。
DeepSeek Harness 修复后的复测与边界
修完只测「这一轮不报错」不够,要测「历史里再也不会出现带 $ref 的结果」。 步骤:
- 在 Gemini 下连续调用两次
cordis_inspect_query,预期两轮都成功。 - 检查会话日志里该工具结果的 JSON,预期不含
#/开头的$ref。 - 用不带
$defs的普通查询做对照,预期行为与修复前一致;再换一个非 Gemini 的 provider 跑同一查询,预期工具结果仍能被正常读取。
边界只有一条:这套修复只对「原生 function_response 结构化回传」敏感。如果哪天结果改成纯文本塞入,问题会消失,但结构化能力也会一起丢掉。
常见问题
DeepSeek Harness 里的 cordis_inspect_query 把一份带 $defs 的 JSON Schema 当作工具结果写进了会话历史,下一轮以 function_response 重放时,Gemini 把 $ref 当成对多模态附件 display_name 的引用去查找,找不到就返回 400。历史只增不减,所以之后每轮都失败。
DeepSeek Harness 里内联 $ref 会保留完整结构,只是不再有引用,消费方读到的仍是同一份 schema。删掉 schema 会让返回变成摘要或句柄,语义信息减少,所有依赖该字段的调用方都要改。
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
来源
- deepseek-harness Discussion #10167:Gemini 3.x 调用 cordis_inspect_query 后永久 400· deepseek-ai(GitHub Discussions)
- deepseek-harness 仓库:核对 tool-cordis 的 config.ts 返回值· deepseek-ai(GitHub)