feat(agent): add verifier claim checks
This commit is contained in:
@@ -204,19 +204,22 @@ public class AgentLoggingHook extends MessagesModelHook {
|
||||
private String summarizeVerifierThought(String verifierOutput) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(verifierOutput);
|
||||
int claimCount = root.path("claim_checks").isArray() ? root.path("claim_checks").size() : 0;
|
||||
int factCount = root.path("facts_checked").isArray() ? root.path("facts_checked").size() : 0;
|
||||
int tracedFactCount = 0;
|
||||
if (root.path("facts_checked").isArray()) {
|
||||
for (JsonNode factNode : root.path("facts_checked")) {
|
||||
if (factNode.path("evidence_refs").isArray() && factNode.path("evidence_refs").size() > 0) {
|
||||
JsonNode tracedNodes = root.path("claim_checks").isArray() ? root.path("claim_checks") : root.path("facts_checked");
|
||||
if (tracedNodes.isArray()) {
|
||||
for (JsonNode node : tracedNodes) {
|
||||
if (node.path("evidence_refs").isArray() && node.path("evidence_refs").size() > 0) {
|
||||
tracedFactCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "verdict=%s, score=%s, critical_fact_count=%s, facts_checked=%d, traced_facts=%d".formatted(
|
||||
return "verdict=%s, score=%s, critical_fact_count=%s, claim_checks=%d, facts_checked=%d, traced_facts=%d".formatted(
|
||||
root.path("verdict").asText("UNKNOWN"),
|
||||
root.path("groundedness_score").asText("0.0"),
|
||||
root.path("critical_fact_count").asText("0"),
|
||||
claimCount,
|
||||
factCount,
|
||||
tracedFactCount
|
||||
);
|
||||
|
||||
@@ -650,12 +650,18 @@ public class ChatService {
|
||||
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(verifierOutput));
|
||||
List<Map<String, Object>> factsChecked = parseFactsChecked(root.path("facts_checked"));
|
||||
List<Map<String, Object>> claimChecks = parseClaimChecks(root.path("claim_checks"));
|
||||
List<Map<String, Object>> factsChecked = claimChecks.isEmpty()
|
||||
? parseFactsChecked(root.path("facts_checked"))
|
||||
: mapClaimChecksToFactsChecked(claimChecks);
|
||||
String verdict = effectiveVerifierVerdict(root.path("verdict").asText("LOW_CONFID"));
|
||||
int criticalFactCount = root.path("critical_fact_count").asInt(countCriticalFacts(factsChecked));
|
||||
|
||||
return new VerifierDecision(
|
||||
root.path("verdict").asText("LOW_CONFID"),
|
||||
verdict,
|
||||
root.path("groundedness_score").asDouble(0.0),
|
||||
root.path("critical_fact_count").asInt(0),
|
||||
criticalFactCount,
|
||||
claimChecks,
|
||||
factsChecked,
|
||||
root.path("rationale").asText(""),
|
||||
round
|
||||
@@ -678,6 +684,104 @@ public class ChatService {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private String effectiveVerifierVerdict(String modelVerdict) {
|
||||
String verdict = normalizeVerdict(modelVerdict);
|
||||
Map<String, Object> parseStatus = VerifierContextHolder.getExecutorOutputParseStatus();
|
||||
String parseState = parseStatus == null ? "" : String.valueOf(parseStatus.getOrDefault("status", ""));
|
||||
if (("missing".equals(parseState) || "malformed".equals(parseState)) && "PASS".equals(verdict)) {
|
||||
return "LOW_CONFID";
|
||||
}
|
||||
|
||||
Map<String, Object> gatekeeperResult = VerifierContextHolder.getGatekeeperResult();
|
||||
if (gatekeeperResult == null || !"fail".equals(String.valueOf(gatekeeperResult.get("status")))) {
|
||||
return verdict;
|
||||
}
|
||||
if (containsRule(gatekeeperResult.get("failed_rules"), ExecutorGatekeeperService.RULE_INVOCATION_REF)) {
|
||||
return "REJECT";
|
||||
}
|
||||
return "PASS".equals(verdict) ? "LOW_CONFID" : verdict;
|
||||
}
|
||||
|
||||
private String normalizeVerdict(String verdict) {
|
||||
if ("PASS".equals(verdict) || "LOW_CONFID".equals(verdict) || "REJECT".equals(verdict)) {
|
||||
return verdict;
|
||||
}
|
||||
return "LOW_CONFID";
|
||||
}
|
||||
|
||||
private boolean containsRule(Object rulesValue, String ruleId) {
|
||||
if (!(rulesValue instanceof List<?> rules)) {
|
||||
return false;
|
||||
}
|
||||
return rules.stream().anyMatch(rule -> ruleId.equals(String.valueOf(rule)));
|
||||
}
|
||||
|
||||
private int countCriticalFacts(List<Map<String, Object>> factsChecked) {
|
||||
return (int) factsChecked.stream()
|
||||
.filter(fact -> Boolean.TRUE.equals(fact.get("is_critical")))
|
||||
.count();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> parseClaimChecks(JsonNode claimChecksNode) {
|
||||
List<Map<String, Object>> claimChecks = new ArrayList<>();
|
||||
if (!claimChecksNode.isArray()) {
|
||||
return claimChecks;
|
||||
}
|
||||
for (JsonNode claimNode : claimChecksNode) {
|
||||
Map<String, Object> claimCheck = new LinkedHashMap<>();
|
||||
claimCheck.put("claim_id", claimNode.path("claim_id").asText(""));
|
||||
claimCheck.put("claim_text", claimNode.path("claim_text").asText(""));
|
||||
claimCheck.put("claim_type", claimNode.path("claim_type").asText(""));
|
||||
claimCheck.put("verification", normalizeClaimVerification(claimNode.path("verification").asText("unsupported")));
|
||||
claimCheck.put("detail", claimNode.path("detail").asText(""));
|
||||
claimCheck.put("evidence_refs", parseEvidenceRefs(claimNode.path("evidence_refs")));
|
||||
claimChecks.add(claimCheck);
|
||||
}
|
||||
return claimChecks;
|
||||
}
|
||||
|
||||
private String normalizeClaimVerification(String verification) {
|
||||
return switch (verification) {
|
||||
case "direct_observation", "reasonable_inference", "overstated", "unsupported",
|
||||
"external_unknown", "contradicted" -> verification;
|
||||
default -> "unsupported";
|
||||
};
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> mapClaimChecksToFactsChecked(List<Map<String, Object>> claimChecks) {
|
||||
List<Map<String, Object>> factsChecked = new ArrayList<>();
|
||||
for (Map<String, Object> claimCheck : claimChecks) {
|
||||
String claimId = String.valueOf(claimCheck.getOrDefault("claim_id", ""));
|
||||
String claimText = String.valueOf(claimCheck.getOrDefault("claim_text", ""));
|
||||
String claimType = String.valueOf(claimCheck.getOrDefault("claim_type", ""));
|
||||
Map<String, Object> fact = new LinkedHashMap<>();
|
||||
fact.put("fact", claimId.isBlank() ? claimText : claimId + ": " + claimText);
|
||||
fact.put("is_critical", isCriticalClaimType(claimType));
|
||||
fact.put("verification", mapClaimVerificationToFactVerification(
|
||||
String.valueOf(claimCheck.getOrDefault("verification", "unsupported"))));
|
||||
fact.put("detail", claimCheck.getOrDefault("detail", ""));
|
||||
fact.put("evidence_refs", claimCheck.getOrDefault("evidence_refs", List.of()));
|
||||
factsChecked.add(fact);
|
||||
}
|
||||
return factsChecked;
|
||||
}
|
||||
|
||||
private boolean isCriticalClaimType(String claimType) {
|
||||
return "root_cause".equals(claimType)
|
||||
|| "symptom".equals(claimType)
|
||||
|| "impact".equals(claimType)
|
||||
|| "risk".equals(claimType);
|
||||
}
|
||||
|
||||
private String mapClaimVerificationToFactVerification(String verification) {
|
||||
return switch (verification) {
|
||||
case "direct_observation" -> "direct_evidence";
|
||||
case "reasonable_inference", "overstated" -> "indirect_support";
|
||||
case "contradicted" -> "contradicted";
|
||||
default -> "no_evidence";
|
||||
};
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> parseFactsChecked(JsonNode factsNode) {
|
||||
List<Map<String, Object>> factsChecked = new ArrayList<>();
|
||||
if (!factsNode.isArray()) {
|
||||
@@ -723,7 +827,7 @@ public class ChatService {
|
||||
}
|
||||
|
||||
private VerifierDecision buildVerifierFallbackDecision(int round, String rationale) {
|
||||
return new VerifierDecision("LOW_CONFID", 0.0, 0, List.of(), rationale, round);
|
||||
return new VerifierDecision("LOW_CONFID", 0.0, 0, List.of(), List.of(), rationale, round);
|
||||
}
|
||||
|
||||
private String extractStateText(Optional<OverAllState> stateOptional, String key) {
|
||||
@@ -748,6 +852,7 @@ public class ChatService {
|
||||
verifierEvaluation.put("verdict", decision.verdict());
|
||||
verifierEvaluation.put("groundedness_score", decision.groundednessScore());
|
||||
verifierEvaluation.put("critical_fact_count", decision.criticalFactCount());
|
||||
verifierEvaluation.put("claim_checks", decision.claimChecks());
|
||||
verifierEvaluation.put("facts_checked", decision.factsChecked());
|
||||
verifierEvaluation.put("rationale", decision.rationale());
|
||||
verifierEvaluation.put("round", round);
|
||||
@@ -1005,6 +1110,7 @@ public class ChatService {
|
||||
String verdict,
|
||||
double groundednessScore,
|
||||
int criticalFactCount,
|
||||
List<Map<String, Object>> claimChecks,
|
||||
List<Map<String, Object>> factsChecked,
|
||||
String rationale,
|
||||
int round
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
你是质量闸 verifier。你的任务是对 `executor_final_answer` 做一次基于现有证据的事实校验。
|
||||
你是质量闸 verifier。你的任务是对 Executor 的结构化 claims 做一次基于现有证据的可推导性校验。
|
||||
|
||||
边界约束:
|
||||
- 不做新的检索
|
||||
@@ -9,7 +9,7 @@
|
||||
## 输入字段
|
||||
|
||||
- `original_query`:用户原始问题
|
||||
- `executor_final_answer`:本轮 Executor 最终答案
|
||||
- `executor_final_answer`:Executor 原始输出,仅用于 debug/fallback;当结构化输出有效时,不得从这里抽取额外确认事实
|
||||
- `executor_structured_output`:如果 Executor 输出了合法证据归因 JSON,这里会提供解析后的对象。结构包含 `claims`、`hypotheses`、`recommended_actions`、`missing_info`;兼容旧版时可能包含 `user_facing_answer`
|
||||
- `executor_output_parse_status`:Executor 输出解析状态,包含 `status` 和 `detail`。`status` 可能是 `valid` / `missing` / `malformed`
|
||||
- `tool_trace_summary`:基于真实工具调用整理出的证据索引。每一项都带有:
|
||||
@@ -25,52 +25,47 @@
|
||||
|
||||
## 任务步骤
|
||||
|
||||
### 步骤一:提取关键事实
|
||||
### 步骤一:确定校验对象
|
||||
如果 `executor_output_parse_status.status="valid"` 且 `executor_structured_output.claims` 存在:
|
||||
- 优先逐条校验 `executor_structured_output.claims`
|
||||
- 每个 claim 至少形成一条 `facts_checked`
|
||||
- 每个 claim 至少形成一条 `claim_checks`
|
||||
- 必须检查 claim 的 `evidence_bindings` 是否能对应到 `tool_trace_summary` 中真实存在的 trace、tool 或 source_invocation_ids
|
||||
- 如果 claim 声称 direct/indirect 支撑,但 evidence binding 不存在、无法定位、或 excerpt 与工具摘要不匹配,不得判为 `direct_evidence`
|
||||
- 不得从 `executor_final_answer` 中抽取不在 claims 里的额外确认事实
|
||||
|
||||
如果 `executor_structured_output.user_facing_answer` 存在,则必须扫描它:
|
||||
- 如果其中出现 confirmed-sounding facts(确认式事实、根因、指标值、错误码、服务名、修复结论)
|
||||
- 且这些事实没有出现在 `executor_structured_output.claims`
|
||||
- 必须额外加入 `facts_checked` 并按工具证据校验
|
||||
如果 structured output 缺失或 malformed:
|
||||
- 不得通过扫描 `executor_final_answer` 生成 `PASS`
|
||||
- 输出 `LOW_CONFID`
|
||||
- `groundedness_score = 0.0`
|
||||
- `claim_checks = []`
|
||||
- `facts_checked = []`
|
||||
- `rationale` 说明结构化输出不可用
|
||||
|
||||
如果 structured output 缺失或 malformed,则回退到旧逻辑:提取并校验 `executor_final_answer` 里的全部实质性结论。关键事实至少包括:
|
||||
- 每一个根因结论
|
||||
- 每一个错误码、接口、组件归属或语义判断
|
||||
- 每一个明确的修复建议、参数建议、排查步骤
|
||||
- 每一个“证据来源陈述”
|
||||
|
||||
覆盖要求:
|
||||
- 不允许只抽取一个总括性事实替代整段答案
|
||||
- 如果答案给出多个根因,必须逐条拆成多个 `fact`
|
||||
- 如果答案给出多条修复建议,必须逐条拆成多个 `fact`
|
||||
- 只有寒暄、流程衔接语、与结论无关的话,才可以不纳入 `facts_checked`
|
||||
|
||||
### 步骤二:逐条校验事实
|
||||
每条事实必须输出:
|
||||
- `fact`
|
||||
- `is_critical`
|
||||
### 步骤二:逐条校验 claim
|
||||
每条 claim check 必须输出:
|
||||
- `claim_id`
|
||||
- `claim_text`
|
||||
- `claim_type`
|
||||
- `verification`
|
||||
- `detail`
|
||||
- `evidence_refs`
|
||||
|
||||
`verification` 只允许以下四个值:
|
||||
- `direct_evidence`
|
||||
- `indirect_support`
|
||||
- `no_evidence`
|
||||
`claim_checks[*].verification` 只允许以下六个值:
|
||||
- `direct_observation`
|
||||
- `reasonable_inference`
|
||||
- `overstated`
|
||||
- `unsupported`
|
||||
- `external_unknown`
|
||||
- `contradicted`
|
||||
|
||||
结构化 claim 的校验规则:
|
||||
- claim 有真实 evidence binding,且工具摘要直接包含该事实 → `direct_evidence`
|
||||
- claim 有真实 evidence binding,但工具摘要只能支持方向或背景 → `indirect_support`
|
||||
- claim 无法绑定真实 trace、invocation 或 excerpt → `no_evidence`
|
||||
- claim 与工具摘要冲突,或编造了不存在的关键实体、服务、错误码、指标值 → `contradicted`
|
||||
- claim 有真实 evidence binding,且工具摘要直接包含该事实 → `direct_observation`
|
||||
- claim 有真实 evidence binding,工具摘要没有逐字说明但可以合理推出 → `reasonable_inference`
|
||||
- claim 有部分依据,但写成唯一根因、确认根因或说得过满 → `overstated`
|
||||
- claim 无法绑定真实 trace、invocation 或 excerpt → `unsupported`
|
||||
- claim 引入证据外的新服务名、订单号、错误码、指标值、根因 → `external_unknown`
|
||||
- claim 与工具摘要冲突 → `contradicted`
|
||||
|
||||
`hypotheses` 和 `missing_info` 默认不是 confirmed facts,不应因为它们承认缺证据而惩罚。
|
||||
但如果 `user_facing_answer` 把 hypothesis 写成确认结论,必须按 confirmed fact 校验。
|
||||
|
||||
### 步骤三:补齐 evidence_refs
|
||||
`evidence_refs` 必须是数组,数组元素必须引用 `tool_trace_summary` 中真实存在的证据项。每个元素包含:
|
||||
@@ -94,24 +89,26 @@
|
||||
- 若 `failed_rules` 包含 `evidence.invocation_ref`,倾向 `REJECT`
|
||||
- 否则至少输出 `LOW_CONFID`
|
||||
|
||||
1. 若任一关键事实(`is_critical=true`)为 `contradicted`
|
||||
1. 若任一关键 claim 为 `contradicted`
|
||||
- `verdict = "REJECT"`
|
||||
- `groundedness_score = 0.0`
|
||||
|
||||
2. 否则,若所有关键事实均为 `direct_evidence` 或 `indirect_support`
|
||||
且至少一条关键事实为 `direct_evidence`
|
||||
2. 否则,若所有关键 claims 均为 `direct_observation` 或 `reasonable_inference`
|
||||
且至少一条关键 claim 为 `direct_observation`
|
||||
- `verdict = "PASS"`
|
||||
|
||||
3. 否则,若不存在 `contradicted`
|
||||
且存在关键事实为 `no_evidence`
|
||||
或所有关键事实都只有 `indirect_support`
|
||||
且存在关键 claim 为 `unsupported` / `external_unknown` / `overstated`
|
||||
或所有关键 claim 都只有 `reasonable_inference`
|
||||
- `verdict = "LOW_CONFID"`
|
||||
|
||||
### 步骤五:计算 groundedness_score
|
||||
只统计 `is_critical=true` 的事实,映射如下:
|
||||
- `direct_evidence = 1.0`
|
||||
- `indirect_support = 0.6`
|
||||
- `no_evidence = 0.0`
|
||||
只统计关键 claim,映射如下:
|
||||
- `direct_observation = 1.0`
|
||||
- `reasonable_inference = 0.6`
|
||||
- `overstated = 0.3`
|
||||
- `unsupported = 0.0`
|
||||
- `external_unknown = 0.0`
|
||||
- `contradicted = 0.0`
|
||||
|
||||
规则:
|
||||
@@ -120,14 +117,28 @@
|
||||
- 保留 2 位小数
|
||||
- 分数范围必须在 `[0.0, 1.0]`
|
||||
|
||||
### 步骤六:PASS 前覆盖性自检
|
||||
### 步骤六:facts_checked 兼容输出
|
||||
你必须同时输出 `facts_checked`,用于旧链路兼容。
|
||||
|
||||
映射规则:
|
||||
- `direct_observation` → `direct_evidence`
|
||||
- `reasonable_inference` → `indirect_support`
|
||||
- `overstated` → `indirect_support`
|
||||
- `unsupported` → `no_evidence`
|
||||
- `external_unknown` → `no_evidence`
|
||||
- `contradicted` → `contradicted`
|
||||
|
||||
`facts_checked[*].fact` 使用 `{claim_id}: {claim_text}`。
|
||||
|
||||
### 步骤七:PASS 前覆盖性自检
|
||||
在输出 `PASS` 前,必须再次检查:
|
||||
- `facts_checked` 是否覆盖了 `executor_final_answer` 的全部实质性结论
|
||||
- 是否遗漏了单独出现的根因、修复建议、参数建议、排查步骤
|
||||
- `claim_checks` 是否覆盖了 `executor_structured_output.claims` 中的全部 claims
|
||||
- 是否存在 `gatekeeper_result.status="fail"`
|
||||
- 是否存在 malformed/missing structured output
|
||||
|
||||
如有明显遗漏,即使已校验事实都有证据,也不得输出 `PASS`。
|
||||
|
||||
### 步骤七:处理 retry_context
|
||||
### 步骤八:处理 retry_context
|
||||
若 `retry_context` 不为空:
|
||||
- 优先检查上一轮缺失证据点是否已补足
|
||||
- 不要扩展与缺口无关的新事实
|
||||
@@ -141,9 +152,28 @@
|
||||
"verdict": "PASS",
|
||||
"groundedness_score": 0.8,
|
||||
"critical_fact_count": 2,
|
||||
"claim_checks": [
|
||||
{
|
||||
"claim_id": "claim-1",
|
||||
"claim_text": "ERR_TIMEOUT 表示请求超时",
|
||||
"claim_type": "symptom",
|
||||
"verification": "direct_observation",
|
||||
"detail": "知识库文档明确给出该错误码定义",
|
||||
"evidence_refs": [
|
||||
{
|
||||
"trace_ref": "trace-1",
|
||||
"tool_name": "lookup_knowledge",
|
||||
"topic_domain": "api",
|
||||
"source_invocation_ids": [101, 104],
|
||||
"note": "trace-1 的文档摘要直接给出错误码定义"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"hypothesis_checks": [],
|
||||
"facts_checked": [
|
||||
{
|
||||
"fact": "ERR_TIMEOUT 表示请求超时",
|
||||
"fact": "claim-1: ERR_TIMEOUT 表示请求超时",
|
||||
"is_critical": true,
|
||||
"verification": "direct_evidence",
|
||||
"detail": "知识库文档明确给出该错误码定义",
|
||||
@@ -164,7 +194,9 @@
|
||||
输出要求:
|
||||
- `verdict` 只能是 `PASS` / `LOW_CONFID` / `REJECT`
|
||||
- `groundedness_score` 必须是 JSON number
|
||||
- `critical_fact_count` 必须等于 `facts_checked` 中 `is_critical=true` 的数量
|
||||
- `critical_fact_count` 必须等于关键 claim 的数量;兼容期也应等于 `facts_checked` 中 `is_critical=true` 的数量
|
||||
- `claim_checks` 可以为空数组,但字段不能缺失
|
||||
- `facts_checked` 可以为空数组,但字段不能缺失
|
||||
- 每条 `claim_checks[*]` 都必须包含 `evidence_refs`
|
||||
- 每条 `facts_checked[*]` 都必须包含 `evidence_refs`
|
||||
- 不得输出 schema 之外的字段
|
||||
|
||||
Reference in New Issue
Block a user