Agent 与 Turn 流程
这一页讲用户发出一句话后,DSH 怎样把它变成模型请求、工具执行和最终结果。官方架构里的 Turn 是一轮工作,Step 是这一轮中的一次模型请求加上它发出的工具调用。
亲手看一次 Turn 展开
一次 Turn 不是一次模型调用。下面这个组件把一次带两个工具调用的 Turn 展开成 20 步,其中模型请求出现 4 次——每拿到一个工具结果都要把它带回模型才能继续。同一个组件切到「不调用工具」场景时,模型请求只有 1 次。组件下半部分是一个沙盒:消息长度、工具次数、失败位置、被拒开关和中止红线五个输入连续可调,拖到「首次领取被拒」就能亲手拼出一条零 Step 的 Turn。
组件的横轴是步骤序号,不是时间:它不能说明真实 token 数、真实耗时或真实重试次数,也不能说明真实 DSH 的阶段名与这里相同。
往下滚动时,右边六段解说会逐段展开同一条轨迹:数据来自和上面组件完全相同的模型函数,只是把每一步的来龙去脉拆开讲。不想滚动就点任意一段跳转;整条轨迹在下方表格里也逐行列出。
一次最完整的流程
turn/start
领取下一步输入和一条排队消息
组装提示词片段和工具 schema
agent/pre-step
拒绝 -> 直接关闭 Turn
接受 -> step/start
记录进入模型的 user/message
从 Session 日志推导模型历史
agent/request
-> llm/stream
-> assistant/chunk 许多次
-> assistant/message
如果有 tool-call
-> tool/call
-> tools/pre-execute
-> tools/execute
-> tools/post-execute
-> tool/result
step/end
还有工具结果要处理或有下一步输入 -> 再开一个 Step
没有欠账 -> agent/turn-stopping
turn/end这张图中只有一部分是直接函数调用。turn/*、step/*、user/message、assistant/* 和 tool/* 是写入 Session 日志的持久事件(官方架构文档的分类),而 agent/pre-step、agent/request、llm/stream 和三个 tools/* 阶段是运行中的 live 扩展点,插件因此可以在中间观察、修改、拒绝或提供能力。要判断某件事的先后顺序,应以事件定义和测试为准。
为什么要分 Turn 和 Step
一个用户目标可能需要模型先读文件,再调用工具,再根据工具结果继续回答。把每一次模型请求叫 Step,可以明确一次请求的开始和结束;把一整段连续工作叫 Turn,可以知道何时应该把“这次工作”视为结束。
这样拆开还有三个好处:
- Session 可以记录一个 Turn 没有 Step 就被拒绝或取消的情况。
- 工具执行的结果可以让同一个 Turn 进入下一个 Step,而不是伪装成新的用户请求。
- 取消和错误可以分别关闭当前 Step、当前 Turn 或整个 Agent 工厂。
输入不是直接塞进 prompt
Agent 有 inbox。直接用户消息、注入的运行时上下文和 goal continuation 都要经过领取和事件记录。next-turn 的消息会唤醒下一轮,next-step 的消息要等当前工作继续时被领取。队列的修改会进入 agent/inbox/spliced,恢复和调试时能据此从日志还原每次改动。
agent/pre-step 为什么重要
这是决定哪些消息进入 Step、并可以拒绝本次输入的关键扩展点。插件可以在这里加入工作区指令、时间、技能或目标上下文;之后仍会经过 agent/request 和 llm/stream。首次领取被拒绝时,DSH 仍然记录一个没有 Step 的 Turn 结束,这让日志能解释“为什么用户发了话却没有模型请求”。
工具调用怎样回来
模型流里出现 tool-call block 时,Agent Loop 不直接执行函数,而是把它交给 Tools 服务。Tools 会重新检查工具是否可见(三层集合怎样收窄,见工具可见性实验)、参数是否是可接受的 JSON、是否需要审批(ask 的完整生命周期,见审批流实验)、是否允许并发,然后发布执行和结果事件。结果回到 Session 后,下一次 prompt 由日志推导,而不是由 Agent 私自拼一段隐藏文本。
取消和失败
取消有来源:用户、父 agent、hook 或 dispose。工具可能在真正启动前被取消,也可能已经启动后被取消;这两种结果需要区分。LLM 的认证、限流、网络和流截断也会变成结构化 LlmFailure,最终由 Turn 记录原因。
打开 agent.ts 时值得停留的三处
教材到这里给了你流程图和扩展点的地图。下面三处是你自己打开 packages/core/agent-loop/src/agent.ts 时容易走过但值得停下来的地方——每处只告诉你它是什么,不替你读。
三态生命周期。 Agent 不只是"在跑"和"没跑"。Phase 类型有三个分支:idle、maintenance 和 running。maintenance 阶段处理恢复和清理,不接受新的 Step。如果你只看 Turn 事件,会以为 Agent 只有两种状态;打开源码看 setPhase 才能发现中间态的存在和它为什么必要。
插件提议前的默认值剥离。 requestProposal 函数在插件提出下一次请求配置之前,把 adapter 写入的 reasoningEffort 和 maxTokens 从 header 中删掉。这确保插件看到的是用户可配置的参数,而不是 adapter 的内部实现细节。如果你在日志里发现这两个字段"消失了",这就是原因。
请求锚点的防重入。 requestHeaderLogged 布尔标志保证 initial/resume 请求的锚点事件只写一次。没有这个标志,每次 Agent 从日志恢复都会重复写一条锚点,日志里就会出现无法配对的孤儿事件。
这三处都不影响 Turn 流程的主干——但如果你在调试"为什么恢复后行为不对"或"为什么插件看不到某个配置",答案大概率在这三个地方。
推荐阅读和验证
先读核心文件精读里的 packages/core/agent-loop/src/agent.ts、packages/core/agent-loop/src/tool-calls.ts 和 packages/core/agent-loop/src/runtime-context.ts,再读官方架构中的 Turn flow。测试从这些开始:
packages/core/agent-loop/tests/loop.spec.tspackages/core/agent-loop/tests/tool-order.spec.tspackages/core/agent-loop/tests/cancel.spec.tspackages/core/agent-loop/tests/request-reconstruction.spec.tspackages/core/agent-loop/tests/request-error.spec.tspackages/core/agent-loop/tests/resume.spec.ts
一个适合初学者的调试问题
当你发现“模型没有按预期继续”时,按这个顺序问:输入是否进入 inbox?是否被 agent/pre-step 拒绝?是否写入 user/message?LLM 是否产生了 finish?tool-call 是否通过了 Tools 的准备和审批?tool/result 是否写回 Session?下一次请求是否从日志重建?这个顺序比一上来猜某个 UI 组件更容易找到真正的断点。