Turn 与 Step 执行周期
前四章讲的是静态结构,插件、事件、日志。从本章起视角切换到动态,看一次用户输入如何变成一次回复。dsh 给执行周期定了两个单位,Step 和 Turn,定义只有两句话。一个 Step 是一次模型请求加上它引发的全部工具调用。一个 Turn 是零个或多个 Step,在第一份输入被认领之前开启,在不再欠任何回复时关闭。
从定义能看出这两个单位的用意。模型的推理天然是循环的。模型收到请求,可能直接回答,也可能说我要先调用某个工具。工具的结果要再发给模型,模型可能又要调下一个工具。这个循环转几圈事先不知道,但每一圈的结构是相同的,一次请求加一批工具调用。Step 把一圈打包,Turn 把整个任务打包。会话日志里的 turn/* 和 step/* 事件,就是按这两个单位开合的。
完整走一遍流程。用户消息到达,先进入一个收件箱。收件箱是 dsh 驱动层的入口,所有输入,无论来自聊天界面、命令行还是程序调用,都在这里排队。有些消息会立刻唤醒驱动,有些注入的上下文材料则安静地等在收件箱里,直到下一条消息把它们一起带走。
驱动被唤醒,一个 Turn 开启,记下 turn/start。它从收件箱认领下一步的输入,同时装配两样东西,提示词的各个分节,和当前可用的工具清单。这两样同样由插件注册,第 8 章会讲不同 Agent 如何拿到不同的工具集。
接下来是一个关键的中转站,agent/pre-step 事件。它决定模型看到什么。监听者可以改写认领到的消息,也可以直接拒绝。一个值得注意的细节是,即使第一步就被拒绝,这个 Turn 也会以开启过、未消费任何 Step 的形式记入日志。设计者显然权衡过,一次拒绝也是历史的一部分,审计时应当可见。
放行之后,一个 Step 开始,记下 step/start。进入的消息以 user/message 事件落入日志,模型历史由日志现算。请求经过 agent/request 与 llm/stream 两条 waterfall 链,发往模型服务,流式分片与最终消息依次入日志。如果模型决定调用工具,执行经过工具管道(第 7 章细讲),工具结果以 tool/result 入日志,Step 关闭。
Step 结束时驱动检查两件事。工具的结果是否还需要模型再解释一遍,收件箱里是否又来了新输入。两者任一成立,就再开一个 Step。都不成立,Turn 就到了收口的时候。收口前还有最后一个事件 agent/turn-stopping,监听者以 serial 模式投票,任何一票说不,回合就继续。全票通过,turn/end 记入日志,这次任务结束。
把这套流程和第 3、4 章的内容对上,能看到一个清晰的分层。Turn 和 Step 的开合是耐久事件,进日志。请求、流式、工具执行的中转是运行时事件,挂 waterfall,策略在流动中生效。日志负责事实,事件链负责策略,两层互不越界。改动循环本身的需求在 dsh 里极少出现,因为绝大多数合理的干预点都已经留在了外围。
这套周期定义也解释了 dsh 对取消和错误的态度。会话中途用户按了停止,当前 Step 的清理、Turn 的收口都要走完整流程,事件不缺席。模型服务超时或报错,错误同样作为事实进入处理路径,回合要么重试要么体面结束,内存里不留半途状态。对一份以日志为唯一事实的系统来说,任何绕过日志的状态都是隐患。
收件箱的细节再多说一句,它区分两类输入。用户消息有资格唤醒驱动,被注入的上下文材料则安静排队。这个区分解决一个真实问题,后台收集到的材料不该单独吵醒模型,等用户真的说话时捎带上就行。对构建长驻 Agent 的人来说,什么时候值得花一次模型调用,是一个成本与体验的权衡,dsh 把答案固化在了收件箱的语义里。
取消与出错的处理同样嵌在周期里。每个 Agent 对外有一个操作句柄,调用方可以通过它打断当前回合。打断发生时,正在飞行中的模型流与工具执行要停干净,已发生的部分照实入日志,回合按流程收口。模型服务报错时同理,错误进入事件流,回合要么换一条路重试,要么带着失败原因结束。任何时候内存里都不存在一份日志之外的进行中状态,这是周期设计与日志纪律互相咬合的地方。
执行周期讲完了,但还有一半动态没讲。模型调用工具时,那次调用经历了什么。下一章先岔开一步,讲 dsh 如何定义一种能力,第 7 章再回到工具执行的三道关口。