状态机
智能体通常从一个循环开始:调用模型,运行它请求的工具,然后重复。当应用还需要审批、重试、压缩、外部事件,或者停止后再恢复的能力时,这种方式就不够了。这些需求需要在执行顺序中有自己的位置,而不是在提示词里多写几条指令。
GDK 把执行表示为一个有序的步骤列表。你用操作和推理步骤组合出一个 StateMachine。要改变智能体,就改变这个组合或编写新的操作,而不是扩展状态机本身。
执行循环
每一轮处理中,状态机会:
- 通过你的
SessionLoader加载会话。 - 按顺序尝试各个步骤,直到有一个适用。
- 把该步骤的效果交给你的
EffectHandler。 - 带着更新后的会话重新开始,或把控制权交还给调用方。
一个步骤可以拒绝、应用效果后继续,或应用效果后让出。如果没有步骤适用,状态机就会让出。步骤不是程序计数器:下一轮从列表顶部开始,而不是从后一个步骤开始。
一个简单的组合如下所示。tools 和 inference 是由你的应用配置的实例,使用相同的会话和效果类型:
let machine = StateMachine::new(
vec![
Step::Operation(Arc::new(tools)),
Step::Inference(Arc::new(inference)),
],
cancel,
);
用户发来消息后,工具操作无事可做,于是运行推理。如果模型请求某个工具,这个请求就会成为对话的一部分。下一轮中,工具操作执行它,并把结果作为效果返回。再下一轮到达推理时,上下文中已经包含这个结果。当模型完成且没有更多工作时,推理步骤拒绝,状态机把控制权交还给应用。
推理之前,状态机还会向各个操作询问它们的工具定义和提示词贡献。因此,即使某个组件的 run 方法无事可做,它也能影响模型请求。
状态属于应用
MachineSession 公开会话 ID 和对话。你的会话类型还可以保存应用数据,例如工作目录、审批状态或业务记录。效果描述要应用的更改:追加一条消息、替换压缩后的历史,或更新应用特有的状态。
状态机不规定使用哪种数据库。你针对文件、数据库或内存存储来实现加载和应用效果。Emitter 事件是另一回事:它们让客户端在工作进行时显示输出,但显示事件并不会持久化其效果。
由于决策是根据已加载的状态做出的,全新的状态机可以接着已保存的对话继续,而不必记住自己当时处于循环的哪个分支。操作必须记录对后续运行有影响的决策,而不是只把它们保存在局部变量里。Hello World 的会话存储展示了这条边界的文件存储版本。
让出与恢复
让出意味着把控制权交还给调用方。它可能表示智能体已经完成、需要人工审批,或必须等待外部事件。它不会启动后台工作线程,也不会自动安排之后的运行。
你的应用持久化新的输入或决定,构造合适的操作,然后再次调用状态机。例如,审批操作可以在工具请求等待决定时让出,待决定保存之后再允许执行。它必须出现在任何会执行未获批请求或越过它继续推理的步骤之前。
仅靠持久化并不能让外部动作做到恰好执行一次。如果进程在付款或发送消息之后、保存结果之前停止,应用仍然需要有办法对账。工作流需要时,请保存标识符并使用幂等的外部 API。
运行、单步和停止
用 run 让状态机加载、执行步骤并应用,直到让出或被取消:
let session = machine.run(&runtime, &session_id, &emitter).await?;
需要更精细控制的应用可以自己驱动 step 和 apply。返回的 StepResult 会标明应用了哪个步骤,以及运行是继续、已让出还是已取消。自己调用这些方法的调用方,在离开运行时还必须调用 finalize。
取消会丢弃正在运行的步骤的 future,然后让各个操作有机会保留已收到的输出,并收尾被中断的工具响应。run 会在返回前处理这部分收尾。这无法撤销外部副作用,也不能保证在异步任务之外启动的工作已经停止。
接下来,请阅读操作,了解如何把策略组合进这个循环。完整 API 见 goose-agent::machine。