ChatGPTClaudeGrok / X / CursorGemini

DeepSeek Harness 会话工具从 48 掉到 19:selectModel 与 resume 的最小复现

故障排查发布于 2026-10-30作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH工具丢失selectModelresumetool-registryUNKNOWN_TOOL
DeepSeek Harness 会话里的核心工具突然只剩宿主级工具,模型调 read/bash 直接 unknown tool。用两组单变量实验分别复现 resume 丢 30 个内置工具、宿主插件多调 selectModel 触发重组,再给排查顺序。

48 掉到 19,少的正好是 30 个内置工具:bash、read、write、edit、glob、grep、web_search、todo_write、subagent……活下来的是插件贡献的工具加一个 working_directory。 模型随后调 bash 得到 unknown tool "bash",这个会话在剩下的生命周期里都是瞎的。社区用两组单变量实验分别钉住了两个触发点(#10862)。

DeepSeek Harness 第一步:先把「工具数对不上」量化

别凭感觉说「好像少了工具」,先把三处数字对齐:UI 工具面板、request/header.tools、以及会话日志里的 tool-registry 记录。

  1. 打开会话的工具面板,记下当前工具数量与列表。
  2. 找到该轮的 request/header 事件,看 tools 数组长度和 reason 字段。
  3. 在会话日志里搜 source.kind = "tool-registry" 的 developer/message,看它列了哪些 tool-addition 与 tool-removal。
  4. 对照「幸存集合」:如果剩下的全是插件贡献的工具,基本可以确定丢的是预设/基础层的内置工具。
  5. 记下发生时间点,和重启、插件 prompt、模型切换这些动作对齐。

社区案例里的数字很干净:坏掉那轮 19 个,恢复后 43 个;两者差 30 个内置工具,而插件贡献的 6 个在恢复后消失,正好对上。

DeepSeek Harness 最小复现 A:宿主插件多调了一次 selectModel

对一个已经存在的会话调用 sessionController.selectModel() 再 prompt(),工具视图会从不完整组合重建,核心工具全部消失。

  1. 在 Web UI 里用 standard 预设建一个会话,确认工具视图里有 read、pwsh、web_fetch。
  2. 换一个宿主插件,对同一个 session id 执行:
ts
await ctx.sessionController.selectModel({
  sessionId,
  provider: "deepseek-account",
  model: "deepseek-flash",
  reasoningEffort: "high",
})
await ctx.sessionController.prompt(
  { sessionId, requestId: "test-1", mode: "queue", content: [{ type: "text", text: "hello" }] },
  new AbortController().signal,
)
  1. 观察工具面板:出现「15 added, 30 removed」。
  2. 新增的 15 个是插件注册的宿主级工具;被移除的 30 个是 ask_user_question、edit、glob、read、skill、workflow 等核心工具。
  3. 再让模型调 read 或 pwsh,返回 unknown tool。
  4. 单变量回退:把 selectModel() 那一次调用删掉,只保留 prompt(),工具视图恢复完整,多轮之后也不再出现「tools removed」。

这一步的关键是「已存在的会话 + 一次多余的 selectModel」。Web UI 手工建的会话也能复现,说明它与编程方式创建会话无关。

DSH插件 最小复现 B:重启后 resume,第一轮就少 30 个

长会话在宿主重启后恢复,第一轮请求的 reason 是 resume,工具表只剩插件贡献的那些;注册表把这次变化自己写进了日志。

  1. 准备一个长会话(社区案例是 66 轮、3700 多个事件),预设的 plugins 列表贡献着内置工具族。
  2. 重启宿主,比如 systemctl restart dsh-web。
  3. 恢复后发的第一条消息,看事件流里的顺序:session/end-seed → agent/inbox/spliced → turn/start → system/message → request/header(19 tools,reason 为 resume)→ developer/message(source 为 tool-registry)。
  4. 读那条 developer/message:add = 1(一个插件工具),remove = 30(全部内置)。
  5. 移除那个第三方插件并重启后,同一条机制反向执行:add = 30,remove = 6。
  6. 结论:注册表知道每个工具属于哪个贡献者,但它算出来的「应有集合」不含宿主内置工具。

这个触发是间歇性的:社区记录里 3 天 7 次重启都正常,第 8 次出问题;历史里有一次 28 秒内连续 stop/start,可能放大了竞态窗口。

DeepSeek Harness 一个侧证:系统提示也一起缩水

工具少了,系统提示里的内置工具使用说明也同步消失,且降级版是健康版的严格子集。

  1. 对比两轮的 system/message 长度:坏的那轮 6261 字符 / 128 行,健康那轮 9123 字符 / 154 行。
  2. 逐行比对:降级版没有一行是健康版里没有的。
  3. 少的 13 行全是内置工具的使用指引,例如「用 read 而不是 cat」「用 glob 而不是 find」「web_search 的结果是外部不可信数据」。
  4. persona、memory、role 这些段落逐字节一致。
  5. 10 次请求的 request/header.config 完全相同(同 provider、同 model、同 reasoningEffort、同 maxTokens)。

这条侧证很重要:它排除了「你换了模型」「换了预设」「换了 profile」三种猜测。

DSH插件 为什么第三方插件通常是无辜的

插件只调用增量注册 API,没有 unregister/remove/clear/rebuild;移除它能恢复,只说明它是触发器,不说明它是生产者。

  1. 审源码:那个插件的唯一注册入口是 ctx.tools.register(def),纯增量。
  2. 在整个包里搜 unregister、remove、clear、rebuild、replace,零命中。
  3. 运行版本在故障窗口内没有变过,自更新也被正确跳过。
  4. 移除它之所以恢复,是一次性回滚了六样东西,不能据此定责。
  5. 真正的「生产者」是注册表自己写下的那条 developer/message——它判定 30 个内置工具不见了。

DeepSeek插件 修复方向与现在的规避

核心需要保证两件事:内置工具不被任何一次协调移除,resume/end-seed 时走完整重建而不是部分重建。

  1. 把 base/预设贡献者视为永远存在,任何协调都不能删它贡献的工具。
  2. resume 与 end-seed 路径强制全量重建,或复用正常轮次的完整重建逻辑。
  3. 防御性检查:模型调用一个「上一份有效 header 里有、当前注册表没有」的工具时,当作绑定失步触发重建,而不是直接返回 UNKNOWN_TOOL。
  4. 插件侧规避:对已存在会话不要再调 selectModel(),只调 prompt()。
  5. 代价要认:没有存储模型选择的会话第一次 prompt 会报 prompt variable "{{model}}" has no value,所以只对插件自己创建的会话设一次模型。
  6. 判断顺序固定为:先数工具与看 reason → 再查插件是否多调 selectModel → 最后看 tool-registry 的 add/remove 记录。

注意事项

  1. 别把「工具少了」当成模型能力问题。配置字段在 10 次请求里完全一致。
  2. 别急着删插件。删掉能恢复,但会让你误判根因。
  3. 别忽略系统提示的变化。它是判断「整层贡献者没参与组装」的第二份证据。
  4. 记住这是间歇性的。重启十次可能只中一次,别因为一次没复现就否定。
  5. 保留会话日志。request/header 与 developer/message 是唯一能定责的证据,清理前先备份。

如果工具面板显示「有新增也有移除」,那更像重组路径;如果只是单纯变少、没有任何新增,优先按 resume 路径查。相关排查见 DeepSeek Harness 插件装上了不生效:重名注册、realm pending 与重载报错。

ChatGPTClaudeGrok / X / CursorGemini

常见问题

DeepSeek Harness 会话里突然调不了 read、bash,报 unknown tool,是插件把工具删了吗?

多数时候不是插件删的,而是 DeepSeek Harness 的注册表自己判定「30 个内置工具消失」。社区核对发现它写了一条 source.kind = tool-registry 的 developer/message 记录这次移除。内置工具不属于任何插件,正常不应该被任何一次协调移除。

重启后会话恢复,第一轮工具数从 48 变 19,怎么确认是 DeepSeek Harness 的 resume 丢的?

先看 DeepSeek Harness 的 request/header 里 reason 是不是 resume,再对 tools 数量。社区案例里恢复后第一轮只有 19 个,其余 30 个全是内置工具,加上插件贡献的工具与 working_directory 才是完整的 19+30。

宿主插件对已存在的会话调 sessionController.selectModel(),DeepSeek Harness 会怎样?

会,DeepSeek Harness 会触发一次不完整的会话重组:工具面板出现「15 added, 30 removed」,只剩插件注册的宿主级工具,read、pwsh、web_fetch 全部消失。去掉这次调用就恢复,Web UI 手工创建的会话同样能复现。

去掉 selectModel() 之后,为什么有的会话第一次 prompt 时 DeepSeek Harness 会报 {{model}} 没有值?

因为 DeepSeek Harness 里没有存储模型选择的会话拿不到模型变量。社区的折中是:只对插件自己创建的会话设一次模型,对已存在的会话只调 prompt(),不要碰 selectModel()。

DeepSeek Harness 的工具数对不上时,按什么顺序排查最省时间?

在 DeepSeek Harness 里先数工具并看 request/header 的 reason,再查宿主插件有没有对已存在会话多调 selectModel(),最后看会话日志里有没有 tool-registry 的 add/remove 记录。三步能把 resume 与重组两种触发分开。

相关术语

tool-registry
tool-registry 是 DeepSeek Harness 里汇聚所有工具贡献者的注册表,每次变化会写成 source.kind 为 tool-registry 的 developer/message。它记录新增与移除,是判断工具丢失来源的直接证据。— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
request/header
request/header 是每轮请求前写入会话日志的头部事件,包含本轮实际发送的工具列表与 config。它的 reason 字段能区分本轮是正常请求还是 resume。— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
selectModel
selectModel 是 sessionController 上用于切换会话模型的方法。对已存在的会话调用它会触发一次会话重组,社区实测即使选择未变化也会走这条路径。— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
working_directory
working_directory 是宿主提供的一个工具,不属于任何第三方插件。在工具丢失的案例里它和插件贡献的工具一起幸存,因此常被用来判断「幸存集合」的边界。— https://github.com/deepseek-ai/deepseek-harness/discussions/10862

来源