1. 为什么知识获取管道是 AI Agent 落地的第一道坎
做 AI Agent 开发的人,绕不开一个尴尬的现实:模型本身很聪明,但它不知道你公司内部的业务规则、不知道你昨天刚更新的产品文档、更不知道你私有的那几百份技术手册里写了什么。你问它一个通用问题,它对答如流;你问它一个只有你们团队才知道的细节,它要么胡编,要么直接说“我不知道”。这不是模型不行,而是它的知识边界就停在那里。
知识获取管道要解决的就是这个问题。它的核心任务只有一句话:在 Agent 需要回答问题的那个瞬间,把最相关的、最新的、最准确的知识片段,塞进模型的上下文里。这件事听起来简单,做起来全是细节。而RAG(检索增强生成)就是目前最主流、最成熟的一套工程方案。
我见过太多团队在搭建 Agent 时,把 80% 的精力花在 Prompt 调优和工具调用上,结果知识管道做得一塌糊涂——文档切分随意、向量模型选型拍脑袋、检索结果不评估,最后 Agent 回答质量忽高忽低,排查半天发现是检索环节把不相关的内容塞进了上下文。所以这一篇,我想把 RAG 这条管道从设计到落地的完整思路讲清楚,尤其是那些文档里不会写的坑。
这篇文章适合谁看?如果你正在用 TypeScript 搭建 AI Agent,或者你已经在用 LangChain、Spring AI 这类框架但 RAG 效果始终不理想,又或者你只是想知道“RAG 到底是什么、为什么大家都在提”,那这篇内容应该能帮你少走不少弯路。我会尽量用从业者的视角,把原理、选型、实操和踩坑经验揉在一起讲,而不是给你一份干巴巴的 API 文档翻译。
2. RAG 到底在解决什么问题:从模型的知识盲区说起
2.1 大模型的三层知识边界
要理解 RAG 的价值,先得搞清楚大模型的知识是怎么来的。我习惯把它分成三层:
第一层是预训练知识,也就是模型在训练阶段从海量公开语料里学到的内容。这部分知识覆盖面广,但有两个致命问题:一是有截止日期,训练完成之后发生的事情它一概不知;二是它是“压缩”过的,细节容易丢失或混淆。
第二层是微调知识,通过特定领域的数据对模型进行再训练,让它更懂某个垂直领域。但微调成本高、周期长,而且每次知识更新都要重新训练,对于快速变化的业务场景根本不现实。
第三层就是上下文知识,也就是在推理时通过 Prompt 注入的信息。RAG 本质上就是在做这一层——它不改变模型本身,而是在每次提问时动态地把相关知识“喂”给模型。
提示:很多人会把 RAG 和微调混为一谈。简单区分:微调是“让模型学会一种能力或风格”,RAG 是“让模型在回答时能看到正确的资料”。知识频繁更新的场景,优先考虑 RAG。
2.2 没有 RAG 的 Agent 会怎样
我拿一个真实场景举例。假设你做了一个企业内部的技术支持 Agent,用户问:“我们的订单服务在高峰期超时,应该调整哪个参数?”
没有 RAG 的情况下,模型可能会给你一段通用的微服务调优建议,听起来很有道理,但完全没用到你们内部的配置文档。更糟的情况是,它会编造一个根本不存在的参数名,因为它在预训练数据里见过太多类似的技术文档,把不同系统的内容混在了一起。
有了 RAG 之后,流程变成:用户提问 → 系统在知识库中检索“订单服务 超时 参数配置”相关文档 → 找到你们内部那篇《订单服务性能调优指南》 → 把相关段落注入上下文 → 模型基于真实文档给出准确回答。
这就是知识获取管道的价值:它让 Agent 的回答有据可查,而不是靠模型的“记忆”去猜。
2.3 RAG 管道的四个核心环节
一条完整的 RAG 管道,不管用什么框架实现,本质上都包含四个环节:
| 环节 | 核心任务 | 常见问题 |
|---|---|---|
| 文档加载 | 把各种格式的原始资料读进来 | 格式解析失败、编码乱码 |
| 文本切分 | 把长文档拆成适合检索的片段 | 切得太碎丢上下文,切得太大检索不准 |
| 向量化与存储 | 把文本转成向量并建立索引 | 模型选型不当、索引结构不合理 |
| 检索与注入 | 根据问题找到最相关的片段 | 召回率低、相关性差、上下文超长 |
这四个环节环环相扣,任何一个环节出问题,最终的回答质量都会大打折扣。接下来我会逐个拆解,重点讲每个环节里那些容易踩的坑。
3. 文档加载与切分:RAG 管道里最容易被低估的环节
3.1 文档加载不只是“读文件”
很多人觉得文档加载就是调个 API 把文件读进来,能有多难?实际操作中,这一步的坑多得超出想象。
首先是格式问题。你的知识库可能包含 PDF、Word、Markdown、HTML、Excel,甚至扫描件。PDF 尤其麻烦——有些 PDF 是文本型的,直接解析就行;有些是图片扫描的,需要 OCR;还有些 PDF 的排版极其复杂,表格、多栏、脚注混在一起,解析出来顺序全乱。
我在 TypeScript 项目里常用的方案是pdf-parse处理文本型 PDF,遇到扫描件就得上 OCR 服务。但即便这样,表格数据的提取仍然是个难题。我的经验是:如果知识库里表格很多,不要指望自动解析能完美还原,关键表格建议人工整理成结构化数据单独存储。
其次是编码问题。中文文档经常遇到 GBK 和 UTF-8 混用的情况,读进来全是乱码。这个没有捷径,只能在上传环节做编码检测和转换。
还有一个容易被忽略的点:文档的元数据。每份文档的来源、更新时间、所属分类、权限级别,这些信息在后续检索时非常有用。比如你可以根据时间过滤掉过期的文档,或者根据权限控制不同用户能检索到的内容。建议在加载阶段就把这些元数据一并提取并存储。
3.2 文本切分的艺术:粒度决定成败
文本切分是 RAG 管道里最需要经验判断的环节。切得太碎,每个片段缺乏完整语义,检索出来模型看不懂;切得太大,一个片段里混了多个主题,检索精度下降,还会浪费上下文窗口。
我一般会遵循几个原则:
按语义边界切分,而不是按固定字数。最简单的做法是按段落切,因为段落本身就是作者划分的语义单元。如果段落太长,再按句子切。TypeScript 里可以用LangChain的RecursiveCharacterTextSplitter,它支持按分隔符优先级递归切分,效果比固定长度好很多。
设置合理的重叠区域。相邻片段之间保留 10% 到 20% 的重叠,可以避免关键信息刚好被切断的情况。比如一个 500 字的片段,重叠 50 到 100 字。
根据内容类型调整策略。技术文档适合按章节切,FAQ 适合按问答对切,代码文档适合按函数或类切。不要指望一套参数打天下。
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 80, separators: ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], }); const chunks = await splitter.splitText(documentContent);这段代码里,separators的顺序很关键。它会优先按双换行(段落)切,如果切出来还太大,再按单换行、句号、感叹号依次降级。中文场景下把中文标点加进去很重要,否则会按空格切,中文文档里空格很少,等于没切。
3.3 切分策略的实测对比
我在一个技术文档知识库上做过对比测试,同样的 200 份文档,三种切分策略的检索命中率差异明显:
| 切分策略 | 片段平均长度 | 检索命中率 | 回答准确率 |
|---|---|---|---|
| 固定 500 字 | 500 字 | 62% | 58% |
| 按段落切分 | 约 320 字 | 78% | 74% |
| 按段落+重叠 80 字 | 约 380 字 | 85% | 81% |
这个数据不是绝对的,但趋势很清楚:尊重语义边界的切分策略,效果明显优于机械的固定长度切分。重叠区域虽然增加了存储和检索成本,但对命中率的提升是值得的。
注意:切分参数没有万能解。文档类型变了、问题类型变了,最优参数也会变。建议在项目初期就建立一套评估集,用真实问题去测试不同切分策略的效果,而不是凭感觉调参。
4. 向量化与检索:选型逻辑比参数调优更重要
4.1 嵌入模型怎么选
文本向量化就是把一段文字转成一串数字(向量),让语义相近的文字在向量空间里距离更近。这一步的核心是嵌入模型的选择。
市面上嵌入模型很多,选型时我主要看三个维度:
语言支持。如果你的知识库以中文为主,必须选中文能力强的模型。有些英文模型在中文上表现很差,语义相近的中文句子向量距离可能很远。
维度与性能的平衡。向量维度越高,表达能力越强,但存储和计算成本也越高。常见的有 768 维、1024 维、1536 维。对于大多数企业知识库场景,1024 维左右通常够用。
部署方式。是用云端 API 还是本地部署?云端 API 省事但有调用成本和数据隐私顾虑;本地部署可控但需要 GPU 资源。我的建议是:原型阶段用云端 API 快速验证,生产环境根据数据敏感度和调用量决定是否本地化。
在 TypeScript 项目里,如果用的是 OpenAI 兼容的嵌入接口,可以直接用openai包调用。如果是本地模型,可以用@xenova/transformers在 Node.js 里跑推理,虽然速度慢一些,但胜在零依赖。
4.2 向量数据库的选型思路
向量存到哪里?这也是个绕不开的问题。我按不同规模给个参考:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 原型验证、小规模 | 内存向量存储 | 零配置,改代码即生效 |
| 中等规模、单机 | SQLite + 向量扩展 | 轻量,无需额外服务 |
| 生产环境、大规模 | 专用向量数据库 | 支持分布式、高并发、复杂过滤 |
在 TypeScript 生态里,LangChain提供了MemoryVectorStore用于快速验证,生产环境可以接Pinecone、Weaviate、Qdrant等。如果不想引入额外服务,pgvector也是个很务实的选择——直接用现有的 PostgreSQL 就能搞定。
我个人的经验是:不要一上来就上重型向量数据库。很多团队的知识库规模其实只有几万到几十万个片段,用pgvector或者轻量方案完全够用。等真正遇到性能瓶颈再迁移,成本远低于一开始就过度设计。
4.3 检索策略:从“能查到”到“查得准”
基础的向量检索是拿用户问题的向量,去向量库里找距离最近的 Top-K 个片段。但实际用起来,单纯靠向量相似度经常不够准。
混合检索是我最推荐的改进方向。它把向量检索和关键词检索(比如 BM25)结合起来,既能捕捉语义相似,又能保证关键词精确匹配。比如用户问“TypeScript 7.0 弃用了哪些选项”,关键词检索能精确找到包含“TypeScript 7.0”和“弃用”的文档,而向量检索能理解“弃用”和“停止运行”是相近的意思。两者结合,召回率和准确率都会提升。
重排序是另一个有效手段。先通过向量检索召回比如 20 个候选片段,再用一个重排序模型对这 20 个片段做精细打分,选出最相关的 5 个注入上下文。重排序模型通常比嵌入模型更重,但只对少量候选做计算,成本可控。
// 混合检索的简化示意 const vectorResults = await vectorStore.similaritySearch(query, 20); const keywordResults = await keywordSearch(query, 20); // 合并去重后重排序 const merged = deduplicate([...vectorResults, ...keywordResults]); const reranked = await rerankModel.rank(query, merged); const topChunks = reranked.slice(0, 5);这段逻辑看起来简单,但每一步都有讲究。比如合并去重时怎么判断两个片段是“同一个”,重排序模型怎么选,Top-K 的 K 设多少合适,都需要根据实际数据去调。
4.4 检索质量的评估不能靠感觉
我见过太多团队做完 RAG 之后,随便问几个问题觉得“还行”就上线了。结果用户一问稍微偏一点的问题,回答质量就崩了。
建立评估集是必须的。不需要很复杂,准备 50 到 100 个真实用户问题,每个问题标注好“正确答案应该来自哪几个文档片段”。然后每次调整切分策略、换嵌入模型、改检索参数,都跑一遍评估集,看命中率和准确率的变化。
关键指标有两个:召回率(相关片段有没有被检索出来)和精确率(检索出来的片段有多少是真正相关的)。这两个指标往往此消彼长,需要根据业务场景找平衡点。比如客服场景更怕漏掉关键信息,就偏向高召回;而法律文书场景更怕引入不相关内容,就偏向高精确。
5. 从检索结果到模型回答:上下文注入的工程细节
5.1 上下文窗口不是越大越好
检索到相关片段之后,下一步是把它们拼成 Prompt 注入模型。这里有个常见误区:觉得塞得越多越好。实际上,上下文太长会带来三个问题:
一是成本上升,Token 数量直接决定调用费用;二是噪声增加,不相关的片段会干扰模型判断;三是**“迷失在中间”现象**,有研究表明模型对上下文中间部分的信息利用率明显低于开头和结尾。
我的做法是:检索阶段多召回,注入阶段精选。比如检索 20 个候选,重排序后只取最相关的 3 到 5 个注入。每个片段的长度也要控制,太长的片段可以再截取核心部分。
5.2 Prompt 模板的设计要点
把检索结果注入 Prompt 时,模板设计直接影响回答质量。一个我常用的结构是这样的:
const prompt = `你是一个技术支持助手。请根据以下参考资料回答用户问题。 如果参考资料中没有相关信息,请明确告知用户,不要编造。 参考资料: ${chunks.map((c, i) => `[${i + 1}] ${c.content}`).join("\n\n")} 用户问题:${question} 回答要求: 1. 只使用参考资料中的信息 2. 引用时标注来源编号 3. 如果资料不足,说明缺少什么信息`;这个模板里有几个关键设计:明确告知“不要编造”,减少幻觉;要求标注来源,方便追溯和验证;允许说“不知道”,避免强行回答。这些细节看起来不起眼,但对实际效果影响很大。
5.3 引用溯源:让回答可信
在知识库场景里,用户往往不只需要答案,还需要知道答案从哪来。所以我在注入上下文时,会给每个片段带上来源信息(文档名、章节、更新时间),并要求模型在回答时引用。
这样做有两个好处:一是用户可以直接去原文核对,增加信任感;二是当回答出错时,能快速定位是检索环节的问题还是模型理解的问题。
实现上,可以在片段存储时就带上元数据,检索出来后一并注入。如果用的是LangChain的Document对象,metadata字段就是干这个的。
6. 踩坑实录:那些让我熬夜排查的 RAG 问题
6.1 检索结果“看起来相关”但回答不对
有一次我遇到一个诡异的问题:检索出来的片段明明和问题很相关,但模型回答就是不对。排查了半天,发现是片段切分把关键信息切断了。用户问的是“配置文件的超时参数怎么设置”,检索到的片段包含了“超时参数”这个词,但具体的数值和配置方法在下一个片段里。
这个问题的根因是切分时没有保留足够的上下文。解决办法有两个:一是增加重叠区域,二是检索时不仅返回命中的片段,还返回它前后相邻的片段。我后来在检索环节加了一个“上下文扩展”步骤,命中片段前后各多取一个片段一起注入,问题就解决了。
6.2 中文标点导致的切分异常
另一个坑是中文标点。早期我用默认的切分器,分隔符只有英文标点和空格。结果中文文档切出来的片段长度极不均匀,有的几百字,有的只有几个字。后来把中文的句号、问号、感叹号、分号都加进分隔符列表,切分质量立刻上了一个台阶。
提示:中文场景下,切分器的分隔符列表一定要包含中文标点。这个细节很多英文文档不会提,但实际影响很大。
6.3 向量模型换了一个,全部要重建
嵌入模型一旦更换,之前所有文档的向量都要重新生成。如果知识库很大,这个重建过程可能要好几个小时。我踩过的坑是:没有把向量生成做成可配置、可批量重跑的流程,换模型时手动跑脚本,跑到一半断了又得重来。
后来我改成用队列任务的方式,每个文档的向量化作为一个独立任务,支持断点续跑和进度查询。虽然前期多花了一点功夫,但后续迭代效率高了很多。
6.4 检索延迟忽高忽低
生产环境上量之后,发现检索延迟不稳定,有时候几十毫秒,有时候好几秒。排查后发现两个原因:一是向量数据库的索引没有预热,冷启动时慢;二是并发请求上来之后,嵌入模型的调用成了瓶颈。
解决办法:向量索引提前加载到内存;嵌入模型调用加缓存,相同或相似的查询直接命中缓存;对检索结果做分页和超时控制,避免单个慢查询拖垮整个请求。
7. 把 RAG 管道串起来:一个可落地的 TypeScript 实现思路
7.1 整体架构分层
把前面讲的环节串起来,一个完整的 RAG 管道在 TypeScript 里可以分成四层:
数据层负责文档的加载、解析、切分和向量化,产出带元数据的片段集合。这一层通常是离线运行的,可以做成定时任务或手动触发。
存储层负责向量和元数据的持久化。向量存向量库,元数据存关系库或文档库,两者通过 ID 关联。
检索层负责接收查询,执行混合检索和重排序,返回精选片段。这一层是在线服务,对延迟敏感。
生成层负责组装 Prompt、调用模型、处理流式输出和引用溯源。
分层的好处是每层可以独立替换和优化。比如换嵌入模型只影响数据层,换重排序策略只影响检索层,互不干扰。
7.2 关键代码结构
// 数据层:文档处理管道 interface DocumentProcessor { load(source: string): Promise<RawDocument>; split(doc: RawDocument): Promise<TextChunk[]>; embed(chunks: TextChunk[]): Promise<EmbeddedChunk[]>; } // 检索层:混合检索 interface Retriever { retrieve(query: string, topK: number): Promise<ScoredChunk[]>; rerank(query: string, chunks: ScoredChunk[]): Promise<ScoredChunk[]>; } // 生成层:上下文组装与模型调用 interface Generator { buildPrompt(query: string, chunks: ScoredChunk[]): string; generate(prompt: string): Promise<GeneratedAnswer>; }这套接口看起来简单,但每个方法的实现都有很多细节。比如split方法要根据文档类型选择不同的切分策略,retrieve方法要处理向量检索和关键词检索的融合,buildPrompt要控制上下文长度和格式。
7.3 迭代节奏的建议
我的建议是分三步走:
第一步,跑通最小闭环。用最简单的切分、最基础的向量检索,先把“文档进、回答出”的流程跑通。这个阶段不要追求效果,重点是验证架构可行。
第二步,建立评估集。准备真实问题和标注,有了评估集才能量化每次优化的效果。这一步很多人跳过,结果后面调优全靠感觉。
第三步,逐环节优化。根据评估结果,看是切分问题、检索问题还是生成问题,有针对性地改进。每次只改一个变量,改完跑评估集对比。
这个节奏看起来慢,但实际上比盲目调参快得多。我见过太多团队在没有任何评估的情况下反复改 Prompt,改了几十版也不知道到底哪版更好。
8. 关于 RAG 进阶方向的一些个人判断
RAG 这个领域变化很快,但有些方向我觉得是值得关注的。
Agentic RAG是最近讨论比较多的概念。传统 RAG 是“一次检索、一次生成”,而 Agentic RAG 让 Agent 自己决定什么时候检索、检索什么、要不要多轮检索。比如 Agent 可以先判断问题类型,如果是简单事实查询就直接检索,如果是复杂推理就拆成多个子问题分别检索再综合。这个方向对复杂场景很有价值,但实现复杂度也高不少。
GraphRAG则是把知识图谱和 RAG 结合起来,解决传统 RAG 难以处理“跨文档关联推理”的问题。比如问“A 项目的负责人还负责过哪些项目”,传统 RAG 很难回答,因为信息分散在多个文档里。GraphRAG 通过实体和关系抽取构建图谱,能更好地处理这类问题。不过它的构建成本高,适合知识关联性强的场景。
我的判断是:大多数企业场景,把基础 RAG 做扎实,效果已经能满足 80% 的需求。进阶方案是在基础方案遇到明确瓶颈时才考虑,而不是一上来就追求最新最复杂的技术。
最后分享一个我在实际项目中体会最深的一点:RAG 的效果上限不取决于模型有多强,而取决于你的知识库质量有多高。文档本身写得乱、更新不及时、格式不统一,再好的检索算法也救不回来。所以在搭建管道之前,先把知识库治理好,这一步的投入回报比任何技术优化都高。