
1. 为什么知识获取管道是 AI Agent 落地的第一道坎做 AI Agent 开发的人绕不开一个尴尬的现实模型本身很聪明但它对你私有的、垂直的、每天都在变的知识一无所知。你问它公司内部的报销流程它只能编你问它某个产品的接口参数它给你的答案可能停留在两年前的训练数据里。这不是模型不行而是它的知识边界被训练截止日期和公开语料锁死了。解决这个问题的核心手段就是RAGRetrieval-Augmented Generation检索增强生成。直白点说RAG 就是给模型外挂一个知识获取管道用户提问时系统先去你的知识库里捞出最相关的资料片段再把问题 资料一起喂给模型让它基于真实材料作答而不是凭空发挥。我在多个 AI Agent 项目里反复验证过一件事Agent 的能力上限往往不取决于你选了哪个大模型而取决于这条知识获取管道修得好不好。管道漏了、堵了、或者捞上来的全是垃圾再强的模型也救不回来。这也是为什么我把 RAG 放在走进 AI Agent系列的第四篇——它是从能聊天的玩具走向能干活的生产力工具的分水岭。这篇内容适合三类人正在从 0 到 1 搭建 AI Agent 的开发者、想给自己的应用加知识库能力的工程师、以及被 RAG 检索命中率折磨过的同行。我会用 TypeScript 作为主要实现语言生态成熟、类型安全、和前端/Node 服务天然贴合把 RAG 的基础管道从原理到代码完整拆一遍重点讲清楚那些文档里不会写、但实际会踩的坑。提示RAG 不是向量数据库 大模型这么简单。真正的难点在于切分策略、检索质量、上下文组装这三个环节本文会把它们逐个拆开。2. RAG 管道的四个环节与数据流转真相2.1 一条完整的知识获取链路长什么样很多人对 RAG 的理解停留在把文档塞进向量库然后搜出来给模型。这个理解不算错但太粗。一条能上生产的 RAG 管道实际包含四个阶段每个阶段都有独立的失败点。第一阶段是离线索引Indexing把原始文档PDF、Markdown、网页、数据库记录读进来清洗掉噪声切成合适大小的片段chunk再用嵌入模型Embedding Model把每个片段转成向量存进向量数据库。这一步是备菜菜没备好后面全白搭。第二阶段是查询理解Query Understanding用户输入一个问题系统需要判断这个问题该怎么检索。是直接拿原句去搜还是先做改写、扩展、拆解这一步决定了你能不能问对问题。第三阶段是检索Retrieval用查询向量去向量库里找最相似的 Top-K 片段。这里涉及相似度算法、过滤条件、重排序Rerank等细节。第四阶段是生成Generation把检索到的片段和用户问题拼成一个 Prompt交给大模型生成答案。Prompt 怎么拼、放多少片段、怎么处理冲突信息直接决定最终回答质量。我用一个表格把四个阶段的核心任务和常见失败点列出来方便你对照排查阶段核心任务常见失败点离线索引清洗、切分、向量化、入库切分太碎丢上下文、切分太大噪声多查询理解改写、扩展、意图识别原句检索命中率低、多意图问题漏检检索相似度匹配、过滤、重排Top-K 太小漏信息、太大引入噪声生成组装 Prompt、调用模型上下文超长被截断、指令冲突2.2 为什么切分是整条管道最容易被低估的环节我见过太多项目向量库选得很高级模型用的是顶配但检索效果一塌糊涂。追根溯源问题往往出在切分上。切分的本质矛盾是片段太小语义不完整片段太大噪声稀释相关性。假设你把一份 50 页的产品手册按每 200 字硬切结果就是点击设置按钮和进入高级选项被切到两个片段里用户问怎么进高级设置两个片段单独看都不完整检索出来模型也拼不出完整答案。反过来如果你按每 2000 字切一个片段里塞了五六个不相关的主题向量表示会被平均掉检索时反而不容易命中真正相关的那一小段。我的经验是切分要顺着文档的语义结构走而不是顺着字数走。Markdown 按标题层级切代码按函数/类切PDF 按章节切聊天记录按对话轮次切。切完之后再对超长片段做二次切分并保留一定的重叠overlap通常是片段长度的 10%~20%用来防止关键信息正好卡在边界上被切断。2.3 向量检索到底在算什么向量检索的核心是余弦相似度或点积。嵌入模型把文本映射到一个高维空间常见 768 维、1024 维、1536 维语义相近的文本在这个空间里距离更近。检索时把查询也转成向量然后找空间里离它最近的 K 个片段。这里有个反直觉的点向量相似度高不代表内容真的有用。比如用户问如何退款一个片段讲的是退款政策的历史沿革另一个片段讲的是退款操作步骤。前者可能因为大量出现退款这个词而相似度很高但对用户毫无帮助。这就是为什么生产级 RAG 一定要加重排序Rerank先用向量召回一批候选比如 20 个再用一个更精细的交叉编码器模型对这 20 个重新打分挑出真正相关的 3~5 个。2.4 用 TypeScript 搭一个最小可跑的管道骨架理论讲完直接上代码。下面是一个最小可跑的 RAG 管道骨架用 TypeScript 写把四个阶段串起来。我刻意把每个环节拆成独立函数方便你替换实现。// rag-pipeline.ts interface Chunk { id: string; content: string; metadata: Recordstring, unknown; embedding?: number[]; } interface RetrievedChunk extends Chunk { score: number; } // 1. 切分按段落切保留重叠 function splitIntoChunks(text: string, maxLen 500, overlap 80): string[] { const paragraphs text.split(/\n\s*\n/).filter(Boolean); const chunks: string[] []; let buffer ; for (const para of paragraphs) { if ((buffer para).length maxLen buffer.length 0) { chunks.push(buffer.trim()); // 保留尾部 overlap 字符作为下一段开头 buffer buffer.slice(-overlap) \n para; } else { buffer (buffer ? \n : ) para; } } if (buffer.trim()) chunks.push(buffer.trim()); return chunks; } // 2. 向量化调用嵌入模型此处用伪接口实际替换为你的模型 SDK async function embed(texts: string[]): Promisenumber[][] { // 实际项目中这里调用 embedding API 或本地模型 return texts.map(() new Array(768).fill(0).map(() Math.random())); } // 3. 检索余弦相似度 Top-K function cosineSimilarity(a: number[], b: number[]): number { let dot 0, normA 0, normB 0; for (let i 0; i a.length; i) { dot a[i] * b[i]; normA a[i] * a[i]; normB b[i] * b[i]; } return dot / (Math.sqrt(normA) * Math.sqrt(normB) 1e-8); } function retrieve(queryVec: number[], chunks: Chunk[], topK 5): RetrievedChunk[] { return chunks .map((c) ({ ...c, score: cosineSimilarity(queryVec, c.embedding!) })) .sort((a, b) b.score - a.score) .slice(0, topK); } // 4. 生成组装 Prompt function buildPrompt(query: string, contexts: RetrievedChunk[]): string { const contextText contexts .map((c, i) [资料${i 1}]\n${c.content}) .join(\n\n); return 你是一个严谨的助手。请仅根据以下资料回答问题资料中没有的信息请明确说明资料未提及。\n\n${contextText}\n\n问题${query}\n\n回答; }这段代码能跑但离生产还有距离。它缺了重排序、缺了查询改写、缺了错误处理。不过作为理解管道结构的起点它足够清晰。接下来几节我会把每个环节的生产级做法补上。3. 文档切分与向量化的实操细节3.1 不同文档类型的切分策略差异切分没有万能公式得看文档类型。我按常见类型整理了一套策略都是实际项目里验证过的Markdown / 结构化文档优先按标题层级切。一级标题下的内容作为一个大块如果超过阈值再按二级标题切。这样每个片段天然带语义边界。切完后在片段头部保留标题路径如产品手册 高级设置 网络配置检索时这段路径本身就是强信号。PDFPDF 是最麻烦的因为它本质是排版格式不是语义格式。我的做法是先用解析库抽出文本和段落结构识别出章节标题通常字号更大、加粗再按章节切。表格要单独处理转成 Markdown 表格或结构化 JSON不要和正文混在一起切。代码文件按函数、类、方法切保留函数签名和注释。代码的检索场景通常是这个功能怎么实现的所以片段里必须包含足够的上下文导入、类型定义。对话记录 / 工单按对话轮次或工单切一个完整的问题-解决过程作为一个片段。这类数据的价值在于问答对切碎了就失去意义。3.2 重叠窗口到底设多少合适重叠overlap的作用是防止关键信息卡在片段边界。设多少我的经验值是片段长度的10%~20%。比如片段 500 字重叠 50~100 字。但重叠不是越多越好。重叠太多会导致同一段内容在多个片段里重复出现检索时可能召回一堆高度相似的片段浪费上下文窗口。而且重叠会增大索引体积和存储成本。有个更聪明的做法按语义边界切分时重叠可以设小甚至不设因为语义边界本身就保证了信息完整。只有在按固定长度硬切时才需要较大的重叠来兜底。3.3 嵌入模型选型不是越贵越好嵌入模型的选择直接影响检索质量。市面上的模型大致分几类通用大模型厂商提供的嵌入接口、开源本地模型、以及针对特定语言优化的模型。选型时我关注三个指标检索命中率、维度、推理成本。维度不是越高越好768 维在很多场景已经够用1536 维会显著增加存储和计算开销。中文场景要特别测试模型对中文语义的捕捉能力有些英文强的模型在中文上表现平平。还有一个容易被忽略的点索引用的嵌入模型和查询用的嵌入模型必须是同一个。我见过有人索引时用 A 模型查询时换了 B 模型结果检索全乱套。因为不同模型的向量空间不通用混用等于在错误的坐标系里找最近邻。3.4 批量向量化的工程细节实际项目里文档量可能上万向量化是个耗时任务。几个工程要点批量调用嵌入接口通常支持一次传多个文本批量调用比逐条快得多但要注意单次请求的 token 上限。并发控制别一次性发几千个请求会被限流。用信号量或队列控制并发数我一般设 5~10。失败重试网络抖动、限流都会导致失败要有指数退避重试。断点续传大批量任务要记录进度失败后能从断点继续而不是从头再来。幂等去重用内容哈希做去重同一份文档重复索引时跳过已处理的片段。// 带并发控制和重试的批量向量化 async function batchEmbed( texts: string[], concurrency 5, maxRetries 3 ): Promisenumber[][] { const results: number[][] new Array(texts.length); let cursor 0; async function worker() { while (cursor texts.length) { const idx cursor; for (let attempt 0; attempt maxRetries; attempt) { try { const [vec] await embed([texts[idx]]); results[idx] vec; break; } catch (err) { if (attempt maxRetries) throw err; await new Promise((r) setTimeout(r, 2 ** attempt * 500)); } } } } await Promise.all(Array.from({ length: concurrency }, worker)); return results; }4. 检索质量从能搜到到搜得准4.1 查询改写让用户的问题变成好的检索词用户提问往往很口语化、很模糊。那个退款的东西怎么弄——直接拿这句去检索向量可能匹配到一堆泛泛而谈的文档。查询改写的目标是把口语问题转成信息密度更高的检索表达。常见的改写手段有三种同义扩展把退款扩展成退款、退货、取消订单、售后。这能覆盖用户用词和文档用词不一致的情况。问题拆解用户问我买了会员但想退款流程是什么多久到账这其实是两个问题流程 时效。拆成两个子查询分别检索再合并结果命中率会明显提升。假设文档生成HyDE让模型先假装生成一个理想答案再用这个答案去检索。因为答案的措辞通常比问题更接近文档内容检索效果往往更好。这个技巧在冷启动、文档措辞和用户提问差异大时特别有效。4.2 混合检索向量 关键词的双保险纯向量检索有个软肋对精确匹配不敏感。用户问错误码 E5021 怎么解决向量检索可能召回一堆讲错误处理的泛泛文档却漏掉那个精确提到 E5021 的片段。解决办法是混合检索Hybrid Search向量检索负责语义相似关键词检索BM25 等负责精确匹配两路结果融合。融合算法常用 RRFReciprocal Rank Fusion它对两路排名做加权不需要归一化分数简单又稳。我的实测经验是在包含大量专有名词、代码、编号的知识库里混合检索比纯向量检索的命中率能高出 20%~30%。这个提升在客服、技术支持类 Agent 上尤其明显。4.3 重排序把真正有用的片段顶上来前面提过向量相似度高不等于内容有用。重排序Rerank就是来解决这个问题的。流程是向量检索先召回 20~50 个候选然后用交叉编码器Cross-Encoder模型对查询-片段对逐个打分。交叉编码器会把查询和片段拼在一起过模型能捕捉到更细粒度的相关性比单纯的向量相似度准得多。缺点是慢所以只对少量候选做。重排序之后取 Top 3~5 个片段进 Prompt。这个先粗召回、再精排序的两阶段结构是生产级 RAG 的标准配置。检索方式优点缺点适用场景纯向量语义理解强精确匹配弱概念性、描述性问题纯关键词精确匹配强语义泛化弱编号、代码、专名查询混合检索兼顾两者实现复杂通用生产场景混合 重排精度最高延迟增加对准确率要求高的场景4.4 Top-K 怎么定一个动态调整的思路Top-K 设多少设小了漏信息设大了引入噪声还占上下文。固定值往往不是最优。我的做法是动态 Top-K先取一个较大的候选集比如 20经过重排序后看分数分布。如果前 3 个分数明显高于后面就只取前 3 个如果分数平滑下降说明相关信息分散多取几个。这样能自适应不同问题的信息密度。另一个技巧是按 token 预算截断不管取几个片段总 token 数不超过模型上下文窗口的某个比例比如 40%留足空间给问题和回答。超出预算就按分数从低到高砍。5. 上下文组装与生成阶段的坑5.1 Prompt 里怎么摆资料模型才不跑偏检索到好资料不代表模型会用。Prompt 的组装方式很关键。我踩过的坑包括资料堆在一起没有分隔模型分不清哪段是哪段资料顺序混乱模型被不相关内容带偏没有明确指令模型开始自由发挥。我的 Prompt 模板遵循几个原则明确指令开头就说清楚仅根据以下资料回答资料未提及的内容明确说明。编号分隔每段资料加编号和来源标记方便模型引用也方便你调试。相关资料优先把重排序分数最高的放最前面模型对开头内容注意力更强。冲突处理如果资料之间有矛盾指令里要说明如资料冲突以编号靠前的为准或指出冲突。5.2 上下文超长怎么办模型上下文窗口有限检索片段多了会超。处理策略有三层第一层是检索阶段控制通过动态 Top-K 和 token 预算从源头限制总量。第二层是压缩对每个片段做摘要或抽取关键句只保留和问题最相关的部分。这可以用一个小模型来做成本可控。第三层是分块生成如果问题复杂、资料多可以分多次生成每次处理一部分资料最后汇总。这适合长文档问答场景。5.3 引用与溯源让答案可信生产级 RAG 必须能溯源。用户看到答案要能知道这个信息来自哪份文档的哪一段。做法是在 Prompt 里要求模型标注引用编号生成后把编号映射回原始片段和文档。这不仅是可信度问题也是调试利器。当答案出错时你能快速定位是检索错了还是生成错了。如果引用指向的片段本身就不相关那是检索问题如果片段相关但答案错了那是生成问题。5.4 一个完整的生成函数示例interface Source { docId: string; chunkId: string; content: string; } function assemblePrompt( query: string, sources: Source[], maxContextTokens 3000 ): string { // 粗略估算中文约 1.5 字符/token let budget maxContextTokens * 1.5; const selected: Source[] []; for (const s of sources) { if (budget - s.content.length 0) break; selected.push(s); budget - s.content.length; } const contextBlock selected .map((s, i) [${i 1}] (来源: ${s.docId})\n${s.content}) .join(\n\n); return [ 你是一个严谨的知识助手。请严格依据下方资料回答问题。, 要求, 1. 只使用资料中的信息不要编造。, 2. 资料未提及的内容回答\资料未提及\。, 3. 在答案中用 [编号] 标注信息来源。, , 资料, contextBlock, , 问题${query}, , 回答, ].join(\n); }6. 那些文档不会写的踩坑记录6.1 检索命中率上不去的排查链路有段时间我做的客服 Agent 命中率一直卡在 60% 左右用户经常反馈答非所问。我按下面的链路一步步排查最后定位到问题。第一步先看检索结果本身。我把用户问题和检索到的 Top-5 片段打出来人工看。发现有一半的情况正确答案根本不在 Top-5 里。这说明是检索环节的问题不是生成环节。第二步检查切分。我把命中的片段和原始文档对照发现很多片段被切得七零八落一个完整的操作步骤被切成三段。用户问完整流程任何单段都答不全。这是切分策略的问题——我当初按固定字数切没考虑语义边界。第三步检查嵌入模型。我换了一个对中文优化更好的模型重试命中率提升了约 10 个百分点。说明原模型在中文语义上确实偏弱。第四步加混合检索。因为知识库里有大量产品型号和错误码纯向量检索对精确匹配不敏感。加上关键词检索后涉及型号、编号的查询命中率大幅提升。第五步加重排序。前面几步做完召回率上去了但 Top-5 里还是混着不相关片段。加了重排序后真正相关的片段被顶到前面最终命中率稳定在 85% 以上。这个排查顺序很重要先确认是检索问题还是生成问题再从切分、模型、检索方式、重排逐层优化。别一上来就换模型很多时候问题在切分。6.2 向量库选型的现实考量向量库的选择我建议按数据规模和部署条件来定别盲目追新。数据量在十万级以下很多方案都能扛选你团队最熟悉的即可。数据量到百万、千万级就要认真考虑索引类型HNSW、IVF 等、内存占用、分布式能力。如果对数据隐私要求高需要本地部署那就得选支持自托管且运维成本可控的方案。我踩过的一个坑是早期选了一个功能很全但运维复杂的向量库结果团队没人会调优索引构建慢、查询延迟高。后来换成一个更轻量的方案反而跑得更稳。工具是拿来用的不是拿来炫的。6.3 增量更新知识库不是一次性的知识库会变。文档会更新、会新增、会删除。如果每次变更都全量重建索引成本高、耗时长。正确做法是增量更新用文档 ID 和内容哈希做标识新增文档只索引新片段修改文档只更新变化的片段删除文档按 ID 清理。这要求你在索引时就存好元数据文档 ID、片段 ID、内容哈希、更新时间。还有个细节删除要彻底。我见过向量库里残留了大量已删除文档的片段检索时被召回导致模型基于过时信息作答。定期做一致性校验把孤儿片段清掉。6.4 评估没有度量就没有优化RAG 最怕感觉还行。你必须有一套评估机制否则优化全靠猜。我常用的评估指标有三个命中率RecallK——正确答案在 Top-K 里的比例准确率PrecisionK——Top-K 里相关片段的比例答案质量——人工或模型对最终答案打分。评估集怎么来从真实用户问题里采样人工标注每个问题的正确答案来源。这个工作费时但值得有了评估集你每次改动都能量化对比而不是凭感觉。7. 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 是一问一检索一答的固定流程。但真实场景里问题往往需要多步检索、多轮推理。这就是Agentic RAG的方向让 Agent 自己决定什么时候检索、检索什么、检索几次。比如用户问对比 A 产品和 B 产品的退款政策差异基础 RAG 一次检索可能只捞到其中一个产品的政策。Agentic RAG 会先检索 A 的政策再检索 B 的政策然后对比。它把检索变成了 Agent 的一个工具由 Agent 根据推理需要自主调用。实现上这需要给 Agent 配备检索工具并在 Prompt 里教会它什么时候该检索。同时要处理多轮检索的结果合并、去重、冲突消解。这是比基础 RAG 复杂一个量级的问题但也是让 Agent 真正会查资料的关键。我在实际项目里的体会是先把基础 RAG 的每个环节做扎实再考虑 Agentic 化。基础管道都跑不稳加再多花活也是空中楼阁。切分、检索、重排、组装这四步的功夫下够了Agentic RAG 才有意义。最后分享一个我常用的调试技巧把每次检索的查询、召回片段、重排分数、最终 Prompt 全部落日志。出问题时你能完整复现整个决策链路快速定位是哪一环掉了链子。这个日志的价值在你被线上问题追着跑的时候会体现得淋漓尽致。