数据截至 (上游 commit 65b4508389c8)
第 5 章 · 进阶机制、边界与总代码地图
本章讲什么: 前四章讲了主干(抽象 / agent 循环 / 工具 / RAG)。这一章讲把 Rig 用到生产的那些机制——hooks、流式、记忆、结构化输出的取舍——再诚实地划出边界、和兄弟项目对比,最后给一张能直接跳源码的总地图。
5.1 hooks:分事件的生命周期拦截
hooks 让你在 agent 循环的每个关键点插一脚——记日志、改请求、拦工具、重试一轮、提前终止。本版的 AgentHook 特征(crates/rig-agent/src/agent/hook.rs:1206)做了一次大改:不再是一个万能的 on_event 方法,而是每个事件一个生命周期方法、每种方法配自己的 action 类型(模块文档 crates/rig-agent/src/agent/hook.rs:3 明说这是为了把「不支持的事件/action 组合」从运行期解释变成编译期拒绝):
// 示意,摘自 crates/rig-agent/src/agent/hook.rs:1206 AgentHook(节选)
pub trait AgentHook: WasmCompatSend + WasmCompatSync {
fn on_completion_call(&self, ctx: &HookContext, event: CompletionCall<'_>)
-> impl Future<Output = CompletionCallAction>; // 每事件一个 action 类型
fn on_tool_call(&self, ctx: &HookContext, event: ToolCall<'_>)
-> impl Future<Output = ToolCallAction>;
// …其余方法同构,见下表
}
十一个事件方法,各自能干什么:
| 方法 | 何时跑 | 返回的 action 能做什么 |
|---|---|---|
on_model_select | 每次调模型前选模型 | 换模型 / 保持 / 停 |
on_completion_call | 即将发 completion 请求 | 贴一个本轮请求补丁 / 放行 / 停 |
on_completion_response | 模型回复到手 | 观察(放行/停) |
on_model_turn_finished | 一轮模型输出敲定时 | 接受 / 重试(仅无工具轮,吃总预算) |
on_invalid_tool_call | 检测到非法工具调用(第 2/3 章的四档恢复) | 失败/重试/修复/跳过/停,None 交给后面的 hook |
on_tool_call | 工具执行前 | 改参数 / 跳过 / 执行 / 停 |
on_tool_result | 工具出结果后、呈现给模型前 | 改呈现(presentation)/ 保留 / 停 |
on_text_delta / on_reasoning_delta / on_tool_call_delta | 流式文本/推理/工具参数增量 | 观察(流式特有) |
on_stream_response_finish | 流式回复结束 | 观察 |
两个跨方法的公共约定。第一,注册顺序 + 按事件短路:多个 hook 组成 HookStack(crates/rig-agent/src/agent/hook.rs:1490)按注册顺序跑,补丁累加合并、参数改写链式传给后面的 hook;某个 hook 返回重试或停止就短路该事件的其余 hook(模块文档,crates/rig-agent/src/agent/hook.rs:10)。第二,observes(kind) 这个性能提示方法(crates/rig-agent/src/agent/hook.rs:810 的 StepEventKind 只剩观察兴趣枚举的用途)让高频 delta 事件可以按需跳过。
值得学的一点:on_completion_call 能返回 RequestPatch——一个只作用于当前轮、非粘性的请求补丁(crates/rig-agent/src/agent/hook.rs:823 文档,结构体在 :838):Some 字段覆盖这一轮,None 继承 agent 基线,下一轮又从基线重新开始;多个 hook 的补丁按注册顺序合并(上下文追加、JSON 对象浅合并、工具白名单求交、标量后写者胜)。这让「临时改一轮的 temperature/preamble」不会污染 agent 配置。
5.2 流式与多轮流式
第 2 章已点破核心:流式和 blocking 共用同一台状态机 AgentRun 和同一个 drive_agent 循环,只是 TurnSource 实现不同。这里补流式独有的东西。
流式入口是 StreamingPromptRequest(crates/rig-agent/src/agent/prompt_request/streaming.rs:257),产出一个 MultiTurnStreamItem 的流(crates/rig-agent/src/agent/prompt_request/streaming.rs:52)。流里不只有文本块,还有:模型产出的完整工具调用项、工具执行确认项、工具结果项、每次 completion 的用量项、hook 拒绝重试通知项、最终回复项。所以「多轮流式」意味着你能实时看到 agent 每一轮在调什么工具、拿到什么结果,而不只是最终文本。
流式产出(MultiTurnStreamItem 序列):
文本块... 文本块... StreamAssistantItem(ToolCall: get_weather)
ToolExecutionCommitted StreamUserItem(ToolResult: 晴25℃)
文本块... 文本块... CompletionCall(用量) FinalResponse(最终回复)
值得单说的两个新成员:ToolExecutionCommitted 不是实时开始通知——它和工具结果一起、在整个批次成功落定后才吐出;ModelTurnRetried 告诉你刚才那轮的增量被 hook 拒了,消费端应当把该轮已渲染的输出作废。最终项 FinalResponse 装的是与 blocking 界面同一个 PromptResponse(crates/rig-agent/src/agent/prompt_request/streaming.rs:126)。
为什么值得强调「共用状态机」:很多框架流式和非流式两套代码,行为会漂移(比如流式下工具顺序不对、用量算错)。Rig 用共享循环从根上保证一致,仓库里成对的 blocking_hook / streaming_hook 断言测试就是在守这条线(crates/rig-agent/src/agent/runner.rs:2727 起等处)。
5.3 对话记忆:ConversationMemory
记忆让 agent 跨请求记住对话,按 conversation_id 透明存取(ConversationMemory 特征留在可移植契约里,crates/rig-core/src/memory.rs:92)。接法:
- 用
AgentBuilder::memory(backend)挂一个后端(crates/rig-agent/src/agent/builder.rs:265)。 - 每次请求用
PromptRequest::conversation(id)指定是哪段对话(crates/rig-agent/src/agent/prompt_request/mod.rs:159)。
机制上,记忆在共享驱动循环的 Done 分支写入(append_run_messages