AI 应用的可解释性——当用户问"为什么"时怎么回答
AI 应用的可解释性——当用户问"为什么"时怎么回答
适读人群:AI 应用工程师 / 产品经理 | 阅读时长:约15分钟 | 核心价值:AI 解释性的工程实践,从引用溯源到决策路径,有代码有产品设计
有次我做演示,客户问了一句话,把我问住了。
我们做的是一个合同条款审查工具,AI 扫描合同后,标红了一条"付款条款存在风险"。客户问:"它为什么说这里有风险?它参考的是什么标准?"
我当时的答案是……"模型认为……嗯……它分析了整个合同……"
我自己都觉得这个回答在胡说。我不知道模型为什么这么判断,没有任何解释信息。
那次之后,我把 AI 解释性列为了项目的一级功能,不是可选的,是必须做的。
解释性不只是用户体验问题,它是产品可信任度的基础。没有解释性的 AI 应用,用户要么盲目信任(危险),要么根本不用(没价值)。
解释性的三个维度
工程上的 AI 解释性,我把它分成三个维度:
维度1:信息溯源(这个结论从哪来)
"AI 说合同第 5 条有风险"——参考了哪条法规?根据了哪个先例?
维度2:置信度(AI 有多确定)
"AI 的这个判断是非常确定的,还是只是猜测?"
维度3:推理路径(AI 怎么想的)
"AI 看了什么,分析了什么,怎么得出这个结论的?"
不是所有场景都需要三个维度,但都要考虑一下该给哪些。
维度1:信息溯源——RAG 的引用设计
RAG 场景里,AI 的回答基于检索到的文档。做引用溯源,就是告诉用户"这段回答基于这几篇文档"。
后端:在 Prompt 里要求模型标注引用
@Service
public class CitedRagService {
@Autowired
private VectorStore vectorStore;
@Autowired
private ChatClient chatClient;
public CitedResponse queryWithCitations(String question, String userId) {
// 检索相关文档
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5));
if (docs.isEmpty()) {
return CitedResponse.noContext("未找到相关文档,以下回答基于模型自身知识");
}
// 给文档编号,让模型在回答中引用编号
StringBuilder contextBuilder = new StringBuilder();
for (int i = 0; i < docs.size(); i++) {
Document doc = docs.get(i);
contextBuilder.append(String.format(
"[文档%d] 来源:%s\n内容:%s\n\n",
i + 1,
doc.getMetadata().get("source"),
doc.getContent()
));
}
String prompt = String.format("""
请根据以下参考文档回答问题。在回答中,用 [文档N] 的格式标注信息来源。
如果某个信息不在文档中,请明确说明是你自己的判断,不要伪装成来自文档。
如果文档中信息不足以回答问题,直接说明。
参考文档:
%s
问题:%s
请回答(在适当位置插入引用标注):
""", contextBuilder, question);
String rawAnswer = chatClient.prompt()
.user(prompt)
.call()
.content();
// 解析引用标注
ParsedAnswer parsed = parseCitations(rawAnswer, docs);
return CitedResponse.builder()
.answer(parsed.getCleanAnswer())
.citations(parsed.getCitations())
.sourceDocuments(docs.stream()
.map(this::toDocumentSummary)
.collect(Collectors.toList()))
.build();
}
/**
* 解析模型在回答中插入的 [文档N] 引用
*/
private ParsedAnswer parseCitations(String rawAnswer, List<Document> docs) {
List<Citation> citations = new ArrayList<>();
// 找出所有 [文档N] 引用
Pattern pattern = Pattern.compile("\\[文档(\\d+)\\]");
Matcher matcher = pattern.matcher(rawAnswer);
while (matcher.find()) {
int docIndex = Integer.parseInt(matcher.group(1)) - 1;
if (docIndex >= 0 && docIndex < docs.size()) {
Document doc = docs.get(docIndex);
// 找到这个引用在文本中的位置(用于高亮)
int position = matcher.start();
citations.add(Citation.builder()
.marker(matcher.group(0))
.position(position)
.documentId((String) doc.getMetadata().get("id"))
.documentTitle((String) doc.getMetadata().get("title"))
.documentSource((String) doc.getMetadata().get("source"))
.relevantPage((String) doc.getMetadata().get("page"))
.snippet(doc.getContent().substring(0, Math.min(100, doc.getContent().length())))
.build());
}
}
return ParsedAnswer.builder()
.rawAnswer(rawAnswer)
.cleanAnswer(rawAnswer) // 保留引用标注,前端会渲染成链接
.citations(citations)
.build();
}
private DocumentSummary toDocumentSummary(Document doc) {
return DocumentSummary.builder()
.id((String) doc.getMetadata().get("id"))
.title((String) doc.getMetadata().get("title"))
.source((String) doc.getMetadata().get("source"))
.similarity((Double) doc.getMetadata().get("distance"))
.build();
}
}前端:渲染引用链接
function renderAnswerWithCitations(answer, citations) {
let rendered = answer;
// 把 [文档N] 替换成可点击的引用标注
citations.forEach((citation, index) => {
const marker = `[文档${index + 1}]`;
const citationHtml = `
<span class="citation-marker"
data-citation-id="${citation.documentId}"
onclick="showCitationDetail('${citation.documentId}')">
<sup>[${index + 1}]</sup>
</span>
`;
rendered = rendered.replaceAll(marker, citationHtml);
});
return rendered;
}
function showCitationDetail(documentId) {
// 在侧边栏显示原始文档片段
const citation = citations.find(c => c.documentId === documentId);
sidePanel.show({
title: citation.documentTitle,
source: citation.documentSource,
snippet: citation.snippet,
link: citation.documentUrl
});
}维度2:置信度展示
不是每个 AI 回答都同样可靠。让用户知道哪些回答更确定,哪些只是推测,是负责任的做法。
@Service
public class ConfidenceEstimationService {
@Autowired
private ChatClient chatClient;
/**
* 对 AI 的回答进行置信度评估
*
* 方案1(推荐):让模型自己评估置信度
* 这比用模型的 logprobs 更直观,也更容易解释给用户
*/
public ConfidenceScore estimateConfidence(String question,
String answer,
List<Document> usedDocuments) {
String evaluationPrompt = String.format("""
请评估以下回答的可靠程度。
问题:%s
回答:%s
参考文档数量:%d 篇
文档相关度(0-1):%s
请评估:
1. 回答中有多少内容有文档支撑(0-100%%)
2. 文档内容与问题的相关程度(高/中/低)
3. 回答中有没有推断性内容(有文档依据的推断 vs 纯粹猜测)
4. 综合置信度:高(>80%%有据可查)/ 中(50-80%%有据)/ 低(<50%%有据)
请用 JSON 格式回答:
{
"documentCoverage": 数字,
"documentRelevance": "高/中/低",
"hasSpeculation": true/false,
"speculationDescription": "描述推断部分",
"overallConfidence": "高/中/低",
"confidenceExplanation": "一句话解释"
}
""",
question, answer, usedDocuments.size(),
formatDocumentRelevances(usedDocuments)
);
String evaluationJson = chatClient.prompt()
.user(evaluationPrompt)
.call()
.content();
return parseConfidenceJson(evaluationJson);
}
/**
* 方案2:基于规则的简单置信度估算
* 速度快,不需要额外调用 AI
* 适合对延迟敏感的场景
*/
public ConfidenceScore estimateConfidenceByRules(String answer,
List<Document> docs,
double maxSimilarity) {
ConfidenceLevel level;
String explanation;
if (docs.isEmpty()) {
level = ConfidenceLevel.LOW;
explanation = "未找到相关参考文档,回答基于模型自身知识";
} else if (maxSimilarity > 0.85 && docs.size() >= 3) {
level = ConfidenceLevel.HIGH;
explanation = "找到多篇高度相关文档,回答有充分依据";
} else if (maxSimilarity > 0.7 || docs.size() >= 2) {
level = ConfidenceLevel.MEDIUM;
explanation = "找到相关文档,但相关度或数量有限";
} else {
level = ConfidenceLevel.LOW;
explanation = "参考文档相关度较低,建议以文档原文为准";
}
// 额外检查:回答中的不确定性语言
if (answer.contains("可能") || answer.contains("或许") ||
answer.contains("我认为") || answer.contains("不确定")) {
if (level == ConfidenceLevel.HIGH) {
level = ConfidenceLevel.MEDIUM;
explanation += "(回答本身包含不确定性表述)";
}
}
return ConfidenceScore.builder()
.level(level)
.explanation(explanation)
.documentCount(docs.size())
.maxSimilarity(maxSimilarity)
.build();
}
}前端置信度展示设计原则:
不要用百分比("73% 置信度"会让用户误解为精确数字),用语义标签:
- 高置信度:绿色 "有据可查"
- 中置信度:橙色 "仅供参考"
- 低置信度:红色 "需要核实"
维度3:推理路径记录
对于复杂决策(比如合同风险审查),用户需要看到 AI 的推理过程,而不只是结论。
@Service
public class ReasoningTraceService {
@Autowired
private ChatClient chatClient;
/**
* 使用 Chain-of-Thought Prompting 让模型展示推理过程
*/
public ReasoningResponse analyzeWithTrace(String document, String analysisTask) {
// 让模型在回答前先"思考"
String prompt = String.format("""
请分析以下文档并完成任务。
重要:在给出结论之前,请先展示你的分析过程,格式如下:
【分析过程】
1. 我首先关注了...因为...
2. 在第X条中,我注意到...这意味着...
3. 对比第Y条和第Z条...
4. 根据以上分析,我的判断是...
【结论】
...
【风险等级】高/中/低
---
文档内容:
%s
分析任务:%s
""", document, analysisTask);
String rawResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
// 解析结构化响应
ReasoningResponse parsed = parseReasoningResponse(rawResponse);
// 保存推理记录,用于审计
reasoningLogRepository.save(ReasoningLog.builder()
.taskId(UUID.randomUUID().toString())
.inputDocument(document)
.task(analysisTask)
.rawResponse(rawResponse)
.steps(parsed.getReasoningSteps())
.conclusion(parsed.getConclusion())
.timestamp(Instant.now())
.build());
return parsed;
}
/**
* 解析 CoT 格式的响应
*/
private ReasoningResponse parseReasoningResponse(String rawResponse) {
List<String> steps = new ArrayList<>();
String conclusion = "";
String riskLevel = "";
String[] sections = rawResponse.split("【");
for (String section : sections) {
if (section.startsWith("分析过程】")) {
String stepsText = section.substring("分析过程】".length()).trim();
// 解析编号步骤
steps = Arrays.asList(stepsText.split("\n"))
.stream()
.filter(line -> line.matches("\\d+\\..*"))
.collect(Collectors.toList());
} else if (section.startsWith("结论】")) {
conclusion = section.substring("结论】".length()).trim();
} else if (section.startsWith("风险等级】")) {
riskLevel = section.substring("风险等级】".length()).trim();
}
}
return ReasoningResponse.builder()
.reasoningSteps(steps)
.conclusion(conclusion)
.riskLevel(riskLevel)
.rawResponse(rawResponse)
.build();
}
}那次演示之后的改进
回到开头的合同审查工具。改进之后,当 AI 标红"付款条款存在风险"时,用户可以点击查看:
风险说明:
AI 认为第 8 条"付款期限不超过 90 天"存在潜在风险
分析依据:
[文档1] 《中华人民共和国中小企业促进法》第 36 条:大型企业不得超过 60 天
[文档2] 公司过往 3 份合同中,标准付款期限为 30-45 天
推理过程:
1. 第 8 条规定付款期限"不超过 90 天"
2. 查询参考资料:法律规定大型企业付款不超过 60 天
3. 该合同甲方为大型企业,90 天条款可能超出法规上限
4. 建议核实甲方企业规模,必要时修改为"不超过 60 天"
置信度:中(仅供参考)
原因:需要核实甲方企业是否符合"大型企业"的法定标准
[查看原始文档] [查看相关法规]客户看到这个界面,第一反应是:"这才是我想要的。"
他可以验证 AI 的依据,可以判断 AI 的推理是否合理,可以决定是否采纳建议。AI 从一个黑盒变成了一个可以信任的助手。
实践中的几点注意
1. 解释本身要简洁
解释不是越多越好。冗长的解释会让用户失去耐心。重点是:最关键的依据是什么,一句话能说清楚就别写三段。
2. 不确定时老实说,不要强行解释
如果 AI 没有找到好的依据,但给出了一个听起来有道理的解释,这比没有解释更危险。要让模型在不确定时明确说"我不确定",而不是生成一个听起来有依据的假解释。
3. 解释要和用户能力匹配
给法律专业人士的解释,可以引用具体法条。给普通用户的解释,要用他们能理解的语言。不要用技术性的模型评分来"解释",用户不知道 0.87 的相似度意味着什么。
4. 保存推理记录用于审计
特别是高风险决策(医疗、法律、金融),AI 的推理记录必须持久化保存。不只是产品需要,很多场景是合规要求。
可解释性不是选配,它是 AI 应用走向生产环境的门票。没有解释的 AI,用户没有理由信任它,也没有能力质疑它。这不是好的产品,也不是好的工程。
