2026 年 4 月,我们一个基于 LangChain + Qdrant + GPT-4o 的内部知识库问答助手(kb-bot),被审计部门提出严肃问题:"过去三个月里有 23 次,LLM 凭空捏造了内部文档里根本不存在的 SOP 步骤,且回答得无比自信"。这些被错引的 SOP 包括"灾备演练流程"、"安全工单升级路径"等敏感内容,如果员工照着 hallucinated 出来的步骤执行,会引发严重的合规事故。审计要求我们在 30 天内把"无依据回答"的比例降到 0.5% 以下。
这次复盘是我们 6 周治理 LLM 幻觉的完整路径——从最初的 11% 幻觉率,通过 RAG 强约束 + Citation Verification + Confidence Threshold + Fallback Refusal 四层叠加,最终降到 0.3%。这套方案没有用到任何"模型微调"或"奇技淫巧",全是工程层面的"约束 + 兜底"组合,但效果直接可量化。如果你的团队也在做企业 LLM 应用,这篇能给你一份"幻觉治理清单"。
故障背景:这个助手的规模
| 维度 | 规模/参数 |
|---|---|
| 用户量 | 4200 名员工(全公司) |
| 日均查询量 | 8500 次,峰值 350 qps |
| 知识库规模 | 1.2 万篇内部文档,80GB,4.3 亿 token |
| 向量数据库 | Qdrant,768 维,1.2 亿 chunk |
| Embedding | BGE-large-zh + OpenAI text-embedding-3-large 双路 |
| LLM | GPT-4o(95%) + Claude 3.5 Sonnet(降级) |
| 问答模式 | RAG(检索增强生成),每次注入 5-8 个 chunk |
| 治理前幻觉率 | 11.2%(人工抽检 500 条得出) |
| 治理目标 | < 0.5% |
"幻觉率"我们定义为"回答中出现的关键事实在被引用的 chunk 里找不到"。它不是"回答错误率"——LLM 完全可以基于正确的 chunk 给出错误的解读,那是另一个问题(reasoning error)。我们这次治理只针对"凭空编造"。
事故时间线
| 时间 | 事件 |
|---|---|
| W1 (D1-D7) | 审计开会,要求 30 天内整改;抽检 500 条问答,基线幻觉率 11.2% |
| W2 (D8-D14) | 原型 1:Prompt 加强 "must cite source",幻觉率从 11.2% 降到 6.8%,但 LLM 经常引错来源 |
| W3 (D15-D21) | 原型 2:加 Citation Verifier 校验引用与原文一致,幻觉率到 2.4% |
| W4 (D22-D28) | 原型 3:加 Confidence Threshold + Fallback Refusal,幻觉率到 0.7% |
| W5 (D29-D35) | 原型 4:Self-Consistency Check(同问题问 3 次比对),幻觉率到 0.3% |
| W6 (D36-D42) | 全量验证 + 监控建设,审计接受 |
第一轮治理:Prompt 加强的局限
最容易想到的方案是"加强 Prompt 让 LLM 不要编造"。我们试过这种 Prompt:
SYSTEM_PROMPT = """你是公司知识库助手。回答必须满足:
1. 只能基于以下 [文档片段] 中的信息回答。
2. 如果信息不足以回答,必须明确说"我在知识库中找不到相关信息"。
3. 每个事实陈述后必须标注引用源,格式:[来源:文档名#段落]。
4. 严禁编造、推测、引申。
5. 不确定时,优先选择"不知道"而不是猜测。
"""
# 用户问题前注入检索到的 5 个 chunk
USER_TEMPLATE = """
[文档片段]
{retrieved_chunks}
[用户问题]
{user_question}
"""
效果是幻觉率从 11.2% 降到 6.8%。但仔细分析剩下的 6.8% 幻觉,我们发现两个问题:
- LLM 引了"看起来对"的来源:回答里说"根据《灾备 SOP》第 3 节",但实际 chunk 里第 3 节根本没这内容,LLM 自己编了引用;
- LLM 在 chunk 之间"插值":chunk A 提到流程开始,chunk B 提到流程结束,LLM 自己补出了中间的步骤;
- 多轮对话遗忘约束:用户问到第 3 轮时,LLM 开始引上一轮回答里编造的内容当事实。
这让我们意识到:仅靠 Prompt 约束 LLM 是不够的,LLM 的"幻觉天性"在数据稀疏区域会自动启动,Prompt 是软约束,需要硬验证层。
第二轮治理:Citation Verification(我们最重要的一层)
这是整个治理里最有效的一层——把 LLM 输出里的每个引用都拿回去和原 chunk 字面比对:
实现:
import re
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
embedder = SentenceTransformer("BAAI/bge-large-zh-v1.5")
def verify_citations(answer: str, chunks: dict[str, str]) -> dict:
"""
answer: LLM 生成的回答,含 [来源:doc#para] 标注
chunks: {source_id: chunk_text} 检索到的原文片段
return: 验证结果
"""
# 1. 提取所有 [来源:xxx] 引用
citations = re.findall(r'\[来源:([^\]]+)\]', answer)
sentences = answer.split('。')
results = []
for sent in sentences:
if not sent.strip(): continue
# 找该句关联的 citation
cited_sources = re.findall(r'\[来源:([^\]]+)\]', sent)
if not cited_sources:
results.append({'sent': sent, 'status': 'no_citation'})
continue
# 2. 对每个引用,计算句子和源 chunk 的语义相似度
sent_clean = re.sub(r'\[来源:[^\]]+\]', '', sent).strip()
sent_emb = embedder.encode(sent_clean)
max_sim = 0
for source in cited_sources:
if source not in chunks:
results.append({'sent': sent, 'status': 'invalid_source', 'source': source})
continue
chunk_emb = embedder.encode(chunks[source])
sim = cosine_similarity([sent_emb], [chunk_emb])[0][0]
max_sim = max(max_sim, sim)
# 3. 判定阈值: 相似度 < 0.6 视为幻觉
status = 'valid' if max_sim >= 0.6 else 'hallucinated'
results.append({'sent': sent, 'status': status, 'sim': float(max_sim)})
valid_ratio = sum(1 for r in results if r['status'] == 'valid') / len(results)
return {'sentences': results, 'valid_ratio': valid_ratio}
每次 LLM 返回回答,我们都跑这个 verifier。如果 valid_ratio < 70%,这个回答就不展示给用户,而是触发 fallback。效果:幻觉率从 6.8% 降到 2.4%。
这一层的关键洞察是:幻觉的本质是"输出和原文不匹配",这种不匹配可以用 embedding 相似度量化。语义相似度 < 0.6 几乎 100% 是幻觉(LLM 编造的内容和原文风格完全不同)。这是个简单但极有效的硬验证。
第三轮治理:Confidence Threshold + Fallback Refusal
剩下的 2.4% 幻觉主要来自"信息严重不足时 LLM 强行回答"的场景。比如用户问"X 系统的 2025 年 Q3 KPI 是多少",但知识库里只有 Q1/Q2 数据,LLM 会插值出一个 Q3 数字。修法是显式判断"信息是否足够",不够就拒答:
def check_information_sufficiency(question: str, chunks: list[str]) -> dict:
"""
用一个小 LLM 调用专门判断:这些 chunks 是否包含回答 question 所需的信息?
"""
prompt = f"""判断以下文档片段是否包含回答用户问题所需的完整信息。
[用户问题]
{question}
[文档片段]
{chr(10).join(f"---片段{i+1}---{chr(10)}{c}" for i, c in enumerate(chunks))}
回答必须是以下三选一:
- SUFFICIENT: 文档明确包含问题答案
- PARTIAL: 文档只部分相关,不足以回答完整问题
- INSUFFICIENT: 文档与问题基本无关
只输出标签,不要解释。"""
# 用便宜模型做判断
response = openai.chat.completions.create(
model="gpt-4o-mini", # 比 gpt-4o 便宜 60 倍
messages=[{"role": "user", "content": prompt}],
max_tokens=10,
temperature=0
)
label = response.choices[0].message.content.strip()
return {'label': label, 'should_answer': label == 'SUFFICIENT'}
如果判断结果是 PARTIAL 或 INSUFFICIENT,直接返回:
FALLBACK_RESPONSES = {
'PARTIAL': "我在知识库中找到部分相关信息,但不足以完整回答你的问题。建议联系 {topic} 业务方:\n\n{relevant_chunks}",
'INSUFFICIENT': "我在知识库中没有找到相关信息。你可以:\n1. 重新描述问题\n2. 联系业务方 #{contact}\n3. 提交知识库补充申请"
}
这一层加上后,幻觉率从 2.4% 降到 0.7%。但代价是"拒答率"从 0.5% 涨到 8.3%——用户会问到很多知识库覆盖不到的问题,助手直接拒答。这个体验代价我们和审计、业务方对齐过:"诚实的不知道" 远好于 "自信的编造"。
第四轮治理:Self-Consistency Check
最后剩下的 0.7% 幻觉特别难处理——LLM 在 chunk 之间合理推理但推错了。比如 chunk 说"流程 A 适用 B 类工单",LLM 推出"所以 B 类工单都用流程 A",这个推论看起来对,实际上 B 类工单可能还有其他流程。
我们用 Self-Consistency 治理这类问题:同一个问题用不同温度问 3 次,如果答案不一致,降级处理:
import asyncio
async def self_consistency_check(question: str, chunks: list[str]) -> dict:
"""
并发 3 次询问,温度分别 0, 0.3, 0.7,如果答案不一致就标记为低置信
"""
async def one_query(temperature):
return await openai_async.chat.completions.create(
model="gpt-4o",
messages=build_messages(question, chunks),
temperature=temperature
)
responses = await asyncio.gather(
one_query(0.0),
one_query(0.3),
one_query(0.7)
)
answers = [r.choices[0].message.content for r in responses]
# 用 embedding 算两两相似度
embs = embedder.encode(answers)
sims = [
cosine_similarity([embs[0]], [embs[1]])[0][0],
cosine_similarity([embs[0]], [embs[2]])[0][0],
cosine_similarity([embs[1]], [embs[2]])[0][0]
]
avg_sim = sum(sims) / 3
# 阈值: 三次回答平均相似度 > 0.85 视为高置信
return {
'final_answer': answers[0], # 用 temperature=0 那次作为正式输出
'consistency_score': float(avg_sim),
'is_confident': avg_sim > 0.85
}
如果一致性分数低于 0.85,说明 LLM 在这个问题上不稳定,我们就降级——要么换更强的模型(Claude 3.5 Opus)重试一次,要么直接拒答。效果:幻觉率从 0.7% 压到 0.3%,审计通过。
四层防御对比基准
| 层级 | 方法 | 幻觉率 | 额外成本 | 拒答率 |
|---|---|---|---|---|
| 基线 | 裸 RAG | 11.2% | — | 0.5% |
| Layer 1 | Prompt 加强 | 6.8% | 0% | 0.5% |
| Layer 2 | + Citation Verifier | 2.4% | +15% latency | 1.2% |
| Layer 3 | + Sufficiency Check | 0.7% | +20% cost | 8.3% |
| Layer 4 | + Self-Consistency | 0.3% | +200% cost | 9.1% |
Self-Consistency 是最贵的一层——3 倍 LLM 调用。我们只在高敏感场景(灾备 / 安全 / 合规相关问题)开启它,日常问答只开前 3 层。这种"分级防御"让总成本只比基线高 35%,而不是 200%。
决策树:LLM 幻觉治理该用哪些层
我们立的 12 条 LLM 幻觉治理纪律
- 任何用户可见的 LLM 输出必须有 source citation:无来源的回答一律不展示;
- Citation 必须用程序校验,不依赖 LLM 自报:LLM 报的引用 50% 是编造的;
- 幻觉率必须周期性人工抽检:不能只看自动指标,人工抽 500 条最准;
- "拒答"比"猜答"安全:产品 UX 必须设计成"我不知道"的优雅展示;
- 多轮对话每轮重新检索:别让 LLM 引用历史回答(可能已经污染);
- Embedding 模型必须和 LLM 用同一家:风格一致才能用相似度做幻觉判断;
- 不要相信 LLM 的 "confidence" 自评:LLM 永远说自己很确定,要用 self-consistency 客观判断;
- 温度永远 ≤ 0.3:生产 RAG 系统温度高于此就是给自己挖坑;
- 检索 chunk 数量不是越多越好:超过 8 个 chunk 后,LLM 容易"东拉西扯";
- 所有 fallback 路径必须有埋点:知道用户在哪类问题上拒答最多,才知道知识库要补什么;
- 高敏场景必须用更贵的模型:合规、医疗、金融类问题,GPT-4o → Claude Opus 4.1 是值得的;
- 不要把 LLM 当唯一答案源:重要场景永远有人工兜底通道(联系业务方)。
引申一:为什么"微调"不能治幻觉
事故初期我们也评估过"用公司知识库微调 GPT-4o 或开源模型"。结论是:微调不能根治幻觉。原因:
- 微调让模型"更熟悉"知识库,但不阻止它在边界外推断;
- 知识库每天更新,微调模型会过时:每周重微调成本 $4000+,运维负担大;
- 微调后模型在通用能力上会退化(catastrophic forgetting);
- 微调把"知识"和"推理"耦合:RAG 把两者解耦,更新知识不影响模型。
这条经验泛化:知识应该外置(RAG / 检索),能力应该内置(模型本身)。试图"把知识塞进模型"是反模式,正确的姿势是"让模型知道怎么找到知识"。
引申二:为什么 chunk size 不是越大越好
RAG 调优时大家都会问"chunk size 多大合适"。我们测过 256/512/1024/2048 几个档:
| chunk size | 召回率 | 幻觉率 | token 成本 |
|---|---|---|---|
| 256 | 72% | 15% | 低 |
| 512 | 84% | 11% | 中 |
| 1024 | 88% | 9% | 高 |
| 2048 | 89% | 14% | 很高 |
有意思的是 2048 反而比 1024 幻觉率高——chunk 太大时 LLM 容易"东拉西扯",从无关段落里抓信息编造答案。最优 chunk size 是 512-1024,这是 RAG 系统的"经验值",具体取决于文档类型。代码文档可以小,业务流程文档要大些。
引申三:Hybrid Search 对幻觉的影响
我们的检索一开始只用向量相似度,后来加了 BM25 关键词召回做 hybrid。效果意外地好——不仅召回率提升,幻觉率也降了:
def hybrid_retrieve(question: str, top_k: int = 8) -> list[Chunk]:
# 1. 向量召回 top 20
vec_results = qdrant.search(
collection="kb",
query_vector=embedder.encode(question),
limit=20
)
# 2. BM25 关键词召回 top 20
bm25_results = bm25_index.search(question, top_n=20)
# 3. RRF (Reciprocal Rank Fusion) 融合
scores = {}
for rank, hit in enumerate(vec_results):
scores[hit.id] = scores.get(hit.id, 0) + 1 / (60 + rank)
for rank, hit in enumerate(bm25_results):
scores[hit.id] = scores.get(hit.id, 0) + 1 / (60 + rank)
# 4. 取融合分数最高的 top_k
return sorted(scores.items(), key=lambda x: -x[1])[:top_k]
幻觉率降的原因:BM25 召回的 chunk 通常含问题里的关键词,LLM 更难"绕过"原文编造。纯向量检索可能召回"语义相似但关键词不同"的 chunk,LLM 抓不到锚点容易自由发挥。Hybrid 是 RAG 系统的标配,纯向量已经过时了。
引申四:RAG 评测的 Golden Set 怎么建
所有幻觉治理都需要一个"评测集"来对比。我们的做法:
- 初始 200 条由业务专家手工编写:每条包含 question + ground_truth + expected_chunks;
- 每周新增 50 条来自真实用户提问:从生产 log 抽样,人工标注;
- 分难度梯度:30% 简单(单 chunk 答出)、40% 中等(2-3 chunk 综合)、30% 困难(需推理或拒答);
- 每个版本都跑全量评测:幻觉率、召回率、拒答率、平均延迟四个指标;
- 评测必须有人工抽样验证:自动评测 + 10% 抽样人工复核。
没有评测集就没法持续改进,任何 RAG 优化项目第一周都应该花在建评测集上。评测集质量决定优化天花板——你的指标用错了 metric,优化得再激进也是在错方向上奔跑。
引申五:为什么不能完全依赖"LLM as Judge"
事故初期我们想用 GPT-4o 自己当 judge——给它原 chunk + LLM 回答,问"这个回答是否有幻觉?"。看起来很方便,但实测有几个问题:
- LLM-as-Judge 倾向于宽松:对自己生成的内容判得不严,容易漏掉细微幻觉;
- judge 模型自己也会幻觉:它说"这个回答有问题",但具体哪里错的指认可能编造;
- 偏见叠加:用同一家 LLM 当判官,会复制相同的盲区。
所以我们的最终方案:用 embedding 相似度做硬验证(客观),用 LLM-as-Judge 做趋势监控(辅助),人工抽检做最终把关(权威)。三层叠加才是稳定的。
引申六:成本 vs 质量的边际曲线
事故让我们对 LLM 应用的成本 - 质量曲线有了量化认识:
| 幻觉率 | 每次查询成本 | 需要的工程量 |
|---|---|---|
| 15% | $0.008 | 裸 RAG,1 周 |
| 5% | $0.012 | + Prompt + Citation,3 周 |
| 1% | $0.018 | + Sufficiency check,5 周 |
| 0.3% | $0.035 | + Self-consistency,8 周 |
| 0.1% | $0.08+ | + 双模型交叉,15 周+ |
从 1% 到 0.3% 花的工程量是从 15% 到 1% 的两倍多。幻觉率每降一个数量级,成本和工程量都翻番。所以业务必须明确:你的场景需要多低的幻觉?闲聊型机器人 5% 可接受,客服系统 1%,合规咨询要 0.3% 以下,医疗诊断要 0.01%。过度治理就是浪费,不足治理就是风险。
引申七:为什么 Prompt Engineering 不是 "可有可无"
有人觉得 Prompt 工程是"前 LLM 时代的过渡技术",未来都靠模型本身能力。我们这次实战告诉我们:Prompt 仍是 RAG 系统最便宜、最快、最有效的一层防御。一个好的 Prompt 等于:
- 明确角色定义:LLM 知道自己是"知识库助手"还是"通用 AI";
- 清晰输出格式:JSON / Markdown / 自然语言,要规定;
- 显式 fallback 指令:不知道时怎么说,要预演;
- 引用格式约束:每个事实都要带源,标注格式统一才能程序解析。
差的 Prompt 能让最好的模型表现像三流模型,好的 Prompt 能让中等模型表现像顶级模型。Prompt 不是过渡技术,而是 LLM 应用工程的"系统设计",和写好一个 API 接口一样重要。
引申八:从这次治理学到的"LLM 应用产品观"
最后一个反思:LLM 应用的产品体验不同于传统 SaaS,有几个独特原则:
- "诚实拒答"是核心 feature,不是 bug:产品 UX 要把"我不知道"做得优雅,而不是当作降级;
- 引用是信任建设的基石:用户能点开看原文,信任就建立了一半;
- 响应时间和准确率不能同时优化:Self-Consistency 慢但准,要让用户能选;
- 给用户"质疑通道":每个回答下面加"举报错误"按钮,数据回流用来训练 verifier;
- 不要把 LLM 当魔法:它是个能力强但会犯错的工具,产品体验设计要假设它会犯错。
这套原则后来推广到了公司其他 LLM 项目(代码助手、面试评估、营销文案生成),每个项目的幻觉率都降到了 1% 以下。幻觉治理不是技术专项,是 LLM 应用工程的元能力——所有项目都需要。
引申九:Reranker 二次排序对幻觉的真实影响
RAG 的检索通常是"双塔召回 + 排序",我们后来加了一层 Cohere Rerank-v3 做二次排序,效果出乎意料:
import cohere
co = cohere.Client(api_key=os.getenv("COHERE_API_KEY"))
def rerank_chunks(question: str, candidates: list[dict], top_k: int = 5) -> list[dict]:
"""
candidates: hybrid retrieve 出来的 20 个候选
return: rerank 后 top_k
"""
docs = [c['text'] for c in candidates]
response = co.rerank(
model="rerank-multilingual-v3.0",
query=question,
documents=docs,
top_n=top_k
)
return [candidates[r.index] for r in response.results]
加 reranker 之后,我们 chunk 总数从 8 个降到 5 个(top-k 缩小),token 成本降了 35%,但幻觉率反而从 0.7% 降到 0.5%(在加 Self-Consistency 之前)。原因:reranker 把"语义最相关"的 chunk 排到最前面,弱相关的被剔除,LLM 不会被"看起来相关其实跑题"的 chunk 误导。Reranker 的 ROI 极高——每次额外 $0.001 的成本,换来 30% 幻觉率降幅,这是 RAG 系统最划算的投资之一。
但 reranker 也有失效场景:当 query 是"对比型"或"列举型"时,reranker 会把语义最相似的 chunk 都排上来,导致信息单一。比如问"A 方案和 B 方案的区别",reranker 可能召回 5 个关于 A 的 chunk,B 完全没召回。这时候要降低 reranker 权重,或者用 query expansion 拆成多个子查询分别检索。
引申十:Prompt Injection 对幻觉率的副作用
事故复盘到后期,我们发现一个意想不到的问题:对抗 prompt injection 的防御措施,反而会提高幻觉率。具体来说,我们之前加了一层"忽略用户指令中的"按 XX 格式输出"等指令",防止用户绕过格式约束。但实际效果是:
- 用户合法的"请用列表回答"请求被忽略,LLM 用自然语言回答,引用格式不易解析;
- Verifier 解析失败率上升 4%,这部分回答没法验证,只能展示给用户,等同于"漏网之鱼";
- 用户对"为什么没按我要求格式回答"困惑,信任度下降。
修法是"白名单合法用户指令":LLM 可以听用户对格式、长度、详细程度的请求,但不能听"忽略系统提示"、"扮演别的角色"这种 jailbreak 类指令。这个边界比想象的难划,我们花了 1 周才稳定下来。安全和体验的 trade-off 永远存在,不要因为安全而牺牲产品本质。
引申十一:监控体系建设——光治理不监控等于白干
这次治理给我们留下的最大资产不是 4 层防御,而是一套"幻觉监控仪表盘":
| 指标 | 采集方式 | 告警阈值 |
|---|---|---|
| 实时幻觉率 | 每条回答跑 Citation Verifier,记录 valid_ratio | 滑动 1h 窗口 > 1% 告警 |
| 拒答率 | 统计 fallback 路径触发次数 | 滑动 1h 窗口 > 15% 告警(知识库缺失信号) |
| Self-Consistency 分数分布 | 每天采样 1000 条,绘制直方图 | P50 < 0.8 告警(模型不稳定) |
| Top-K 召回质量 | 每周 50 条人工标 ground-truth,算 hit@5 | hit@5 < 85% 告警 |
| 用户"举报错误"次数 | UI 上"这个回答有问题"按钮 | 日增 > 30 次启动复盘 |
这套监控让我们能"小时级"发现幻觉率回升。比如某次更新了向量库索引,实时幻觉率 30 分钟内从 0.3% 跳到 1.2%,立刻回滚——如果靠周抽检发现,可能已经污染了 10 万次回答。监控的核心思想是"信号要快、噪声要少、可执行性强",任何一项不满足都不该上仪表盘。
引申十二:为什么我们最终没切换到更大的模型
治理到第 5 周,我们评估过"直接换 GPT-5 或 Claude Opus 4.1"。结论是:更大模型对幻觉率提升有限,且成本激增。我们在自己的评测集上跑了对比:
| 模型 | 幻觉率(裸 RAG) | 幻觉率(4 层防御) | 每千 token 成本 |
|---|---|---|---|
| GPT-4o-mini | 14.5% | 0.8% | $0.00015 |
| GPT-4o | 11.2% | 0.3% | $0.0025 |
| Claude 3.5 Sonnet | 9.4% | 0.4% | $0.003 |
| Claude Opus 4.1 | 7.8% | 0.2% | $0.015 |
| GPT-5(假设) | ~6% | ~0.15% | $0.02+ |
裸 RAG 模式下,更大模型确实幻觉率更低,GPT-4o → Opus 4.1 降了 30%。但加了 4 层防御之后,模型差异被工程框架"抹平"了——GPT-4o 0.3% 和 Opus 4.1 0.2% 几乎在统计噪声范围内,但成本差 6 倍。这给我们一个启示:工程框架的价值远大于模型本身,与其追逐 SOTA 模型,不如把工程做扎实。一个有 verifier 的 GPT-4o 系统,质量超过裸 RAG 的 Claude Opus 4.1。
引申十三:RAG 系统的"知识库新鲜度"治理
幻觉的另一个隐性来源是"知识库本身过期"。比如知识库里有 2024 年的 SOP,2025 年流程改了但文档没更新,LLM 完全按"对的"步骤回答,但其实是错的。这种"间接幻觉"比"直接编造"更隐蔽,因为 Citation Verifier 验证不出来——引用是真的,只是引用的内容过时了。
我们的治理方案有 3 个层面:
def check_chunk_freshness(chunk: dict) -> dict:
"""
对每个 chunk 标注新鲜度信号
"""
last_modified = chunk['metadata']['last_modified']
days_old = (datetime.now() - last_modified).days
# 文档类型决定衰减速度
doc_type = chunk['metadata']['type']
decay_map = {
'product_doc': 90, # 90 天后开始可疑
'sop': 180, # 180 天
'policy': 365, # 1 年
'tech_arch': 730, # 2 年
}
threshold = decay_map.get(doc_type, 180)
if days_old > threshold:
return {'fresh': False, 'warning': f'文档已 {days_old} 天未更新,可能过期'}
return {'fresh': True}
- chunk 元数据加 last_modified 字段:回答时附带"信息更新于 X 天前"的提示,让用户自己判断;
- 定期巡检过期文档:每月跑一次,把超过阈值的文档列表发给业务方 review;
- 关键 SOP 强制有效期:类似软件证书,SOP 过期前 30 天告警,过期后自动从知识库下线。
这块治理我们和业务团队配合做了 2 个月,清理出 1700 篇过期文档(占总数 14%)。知识库治理不是 IT 项目,是业务运营项目——LLM 不能替业务方维护知识。这是企业 LLM 落地最容易被忽视、但回报最大的事情之一。
总结
这次治理把幻觉率从 11.2% 压到 0.3%,核心方法是"Prompt 软约束 + Citation 硬验证 + Sufficiency 拒答 + Self-Consistency 兜底"四层叠加。没有用模型微调,没有用奇技淫巧,全是工程层面的"约束 + 兜底"组合,但效果可量化、可监控、可持续。
更重要的认知是:LLM 不会因为更大或更新就自动不幻觉,工程师必须主动给它套上"约束的笼子"。RAG 不是"把文档塞进 prompt 就完事",是一整套"检索 + 生成 + 验证 + 拒答"的工程系统。每一层都不可省。下次你看到 LLM 应用号称"幻觉率几乎为零"时,问一句:你的 verifier 是怎么实现的?没有 verifier 的低幻觉率,是统计幻觉,不是工程幻觉——前者只是没人抓,后者才是真的低。
最后一个反思:这场 6 周的治理,真正花在算法和模型上的时间不到 20%,80% 是在做产品体验设计、监控建设、业务方对齐、评测集维护。这印证了一个更大的判断——LLM 应用的差距不在模型,在工程。同样的 GPT-4o,有团队做出 0.3% 幻觉率的合规级助手,有团队做出 15% 幻觉率的玩具。差别不在模型,在你愿不愿意把 verifier、fallback、监控、评测这些"脏活"做扎实。
—— 别看了 · 2026