跳到主要内容

第 4 章 · RAG、Embedding 与结构化抽取

本章讲什么: 模型的知识停在训练截止日,也不知道你的私有数据。RAG(检索增强生成)就是「先从知识库里查出相关片段,再连问题一起喂给模型」。本章讲 Rig 怎么把「向量化 → 存 → 检索 → 喂给 agent」这条链统一起来,外加让模型返回结构化数据的 Extractor


4.1 先建直觉:RAG 一条链

准备阶段(离线):
你的文档 ──► Embedding 模型向量化 ──► 存进向量库

查询阶段(在线):
用户问题 ──► 向量化 ──► 向量库里找最相近的 N 条 ──► 连问题一起喂给 agent ──► 回答

Rig 把这条链上的每一环都做成统一特征:向量化是 EmbeddingModel,「哪些数据要向量化」是 Embed,检索是 VectorStoreIndex。换向量库只换最后一个的实现。


4.2 标注要向量化什么:Embed 特征

一个结构体里往往只有部分字段需要参与语义检索(比如一篇文章,只有正文该向量化,ID 和时间戳不该)。Embed 特征让类型自己声明「哪些内容进向量」(crates/rig-core/src/embeddings/embed.rs:65):

// 示意,摘自 crates/rig-core/src/embeddings/embed.rs:43 文档示例
fn embed(&self, embedder: &mut TextEmbedder) -> Result<(), EmbedError> {
embedder.embed(self.content.clone()); // 只把 content 送去向量化
Ok(())
}

TextEmbedder 是个收集器(crates/rig-core/src/embeddings/embed.rs:79):你往里 embed(text) 推若干段文本,它收集起来。一个对象可以推多段——支持「一个文档从多个语义方向被检索」。常见基础类型(String 等)都有现成实现,复杂类型可以用 #[derive(Embed)]rig-derive crate 的 embed.rs)自动生成。


4.3 批量向量化:EmbeddingsBuilder 与 EmbeddingModel

EmbeddingModel 是向量化模型的统一特征(crates/rig-core/src/embeddings/embedding.rs:61),有个关键关联常量 MAX_DOCUMENTScrates/rig-core/src/embeddings/embedding.rs:63)——每个供应商单次 API 能塞的文档数上限不同。

EmbeddingsBuilder 负责批量向量化(crates/rig-core/src/embeddings/builder.rs)。它按 MAX_DOCUMENTS 自动分批调 API,你不用操心批次大小。用法(第 1 章 index 示例里出现过):

// 示意,摘自 crates/rig-core/src/agent/mod.rs RAG 文档示例
let embeddings = EmbeddingsBuilder::new(embedding_model.clone())
.documents(vec!["定义A ...", "定义B ...", "定义C ..."])? // 一批文档
.build().await?; // 分批向量化
vector_store.add_documents(embeddings); // 存进向量库

4.4 检索的统一接口:VectorStoreIndex

检索由 VectorStoreIndex 特征统一(crates/rig-core/src/vector_store/mod.rs:84):

// 示意,摘自 crates/rig-core/src/vector_store/mod.rs:84 VectorStoreIndex
pub trait VectorStoreIndex: WasmCompatSend + WasmCompatSync {
fn top_n<T: Deserialize>(&self, query: ..., n: usize) // 查最相近的 n 条(带反序列化的对象)
-> impl Future<Output = Result<Vec<(f64, String, T)>, ...>>;
fn top_n_ids(&self, query: ..., n: usize) // 只要 ID + 分数(更省)
-> impl Future<Output = Result<Vec<(f64, String)>, ...>>;
}

两个方法就是 RAG 检索的全部:top_n 返回「分数 + ID + 反序列化后的对象」,top_n_ids 只返回分数和 ID(不需要取回原文时更省)。MongoDB、Qdrant、Postgres 等约 10 个后端各实现一遍这个特征,检索语义就统一了。

还有一个 VectorStoreIndexDyncrates/rig-core/src/vector_store/mod.rs:106)——和第 3 章工具的 ToolDyn 同一个套路,做类型擦除以便动态存放。核心库自带一个 InMemoryVectorStorecrates/rig-core/src/vector_store/in_memory_store.rs)用于测试和小规模场景,还带一个 LSH(局部敏感哈希)实现(lsh.rs)加速近似检索。


4.5 RAG 落到 agent:dynamic_context 与 dynamic_tools

把检索接进 agent,靠 AgentBuilder 的两个方法(crates/rig-core/src/agent/builder.rs):

方法干什么位置
dynamic_context(n, index)每次 prompt 时,从 index 检索 top-n 文档,作为上下文注入crates/rig-core/src/agent/builder.rs:182
dynamic_tools(n, index, toolset)每次 prompt 时,从 index 检索 top-n 工具,动态提供给模型crates/rig-core/src/agent/builder.rs:515

「静态」和「动态」的区别是理解 RAG agent 的关键:

静态上下文 static_context ── 永远提供(每次请求都带上)
动态上下文 dynamic_context ── prompt 时按相似度现查(RAG)

静态工具 ── 永远可用
动态工具 dynamic_tools ── prompt 时按相似度现查(工具太多时,只给相关的)

dynamic_context 解决「知识库太大塞不进上下文窗口」——只检索最相关的几条。dynamic_tools 解决「工具太多,全给模型会干扰选择」——按当前问题只暴露相关工具。检索发生在组装 CompletionRequest 的时候(build_completion_request 里对 dynamic_context 的处理,crates/rig-core/src/agent/completion.rs:249 附近的检索 futures)。

这里回扣第 2 章:动态上下文/工具的检索,就是在状态机吐 CallModel 之后、真正发请求之前,由驱动器组装请求时完成的(build_prepared_completion_request,第 2 章 drive_agent 里调用)。


4.6 结构化抽取:Extractor

RAG 是「让模型知道更多」,Extractor 是「让模型的输出更规整」——把自由文本变成能反序列化的 Rust 结构体(crates/rig-core/src/extractor.rs:70)。

直觉:你想从一段话里抽出 { city, temperature, conditions }。传统做法是让模型输出 JSON 再手动解析、失败了再重试。Extractor 把这套封装好:

// 示意,基于 crates/rig-core/src/extractor.rs Extractor::extract
let extractor: Extractor<M, WeatherForecast> = /* 从 agent 构建,附上 T 的 schema */;
let forecast: WeatherForecast = extractor.extract("纽约今天怎么样").await?;

它的实现套路(extractcrates/rig-core/src/extractor.rs:92):把目标类型 T 的 JSON Schema 生成出来告诉模型,收回后反序列化成 T,解析失败按 retries 字段重试。底层其实是用一个「提交结构化结果」的合成工具驱动模型——这和第 5 章要讲的 OutputMode::Tool 是同一套机制。

Extractor 有多个变体覆盖不同需求:extract_with_chat_history(带历史)、extract_with_usage(同时返回 token 用量)等(crates/rig-core/src/extractor.rs:124 / :164)。

第 1 章提过的 TypedPrompt::prompt_typed::<T>() 是更轻量的入口——不显式造 Extractor,直接在 agent 上要结构化输出。两者殊途同归。


4.7 本章小结与去向

  • RAG 一条链在 Rig 里全是统一特征:Embed(标注哪些进向量)→ EmbeddingsBuilder/EmbeddingModel(批量向量化,按 MAX_DOCUMENTS 自动分批)→ VectorStoreIndextop_n 检索)。
  • dynamic_context/dynamic_tools 让 agent 在 prompt 时现查上下文/工具,对付「知识库/工具太多」。
  • 静态 = 每次都带;动态 = 按相似度现查。
  • Extractor(和 prompt_typed)做结构化输出,底层用「提交结果」合成工具,失败可重试。
  • 结构化输出的三种模式(Native/Tool/Prompted)与 issue #1928 → 第 5 章。

代码地图

主题文件符号
标注向量化字段crates/rig-core/src/embeddings/embed.rsEmbed / TextEmbedder
向量化模型特征crates/rig-core/src/embeddings/embedding.rsEmbeddingModel / MAX_DOCUMENTS
批量向量化crates/rig-core/src/embeddings/builder.rsEmbeddingsBuilder
检索特征crates/rig-core/src/vector_store/mod.rsVectorStoreIndex / top_n
检索特征(擦除)crates/rig-core/src/vector_store/mod.rsVectorStoreIndexDyn
内存向量库crates/rig-core/src/vector_store/in_memory_store.rsInMemoryVectorStore
动态上下文/工具crates/rig-core/src/agent/builder.rsdynamic_context / dynamic_tools
结构化抽取crates/rig-core/src/extractor.rsExtractor / Extractor::extract