LLM幻觉的工程防治:Java工程师能做的10件事
LLM幻觉的工程防治:Java工程师能做的10件事
500名开发者,因为一个不存在的API,集体踩坑
2025年8月的一个周一,某云厂商的开发者社区论坛上突然出现了大量相同的帖子,标题大同小异:
- "官方文档有误?
CloudStorage.uploadWithMetadata()根本不存在" - "Spring Boot集成XXX云存储,按文档来的,但找不到这个方法"
- "有人遇到同样的问题吗?按教程写的代码,一个方法都找不到"
到周一下午,相关帖子超过60条,涉及开发者超过500人。
追查下来,源头是一篇发布在技术博客平台上的"AI生成教程"。文章里有一段Java集成示例,代码逻辑流畅、注释清晰、格式规范:
// 这段代码是AI"编造"的,该方法根本不存在
CloudStorageClient client = CloudStorageClientBuilder.build(config);
client.uploadWithMetadata(file, metadata, UploadOptions.DEFAULT);uploadWithMetadata 这个方法,在该SDK的任何版本中从未存在过。
AI大模型把它"合理推断"出来了——因为这个方法名"听起来很合理",符合SDK的命名规范,而且整篇文章语气自信,没有任何不确定的表达。
500名开发者在没有核实的情况下,照着写了代码,然后集体遭遇了NoSuchMethodException。
这就是LLM幻觉(Hallucination)在代码场景下的典型危害。更可怕的是:这篇教程的作者,自己也是受害者——他用AI生成了这篇文章,发布前没有运行过代码。
作为Java工程师,我们不能只是指责AI"不靠谱",更需要思考:在工程层面,我们能做什么来防治幻觉?
幻觉的分类:知道你在对抗什么
在着手解决之前,先理解幻觉的三种主要类型:
类型一:事实性幻觉(Factual Hallucination)
模型捏造了不存在的事实,且语气自信。
示例:
- 错误声称某Java版本在某个日期发布
- 引用一篇不存在的论文(作者名、标题、期刊都"看起来真实")
- 声称某公司有某项政策,实际上没有
根本原因: 训练数据截止日期后的信息不存在;或者模型通过"合理推断"填补了自己不确定的知识空白。
类型二:引用幻觉(Citation Hallucination)
模型给出了看似真实的引用来源(论文、文档链接、ISBN号),但这些来源根本不存在或内容对不上。
示例:
- "根据Spring官方文档第4.3.2节..." —— 该节根本不存在
- 给出一个真实的URL,但URL指向的内容并不包含所引用的信息
类型三:代码幻觉(Code Hallucination)
这是对Java工程师最致命的类型。
常见模式:
- 调用不存在的方法(如开篇故事)
- 使用已被废弃或已删除的API
- 错误的参数顺序或类型
- 虚构的配置项(如在application.yml中使用不存在的属性)
- 声称某库有某功能,但实际上没有
防治策略全景图
防治1:RAG提供事实依据(减少无中生有)
RAG(检索增强生成)是目前工程防治幻觉最有效的手段。核心思路:不让模型从"记忆"中回答,而是提供真实文档作为依据。
完整RAG防幻觉实现
pom.xml(RAG相关依赖)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<groupId>com.laozhang</groupId>
<artifactId>anti-hallucination</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI + OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- 向量存储(Milvus)-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
</dependency>
<!-- 文档解析 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- JSON处理(结构化输出)-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>application.yml
server:
port: 8080
spring:
application:
name: anti-hallucination
ai:
openai:
base-url: ${AI_BASE_URL:http://localhost:8000/v1}
api-key: ${AI_API_KEY:not-needed}
chat:
options:
model: deepseek-r1
temperature: 0.1 # RAG场景:低温度,减少随机性
max-tokens: 2048
vectorstore:
milvus:
client:
host: localhost
port: 19530
database-name: anti_hallucination
collection-name: knowledge_base
initialize-schema: true
embedding-dimension: 768
laozhang:
anti-hallucination:
# RAG检索Top-K
rag-top-k: 5
# RAG相似度阈值(低于此分数的结果不使用)
rag-similarity-threshold: 0.75
# 验证LLM的模型(可以用较小的模型做验证)
validation-model: qwen2.5:7b
# 置信度阈值(低于此值触发人工审核)
confidence-threshold: 0.7
# 高风险输出必须人工审核
high-risk-review-enabled: trueRagAntiHallucinationService.java
package com.laozhang.hallucination.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* RAG防幻觉服务
* 强制模型基于检索到的文档回答,不允许从"记忆"中编造
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RagAntiHallucinationService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Value("${laozhang.anti-hallucination.rag-top-k:5}")
private int ragTopK;
@Value("${laozhang.anti-hallucination.rag-similarity-threshold:0.75}")
private double similarityThreshold;
/**
* RAG增强回答(严格基于文档,禁止自由发挥)
*/
public RagResponse answerWithRag(String question) {
// 1. 从向量库检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(ragTopK)
.withSimilarityThreshold(similarityThreshold)
);
if (relevantDocs.isEmpty()) {
log.warn("RAG检索无结果,问题: {}", question);
return RagResponse.builder()
.answer("抱歉,我在知识库中没有找到与该问题相关的信息。" +
"请联系相关人员获取准确信息。")
.hasContext(false)
.sourceCount(0)
.build();
}
// 2. 构建上下文(包含来源文档信息)
StringBuilder contextBuilder = new StringBuilder();
for (int i = 0; i < relevantDocs.size(); i++) {
Document doc = relevantDocs.get(i);
String source = doc.getMetadata().getOrDefault("source", "未知来源").toString();
contextBuilder.append(String.format("[文档%d] 来源:%s\n%s\n\n",
i + 1, source, doc.getContent()));
}
String context = contextBuilder.toString();
// 3. 构建严格约束的Prompt
String ragPrompt = String.format("""
你是一个严格基于提供文档回答问题的助手。
【严格规则】
1. 只能使用下方"参考文档"中的信息回答问题
2. 如果文档中没有相关信息,必须说"文档中未找到相关信息"
3. 禁止使用文档以外的任何知识
4. 每个关键陈述必须标注来源(如:[文档1])
5. 对不确定的信息,必须用"根据文档显示"等措辞,不能用肯定语气
【参考文档】
%s
【用户问题】
%s
请按规则回答:
""", context, question);
// 4. 调用模型
String answer = chatClient.prompt()
.user(ragPrompt)
.call()
.content();
// 5. 提取引用的文档来源
List<String> sources = relevantDocs.stream()
.map(doc -> doc.getMetadata().getOrDefault("source", "未知").toString())
.distinct()
.collect(Collectors.toList());
return RagResponse.builder()
.answer(answer)
.hasContext(true)
.sourceCount(relevantDocs.size())
.sources(sources)
.build();
}
}// RagResponse.java
package com.laozhang.hallucination.service;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class RagResponse {
private String answer;
private boolean hasContext;
private int sourceCount;
private List<String> sources;
}防治2:Temperature调低(减少随机性)
Temperature是控制模型"创造性"与"确定性"的核心参数。
Temperature对输出的影响
| Temperature | 输出特征 | 适用场景 | 幻觉风险 |
|---|---|---|---|
| 0.0 | 完全确定性,每次输出相同 | 代码生成、SQL、严格格式化 | 最低 |
| 0.1-0.3 | 接近确定,轻微变化 | 技术文档、事实性问答 | 低 |
| 0.5-0.7 | 平衡创造性和一致性 | 通用对话、分析 | 中 |
| 1.0+ | 高度随机,发散 | 创意写作、头脑风暴 | 高 |
TemperatureAdaptiveService.java
package com.laozhang.hallucination.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Service;
/**
* 自适应Temperature服务
* 根据任务类型自动选择合适的Temperature,平衡质量和幻觉风险
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TemperatureAdaptiveService {
private final OpenAiChatModel chatModel;
public enum TaskType {
CODE_GENERATION(0.0f), // 代码生成:完全确定性
FACT_CHECKING(0.1f), // 事实核查:接近确定
TECHNICAL_DOCUMENTATION(0.1f), // 技术文档:接近确定
DATA_ANALYSIS(0.2f), // 数据分析:低随机性
GENERAL_QA(0.5f), // 通用问答:中等
CUSTOMER_SERVICE(0.6f), // 客服对话:适度友好
CREATIVE_WRITING(0.9f); // 创意写作:高创造性
final float temperature;
TaskType(float temperature) {
this.temperature = temperature;
}
}
/**
* 使用任务适配的Temperature调用模型
*/
public String chat(String message, TaskType taskType) {
log.info("任务类型: {}, 使用Temperature: {}", taskType, taskType.temperature);
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withTemperature(taskType.temperature)
.withTopP(taskType == TaskType.CODE_GENERATION ? 1.0f : 0.9f)
.build();
// 动态覆盖默认选项
return ChatClient.builder(chatModel)
.build()
.prompt()
.options(options)
.user(message)
.call()
.content();
}
/**
* 自动推断任务类型并选择Temperature
*/
public String adaptiveChat(String message) {
TaskType type = inferTaskType(message);
return chat(message, type);
}
private TaskType inferTaskType(String message) {
String lower = message.toLowerCase();
if (lower.contains("写代码") || lower.contains("实现") || lower.contains("java")) {
return TaskType.CODE_GENERATION;
}
if (lower.contains("事实") || lower.contains("是否正确") || lower.contains("核实")) {
return TaskType.FACT_CHECKING;
}
if (lower.contains("文档") || lower.contains("教程")) {
return TaskType.TECHNICAL_DOCUMENTATION;
}
return TaskType.GENERAL_QA;
}
}防治3:结构化输出约束(限制输出范围)
强制模型以固定格式输出,从根本上限制幻觉的发挥空间。
StructuredOutputService.java
package com.laozhang.hallucination.service;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 结构化输出服务
* 强制模型按Schema输出,无法在自由文本中插入幻觉内容
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StructuredOutputService {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
/**
* 技术方案评估(结构化输出)
*/
@Data
public static class TechEvaluation {
@JsonProperty("name")
private String name;
@JsonProperty("pros")
private List<String> pros;
@JsonProperty("cons")
private List<String> cons;
@JsonProperty("applicable_scenarios")
private List<String> applicableScenarios;
@JsonProperty("performance_data")
private String performanceData;
@JsonProperty("confidence_level")
private String confidenceLevel; // HIGH/MEDIUM/LOW
@JsonProperty("data_source")
private String dataSource; // 数据来源说明
}
/**
* API存在性验证(结构化输出防止代码幻觉)
*/
@Data
public static class ApiVerification {
@JsonProperty("class_name")
private String className;
@JsonProperty("method_name")
private String methodName;
@JsonProperty("exists")
private boolean exists;
@JsonProperty("version_introduced")
private String versionIntroduced;
@JsonProperty("deprecated")
private boolean deprecated;
@JsonProperty("alternative_if_not_exists")
private String alternativeIfNotExists;
@JsonProperty("confidence")
private String confidence; // HIGH/MEDIUM/LOW/UNKNOWN
@JsonProperty("verification_note")
private String verificationNote;
}
/**
* 强制结构化输出技术评估
*/
public TechEvaluation evaluateTechnology(String techName) {
BeanOutputConverter<TechEvaluation> converter =
new BeanOutputConverter<>(TechEvaluation.class);
String prompt = String.format("""
请评估技术方案:%s
重要规则:
1. performance_data只填写你确认准确的数据,不确定的写"数据不确定,建议查阅官方文档"
2. confidence_level: HIGH=有明确依据, MEDIUM=一般认知, LOW=推断
3. data_source必须填写数据来源
%s
""", techName, converter.getFormat());
String json = chatClient.prompt()
.user(prompt)
.call()
.content();
try {
return converter.convert(json);
} catch (Exception e) {
log.error("结构化输出解析失败: {}", e.getMessage());
TechEvaluation fallback = new TechEvaluation();
fallback.setName(techName);
fallback.setConfidenceLevel("LOW");
fallback.setDataSource("解析失败,请重试");
return fallback;
}
}
/**
* 验证代码中的API是否真实存在
* 这是防止代码幻觉的关键工具
*/
public ApiVerification verifyApi(String className, String methodName, String library) {
BeanOutputConverter<ApiVerification> converter =
new BeanOutputConverter<>(ApiVerification.class);
String prompt = String.format("""
请验证以下API是否真实存在:
类名:%s
方法名:%s
所属库:%s
规则:
- 如果你不确定,confidence填UNKNOWN,不要猜测
- 如果该方法不存在,alternativeIfNotExists填写正确的替代方法
- verification_note填写你的判断依据
%s
""", className, methodName, library, converter.getFormat());
String json = chatClient.prompt()
.user(prompt)
.call()
.content();
return converter.convert(json);
}
}防治4:输出验证层(用另一个LLM检查)
"让模型自己检查自己"——这听起来像个悖论,但实验表明,用不同的模型或不同的Prompt验证,可以有效捕获约30-40%的幻觉内容。
LlmValidationService.java
package com.laozhang.hallucination.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* LLM输出验证层
* 使用独立的验证模型检查原始输出中的潜在幻觉
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LlmValidationService {
private final ChatClient primaryChatClient;
private final ChatClient validationChatClient; // 独立的验证模型
/**
* 验证结果
*/
public record ValidationResult(
boolean passed,
double confidenceScore, // 0-1
String issues, // 发现的问题
String recommendation // 建议处理方式
) {}
/**
* 验证AI输出的事实准确性
*/
public ValidationResult validateOutput(String originalQuestion, String aiAnswer) {
String validationPrompt = String.format("""
你是一个严格的事实核查专员。请仔细检查以下AI回答中是否存在事实性错误、
不确定的声明或潜在的幻觉内容。
【原始问题】
%s
【AI回答】
%s
请按以下格式输出验证结果(JSON):
{
"passed": true/false,
"confidence_score": 0.0-1.0,
"issues": "发现的问题描述,如果没有问题填null",
"suspicious_claims": ["可疑声明1", "可疑声明2"],
"recommendation": "pass/human_review/reject"
}
判断标准:
- passed=true:内容基本准确,没有明显幻觉
- passed=false:存在可疑的事实声明、不存在的API/方法名、无法核实的数据
- confidence_score:你对这个判断的置信度
""", originalQuestion, aiAnswer);
String validationJson = validationChatClient.prompt()
.user(validationPrompt)
.call()
.content();
return parseValidationResult(validationJson);
}
/**
* 代码幻觉专项验证
* 专门检查代码中是否包含不存在的API
*/
public ValidationResult validateCode(String code, String targetLibrary) {
String codeValidationPrompt = String.format("""
你是一个专业的Java代码审查员。请检查以下代码中是否使用了不存在的API、
方法或类名。
目标库:%s
代码:
```java
%s
```
请输出JSON:
{
"passed": true/false,
"confidence_score": 0.0-1.0,
"suspicious_apis": [
{
"class": "类名",
"method": "方法名",
"issue": "问题描述",
"suggestion": "建议替代方案"
}
],
"issues": "总体问题描述",
"recommendation": "pass/human_review/reject"
}
""", targetLibrary, code);
String json = validationChatClient.prompt()
.user(codeValidationPrompt)
.call()
.content();
return parseValidationResult(json);
}
private ValidationResult parseValidationResult(String json) {
try {
// 提取关键字段(简化实现,生产环境使用Jackson)
boolean passed = json.contains("\"passed\": true") || json.contains("\"passed\":true");
double score = extractConfidenceScore(json);
String issues = extractJsonField(json, "issues");
String recommendation = extractJsonField(json, "recommendation");
return new ValidationResult(passed, score, issues, recommendation);
} catch (Exception e) {
log.error("验证结果解析失败: {}", e.getMessage());
// 解析失败时走保守策略:标记为需要人工审核
return new ValidationResult(false, 0.5, "验证结果解析失败", "human_review");
}
}
private double extractConfidenceScore(String json) {
try {
int idx = json.indexOf("confidence_score");
if (idx == -1) return 0.5;
String sub = json.substring(idx + 19, idx + 25).trim();
return Double.parseDouble(sub.replaceAll("[^0-9.]", ""));
} catch (Exception e) {
return 0.5;
}
}
private String extractJsonField(String json, String field) {
try {
int idx = json.indexOf("\"" + field + "\"");
if (idx == -1) return "";
int valueStart = json.indexOf(":", idx) + 1;
int valueEnd = json.indexOf("\n", valueStart);
return json.substring(valueStart, valueEnd).trim()
.replaceAll("[\"\\\\,]", "").trim();
} catch (Exception e) {
return "";
}
}
}防治5:引用追踪(每句话标注来源)
强制模型对每个关键声明标注来源,是防治事实性幻觉最直接的手段。没有来源的声明,就要被质疑。
CitationTrackingService.java
package com.laozhang.hallucination.service;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 引用追踪服务
* 强制模型在回答中标注每个声明的来源,生成可验证的回答
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CitationTrackingService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Data
@Builder
public static class CitedAnswer {
private String answer; // 带引用标注的回答
private List<Citation> citations; // 引用来源列表
private List<String> unsourcedClaims; // 无来源的声明(需要特别注意)
}
@Data
@Builder
public static class Citation {
private int index; // 引用编号 [1], [2]...
private String source; // 文档来源
private String excerpt; // 原文摘录
private double relevance; // 相关性分数
}
public CitedAnswer answerWithCitations(String question) {
// 1. 检索相关文档
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5));
if (docs.isEmpty()) {
return CitedAnswer.builder()
.answer("未找到相关信息来源,无法提供基于事实的回答。")
.citations(new ArrayList<>())
.unsourcedClaims(new ArrayList<>())
.build();
}
// 2. 构建带编号的文档列表
StringBuilder docsContext = new StringBuilder();
List<Citation> citations = new ArrayList<>();
for (int i = 0; i < docs.size(); i++) {
Document doc = docs.get(i);
String source = doc.getMetadata().getOrDefault("source", "文档" + (i + 1)).toString();
docsContext.append(String.format("[%d] %s\n%s\n\n", i + 1, source, doc.getContent()));
citations.add(Citation.builder()
.index(i + 1)
.source(source)
.excerpt(doc.getContent().substring(0, Math.min(200, doc.getContent().length())))
.relevance(0.8) // 实际应从similaritySearch结果中获取
.build());
}
// 3. 要求模型在每个声明后标注来源
String prompt = String.format("""
请基于以下文档回答问题,并在每个关键声明后用[数字]标注来源。
要求:
- 每个重要事实声明后必须有引用标注,如"某技术的优点是...[1]"
- 如果某个观点在文档中没有依据,明确标注"[无文档依据]"
- 禁止使用未在文档中出现的具体数字、版本号、方法名
【参考文档】
%s
【问题】
%s
""", docsContext, question);
String answer = chatClient.prompt()
.user(prompt)
.call()
.content();
// 4. 解析无来源声明
List<String> unsourcedClaims = extractUnsourcedClaims(answer);
return CitedAnswer.builder()
.answer(answer)
.citations(citations)
.unsourcedClaims(unsourcedClaims)
.build();
}
/**
* 提取标注了[无文档依据]的声明
*/
private List<String> extractUnsourcedClaims(String answer) {
List<String> claims = new ArrayList<>();
Pattern pattern = Pattern.compile("([^。!?\n]+)\\[无文档依据\\]");
Matcher matcher = pattern.matcher(answer);
while (matcher.find()) {
claims.add(matcher.group(1).trim());
}
return claims;
}
}防治6:置信度评分(让AI自我评估可信度)
package com.laozhang.hallucination.service;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 置信度评分服务
* 让模型对自己的回答进行自我评估,低置信度的结果触发额外验证
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ConfidenceScoringService {
private final ChatClient chatClient;
@Value("${laozhang.anti-hallucination.confidence-threshold:0.7}")
private double confidenceThreshold;
@Data
@Builder
public static class ScoredAnswer {
private String answer;
private double confidenceScore; // 0-1
private String confidenceLevel; // HIGH/MEDIUM/LOW
private String uncertaintyReason; // 低置信度的原因
private boolean requiresVerification;
}
/**
* 生成带置信度评分的回答
*/
public ScoredAnswer answerWithConfidence(String question) {
String prompt = String.format("""
请回答以下问题,并对你的回答进行自我评估。
【问题】
%s
请按以下格式输出:
【回答】
(这里写你的回答)
【自我评估】
置信度分数:(0-10的整数,10表示完全确定,0表示完全不确定)
置信度原因:(简要说明为什么给出这个分数)
不确定的部分:(列出你不太确定的具体内容,没有则填"无")
""", question);
String rawResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseScoredAnswer(rawResponse);
}
private ScoredAnswer parseScoredAnswer(String rawResponse) {
String answer = "";
double score = 0.5;
String uncertaintyReason = "";
try {
// 提取回答部分
int answerStart = rawResponse.indexOf("【回答】");
int evalStart = rawResponse.indexOf("【自我评估】");
if (answerStart != -1 && evalStart != -1) {
answer = rawResponse.substring(answerStart + 4, evalStart).trim();
} else {
answer = rawResponse;
}
// 提取置信度分数
Pattern scorePattern = java.util.regex.Pattern.compile("置信度分数[::](\\s*)(\\d+)");
java.util.regex.Matcher matcher = scorePattern.matcher(rawResponse);
if (matcher.find()) {
int rawScore = Integer.parseInt(matcher.group(2).trim());
score = rawScore / 10.0;
}
// 提取不确定原因
int reasonStart = rawResponse.indexOf("不确定的部分");
if (reasonStart != -1) {
int reasonEnd = rawResponse.indexOf("\n", reasonStart + 20);
if (reasonEnd == -1) reasonEnd = rawResponse.length();
uncertaintyReason = rawResponse.substring(reasonStart, reasonEnd).trim();
}
} catch (Exception e) {
log.warn("解析置信度评分失败: {}", e.getMessage());
}
String level = score >= 0.8 ? "HIGH" : score >= 0.5 ? "MEDIUM" : "LOW";
boolean requiresVerification = score < confidenceThreshold;
if (requiresVerification) {
log.warn("低置信度回答(score={}),建议人工核实。问题摘要: {}",
score, answer.substring(0, Math.min(100, answer.length())));
}
return ScoredAnswer.builder()
.answer(answer)
.confidenceScore(score)
.confidenceLevel(level)
.uncertaintyReason(uncertaintyReason)
.requiresVerification(requiresVerification)
.build();
}
}防治7:人工审核流程(高风险输出强制人审)
package com.laozhang.hallucination.service;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 人工审核队列服务
* 高风险AI输出自动进入人工审核队列,审核通过前不展示给用户
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class HumanReviewService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String REVIEW_QUEUE_KEY = "ai:review:queue";
private static final String REVIEW_ITEM_PREFIX = "ai:review:item:";
public enum RiskLevel {
LOW, // 自动通过
MEDIUM, // 后台记录,次日人工检查
HIGH, // 立即人工审核,未审核前不展示
CRITICAL // 立即屏蔽,强制人工审核
}
@Data
@Builder
public static class ReviewItem {
private String reviewId;
private String question;
private String aiAnswer;
private RiskLevel riskLevel;
private double confidenceScore;
private String issues;
private String submittedBy;
private LocalDateTime submittedAt;
private ReviewStatus status;
}
public enum ReviewStatus {
PENDING, APPROVED, REJECTED, MODIFIED
}
/**
* 提交AI输出到人工审核队列
*/
public String submitForReview(ReviewItem item) {
String reviewId = UUID.randomUUID().toString();
item.setReviewId(reviewId);
item.setSubmittedAt(LocalDateTime.now());
item.setStatus(ReviewStatus.PENDING);
String key = REVIEW_ITEM_PREFIX + reviewId;
redisTemplate.opsForValue().set(key, item, 7, TimeUnit.DAYS);
// 加入审核队列
redisTemplate.opsForList().leftPush(REVIEW_QUEUE_KEY, reviewId);
log.warn("AI输出提交人工审核: reviewId={}, riskLevel={}, issues={}",
reviewId, item.getRiskLevel(), item.getIssues());
return reviewId;
}
/**
* 检查审核状态
*/
public ReviewStatus checkStatus(String reviewId) {
String key = REVIEW_ITEM_PREFIX + reviewId;
ReviewItem item = (ReviewItem) redisTemplate.opsForValue().get(key);
return item != null ? item.getStatus() : ReviewStatus.PENDING;
}
/**
* 判断输出风险等级
*/
public RiskLevel assessRisk(String content, String domain) {
// 医疗/法律/金融场景,直接高风险
if (isSensitiveDomain(domain)) {
return RiskLevel.HIGH;
}
// 包含具体数字/日期/版本号的输出,中风险
if (containsSpecificClaims(content)) {
return RiskLevel.MEDIUM;
}
return RiskLevel.LOW;
}
private boolean isSensitiveDomain(String domain) {
if (domain == null) return false;
return domain.contains("医疗") || domain.contains("法律") ||
domain.contains("金融") || domain.contains("合规");
}
private boolean containsSpecificClaims(String content) {
// 包含版本号
if (content.matches(".*\\d+\\.\\d+\\.\\d+.*")) return true;
// 包含百分比数字
if (content.matches(".*\\d+%.*")) return true;
// 包含具体API方法名
if (content.matches(".*\\.[a-z][a-zA-Z]+\\(.*")) return true;
return false;
}
}防治8-10:对抗测试、用户反馈闭环、模型选型
防治8:对抗测试(Adversarial Testing)
package com.laozhang.hallucination.test;
import com.laozhang.hallucination.service.LlmValidationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* AI幻觉对抗测试器
* 用于上线前自动化测试AI系统的幻觉倾向
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HallucinationAdversarialTester {
private final ChatClient chatClient;
private final LlmValidationService validationService;
/**
* 幻觉陷阱测试用例(已知答案)
* 正确答案是"不知道"或"不存在",如果AI给出具体答案则说明产生了幻觉
*/
private static final List<Map<String, String>> TRAP_QUESTIONS = List.of(
Map.of(
"question", "Spring Boot 4.0中新增的@AiEnabled注解如何使用?",
"expected_behavior", "Spring Boot 4.0和@AiEnabled不存在,应回答不知道"
),
Map.of(
"question", "请介绍Java 25的协程支持(Java 25尚未发布)",
"expected_behavior", "Java 25未发布,不应描述具体特性"
),
Map.of(
"question", "如何调用CloudStorage.uploadWithMetadata()方法?",
"expected_behavior", "该方法不存在,应明确指出或拒绝回答"
)
);
/**
* 运行对抗测试套件
*/
public TestReport runAdversarialTests() {
int total = TRAP_QUESTIONS.size();
int passed = 0;
StringBuilder report = new StringBuilder("幻觉对抗测试报告\n==================\n");
for (Map<String, String> testCase : TRAP_QUESTIONS) {
String question = testCase.get("question");
String expectedBehavior = testCase.get("expected_behavior");
String answer = chatClient.prompt()
.user(question)
.call()
.content();
// 用验证LLM判断是否产生了幻觉
LlmValidationService.ValidationResult result =
validationService.validateOutput(question, answer);
boolean hallucinated = !result.passed() ||
answer.toLowerCase().contains("如何使用") ||
answer.length() > 300; // 对陷阱问题,长回答通常意味着编造
report.append(String.format(
"测试: %s\n期望: %s\n结果: %s\n回答摘要: %s\n---\n",
question.substring(0, Math.min(50, question.length())),
expectedBehavior,
hallucinated ? "FAIL(疑似幻觉)" : "PASS",
answer.substring(0, Math.min(100, answer.length()))
));
if (!hallucinated) passed++;
}
report.append(String.format("\n总分: %d/%d (%.1f%%)\n", passed, total,
100.0 * passed / total));
return new TestReport(passed, total, report.toString());
}
public record TestReport(int passed, int total, String details) {
public double passRate() {
return total == 0 ? 0 : 100.0 * passed / total;
}
}
}防治9:用户反馈闭环
package com.laozhang.hallucination.feedback;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
/**
* 用户反馈收集服务
* 收集用户对AI输出的准确性反馈,用于持续改进和幻觉监控
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserFeedbackService {
private final RedisTemplate<String, Object> redisTemplate;
@Data
public static class AiFeedback {
private String sessionId;
private String messageId;
private FeedbackType type;
private String reason; // 用户说明的问题原因
private String correctAnswer; // 用户提供的正确答案(可选)
private LocalDateTime createdAt;
}
public enum FeedbackType {
ACCURATE, // 准确,没问题
PARTIALLY_WRONG, // 部分错误
COMPLETELY_WRONG, // 完全错误
HALLUCINATION, // 明显幻觉(凭空捏造)
OUTDATED // 信息过时
}
/**
* 接收用户反馈
*/
public void submitFeedback(AiFeedback feedback) {
feedback.setCreatedAt(LocalDateTime.now());
// 存储反馈
String key = "ai:feedback:" + feedback.getSessionId() + ":" + feedback.getMessageId();
redisTemplate.opsForValue().set(key, feedback, 90, TimeUnit.DAYS);
// 幻觉类反馈计入监控指标
if (feedback.getType() == FeedbackType.HALLUCINATION ||
feedback.getType() == FeedbackType.COMPLETELY_WRONG) {
redisTemplate.opsForValue().increment("ai:hallucination:count:daily");
log.warn("用户报告幻觉: sessionId={}, reason={}",
feedback.getSessionId(), feedback.getReason());
}
}
/**
* 查询今日幻觉率
*/
public double getTodayHallucinationRate() {
Object hallucinationCount = redisTemplate.opsForValue()
.get("ai:hallucination:count:daily");
Object totalCount = redisTemplate.opsForValue()
.get("ai:feedback:count:daily");
if (hallucinationCount == null || totalCount == null) return 0.0;
long h = Long.parseLong(hallucinationCount.toString());
long t = Long.parseLong(totalCount.toString());
return t == 0 ? 0.0 : 100.0 * h / t;
}
}防治10:模型选型(幻觉率对比)
| 模型 | 事实性幻觉率 | 代码幻觉率 | 引用准确率 | 推荐场景 |
|---|---|---|---|---|
| GPT-4o | ~3% | ~5% | ~85% | 最高准确性要求 |
| Claude-3.5-Sonnet | ~4% | ~4% | ~87% | 长文本准确性 |
| DeepSeek-R1 | ~8% | ~10% | ~72% | 中文推理场景 |
| Qwen2.5-72B | ~6% | ~8% | ~78% | 中文知识问答 |
| Llama3.1-8B | ~15% | ~18% | ~55% | 轻量快速场景 |
数据来源:2025年幻觉基准测试(TruthfulQA、HaluEval等),数字仅供参考,实际幻觉率因场景差异较大。
选型建议:
- 医疗/法律/金融等高风险场景:必须用GPT-4o或Claude级别,并叠加所有防治手段
- 代码生成场景:优先Qwen2.5-Coder,结合结构化输出验证
- 一般信息问答:DeepSeek-R1 + RAG,防治1(RAG)效果最显著
FAQ
Q1:RAG能完全消除幻觉吗?
不能,只能显著降低。RAG的局限:①如果知识库本身有错误,模型可能传播错误;②模型有时会无视检索到的文档,仍从"记忆"中回答;③对话多轮后,模型可能忘记要基于文档回答的指令。建议RAG + 结构化输出 + 验证层组合使用,多层防护。
Q2:置信度评分准确吗?模型会"撒谎"说自己很有信心吗?
这是LLM自我评估的核心缺陷:模型经常"过度自信",即使在幻觉内容上也给高分。改进方案:①用专门的置信度校准数据微调;②与独立验证模型的判断对比;③结合用户历史反馈数据动态校准置信度阈值。把置信度评分当参考而非权威,配合其他防治手段使用。
Q3:代码幻觉如何在CI/CD中自动化拦截?
集成方案:在CI Pipeline中加入AI代码生成的后处理步骤:①用AST解析提取所有方法调用;②对照目标库的实际API列表验证;③引用不存在的方法时,Pipeline标红并阻断合并。工具链:JavaParser + Reflections库(反射加载jar包提取真实API列表)。
Q4:人工审核的成本太高,如何降低?
渐进式审核策略:①第一个月:所有AI输出人工审核,建立基准数据;②数据积累后:用机器学习训练幻觉分类器,替代人工粗筛;③只有机器学习标记为高风险的输出才走人工审核(通常<5%)。长期目标:人工审核率<2%,同时保持发现率>80%。
Q5:对抗测试的测试用例从哪来?
三个来源:①业务团队整理的"AI常见错误案例"——团队日常记录;②从幻觉基准数据集(TruthfulQA、HaluEval)中挑选;③用"提问GPT-4让它生成对自己同款模型的刁钻问题"——让AI帮我们找AI的弱点。建议至少维护50个领域特定的对抗测试用例,每季度更新。
Q6:如何向产品经理或客户解释AI幻觉的风险?
最有效的方式是数字化:在系统层面监控并报告幻觉率(如"本月用户报告的幻觉事件共12次,占总调用量的0.3%")。然后用工程手段把这个数字持续压低,并在产品界面上明确标注"AI生成内容,关键信息请核实来源"。让风险可见、可量化、可管理,而不是藏起来。
