
1. 为什么知识获取管道是 AI Agent 的分水岭做 AI Agent 开发的人迟早会撞上一堵墙模型本身很聪明但它不知道你公司内部的业务规则、不知道你上周刚更新的产品文档、更不知道你私有的那套运维手册里写了什么。你问它一个非常具体的问题它要么一本正经地胡说八道要么给你一个“根据我的训练数据”这种毫无用处的回答。这不是模型不行而是它的知识边界被锁死在了训练截止日期那一刻。知识获取管道要解决的就是这个问题。它的核心思路是在模型生成回答之前先从外部知识库里检索出相关内容把这些内容作为上下文塞进提示词再让模型基于这些真实材料来回答。这套方法论就是RAGRetrieval-Augmented Generation检索增强生成。你可以把它理解成开卷考试——模型不再靠死记硬背答题而是先翻书找到相关章节再组织语言作答。我之所以把这个话题放在 AI Agent 系列的第四篇是因为前几篇聊的都是 Agent 的“骨架”——规划、工具调用、记忆管理。但一个没有知识获取能力的 Agent就像一个没有资料库的客服只能靠常识应付遇到专业问题就露馅。RAG 就是给 Agent 接上资料库的那根管道而且是整个 Agent 系统里最容易出瓶颈、最需要精细调优的一环。这篇文章适合谁看如果你正在用TypeScript搭建 AI Agent或者你已经在用 LangChain、Spring AI 之类的框架做RAG 实战但总觉得检索效果不稳定、回答质量忽高忽低那这篇内容就是写给你的。我会从管道设计的整体思路讲起一路拆到文档切分、向量化、检索策略、重排序这些关键环节最后给出可以直接抄作业的 TypeScript 实现方案和踩坑记录。2. RAG 管道的整体设计与核心思路拆解2.1 为什么是 RAG 而不是微调很多人一遇到“模型不懂我的数据”这个问题第一反应是微调。但微调有几个硬伤成本高、周期长、每次知识更新都要重新训练、而且微调后的模型仍然可能产生幻觉。RAG 的优势在于知识库和模型是解耦的——你更新知识库只需要重新索引文档模型完全不用动。对于知识频繁变化的场景比如产品文档、内部 Wiki、客服话术库RAG 是唯一合理的选择。另一个关键考量是可追溯性。RAG 检索出来的内容可以作为引用来源展示给用户用户能看到答案是基于哪段文档生成的。这在企业场景里非常重要因为没有人会信任一个说不出依据的 AI 回答。微调模型做不到这一点它的知识是隐式编码在参数里的你没法追溯。2.2 管道的四个核心阶段一条完整的 RAG 管道可以拆成四个阶段我用一个生活化的类比来解释假设你要建一个图书馆让一个博学但没读过你这些书的人来当图书管理员。第一阶段是文档摄取与切分相当于把书拆成一个个小册子方便快速查找。你不能把一整本书塞给管理员他看不过来。所以要切成合适大小的块每块讲一个相对完整的知识点。第二阶段是向量化与索引相当于给每个小册子编一个“语义指纹”。这个指纹不是关键词而是一串数字向量它捕捉的是内容的语义特征。语义相近的内容指纹也相近。这样管理员就能通过比对指纹找到相关内容而不是靠关键词匹配。第三阶段是检索用户提问时系统把问题也转成指纹然后在索引里找最相近的几个小册子。这一步的难点在于怎么保证找出来的确实是最相关的而不是表面上像但实际跑偏的。第四阶段是生成把检索到的内容连同用户问题一起交给模型让它基于这些材料组织回答。这一步要处理的是上下文长度限制、内容冲突、以及如何引导模型正确使用检索结果。2.3 TypeScript 技术栈的选型逻辑为什么用 TypeScript 来做 RAG很多人觉得 Python 才是 AI 的主场但实际工程中你的 Agent 系统很可能已经是一个 Node.js 后端服务前端也是 React 或 Vue。如果 RAG 管道用 Python 写你就得维护两套运行时、两套依赖管理、两套部署流程。用 TypeScript 统一技术栈开发和运维成本都会低很多。具体到工具选型我推荐这几个LangChain.js提供了完整的 RAG 抽象文档加载器、文本切分器、向量存储接口都有现成实现OpenAI SDK或者兼容 OpenAI 接口的国内模型 SDK 负责 Embedding 和生成向量数据库方面开发阶段可以用Chroma的 JS 客户端或者LanceDB生产环境可以考虑Qdrant或Milvus它们都有 TypeScript 客户端。这套组合的好处是生态成熟、文档齐全、社区活跃遇到问题容易找到答案。注意选型时不要盲目追求“最新最热”的框架。RAG 的核心逻辑其实很简单过度依赖框架的抽象反而会让你在出问题时无从下手。我建议先用原生 SDK 把流程跑通一遍理解每个环节在做什么再决定要不要引入框架。3. 文档切分与向量化管道的第一公里3.1 文档切分的核心原则与常见误区文档切分看起来简单实际上是最容易埋雷的环节。我见过太多项目检索效果差就是因为切分策略不对。核心原则只有一条每个块应该是一个语义完整的单元。什么意思如果一个块讲了一半的概念就断了检索出来也没法用如果一个块塞了三个不同的主题向量化后的语义指纹就会模糊检索精度下降。常见的切分策略有三种。固定长度切分最简单按字符数或 token 数硬切但容易切断句子和段落。递归字符切分会优先按段落切段落太长再按句子切句子太长再按字符切这是 LangChain 的默认策略适合大多数场景。语义切分最智能用模型判断句子之间的语义相似度在语义转折处切分但计算成本高适合对精度要求极高的场景。我在实际项目中的经验是递归字符切分 适当的重叠能覆盖 80% 的场景。块大小设置在 500 到 1000 个字符之间重叠 100 到 200 个字符。重叠的目的是防止关键信息刚好落在切分边界上被切断。比如一个定义跨了两个块有重叠的话至少有一个块包含完整定义。import { RecursiveCharacterTextSplitter } from langchain/text_splitter; const splitter new RecursiveCharacterTextSplitter({ chunkSize: 800, chunkOverlap: 150, separators: [\n\n, \n, 。, , , ., , ], }); const chunks await splitter.splitText(rawDocument);注意separators的顺序很重要它决定了切分的优先级。中文场景下要把中文标点放在英文标点前面否则模型会优先在英文句号处切分导致中文句子被切得七零八落。3.2 元数据设计被大多数人忽略的关键切分出来的块不能光有文本内容还要附带元数据。元数据在检索和生成阶段都有大用。最基本的元数据包括来源文档名、块在文档中的位置、所属章节标题。进阶一点可以加上文档类型、更新时间、权限标签。为什么元数据重要举个例子用户问“最新的退款政策是什么”如果你的块带有更新时间元数据检索时就可以优先返回最新的块或者在生成时告诉模型“以下内容按时间倒序排列”。再比如多租户系统里不同用户能访问的文档不同权限标签可以在检索阶段就过滤掉无权访问的内容避免信息泄露。const chunksWithMetadata chunks.map((chunk, index) ({ pageContent: chunk, metadata: { source: refund-policy-2026.pdf, chunkIndex: index, section: extractSectionTitle(chunk), updatedAt: 2026-01-15, accessLevel: internal, }, }));3.3 向量化模型的选择与成本权衡向量化就是把文本转成向量。这一步用的是 Embedding 模型和生成模型是两回事。选 Embedding 模型要考虑三个因素语义捕捉能力、维度、成本。语义捕捉能力决定了检索的准确度。一般来说更大的模型效果更好但成本也更高。维度方面常见的有 768 维、1024 维、1536 维。维度越高表达能力越强但存储和计算成本也越高。成本方面有些模型按 token 收费有些自部署的模型只算 GPU 成本。我的建议是先用中等规模的模型跑通流程再根据实际检索效果决定要不要升级。不要一上来就用最贵的模型因为检索效果差往往不是 Embedding 模型的问题而是切分策略或检索策略的问题。国内可用的 Embedding 模型有不少选择具体选哪个要看你的部署环境和预算。import OpenAI from openai; const client new OpenAI({ apiKey: process.env.EMBEDDING_API_KEY, baseURL: process.env.EMBEDDING_BASE_URL, }); async function embedText(text: string): Promisenumber[] { const response await client.embeddings.create({ model: your-embedding-model, input: text, }); return response.data[0].embedding; }提示批量向量化时要注意 API 的速率限制。我通常会把块分批每批 20 到 50 个批之间加一个短延迟。如果文档量很大建议写一个带重试和断点续传的脚本避免跑到一半失败要重头来。4. 检索策略与重排序决定 RAG 效果的关键环节4.1 向量检索的局限性与混合检索纯向量检索有个天然缺陷它对精确匹配不敏感。比如用户问“错误码 E5021 是什么意思”向量检索可能会返回一堆讲错误处理的文档但就是找不到精确提到 E5021 的那一篇。因为向量捕捉的是语义相似度而 E5021 这种专有标识符的语义信息很弱。解决方案是混合检索同时做向量检索和关键词检索比如 BM25然后把两边的结果融合。向量检索负责捕捉语义相关性关键词检索负责精确匹配。融合策略有几种最简单的是加权求和给两种检索的分数各乘一个权重再加起来。RRFReciprocal Rank Fusion更常用它不依赖分数绝对值只看排名对不同检索方式的分数尺度差异更鲁棒。function reciprocalRankFusion( vectorResults: SearchResult[], keywordResults: SearchResult[], k: number 60 ): SearchResult[] { const scoreMap new Mapstring, number(); vectorResults.forEach((result, index) { const score 1 / (k index 1); scoreMap.set(result.id, (scoreMap.get(result.id) || 0) score); }); keywordResults.forEach((result, index) { const score 1 / (k index 1); scoreMap.set(result.id, (scoreMap.get(result.id) || 0) score); }); return Array.from(scoreMap.entries()) .sort((a, b) b[1] - a[1]) .map(([id]) findResultById(id, vectorResults, keywordResults)); }4.2 重排序用精排模型提升精度混合检索出来的结果通常取前 20 到 50 个候选。但这些候选里仍然可能混着不相关的内容。这时候就需要重排序用一个专门的精排模型对每个候选和用户问题的相关性打分然后按分数重新排序取前 3 到 5 个送给生成模型。重排序模型和 Embedding 模型不同它做的是交叉编码——把问题和文档拼在一起输入模型输出一个相关性分数。这种方式比向量点积更精确但计算成本也更高所以只适合对少量候选做精排。async function rerank( query: string, candidates: SearchResult[], topK: number 5 ): PromiseSearchResult[] { const pairs candidates.map((c) ({ query, document: c.content, })); const scores await rerankModel.score(pairs); return candidates .map((c, i) ({ ...c, rerankScore: scores[i] })) .sort((a, b) b.rerankScore - a.rerankScore) .slice(0, topK); }实测下来加了重排序之后检索命中率通常能提升 15% 到 30%。这个投入产出比非常高我强烈建议在生产环境里加上这一步。4.3 查询改写让用户的问题更容易被检索到用户提问的方式和文档写作的方式往往不一致。用户可能问“怎么退钱”而文档里写的是“退款申请流程”。这种词汇鸿沟会导致检索失败。查询改写就是让模型把用户问题改写成更适合检索的形式或者生成多个不同角度的查询分别检索后合并结果。async function rewriteQuery(originalQuery: string): Promisestring[] { const prompt 你是一个检索查询优化助手。请将以下用户问题改写成3个不同角度的检索查询每行一个不要编号 用户问题${originalQuery}; const response await llm.invoke(prompt); return response.content.split(\n).filter((q) q.trim()); }多查询检索的好处是覆盖面更广。比如“怎么退钱”可以改写成“退款流程”、“退款申请条件”、“退款到账时间”三个查询分别检索后合并去重命中率会明显提升。代价是检索次数增加延迟和成本都会上升需要根据场景权衡。5. 生成阶段的上下文组装与提示词设计5.1 上下文窗口的分配策略检索出来的内容不能一股脑全塞给模型因为上下文窗口是有限的。你需要决定给系统提示词留多少、给检索内容留多少、给对话历史留多少、给模型输出留多少。我的经验分配是系统提示词控制在 200 到 500 token检索内容根据模型窗口大小灵活调整对话历史保留最近 3 到 5 轮输出预留 1000 到 2000 token。如果检索内容太多优先保留重排序分数高的或者对内容做摘要压缩。function assembleContext( systemPrompt: string, retrievedChunks: SearchResult[], conversationHistory: Message[], maxTokens: number ): Message[] { const messages: Message[] [ { role: system, content: systemPrompt }, ]; // 从最近的对话历史开始加直到接近预算上限 const historyBudget Math.floor(maxTokens * 0.2); let historyTokens 0; const recentHistory: Message[] []; for (let i conversationHistory.length - 1; i 0; i--) { const msg conversationHistory[i]; const tokens estimateTokens(msg.content); if (historyTokens tokens historyBudget) break; recentHistory.unshift(msg); historyTokens tokens; } messages.push(...recentHistory); // 组装检索内容 const contextBudget Math.floor(maxTokens * 0.5); let contextTokens 0; const contextParts: string[] []; for (const chunk of retrievedChunks) { const tokens estimateTokens(chunk.content); if (contextTokens tokens contextBudget) break; contextParts.push([来源: ${chunk.metadata.source}]\n${chunk.content}); contextTokens tokens; } messages.push({ role: user, content: 请基于以下参考资料回答问题。如果参考资料中没有相关信息请明确说明。\n\n参考资料\n${contextParts.join(\n\n---\n\n)}, }); return messages; }5.2 提示词中的关键约束生成阶段的提示词设计有几个关键点。第一明确告诉模型只基于检索内容回答不要自己发挥。第二要求模型标注引用来源这样用户能验证答案的可靠性。第三处理检索内容冲突的情况比如两份文档说法不一致要告诉模型优先采信更新的或更权威的来源。第四定义好“不知道”的行为当检索内容不足以回答时模型应该明确说“根据现有资料无法回答”而不是硬编一个答案。注意提示词里不要写“请尽可能回答”这种模糊指令模型会倾向于编造内容来满足你的要求。要写“如果资料不足请直接说明”给模型一个安全的退路。5.3 流式输出与引用标注的实现用户体验层面流式输出几乎是必须的。用户不想等好几秒才看到第一个字。TypeScript 里实现流式输出很直接用 SDK 的 stream 接口就行。引用标注可以在流式输出结束后把检索到的来源列表附在回答末尾。async function* streamAnswer(messages: Message[]) { const stream await llm.stream(messages); for await (const chunk of stream) { yield chunk.content; } } // 使用示例 const sources retrievedChunks.map((c) ({ title: c.metadata.source, section: c.metadata.section, })); for await (const token of streamAnswer(messages)) { process.stdout.write(token); } console.log(\n\n参考来源); sources.forEach((s, i) { console.log(${i 1}. ${s.title} - ${s.section}); });6. 常见问题与排查技巧实录6.1 检索命中率低的排查思路检索命中率低是最常见的问题。排查时按这个顺序来先看切分把检索到的块打印出来看看内容是否完整、是否包含答案。如果块本身就不包含答案那问题在切分策略。再看 Embedding把用户问题和检索到的块的向量相似度算出来看看分数分布。如果所有分数都很低可能是 Embedding 模型不适合你的领域。最后看检索策略试试混合检索和重排序看命中率有没有提升。我整理了一个速查表遇到问题时可以对照排查现象可能原因排查方法解决方案检索结果完全不相关Embedding 模型不匹配检查相似度分数分布换用领域适配的 Embedding 模型检索结果部分相关但缺关键信息切分粒度太粗打印块内容检查减小块大小增加重叠精确术语检索不到纯向量检索的局限用关键词检索测试引入混合检索相似问题检索结果不稳定向量索引参数不当调整检索返回数量增加候选数加重排序多轮对话后检索跑偏查询未结合上下文检查实际检索查询加入查询改写融合对话历史6.2 生成阶段胡编乱造的抑制方法模型胡编乱造通常有三个原因检索内容不足、提示词约束不够、模型本身倾向。对应的解法是提高检索召回确保相关内容能被找到强化提示词约束明确要求基于资料回答降低生成温度温度参数调到 0.1 到 0.3 之间减少随机性。还有一个技巧是让模型先复述再回答。在提示词里要求模型先列出它从资料中找到的关键信息再基于这些信息组织回答。这样相当于强制模型先“看”资料再“说”话能显著降低幻觉率。6.3 性能优化的实操经验RAG 管道的延迟主要来自三个地方Embedding 计算、向量检索、生成。优化手段分别是Embedding 缓存对相同或相似的查询缓存向量结果索引优化向量数据库建 HNSW 索引检索速度能提升一个数量级并行化混合检索的两路可以并行执行查询改写也可以并行。async function parallelRetrieve(query: string) { const [vectorResults, keywordResults] await Promise.all([ vectorStore.search(query, 20), keywordStore.search(query, 20), ]); return reciprocalRankFusion(vectorResults, keywordResults); }提示如果你的文档量在 10 万块以下很多向量数据库的单机模式完全够用不需要上分布式集群。过早优化架构只会增加运维复杂度。6.4 知识库更新的处理策略知识库不是建好就完事了文档会更新、会新增、会删除。全量重建索引成本太高需要增量更新。实现方式是给每个块一个唯一 ID比如文档路径加块序号更新时先删除旧块再插入新块。如果文档只是小改动可以只重新索引变化的文档。async function upsertDocument(docPath: string, content: string) { // 先删除该文档的所有旧块 await vectorStore.delete({ filter: { source: docPath } }); // 重新切分和索引 const chunks await splitter.splitText(content); const vectors await Promise.all(chunks.map(embedText)); await vectorStore.upsert( chunks.map((chunk, i) ({ id: ${docPath}#${i}, vector: vectors[i], content: chunk, metadata: { source: docPath, chunkIndex: i }, })) ); }这套增量更新逻辑看起来简单但实际跑起来要注意并发控制。如果多个文档同时更新删除和插入可能交叉执行导致数据不一致。我的做法是加一个简单的队列同一文档的更新操作串行执行。7. 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 跑通之后你会发现它仍然有局限。比如用户问一个需要多步推理的问题单次检索可能找不到足够的信息。这时候就需要Agentic RAG让 Agent 自己决定什么时候检索、检索什么、检索几次。Agent 可以先检索一次发现信息不够再换个角度检索或者调用其他工具补充信息最后综合所有材料生成回答。这个演进方向的核心变化是检索从“固定流程”变成“Agent 的一个工具”。Agent 根据当前任务状态动态决定是否调用检索工具以及用什么查询去检索。这需要 Agent 具备一定的规划能力和自我评估能力——它要能判断“我现在掌握的信息够不够回答问题”。实现上你可以把检索封装成一个工具注册到 Agent 的工具列表里。Agent 在推理过程中自主决定调用时机。这比固定管道的灵活性高很多但也带来了新的挑战如何防止 Agent 过度检索、如何评估检索结果的质量、如何在多轮检索中保持上下文一致性。这些话题我会在后续的文章里展开。回到基础 RAG我的建议是先把这条管道跑通、跑稳再考虑进阶。很多团队一上来就追求 Agentic RAG结果基础检索都没做好Agent 再智能也找不到有用的信息。基础 RAG 的每个环节——切分、向量化、检索、重排序、生成——都值得花时间打磨。这些环节的调优经验在进阶到 Agentic RAG 之后同样适用。我在实际项目里踩过最大的坑就是一开始太信任默认参数。LangChain 的默认切分大小是 1000 字符默认检索数量是 4这些参数在通用场景下能用但在专业领域往往不够。花半天时间做参数调优比换更贵的模型效果提升更明显。另外一定要建一个评估集——准备 50 到 100 个典型问题和标准答案每次调整参数后跑一遍评估用数据说话不要凭感觉。