DSH plugin goal 回合 ask_user_question 答案被丢弃?自治提问机制排查

故障排查发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH plugingoal 回合ask_user_questionagent 空转
自治 goal 回合里模型调用 ask_user_question、你答了却没人收到,模型继续空转几十轮?根因是 goal 回合与用户同 runtime root 且嵌套答案只在 log/return 可见;用 agent-scoped ctx.tools.guard() 在自治回合禁用交互提问。

在自治 goal 回合里,模型调用 ask_user_question 拿到的不是你的答案,而是一句空洞的返回值,于是它继续空转几十轮——根因是两个独立事实叠加:goal 回合与用户共享同一个 runtime root(现有的交互拦截只针对子代理,管不到自治回合),而 Code Mode 下嵌套的工具答案只有被程序 log 或 return 才会重新进入模型上下文。 正解是用 agent-scoped 的 ctx.tools.guard() 在该回合生命周期内拒绝 ask_user_question,让模型转向 update_goal(blocked);官方 in-tree 修复已就绪待有人提 PR,过渡期有行为等价的社区插件。

DeepSeek Harness goal 回合空转现象

现象看起来像「模型不听人话」,实际是「模型根本没收到人话」。 事故记录如下:

  1. 嵌套提问 + 两次回答都没生效:自治 goal 运行(Code Mode / run_code,同会话 goal,256 回合上限)中,模型在 run_code 里嵌套调用了 await tools.ask_user_question({...}); return "asked";。用户回答了两次,答案在会话日志里确实作为内层 tool/code-dispatch 结果存在,但模型看到的外层 tool/result 两次都只是 "asked"#6074)。
  2. 随后是长尾空转:模型接着跑了 30+ 个 goal 回合,每轮都在执行验证切片,并且都以「Awaiting your two decisions」收尾——它在等一个永远不会到达的答案。
  3. 驱动被挂起的问题拖住:一个待答问题不声明任何超时预算,驱动会一直等。实测第一个答案花了约 8.5 分钟才到达,这期间整个自治流程处于停滞。
  4. 两个原因各占一半:① 自治回合没有交互提问的封锁机制;② Code Mode 的返回值语义让嵌套答案静默丢失。只修其中一个都只能缓解一半。
  5. 调试时的关键判据:判断是不是这种情况,不要看「我问了、模型也说在等」,而要对齐三层——内层 tool/code-dispatch 回执里有没有答案、外层 tool/result 里有没有答案、以及程序是否把它写回了上下文。

DeepSeek Harness 机制:goal 回合与用户同 runtime root

这条裂缝的成因是两个精确的源码事实,都不是「模型不听话」。 逐层拆解:

  1. 交互拦截只覆盖子代理dsh-user-questions 只拒绝活跃子代理(DELEGATED_CALLER),而 goal 回合作为同一个 runtime root 运行,因此没有任何东西阻止它在自治过程中发起交互询问。
  2. 挂起问题没有预算:待答问题不声明超时,驱动无限期等待,于是「没人回答」这件事永远不会被翻译成「该 blocked 了」,回合只能悬停。
  3. Code Mode 只回灌 log/return:在 Code Mode 下,只有程序自身的 log 与 return 会重新进入模型上下文;嵌套在 run_code 内的工具返回值必须由程序显式写回,否则执行成功也等于没发生。
  4. ctx.tools.guard() 是现成的核心原语packages/core/tools/src/index.ts:704 定义了 type ToolGuard = (execution) => string | undefined——返回字符串即拒绝该次执行:1100 注册 guard(guard: ToolGuard): () => void,注释写明它注册在可扩展的 tools/pre-execute waterfall 之后,并且「普通上下文的 guard 全局生效,通过 agent.ctx 注册的 guard 只对该 agent 生效」。这正是「仅在自治回合禁用」需要的 scoped 语义。
  5. 为什么不走 restrict()tools.restrict():1061)同样要求 scoped context,:1064 会直接抛「requires a scoped context (agent.ctx)」;更关键的是它把工具从菜单里藏起来,而 guard 的拒绝是可捕获的 ToolCallError——模型能明确看到「这个工具在当前回合被禁」,从而转向 update_goal(blocked) 或继续推进,这正是自治回合需要的行为。
  6. 插件可挂载在同一接缝上packages/core/agent-loop/src/inbox.ts:114agent/inbox/claimed 事件(claim 一条消息时触发)加上 session/eventuser/message(对应 admitted)与 turn/end / turn/start,让插件可以在不触碰 driver 源码的前提下复现同样的语义。

用 DSH plugin 的 ctx.tools.guard() 禁用交互提问

做法是「识别 goal 窗口 → 装 agent-scoped guard → 在窗口关闭时卸载」,官方修复与社区插件共用同一套原语——这也是当前 DSH插件 生态与其它 DeepSeek插件 通用的挂载方式。 具体如下:

  1. 窗口识别:与 goal-round-driver 完全一致——source.kind === 'goal'round > 0 才算自治回合,因此不会误伤用户在非 goal 回合的正常提问,窗口外一律透传。
  2. 安装点:在 agent/created + agent/inbox/claimed 上注册 agent-scoped 的 ctx.tools.guard(),对 ask_user_question 返回拒绝原因。官方 in-tree 版本在 packages/goal/goal-round-driver/ 内实现(新增 DriverState.askGuardDisposeGOAL_ROUND_ASK_DENIALinstallAskGuard / clearAskGuard 帮助函数),核心那段很短:
ts
/** Deny interactive human questions while an admitted goal round owns the turn. */
const denyAskDuringRound: ToolGuard = execution =>
  execution.name === 'ask_user_question' ? GOAL_ROUND_ASK_DENIAL : undefined
  1. 卸载点必须齐全:只在 turn/end 清是不够的——被丢弃或被抢占的已认领回合如果只在回合结束时清理,guard 会在该回合剩余时间里继续误拦合法的非 goal 提问。完整清单还包括 agent/session-startagent/inbox/inserted(竞争输入)、agent/inbox/discarded(被取代)与 agent/disposed;多代理宿主上 turn/end 还要按 agent 路由(经 session.id → agent),否则 A 的回合结束会提前清掉 B 的窗口。
  2. 拒绝文案要指路:把拒绝写成「该回合将继续自行推进」还不够,应给出与 in-tree 相同的 GOAL_ROUND_ASK_DENIAL 文本,明确引导模型用 update_goal(blocked)(附具体 blocked_reason)把需求记录下来,而不是静默重试。
  3. 过渡期用社区插件@argszero/cordis-plugin-goal-ask-guard 用上述原语在 agent/created 接缝复现同等语义,v0.1.1 补齐了按 agent 路由的 turn/end、额外的清理点、监听器卸载与共享拒绝文案(13/13 测试通过):
sh
npm install @argszero/cordis-plugin-goal-ask-guard
yaml
- insert:
    - id: goal-ask-guard
      name: '@argszero/cordis-plugin-goal-ask-guard'
  1. 插件装卸走插件市场:在 DSH Plugin Hub 的「设置 → 插件市场」安装与更新这类 guard 插件,比手工往 profile 里写 mount 行更安全(失败会回滚 manifest)。若你同时在排查其它 guard 类兜底(重复调用、参数失控),参见 工具参数 runaway 排查

DSH plugin 排查注意事项

别只怪模型——要分三层对齐「答案到底有没有回到模型上下文」,这才是本问题的关键判据。 六条要点:

  1. 别只怪模型:内层回执、外层 tool/result、程序是否写回上下文这三层要分别看。
  2. DELEGATED_CALLER 管不到自治回合:goal 回合与用户同 root,所以不会被当成子代理限制。
  3. guard 不要用 restrict() 替代:藏工具会让模型无从判断原因,可捕获的拒绝才能让它转向 update_goal(blocked)
  4. 清理点宁多勿少turn/end 之外还有 session-start、竞争输入、被取代与 disposed。
  5. 多代理宿主注意作用域:窗口按 agent 隔离,别用全局 turn/end 监听。
  6. Code Mode 下答案要显式写回:这是独立于 guard 的另一半根因,工具返回值不会被自动回灌。
DSH Plugin Hub 插件市场:安装 goal 回合 guard 插件、查看版本与更新

来源:Discussion #6074cordis-plugin-goal-ask-guardnpm @argszero/cordis-plugin-goal-ask-guard

常见问题

为什么在 DeepSeek Harness 的 goal 自治回合里我回答了 ask_user_question,模型却像从没收到?

在 DeepSeek Harness 里这是两个独立事实叠加的结果:goal 回合与用户共享同一个 runtime root,而负责拦截交互提问的 dsh-user-questions 只拒绝活跃子代理(DELEGATED_CALLER),没有任何机制阻止自治回合发起交互询问。同时在 Code Mode 下只有程序自己的 log/return 会重新进入模型上下文,所以嵌套在 run_code 里的答案会被静默丢弃。实测里外层 tool/result 两次都只回了 "asked",模型随后跑了 30+ 个 goal 回合,每轮都以「Awaiting your two decisions」结束(来源:Discussion #6074)。

在 DeepSeek Harness 中,为什么 goal 回合挂起的 ask_user_question 不会超时,反而把整个回合拖住?

在 DeepSeek Harness 中,一个待答问题不声明任何超时预算,所以驱动会一直等下去。实测里第一个答案花了约 8.5 分钟才到达,整段时间驱动处于停滞状态。它既不会被自动取消,也不会让模型知道「没人会回答」,于是自治回合只能悬在那里(来源:Discussion #6074)。

在 DeepSeek Harness 中,修复 goal 回合交互提问的正确落点是什么,为什么不用 tools.restrict() 把工具藏起来?

在 DeepSeek Harness 中,正确落点是 agent-scoped 的 ctx.tools.guard():guard 返回字符串即拒绝该次执行,且通过 agent.ctx 注册的 guard 只对该 agent 生效,正好匹配「仅在自治回合禁用」的语义。它比 restrict()-hiding 更合适,因为 guard 的拒绝是一个**可捕获的 ToolCallError**——模型能看到「此工具在当前回合被禁」,从而转向 update_goal(blocked) 记录需求或继续推进,而不是面对一个凭空消失的工具(来源:Discussion #6074)。

DeepSeek Harness 官方修了这个问题没有?在维护者合并之前,等待期间有什么过渡办法?

DeepSeek Harness 的修复已以 in-tree 分支形式就绪,只是还缺少有权限的维护者开启 PR。具体是 packages/goal/goal-round-driver/ 增加 DriverState.askGuardDisposeGOAL_ROUND_ASK_DENIALinstallAskGuard/clearAskGuard,88 插入 4 文件,包内 57/57 测试通过,但提交者没有该仓库的 PR 权限,需维护者从分支开启。过渡期可用社区插件 @argszero/cordis-plugin-goal-ask-guard(v0.1.1,13/13 测试),它用同一套既有原语在不碰 driver 源码的前提下复现同等语义(来源:Discussion #6074)。

相关术语

goal round(goal 回合)
`goal round` 是自治目标模式下由 continuation 驱动的一轮执行,通过消息来源 `source.kind === 'goal'` 且 `round > 0` 识别。它与用户交互共享同一个 runtime root,因此不会被当成子代理来限制交互提问。https://github.com/deepseek-ai/deepseek-harness/discussions/6074
ctx.tools.guard()
`ctx.tools.guard()` 是核心已有的工具守卫原语:在可扩展的 tools/pre-execute waterfall 之后注册一个单调 guard,返回字符串即拒绝该次执行;通过 agent.ctx 注册时仅对该 agent 生效(工具菜单不会隐藏,拒绝以可捕获的 ToolCallError 形式暴露)。https://github.com/deepseek-ai/deepseek-harness/discussions/6074
Code Mode return-value semantics(Code Mode 返回值语义)
`Code Mode return-value semantics` 是指在 Code Mode 下模型只能看到程序自身的 log 与 return,工具返回值必须由程序显式写回,否则即使执行成功也不会重新进入模型上下文。https://github.com/deepseek-ai/deepseek-harness/discussions/6074

来源