LLM 幻觉检测:用输出一致性检查降低业务风险,别让模型在关键场景胡编
LLM 幻觉检测用输出一致性检查降低业务风险别让模型在关键场景胡编一、客服机器人给用户退了 500 元——模型幻觉的真实代价某电商客服机器人接到用户提问我买的东西坏了怎么办模型在幻觉状态下编造了一个不存在的退款政策告诉用户可以全额退款 500 元。用户在工单系统里贴出了聊天截图客服团队不得不按承诺退款。这是模型幻觉在业务场景下的真实代价——不是技术指标上的准确率下降 2%而是实打实的经济损失。基础设施不需要漂亮话LLM 的幻觉Hallucination问题没有完美的技术解决方案。模型生成的内容本质上是一个概率分布上的采样任何温度 0 的配置下都有概率产生偏离事实的输出。但工程上可以做的是在应用层加一层检测护栏——在模型输出给用户之前通过一系列可计算的检查手段识别和拦截高风险输出。输出一致性检查Self-Consistency Check是其中性价比最高的方案对同一个 Prompt用不同的采样参数或微调策略生成多个候选回复然后比较它们之间的一致性。如果 5 个候选回复中 3 个说可以退款 5002 个说不支持退款这个回答的一致性得分就是 60%——低于阈值的回答被自动拦截不展示给用户。二、输出一致性检查的原理与流程核心假设是对于事实性问题factual questions模型的多个独立采样应该收敛到相似答案。如果多个采样互相矛盾说明模型在这个问题上的 confidence 很低大概率在胡编flowchart TD A[用户提问] -- B[Prompt 模板br/ System Prompt] B -- C1[采样 1br/temperature0.3] B -- C2[采样 2br/temperature0.5] B -- C3[采样 3br/temperature0.7] B -- C4[采样 4br/temperature0.9] B -- C5[采样 5br/temperature1.0] C1 -- D[候选回复 1] C2 -- D2[候选回复 2] C3 -- D3[候选回复 3] C4 -- D4[候选回复 4] C5 -- D5[候选回复 5] D D2 D3 D4 D5 -- E[相似度矩阵计算] E -- F{一致性得分 阈值br/默认 0.7?} F --|是| G[通过检测br/返回多数共识回复] F --|否| H[拦截输出br/返回安全兜底回复] H -- I[记录日志br/采样 5 次仍不一致 高风险] G -- J[正常返回用户]相似度计算候选回复之间的相似度可以用多种方式衡量。最简单的是基于 token overlap 的 Jaccard 相似度——两个回复的共享 token 数除以总 token 数。更准确的是用 Sentence-BERT 这类轻量 embedding 模型计算语义相似度能识别同义表达可以退款和支持退款在语义上是一致的但在 token 层面 overlap 很低。温度参数的梯度设计五个采样使用递增的温度0.3 → 0.5 → 0.7 → 0.9 → 1.0目的是在保守采样和多样采样之间做对比。低温度采样倾向于生成模型认为最安全的回答高温度采样更容易暴露不确定性。如果低温和高温采样结果大相径庭说明这个问题的答案在模型的概率分布上本身就是分散的——典型的幻觉高风险场景。兜底策略一致性检查不通过时返回的不是我不知道而是预定义的兜底回复。兜底回复分为两类关键场景如退款政策、医疗建议的兜底是请咨询人工客服非关键场景如闲聊的兜底是通用安全回复。三、生产级幻觉检测网关实现以下 Go 代码实现了一个 LLM 输出一致性检查网关核心流程是并行多采样 语义相似度矩阵计算 阈值判定package hallucination import ( context fmt math sort strings sync golang.org/x/sync/errgroup ) // ConsistencyChecker 输出一致性检测器 type ConsistencyChecker struct { // LLM 调用接口由外部注入适配不同模型提供商 llmClient LLMClient // 采样次数默认 5 numSamples int // 各次采样的温度参数 temperatures []float64 // 一致性阈值0.0 - 1.0低于此值判定为幻觉高风险 consistencyThreshold float64 // 关键场景更高的一致性阈值金融/医疗等 criticalThreshold float64 } // LLMClient 抽象 LLM 调用接口 // 生产环境中可能对接 OpenAI、vLLM、自建推理服务等 type LLMClient interface { // Generate 生成单个回复 // prompt: 完整的 PromptSystem User // temperature: 采样温度 Generate(ctx context.Context, prompt string, temperature float64) (string, error) } // ConsistencyResult 一致性检查结果 type ConsistencyResult struct { Passed bool // 是否通过检查 Score float64 // 一致性得分 (0-1) ConsensusAnswer string // 多数共识回复 Candidates []string // 所有候选回复 RiskLevel string // low / medium / high } // NewConsistencyChecker 创建一致性检测器 func NewConsistencyChecker(client LLMClient, numSamples int, baseThreshold, criticalThreshold float64) *ConsistencyChecker { if numSamples 2 { numSamples 2 // 至少 2 次采样才有比较意义 } if numSamples 10 { numSamples 10 // 上限防滥用 } // 构建温度梯度从保守到激进 temps : make([]float64, numSamples) for i : 0; i numSamples; i { // 温度范围: 0.3 → 1.0线性分布 temps[i] 0.3 (float64(i) / float64(numSamples-1)) * 0.7 } return ConsistencyChecker{ llmClient: client, numSamples: numSamples, temperatures: temps, consistencyThreshold: baseThreshold, criticalThreshold: criticalThreshold, } } // Check 执行一致性检查 // prompt: 完整的 Prompt // isCritical: 是否为关键场景金融/医疗等使用更高阈值 func (c *ConsistencyChecker) Check(ctx context.Context, prompt string, isCritical bool) (*ConsistencyResult, error) { // 1. 并行多次采样 candidates, err : c.parallelSample(ctx, prompt) if err ! nil { return nil, fmt.Errorf(parallel sampling: %w, err) } // 2. 计算候选回复之间的语义相似度矩阵 simMatrix : c.computeSimilarityMatrix(candidates) // 3. 计算一对一致性得分 score : c.computeConsistencyScore(simMatrix) // 4. 确定阈值 threshold : c.consistencyThreshold if isCritical { threshold c.criticalThreshold } // 5. 判定风险等级 passed : score threshold riskLevel : low if !passed { if score threshold*0.5 { riskLevel high } else { riskLevel medium } } // 6. 选择共识回复取与所有其他回复平均相似度最高的那个 consensus : c.selectConsensus(candidates, simMatrix) return ConsistencyResult{ Passed: passed, Score: score, ConsensusAnswer: consensus, Candidates: candidates, RiskLevel: riskLevel, }, nil } // parallelSample 并行多次采样使用 errgroup 管理 goroutine 生命周期 func (c *ConsistencyChecker) parallelSample(ctx context.Context, prompt string) ([]string, error) { results : make([]string, c.numSamples) var mu sync.Mutex g, ctx : errgroup.WithContext(ctx) // 限制并发数为 3防止打爆 LLM API g.SetLimit(3) for i : 0; i c.numSamples; i { idx : i temp : c.temperatures[i] g.Go(func() error { resp, err : c.llmClient.Generate(ctx, prompt, temp) if err ! nil { return fmt.Errorf(sample %d (temp%.2f): %w, idx, temp, err) } mu.Lock() results[idx] resp mu.Unlock() return nil }) } if err : g.Wait(); err ! nil { return nil, err } return results, nil } // computeSimilarityMatrix 计算候选回复两两之间的相似度矩阵 func (c *ConsistencyChecker) computeSimilarityMatrix(candidates []string) [][]float64 { n : len(candidates) matrix : make([][]float64, n) for i : 0; i n; i { matrix[i] make([]float64, n) matrix[i][i] 1.0 // 自相似度为 1 } for i : 0; i n; i { for j : i 1; j n; j { // 使用 Jaccard 相似度作为轻量级度量 // 生产环境替换为 Sentence-BERT 的余弦相似度 sim : jaccardSimilarity(candidates[i], candidates[j]) matrix[i][j] sim matrix[j][i] sim // 对称矩阵 } } return matrix } // computeConsistencyScore 从相似度矩阵计算整体一致性得分 // 策略所有非对角线元素的平均值 func (c *ConsistencyChecker) computeConsistencyScore(matrix [][]float64) float64 { n : len(matrix) if n 1 { return 1.0 } var sum float64 count : 0 for i : 0; i n; i { for j : 0; j n; j { if i ! j { sum matrix[i][j] count } } } if count 0 { return 0 } return math.Round(sum/float64(count)*10000) / 10000 // 保留 4 位小数 } // selectConsensus 选择共识回复与所有其他回复平均相似度最高的候选 func (c *ConsistencyChecker) selectConsensus(candidates []string, matrix [][]float64) string { n : len(candidates) bestIdx : 0 bestAvg : 0.0 for i : 0; i n; i { var sum float64 count : 0 for j : 0; j n; j { if i ! j { sum matrix[i][j] count } } avg : sum / float64(count) if avg bestAvg { bestAvg avg bestIdx i } } return candidates[bestIdx] } // jaccardSimilarity 计算两个文本的 Jaccard 相似度 func jaccardSimilarity(a, b string) float64 { tokensA : tokenize(a) tokensB : tokenize(b) if len(tokensA) 0 len(tokensB) 0 { return 1.0 } // 构建 set A setA : make(map[string]bool, len(tokensA)) for _, t : range tokensA { setA[t] true } // 计算交集和并集 intersection : 0 setB : make(map[string]bool, len(tokensB)) for _, t : range tokensB { setB[t] true if setA[t] { intersection } } // 并集大小 union : len(setA) for t : range setB { if !setA[t] { union } } if union 0 { return 0 } return float64(intersection) / float64(union) } // tokenize 简单中文分词基于字符 bigram // 生产环境替换为 jieba 分词器 func tokenize(text string) []string { // 去除空白字符 text strings.TrimSpace(text) runes : []rune(text) if len(runes) 1 { return []string{text} } // Bigram 分词对相邻字符对建 token tokens : make([]string, 0, len(runes)-1) for i : 0; i len(runes)-1; i { tokens append(tokens, string(runes[i:i2])) } return tokens }网关层集成示例package gateway import ( context net/http ) // HallucinationGuard 幻觉检测网关中间件 type HallucinationGuard struct { checker *hallucination.ConsistencyChecker criticalPaths map[string]bool // 关键业务路径映射 } // NewHallucinationGuard 创建幻觉检测网关 func NewHallucinationGuard(checker *hallucination.ConsistencyChecker) *HallucinationGuard { return HallucinationGuard{ checker: checker, criticalPaths: map[string]bool{ /api/refund: true, // 退款相关 /api/medical: true, // 医疗相关 /api/finance: true, // 金融相关 }, } } // Handle 处理推理请求在模型输出后执行一致性检查 func (g *HallucinationGuard) Handle(ctx context.Context, prompt string, path string) (string, error) { isCritical : g.criticalPaths[path] result, err : g.checker.Check(ctx, prompt, isCritical) if err ! nil { // 一致性检查本身失败时降级为直接返回 LLM 原始输出 // 同时在日志中标记采样失败事件 return , fmt.Errorf(consistency check failed: %w, err) } if !result.Passed { // 一致性检查不通过返回兜底回复 if isCritical { return 抱歉我暂时无法准确回答这个问题建议您联系人工客服获取帮助。, nil } return 抱歉我没有找到确定的答案请您换个方式描述问题。, nil } return result.ConsensusAnswer, nil }四、边界权衡与工程代价计算成本翻 5 倍一致性检查要求对同一个 Prompt 做 5 次独立推理。如果单次推理耗时 200ms一致性检查就需要 1 秒。这对于实时对话场景是难以接受的延迟。两个优化方向并行推理——5 次采样同时发起总耗时约等于最慢的那次采样而非 5 次之和分级检查——先做两次采样temperature 0.3 和 0.9如果这两次就足够一致相似度 0.9跳过后续 3 次额外采样节约 60% 计算。相似度度量选择Jaccard 相似度对中文分词质量高度敏感。如果分词器把可以退款切成[可, 以, 退, 款]而把支持退款切成[支持, 退款]Jaccard 相似度就是 0——即使语义相同。对于中文场景建议使用 Sentence-BERT 或 text2vec 做语义相似度计算牺牲一点性能换取准确性。但如果已经有成熟的中文分词器如 jiebaJaccard 同义词扩展也是可行的轻量方案。幻觉类型的不对称性输出一致性检查擅长检测事实型幻觉编造不存在的信息但几乎检测不到遗漏型幻觉该说的没说。比如用户问退货需要哪些材料模型回答需要订单号和商品遗漏了还需要退货理由。5 次采样可能都遗漏了同一条信息一致性检查通过但回答仍然不完整。这种场景需要知识库召回验证——用 RAG 检索到的文档片段和模型输出做交叉比对检查输出中的每个 claim 在知识库中是否有依据。对创造性任务不适用写诗、写代码、头脑风暴这类开放性任务天然存在多种合理答案。如果用一致性检查去评判5 个候选回复互不相同一致性得分接近 0但它不是幻觉——只是合理多样性。一致性检查仅适用于有确定性答案的场景知识问答、政策查询、数据提取不能用于评判创造性输出。五、总结LLM 幻觉检测的输出一致性检查是低成本、高收益的工程护栏5 次并行采样 温度梯度用 0.3-1.0 的温度梯度生成 5 个候选回复并行推理控制总耗时在 300ms 以内。语义相似度替代 token overlap中文场景用 Sentence-BERT 做相似度计算避免分词偏差导致的误判。分级检查 阈值可调两次采样一致相似度 0.9就跳过额外采样一致性阈值默认 0.7关键场景金融/医疗提高到 0.85。明确适用边界只检查事实型幻觉不适用于创造性任务。结合 RAG 的知识库比对补齐遗漏型幻觉检测。用输出一致性检查作为检测护栏虽然不能消除模型幻觉但至少能把胡编出来的退款政策拦截在展示给用户之前。