Repository navigation
Replies: 1 comment 1 reply
|
解决了吗? |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
TL;DR(English)
When the model ends a turn with a narration such as "现在做 X" and no tool call,
dsh-agent-loopcorrectly recordsturn/end {kind:'completed'}— becausetoolCalls.length === 0is the only signal it has, and it cannot tell "I am done" from "I am about to act".In a real 29-turn session, 7 of 7 turns that the user had to answer with 「中断了请继续」 ended exactly this way, and all 7 carried the model's own
finish: {kind:"stop"}. 0 turns that ended on a tool call were ever followed by that message.The mechanical seam already exists and needs no core change:
packages/core/agent-loop/src/agent.ts:359-363re-checksinbox.nextStepafter dispatchingagent/turn-stopping, andAgent.steer()(:167-168) appends tonext-step. The shippedhooks-codexStop bridge (:266-274) already uses it, and the community plugindsh-wait-guard(#8082) verified it in production.What is actually missing is the predicate: which closing shapes should be held open? I ran an A/B on one real session — a closing-text semantic predicate scores 7/7 recall with 1 false positive; a todos/steps predicate scores 6/7 with 6 false positives (all of them "asked the user and is waiting"). I would like to know which layer upstream wants this predicate in, and against which signal.
一、具体问题
模型做完一段工作后,用一句「下一步要做什么」的旁白收尾、不调用任何工具。turn 就此结束,而它刚刚宣布的动作从未发生。 用户看到对话毫无理由地停住,只能手动输入「继续」。
这不是偶发。下面是一份真实会话的统计。
二、问题链
关键点:第 2 步的判定本身没有错。 「末条消息没有工具调用」在绝大多数情况下确实意味着这轮结束了 —— 问答应答、任务完成、请求用户决策,全都长这样。loop 看不见模型"打算做什么",它只看得见"有没有发起工具调用"。所以问题不在判定,在于没有任何一层在 turn 关闭前检查"你刚宣布的事做了没有"。
三、具体代码
以下行号取自 master
639ed015397290b3745d163aafe02ffee4aa3f84(2026-09-29),packages/core/agent-loop/src/agent.ts:终答判定:
钩子与它之后的重查(这是关键):
那行重查没有任何默认功能会用到。它存在的意义就是:如果某个监听器在派发期间把消息放回 inbox,turn 就不结束,循环多跑一步(
:364 target = 'next-step')。监听器可用的杠杆:
官方先例 ——
packages/hooks/hooks-codex/src/index.ts:266-274,同一个钩子、同一个手法:(另:
agent字段由融合分发器注入 ——packages/core/agent/src/dispatch.ts的fused = (payload) => ({ ...payload, agent }),所以派发点虽然只传{ turn, signal },监听器拿得到完整 payload。)四、相关日志
环境:Windows / DSH 0.1.7-rc.2 桌面版 /
provider: buddy,model: deepseek-v4.1-flash,会话日志session.v4.jsonl(1296 行,28 个turn/end)。turn/end 理由分布:
{"completed": 20, "aborted(user)": 8}(那 8 次aborted(user)是用户自己按的停止,与本文无关)。7 次「用户被迫输入继续」的收尾原文与 finish 信号:
现在做最后一个判决性测试:一条格式完整的多行块,确认多行命令能正常跑完。
{"kind":"stop"}>>。现在发一条单行命令调用它——这一次动作同时判定两件事:自愈是否生效、多行逻辑能否跑完。{"kind":"stop"}node_modules里断掉的dsh-quick-send链接打断。改走定向查询——只看配置文件,且不碰.credentials.yaml(凭据红线)。{"kind":"stop"}read走的是文本工具,图片是同一包里的另一条路径。看它注册成哪个工具、怎么触发。{"kind":"stop"}read_image,校验。{"kind":"stop"}officecli 1.0.153已就位。现在放 skill 到技能库。{"kind":"stop"}{"kind":"stop"}三点值得注意:
{kind:"stop"}—— 不是被截断,不是撞max_steps,不是 token 耗尽。生成完那个 text 块就落了停止符。read_image)。中间只隔了用户打的四个字。旁证:钩子在生产中确实被派发。 日志里有 6 个
workspace/changes事件(dsh-workspace-changes正是agent/turn-stopping的订阅者),其中 5 个紧贴turn/end之前:五、初步根因分析(三层)
① 直接原因(已验证) —— 末条消息的 text 块以「现在做 X / 下一步 X / 改走 X」结尾,模型随即发出停止符。从宿主的视角这轮确实结束了,turn/end 被正确记录。
② 结构性原因(推断) —— 模型的输出规范里存在两条硬要求:「结尾给一个具体下一步」与「每轮重述状态」。本意是让输出可扫读,但副作用是每条消息都被塑造成以「下一步:X」结尾。在里程碑点(工具刚成功),这个形状恰好与"收尾"重合,于是模型在这个位置画了句号,而不是继续调工具。我只在单模型单配置上观察到这个相关性,没有做对照实验,所以标为推断。
③ 无兜底(已验证) ——
turn-stopping是留给部署方的策略口,但默认组合里没有任何监听器做这件事。空着的口子等于没有。六、缺口在"判据",不在"机制"
这一点是我这轮调查最想说清楚的,因为它影响方案选型。
机制侧已经没有障碍:
steer()能拦住关闭,官方与社区插件都验证过。难的是判断什么形态该拦。 我在同一份会话日志上回放了两套判据(真值 = 用户下一条是否为「中断了请继续」):第二套的 6 个误报全部是「问了用户、正在等回答」的 turn —— 那类 turn 也是"干了很多活、没干完、无工具调用",但停下是正确的。这个判据分不出「停得对」和「停错了」。
第一套也不是免费午餐:它的误报来自模型陈述自身状态时碰巧用了动作动词;而它是语言/模型相关的(本例是中文),换模型或换语言要重新调。
七、一处与 #5583 回复不一致的实测(请指正)
#5583(终答前检查 todos 未完成项并注回提醒)提出用同一钩子做同一件事,方向我认为是对的。但该帖唯一一条回复给出了一个会劝退这条路的结论,我核对后认为它是错的,把证据放在这里供判断:
该回复自己引用的代码块里,最后一行就是那次重查。 我在四个快照上核对过这四行,全都存在:
dsh-agent-loop/lib/index.js):984:990639ed01(09-29):360:363891f07a2(09-04,回复发布当天):305:30887f24bb9(08-10):304:307即:该重查至少从 8 月 10 日起就在,不是后来补的,因此不能解释为版本差异。
回复中另有一句是对的("the loop reads
turnEnds... a guard cannot reach that variable through a serial event")—— 但break的条件是turnEnds && nextStep.length === 0,两个条件推翻一个就够。够不着turnEnds,够得着 inbox。同一版块里已经有人做成了这件事:
dsh-wait-guard(#8082)自带对照实验 —— 挂载时提前收尾被扣住 11–61 秒、turn/end全程为 0;卸载后同样的动作 2 秒就收尾。它的原话是 "anything injected keeps the turn alive for one more step"。(一个真实的坑,值得写进文档:
steer必须在被 await 的那个监听器里同步调用。若放到setTimeout或未被 await 的 promise 里,落点会晚于第二次检查,那时才真的会被忽略。此为推断,依据是await dispatch.serial的时序语义,我没有写反例测试。)八、想知道怎么解决
具体想请上游判断的:
这算谁的责任? 「纯文字收尾但宣布了动作」——是应当由某个 shipped guard 覆盖的场景,还是明确留给部署方的策略?如果是后者,我会照做,只是希望能有一句明确的表述,避免后来者再花一轮去争论机制可行性。
判据落在哪一层、以什么信号为准? 目前能看到三类候选:
todos投影还有未完成项)—— 精确率低,会把"等用户回答"误判;concludesTurn属性、或"本步是否有过工具调用意图"。若有,判据可以做得比正则可靠得多。#1514提到的Agent.cancel({kind:'hook'})是否才是"不许结束"的正式表达? 换句话说:Stop 语义应当表达为 cancel(中止本轮)还是 steer(延长本轮)?我这里选的是 steer,因为目标是"多跑一步"而不是"取消"。steer在turn-stopping里的时序契约是否需要写进文档?(见第七节末尾那条。)异步调用会静默失效,而失效的表现与"插件没装"完全一样。是否值得把
agent/turn-stopping列进docs/? Proposal: add `agent/turn-starting` and `agent/turn-aborting` hooks for turn-boundary observability #506 提议补turn-starting/turn-aborting时顺带讨论过它的语义,但那帖已关闭且没有结论。就目前而言,"这个钩子能拦住关闭"这件事只能靠读agent.ts的第 363 行才知道 —— 而有人读了那一行,仍然得出了相反结论,这或许说明它值得一句显式文档。九、我这边的状态(诚实标注)
finish信号;workspace/changes与turn/end的邻接;两套判据在同一日志上的 A/B 数字;官方hooks-codex的同手法用法。agent/turn-stopping监听 + 末句判据 +agent.steer),单测 20 个全通、Session.append接受手工构造的 user message 形状(用真实Session对象验过)。但端到端行为没跑过 —— 装进 profile 需要重启宿主,我这轮没做。所以上文的 A/B 是判据逻辑对日志的离线回放,不是插件在 loop 里的实测。import '@deepseek-ai/dsh-llm'。我的探针显示 ESM 解析成功,但不止一处社区文档记载会ERR_MODULE_NOT_FOUND。我按保守处理,插件零 import、手工构造消息对象。这个分歧本身可能也值得澄清。如果上游已有既定方案或已在别处讨论过,请指路,我照那个走。
All reactions