13.3 检索增强生成(RAG)

`rust-agent-rag` 提供了完整的 RAG(Retrieval-Augmented Generation)管道抽象,包括文档加载、智能分块、嵌入向量化、向量存储和语义检索。框架不绑定特定实现,通过 trait 抽象允许替换任意组件。

RAG 管道架构

flowchart LR
    subgraph "数据摄入"
        DL[DocumentLoader]
        CH[Chunker]
    end

    subgraph "向量化"
        EM[IEmbeddingModel]
    end

    subgraph "存储与检索"
        VS[IVectorStore]
        RT[IRetriever]
    end

    subgraph "Agent 使用"
        AG[IAgent]
    end

    DL -->|原始文档| CH
    CH -->|文本块| EM
    EM -->|向量| VS
    VS -->|查询| RT
    RT -->|相关块| AG

核心 Trait

DocumentLoader — 文档加载器

负责从各种来源加载文档:

pub trait DocumentLoader: Send + Sync {
    /// 加载文档并返回结构化内容
    async fn load(&self, source: &str) -> Result<Document>;

    /// 支持的来源类型(file, url, text 等)
    fn supported_sources(&self) -> Vec<&str>;
}

Chunker — 文档分块器

将长文档智能分割为适合嵌入的文本块:

pub trait Chunker: Send + Sync {
    /// 将文档分割为文本块
    fn chunk(&self, document: &Document) -> Result<Vec<Chunk>>;

    /// 分块策略名称
    fn strategy(&self) -> &str;
}

支持的分块策略:

| 策略 | 原理 | 适用场景 | |------|------|---------| | **Recursive** | 按段落→句子→词递归分割 | 通用文档 | | **Semantic** | 基于语义边界分割 | 技术文档 | | **Fixed-size** | 固定 token 数分割 | 简单场景 | | **Markdown-aware** | 识别 Markdown 标题结构 | Markdown 文档 |

IEmbeddingModel — 嵌入模型

pub trait IEmbeddingModel: Send + Sync {
    /// 将文本转换为向量
    async fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>>;

    /// 模型的向量维度
    fn dimension(&self) -> usize;

    /// 模型标识符
    fn model_id(&self) -> &str;
}

IVectorStore — 向量存储

pub trait IVectorStore: Send + Sync {
    /// 存储向量及其元数据
    async fn store(&self, chunks: Vec<StoredChunk>) -> Result<()>;

    /// 按向量相似度搜索
    async fn search(
        &self,
        query_vector: &[f32],
        top_k: usize,
    ) -> Result<Vec<ScoredChunk>>;

    /// 删除指定文档的所有块
    async fn delete(&self, document_id: &str) -> Result<()>;
}

IRetriever — 检索器

pub trait IRetriever: Send + Sync {
    /// 根据查询文本检索相关块
    async fn retrieve(
        &self,
        query: &str,
        top_k: usize,
    ) -> Result<Vec<RetrievedChunk>>;
}

完整 RAG 管道示例

use rust_agent_rag::{
    DocumentLoader, Chunker, IEmbeddingModel, IVectorStore, IRetriever,
};

async fn build_rag_pipeline() -> anyhow::Result<()> {
    // 1. 加载文档
    let loader = MyDocumentLoader::new();
    let document = loader.load("./docs/technical_manual.md").await?;
    println!("文档已加载: {} 字符", document.content.len());

    // 2. 分块
    let chunker = RecursiveChunker::new()
        .with_chunk_size(512)      // 每块 512 tokens
        .with_chunk_overlap(50);   // 块间重叠 50 tokens

    let chunks = chunker.chunk(&document)?;
    println!("分割为 {} 个文本块", chunks.len());

    // 3. 嵌入向量化
    let embedding_model = MyEmbeddingModel::new("text-embedding-3-small");
    let texts: Vec<String> = chunks.iter().map(|c| c.text.clone()).collect();
    let vectors = embedding_model.embed(&texts).await?;
    println!("生成 {} 个向量 (维度: {})", vectors.len(), embedding_model.dimension());

    // 4. 存储到向量库
    let vector_store = MyVectorStore::connect("redis://localhost:6379").await?;
    let stored_chunks: Vec<StoredChunk> = chunks.iter()
        .zip(vectors.iter())
        .map(|(chunk, vector)| StoredChunk {
            id: chunk.id.clone(),
            document_id: document.id.clone(),
            text: chunk.text.clone(),
            vector: vector.clone(),
            metadata: chunk.metadata.clone(),
        })
        .collect();

    vector_store.store(stored_chunks).await?;
    println!("已存储到向量库");

    // 5. 检索
    let retriever = DefaultRetriever::new(embedding_model, vector_store);
    let results = retriever.retrieve("Rust async trait 如何定义?", 5).await?;

    println!("检索结果:");
    for (i, chunk) in results.iter().enumerate() {
        println!("{}. [分数: {:.4}] {}", i + 1, chunk.score, 
            &chunk.text[..100.min(chunk.text.len())]);
    }

    // 6. 构建上下文注入 Agent
    let context = results.iter()
        .map(|c| format!("---\n来源: {}\n\n{}", 
            c.metadata.get("source").unwrap_or(&"unknown".to_string()),
            c.text))
        .collect::<Vec<_>>()
        .join("\n\n");

    Ok(())
}

分块策略详解

RecursiveChunker 递归分块

pub struct RecursiveChunker {
    chunk_size: usize,         // 目标块大小(tokens)
    chunk_overlap: usize,      // 块间重叠(tokens)
    separators: Vec<String>,   // 分隔符优先级:["\n\n", "\n", "。", ".", " ", ""]
}

分块过程: 1. 尝试使用最高优先级分隔符(段落分隔 `\n\n`)分割 2. 如果某段超过 `chunk_size`,使用下一级分隔符 3. 递归直到使用字符级分割(空字符串) 4. 相邻块保留 `chunk_overlap` tokens 的重叠

SemanticChunker 语义分块

pub struct SemanticChunker {
    similarity_threshold: f32,  // 语义断点阈值(默认 0.7)
    min_chunk_size: usize,      // 最小块大小
    max_chunk_size: usize,      // 最大块大小
}

使用嵌入相似度检测语义边界: 1. 计算相邻句子的嵌入向量 2. 当相似度低于阈值时标记为断点 3. 在断点处分割,确保块在 min/max 范围内

与 Agent 集成

构建 RAG 工具

use rust_agent_macros::tool;

#[tool(description = "从知识库中检索相关信息")]
async fn search_knowledge_base(
    #[param(desc = "搜索查询")] query: String,
    #[param(desc = "最大结果数")] top_k: Option<i64>,
) -> rust_agent_core::ToolResult {
    let retriever = get_global_retriever();
    let results = retriever.retrieve(
        &query,
        top_k.unwrap_or(5) as usize,
    ).await.map_err(|e| /* ... */)?;

    let formatted = results.iter()
        .enumerate()
        .map(|(i, c)| format!("[{}] (相关度: {:.2})\n{}", i + 1, c.score, c.text))
        .collect::<Vec<_>>()
        .join("\n\n");

    rust_agent_core::ToolResult::success(serde_json::json!({
        "results": formatted,
        "count": results.len(),
    }))
}

// 注册到 Agent
let agent = AgentBuilder::new("rag_agent")
    .chat_client(client)
    .instructions("你可以使用 search_knowledge_base 工具检索内部知识库。")
    .with_tool(SearchKnowledgeBase)
    .build()?;

使用 ContextProvider 自动注入

struct RAGContextProvider {
    retriever: Arc<dyn IRetriever>,
}

#[async_trait]
impl IContextProvider for RAGContextProvider {
    fn name(&self) -> &str { "RAGContextProvider" }
    fn kind(&self) -> &str { "knowledge" }

    async fn on_invoking(
        &self,
        _agent: &dyn IAgent,
        _session: &dyn ISession,
        messages: &[ChatMessage],
        _options: &AgentRunOptions,
    ) -> Result<ContextResult> {
        // 从最后一条用户消息中提取查询
        let last_user_msg = messages.iter()
            .rev()
            .find(|m| m.role == "user")
            .map(|m| m.content());

        if let Some(query) = last_user_msg {
            let results = self.retriever.retrieve(&query, 3).await?;
            let context = format_context(results);
            return Ok(ContextResult {
                instructions: Some(format!(
                    "以下是相关知识库内容,请基于这些内容回答问题:\n\n{}",
                    context
                )),
                ..Default::default()
            });
        }

        Ok(ContextResult::default())
    }
}

注意事项

1. **嵌入模型选择**:需要自行集成嵌入模型(如 OpenAI text-embedding-3-small、本地模型等) 2. **向量存储后端**:需要自行选择向量存储(Redis、Qdrant、Milvus、FAISS 等) 3. **chunk_size 调优**:过小丢失上下文,过大降低检索精度。推荐 512-1024 tokens 4. **chunk_overlap 设置**:推荐 chunk_size 的 10-15%,确保跨块上下文连续性 5. **分块策略选择**:通用文档用 RecursiveChunker,技术文档用 SemanticChunker