AI、LLM、GenAI 是当前技术社区讨论热度最高的几个词,但真正要把这些能力落到业务系统里,RAG 是无法绕开的关键工程路径。RAG 的全称是 Retrieval-Augmented Generation,也就是检索增强生成:先从知识库中检索出与问题相关的资料片段,再让大模型基于这些片段生成回答。它解决的是大模型训练数据截止后的知识盲区、私有数据隔离、以及回答幻觉三个实际问题。
这篇内容定位在 AI 与 LLM 工程实战学习路径的第三阶段,重点不是再讲一遍大模型 API 怎么调用,而是围绕 RAG 做完整的工程化落地:从最小链路搭起,到分块、向量化、检索、生成的关键参数,再到知识库指标、进阶架构和生产排错。适合已经会调用大模型接口、写过一个简单 Demo,但准备把知识库问答做成可用系统的开发者。
读完这篇文章,你可以独立搭建一个 RAG 问答原型,知道哪些参数会影响效果,能够用指标而不是肉眼判断来评估知识库质量,并且遇到“回答不对、引用不对、检索不到”的问题时,知道按哪条链路去排查。
1. 先理解RAG在LLM工程里的位置,再决定要不要用RAG
1.1 RAG解决的核心问题:知识时效、私有数据和幻觉
先给RAG一个通俗的定义。大模型本身像一个知识面很广、但训练数据截止之后就很难更新的专家。你问它常识问题,它可以回答得很好;你问它公司内部最近更新的项目文档,它要么说不知道,要么凭训练数据里的近似内容编一个答案。RAG 的思路就是把这个场景变成“开卷考试”:先让检索器从知识库里找出相关资料,再让大模型结合资料作答。
从技术定义上看,RAG 是一个由检索器(Retriever)和生成器(Generator)组成的框架。检索器负责从外部知识库中召回与用户问题相关的文档片段,生成器负责把这些片段作为上下文输入给大模型,最终生成回答。这套思路在 2020 年 Lewis 等人的论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中得到了系统化表述,之后成为知识密集型 NLP 任务的主流方案之一。
RAG 能解决三类问题:
- 知识时效性。大模型训练数据有截止时间,而业务文档、行业规范、技术方案可能每天都在变化。RAG 可以在查询阶段实时检索最新内容,不需要重训模型。
- 私有知识。公司内部文档、协议、专利、日志、业务规则不可能进入公开训练数据。RAG 可以在不修改模型权重的前提下,把这些私有内容放进知识库,让模型按需读取。
- 幻觉。大模型在没有事实依据时容易编造内容。RAG 通过把回答范围限制在检索出的上下文里,大幅降低无依据生成的概率。
这里要强调一个容易误解的点:RAG 不能保证 100% 消除幻觉,它只是把幻觉的来源从“模型记忆”转移到了“检索上下文”。如果检索器召回的内容本身不正确、不完整,或者被切碎导致语义丢失,生成器依然会基于错误上下文给出错误答案。所以做 RAG 项目时,不能只盯着生成端的模型能力,检索质量往往是决定效果上限的地方。
1.2 RAG 和微调、长上下文、Agent 怎么选
RAG 不是唯一的知识注入方式。实际项目中经常有人问:到底该用 RAG、微调、长上下文,还是引入 Agent?它们解决的问题有重叠,但适用边界不一样。
| 方案 | 核心思路 | 适合场景 | 主要成本 |
|---|---|---|---|
| RAG | 检索外部知识后生成 | 知识更新频繁、需要引用来源、私有数据隔离 | 索引构建、检索链路、向量库运维 |
| 微调 | 用训练数据调整模型权重 | 固定写作风格、领域术语、能力塑造 | GPU 训练资源、数据标注、版本管理 |
| 长上下文 | 把大量文档直接放入 Prompt | 一次性分析少量长文档、跨章节连续理解 | Token 费用、接口延迟、上下文窗口占用 |
| Agent | 让模型规划多步工具调用 | 多轮查询、跨库汇总、需要调用多个 API | 编排复杂度、错误传播、调试成本 |
从这些对比可以看出,RAG 的核心优势是知识可更新、答案可溯源。当你需要回答“这个问题来自哪份文档的哪一段”时,RAG 天然比微调和长上下文更合适。微调更适合改变模型的行为和风格,而不是更新事实性知识。长上下文适合单次读取大文档,但每次都把全部内容塞进 Prompt 会非常昂贵,也不适合频繁更新的数据。
Agent 和 RAG 不是对立关系,而是组合关系。Agent 负责判断“这个问题需要几步、调用哪些工具”,RAG 可以作为 Agent 的一个工具,负责从知识库检索。后面讲 Agentic RAG 时会再展开。
1.3 学习环境与生产环境的差异
很多教程只演示了脚本级别的 RAG,读者照做之后觉得“跑通了”,但把它放在生产环境立刻就会出现问题。差异主要体现在几个方面:
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据量级 | 几份文档、几百个分块 | 几十万甚至上百万分块 |
| 向量库 | Chroma、FAISS 本地存储 | Milvus、pgvector、Elasticsearch 等独立服务 |
| 请求模式 | 单线程手工调用 | 高并发、超时、限流 |
| 数据更新 | 手动重新建库 | 增量更新、数据版本管理 |
| 权限控制 | 无或极简 | 按用户、部门、文档级别隔离 |
| 日志监控 | 无 | 全链路日志、检索耗时、指标监控 |
| 异常处理 | 报错就重跑 | 降级、重试、告警 |
学习阶段最重要的是把链路跑通,理解每个环节的作用;生产阶段则要考虑知识库替换、权限隔离、检索延迟和成本。文章后面会单独讲生产落地问题。
2. 搭建最小可运行的RAG链路
这一部分从零开始搭建一个 RAG 最小系统。技术栈选择 Python 生态的 LangChain 作为编排工具,Chroma 作为本地向量库,HuggingFace 的本地 Embedding 模型做向量化,LLM 使用任意兼容 OpenAI Chat 接口的服务或本地部署模型。下面示例用于说明完整流程,实际项目要结合自己的包名、路径和模型版本调整。
2.1 环境准备与依赖版本
建议使用 Python 3.9 以上版本。安装依赖时不需要一次性装完所有 LangChain 相关包,只需要当前链路需要的部分:
python --version pip install langchain langchain-community langchain-huggingface chromadb sentence-transformers pypdf各依赖的作用:
langchain:提供文本分割、Chain 编排、Prompt 模板等核心能力。langchain-community:提供非核心的文档加载器、向量库封装等集成组件。langchain-huggingface:提供HuggingFaceEmbeddings,用于加载本地 Embedding 模型。chromadb:轻量级本地向量数据库,适合学习和小规模原型。sentence-transformers:Embedding 模型的推理依赖。pypdf:用于解析 PDF 文件。
注意:LangChain 在 0.1、0.2、0.3 等多个版本中 API 变化较大,很多类从langchain.xxx迁移到了langchain_community.xxx。如果你使用的是旧版教程代码,导入报错时优先检查包名是否已经迁移。
2.2 文档加载与解析全流程
RAG 第一步是把原始文档变成可以被后续处理的 Document 对象。不要把整个 PDF 直接扔给模型,因为 PDF、Word 等格式本质上是排版文件,里面包含页眉、页脚、表格、图片等复杂结构,不解析就直接切分会产生大量噪声。
先看一个加载 PDF 的示例:
from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("docs/project_manual.pdf") pages = loader.load() print(f"加载页数: {len(pages)}") print(pages[0].page_content[:200]) print(pages[0].metadata)loader.load()返回的是一个Document列表,每个Document包含page_content和metadata。metadata中通常会包含页码、来源路径等信息,这些信息在后续做来源引用时非常有用。
如果是批量加载文本类文件,可以使用DirectoryLoader:
from langchain_community.document_loaders import DirectoryLoader, TextLoader loader = DirectoryLoader( "docs/", glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}, ) documents = loader.load() print(f"加载文档数: {len(documents)}")加载完成后,还需要做一轮文本清洗。PDF 解析经常出现页眉页脚重复、多余换行、多余空格等情况:
import re def clean_text(text: str) -> str: # 合并连续三个以上换行 text = re.sub(r"\n{3,}", "\n\n", text) # 合并连续空格和制表符 text = re.sub(r"[ \t]+", " ", text) return text.strip() for doc in documents: doc.page_content = clean_text(doc.page_content)这里的检查点很简单:加载完成后,打印总字符数和文档条数,快速判断内容是否完整、是否出现大量乱码或空页。一个常见问题是扫描版 PDF,它本质上是一张张图片,pypdf解析出来可能只有空白内容,此时需要 OCR 技术配合处理,这属于另一条技术线。
2.3 文本分块策略与参数
大模型的上下文窗口有限,Embedding 模型通常也只适合处理一段几百字以内的文本,所以必须把长文档切分成多个文本块。分块质量直接影响后续检索质量。
推荐使用 LangChain 的RecursiveCharacterTextSplitter,它会按优先级逐级尝试分隔符,尽量保持语义完整:
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""], ) chunks = text_splitter.split_documents(documents) print(f"分块数量: {len(chunks)}") print(chunks[0].page_content)参数含义:
chunk_size:每个文本块的最大字符数。chunk_overlap:相邻文本块之间重叠的字符数。separators:切分时使用的分隔符优先级列表。
为什么需要chunk_overlap?因为如果一句话被恰好切到上一块末尾,下一块没有开头,语义就断掉了。重叠部分可以让前后块之间保留上下文衔接。
分块大小对检索效果的影响可以用下表简单概括:
| chunk_size | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 偏小(200 左右) | 检索粒度细,定位准确 | 上下文信息少,容易语义不完整 | 事实型问答、关键词检索 |
| 适中(500 左右) | 平衡召回率和上下文 | 中等长度,需要结合 overlap | 大多数通用文档 |
| 偏大(1000 以上) | 上下文完整,适合总结 | 向量平均化严重,检索精度下降 | 需要大段理解的技术报告 |
这里提到的“向量平均化”是指:当一段文本包含多个主题时,Embedding 会把所有信息压缩成一个向量,导致这段文本与任何单个问题的相似度都不高,检索阶段反而召不回相关内容。所以分块并不是越大越好。
2.4 向量化、存储与检索
文本块准备好之后,需要把它们转换成向量。向量化这一步由 Embedding 模型完成,它的目标是把语义相近的文本映射到向量空间里相近的位置。
中文场景可以优先选择支持中文的 Embedding 模型。下面使用BAAI/bge-m3,它是一个支持中英双语的模型,效果和本地部署成本都比较均衡:
from langchain_huggingface import HuggingFaceEmbeddings embedding_model = HuggingFaceEmbeddings( model_name="BAAI/bge-m3", encode_kwargs={"normalize_embeddings": True}, )首运行时会下载模型权重到本地缓存目录。如果网络环境受限,可以提前在其他机器下载权重,放到本地目录后再通过model_name指定本地路径。
向量存储和检索使用 Chroma:
from langchain_community.vectorstores import Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )执行完成后,./chroma_db目录下会保存向量索引。后续再次运行可以通过Chroma(persist_directory="./chroma_db", embedding_function=embedding_model)重新加载,而不需要重新分块和向量化。
检索最小示例如下:
query = "项目的部署环境有哪些要求?" retrieved = vectorstore.similarity_search(query, k=4) for i, doc in enumerate(retrieved): print(f"--- 第 {i + 1} 个结果 ---") print(doc.page_content[:150])k=4表示召回与问题最相似的 4 个文本块。这一步是 RAG 中最关键的动作,后续生成质量完全取决于这 k 个文本块是否正确。
2.5 生成回答并验证
最后一步是把检索到的文本块和用户问题一起交给大模型。这里使用一个通用 Prompt 模板:
from langchain.prompts import PromptTemplate template = """你是一个严谨的知识库问答助手。请只根据下面的上下文回答问题。 如果上下文中没有足够信息,请直接回答“根据当前资料无法确定”,不要编造。 回答时尽量简洁,并注明信息来源。 上下文: {context} 问题:{question} 回答:""" prompt = PromptTemplate( input_variables=["context", "question"], template=template, )通过 LangChain 的RetrievalQA串联检索和生成:
from langchain.chains import RetrievalQA # 假设 llm 已经初始化,具体初始化方式取决于你使用的模型服务 llm = None # 替换为你的 LLM 实例 qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), return_source_documents=True, chain_type_kwargs={"prompt": prompt}, ) result = qa_chain({"query": "项目的部署环境有哪些要求?"}) print(result["result"])需要把llm替换成真实可用的模型实例。常见的接入方式包括 OpenAI 兼容的 Chat 接口、Ollama 本地模型、vLLM 部署的服务等,使用哪种取决于你的资源和场景。
运行之后有两个校验点:
- 回答是否忠于检索上下文,还是出现了上下文里没有的内容。
- 返回的
source_documents中是否真的包含能支撑回答的片段。
这两点往往比“回答流不流畅”更重要。如果回答流畅但内容完全来自模型自身记忆,那 RAG 的约束就没有起作用。
3. 影响RAG效果的关键参数与调优
很多 RAG 项目第一版跑通后效果不佳,问题往往不是出在大模型上,而是出在分块、向量化、检索策略和 Prompt 这几个环节。
3.1 分块大小与重叠参数
分块是 RAG 效果波动最大的环节之一。chunk_size和chunk_overlap需要根据文档类型调整:
- 对于条款类文档,比如协议、规范,一个条款通常就是一个语义完整单元,可以按条款编号、章节标题切分,比固定字符数切分更可靠。
- 对于技术手册,段落之间有较强上下文关联,建议使用较大的
chunk_overlap,比如 80 到 100 字符。 - 对于表格和代码块,固定字符切分会破坏结构,最好先把表格转成 Markdown 表格、代码块保持整体,再做切分。
判断分块是否合理的信号有三个:
- 检索召回的前 k 个块中,是否总包含正确答案所在的片段。
- 单个块内部是否只讲一个主题。
- 关键术语是否在块内首次出现时就有合理解释。
这里常犯的错误是直接照抄教程里的 500/50 参数。不同的 Embedding 模型对文本长度敏感度不同,落地前应该用你自己的文档做一组小实验,对比不同分块参数下的召回效果。
3.2 Embedding 模型与向量库选型
Embedding 模型决定“语义相似”怎么计算。中文场景常用的几类选择:
| 模型 | 特点 | 适用场景 |
|---|---|---|
| BAAI/bge-m3 | 中英双语,支持长文本,效果稳定 | 多数中文知识库 |
| m3e-base / m3e-large | 中文优化,轻量 | 中文问答、小规模项目 |
| text2vec-large-chinese | 中文匹配效果好 | 中文短文本检索 |
| 商业 Embedding API | 调用简单,维护成本低 | 对数据外发无限制的项目 |
这里有一个容易被忽略的点:检索阶段使用的 Embedding 模型必须和入库阶段一致。如果文档入库存量时用的是 A 模型,查询时换成了 B 模型,两个模型的向量空间不兼容,检索结果会完全不可用。
向量库选型同样重要:
| 向量库 | 部署方式 | 适合规模 | 特点 |
|---|---|---|---|
| Chroma | 本地/嵌入式 | 小规模原型 | 零运维,入门快 |
| FAISS | 本地/服务 | 中等规模 | 检索性能高,无内置数据管理 |
| Milvus | 独立服务 | 大规模生产 | 分布式、过滤、权限控制完整 |
| pgvector | PostgreSQL 插件 | 中等规模 | 与业务库同库,避免多系统 |
| Elasticsearch | 独立服务 | 混合检索场景 | 原生支持关键词和向量检索 |
学习阶段用 Chroma 就够了,生产环境则要考虑数据规模、并发、备份和权限,这些不是本地嵌入式向量库能直接承担的。
3.3 检索策略参数
RAG 生成质量的上限由检索决定。下面几个参数是排查效果问题时的重点关注对象。
k(召回数量):
- 默认常见值是 3 到 5。
k太小,可能漏掉正确答案。k太大,会引入噪声块,稀释上下文。- 调参时先看一个直观现象:正确答案通常排在第几位。如果正确答案稳定排在第一位,
k=3通常够用;如果正确答案经常排到第五位以后,应该优先优化分块和 Embedding,而不是继续增大k。
相似度阈值:
retriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.5, "k": 4}, )score_threshold表示只保留相似度超过阈值的文档。阈值调太高,容易召回为空;调太低,噪声块大量涌入。不同 Embedding 模型产出的分数范围不一致,不要直接套用别的项目的阈值,先打印一批真实查询的相似度分数,再决定阈值。
混合检索:
向量检索擅长语义匹配,但遇到精确 ID、编号、型号、全文包含的专有名词时,关键词检索往往更可靠。生产项目常见做法是向量检索 + BM25 关键词检索,再把两类结果做合并和去重,之后进入 Rerank 阶段。
Rerank:
召回阶段为了召回率会保留较多候选文档,Rerank 用一个专门的排序模型对这些候选重新打分,把最相关的文档排到最前面。这在文档数量较大的知识库中提升明显。常用的方案有bge-reranker等模型,使用时把它放在向量检索之后、生成之前。
3.4 Prompt 模板设计
Prompt 在 RAG 中承担的任务不是“让模型更有创意”,而是“把模型约束在事实范围内”。一份合格的 RAG Prompt 通常包含四类指令:
- 角色和身份:告诉模型它是知识库问答助手。
- 任务描述:要求基于上下文回答,而不是凭记忆。
- 约束条件:不确定时明确说不知道,不要编造。
- 输出格式:是否需要引用来源、是否使用列表、回答长度。
上面第 2.5 节给出的模板已经覆盖了前三点。再补充一个带来源引用的示例:
template = """你是知识库问答助手。请基于下面的上下文回答问题。 在回答末尾,列出你参考的文档编号。 约束: 1. 只使用上下文中的信息。 2. 如果上下文不足,回答“根据当前资料无法确定”。 3. 不要输出上下文外的推测。 上下文: {context} 问题:{question} 回答: 【结论】 【参考文档】 """实际项目可以在context中携带文档的 metadata,例如来源:project_manual.pdf 第 3 页,模型就能在回答时引用具体来源。
3.5 从“能跑”到“精准”的建设路径
如果现有 RAG 表现不稳定,按下面的顺序逐层调优:
- 先确认文档加载和清洗是否正确,有没有乱码和空白页。
- 再检查分块是否切碎关键句子。
- 然后检查检索 TopK 里是否包含正确答案。
- 接着检查 Rerank 是否把正确答案排到前面。
- 最后检查 Prompt 是否把生成约束在上下文中。
很多团队跳过前几步直接换大模型,结果换了更强的模型,回答依然错误。原因往往是检索层已经丢了正确答案,生成端再强也没有用。
4. RAG知识库指标:怎么选、怎么理解、怎么用
判断 RAG 效果不能只靠肉眼抽查几条回答。肉眼看不全面,也无法在文档更新后做回归对比。要建立一套可重复的评测方式,把检索质量和生成质量分开衡量。
4.1 为什么要单独评测
一个端到端的 RAG 系统由“检索”和“生成”两个阶段组成。如果最终回答错误,可能是检索召回的内容不对,也可能是检索正确但生成时没有正确利用上下文。如果不分开评测,你就无法定位问题在哪一层。
所以评测要拆成两个维度:
- 检索质量:模型有没有把正确答案对应的文本块召回出来。
- 生成质量:模型有没有基于召回的文本块生成忠实回答。
4.2 检索质量指标
构建评测集时,准备若干条问题,并为每个问题标注“正确答案所在的文本块编号”。然后让检索器返回TopK结果,计算以下指标:
Hit Rate(命中率)
- 含义:在数据集中,
TopK结果里至少包含一个正确答案的查询占比。 - 用途:判断检索是否基本可用。
- 示例:10 个问题中有 8 个在 Top5 里召回了正确答案,Hit Rate@5 = 0.8。
MRR(Mean Reciprocal Rank,平均倒数排名)
- 含义:对每个查询,取第一个正确答案的排名倒数,再对所有查询取平均。
- 用途:判断正确答案排名是否尽量靠前。
- 示例:两个查询中正确答案分别排在第一位和第三位,MRR = (1/1 + 1/3) / 2 = 0.667。
Recall@K 和 Precision@K
- Recall@K:TopK 召回的相关文档占全部相关文档的比例。
- Precision@K:TopK 中相关文档占 TopK 的比例。
- 用途:在“一个查询对应多个相关片段”的场景中使用。
下面是一个模拟命中率计算的 Python 示例:
def hit_rate(retrieved_ids, ground_truth_ids): hit_count = 0 for retrieved, truth in zip(retrieved_ids, ground_truth_ids): if set(retrieved) & set(truth): hit_count += 1 return hit_count / len(retrieved_ids) # 示例数据 retrieved_ids = [ [12, 34, 56, 78], # 第一个查询召回的块ID [10, 20, 30, 40], # 第二个查询召回的块ID ] ground_truth_ids = [ [56], # 第一个查询的正确答案块ID [99], # 第二个查询的正确答案块ID ] print(hit_rate(retrieved_ids, ground_truth_ids)) # 输出 0.5这段代码只是用 Python 原生列表模拟计算,实际项目中把向量库返回的Document的metadata或id传入即可。
4.3 生成质量指标
生成质量的评估比检索更复杂,因为它涉及语义判断。社区中比较常用的三类指标:
Faithfulness(忠实度)
- 含义:回答内容是否完全由上下文支持,没有编造。
- 判断方式:逐个检查回答中的事实点,是否都能在上下文中找到依据。
- 典型场景:回答中出现了“根据文档,该系统支持 XX 功能”,但上下文中根本没有提到 XX,则忠实度低。
Answer Relevance(答案相关性)
- 含义:回答是否针对用户问题,而不是答非所问。
- 判断方式:删除上下文后,单独看回答和问题的相关性。
- 典型场景:用户问“部署需要什么硬件”,回答却介绍软件架构,答案相关性低。
Context Relevance(上下文相关性)
- 含义:检索出的上下文是否与问题相关。
- 判断方式:逐句检查上下文中的内容有多少比例和问题相关。
- 典型场景:问题问的是“配置项怎么改”,召回结果却全是“产品简介”的内容,上下文相关性低。
这三类指标在 Ragas 等评测库中有对应的实现思路。Ragas 采用 LLM 辅助打分,即把上下文、问题、回答组织成评测 Prompt,让一个强模型输出打分。使用时要留意:LLM 辅助评测本身也有偏差,因此评测模型最好与生成模型不同,避免自评偏差。
4.4 评测集和落地方式
一个完整的 RAG 评测流程包含:
- 构建评测集。从真实场景中收集 50 到 200 条问题,越多越能反映真实分布。问题要覆盖简单事实查询、组合查询、边界查询。
- 标注正确答案。为每条问题标注检索层面的正确块 ID 和生成层面的期望回答要点。
- 运行评测。分别计算检索指标和生成指标。
- 对比回归。修改分块参数、更换 Embedding 模型、调整 Prompt 后,重新运行同一套评测集,用指标对比效果变化。
评测集分为“检索评测集”和“生成评测集”。检索评测集只包含问题和正确答案块 ID,不需要完整参考答案;生成评测集还需要参考答案或至少标注回答要点。这样拆开,可以在调检索时不重复评估生成。
指标怎么理解要结合场景:
- 如果业务对答案准确性要求极高,优先保证 Faithfulness。
- 如果业务关注用户能否快速找到信息,优先优化 Hit Rate 和 MRR。
- 如果回答经常跑题,先检查 Answer Relevance。
- 如果回答内容完整但引用的上下文不对,先检查 Context Relevance。
5. 进阶路线:Agentic RAG、Graph RAG、OAG 与工具化平台
基础 RAG 已经能解决不少问题,但面对复杂查询、跨文档整合、高度结构化文档时,需要演进到更高级的架构。
5.1 Agentic RAG:把检索变成可规划的步骤
经典 RAG 是“一问一检”:用户提交问题,系统检索一次,生成一次回答。Agentic RAG 则让大模型作为 Agent,自己判断是否需要检索、检索几次、使用什么工具。
典型场景包括:
- 用户问题同时涉及多个文档,需要多次检索并整合。
- 用户问题本身不清晰,Agent 先反问澄清,再检索。
- 用户需要执行“先查条件,再筛选数据”的复合流程。
实现思路上,Agent 通过 ReAct 循环工作:接收用户问题,决策是否调用检索工具,观察检索结果,再决定下一步动作,最终生成回答。LangChain 的create_retriever_tool可以把检索器包装成一个 Agent 可调用的工具。
Agentic RAG 的优势是灵活,代价是复杂度上升:模型可能做出错误决策、调用次数增加、延迟变高、错误可能被多步放大。生产环境中要给 Agent 设置最大迭代次数、单次检索返回数量上限,并对超时和失败做兜底。
5.2 Graph RAG 与 Ontology RAG:让知识组织更精确
Graph RAG 是在 RAG 链路中引入知识图谱技术。与向量库只存文本块不同,Graph RAG 把实体、关系和属性存成图结构。查询时先通过实体匹配定位到图节点,再沿关系扩展相关上下文,最后把扩展结果交给生成器。
它的典型优势在于处理“实体关系型”问题,比如:
- “某个协议中,A 网元与 B 网元的接口流程是什么?”
- “某个专利的权利要求依赖哪些从属权利要求?”
- “某类故障会导致哪些下游模块受影响?”
这类问题如果只靠向量相似度检索,往往只能召回包含关键词的片段,无法沿着关系链路找到完整答案。Graph RAG 更适合。
Ontology RAG 更进一步,在图中定义清晰的类型、属性和约束关系。比如在 3GPP 协议文档中,定义“网元”“接口”“流程”“参数”等类型,以及它们之间的关联关系。检索时先判断问题涉及的实体类型,再按本体约束检索,能显著提高召回准确率。
它的落地成本也比较高:需要人工或半自动抽取实体关系,构建和维护图谱,对文档结构变化敏感。适合协议、专利、法规等高度结构化、长期稳定的文档领域。
5.3 RAG 与 OAG 的差异
社区讨论中还有一个概念是 OAG,即 Online Augmented Generation,在线增强生成。相对于经典 RAG 的“离线构建索引 + 在线检索生成”,OAG 更强调在线检索与生成的实时联动,典型做法是从搜索引擎、实时 API、实时数据库获取最新信息,并即时组织回答。
这两者的边界在工程中并不需要严格划分。离线知识库适合公司内部私有文档,在线检索适合时效性要求高的公开信息。实际系统往往同时包含两条链路:先查内部向量库,查不到或需要最新资讯时再调用在线搜索引擎,最后统一交给模型生成。设计时重点不是争论概念,而是明确数据源、查询路由和结果合并策略。
5.4 常用框架与平台选型
工程落地时,选择框架和平台会直接影响开发效率。常见的选择如下:
| 工具/框架 | 定位 | 适合场景 |
|---|---|---|
| LangChain | 开发框架 | 深度定制 RAG 流程、需要细粒度控制 |
| LlamaIndex | 开发框架 | 文档索引和知识库构建场景 |
| Dify | 可视化平台 | 快速搭建应用、多人协作、包含工作流 |
| AnythingLLM | 本地知识库工具 | 个人和小团队快速使用私有知识库 |
| Spring AI | Java 生态框架 | 已有 Java 技术栈的团队做 AI 集成 |
选型原则很简单:如果你的核心能力是业务逻辑和模型策略,直接用 LangChain 或 LlamaIndex;如果团队需要快速做应用演示,Dify 这类平台能省去很多前端和运维工作;如果是 Java 团队,优先考虑 Spring AI,避免引入异构技术栈。
6. 生产落地:常见问题、排查链路和最佳实践
6.1 排查链路:从用户输入到回答逐层定位
RAG 问题的定位顺序应该是:输入 -> 文档加载 -> 分块 -> 向量化 -> 检索 -> 生成。每一层都可能引入错误,排查时按层检查,不要跳过。
| 排查层 | 核心问题 | 检查方法 |
|---|---|---|
| 用户输入 | 问题本身是否清晰 | 打印原始 query |
| 文档加载 | 文档是否被正确解析 | 检查页数、字符数、乱码 |
| 分块 | 内容是否被切碎 | 打印样本块,看语义是否完整 |
| 向量化 | 向量是否有效 | 做相似度自检,同一句话检索自己 |
| 检索 | 正确答案是否被召回 | 打印 TopK 文档,人工核对 |
| 生成 | 模型是否正确利用上下文 | 对比上下文与回答的事实点 |
6.2 常见坑和解决方案
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 回答总是“不知道” | 阈值过高或检索结果为空 | 打印检索结果和相似度分数 | 降低score_threshold,检查知识库是否包含答案 |
| 回答流畅但与文档不符 | 模型依赖自身记忆,没有遵循上下文 | 对比回答与检索内容 | 强化 Prompt 中的约束,引入来源引用 |
| 检索结果全是相近内容 | 分块过大导致向量平均化 | 打印分块内容和检索 TopK | 减小 chunk_size,按文档结构调整分块 |
| 某类关键词永远检索不到 | 向量检索对精确编号不敏感 | 用关键词直接搜索原文档 | 加入 BM25 关键词检索,做混合检索 |
| 换模型后检索效果变差 | 入库和查询使用了不同 Embedding | 检查两个阶段模型名 | 统一入库 |