feat(agent): add composer final answer

This commit is contained in:
aruo
2026-07-08 09:51:07 +08:00
parent a5b4502c72
commit 6015bcbf6f
20 changed files with 909 additions and 316 deletions
@@ -125,6 +125,7 @@ public class ChatService {
private String chatPlannerPrompt;
private String chatExecutorPrompt;
private String chatVerifierPrompt;
private String chatComposerPrompt;
private final ObjectMapper objectMapper = new ObjectMapper();
@PostConstruct
@@ -140,6 +141,9 @@ public class ChatService {
chatVerifierPrompt = new String(
new ClassPathResource("prompts/chat-verifier-prompt.md").getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
chatComposerPrompt = new String(
new ClassPathResource("prompts/chat-composer-prompt.md").getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
logger.info("Chat 多 Agent Prompts 加载成功");
} catch (IOException e) {
logger.error("加载 Chat Prompt 文件失败", e);
@@ -420,8 +424,9 @@ public class ChatService {
Optional<OverAllState> stateOptional = workflow.invoke(workflowInput, config);
if (stateOptional.isEmpty()) {
finalDecision = buildVerifierFallbackDecision(round, "workflow 未返回有效状态");
answer = buildLowConfidenceOutput(answer, finalDecision);
persistVerifierEvaluation(session, finalDecision, round);
ComposerRenderResult renderResult = buildFixedFallbackAnswer(question, finalDecision);
answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break;
}
@@ -441,25 +446,23 @@ public class ChatService {
if (finalDecision == null) {
finalDecision = buildVerifierFallbackDecision(round, "verifier_output 缺失或无法解析");
answer = buildLowConfidenceOutput(answer, finalDecision);
persistVerifierEvaluation(session, finalDecision, round);
ComposerRenderResult renderResult = buildFixedFallbackAnswer(question, finalDecision);
answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break;
}
if ("PASS".equals(finalDecision.verdict())) {
String executorAnswer = answer;
answer = extractUserFacingAnswer(executorAnswer)
.or(() -> renderStructuredExecutorAnswer(executorAnswer))
.orElse(executorAnswer == null || executorAnswer.isBlank()
? "抱歉,多 Agent 分析未能生成有效结论。"
: executorAnswer);
persistVerifierEvaluation(session, finalDecision, round);
ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break;
}
if ("REJECT".equals(finalDecision.verdict())) {
answer = buildDegradedOutput(finalDecision);
persistVerifierEvaluation(session, finalDecision, round);
ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break;
}
@@ -467,8 +470,9 @@ public class ChatService {
&& finalDecision.groundednessScore() < verifierLowConfidenceThreshold
&& round < 2;
if (!shouldRetry) {
answer = buildLowConfidenceOutput(answer, finalDecision);
persistVerifierEvaluation(session, finalDecision, round);
ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break;
}
@@ -549,6 +553,17 @@ public class ChatService {
.build();
}
private ReactAgent buildChatComposerAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("chat_composer")
.description("负责将 Verifier 允许的材料组织为最终用户答案")
.model(chatModel)
.systemPrompt(chatComposerPrompt)
.hooks(new AgentLoggingHook(agentStepRepository, "composer"))
.outputKey("composer_output")
.build();
}
private ReactAgent buildChatExecutorAgent(ChatModel chatModel, ToolCallback[] toolCallbacks,
List<Map<String, String>> history, String retryContext) {
StringBuilder prompt = new StringBuilder(chatExecutorPrompt);
@@ -845,6 +860,11 @@ public class ChatService {
}
private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round) {
persistVerifierEvaluation(session, decision, round, null);
}
private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round,
Map<String, Object> composerOutput) {
if (decision == null) {
return;
}
@@ -866,12 +886,337 @@ public class ChatService {
verifierEvaluation.put("gatekeeper_result",
Optional.ofNullable(VerifierContextHolder.getGatekeeperResult())
.orElse(Map.of("status", "pass", "failed_rules", List.of(), "warnings", List.of(), "errors", List.of())));
if (composerOutput != null) {
verifierEvaluation.put("composer_output", composerOutput);
}
String merged = selfEvaluationMergeService.mergeVerifierEvaluation(session.getSelfEvaluation(), verifierEvaluation);
session.setSelfEvaluation(merged);
diagnosisSessionRepository.save(session);
}
private ComposerRenderResult composeFinalAnswer(ChatModel chatModel, String originalQuery,
VerifierDecision decision, RunnableConfig config) {
Map<String, Object> composerInput = buildComposerInput(originalQuery, decision);
try {
ReactAgent composer = buildChatComposerAgent(chatModel);
String composerOutput = composer.call(objectMapper.writeValueAsString(composerInput), config).getText();
return parseComposerOutput(composerOutput, composerInput);
} catch (Exception e) {
logger.error("chat_composer 执行失败,使用安全降级模板", e);
return buildFixedFallbackAnswer(composerInput, "composer_exception");
}
}
private ComposerRenderResult buildFixedFallbackAnswer(String originalQuery, VerifierDecision decision) {
return buildFixedFallbackAnswer(buildComposerInput(originalQuery, decision), "fixed_fallback");
}
private Map<String, Object> buildComposerInput(String originalQuery, VerifierDecision decision) {
Map<String, Object> input = new LinkedHashMap<>();
input.put("original_query", originalQuery);
input.put("verdict", decision.verdict());
Map<String, Map<String, Object>> claimsById = indexExecutorClaims();
List<Map<String, Object>> allowedClaims = new ArrayList<>();
List<Map<String, Object>> allowedHypotheses = new ArrayList<>();
List<String> missingInfo = extractStructuredMissingInfo();
List<Map<String, Object>> recommendedActions = extractStructuredRecommendedActions();
if (decision.claimChecks().isEmpty()) {
addLegacyFactsToComposerInput(decision, allowedClaims, allowedHypotheses, missingInfo);
} else {
for (Map<String, Object> check : decision.claimChecks()) {
String verification = String.valueOf(check.getOrDefault("verification", "unsupported"));
String claimId = String.valueOf(check.getOrDefault("claim_id", ""));
String claimText = String.valueOf(check.getOrDefault("claim_text", ""));
String detail = String.valueOf(check.getOrDefault("detail", ""));
Map<String, Object> claim = buildComposerClaim(claimsById.get(claimId), check);
switch (verification) {
case "direct_observation", "reasonable_inference" -> allowedClaims.add(claim);
case "overstated" -> allowedHypotheses.add(Map.of(
"hypothesis_text", claimText,
"basis", detail.isBlank() ? "当前证据只能支持部分判断,不能作为确认结论" : detail
));
case "unsupported", "external_unknown", "contradicted" ->
addMissingInfo(missingInfo, claimText, detail);
default -> addMissingInfo(missingInfo, claimText, detail);
}
}
}
if ("REJECT".equals(decision.verdict())) {
allowedHypotheses = List.of();
}
input.put("allowed_claims", allowedClaims);
input.put("allowed_hypotheses", allowedHypotheses);
input.put("missing_info", missingInfo);
input.put("recommended_actions", recommendedActions);
input.put("rationale", decision.rationale());
return input;
}
private Map<String, Map<String, Object>> indexExecutorClaims() {
Map<String, Map<String, Object>> claimsById = new LinkedHashMap<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object claims = structuredOutput == null ? null : structuredOutput.get("claims");
if (claims instanceof List<?> claimList) {
for (Object item : claimList) {
if (item instanceof Map<?, ?> rawClaim) {
Map<String, Object> claim = new LinkedHashMap<>();
rawClaim.forEach((key, value) -> claim.put(String.valueOf(key), value));
String claimId = String.valueOf(claim.getOrDefault("claim_id", ""));
if (!claimId.isBlank()) {
claimsById.put(claimId, claim);
}
}
}
}
return claimsById;
}
private Map<String, Object> buildComposerClaim(Map<String, Object> executorClaim, Map<String, Object> claimCheck) {
Map<String, Object> claim = new LinkedHashMap<>();
claim.put("claim_id", valueFrom(executorClaim, claimCheck, "claim_id"));
claim.put("claim_type", valueFrom(executorClaim, claimCheck, "claim_type"));
claim.put("claim_text", valueFrom(executorClaim, claimCheck, "claim_text"));
claim.put("support_level", executorClaim == null ? "" : String.valueOf(executorClaim.getOrDefault("support_level", "")));
claim.put("verification", String.valueOf(claimCheck.getOrDefault("verification", "")));
claim.put("detail", String.valueOf(claimCheck.getOrDefault("detail", "")));
return claim;
}
private String valueFrom(Map<String, Object> primary, Map<String, Object> fallback, String key) {
Object value = primary == null ? null : primary.get(key);
if (value == null || String.valueOf(value).isBlank()) {
value = fallback.get(key);
}
return value == null ? "" : String.valueOf(value);
}
private List<String> extractStructuredMissingInfo() {
List<String> missingInfo = new ArrayList<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object missing = structuredOutput == null ? null : structuredOutput.get("missing_info");
if (missing instanceof List<?> missingList) {
for (Object item : missingList) {
String text = String.valueOf(item);
if (!text.isBlank() && !missingInfo.contains(text)) {
missingInfo.add(text);
}
}
}
return missingInfo;
}
private List<Map<String, Object>> extractStructuredRecommendedActions() {
List<Map<String, Object>> actions = new ArrayList<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object recommendedActions = structuredOutput == null ? null : structuredOutput.get("recommended_actions");
if (recommendedActions instanceof List<?> actionList) {
for (Object item : actionList) {
if (item instanceof Map<?, ?> rawAction) {
Map<String, Object> action = new LinkedHashMap<>();
action.put("action_text", textValue(rawAction.get("action_text")));
action.put("reason", textValue(rawAction.get("reason")));
if (!String.valueOf(action.get("action_text")).isBlank()) {
actions.add(action);
}
}
}
}
return actions;
}
private void addLegacyFactsToComposerInput(VerifierDecision decision, List<Map<String, Object>> allowedClaims,
List<Map<String, Object>> allowedHypotheses,
List<String> missingInfo) {
for (Map<String, Object> fact : decision.factsChecked()) {
String verification = String.valueOf(fact.getOrDefault("verification", ""));
String factText = String.valueOf(fact.getOrDefault("fact", ""));
String detail = String.valueOf(fact.getOrDefault("detail", ""));
if ("direct_evidence".equals(verification)) {
allowedClaims.add(Map.of(
"claim_id", "",
"claim_type", "",
"claim_text", factText,
"support_level", "direct",
"verification", verification,
"detail", detail
));
} else if ("indirect_support".equals(verification)) {
allowedHypotheses.add(Map.of(
"hypothesis_text", factText,
"basis", detail.isBlank() ? "当前仅有间接支持,不能作为确认结论" : detail
));
} else {
addMissingInfo(missingInfo, factText, detail);
}
}
}
private void addMissingInfo(List<String> missingInfo, String text, String detail) {
if (text == null || text.isBlank()) {
return;
}
String value = detail == null || detail.isBlank() ? text : text + ":" + detail;
if (!missingInfo.contains(value)) {
missingInfo.add(value);
}
}
private ComposerRenderResult parseComposerOutput(String composerOutput, Map<String, Object> composerInput) {
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(composerOutput));
String answerSummary = root.path("answer_summary").asText("");
String userFacingAnswer = root.path("user_facing_answer").asText("");
if (answerSummary.isBlank() || userFacingAnswer.isBlank() || !root.path("recommended_actions").isArray()) {
return buildFixedFallbackAnswer(composerInput, "composer_schema_invalid");
}
Map<String, Object> audit = new LinkedHashMap<>();
audit.put("status", "valid");
audit.put("answer_summary", answerSummary);
audit.put("recommended_actions", parseComposerActions(root.path("recommended_actions")));
audit.put("user_facing_answer", userFacingAnswer);
return new ComposerRenderResult(userFacingAnswer, audit);
} catch (Exception e) {
logger.warn("解析 composer_output 失败,使用安全降级模板");
return buildFixedFallbackAnswer(composerInput, "composer_malformed");
}
}
private List<Map<String, Object>> parseComposerActions(JsonNode actionsNode) {
List<Map<String, Object>> actions = new ArrayList<>();
if (!actionsNode.isArray()) {
return actions;
}
for (JsonNode actionNode : actionsNode) {
Map<String, Object> action = new LinkedHashMap<>();
action.put("action_text", actionNode.path("action_text").asText(""));
action.put("reason", actionNode.path("reason").asText(""));
actions.add(action);
}
return actions;
}
private ComposerRenderResult buildFixedFallbackAnswer(Map<String, Object> composerInput, String status) {
String answer = renderSafeFallback(composerInput);
Map<String, Object> audit = new LinkedHashMap<>();
audit.put("status", status);
audit.put("detail", "used safe fallback rendering");
audit.put("answer_summary", firstSentence(answer));
audit.put("recommended_actions", composerInput.getOrDefault("recommended_actions", List.of()));
audit.put("user_facing_answer", answer);
return new ComposerRenderResult(answer, audit);
}
@SuppressWarnings("unchecked")
private String renderSafeFallback(Map<String, Object> composerInput) {
String verdict = String.valueOf(composerInput.getOrDefault("verdict", "LOW_CONFID"));
List<Map<String, Object>> allowedClaims =
(List<Map<String, Object>>) composerInput.getOrDefault("allowed_claims", List.of());
List<Map<String, Object>> allowedHypotheses =
(List<Map<String, Object>>) composerInput.getOrDefault("allowed_hypotheses", List.of());
List<String> missingInfo = (List<String>) composerInput.getOrDefault("missing_info", List.of());
List<Map<String, Object>> recommendedActions =
(List<Map<String, Object>>) composerInput.getOrDefault("recommended_actions", List.of());
StringBuilder output = new StringBuilder();
if ("REJECT".equals(verdict)) {
output.append(DEGRADED_PREFIX);
} else if ("LOW_CONFID".equals(verdict)) {
output.append(LOW_CONFID_DISCLAIMER);
}
output.append("\n\n已确认信息:");
if (allowedClaims.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (Map<String, Object> claim : allowedClaims) {
String text = String.valueOf(claim.getOrDefault("claim_text", ""));
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
}
if (!"REJECT".equals(verdict) && !allowedHypotheses.isEmpty()) {
output.append("\n\n可能方向:");
for (Map<String, Object> hypothesis : allowedHypotheses) {
output.append("\n- ").append(hypothesis.getOrDefault("hypothesis_text", ""));
String basis = String.valueOf(hypothesis.getOrDefault("basis", ""));
if (!basis.isBlank()) {
output.append("(").append(basis).append(")");
}
}
}
output.append("\n\n").append("REJECT".equals(verdict) ? "证据缺口:" : "当前缺口:");
if (missingInfo.isEmpty()) {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
} else {
for (String gap : missingInfo) {
output.append("\n- ").append(gap);
}
}
output.append("\n\n建议下一步:");
if (recommendedActions.isEmpty()) {
for (String suggestion : buildNextStepSuggestionsFromTrace()) {
output.append("\n- ").append(suggestion);
}
} else {
for (Map<String, Object> action : recommendedActions) {
String text = String.valueOf(action.getOrDefault("action_text", ""));
if (!text.isBlank()) {
output.append("\n- ").append(text);
String reason = String.valueOf(action.getOrDefault("reason", ""));
if (!reason.isBlank()) {
output.append(":").append(reason);
}
}
}
}
return output.toString().trim();
}
private String firstSentence(String text) {
if (text == null || text.isBlank()) {
return "";
}
int end = text.indexOf('\n');
return end < 0 ? text : text.substring(0, end);
}
private String textValue(Object value) {
if (value == null) {
return "";
}
String text = String.valueOf(value);
return "null".equals(text) ? "" : text;
}
private List<String> buildNextStepSuggestionsFromTrace() {
List<String> suggestions = new ArrayList<>();
List<Map<String, Object>> toolSummary = toolTraceSummaryService.buildVerifierTraceSummary(SessionContextHolder.getSessionId(), null);
boolean hasKnowledgeTool = toolSummary.stream().anyMatch(item -> "lookup_knowledge".equals(item.get("tool_name")));
boolean hasFailedEvidence = toolSummary.stream().anyMatch(item -> !Boolean.TRUE.equals(item.get("success")));
if (!hasKnowledgeTool) {
suggestions.add("补充知识库或业务文档检索结果,建立可引用的证据锚点");
}
if (hasFailedEvidence) {
suggestions.add("优先重试失败的证据型查询,补齐日志、指标或知识库侧证据");
}
if (suggestions.isEmpty()) {
suggestions.add("围绕上述证据缺口补充只读查询,再由人工复核最终结论");
}
return suggestions;
}
private String buildRetryContext(VerifierDecision decision) {
try {
List<String> missingFacts = extractEvidenceGaps(decision);
@@ -886,187 +1231,6 @@ public class ChatService {
}
}
private String buildLowConfidenceOutput(String executorAnswer, VerifierDecision decision) {
StringBuilder output = new StringBuilder(LOW_CONFID_DISCLAIMER);
List<String> confirmedFacts = extractConfirmedFacts(decision);
output.append("\n\n已确认信息:");
if (confirmedFacts.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (String fact : confirmedFacts) {
output.append("\n- ").append(fact);
}
}
List<String> gaps = extractEvidenceGaps(decision);
output.append("\n\n当前缺口:");
if (!gaps.isEmpty()) {
for (String gap : gaps) {
output.append("\n- ").append(gap);
}
} else {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
}
output.append("\n\n建议下一步:");
for (String suggestion : buildNextStepSuggestions(decision)) {
output.append("\n- ").append(suggestion);
}
return output.toString();
}
private Optional<String> extractUserFacingAnswer(String executorAnswer) {
if (executorAnswer == null || executorAnswer.isBlank()) {
return Optional.empty();
}
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(executorAnswer));
String userFacingAnswer = root.path("user_facing_answer").asText("");
if (!userFacingAnswer.isBlank()) {
return Optional.of(userFacingAnswer);
}
} catch (Exception e) {
logger.debug("Executor answer is not structured JSON, keep raw answer");
}
return Optional.empty();
}
private Optional<String> renderStructuredExecutorAnswer(String executorAnswer) {
if (executorAnswer == null || executorAnswer.isBlank()) {
return Optional.empty();
}
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(executorAnswer));
if (!"executor_evidence_v2".equals(root.path("answer_version").asText(""))) {
return Optional.empty();
}
StringBuilder output = new StringBuilder();
appendTextArraySection(output, "已确认信息", root.path("claims"), "claim_text", "暂无可稳定确认的信息");
appendHypothesesSection(output, root.path("hypotheses"));
appendStringArraySection(output, "当前缺口", root.path("missing_info"), "当前缺少足够的直接证据支撑完整结论");
appendRecommendedActionsSection(output, root.path("recommended_actions"));
String rendered = output.toString().trim();
return rendered.isBlank() ? Optional.empty() : Optional.of(rendered);
} catch (Exception e) {
logger.debug("Failed to render executor_evidence_v2 answer", e);
return Optional.empty();
}
}
private void appendTextArraySection(StringBuilder output, String title, JsonNode items,
String fieldName, String emptyText) {
output.append(title).append(":");
if (!items.isArray() || items.isEmpty()) {
output.append("\n- ").append(emptyText);
return;
}
for (JsonNode item : items) {
String text = item.path(fieldName).asText("");
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
if (output.charAt(output.length() - 1) == ':') {
output.append("\n- ").append(emptyText);
}
}
private void appendHypothesesSection(StringBuilder output, JsonNode hypotheses) {
if (!hypotheses.isArray() || hypotheses.isEmpty()) {
return;
}
output.append("\n\n可能方向:");
for (JsonNode hypothesis : hypotheses) {
String text = hypothesis.path("hypothesis_text").asText("");
if (text.isBlank()) {
continue;
}
String basis = hypothesis.path("basis").asText("");
output.append("\n- ").append(text);
if (!basis.isBlank()) {
output.append("(").append(basis).append(")");
}
}
}
private void appendStringArraySection(StringBuilder output, String title, JsonNode items, String emptyText) {
output.append("\n\n").append(title).append(":");
if (!items.isArray() || items.isEmpty()) {
output.append("\n- ").append(emptyText);
return;
}
for (JsonNode item : items) {
String text = item.asText("");
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
}
private void appendRecommendedActionsSection(StringBuilder output, JsonNode actions) {
output.append("\n\n建议下一步:");
if (!actions.isArray() || actions.isEmpty()) {
output.append("\n- 围绕上述证据缺口补充只读查询,再由人工复核最终结论");
return;
}
for (JsonNode action : actions) {
String text = action.path("action_text").asText("");
if (text.isBlank()) {
continue;
}
String reason = action.path("reason").asText("");
output.append("\n- ").append(text);
if (!reason.isBlank()) {
output.append(":").append(reason);
}
}
}
private String buildDegradedOutput(VerifierDecision decision) {
StringBuilder output = new StringBuilder(DEGRADED_PREFIX);
List<String> confirmedFacts = extractConfirmedFacts(decision);
List<String> gaps = extractEvidenceGaps(decision);
List<String> suggestions = buildNextStepSuggestions(decision);
output.append("\n\n已确认信息:");
if (confirmedFacts.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (String fact : confirmedFacts) {
output.append("\n- ").append(fact);
}
}
output.append("\n\n证据缺口:");
if (gaps.isEmpty()) {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
} else {
for (String gap : gaps) {
output.append("\n- ").append(gap);
}
}
output.append("\n\n建议下一步:");
for (String suggestion : suggestions) {
output.append("\n- ").append(suggestion);
}
return output.toString();
}
private List<String> extractConfirmedFacts(VerifierDecision decision) {
List<String> confirmedFacts = new ArrayList<>();
for (Map<String, Object> fact : decision.factsChecked()) {
String verification = String.valueOf(fact.get("verification"));
boolean critical = Boolean.TRUE.equals(fact.get("is_critical"));
if (critical && "direct_evidence".equals(verification)) {
confirmedFacts.add(String.valueOf(fact.get("fact")));
}
}
return confirmedFacts;
}
private List<String> extractEvidenceGaps(VerifierDecision decision) {
List<String> gaps = new ArrayList<>();
for (Map<String, Object> fact : decision.factsChecked()) {
@@ -1088,24 +1252,6 @@ public class ChatService {
return gaps;
}
private List<String> buildNextStepSuggestions(VerifierDecision decision) {
List<String> suggestions = new ArrayList<>();
List<Map<String, Object>> toolSummary = toolTraceSummaryService.buildVerifierTraceSummary(SessionContextHolder.getSessionId(), null);
boolean hasKnowledgeTool = toolSummary.stream().anyMatch(item -> "lookup_knowledge".equals(item.get("tool_name")));
boolean hasFailedEvidence = toolSummary.stream().anyMatch(item -> !Boolean.TRUE.equals(item.get("success")));
if (!hasKnowledgeTool) {
suggestions.add("补充知识库或业务文档检索结果,建立可引用的证据锚点");
}
if (hasFailedEvidence) {
suggestions.add("优先重试失败的证据型查询,补齐日志、指标或知识库侧证据");
}
if (suggestions.isEmpty()) {
suggestions.add("围绕上述证据缺口补充只读查询,再由人工复核最终结论");
}
return suggestions;
}
private record VerifierDecision(
String verdict,
double groundednessScore,
@@ -1117,6 +1263,9 @@ public class ChatService {
) {
}
private record ComposerRenderResult(String answer, Map<String, Object> audit) {
}
/** 从 agent_step 和 tool_invocation 汇总指标回填 diagnosis_session */
private void backfillSessionMetrics(DiagnosisSession session) {
try {
@@ -0,0 +1,61 @@
你是 Answer Composer。你的职责是把 Verifier 允许输出的结构化材料组织成用户可读的中文答案。
边界约束:
- 你不是诊断 Agent。
- 你不调用工具。
- 你不重新判断根因。
- 你不补充输入中不存在的新事实。
- 你只能使用输入中的 `allowed_claims`、`allowed_hypotheses`、`missing_info`、`recommended_actions`、`rationale`。
- 禁止使用模型经验添加新的服务名、订单号、时间、指标值、错误码、根因或修复理由。
- 只输出一个合法 JSON 对象,不输出 Markdown,不输出代码块,不输出额外说明。
## 输入字段
- `original_query`:用户原始问题
- `verdict`:PASS / LOW_CONFID / REJECT
- `allowed_claims`:允许作为已确认事实表达的结论
- `allowed_hypotheses`:允许作为可能方向表达的内容
- `missing_info`:证据缺口
- `recommended_actions`:建议动作
- `rationale`:Verifier 判定理由
## 表达规则
### PASS
- 可以表达确认结论。
- 只能使用 `allowed_claims` 和 `recommended_actions`。
- 只有当 `allowed_claims` 中存在 `claim_type=root_cause` 的 claim 时,才允许表达“根因已确认”。
### LOW_CONFID
- 必须说明当前证据仍有缺口。
- 必须区分“已确认信息”和“可能方向”。
- 不得把 `allowed_hypotheses` 写成确认结论。
### REJECT
- 必须说明当前无法基于已获取证据生成可靠结论。
- 不得输出根因结论。
- 只能输出已确认信息、证据缺口和下一步建议。
## 输出协议
必须输出且只能输出以下 JSON 结构:
{
"answer_summary": "...",
"recommended_actions": [
{
"action_text": "...",
"reason": "..."
}
],
"user_facing_answer": "..."
}
输出要求:
- `answer_summary` 用 1-2 句话概括当前可表达结论。
- `recommended_actions` 可以为空数组,但字段不能缺失。
- `user_facing_answer` 是最终给用户看的中文答案。
- 不得输出 schema 之外的字段。
@@ -58,7 +58,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\""));
assertEquals("sequential-test-session", result.sessionId());
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt);
}
@@ -82,6 +82,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted low confidence"
}
""");
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex(
chatModel,
@@ -94,7 +95,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().startsWith("以下结论基于当前已获取证据"));
assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER"));
assertTrue(result.answer().contains("当前缺口"));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
}
@Test
@@ -131,6 +132,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted low confidence"
}
""");
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex(
chatModel,
@@ -141,8 +143,9 @@ class ChatServiceSequentialAgentTest {
);
assertTrue(result.answer().contains("已确认信息:\n- 连接池耗尽 active=50/50"));
assertFalse(result.answer().contains("临时扩容连接池到 80"));
assertTrue(result.answer().contains("当前缺口:\n- OOM 导致连接泄漏:missing OOM log"));
assertTrue(result.answer().contains("80"));
assertTrue(result.answer().contains("suggestion inferred from evidence"));
assertTrue(result.answer().contains("missing OOM log"));
assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER"));
}
@@ -176,7 +179,6 @@ class ChatServiceSequentialAgentTest {
"sequential-invalid-verifier-session"
);
assertTrue(result.answer().startsWith("以下结论基于当前已获取证据"));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
}
@@ -200,6 +202,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted reject"
}
""");
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex(
chatModel,
@@ -228,7 +231,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\""));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt);
}
@@ -282,6 +285,7 @@ class ChatServiceSequentialAgentTest {
void executeChatComplexRendersExecutorEvidenceV2InsteadOfRawJsonOnPass() throws Exception {
ChatService chatService = createChatService();
ScriptedChatModel chatModel = new ScriptedChatModel();
chatModel.composerOutput = "not-json";
chatModel.executorOutput = """
{
"answer_version": "executor_evidence_v2",
@@ -329,7 +333,6 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("已确认信息"));
assertTrue(result.answer().contains("连接池 active 达到上限"));
assertTrue(result.answer().contains("可能方向"));
assertTrue(result.answer().contains("建议下一步"));
assertFalse(result.answer().contains("\"answer_version\""));
assertFalse(result.answer().contains("executor_evidence_v2"));
@@ -476,6 +479,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "model tried pass"
}
""");
chatModel.composerOutput = "not-json";
chatModel.executorOutput = """
{
"answer_version": "executor_evidence_v2",
@@ -530,6 +534,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "model tried pass"
}
""");
chatModel.composerOutput = "not-json";
chatModel.executorOutput = "{ not-json";
ChatService.ChatResult result = chatService.executeChatComplex(
@@ -670,6 +675,7 @@ class ChatServiceSequentialAgentTest {
ReflectionTestUtils.setField(chatService, "chatPlannerPrompt", "PLANNER_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatExecutorPrompt", "EXECUTOR_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatVerifierPrompt", "VERIFIER_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatComposerPrompt", "COMPOSER_TEST_PROMPT");
return chatService;
}
@@ -707,6 +713,7 @@ class ChatServiceSequentialAgentTest {
private String plannerPromptText = "";
private String executorPromptText = "";
private String verifierPromptText = "";
private String composerPromptText = "";
private String executorOutput = """
{
"answer_version": "executor_evidence_v2",
@@ -732,6 +739,18 @@ class ChatServiceSequentialAgentTest {
"missing_info": []
}
""";
private String composerOutput = """
{
"answer_summary": "已确认连接池 active 达到上限。",
"recommended_actions": [
{
"action_text": "补充查询连接池泄漏检测日志",
"reason": "用于确认是否存在连接未释放"
}
],
"user_facing_answer": "已确认连接池 active 达到上限。建议补充查询连接池泄漏检测日志。"
}
""";
private boolean sawVerifierPrompt;
private final java.util.List<String> verifierOutputs;
private int verifierOutputIndex;
@@ -778,6 +797,10 @@ class ChatServiceSequentialAgentTest {
int index = Math.min(verifierOutputIndex, verifierOutputs.size() - 1);
text = verifierOutputs.get(index);
verifierOutputIndex++;
} else if (promptText.contains("COMPOSER_TEST_PROMPT")) {
agentCalls.add("chat_composer");
composerPromptText = promptText;
text = composerOutput;
} else {
text = "UNEXPECTED_PROMPT";
}