Agent最让人头疼的问题不是答不出来,而是答得理直气壮却完全错误。用户问一个产品的退货政策,它编出一条不存在的规则;让它总结一份财报,它给出的数据与原文对不上。这类幻觉问题如果不从架构层面解决,单靠换更大的模型或调整提示词,收效非常有限。本文围绕两条主线展开:一是用检索增强生成(RAG)让Agent先查资料再回答,二是用事实核查机制在输出前做最后一道拦截。

幻觉从哪里来:先搞清楚根因再谈方案
大模型的知识存储在训练时的参数里,本质是一种有损压缩。训练数据截止之后发生的事、企业内部文档、实时价格等,模型根本不知道,但它被训练成要给出流畅完整的回答,于是只能靠"合理推测"填补空白,这就是幻觉的核心来源。参数化知识的特点是模糊且不可追溯,你无法问模型"这条结论你从哪学来的"。
另一类幻觉来自指令压力。当用户明确要求一个答案,而上下文里信息不足时,模型倾向于编一个满足格式要求的答案,而不是承认不知道。尤其是Agent场景下,系统提示词往往写死了输出格式和角色设定,这种压力会被放大。此外,长上下文中的信息丢失、检索结果与问题语义错位、多轮推理中的错误累积,都会让幻觉进一步加剧。
理解这些根因后可以得出结论:幻觉无法彻底消除,只能通过架构手段显著压低。核心思路有两点,一是给模型外挂可验证的知识来源(检索增强),二是给输出加上验证关卡(事实核查)。下面分别展开。
检索增强生成:让Agent先查资料再开口
RAG的基本流程是:将知识库文档切分后向量化存入向量库,用户提问时先检索出最相关的片段,拼入提示词中让模型基于这些片段作答。模型从"凭记忆回答"变成"开卷考试",幻觉率会大幅下降,而且每个答案都能溯源到具体文档,可解释性完全不同。
文档切分是最容易被忽视的环节。固定长度切分会把完整的语义单元拦腰斩断,比如一个退货政策条款被切成两半,召回后模型只看到半句话,很容易给出错误解读。推荐按结构化边界切分,比如标题、段落、条款编号,每块控制在300到500字符,并保留前后少量重叠内容,让上下文衔接不断裂。
import re
def split_by_structure(text, max_len=500, overlap=80):
# 先按段落切,再按句子细切,避免截断完整语义
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
chunks = []
for para in paragraphs:
if len(para) <= max_len:
chunks.append(para)
continue
# 长段落按句子边界切分,保留重叠区域
sentences = re.split(r'(?<=[。!?.!?])', para)
current = ''
for s in sentences:
if len(current) + len(s) > max_len and current:
chunks.append(current)
current = current[-overlap:] + s
else:
current += s
if current:
chunks.append(current)
return chunks
召回阶段建议采用混合检索:向量检索负责语义相似,关键词检索(如BM25)负责精确匹配型号、编号、专有名词。两者结果合并去重后再经过一个重排序模型(如bge-reranker),把真正相关的片段排到前面。只靠向量检索时,专有名词的召回经常翻车,用户问"SKU-A3821的保修期",向量检索可能返回一堆语义相近但产品完全不同的内容,模型一拼接就产生了张冠李戴式的幻觉。
提示词层面同样关键。要让模型明确知道"只根据给定资料回答,资料里没有就说不知道",并把这句话放在系统提示的显眼位置。实测中,加入一句"如果资料中没有相关信息,请直接回答无法确认",能明显降低模型硬编答案的概率。
prompt = f"""你是一个企业知识库助手。请严格遵守以下规则:
1. 只根据【参考资料】中的内容回答问题,禁止使用你自己的记忆补充
2. 回答中每一条事实都必须能在参考资料中找到对应依据
3. 如果参考资料不足以回答问题,直接说明"根据现有资料无法确认"
4. 回答末尾列出所引用资料的编号
【参考资料】
{retrieved_chunks}
【用户问题】
{question}
"""
这套约束的效果是双向的:既压制了模型自由发挥的空间,又强制答案可溯源,为后面的事实核查提供了锚点。
事实核查链路:在输出到用户之前拦住错误
RAG降低了幻觉概率,但不能降到零。检索可能召回了错误片段,模型可能在拼接资料时曲解原意,也可能在多步推理的中间环节引入偏差。因此需要在Agent输出前增加一道独立的验证环节,这就是事实核查链路。
最实用的方案是自然语言推理(NLI)式校验:把Agent的答案拆成一条条原子化陈述,逐条与检索到的原文比对,判断这条陈述是被原文支持、与原文矛盾还是无从判断。现在有不少开源模型专门做这类任务,部署成本不高。只有被判定为"支持"的陈述才允许出现在最终答案里,矛盾和无从判断的部分要么删除,要么改写成"资料中未提及"。
def fact_check(answer_sentences, evidence_chunks, nli_model):
results = []
for sent in answer_sentences:
verdict = None
for chunk in evidence_chunks:
# premise为检索原文,hypothesis为Agent的陈述
label = nli_model.predict(premise=chunk, hypothesis=sent)
if label == 'contradiction':
verdict = '矛盾'
break
if label == 'entailment':
verdict = '支持'
break
if verdict is None:
verdict = '无法验证'
results.append((sent, verdict))
return results
对于数字、日期、金额这类高风险信息,还可以做规则级强校验:用正则从答案中抽取所有数值,与原文中的数值精确比对,任何一个对不上就整条陈述打回。数值类幻觉的破坏力远高于文字类,一句"保修期两年"写错成"三年"可能直接引发客诉,值得多花一层校验成本。
多路验证是另一道保险。对同一个问题,用不同温度或不同模型各生成一次答案,若关键事实出现分歧,说明这条信息置信度低,应降级为人工审核或要求模型重新检索。工程上不必对每条回答都做三路生成,可以按业务风险分级:涉及价格、法律条款、医疗建议的回答走完整核查,闲聊类内容直接放行,在可靠性和成本之间取得平衡。
工程落地:监控指标与持续优化
上线之后必须用数据说话。推荐跟踪三个核心指标:忠实度(答案内容与检索资料的一致程度)、答案相关性(是否切题回答了用户问题)、检索命中率(正确资料是否出现在召回结果中)。三个指标要分开看,因为它们指向不同的优化动作——忠实度低说明模型不守规矩,相关性低可能是提示词问题,命中率低则要回头优化切分和检索策略。
可以定期从线上日志中抽样,用人工标注或LLM评估的方式打分,形成回归测试集。每次调整切分策略、更换Embedding模型、修改提示词后都跑一遍这个测试集,防止改好一处又破坏另一处。幻觉治理是一个持续迭代的过程,不存在一劳永逸的配置。
最后要注意用户体验层面的设计。当核查链路判定某条信息无法验证时,与其强行输出一个存疑答案,不如坦率告诉用户当前信息不足,并引导用户补充上下文。一个会说"我不确定"的Agent,长期信任度远高于一个永远自信满满的Agent。