拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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

1. 为什么知识获取管道是 AI Agent 落地的第一道坎

做 AI Agent 开发的人,绕不开一个尴尬的现实:模型本身很聪明,但它不知道你公司内部的业务规则、不知道你昨天刚更新的产品文档、更不知道你私有的那套数据长什么样。你问它一个关于内部系统的问题,它要么一本正经地胡说八道,要么直接告诉你“我无法访问该信息”。这不是模型不行,而是它缺了一条把外部知识喂给它的管道。

这条管道,就是RAG(Retrieval-Augmented Generation,检索增强生成)。名字听起来挺唬人,拆开看其实很朴素:检索 + 增强 + 生成。先从知识库里把相关内容捞出来,再把捞出来的内容塞进提示词里,最后让模型基于这些内容生成回答。就这么三步,但它解决了大模型落地中最核心的一个矛盾——通用能力与私有知识之间的鸿沟。

我见过太多团队在搭建 AI Agent 时,一上来就急着调工具、接 API、写 Agent 循环,结果跑起来发现 Agent 回答质量惨不忍睹。排查半天,问题不在 Agent 逻辑,而在知识获取这一层根本没搭好。Agent 拿到的上下文是错的、过时的、不完整的,后面再怎么优化推理链路都是白搭。所以我把知识获取管道放在整个 AI Agent 系列的第四篇来讲,不是因为它简单,而是因为它太重要了,重要到值得你花大力气把它做扎实。

这篇文章面向的是已经了解 AI Agent 基本概念、准备动手搭建知识库的开发者。不管你用的是 TypeScript 还是 Java,不管你选 LangChain 还是自己手写,RAG 的核心原理和工程要点是相通的。我会从整体设计思路讲起,然后拆解每个环节的实操细节,最后把我踩过的坑和排查经验整理出来。目标是让你读完能直接动手搭一套可用的 RAG 管道,而不是停留在“我知道 RAG 是什么”的层面。

2. RAG 管道的整体设计与核心思路拆解

2.1 一条完整的知识获取管道长什么样

很多人对 RAG 的理解停留在“把文档切块、存进向量库、查询时做相似度匹配”这个层面。这个理解不算错,但太粗糙了。一条真正能在生产环境跑起来的 RAG 管道,至少包含以下几个环节:

  • 文档加载:从各种来源(本地文件、数据库、API、网页)把原始知识读进来
  • 文档解析:把 PDF、Word、HTML 等格式转成纯文本,保留结构信息
  • 文本分块:把长文档切成适合检索和放入上下文的小块
  • 向量化:用 Embedding 模型把文本块转成向量
  • 向量存储:把向量和原文存进向量数据库
  • 查询处理:对用户输入做改写、扩展、意图识别
  • 检索召回:从向量库中找出最相关的文本块
  • 重排序:对召回结果做精排,提升相关性
  • 上下文组装:把检索结果和用户问题拼成最终提示词
  • 生成回答:调用大模型生成最终答案

这十个环节,每一个都有坑。有人觉得分块随便切切就行,结果检索出来的内容支离破碎;有人觉得向量化用默认模型就行,结果中文语义匹配一塌糊涂;有人觉得检索 Top-K 设大一点总没错,结果上下文塞满了无关内容,模型反而被干扰。

2.2 为什么选择 RAG 而不是微调

这是我在技术选型阶段被问得最多的问题。答案其实不复杂:RAG 解决的是“知识”问题,微调解决的是“能力”问题。如果你需要模型掌握新的知识事实,用 RAG;如果你需要模型学会新的输出格式、新的推理风格,用微调。两者不是互斥的,但在知识获取这个场景下,RAG 有几个微调无法比拟的优势。

第一,知识更新成本极低。业务文档改了,你只需要重新索引那部分文档,不需要重新训练模型。微调一次动辄几小时到几天,RAG 更新一次可能就几秒钟。

第二,可溯源。RAG 检索出来的内容是有出处的,你可以告诉用户“这个答案来自某某文档第几页”。微调后的模型你根本不知道它为什么这么回答。

第三,成本可控。微调需要 GPU 资源,RAG 主要消耗的是 Embedding 和向量存储的成本,量级差很多。

第四,避免灾难性遗忘。微调容易让模型在学会新知识的同时忘掉旧能力,RAG 不存在这个问题。

当然 RAG 也有它的局限:检索质量高度依赖分块策略和 Embedding 模型,上下文窗口有限导致不能塞太多内容,多跳推理能力弱于微调模型。但对于绝大多数企业知识库场景,RAG 是性价比最高的选择。

2.3 TypeScript 技术栈下的 RAG 选型考量

既然热搜词里出现了 TypeScript,我就专门聊聊 TS 生态下的 RAG 选型。很多做 AI Agent 的团队后端是 Node.js 或 Bun,前端是 React,整个技术栈都是 TypeScript,这时候引入 Python 的 LangChain 就很别扭——跨语言调用、部署复杂度、团队技能栈不匹配,都是问题。

TypeScript 生态下目前比较成熟的 RAG 方案有几类:

方案类型代表工具适用场景注意事项
全栈框架LangChain.js快速原型、功能全面抽象层较厚,调试困难
轻量库LlamaIndex.TS专注检索场景文档相对少
向量库客户端Chroma、Qdrant、Pinecone 的 TS SDK已有自研管道需要自己组装流程
自研直接调 Embedding API + 向量库追求可控性工作量大但最灵活

我的建议是:原型阶段用 LangChain.js 快速验证,生产阶段逐步替换成自研管道。LangChain 的抽象层在调试时非常痛苦,你很难知道它内部到底做了什么。当你需要精细控制分块策略、检索参数、重排序逻辑时,自研反而更省时间。

另外提醒一句,TypeScript 7.0 中baseUrl和moduleResolution=node10这些选项已经弃用,如果你在搭建项目时看到相关警告,别忽略,尽早迁移到bundler或node16解析模式,否则后续升级会很痛苦。

3. 核心环节的实操要点与避坑指南

3.1 文档解析:别小看这一步,它决定了后续所有环节的上限

文档解析是整条管道最容易被低估的环节。很多人觉得“不就是把 PDF 转成文本吗”,结果转出来的文本全是乱码、表格错位、段落粘连,后面分块和检索全崩。

我处理过的文档类型里,难度从低到高大致是:纯文本 > Markdown > HTML > Word > PDF > 扫描件。PDF 是重灾区,尤其是那种多栏排版、带表格、带公式的学术论文或产品手册。

实操建议:

  • 纯文本和 Markdown:直接读,但要注意编码问题,统一转成 UTF-8
  • HTML:用cheerio或jsdom提取正文,去掉导航、广告、页脚
  • Word:用mammoth转 Markdown,保留标题层级
  • PDF:优先用pdf-parse或pdfjs-dist,如果排版复杂,考虑用云服务做 OCR 和版面分析
  • 扫描件:必须走 OCR,Tesseract 或者云服务都行,但准确率要实测

注意:PDF 解析出来的文本经常带有页眉页脚、页码、水印,这些噪声会严重干扰检索。我一般会在解析后加一步清洗,用正则去掉重复出现的页眉页脚模式。

还有一个细节:保留文档的元数据。文件名、标题、章节、页码、更新时间,这些信息在检索时可以作为过滤条件,也能在生成回答时作为引用来源。很多人只存文本内容,后面想加过滤功能时发现元数据全丢了,只能重新索引。

3.2 文本分块:切得好不好,直接决定检索准不准

分块是 RAG 管道里最需要反复调优的环节。切太大,检索出来的内容包含太多无关信息,干扰模型;切太小,语义不完整,检索出来的片段读不懂。

常见的分块策略有几种:

固定长度分块:按字符数或 token 数切,简单粗暴。比如每 500 个 token 一块,重叠 50 个 token。优点是实现简单,缺点是经常在句子中间切断,语义不完整。

递归字符分块:按分隔符优先级递归切分,先按段落切,段落太长再按句子切,句子太长再按字符切。这是 LangChain 的默认策略,效果比固定长度好很多。

语义分块:用 Embedding 计算相邻句子的相似度,相似度低的地方作为切分点。效果最好,但计算成本高。

结构化分块:按文档本身的结构切,比如 Markdown 的标题层级、代码的函数定义。这是我最推荐的方式,前提是你的文档有清晰的结构。

我的实操经验是:优先用结构化分块,退而求其次用递归字符分块,固定长度分块只在万不得已时用。分块大小没有万能值,一般 300 到 800 token 之间比较合适,具体要看你的文档类型和查询粒度。技术文档可以小一点,叙述性内容可以大一点。

重叠窗口也很关键。我一般设置 10% 到 20% 的重叠,防止关键信息刚好被切在边界上。但重叠太多会导致检索结果重复,浪费上下文窗口。

3.3 向量化:Embedding 模型选不对,后面全白费

Embedding 模型的选择直接决定了检索的语义匹配能力。选型时主要看几个维度:

  • 语言支持:中文场景必须选中文优化过的模型,直接用 OpenAI 的 text-embedding-ada-002 处理中文,效果会打折扣
  • 维度:维度越高表达能力越强,但存储和计算成本也越高。常见的有 768、1024、1536、3072 维
  • 最大输入长度:决定了你的分块上限,一般是 512 或 8192 token
  • 成本:API 调用按 token 计费,自部署则看 GPU 资源
  • 是否支持本地部署:数据敏感场景必须本地部署

中文场景下,我实测过几个方案:BGE 系列(BAAI 出品)在中文语义匹配上表现很好,支持本地部署;M3E 系列也不错;如果预算充足,可以用云服务的中文 Embedding API。

注意:Embedding 模型一旦选定,索引和查询必须用同一个模型。中途换模型意味着所有向量都要重新计算,这个成本很高。所以选型时要考虑长期可用性。

还有一个容易忽略的点:查询和文档要用相同的 Embedding 方式。有些模型对查询和文档有不同的前缀要求,比如查询前加 "query:",文档前加 "passage:"。不按规范来,检索效果会明显下降。

3.4 向量存储:选对数据库,省一半运维精力

向量数据库的选择要看你的数据规模、部署环境和预算。小规模场景(几万条以内)用内存向量库就够了,比如hnswlib-node或者 Chroma 的本地模式。中等规模(百万级)可以考虑 Qdrant、Weaviate、Milvus。大规模(千万级以上)一般用云服务,比如 Pinecone、Zilliz Cloud。

TypeScript 生态下,我比较推荐 Qdrant,它的 TS SDK 写得很干净,本地部署也简单,Docker 一条命令就能跑起来。Chroma 也不错,但它的 TS 客户端功能比 Python 版少一些。

存储时除了向量本身,还要存原文、元数据、以及一个唯一 ID。元数据的设计要考虑后续的过滤需求,比如按文档类型、按时间范围、按权限过滤。

3.5 检索策略:Top-K 不是越大越好

检索环节最常见的误区就是“Top-K 设大一点,总能捞到相关内容”。实际上 Top-K 太大有两个问题:一是上下文窗口被无关内容占满,模型注意力被分散;二是噪声增加,模型可能被错误信息误导。

我的经验是:先用向量检索召回 20 到 50 条候选,再用重排序模型精排到 3 到 5 条。这样既保证了召回率,又保证了精度。

重排序模型(Reranker)是提升检索质量的关键武器。它和 Embedding 模型的区别在于:Embedding 是双塔结构,查询和文档分别编码后算相似度,速度快但精度有限;Reranker 是交叉编码,查询和文档一起输入模型打分,速度慢但精度高。所以典型流程是 Embedding 粗排 + Reranker 精排。

常见的 Reranker 有 BGE-Reranker 系列、Cohere Rerank 等。中文场景下 BGE-Reranker 表现不错,支持本地部署。

另外,混合检索也值得一试。纯向量检索对关键词匹配不敏感,比如用户搜一个产品型号 "XYZ-2000",向量检索可能召回一堆语义相似但型号不对的内容。这时候结合 BM25 关键词检索,用加权融合的方式合并结果,效果会好很多。

3.6 上下文组装:怎么把检索结果喂给模型

检索出来的内容不能直接一股脑塞给模型,需要做组装。组装时要考虑几个问题:

顺序:相关度高的放前面还是后面?研究表明,模型对上下文开头和结尾的内容注意力更强,中间部分容易被忽略。所以把最相关的放开头,次相关的放结尾。

格式:给每个片段加上来源标记,比如[文档1]、[文档2],方便模型引用,也方便你后续做溯源。

长度控制:总 token 数不能超过模型的上下文窗口,还要给系统提示词和用户问题留空间。一般检索内容控制在上下文窗口的 50% 到 70%。

去重:如果多个片段内容高度重叠,要去重,避免浪费窗口。

一个典型的组装模板长这样:

你是一个知识助手,请基于以下参考资料回答用户问题。 如果参考资料中没有相关信息,请明确告知用户你不知道。 参考资料: [文档1] {内容} [文档2] {内容} [文档3] {内容} 用户问题:{question} 请给出准确、简洁的回答,并标注引用的文档编号。

4. 从零搭建一套可用的 RAG 管道

4.1 环境准备与依赖安装

假设你用 TypeScript 做开发,Node.js 20 以上版本。先初始化项目:

mkdir rag-pipeline && cd rag-pipeline npm init -y npm install typescript tsx @types/node npx tsc --init

tsconfig.json里注意把moduleResolution设成bundler或node16,别用已经弃用的node10。target设成ES2022以上。

然后安装核心依赖:

npm install @qdrant/js-client-rest openai cheerio mammoth pdf-parse npm install -D @types/pdf-parse

这里用 OpenAI 的 Embedding API 做演示,实际生产可以换成 BGE 的本地服务。Qdrant 用 Docker 起一个:

docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant

4.2 文档加载与解析实现

先写一个通用的文档加载器,根据文件扩展名选择解析方式:

import fs from 'fs/promises'; import path from 'path'; import mammoth from 'mammoth'; import pdfParse from 'pdf-parse'; import * as cheerio from 'cheerio'; interface RawDocument { content: string; metadata: { source: string; title: string; type: string; updatedAt: string; }; } async function loadDocument(filePath: string): Promise<RawDocument> { const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath); let content = ''; if (ext === '.txt' || ext === '.md') { content = await fs.readFile(filePath, 'utf-8'); } else if (ext === '.html' || ext === '.htm') { const html = await fs.readFile(filePath, 'utf-8'); const $ = cheerio.load(html); $('script, style, nav, footer, header').remove(); content = $('body').text(); } else if (ext === '.docx') { const result = await mammoth.extractRawText({ path: filePath }); content = result.value; } else if (ext === '.pdf') { const buffer = await fs.readFile(filePath); const result = await pdfParse(buffer); content = result.text; } else { throw new Error(`Unsupported file type: ${ext}`); } content = cleanText(content); return { content, metadata: { source: filePath, title: fileName, type: ext.slice(1), updatedAt: new Date().toISOString(), }, }; } function cleanText(text: string): string { return text .replace(/\r\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .replace(/[ \t]{2,}/g, ' ') .trim(); }

这段代码的关键在于cleanText函数。PDF 解析出来的文本经常有多余空行和空格,不清洗的话分块效果会很差。

4.3 文本分块的具体实现

我推荐用递归字符分块,实现一个简化版:

interface Chunk { content: string; metadata: Record<string, any>; } function splitText( text: string, chunkSize: number = 500, overlap: number = 50 ): string[] { const separators = ['\n\n', '\n', '。', '!', '?', '.', '!', '?', ' ', '']; const chunks: string[] = []; function split(text: string, separatorIndex: number): void { if (text.length <= chunkSize) { if (text.trim()) chunks.push(text.trim()); return; } if (separatorIndex >= separators.length) { for (let i = 0; i < text.length; i += chunkSize - overlap) { chunks.push(text.slice(i, i + chunkSize).trim()); } return; } const separator = separators[separatorIndex]; const parts = text.split(separator); let current = ''; for (const part of parts) { const candidate = current ? current + separator + part : part; if (candidate.length <= chunkSize) { current = candidate; } else { if (current) chunks.push(current.trim()); if (part.length > chunkSize) { split(part, separatorIndex + 1); current = ''; } else { current = part; } } } if (current.trim()) chunks.push(current.trim()); } split(text, 0); return chunks; }

这个实现优先按段落切,段落太长按句子切,句子太长按字符切。中文场景下我把中文标点也加进了分隔符列表,效果比只用换行符好很多。

分块大小我一般设 500 字符,重叠 50 字符。如果你的文档是技术手册,可以设小一点到 300;如果是叙述性内容,可以设大到 800。

4.4 向量化与入库

接下来把分块后的内容向量化并存入 Qdrant:

import OpenAI from 'openai'; import { QdrantClient } from '@qdrant/js-client-rest'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const qdrant = new QdrantClient({ url: 'http://localhost:6333' }); const COLLECTION_NAME = 'knowledge_base'; const VECTOR_SIZE = 1536; async function ensureCollection(): Promise<void> { const collections = await qdrant.getCollections(); const exists = collections.collections.some(c => c.name === COLLECTION_NAME); if (!exists) { await qdrant.createCollection(COLLECTION_NAME, { vectors: { size: VECTOR_SIZE, distance: 'Cosine' }, }); } } async function embedTexts(texts: string[]): Promise<number[][]> { const response = await openai.embeddings.create({ model: 'text-embedding-3-small', input: texts, }); return response.data.map(d => d.embedding); } async function indexChunks(chunks: Chunk[]): Promise<void> { const batchSize = 100; for (let i = 0; i < chunks.length; i += batchSize) { const batch = chunks.slice(i, i + batchSize); const vectors = await embedTexts(batch.map(c => c.content)); await qdrant.upsert(COLLECTION_NAME, { points: batch.map((chunk, idx) => ({ id: i + idx, vector: vectors[idx], payload: { content: chunk.content, ...chunk.metadata, }, })), }); } }

批量处理很重要,一条一条调 Embedding API 会慢到让你怀疑人生。100 条一批是比较稳妥的选择,太大可能触发 API 限制。

4.5 检索与重排序

检索部分先做向量召回,再用 Reranker 精排:

interface SearchResult { content: string; score: number; metadata: Record<string, any>; } async function search( query: string, topK: number = 5, recallK: number = 30 ): Promise<SearchResult[]> { const [queryVector] = await embedTexts([query]); const results = await qdrant.search(COLLECTION_NAME, { vector: queryVector, limit: recallK, with_payload: true, }); const candidates = results.map(r => ({ content: r.payload?.content as string, score: r.score, metadata: r.payload as Record<string, any>, })); return rerank(query, candidates).slice(0, topK); } async function rerank( query: string, candidates: SearchResult[] ): Promise<SearchResult[]> { // 这里用简单的关键词重叠度做演示 // 生产环境应替换为真正的 Reranker 模型 const queryTerms = new Set(query.toLowerCase().split(/\s+/)); return candidates .map(c => { const contentTerms = c.content.toLowerCase().split(/\s+/); const overlap = contentTerms.filter(t => queryTerms.has(t)).length; const keywordScore = overlap / Math.max(queryTerms.size, 1); return { ...c, score: c.score * 0.7 + keywordScore * 0.3, }; }) .sort((a, b) => b.score - a.score); }

生产环境里,Reranker 应该用专门的模型,比如 BGE-Reranker。可以部署一个本地服务,通过 HTTP 调用。这里用关键词重叠度做演示,是为了让你能直接跑起来看效果。

4.6 上下文组装与生成

最后把检索结果组装成提示词,调用大模型生成回答:

async function generateAnswer( query: string, contexts: SearchResult[] ): Promise<string> { const contextText = contexts .map((c, i) => `[文档${i + 1}] ${c.content}`) .join('\n\n'); const prompt = `你是一个知识助手,请基于以下参考资料回答用户问题。 如果参考资料中没有相关信息,请明确告知用户你不知道,不要编造答案。 参考资料: ${contextText} 用户问题:${query} 请给出准确、简洁的回答,并标注引用的文档编号。`; const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], temperature: 0.1, }); return response.choices[0].message.content || ''; }

temperature设低一点,0.1 到 0.3 之间,保证回答稳定。系统提示词里明确要求“不知道就说不知道”,能有效减少幻觉。

把整个流程串起来:

async function main() { await ensureCollection(); const doc = await loadDocument('./docs/product-manual.pdf'); const textChunks = splitText(doc.content, 500, 50); const chunks: Chunk[] = textChunks.map(content => ({ content, metadata: doc.metadata, })); await indexChunks(chunks); console.log(`Indexed ${chunks.length} chunks`); const query = '产品的保修期是多久?'; const results = await search(query, 5, 30); const answer = await generateAnswer(query, results); console.log('Answer:', answer); } main().catch(console.error);

这套代码跑通之后,你就有了一个最基础的 RAG 管道。接下来就是根据实际效果不断调优。

5. 常见问题与排查技巧实录

5.1 检索结果不相关怎么办

这是最高频的问题。排查思路按优先级来:

第一步,检查分块质量。把检索出来的片段打印出来看看,是不是被切得支离破碎。如果是,调整分块大小和重叠窗口。中文内容建议用中文标点做分隔符。

第二步,检查 Embedding 模型。中文场景用英文优化的模型,效果肯定差。换 BGE 或 M3E 试试。另外确认查询和文档用的是同一个模型,前缀规范有没有遵守。

第三步,检查查询本身。用户的问题可能太短、太模糊,或者包含大量口语化表达。可以加一步查询改写,用大模型把用户问题改写成更适合检索的形式。

第四步,加 Reranker。如果向量召回的结果里有相关内容但排名靠后,Reranker 能把它提到前面。

第五步,考虑混合检索。纯向量检索对精确匹配不敏感,加上 BM25 关键词检索做融合。

5.2 模型回答出现幻觉怎么处理

幻觉的根源通常是检索内容里没有答案,但模型还是硬编了一个。解决办法:

  • 系统提示词里明确要求“仅基于参考资料回答,不知道就说不知道”
  • 降低 temperature
  • 在检索结果里加一个相关性阈值,低于阈值的结果不传给模型
  • 要求模型在回答中标注引用来源,没有来源的内容不允许输出

我实测下来,明确要求模型标注引用这一招最有效。模型一旦被要求给出出处,它就不太敢乱编了。

5.3 索引速度太慢怎么优化

索引慢通常卡在 Embedding API 调用上。优化方向:

  • 批量调用,一次传多条文本
  • 并发调用,但注意 API 的速率限制
  • 用本地 Embedding 模型,虽然单次可能慢,但没有网络延迟和速率限制
  • 增量索引,只处理新增或修改的文档,不要每次全量重建

5.4 上下文窗口不够用怎么办

检索内容太多导致超出上下文窗口,几个应对策略:

  • 减少 Top-K,从 5 降到 3
  • 压缩检索内容,用大模型对检索结果做摘要
  • 用支持更长上下文的模型
  • 分多次检索,先检索再基于中间结果做二次检索

5.5 常见问题速查表

问题现象可能原因排查方向解决方案
检索结果完全不相关Embedding 模型不匹配检查模型语言支持换中文优化模型
检索结果相关但排名靠后缺少精排检查是否有 Reranker加 Reranker 模型
回答内容不完整分块太小检查分块大小增大分块或增加重叠
回答包含无关信息Top-K 太大检查召回数量减小 Top-K 或加阈值过滤
模型编造答案检索无结果但未告知检查提示词明确要求不知道就说不知道
索引速度慢API 调用未批量检查调用方式批量+并发调用
中文检索效果差模型英文优化检查模型换 BGE 或 M3E
重复内容多重叠窗口太大检查重叠比例降低重叠到 10%

5.6 几个我踩过的坑

坑一:PDF 解析出来的文本顺序错乱。多栏排版的 PDF,解析出来文字顺序是乱的,读起来前言不搭后语。解决办法是用支持版面分析的解析工具,或者干脆把 PDF 转成图片走 OCR。

坑二:Embedding 模型换了但索引没重建。查询用新模型,索引是旧模型生成的,检索结果完全不可用。换模型一定要全量重建索引。

坑三:元数据没存全。后面想加按时间过滤的功能,发现索引时没存时间字段,只能重新索引。索引时多存点元数据没坏处。

坑四:Top-K 设太大导致模型被干扰。检索出来 20 条,其中 15 条是无关的,模型被这些噪声带偏了。后来改成召回 30 条精排到 3 条,效果立竿见影。

坑五:忽略查询改写。用户问“这个东西怎么用”,检索系统根本不知道“这个东西”指什么。加一步查询改写,把指代词还原成具体名词,检索准确率提升明显。

6. 进阶方向与后续扩展思路

基础管道跑通之后,有几个方向可以继续深挖。

Agentic RAG:让 Agent 自己决定什么时候检索、检索什么、检索几次。传统 RAG 是一次检索然后生成,Agentic RAG 可以多轮检索、自我反思、动态调整查询。这需要把 RAG 和 Agent 循环结合起来,复杂度高但效果上限也高。

GraphRAG:把知识图谱和 RAG 结合,解决多跳推理问题。传统 RAG 只能检索到和查询直接相关的片段,GraphRAG 可以通过实体关系链找到间接相关的信息。适合知识关联性强的场景,比如医疗、法律、科研。

多模态 RAG:不只检索文本,还能检索图片、表格、视频。这需要多模态 Embedding 模型和对应的向量库支持。

RAG as a Service:把 RAG 管道封装成服务,提供 API 给多个 Agent 调用。AgentScope 2.0 提到的 RAG as Service 就是这个思路。好处是知识库统一管理,多个 Agent 共享,更新一次全部生效。

评估体系:RAG 效果好不好,不能靠感觉,要有量化指标。常用的有检索命中率(Hit Rate)、MRR(Mean Reciprocal Rank)、答案忠实度(Faithfulness)、答案相关性(Answer Relevancy)。可以搭一套自动化评估流程,每次调参后跑一遍看指标变化。

我个人在实际操作中的体会是,RAG 管道没有一步到位的完美方案,都是根据实际数据不断调出来的。同样的参数,换一个知识库可能效果就完全不同。所以别迷信别人的最佳实践,自己动手测、动手调,才是正道。先跑通最小可用版本,然后针对具体问题逐个优化,比一上来就追求完美架构要务实得多。

返回列表