跳到主要内容

数据截至 (上游 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 组成 HookStackcrates/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:810StepEventKind 只剩观察兴趣枚举的用途)让高频 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 实现不同。这里补流式独有的东西。

流式入口是 StreamingPromptRequestcrates/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 界面同一个 PromptResponsecrates/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,定义在 crates/rig-agent/src/agent/runner.rs:560Done 分支的调用点 crates/rig-agent/src/agent/prompt_request/streaming.rs:630):一次 run 结束,把本轮新产生的消息追加进对应 conversation。加载则在请求准备阶段。也能用 without_memory() 单次跳过(crates/rig-agent/src/agent/prompt_request/mod.rs:167)。

注意第 2 章那个「可序列化 run 状态」的警告同样适用记忆:序列化的 run 内嵌完整对话和每轮已完成调用的 provider 原始响应,持久化它就继承了这些内容的敏感度,且随每轮响应增长(crates/rig-agent/src/agent/run/mod.rs:23 模块文档;驱动器不想留原始报文,可在持久化前自行清掉 raw)。


5.4 结构化输出的真正难点:OutputMode 与 #1928

第 4 章说 Extractor 用「合成工具」拿结构化输出。但这里有个真实的坑,Rig 专门处理了(issue #1928):

原生结构化输出和工具调用可能互相打架。 有些 provider 一旦开了「原生结构化输出」约束,模型就只吐符合 schema 的 JSON,不再调工具了

所以「让模型返回结构化数据」有三条路,Rig 用 OutputMode 表示(crates/rig-agent/src/agent/run/output_mode.rs:28):

模式怎么拿结构化输出保证类比 pydantic-ai
Native用 provider 原生结构化输出约束强约束(provider 保证)NativeOutput
Tool用一个合成的「输出工具」,模型调它=提交结果尽力而为ToolOutput
Prompted纯靠 prompt 里请求 JSON尽力而为PromptedOutput
Auto按 provider 智能路由(见下)视情况——

Auto 是默认,也是最见功力的一档(crates/rig-agent/src/agent/run/output_mode.rs:28 变体文档):

  • agent 有工具 + 有 schema 时,只在「原生约束会压制工具调用」的 provider 上退到 Tool;
  • 在「原生约束能和工具共存」的 provider(OpenAI、Anthropic)上,保持 Native 的强保证。

这个「能不能共存」的判断,正是第 1 章那个 ProviderCapabilities::composes_native_output_with_toolscrates/rig-core/src/completion/request.rs:626 字段,:633 默认 false,支持的 provider 覆写为 true)。三章在这里闭环。

Tool 模式还有精细的收尾逻辑(crates/rig-agent/src/agent/run/mod.rs:795 起):模型调了输出工具就用其参数当最终答案;缺必填字段时会在预算内重新提示模型补齐(missing_required_output_fieldsreprompt_for_outputcrates/rig-agent/src/agent/run/mod.rs:795,预算闸门 can_reprompt_for_output:517);把最终轮存成助手文本而非原始工具调用,免得历史里留一个没被应答的 tool_use 让下一轮请求被 provider 拒(crates/rig-agent/src/agent/run/mod.rs:813 的 Finalize 注释)。这些都是踩过的坑。


5.5 遥测:GenAI 语义约定

Rig 内建 OpenTelemetry 遥测(约定与基建在 crates/rig-core/src/telemetry/,agent 循环里的 span 编排在 crates/rig-agent/src/agent/runner.rs),且对齐 OpenTelemetry 的 GenAI 语义约定(标准化的 gen_ai.* span 字段)。agent 循环里每次调模型开一个 chat span、每次跑工具开一个 execute_tool span(new_execute_tool_span,crates/rig-agent/src/agent/runner.rs:790)。

blocking 界面还把这些 span 串成线性因果链(chat → tool → chat),用 follows_from 关联(UnaryTurnSource 的文档与实现,crates/rig-agent/src/agent/runner.rs:807)。意义:你在 APM 里能看到一次 agent 运行的完整轨迹,且字段名是行业标准,能直接喂进现成的可观测性工具。


5.6 边界与局限(诚实)

边界说明依据
API 频繁破坏性变更README 明确警告未来更新含 breaking changes,版本 0.42,尚未 1.0README.md:46 顶部 warning;Cargo.toml:84
run 序列化无跨版本稳定保证序列化的 run 状态只能用「挂起它的同一个 rig 版本」恢复crates/rig-agent/src/agent/run/mod.rs:26
抽象是「最小公倍数 + 逃生舱」供应商独有特性靠 additional_params 透传,不进统一抽象;翻译可能有损crates/rig-core/src/completion/request.rs:746;crates/rig-core/src/completion/message.rs:16 注释「转换可能有损」
结构化输出多为尽力而为只有 Native 是强约束,Tool/Prompted 不保证,靠重试兜底crates/rig-agent/src/agent/run/output_mode.rs:19
WASM 只覆盖主干 crate浏览器 WASM 支持限于可移植核心 + 经典运行时;WASI 不支持,rmcp 仅原生,伴生 crate 不保证README.md:73;crates/rig-agent/src/lib.rs:24
能力按 provider 存在与否某 provider 不支持 embedding/流式就不实现对应特征,编译期挡住crates/rig-core/src/providers/mod.rs

5.7 横向对比(同 shelf agent 框架)

Rig 在 agent 框架货架里的取舍,概括成几条:

维度Rig 的选择对比
语言/定位Rust,强类型、可编译 WASM、面向生产性能多数 agent 框架是 Python(LangChain 等),Rig 走静态类型 + 零成本抽象路线
agent 循环sans-IO 可序列化状态机,决策/执行分离多数框架把循环和 IO 耦在一起;Rig 的循环能持久化、能换进程恢复,较少见
流式一致性blocking/streaming 共用一台状态机不少框架两套实现、行为易漂移
结构化输出三模式 + Auto 按 provider 路由,显式处理「原生约束压制工具」借鉴 pydantic-ai 的 Output 分类,并解决工具共存问题(#1928)
抽象哲学窄腰统一 + additional_params 逃生舱在「统一」和「不锁死供应商特性」之间取平衡

设计上明显能看到对 pydantic-ai 的借鉴(OutputMode 三态、输出重试预算的注释都直接点名),但把它落到 Rust 的类型系统和 sans-IO 架构上。


5.8 总代码地图(全库导航)

按「你想干嘛」查该打开哪个文件:

你想…打开关键符号
换供应商 / 看统一请求crates/rig-core/src/completion/request.rsCompletionModel / CompletionRequest
理解消息模型crates/rig-core/src/completion/message.rsMessage / UserContent / AssistantContent
读懂多轮循环(精华)crates/rig-agent/src/agent/run/mod.rsAgentRun / AgentRunStep / next_step
看驱动器/共享循环crates/rig-agent/src/agent/prompt_request/streaming.rsdrive_agent / drive_tool_calls
配置 agentcrates/rig-agent/src/agent/builder.rsAgentBuilder
延迟执行的请求crates/rig-agent/src/agent/prompt_request/mod.rsPromptRequest
写工具crates/rig-agent/src/tool/mod.rsTool / DynamicTool / ToolSet
少写工具样板crates/rig-derive/src/lib.rsrig_tool
做 RAGcrates/rig-core/src/vector_store/mod.rs / embeddings/VectorStoreIndex / EmbeddingsBuilder / Embed
结构化输出crates/rig-agent/src/extractor.rs / agent/run/output_mode.rsExtractor / OutputMode
插生命周期钩子crates/rig-agent/src/agent/hook.rsAgentHook / HookStack / RequestPatch
加对话记忆crates/rig-core/src/memory.rsConversationMemory
接遥测crates/rig-core/src/telemetry/ + crates/rig-agent/src/agent/runner.rsexecute_tool span / UnaryTurnSource
找具体后端实现crates/rig-*/(伴生 crate + 门面 feature)各自实现主干 crate 特征

5.9 一句话收束

Rig 的精华不是「支持多少供应商」,而是两条设计线:

  1. 窄腰抽象——CompletionModel/Tool/VectorStoreIndex 三堵承重墙 + additional_params 逃生舱,让你依赖抽象而不被锁死。
  2. sans-IO 状态机——把 agent 多轮循环的「决策」和「IO」彻底分开,换来可测、可序列化、可换进程恢复、blocking/streaming 天然一致。

看懂这两条,你带走的就不只是「怎么用 Rig」,而是「一个好的 LLM 框架该怎么设计」。