ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG基础实战:从零搭建AI Agent知识获取管道

RAG基础实战:从零搭建AI Agent知识获取管道 1. 项目概述为什么“知识获取管道”是AI Agent落地的生死线你有没有遇到过这样的情况花两周时间搭好一个AI Agent逻辑清晰、工具调用流畅结果一上线用户问个“我们上季度华东区退货率是多少”它张口就来个“我无法访问数据库”或者问“新员工入职流程第三步要交什么材料”它翻遍提示词也答不出——不是模型不行是它根本不知道该去哪找答案。这背后暴露的正是当前绝大多数AI Agent项目最常被忽视的底层命门知识获取管道没打通。而RAGRetrieval-Augmented Generation就是目前工程实践中最成熟、最可控、最易落地的知识接入方案。它不依赖模型本身记住所有细节而是像给Agent配了个随身图书馆管理员用户一提问管理员立刻从企业文档、产品手册、历史工单、内部Wiki里精准翻出相关页再把原文片段和问题一起递给大模型做理解与生成。这不是锦上添花的功能模块而是决定Agent能否走出Demo、真正进业务系统的分水岭。本文聚焦“RAG基础”不讲抽象概念不堆论文公式只拆解一个真实从业者从零搭建知识获取管道时必须踩过的每一块砖为什么选RAG而不是微调向量库选FAISS还是ChromaEmbedding模型怎么选才不翻车Chunk切分到底按字数还是按语义检索结果怎么过滤才不漏关键信息这些细节直接决定了你的Agent是能准确回答“合同模板第5.2条怎么写”还是只会说“请查阅法务部共享文件夹”。适合刚接触AI Agent开发的工程师、想把现有业务系统接入LLM的产品经理以及正在准备AI方向技术面试的开发者——因为所有面试官问“RAG和微调的区别”本质上是在考你是否理解知识接入的工程权衡。2. 核心设计思路RAG不是技术拼图而是知识流的精密调度系统2.1 为什么RAG是当前阶段最务实的选择很多人一上来就想微调模型觉得“让模型自己学会业务知识”更彻底。但实操中你会发现微调成本高得离谱一个中等规模的企业知识库比如500份PDF、2000条FAQ、3万行代码注释想靠LoRA微调让模型真正掌握细节至少需要2张A100显卡跑3天且微调后模型会“遗忘”通用能力回答“地球到月球距离”这种基础问题都可能出错。而RAG的思路截然不同它把“记忆”和“推理”彻底解耦。模型只负责理解问题、整合信息、生成语言知识存储、检索、过滤全部交给独立模块。这带来三个硬性优势第一知识更新零延迟——今天法务部更新了合同模板你只需重新索引那一页PDF明天Agent就能引用最新条款不用重训模型第二可解释性强——当Agent回答“根据《2024版供应商管理规范》第3.1条”你能立刻查到它引用的原始段落审计、纠错、溯源全部可操作第三硬件成本可控——主流向量库如FAISS、Chroma在单台16G内存的服务器上就能支撑百万级文档检索比动辄需要8卡A100的微调方案友好太多。我去年帮一家制造企业做设备故障诊断Agent他们原有知识库是20年积累的维修手册扫描件Excel故障代码表尝试微调Qwen-7B失败3次后转向RAG最终用一台旧Mac Mini32G内存跑通全流程响应时间稳定在1.2秒内。这不是理论推演是血泪教训换来的选择。2.2 RAG管道的四大核心环节及其不可替代性一个健壮的RAG管道绝不是“加载文档→扔给向量库→召回→喂给LLM”这么简单。它由四个环环相扣的环节组成缺一不可每个环节的失误都会导致下游雪崩知识预处理Ingestion这是整个管道的地基。你不能直接把PDF丢给向量库。PDF里的页眉页脚、扫描件的OCR噪点、表格跨页断裂、代码块中的缩进空格——这些都会污染向量表示。真正的预处理要分三步走先用PyMuPDF或pdfplumber做结构化解析保留标题层级和段落边界再用正则清洗掉页码、水印、重复页眉最后对技术文档这类强结构内容要识别代码块、表格、公式并单独标记。我见过最惨的案例是一家金融科技公司把带大量数字表格的监管文件直接切块结果向量库把“2023年净利润1.2亿”和“2024年预算1.5亿”当成相似语义召回Agent回答“今年利润目标是1.5亿”差点引发合规事故。向量化与索引Embedding Indexing这里的关键不是“用哪个模型”而是“用哪个模型解决什么问题”。开源Embedding模型如bge-m3、text2vec-large-chinese在中文长文本上表现稳定但如果你的知识库含大量专业术语比如PLC编程指令、半导体工艺参数通用模型会把“MOV指令”和“MOVE指令”向量距离拉得很远。这时必须微调Embedding模型——不是微调LLM而是用企业术语对训练一个轻量级Adapter。索引策略同样重要FAISS适合单机高性能场景但它的HNSW索引在数据量超50万后重建耗时剧增Chroma支持动态增删但默认的HNSW参数在中文短句检索时hit rate命中率只有68%。我们实测发现将Chroma的ef_construction从64调到200m从32调到64配合bge-m3的query_instruction_for_retrieval参数能将金融合同类检索的hit rate从68%提升到92.3%。检索与重排序Retrieval Re-ranking初学者常犯的错误是“召回越多越好”。实际上LLM上下文窗口有限GPT-4-turbo约128K但实际业务中为控制成本多设为8K塞入20个无关段落反而稀释关键信息。我们的标准流程是先用向量检索召回Top 50再用Cross-Encoder如bge-reranker-large做精排只保留Top 5。这里有个反直觉技巧重排序模型的输入不是“问题段落”而是“问题段落摘要”。比如原始段落是“根据《安全生产法》第38条生产经营单位应当对安全设备进行经常性维护、保养并定期检测保证正常运转”摘要生成“安全设备需定期维护检测”重排序模型对摘要的理解更稳定避免长文本噪声干扰。实测显示加摘要层后法律条文类查询的准确率提升27%。生成增强Generation Augmentation这是最容易被忽略的“最后一公里”。很多团队把召回的Top 5段落原样拼接喂给LLM结果模型在冗余信息中迷失。我们必须做三件事第一强制要求LLM只基于提供的上下文作答用system prompt明确约束“你只能依据以下【参考资料】回答问题禁止编造、禁止使用外部知识”第二对召回段落做来源标注比如“【来源2024版采购流程V3.2_第4章】……”这样LLM生成时会自然带上引用依据第三设置置信度阈值——当LLM生成答案中出现“可能”、“大概”、“据我所知”等模糊表述时自动触发fallback机制返回“未找到确切依据请联系XX部门确认”。这个机制在医疗、法律等强合规场景中是规避责任风险的底线。2.3 RAG与Agent架构的深度耦合逻辑RAG不是Agent的附属插件而是其认知架构的核心组件。在典型的ReActReasoning ActingAgent框架中RAG承担着“Act”环节中最关键的“知识调用”动作。当Agent执行到retrieve_knowledge(query)这一步时它调用的不是一个静态API而是一个具备状态感知的管道如果用户连续追问“那这个流程的审批人是谁”RAG管道必须能识别上下文关联自动将前序问题“新员工入职流程第三步”作为元信息注入本次检索避免召回无关的“财务审批人名单”。这要求RAG模块支持Session-aware检索——我们在Agentscope 2.0中实现的方式是将对话历史的摘要向量与当前问题向量做加权融合历史权重0.3当前问题权重0.7再投入向量库检索。另一个关键耦合点是工具调用Tool Calling。当Agent判断需要查知识库时它发出的不是原始问题而是经过意图解析后的结构化查询。比如用户问“上个月深圳仓的发货延迟率”Agent先调用SQL工具查出延迟订单ID列表再将“订单ID: [1001,1002]”作为关键词驱动RAG去检索《物流异常处理SOP》中对应章节。这种“LLM决策→工具执行→RAG补充”的闭环才是Agentic RAG的真谛而非简单地把RAG塞进Agent的prompt里。3. 核心细节解析从文档到向量每一步都是经验之谈3.1 知识源的类型适配与预处理实战不同知识源的处理方式天差地别没有一套通用方案。以下是我们在12个行业项目中沉淀的实操清单PDF文档占比65%扫描件PDF必须先用PaddleOCR做高精度识别重点校验数字、字母、符号的识别准确率我们用自建的1000条测试集验证OCR错误率3%的页面手动修正原生PDF用pdfplumber解析但要禁用extract_words()改用extract_text(x_tolerance1, y_tolerance1)否则表格文字会错位技术手册类PDF启用layoutTrue参数保留标题层级后续切块时按h1→h2→p三级嵌套切分确保“原理说明”和“操作步骤”不混在一起。网页/HTML占比15%用BeautifulSoup提取正文时务必移除script、style、nav标签但保留table和code对于动态渲染的SPA网站如Vue前端必须用Playwright启动真实浏览器抓取否则div idcontent里是空的关键技巧提取meta namedescription和h1作为该页面的“元描述”与正文向量化时拼接大幅提升品牌词、产品名的检索权重。数据库/ERP导出占比12%不要直接导出CSV喂给RAG。先用SQL生成结构化描述“表名t_order字段order_id订单号、status状态、delay_days延迟天数”再将字段说明、业务规则如“status3表示已发货”作为知识条目索引对敏感字段如客户手机号在预处理阶段做脱敏标记“【脱敏字段customer_phone】”避免LLM在生成中意外泄露。会议纪要/IM聊天记录占比8%这类非结构化文本最难处理。我们采用“发言人时间戳语义块”三元组切分先用正则r(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\s(.*?):识别发言单元再对每段发言用TextRank提取关键词仅保留含3个以上业务关键词如“交付”、“验收”、“UAT”的语义块绝对禁止将整场2小时会议记录切成500字块——信息密度太低向量表示失效。提示所有预处理脚本必须输出日志文件记录每份文档的原始大小、解析后文本长度、有效字符率非空格/换行符占比。我们曾发现某批采购合同PDF解析后有效字符率仅41%追查发现是Adobe Acrobat导出时启用了“压缩图像”选项导致OCR失败。没有日志这种问题永远定位不到。3.2 Chunk切分不是越小越好而是要匹配业务语义粒度Chunk切分是RAG效果的隐形杀手。新手常按固定字数如512字符切分结果一段完整的故障处理步骤被硬生生劈成两半前半段说“第一步断电”后半段说“第二步更换保险丝”向量库召回前半段时LLM根本无法生成完整操作。正确的切分必须遵循“语义完整性”原则技术文档/操作手册按“任务”切分。识别动词开头的句子“打开XXX”、“点击YYY”、“检查ZZZ”将同一任务下的所有步骤合并为一个Chunk。我们用spaCy训练了一个轻量级任务识别模型F1值达92.7%切分后任务完整率从58%提升至96%。政策法规/合同条款按“条款”切分。利用正则r第[零一二三四五六七八九十百千\d]条定位条款起始结合r(?:\n\s*第[零一二三四五六七八九十百千\d]条|\n\s*【.*?】)识别条款结束。特别注意“但书条款”如“……但下列情形除外”必须将其与主条款合并否则检索“例外情形”时会漏掉主条款约束。FAQ/知识库问答按“Q-A对”切分。但要注意很多企业FAQ的“答案”部分包含多个子点如“1. 准备材料2. 提交申请3. 等待审核”需用ol或ul标签包裹预处理时转为Markdown列表确保向量模型理解层级关系。代码/配置文件按“函数”或“配置块”切分。对Python用ast.parse解析AST树提取FunctionDef节点对YAML用PyYAML加载后按一级key切分如database:、cache:并在Chunk开头标注【代码块database_config】。注意所有Chunk必须添加唯一ID和元数据。ID格式为{source_type}_{source_id}_{chunk_index}如pdf_contract_2024001_3元数据至少包含source_url、update_time、author。这是后续审计、更新、权限控制的基础绝不能省略。3.3 Embedding模型选型开源模型的实战调优指南选Embedding模型不是看排行榜而是看它在你的数据上是否“懂行”。我们实测了7个主流中文Embedding模型在制造业知识库上的表现测试集300条设备故障查询对应标准答案模型平均Hit5长文本稳定性专业术语识别单次推理耗时A10Gtext2vec-base-chinese72.1%差1000字时下降35%弱“PLC”与“PLC程序”向量距离0.8212msbge-m389.6%优2000字仅降5%中“MOV指令”与“MOVE指令”距离0.4128msm3e-base85.3%中强“光刻机”与“EUV光刻机”距离0.2318msbge-reranker-base———45ms仅用于重排结论很清晰bge-m3是综合最优解但有两个致命陷阱必须避开Query与Passage的编码差异bge-m3官方要求对查询query和文档passage使用不同的instruction前缀。很多开发者直接用model.encode(text)导致检索失准。正确用法是# 查询编码必须加instruction query_emb model.encode( f为这个句子生成表示以用于检索相关文章{query}, convert_to_tensorTrue, normalize_embeddingsTrue ) # 文档编码不加instruction passage_emb model.encode( passage_text, convert_to_tensorTrue, normalize_embeddingsTrue )中文标点与空格的向量污染bge-m3对全角标点。和中文空格 敏感。预处理时必须统一替换为半角标点并删除中文空格。我们用正则re.sub(r[。“”‘’【】《》、\u3000], lambda m: {:,,。:.,:!,:?}[m.group(0)], text)处理使同义词向量距离标准差降低63%。实操心得不要迷信“更大更好”。我们曾用bge-large-chinese4.2GB替换bge-m31.2GB在相同硬件上吞吐量下降40%但Hit5仅提升0.8个百分点。工程上速度、内存、效果的三角平衡点往往在中等规模模型上。4. 实操全流程从零搭建一个可运行的RAG管道4.1 环境准备与依赖安装我们采用最小可行环境MVE原则避免过度依赖复杂框架。核心依赖仅5个全部pip install可得# Python 3.10 环境 pip install torch2.1.2cu118 torchvision0.16.2cu118 --extra-index-url https://download.pytorch.org/whl/cu118 pip install transformers4.38.2 sentence-transformers2.2.2 chromadb0.4.24 langchain0.1.16 # 可选加速PDF解析 pip install PyMuPDF1.23.24 pdfplumber0.10.2注意ChromaDB 0.4.24是最后一个支持SQLite后端的版本适合单机开发若需分布式升级到0.5需切换到PostgreSQL。LangChain 0.1.16是最后一个兼容原生Chroma API的版本0.2改为异步接口会增加调试复杂度。生产环境宁可牺牲新特性也要保证链路稳定。4.2 知识库构建以《设备维修手册》为例假设我们有一份manual.pdf共128页含目录、章节、表格、代码块。构建流程如下步骤1结构化解析import fitz # PyMuPDF doc fitz.open(manual.pdf) all_text for page in doc: # 提取文本时保留位置信息便于后续识别标题 blocks page.get_text(blocks) for b in sorted(blocks, keylambda x: x[1]): # 按y坐标排序 if b[4].strip() and len(b[4].strip()) 10: # 过滤短文本和空块 all_text b[4].strip() \n # 输出中间文件 manual_parsed.txt供人工抽检 with open(manual_parsed.txt, w, encodingutf-8) as f: f.write(all_text)步骤2语义切分from langchain.text_splitter import RecursiveCharacterTextSplitter # 针对技术手册优化的切分器 splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , , , ], keep_separatorTrue ) # 但关键一步先按标题切分 import re sections re.split(r(第[零一二三四五六七八九十\d]章\s.*), all_text) chunks [] for sec in sections: if re.match(r第[零一二三四五六七八九十\d]章\s, sec): # 章节标题单独成块 chunks.append(sec.strip()) else: # 内容按语义切分 sub_chunks splitter.split_text(sec.strip()) chunks.extend(sub_chunks)步骤3向量化与入库from sentence_transformers import SentenceTransformer from chromadb import Client import chromadb.utils.embedding_functions as embedding_functions # 加载bge-m3模型需提前下载到本地 model SentenceTransformer(/path/to/bge-m3) ef embedding_functions.SentenceTransformerEmbeddingFunction( model_name/path/to/bge-m3, devicecuda ) client Client() collection client.create_collection( namedevice_manual, embedding_functionef, metadata{hnsw:space: cosine} # 余弦相似度 ) # 批量插入避免逐条insert性能差 documents [] metadatas [] ids [] for i, chunk in enumerate(chunks): documents.append(chunk) metadatas.append({ source: manual.pdf, page: i // 10 1, # 粗略页码 chunk_id: fmanual_{i} }) ids.append(fchunk_{i}) collection.add( documentsdocuments, metadatasmetadatas, idsids )4.3 检索增强生成端到端调用示例现在我们用一个真实查询测试管道def rag_query(query: str): # Step 1: 向量检索 results collection.query( query_texts[f为这个句子生成表示以用于检索相关文章{query}], n_results5, include[documents, metadatas, distances] ) # Step 2: 构建上下文带来源标注 context_parts [] for i, (doc, meta) in enumerate(zip(results[documents][0], results[metadatas][0])): source meta.get(source, 未知) page meta.get(page, ?) context_parts.append(f【来源{source}_第{page}页】{doc}) context \n\n.join(context_parts) # Step 3: 调用LLM生成以Ollama本地模型为例 import requests response requests.post( http://localhost:11434/api/chat, json{ model: qwen2:7b, messages: [ { role: system, content: 你是一个专业的设备维修顾问。请严格依据【参考资料】回答问题禁止编造、禁止使用外部知识。回答必须简洁直接给出操作步骤。 }, { role: user, content: f问题变频器报F001故障代码如何处理\n\n【参考资料】\n{context} } ], stream: False } ) return response.json()[message][content] # 执行查询 answer rag_query(变频器报F001故障代码如何处理) print(answer) # 输出示例【来源manual.pdf_第45页】F001表示过电流故障。处理步骤1. 检查电机电缆是否短路2. 检查负载是否过重3. 重启变频器。关键细节query_texts中必须包含instruction前缀system prompt中必须有“严格依据【参考资料】”的强约束且上下文用【来源...】明确标注。这三处是保证答案可追溯、可审计的铁律。4.4 性能调优让RAG从“能用”到“好用”上线后我们发现平均响应时间3.2秒用户抱怨“比查Excel还慢”。通过cProfile分析瓶颈在向量检索占时68%。优化方案索引参数调优Chroma默认HNSW参数过于保守。修改chroma_server.ymlchroma_db_impl: duckdbparquet hnsw: ef_construction: 200 # 从64提升提高索引质量 m: 64 # 从32提升增加邻居数 ef: 100 # 检索时扩展因子缓存高频查询对Top 100高频问题如“开机无反应”、“屏幕黑屏”建立LRU缓存命中直接返回绕过向量检索。缓存键用md5(query model_name)生成避免不同模型混用。异步预检索在用户输入问题时前端就触发collection.query等用户按下回车时检索结果已就绪。我们用WebSocket实现首字响应时间从3.2秒降至0.8秒。混合检索兜底当向量检索Hit5 0.7时自动触发关键词检索BM25用whoosh库实现召回结果与向量结果加权融合。这招在处理缩写词如“PLC”查“可编程逻辑控制器”时准确率提升41%。5. 常见问题与排查技巧实录那些没人告诉你的坑5.1 Hit Rate低不是模型问题是数据在“说谎”现象collection.query(n_results5)返回的5个结果中只有1个相关Hit520%。排查路径先人工抽检随机选10个查询用collection.peek()看原始Chunk内容。我们曾发现某批合同PDF解析后所有“甲方”、“乙方”被OCR识别为“甲万”、“乙万”向量库当然找不到。检查Embedding编码用model.encode(甲方)和model.encode(甲方)计算余弦相似度应为1.0。如果不是说明模型加载异常或文本预处理污染。验证检索逻辑用collection.query(query_texts[甲方], n_results10)看是否召回含“甲方”的Chunk。如果没召回问题在索引如果召回了但排序靠后问题在向量表示。独家技巧用t-SNE可视化向量空间。取100个典型查询和对应Chunk降维后画散点图。如果“设备故障类”查询和“采购流程类”Chunk混在一起说明Embedding模型未学好领域区分必须微调。5.2 LLM胡说八道约束失效的三大原因现象LLM回答“根据《维修手册》第5.2条需更换主板”但手册中根本没有第5.2条。根因分析Prompt约束力不足system prompt中“禁止编造”力度不够。必须改用“你只能依据以下【参考资料】回答问题。如果【参考资料】中未提及必须回答‘未找到依据’。”上下文污染召回的Chunk中混入了其他文档的无关段落。解决方案在collection.query后用reranker.score(query, chunk)对每个结果打分剔除score0.3的低质结果。LLM幻觉惯性某些模型如早期Qwen对“根据XX”句式有强生成偏好。对策在messages中将参考资料放在user消息末尾并加粗【参考资料】字样视觉强化约束。5.3 中文检索不准标点、空格、繁简体的隐形陷阱现象搜“PLC编程”召回“PLC程序设计”搜“光刻机”召回“刻蚀机”。解决方案标准化预处理建立企业术语映射表{PLC编程: PLC程序设计, 光刻机: EUV光刻机}查询时自动扩展同义词。多粒度检索对查询词同时生成ngramPLC、PLC编、PLC编程、jieba分词PLC/编程、同义词扩展PLC/可编程逻辑控制器三组向量取并集。繁简体统一用opencc库将所有文本转为简体查询时也强制转简体。实操心得在collection.add()前对所有documents执行opencc.convert(text, configs2t.json)看似多一步却避免90%的繁简体检索失败。5.4 生产环境稳定性监控与熔断的必备清单RAG管道上线后必须部署四层监控数据层监控collection.count()每日增量突降50%说明PDF解析失败检索层记录每次query的distances数组min(distances) 0.8时告警说明检索完全失效生成层统计LLM回复中“未找到依据”、“请查阅XX”等fallback话术的占比30%说明知识库覆盖不足业务层埋点用户点击“答案有用/无用”按钮用chi-square检验不同查询类型的满意度差异。熔断策略当连续3次min(distances) 0.8自动切换至关键词检索Whoosh当fallback占比40%触发知识库覆盖率分析脚本输出缺失主题报告。最后分享一个小技巧在collection.add()后立即执行collection.get(limit1)验证数据是否真正写入。我们曾因Chroma的SQLite WAL模式未关闭导致add后get查不到浪费两天排查时间。所有写操作后必须有读操作验证。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进