1. 为什么知识获取管道是 AI Agent 的分水岭做 AI Agent 开发到第三篇之后基本都会撞上同一堵墙模型本身很聪明但你问它公司内部的报销标准、上周刚更新的接口文档、某个客户的特殊约定它要么一本正经地胡说要么干脆说不知道。这不是模型不行而是它的知识被冻结在训练截止那一天而且它压根没见过你的私有数据。知识获取管道要解决的就是把这个断层补上——让 Agent 在回答问题之前先去查资料再开口说话。这套机制里最主流、最成熟的一条路线就是RAGRetrieval-Augmented Generation检索增强生成。我先把话说在前面RAG 不是某个库、某个框架而是一种架构模式。它的核心思想朴素到有点土——先检索再生成。用户问一个问题系统先从知识库里捞出最相关的若干段文本把这些文本塞进提示词再让大模型基于这些证据来回答。这样一来模型不需要记住你的知识它只需要会读你递给它的材料。这个转变非常关键它把知识更新从重新训练模型这种昂贵操作降级成了往数据库里加一条记录这种日常操作。这篇是走进 AI Agent系列的第四篇主题锁定在知识获取管道的基础层。我会用TypeScript作为主要实现语言来串讲因为现在大量 Agent 应用跑在 Node 侧前端工程师转过来做 Agent 开发TypeScript 几乎是零门槛的入口。整篇内容适合三类人一是刚接触 RAG、想知道它到底怎么跑起来的新手二是已经用过 LangChain 之类框架、但对其内部机制一知半解的开发者三是准备把 RAG 落到真实业务里、需要知道坑在哪里的工程负责人。我不会只给你一段能跑的 demo而是把每个环节为什么这么设计讲透让你在换框架、换模型、换数据库的时候心里有底。需要提前说明的是RAG 的完整链路其实很长从文档解析、切分、向量化、存储、检索、重排到最终拼装提示词每一环都有讲究。这一篇聚焦基础也就是把主干链路打通把最核心的几个概念讲清楚。更进阶的 Agentic RAG、GraphRAG、多路召回融合这些我会在后续篇章里单独展开。先把地基打牢比一上来就堆花活重要得多。2. RAG 的整体设计与核心思路拆解2.1 一句话讲清 RAG 到底在干什么打个比方。你请了一位博学但记性只停留在去年的顾问他现在要回答你公司今年的新政策。你不会让他去重新读一遍公司所有文件那相当于重新训练而是会在他回答前先让助理把相关的几页文件找出来放到他面前说你就照着这几页回答。RAG 里的检索器就是那个助理生成器就是那位顾问。用技术语言描述RAG 把一次问答拆成两个阶段。离线阶段索引把你的知识文档切块、转成向量、存进向量数据库建好索引。在线阶段查询用户提问时把问题也转成向量去数据库里找最相似的若干块把这些块和问题一起交给大模型生成答案。离线阶段是一次性的、可以慢慢跑在线阶段要求快通常要在几百毫秒到几秒内完成。这个拆分带来一个巨大的好处知识更新和模型解耦。文档变了你只需要重新索引那部分文档模型一行都不用动。反过来模型升级了你的知识库也完全不受影响。这种解耦是 RAG 能在大规模生产环境里活下来的根本原因。2.2 为什么是检索增强而不是微调很多人第一反应是我直接拿私有数据微调一个模型不就行了能行但代价和局限都很明显。微调是把知识焊死进模型权重里更新一次就要重训一次成本高、周期长而且微调擅长的是让模型学会某种风格或格式对于精确的事实召回它并不比检索可靠——模型可能记住了大概但记错了细节你还很难发现。RAG 则把事实来源外置成可审计的文档答案错了你能顺着引用去查是哪块资料的问题。从工程角度看RAG 的迭代速度是微调没法比的。改一个切分策略、换一个 embedding 模型、调一下 top-k改完立刻能测效果。微调你要等训练、等评估一轮下来半天没了。所以在绝大多数让 Agent 掌握私有知识的场景里RAG 是默认首选微调是补充手段两者不冲突——常见做法是 RAG 负责事实微调负责语气和格式。2.3 主干链路的五个关键环节把 RAG 拆开主干上有五个绕不开的环节我用一张表先给你一个全局印象后面逐个展开。环节输入输出核心目标常见坑文档加载原始文件PDF/MD/HTML纯文本把各种格式统一成文本表格、公式丢失文本切分长文本文本块chunk块大小适中、语义完整切断句子、块过大向量化文本块向量语义可比较模型选错、维度不匹配存储与检索向量原文Top-K 相关块快速找到相关内容相似度阈值难定生成问题相关块答案基于证据回答提示词没约束好这五步里切分和检索是最容易被低估、也最影响效果的两环。新手往往把注意力全放在用哪个大模型上结果发现换了最强的模型答案还是不准——问题根本不在生成端而在检索端压根没捞到正确的资料。记住一句话RAG 的效果上限由检索决定生成只是把检索到的东西说清楚。2.4 TypeScript 技术栈的选型考量为什么用 TypeScript 来讲因为 Agent 应用越来越往应用层走而应用层大量是 Web 技术栈。用 TypeScript 做 RAG好处是前后端同构、类型安全、生态成熟。核心依赖大致是这几类文档解析用pdf-parse、mammoth之类切分和编排可以自己写也可以用langchain的 JS 版本向量化调用各家 embedding API向量存储可以用内存版开发或专业向量库生产。选型上我的建议是开发阶段尽量少依赖框架把链路自己手写一遍。原因很实在——RAG 的坑大多藏在细节里框架帮你封装了细节也帮你藏起了问题。你自己手写一遍切分、手写一遍余弦相似度再去看框架的源码理解会完全不一样。等链路跑通了再决定要不要上框架提效。这一篇的代码示例会偏向手写为主、框架为辅的风格。3. 核心细节解析与实操要点3.1 文档加载别小看把文件读成文本这一步文档加载听起来最没技术含量实际上是最脏最累的一环。你的知识可能散落在 PDF、Word、Markdown、网页、数据库里每种格式的解析质量天差地别。PDF 尤其麻烦扫描件需要 OCR带表格的 PDF 解析出来经常错位双栏排版的 PDF 可能把左右两栏的文字交错读出来读出来就是一堆乱码般的句子。实操上我的经验是能拿到结构化源文件就绝不用 PDF。如果知识源头是 Confluence、飞书文档、Markdown 仓库优先走它们的 API 或原始格式PDF 只作为最后兜底。如果非解析 PDF 不可选一个靠谱的解析库并且一定要人工抽查解析结果——随机抽十页看看文字顺序对不对、表格有没有散架。这一步偷懒后面全盘皆输因为垃圾进必然垃圾出。在 TypeScript 里一个最小化的加载函数大概长这样import fs from fs/promises; import pdfParse from pdf-parse; async function loadDocument(filePath: string): Promisestring { const ext filePath.split(.).pop()?.toLowerCase(); if (ext pdf) { const buffer await fs.readFile(filePath); const result await pdfParse(buffer); return result.text; } // md / txt 直接读 return await fs.readFile(filePath, utf-8); }这段代码能跑但你要清楚它的局限pdf-parse对复杂版式无能为力。生产环境里我通常会加一层清洗——把连续多个空行压成一个、去掉页眉页脚的重复文本、修掉断行导致的错误换行。这些清洗规则要根据你的文档特点定制没有通用解。3.2 文本切分RAG 里最需要手感的一步切分是 RAG 的灵魂。为什么不能把整篇文档直接丢进去检索因为 embedding 模型对长文本的表达能力会衰减一整篇几千字的文档压成一个向量语义会被平均掉检索时既不够精确又浪费上下文窗口。所以要切成块。但切多大、怎么切全是学问。块太小一句话被切成两半检索到的块缺上下文模型看不懂。块太大一个块里混了好几个主题向量表达模糊检索精度下降还挤占上下文。业界常见的起点是500 到 1000 个字符一块块之间留10% 到 20% 的重叠overlap。重叠的作用是防止关键信息正好卡在切分边界上被切断——上一块的结尾和下一块的开头有重复内容保证语义连续。切分策略上最朴素的是按固定长度硬切但会切断句子。好一点的是按语义边界切优先在段落、句子、标点处断开。再进一步是递归切分——先按大分隔符如双换行切块还是太大就按单换行切再大就按句号切层层降级尽量保持语义完整。下面是一个递归切分的简化实现function splitText( text: string, chunkSize 800, overlap 120 ): string[] { const separators [\n\n, \n, 。, , , . , ]; const chunks: string[] []; function recursiveSplit(segment: string, sepIndex: number): void { if (segment.length chunkSize || sepIndex separators.length) { chunks.push(segment); return; } const sep separators[sepIndex]; const parts segment.split(sep); let buffer ; for (const part of parts) { const candidate buffer ? buffer sep part : part; if (candidate.length chunkSize buffer) { chunks.push(buffer); // 保留 overlap从上一块尾部截取 buffer buffer.slice(-overlap) sep part; } else { buffer candidate; } } if (buffer) chunks.push(buffer); } recursiveSplit(text, 0); return chunks; }这段代码不是最优解但足够让你理解递归切分的思路。实际项目里我强烈建议针对不同文档类型用不同策略技术文档按标题层级切每个小节一块FAQ 按问答对切代码按函数切。一刀切的切分策略效果一定平庸。注意overlap 不是越大越好。重叠太多会导致检索时同一内容反复命中浪费上下文还引入冗余。10% 到 20% 是个稳妥区间具体要拿你的数据实测。3.3 向量化把语义变成可以算距离的数字切完块下一步是把每个块转成向量。向量就是一串浮点数比如 1536 维的数组。它的神奇之处在于语义相近的文本向量在空间里的距离也近。如何申请报销和报销流程是什么字面不同但向量会很接近而如何申请报销和今天天气不错向量就离得远。检索的本质就是在向量空间里找离问题向量最近的几个块。向量化靠 embedding 模型完成。选型上有几个硬指标要看维度越高表达力越强但存储和计算越贵、最大输入长度决定单块能多长、语言支持中文场景要选中文表现好的、成本按 token 计费量大时很敏感。这里有个铁律索引时用的 embedding 模型和查询时必须用同一个。用 A 模型索引、用 B 模型查询向量空间对不上检索结果就是随机的。这个错误新手极容易犯换模型时忘了重建索引。调用 embedding 的代码大致是这样async function embed(texts: string[]): Promisenumber[][] { const res await fetch(https://your-embedding-endpoint/v1/embeddings, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.EMBED_API_KEY}, }, body: JSON.stringify({ model: your-embedding-model, input: texts, }), }); const data await res.json(); return data.data.map((d: any) d.embedding); }批量调用时注意两点一是控制单次请求的文本数量太多会被限流或超时二是做好失败重试网络抖动很常见索引阶段丢了几条数据检索时就永远查不到。我一般会加一个带指数退避的重试封装并且把每条文本的索引状态落库方便断点续传。3.4 存储与检索相似度怎么算、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)); }开发阶段几百上千个块用内存数组暴力算完全够用。生产环境块数上万甚至百万就要上专业向量库它们用近似最近邻ANN算法把检索从线性复杂度降到对数级。选向量库时关注是否支持元数据过滤比如只搜某个部门的文档、是否支持混合检索向量关键词、运维成本如何。Top-K 怎么定是个高频问题。K 太小可能漏掉正确资料K 太大塞进一堆无关内容干扰模型还费 token。常见起点是K3 到 5。但更聪明的做法是加相似度阈值低于某个分数的块直接丢掉哪怕它排在前 K。因为有时候知识库里压根没有相关内容硬塞进去只会让模型编。我通常会把阈值和 K 一起调观察检索命中率这个指标——正确资料出现在 Top-K 里的比例。提示检索效果差先别急着换模型。八成问题出在切分和 embedding 上。把检索到的块打印出来人工看一眼比调任何参数都管用。4. 实操过程与核心环节实现4.1 从零搭一个最小可用的 RAG 管道前面拆了零件这一节把它们组装成一条能跑的完整链路。我用 TypeScript 写一个内存版的 RAG不依赖任何向量库方便你本地跑通、理解全貌。整个流程分索引和查询两段。先定义数据结构。每个块除了向量还要保留原文和来源信息方便后面做引用interface Chunk { id: string; text: string; source: string; embedding: number[]; }索引阶段加载文档、切分、向量化、存进内存数组。const knowledgeBase: Chunk[] []; async function indexDocument(filePath: string): Promisevoid { const raw await loadDocument(filePath); const chunks splitText(raw, 800, 120); const embeddings await embed(chunks); chunks.forEach((text, i) { knowledgeBase.push({ id: ${filePath}-${i}, text, source: filePath, embedding: embeddings[i], }); }); }查询阶段把问题向量化和所有块算相似度取 Top-K拼提示词交给大模型。async function query(question: string, topK 4): Promisestring { const [qVec] await embed([question]); const scored knowledgeBase.map((chunk) ({ chunk, score: cosineSimilarity(qVec, chunk.embedding), })); scored.sort((a, b) b.score - a.score); const top scored.slice(0, topK).filter((s) s.score 0.3); const context top .map((s, i) [资料${i 1}] ${s.chunk.text}) .join(\n\n); const prompt 你是一个严谨的助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关信息请直接说资料中未提及不要编造。 资料 ${context} 问题${question} 回答; return await callLLM(prompt); }这段代码不到一百行但 RAG 的骨架全在里面了。你可以拿一份自己的 Markdown 文档跑一遍问几个问题感受一下效果。跑通之后再逐个环节替换成更专业的实现——切分换成递归切分、内存数组换成向量库、加一层重排效果会一步步提升。4.2 提示词里的约束让模型只基于资料回答RAG 效果好不好提示词占很大比重。上面那段 prompt 里有几个关键设计值得单独说。第一明确角色和任务——严谨的助手告诉模型它的定位。第二强制基于资料——仅根据下面提供的资料切断模型自由发挥的冲动。第三给出兜底话术——资料中未提及让模型有台阶下而不是硬编。第四给资料编号——方便模型引用也方便你事后核对。我踩过的一个坑是早期提示词写得太宽松模型经常把资料和自己的先验知识混在一起答案看着对其实掺了训练数据里的旧信息。后来加了仅根据资料和未提及就说未提及这两条幻觉率明显下降。另一个技巧是要求模型标注引用来源比如回答时请注明依据的是哪条资料这样答案可追溯用户信任度也高。注意提示词里的资料顺序会影响模型注意力。把最相关的资料放在最前面或最后面效果通常比放中间好。这是中间遗忘现象长上下文模型普遍存在。4.3 参数计算块大小、Top-K 与上下文预算RAG 里几个参数不是拍脑袋定的背后有预算约束。假设你的大模型上下文窗口是 8K token你要留出空间给系统提示、用户问题、模型回答。粗略分配系统提示 200 token问题 100 token回答预留 1000 token那么留给资料的只有约 6700 token。中文大致 1 个 token 对应 1.5 到 2 个字符6700 token 约等于 1 万到 1.3 万字符。如果每块 800 字符Top-K4资料总量约 3200 字符远没到上限说明你还有空间可以增大 K 或增大块。反过来如果 K10、每块 1500 字符就是 1.5 万字符超预算了模型会截断反而丢信息。所以块大小和 Top-K 要一起算让块大小 × K落在上下文预算内并留出安全余量。这个计算过程很多人跳过结果要么浪费窗口要么被静默截断还找不到原因。4.4 检索质量的自测方法链路跑通后怎么知道检索好不好最土也最有效的办法是建一个小型评测集准备 20 到 50 个真实问题每个问题人工标注正确答案应该来自哪几个块。然后跑检索看正确块有没有出现在 Top-K 里算出命中率。命中率低说明切分或 embedding 有问题命中率高但最终答案差说明问题在生成端或提示词。这个评测集不用大几十条就能暴露大部分问题。我习惯把它做成一个 JSON 文件每次改完切分策略或换 embedding 模型跑一遍看命中率变化。没有这个基线你调参数就是盲人摸象改了半天不知道是变好还是变坏。这是我从凭感觉调转向数据驱动调的关键一步强烈建议你也建一个。5. 常见问题与排查技巧实录5.1 检索不到正确内容问题出在哪这是最高频的问题。排查顺序我总结成一张表从上往下查基本能定位到根因。现象可能原因排查方法解决方向正确块完全不在 Top-K切分把关键信息切碎打印所有块找目标内容调整切分策略正确块排名靠后embedding 模型不适合换模型对比命中率换中文强的模型检索结果全是无关内容索引和查询模型不一致核对两侧模型名统一模型并重建索引相似度普遍偏低阈值设太高看分数分布下调阈值或去掉同一内容反复命中overlap 过大看重复块比例减小 overlap我遇到最多的是第一种关键信息被切分切碎了。比如一个操作步骤跨了两块检索只命中前半块模型看到的是残缺信息。解决办法是增大 overlap或者改用按语义边界切分。第二种也很常见用了英文为主的 embedding 模型处理中文效果自然差换成中文优化的模型立刻改善。5.2 答案里有幻觉明明资料里没有模型编造内容通常有三个来源。一是检索没命中但提示词没约束好模型就自由发挥了。二是资料里有相似但不完全对应的内容模型做了合理推断推错了。三是问题本身有歧义模型理解偏了。针对第一种加未提及就说未提及的兜底话术针对第二种要求模型严格引用原文不要推断针对第三种可以在检索前先做一次问题改写把模糊问题澄清成明确查询。还有一个隐蔽的坑资料块之间互相矛盾。知识库里有新旧两版文档检索同时命中模型不知道该信哪个。这种情况要在索引阶段做版本管理旧版本标记为失效检索时过滤掉。知识库的卫生和代码库一样重要脏数据不清理再好的模型也救不了。5.3 性能与成本什么时候该优化开发阶段别过早优化。内存暴力检索、同步调用 embedding几百个块完全够用。等块数上万、查询并发上来再考虑优化。优化方向有几个向量检索上 ANN 索引把 O(n) 降到 O(log n)embedding 结果缓存相同文本不重复调用批量索引减少网络往返异步并发索引阶段可以并发处理多个文档。成本上embedding 调用和 LLM 调用是两大头。embedding 是一次性成本除非频繁更新LLM 是每次查询都产生。控制 LLM 成本的关键是别塞太多资料——Top-K 不是越大越好塞进去的无关内容既费钱又干扰模型。我见过有人 K 设成 20结果一半是噪音成本翻几倍效果还更差。精而准永远比多而杂强。5.4 几个我踩过的坑直接告诉你第一个坑换 embedding 模型忘了重建索引。我换了个新模型查询效果突然变得极差查了半天才发现索引还是旧模型生成的向量空间对不上。记住embedding 模型和索引是绑定的换模型必须全量重建。第二个坑切分时把代码块切断了。技术文档里有代码示例按固定长度切会把一个函数切成两半检索到的代码根本没法看。后来我加了规则遇到代码块标记就整体保留不参与切分。第三个坑忽略了元数据。一开始我只存了文本和向量后来想按文档类型过滤、想显示来源发现没存这些信息只能重建索引。所以索引时一定要把 source、类型、时间、权限等元数据一起存进去后面做过滤和引用都用得上。第四个坑中文标点没处理好。英文按空格切词中文没有空格按字符切容易在词中间断开。中文场景要按标点。切或者用专门的中文分词。这个细节不注意切出来的块读起来很别扭。提示RAG 调优是个迭代过程别指望一次到位。建好评测集每次只改一个变量观察指标变化慢慢就摸到你这套数据的脾气了。6. 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 跑通之后你会发现它有几个天生的局限。第一它只会检索一次——问题进来检索一轮生成结束。但很多问题需要多步推理比如对比 A 和 B 两个方案的差异得先分别查 A 和 B再对比。第二它不会判断该不该检索——有些问题模型自己就能答硬去检索反而引入噪音。第三它不会自我纠错——检索到的资料不对它也不知道回头重查。这些局限催生了Agentic RAG把 RAG 从一条固定流水线变成一个会思考、会决策、会多轮检索的 Agent。它能在检索前判断这个问题需要查资料吗能在检索后评估资料够不够、对不对不够就换个查询词再查一轮甚至能调用多个知识源交叉验证。这已经不是检索增强生成而是检索作为 Agent 的一个工具。从基础 RAG 往 Agentic RAG 走技术上要补几块查询改写把用户问题转成更好的检索查询、多路召回向量检索关键词检索图检索融合、重排用更精细的模型对候选块重新排序、自我反思让 Agent 评估检索质量并决定是否重试。这些每一块都够写一篇我会在后续篇章里逐个拆。你现在要做的是先把基础链路跑扎实把评测集建起来把切分和检索的手感练出来。地基不牢上面盖什么都是空中楼阁。我个人在实际项目里的体会是RAG 的收益八成来自把数据整理干净、把切分做对、把评测建起来这三件笨功夫剩下两成才是模型和框架的选择。很多人反过来花大量时间比较模型和框架却不肯花半天把文档解析和切分调好结果一直在低水平上打转。先把笨功夫做足你会发现后面的一切都顺了。