跳至内容

Agent 与 Turn 流程

这一页讲用户发出一句话后,DSH 怎样把它变成模型请求、工具执行和最终结果。官方架构里的 Turn 是一轮工作,Step 是这一轮中的一次模型请求加上它发出的工具调用。

亲手看一次 Turn 展开

一次 Turn 不是一次模型调用。下面这个组件把一次带两个工具调用的 Turn 展开成 20 步,其中模型请求出现 4 次——每拿到一个工具结果都要把它带回模型才能继续。同一个组件切到「不调用工具」场景时,模型请求只有 1 次。组件下半部分是一个沙盒:消息长度、工具次数、失败位置、被拒开关和中止红线五个输入连续可调,拖到「首次领取被拒」就能亲手拼出一条零 Step 的 Turn。

互动组件Turn 流程与日志对应实验单独打开

正在载入互动组件……

不看组件也能读的说明

不打开组件也能得到结论:这一次 Turn 有 20 步、4 次模型请求、9 个日志事件。其中 6 份内容进入过模型请求,而它们全部有对应的日志事件(不可重建的有 0 份)——这就是「凡是能到达模型请求的输入都要能从 Session 日志重建」这条规则在一次 Turn 里的样子。沙盒里把「首次领取被拒」勾上,轨迹只剩一条结束事件:没有 Step 的 Turn 真实存在。组件里的每一步都在它自己的表格里逐行给出。

组件的横轴是步骤序号,不是时间:它不能说明真实 token 数、真实耗时或真实重试次数,也不能说明真实 DSH 的阶段名与这里相同。

往下滚动时,右边六段解说会逐段展开同一条轨迹:数据来自和上面组件完全相同的模型函数,只是把每一步的来龙去脉拆开讲。不想滚动就点任意一段跳转;整条轨迹在下方表格里也逐行列出。

一次最完整的流程

text
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/messageassistant/*tool/* 是写入 Session 日志的持久事件(官方架构文档的分类),而 agent/pre-stepagent/requestllm/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/requestllm/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 类型有三个分支:idlemaintenancerunning。maintenance 阶段处理恢复和清理,不接受新的 Step。如果你只看 Turn 事件,会以为 Agent 只有两种状态;打开源码看 setPhase 才能发现中间态的存在和它为什么必要。

插件提议前的默认值剥离。 requestProposal 函数在插件提出下一次请求配置之前,把 adapter 写入的 reasoningEffortmaxTokens 从 header 中删掉。这确保插件看到的是用户可配置的参数,而不是 adapter 的内部实现细节。如果你在日志里发现这两个字段"消失了",这就是原因。

请求锚点的防重入。 requestHeaderLogged 布尔标志保证 initial/resume 请求的锚点事件只写一次。没有这个标志,每次 Agent 从日志恢复都会重复写一条锚点,日志里就会出现无法配对的孤儿事件。

这三处都不影响 Turn 流程的主干——但如果你在调试"为什么恢复后行为不对"或"为什么插件看不到某个配置",答案大概率在这三个地方。

推荐阅读和验证

先读核心文件精读里的 packages/core/agent-loop/src/agent.tspackages/core/agent-loop/src/tool-calls.tspackages/core/agent-loop/src/runtime-context.ts,再读官方架构中的 Turn flow。测试从这些开始:

  • packages/core/agent-loop/tests/loop.spec.ts
  • packages/core/agent-loop/tests/tool-order.spec.ts
  • packages/core/agent-loop/tests/cancel.spec.ts
  • packages/core/agent-loop/tests/request-reconstruction.spec.ts
  • packages/core/agent-loop/tests/request-error.spec.ts
  • packages/core/agent-loop/tests/resume.spec.ts

一个适合初学者的调试问题

当你发现“模型没有按预期继续”时,按这个顺序问:输入是否进入 inbox?是否被 agent/pre-step 拒绝?是否写入 user/message?LLM 是否产生了 finish?tool-call 是否通过了 Tools 的准备和审批?tool/result 是否写回 Session?下一次请求是否从日志重建?这个顺序比一上来猜某个 UI 组件更容易找到真正的断点。