跳到主要内容

数据截至 (上游 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:719CompletionRequest):

字段装什么
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:894normalized_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 默认 falsecrates/rig-core/src/completion/request.rs:626 字段、:633 默认值)——这是个诚实的安全默认:只有确认「原生结构化输出能和工具调用共存」的 provider(如 OpenAI、Anthropic)才覆写成 true。细节见第 5 章 OutputMode

统一回复:CompletionResponse

无论哪个 provider,completion 都返回同一个 CompletionResponsecrates/rig-core/src/completion/request.rs:302):

字段装什么
choice模型这次回复的内容,Vec<AssistantContent>(可能是文本、也可能是一个或多个工具调用)
usagetoken 用量统计(见下)
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> } 助手回复

用户内容和助手内容是两组不同的枚举(因为「用户能发的」和「模型能回的」不是一回事):

枚举变体说明
UserContentText / ToolResult / Image / Audio / Video / Document用户侧内容,含工具执行结果(crates/rig-core/src/completion/message.rs:108
AssistantContentText / 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 把推理当成一等公民,能在多轮里保留和回传(含加密/脱敏的推理载荷,ReasoningContentcrates/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):

特征方法语义位置
Promptprompt(msg)一句 prompt 进、一个 String 出,「经运行时编排后的最终文本」——若模型要调工具,编排就包括执行工具再来一轮crates/rig-agent/src/completion.rs:140
Chatchat(msg, &mut history)带历史的 prompt;本轮已提交的消息才追加进 historycrates/rig-agent/src/completion.rs:149
TypedPromptprompt_typed::<T>(msg)结构化输出:自动生成 T 的 JSON schema,把回复反序列化成 Tcrates/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.rsCompletionRequest
文档归一化crates/rig-core/src/completion/request.rsnormalized_documents
核心模型特征crates/rig-core/src/completion/request.rsCompletionModel
供应商能力声明crates/rig-core/src/completion/request.rsProviderCapabilities
统一回复crates/rig-core/src/completion/request.rsCompletionResponse
token 用量crates/rig-core/src/completion/request.rsUsage
消息模型crates/rig-core/src/completion/message.rsMessage / UserContent / AssistantContent
高层特征crates/rig-agent/src/completion.rsPrompt / Chat / TypedPrompt
provider 客户端特征crates/rig-core/src/client/mod.rsProviderClient / CompletionClient
agent 工厂crates/rig-agent/src/client.rsAgentClientExt::agent