2024 年我做一个企业内部的知识库问答助手——员工用自然语言问公司的制度、产品、流程,助手调用大模型把答案讲出来。这件事我没多想,就有了方案:把员工的问题直接发给大模型,让它回答。第一版我做得很顺手——一个接口,收到问题,拼一个提示词发给模型,把模型的回答返回。本地拿几个常见问题一测,问"报销流程是什么""年假有几天",模型答得有模有样、条理清晰,我心里很笃定:大模型这么聪明,问它公司的事,它答得头头是道,这问答助手稳了。可等真正交给员工用,一串问题冒了出来。第一种最先把我打懵:有人问一个具体的制度条款,模型给出了一个根本不存在的规定,还煞有介事地编了个文件编号,语气无比肯定,员工信了、照做了,出了事。第二种最难缠:我后来接了知识库检索,把公司真实文档喂给模型,结果它还是错——检索到的资料明明白白写着 A,模型回答却说成了 B。第三种最头疼:模型把两份不同文档的内容混在一起,把 A 产品的价格安到了 B 产品上,张冠李戴。第四种最莫名其妙:有人问了一个公司文档里压根没有的事,模型不说"我不知道",而是顺着话头编了一整段,编得有鼻子有眼。我盯着这一连串问题想了很久,才彻底想明白:第一版错在一个根本的认知上。我以为大模型答错,是因为它"不知道"——它脑子里没有公司这部分知识;那我只要把正确的资料喂给它,它知道了,就不会瞎编了。可这个认知是错的。幻觉的根子,根本不是"缺知识"。大模型在训练中被塑造成了一个"无论如何都要给出一个流畅、自信的回答"的程序——它没有"我不知道"这个默认选项,一旦遇到不确定的地方,它会用最像样、最通顺的话,把那个空白填上。要把问答助手做扎实,根上要明白:你要做的不是"让模型知道得更多",而是一整套围绕"约束模型、不让它乱填空白"的工程——把回答锚定在真实资料上、允许并鼓励它拒答、让每句话都能溯源。本文从头梳理:为什么"把资料喂给它"不等于消除了幻觉,幻觉到底从哪来,怎么用检索把回答锚定在事实上,怎么让模型敢说"不知道",怎么给回答配上出处,以及一些把它做扎实要避开的工程坑。
问题背景
先把"幻觉"这个词说清楚。大模型的幻觉(hallucination),指的是模型生成了一段看起来通顺、自信,但实际上不符合事实、或没有依据的内容。它不是模型"出 bug 了",恰恰相反,它是模型在正常工作——模型的本职就是生成流畅的文本,而"流畅"和"正确"是两回事。
错误认知是:模型答错是因为它知识不够,补上知识就好了。真相是:幻觉的根源是模型的生成机制本身——它被训练成一个"总要往下接出一段合理文字"的程序,在它不确定的地方,它不会停下,而是用最可能的词去填。把这一点摊开,第一版的几类问题就都能解释了:
- 编造不存在的条款:模型没有这个知识,但它不会说"没有",它会按"一个制度条款大概长什么样"去生成一个像模像样的假条款。
- 喂了资料还是答错:资料只是放进了上下文,模型并没有义务"忠实于"它,模型仍可能按自己训练时的倾向回答,忽略眼前的资料。
- 两份资料张冠李戴:模型把上下文里的多段信息揉在一起生成,它分不清哪个属性属于哪个对象,容易错配。
- 没有的也硬答:模型缺少"这个问题我答不了"的判断和表达,默认行为是"接着往下生成"。
所以让问答助手做对,核心不是"喂更多知识",而是建一套约束机制:把回答锚定在检索到的真实资料上,允许模型拒答,让回答可溯源。下面六节,就从第一版"喂了资料就放心"的想当然讲起。
一、为什么"把资料喂给它"不等于消除了幻觉
第一版踩了第一个坑后,我做的第一个改进,是接知识库检索:用户问问题,先去公司文档里检索相关段落,把段落和问题一起发给模型。这个方向是对的,但我当时对它寄予了一个错误的期望——我以为"把正确资料放进上下文",模型就会"照着资料答",幻觉就该消失了。结果是,幻觉减少了,但远没有消失。模型照样会无视眼前白纸黑字的资料,给出一个和资料矛盾的答案。
# 反面教材:以为"把资料塞进提示词"模型就会老实照着答
def answer_v2(question, docs):
# docs 是检索到的真实文档段落
context = "\n\n".join(docs)
prompt = f"""参考资料:
{context}
问题:{question}
请回答上面的问题。"""
return call_model(prompt)
# 我以为:资料就在眼前,模型总该照着答了吧?
# 实际上:模型并没有"必须忠实于这段资料"的义务。
# 它依然在做它唯一会做的事——生成最流畅的下一段文字,
# 而这段文字,可能来自资料,也可能来自它训练时的记忆,
# 甚至可能是把两者搅在一起的产物。
问题出在一个认知错位上。我把"参考资料"理解成了"模型必须遵守的标准答案",但对模型来说,放进提示词里的资料,只是它生成回答时"可以参考的上下文"之一。模型不会区分"这段文字是用户给的权威资料"和"这段文字是闲聊"——在它眼里,提示词就是一整段文本,它的任务是在这段文本后面,接出一段通顺的回答。资料能影响它的输出,但不能强制它的输出。
这一节要建立的认知是:把资料喂给模型,改变的是"模型生成时能看到什么",而不是"模型必须服从什么"——资料是一个强烈的提示,但不是一道不可违抗的命令。第一版到第二版,我的进步是意识到了"要给模型真实资料";但我没意识到的是,"给了资料"和"模型忠实于资料"之间,还隔着一大段工程。模型本质上是个概率生成器,你给它的资料,是在调整它生成时的概率分布,让它更倾向于说出和资料一致的话——但只要资料和它训练记忆冲突、或者资料本身有歧义,它仍可能滑向幻觉。所以"消除幻觉"这件事,不存在"喂对资料"这一个银弹,它需要的是后面几节讲的一整套配合:锚定、拒答、溯源。把"幻觉"理解成一个需要持续工程对抗的倾向,而不是一个"补上知识就修好"的 bug,是做对这件事的起点。
二、幻觉从哪来:模型被训练成"必须给出流畅回答"
要对付幻觉,得先理解它的来源。大模型的核心工作方式,是"预测下一个 token":给定前面的一段文本,它计算出下一个最可能的字/词,接上;再以新的文本为基础,预测再下一个。一个回答,就是这样一个 token 一个 token 接出来的。这里的关键是:在每一步,模型总能算出一个"最可能的下一个 token"——哪怕它对内容毫无把握,这个概率分布也总是存在的,它永远有词可接。
# 模型生成的本质:每一步都在"接最可能的下一个词"
# 下面是对这个过程的极度简化示意
def generate(prompt):
text = prompt
while not is_end(text):
# 模型对"下一个 token"给出一个概率分布
# 注意:无论模型对内容是否有把握,这个分布总是存在的
next_token = pick_most_likely(text)
text += next_token
return text
# 关键点:这个循环里,没有任何一步会问
# "我对接下来要说的话有把握吗?"
# 模型不会因为"不确定"而停下,它只会继续接词。
# 一个编造的条款,和一个真实的条款,
# 在"语言上是否流畅"这个维度上,可以一模一样。
更深一层的原因在训练。大模型在训练和对齐阶段,被大量"问题—优质回答"的样本塑造,而这些优质回答,绝大多数是直接、自信、流畅地把问题答了。模型从中学到的隐含规律是:"面对一个问题,给出一个完整、肯定的回答,是好的。"相对地,"我不知道""资料里没有提到"这类回答,在训练数据里是少数,模型对它们的倾向天然就弱。于是模型形成了一种根深蒂固的行为:无论如何,先把回答给得漂漂亮亮的。
这一节的认知是:幻觉不是模型的"故障",而是模型设计目标的"副产品"——它被造出来就是为了生成流畅、自信的文本,而流畅自信的假话和流畅自信的真话,在它的生成机制里没有任何区别。这个认知很重要,因为它决定了你对付幻觉的心态。如果你以为幻觉是 bug,你会期待有一个"修复"它的办法,然后一劳永逸。但幻觉不是 bug,它和模型的能力是同一枚硬币的两面——正是那个"总能流畅地接下去"的能力,让模型既能写出好答案,也能编出假答案。你没法"关掉"幻觉,你只能在模型外面,套上一层又一层的约束,把它流畅生成的能力,尽量框定在真实的事实范围内。理解了幻觉的"先天性",你就不会再幻想银弹,而会踏实地去搭后面那套工程。
三、用检索把回答"锚定"在事实上
对付幻觉,第一根支柱,是检索增强(也就是常说的 RAG)。它的思路前面提过:回答前,先从可信的知识库里检索出相关资料,放进上下文。第一版的问题不在"用了检索",而在"用得太粗"。要让检索真正起到"锚定"作用,有两个关键:一是检索本身要准,二是提示词要把"以资料为准"这件事说死。先看检索这一环——检索回来的资料如果是错的、不相关的,后面全白搭。
# 第一步:检索要准,而且要给资料带上来源标识
def retrieve(question, knowledge_base, top_k=5):
# 把问题向量化,在知识库里找语义最相近的段落
q_vec = embed(question)
hits = knowledge_base.search(q_vec, top_k=top_k)
docs = []
for i, hit in enumerate(hits):
# 关键:每段资料都带上一个编号和它的出处
# 编号后面要用来做引用溯源
docs.append({
"id": i + 1,
"source": hit.source_file,
"score": hit.score, # 相似度分数,后面要用来过滤
"text": hit.text,
})
return docs
检索回来之后,提示词的写法,要从第一版那种含糊的"请参考资料",改成一套严格的指令:明确告诉模型,回答只能基于给定的资料,资料里没有的不许自己补充。这一步,是把"资料"从"可有可无的参考"升级成"唯一的事实来源"。
# 第二步:用严格的提示词,把回答死死锚定在资料上
GROUNDED_PROMPT = """你是公司知识库助手。请严格遵守以下规则回答问题:
1. 你的回答必须完全基于下面提供的【参考资料】。
2. 不允许使用参考资料之外的任何知识,即使你"知道"答案。
3. 如果参考资料里没有足以回答问题的信息,
你必须明确回答"根据现有资料,我无法回答这个问题",
不允许猜测、不允许编造、不允许用常识补充。
4. 回答中的每一个关键信息,都要标注它来自第几条资料,
格式如:[资料1]。
【参考资料】
{context}
【问题】
{question}
【回答】"""
def build_prompt(question, docs):
context = "\n\n".join(
f"[资料{d['id']}] (来源: {d['source']})\n{d['text']}"
for d in docs
)
return GROUNDED_PROMPT.format(context=context, question=question)
这套提示词里,有几个词是反复强调的:"严格""完全基于""即使你知道也不允许"。这种近乎啰嗦的强调是必要的——前面说过,模型有"自信作答"的天然倾向,你必须用足够强硬、足够明确的指令,去对冲这个倾向。含糊的"请参考资料",对冲不了;斩钉截铁的"只能用资料、不许用别的",才有机会。
这一节的认知是:检索增强的作用,不是"让模型多知道一点",而是"给模型的回答提供一个可以被核对的事实锚点"——它把回答从"模型脑子里那团说不清的记忆",拉到了"一段你能指着看的具体文字"上。这个转变的意义极大。模型记忆里的知识,是弥散的、无法审查的,你没法知道它"为什么这么说";而检索到的资料,是具体的、有出处的,你可以一字一句去核对。检索增强真正给你的,是"可核对性"。但要记住,锚点能不能起作用,取决于两件事:锚下去的资料本身要准(检索质量),以及提示词要强硬到能压住模型自由发挥的倾向(指令强度)。这两者缺一不可——检索再准,提示词软,模型照样跑偏;提示词再硬,检索回来的是垃圾,模型只能基于垃圾作答。把检索和提示词当成一对必须同时做硬的搭档,锚定才真的锚得住。
四、让模型"敢说不知道":拒答设计
上一节的提示词里,已经埋了一条关键规则:资料不足时,要回答"无法回答"。这一节专门讲它,因为它是对付幻觉里最反直觉、也最重要的一环。第一版乃至很多人的直觉是:问答助手嘛,就是要能回答问题,"答不上来"是一种失败。但恰恰相反——一个会在该闭嘴时闭嘴的助手,才是可信的助手。第一版"没有的也硬答",根子就在它的设计里压根没有"拒答"这个选项。
要让模型敢拒答,光在提示词里写一句"不知道就说不知道"还不够,因为模型自信作答的倾向太强了。你得从两个层面同时使力:一是在提示词里,把"什么情况下该拒答"描述得非常具体;二是在代码里,做一道"拒答前置判断"——在把问题交给模型之前,先用检索分数判断"这次检索到的资料,够不够格回答"。
# 拒答前置判断:资料质量不达标,根本不交给模型生成
def should_refuse_before_llm(docs, score_threshold=0.75):
if not docs:
# 一条都没检索到,直接拒答
return True
# 看最相关的那条资料,相似度够不够高
best_score = max(d["score"] for d in docs)
if best_score < score_threshold:
# 最相关的资料都不够相关,说明知识库里大概率没有答案
# 这种情况别让模型硬答,直接拒答
return True
return False
def answer(question, knowledge_base):
docs = retrieve(question, knowledge_base)
if should_refuse_before_llm(docs):
# 不交给模型,直接返回标准拒答话术
return {
"answer": "根据现有资料,我无法回答这个问题。",
"refused": True,
}
# 资料质量过关,才进入正常的生成流程
prompt = build_prompt(question, docs)
return {"answer": call_model(prompt), "refused": False}
这道前置判断很重要:它意味着,有相当一部分"知识库里根本没有答案"的问题,压根不会进入模型生成环节。模型连编的机会都没有。这比"让模型生成完再判断它有没有编"要可靠得多——最好的防幻觉,是不给它幻觉的机会。当然,前置判断拦不住的(资料检索到了、但不足以回答)那部分,还要靠提示词里的拒答规则兜底。
这一节的认知是:对一个问答系统来说,"拒答"不是能力的缺失,而是可信度的来源——一个偶尔会说"我不知道"的助手,它说出口的每一个肯定回答,才更值得相信。这件事的逻辑是这样的:如果一个助手对任何问题都给出肯定回答,那你就无法从它的"肯定"里获得任何信息,因为它对它瞎编的答案,也一样肯定。反过来,如果一个助手会在资料不足时明确拒答,那它的"拒答"就成了一个有意义的信号,而它的"作答",也因此变得可信——因为你知道,它作答,是因为它确实有据可依。所以做问答助手,你要主动地、刻意地去设计拒答:提示词里写明拒答条件,代码里加拒答前置判断,产品上把拒答话术做得体面。把"敢拒答"当成一个要专门去实现的功能,而不是一个要消灭的缺陷,你的助手才谈得上可信。
把一个回答从生成到决定能不能给用户的完整把关流程画出来,就是下面这张图:
[mermaid]
flowchart TD
A[用户提问] --> B[检索知识库]
B --> C{资料质量达标吗}
C -->|否| D[直接拒答 不交给模型]
C -->|是| E[带严格指令交给模型生成]
E --> F{回答里每句都有引用吗}
F -->|否| G[标记可疑 或要求模型重答]
F -->|是| H{引用与原文一致吗}
H -->|否| G
H -->|是| I[连同出处一起返回用户]
五、给回答配"出处":引用与可溯源
检索锚定了事实,拒答堵住了"无中生有",但还有一类幻觉漏在外面——第三种问题,张冠李戴:资料是真的,模型也确实在用资料,但它把资料里的信息组合错了,把 A 的属性安到了 B 头上。这种幻觉最隐蔽,因为它的每一个零件都是真的,只是装错了。对付它,要靠最后一根支柱:引用溯源。
引用溯源的核心思路是:要求模型在回答的每一句关键陈述后面,标注这句话依据的是第几条资料(前面的提示词里已经要求了 [资料1] 这种格式)。然后,在拿到模型回答后,你的程序去做一件事——把每一处引用解析出来,逐一核对:模型说这句话来自资料 3,那就去看资料 3 里到底有没有支撑这句话的内容。
# 解析回答里的引用标注,做一次溯源核对
import re
def extract_citations(answer_text):
# 把回答里所有形如 [资料3] 的引用编号取出来
return [int(n) for n in re.findall(r"\[资料(\d+)\]", answer_text)]
def check_grounding(answer_text, docs):
cited = extract_citations(answer_text)
doc_ids = {d["id"] for d in docs}
# 检查一:回答引用的资料编号,是否真实存在
# 模型有时会编造一个不存在的资料编号
for c in cited:
if c not in doc_ids:
return {"ok": False, "reason": f"引用了不存在的资料{c}"}
# 检查二:回答里是否存在"裸句子"——没有任何引用的关键陈述
# 把回答按句拆开,没带引用标注的句子是可疑的
sentences = [s for s in re.split(r"[。\n]", answer_text) if s.strip()]
unsupported = [
s for s in sentences
if len(s) > 10 and "[资料" not in s
]
if unsupported:
return {"ok": False, "reason": "存在无出处的陈述", "lines": unsupported}
return {"ok": True}
更进一步,对要求高的场景,可以再加一道"忠实度核对":把模型的某句回答,和它声称的那条资料,再发给模型(或另一个模型)做一次判断——"这句话,能不能从这条资料里推出来?"这相当于让模型给自己的回答当一次审稿人。这道核对成本不低,但对金融、医疗、法务这类输不起的场景,值得。
# 进阶:让模型核对"某句回答"是否真的被"某条资料"支撑
VERIFY_PROMPT = """请判断【陈述】是否可以由【资料】直接推出。
只回答一个词:支持 / 不支持 / 资料不足。
【资料】
{doc_text}
【陈述】
{claim}
【判断】"""
def verify_claim(claim, doc):
prompt = VERIFY_PROMPT.format(doc_text=doc["text"], claim=claim)
verdict = call_model(prompt).strip()
# 只有明确"支持",才算这句话站得住
return verdict == "支持"
这一节的认知是:引用溯源真正的价值,不是"让回答看起来更专业",而是把模型那个"黑箱式的回答"变成了一个"可以被逐句审查的结构"——它让幻觉无处藏身。没有引用的回答,是一个整体,你只能"信"或"不信",没有中间地带,更没法定位问题。有了逐句引用的回答,它就被拆成了一条条"陈述 + 出处"的单元,每一个单元都可以独立地被核对:出处是真的吗?出处支撑这句话吗?于是,审查回答这件事,从"凭感觉判断整段对不对",变成了"机械地逐条核对"。可机械核对,正是程序最擅长的。引用溯源,本质上是把"对抗幻觉"这件事,从一个需要人去"感觉"的模糊问题,转化成了一个程序能去"检查"的明确问题。能把问题转化成可检查的形式,你才能规模化地、稳定地去对抗它。
六、把幻觉缓解做扎实,要避开的工程坑
前面五节,搭出了"检索锚定 + 拒答 + 溯源"这套对抗幻觉的主体框架。但要在生产里真正用好,还有几个坑得专门讲。第一个,也是最容易被忽略的:整套系统的上限,是由检索质量决定的。你前面的拒答、溯源做得再漂亮,如果检索这一步给模型的就是不相关的资料,那模型基于错料给出的答案,既会通过拒答(因为分数可能不低)、又会带着引用(因为它确实"引用"了那段错料)——你被一个看起来合规的错误答案骗了。
# 坑一:检索质量是整个系统的天花板,要单独评估
def evaluate_retrieval(test_cases, knowledge_base):
# test_cases: [(问题, 这个问题的答案应该来自哪个文档)]
hit = 0
for question, expected_source in test_cases:
docs = retrieve(question, knowledge_base, top_k=5)
sources = {d["source"] for d in docs}
if expected_source in sources:
hit += 1
else:
# 这条没检索到正确文档,要记下来分析
print(f"检索未命中: {question}")
# 命中率上不去,先别折腾后面的生成,回去优化检索
print(f"检索命中率: {hit}/{len(test_cases)}")
第二个坑,是别让模型去做它不擅长的事——尤其是数值计算和多步推理。模型在算数字、做逻辑推演时,幻觉率特别高,因为这些任务的答案没法靠"语言流畅"蒙对。如果你的问答涉及计算(比如"我入职 3 年 8 个月,年假有几天"),正确做法是:让模型只负责从资料里提取规则,真正的计算交给确定性的代码。
# 坑二:数值计算别让模型做,让它提取规则、代码来算
def annual_leave(years):
# 这是从制度文档里提取出来的确定规则,用代码实现
# 模型只负责把"年假规则"这段文字找出来,不负责算
if years < 1:
return 0
elif years < 10:
return 5
elif years < 20:
return 10
else:
return 15
# 流程:模型从资料里读懂规则 -> 抽取出工龄这个参数
# -> 调用上面的函数算出结果 -> 模型把结果组织成回答
# 计算这一步是确定性的代码,不经过模型,也就没有幻觉
还有几个坑值得点一下。其一,幻觉不可能百分之百消除,所以高风险场景一定要留人工兜底——回答里带上"以上信息请以正式文件为准",并提供一键转人工。其二,评估幻觉,不能只用"正常问题"测,要专门构造对抗性问题:问知识库里根本没有的事、问有歧义的事、问需要计算的事,看系统会不会乖乖拒答。其三,模型和提示词都会更新,每次更新后,那套对抗性测试集都要重新跑一遍,防止"改好的又坏了"。下面把这套对抗幻觉的手段集中对照一下:
对抗幻觉的几种手段对照
手段 作用 管住的幻觉类型
--------------------------------------------------------------
检索锚定 给回答提供真实事实来源 凭空编造
严格提示词 压住模型自由发挥的倾向 无视资料乱答
拒答前置判断 资料不够干脆不让它生成 没有的也硬答
引用溯源 每句话可逐条核对出处 张冠李戴 错误组合
忠实度核对 二次确认陈述被资料支撑 隐蔽的细节错误
数值计算外置 确定性任务交给代码 算错 推错
原则:幻觉无法根除,只能层层设防;
每一层拦住一类,叠起来才够用。
这一节这几个坑,串起来是同一个意思:幻觉缓解不是"加一套 RAG"就完事的一次性工作,它是一个需要持续评估、持续对抗的系统工程。检索质量是天花板,要单独量化;数值计算是模型的软肋,要剥离给代码;百分百消除做不到,要留人工兜底;系统会随模型和提示词更新而退化,要靠对抗性测试集守住。幻觉这个对手有一个特点:它平时藏得很好,你用常规问题怎么测都正常,它专挑那些边角的、刁钻的、你没想到的问题暴露。所以你不能被动地等它出现,你得主动地、用对抗性的问题去逼它现形。把幻觉缓解当成一个要长期投入、要主动攻击自己系统的工程,而不是一个配置好就一劳永逸的功能——这样你的问答助手,才能在真实用户五花八门的提问下,守住"可信"这条底线。
关键概念速查
| 概念 | 说明 |
|---|---|
| 幻觉 | 模型生成的看似通顺自信、实则不符事实或无依据的内容 |
| 下一个 token 预测 | 模型逐字生成文本的机制,每一步都能接出词,从不停顿 |
| 检索增强 RAG | 回答前先检索可信资料放入上下文,给回答提供事实锚点 |
| 锚定 grounding | 用真实资料约束模型,把回答绑定到可核对的具体文字上 |
| 严格提示词 | 明确指令模型只能用给定资料、不许用其他知识作答 |
| 拒答 | 资料不足时明确回答无法回答,而非编造,是可信度的来源 |
| 拒答前置判断 | 按检索分数提前判定资料不够,根本不进入模型生成 |
| 引用溯源 | 要求回答逐句标注资料出处,使回答可被逐条核对 |
| 忠实度核对 | 二次判断某句回答是否真能由其声称的资料推出 |
| 对抗性测试 | 专门用没答案的歧义的需计算的问题去考验系统会否拒答 |
避坑清单
- 不要以为补上知识就消除了幻觉:幻觉的根源是生成机制,不是知识缺失。
- 不要以为把资料塞进提示词模型就会照着答:资料能影响输出,不能强制输出。
- 不要用含糊的"请参考资料":要用强硬明确的指令压住模型自由发挥的倾向。
- 不要把检索质量当背景板:它是整个系统的天花板,要单独构造用例评估。
- 不要把"拒答"当成失败:会拒答的助手,它的肯定回答才值得相信。
- 不要只靠提示词拒答:加一道按检索分数的前置判断,不给模型编的机会。
- 不要接受没有出处的回答:逐句引用让回答可核对,无引用的陈述视为可疑。
- 不要让模型做数值计算和多步推理:提取规则交给模型,计算交给确定性代码。
- 不要指望幻觉百分百消除:高风险场景必须留人工兜底和正式文件提示。
- 不要只用正常问题测试:要用对抗性问题逼系统现形,模型更新后重跑测试集。
总结
回头看第一版那个"喂上知识就不会编了"的问答助手,它的错误很典型。它不在某一行代码,而在一个对幻觉的根本误解:以为模型答错是因为它"不知道",补上知识它就老实了。真相是,幻觉的根源不是缺知识,而是模型的生成机制本身——它被训练成一个总要给出流畅、自信回答的程序,在它不确定的地方,它不会停,而是用最像样的话去填空白。流畅的假话和流畅的真话,在它眼里没有区别。所以幻觉不是一个能"修好"的 bug,它是模型能力的影子,你只能在模型外面层层设防。
而把幻觉缓解做对,工程量并不小。它不是接一套 RAG 那么简单,而是要理解幻觉的先天来源,要让检索足够准、提示词足够硬地把回答锚定在事实上,要刻意地设计拒答、让模型敢说不知道,要给回答配上逐句出处让它可溯源,还要把检索质量单独评估、把数值计算剥离给代码、用对抗性问题持续考验系统。一套真正可信的问答助手,是这些环节一个不少地拼起来的。
这件事其实很像带一个口才极好、但爱面子的新人。这个新人(大模型)反应快、表达流畅,你问什么他都能对答如流——问题恰恰出在这"对答如流"上:他太想表现得无所不知,哪怕一个他根本不清楚的事,他也会凭着好口才编一段听起来很专业的话,而且说得比谁都肯定。你要让他变得可靠,靠的不是"多给他培训"——他不是不够聪明,他是太爱面子。你要做的是立规矩:回答必须拿着文件原文说话(检索锚定),不许凭印象发挥(严格提示词);文件里查不到的,必须老老实实说"这个我得查一下"而不许硬编(拒答);说的每句话都要能指出是文件第几页(引用溯源);算账这种事别用嘴算,拿计算器(数值外置)。当这个新人学会了在不知道的时候说"不知道",他说出口的那些"知道",才终于变得可信。
这类问题还有一个共同的麻烦:它在开发和测试时很难暴露。你自己测,问的都是知识库里明明白白有答案的标准问题,模型检索到资料、流畅作答,答得又快又准——你会觉得这套问答助手天衣无缝。真正会把幻觉撑开的,是上线后真实用户那些五花八门的提问:问知识库里压根没有的事、问表述含糊有歧义的事、问需要绕几个弯计算的事。模型面对这些它"接不下去"的问题时,不会停,而是编——而用户往往分不清它是在答还是在编,因为两者一样流畅、一样自信。所以如果你正在做一个基于大模型的问答系统,别等用户拿着一个编造的答案来投诉,才回头怀疑模型。在写下第一个提示词的时候就想清楚:模型天生会编,我的检索锚得住吗、我的系统敢拒答吗、我的回答能溯源吗——把"让模型知道"和"不让模型乱编"当成两件必须分别去做的事,这是这篇文章最想留给你的一句话。
—— 别看了 · 2026