第2479篇:AI系统的Debug工程——当LLM输出不符合预期时如何高效定位
2026/4/30大约 6 分钟
第2479篇:AI系统的Debug工程——当LLM输出不符合预期时如何高效定位
适读人群:AI工程师、后端工程师 | 阅读时长:约16分钟 | 核心价值:建立AI系统的Debug方法论,从LLM"幻觉"、Prompt问题到RAG检索缺陷的系统性定位方法
调试普通代码有标准流程:看日志、加断点、打印变量,一步步追溯到问题所在。
调试AI系统就不一样了。LLM的输出是概率性的,同样的输入可能不同输出,而且LLM不会报错,它只会给你一个"听起来合理"的答案,哪怕这个答案是错的。
我碰到过的最让人崩溃的情况:一个RAG系统,某些问题的回答明显不对,但不知道问题出在哪——是Prompt写得不好?还是检索出来的文档不对?还是LLM没有按Prompt的要求来?每次排查都要花大量时间,找到原因全靠运气。
AI系统的问题层次
排查前先建立一个框架,把AI系统的问题分层:
Debug工具链
1. LLM调用追踪
首先要能看到每次LLM调用的完整输入输出:
@Configuration
public class LLMObservabilityConfig {
@Bean
public ChatClient chatClientWithTracing(
OpenAiChatClient baseClient,
LLMCallTracer tracer) {
return new TracingChatClient(baseClient, tracer);
}
}
@Component
public class TracingChatClient implements ChatClient {
private final ChatClient delegate;
private final LLMCallTracer tracer;
@Override
public ChatResponse call(Prompt prompt) {
String traceId = generateTraceId();
// 记录完整输入
LLMCallTrace trace = LLMCallTrace.builder()
.traceId(traceId)
.startTime(Instant.now())
.messages(prompt.getInstructions())
.modelOptions(extractModelOptions(prompt))
.build();
try {
ChatResponse response = delegate.call(prompt);
// 记录完整输出
trace.setResponse(response.getResult().getOutput().getContent());
trace.setCompletionTokens(response.getMetadata().getUsage().getCompletionTokens());
trace.setPromptTokens(response.getMetadata().getUsage().getPromptTokens());
trace.setEndTime(Instant.now());
trace.setSuccess(true);
tracer.record(trace);
return response;
} catch (Exception e) {
trace.setError(e.getMessage());
trace.setEndTime(Instant.now());
trace.setSuccess(false);
tracer.record(trace);
throw e;
}
}
}2. Prompt评估器
定期评估Prompt的质量,发现系统性问题:
@Service
public class PromptEvaluationService {
private final ChatClient chatClient;
private final LLMCallTraceRepository traceRepository;
/**
* 对最近一段时间的LLM调用做质量评估
* 发现Prompt问题的规律
*/
public PromptQualityReport evaluateRecentCalls(Duration period) {
List<LLMCallTrace> recentCalls = traceRepository.findRecent(period);
List<SingleCallEvaluation> evaluations = recentCalls.parallelStream()
.map(this::evaluateSingleCall)
.collect(toList());
// 分析失败模式
Map<FailurePattern, Long> failurePatterns = evaluations.stream()
.filter(e -> !e.isGood())
.collect(groupingBy(SingleCallEvaluation::getFailurePattern, counting()));
return PromptQualityReport.builder()
.totalCalls(recentCalls.size())
.goodResponseRate(computeGoodRate(evaluations))
.failurePatterns(failurePatterns)
.worstCases(getWorstCases(evaluations, 10))
.improvementSuggestions(generateImprovementSuggestions(failurePatterns))
.build();
}
private SingleCallEvaluation evaluateSingleCall(LLMCallTrace trace) {
// 用另一个LLM来评估这个LLM调用的质量(LLM-as-Judge)
String evaluationPrompt = """
评估以下LLM对话的质量:
System Prompt:
%s
User Message:
%s
LLM回答:
%s
评估维度:
1. 是否遵循了System Prompt的指令?(评分1-5)
2. 回答是否准确、完整?(评分1-5)
3. 格式是否符合要求?(评分1-5)
4. 是否存在幻觉(编造信息)?(是/否)
5. 主要问题是什么?(如果有)
返回JSON。
""".formatted(
extractSystemPrompt(trace),
extractUserMessage(trace),
trace.getResponse()
);
ChatResponse evalResponse = chatClient.call(new Prompt(
new UserMessage(evaluationPrompt),
OpenAiChatOptions.builder()
.withModel("gpt-4o-mini") // 用便宜的模型做评估
.withResponseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT))
.build()
));
return parseEvaluation(evalResponse.getResult().getOutput().getContent(), trace);
}
}3. RAG检索质量分析
RAG系统的检索质量是常见的问题来源:
@Service
public class RAGQualityAnalyzer {
private final VectorStore vectorStore;
private final EmbeddingClient embeddingClient;
private final ChatClient chatClient;
/**
* 分析RAG检索的质量问题
* 用于回答:"检索出来的文档和问题是否真的相关?"
*/
public RAGQualityReport analyzeRetrieval(
String userQuery,
List<Document> retrievedDocs) {
// 计算每个检索文档和问题的相关度
List<DocumentRelevanceScore> relevanceScores = new ArrayList<>();
for (Document doc : retrievedDocs) {
double semanticSimilarity = computeSemanticSimilarity(userQuery, doc.getContent());
double llmRelevanceScore = evaluateRelevanceWithLLM(userQuery, doc.getContent());
relevanceScores.add(DocumentRelevanceScore.builder()
.documentId(doc.getId())
.documentSnippet(doc.getContent().substring(0, Math.min(200, doc.getContent().length())))
.semanticSimilarity(semanticSimilarity)
.llmRelevanceScore(llmRelevanceScore)
.isRelevant(llmRelevanceScore > 0.6)
.build());
}
long relevantCount = relevanceScores.stream().filter(DocumentRelevanceScore::isRelevant).count();
double precision = (double) relevantCount / retrievedDocs.size();
// 检测是否遗漏了重要信息
boolean likelyMissingContext = precision < 0.5; // 超过一半的检索文档不相关,说明检索有问题
return RAGQualityReport.builder()
.query(userQuery)
.retrievedCount(retrievedDocs.size())
.relevantCount((int) relevantCount)
.precision(precision)
.relevanceScores(relevanceScores)
.likelyMissingContext(likelyMissingContext)
.build();
}
private double evaluateRelevanceWithLLM(String query, String docContent) {
String prompt = """
评估以下文档片段和用户问题的相关程度:
用户问题: %s
文档片段:
%s
打分:0.0(完全不相关)到1.0(高度相关)。只返回数字。
""".formatted(query, docContent.substring(0, Math.min(500, docContent.length())));
String score = chatClient.call(new Prompt(new UserMessage(prompt)))
.getResult().getOutput().getContent().trim();
try {
return Double.parseDouble(score);
} catch (NumberFormatException e) {
return 0.5; // 无法解析时给中间值
}
}
}4. 幻觉检测器
@Service
public class HallucinationDetector {
private final ChatClient chatClient;
/**
* 检测LLM回答中是否包含幻觉
* 通过对比回答和参考文档/知识库来判断
*/
public HallucinationReport detect(
String userQuery,
String llmAnswer,
List<Document> referenceDocuments) {
// 提取LLM回答中的关键声明
List<String> claims = extractClaims(llmAnswer);
// 对每个声明进行核实
List<ClaimVerification> verifications = claims.stream()
.map(claim -> verifyClaim(claim, referenceDocuments))
.collect(toList());
long supportedClaims = verifications.stream()
.filter(v -> v.getStatus() == ClaimStatus.SUPPORTED)
.count();
long unsupportedClaims = verifications.stream()
.filter(v -> v.getStatus() == ClaimStatus.UNSUPPORTED)
.count();
long contradictedClaims = verifications.stream()
.filter(v -> v.getStatus() == ClaimStatus.CONTRADICTED)
.count();
boolean likelyHallucination = contradictedClaims > 0 ||
(unsupportedClaims > 0 && unsupportedClaims > supportedClaims);
return HallucinationReport.builder()
.answer(llmAnswer)
.claims(claims)
.verifications(verifications)
.likelyHallucination(likelyHallucination)
.confidence(computeConfidence(verifications))
.build();
}
private ClaimVerification verifyClaim(String claim, List<Document> references) {
String refContent = references.stream()
.map(Document::getContent)
.collect(joining("\n\n---\n\n"));
String verificationPrompt = """
根据以下参考资料,判断这个声明是否有依据:
声明: %s
参考资料:
%s
判断结果(只返回以下之一):
- SUPPORTED: 参考资料中有证据支持
- UNSUPPORTED: 参考资料中找不到相关信息
- CONTRADICTED: 参考资料中有相反的信息
""".formatted(claim, refContent.substring(0, Math.min(3000, refContent.length())));
String result = chatClient.call(new Prompt(new UserMessage(verificationPrompt)))
.getResult().getOutput().getContent().trim();
ClaimStatus status;
try {
status = ClaimStatus.valueOf(result);
} catch (IllegalArgumentException e) {
status = ClaimStatus.UNSUPPORTED; // 无法解析时保守处理
}
return ClaimVerification.of(claim, status);
}
}Debug的系统性流程
当AI系统输出不符合预期时,按这个顺序排查:
第一步:检查输入。把实际发给LLM的完整Prompt(包括System Message和所有上下文)打印出来,用人眼看一遍。很多时候问题就在这里——Prompt写了但没生效,或者RAG检索的文档是错的。
第二步:检查RAG检索质量。如果是RAG系统,检查检索出来的文档是否和问题相关。不相关的文档混入上下文是LLM输出质量差的最常见原因。
第三步:隔离LLM。把检索出来的正确文档直接粘贴到Claude或ChatGPT里,手工问同样的问题。如果手工问也是错的,说明Prompt本身有问题;如果手工问是对的,说明系统集成出了问题。
第四步:检查是否有幻觉。对LLM的关键声明进行核实,看有没有编造的信息。
