第2120篇:LLM幻觉检测与事实核查——让AI输出更可靠
2026/4/30大约 10 分钟
第2120篇:LLM幻觉检测与事实核查——让AI输出更可靠
适读人群:对准确性要求较高的LLM应用工程师 | 阅读时长:约20分钟 | 核心价值:理解幻觉的成因,建立多层次的幻觉检测机制,降低AI产生错误信息的风险
"AI说的这个数据是错的!"
这是高准确性LLM应用最头疼的问题。LLM有一个固有的缺陷:当它不知道答案时,不会说"我不知道",而是倾向于"生成一个看起来合理的答案"。
这就是幻觉(Hallucination)——模型自信地给出了错误的信息。
在普通问答场景,幻觉的代价是用户体验差。但在医疗、法律、财务等高风险场景,幻觉的代价可能非常严重。即使是企业内部知识问答,如果AI说错了某个政策或流程,可能导致员工做出错误决策。
这篇文章讲如何工程性地检测和减少幻觉。
幻觉的分类
/**
* LLM幻觉的主要类型
*
* ===== 类型一:事实性幻觉(Factual Hallucination)=====
*
* 捏造根本不存在的事实
*
* 例:
* "GPT-4o于2023年2月发布" → 实际是2024年5月
* "中国人口是16亿" → 实际约14亿
* "《数字安全法》第X条规定..." → 条款内容是编造的
*
* ===== 类型二:归因幻觉(Attribution Hallucination)=====
*
* 内容存在但归因错误(张冠李戴)
*
* 例:
* "根据您提供的文档,该规定是..." → 文档里没有这条规定
* 在RAG场景中,把文档A的内容归因到文档B
*
* ===== 类型三:推断过度(Over-Inference)=====
*
* 从真实信息推断出超出依据的结论
*
* 例:
* 文档说"A产品适合小企业"
* AI说"A产品不适合大企业" → 文档没这么说
*
* ===== 类型四:合并幻觉(Conflation Hallucination)=====
*
* 把不同来源的信息错误地混合
*
* 例:
* 把A公司的政策和B公司的政策混着说
* 把2022年和2023年的数据混淆
*
* ===== 检测难度 =====
*
* 事实性幻觉 > 推断过度 > 合并幻觉 > 归因幻觉
* (越难检测越靠前)
*
* 在RAG场景中,归因幻觉是最常见但也是最容易检测的
* 因为我们有检索的文档作为参考
*/RAG场景的幻觉检测
/**
* RAG幻觉检测器
*
* 在RAG场景中,我们有检索到的文档作为"事实基础"
* 可以验证AI的回答是否超出了这些事实
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RagHallucinationDetector {
private final ChatLanguageModel evaluatorModel;
/**
* 检测AI回答是否基于检索内容
*
* @param query 用户问题
* @param retrievedContext 检索到的文档内容
* @param aiAnswer AI生成的答案
* @return 幻觉检测结果
*/
public HallucinationDetectionResult detect(
String query, String retrievedContext, String aiAnswer) {
// 使用强力评估模型(通常比生成模型更强,或者用同等级不同provider的模型)
String prompt = buildDetectionPrompt(query, retrievedContext, aiAnswer);
try {
String response = evaluatorModel.generate(prompt);
return parseDetectionResult(response, aiAnswer);
} catch (Exception e) {
log.error("幻觉检测调用失败: {}", e.getMessage());
return HallucinationDetectionResult.unknown();
}
}
private String buildDetectionPrompt(
String query, String context, String answer) {
return """
请检查AI助手的回答是否基于提供的参考资料,或者包含了参考资料中没有的信息。
用户问题:
%s
参考资料:
%s
AI助手的回答:
%s
请逐句检查AI的回答,找出所有声明,判断每个声明是否能在参考资料中找到依据。
返回JSON:
{
"overallVerdict": "GROUNDED/PARTIALLY_GROUNDED/HALLUCINATED",
"hallucinationRate": 0-1,
"claims": [
{
"claim": "声明内容",
"verdict": "SUPPORTED/UNSUPPORTED/AMBIGUOUS",
"evidence": "参考资料中的依据(如果有)",
"explanation": "判断理由"
}
],
"summary": "总体评估(1-2句话)"
}
注意:
- GROUNDED:所有声明都有参考资料支撑
- PARTIALLY_GROUNDED:部分声明有依据,部分没有
- HALLUCINATED:主要声明超出了参考资料范围
只返回JSON。
""".formatted(query, truncateContext(context), answer);
}
private String truncateContext(String context) {
// 评估Prompt不能太长,截断上下文
return context.length() > 3000 ? context.substring(0, 3000) + "..." : context;
}
private HallucinationDetectionResult parseDetectionResult(
String response, String answer) {
try {
String json = extractJson(response);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
String verdict = root.path("overallVerdict").asText("AMBIGUOUS");
double hallucinationRate = root.path("hallucinationRate").asDouble(0);
String summary = root.path("summary").asText("");
List<ClaimVerification> claims = new ArrayList<>();
for (JsonNode claim : root.path("claims")) {
claims.add(new ClaimVerification(
claim.path("claim").asText(),
claim.path("verdict").asText(),
claim.path("evidence").asText(""),
claim.path("explanation").asText("")
));
}
HallucinationLevel level = switch (verdict) {
case "GROUNDED" -> HallucinationLevel.NONE;
case "PARTIALLY_GROUNDED" -> hallucinationRate > 0.3 ?
HallucinationLevel.MEDIUM : HallucinationLevel.LOW;
case "HALLUCINATED" -> HallucinationLevel.HIGH;
default -> HallucinationLevel.UNKNOWN;
};
return new HallucinationDetectionResult(level, hallucinationRate, summary, claims);
} catch (Exception e) {
log.warn("幻觉检测结果解析失败: {}", e.getMessage());
return HallucinationDetectionResult.unknown();
}
}
private String extractJson(String s) {
int start = s.indexOf('{'); int end = s.lastIndexOf('}');
return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
}
public enum HallucinationLevel { NONE, LOW, MEDIUM, HIGH, UNKNOWN }
record ClaimVerification(String claim, String verdict, String evidence, String explanation) {}
@Data
@Builder
public static class HallucinationDetectionResult {
private HallucinationLevel level;
private double hallucinationRate;
private String summary;
private List<ClaimVerification> claims;
public static HallucinationDetectionResult unknown() {
return HallucinationDetectionResult.builder()
.level(HallucinationLevel.UNKNOWN)
.hallucinationRate(0.5)
.summary("检测失败").claims(List.of()).build();
}
public boolean isSafe() {
return level == HallucinationLevel.NONE || level == HallucinationLevel.LOW;
}
}
}防幻觉Prompt设计
/**
* 防幻觉的Prompt设计策略
*
* 从源头减少幻觉:在Prompt设计上做约束
*/
@Component
public class AntiHallucinationPromptBuilder {
/**
* 构建强约束的RAG Prompt
*
* 核心原则:
* 1. 明确告诉LLM,只能基于提供的资料回答
* 2. 如果资料不足,要说"根据现有资料无法回答"
* 3. 不允许LLM用"通常"/"一般来说"等模糊的补充
*/
public String buildStrictRagPrompt(
String question, String context, StrictnessLevel level) {
return switch (level) {
case STANDARD -> buildStandardPrompt(question, context);
case STRICT -> buildStrictPrompt(question, context);
case ULTRA_STRICT -> buildUltraStrictPrompt(question, context);
};
}
private String buildStandardPrompt(String question, String context) {
return """
请根据以下参考资料回答用户的问题。
参考资料:
%s
用户问题:%s
请基于参考资料给出准确的回答。如果参考资料中没有相关信息,请明确说明。
""".formatted(context, question);
}
private String buildStrictPrompt(String question, String context) {
return """
你是一个严格基于参考资料的问答助手。
## 严格规则:
1. 只能使用参考资料中明确包含的信息来回答
2. 不能补充参考资料以外的知识,哪怕你认为是常识
3. 如果参考资料不足以完整回答,要明确指出"以下问题参考资料未涉及:..."
4. 引用参考资料时,可以标注"根据参考资料..."
5. 不允许使用"通常"、"一般来说"、"大多数情况下"等依赖外部知识的表达
参考资料:
%s
用户问题:%s
请严格按照规则回答。
""".formatted(context, question);
}
private String buildUltraStrictPrompt(String question, String context) {
return """
你是一个高精度信息提取助手,工作在严格的事实核查模式。
## 绝对约束:
- 你的回答必须完全来自提供的参考资料
- 资料中没有的信息,一律不能出现在回答中
- 如果无法仅基于参考资料回答,你必须说:"参考资料中未包含足够信息回答此问题"
- 禁止任何形式的推断、延伸或补充
参考资料:
===START_CONTEXT===
%s
===END_CONTEXT===
问题:%s
回答(仅基于以上参考资料):
""".formatted(context, question);
}
/**
* 让LLM在回答中标注置信度
*
* 这种方式让LLM自己暴露不确定性
*/
public String buildWithConfidencePrompt(String question, String context) {
return """
请回答以下问题,并对每个关键声明标注置信度。
参考资料:%s
问题:%s
回答格式要求:
- 对于有参考资料明确支持的内容,正常陈述
- 对于推断的内容,用[推断: 说明]标注
- 对于不确定的内容,用[不确定]标注
- 对于超出参考资料范围的内容,用[超出资料范围]标注
""".formatted(context, question);
}
public enum StrictnessLevel { STANDARD, STRICT, ULTRA_STRICT }
}自我一致性检测
/**
* 自我一致性检测
*
* 思路:对同一个问题生成多个答案,看答案是否一致
* 如果多次回答结论不同,说明这个问题对LLM来说不确定
*
* 这个方法不需要外部事实库,只需要LLM自身
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SelfConsistencyChecker {
private final ChatLanguageModel llm;
/**
* 通过多次采样检测答案一致性
*
* @param numSamples 采样次数(通常3-5次)
* @return 一致性检测结果
*/
public ConsistencyCheckResult check(String prompt, int numSamples) {
List<String> answers = new ArrayList<>();
for (int i = 0; i < numSamples; i++) {
try {
// 注意:需要用高temperature(0.7-1.0)才能产生有意义的多样性
// 低temperature的多次采样结果几乎相同
String answer = llm.generate(prompt);
answers.add(answer);
} catch (Exception e) {
log.warn("采样失败: {}", e.getMessage());
}
}
if (answers.size() < 2) {
return ConsistencyCheckResult.insufficient();
}
// 用LLM判断多个答案是否一致
return analyzeConsistency(answers);
}
private ConsistencyCheckResult analyzeConsistency(List<String> answers) {
String answersText = IntStream.range(0, answers.size())
.mapToObj(i -> "答案" + (i+1) + ":\n" + answers.get(i))
.collect(Collectors.joining("\n\n---\n\n"));
String prompt = """
以下是对同一个问题的%d个回答,请判断这些回答在核心事实和结论上是否一致。
%s
返回JSON:
{
"isConsistent": true/false,
"consistencyScore": 0-1,
"inconsistencies": ["不一致点1", "不一致点2"],
"mostReliableAnswer": 最可靠答案的编号(1/%d),
"reason": "判断理由"
}
注意:细节措辞不同不算不一致,核心事实或结论不同才算。
只返回JSON。
""".formatted(answers.size(), answersText, answers.size());
try {
String response = llm.generate(prompt);
String json = extractJson(response);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
boolean isConsistent = root.path("isConsistent").asBoolean(true);
double score = root.path("consistencyScore").asDouble(1.0);
List<String> inconsistencies = new ArrayList<>();
for (JsonNode inc : root.path("inconsistencies")) {
inconsistencies.add(inc.asText());
}
int bestAnswerIndex = Math.max(0, root.path("mostReliableAnswer").asInt(1) - 1);
String bestAnswer = bestAnswerIndex < answers.size() ? answers.get(bestAnswerIndex) : answers.get(0);
return new ConsistencyCheckResult(
isConsistent, score, inconsistencies, bestAnswer, answers
);
} catch (Exception e) {
return ConsistencyCheckResult.insufficient();
}
}
private String extractJson(String s) {
int start = s.indexOf('{'); int end = s.lastIndexOf('}');
return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
}
@Data
@Builder
public static class ConsistencyCheckResult {
private boolean isConsistent;
private double consistencyScore;
private List<String> inconsistencies;
private String mostReliableAnswer;
private List<String> allAnswers;
public static ConsistencyCheckResult insufficient() {
return ConsistencyCheckResult.builder()
.isConsistent(true).consistencyScore(0.5)
.inconsistencies(List.of()).build();
}
public boolean isHighRisk() {
return !isConsistent && consistencyScore < 0.6;
}
}
}幻觉处理策略
/**
* 幻觉检测后的处理策略
*
* 根据检测结果决定如何处理
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class HallucinationResponseStrategy {
private final RagHallucinationDetector detector;
public FinalResponse handleWithDetection(
String query, String context, String rawAnswer) {
// 对高风险场景(医疗、法律、财务)启用检测
HallucinationDetectionResult detection = detector.detect(query, context, rawAnswer);
if (detection.isSafe()) {
// 通过检测,直接返回
return FinalResponse.safe(rawAnswer);
}
return switch (detection.getLevel()) {
case LOW -> {
// 低风险:在答案后面加免责声明
String enhanced = rawAnswer + "\n\n" +
"⚠️ 部分信息可能超出参考资料范围,请以官方文档为准。";
yield FinalResponse.withWarning(enhanced, detection.getSummary());
}
case MEDIUM -> {
// 中风险:高亮可疑声明,提示用户核实
String enhanced = rawAnswer + "\n\n" +
"⚠️ AI回答中包含部分推断性内容,建议核实以下信息:\n" +
detection.getClaims().stream()
.filter(c -> "UNSUPPORTED".equals(c.verdict()))
.map(c -> "- " + c.claim())
.collect(Collectors.joining("\n"));
yield FinalResponse.withWarning(enhanced, detection.getSummary());
}
case HIGH -> {
// 高风险:拒绝返回原答案,改为保守回答
String conservative = buildConservativeAnswer(query, context);
yield FinalResponse.replaced(conservative,
"原答案包含大量未经验证的内容,已替换为基于参考资料的保守回答");
}
default -> FinalResponse.safe(rawAnswer);
};
}
private String buildConservativeAnswer(String query, String context) {
// 从参考资料中直接提取相关段落,不依赖LLM生成
if (context == null || context.isEmpty()) {
return "抱歉,根据现有参考资料无法准确回答此问题。";
}
return "根据参考资料,相关信息如下:\n\n" +
context.substring(0, Math.min(500, context.length())) +
(context.length() > 500 ? "\n\n(以上为参考资料节选)" : "");
}
@Data
@Builder
public static class FinalResponse {
private String content;
private ResponseType type;
private String note;
public static FinalResponse safe(String content) {
return FinalResponse.builder().content(content).type(ResponseType.SAFE).build();
}
public static FinalResponse withWarning(String content, String note) {
return FinalResponse.builder().content(content).type(ResponseType.WARNING).note(note).build();
}
public static FinalResponse replaced(String content, String note) {
return FinalResponse.builder().content(content).type(ResponseType.REPLACED).note(note).build();
}
public enum ResponseType { SAFE, WARNING, REPLACED }
}
}实践建议
不是所有场景都需要幻觉检测
幻觉检测会增加延迟(需要额外的LLM调用)和成本。对于闲聊类、创意类应用,幻觉的影响相对小,不值得投入。但对于医疗咨询、法律解读、财务数据分析等高风险场景,幻觉检测是必须的。先识别你的应用里哪些场景是高风险的,针对性地启用检测。
防幻觉最有效的方法是Prompt工程,而不是后处理
事后检测幻觉,是发现了问题再处理。更高效的做法是从一开始就减少幻觉的产生。两个最有效的Prompt技巧:(1)明确告诉LLM"只能基于提供的资料回答,资料里没有的不要说";(2)让LLM输出时标注置信度("我不确定"这几个字往往能减少自信的错误)。好的Prompt能把幻觉率降低30-50%。
用"引用验证"而不是"语义验证"
最简单、最可靠的幻觉检测方法:让AI在回答时标注每个声明来自哪个文档的哪部分,然后自动验证这些引用是否真实存在。如果AI引用了"文档A第3段",但文档A第3段根本没有这个内容,说明产生了归因幻觉。这种方法比让另一个LLM判断"语义是否一致"更精确,误报率也更低。
