# Harness 组件全景:职责、设计原因与边界 **更新日期**:2026-07-29 **适用代码**:`src/main/java/com/superbiz/agent/harness` **术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) **配套主文**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md) > 本文是完整组件参考手册,不建议第一次接触 Harness 时顺序阅读。入门请先读 [Harness 阅读入口](README.md),需要逐组理解组件时使用[组件渐进式导读](components/README.md)。 ## 1. 这份文档怎样定义“全部组件” 当前 `harness` 目录包含 10 个一级职责域、189 个 Java 源文件。它们并不都是独立运行的“服务”: - **执行组件**拥有行为,例如 Core、Interceptor、Boundary、Guard、Release、Executor; - **端口与适配器**隔离框架、Redis、JPA、JDBC 和业务 Tool; - **状态与契约类型**固定跨组件语言,防止字符串协议漂移; - **Limits、Prompt 和异常类型**把边界配置与失败语义显式化。 因此本篇先解释 10 个职责域为什么存在,再列出每个生产类型。判断某个类应该放在哪里时,只问三个问题:它拥有什么状态、它能作出什么决定、它绝不能决定什么。 ## 2. 组件地图 | 职责域 | 文件数 | 解决的问题 | 核心组件 | |---|---:|---|---| | `application` | 34 | 谁创建 Run、路由请求、持久化和映射公开结果 | `ChatApplicationUseCase`、三个 Executor、`ChatRunStore` | | `core` | 14 | deadline、取消、预算和唯一终态由谁拥有 | `DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunLifecycle` | | `agent` | 17 | 如何把框架 ReAct 接入 Harness,而不复制 ReAct | `DiagnosisAgentUseCase`、Factory、Model/Tool Interceptor | | `progress` | 14 | 如何识别无增益、重复和协议空转 | `DiagnosisProgressTracker`、Projector、scope normalizer | | `tool` | 49 | Tool 如何安全执行、保存真相并只暴露必要内容 | `ToolBoundary`、Adapters、Projectors、Canonical Store、MySQL sandbox | | `guard` | 15 | 如何分开验证引用真实性和结论支持度 | `EvidenceGuard`、`SemanticGuard`、`GuardModelCall` | | `release` | 6 | 谁拥有最终 SUCCESS / FALLBACK 决策 | `DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory` | | `retry` | 8 | 哪些失败允许重试、attempt 如何可见 | `HarnessRetryExecutor`、Policies、Failure taxonomy | | `audit` | 17 | 如何重放决策而不复制敏感正文 | Trace、Tool audit、Model ledger、Agent hook | | `contract` | 15 | 跨层公开语言如何保持类型化 | Draft、PublishedResult、Fallback、状态枚举 | ```mermaid flowchart LR APP["application
Run 与公开用例"] --> CORE["core
执行不变量"] APP --> AGENT["agent
ReAct 接入"] AGENT --> PROGRESS["progress
收敛控制"] AGENT --> TOOL["tool
证据边界"] APP --> RELEASE["release
唯一发布"] RELEASE --> GUARD["guard
真实性与支持度"] CORE --> RETRY["retry
显式 attempt"] CORE -.-> AUDIT["audit
可观测账本"] AGENT -.-> AUDIT TOOL -.-> AUDIT RELEASE -.-> AUDIT CONTRACT["contract
类型化语言"] -.-> APP CONTRACT -.-> AGENT CONTRACT -.-> TOOL CONTRACT -.-> GUARD CONTRACT -.-> RELEASE ``` ## 3. Application:Run 的应用所有者 ### 为什么需要 Core 只知道一次 Run 是否活跃,并不知道 HTTP、SSE、意图路由、数据库持久化和上一轮上下文。若这些职责塞进 Core,Harness 会变成业务工作流引擎;若散落在 Controller,则每个入口都可能产生不同的终态和 Fallback。 ### 核心组件 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `ChatApplicationUseCase` | 为一次请求建立唯一应用事务边界 | 解析 session、读取历史、创建 Run、路由、执行分支、落终态和安全输出 | 不做诊断推理,不自行构造诊断 Fallback | | `IntentRouter` | 路由也会消耗模型、超时并返回非法 JSON | 在有界输入、timeout 和显式 retry 下输出唯一 `IntentType` | 不执行业务 Tool,不生成最终回答 | | `SystemChatExecutor` | 系统问答不需要 ReAct,但仍必须受模型预算控制 | 单轮回答产品能力和闲聊 | 不声称查询了实时数据 | | `KnowledgeQueryExecutor` | 知识问答需要一次 RAG 和一次受控生成,但不需要完整诊断链 | 调用知识 Tool、校验 Tool result、生成带来源答案 | 首版不进入 SemanticGuard,不执行多 Tool 诊断 | | `DiagnosisChatExecutor` | Agent 执行和安全发布需要一个明确接合点 | 执行 Agent、处理合法停止/非法 Draft、调用 Release、映射公开内容 | 不重复 Guard 或 Release 决策 | | `ChatRunStore` / `JpaChatRunStore` | 内存 Run 状态与长期数据库状态职责不同 | 保存 Run 开始、intent、结果、预算摘要;读取安全上一轮 | 不保存 canonical raw;只允许安全发布结果进入 PreviousTurn | | `PublishedResultPolicy` | 直接复用上一轮完整结果会让上下文无限增长并传播失败内容 | 生成可持久化 PublishedResult 和有界 PreviousTurn | Fallback、失败、raw evidence 不进入下一轮 | ### 类型清单 | 类型 | 分类与功能 | |---|---| | `ChatApplicationRequest`、`ChatApplicationResult` | 应用入口和出口 DTO;固定 session、run、intent、outcome 和内容类型 | | `ChatApplicationContent`、`ChatContentType` | 公开内容的 sealed/typed 边界,避免任意对象直接发给 SSE | | `DiagnosisContent`、`KnowledgeContent`、`SystemChatContent`、`FallbackContent` | 四种公开内容载体;分别包装安全诊断、知识答案、系统回答和 Fallback | | `ChatApplicationStatus`、`ChatApplicationObserver` | 向 SSE 报告有界阶段,不泄露模型内部步骤 | | `ChatRunControl` | 只向入口暴露 exact session/run 和客户端断开取消能力 | | `ChatApplicationException`、`ChatFailureCode` | 把内部异常映射为稳定、可公开的失败语义 | | `IntentRouting`、`SystemChatOperation`、`KnowledgeQueryOperation`、`DiagnosisOperation` | 四个应用端口;使主用例不依赖具体模型或执行器 | | `DiagnosisExecutionResult` | Diagnosis 分支的内部返回,携带 outcome、content、published result 和预算已处理标记 | | `IntentRouterInput`、`IntentRouterLimits`、`IntentRouterPrompt`、`IntentRoutingException` | 路由输入、边界、Prompt 和失败类型 | | `KnowledgeQueryLimits`、`SingleTurnExecutorLimits` | 知识与单轮模型路径的输入、输出、timeout 上限 | | `ChatRunStore`、`RoutingHistory` | 持久化端口及最小路由历史 | | `PreviousTurnLimits`、`PublishedResultPolicy` | 安全历史的字段/大小限制与投影策略 | ## 4. Core:每个 Run 的执行不变量 ### 为什么需要 模型、Tool、Guard 和 Application 都要检查取消、deadline 和预算。如果每层各自维护计数或终态,会出现多个真相源;如果依赖 ThreadLocal,则异步线程无法可靠继承。 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `DiagnosisHarnessCore` | 所有执行边界需要同一套 active / budget / terminal 规则 | 创建 RunContext,执行模型/Tool/Token/bytes 门禁,处理取消和终态 | 不持久化,不调用 Agent/Tool,不维护全局 Run Map | | `RunContext` | Run 身份和状态句柄必须一起显式传播 | 固定 sessionId、runId、deadline 及 per-run handles | record 结构不可变,不代表内部计数不变化 | | `RunLifecycle` | 成功、失败、取消可能竞态到达 | 原子 `compareAndSet` 实现 first-terminal-wins | 不映射公开 ReleaseOutcome | | `RunCancellation` | 取消原因和回调只能被第一个请求确定 | first-reason-wins,并通知 lifecycle/资源回调 | 不承诺强杀同步 Provider 请求 | | `RunBudget` | 多维预算必须原子地先检查再计数 | 模型、Tool、单 Tool、输入/输出/总 Token 和 bytes 计量 | 不判断信息是否有价值 | | `RunCapacityCounter` | bytes 可能由并发边界累计 | CAS 方式维护 Run 总容量 | 不负责字段级截断策略 | ### 类型清单 | 类型 | 分类与功能 | |---|---| | `RunBudgetLimits`、`RunBudgetUsage` | 预算配置和值快照;区分限制与已使用量 | | `BudgetKind`、`BudgetExceededException` | 明确指出耗尽的是模型、Tool、Token 还是 bytes | | `RunState`、`RunTermination` | 内存执行状态与不可变终止快照 | | `RunCancellationReason` | 客户端断开、用户请求、deadline、预算和内部失败的取消分类 | | `RunAbortedException` | 将已经确定的 RunTermination 穿过深层调用栈,不丢失终态 | ## 5. Agent:框架 ReAct 与 Harness 的接合层 ### 为什么需要 业务需要框架原生 Tool Calling 和 ReAct loop,但框架默认并不知道项目的 RunContext、预算、审计、Tool 双视图和停止协议。接合层的目标是“拦截边界”,不是重新实现 Agent 循环。 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `DiagnosisAgentFactory` | 每个 Run 的 interceptor 和 metadata 不同 | 为当前 Run 创建 ReactAgent,注册 Tool callback、Prompt、Hook 和 interceptor | 不缓存跨 Run Agent 状态 | | `DiagnosisAgentUseCase` | 框架输入输出是字符串,业务要求严格 Draft 和 bytes 边界 | 序列化输入、显式注入 Run metadata、调用 Agent、严格解析 Draft、映射受控停止 | 不执行证据和语义校验 | | `HarnessModelInterceptor` | 每一轮 ReAct 模型调用都必须进入预算和 Token 账本 | 调用前 reserve,调用后记录 Provider usage 并再次检查 active | 不重试模型 | | `HarnessToolInterceptor` | 模型 Tool Call 中混有 Harness 进展协议和业务参数 | 校验 Envelope、上一轮增益、重复/饱和、调用 Tool、投影 observation、交付 STOP_REQUIRED | 不执行 backend,不复制 ToolBoundary 预算 | | `HarnessEvidenceTools` | Tool schema 必须由服务端原生注册,且业务 Tool 可选启用 | 注册 RAG/log/MySQL callback,严格解析通用 Envelope,桥接 Adapter | Prompt 不写死 Tool schema;未配置 MySQL 时不暴露死 Tool | | `ToolResultViewProjector` | canonical agent_result 仍含 Harness 控制字段 | 生成 Control View 和白名单 Model Observation | 不读取 raw response,不判断根因 | ### 类型清单 | 类型 | 分类与功能 | |---|---| | `DiagnosisAgentInput`、`DiagnosisAgentExecution` | Agent 用例输入,以及 Draft/ProgressSnapshot/stop reason 的执行结果 | | `DiagnosisAgentLimits` | query、previous turn、总输入和 Draft 的 UTF-8 bytes 上限 | | `DiagnosisAgentPrompt`、`DiagnosisDraftOutputSchema` | 最小职责 Prompt 与严格结构化输出 Schema | | `EvidenceToolInvoker` | Agent 层到具体 Adapter 的函数端口 | | `ParsedAgentToolCall` | 解包后的 previous observation、typed business input 和 JSON 参数 | | `ToolControlView` | Harness 消费的 evidence status、count、scope 等控制视图 | | `DiagnosisAgentLimitException` | Agent 输入或输出越界 | | `DiagnosisAgentOutputException` | 空、非法 JSON、Schema 不合格 Draft,并可携带安全 ProgressSnapshot | | `DiagnosisCollectionStoppedException` | STOP_REQUIRED 后仍请求 Tool 时,把受控停止穿出框架 loop | ## 6. Progress:从资源上限到正常收敛 ### 为什么需要 预算只能阻止无限消耗,不能识别“连续查询没有推进诊断”。Progress 子系统只保存 Run 内最小控制状态,不复制完整证据。 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `DiagnosisProgressTracker` | 连续无增益、待评价调用和协议错误需要线程安全单一所有者 | 记录 completed scope、pending evaluation、NO_GAIN、协议错误、饱和和一次停止指令 | 不保存 raw/agent_result,不判断非空内容的业务价值 | | `ToolScopeNormalizer` | 字段顺序或无关格式不应绕过重复检测 | 将各 Tool typed input 规范化为稳定 scope | 首版不做自然语言语义去重 | | `DiagnosisProgressProjector` | 受控停止或非法 Draft 后仍需安全说明已检查内容 | 按 Tracker identity 回读 canonical READY 记录,生成有界事实、来源和限制 | 无法验真的记录直接排除,不输出 raw | | `DiagnosisProgressProjection` | Agent 用例不应依赖具体 Redis projector | 定义 Run 到安全快照的端口,并提供 empty 实现 | 不决定 Fallback 类型 | ### 类型清单 | 类型 | 分类与功能 | |---|---| | `InformationGain` | 仅 `GAINED / NO_GAIN`,避免引入含混质量等级 | | `DiagnosisCollectionState` | `COLLECTING / SATURATED`,只描述信息收集状态 | | `DiagnosisStopReason` | 区分信息饱和、预算限制和进展协议错误 | | `PreviousObservation` | 模型在下一次 Tool Call 回传上一轮 `tool_call_id + information_gain` | | `CompletedToolCall`、`ToolScopeIdentity` | 保存已完成调用的 identity 和规范化 scope,不保存 payload | | `DiagnosisProgressSnapshotState` | Tracker 的内部控制快照,含计数、pending ID 和停止指令状态 | | `DiagnosisProgressSnapshot` | Release 可消费的安全快照,含 verified sources、observed facts 和 limitations | | `ProgressProtocolViolationType`、`ProgressProtocolViolationException` | 缺字段、乱序评价、意外评价和非法 Envelope 的稳定分类 | ## 7. Tool:执行、真相、投影与后端安全 Tool 是文件最多的职责域,但可以按四层理解。 ### 7.1 Boundary:所有 Tool 共用的确定性入口 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `ToolBoundary` | 每个 Adapter 自己实现授权、预算、store 和 audit 会产生漂移 | 统一 preflight、active、read-only、Tool budget、bytes、canonical 状态迁移和 audit | 不理解 Tool 业务内容;不做信息增益判断 | | `ToolCallRequestEnvelope` | 调用必须同时证明 Run、ID、Tool、参数、授权和只读意图 | Boundary 的内部调用信封 | 不等同于模型侧 progress Envelope | | `ToolExecutor` | Boundary 不依赖具体 backend | raw 执行函数端口 | 不投影结果 | | `ToolResultProjector` | raw 到 canonical agent_result 的逻辑因 Tool 而异 | 标准化、限制、计算客观 evidence status | 不判断结论支持度 | | `ToolBoundaryResult` | 只允许 READY 或 ERROR 离开 Boundary | 向上返回 ID、状态、agent result、evidence status 或稳定错误码 | PROJECTING 不对外暴露 | | `ProjectedToolResult` | projector 同时返回有界 agent result 与客观状态 | Boundary/store 的中间值 | 不是最终 Model Observation | | `ToolBoundaryErrorCode` | 不能把内部异常正文交给 Agent | 固定非法 ID、Run mismatch、未授权、非只读、超限、执行/投影/store 等错误 | 不包含敏感原因 | ### 7.2 Canonical Store:TTL 内的完整 Tool 真相 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `CanonicalInvocationStore` | Guard 需要独立于 Agent 上下文读取原始调用真相 | 定义 begin/find/markReady/markError 状态端口 | 不负责长期审计 | | `RedisCanonicalInvocationStore` | 完整 request/raw 有敏感性和容量,适合短期 TTL 存储 | 原子创建,保持剩余 TTL 的状态更新,读取不续期 | 只有该 Adapter 访问 Redis | | `CanonicalToolInvocation` | request、raw、agent_result 和两个状态必须形成合法组合 | 封装 PROJECTING -> READY/ERROR 转换及 Run 可引用判断 | ERROR/PROJECTING 不可被 EvidenceGuard 引用 | | `CanonicalInvocationLimits` | Redis record、raw 候选和 agent result 需要独立硬限制 | 统一 UTF-8 bytes 校验 | 不执行截断 | | `ToolCallKeyFactory` | Key 要隔离 Run 且保留框架 ID | 校验安全 segment,生成 prefix/runId/toolCallId | 不生成或改写 tool_call_id | | `DuplicateInvocationException`、`InvocationStateException`、`CanonicalStoreException`、`ResultTooLargeException` | Store 失败需要可分类而不是字符串猜测 | 区分重复、非法迁移、基础设施错误和容量错误 | 最终对 Agent 仍映射为安全错误码 | ### 7.3 Adapter 与 Projector:隔离业务后端 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `RagToolAdapter` | 检索实现会演进,但 Agent schema 和 Boundary 不应随之变化 | 解析 RAG request,经 Boundary 调 backend 和 `RagResultProjector` | 不把检索轨迹直接给模型 | | `QueryLogsToolAdapter` | 现有日志 Tool 的返回和时间语义需要规范化 | 校验范围、桥接 backend、投影日志事件 | 0 命中是 `NO_EVIDENCE`,不是技术失败 | | `MysqlToolAdapter` | LLM 生成 SQL 必须经过授权和只读沙箱 | 解析 request、先校验 SQL,再经 Boundary 执行和投影 | 未配置数据源时 Tool 不注册 | | `RagResultProjector` | 上游 evidence、score、relevance 表达不稳定 | 输出有界证据,并保留粗粒度 `relevance_level` | 非空/REFERENCE 不等于支持根因 | | `QueryLogsResultProjector` | raw 日志不可直接进入上下文 | 生成有界 events、pattern、scope 和截断标记 | 不泄露无限日志正文 | | `MysqlResultProjector` | JDBC rows 和元数据需要稳定 Agent contract | 限制行列、单元格和 bytes,输出 rows/columns/scope | 不执行 SQL 安全判断 | | `ToolProjectionLimits`、`MysqlToolLimits` | 各投影边界必须集中、可测试 | 配置证据数、文本、行列和结果上限 | 不改变 Run 总 bytes 预算 | ### 7.4 MySQL 只读沙箱 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `MysqlSqlValidator` | `readOnly=true` 声明不能证明 SQL 安全 | 解析并限制单条 SELECT、数据源、schema/table/column、limit 等 | 不执行查询 | | `MysqlDataSourceDefinition` | 连接存在不代表 Agent 可访问所有表列 | 保存逻辑数据源和 allowlist | 不携带密码 | | `MysqlQueryPlan` | 校验后的内容不应在 Executor 再解析原始请求 | 固定已批准数据源、SQL 和限制 | 只能由 Validator 产生 | | `MysqlReadOnlyExecutor` / `JdbcMysqlReadOnlyExecutor` | JDBC 细节与 Harness Boundary 解耦 | 在只读连接、timeout 和 row limit 下执行 plan | 不接收未经验证的 request | | `MysqlRawResult` | JDBC 原始但结构化的执行结果 | 携带 columns、rows、truncated、duration | 仍需 Projector 后才能进入 canonical agent_result | | `MysqlSecurityException` | 安全拒绝必须与基础设施错误区分 | 表达非法 SQL、未授权对象等 | 不向 Agent泄露详细策略 | ### 7.5 Tool contract 完整清单 | 类型 | 设计原因与功能 | |---|---| | `AgentToolContracts` | Tool 名、描述和服务端注册契约的唯一常量源,防止 Prompt/代码漂移 | | `RagToolCall`、`QueryLogsToolCall`、`MysqlToolCall` | 模型侧统一 Envelope:`previous_observation + input` | | `RagToolRequest`、`QueryLogsRequest`、`MysqlToolRequest` | 业务 Tool 的 typed input;Interceptor 解包后仍保持原业务协议 | | `RagToolResult`、`QueryLogsToolResult`、`MysqlToolResult` | canonical agent-facing 标准结果,不等同于 raw backend response | | `RagEvidence`、`SourceDocument` | 有界知识证据及文档身份 | | `RagRelevanceLevel` | `PRECISE / HIGHLY_RELEVANT / REFERENCE` 等客观检索相关度,不是诊断置信度 | | `LogQueryScope`、`LogEvent`、`LogPattern` | 日志查询实际范围、事件和聚合模式 | | `LogSourceKind`、`LogTopic` | 限制日志来源和主题的可选集合 | | `ToolContractCollections` | 对 contract 集合做 defensive copy 和空值规范化 | ## 8. Guard:把两个验证命题分开 ### 8.1 Evidence Guard | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `EvidenceGuard` | 模型不能被信任去验证自己引用的 ID 和 Run 归属 | 严格验证 Draft、analysis ID、canonical READY、当前 Run 所有权、evidence status 和引用闭包 | 不调用模型,不判断结论语义是否成立 | | `EvidenceGuardResult` | 校验只能是 verified snapshot 或 violations | 阻止半有效结果继续发布 | 不包含原始 Draft 修复逻辑 | | `VerifiedEvidenceSnapshot` | SemanticGuard 只能看到已验真的证据投影 | 汇总 verified analyses 和 sources | 不包含 raw Tool payload | | `VerifiedAnalysisEvidence`、`VerifiedEvidence` | 保持 analysis 到证据的归属关系 | 提供来源、scope、excerpt 等安全证据 | 不提升为业务结论 | | `EvidenceViolation`、`EvidenceViolationCode` | Repair 和 Fallback 需要稳定失败原因 | 标识缺 analysis、未知调用、Run mismatch、非 READY、引用不闭合等 | 不保存内部异常栈 | ### 8.2 Semantic Guard 与模型调用边界 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `SemanticGuard` | 引用真实仍可能无法支持结论 | 用隔离单轮调用输出 `SUPPORTED / UNSUPPORTED`,严格解析并有限 retry | 无 Tool、无记忆、不访问 Redis、不改写 Draft | | `GuardModelCall` | Router、单轮回答、Repair、Semantic 都需一致的模型预算、timeout 和 bytes 控制 | 在线程池中执行单轮 ChatModel,记录 usage,超时取消 future | 不决定业务 retry policy | | `SemanticDraftView` | Guard/Repair 只应比较用户可见语义 | 从 Draft 提取稳定语义视图,并判断修复前后是否一致 | 不包含引用实现细节 | | `SemanticGuardInput`、`SemanticGuardDecision` | 固定 Guard 输入和带 verdict 的输出 | 只传 query、Draft view 和 verified snapshot | 不传 Prompt 历史或 raw Tool response | | `SemanticGuardLimits`、`SemanticGuardPrompt` | 输入、输出、单次/总 timeout 和判定职责可测试 | 限定一次语义审查的成本与 Prompt | 不暴露给 Diagnosis Agent | | `GuardModelCallException` | 单轮模型失败要按 timeout、transport、parse、schema 分类 | 为 Harness retry 提供类型化信号 | 不直接映射用户内容 | ## 9. Release:唯一公开决策点 ### 为什么需要 如果 Agent、Guard、Application 都可以各自构造结果,同一个失败会出现不同用户语义,迟到内容也可能绕过安全校验。Release 必须集中回答一个问题:当前 Run 有哪些内容可以公开? | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `DiagnosisReleaseUseCase` | Draft、停止、Guard 和 Repair 的组合分支必须只有一个所有者 | 处理有结论、无结论、受控停止、非法 Draft;协调 Guard/Repair/Semantic;返回成功或 Fallback | 不持久化、不发送 SSE、不写新结论 | | `EvidenceRepair` | 引用或结构小错不应总是丢掉语义正确的 Draft | 单轮修复引用,严格解析,并用 `SemanticDraftView` 保证用户语义不变 | 只 attempt 一次;不新增事实、不改结论 | | `SafeFallbackFactory` | 失败文案若交给模型生成会再次引入幻觉 | 确定性构造 evidence failed、semantic unsupported/unavailable、insufficient evidence、missing context | 只使用已验真事实和有界问题码 | | `DiagnosisReleaseResult` | 下游不能同时收到 Draft 和 Fallback | 类型化承载 outcome、Draft、verified evidence 或 SafeFallback | 不等同于 RunState | | `EvidenceRepairLimits`、`EvidenceRepairPrompt` | Repair 的成本与职责必须比 Diagnosis Agent 更窄 | 限制输入/输出/timeout,固定只修引用的指令 | 不允许 Tool Calling | ## 10. Retry:显式、类型化、可审计的 attempt ### 为什么需要 重试会改变成本、延迟和副作用,必须是调用者的有意识决策。统一 Executor 负责循环,但具体组件持有自己的 Policy。 | 类型 | 设计原因与功能 | |---|---| | `HarnessRetryExecutor` | 在每个 attempt 前检查 Run active,统一记录成功/失败,并绝不吞掉取消和预算耗尽 | | `HarnessRetryPolicies` | 集中定义 Router/SemanticGuard 最多两次,其余一次的严格策略 | | `RetryPolicy` | `maxAttempts + retryable failures` 的不可变值,避免布尔 `retry=true` | | `RetryFailure` | timeout、transport、parse、schema、invalid output、cancel、budget 等稳定分类 | | `RetryAttempt` | 记录 attempt 序号、是否成功和失败类型,供 Trace 使用 | | `RetryOperation`、`RetryFailureClassifier` | 将执行和异常分类作为端口注入,Executor 不依赖具体模型组件 | | `RetryExecutionException` | 重试终止时保留最终 attempt 和失败分类 | ## 11. Audit:记录控制事实,而不是复制业务正文 ### 为什么需要 诊断系统需要回答“为什么停止、调用了什么、Token 是否对账、发布为何降级”,但普通观察面不应长期保存 Prompt、SQL、日志 query、raw response 或 reasoning。 | 组件 | 为什么设计 | 作用 | 明确边界 | |---|---|---|---| | `DiagnosisTraceRecorder` / `JpaDiagnosisTraceRecorder` | 所有阶段需要同一 exact-run timeline | 按 Run 分配 sequence,追加安全事件;失败 best-effort | 不改变业务结果,不保存敏感正文 | | `TraceAuditEvents` | 各组件手写 details 容易字段漂移或泄露 | 集中构造 run/routing/model/tool/progress/guard/release 事件 | 只接受有界、安全字段 | | `DiagnosisTraceAuditEvent` | Recorder 与业务组件解耦 | 统一事件 identity、phase、type、status、details | 不是领域事件总线 | | `ToolInvocationAuditSink` / `JpaToolInvocationAuditSink` | canonical raw 不能长期保存,但调用元数据要留存 | 保存 exact Run、Tool ID、状态、耗时、bytes 和有界 enrichments | 不保存完整 request/raw/agent result | | `RagLookupAuditEnricher` | RAG 需要有限的质量诊断字段 | 从受限输入提取 step/query/relevance 等安全摘要 | 不打印完整 rewritten/raw query | | `HarnessAgentAuditHook` | 框架每轮 Agent 模型步骤需关联数据库 step 和 Provider reasoning 可用性 | 写 AgentStep metadata,并把 reasoning/assistant text 放独立受限存储 | reasoning 不进普通 Timeline、Guard 或下一轮上下文 | | `AgentStepAuditTracker` | Tool audit 需要关联当前 Agent step,但不能使用 ThreadLocal | 按 runId 显式绑定/查询/清理 stepId | 不保存 Step 实体 | | `ModelCallAuditor` / `ModelCallLedger` | 所有模型入口都要按组件、轮次对账 Token | begin call、记录 Provider usage、写 MODEL_TOKEN_USAGE、生成 Run reconciliation | Usage 缺失时标 unavailable,不估算 | | `RunConclusionExtractor` | Trace/DB 常需直接读取安全发布结论 | 从 public JSON 提取 conclusion | 不读取 Provider reasoning | ### Audit 类型清单 | 类型 | 分类与功能 | |---|---| | `ModelCallComponent` | Router、System、Knowledge、Diagnosis、Repair、Semantic 的统一组件枚举,并映射 Trace phase | | `ToolInvocationAuditEvent` | durable Tool metadata 事件 | | `TracePhase`、`TraceEventType`、`TraceEventStatus` | Timeline 的阶段、事件和结果词汇表 | ## 12. Contract:跨组件唯一语言 ### 为什么需要 Harness 横跨模型 JSON、Java 对象、Redis、JPA 和 SSE。若状态以自由字符串在各层重复定义,`SUCCESS`、`READY`、`EVIDENCE_FOUND` 很容易被混为一谈。Contract 包将不同维度保持正交。 | 类型 | 为什么存在与表达什么 | |---|---| | `DiagnosisDraft` | Diagnosis Agent 唯一结构化输出;analysis 绑定 Tool Call 引用,允许 `conclusion=null` | | `KnowledgeAnswerDraft` | Knowledge Query 单轮模型输出,答案项绑定文档来源 | | `PublishedResult` | 只有成功、安全的诊断结果可持久化为下一轮候选 | | `PreviousTurn` | PublishedResult 的有界历史投影,不是完整会话记录 | | `SafeFallback` | 确定性公开降级结构,包含 verified sources、observed facts、limitations、next steps 和 validation issues | | `SourceDocument` | 知识回答中的文档身份与来源 | | `IntentType` | `SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS` 路由结果 | | `InvocationStatus` | Tool invocation 生命周期:`PROJECTING / READY / ERROR` | | `EvidenceStatus` | Tool 客观结果:`EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | | `SemanticVerdict` | 最终推论支持度:`SUPPORTED / UNSUPPORTED` | | `ReleaseOutcome` | Harness 发布结果:`SUCCESS / FALLBACK / FAILED / CANCELLED` | | `SseOutcome` | 当前未被运行时消费的遗留枚举;公开 `done` 实际使用 `ReleaseOutcome`,不要把它作为 SSE 协议真理源 | | `FallbackType` | evidence validation、semantic、insufficient evidence、missing context 等降级原因 | | `AnalysisKind` | 区分正向证据分析与限定范围的负向观察 | | `ContractCollections` | 对公共 contract 集合做 defensive copy、去空和不可变处理 | ## 13. 配置装配:组件如何真正连起来 `HarnessChatConfiguration` 不在 `harness` 包内,但它是运行时组件图的 Composition Root: - 构造两个有界线程池:Chat worker 与 Harness model executor; - 从 `ChatHarnessProperties` 创建 Run、预算、停止阈值和各模型调用 limits; - 装配 Redis canonical store、ToolBoundary、三类 Adapter/Projector; - 仅在存在有效逻辑数据源时注册 `query_mysql`; - 为每个 Run 创建带 Model/Tool interceptor 和 Audit Hook 的 Agent; - 装配 Guard、Repair、Release、Router、三个执行分支和顶层 Application UseCase。 它存在的原因是让所有限制和替换点在一个地方可见。组件内部不应自行读取 Spring 配置或寻找全局 Bean,否则 focused test 很难证明其边界。 ## 14. 用调用链快速定位组件 | 想回答的问题 | 首先阅读 | 接着阅读 | |---|---|---| | 一次请求如何创建并结束 Run | `ChatApplicationUseCase` | `DiagnosisHarnessCore`、`JpaChatRunStore` | | ReAct 每轮如何计预算 | `DiagnosisAgentFactory` | `HarnessModelInterceptor`、`ModelCallAuditor` | | Tool 为什么被拒绝 | `HarnessToolInterceptor` | `DiagnosisProgressTracker`、`ToolBoundary` | | Tool 结果为何没有原样给模型 | `ToolBoundary` | 具体 ResultProjector、`ToolResultViewProjector` | | 一条 evidence_ref 如何验真 | `EvidenceGuard` | `CanonicalToolInvocation`、具体 ToolResult contract | | 为什么最终是 FALLBACK | `DiagnosisReleaseUseCase` | `SafeFallbackFactory`、Trace 的 RELEASE 事件 | | 为什么诊断停止继续查 | `DiagnosisProgressTracker` | Tool progress / rejection / collection stop Trace | | Token 为什么对不上 | `ModelCallAuditor` | `ModelCallLedger`、`RUN_FINISHED` reconciliation | ## 15. 组件边界自检 未来新增能力时,可以用下面的问题判断放置位置: 1. 它是在判断业务根因吗?应留在 Diagnosis Agent,而不是 Core/Tool/Guard。 2. 它能由代码机械证明吗?应放在 Boundary、Tracker 或 EvidenceGuard。 3. 它需要完整 Tool 真相吗?读取 canonical store,不要从 Agent observation 反推。 4. 它要决定公开内容吗?只能进入 Release,不要在 Application 或 Guard 私自构造。 5. 它是短期验真数据还是长期运营元数据?前者 canonical,后者 metadata audit。 6. 它会再次调用模型吗?必须进入 Core budget、ModelCallAuditor、timeout 和显式 retry policy。 7. 它引入了新的状态吗?先确认是否只是 error code、stop reason 或 fallback reason,避免再造全局生命周期。