ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rust开发AI Agent:RAG检索增强生成入门落地指南

Rust开发AI Agent:RAG检索增强生成入门落地指南 在 Rust 里做 AI Agent日常对话、工具调用、任务编排这些环节跑通之后一定会撞上一个更现实的问题Agent 怎么拿到自己知识范围之外的资料。直接让模型硬答只能靠训练数据里的旧记忆把整本文档塞进上下文又很快被 token 长度和费用卡死。这个问题的常用解法就是 RAG也就是检索增强生成。简单说先从一个外部知识库里检索出和用户问题相关的片段再把片段作为上下文交给模型让模型基于这些材料回答。这篇内容围绕“使用 Rust 开发 AI Agent 系列第 08 篇”展开只讲 RAG 的入门落地它解决什么问题、在 Rust 项目里怎么组织、最常见的流程是什么、有哪些坑必须提前知道。1. 先搞清楚 RAG 在 Agent 里的职责边界RAG 不是模型不是向量数据库也不等于 Embedding 接口。它是一套流程把知识库内容切成块给块做向量化把向量存进数据库收到用户问题后把问题也向量化用相似度检索出相关片段最后把片段、问题、Agent 指令一起拼进 Prompt交给模型生成回答。在整个 AI Agent 架构里RAG 负责的是知识供给和上下文精简这两件事。1.1 为什么 Agent 不能只靠模型记忆大模型训练完成后知识是固定的。它能写出 2023 年之前的通用知识但拿不到你公司内部的故障手册、产品文档、项目日志或最新规章制度。想让它回答这些内容有几种临时办法把全文塞进 Prompt小文档可以大文档会爆用模型微调成本高、周期长而且每次文档更新都要重新训练让 Agent 自己去网上搜能拿到外部信息但噪声大、来源不可控、还要额外处理网络请求和内容清洗。RAG 的价值在于文档更新不需要重新训练模型只要重新切块、重新写向量库回答时只把检索出的片段送入上下文token 消耗可控检索过程可以针对知识库范围做过滤和排序比全量 Prompt 更精准。在 Rust 里做 Agent这些环节都能用库和工具组装起来不需要自己从零写一套搜索引擎。1.2 Agent 和 RAG 的典型配合方式假设你正在写一个 Rust Agent负责回答公司内部的技术规范问题。完整流程是这样的用户提问Agent 先判断问题是否需要知识库支持。如果需要Agent 调用 RAG 检索模块。RAG 模块把问题向量化在向量库里做相似度检索。检索结果经过重排或过滤取前 N 条片段。Agent 把片段和原始问题拼进 Prompt。模型根据 Prompt 生成回答。Agent 把回答返回给用户并在日志里记录使用到的片段来源。这个过程中RAG 不是独立服务而是 Agent 内部的一个能力模块。在 Rust 工程里它可以是一个子模块、一个 trait 的实现也可以是一个独立进程通过 mcp 协议暴露给 Agent 调用。关键点在于RAG 的职责是“给 Agent 找材料”不是“替 Agent 做决定”。决定怎么回答、是否引用、何时承认不知道仍然由 Agent 核心逻辑控制。2. RAG 的完整链路拆解RAG 看起来只有“存”和“查”两步实际拆开包含很多子任务。下面按一个 Rust 项目从零搭建的顺序展开。2.1 知识库准备格式清洗和分块策略第一步是把原始文档变成可检索的文本片段。不要直接拿 PDF、Word、HTML 原文去向量化必须先清洗。PDF 可能带页眉页脚Word 可能有表格HTML 可能包含导航和脚本代码。清洗的目的不是把格式美化而是避免无意义内容进入向量库。我的建议是先转成纯 Markdown 或纯文本再按章节和段落切块。分块策略直接影响检索质量。常见的分块方式有固定字符切块、按段落切块、按标题层级切块、按语义边界切块。固定字符切块最简单字符数可以设成 500 到 800重叠设 50 到 100。但边界容易切断一句话。按段落切块更自然但长段落的块会过大。最稳的做法是混合策略先按标题拆分大体结构再对超长段落做二次切块切块时保留上下文重叠。在 Rust 项目里这些逻辑不需要造轮子。读取文件可以用标准库或std::fs文本清洗可以用正则表达式regexMarkdown 结构解析可以借用pulldown-cmark或markdowncrate。切块时有一个重要细节每个片段要保留元数据比如来源文件、章节标题、段落序号。后面检索结果展示和引用溯源都靠这些元数据。2.2 Embedding把文本变成向量文本本身不能直接比较相似度需要先转换成向量。Embedding 模型的作用是把一段文本映射到高维向量空间语义相近的文本在向量空间中距离更近。用 Rust 开发时有两种接入方式第一种是调用外部 Embedding API通过 HTTP 请求把文本发送过去拿到返回的向量数组。这种方式实现简单模型升级和计算资源都由服务方管理。缺点是有网络延迟和接口费用而且在本地开发时容易遇到网络不稳定。第二种是本地运行 Embedding 模型例如通过 llama.cpp、fastembed 或 ort 加载模型进行推理。这种方式延迟低、数据不出本机适合隐私要求高的场景。缺点是部署成本高一点需要准备模型文件和推理依赖。具体选哪种取决于你的项目是给个人开发用还是生产环境用。个人学习阶段可以先调 API把流程跑通生产环境如果知识库更新频繁、查询量大再考虑本地化部署。需要提醒的是不要用一个 Embedding 模型建库又用另一个模型做查询。不同模型生成的向量空间不一致相似度结果会失真。这属于最隐蔽的 RAG 错误之一。2.3 向量存储从零实现还是接入数据库向量存哪里是 RAG 入门时最容易纠结的问题。能跑 Demo 的方案很多可以用一个 JSON 文件把所有向量存起来查询时算余弦相似度可以引入ndarray做矩阵运算可以用 SQLite 加扩展存向量也可以接入独立的向量数据库比如 Qdrant、Milvus、Chroma、pgvector 等。我先给一个判断标准知识库只有几十到几百条片段学习或 Demo 阶段用内存或本地文件足够。知识库有几千到几万条片段并发查询要求不高用 SQLite 加向量扩展或轻量数据库足够。知识库达到十万级以上查询并发高需要过滤和混合检索再上专用向量数据库。入门阶段不建议一上来就上重组件。我通常会先用一个简单的「文本 向量」结构跑通流程比如用serde_json存储 JSON 文件每次查询时加载并算相似度。这个方式慢但每一步都透明方便调试。真正要考虑检索性能时再切换到向量数据库。2.4 检索相似度计算和召回逻辑检索阶段要把用户问题向量化然后与库里的所有片段向量计算相似度。最常用的是余弦相似度。余弦相似度取值从 -1 到 1越接近 1 表示方向越一致、语义越相近。用 Rust 实现时可以把向量保存成Vecf32相似度计算写成简单的点积除以模长。这里有一个关键点连续向量化所有片段是 O(N) 开销。知识库小的时候没问题几十条向量可以毫秒级算完。知识库大的时候就要考虑索引结构比如 HNSW 或 IVFFlat这部分通常由向量数据库完成。入门阶段我建议先不碰索引算法把精力放在“能不能检索出正确片段”上。检索不能只看相似度分数。还要考虑Top-K 选择返回多少条最好。通常取 3 到 5 条太多会稀释模型注意力太少可能漏掉关键信息。阈值过滤相似度低于某个值的片段最好丢弃避免把无关内容拼进 Prompt。元数据过滤例如按来源文件、章节过滤让检索只限定在某个目录或某个版本范围内。2.5 Prompt 拼接和生成检索完成后把片段拼进 Prompt。Rust 工程里我习惯把 Prompt 模板做成一个独立文件或常量片段部分用占位符替换。这样可以避免在代码里到处拼接字符串。模板大致长这样你是一个基于知识库回答问题的助手。 请只根据以下资料回答问题不要编造事实。 如果资料中没有相关内容请明确说“知识库中没有找到相关信息”。 资料 {context} 问题 {question}注意几个细节上下文和问题之间要有明确分隔。要明确告诉模型“不要编造”。如果业务要求引用来源要在 Prompt 里要求模型给出片段编号或文档标题。片段拼接顺序一般按检索分数从高到低。但某些场景下按文档逻辑顺序组织上下文可能更好这个要测试后决定。当 Agent 还包含工具调用时RAG 的上下文不能直接覆盖 Agent 的全部指令。我的经验是把 RAG 检索结果作为“临时记忆”注入当前这一轮 Prompt不要污染 Agent 的系统提示词。这样可以避免长期对话中上下文越来越乱。3. 在 Rust 项目里落地一个最小 RAG 模块下面给一个最小可运行的 Rust RAG 模块设计。代码不追求完整重点是看清模块边界和调用顺序。3.1 项目结构和 Cargo 依赖假设你的项目名是agent_rag_demo推荐目录结构agent_rag_demo/ ├── Cargo.toml ├── data/ │ └── docs/ │ ├── 01_intro.md │ └── 02_guide.md ├── src/ │ ├── main.rs │ ├── ingestion.rs │ ├── embedding.rs │ ├── store.rs │ └── retrieve.rs入门阶段需要的依赖比较杂我列一个常见组合serde和serde_json处理 JSON 配置、切块结果、向量存储。anyhow或thiserror统一错误处理主流程写起来省事。reqwest调用 Embedding API 或 LLM API。regex文本清洗和简单切块。tokio异步运行时Agent 和 API 调用通常需要。clap命令行参数解析方便支持“建库/查询”两种模式。Cargo.toml里的依赖版本不建议照抄某个文章。先用cargo add添加让 Cargo 自动解析当前最新的合适版本。3.2 切块和入库流程入库流程分成三步读文件、切块、向量化后存储。代码逻辑比较容易理解use std::fs; use std::path::Path; pub struct Chunk { pub id: String, pub content: String, pub source: String, pub embedding: Vecf32, } pub fn ingest_file(path: Path) - VecString { let content fs::read_to_string(path) .expect(failed to read file); let cleaned content .replace(\r\n, \n) .trim() .to_string(); // 简单切块按段落切超长段落再按字符切 let paragraphs: Vecstr cleaned.split(\n\n).collect(); let mut chunks Vec::new(); for para in paragraphs { if para.trim().is_empty() { continue; } chunks.push(para.trim().to_string()); } chunks }这不是生产级代码但足以说明切块的顺序先读、先清洗、再切。真实项目里要把清洗规则扩展成针对 PDF、Word、Markdown 的不同处理流程。入库时每个片段要生成唯一 ID。我一般用来源文件 段落序号拼一个 ID比如01_intro.md#03这样后续调试时能快速定位片段在文档中的位置。3.3 查询流程查询流程同样按顺序执行pub fn retrieve(query: str, chunks: [Chunk], top_k: usize) - VecChunk { let query_embedding get_embedding(query); let mut scored: Vec(f32, Chunk) chunks .iter() .map(|c| (cosine_similarity(query_embedding, c.embedding), c)) .collect(); scored.sort_by(|a, b| b.0.partial_cmp(a.0).unwrap()); scored.truncate(top_k); scored.into_iter().map(|(_, c)| c.clone()).collect() } fn cosine_similarity(a: [f32], b: [f32]) - f32 { let dot: f32 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); let norm_a: f32 a.iter().map(|x| x * x).sum::f32().sqrt(); let norm_b: f32 b.iter().map(|x| x * x).sum::f32().sqrt(); dot / (norm_a * norm_b 1e-8) }这个查询流程的优点是直观缺点是每次查询都要遍历所有向量。先用它跑通链路再谈优化。3.4 封装成 Agent 的一个工具在 Agent 的架构里RAG 不要直接裸调用。我建议把 RAG 封装成一个工具函数或一个 trait 对象Agent 根据用户意图决定是否调用。pub trait KnowledgeTool { fn search(self, query: str) - VecKnowledgeItem; } pub struct RagKnowledgeTool { chunks: VecChunk, } impl KnowledgeTool for RagKnowledgeTool { fn search(self, query: str) - VecKnowledgeItem { let results retrieve(query, self.chunks, 3); results.into_iter().map(|c| KnowledgeItem { content: c.content, source: c.source, }).collect() } }这样做有两个好处一是 Agent 可以多接几个不同的知识工具比如数据库检索、文件搜索、API 搜索RAG 只是其中之一二是测试时可以用 mock 工具替换 RAG方便单独验证 Agent 的决策逻辑。4. 检索质量不高时按顺序排查很多人在 RAG 上踩坑后第一反应是换模型。实际上检索质量不高通常是流程问题不是模型能力问题。建议按下面顺序排查。4.1 先看输入文档能不能被切出有效片段如果知识库文档本身是扫描版 PDF文本内容可能乱码如果文档是 PPT 里的图片截图连文本都提取不到如果表格结构被切块切散了检索结果自然碎片化。这里的判断标准是把切块结果直接打印出来人眼能不能看懂每一块在说什么。如果切出来都是“第 3 章”“续表”“来源…”这类碎片说明清洗和切块策略有问题。4.2 再看切块尺寸是否合适切块太小一块只包含半句话检索到的内容缺少上下文切块太大一块包含多个主题检索命中时会把无关内容带进 Prompt。经验范围是每个块 300 到 800 个中文字符重叠 50 到 100 字符。但更重要的是看内容边界每个块尽量只讲一个完整主题。比固定字符串更稳的是按标题切块标题可以成为非常自然的边界。4.3 然后看 Embedding 模型是否匹配有些模型擅长中文有些擅长英文有些适合短文本匹配有些适合长文本。如果你的知识库是中文技术文档却用了主要针对英文优化的模型检索效果不会好。改用另一套模型后必须重新生成全部向量不能只重新查询。4.4 最后看 Prompt 上下文是否需要重排有时候检索结果里第 1 名确实相关但第 2 名、第 3 名质量不高。这时候不要盲目扩大 Top-K而是考虑引入重排序模型把召回结果再精排一遍。生产级 RAG 中重排序是提升回答质量的重要环节但它不是入门第一个要做的优化点。先把切块和 Embedding 做对再考虑重排序。5. Rust 生态里可以关注的方向RAG 在 Rust 生态里没有像 Python 那样形成一套全家桶但关键组件的选择并不少。5.1 Embedding 和向量数据库选型Rust 生态里做 Embedding可以关注fastembed它的思路是加载 ONNX 格式的嵌入模型本地推理。也有项目通过ort直接跑 ONNX Runtime。API 方式更常见直接基于reqwest封装服务商的接口。向量数据库方面Qdrant 有 Rust 客户端pgvector 可以通过 SQLx 或 SeaORM 操作Milvus 的 Rust 客户端也有人在维护。入门阶段建议选一个你最熟悉生态的组合。比如你已经用 PostgreSQL那就用 pgvector如果你不想维护独立数据库可以先开一个 Qdrant 本地容器。5.2 Agent 框架与 RAG 的集成Rust 的 AI Agent 生态相比 Python 小很多但也不是空白。你可以选择直接手写 Agent 循环也可以使用社区 crate 组合。无论是自己封装还是用框架RAG 集成点通常都在“模型调用前”。只要 Agent 能拿到工具调用结果RAG 就能作为一个工具模块接入。我建议在项目早期就把 RAG 模块的接口设计成 trait避免以后框架切换时重构大改。5.3 RAG 之上的进阶方向如果 RAG 基础链路已经稳定可以进一步探索Agentic RAG把检索拆成多步Agent 先判断需要什么信息再决定查哪个知识库、用什么关键词、是否追问用户。知识图谱增强 RAG实体、关系、属性作为额外索引用来弥补纯向量检索对关系型问题的不足。混合检索结合关键词检索和向量检索再用重排序合并适合专业术语多、同义词多的知识库。查询改写用户问题太长时先用模型提取关键词或生成多路查询再分别检索汇总。这些方向都有一个共同前提基础链路稳定可观测性到位。如果查询日志都没有先别急着上高级模块。6. 一个可执行的入门验收清单学习 RAG 时不建议只背概念也不建议直接上生产架构。按照下面的验收清单推进更容易建立正确判断。能用自己的知识库文档完成入库流程确认切块数量合理。能打印出每个切块的内容和元数据人眼判断切割是否合理。能对 3 个不同问题执行检索观察返回片段是否相关。能把检索结果拼进 Prompt让模型根据片段回答并标注“来自哪个来源”。能构造一个问题让模型明确说“知识库中没有找到相关信息”验证 Prompt 约束生效。能对比不同 Top-K 参数、不同切块大小下的回答质量差异。如果以上 6 点都做到了RAG 入门就算基本过关。这样再去用 Rust 开发更多 AI Agent 功能时知识供给部分就不会成为瓶颈。我自己做这个系列时第一版 RAG 也是直接读文档、暴力切块、内存存向量。当时检索结果非常不稳定。后来发现问题是切块没有保留上下文重叠导致一个问题的关键信息被切到了两块边缘。调整完分块策略效果立刻提升。这个场景说明RAG 入门阶段最值得花时间的不是换更强的模型而是把数据准备流程做扎实。
返回列表