数据截至 (上游 commit 65b4508389c8)
第 1 章 · 统一 completion 抽象与 provider 层
本章讲什么: Rig 最底层的那块承重墙——它怎么用一个
CompletionModel特征,把 20+ 个 API 各不相同的供应商,包成同一个「请求进、回复出」的接口。看懂这一章,你就懂了「换供应商只改一行」背后的机制。
1.1 先建直觉:为什么需要一层抽象
每个 LLM 供应商的 HTTP API 都不一样:字段名不同、消息格式不同、工具调用的表示不同、返回的 token 统计口径也不同。
如果业务代码直接调某家 API,换供应商 = 重写。Rig 的解法是经典的「窄腰(narrow waist)」设计:
各种业务写法 一个规范中间层 各种供应商
agent.prompt ─┐ ┌─ OpenAI HTTP
extractor ─┼─► CompletionRequest ──► ┼─ Anthropic HTTP
completion 构建器─┘ (Rig 规范请求) └─ Cohere HTTP …
上面收敛到一个 CompletionRequest,下面每个 provider 只干一件事:把 CompletionRequest 翻译成自家请求体,再把自家回复翻译回 Rig 的 Message。 中间这个「规范请求」就是窄腰。
1.2 规范请求:CompletionRequest
CompletionRequest 是 Rig 里「一次模型调用」的中立表示,字段覆盖了所有供应商的公共需求(crates/rig-core/src/completion/request.rs:719,CompletionRequest):
| 字段 | 装什么 |
|---|---|
chat_history | 完整对话历史,最后一条永远是当前 prompt(现在是 Vec<Message>;「至少一条」从类型保证改成了请求边界校验 validate_message_content) |
preamble | 遗留的系统提示字段;新代码建议改用 chat_history 里领头的 Message::System |
documents | 要塞给模型的上下文文档(RAG 检索出来的片段) |
tools | 本次可用的工具定义(名字 + 描述 + JSON schema) |
temperature / max_tokens / tool_choice | 通用采样与工具约束参数 |
additional_params | 供应商专属参数 的逃生舱(任意 JSON,直接透传) |
output_schema | 结构化输出的 JSON Schema(支持原生结构化输出的 provider 会用它约束模型) |
注意 additional_params 这个逃生舱:统一抽象总有覆盖不到的供应商特性,Rig 不强行统一,而是留一个「任意 JSON 透传」的口子,避免抽象把人锁死。
关于文档还有个巧处:大多数供应商的 API 不接受「文档」这种输入类型,所以 normalized_documents 会把 documents 转成一条 Message::User 混进对话历史(crates/rig-core/src/completion/request.rs:894,normalized_documents)。抽象层在这里替 provider 抹平了差异。
1.3 核心特征:CompletionModel
所有 completion 模型都实现 CompletionModel 特征(crates/rig-core/src/completion/request.rs:659)。它的核心就两个方法:
// 示意,摘自 crates/rig-core/src/completion/request.rs:659 CompletionModel
pub trait CompletionModel: WasmCompatSend + WasmCompatSync {
// 非流式:喂进规范请求,吐出规范回复
fn completion(&self, request: CompletionRequest)
-> impl Future<Output = Result<CompletionResponse, CompletionError>>;
// 流式:吐出一个回复流
fn stream(&self, request: CompletionRequest)
-> impl Future<Output = Result<StreamingCompletionResponse, CompletionError>>;
// 供应商行为能力声明(默认保守,见下)
fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities::default() }
}
三个关键设计:
- 回复没有关联类型,原始回复序列化进
raw。 新版CompletionModel不再用type Response让每个 provider 保留自己的原始类型,而是统一返回具体的CompletionResponse;provider 的原始回复被序列化成 JSON 存进CompletionResponse::raw字段,每个 provider 接缝都会填(crates/rig-core/src/completion/request.rs:302文档注释)。你既用统一视图,也能从raw抠细节;要类型化的原始回复,就拿着具体模型调它自带的raw_completion方法。 - 返回的是
impl Future,不是async fn。 这是为了兼容 WASM(WasmCompatSend在非 WASM 下是Send,WASM 下是空约束),让核心库能编译到浏览器。 capabilities()报告供应商差异,默认保守。ProviderCapabilities里最重要的字段composes_native_output_with_tools默认false(crates/rig-core/src/completion/request.rs:626字段、:633默认值)——这是个诚实的安全默认:只有确认「原生 结构化输出能和工具调用共存」的 provider(如 OpenAI、Anthropic)才覆写成true。细节见第 5 章OutputMode。
统一回复:CompletionResponse
无论哪个 provider,completion 都返回同一个 CompletionResponse(crates/rig-core/src/completion/request.rs:302):
| 字段 | 装什么 |
|---|---|
choice | 模型这次回复的内容,Vec<AssistantContent>(可能是文本、也可能是一个或多个工具调用) |
usage | token 用量统计(见下) |
raw | 该 provider 的原始回复(序列化 JSON),需要抠细节时用 |
message_id / response_id / provider_request_id | 三层身份标识:provider 分配的助手消息 ID(如 OpenAI 的 msg_)、整响应 ID(如 chatcmpl-)、传输层请求 ID(HTTP 响应头里的 request-id),多轮配对与排障各用各的 |
finish_reason | 模型为何停笔(Stop / Length / ContentFilter / ToolCalls / Other),写路径统一过 reconcile_with_output 校正 |
token 用量:Usage
Usage 把各家口径不一的 token 统计统一成一个结构(crates/rig-core/src/completion/request.rs:536),字段覆盖输入/输出/总数/缓存读写/推理 token 等。有两个值得学的约定:
- 零值是「provider 没报用量」的哨兵。
has_values()判断是否全零,全零表示这次调用没拿到用量数据(crates/rig-core/src/completion/request.rs:573)。 - 实现了
Add/AddAssign。 多轮循环里各轮用量能直接累加(crates/rig-core/src/completion/request.rs:584/:593),这在第 2 章的用量聚合里会用到。
1.4 数据模型:Message
CompletionRequest 里流动的是 Message——provider 无关的消息模型(crates/rig-core/src/completion/message.rs:20)。它按角色分三种:
Message ──┬── System { content: String } 系统指令
├── User { content: Vec<UserContent> } 用户消息(可多模态)
└── Assistant{ id, content: Vec<AssistantContent> } 助手回复
用户内容和助手内容是两组不同的枚举(因为「用户能发的」和「模型能回的」不是一回事):
| 枚举 | 变体 | 说明 |
|---|---|---|
UserContent | Text / ToolResult / Image / Audio / Video / Document | 用户侧内容,含工具执行结果(crates/rig-core/src/completion/message.rs:108) |
AssistantContent | Text / ToolCall / Reasoning / Image | 模型侧回复:文本、工具调用、推理块、图像(crates/rig-core/src/completion/message.rs:131) |
注意 ToolResult 属于 UserContent:工具执行结果是以「用户消息」的身份塞回对话的——这符合大多数供应商 API 的约定(工具结果作为 user 角色的一种内容)。这个细节在第 2 章多轮循环里是关键。
还有 Reasoning(推理块,crates/rig-core/src/completion/message.rs:162):支持 Gemini thinking、Anthropic extended thinking、OpenAI o 系列这类会输出「思考过程」的模型。Rig 把推理当成一等公民,能在多轮里保留和回传(含加密/脱敏的推理载荷,ReasoningContent,crates/rig-core/src/completion/message.rs:145)。
模块文档也把「窄腰」的代价说破了:provider 模块负责把这些通用消息翻译成自家请求体,翻译可能有损——provider 不支持的内容类型会被丢掉或降级(crates/rig-core/src/completion/message.rs:16 模块文档)。
1.5 高层接口:Prompt / Chat / TypedPrompt
业务代码一般不直接碰 CompletionModel,而是用三个更友好的特征。注意它们已经跟着 agent 运行时搬进了 rig-agent crate(都在 crates/rig-agent/src/completion.rs):
| 特征 | 方法 | 语义 | 位置 |
|---|---|---|---|
Prompt | prompt(msg) | 一句 prompt 进、一个 String 出,「经运行时编排后的最终文本」——若模型要调工具,编排就包括执行工具再来一轮 | crates/rig-agent/src/completion.rs:140 |
Chat | chat(msg, &mut history) | 带历史的 prompt;本轮已提交的消息才追加进 history | crates/rig-agent/src/completion.rs:149 |
TypedPrompt | prompt_typed::<T>(msg) | 结构化输出:自动生成 T 的 JSON schema,把回复反序列化成 T | crates/rig-agent/src/completion.rs:159 |
Prompt::prompt 的返回类型说明了一切:Agent::prompt 不直接返回 future,而是返回一个 PromptRequest 构建器(crates/rig-agent/src/agent/completion.rs:735),.await 它才真正跑完整个多轮循环。这句「自动」背后,就是第 2 章的状态机。
低层入口则留在了 rig-core:CompletionModel::completion_request(prompt) 返回一个请求构建器(crates/rig-core/src/completion/request.rs:674),让你在发送前微调规范请求——不再需要一个单独的 Completion 特征。
1.6 provider 怎么实现
每个 provider 是 crates/rig-core/src/providers/ 下的一个模块(26 个)。模块内定义一个 Client 类型和各能力的 model 类型,然后按需实现能力特征(crates/rig-core/src/providers/mod.rs 顶部有 provider 实现清单)。
关键原则:能力特征只在 provider 真支持时才实现。 比如某 provider 不支持 embedding,就不实现 EmbeddingsClient——编译期就挡住误用(crates/rig-core/src/providers/mod.rs:31 模块文档)。这些能力特征包括:
| 能力特征 | 提供什么 | 位置 |
|---|---|---|
ProviderClient | 从环境变量/API key 构造客户端 | crates/rig-core/src/client/mod.rs:111 |
CompletionClient | .completion_model(model) 工厂方法 | crates/rig-core/src/client/completion.rs:7 |
EmbeddingsClient | .embedding_model(model) | crates/rig-core/src/client/embeddings.rs:6 |
你在示例里调的那个 .agent(model) 不在 rig-core 里——agent 运行时拆去了 rig-agent crate,.agent() 由扩展特征 AgentClientExt 提供,并对所有实现了 CompletionClient 的类型自动生效(blanket impl,crates/rig-agent/src/client.rs:48):
// 示意,摘自 crates/rig-agent/src/client.rs:26 AgentClientExt::agent
fn agent(&self, model: impl Into<String>) -> AgentBuilder {
AgentBuilder::new(self.completion_model(model))
}
一行:拿 model 名字造一个 completion model,再包成 AgentBuilder。这就是「换供应商只改 Client」的落点——AgentBuilder 及以上全部代码对 provider 一无所知。同一处还有 .extractor::<T>(model) 工厂(crates/rig-agent/src/client.rs:34,见第 4 章)。
顺带说清这次的 crate 拆分:rig-core 持有可移植的契约(provider / 消息 / 工具 / 存储),rig-agent 持有经典运行时(builder、AgentRun 状态机、hooks、extractor、共享驱动循环),rig-agent 通过显式的 core 命名空间回指 rig-core 的全部导出(crates/rig-agent/src/lib.rs:12 模块文档)。
1.7 本章小结与去向
- Rig 用
CompletionRequest(规范请求)+Message(中立消息模型)当窄腰,provider 只做双向翻译。 CompletionModel特征返回具体的CompletionResponse;provider 原始回复序列化进raw,差异行为靠capabilities()声明。- 业务用
Prompt/Chat/TypedPrompt三个高层特征(在 rig-agent);Prompt::prompt遇到工具调用会「自动循环」。 - 那个「自动循环」是怎么实现的、为什么能持久化 → 见第 2 章。
- 工具本身怎么定义、乱调怎么办 → 见第 3 章。
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 规范请求 | crates/rig-core/src/completion/request.rs | CompletionRequest |
| 文档归一化 | crates/rig-core/src/completion/request.rs | normalized_documents |
| 核心模型特征 | crates/rig-core/src/completion/request.rs | CompletionModel |
| 供应商能力声明 | crates/rig-core/src/completion/request.rs | ProviderCapabilities |
| 统一回复 | crates/rig-core/src/completion/request.rs | CompletionResponse |
| token 用量 | crates/rig-core/src/completion/request.rs | Usage |
| 消息模型 | crates/rig-core/src/completion/message.rs | Message / UserContent / AssistantContent |
| 高层特征 | crates/rig-agent/src/completion.rs | Prompt / Chat / TypedPrompt |
| provider 客户端特征 | crates/rig-core/src/client/mod.rs | ProviderClient / CompletionClient |
| agent 工厂 | crates/rig-agent/src/client.rs | AgentClientExt::agent |