
1. 为什么知识获取管道是 AI Agent 从玩具走向工具的分水岭做 AI Agent 开发的人大概都经历过这样一个阶段Demo 跑得飞起一旦接入真实业务数据就开始胡言乱语。你问它公司内部的报销标准它给你编一个看起来很像那么回事的数字你让它查某个产品的技术参数它把两个型号的参数混在一起说得头头是道。这不是模型不够聪明而是它压根不知道你的数据长什么样。知识获取管道要解决的就是这个问题。它的核心任务只有一句话在 Agent 需要做决策或生成回答之前把正确、相关、最新的知识送到模型面前。听起来简单但真正落地时会发现从数据源接入、文档切分、向量化、索引构建到检索排序每一步都有大量需要做决策的地方而这些决策直接决定了最终回答的质量。RAG检索增强生成是目前构建知识获取管道最主流的技术路线。它的基本思路不复杂把知识库里的文档转成向量存起来用户提问时把问题也转成向量然后找最相似的文档片段连同问题一起塞给大模型生成回答。但基本思路不复杂和工程上能跑通之间隔着一条很宽的河。这篇文章面向的是正在用 TypeScript 构建 AI Agent、准备给 Agent 接入知识库的开发者。我会从零开始拆解 RAG 管道的每个环节重点讲清楚每个环节为什么这样设计、有哪些容易踩的坑、以及在 TypeScript 技术栈下怎么落地。文章不会只给你一个Hello World级别的示例而是会覆盖从文档处理到检索优化的完整链路让你看完之后能直接在自己的项目里复现。提示本文假设你已经对 AI Agent 的基本概念有了解知道什么是工具调用、什么是 Agent 循环。如果你还没接触过 Agent 开发建议先补一下基础概念再来看这篇。2. RAG 管道的四个核心环节与数据流转逻辑2.1 从原始文档到可检索知识块的完整链路一条完整的 RAG 管道数据要经过四次形态变化。理解这四次变化比记住任何代码都重要。第一次变化发生在数据接入层。你的知识可能来自 Markdown 文件、PDF、数据库表、API 返回的 JSON甚至是网页抓取的内容。这一层的任务是把这些异构数据统一成纯文本加元数据的格式。元数据包括来源、标题、创建时间、所属分类等这些信息在后续检索和过滤时会派上大用场。第二次变化发生在切分层。原始文档往往很长直接整篇向量化会导致检索粒度太粗。你需要把文档切成大小合适的块chunk每个块独立向量化。切分策略直接决定了检索的精度和召回率这是整个管道里最容易被低估的环节。第三次变化发生在向量化层。每个文本块通过嵌入模型转换成一个高维向量这个向量捕捉了文本的语义信息。向量化之后文本块和它的向量、元数据一起存入向量数据库。第四次变化发生在检索层。用户提问时问题被同样的嵌入模型转成向量然后在向量数据库中查找最相似的若干个块。这些块经过排序和过滤后作为上下文注入到提示词中。这四次变化对应四个核心环节接入、切分、向量化、检索。每个环节都有独立的优化空间而且上游环节的问题会向下游传导。比如切分粒度太粗检索时就会召回大量无关内容元数据没保留好检索时就没法做过滤。2.2 为什么不能把整篇文档直接塞给模型有人可能会想现在大模型的上下文窗口都很大了直接把整个知识库塞进去不就行了何必搞这么复杂这个想法在两种情况下会碰壁。第一种是知识库规模稍大一点比如几百篇文档总 token 数轻松超过百万即使上下文窗口够大推理成本和延迟也会高到无法接受。第二种是知识库更新频繁每次更新都要重新构造完整的上下文这在工程上不可行。更重要的是即使上下文窗口装得下模型对长上下文的注意力也是不均匀的。大量实验表明当上下文中包含大量无关信息时模型对关键信息的提取能力会显著下降。RAG 的价值不仅在于节省 token更在于把模型的注意力聚焦在真正相关的信息上。2.3 TypeScript 技术栈下的模块划分在 TypeScript 项目里我习惯把 RAG 管道拆成四个独立的模块每个模块有清晰的输入输出接口Loader 模块负责从各种数据源读取原始内容输出统一的Document对象数组每个对象包含content和metadata。Splitter 模块接收Document数组输出Chunk数组每个 chunk 包含切分后的文本和继承自父文档的元数据。Embedder 模块接收文本数组输出向量数组。这个模块要处理批量请求、重试、速率限制等问题。Retriever 模块接收查询文本输出排序后的相关 chunk 数组。内部可能包含向量检索、关键词检索、重排序等多个子步骤。这种模块化设计的好处是每个环节都可以独立替换和测试。比如你想从 OpenAI 的嵌入模型换成开源的 BGE 模型只需要改 Embedder 模块的实现其他部分不受影响。3. 文档切分RAG 管道里最容易被做坏的一步3.1 固定长度切分的三个致命问题大多数 RAG 教程教你的第一种切分方式是固定长度切分每 500 个字符切一刀或者每 200 个 token 切一刀。这种方式实现简单但在真实场景下会带来三个严重问题。第一个问题是语义断裂。一个完整的论述可能被从中间切开前半段在一个 chunk 里后半段在另一个 chunk 里。检索时如果只召回了前半段模型看到的就是一个不完整的论述很容易产生误解。第二个问题是噪声引入。固定长度切分不考虑文档的自然结构可能把标题和正文切在一起也可能把两个不相关的段落切在同一个 chunk 里。这些噪声会稀释 chunk 的语义信号降低检索精度。第三个问题是元数据丢失。固定长度切分通常只保留最基础的元数据比如来源文件。但文档内部的层级信息比如这段文字属于哪个章节、哪个小节往往在切分时就丢掉了。这些层级信息在检索时其实很有价值可以用来做上下文扩展或结果过滤。3.2 递归字符切分的实现与参数调优递归字符切分是目前最实用的通用切分策略。它的思路是先尝试用最大的分隔符切分如果切出来的块还是太大就用更小的分隔符继续切直到所有块都在目标大小以内。在 TypeScript 里你可以用 LangChain.js 的RecursiveCharacterTextSplitter也可以自己实现。核心逻辑是维护一个分隔符优先级列表比如[\n\n, \n, 。, , , , , , ]从高优先级到低优先级依次尝试。参数调优方面有几个经验值可以参考。chunkSize设置在 500 到 1000 个字符之间比较合适具体取决于你的文档类型和嵌入模型。技术文档可以小一点因为概念密度高叙述性文档可以大一点因为需要更多上下文才能理解。chunkOverlap设置在chunkSize的 10% 到 20% 之间目的是保证被切断的语义在相邻 chunk 中有重叠减少信息丢失。但递归字符切分也不是万能的。它对格式规整的文档效果好对格式混乱的文档效果一般。如果你的文档有明确的 Markdown 标题层级更好的做法是按标题切分把每个小节作为一个独立的 chunk同时把标题路径作为元数据保留下来。3.3 按文档结构切分Markdown 标题与代码块的处理对于 Markdown 文档我强烈建议按标题层级切分。具体做法是解析 Markdown 的 AST找到所有标题节点然后以标题为边界切分内容。每个 chunk 的元数据里记录完整的标题路径比如[第三章, 3.2 节, 3.2.1 小节]。这样做的好处是检索时可以利用标题路径做过滤。比如用户问的是第三章里的某个概念你可以只在前三章的 chunk 里检索大幅缩小搜索范围。另外标题路径本身也是很好的语义信号可以在向量化时把标题拼接到 chunk 内容前面增强语义表达。代码块的处理需要特别小心。代码块不能被从中间切断否则检索出来的代码片段无法运行。在切分时要把代码块视为原子单元如果代码块本身超过了chunkSize要么单独成块要么按函数或类切分。同时代码块的元数据里要标注语言类型方便后续做语法高亮或过滤。3.4 切分质量的验证方法切分做完之后怎么知道切得好不好我常用的方法是抽样检查加检索测试。抽样检查就是随机抽取 20 到 30 个 chunk人工看一遍检查有没有语义断裂、有没有噪声混入、元数据是否完整。这个步骤很枯燥但能发现很多自动化指标发现不了的问题。检索测试是构造一批查询看检索结果是否合理。具体做法是准备 10 到 20 个问题每个问题你心里有一个标准答案所在的文档位置然后看检索器返回的 top-5 结果里有没有包含这个位置。这个指标叫 hit rate是衡量 RAG 管道质量最直接的指标之一。注意切分策略没有绝对的最优解只有最适合你数据特征的解。不要迷信任何教程里的固定参数一定要用自己的数据做验证。4. 向量化与索引构建选型、批处理与成本控制4.1 嵌入模型选型的四个考量维度选嵌入模型不能只看排行榜上的分数要结合你的实际场景综合考虑四个维度。语言支持是第一个维度。如果你的知识库主要是中文就要选中文语义理解能力强的模型。很多在英文榜单上排名很高的模型中文表现其实一般。目前中文场景下表现比较稳定的有 BGE 系列、M3E 系列以及一些商业 API 提供的中文优化版本。维度与存储成本是第二个维度。嵌入向量的维度从 384 到 3072 不等维度越高向量数据库的存储和检索成本越高。对于大多数场景768 到 1024 维已经足够没必要追求最高维度。推理方式是第三个维度。商业 API 调用简单但按 token 计费数据量大的时候成本可观而且有网络延迟和速率限制。本地部署的模型一次性投入算力后续边际成本低但需要自己处理模型加载、批处理、GPU 显存管理等问题。领域适配是第四个维度。通用嵌入模型在通用语料上表现好但在法律、医疗、金融等专业领域可能不够精准。如果预算允许可以用领域数据对模型做微调或者选择在相关领域预训练过的模型。4.2 批量向量化的并发控制与错误重试向量化是一个 I/O 密集型操作尤其是调用远程 API 时。如果不做并发控制要么串行执行慢得让人抓狂要么并发太高被限流。我的做法是用一个带并发上限的任务队列。在 TypeScript 里可以用p-limit这个库设置并发数为 5 到 10具体取决于 API 的速率限制。每个批次处理 100 到 500 个文本块批次太大容易超时批次太小则请求次数过多。错误重试是必须的。网络抖动、API 临时不可用、速率限制触发这些都会导致请求失败。重试策略建议用指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试 3 到 5 次。如果连续失败就把这批数据记录下来等全部处理完之后再单独重试。还有一个容易被忽略的点是幂等性。如果你的向量化任务中途失败需要重跑要确保已经处理过的数据不会被重复处理。做法是在向量数据库里用文档 ID 加 chunk 索引作为唯一键插入时做 upsert 而不是 insert。4.3 向量数据库的选型对比与本地开发方案向量数据库的选型要考虑数据规模、查询延迟、过滤能力、运维成本等因素。下面是我用过的几种方案对比方案适用规模过滤能力运维成本典型场景内存向量库万级以下弱极低本地开发、DemoSQLite 扩展十万级中等低单机应用、小团队专用向量数据库百万到十亿级强中等生产环境、中大型项目云托管服务弹性强低快速上线、不想运维本地开发阶段我建议用内存向量库或者 SQLite 扩展启动快、零配置、方便调试。等数据量和查询量上来了再迁移到专用向量数据库。迁移时要注意不同数据库的相似度计算方式可能不同余弦相似度和内积相似度的结果会有差异需要重新校准阈值。4.4 索引更新策略全量重建还是增量更新知识库不是一成不变的新文档会加入旧文档会修改或删除。索引更新策略直接影响系统的可用性和数据一致性。全量重建最简单把所有文档重新切分、向量化、写入。缺点是耗时耗资源数据量大时可能几小时都跑不完而且重建期间检索服务不可用。增量更新只处理变化的文档。做法是给每个文档记录内容哈希定期扫描数据源只对哈希变化的文档重新处理。删除文档时根据文档 ID 删除对应的所有 chunk。这种方式效率高但实现复杂度也高需要处理好文档版本管理和并发写入的问题。我的建议是数据量小、更新频率低时用全量重建简单可靠数据量大、更新频繁时用增量更新但要做好监控和回滚机制。5. 检索策略从向量相似度到混合检索的演进5.1 纯向量检索的语义漂移问题纯向量检索的核心假设是语义相似的文本在向量空间里距离近。这个假设在大多数情况下成立但有两种情况会失效。第一种是专有名词和缩写。比如你问ABC 协议的端口号是多少向量检索可能召回一堆讲其他协议的内容因为ABC这个词在嵌入模型训练语料里可能很少出现模型无法准确捕捉它的语义。但关键词检索可以精确匹配ABC直接找到包含这个词的文档。第二种是否定和条件查询。比如你问哪些型号不支持蓝牙向量检索可能召回一堆讲支持蓝牙的型号因为蓝牙这个词的语义权重很高而不支持这个否定语义在向量空间里很难表达。这两种情况说明纯向量检索在精确匹配和逻辑运算方面有天然短板。实际生产系统里纯向量检索的 hit rate 往往在 60% 到 80% 之间剩下的 20% 到 40% 就是这些短板造成的。5.2 关键词检索与向量检索的互补逻辑关键词检索比如 BM25 算法基于词频和逆文档频率计算相关性擅长精确匹配和稀有词查询。它的短板是无法理解同义词和语义相似性你搜电脑它不会返回计算机的文档。向量检索和关键词检索的优缺点是互补的。向量检索擅长语义匹配关键词检索擅长精确匹配。把两者的结果融合起来可以显著提升召回率。这就是混合检索的基本思路。融合的方法有几种。最简单的是加权求和给向量检索和关键词检索的分数各分配一个权重比如 0.7 和 0.3然后按加权分数排序。更精细的做法是 Reciprocal Rank FusionRRF它不依赖原始分数只依赖排名对不同检索器的分数尺度不敏感鲁棒性更好。5.3 重排序模型在管道中的位置与价值混合检索解决了召回问题但召回的结果里仍然可能有很多不相关的。重排序Rerank的作用是对召回结果做精细排序把最相关的排到最前面。重排序模型通常是交叉编码器Cross-Encoder它把查询和文档拼接在一起输入模型直接输出相关性分数。这种方式比向量检索的双编码器Bi-Encoder精度高很多但计算量大无法对全量文档做检索只能对召回结果做重排。在管道里的位置是向量检索和关键词检索各召回 top-20合并去重后得到 30 到 40 个候选然后用重排序模型对这 30 到 40 个候选打分取 top-5 作为最终上下文。这个流程在实测中能把 hit rate 从 70% 左右提升到 90% 以上。重排序模型的选型要考虑语言支持和推理速度。中文场景下BGE-Reranker 系列是比较稳妥的选择。推理速度方面如果候选集是 30 到 40 个用 GPU 推理延迟通常在 100 毫秒以内可以接受。5.4 元数据过滤与查询改写元数据过滤是在检索前缩小搜索范围的手段。比如用户问的是2024 年的销售政策你可以在检索时加上时间过滤条件只搜 2024 年之后的文档。这能大幅减少无关结果提升检索精度。查询改写是在检索前优化查询文本的手段。用户的原始提问可能很口语化或者包含指代和省略。查询改写可以把那个东西怎么用来着改写成XX 功能的使用方法让检索更精准。查询改写的实现方式有几种。最简单的是用大模型做改写把用户问题连同对话历史一起发给模型让它输出一个更适合检索的查询。更复杂的方式是生成多个查询变体分别检索后合并结果这叫多查询检索Multi-Query Retrieval。提示元数据过滤和查询改写都是锦上添花的优化不要一开始就上。先把基础的切分、向量化、混合检索跑通有了 baseline 之后再逐步加这些优化才能准确衡量每个优化的收益。6. 把管道串起来TypeScript 实现中的工程细节6.1 管道编排与错误传播把四个模块串起来的时候错误处理是最容易出问题的地方。一个常见的错误是向量化阶段某个批次失败了但管道没有正确传播这个错误导致后续检索时发现数据不完整但不知道哪里出了问题。我的做法是给每个阶段定义明确的错误类型并且在管道入口处做统一的错误收集。比如LoadError、SplitError、EmbedError、RetrieveError每个错误携带上下文信息比如失败的文档 ID、批次索引、原始错误信息。管道执行时如果某个阶段出错记录错误但继续处理其他数据最后汇总报告。这样做的好处是一次执行可以处理尽可能多的数据而不是遇到第一个错误就中断。对于批量处理场景这能大幅提升效率。6.2 缓存策略嵌入缓存与检索缓存嵌入计算是 RAG 管道里最耗时的环节之一。如果同一段文本被多次向量化就是纯粹的浪费。加一层嵌入缓存可以显著提速。缓存的键可以用文本内容的哈希值是对应的向量。缓存可以放在内存里也可以持久化到磁盘或 Redis。内存缓存适合开发阶段持久化缓存适合生产环境因为服务重启后缓存不会丢失。检索缓存是另一层优化。如果两个用户问了相似的问题检索结果可能高度重叠。缓存查询向量到检索结果的映射可以避免重复的向量检索和重排序计算。但检索缓存的失效策略要设计好知识库更新后要及时清理相关缓存。6.3 可观测性日志、指标与追踪RAG 管道上线之后你需要知道它运行得好不好。可观测性包括三个层面日志、指标、追踪。日志记录每个阶段的输入输出摘要比如加载了多少文档、切分了多少 chunk、向量化了多少文本、检索返回了多少结果。日志级别要可配置开发时用 debug 级别看细节生产时用 info 级别看概况。指标是聚合后的量化数据比如平均切分耗时、向量化成功率、检索平均延迟、hit rate 等。这些指标可以接入监控系统设置告警阈值。追踪是记录单个请求的完整链路从用户提问到最终回答每个阶段的耗时和中间结果都记录下来。这对于排查为什么这个问题的回答质量差这类问题非常有用。6.4 一个可复用的管道类设计基于上面的讨论我通常会设计一个RAGPipeline类把四个模块组合起来对外暴露ingest和query两个方法。ingest方法接收数据源配置依次执行加载、切分、向量化、索引写入返回处理报告。query方法接收查询文本和可选的过滤条件执行查询改写、混合检索、重排序返回最终的上下文块数组。这个类的构造函数接收四个模块的实例支持依赖注入。这样在测试时可以用 mock 模块替换真实模块在生产时可以用真实模块。模块之间的接口要定义清楚用 TypeScript 的 interface 来约束保证替换实现时不会破坏管道。7. 实测中遇到的五个典型问题与排查思路7.1 检索结果总是差一点从切分粒度开始排查检索结果不理想时我的排查顺序是先看切分再看嵌入最后看检索策略。切分问题的典型表现是检索到的 chunk 内容不完整或者包含大量无关内容。排查方法是把检索到的 chunk 打印出来人工看一遍。如果 chunk 明显被切断就调整chunkSize和chunkOverlap。如果 chunk 里混入了标题、页眉、页脚等噪声就在切分前做数据清洗。嵌入问题的典型表现是语义相似的查询和文档向量距离却很远。排查方法是构造几个已知相关的查询-文档对计算它们的向量相似度看是否在合理范围内。如果相似度普遍偏低可能是嵌入模型不适合你的语言或领域需要换模型。检索策略问题的典型表现是相关文档在候选集里但排名很靠后。排查方法是看召回结果和重排序结果的对比如果召回里有但重排后没了说明重排序模型有问题如果召回里就没有说明召回策略需要调整。7.2 向量化成本失控批量大小与模型选择的权衡向量化成本主要来自 API 调用费用或 GPU 算力消耗。控制成本的手段有几个。批量大小要合理。批量太小请求次数多每次请求的固定开销占比高批量太大单次请求 token 数多可能触发 API 的最大 token 限制。我的经验是每批 100 到 200 个文本块每个块 500 到 1000 字符这样单次请求的 token 数在 5 万到 20 万之间大多数 API 都能接受。模型选择要匹配场景。如果只是做语义搜索中等维度的模型就够了没必要用最高维度的。如果预算紧张可以考虑本地部署开源模型一次性投入 GPU 算力后续边际成本几乎为零。缓存要充分利用。开发阶段反复调试时嵌入缓存能省下大量重复计算。生产环境如果知识库更新不频繁缓存命中率会很高。7.3 中文分词的坑嵌入模型与关键词检索的配合中文没有天然的空格分隔关键词检索需要先分词。分词的粒度直接影响 BM25 的效果。分得太细单个字或词的信息量不足分得太粗匹配精度下降。我常用的中文分词方案是 jieba 或者 nodejieba支持自定义词典。对于专业领域的术语要把它们加入自定义词典避免被切碎。比如检索增强生成如果不加词典可能被切成检索增强生成三个词匹配时就会丢失这个术语的整体语义。嵌入模型方面中文场景要选在中文语料上训练过的模型。有些模型虽然支持多语言但中文表现明显弱于英文。选型时要用中文数据做实际测试不要只看模型卡上的多语言支持声明。7.4 检索延迟优化从索引结构到缓存层级检索延迟直接影响用户体验。优化手段从底层到上层依次是索引结构、检索算法、缓存。索引结构方面向量数据库通常支持多种索引类型比如扁平索引、IVF 索引、HNSW 索引。扁平索引精度最高但速度最慢HNSW 索引速度快但精度略有损失。数据量小时用扁平索引数据量大时用 HNSW。检索算法方面混合检索的两路召回可以并行执行用Promise.all同时发起向量检索和关键词检索总延迟取决于较慢的那一路而不是两路之和。缓存方面查询向量缓存和检索结果缓存都能显著降低延迟。对于高频查询缓存命中时延迟可以降到毫秒级。7.5 知识库更新后的检索不一致知识库更新后有时会出现新旧数据混在一起、检索结果不一致的问题。这通常是索引更新策略的问题。如果是全量重建要确保重建期间检索服务要么暂停要么指向旧索引重建完成后再切换。切换时要用原子操作避免部分请求走新索引、部分走旧索引。如果是增量更新要处理好文档的增删改。新增文档直接插入修改文档先删旧 chunk 再插新 chunk删除文档根据 ID 删除所有相关 chunk。这些操作要保证原子性避免出现旧 chunk 已删、新 chunk 未插的中间状态。还有一个容易忽略的点是嵌入模型的版本一致性。如果更新时换了嵌入模型新旧向量就不在同一个向量空间里相似度计算会完全失效。换模型时必须全量重建索引不能增量更新。8. 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 管道跑通之后下一步的优化方向是让检索变得更智能、更自适应。这就是 Agentic RAG 的思路把检索本身也交给 Agent 来决策。具体来说Agentic RAG 会让 Agent 根据问题类型决定检索策略。简单的事实性问题走单次检索复杂的多跳问题走迭代检索需要对比的问题走多路检索。Agent 还可以判断检索结果是否足够如果不够就改写查询再检索一轮。另一个方向是 GraphRAG把知识库构建成图结构实体和关系作为节点和边。检索时不仅找相似的文本块还沿着图结构扩展相关实体能回答需要多跳推理的问题。GraphRAG 的构建成本比基础 RAG 高很多适合知识密度高、关系复杂的场景。还有一个方向是自适应切分根据文档内容动态决定切分粒度。技术文档切细一点叙述文档切粗一点代码块单独处理。这需要在切分前对文档做分类实现复杂度较高但效果提升明显。这些进阶方向都建立在基础 RAG 管道之上。基础管道没跑通直接上这些高级方案只会让问题更难排查。我的建议是先把本文讲的四个环节做扎实有了稳定的 baseline 和可观测性之后再逐步引入更复杂的策略。我在实际项目里的体会是RAG 管道的质量提升不是线性的而是阶梯式的。每解决一个瓶颈效果会明显提升一截然后进入平台期直到找到下一个瓶颈。切分、嵌入、检索、重排序每个环节都可能是瓶颈排查时要有耐心用数据说话不要凭感觉调参。