尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Rust实现RAG:为AI Agent构建知识库与长期记忆的完整指南

Rust实现RAG:为AI Agent构建知识库与长期记忆的完整指南 这次我们来看一个偏工程向的话题用 Rust 开发 AI Agent 时如何给 Agent 接上“长期记忆”和“外部知识”。前面几篇如果已经跑通了 Agent 的基础调用链那么进入 RAG 之后主要解决的就是一个问题让模型在回答时不再只靠训练参数里的旧知识而是能从你指定的文档库里检索相关内容再生成答案。RAGRetrieval-Augmented Generation检索增强生成这个概念这几年已经被讲得很多了但真正落到 Rust 生态里很多资料其实比较零散。这次的系列第 08 篇我们就围绕 RAG 做一个完整梳理从 RAG 的基本流程开始到 Rust 里怎么选型、怎么设计文档切块、怎么调用向量数据库、怎么把检索结果交给 LLM 生成最终回答最后再补上接口 API、批量任务、性能观察和常见排查思路。如果你正准备在 Rust 项目里做知识库问答、私有文档检索、或者给 Agent 增加长期记忆这篇文章可以直接收藏。1. RAG 核心能力速览能力项说明项目类型Rust 实现 RAG 技术方案与代码示例基于开源生态组件搭建核心功能文档加载、文本切块、向量化、向量存储、相似度检索、LLM 生成适用场景本地知识库问答、私有文档检索、Agent 长期记忆、RAG 实战与 agentic rag 扩展技术栈Rust、LLM API 或本地推理服务、向量数据库按项目选型是否支持批量任务支持。文档批量入库、批量检索可设计为独立任务队列是否支持 API支持。可以通过 HTTP 接口暴露检索与问答能力启动方式命令行工具 / 后台服务 / 可嵌入库部署复杂度中。依赖项较多但整体可控适合读者有 Rust 基础正在做 AI Agent 开发需要接入知识库能力的开发者这里先说明一个前提RAG 本身不是某个单一框架而是一套技术流程。Rust 生态里目前没有像 Python 的 LangChain/LlamaIndex 那样大一统的框架但这不构成阻碍。Rust 的强项在于性能和可控性你可以把 RAG 流程拆成几个清晰的模块然后自己组装。2. RAG 适用场景与使用边界2.1 适合解决的场景私有知识库问答公司内部文档、产品手册、技术规范、法律条文这类内容不适合直接塞进 prompt更不适合微调。RAG 可以在问答时动态检索相关段落让 LLM 基于检索到的内容回答。Agent 长期记忆AI Agent 的对话上下文有长度限制而且多轮会话之间往往会丢失信息。把关键信息写入向量库Agent 在后续任务中按需检索可以显著提升连续性。领域术语和事实纠偏通用模型对垂直领域的新增信息不了解。RAG 可以把最新的业务文档、版本变更记录、配置说明等注入生成过程减少幻觉。减少 prompt 长度长文档不能全部塞进上下文RAG 通过检索只取最相关的片段控制 token 成本。RAG 实战与 agentic rag 扩展在基础 RAG 之上还可以让 Agent 自主决定“什么时候检索、检索什么、检索结果不够时怎么调整”这就演化为 agentic rag。Rust 的优势是并发调度和资源控制更适合做这类复杂流程。2.2 不适合或需要谨慎的场景需要精确数字和严格推理的场景RAG 的检索结果本身可能有遗漏如果问题需要全局统计、跨多个文档整合单纯靠 top-k 检索容易出现信息缺失。此时需要引入更复杂的查询改写、多路召回或图结构。高实时性写入场景向量化有成本如果文档频繁变更且要求秒级生效需要设计好增量更新机制。涉及版权和个人隐私的内容不要把未授权的文档、人脸信息、声音信息、个人隐私数据直接导入知识库并对外提供服务。本地测试可以但上线前必须确认数据来源合法和授权边界。复杂推理被高估RAG 擅长“找到内容并重新表述”但不太擅长“基于多篇文档做深度推理”。Agent 类任务可以配合 ReAct、多步调用等方式弥补但不要把 RAG 当成万能推理引擎。2.3 合规与安全边界无论是本地知识库还是对外 API 服务都必须注意用于测试的文档、图片、语音素材需要确认版权与授权如果系统会处理人脸、声音或个人信息需要遵守相关隐私法规向量库中不要长期保存敏感明文确有需要时应做好权限控制和加密存储API 服务不能默认对公网开放应限制访问范围和调用频率。3. RAG 技术基础与设计思路在写 Rust 代码之前先把 RAG 主链路理清楚。不管用哪种语言RAG 的流程基本一致文档加载 - 文本切块 - 向量化 - 向量存储 - 用户查询 - 查询向量化 - 相似度检索 - 返回候选片段 - 构造 prompt - LLM 生成回答3.1 文本切块策略切块质量直接决定 RAG 效果。常见切块方式固定长度切块按字符数或 token 数切块简单但容易切断语义段落切块按 Markdown 标题、换行、章节等结构切块适用于技术文档滑动窗口切块相邻块之间保留重叠部分避免上下文断裂语义切块通过 embedding 判断句子边界成本较高但效果更细。对于 Rust 实现起步阶段建议先做“段落级别 Markdown 切块 固定 token 上限 重叠窗口”。原因很简单可控、灵活、不引入额外模型依赖。3.2 Embedding 模型选型向量化的核心是文本嵌入模型。Rust 项目可以通过 HTTP 调用外部 embedding 服务也可以加载本地模型。两类方案方案优点缺点调用外部 Embedding API实现简单向量质量稳定数据要发给外部服务有隐私风险本地推理服务数据不出内网可控性高需要 GPU/CPU 资源部署复杂度增加Rust 本地模型推理单进程内完成延迟低依赖 crates 较多模型格式兼容性要测试务实的选择先用外部 API 跑通流程本地部署需求明确之后再用 llama.cpp 或其他推理服务提供本地方案。如果要完全离线可以基于 llama.cpp 的 HTTP 服务跑一个 embedding 模型Rust 端只需要用 HTTP 客户端请求即可不需要在 Rust 里直接推理模型。3.3 向量数据库选型向量数据库部署方式Rust 支持适用场景QdrantDocker / 本地 / 云官方 Rust 客户端中小规模知识库功能完善MilvusDocker / K8sHTTP / gRPC 客户端大规模向量检索pgvectorPostgreSQL 扩展sqlx / deadpool 等已有 PostgreSQL 的场景sqlite-vecSQLite 扩展Rust 可通过 FFI 或 HTTP 模块调用轻量级本地试验自研内存检索进程内实现余弦相似度无额外依赖小规模、快速原型从实际体验看Qdrant 的 Rust 客户端比较友好启动快功能覆盖足够pgvector 则适合不想引入额外服务的团队。原型阶段甚至可以先用一个本地的 JSON 文件存向量靠 brute force 计算相似度先把链路跑通再换正规数据库。4. Rust 本地 RAG 环境准备4.1 基础环境项目要求操作系统Windows / Linux / macOS 均可Rust 工具链stable 版本即可建议保持更新LLM 服务OpenAI 兼容 API或本地 llama.cpp Qwen2-7B 等模型Embedding 服务OpenAI Embedding API或本地 embedding 模型服务向量数据库按选型准备 Docker 或本地服务网络如果需要调用外部 API确保网络可访问本地部署则无需外网这里额外说一句 Rust 工具链安装。在 Windows 上如果不想装 MSVC 环境可以使用 GNU 工具链或者用rustup安装时选择x86_64-pc-windows-msvc之外的 target。具体命令rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu如果你需要更新 Rust 或者更换镜像源可以通过环境变量指定源。比如export RUSTUP_DIST_SERVERhttps://rsproxy.cn export RUSTUP_UPDATE_ROOThttps://rsproxy.cn/rustup这些操作属于常规 Rust 环境配置不是 RAG 专属但部署阶段经常会遇到提前准备好可以减少折腾时间。4.2 项目依赖创建一个新的 Rust 项目cargo new rust-rag-demo cd rust-rag-demoCargo.toml里可以按需添加以下依赖[package] name rust-rag-demo version 0.1.0 edition 2021 [dependencies] tokio { version 1, features [full] } reqwest { version 0.11, features [json, multipart] } serde { version 1, features [derive] } serde_json 1 anyhow 1 qdrant-client 1 clap { version 4, features [derive] } rag-tool 0.1这里的rag-tool是一个示例名实际使用时需要替换为你选择的具体 crate。如果没有现成的 rga 框架可以自己组织代码模块。qdrant-client是 Qdrant 的官方 Rust 客户端如果你用 pgvector则替换为sqlx相关依赖。4.3 服务准备准备一个 OpenAI 兼容的 LLM 服务和一个 embedding 服务。如果是本地部署可以基于 llama.cpp 启动# 启动 llama.cpp 的 OpenAI 兼容服务模型路径按实际修改 ./llama-server -m ./models/qwen2-7b-instruct-q4_k_m.gguf --host 127.0.0.1 --port 8080然后验证服务是否就绪curl http://127.0.0.1:8080/v1/models向量数据库用 Qdrant 的话可以直接用 Docker 启动docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant启动后在浏览器访问http://127.0.0.1:6333/dashboard确认 dashboard 可打开。5. Rust 实现 RAG从文档切块到向量检索5.1 文档加载与切块先实现一个简单的文档加载模块支持读取目录下所有 Markdown 文件并按段落切分。use anyhow::Result; use std::fs; use std::path::Path; pub fn load_documents(dir: str) - ResultVecString { let mut docs Vec::new(); for entry in fs::read_dir(dir)? { let entry entry?; let path entry.path(); if path.extension().map_or(false, |ext| ext md || ext txt) { let content fs::read_to_string(path)?; docs.push(content); } } Ok(docs) } pub fn chunk_text(text: str, max_chars: usize, overlap: usize) - VecString { let mut chunks Vec::new(); let bytes text.as_bytes(); let mut start 0; while start bytes.len() { let end (start max_chars).min(bytes.len()); // 尽量在换行处截断减少语义割裂 let mut cut end; if end bytes.len() { if let Some(pos) text[start..end].rfind(\n) { cut start pos 1; } } let chunk text[start..cut]; if !chunk.trim().is_empty() { chunks.push(chunk.to_string()); } if cut start { break; } start cut.saturating_sub(overlap); } chunks }这里切块逻辑做了两件事限制最大字符数并且截断时优先寻找换行符。overlap参数控制相邻块的重复长度避免一句话被切到两个块里。5.2 调用 Embedding 服务通过 HTTP 调用 embedding 接口。这里以 OpenAI 兼容 API 为例use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Debug, Serialize)] struct EmbeddingRequest { model: String, input: VecString, } #[derive(Debug, Deserialize)] struct EmbeddingResponse { data: VecEmbeddingData, } #[derive(Debug, Deserialize)] struct EmbeddingData { embedding: Vecf32, index: usize, } pub async fn get_embeddings( client: reqwest::Client, api_base: str, api_key: str, model: str, texts: [String], ) - ResultVec(usize, Vecf32) { let url format!({}/v1/embeddings, api_base); let resp client .post(url) .bearer_auth(api_key) .json(EmbeddingRequest { model: model.to_string(), input: texts.to_vec(), }) .send() .await?; let result: EmbeddingResponse resp.json().await?; let mut items result.data; items.sort_by_key(|d| d.index); Ok(items.into_iter().map(|d| (d.index, d.embedding)).collect()) }如果是本地 llama.cpp 服务api_key可以随便填一个占位字符串因为本地服务通常不校验。需要确认服务端是否实现了/v1/embeddings接口。5.3 向量入库到 Qdrant用 Qdrant 官方客户端写入向量use qdrant_client::Qdrant; use qdrant_client::qdrant::{ CreateCollectionBuilder, Distance, PointStructBuilder, VectorParamsBuilder, }; use qdrant_client::qdrant::PointId; use std::collections::HashMap; pub async fn upsert_chunks( collection_name: str, chunks: [String], embeddings: [(usize, Vecf32)], ) - Result() { let client Qdrant::from_url(http://127.0.0.1:6334).build()?; client .create_collection( CreateCollectionBuilder::new(collection_name) .vectors_config(VectorParamsBuilder::new(embeddings[0].1.len() as u64, Distance::Cosine)), ) .await?; let mut points Vec::new(); for (idx, chunk) in chunks.iter().enumerate() { let (_, vector) embeddings.iter().find(|(i, _)| *i idx).unwrap(); let mut payload HashMap::new(); payload.insert(text.to_string(), chunk.clone()); points.push( PointStructBuilder::new( PointId::from(idx as u64), vector.clone(), payload, ) ); } client.upsert_points(collection_name, points, None).await?; Ok(()) }注意create_collection在集合已存在时会报错生产环境中需要先检查集合是否存在再决定是否创建。5.4 相似度检索pub async fn search( collection_name: str, query_vector: Vecf32, top_k: u64, ) - ResultVecString { let client Qdrant::from_url(http://127.0.0.1:6334).build()?; let result client .search_points( collection_name, query_vector, top_k, None, ) .await?; let mut texts Vec::new(); for point in result { if let Some(text) point.payload.get(text) { if let Ok(s) serde_json::from_value::String(text.clone()) { texts.push(s); } } } Ok(texts) }检索完拿到的texts就是候选片段下一步是拼进 prompt。5.5 构造 prompt 并调用 LLMpub async fn generate_answer( client: reqwest::Client, api_base: str, api_key: str, model: str, question: str, contexts: [String], ) - ResultString { let context_block contexts .iter() .enumerate() .map(|(i, c)| format!([{i}]\n{c})) .collect::Vec_() .join(\n\n); let system_prompt 你是一个知识库问答助手。请基于提供的参考资料回答问题。\ 如果参考资料中没有相关信息请直接说明不要编造。; let user_prompt format!( 参考资料:\n{context_block}\n\n问题:\n{question} ); let url format!({}/v1/chat/completions, api_base); let body json!({ model: model, messages: [ {role: system, content: system_prompt}, {role: user, content: user_prompt} ], temperature: 0.2 }); let resp client .post(url) .bearer_auth(api_key) .json(body) .send() .await?; let data: serde_json::Value resp.json().await?; let answer data[choices][0][message][content] .as_str() .unwrap_or_default() .to_string(); Ok(answer) }这套代码已经串起了完整链路。实际项目中可以把每一步拆成独立模块loader、splitter、embedder、vector_store、generator这样替换组件时不需要改动主流程。6. 功能测试与效果验证6.1 测试目标测试项预期结果文档加载能读取指定目录下的 md/txt 文件文本切块输出块数合理无空块向量化能拿到 embedding 向量维度正确向量入库Qdrant dashboard 能看到 collection 和点数量检索查询向量能返回语义相关的片段生成LLM 基于检索片段回答不产生明显幻觉6.2 测试步骤先准备三份测试文档内容不要用泛泛的范文而是用真实的、有辨识度的技术内容。比如文档 A 写“Rust 的异步运行时 Tokio 默认是多线程调度”文档 B 写“RAG 的切块策略会影响检索效果”文档 C 写“Qdrant 支持余弦相似度和点积相似度”。这样测试检索时很容易判断到底是不是语义匹配。然后写一个测试入口#[tokio::main] async fn main() - Result() { let docs load_documents(./docs)?; let mut all_chunks Vec::new(); for doc in docs { let chunks chunk_text(doc, 200, 20); all_chunks.extend(chunks); } let client reqwest::Client::new(); let embeddings get_embeddings( client, http://127.0.0.1:8080, not-needed, qwen2.5-7b-instruct, all_chunks, ).await?; upsert_chunks(rust_rag_test, all_chunks, embeddings).await?; // 查询测试 let query 什么是 RAG?; let query_embeddings get_embeddings( client, http://127.0.0.1:8080, not-needed, qwen2.5-7b-instruct, [query.to_string()], ).await?; let query_vector query_embeddings[0].1.clone(); let contexts search(rust_rag_test, query_vector, 3).await?; println!(检索到的片段:); for (i, c) in contexts.iter().enumerate() { println!([{i}] {c}); } let answer generate_answer( client, http://127.0.0.1:8080, not-needed, qwen2.5-7b-instruct, query, contexts, ).await?; println!(回答: {answer}); Ok(()) }6.3 判断标准与排查方向现象判断检索到的片段与问题明显相关链路正常切块合理检索结果与问题无关检查 embedding 模型是否合适、切块是否太碎生成回答偏离资料调低 temperature加强 system prompt 约束全部不在资料内很可能模型没有遵守指令需要调整提示词答案明显包含资料内容RAG 生效答案编造了资料外的事实需要明确要求模型在无信息时拒绝回答7. 接口 API 与批量任务7.1 用 Actix Web 暴露问答 APIRust 生态中actix-web是成熟的 Web 框架。把 RAG 主流程包装成一个 HTTP 服务可以让前端工具或其他服务调用。[dependencies] actix-web 4use actix_web::{web, App, HttpServer, HttpResponse, post}; use serde::{Deserialize, Serialize}; use std::sync::Arc; #[derive(Debug, Deserialize)] struct QueryRequest { question: String, top_k: Optionu64, } #[derive(Debug, Serialize)] struct QueryResponse { answer: String, contexts: VecString, } #[post(/api/rag/query)] async fn query_rag( req: web::JsonQueryRequest, state: web::DataArcAppState, ) - HttpResponse { let top_k req.top_k.unwrap_or(3); let query_embeddings get_embeddings( state.client, state.embedding_base, state.embedding_key, state.embedding_model, [req.question.clone()], ).await; let query_embeddings match query_embeddings { Ok(v) v, Err(e) return HttpResponse::InternalServerError().json(format!(embedding error: {e})), }; let contexts search(state.collection_name, query_embeddings[0].1.clone(), top_k).await; let contexts match contexts { Ok(v) v, Err(e) return HttpResponse::InternalServerError().json(format!(search error: {e})), }; let answer generate_answer( state.client, state.llm_base, state.llm_key, state.llm_model, req.question, contexts, ).await; let answer match answer { Ok(v) v, Err(e) return HttpResponse::InternalServerError().json(format!(generate error: {e})), }; HttpResponse::Ok().json(QueryResponse { answer, contexts }) } struct AppState { client: reqwest::Client, embedding_base: String, embedding_key: String, embedding_model: String, llm_base: String, llm_key: String, llm_model: String, collection_name: String, } #[actix_web::main] async fn main() - std::io::Result() { let state Arc::new(AppState { client: reqwest::Client::new(), embedding_base: http://127.0.0.1:8080.into(), embedding_key: not-needed.into(), embedding_model: qwen2.5-7b-instruct.into(), llm_base: http://127.0.0.1:8080.into(), llm_key: not-needed.into(), llm_model: qwen2.5-7b-instruct.into(), collection_name: rust_rag_test.into(), }); HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) .service(query_rag) }) .bind(127.0.0.1:8081)? .run() .await }这里只是一个最小示例。生产环境还需要加上请求校验、错误日志、超时控制、并发限制和鉴权。7.2 curl 调用示例curl -X POST http://127.0.0.1:8081/api/rag/query \ -H Content-Type: application/json \ -d {question: 什么是 RAG?, top_k: 3}返回格式类似{ answer: RAG 是检索增强生成先检索相关文档片段再让模型基于片段生成回答。, contexts: [ RAG 通过检索外部知识来增强模型生成能力..., 文本切块策略包括固定长度、段落切块和滑动窗口..., 向量数据库负责存储和检索文本向量... ] }7.3 批量任务设计实际项目中文档入库很少是单条执行。常见做法是设计一个目录监听或任务列表pending状态等待处理的文档列表processing状态正在执行切块和向量化done状态写入向量库完成failed状态处理失败记录错误原因。批量处理的核心原则失败重试要可观测不能出现静默失败。建议至少做三件事为每个文档生成唯一 ID利用数据库记录处理状态每处理一个文档就写入日志记录文档名、切块数、向量化耗时和入库耗时失败任务自动重试 2 到 3 次仍然失败则标记并通知。Rust 的tokio适合做这类并发任务。可以用mpsc通道把文档分发到多个 workeruse tokio::sync::mpsc; let (tx, mut rx) mpsc::channel::String(100); for _ in 0..4 { let mut rx rx.clone(); tokio::spawn(async move { while let Some(path) rx.recv().await { // 处理单个文档 println!(processing {path}); } }); }注意mpsc::channel需要为每个 workerclone一份 receiver发送端保持一个tx即可。8. 资源占用与性能观察8.1 性能观察指标Rust 实现的 RAG 服务需要重点观察几个指标指标观察方式影响因素文档切块耗时日志打点文档大小、切块算法复杂度Embedding 耗时日志打点文本长度、并发请求数、模型服务性能向量检索耗时Qdrant 监控collection 大小、向量维度、索引类型LLM 生成耗时日志打点模型大小、输出长度、GPU 推理负载内存占用top/htop加载的文档数量、Qdrant 缓存显存占用nvidia-smiLLM 模型和 embedding 模型是否本机推理如果是本地 llama.cpp 部署nvidia-smi能看到模型加载后的显存占用。具体数字取决于模型量化版本、上下文长度和 batch size。qwen2-7b 的 4-bit 量化模型通常在消费级显卡上可以运行但实际占用需要以本机测试为准。这里不写死具体数字以免误导。8.2 降低资源占用的通用手段使用量化模型Q4_K_M、Q5_K_M 等量化格式可以显著降低显存占用控制上下文长度不要盲目拉长-c参数上下文越长KV cache 占用越大批量向量化embedding 服务支持批量输入一次请求传多段文本减少 HTTP 开销限制检索 top_ktop_k 过大不仅增加 token 成本也会让回答受无关片段干扰切块大小收敛过大的切块会导致向量化成本升高过小则语义不完整建议从 200 到 500 字符起步测试关闭不需要的日志生产环境使用结构化日志避免println!高频输出。8.3 性能测试思路第一次跑通后可以进行两轮压测第一轮文档入库性能测试。准备 100 份 Markdown 文档记录从加载到入库的总耗时观察向量化是否成为瓶颈。如果是提高 embedding 服务的并发能力或者减小单次请求的文本量。第二轮问答延迟测试。用相同的 50 个问题连续请求 API统计 P50、P95 延迟。如果检索耗时占比不高说明瓶颈在 LLM 生成如果检索耗时异常高检查 Qdrant 集合是否需要重建索引。9. 常见问题与排查方法问题现象可能原因排查方式解决方案Rust 依赖安装失败网络不稳定或镜像源缺失检查 cargo 日志设置RUSTUP_DIST_SERVER、更换 crates 镜像编译报错linker not foundWindows 缺少 MSVC 或 GNU 工具链rustc --print sysroot检查 toolchain安装对应工具链或切换 GNUQdrant 连接失败服务未启动或端口错误curl http://127.0.0.1:6333/collections确认 Docker 容器状态创建集合报 already exists集合已存在Qdrant dashboard 查看集合列表先检查再创建或删除旧集合Embedding 返回 404服务未实现/v1/embeddings查看服务文档和日志换用支持 embedding 接口的服务版本检索结果为空向量未写入或集合名错误调用 collection 点数量接口确认 upsert 成功检查集合名检索结果不相关切块策略不合理或 embedding 模型不合适打印中间检索片段调整切块大小、重叠、top_kLLM 回答与参考资料无关提示词约束不够或 temperature 过高先人工查看 prompt加强 system prompt降低 temperatureAPI 请求超时LLM 生成时间过长设置客户端超时给 HTTPServer 加 timeout或使用流式输出批量任务卡住并发 worker 数量不足或死锁检查任务日志增加 worker 数检查 channel 是否关闭显存不足模型过大或 batch 过大nvidia-smi查看占用换量化模型减小 batch缩短上下文端口冲突上次服务未退出lsof -i :8081或 Windows 下netstat杀掉残留进程或换端口10. 最佳实践与使用建议10.1 工程化建议先小参数验证第一次跑通时用 3 到 5 份文档、top_k 设为 2不要一上来就灌入大量数据。链路通了再逐步扩大规模。保留最小可运行配置把 LLM 服务地址、embedding 模型、向量库地址都做成环境变量配置不用改代码。目录规范建议把docs原始文档、chunks切块缓存、vectors向量备份、logs运行日志分目录管理。批量任务加日志与重试任何入库任务都必须有可观测的失败记录否则排错成本很高。接口服务限制访问范围API 默认绑定127.0.0.1不要直接绑0.0.0.0需要对外服务时用反向代理加鉴权。涉及人脸、声音、版权素材时必须确认授权RAG 可以检索图片、音频、视频元数据但不要对未授权内容做向量化和对外提供生成结果。发布前做效果复核定期抽检问答效果观察是否有检索偏移、答案重复或幻觉加重的情况。10.2 Agent 集成建议RAG 作为 AI Agent 的能力模块有两种集成方式方式说明工具调用Agent 在任务中调用 RAG 查询工具拿到结果后继续处理记忆增强Agent 的历史对话摘要和关键事实写入向量库后续会话按需检索建议先做工具调用因为实现更简单调试也更直观。Agent 调用 RAG 工具时要传清楚查询意图而不是原文拼接。比如用户说“帮我看看之前那个上线文档里关于端口配置的部分”Agent 拆解成“端口配置 上线文档”去检索效果会好很多。10.3 从基础 RAG 到 agentic rag基础 RAG 的问题在于“一次检索、一次生成”如果首轮检索不理想答案质量会直接下降。agentic rag 的思路是让 Agent 自己决定检索策略先做一次粗检索判断结果是否足够回答问题不够就改写查询词、换一个角度再检索结合多轮结果生成最终答案。Rust 做 agentic rag 时可以在代码里维护一个查询改写循环把“当前问题、已检索片段、缺失信息判断”作为 prompt 输入给 LLM让 LLM 输出是否继续检索以及新的查询词。这个模式对并发和资源控制要求高一点但 Rust 的tokio生态完全能支撑。11. 总结与下一步这次内容从 RAG 的技术链路出发完整梳理了 Rust 环境下实现知识库问答的步骤文档切块、embedding 调用、Qdrant 向量入库、相似度检索、prompt 构造、LLM 生成、HTTP API 暴露和批量任务设计。最值得先尝试的点是把最小链路跑通确认“文档 - 向量 - 检索 - 回答”这条主流程没有断点。可以先不用本地模型直接调外部 embedding 和 LLM API等逻辑验证没问题之后再切换到本地推理服务。最容易踩的坑主要在三个地方切块策略不合理导致检索不相关问题、embedding 模型与检索任务不匹配、向量库集合创建逻辑重复执行报错。下一步可以做的方向包括为 RAG 服务增加流式输出、接入 pgvector 做统一存储、把文档更新做成增量同步、在 Agent 中实现查询改写和多轮检索也就是往 agentic rag 方向演进。RAG 的最终效果取决于三个因素文档质量、切块策略、检索准确性而 Rust 在整个过程中提供的是可控的性能表现和稳定的服务能力。建议把这份示例代码保存下来后面做 Agent 知识库、私有问答系统或本地文档助手时可以直接作为基线。
返回列表