feat(agent): add composer final answer
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user