第2306篇:反思Agent模式——让AI在给出答案前自我检查
第2306篇:反思Agent模式——让AI在给出答案前自我检查
适读人群:AI应用开发工程师、关注AI输出质量的架构师 | 阅读时长:约16分钟 | 核心价值:理解Reflexion/Self-Critique模式的工程原理,在生产环境中显著提升AI输出质量
有一次,我们的AI客服助手给用户回复了一个退款流程,说"提交申请后3个工作日内到账"。实际上公司政策是5个工作日。这个错误信息在上线后两周被我们自己的QA发现,那两周里大概有几百个用户因为这个错误信息对我们的服务产生了不满。
我们检查了那条回复的生成过程。LLM有检索到正确文档,文档里写着5个工作日。但不知道为什么,在生成回复时它"自信地"给出了3个工作日。
这个问题的解法不是换个更强的模型,而是引入反思机制——让AI在最终输出之前,主动检查自己的答案是否与证据一致。
反思模式的核心思想
人类的高质量工作通常不是一气呵成的,而是"写初稿——自我检查——修改"这样的循环。反思Agent模仿的就是这个过程:
Critic和Generator可以是同一个LLM(使用不同的Prompt),也可以是不同的模型。在成本敏感的场景,通常用同一个LLM但切换角色。
基础反思循环的实现
@Service
public class ReflexionAgent {
private final ChatClient generatorClient;
private final ChatClient criticClient;
private final int maxIterations;
private static final String GENERATOR_SYSTEM = """
你是一个严谨的助手。根据提供的上下文信息,准确回答用户问题。
如果得到了改进建议,请在保持回答准确性的前提下进行修正。
""";
private static final String CRITIC_SYSTEM = """
你是一个严格的质量审核员,负责检查AI助手的回答。
检查维度:
1. 事实准确性:回答中的所有事实是否与提供的上下文一致?有无捏造?
2. 完整性:问题的关键点是否都被覆盖?
3. 逻辑性:回答的逻辑是否清晰无矛盾?
4. 相关性:回答是否偏题,有无无关内容?
输出格式(JSON):
{
"passed": true/false,
"score": 0-100,
"issues": ["问题1", "问题2"],
"suggestions": ["改进建议1", "改进建议2"]
}
如果passed为true,issues和suggestions可以为空数组。
评分标准:90+为通过,低于90需要改进。
""";
public ReflexionResult answer(String userQuestion, String context) {
List<ReflexionIteration> iterations = new ArrayList<>();
String currentAnswer = null;
CriticReport lastReport = null;
for (int i = 0; i < maxIterations; i++) {
// 生成答案
String generatorPrompt = buildGeneratorPrompt(
userQuestion, context, currentAnswer, lastReport
);
currentAnswer = generatorClient.prompt()
.system(GENERATOR_SYSTEM)
.user(generatorPrompt)
.call()
.content();
// 批判性审查
String criticPrompt = buildCriticPrompt(userQuestion, context, currentAnswer);
String criticResponse = criticClient.prompt()
.system(CRITIC_SYSTEM)
.user(criticPrompt)
.call()
.content();
lastReport = parseCriticReport(criticResponse);
iterations.add(new ReflexionIteration(i + 1, currentAnswer, lastReport));
log.info("第{}轮反思: score={}, passed={}", i + 1, lastReport.score(), lastReport.passed());
if (lastReport.passed()) {
break;
}
}
return new ReflexionResult(currentAnswer, iterations, lastReport);
}
private String buildGeneratorPrompt(String question, String context,
String previousAnswer, CriticReport report) {
if (previousAnswer == null) {
// 首次生成
return """
上下文信息:
%s
用户问题:%s
""".formatted(context, question);
} else {
// 基于反思改进
return """
上下文信息:
%s
用户问题:%s
你上一次的回答:
%s
审查发现的问题:
%s
改进建议:
%s
请根据以上反馈,修正你的回答。特别注意解决指出的问题。
""".formatted(
context, question, previousAnswer,
String.join("\n", report.issues()),
String.join("\n", report.suggestions())
);
}
}
private String buildCriticPrompt(String question, String context, String answer) {
return """
上下文信息(权威来源):
%s
用户的原始问题:
%s
AI助手的回答:
%s
请按照要求审查这个回答。
""".formatted(context, question, answer);
}
private CriticReport parseCriticReport(String response) {
try {
// 提取JSON部分
String json = extractJson(response);
JsonNode node = objectMapper.readTree(json);
List<String> issues = new ArrayList<>();
node.get("issues").forEach(n -> issues.add(n.asText()));
List<String> suggestions = new ArrayList<>();
node.get("suggestions").forEach(n -> suggestions.add(n.asText()));
return new CriticReport(
node.get("passed").asBoolean(),
node.get("score").asInt(),
issues,
suggestions
);
} catch (Exception e) {
// 解析失败时保守处理:认为未通过
return new CriticReport(false, 0,
List.of("审查结果解析失败"), List.of("请重新生成"));
}
}
}分维度的专项Critic
通用Critic的问题是面面俱到但不够深入。对于关键场景,要设计专项Critic:
/**
* 事实一致性专项检查
* 专门检查回答是否与上下文文档一致
*/
@Component
public class FactualConsistencyCritic implements Critic {
private static final String FACTUAL_CRITIC_PROMPT = """
你的任务是检查AI回答中的每一个具体声明(数字、日期、名称、流程、规则等),
验证是否能在上下文文档中找到支持证据。
对于每个声明:
- 如果文档中有明确支持,标记为 SUPPORTED
- 如果文档中没有相关内容(但声明合理),标记为 UNSUPPORTED
- 如果与文档内容矛盾,标记为 CONTRADICTED(这是最严重的问题)
输出格式(JSON):
{
"claims": [
{"claim": "声明内容", "status": "SUPPORTED/UNSUPPORTED/CONTRADICTED",
"evidence": "文档中的相关原文(如有)"}
],
"hasContradiction": true/false,
"overallScore": 0-100
}
""";
@Override
public CriticReport evaluate(String question, String context, String answer) {
String response = chatClient.prompt()
.system(FACTUAL_CRITIC_PROMPT)
.user("文档内容:\n%s\n\nAI回答:\n%s".formatted(context, answer))
.call()
.content();
FactualReport report = parseFactualReport(response);
List<String> issues = report.claims().stream()
.filter(c -> c.status().equals("CONTRADICTED"))
.map(c -> "矛盾声明:「" + c.claim() + "」- 文档原文:" + c.evidence())
.toList();
boolean passed = !report.hasContradiction() && report.overallScore() >= 85;
return new CriticReport(passed, report.overallScore(), issues,
issues.stream()
.map(i -> "请根据文档原文修正:" + i)
.toList());
}
}
/**
* 安全性专项检查
* 检查回答是否含有敏感信息泄露、有害建议等
*/
@Component
public class SafetyCritic implements Critic {
@Override
public CriticReport evaluate(String question, String context, String answer) {
// 先用规则做快速过滤
List<String> ruleViolations = checkRules(answer);
if (!ruleViolations.isEmpty()) {
return CriticReport.failed(ruleViolations);
}
// 规则通过后再用LLM做深度检查
return llmSafetyCheck(answer);
}
private List<String> checkRules(String answer) {
List<String> violations = new ArrayList<>();
// 检查是否包含手机号码
if (PHONE_PATTERN.matcher(answer).find()) {
violations.add("回答包含手机号码,可能泄露隐私");
}
// 检查是否包含身份证号
if (ID_CARD_PATTERN.matcher(answer).find()) {
violations.add("回答包含身份证号,可能泄露隐私");
}
// 检查是否包含内部系统账号信息
if (INTERNAL_SYSTEM_PATTERN.matcher(answer).find()) {
violations.add("回答可能包含内部系统信息");
}
return violations;
}
}多Critic的串联与并联
对于高要求场景,可以配置多个Critic,既支持串联(必须全部通过)也支持并联(快速失败):
@Service
public class MultiCriticPipeline {
private final List<Critic> serialCritics; // 串联:全部通过才算通过
private final List<Critic> parallelCritics; // 并联:任一失败即失败
public CombinedCriticReport evaluate(String question, String context, String answer) {
// 并联执行所有Critic(节省时间)
List<CompletableFuture<CriticReport>> futures = new ArrayList<>();
List<Critic> allCritics = new ArrayList<>();
allCritics.addAll(serialCritics);
allCritics.addAll(parallelCritics);
for (Critic critic : allCritics) {
futures.add(CompletableFuture.supplyAsync(
() -> critic.evaluate(question, context, answer)
));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
List<CriticReport> reports = futures.stream()
.map(CompletableFuture::join)
.toList();
// 汇总结果
boolean allPassed = reports.stream().allMatch(CriticReport::passed);
double avgScore = reports.stream()
.mapToInt(CriticReport::score)
.average()
.orElse(0);
List<String> allIssues = reports.stream()
.flatMap(r -> r.issues().stream())
.distinct()
.toList();
List<String> allSuggestions = reports.stream()
.flatMap(r -> r.suggestions().stream())
.distinct()
.toList();
return new CombinedCriticReport(allPassed, (int) avgScore,
allIssues, allSuggestions, reports);
}
}成本控制:不是每次都需要反思
反思是有代价的——每次反思循环都要多调用一两次LLM。不能无脑地对所有请求都做反思:
@Service
public class AdaptiveReflexionAgent {
private final ReflexionAgent fullReflexionAgent;
private final ChatClient fastAgent;
private final QueryComplexityAnalyzer complexityAnalyzer;
public String answer(String question, String context) {
QueryComplexity complexity = complexityAnalyzer.analyze(question, context);
return switch (complexity) {
case SIMPLE -> {
// 简单问题:直接回答,不反思
// 例如:"营业时间是几点?"
yield fastAgent.prompt()
.user(question)
.call()
.content();
}
case MEDIUM -> {
// 中等复杂度:单轮反思
yield fullReflexionAgent.answerWithMaxIterations(question, context, 1)
.finalAnswer();
}
case COMPLEX -> {
// 复杂问题:完整反思循环(最多3轮)
// 例如:涉及多个政策条款的复杂退款场景
yield fullReflexionAgent.answer(question, context).finalAnswer();
}
};
}
}
@Component
public class QueryComplexityAnalyzer {
public QueryComplexity analyze(String question, String context) {
// 基于启发式规则的快速判断,不调用LLM
// 包含"如何"、"为什么"、"比较"等词,通常是复杂问题
boolean hasComplexKeywords = COMPLEX_KEYWORDS.stream()
.anyMatch(kw -> question.contains(kw));
// 问题包含多个子问题(用"和"、"以及"连接的问句)
boolean hasMultipleSubQuestions = question.contains("?")
&& question.chars().filter(c -> c == '?').count() > 1;
// 上下文文档超长,意味着需要从大量信息中提炼
boolean hasLongContext = context.length() > 3000;
if (hasComplexKeywords || hasMultipleSubQuestions) {
return QueryComplexity.COMPLEX;
} else if (hasLongContext) {
return QueryComplexity.MEDIUM;
} else {
return QueryComplexity.SIMPLE;
}
}
}生产落地的关键经验
反思不是万能的。如果RAG检索到的上下文本身就是错的,Critic基于错误上下文做的审查也是错的,反思无法发现文档本身的错误。反思解决的是"回答与上下文不一致",而不是"上下文本身不准确"。
迭代次数要有硬上限。我们设置2轮反思。超过2轮说明这个问题对当前系统来说太难了,应该转人工或返回"无法确定",而不是无限循环。
把反思日志记下来。每一轮Critic的输出都是宝贵的数据,定期分析高频问题类型,可以指导知识库建设和Prompt优化。
我们上线反思机制后,事实错误率从1.2%降到了0.3%,这个数字背后是大量用户体验的改善。
