资讯动态

用Agentic CRAG根治大模型幻觉:RAG工程实践全解析

发布时间:2026/10/21 3:14:20 来源:尧图企业网站定制
大模型幻觉这个词搞AI应用的同行应该都不陌生。无论是客服问答、知识库助手还是报告生成模型一本正经地编造事实永远是上线前最让人头疼的问题。传统做法是上RAG检索增强生成让模型先检索再回答把答案“锚定”在真实资料上。但RAG不是银弹——检索结果本身不靠谱的时候模型照样会被带偏甚至把错的资料当成真理输出。我在这个方向折腾了大半年试过调阈值、换embedding模型、加重排序效果都不稳定。直到把纠正机制Corrective RAG和Agent思想结合起来做成Agentic CRAG幻觉率才真正降到了可接受的范围。这篇文章就把我整个工程化的过程拆开讲一遍从设计思路到落地的每一步都会给到可复现的方案和踩坑记录适合正在做RAG应用、被幻觉问题折磨的同路人。1. 先理解问题大模型幻觉到底是怎么来的1.1 幻觉的本质与高频场景得先把这个概念聊透。所谓幻觉就是模型生成了在语法上流畅、逻辑上自洽但事实上没有依据的内容。把大模型理解成一个“受过海量文本训练的概率完型填空器”它在生成每个token时都在预测下一个token最可能是什么。这个机制决定了只要遇到训练语料里没有明确覆盖的知识或者问题表述存在歧义它就倾向于用“最常见的补全”而不是“最准确的补全”。所以在事实问答、行业知识库、企业内部数据查询这些任务上幻觉几乎是必然发生的不是偶尔出错而是天然的缺陷。什么场景下幻觉最容易被引爆我总结过三类。第一类是知识边界外的问题。比如内部知识库只更新到上季度用户问的恰恰是这个季度的新政策。模型没有见过正确的资料会按照旧的知识“合理推断”一个新答案出来表面上看着像回事实际全错。第二类是检索噪声污染。哪怕有RAG检索回来的文档本身可能和问题只是“沾边”。比如用户问产品故障码的含义检索结果把一堆包含相同关键词但不相关的维修手册给出来了模型照单全收从一个错误context出发沿着错误方向输出。第三类是模板套用。模型一旦识别出熟悉的提问模式比如“如何配置某功能”就会直接套用最常见的行文套路把不存在的参数、不存在的配置项编出来填进去。这三类场景的共同点是模型拿到了错误的上下文或没有上下文却仍然被要求给出答案。理解了这一点后面所有方案都是围绕“如何让系统在上游拦截错误上下文而不是在下游祈祷模型不乱说”展开的。为了量化这件事通常在做RAG应用上线前会手工抽100到200条困难样本用人工评审来统计“无中生有率”。我见过的基线数据大概是直接在通用大模型上跑幻觉率15%到30%都是常态上了传统RAG之后可以压到5%到10%但要再往下走就需要动检索和判别这一步了。1.2 为什么常规RAG治标不治本RAG的基本流程大家应该都熟悉把资料切成chunk用embedding模型向量化存入向量库用户提问时把query向量化后在库里做相似度检索取top-k个chunk最后把这些chunk拼进上下文交给大模型生成答案。这本质上是对生成过程做一次“外部知识约束”让模型先看到真实资料再开口。这样做能挡住一部分幻觉原因是模型不再完全依赖参数记忆而是有了可参考的事实文本。但它有三个明显的漏洞。第一检索质量无保证。向量相似度和语义相关度并不是一回事。两个句子用词不同但意思相同相似度可能很低反之一个精确包含query关键词但意思完全无关的段落相似度反而很高。top-k一旦混入了这种“关键词命中但语义跑偏”的噪声模型就会诚实地把噪声内容当成依据输出。第二模型没有“质疑”能力。输入给模型的上下文质量标准没有做判断模型生成时默认的是“上下文里说的一定是对的”。哪怕检索回来的资料自相矛盾或者资料与query的时间版本不匹配模型也会直接采信不会主动指出资料有问题。第三失败时没有兜底策略。RAG链路一旦检索到空集、低相关集绝大多数实现是硬着头皮把拿到的内容塞给模型让模型看着答。这不是RAG的错而是没有定义一个“拿着垃圾资料该怎么办”的决策机制。普通RAG的架构里压根不包含这个决策层。传统CRAG的出现就是给这个链路加了一个“检索质量评估器”评估当前检索结果的质量再根据质量分走不同的纠正分支。这是对的但第一代CRAG的问题是评估完之后只做一次性纠正质量差就重写一次query再检索再不行就放弃。这本质上是一个线性if-else缺少循环和反思遇到复杂一点的query就力不从心。所以后来我把Agent那一套决策、循环、工具调用的思路搬进来做成Agentic CRAG算是把最后一块拼图补上了。2. Agentic CRAG的设计思路让系统自己判断该不该信检索结果2.1 三个核心角色评估器、纠正器、生成器Agentic CRAG的核心不是某个模型而是三个角色加一个循环。评估器负责回答“我们当前检索到的资料到底能不能回答当前问题”纠正器负责回答“如果不能该怎么调整检索策略”生成器负责在确认上下文可用之后产出最终答案。这三个角色可以由同一个大模型实例扮演也可以分开用不同模型工程上各有利弊。评估器是整个系统的“守门员”。它输入一条query和一组候选文档输出每个文档的相关性得分然后聚合成一个整体置信度。建议不要拿那种参数量特别大的商用模型来做这件事因为评估是在每条query上都要触发的高频操作成本会被打爆。更稳妥的做法是用一个中小尺寸的开源模型配一个固定的评估提示词设置temperature为0要求直接输出JSON格式的分数和简要理由然后解析结果再归一化到0到1之间。如果团队有能力拿到历史badcase标注数据用一个小型cross-encoder跑一个相关性回归模型会更稳延迟也能压到几十毫秒。纠正器是后面所有动作的调度中心。它的输入是原始query、当前检索到的文档、评估器的打分结果以及前几轮已经做过的尝试记录。输出是下一步动作可能包括原封不动去生成、改写query后重新检索、换一个检索字段或filter重试、切换到外部搜索引擎补充资料、或者直接放弃检索转入明确的“无法回答”流程。这里的关键不只是干什么而是要让纠正器记住“我上一轮已经试过什么”否则很容易原地打转。生成器相对常规它可以复用RAG链路里原本那一个生成模型。但提示词要单独写不能和普通问答共用一套。Agentic CRAG里的生成器至少要接收两个附加信息一是检索资料的置信度评估结果让它在面对低置信度资料时主动收敛语气二是一个明确许可——如果资料不足或全部不相关输出“根据现有资料无法回答”是允许且被鼓励的。这一条我在后面展开因为很多人幻觉压不下来就是卡在生成器上。2.2 状态机设计决定什么时候该重检索、什么时候该生成把一个Agentic CRAG系统拆成状态机来建模最清晰。常见的工程实践是设计一个“初始检索→质量评估→分支选择→执行纠正→再评估→生成或终止”的循环每条query最多迭代N轮N一般取2到4。为什么必须有迭代上限因为纠正本身是有成本、有失败率的一个永远绕着圈找资料的agent和一个干脆认输的agent相比前者会让用户等待更久、消耗更多token并且结果未必更好。工程上更倾向后者那种“有限的努力有尊严地止损”。在这个状态机里最关键的一步是“分支选择”的决策规则。我把它分成三级。第一级整体置信度高于高阈值比如0.8。这种情况直接进入生成中间不插入额外步骤。注意生成前建议加一个“上下文精筛”操作即从12到16篇候选文档里只保留得分最高的3到5篇再拼成一个精简的上下文。不要贪多文档太多反而会稀释生成器对关键事实的注意力。第二级整体置信度介于中阈值和高阈值之间比如0.4到0.8。这表示检索到了相关线索但不完全匹配。触发query改写鼓励生成多个平行改写版本再做一次多路检索把各路结果汇总后再次评估。query改写要强制保留原始query中最核心的实体或关键词防止模型把问题问偏。举例来说原始query是“某型号路由器如何恢复出厂设置”改写可以是“某型号路由器重置方法”“某路由器恢复默认配置的操作步骤”但绝不能改写成一个不含型号的泛化问题。第三级整体置信度低于中阈值比如0.4。这表示知识库里的资料大概率完全不覆盖该问题。此时不能硬编硬套我的做法是先尝试一个“库内扩展”不做向量相似检索而是降低相似度阈值、放宽过滤条件再捞一次看是否存在漏网之鱼。如果还是没有就退出到“明确表示无法回答”的分支同时把该条query标记为“知识库覆盖缺口”后续转人工或沉淀到知识库里。这套三级决策看起来是几个if-else但配合多轮循环、重写记忆和置信度累积效果远好于一次性纠正。还有个重要细节每轮评估的结果要保留在状态里最后生成时可以把“我经过了X轮检索最终置信度是Y”也拼进上下文让生成器对“当前资料是否可信”有个全局感知。2.3 与普通RAG和CRAG相比多了什么把三种方案放在一起对比过最直观。环节传统RAGCRAGAgentic CRAG检索质量评估无有单次有每轮循环失败处理无兜底单次纠正多轮决策多重兜底查询改写无有但固定可生成多个改写并多路检索工具调用无单一内库内库外部检索规则扩展迭代/反思无无有带历史记忆明确的“不知道”输出无少数内建为合法状态从这张表能看出来Agentic CRAG真正多出来的不是“多了一次重写”而是把系统从“线性流水线”升级成“带反馈回路的闭环”。线性流水线里任何一环出错错误只能顺流而下闭环系统里错误会在评估环节被拦截触发纠正实在纠正不过来也不会让错误流到生成环节。用控制论的类比来说这不是更强的过滤器而是引入了负反馈让系统在错误频发的高噪声环境下也能收敛到一个可使用状态。这就是它能把幻觉压到更低水平的根本原因。3. 工程实现从零搭建一个可运行的Agentic CRAG3.1 系统架构与模块划分动手写代码之前先把模块边界定好。我的项目里分了六个模块。检索器Retriever封装向量库检索、关键词检索和外部检索统一暴露成retrieve(query, top_k, filters)接口。评估器Evaluator对query加每个候选文档做相关性打分提供evaluate(query, docs)接口返回文档级分数列表和整体置信度。决策器Decider读评估结果和历史记录输出下一步动作实现decision(state)状态机逻辑。修正器Rewriter调用LLM生成query改写版本实现rewrite(original_query, current_docs, history)。压缩器Compressor对最终选中的文档做去重和截断组装成不超过生成器上下文窗口的“事实包”。生成器Generator消费事实包生成最终答案或输出“无法回答”。模块之间不互相依赖只是通过一个共享的状态字典交换数据。后续想替换评估模型、增加新的检索源都不需要动其他模块。这也是我做工程化改造时最大的心得算法可以花哨但模块边界必须朴素否则上线后一改就崩。建议的目录结构大致是这样src/ retriever/ __init__.py base.py vector_store.py keyword_fallback.py external_search.py evaluator/ __init__.py llm_evaluator.py score_normalizer.py decision/ __init__.py state_machine.py actions.py generator/ __init__.py answer_generator.py prompt_builder.py schemas/ state.py types.py tests/ eval_set/ golden_queries.json3.2 检索评估器的实现打分模型怎么选评估器是整套系统能不能压住幻觉的关键值得单独说说。评估器直接决定“当前资料行不行”的判断判断错一次后面全错。第一个选择是用大模型API加评估提示词来做。优点是零样本就能上路语义理解强能给出可解释的理由缺点是延迟高、费用高而且大模型打分存在偏差有时候会为了显得合理给低质量文档打出偏高分数。第一批做demo时就是这么干的配temperature0和强制JSON输出能跑通但每天耗费的token量确实肉疼。第二个选择是训练一个小型回归模型。如果有标注好的query-doc对数据用cross-encoder架构做一个打分模型很合适。训练数据不用太多几千条高质量标注就能让效果不低于大模型评估关键是latency能压到几十毫秒费用接近零。没有标注数据的情况下可以用大模型API对一批历史query做标注生成soft label之后再蒸馏到小模型这是一条工程上非常推荐的路线。第三个选择是纯规则加向量相似度。比如直接用相似度top-score做置信度简单省事但精度有限。我会用它作为评估器的baseline方便后续验证“花力气做的评估模型到底带来了多少提升”。无论选哪个最后都要统一输出一个0到1之间的得分。常用的归一化方式有两种如果评估器输出离散等级比如1到5分就按“分数减1除以4”映射到0到1如果输出连续分直接除以满分的最大值。文档级得分算出后整体置信度我用max(t_doc)做主信号、mean(t_doc)做辅信号加权合并权重大概七三开。max能反映“我们有没有找到一篇一击命中的好文档”mean则反映“整体质量别太拉胯”。3.3 决策与纠正流程的代码骨架下面直接贴核心状态机的代码骨架。整体思路是维护一个状态字典在一个循环里执行“检索-评估-决策”直到达到终止条件。为了让代码可读性更好没有套用具体的外部框架而是用原生Python写了一个简化版方便把核心逻辑摘出去复用到自己的项目里。import json from dataclasses import dataclass, field HIGH_CONF_THRESHOLD 0.80 MID_CONF_THRESHOLD 0.40 MAX_ITERATIONS 3 dataclass class State: original_query: str current_query: str docs: list field(default_factorylist) scores: list field(default_factorylist) overall_confidence: float 0.0 iteration: int 0 history: list field(default_factorylist) best_docs: list field(default_factorylist) best_conf: float 0.0 finished: bool False final_action: str generate def evaluate(query: str, docs: list) - tuple[list, float]: # 调用评估器返回(文档级得分列表, 整体置信度) # 文档级得分与docs一一对应0~1 # 整体置信度按 max 和 mean 加权如 0.7*max 0.3*mean pass def rewrite_query(s: State) - str: # 调用LLM生成改写后的query保留原始关键实体 pass def retrieve(query: str, top_k: int 16) - list: # 调用检索器返回候选文档列表 pass def compress(docs: list, scores: list, max_tokens: int 3000) - str: # 按得分排序截断组装成事实包 pass def generate_answer(query: str, evidence_pack: str, confidence: float) - str: # 调用生成器附带置信度和“可输出无法回答”的提示 pass def run_agentic_crag(query: str) - str: s State(original_queryquery, current_queryquery) while not s.finished and s.iteration MAX_ITERATIONS: # 1. 检索 s.docs retrieve(s.current_query, top_k16) if not s.docs: # 库里空结果直接标记挂起 s.final_action insufficient break # 2. 评估 s.scores, s.overall_confidence evaluate(s.current_query, s.docs) # 记录当前轮次最佳结果 if s.overall_confidence s.best_conf: s.best_docs s.docs s.best_conf s.overall_confidence # 3. 决策 if s.overall_confidence HIGH_CONF_THRESHOLD: s.finished True s.final_action generate break elif s.overall_confidence MID_CONF_THRESHOLD: # 有线索但不完全匹配改写query再搜 s.history.append({ iter: s.iteration, query: s.current_query, conf: s.overall_confidence, action: rewrite }) s.current_query rewrite_query(s) s.iteration 1 else: # 低置信度先尝试放宽条件搜一次不行就退出 fallback_docs retrieve(s.current_query, top_k32) fb_scores, fb_conf evaluate(s.current_query, fallback_docs) if fb_conf s.best_conf: s.best_docs fallback_docs s.best_conf fb_conf if fb_conf MID_CONF_THRESHOLD: s.docs, s.scores, s.overall_confidence fallback_docs, fb_scores, fb_conf s.history.append({iter: s.iteration, action: fallback_retrieve}) s.iteration 1 continue s.final_action insufficient s.finished True break # 4. 生成 if s.final_action insufficient: return 根据现有资料暂时无法回答这个问题。 docs s.best_docs if s.best_docs else s.docs scores s.scores if s.best_docs is None or s.best_conf s.overall_confidence else \ [0.0] * len(docs) evidence_pack compress(docs, scores, max_tokens3000) answer generate_answer(s.original_query, evidence_pack, s.best_conf) return answer这段代码为了篇幅刻意精简了但核心状态流转是完整的。实际工程里可以把evaluate、rewrite_query、retrieve这些函数替换成自己的实现状态机部分基本不用大改。为什么强调在循环里记录best_docs和best_conf因为有相当多的情况是第0轮检索置信度0.5改写了一轮后置信度掉到0.3如果最后拿第1轮的垃圾文档去生成结果一定很差。保留历史最佳结果至少保证最差也能回到一个尚可的上下文上。3.4 与生成器衔接上下文压缩与提示词设计检索和评估都完成后再有一道容易被忽视的工序把选中的文档压缩成“事实包”。直接把所有候选文档原文按顺序塞给生成器在工程上是灾难。文档一多生成器注意力被分散无关细节经过幻觉补全后被放大文档超过上下文窗口还要被截断截断位置往往正好切掉关键事实。我的经验是按评估得分从高到低选不超过5篇每篇截断到可接受长度然后合并成一个结构清晰的事实包。格式可以类似这样【资料1】来源产品手册v2.3 型号A300 恢复出厂设置方式长按reset键10秒... 【资料2】来源FAQ-2024-Q3 ...给生成器交代两件事。第一事实包里的内容是参考依据不是必须全文复述的模板。第二如果事实包与问题对不上输出“根据现有资料无法回答”完全合法不算模型失败。这第二点极其重要很多RAG应用幻觉压不下来不是检索差是生成器没有说“我不知道”的许可。它宁可编一个流畅答案也不愿意承认自己不会这是模型训练目标决定的必须在提示词层面把它掰过来。4. 效果评测与调优不要凭感觉说“幻觉少了”4.1 一套可复用的评测集与指标在交付现场被问到最多的一个问题就是“你说幻觉率降了怎么证明”如果不建评测集所有的优化都是拍脑袋。评测集我建议至少包括四类样本各50条知识库内的常规问题、知识库边缘问题即需要多篇文档拼接才能答全的问题、知识库覆盖之外的问题、以及对抗性样本就是故意用高度相似但错误的表述去误导检索的那些问题。指标方面我重点关注三个。幻觉率生成回答中无法被给定资料支持的陈述比例可以用NLI模型或LLM裁判来逐句判定人工抽检复核。回答相关率答案与用户问题在语义上是否契合防止系统为了追求忠实度而给出“正确但没用”的回答。上下文命中率最终回答的事实是否都能在压缩后的事实包里找到出处。上线前后通常会让同一批测试集分别跑“无RAG生成”“传统RAG”“CRAG”“Agentic CRAG”四条链路把三个指标并排摆出来。这个对比除了验证收益还有一个额外用途让团队里不参与算法的人直观理解为什么要把链路搞复杂。4.2 阈值、迭代上限、模型选择的实战调参阈值怎么定刚开始也是拍脑袋后来发现还是要靠评测集来找。我的做法是把高阈值和中阈值拆开调固定中阈值为0.4把高阈值从0.6扫到0.95观察每条链路的幻觉率和平均耗时。高阈值设太低本来高质量的检索结果也要被改写成低质量内容设太高一部分本来可答的问题被分流进低置信度分支增加无谓的迭代。在一组知识库问答数据上最终取了0.82比0.75整体幻觉率低了约一个点比0.9的耗时少了约30%。迭代上限最终定为3轮。规则是第1轮直接检索第2轮改写后多路检索第3轮再检索一次并强制收敛。超过3轮之后再多迭代的边际收益几乎趋近于零但时间和成本同步翻倍。如果场景是允许更长延迟的深度研究型应用上限可以适当放大到5但个人不建议无上限因为无上限的agent在遇到一个根本不在知识库里的query时会陷入“检索-评估-重写-检索”的螺旋里很浪费资源。模型选择这里只说结论评估器比生成器更值得花钱。如果预算有限把精力和模型成本投到评估器上让评估结果更准确比升级生成器更有效。因为Agentic CRAG的纠错链路是围绕评估结果来决策的一个模糊的评估器会把所有下游动作都带偏。4.3 线上运行要注意的工程细节说几个上线后才能发现的问题。缓存策略是第一个同样或高度相似的query在评估、重写环节的LLM调用结果完全没必要重复计算。建议把query的embedding结果、评估结果、最终答案都做缓存按小时或按天失效线上命中率大概能到20%到30%能省不少成本。第二个是超时和熔断。评估、改写、外部检索都可能超过预定的响应时间要求。我在状态机里给每个节点单独设了超时阈值单个评估节点如果超过1秒就降级用上一轮结果或直接跳过外部检索超时就回退到知识库内检索。宁可让回答质量略降也不能让用户挂在那里等。第三个是日志。跑Agentic CRAG这种多轮系统一定要把每一轮的query、得分、动作、耗时完整落日志。这样将来任何一个case出问题能像放电影一样回放它每一步的判断否则排查起来真的头大。5. 踩坑记录与问题排查5.1 高频问题速查表在实际调试中把反复踩过的几个坑整理在了下面这张表里。现象根因解决思路改写后答案与原始问题无关生成的新query偏离原意提示词中强制保留原始实体词改写后做一次与原始query的相似度校验程序跑到第N轮还在检索缺迭代上限或上下文里没有记录历史轮次设上限3以内状态里记录history并在下一轮决策时读取核心实体被改写成同义词后检索不到embedding模型对专业实体不敏感重写时保留专有名词必要时在重写结果中直接拼回原始query高置信度但仍答错评估器被“看上去像相关”的文档骗过换更严格的评估器如交叉编码器或提高高阈值低置信度事件被触发过多中阈值设得过高或知识库覆盖确实不够降低中阈值观察误杀率扩充知识库外部检索把噪声带进上下文外部结果与内库结果冲突加仲裁逻辑内库结果置信度高时优先冲突时拒绝使用外部内容生成器经常输出“不知道”但又业务不允许提示词没有对“不知道”的场景做区分分成“绝对不知道”和“资料不足”两条输出模板5.2 三个印象最深的真实案例第一个是某电商客服项目。用户问的是某新产品的保修政策但知识库里只有旧产品手册。传统RAG下模型检索到旧手册的保修条款直接套用给出了错误日期。改成Agentic CRAG后评估器判出新手册与当前问题相关度只有中等触发重写“该新产品的保修政策”随后在多路检索中捞到了内部刚上传的新政策文档。整个过程多花了约1.2秒但答案从完全错误变成严格依据新政策。这个case给我的感触是多轮纠正在版本更新类问题上几乎是刚需。第二个是某法规问答场景用户问的问题知识库里真的没有。老系统会拼一个看似合理的答复内容涉及不存在的条款编号。Agentic CRAG在评估后整体置信度只有0.15先做了库内放宽检索还是没找到最后走“无法回答”分支输出的是一句明确的“当前资料暂未收录该条款请咨询人工”。这个知道不该答的行为比任何长篇大论都更能保住系统的可信度。第三个是让我改掉了一个坏习惯的case。有一次线上日志显示同一个问题反复触发重写循环检查后发现是把迭代上限调没了——当时为了追求答案质量把上限设成了10。结果某知识库盲区问题连跑4、5轮每轮检索到的都是无关内容白白烧了几万token最终答案也没好到哪里去。从那以后老实把上限调到3并且加上了“连续两轮置信度不再上升就提前终止”的规则。5.3 落地前最后的自检清单如果准备在自己项目里落地Agentic CRAG最后给一份清单。先确认评估器有稳定的输出格式不会因为输入分布变化而频繁返回解析失败的JSON再确认评测集有足够的覆盖来支撑阈值调优之后是生成提示词里“无法回答”分支的业务口径是否定好了同时把状态机的超时、熔断、降级逻辑单测覆盖上最后带着评估集跑一次从无RAG到Agentic CRAG的横向对比确保收益是实打实的再推到全量。我个人在实际操作中最大的体会是Agentic CRAG不是某个模型的功劳而是靠“评估-纠正-再评估”这个闭环把错误挡在生成之前。真正难的不是写状态机而是给评估器找到合适精度和成本的平衡点。如果刚开始没有预算训练专用的评估模型先用大模型API加JSON输出跑通全流程把业务价值验证出来再逐步替换成更轻量的小模型。这个方向后续还可以扩展的地方很多比如多路召回结果融合、跨知识源一致性仲裁、以及把多轮纠正记录做成反馈回流到检索排序模型里每一步都能让系统再稳一截。希望这篇工程记录能帮你在幻觉问题上少走一些我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑