ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TypeScript 实战:AI Agent 知识获取管道与 RAG 检索增强生成落地指南

TypeScript 实战:AI Agent 知识获取管道与 RAG 检索增强生成落地指南 1. 为什么知识获取管道是 AI Agent 的分水岭做 AI Agent 开发的人迟早会撞上一堵墙模型本身很聪明但它不知道你公司内部的 API 文档、不知道你上周刚改的产品规格、不知道你私有的业务规则。你问它它要么一本正经地胡说八道要么礼貌地告诉你“我无法获取实时信息”。这不是模型不行而是它的知识被冻结在训练截止的那一天。知识获取管道就是解决这个问题的。它的核心思路是在模型生成回答之前先去外部知识库里检索相关内容把检索结果作为上下文塞进提示词再让模型基于这些上下文来回答。这套方法有个更广为人知的名字——RAGRetrieval-Augmented Generation检索增强生成。我接触 RAG 是从一个内部知识问答需求开始的。当时团队有几百份技术文档散落在各个仓库里新人找一个配置项要翻半天。最初想的是微调模型但算了一下成本标注数据、训练、迭代、部署周期至少一个月而且文档一更新就得重来。后来换成 RAG两天跑通原型一周上线文档更新只需要重新索引模型完全不用动。这个对比让我彻底认清了 RAG 在 AI Agent 体系里的位置——它不是可选项而是知识获取管道的基础设施。这篇文章适合谁看如果你正在用 TypeScript 做 AI Agent 开发想搞清楚 RAG 到底怎么落地或者你已经用过一些 RAG 框架但总觉得效果不稳定那这篇内容就是为你准备的。我会从整体设计思路讲到具体实现细节包括分块策略、向量化、检索排序、上下文组装这些关键环节也会分享我在实际项目中踩过的坑和总结出来的调参经验。代码示例以 TypeScript 为主因为这是目前 Agent 开发中最主流的技术栈之一。提示RAG 不是银弹。它解决的是“模型不知道”的问题不解决“模型不会推理”的问题。如果你的任务本身需要复杂逻辑推理RAG 只能提供素材最终还是要靠模型自己的能力。2. RAG 知识获取管道的整体设计与核心思路2.1 从“一次性问答”到“管道化流程”的思维转变很多人第一次接触 RAG脑子里想的是“用户提问 → 检索文档 → 拼进提示词 → 模型回答”这么一条直线。这个理解没错但太粗糙了。真正在生产环境跑起来的 RAG是一个多阶段的管道每个阶段都有独立的输入输出和可调参数。我习惯把 RAG 管道拆成四个核心阶段索引阶段、检索阶段、排序阶段、生成阶段。索引阶段负责把原始文档变成可检索的结构化数据检索阶段根据用户查询从索引中召回候选内容排序阶段对候选内容做精排挑出最相关的几条生成阶段把选中的内容和用户问题一起交给模型产出最终回答。为什么要拆这么细因为每个阶段的优化手段完全不同。索引阶段可以调分块大小和重叠长度检索阶段可以选不同的向量模型和相似度算法排序阶段可以加交叉编码器或者规则过滤生成阶段可以调提示词模板和温度参数。如果你把它们揉在一起出了问题根本不知道是哪一环的锅。2.2 为什么选择 TypeScript 作为实现语言热词里出现了 TypeScript这不是偶然。AI Agent 开发正在从 Python 单极向多语言生态扩散而 TypeScript 在这个领域有几个天然优势。第一Agent 应用通常需要和前端、后端服务深度集成TypeScript 的全栈能力让前后端共享类型定义变得非常自然。你定义了一个Document接口前端展示、后端检索、向量库存储都用同一套类型改一个字段编译器会帮你找出所有需要同步修改的地方。第二Node.js 的异步 I/O 模型非常适合 RAG 这种“多次网络调用”的场景。一次完整的 RAG 请求可能涉及嵌入模型调用、向量数据库查询、重排序模型调用、大模型生成这些操作都是 I/O 密集型的Node.js 的事件循环能很好地处理并发。第三TypeScript 的生态里已经有成熟的向量数据库客户端和 LLM SDK。比如langchain/openai、qdrant/js-client-rest、chromadb这些库都提供了完善的类型定义开发体验比裸写 HTTP 请求好太多。当然Python 在模型训练和数据处理方面仍然有优势但如果你做的是 Agent 应用层开发TypeScript 的生产力更高。我的建议是模型训练用 PythonAgent 管道用 TypeScript两者通过 API 或消息队列解耦。2.3 核心架构选型朴素 RAG 还是 Agentic RAG热词里同时出现了“rag”和“agentic rag”这两个概念需要区分清楚。朴素 RAG的流程是固定的用户提问 → 检索 → 生成。每次请求都走同一条路径没有分支没有循环。它的优点是简单、快、可控适合 FAQ 问答、文档检索这类场景。Agentic RAG则把检索能力封装成 Agent 的一个工具由 Agent 自己决定什么时候检索、检索什么、检索几次。比如用户问“对比一下我们产品上个月和这个月的 API 变更”Agent 可能会先检索上个月的变更日志再检索这个月的然后对比生成。这个过程里检索的次数和内容都是动态决定的。我的建议是从朴素 RAG 开始遇到瓶颈再升级到 Agentic RAG。朴素 RAG 能解决 80% 的知识获取需求而且调试起来简单得多。Agentic RAG 虽然灵活但引入了不确定性——Agent 可能不检索、可能检索错、可能陷入循环调试成本高很多。下面这张表对比了两种架构的关键差异维度朴素 RAGAgentic RAG检索时机每次请求固定检索Agent 动态决定检索次数通常 1 次可能多次实现复杂度低高调试难度低高适用场景FAQ、文档问答复杂推理、多跳问答延迟稳定波动大2.4 知识获取管道的完整数据流把视角拉高一个完整的 RAG 知识获取管道包含两条独立的数据流写入流和查询流。写入流是离线的负责把原始文档处理成可检索的索引。它的步骤是文档加载 → 文本清洗 → 分块 → 向量化 → 存入向量数据库。这条流通常由定时任务或文档更新事件触发不需要实时响应。查询流是在线的负责响应用户提问。它的步骤是查询改写 → 向量化 → 向量检索 → 重排序 → 上下文组装 → 模型生成。这条流对延迟敏感每个环节都要控制在毫秒级。两条流共享同一个向量数据库和嵌入模型但它们的优化目标不同。写入流追求索引质量和覆盖率查询流追求检索精度和响应速度。理解这个分离是设计 RAG 系统的第一步。3. 核心细节解析与实操要点3.1 文档分块RAG 效果的第一道分水岭分块Chunking是 RAG 管道里最容易被低估的环节。很多人随便按固定字数切一切就完事了结果检索出来的内容要么缺头少尾要么包含大量无关信息。我见过太多 RAG 效果不好的案例追根溯源都是分块策略有问题。分块的核心矛盾是块太大检索精度下降块太小上下文不完整。假设你把一篇 5000 字的技术文档按 200 字切块用户问一个需要跨段落理解的问题检索出来的块可能只包含答案的一部分模型拿到残缺的上下文回答自然不完整。反过来如果按 2000 字切块一个块里可能包含多个主题检索时匹配到了其中一个主题但整个块都被塞进提示词浪费了宝贵的上下文窗口。我的经验是技术文档按 500-800 字分块FAQ 按单条问答分块代码按函数或类分块。这个范围不是拍脑袋定的而是基于两个考虑一是嵌入模型对 500 字左右的文本表征效果最好二是这个长度在提示词里占用的 token 数可控。除了块大小重叠长度也很关键。我通常设置 10%-20% 的重叠比如 600 字的块配 100 字重叠。重叠的作用是防止关键信息刚好被切在边界上导致两个块都拿不到完整语义。这个成本是值得的多存一点冗余数据换来的是检索召回率的提升。还有一种更高级的分块策略叫语义分块它不按固定字数切而是根据文本的语义相似度找自然断点。比如连续几句话都在讲同一个概念就归为一个块话题切换了就切分。这种策略效果更好但实现复杂度高需要调用嵌入模型计算句子间的相似度。我的建议是先用固定分块跑通流程等有精力了再优化成语义分块。interface ChunkOptions { chunkSize: number; // 每块目标字数 chunkOverlap: number; // 相邻块重叠字数 separator: string; // 优先分割符 } function splitText(text: string, options: ChunkOptions): string[] { const { chunkSize, chunkOverlap, separator } options; const chunks: string[] []; let start 0; while (start text.length) { let end Math.min(start chunkSize, text.length); // 尝试在分隔符处断开避免切断句子 if (end text.length) { const lastSep text.lastIndexOf(separator, end); if (lastSep start chunkSize * 0.5) { end lastSep separator.length; } } chunks.push(text.slice(start, end).trim()); start end - chunkOverlap; } return chunks.filter(c c.length 0); }注意分块时一定要保留元数据。每个块除了文本内容还要记录它来自哪个文档、哪个章节、在原文中的位置。这些元数据在检索时可以用于过滤在生成时可以用于引用溯源。3.2 嵌入模型选型不是越贵越好嵌入模型负责把文本转换成向量。选型时主要看三个指标维度、语义表征能力、推理速度。维度决定了向量数据库的存储成本和检索速度。常见的维度有 384、768、1024、1536、3072。维度越高表征能力通常越强但存储和计算成本也越高。我的经验是768 维是性价比的甜点区对于大多数企业知识库场景够用了。除非你的文档涉及非常细粒度的语义区分否则没必要上 3072 维。语义表征能力需要看具体任务。有些模型在通用语义相似度上表现好但在特定领域比如法律、医疗、代码上可能拉胯。选型时最好用你自己的数据做一次小规模评测准备 50 组“问题-正确文档”对看模型能不能把正确文档排进前 5。推理速度直接影响查询延迟。如果你用 API 调用嵌入模型网络延迟通常比计算延迟还大。这种情况下可以考虑本地部署小模型或者用缓存减少重复调用。热词里提到了“rag hit rate”这其实就是检索命中率。提升命中率的手段除了换更好的嵌入模型还可以做查询改写。用户的问题往往口语化、有歧义直接拿去做向量检索效果不好。可以先用一个小模型把用户问题改写成更适合检索的形式比如补充同义词、拆解成多个子问题。// 查询改写的简单实现 async function rewriteQuery(originalQuery: string): Promisestring[] { const prompt 将以下问题改写成3个不同角度的检索查询每行一个 问题${originalQuery} 要求保留核心意图补充同义词和相关术语。; const response await llm.complete(prompt); return response.split(\n).filter(q q.trim().length 0); } // 多查询检索对每个改写后的查询分别检索合并结果 async function multiQueryRetrieve(query: string, topK: number) { const queries await rewriteQuery(query); const allResults await Promise.all( queries.map(q vectorStore.search(q, topK)) ); // 去重合并 return deduplicate(allResults.flat()); }3.3 向量数据库选型的三个关键维度向量数据库的选择取决于你的部署环境和数据规模。我按三个维度来分类第一维度托管还是自建。托管服务省心但数据要出你的服务器自建可控但运维成本高。如果数据敏感建议自建如果追求快速上线托管服务更合适。第二维度专用还是通用。专用向量数据库如 Qdrant、Milvus、Weaviate针对向量检索做了深度优化性能好但需要单独部署。通用数据库的向量扩展如 PostgreSQL 的 pgvector胜在架构简单不用引入新组件。第三维度内存还是磁盘。内存型数据库如 Redis Vector速度快但容量受限于内存大小。磁盘型数据库如 Milvus容量大但延迟稍高。对于百万级以下的向量内存型完全够用千万级以上建议用磁盘型。我的实际选择是中小规模用 pgvector大规模用 Qdrant。pgvector 的好处是你不需要额外维护一个数据库事务、备份、监控都复用现有的 PostgreSQL 体系。Qdrant 的好处是检索性能强支持丰富的过滤条件而且 TypeScript 客户端很完善。import { QdrantClient } from qdrant/js-client-rest; const client new QdrantClient({ url: http://localhost:6333 }); // 创建集合 await client.createCollection(knowledge_base, { vectors: { size: 768, distance: Cosine, }, }); // 写入向量 await client.upsert(knowledge_base, { wait: true, points: chunks.map((chunk, i) ({ id: i, vector: chunk.embedding, payload: { text: chunk.text, source: chunk.source, section: chunk.section, }, })), }); // 检索 const results await client.search(knowledge_base, { vector: queryEmbedding, limit: 10, with_payload: true, });3.4 重排序把最相关的推到最前面向量检索返回的结果是按相似度排序的但向量相似度高不等于内容真的相关。这是因为嵌入模型把文本压缩成了固定维度的向量信息有损。两个文本向量相似可能只是因为它们用了相似的词汇而不是因为它们讲的是同一件事。**重排序Reranking**就是解决这个问题的。它的做法是先用向量检索召回一批候选比如 20 条然后用一个更精细的模型对这 20 条逐一打分按分数重新排序取前 5 条送给大模型。重排序模型通常是交叉编码器Cross-Encoder它把查询和文档拼在一起输入模型直接输出相关性分数。这种方式比向量点积精确得多但计算量大不能对全量文档做只能对召回结果做。热词里提到的“rag瓶颈”很多时候瓶颈就在检索精度上。加了重排序之后检索精度通常能提升 10-20 个百分点。这个投入产出比非常高建议所有生产级 RAG 都加上。// 使用重排序模型对候选结果精排 async function rerank( query: string, candidates: SearchResult[], topN: number ): PromiseSearchResult[] { const pairs candidates.map(c ({ query, document: c.text, })); const scores await rerankModel.score(pairs); return candidates .map((c, i) ({ ...c, rerankScore: scores[i] })) .sort((a, b) b.rerankScore - a.rerankScore) .slice(0, topN); }4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把项目骨架搭起来。我用的是 Node.js 20 TypeScript 5包管理用 pnpm。核心依赖包括向量数据库客户端、嵌入模型 SDK、大模型 SDK、文本处理工具。pnpm init pnpm add qdrant/js-client-rest openai langchain/textsplitters pnpm add -D typescript types/node tsxtsconfig.json的配置要注意几个点module设为NodeNexttarget设为ES2022开启strict模式。热词里提到了 TypeScript 7.0 会弃用一些旧选项所以现在就把配置写规范避免以后迁移麻烦。{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, skipLibCheck: true, outDir: ./dist }, include: [src/**/*] }4.2 文档加载与清洗文档加载看起来简单实际上有很多细节。不同格式的文档需要不同的解析器PDF 用pdf-parseWord 用mammothMarkdown 直接读文本HTML 用cheerio提取正文。清洗的目标是去掉对检索无用的内容页眉页脚、导航栏、广告、重复的版权声明。这些内容如果不去掉会污染向量空间导致检索时匹配到一堆无关的块。interface RawDocument { content: string; source: string; metadata: Recordstring, string; } async function loadDocument(filePath: string): PromiseRawDocument { const ext path.extname(filePath).toLowerCase(); let content ; switch (ext) { case .md: case .txt: content await fs.readFile(filePath, utf-8); break; case .pdf: const pdfData await pdfParse(await fs.readFile(filePath)); content pdfData.text; break; case .html: const $ cheerio.load(await fs.readFile(filePath, utf-8)); $(script, style, nav, footer, header).remove(); content $(body).text(); break; default: throw new Error(Unsupported format: ${ext}); } // 清洗去掉多余空行、统一换行符 content content .replace(/\r\n/g, \n) .replace(/\n{3,}/g, \n\n) .trim(); return { content, source: path.basename(filePath), metadata: { path: filePath, loadedAt: new Date().toISOString() }, }; }4.3 分块与向量化流水线把加载和分块串起来形成一个完整的索引流水线。这里的关键是批量处理不要一条一条调嵌入 API那样网络往返次数太多。把 100 个块攒成一批一次调用搞定。async function indexDocuments(filePaths: string[]) { const allChunks: Chunk[] []; for (const filePath of filePaths) { const doc await loadDocument(filePath); const chunks splitText(doc.content, { chunkSize: 600, chunkOverlap: 100, separator: \n\n, }); chunks.forEach((text, i) { allChunks.push({ text, source: doc.source, chunkIndex: i, metadata: doc.metadata, }); }); } // 批量向量化 const batchSize 100; for (let i 0; i allChunks.length; i batchSize) { const batch allChunks.slice(i, i batchSize); const embeddings await embedBatch(batch.map(c c.text)); batch.forEach((chunk, j) { chunk.embedding embeddings[j]; }); console.log(Indexed ${Math.min(i batchSize, allChunks.length)}/${allChunks.length}); } // 写入向量数据库 await client.upsert(knowledge_base, { wait: true, points: allChunks.map((chunk, i) ({ id: i, vector: chunk.embedding, payload: { text: chunk.text, source: chunk.source, chunkIndex: chunk.chunkIndex, }, })), }); return allChunks.length; }4.4 查询管道从用户输入到最终回答查询管道是 RAG 的核心。我把它拆成六个步骤每一步都有优化空间。第一步查询预处理。去掉用户输入里的噪音比如多余的空格、特殊符号。如果用户输入太短比如只有两个字可以提示补充信息。第二步查询改写。用 LLM 把口语化的问题改写成适合检索的形式。这一步是可选的如果延迟敏感可以跳过。第三步向量检索。把查询向量化从向量数据库召回 topK 个候选。topK 通常设 10-20给后续重排序留足空间。第四步重排序。用交叉编码器对候选精排取 topN通常 3-5送给大模型。第五步上下文组装。把选中的块拼成一段上下文加上来源标注。注意控制总 token 数不要超过模型的上下文窗口。第六步生成回答。把上下文和用户问题一起交给大模型要求它基于上下文回答不要编造。async function ragQuery(userQuery: string): Promisestring { // 1. 查询改写 const rewrittenQueries await rewriteQuery(userQuery); // 2. 多路检索 const allCandidates await Promise.all( rewrittenQueries.map(q vectorSearch(q, 15)) ); const candidates deduplicate(allCandidates.flat()); // 3. 重排序 const topChunks await rerank(userQuery, candidates, 5); // 4. 组装上下文 const context topChunks .map((c, i) [${i 1}] 来源${c.source}\n${c.text}) .join(\n\n---\n\n); // 5. 生成回答 const prompt 基于以下参考资料回答问题。如果资料中没有相关信息请明确说明。 参考资料 ${context} 问题${userQuery} 回答要求 - 只使用参考资料中的信息 - 引用时标注来源编号 - 如果资料不足说明缺少什么信息; const answer await llm.complete(prompt); return answer; }4.5 参数调优的实操记录参数调优没有理论最优解只能靠实验。我记录了一次典型的调优过程供你参考。初始配置chunkSize1000topK5无重排序。测试集是 50 个真实用户问题人工标注了正确答案所在的文档。评测指标是 Hit Rate5即正确答案是否在前 5 条检索结果里。第一轮结果Hit Rate5 62%。分析失败案例发现大部分是因为块太大检索到的块包含答案但被大量无关内容稀释重排序时排到了后面。调整chunkSize 从 1000 降到 600chunkOverlap 从 0 加到 100。Hit Rate5 提升到 71%。第二轮分析有些问题需要跨多个块才能回答单次检索拿不全。加入查询改写每个问题生成 3 个变体分别检索后合并。Hit Rate5 提升到 79%。第三轮分析合并后的候选有 45 条直接取前 5 条精度不够。加入重排序模型对 45 条精排后取前 5。Hit Rate5 提升到 88%。第四轮分析剩下的失败案例主要是问题本身有歧义或者答案分散在多个文档里。这类问题需要 Agentic RAG 才能解决暂时不在朴素 RAG 的优化范围内。最终配置chunkSize600chunkOverlap100多查询改写3 个变体重排序 topN5。这个配置在生产环境跑了三个月用户反馈良好。5. 常见问题与排查技巧实录5.1 检索结果不相关从四个方向排查检索不相关是最常见的问题。我总结了一个排查顺序从易到难先看分块。把检索到的块打印出来看内容是否完整。如果块被切得七零八落先调分块参数。再看嵌入模型。用几个典型问题测试嵌入模型看它能不能把语义相近的文本映射到相近的向量。如果不行考虑换模型。然后看查询。用户的问题和文档的表述方式可能差异很大。比如用户问“怎么改密码”文档里写的是“密码重置流程”。这种情况下查询改写能帮上忙。最后看排序。如果召回的内容里有正确答案但排在了后面那就是排序问题。加重排序模型或者调整相似度阈值。问题现象可能原因排查方法解决方案检索结果完全不相关嵌入模型不匹配用已知相关对测试换嵌入模型检索结果部分相关分块不合理检查块边界调整块大小和重叠正确答案排在后位排序策略问题看召回列表加重排序跨文档问题答不全单次检索局限分析问题类型多查询检索回答编造信息提示词约束不足检查生成提示词强化约束条件5.2 上下文窗口不够用优先级排序策略大模型的上下文窗口是有限的。如果你检索了 10 个块每个块 600 字加起来 6000 字再加上系统提示词和对话历史很容易超限。我的做法是按重排序分数排序从高到低填充直到接近窗口上限。同时给每个块设一个最大长度超过就截断。截断时保留块的开头部分因为开头通常是主题句。还有一个技巧是压缩上下文。用一个小模型把每个块压缩成一句话摘要只把摘要送给大模型。这样能塞进更多块但会损失细节。适合对精度要求不高的场景。5.3 索引更新增量还是全量文档会更新索引也要跟着更新。全量重建简单但慢增量更新快但复杂。我的建议是小规模知识库1000 文档用全量重建大规模用增量更新。全量重建的好处是不会有一致性问题每次都是干净的索引。增量更新需要处理文档删除、修改、新增三种情况还要考虑向量数据库的 upsert 语义。如果文档更新频繁可以做一个变更检测机制记录每个文档的哈希值定时扫描发现哈希变了就重新索引那个文档。这样比全量重建快得多。async function incrementalIndex(docPath: string) { const content await fs.readFile(docPath, utf-8); const hash createHash(md5).update(content).digest(hex); const existing await client.scroll(knowledge_base, { filter: { must: [{ key: source, match: { value: path.basename(docPath) } }], }, }); if (existing.points.length 0) { const oldHash existing.points[0].payload?.hash; if (oldHash hash) { console.log(Skipping ${docPath}, unchanged); return; } // 删除旧数据 await client.delete(knowledge_base, { filter: { must: [{ key: source, match: { value: path.basename(docPath) } }], }, }); } // 重新索引 await indexSingleDocument(docPath, hash); }5.4 延迟优化把响应时间压到 2 秒以内RAG 的延迟主要来自四个地方查询改写、向量检索、重排序、大模型生成。其中大模型生成通常占大头但其他三个环节也不能忽视。优化手段按性价比排序第一缓存。相同或相似的查询直接返回缓存结果。可以用查询的向量做键设置一个相似度阈值超过阈值就命中缓存。第二并行化。多查询检索可以并行发起不用串行等待。重排序也可以和上下文组装并行。第三模型选择。查询改写用一个小模型就够了不需要上大模型。重排序模型也有大小之分选一个速度和精度平衡的。第四流式输出。大模型生成用流式返回用户能更快看到第一个字感知延迟会低很多。提示不要为了降低延迟牺牲检索质量。用户宁愿等 3 秒拿到正确答案也不愿意 1 秒拿到错误答案。延迟优化的前提是检索质量已经达标。5.5 评测没有评测就没有优化RAG 系统最怕的是“感觉还行”。没有量化评测你根本不知道改动是变好了还是变差了。我建议至少维护一个 50-100 条问题的评测集覆盖不同类型的问题事实型、对比型、多跳型、否定型。每条问题标注正确答案所在的文档和块。评测指标用 Hit RateK 和 MRRMean Reciprocal Rank。每次改动参数或换模型都跑一遍评测集对比指标变化。这样才能确保优化方向是对的。interface EvalCase { question: string; expectedSource: string; expectedChunkIndex: number; } async function evaluate(cases: EvalCase[], topK: number) { let hits 0; let mrrSum 0; for (const c of cases) { const results await ragRetrieve(c.question, topK); const rank results.findIndex( r r.source c.expectedSource r.chunkIndex c.expectedChunkIndex ); if (rank 0) { hits; mrrSum 1 / (rank 1); } } return { hitRate: hits / cases.length, mrr: mrrSum / cases.length, }; }6. 从朴素 RAG 到 Agentic RAG 的演进路径6.1 什么时候该升级到 Agentic RAG朴素 RAG 能解决大部分知识获取需求但有几类问题它搞不定需要多跳推理的问题先查 A 再查 B 再对比、需要动态决定检索策略的问题有些问题不需要检索有些需要检索多次、需要结合工具使用的问题检索完还要调 API 算一下。如果你发现评测集里超过 20% 的问题属于上述类型就该考虑升级到 Agentic RAG 了。6.2 Agentic RAG 的核心设计Agentic RAG 把检索封装成一个工具交给 Agent 调度。Agent 的决策逻辑是先判断问题是否需要检索如果需要生成检索查询调用检索工具拿到结果后判断是否足够回答不够就再检索够了就生成回答。这个循环的关键是终止条件。必须设置最大检索次数否则 Agent 可能无限循环。我通常设 3-5 次超过就强制生成回答。async function agenticRag(question: string, maxIterations 5) { const messages: Message[] [ { role: system, content: 你是一个知识助手可以调用检索工具获取信息。 }, { role: user, content: question }, ]; for (let i 0; i maxIterations; i) { const response await llm.chat(messages, { tools: [retrievalTool], }); if (response.toolCalls.length 0) { return response.content; } for (const call of response.toolCalls) { const result await retrievalTool.execute(call.arguments); messages.push({ role: tool, toolCallId: call.id, content: JSON.stringify(result), }); } } // 达到最大迭代次数强制生成 messages.push({ role: user, content: 请基于已有信息给出最终回答。, }); const final await llm.chat(messages); return final.content; }6.3 知识图谱与 RAG 的结合热词里出现了“graphrag”和“ontology rag”这是 RAG 的一个进阶方向。传统 RAG 把文档切成孤立的块块与块之间的关系丢失了。知识图谱 RAG 则把文档中的实体和关系抽取出来构建成图结构检索时不仅检索文本块还检索相关的实体和关系。这种方式的优势是能回答需要关系推理的问题。比如“A 产品的负责人是谁”传统 RAG 需要文档里恰好有一句话写了这个信息而知识图谱 RAG 可以通过“A 产品 → 负责人 → 张三”这条路径找到答案。但知识图谱 RAG 的构建成本很高需要实体抽取、关系抽取、图谱存储和查询。我的建议是先用传统 RAG遇到关系推理的瓶颈再考虑知识图谱。不要为了技术先进性而过度设计。7. 我在实际项目中的几点体会做 RAG 这一年多最大的体会是效果好坏不取决于你用了多先进的模型而取决于你对数据的理解有多深。我见过用最便宜的嵌入模型做出 90% 命中率的系统也见过用最贵的模型做出 60% 命中率的系统。差距就在分块策略、查询改写、重排序这些“脏活累活”上。另一个体会是评测集是 RAG 项目的生命线。没有评测集你就是在盲人摸象。我现在的习惯是每接一个新领域的 RAG 项目第一件事就是花两天时间构建评测集。这两天投入的时间会在后续的优化中十倍百倍地省回来。最后分享一个小技巧把检索到的块和最终回答一起展示给用户。这样用户能判断回答是否可信也能帮你发现检索错误。很多检索问题都是用户反馈“这个来源不对”才发现的。透明化不仅能提升用户体验还能帮你持续优化系统。这个知识获取管道后续还可以这样扩展加入多模态检索支持图片和表格加入时效性权重让新文档排得更靠前加入用户反馈循环根据用户的点赞点踩自动调整检索策略。RAG 的优化是没有尽头的但只要核心管道搭好了后续的迭代就是在这个基础上做加法。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进