跳到主要内容

第 5 章 · 进阶机制、边界与总代码地图

本章讲什么: 前四章讲了主干(抽象 / agent 循环 / 工具 / RAG)。这一章讲把 Rig 用到生产的那些机制——hooks、流式、记忆、结构化输出的取舍——再诚实地划出边界、和兄弟项目对比,最后给一张能直接跳源码的总地图。


5.1 hooks:事件驱动的生命周期拦截

hooks 让你在 agent 循环的每个关键点插一脚——记日志、改请求、拦工具、提前终止。设计是事件驱动AgentHook 特征只有一个方法 on_eventcrates/rig-core/src/agent/hook.rs:626):

// 示意,摘自 crates/rig-core/src/agent/hook.rs:626 AgentHook
pub trait AgentHook<M>: WasmCompatSend + WasmCompatSync {
fn on_event(&self, event: StepEvent<'_, M>) -> impl Future<Output = Flow>;
}

循环在每个节点发一个 StepEvent,hook 返回一个 Flow 决定继续还是干预。八种事件(StepEventKindcrates/rig-core/src/agent/hook.rs:256):

事件何时发
CompletionCall即将调模型(可返回 OverrideRequest 改本轮请求)
CompletionResponse模型回复到手
InvalidToolCall检测到非法工具调用(对应第 2/3 章的四档恢复)
ToolCall / ToolResult工具执行前/后
TextDelta / ToolCallDelta流式文本块/工具调用块
StreamResponseFinish流式回复结束

Flowcrates/rig-core/src/agent/hook.rs:404)是 hook 的返回语义:Continue 放行,或返回干预(覆盖请求、终止运行等)。多个 hook 按注册顺序跑,第一个返回非 Continue 的短路其余crates/rig-core/src/agent/runner.rs:324 文档)。

值得学的一点:CompletionCall 事件能返回 RequestOverride——一个逐轮、非粘性的请求补丁(crates/rig-core/src/agent/hook.rs RequestOverride 文档):某字段 Some 就覆盖这一轮,None 就继承 agent 基线;下一轮又从基线重新解析。这让「临时改一轮的 temperature/preamble」不会污染 agent 配置。


5.2 流式与多轮流式

第 2 章已点破核心:流式和 blocking 共用同一台状态机 AgentRun 和同一个 drive_agent 循环,只是 TurnSource 实现不同。这里补流式独有的东西。

流式入口是 StreamingPromptRequestcrates/rig-core/src/agent/prompt_request/streaming.rs:288),产出一个 MultiTurnStreamItem 的流(crates/rig-core/src/agent/prompt_request/streaming.rs:45)。流里不只有文本块,还有:工具调用项、工具结果项、每次 completion 的用量项、最终回复项。所以「多轮流式」意味着你能实时看到 agent 每一轮在调什么工具、拿到什么结果,而不只是最终文本。

流式产出(MultiTurnStreamItem 序列):
文本块... 文本块... ToolCall(get_weather) ToolResult(晴25℃)
文本块... 文本块... CompletionCall(用量) 最终回复

为什么值得强调「共用状态机」:很多框架流式和非流式两套代码,行为会漂移(比如流式下工具顺序不对、用量算错)。Rig 用共享循环从根上保证一致,仓库里成对的 blocking_hook / streaming_hook 断言测试就是在守这条线(crates/rig-core/src/agent/runner.rs:1093:2518 等多处)。


5.3 对话记忆:ConversationMemory

记忆让 agent 跨请求记住对话,按 conversation_id 透明存取(ConversationMemory 特征,crates/rig-core/src/memory.rs)。接法:

  • AgentBuilder::memory(backend) 挂一个后端。
  • 每次请求用 PromptRequest::conversation(id) 指定是哪段对话(crates/rig-core/src/agent/prompt_request/mod.rs:57)。

机制上,记忆在 drive_agentDone 分支写入(append_run_messagescrates/rig-core/src/agent/prompt_request/streaming.rs:642):一次 run 结束,把本轮新产生的消息追加进对应 conversation。加载则在请求准备阶段。也能用 without_memory() 单次跳过(crates/rig-core/src/agent/prompt_request/mod.rs:65)。

注意第 2 章那个「可序列化 run 状态」的警告同样适用记忆:序列化的 run 内嵌完整对话,持久化它就继承了对话内容的敏感度(crates/rig-core/src/agent/run/mod.rs:19 模块文档)。


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

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

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

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

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

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

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

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

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


5.5 遥测:GenAI 语义约定

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

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


5.6 边界与局限(诚实)

边界说明依据
API 频繁破坏性变更README 明确警告未来更新含 breaking changes,版本 0.39,尚未 1.0README.md 顶部 warning
run 序列化无跨版本稳定保证序列化的 run 状态只能用「挂起它的同一个 rig 版本」恢复crates/rig-core/src/agent/run/mod.rs:22
抽象是「最小公倍数 + 逃生舱」供应商独有特性靠 additional_params 透传,不进统一抽象;翻译可能有损crates/rig-core/src/completion/request.rs:690;message.rs:18 注释「转换可能有损」
结构化输出多为尽力而为只有 Native 是强约束,Tool/Prompted 不保证,靠重试兜底crates/rig-core/src/agent/run/output_mode.rs:16
WASM 只覆盖核心库全 WASM 兼容仅限 core library,伴生向量库 crate 不保证README.md features
能力按 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-core/src/agent/run/mod.rsAgentRun / AgentRunStep / next_step
看驱动器/共享循环crates/rig-core/src/agent/prompt_request/streaming.rsdrive_agent / drive_tool_calls
配置 agentcrates/rig-core/src/agent/builder.rsAgentBuilder
延迟执行的请求crates/rig-core/src/agent/prompt_request/mod.rsPromptRequest
写工具crates/rig-core/src/tool/mod.rsTool / ToolDyn / ToolSet
少写工具样板crates/rig-derive/src/lib.rsrig_tool
做 RAGcrates/rig-core/src/vector_store/mod.rs / embeddings/VectorStoreIndex / EmbeddingsBuilder / Embed
结构化输出crates/rig-core/src/extractor.rs / agent/run/output_mode.rsExtractor / OutputMode
插生命周期钩子crates/rig-core/src/agent/hook.rsAgentHook / StepEvent / Flow
加对话记忆crates/rig-core/src/memory.rsConversationMemory
接遥测crates/rig-core/src/telemetry/execute_tool span
找具体后端实现crates/rig-*/(约 18 个 crate)各自实现核心特征

5.9 一句话收束

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

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

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