首页 / 资讯中心 / 文章详情

AI Agent知识获取管道:TypeScript实现RAG检索增强生成全链路

AI Agent知识获取管道:TypeScript实现RAG检索增强生成全链路 ★ FEATURED ARTICLE
1. 为什么知识获取管道是 AI Agent 落地的第一道坎做 AI Agent 开发的人绕不开一个现实问题模型本身很聪明但它不知道你公司内部的业务规则、不知道你昨天刚更新的产品文档、更不知道你私有的那套数据表结构。你问它一个关于内部流程的问题它要么一本正经地胡说八道要么直接告诉你“我无法回答”。这不是模型不行而是它的知识边界就停在那里。知识获取管道要解决的就是这件事——把外部知识在合适的时机、以合适的形式喂给模型让它基于真实资料来回答。而RAG检索增强生成就是目前工程上最成熟、性价比最高的一条路径。它的核心思路用一句话说清楚先把知识切片存起来用户提问时先检索出最相关的片段再把这些片段塞进提示词里让模型生成答案。我见过太多团队一上来就想着微调模型结果数据准备了两周、训练跑了三天上线后发现业务文档一天改三版微调根本追不上变化。RAG 的好处在于知识库和模型解耦——文档更新了重新灌一次向量库就行模型完全不用动。这也是为什么从 2024 年到现在几乎所有企业级 AI Agent 项目的第一期都是先搭一条 RAG 管道。这篇文章是“走进 AI Agent”系列的第四篇前面几篇聊了 Agent 的整体架构、工具调用和记忆机制这一篇专门把知识获取管道拆开讲透。我会用TypeScript作为主要实现语言因为它在前后端通吃、类型系统能帮你在管道各环节少踩很多坑。内容会覆盖从文档加载、切片、向量化、存储、检索到重排的完整链路也会聊到实际项目里那些文档不会告诉你的坑。不管你是刚接触 RAG 的新手还是已经搭过一版想优化的老手应该都能从里面找到能直接用的东西。2. RAG 管道的五个核心环节与数据流转2.1 从原始文档到可检索片段的完整链路一条标准的 RAG 管道数据要经过五个环节才能变成模型能用的知识。我用一个实际的企业知识库场景来串一遍假设你有一批产品手册 PDF、一批 Markdown 格式的 FAQ、还有一堆 Confluence 导出的 HTML 页面。第一个环节是加载Loading。不同格式的文档需要不同的解析器。PDF 要用 pdf-parse 或 pdfjs 提取文本HTML 要用 cheerio 或 jsdom 清洗标签Markdown 相对简单直接读就行。这一步的坑在于 PDF 里的表格和分栏排版直接提取出来的文本顺序经常是乱的后面切片就会切出莫名其妙的片段。第二个环节是切片Chunking。把长文档切成一段段适合检索的小块。切片的粒度直接决定检索质量——切太大一个片段里混了好几个主题检索出来噪音多切太小一个完整的逻辑被切断模型拿到手里拼不出完整意思。常见的做法是按字符数切比如 500 到 1000 字符一块块之间留 10% 到 20% 的重叠防止关键信息正好卡在边界上被切掉。第三个环节是向量化Embedding。把每个文本块通过嵌入模型转成一串浮点数向量。语义相近的文本向量在空间里的距离也近。这一步决定了你的检索是“按关键词匹配”还是“按语义匹配”。比如用户问“怎么退款”文档里写的是“申请退货流程”关键词对不上但语义很近向量检索就能命中。第四个环节是存储Storage。向量要存进向量数据库同时把原始文本和元数据来源文件、页码、章节标题一起存进去。检索出来向量后你得能拿回对应的原文片段。常见的选型有 pgvector、Qdrant、Milvus、Chroma 等小项目用 pgvector 就够了不用额外维护一套数据库。第五个环节是检索与重排Retrieval Rerank。用户提问时先把问题也向量化然后在向量库里找最相似的 Top-K 个片段。但纯向量检索有个问题它偏向语义相似有时候会漏掉关键词精确匹配的结果。所以工程上通常会做混合检索——向量检索和关键词检索BM25各出一批结果再用 RRF倒数排名融合合并。合并之后还可以加一层重排模型Reranker用交叉编码器对候选片段重新打分把真正最相关的排到前面。2.2 为什么每个环节都不能偷懒很多人搭第一版 RAG 的时候喜欢用现成的框架一把梭LangChain 或者 LlamaIndex 几行代码就跑通了。跑通没问题但上线之后效果不好你根本不知道是哪个环节出了问题。是切片切坏了是嵌入模型选错了还是检索策略太单一我的经验是每个环节都要能单独观测和调试。切片结果要能导出来人工看嵌入向量要能算相似度验证检索结果要能打印出分数和来源。这样出问题的时候你能快速定位到具体环节而不是对着一个黑盒干瞪眼。还有一个容易被忽略的点元数据的设计。很多人只存文本和向量检索出来一个片段不知道它来自哪个文件、哪一页。这在调试和展示引用来源时非常致命。我的做法是每个片段都带上source文件路径、chunkIndex块序号、heading所属章节标题这几个字段检索结果直接能拼出“根据《产品手册》第 3 章……”这样的引用。3. 用 TypeScript 搭一条最小可用的 RAG 管道3.1 项目结构与依赖选型先说一下技术选型。嵌入模型我推荐用 OpenAI 的text-embedding-3-small1536 维价格便宜效果对大多数场景够用。如果你要完全本地化可以用bge-m3或者nomic-embed-text通过 Ollama 跑。向量库小项目直接用 pgvectorPostgres 你本来就要用不用多维护一个组件。TypeScript 项目结构大概长这样rag-pipeline/ src/ loaders/ # 各种格式的文档加载器 chunker.ts # 切片逻辑 embedder.ts # 向量化封装 store.ts # 向量库读写 retriever.ts # 检索与重排 pipeline.ts # 串联全流程 data/ # 原始文档 scripts/ ingest.ts # 灌数据脚本 query.ts # 查询测试脚本依赖方面核心就几个openai做嵌入pg连 Postgrespdf-parse解析 PDFcheerio清洗 HTML。不需要一上来就上 LangChain自己写反而更可控。3.2 文档加载与清洗的实操细节加载这一步我踩过最大的坑是 PDF。很多 PDF 是扫描件pdf-parse提取出来是空的这种情况必须上 OCR但 OCR 成本高、速度慢能避免就避免。还有一种情况是 PDF 里有多栏排版提取出来的文本是左右栏交错混在一起的读起来完全不通。我的处理方式是如果发现提取文本的换行特别频繁且句子不连贯就标记这个文件需要人工检查。HTML 清洗相对简单用 cheerio 把script、style、nav、footer这些标签去掉只留正文。但要注意有些文档站点的正文在article标签里有些在main里得针对性地写选择器。Markdown 最省心直接读文本就行但要注意代码块和表格不要被切片逻辑破坏。我的做法是在切片前先把代码块和表格用占位符替换掉切完再还原这样能保证它们作为整体不被切断。3.3 切片策略固定长度还是语义切分切片策略我试过三种说说各自的适用场景。固定长度切片最简单按字符数切每 800 字符一块重叠 100 字符。优点是实现快、结果可预测。缺点是经常把一个完整段落从中间切断检索出来的片段读起来没头没尾。按分隔符切片是固定长度的改进版优先在段落边界\n\n、句子边界。、、切切不到再按字符数硬切。这个策略在大多数场景下效果比纯固定长度好实现也不复杂。语义切片最理想用嵌入模型判断相邻句子是否属于同一主题主题变了就切一刀。效果最好但成本高——每个句子都要算一次嵌入。我一般只在知识库质量要求特别高的场景才用。实际项目里我推荐按分隔符切片 固定长度兜底的组合。代码大概这样function chunkText(text: string, maxLen 800, overlap 100): string[] { const paragraphs text.split(/\n\n/); const chunks: string[] []; let current ; for (const para of paragraphs) { if ((current para).length maxLen current.length 0) { chunks.push(current.trim()); // 保留尾部重叠防止边界信息丢失 current current.slice(-overlap) para; } else { current (current ? \n\n : ) para; } } if (current.trim()) chunks.push(current.trim()); return chunks; }注意重叠部分不要设太大否则向量库里会有大量重复内容检索时同一段信息反复出现反而稀释了其他相关内容的权重。10% 到 15% 的重叠比例比较合适。3.4 向量化与入库批量处理与错误重试向量化这一步核心是批量处理和错误重试。嵌入 API 一般支持一次传多个文本比如 OpenAI 的接口一次最多传 2048 个输入。批量传能大幅减少网络往返但要注意单批不要太大否则一个失败整批重来。我的做法是每批 100 个片段失败后指数退避重试三次。入库的时候pgvector 的表结构大概这样CREATE TABLE chunks ( id SERIAL PRIMARY KEY, source TEXT NOT NULL, chunk_index INT NOT NULL, heading TEXT, content TEXT NOT NULL, embedding vector(1536) ); CREATE INDEX ON chunks USING ivfflat (embedding vector_cosine_ops);ivfflat索引适合数据量中等几万到几十万条的场景建索引时要注意lists参数一般设为sqrt(行数)左右。数据量再大就得上 HNSW 索引查询更快但建索引更慢、占内存更多。4. 检索质量上不去的四个真实原因4.1 嵌入模型和业务语料不匹配这是最隐蔽也最常见的问题。通用嵌入模型是在海量互联网文本上训练的对日常语言理解很好但你的业务领域可能有大量专有名词、缩写、内部代号。比如你问“XX 项目的验收标准是什么”模型不知道“XX 项目”是什么向量化出来的结果和文档里的“XX 项目”对不上。解决办法有两个一是换一个在你的领域数据上微调过的嵌入模型二是做查询改写——用户提问后先用 LLM 把问题改写成更完整、更明确的表述再拿去检索。比如把“XX 项目验收”改写成“XX 项目的验收标准和验收流程是什么”检索命中率会明显提升。4.2 切片粒度与查询粒度不匹配用户的问题通常很短比如“退款要多久”。但文档里的相关段落可能是一大段关于退款政策的描述有前提条件、有例外情况、有处理时效。如果切片切得太细检索出来的片段只有“处理时效为 3 到 5 个工作日”缺少前提条件模型可能给出错误答案。我的经验是切片粒度要略大于典型查询的粒度。用户问一句话你检索出来的片段应该能覆盖这句话涉及的完整上下文。800 到 1200 字符是比较稳妥的范围具体要看你的文档密度。4.3 纯向量检索漏掉关键词精确匹配向量检索擅长语义相似但对精确的关键词匹配反而不敏感。比如用户问“错误码 E5021 怎么解决”向量检索可能返回一堆关于“错误处理”的通用文档但真正包含“E5021”的那篇反而排在后面。因为“E5021”这个 token 在嵌入模型眼里可能没什么特殊含义。这就是为什么必须做混合检索。BM25 这类关键词检索算法对精确匹配非常敏感把向量检索和 BM25 的结果用 RRF 融合能同时兼顾语义和关键词。RRF 的公式很简单每个文档的得分是1 / (k rank)的累加k 一般取 60。实现起来也就几十行代码。4.4 缺少重排导致 Top-K 里混入噪音检索出 Top-10 个片段不代表这 10 个都相关。向量相似度高不等于真正回答了问题。这时候需要重排模型做二次筛选。重排模型比如bge-reranker系列会把查询和每个候选片段拼在一起输出一个相关性分数比单纯的向量相似度准确得多。重排的代价是速度——交叉编码器要对每个候选片段单独推理10 个片段就要推理 10 次。所以工程上的做法是向量检索先召回 Top-50重排后取 Top-5 送给模型。这样既保证了召回率又控制了延迟。5. 从 Demo 到生产管道工程化的几个关键决策5.1 增量更新与全量重建的取舍知识库不是一次灌完就不管了。文档会更新、会新增、会删除。全量重建简单粗暴但数据量大时耗时很长而且重建期间检索服务不可用。增量更新复杂但能做到近实时。我的做法是给每个文档算一个内容哈希灌数据时对比哈希变了才重新切片和向量化。删除文档时根据source字段把对应的所有片段删掉。这样每次更新只处理变化的文档速度很快。但要注意如果切片策略变了所有文档的哈希都会变相当于全量重建这个要提前规划好。5.2 检索结果的缓存与降级生产环境里同样的查询可能被反复问到。加一层查询缓存能显著降低延迟和成本。缓存的 key 是查询文本的哈希value 是检索结果。但要注意缓存失效——知识库更新后旧缓存要清掉。降级策略也很重要。如果嵌入 API 挂了检索服务不能整个挂掉。我的做法是嵌入失败时降级到纯关键词检索BM25虽然效果差一些但至少服务可用。向量库连接失败时返回一个友好的错误提示而不是让整个 Agent 卡死。5.3 观测指标命中率、延迟与成本RAG 管道上线后必须有一套观测指标。我最关注的三个命中率Hit Rate检索结果里是否包含真正能回答问题的片段。这个需要人工标注一批测试问题定期跑评估。命中率低于 80% 就说明检索环节有问题得回去查切片和嵌入。延迟Latency从用户提问到返回检索结果的时间。嵌入 API 调用、向量检索、重排各占多少要能拆开看。一般来说嵌入 100ms 以内、向量检索 50ms 以内、重排 200ms 以内是比较理想的。成本Cost每次查询消耗的嵌入 token 数和重排调用次数。嵌入成本很低但重排如果每次都跑 Top-50成本会上去。可以通过缓存和动态调整 Top-K 来控制。6. 那些文档不会告诉你的踩坑记录6.1 中文分词的隐藏陷阱做中文 RAG 的时候BM25 检索需要分词。很多人直接用jieba或者nodejieba但中文分词的粒度对检索效果影响很大。比如“人工智能”如果被切成“人工”和“智能”用户搜“人工智能”时 BM25 可能匹配不上。我的做法是维护一个领域词典把业务专有名词加进去保证它们不被切碎。还有一个坑是中英文混合。文档里经常有“使用 TypeScript 开发”这样的句子分词器可能把“TypeScript”切成“Type”和“Script”导致搜“TypeScript”时匹配不到。解决办法是在分词前先用正则把英文单词和数字提取出来作为整体 token。6.2 向量维度和索引参数的调优pgvector 的ivfflat索引有个probes参数控制查询时扫描多少个聚类。probes越大召回率越高但速度越慢。默认值是 1实际用的时候至少要设到sqrt(lists)左右。我一般设lists 100、probes 10在召回和速度之间取平衡。还有一个容易忽略的点向量归一化。如果你用余弦相似度向量必须归一化否则内积和余弦相似度不等价。OpenAI 的嵌入接口返回的向量默认是归一化的但如果你自己跑本地模型一定要检查这一点。6.3 重排模型的延迟与成本平衡重排模型效果好但延迟是实打实的。我实测下来bge-reranker-base在 CPU 上跑 50 个候选片段大概要 1 到 2 秒GPU 上能压到 200ms 以内。如果你的场景对延迟敏感要么上 GPU要么减少候选数量。还有一个取巧的办法只对 Top-20 做重排。向量检索召回 Top-50先用一个轻量级的规则过滤掉明显不相关的比如完全不包含查询关键词的剩下的 20 个再送重排。这样能在几乎不损失效果的前提下把重排成本降一半。6.4 知识库更新后的缓存一致性前面提到查询缓存这里展开说一个具体的坑。知识库更新后如果缓存没清用户会拿到旧结果。但全量清缓存又会导致缓存命中率骤降。我的做法是给缓存加一个版本号知识库每次更新版本号加一查询时带上版本号作为 key 的一部分。这样旧版本的缓存自然失效新版本缓存逐步建立平滑过渡。7. 下一步可以往哪里走RAG 基础管道跑通之后有几个方向可以继续深入。一个是Agentic RAG让 Agent 自己决定什么时候检索、检索什么、检索几次而不是每次查询都固定走一遍管道。另一个是GraphRAG把知识图谱和向量检索结合起来处理多跳推理类的问题。还有就是多模态 RAG把图片、表格也纳入检索范围。但我的建议是先把基础管道做扎实。切片策略调好、混合检索跑通、重排加上、观测指标建起来。这些基础工作做到位效果已经能覆盖 80% 的场景。后面那些高级玩法等基础管道稳定了再逐步叠加不然出了问题你根本分不清是基础环节的锅还是高级特性的锅。我个人在实际项目里的体会是RAG 的效果提升往往不来自某个炫酷的新技术而是来自把每个环节的细节抠到位。切片多调几版、嵌入模型多试几个、检索参数多跑几组对比这些笨功夫才是真正拉开差距的地方。
阅读完成 · 觉得有帮助?
咨询建站