docs(harness): add evidence-chain, application-audit, contract-state and interview review notes; annotate guard/release core classes
This commit is contained in:
@@ -4,15 +4,41 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 安全回退契约(Release 层 FALLBACK 的对外载荷):不发布根因报告时,
|
||||
* 把「为什么降级 + 已验证事实 + 下一步建议」结构化地交给用户。
|
||||
*
|
||||
* <p>设计要点:
|
||||
* <ul>
|
||||
* <li>{@code conclusion} 恒为 null:降级绝不发布未证明的根因结论;</li>
|
||||
* <li>诚实降级:verified_sources / observed_facts 保留已验证事实
|
||||
* (供用户继续排查),validation_issues 给出失败原因;</li>
|
||||
* <li>有界冻结契约:全部列表不可变,内容经 SafeFallbackFactory 截断去重
|
||||
* (来源 12 条以内、摘要 320 字),绝不泄露 raw / 敏感正文。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>构造:仅 {@code SafeFallbackFactory}(release 域);
|
||||
* 包装对外:{@code FallbackContent}(application 域);
|
||||
* 审计提取:{@code RunConclusionExtractor}(audit 域)。
|
||||
*/
|
||||
public record SafeFallback(
|
||||
/** 降级细分类型:EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / ... */
|
||||
@JsonProperty("type") FallbackType type,
|
||||
/** 恒为 null(降级不发布根因结论,保留字段仅为契约完整性)。 */
|
||||
@JsonProperty("conclusion") String conclusion,
|
||||
/** 一句话降级原因(用户可读)。 */
|
||||
@JsonProperty("message") String message,
|
||||
/** 已验证来源(去重):查过哪些来源。 */
|
||||
@JsonProperty("verified_sources") List<VerifiedSource> verifiedSources,
|
||||
/** 限制声明:检查范围 / 缺失项 / 降级原因。 */
|
||||
@JsonProperty("limitations") List<String> limitations,
|
||||
/** 下一步建议(用户可执行)。 */
|
||||
@JsonProperty("next_steps") List<String> nextSteps,
|
||||
/** 失败阶段标识:DIAGNOSIS_INPUT / DIAGNOSIS_COLLECTION / EVIDENCE_VALIDATION / SEMANTIC_VALIDATION。 */
|
||||
@JsonProperty("failure_stage") String failureStage,
|
||||
/** 已观察事实(去重、有界):每条 = 来源 + 范围 + 摘要,供继续排查。 */
|
||||
@JsonProperty("observed_facts") List<ObservedFact> observedFacts,
|
||||
/** 验证违规明细(EVIDENCE_VALIDATION_FAILED 时携带 code + target)。 */
|
||||
@JsonProperty("validation_issues") List<ValidationIssue> validationIssues) {
|
||||
|
||||
public SafeFallback {
|
||||
@@ -33,12 +59,14 @@ public record SafeFallback(
|
||||
null, List.of(), List.of());
|
||||
}
|
||||
|
||||
/** 已验证来源:来源类型 + 来源 + 范围(发布层可对外展示的最小来源单位)。 */
|
||||
public record VerifiedSource(
|
||||
@JsonProperty("source_type") String sourceType,
|
||||
@JsonProperty("source") String source,
|
||||
@JsonProperty("scope") String scope) {
|
||||
}
|
||||
|
||||
/** 已观察事实:来源类型 + 来源 + 范围 + 有界摘要(一条工具调用结果投影)。 */
|
||||
public record ObservedFact(
|
||||
@JsonProperty("source_type") String sourceType,
|
||||
@JsonProperty("source") String source,
|
||||
@@ -46,6 +74,7 @@ public record SafeFallback(
|
||||
@JsonProperty("summary") String summary) {
|
||||
}
|
||||
|
||||
/** 验证违规明细:违规码 + 目标字段(EVIDENCE_VALIDATION_FAILED 时携带)。 */
|
||||
public record ValidationIssue(
|
||||
@JsonProperty("code") String code,
|
||||
@JsonProperty("target") String target) {
|
||||
|
||||
@@ -26,6 +26,27 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 证据机械验真(证据安全链第 1 道闸):不信任模型自述,以 Canonical Store 账本为准。
|
||||
*
|
||||
* <p>职责:校验 DiagnosisDraft 结构合法 + 每个 tool_call_id 引用真实可查
|
||||
* + 投影内部自洽,并把账本投影「重读重建」为 VerifiedEvidence 快照。
|
||||
*
|
||||
* <p>三个阶段:
|
||||
* <ol>
|
||||
* <li>{@link #validateDraft}:草稿结构完整性(analysis 非空、id 唯一、kind/text/引用齐全、limitations 必填);</li>
|
||||
* <li>{@link #verifyInvocation}:引用验真(key=runId+toolCallId 查账本、记录必须 READY、
|
||||
* 同 Run、agent_result 非空、kind 与证据语义匹配、工具受支持);</li>
|
||||
* <li>readRag/readLogs/readMysql:严格反序列化投影并校验内部一致性,
|
||||
* 重读重建 VerifiedEvidence(证据内容来自账本,不是模型复述)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>纯规则门控、不调大模型:20 个违规码全部可枚举可审计;
|
||||
* 产出 {@link VerifiedEvidenceSnapshot} 供 SemanticGuard 语义裁决与 release 发布。
|
||||
*
|
||||
* <p>被 {@code DiagnosisReleaseUseCase} 调用(validate / validateNoConclusionReferences),
|
||||
* 是「唯一发布点」的第一道门。
|
||||
*/
|
||||
public final class EvidenceGuard {
|
||||
|
||||
private final CanonicalInvocationStore store;
|
||||
@@ -35,6 +56,12 @@ public final class EvidenceGuard {
|
||||
private final ObjectReader mysqlRequestReader;
|
||||
private final ObjectReader mysqlResultReader;
|
||||
|
||||
/**
|
||||
* 构造:注入账本(canonical store)+ key 工厂 + 严格模式 reader。
|
||||
*
|
||||
* <p>四种 reader 全部开启 FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS——
|
||||
* 多余字段、尾随内容一律解析失败(fail closed,不接受「看起来差不多」的投影)。
|
||||
*/
|
||||
public EvidenceGuard(CanonicalInvocationStore store,
|
||||
ToolCallKeyFactory keyFactory,
|
||||
ObjectMapper objectMapper) {
|
||||
@@ -55,6 +82,12 @@ public final class EvidenceGuard {
|
||||
.with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:draft 结构校验 → 逐条 tool_call 引用验真 → 重读投影重建证据。
|
||||
*
|
||||
* <p>任何违规都收集到 violations(不中断,尽量报全);全部通过才产出快照。
|
||||
* 每个 analysis 只保留「证据非空」的条目——引用为空/无效的分析不进快照。
|
||||
*/
|
||||
public EvidenceGuardResult validate(RunContext context, DiagnosisDraft draft) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
List<EvidenceViolation> violations = validateDraft(draft);
|
||||
@@ -79,6 +112,11 @@ public final class EvidenceGuard {
|
||||
: EvidenceGuardResult.invalid(violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无结论场景的引用校验(conclusion == null 时由 release 调用):
|
||||
* 只验引用真实性,不产出快照(返回 empty)——没有结论就没有「是否被支持」可判。
|
||||
* 供 {@code DiagnosisReleaseUseCase.releaseNoConclusion} 发布前兜底验引用。
|
||||
*/
|
||||
public EvidenceGuardResult validateNoConclusionReferences(
|
||||
RunContext context, DiagnosisDraft draft) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
@@ -113,6 +151,11 @@ public final class EvidenceGuard {
|
||||
: EvidenceGuardResult.invalid(violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 A:草稿结构完整性校验(纯规则,不碰 store)。
|
||||
* 先收集全部 analysis id 到集合,再校验报告级引用只能指向这些已登记 id
|
||||
* (封死「结论引用不存在的分析」路径)。
|
||||
*/
|
||||
private List<EvidenceViolation> validateDraft(DiagnosisDraft draft) {
|
||||
List<EvidenceViolation> violations = new ArrayList<>();
|
||||
if (draft == null) {
|
||||
@@ -149,6 +192,10 @@ public final class EvidenceGuard {
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 报告级引用校验:conclusion / action_plan / recommendations 的
|
||||
* based_on_analysis_ids 必须存在且指向已登记 analysis id;limitations 必填。
|
||||
*/
|
||||
private void validateReportReferences(DiagnosisDraft draft, Set<String> ids,
|
||||
List<EvidenceViolation> violations) {
|
||||
if (draft.conclusion() != null) {
|
||||
@@ -178,6 +225,10 @@ public final class EvidenceGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条报告文本 + 其 based_on_analysis_ids 的合法性:
|
||||
* 文本非空、引用列表非空、每个引用都必须指向已登记 analysis id。
|
||||
*/
|
||||
private void validateTextAndReferences(String target, String text, List<String> references,
|
||||
Set<String> ids, List<EvidenceViolation> violations) {
|
||||
if (isBlank(text)) {
|
||||
@@ -200,6 +251,18 @@ public final class EvidenceGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 B(核心):验真单条 tool_call 引用。链条逐环检查,任何一环不过即记违规并跳过:
|
||||
*
|
||||
* <pre>
|
||||
* id 非空 → key 构造(runId 绑定,防跨 Run 引用)→ 账本可查 → id 一致
|
||||
* → isReferencableBy(READY + 同 Run + agent_result 非空 + 合法证据语义)
|
||||
* → kind 匹配证据语义(NORMAL↔FOUND / NEGATIVE_OBSERVATION↔NO_EVIDENCE)
|
||||
* → 工具受支持(RAG / LOGS / MYSQL)
|
||||
* </pre>
|
||||
*
|
||||
* 该引用不被采信不代表整体失败:继续检查其余引用,违规全部汇总。
|
||||
*/
|
||||
private void verifyInvocation(RunContext context, DiagnosisDraft.AnalysisItem analysis,
|
||||
int analysisIndex, String toolCallId,
|
||||
List<VerifiedEvidence> evidence,
|
||||
@@ -250,6 +313,11 @@ public final class EvidenceGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 C(RAG):严格反序列化 RagToolResult 投影,校验内部一致性
|
||||
* (toolCallId / evidenceStatus / returnedCount==evidence.size() / NO_EVIDENCE 时证据必须为空)
|
||||
* 后,把每条命中重建为 VerifiedEvidence。
|
||||
*/
|
||||
private void readRag(CanonicalToolInvocation invocation, List<VerifiedEvidence> evidence,
|
||||
String target, List<EvidenceViolation> violations) {
|
||||
RagToolResult result;
|
||||
@@ -301,6 +369,11 @@ public final class EvidenceGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 C(LOGS):校验 QueryLogsToolResult 结构(topic/query/时间窗/matchCount 齐全、
|
||||
* returnedCount==events.size()、NO_EVIDENCE 时 matchCount 必须为 0 且无 patterns/events),
|
||||
* 把日志模式与事件重建为 VerifiedEvidence。
|
||||
*/
|
||||
private void readLogs(CanonicalToolInvocation invocation, List<VerifiedEvidence> evidence,
|
||||
String target, List<EvidenceViolation> violations) {
|
||||
QueryLogsToolResult result;
|
||||
@@ -364,6 +437,7 @@ public final class EvidenceGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/** 投影通用一致性校验:toolCallId 与 evidenceStatus 必须与账本记录一致(防投影与账本脱节)。 */
|
||||
private boolean projectionMatches(CanonicalToolInvocation invocation, String toolCallId,
|
||||
com.superbiz.agent.harness.contract.EvidenceStatus evidenceStatus,
|
||||
String target, List<EvidenceViolation> violations) {
|
||||
@@ -378,6 +452,10 @@ public final class EvidenceGuard {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 C(MYSQL):校验 MysqlToolResult(列唯一非空、returnedCount==rows.size()、
|
||||
* 每行 keySet 必须恰好等于 columns),把每一行重建为 VerifiedEvidence(带 _row_number)。
|
||||
*/
|
||||
private void readMysql(CanonicalToolInvocation invocation, List<VerifiedEvidence> evidence,
|
||||
String target, List<EvidenceViolation> violations) {
|
||||
MysqlToolRequest request;
|
||||
|
||||
@@ -7,6 +7,10 @@ public record EvidenceGuardResult(
|
||||
List<EvidenceViolation> violations,
|
||||
VerifiedEvidenceSnapshot snapshot) {
|
||||
|
||||
/**
|
||||
* 结构不变量:valid 必须有 snapshot、invalid 必须有 violations,二选一无中间态
|
||||
* (有效结果不可能带违规,无效结果不可能带快照)。
|
||||
*/
|
||||
public EvidenceGuardResult {
|
||||
violations = violations == null ? List.of() : List.copyOf(violations);
|
||||
if (violations.isEmpty() == (snapshot == null)) {
|
||||
|
||||
@@ -24,6 +24,19 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 守卫模型调用器:通用「隔离判定模型」的受控调用(SemanticGuard 等守卫用)。
|
||||
*
|
||||
* <p>与主 Agent 调用不同:守卫模型单轮、无工具、强约束输出,但同样受 Run 生命周期管:
|
||||
* <ul>
|
||||
* <li>core.beforeModelCall / checkActive:与 core 门禁对齐;</li>
|
||||
* <li>auditor.begin / recordUsage:Token 记账(ModelCallLedger);</li>
|
||||
* <li>executor.submit + future.get(timeout):独立线程 + 超时截断;</li>
|
||||
* <li>context.cancellation().onCancel → future.cancel(true):Run 取消强杀在途调用;</li>
|
||||
* <li>输出限制:非文本 / 带 tool_calls / 超 maxOutputBytes → SCHEMA_INVALID 重试;</li>
|
||||
* <li>终态异常(RunAborted / BudgetExceeded)原样穿出,不吞。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class GuardModelCall {
|
||||
|
||||
private final DiagnosisHarnessCore core;
|
||||
@@ -44,6 +57,11 @@ public final class GuardModelCall {
|
||||
this.auditor = Objects.requireNonNull(auditor, "auditor must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* 受控调用:beforeModelCall 门禁 → 记账开始 → 提交执行 → 注册取消回调
|
||||
* → future.get(timeout) 等待。超时/取消/中断/执行异常分别归类映射 RetryFailure;
|
||||
* RunAbortedException / BudgetExceededException 原样穿出(Run 终态事实,不可重试)。
|
||||
*/
|
||||
public String call(RunContext context, ModelCallComponent component,
|
||||
Prompt prompt, Duration timeout, long maxOutputBytes) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
@@ -91,6 +109,11 @@ public final class GuardModelCall {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行侧:真实 chatModel.call,成功则记账 usage 并 checkActive;
|
||||
* 输出形状违规(null/空白/带 tool_calls)或超 maxOutputBytes 归类 SCHEMA_INVALID;
|
||||
* 输出字节也 reserveRunBytes 计入预算(守卫模型的花费不是无底洞)。
|
||||
*/
|
||||
private String invoke(RunContext context, ModelCallLedger.Call call,
|
||||
Prompt prompt, long maxOutputBytes) {
|
||||
ChatResponse response;
|
||||
@@ -119,6 +142,7 @@ public final class GuardModelCall {
|
||||
return output.getText();
|
||||
}
|
||||
|
||||
/** 记账 usage;缺 metadata/usage 时按 0 记账(保持账本完整性,可对账)。 */
|
||||
private void recordUsage(RunContext context, ModelCallLedger.Call call, ChatResponse response) {
|
||||
if (response == null || response.getMetadata() == null) {
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
|
||||
@@ -24,6 +24,24 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 语义裁决(证据安全链第 2 道闸):判「结论是否被已验证证据支持」。
|
||||
*
|
||||
* <p>在 EvidenceGuard 机械验真通过后调用——结构错、引用假根本到不了这里。
|
||||
* 职责:把用户可见视图(SemanticDraftView,不含内部 id)+ 已验证证据交给
|
||||
* 隔离的守卫模型,硬校验输出(恰好 {verdict, reason} 两个字段),
|
||||
* 裁决 SUPPORTED / UNSUPPORTED。
|
||||
*
|
||||
* <p>与 Harness 全栈衔接:
|
||||
* <ul>
|
||||
* <li>预算:输入输出字节都 {@code core.reserveRunBytes} 计入 Run 预算;</li>
|
||||
* <li>重试:走 {@code context.retryPolicies().semanticGuard()},每次 attempt 递减剩余超时;</li>
|
||||
* <li>取消:GuardModelCall 内 onCancel → future.cancel(true);</li>
|
||||
* <li>审计:ModelCallLedger 记账 + TraceAuditEvents.semanticAttempt 落 trace。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>被 {@code DiagnosisReleaseUseCase.releaseConclusion} 调用(唯一入口)。
|
||||
*/
|
||||
public final class SemanticGuard {
|
||||
|
||||
private static final Set<String> OUTPUT_FIELDS = Set.of("verdict", "reason");
|
||||
@@ -65,6 +83,12 @@ public final class SemanticGuard {
|
||||
this.prompt = SemanticGuardPrompt.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:序列化输入(超限即拒绝 SCHEMA_INVALID)→ 计入预算
|
||||
* → 构造 System(prompt)+User(输入 JSON) 双消息
|
||||
* → 经 HarnessRetryExecutor 按 semanticGuard 策略执行模型调用
|
||||
* → 硬校验输出 schema → 返回裁决。每次 attempt 都记审计 trace。
|
||||
*/
|
||||
public SemanticGuardDecision review(RunContext context, SemanticGuardInput input) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
Objects.requireNonNull(input, "input must not be null");
|
||||
@@ -91,6 +115,11 @@ public final class SemanticGuard {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬校验模型输出:必须是 JSON、字段恰好 {verdict, reason}、verdict 合法枚举、
|
||||
* reason 非空;否则按失败类型抛 GuardModelCallException(可重试:
|
||||
* PARSE_ERROR / SCHEMA_INVALID)。防止模型夹带多余字段或输出不完整。
|
||||
*/
|
||||
private SemanticGuardDecision parse(String output) {
|
||||
JsonNode root;
|
||||
try {
|
||||
@@ -115,6 +144,10 @@ public final class SemanticGuard {
|
||||
return new SemanticGuardDecision(verdict, root.path("reason").asText());
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次 attempt 的剩余超时 = min(总超时 - 已用, 单次上限);
|
||||
* 总超时耗尽即抛 TIMEOUT(不再重试)——守卫判定有硬截止线。
|
||||
*/
|
||||
private Duration remainingTimeout(long startedNanos) {
|
||||
long elapsed = Math.max(0L, System.nanoTime() - startedNanos);
|
||||
long remaining = limits.totalTimeout().toNanos() - elapsed;
|
||||
@@ -125,6 +158,10 @@ public final class SemanticGuard {
|
||||
return Duration.ofNanos(Math.min(remaining, limits.perAttemptTimeout().toNanos()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败分类:GuardModelCallException 自带 RetryFailure;
|
||||
* 其余未知异常归 UNKNOWN(不重试,直接失败)。
|
||||
*/
|
||||
private RetryFailure classify(Exception exception) {
|
||||
if (exception instanceof GuardModelCallException guardFailure) {
|
||||
return guardFailure.failure();
|
||||
|
||||
@@ -4,12 +4,29 @@ import com.superbiz.agent.harness.contract.SafeFallback;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 停止后的「安全进展快照」:Run 已完成的验证事实 + 限制声明 + 停止原因。
|
||||
*
|
||||
* <p>由 {@link DiagnosisProgressProjector} 在 Agent 停止后投影生成:
|
||||
* 把 Tracker 里的调用 identity 回读 Canonical Store 的 READY 记录,
|
||||
* 重读投影成有界、去重的事实——不是模型自述,是账本背书。
|
||||
*
|
||||
* <p>被 {@code DiagnosisReleaseUseCase} 消费:
|
||||
* 受控停止 / 无结论 / 非法 Draft 三条降级路径都靠它决定发布形态
|
||||
* (有 observed_facts 才允许发布 INSUFFICIENT_EVIDENCE,否则 fail closed)。
|
||||
*/
|
||||
public record DiagnosisProgressSnapshot(
|
||||
/** 已验证来源(去重后):只到「查过哪些来源」粒度,供 FALLBACK 展示。 */
|
||||
List<SafeFallback.VerifiedSource> verifiedSources,
|
||||
/** 已观察事实(去重后):每条 = 来源类型 + 来源 + 范围 + 有界摘要,
|
||||
* 是「Run 真的查过什么、结果如何」的证据性记录(空查询也算事实)。 */
|
||||
List<SafeFallback.ObservedFact> observedFacts,
|
||||
/** 限制声明:无法验真/不可读/截断等原因的诚实说明。 */
|
||||
List<String> limitations,
|
||||
/** 停止原因(受控停止时):信息饱和 / 预算耗尽 / 协议违规。 */
|
||||
DiagnosisStopReason stopReason) {
|
||||
|
||||
/** 防御:三列表全部转不可变,null 视为空列表。 */
|
||||
public DiagnosisProgressSnapshot {
|
||||
verifiedSources = verifiedSources == null ? List.of() : List.copyOf(verifiedSources);
|
||||
observedFacts = observedFacts == null ? List.of() : List.copyOf(observedFacts);
|
||||
@@ -20,6 +37,11 @@ public record DiagnosisProgressSnapshot(
|
||||
return new DiagnosisProgressSnapshot(List.of(), List.of(), List.of(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「是否有安全进展」的判断依据:observedFacts 非空即视为有已验真事实。
|
||||
* release 域的 fail-closed 分支全靠它——没有事实就不能把
|
||||
* 「没查到」伪装成业务结果发布。
|
||||
*/
|
||||
public boolean hasObservedFacts() {
|
||||
return !observedFacts.isEmpty();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,23 @@ import com.superbiz.agent.harness.guard.evidence.VerifiedEvidenceSnapshot;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 发布裁决结果(Release 域唯一出口的结果类型)。
|
||||
*
|
||||
* <p>结构不变量:SUCCESS 必须有 draft 且不允许带 fallback;
|
||||
* FALLBACK 必须有 fallback 且不允许带 draft——
|
||||
* 成功只能带验证过的草稿、降级只能带安全回退,绝无「半真半假」的中间产物。
|
||||
*/
|
||||
public record DiagnosisReleaseResult(
|
||||
ReleaseOutcome outcome,
|
||||
DiagnosisDraft draft,
|
||||
SafeFallback fallback,
|
||||
VerifiedEvidenceSnapshot verifiedEvidence) {
|
||||
|
||||
/**
|
||||
* 结构不变量:outcome 必填;SUCCESS ↔ draft、FALLBACK ↔ fallback 严格互斥;
|
||||
* 本域只支持 SUCCESS / FALLBACK 两个出口(FAILED/CANCELLED 由 Application 层写)。
|
||||
*/
|
||||
public DiagnosisReleaseResult {
|
||||
Objects.requireNonNull(outcome, "outcome must not be null");
|
||||
Objects.requireNonNull(verifiedEvidence, "verifiedEvidence must not be null");
|
||||
|
||||
@@ -26,6 +26,25 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 引用修复器(证据安全链的一环):EvidenceGuard 验真失败后,「只修引用、不修结论」。
|
||||
*
|
||||
* <p>核心约束:
|
||||
* <ul>
|
||||
* <li>prompt 锁死:只能改 analysis_id / tool_call_ids / based_on_analysis_ids
|
||||
* 三个引用字段,结论、分析正文、kind、limitations 一律禁止动;</li>
|
||||
* <li>语义不变性检查:修复前后 {@link SemanticDraftView#hasSameUserVisibleSemantics}
|
||||
* 逐字段比对——用户可见内容一个字节不许变,变了判 SCHEMA_INVALID 重试;</li>
|
||||
* <li>受控调用:与 SemanticGuard 同款全栈衔接(输入计预算、独立重试策略
|
||||
* evidenceRepair、超时限制、GuardModelCall 受控调用、审计 trace)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>为什么调大模型而不是 Harness 机械替换:修引用需要理解语义
|
||||
* (哪条 analysis 该锚哪个调用),机械替换做不到;但修复器的自由度
|
||||
* 被 prompt + 语义不变性双重锁死。
|
||||
*
|
||||
* <p>被 {@code DiagnosisReleaseUseCase.releaseConclusion} 调用。
|
||||
*/
|
||||
public final class EvidenceRepair {
|
||||
|
||||
private final DiagnosisHarnessCore core;
|
||||
@@ -69,6 +88,14 @@ public final class EvidenceRepair {
|
||||
this.prompt = EvidenceRepairPrompt.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:输入(query + 原 draft + 违规清单)序列化并计预算
|
||||
* → 构造 System(prompt)+User(输入) 双消息
|
||||
* → 按 evidenceRepair 重试策略执行模型修复
|
||||
* → 解析修复结果并做语义不变性检查(变了即失败)。
|
||||
*
|
||||
* @return 修复后的 DiagnosisDraft(仅引用字段可能变化)
|
||||
*/
|
||||
public DiagnosisDraft repair(RunContext context, String query, DiagnosisDraft original,
|
||||
List<EvidenceViolation> violations) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
@@ -110,6 +137,7 @@ public final class EvidenceRepair {
|
||||
});
|
||||
}
|
||||
|
||||
/** 严格反序列化修复输出(FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS),失败归类 PARSE_ERROR。 */
|
||||
private DiagnosisDraft parse(String output) {
|
||||
try {
|
||||
return draftReader.readValue(output);
|
||||
@@ -119,6 +147,7 @@ public final class EvidenceRepair {
|
||||
}
|
||||
}
|
||||
|
||||
/** 失败分类:GuardModelCallException 自带 RetryFailure;其余归 UNKNOWN。 */
|
||||
private RetryFailure classify(Exception exception) {
|
||||
return exception instanceof GuardModelCallException failure
|
||||
? failure.failure() : RetryFailure.UNKNOWN;
|
||||
|
||||
@@ -3,6 +3,10 @@ package com.superbiz.agent.harness.release;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* EvidenceRepair 的限额:输入/输出字节 + 单次修复超时。
|
||||
* 防修复器本身成为无底洞(超大 draft 或无限重试)。
|
||||
*/
|
||||
public record EvidenceRepairLimits(
|
||||
long maxInputBytes,
|
||||
long maxOutputBytes,
|
||||
|
||||
@@ -13,11 +13,28 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 安全回退工厂:构造所有 FALLBACK 形态的有界、去重、诚实降级。
|
||||
*
|
||||
* <p>设计要点:
|
||||
* <ul>
|
||||
* <li>诚实降级:降级不抹掉进展——SEMANTIC_UNSUPPORTED / INSUFFICIENT_EVIDENCE
|
||||
* 保留已验证事实(observed_facts / verified_sources)供用户继续排查;</li>
|
||||
* <li>有界:observed_facts 最多 12 条、摘要 320 字符,missing_info 最多 8 条
|
||||
* (绝不泄露 raw / 敏感正文,空摘要降级为受限审计说明文案);</li>
|
||||
* <li>conclusion 恒为 null:降级不发布根因结论;</li>
|
||||
* <li>fail closed:insufficientEvidence 要求 progress 必须有已验真事实,
|
||||
* missingRequiredContext 要求 missing_info 非空,否则拒绝构造。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>被 {@code DiagnosisReleaseUseCase} 的各个降级出口调用。
|
||||
*/
|
||||
public final class SafeFallbackFactory {
|
||||
|
||||
private static final int MAX_OBSERVED_FACTS = 12;
|
||||
private static final int MAX_SUMMARY_CHARS = 320;
|
||||
|
||||
/** 引用验真失败(含 repair 后仍失败):只给违规明细,零事实。 */
|
||||
public SafeFallback evidenceValidationFailed(List<EvidenceViolation> violations) {
|
||||
return fallback(
|
||||
FallbackType.EVIDENCE_VALIDATION_FAILED,
|
||||
@@ -30,6 +47,7 @@ public final class SafeFallbackFactory {
|
||||
issues(violations));
|
||||
}
|
||||
|
||||
/** 结论不被证据支撑(verdict=UNSUPPORTED):保留已验证事实与来源。 */
|
||||
public SafeFallback semanticUnsupported(VerifiedEvidenceSnapshot snapshot) {
|
||||
return fallback(
|
||||
FallbackType.SEMANTIC_UNSUPPORTED,
|
||||
@@ -42,6 +60,7 @@ public final class SafeFallbackFactory {
|
||||
List.of());
|
||||
}
|
||||
|
||||
/** 语义评审技术不可用(超时等):不发布根因,保留已验证事实。 */
|
||||
public SafeFallback semanticUnavailable(VerifiedEvidenceSnapshot snapshot) {
|
||||
return fallback(
|
||||
FallbackType.SEMANTIC_UNAVAILABLE,
|
||||
@@ -54,6 +73,10 @@ public final class SafeFallbackFactory {
|
||||
List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 有限检查但证据不足(受控停止/无结论/非法 draft 降级的共同出口):
|
||||
* 必须已有已验真事实(否则 fail closed),展示检查过的范围 + 缺失项。
|
||||
*/
|
||||
public SafeFallback insufficientEvidence(
|
||||
DiagnosisProgressSnapshot progress, List<String> missingInfo) {
|
||||
Objects.requireNonNull(progress, "progress must not be null");
|
||||
@@ -78,6 +101,7 @@ public final class SafeFallbackFactory {
|
||||
List.of());
|
||||
}
|
||||
|
||||
/** 缺上下文未开始有效查询:只列缺失项,无事实。 */
|
||||
public SafeFallback missingRequiredContext(List<String> missingInfo) {
|
||||
List<String> safeMissingInfo = boundedMissingInfo(missingInfo);
|
||||
if (safeMissingInfo.isEmpty()) {
|
||||
@@ -112,6 +136,10 @@ public final class SafeFallbackFactory {
|
||||
return Objects.requireNonNull(snapshot, "snapshot must not be null").verifiedSources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从验证快照提取去重后的可观察事实:key = 来源类型+来源+范围+摘要,
|
||||
* 最多 12 条、摘要 320 字符;空摘要降级为受限审计说明(不泄露 raw)。
|
||||
*/
|
||||
private List<SafeFallback.ObservedFact> facts(VerifiedEvidenceSnapshot snapshot) {
|
||||
Objects.requireNonNull(snapshot, "snapshot must not be null");
|
||||
Map<String, SafeFallback.ObservedFact> unique = new LinkedHashMap<>();
|
||||
@@ -133,6 +161,7 @@ public final class SafeFallbackFactory {
|
||||
return List.copyOf(unique.values());
|
||||
}
|
||||
|
||||
/** 把违规明细映射为对外可用的 ValidationIssue 列表(code + target)。 */
|
||||
private List<SafeFallback.ValidationIssue> issues(List<EvidenceViolation> violations) {
|
||||
List<SafeFallback.ValidationIssue> result = new ArrayList<>();
|
||||
for (EvidenceViolation violation : violations == null ? List.<EvidenceViolation>of() : violations) {
|
||||
|
||||
Reference in New Issue
Block a user