Compare commits
3
Commits
7ae9707a3b
...
3a7eee8af4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a7eee8af4 | ||
|
|
584639fa2a | ||
|
|
bdac35567c |
@@ -14,7 +14,7 @@ lookup_knowledge 同文档多 chunk 在后处理与投影阶段被 source 级去
|
||||
|
||||
## 范围
|
||||
|
||||
Delivery 1 only(见 `docs/Milvus-Hybrid接入清单.md` §1.1)。
|
||||
Delivery 1 only(见 `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1)。
|
||||
|
||||
## 非目标
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Reference files:
|
||||
- `RagResultProjector.java`
|
||||
- `LookupKnowledgeToolTest.java`
|
||||
- `RagResultProjectorTest.java`
|
||||
- `docs/Milvus-Hybrid接入清单.md`
|
||||
- `mvp/engineering/rag/Milvus-Hybrid接入清单.md`
|
||||
|
||||
Stack notes:
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
## 规格证据
|
||||
|
||||
- 旧 `openspec/specs/rag-knowledge-retrieval` 要求 source 级 dedup(本 change 以 delta 修正)
|
||||
- `docs/Milvus-Hybrid接入清单.md` §1.1 定义 Delivery 1 地基
|
||||
- `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1 定义 Delivery 1 地基
|
||||
|
||||
## 验证证据
|
||||
|
||||
|
||||
+56
-101
@@ -1,120 +1,75 @@
|
||||
# 文档索引
|
||||
|
||||
## 📂 目录结构
|
||||
**更新日期**:2026-07-29
|
||||
|
||||
```
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
docs/
|
||||
├── README.md # 项目文档总览
|
||||
├── INDEX.md # 本索引文件
|
||||
│
|
||||
├── learning/ # 📚 学习笔记(个人学习理解)
|
||||
│ ├── 00-项目学习路径.md
|
||||
│ ├── 01~08-*.md # 按学习顺序编号
|
||||
│ └── README.md
|
||||
│
|
||||
├── analysis/ # 🔍 分析笔记(代码/问题分析)
|
||||
│ ├── essence-report-*.md
|
||||
│ ├── explore-report.md
|
||||
│ ├── chunking-issues-analysis.md
|
||||
│ └── 功能分析报告.md
|
||||
│
|
||||
├── reports/ # 📝 临时报告(修复/验证报告)
|
||||
│ ├── 修复报告-*.md
|
||||
│ ├── 验证报告-*.md
|
||||
│ └── 日志配置完成总结.md
|
||||
│
|
||||
└── guides/ # 📖 指南文档
|
||||
└── 日志配置与分析指南.md
|
||||
├── INDEX.md # 本索引
|
||||
├── learning/ # 早期学习笔记(可能过时)
|
||||
├── analysis/ # 早期代码/问题分析
|
||||
├── reports/ # 历史修复/验证报告
|
||||
└── guides/ # 操作指南
|
||||
|
||||
mvp/ # 现行 MVP 文档(主入口)
|
||||
├── architecture/ # 现行架构:系统现在怎么跑
|
||||
├── engineering/ # 工程纪要:问题 / 决策 / E2E
|
||||
├── issues/ # 未完成事项
|
||||
├── tables/ # 表结构
|
||||
├── demo/ # Demo
|
||||
└── eval/ # 诊断评测材料
|
||||
```
|
||||
|
||||
**⚠️ 注意:MVP 架构设计文档已移至项目根目录 `../mvp/`**
|
||||
|
||||
查看 [mvp/README.md](../mvp/README.md) 了解 MVP 架构、数据库设计、实施计划等。
|
||||
**现行架构、工程纪要、表结构、Issue 均以 [mvp/README.md](../mvp/README.md) 为准。**
|
||||
本目录 `learning/` / `analysis/` / `reports/` 偏早期学习与历史记录,**可能与当前实现不一致**。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速导航
|
||||
## 快速导航
|
||||
|
||||
### 我是新人/学习者
|
||||
1. [项目学习路径](learning/00-项目学习路径.md) - 从这里开始
|
||||
2. [learning/README.md](learning/README.md) - 学习笔记索引
|
||||
3. 按编号顺序阅读 `learning/` 目录下的文档
|
||||
### 我是开发者(优先)
|
||||
|
||||
### 我是开发者
|
||||
👉 **MVP 架构设计文档已移至 `../mvp/`**
|
||||
1. [mvp/README.md](../mvp/README.md) — MVP 总入口
|
||||
2. [mvp/architecture/](../mvp/architecture/) — 现行架构
|
||||
3. [mvp/engineering/](../mvp/engineering/) — 工程纪要(RAG / 诊断 E2E)
|
||||
4. [mvp/issues/](../mvp/issues/) — 活跃 Issue
|
||||
|
||||
请查看 [mvp/README.md](../mvp/README.md) 了解:
|
||||
- MVP 架构设计
|
||||
- 数据库设计和表结构
|
||||
- 实施规划(Phase 1/2/3)
|
||||
- 会话管理设计
|
||||
### RAG 工程纪要(已迁入 mvp)
|
||||
|
||||
### 我要查看分析报告
|
||||
1. [分析笔记目录](analysis/) - 代码分析和问题分析
|
||||
2. [临时报告目录](reports/) - 修复和验证报告
|
||||
1. [RAG 排序:多路召回与 RRF](../mvp/engineering/rag/RAG排序-多路召回与RRF.md)
|
||||
2. [Hybrid 之后的 qualityScore 与后处理](../mvp/engineering/rag/RAG-Hybrid质量分与后处理.md)
|
||||
3. [Agent 如何读 relevance_level](../mvp/engineering/rag/RAG-Agent如何读relevance_level.md)
|
||||
4. [RAG 离线评测:基线设计](../mvp/engineering/rag/RAG离线评测-基线设计.md)
|
||||
5. [Milvus hybrid 接入清单](../mvp/engineering/rag/Milvus-Hybrid接入清单.md)
|
||||
6. [RAG 审计补丁 E2E](../mvp/engineering/rag/RAG审计补丁-stepid-query-E2E验收.md)
|
||||
7. 架构对照:[RAG 知识检索架构](../mvp/architecture/RAG知识检索架构.md)、[RAG 检索可观测性与审计](../mvp/architecture/RAG检索可观测性与审计.md)
|
||||
|
||||
### RAG 设计讨论(docs 根目录)
|
||||
1. [RAG 排序:多路召回与 RRF](RAG排序-多路召回与RRF.md) - K、融合、L0 边界
|
||||
2. [Hybrid 之后的 qualityScore 与后处理](RAG-Hybrid质量分与后处理.md) - 上一代问题、L2 伪装、统一归一化
|
||||
3. [Agent 如何读 relevance_level](RAG-Agent如何读relevance_level.md) - 粗相关度标签的含义与误读
|
||||
4. [RAG 离线评测:讨论、设计与落地](RAG离线评测-基线设计.md) - Golden/Fixture、流程图、hybrid 对齐与闸门修复
|
||||
5. [Milvus hybrid 接入清单](Milvus-Hybrid接入清单.md)
|
||||
6. [RAG Trace / 审计(架构)](../mvp/architecture/RAG检索可观测性与审计.md) - 请求内 trace、tool_invocation、Trace API
|
||||
### 诊断全流程(已迁入 mvp)
|
||||
|
||||
### 诊断全流程(E2E 导读)
|
||||
1. [一次诊断到底发生了什么](一次诊断全流程-E2E导读.md) - SUCCESS 全流程:阶段拆解、token、timeline、字段词典
|
||||
2. [RAG 审计补丁 E2E:step_id + query](RAG审计补丁-stepid-query-E2E验收.md) - 审计字段 live 验收(含业务 FALLBACK 样本)
|
||||
1. [一次诊断到底发生了什么](../mvp/engineering/diagnosis/一次诊断全流程-E2E导读.md)
|
||||
|
||||
### 我是新人 / 想看早期学习笔记
|
||||
|
||||
1. [项目学习路径](learning/00-项目学习路径.md)
|
||||
2. [learning/README.md](learning/README.md)
|
||||
3. 注意:内容可能过时,实现以 `mvp/architecture` 为准
|
||||
|
||||
### 分析 / 历史报告 / 指南
|
||||
|
||||
- [analysis/](analysis/) — 早期分析
|
||||
- [reports/](reports/) — 历史修复与验证报告
|
||||
- [guides/日志配置与分析指南.md](guides/日志配置与分析指南.md)
|
||||
|
||||
---
|
||||
|
||||
## 📚 学习笔记 (learning/)
|
||||
## 文档维护
|
||||
|
||||
按学习顺序编号,建议按顺序阅读:
|
||||
|
||||
1. [00-项目学习路径](learning/00-项目学习路径.md)
|
||||
2. [01-AI-Ops-核心设计-Essence报告](learning/01-AI-Ops-核心设计-Essence报告.md)
|
||||
3. [02-outputKey-深度解析](learning/02-outputKey-深度解析.md)
|
||||
4. [03-核心疑问解答](learning/03-核心疑问解答.md)
|
||||
5. [04-RAG-分块策略-Essence报告](learning/04-RAG-分块策略-Essence报告.md)
|
||||
6. [05-文件上传自动索引-Essence报告](learning/05-文件上传自动索引-Essence报告.md)
|
||||
7. [06-RAG查询流程-Essence报告](learning/06-RAG查询流程-Essence报告.md)
|
||||
8. [07-Tool定义方式对比与优化](learning/07-Tool定义方式对比与优化.md)
|
||||
9. [08-MethodToolCallback-vs-ToolCallingManager深度分析](learning/08-MethodToolCallback-vs-ToolCallingManager深度分析.md)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 分析笔记 (analysis/)
|
||||
|
||||
代码分析和问题分析文档:
|
||||
|
||||
- [essence-report-rag.md](analysis/essence-report-rag.md)
|
||||
- [essence-report-rag-chunking.md](analysis/essence-report-rag-chunking.md)
|
||||
- [explore-report.md](analysis/explore-report.md)
|
||||
- [chunking-issues-analysis.md](analysis/chunking-issues-analysis.md)
|
||||
- [功能分析报告.md](analysis/功能分析报告.md)
|
||||
|
||||
---
|
||||
|
||||
## 📝 临时报告 (reports/)
|
||||
|
||||
修复报告和验证报告:
|
||||
|
||||
- [修复报告-多轮对话时间查询缓存问题](reports/修复报告-多轮对话时间查询缓存问题.md)
|
||||
- [验证报告-时间查询问题](reports/验证报告-时间查询问题.md)
|
||||
- [日志配置完成总结](reports/日志配置完成总结.md)
|
||||
|
||||
---
|
||||
|
||||
## 📖 指南文档 (guides/)
|
||||
|
||||
- [日志配置与分析指南](guides/日志配置与分析指南.md)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 文档维护
|
||||
|
||||
- **学习笔记** 放在 `learning/` 目录,按编号顺序命名
|
||||
- **分析笔记** 放在 `analysis/` 目录
|
||||
- **临时报告** 放在 `reports/` 目录
|
||||
- **指南文档** 放在 `guides/` 目录
|
||||
- **MVP 架构设计** 已移至项目根目录 `../mvp/`(包含架构、数据库、实施计划)
|
||||
| 类型 | 位置 |
|
||||
|---|---|
|
||||
| 现行架构 | `mvp/architecture/` |
|
||||
| 工程纪要(问题/决策/E2E) | `mvp/engineering/` |
|
||||
| Issue | `mvp/issues/` |
|
||||
| 表结构 | `mvp/tables/` |
|
||||
| 早期学习 | `docs/learning/`(归档向,不充当现行规范) |
|
||||
| 操作指南 | `docs/guides/` |
|
||||
|
||||
@@ -9,8 +9,8 @@ Production knowledge path: `MilvusHybridKnowledgeStore` with `retrieval.search.m
|
||||
|
||||
Related design notes:
|
||||
|
||||
- `docs/RAG-Hybrid质量分与后处理.md`
|
||||
- `docs/RAG-Agent如何读relevance_level.md`
|
||||
- `mvp/engineering/rag/RAG-Hybrid质量分与后处理.md`
|
||||
- `mvp/engineering/rag/RAG-Agent如何读relevance_level.md`
|
||||
- `mvp/architecture/RAG知识检索架构.md` §6
|
||||
|
||||
## Offline vs live
|
||||
|
||||
+11
-30
@@ -1,60 +1,41 @@
|
||||
# SuperBizAgent MVP 文档
|
||||
|
||||
**更新日期**:2026-07-23
|
||||
**更新日期**:2026-07-29
|
||||
|
||||
本目录保存 MVP 阶段的架构、问题、演示、评测和数据表说明。当前材料按“当前入口”和“历史归档”拆开,避免把早期设计稿当成当前实现。
|
||||
本目录保存 MVP 阶段的架构、工程纪要、问题、演示、评测和数据表说明。当前材料按“当前入口”和“历史归档”拆开,避免把早期设计稿当成当前实现。
|
||||
|
||||
## 当前入口
|
||||
|
||||
| 目录/文档 | 用途 |
|
||||
|---|---|
|
||||
| [architecture/README.md](architecture/README.md) | 当前 MVP 架构入口 |
|
||||
| [architecture/README.md](architecture/README.md) | **现行架构**(系统现在怎么跑) |
|
||||
| [engineering/README.md](engineering/README.md) | **工程纪要**(问题 / 决策 / 思路 / E2E 导读) |
|
||||
| [architecture/current-mvp-architecture.md](architecture/current-mvp-architecture.md) | 当前可运行系统架构 |
|
||||
| [architecture/agent-orchestration.md](architecture/agent-orchestration.md) | Agent 编排架构 |
|
||||
| [architecture/harness-quality-gates.md](architecture/harness-quality-gates.md) | Harness 与质量门禁 |
|
||||
| [architecture/session-trace-lifecycle.md](architecture/session-trace-lifecycle.md) | 会话与 Trace 生命周期 |
|
||||
| [architecture/RAG知识检索架构.md](architecture/RAG知识检索架构.md) | 当前 hybrid 检索架构 |
|
||||
| [issues/README.md](issues/README.md) | MVP issue 索引 |
|
||||
| [tables/README.md](tables/README.md) | 当前 MySQL 表说明 |
|
||||
| [demo/README.md](demo/README.md) | Demo 运行和演示材料 |
|
||||
| [eval/README.md](eval/README.md) | 诊断评测材料 |
|
||||
|
||||
当前架构、运行链路和后续规划分别以 `architecture/`、`issues/README.md`、`tables/README.md` 以及 OpenSpec/devflow 的最新记录为准。
|
||||
**读法**:改行为先看 `architecture/`;要理解「为什么这样定、踩过什么坑」再看 `engineering/`。
|
||||
早期个人学习笔记仍在仓库根目录 `docs/learning/` 等,**可能过时**,不以之为现行口径。
|
||||
|
||||
## 文档结构
|
||||
|
||||
```text
|
||||
mvp/
|
||||
architecture/
|
||||
architecture/ # 现行架构规范
|
||||
engineering/ # 工程纪要(问题/决策/E2E)
|
||||
README.md
|
||||
current-mvp-architecture.md
|
||||
agent-orchestration.md
|
||||
harness-quality-gates.md
|
||||
session-trace-lifecycle.md
|
||||
archive/
|
||||
2026-07-05-legacy/
|
||||
2026-07-22-legacy/
|
||||
issues/
|
||||
README.md
|
||||
active/
|
||||
archived/
|
||||
design-notes/
|
||||
rag/
|
||||
diagnosis/
|
||||
issues/
|
||||
tables/
|
||||
README.md
|
||||
*表-*.md
|
||||
archive/
|
||||
demo/
|
||||
README.md
|
||||
ten-minute-interview-demo.md
|
||||
requests/
|
||||
scripts/
|
||||
output/
|
||||
eval/
|
||||
README.md
|
||||
schema.md
|
||||
cases/
|
||||
fixtures/
|
||||
reports/
|
||||
archive/
|
||||
```
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ flowchart LR
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| `mvp/architecture/RAG知识检索架构.md` | 检索主架构 |
|
||||
| `docs/RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
|
||||
| `docs/RAG-Hybrid质量分与后处理.md` | quality / 排序闸门 |
|
||||
| `docs/RAG离线评测-基线设计.md` | 离线评测(非运行时 Trace) |
|
||||
| `../engineering/rag/RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
|
||||
| `../engineering/rag/RAG-Hybrid质量分与后处理.md` | quality / 排序闸门 |
|
||||
| `../engineering/rag/RAG离线评测-基线设计.md` | 离线评测(非运行时 Trace) |
|
||||
| `mvp/architecture/session-trace-lifecycle.md` | 会话/run Trace 总览(若存在) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MVP 架构文档
|
||||
|
||||
**更新日期**:2026-07-28
|
||||
**更新日期**:2026-07-29
|
||||
**状态**:当前单 Diagnosis Agent + Harness 架构
|
||||
|
||||
当前文档入口:
|
||||
@@ -15,6 +15,8 @@
|
||||
| [RAG知识检索架构.md](RAG知识检索架构.md) | 当前 `lookup_knowledge` 检索:MilvusClientV2 dense+BM25 hybrid、chunk 证据身份、重建运维 |
|
||||
| [RAG检索可观测性与审计.md](RAG检索可观测性与审计.md) | RAG Trace / 审计:请求内 retrievalTrace、tool_invocation 富字段、Trace API 读法 |
|
||||
|
||||
**工程纪要**(问题 / 决策 / E2E,非架构规范正文)见 [../engineering/README.md](../engineering/README.md)。
|
||||
|
||||
2026-07-22 前的多角色编排、双入口和旧证据链文档已移动到 `archive/2026-07-22-legacy/`,仅用于历史决策追溯,不代表当前运行时。其中旧 RAG 描述(Spring AI VectorStore 主路径 + Milvus SDK fallback)已被当前 hybrid 实现取代,请以 [RAG知识检索架构.md](RAG知识检索架构.md) 为准。检索可观测与 Trace 以 [RAG检索可观测性与审计.md](RAG检索可观测性与审计.md) 为准(勿再依赖 archive 内旧 retrieval-observability)。
|
||||
|
||||
当前普通 Trace 与 LLM 步骤审计(`agent_reasoning_audit`:`reasoning_content` + `assistant_text`)使用独立存储和独立接口。
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# MVP 工程纪要(Engineering Notes)
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**定位**:项目推进中真实遇到的问题、决策、解决思路与 E2E 验收叙事。
|
||||
|
||||
与其它目录分工:
|
||||
|
||||
| 目录 | 读什么 |
|
||||
|---|---|
|
||||
| [architecture/](../architecture/) | **现行架构**(系统现在怎么跑) |
|
||||
| **engineering/**(本目录) | **为何这样定**、踩坑、方案取舍、live 验收导读 |
|
||||
| [issues/](../issues/) | 未完成事项与状态 |
|
||||
| [tables/](../tables/) | 表结构说明 |
|
||||
| `docs/learning/` 等 | 早期学习/分析(可能过时,不以之为现行口径) |
|
||||
| `devflow/projects/` | 单次变更的 brief/decisions/evidence 切片 |
|
||||
|
||||
本文档**不是** API 规范的唯一真理源;冲突时以 `architecture/` 与代码为准。
|
||||
|
||||
---
|
||||
|
||||
## RAG
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [rag/RAG排序-多路召回与RRF.md](rag/RAG排序-多路召回与RRF.md) | K、多路融合、RRF、L0 边界 |
|
||||
| [rag/RAG-Hybrid质量分与后处理.md](rag/RAG-Hybrid质量分与后处理.md) | qualityScore 统一、L2 伪装废止、后处理 |
|
||||
| [rag/RAG-Agent如何读relevance_level.md](rag/RAG-Agent如何读relevance_level.md) | Agent 侧相关度标签含义与误读 |
|
||||
| [rag/RAG离线评测-基线设计.md](rag/RAG离线评测-基线设计.md) | Golden/Fixture、hybrid 评测与闸门 |
|
||||
| [rag/Milvus-Hybrid接入清单.md](rag/Milvus-Hybrid接入清单.md) | Hybrid 交付拆分与接入清单 |
|
||||
| [rag/RAG审计补丁-stepid-query-E2E验收.md](rag/RAG审计补丁-stepid-query-E2E验收.md) | step_id / query 审计 live 验收 |
|
||||
|
||||
架构对照:
|
||||
|
||||
- [architecture/RAG知识检索架构.md](../architecture/RAG知识检索架构.md)
|
||||
- [architecture/RAG检索可观测性与审计.md](../architecture/RAG检索可观测性与审计.md)
|
||||
|
||||
相关 Issue:
|
||||
|
||||
- [ISS-017 L0 过滤收窄与 Fallback 加固](../issues/active/ISS-017-rag-l0-filter-fallback-hardening.md)(暂缓,保持现网)
|
||||
|
||||
---
|
||||
|
||||
## Harness
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [harness/README.md](harness/README.md) | **从这里开始**:用一次诊断请求理解 Harness,不要求先掌握组件和状态 |
|
||||
| [harness/components/README.md](harness/components/README.md) | **组件渐进式导读**:沿一次请求分四步理解运行控制、Agent 收敛、Tool 事实边界和验证发布 |
|
||||
| [harness/CONTEXT.md](harness/CONTEXT.md) | Harness 统一术语、命名规则、状态维度与已知命名债务 |
|
||||
| [harness/Harness生命周期与状态.md](harness/Harness生命周期与状态.md) | Run、Tool、Progress、Guard、Release、SSE 和持久化生命周期及状态映射 |
|
||||
| [harness/Harness设计-非确定性Agent的确定性控制边界.md](harness/Harness设计-非确定性Agent的确定性控制边界.md) | Harness 根问题、设计不变量、方案取舍、关键决策、代价与真实问题反推 |
|
||||
| [harness/Harness组件全景-职责-设计原因与边界.md](harness/Harness组件全景-职责-设计原因与边界.md) | 当前 Harness 10 个职责域、全部生产类型、设计原因、作用与边界 |
|
||||
| [harness/Harness-Tool双视图-从原始结果到可验证证据.md](harness/Harness-Tool双视图-从原始结果到可验证证据.md) | canonical truth、Control View、Agent Observation 与 metadata audit 的数据边界 |
|
||||
| [harness/Harness证据安全链-从引用真实到结论可发布.md](harness/Harness证据安全链-从引用真实到结论可发布.md) | EvidenceGuard、EvidenceRepair、SemanticGuard 和唯一 Release Policy |
|
||||
| [harness/Harness信息增益停止-让无证据诊断正常收敛.md](harness/Harness信息增益停止-让无证据诊断正常收敛.md) | GAINED/NO_GAIN、重复检测、协议停止、ProgressSnapshot 与过程型 Fallback |
|
||||
|
||||
---
|
||||
|
||||
## 诊断 / E2E
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [diagnosis/一次诊断全流程-E2E导读.md](diagnosis/一次诊断全流程-E2E导读.md) | SUCCESS 全流程:阶段、token、timeline、字段 |
|
||||
| [diagnosis/Session-Run-Trace隔离-从串线到可回放.md](diagnosis/Session-Run-Trace隔离-从串线到可回放.md) | 多轮串线问题、身份拆分、协议迁移与 exact-run E2E |
|
||||
|
||||
架构对照:
|
||||
|
||||
- [architecture/current-mvp-architecture.md](../architecture/current-mvp-architecture.md)
|
||||
- [architecture/session-trace-lifecycle.md](../architecture/session-trace-lifecycle.md)
|
||||
- [architecture/agent-orchestration.md](../architecture/agent-orchestration.md)
|
||||
@@ -0,0 +1,315 @@
|
||||
# Session、Run、Trace 隔离:一次身份建模错误的修复
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**性质**:MVP 工程问题与架构决策复盘
|
||||
**结论状态**:Session / Run 身份模型沿用至当前架构
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题到底是什么
|
||||
|
||||
一句话定义:**系统用同一个 `sessionId`,同时标识“多轮对话”和“一次诊断执行”,导致一次 Trace 不再对应一次真实执行。**
|
||||
|
||||
问题由一个两轮 E2E 暴露。用户在同一会话中连续发起两次请求:
|
||||
|
||||
```text
|
||||
Round 1:诊断支付接口超时
|
||||
Round 2:基于上一轮结论,列出还缺哪些证据
|
||||
```
|
||||
|
||||
两轮复用 `sessionId` 是正确的,因为第二轮需要上一轮上下文。但当时持久化和 Trace 查询也只使用 `sessionId`:
|
||||
|
||||
- `diagnosis_session` 是覆盖写,第二轮把第一轮的 query、answer、status 覆盖掉;
|
||||
- `agent_step` 和 `tool_invocation` 是追加写,两轮明细累积在同一个 `sessionId` 下;
|
||||
- Trace API 再按 `sessionId` 聚合主表和明细。
|
||||
|
||||
结果不是简单的“重复数据”,而是一个系统中从未真实发生过的混合执行:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
R1["Round 1<br/>query A / steps A / tools A"] --> SID["同一个 sessionId"]
|
||||
R2["Round 2<br/>query B / steps B / tools B"] --> SID
|
||||
|
||||
SID --> Main["diagnosis_session<br/>只剩 query B / answer B"]
|
||||
SID --> Steps["agent_step<br/>steps A + steps B"]
|
||||
SID --> Tools["tool_invocation<br/>tools A + tools B"]
|
||||
|
||||
Main --> Trace["混合 Trace"]
|
||||
Steps --> Trace
|
||||
Tools --> Trace
|
||||
|
||||
Trace --> Error["无法回答:<br/>哪组证据支持了哪次回答?"]
|
||||
```
|
||||
|
||||
这个错误会沿数据链继续放大:
|
||||
|
||||
| 消费方 | 错误结果 |
|
||||
|---|---|
|
||||
| Trace 回放 | 一条 Trace 混合两轮步骤和 Tool |
|
||||
| Verifier / Gatekeeper | 当前轮可能读到上一轮证据 |
|
||||
| Evaluation | 评分对象和证据集合不再属于同一次执行 |
|
||||
| Feedback | 无法确定用户评价的是哪一轮回答 |
|
||||
| CaseLibrary | 可能把反馈沉淀到错误的 query/answer 上 |
|
||||
|
||||
因此,核心问题不是某个 Repository 的更新方式,而是**会话边界被错误地当成了证据与审计边界**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计必须守住什么
|
||||
|
||||
修复前先定义四条不变量。后续方案不是凭表结构偏好选择,而是看能否同时满足这些约束。
|
||||
|
||||
### 不变量 1:一次执行只有一个稳定身份
|
||||
|
||||
从请求被接受到最终 `SUCCESS / FALLBACK / FAILED / CANCELLED`,必须有一个不可变 ID。模型步骤、Tool 调用、预算、最终答案和反馈都属于它。
|
||||
|
||||
### 不变量 2:一条 Trace 只描述一次执行
|
||||
|
||||
Trace 中的 Run、AgentStep、ToolInvocation 和生命周期事件必须使用同一个 exact ID 聚合,不能依赖时间邻近或“最新一条”猜测归属。
|
||||
|
||||
### 不变量 3:上下文连续不等于执行合并
|
||||
|
||||
同一个 Session 可以包含多个 Run。后一个 Run 可以读取前一轮安全发布结果,但不能继承前一轮的 Tool、Trace、评分或失败状态。
|
||||
|
||||
### 不变量 4:兼容不能伪造精度
|
||||
|
||||
旧客户端可以有迁移期 fallback,旧数据也可以保留;但系统必须让歧义可观察,不能把无法恢复的历史混合数据伪装成精确多轮记录。
|
||||
|
||||
这四条不变量共同导出一个结论:系统需要两个身份,而不是给 `sessionId` 增加更多解释。
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么不是在旧表上继续修
|
||||
|
||||
设计阶段考虑的不是“拆表还是不拆表”这一个问题,而是如何建立稳定的执行边界。
|
||||
|
||||
| 候选方案 | 能解决什么 | 为什么没有选择 |
|
||||
|---|---|---|
|
||||
| 继续复用 `sessionId`,修正覆盖逻辑 | 避免主表被覆盖 | 明细仍无法区分轮次,根因未解决 |
|
||||
| 增加 `round_no` | 可以表示第几轮 | 并发请求、重试和多个执行入口下顺序不稳定;外部引用仍需复合身份 |
|
||||
| 按时间窗口拆分历史 Step/Tool | 无需改协议 | 时间不能证明归属,会制造看似精确的错误 Trace |
|
||||
| 每次请求插入一条新的 `diagnosis_session` | 形成一行一次执行 | 实际上已经引入 Run 概念,但名称和 Session 生命周期仍混淆,也缺少会话主实体 |
|
||||
| 拆分 `chat_session` 与 `diagnosis_run` | 显式表达一对多生命周期 | 需要 Schema、API 和客户端迁移,但能满足全部不变量 |
|
||||
|
||||
最终选择最后一种。它的判断依据不是“范式更规范”,而是只有它能让对话连续性和执行可审计性同时成立。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心设计
|
||||
|
||||
目标模型是一个清晰的一对多关系:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Client["Client"] --> Session["chat_session<br/>sessionId:多轮对话目录"]
|
||||
Session --> Run1["diagnosis_run<br/>runId 1:一次执行"]
|
||||
Session --> Run2["diagnosis_run<br/>runId 2:另一次执行"]
|
||||
|
||||
Run2 --> Step["agent_step<br/>模型步骤 metadata"]
|
||||
Run2 --> Tool["tool_invocation<br/>Tool 审计 metadata"]
|
||||
Run2 --> Event["diagnosis_trace_event<br/>生命周期 Timeline"]
|
||||
Run2 --> Reasoning["agent_reasoning_audit<br/>受限原文"]
|
||||
|
||||
Run2 --> Trace["普通 Trace API"]
|
||||
Step --> Trace
|
||||
Tool --> Trace
|
||||
Event --> Trace
|
||||
Reasoning --> Audit["独立 Reasoning API"]
|
||||
```
|
||||
|
||||
这里有三个不同的职责:
|
||||
|
||||
- `chat_session` 回答“哪些 Run 属于同一段对话”;
|
||||
- `diagnosis_run` 回答“这次请求的输入、状态、结果和资源消耗是什么”;
|
||||
- Trace 回答“这个 Run 具体经历了什么”,它是按 Run 聚合的读模型。
|
||||
|
||||
---
|
||||
|
||||
## 5. 六项关键决策
|
||||
|
||||
### 决策 1:引入正式的 `runId`
|
||||
|
||||
**选择**:每次有效 Chat 执行创建新的 `runId`,并通过 SSE metadata 返回 `sessionId + runId`。
|
||||
|
||||
**理由**:Run 必须能被 API、数据库、日志、Feedback 和评测独立引用。数据库自增 ID 不适合作为外部协议;轮次编号又不能稳定处理并发和重试。
|
||||
|
||||
**代价**:客户端必须保存并向后续 Trace/Feedback 请求传递 `runId`。
|
||||
|
||||
**边界**:`runId` 是不透明标识。早期实现使用 `run-` + UUID,后续格式发生过演进,客户端不得解析其前缀或长度。
|
||||
|
||||
### 决策 2:拆分会话态与运行态
|
||||
|
||||
**选择**:新增 `chat_session` 和 `diagnosis_run`,旧 `diagnosis_session` 停止承载新的运行写入。
|
||||
|
||||
**理由**:两者生命周期不同。
|
||||
|
||||
| 对象 | 保存内容 | 更新特点 |
|
||||
|---|---|---|
|
||||
| `chat_session` | 会话状态、轮次数、最近活动时间等目录信息 | 跨多轮持续更新 |
|
||||
| `diagnosis_run` | 单次 query、answer、终态、intent、release outcome、预算 | 一次执行内从 RUNNING 走向唯一终态 |
|
||||
|
||||
完整多轮正文没有因为拆表就复制到 MySQL。身份拆分解决的是审计归属,不应顺带扩大长期数据保存范围。
|
||||
|
||||
### 决策 3:Trace 是 Run 的聚合视图,不另造身份
|
||||
|
||||
**选择**:第一阶段复用 `agent_step` 和 `tool_invocation`,增加 `run_id`;不为了修复隔离问题再创建一个独立 `traceId`。
|
||||
|
||||
**理由**:隔离所缺的是执行外键,不是第三套身份。引入 `traceId` 只会产生 `sessionId / runId / traceId` 的映射问题。
|
||||
|
||||
后续单 Agent + Harness 重构新增 `diagnosis_trace_event`,用于表达统一生命周期 Timeline。这是 Run 下的新明细,不是新的聚合根,也没有改变 `runId` 的边界。
|
||||
|
||||
### 决策 4:exact Run 是目标协议,latest Run 只是迁移桥梁
|
||||
|
||||
**选择**:目标查询使用:
|
||||
|
||||
```http
|
||||
GET /api/diagnosis/{sessionId}/trace?runId={runId}
|
||||
```
|
||||
|
||||
服务端同时验证 Run 存在且属于 path 中的 Session,防止跨 Session 串读。
|
||||
|
||||
旧客户端暂时只传 `sessionId` 时,可以解析 latest Run;但这是显式兼容路径,不是新的业务语义。如果必须计算 latest,应按:
|
||||
|
||||
```text
|
||||
created_at DESC, id DESC
|
||||
```
|
||||
|
||||
而不是 `updated_at`。旧 Run 可能因 Feedback 或异步处理再次更新,最近修改不等于最近执行。
|
||||
|
||||
### 决策 5:所有下游语义绑定 Run
|
||||
|
||||
**选择**:Trace、Feedback、Evaluation、Tool evidence 和新 Case provenance 都以 `runId` 为执行边界。
|
||||
|
||||
**理由**:这些对象评价或引用的是一次回答,不是整段会话。
|
||||
|
||||
Feedback 在迁移期缺少 `runId` 时可以绑定 latest Run,但响应必须暴露 `fallbackToLatestRun=true`。兼容如果不可观察,就会从临时措施变成永久歧义。
|
||||
|
||||
`case_library.diagnosis_id` 因复用旧列,在过渡期存在历史 `session_id` 和新 `run_id` 两种语义。这是明确接受的迁移成本,而不是应被隐藏的数据一致性。
|
||||
|
||||
### 决策 6:历史混合数据不做推测性拆分
|
||||
|
||||
**选择**:每条旧 `diagnosis_session` 最多映射为一个 compatibility Run,不根据时间或 Agent 名称猜测真实轮次。
|
||||
|
||||
**理由**:旧数据没有记录边界,任何自动拆分都只能产生无法证明的归属。审计系统宁可明确“不知道”,也不能制造虚假的精确回放。
|
||||
|
||||
---
|
||||
|
||||
## 6. 协议和影响范围
|
||||
|
||||
这是一次有意的行为与协议变化,不是纯内部重构。
|
||||
|
||||
| 范围 | 变化 | 受影响方 |
|
||||
|---|---|---|
|
||||
| Chat / SSE | metadata 增加 `runId` | 前端、脚本、调用方 |
|
||||
| Trace API | 支持 exact `runId` 查询 | Trace UI、排障工具、评测 |
|
||||
| Run API | 提供 Session 下的 Run 列表 | 多轮历史浏览 |
|
||||
| Feedback | request/response 增加 Run 绑定和 fallback 标志 | 前端、CaseLibrary |
|
||||
| 数据库 | 新增两张主表,明细增加 `run_id` | 持久化、迁移、查询脚本 |
|
||||
| 其它入口 | 当时的 AIOps 同步采用 Run 边界 | SSE 消费方、Trace |
|
||||
|
||||
之所以把当时的 AIOps 一并迁移,是因为它同样会产生可回放执行;只修 Chat 会留下第二条具有同类缺陷的数据链。后续 ISS-014 删除了旧 AIOps 双入口,但这不改变当时“所有执行入口必须共享 Run 边界”的设计判断。
|
||||
|
||||
---
|
||||
|
||||
## 7. 风险如何处理
|
||||
|
||||
### 风险 1:兼容路径继续产生歧义
|
||||
|
||||
控制方式是让 fallback 可观察,并把 exact `runId` 定义为目标协议。兼容是迁移机制,不能反向成为领域模型。
|
||||
|
||||
### 风险 2:异步链路丢失或串用身份
|
||||
|
||||
`sessionId` 与 `runId` 必须作为同一执行上下文传播。当前架构将二者放入显式 `RunContext`,ToolBoundary、审计 Hook 和持久化都校验当前 Run,避免只依赖线程隐式状态。
|
||||
|
||||
### 风险 3:历史和新 provenance 共用旧列
|
||||
|
||||
保留旧列降低了迁移破坏性,但查询和文档必须承认双语义,不能把旧 `session_id` 当作非法 `run_id` 清理。
|
||||
|
||||
### 风险 4:旧混合 Trace 永远无法恢复
|
||||
|
||||
这是明确接受的事实。系统保留 compatibility 访问和回滚能力,但不承诺不存在的历史精度。
|
||||
|
||||
---
|
||||
|
||||
## 8. 如何证明设计成立
|
||||
|
||||
验收问题不是“接口里有没有 `runId`”,而是下面五个条件是否同时成立:
|
||||
|
||||
```text
|
||||
同一 Session 连续执行两轮
|
||||
+ 两轮获得不同 runId
|
||||
+ 每个 exact Trace 只返回本 Run 明细
|
||||
+ 数据库不存在跨 Run 混合行
|
||||
+ 第二轮仍能使用会话上下文
|
||||
```
|
||||
|
||||
真实 E2E 使用:
|
||||
|
||||
```text
|
||||
sessionId = e2e-phase6-chat-codex-20260710-2120
|
||||
run1 = run-e2a97696-4398-4abc-90e4-28f45c838f92
|
||||
run2 = run-76ce6a6e-92ab-40c9-800a-eca0c1bb5172
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- 两轮 Chat 成功并复用同一个 `sessionId`;
|
||||
- 两轮返回不同 `runId`;
|
||||
- run1 exact Trace 只返回 run1,run2 exact Trace 只返回 run2;
|
||||
- `diagnosis_run` 中存在两条独立运行记录;
|
||||
- AgentStep:run1 为 10 行,run2 为 9 行;
|
||||
- ToolInvocation:run1 为 14 行,run2 为 8 行;
|
||||
- mixed row check 为 0;
|
||||
- `chat_session.message_pair_count = 2`,上下文连续性没有因隔离而丢失;
|
||||
- focused tests、baseline diff、日志和数据库核验通过,未观察到 baseline drift。
|
||||
|
||||
这组证据同时验证了“该分开的确实分开”和“该连续的仍然连续”。
|
||||
|
||||
---
|
||||
|
||||
## 9. 后续演进验证了什么
|
||||
|
||||
系统后来从多角色 Agent 编排重构为单 Diagnosis ReAct Agent + Harness。执行结构发生了大变化,但 Session / Run 模型没有被替换,反而成为新架构的基础:
|
||||
|
||||
- `ChatApplicationUseCase` 创建和结束 Run;
|
||||
- `RunContext` 显式携带 `sessionId + runId`;
|
||||
- ToolBoundary 校验 Tool 请求属于当前 Run;
|
||||
- Redis canonical invocation 使用 `runId + toolCallId` 定位;
|
||||
- EvidenceGuard 只接受当前 Run 的 READY invocation;
|
||||
- AgentStep、ToolInvocation、TraceEvent 和 ReasoningAudit 都绑定 Run。
|
||||
|
||||
这说明当时解决的不是某一版代码的局部 bug,而是找到了稳定的领域边界。Agent 编排可以替换,Session 与 Run 的生命周期差异不会消失。
|
||||
|
||||
---
|
||||
|
||||
## 10. 可复用的设计判断
|
||||
|
||||
这次问题可以归纳为四条通用经验:
|
||||
|
||||
1. **生命周期不同的对象,不应共享同一个聚合身份。**
|
||||
2. **上下文复用不代表证据、状态和审计记录也可以复用。**
|
||||
3. **兼容 fallback 必须可观察、可退出,不能静默猜测。**
|
||||
4. **无法恢复的历史边界应明确降级,不能伪造精确性。**
|
||||
|
||||
判断类似系统是否存在同类问题,可以直接问:
|
||||
|
||||
- 一次请求是否有独立于 Session 的执行 ID?
|
||||
- 所有 Step、Tool、Event、Feedback 是否都能精确归属一次执行?
|
||||
- “查询最新”是否被误当成“查询指定执行”?
|
||||
- 主表覆盖写、明细追加写是否使用了同一个过宽的关联键?
|
||||
- 历史迁移是在保留不确定性,还是通过猜测制造精确性?
|
||||
|
||||
如果这些问题没有明确答案,那么 Trace 即使看起来完整,也未必能作为可信审计证据。
|
||||
|
||||
---
|
||||
|
||||
## 11. 资料索引
|
||||
|
||||
- [ISS-010:同 session 多轮诊断 Trace 隔离](../../issues/archived/ISS-010-session-run-trace-isolation.md)
|
||||
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md)
|
||||
- [当前 MVP 架构](../../architecture/current-mvp-architecture.md)
|
||||
- [数据表索引](../../tables/README.md)
|
||||
- [devflow brief](../../../devflow/projects/2026-07-10-session-run-trace-isolation/brief.md)
|
||||
- [devflow decisions](../../../devflow/projects/2026-07-10-session-run-trace-isolation/decisions.md)
|
||||
- [devflow acceptance](../../../devflow/projects/2026-07-10-session-run-trace-isolation/acceptance.md)
|
||||
- [devflow evidence](../../../devflow/projects/2026-07-10-session-run-trace-isolation/evidence.md)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**日期**:2026-07-28
|
||||
**状态**:SUCCESS 完整诊断主文档;工具阶段按**现行**审计能力(`step_id` / `query`)说明
|
||||
**文档路径**:`docs/一次诊断全流程-E2E导读.md`
|
||||
**文档路径**:`mvp/engineering/diagnosis/一次诊断全流程-E2E导读.md`
|
||||
|
||||
### 主样本(正文数值与 timeline 来源)
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
**现行审计**(`step_id` 挂 step、`input_params.query` 等)已合入主路径,见 [§3.4.1](#341-step_id--query-审计如何写入2026-07-28-改造)。
|
||||
专项 live 验收(含一次业务 FALLBACK 样本)见独立文档:
|
||||
→ [RAG 审计补丁 E2E:step_id + query](RAG审计补丁-stepid-query-E2E验收.md)
|
||||
→ [RAG 审计补丁 E2E:step_id + query](../rag/RAG审计补丁-stepid-query-E2E验收.md)
|
||||
|
||||
**关联文档**:
|
||||
|
||||
- [RAG Trace / 审计架构](../mvp/architecture/RAG检索可观测性与审计.md)
|
||||
- [RAG qualityScore 与后处理](RAG-Hybrid质量分与后处理.md)
|
||||
- [Agent 如何读 relevance_level](RAG-Agent如何读relevance_level.md)
|
||||
- [知识检索架构](../mvp/architecture/RAG知识检索架构.md)
|
||||
- [RAG Trace / 审计架构\](../../architecture/RAG检索可观测性与审计.md)
|
||||
- [RAG qualityScore 与后处理](../rag/RAG-Hybrid质量分与后处理.md)
|
||||
- [Agent 如何读 relevance_level](../rag/RAG-Agent如何读relevance_level.md)
|
||||
- [知识检索架构\](../../architecture/RAG知识检索架构.md)
|
||||
|
||||
**回放 API**:
|
||||
|
||||
@@ -520,7 +520,7 @@ sequenceDiagram
|
||||
| `tool_invocation.step_id` | 指向上述 step |
|
||||
| `input_params.query` | 工具检索 query(安全截断后) |
|
||||
|
||||
live 数字级验收与 FALLBACK 对照见 → [审计补丁 E2E 专项](RAG审计补丁-stepid-query-E2E验收.md)。
|
||||
live 数字级验收与 FALLBACK 对照见 → [审计补丁 E2E 专项](../rag/RAG审计补丁-stepid-query-E2E验收.md)。
|
||||
|
||||
#### 3.5 分数语义(避免误读)
|
||||
|
||||
@@ -949,7 +949,7 @@ flowchart TB
|
||||
| Token 对账 | 通过 | 13236,`tokens_reconciled=true` |
|
||||
| Trace 闭环 | 通过 | 2 steps / 1 tool / 15 events,persisted=returned |
|
||||
| SUCCESS 报告 | 通过 | `DIAGNOSIS_REPORT`,action/rec 为空,refs=RAG |
|
||||
| 现行审计能力 | 已合入代码 | 见 §3.4.1;专项 live 数字见 [审计补丁 E2E](RAG审计补丁-stepid-query-E2E验收.md) |
|
||||
| 现行审计能力 | 已合入代码 | 见 §3.4.1;专项 live 数字见 [审计补丁 E2E](../rag/RAG审计补丁-stepid-query-E2E验收.md) |
|
||||
|
||||
### 9.2 仍未关闭的缺口
|
||||
|
||||
@@ -957,7 +957,7 @@ flowchart TB
|
||||
|---|------|------|--------|
|
||||
| 1 | 检索 top 混入弱相关源(如 payment-service-latency) | hybrid 生效但纯度一般;报告可克制,sources 仍可能挂噪声 | 后续过滤/重排 |
|
||||
| 2 | Semantic Guard 约占 5k tokens | 正确但贵 | 后评估是否可降 |
|
||||
| 3 | 偶发 hybrid 空结果 → FALLBACK | 环境/Milvus 抖动 | 见 [审计验收页 §4](RAG审计补丁-stepid-query-E2E验收.md#4-业务-fallback-原因与审计无关) |
|
||||
| 3 | 偶发 hybrid 空结果 → FALLBACK | 环境/Milvus 抖动 | 见 [审计验收页 §4](../rag/RAG审计补丁-stepid-query-E2E验收.md#4-业务-fallback-原因与审计无关) |
|
||||
|
||||
缺口 1 示意:
|
||||
|
||||
@@ -1038,4 +1038,5 @@ python scripts/query_mysql.py "SELECT id, step_index, agent_name, has_tool_call,
|
||||
| 2026-07-28 | 初版:SUCCESS 全流程、字段词典与多图 |
|
||||
| 2026-07-28 | 审计补丁代码与 §3.4.1 能力说明 |
|
||||
| 2026-07-28 | 文档迁入 `docs/`;INDEX 挂接 |
|
||||
| 2026-07-28 | **拆分**:FALLBACK 审计验收样本迁至 [RAG审计补丁-stepid-query-E2E验收.md](RAG审计补丁-stepid-query-E2E验收.md);本文只保留 SUCCESS 主线 |
|
||||
| 2026-07-29 | 迁入 `mvp/engineering/diagnosis/`,与 RAG 工程纪要集中到 mvp |
|
||||
| 2026-07-28 | **拆分**:FALLBACK 审计验收样本迁至 [RAG审计补丁-stepid-query-E2E验收.md](../rag/RAG审计补丁-stepid-query-E2E验收.md);本文只保留 SUCCESS 主线 |
|
||||
@@ -0,0 +1,378 @@
|
||||
# Harness Context:统一术语与命名边界
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**状态**:当前实现口径
|
||||
**适用范围**:`com.superbiz.agent.harness`、Chat SSE、Diagnosis Run 持久化与相关工程文档
|
||||
|
||||
> 本文是术语词典,不建议第一次接触 Harness 时顺序阅读。入门请从 [README.md](README.md) 开始,遇到名词歧义时再回到本文查询。
|
||||
|
||||
## 1. 为什么需要这份 Context
|
||||
|
||||
当前系统同时存在 Run 状态、发布结果、Tool 状态、证据状态、收集状态、停止原因和 Fallback 原因。它们都使用了 `SUCCESS`、`ERROR`、`FAILED`、`READY` 等相近词汇,但回答的是不同问题。
|
||||
|
||||
如果把这些词排成一条“大状态机”,会产生错误理解,例如:
|
||||
|
||||
- `NO_EVIDENCE` 被理解为 Tool 调用失败;
|
||||
- `FALLBACK` 被理解为 Run 执行失败;
|
||||
- `SATURATED` 被理解为预算耗尽;
|
||||
- `READY` 被理解为证据足以支持根因;
|
||||
- 数据库 `status=SUCCESS` 被理解为已经找到根因。
|
||||
|
||||
本文件是 Harness 工程文档的术语入口。阅读其他文章前,先以这里的定义区分身份、数据、状态和组件责任。代码与现行架构文档仍是最终事实来源;本文件不创建新的运行协议。
|
||||
|
||||
## 2. 一句话定义 Harness
|
||||
|
||||
**Harness** 是包围非确定性模型执行的确定性控制边界:Agent 负责业务推理和 Draft,Harness 负责 Run 身份、生命周期、预算、取消、Tool 门禁、证据验真、停止控制、发布和审计。
|
||||
|
||||
Harness 不是:
|
||||
|
||||
- 业务工作流引擎;
|
||||
- Planner / Executor / Verifier / Composer 编排图;
|
||||
- 框架 ReAct loop 的第二份实现;
|
||||
- 判断业务根因的规则引擎;
|
||||
- 用于存放所有 Agent 相关代码的泛化名称。
|
||||
|
||||
## 3. 三层范围:不要把 Harness、Core 和 Application 当成同义词
|
||||
|
||||
| 名称 | 定义 | 包含 | 不包含 |
|
||||
|---|---|---|---|
|
||||
| Chat Application | 一次 Chat 请求的应用用例 | Session/Run 创建、路由、分支执行、持久化、公开结果 | HTTP/SSE 连接本身、业务推理细节 |
|
||||
| Diagnosis Harness | Diagnosis Agent 外部的确定性控制系统 | Core、Interceptor、ToolBoundary、Progress、Guard、Release、Audit | 根因推理和 ReAct 规划 |
|
||||
| Harness Core | 最小运行控制内核 | RunContext、deadline、budget、cancel、lifecycle、retry policy | 路由、Tool backend、Guard、持久化、SSE |
|
||||
|
||||
代码包 `com.superbiz.agent.harness` 同时包含 Chat Application 和 Diagnosis Harness 的实现,这是代码组织范围,不代表所有类都属于 Harness Core。
|
||||
|
||||
`DiagnosisHarnessCore` 目前也被 System Chat、Knowledge Query 和 Router 复用预算与生命周期能力。类名前缀保留了演进历史,概念上应理解为当前 Chat Run 的 Harness Core。
|
||||
|
||||
## 4. 身份术语
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
S["Chat Session<br/>sessionId,多轮容器"] -->|"1:N"| R1["Run<br/>runId,一次请求"]
|
||||
S -->|"1:N"| R2["Run<br/>下一次请求"]
|
||||
|
||||
R1 -->|"1:N"| AS["Agent Step<br/>一次模型步骤"]
|
||||
R1 -->|"1:N"| TC["Tool Call / Invocation<br/>framework tool_call_id"]
|
||||
R1 -->|"1:N"| TE["Trace Event<br/>sequence_no"]
|
||||
|
||||
TC --> CI["Canonical Invocation<br/>Run 内短期 Tool 真相"]
|
||||
AS --> TR["Diagnosis Trace<br/>按 exact Run 聚合"]
|
||||
TC --> TR
|
||||
TE --> TR
|
||||
|
||||
R1 -.->|"SUCCESS diagnosis only"| PT["PublishedResult<br/>可投影为下一轮 PreviousTurn"]
|
||||
```
|
||||
|
||||
图中的所有明细都必须绑定 exact runId。Session 只负责组织多轮,不能替代 Run 归属;Trace 是聚合视图,不能反过来成为执行身份。
|
||||
|
||||
### 4.1 Chat Session
|
||||
|
||||
一次多轮对话容器,由 `sessionId` 标识。同一个 Session 可以包含多次 Run。
|
||||
|
||||
Session 用于:
|
||||
|
||||
- 组织多轮请求;
|
||||
- 查找安全的 PreviousTurn;
|
||||
- 查询历史 Run。
|
||||
|
||||
Session 不代表一次执行,不拥有 Tool Call 或模型步骤的终态。
|
||||
|
||||
### 4.2 Run
|
||||
|
||||
一次独立的 Chat Application 执行,由 `runId` 标识。每次请求创建新 Run,无论 intent 是 System Chat、Knowledge Query 还是 Diagnosis。
|
||||
|
||||
代码和表名中仍大量使用 `DiagnosisRun` / `diagnosis_run`,但当前 Chat Application 会为三种 intent 都创建该记录。因此文档中优先使用 **Run**;只有引用 Java 实体或数据库表时才写 `DiagnosisRun`。
|
||||
|
||||
### 4.3 Agent Step
|
||||
|
||||
Diagnosis ReAct Agent 的一次模型步骤。多个 Agent Step 属于一个 Run,用于记录每轮模型调用的有界元数据和 Token。
|
||||
|
||||
Agent Step 不是 Run,也不是 retry attempt。ReAct 的下一轮模型调用是业务循环;retry 是同一技术操作的再次 attempt。
|
||||
|
||||
### 4.4 Tool Call / Tool Invocation
|
||||
|
||||
- **Tool Call**:模型通过框架产生的调用请求,由 framework `tool_call_id` 标识。
|
||||
- **Tool Invocation**:该请求进入 ToolBoundary 后的一次实际执行记录。
|
||||
- **Tool Request Rejection**:在 progress、重复或停止门禁处被拒绝,backend 没有执行,不算 Tool Invocation。
|
||||
|
||||
Harness 不生成第二套 Tool Call ID。
|
||||
|
||||
### 4.5 Trace
|
||||
|
||||
按 exact `sessionId + runId` 聚合的可回放观察视图,包含 Run、Agent Step、Tool metadata 和统一 Timeline。
|
||||
|
||||
Trace 是观察结果,不是新的执行上下文或状态所有者。`TraceEventStatus` 只描述单个事件,不能替代 RunState 或 ReleaseOutcome。
|
||||
|
||||
## 5. 核心执行术语
|
||||
|
||||
### 5.1 RunContext
|
||||
|
||||
一次 Run 的显式执行上下文。结构不可变地携带:
|
||||
|
||||
```text
|
||||
sessionId
|
||||
runId
|
||||
deadline
|
||||
RunCancellation
|
||||
RunBudget
|
||||
ModelCallLedger
|
||||
HarnessRetryPolicies
|
||||
RunLifecycle
|
||||
DiagnosisProgressTracker
|
||||
```
|
||||
|
||||
“结构不可变”表示 record 字段引用不变化;预算、取消、生命周期和进展通过各自线程安全句柄在 Run 内变化。
|
||||
|
||||
### 5.2 Run Lifecycle
|
||||
|
||||
Run 的内存执行终态,由 `RunLifecycle` 所有,状态类型是 `RunState`。它采用 first-terminal-wins,后到达的成功、失败或取消不能覆盖第一个终态。
|
||||
|
||||
### 5.3 Run Cancellation
|
||||
|
||||
请求停止 Run 的协作机制,由 `RunCancellationReason` 记录第一个原因。取消会阻止后续边界和迟到发布,但不承诺一定能立即物理中断已发送给 Provider 的同步请求。
|
||||
|
||||
### 5.4 Run Budget
|
||||
|
||||
单 Run 的资源账本和门禁,包括模型调用、Tool 调用、单 Tool 次数、输入/输出/总 Token 和 Run bytes。
|
||||
|
||||
Budget 只回答“还能不能消耗资源”,不回答“继续诊断是否有价值”。后者属于 Information Gain 和 Collection State。
|
||||
|
||||
### 5.5 Retry Attempt
|
||||
|
||||
同一个技术操作因允许的技术失败而再次执行。当前 Router 和 SemanticGuard 最多 2 次 attempt,Diagnosis Agent、Tool 和 EvidenceRepair 只有 1 次。
|
||||
|
||||
以下不是 retry:
|
||||
|
||||
- ReAct Agent 的下一轮思考;
|
||||
- 改用另一个 Tool;
|
||||
- `NO_EVIDENCE` 后继续查询;
|
||||
- 用户发起下一次 Run。
|
||||
|
||||
## 6. Agent 与输出术语
|
||||
|
||||
### 6.1 Diagnosis Agent
|
||||
|
||||
唯一拥有业务 ReAct Tool loop 的 Agent,负责提出假设、选择 Tool、评价非空结果的信息增益并生成 `DiagnosisDraft`。
|
||||
|
||||
它不负责 Run 生命周期、Tool 授权、证据物理验真、SemanticGuard 或最终发布。
|
||||
|
||||
### 6.2 DiagnosisDraft
|
||||
|
||||
Diagnosis Agent 的结构化草稿,是发布链输入,不是已经发布的报告。Draft 可以有结论,也可以 `conclusion=null`。
|
||||
|
||||
Draft 中的 Tool 引用和结论必须经过 Release Pipeline 后才能成为公开内容。
|
||||
|
||||
### 6.3 Agent Result
|
||||
|
||||
当前 Tool 代码中的 `agent_result` 指 Tool-specific Projector 生成、存入 canonical invocation 的标准化有界结果。
|
||||
|
||||
它不是:
|
||||
|
||||
- Diagnosis Agent 的最终 Draft;
|
||||
- Chat Application 的最终结果;
|
||||
- 直接进入模型上下文的完整内容。
|
||||
|
||||
更准确的理解是 **Canonical Projected Tool Result**。当前字段名因协议兼容保留。
|
||||
|
||||
### 6.4 Model Observation
|
||||
|
||||
从 canonical `agent_result` 再次白名单投影后,真正作为 Tool Response 进入 Diagnosis Agent 上下文的内容。
|
||||
|
||||
预算、阈值、重复指纹、raw response 和完整 Harness 控制状态不进入 Model Observation。
|
||||
|
||||
### 6.5 PreviousTurn 与 PublishedResult
|
||||
|
||||
- `PublishedResult`:只有 `DIAGNOSIS + ReleaseOutcome.SUCCESS` 才能持久化的安全诊断结果。
|
||||
- `PreviousTurn`:从 PublishedResult 生成的有界下一轮上下文。
|
||||
|
||||
Fallback、失败、取消、raw evidence 和完整历史不会进入 PreviousTurn。
|
||||
|
||||
`PublishedResult` 不等于当前请求直接返回的 `ChatApplicationResult`。
|
||||
|
||||
## 7. Tool 与证据术语
|
||||
|
||||
### 7.1 ToolBoundary
|
||||
|
||||
所有业务 Tool 的统一执行边界,负责 Run/ID/授权/只读校验、预算、bytes、canonical 状态迁移和 metadata audit。
|
||||
|
||||
ToolBoundary 不判断信息增益或业务根因。
|
||||
|
||||
### 7.2 Canonical Invocation
|
||||
|
||||
Redis TTL 内的完整 Tool 调用真相,包含 request、raw response、标准化 `agent_result`、调用状态和证据状态。
|
||||
|
||||
它用于当前 Run 的 EvidenceGuard 和 ProgressSnapshot,不是长期审计记录。
|
||||
|
||||
### 7.3 Durable Audit
|
||||
|
||||
长期保存的有界元数据:Run/Tool identity、状态、耗时、bytes、模型步骤和 Token。它不保存 Prompt、完整 Tool 参数、raw response 或 canonical record。
|
||||
|
||||
### 7.4 Evidence
|
||||
|
||||
Tool 在特定 scope 下返回、经过 Projector 标准化并能由当前 Run canonical record 验真的事实或负向观察。
|
||||
|
||||
`EVIDENCE_FOUND` 只代表存在候选内容,不代表它支持根因。
|
||||
|
||||
### 7.5 Negative Observation
|
||||
|
||||
`READY + NO_EVIDENCE` 形成的限定范围事实,例如“在时间窗 T、服务 S、查询 Q 下没有匹配日志”。
|
||||
|
||||
它不能被解释为“故障不存在”或“系统健康”。Draft 中使用 `AnalysisKind.NEGATIVE_OBSERVATION` 表达这种分析。
|
||||
|
||||
### 7.6 VerifiedEvidenceSnapshot
|
||||
|
||||
EvidenceGuard 从 canonical invocation 中投影出的已验真、最小证据集合,供 SemanticGuard 使用。它不包含 raw response。
|
||||
|
||||
### 7.7 ProgressSnapshot
|
||||
|
||||
Tool loop 结束后,根据已完成 Tool Call identity 回读 canonical records 生成的有界过程视图,用于受控停止或无结论 Fallback。
|
||||
|
||||
VerifiedEvidenceSnapshot 面向“有结论报告的语义审查”;ProgressSnapshot 面向“没有可发布结论时说明已经检查了什么”,两者用途不同。
|
||||
|
||||
## 8. Guard 与发布术语
|
||||
|
||||
### 8.1 EvidenceGuard
|
||||
|
||||
确定性引用验真器。检查 Draft 结构、analysis 引用闭包、当前 Run 所有权、READY 状态、EvidenceStatus 和 typed projection 自洽性。
|
||||
|
||||
不调用模型,不判断结论是否被证据支持。
|
||||
|
||||
### 8.2 EvidenceRepair
|
||||
|
||||
一次性的模型修复步骤,只修结构和引用。修复前后 `SemanticDraftView` 必须保持用户可见语义一致,之后重新执行 EvidenceGuard。
|
||||
|
||||
它不是新的报告作者,也不是 retry Agent。
|
||||
|
||||
### 8.3 SemanticGuard
|
||||
|
||||
隔离的单轮语义审查器,只判断 verified evidence 是否支持 Draft,输出 `SUPPORTED / UNSUPPORTED`。
|
||||
|
||||
它使用模型,但无 Tool、无记忆、无 ReAct loop、无报告改写权,因此文档中不要称它为第二个业务 Agent。
|
||||
|
||||
### 8.4 Release Pipeline
|
||||
|
||||
从 DiagnosisDraft 或受控停止输入,到 `DiagnosisReleaseResult` 的安全决策链:EvidenceGuard、可选 Repair/Recheck、SemanticGuard 和 SafeFallback。
|
||||
|
||||
### 8.5 ReleaseOutcome
|
||||
|
||||
应用层最终处理结果:
|
||||
|
||||
- `SUCCESS`:发布安全正常内容;
|
||||
- `FALLBACK`:请求已被安全处理,但没有发布正常诊断结论;
|
||||
- `FAILED`:无法形成安全业务结果;
|
||||
- `CANCELLED`:Run 被取消。
|
||||
|
||||
ReleaseOutcome 不等于 RunState。尤其 `FALLBACK` 不是 RunState。
|
||||
|
||||
### 8.6 SafeFallback 与 FallbackType
|
||||
|
||||
SafeFallback 是确定性公开内容;FallbackType 解释为什么没有发布正常诊断结论,例如:
|
||||
|
||||
- `EVIDENCE_VALIDATION_FAILED`;
|
||||
- `SEMANTIC_UNSUPPORTED`;
|
||||
- `SEMANTIC_UNAVAILABLE`;
|
||||
- `INSUFFICIENT_EVIDENCE`;
|
||||
- `MISSING_REQUIRED_CONTEXT`。
|
||||
|
||||
FallbackType 是 `ReleaseOutcome.FALLBACK` 的原因,不是新的生命周期状态。
|
||||
|
||||
## 9. 状态维度速查
|
||||
|
||||
| 类型 | 所有者 | 回答的问题 | 不能回答的问题 |
|
||||
|---|---|---|---|
|
||||
| `RunState` | RunLifecycle | Run 的内存执行是否终止、如何终止 | 发布了正常内容还是 Fallback |
|
||||
| `RunCancellationReason` | RunCancellation | 谁首先请求取消、为什么 | 最终公开结果是什么 |
|
||||
| `ChatApplicationStatus` | Application Observer | 当前向用户展示哪个处理阶段 | Run 是否已经终止 |
|
||||
| `InvocationStatus` | Canonical Invocation | Tool 调用记录是否完成 | 是否找到候选证据 |
|
||||
| `EvidenceStatus` | Tool Projector | 当前 scope 是否有候选证据 | 是否支持根因 |
|
||||
| `InformationGain` | Harness/Diagnosis Agent | 结果是否推进当前诊断 | Tool 是否技术成功 |
|
||||
| `DiagnosisCollectionState` | ProgressTracker | 是否允许继续调用证据 Tool | Run 是否终止 |
|
||||
| `DiagnosisStopReason` | ProgressTracker | 为什么停止继续收集 | 对外发布什么内容 |
|
||||
| `SemanticVerdict` | SemanticGuard | 已验真证据是否支持 Draft | Run 是否成功执行 |
|
||||
| `ReleaseOutcome` | Application/Release | 对外处理结果属于成功、降级、失败还是取消 | Tool 或收集过程的内部状态 |
|
||||
| `FallbackType` | SafeFallbackFactory | FALLBACK 的业务/安全原因 | 整个 Run 的执行终态 |
|
||||
| `TraceEventStatus` | 单个 Trace Event | 某条事件的局部结果 | 全局生命周期 |
|
||||
| SSE Session `State` | ChatSseSession | 连接能否继续发送事件 | Harness Run 的业务结果 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Execution["执行控制维度"]
|
||||
RS["RunState<br/>RUNNING -> terminal"]
|
||||
CS["CollectionState<br/>COLLECTING / SATURATED"]
|
||||
SR["StopReason<br/>停止收集原因"]
|
||||
end
|
||||
|
||||
subgraph Tool["单次 Tool 维度"]
|
||||
IS["InvocationStatus<br/>PROJECTING / READY / ERROR"]
|
||||
ES["EvidenceStatus<br/>FOUND / NO_EVIDENCE / ERROR"]
|
||||
IG["InformationGain<br/>GAINED / NO_GAIN"]
|
||||
end
|
||||
|
||||
subgraph Validation["验证维度"]
|
||||
EV["EvidenceGuardResult<br/>valid / violations"]
|
||||
SV["SemanticVerdict<br/>SUPPORTED / UNSUPPORTED"]
|
||||
end
|
||||
|
||||
subgraph Publication["发布与观察维度"]
|
||||
RO["ReleaseOutcome<br/>SUCCESS / FALLBACK / FAILED / CANCELLED"]
|
||||
FT["FallbackType<br/>FALLBACK 原因"]
|
||||
SSE["SSE Session State<br/>连接发送状态"]
|
||||
DB["diagnosis_run.status<br/>持久化通用状态"]
|
||||
end
|
||||
|
||||
IS --> ES
|
||||
ES --> IG --> CS
|
||||
CS --> SR
|
||||
ES --> EV --> SV
|
||||
SR --> RO
|
||||
SV --> RO
|
||||
RS --> RO
|
||||
RO --> FT
|
||||
RO --> SSE
|
||||
RO --> DB
|
||||
```
|
||||
|
||||
箭头表示信息参与后续决策,不表示枚举之间一一转换。例如 `READY + EVIDENCE_FOUND` 仍可能得到 `NO_GAIN`,`RunState.SUCCESS` 也可能对应 `ReleaseOutcome.FALLBACK`。
|
||||
|
||||
完整状态转换和跨层映射见[Harness生命周期与状态.md](Harness生命周期与状态.md)。
|
||||
|
||||
## 10. 命名规则
|
||||
|
||||
后续代码和文档遵守以下用词:
|
||||
|
||||
1. 说“Run 成功/失败/取消/超时/预算耗尽”时,明确写 `RunState`。
|
||||
2. 说“发布正常内容/Fallback/失败/取消”时,明确写 `ReleaseOutcome`。
|
||||
3. 不单独写“Tool 成功”,改写为 `InvocationStatus=READY`,并同时说明 EvidenceStatus。
|
||||
4. 不写“找到有效证据”,除非已经说明是候选内容、已验真事实还是足以支持结论。
|
||||
5. `NO_EVIDENCE` 必须附带 scope,不得写成全局否定。
|
||||
6. `SATURATED` 只用于 Collection State;预算耗尽使用 `BUDGET_LIMIT_REACHED` 或 `RunState.BUDGET_EXHAUSTED`。
|
||||
7. `Fallback` 只指安全发布降级,不用来泛指异常兜底代码。
|
||||
8. `Agent` 默认指 Diagnosis Agent;SemanticGuard 和 EvidenceRepair 分别称“语义审查器”和“引用修复步骤”。
|
||||
9. `agent_result` 引用字段名时保留原名,概念说明使用“canonical projected Tool result”。
|
||||
10. `status` 单独出现没有意义,必须注明所属类型或存储字段。
|
||||
|
||||
## 11. 已知命名债务
|
||||
|
||||
### 11.1 `DiagnosisRun` 名称大于实际诊断范围
|
||||
|
||||
当前 Chat Application 对三种 intent 都写入 `diagnosis_run`。文档统一称 Run;是否重命名实体和表属于单独的协议/迁移决策,本次不修改代码。
|
||||
|
||||
### 11.2 `SseOutcome` 当前未被运行时使用
|
||||
|
||||
`SseOutcome` 枚举存在,但当前 `ChatSseEvent.Done` 直接携带 `ReleaseOutcome`,并禁止公开 `CANCELLED`。因此当前 SSE 真理源是 `ReleaseOutcome`,不要再基于 `SseOutcome` 推导协议。
|
||||
|
||||
### 11.3 `FallbackType.BUDGET_EXHAUSTED` 不是当前诊断发布主路径
|
||||
|
||||
枚举值仍存在,但当前 Diagnosis Release 对预算受控停止的行为是:有已验真进展时发布 `INSUFFICIENT_EVIDENCE`,无安全进展时保持失败。文档不能仅因枚举存在就声称系统会发布 `BUDGET_EXHAUSTED` Fallback。
|
||||
|
||||
### 11.4 数据库 `status=SUCCESS` 不等于找到根因
|
||||
|
||||
`JpaChatRunStore` 将 `ReleaseOutcome.SUCCESS` 和 `FALLBACK` 都映射为数据库 `status=SUCCESS`,表示请求被正常处理。是否发布根因必须结合 `release_outcome`、`content_type` 和 FallbackType 判断。
|
||||
|
||||
### 11.5 RunState 没有直接作为独立字段持久化
|
||||
|
||||
当前持久化主记录保存通用 `status` 和 `release_outcome`,Trace 保存阶段事件;内存 `RunTermination.state/reason` 不是独立数据库字段。排查时不能只靠数据库 `status` 反推 TIMED_OUT 或 BUDGET_EXHAUSTED 的精确内部终态。
|
||||
|
||||
## 12. 如何使用本文
|
||||
|
||||
本文不是必读的第一章,而是遇到名词歧义时使用的词典。第一次接触 Harness,请先读 [README.md](README.md) 建立最小心智模型;需要理解某个状态或概念时,再回到本文对应章节查询。
|
||||
@@ -0,0 +1,280 @@
|
||||
# Harness Tool 双视图:从原始结果到可验证证据
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**主题**:ToolBoundary、Canonical Invocation、Harness Control View 与 Agent Observation
|
||||
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
|
||||
**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
|
||||
|
||||
## 1. 要解决的不是 Tool 调用,而是 Tool 结果的所有权
|
||||
|
||||
Agent 调用 Tool 后,最直接的实现是把 backend 返回的 JSON 原样放进模型上下文。这在 Demo 中可以工作,但进入可验证的诊断系统后会出现一个根本矛盾:同一份结果需要同时服务推理、控制、验真和审计,而这些消费者需要的数据范围完全不同。
|
||||
|
||||
以日志查询为例:
|
||||
|
||||
- Agent 需要少量匹配事件、模式、实际时间范围和是否截断;
|
||||
- Harness 需要返回数量、规范化 scope、重复身份和客观证据状态;
|
||||
- EvidenceGuard 需要证明这份结果确实来自当前 Run 的某次 READY 调用;
|
||||
- 审计需要 Tool 名、调用 ID、状态、耗时和字节数;
|
||||
- 安全边界又要求密码、Token、主机、IP、SQL 字面量和无限日志正文不能进入模型或长期审计。
|
||||
|
||||
如果只保留一份 JSON,只能在两个错误方向中选择:要么信息过多导致泄露和上下文膨胀,要么信息过少导致后续无法验真。
|
||||
|
||||
因此这项设计的核心不是“增加一个 Redis Store”,而是重新定义 Tool 数据所有权:
|
||||
|
||||
> backend raw 属于 Harness;模型只能获得为推理目的生成的有界观察;长期审计只保留允许运营保存的元数据。
|
||||
|
||||
## 2. 三个消费者,三种数据责任
|
||||
|
||||
虽然实现中常称为“Tool 双视图”,完整的数据分层实际上包含三种用途:
|
||||
|
||||
| 数据形态 | 消费者 | 解决的问题 | 生命周期 |
|
||||
|---|---|---|---|
|
||||
| Canonical Invocation | ToolBoundary、EvidenceGuard、ProgressProjector | 这次 Tool 真实执行了什么、属于哪个 Run、能否引用 | Redis 短 TTL |
|
||||
| Harness Control View / Agent Observation | Harness / Diagnosis Agent | 是否重复、是否为空、模型下一步需要看到什么 | 当前 Run / 模型上下文 |
|
||||
| Durable Audit | Trace、运营排障 | 何时调用、状态、耗时、大小、关联 Step | MySQL 长期 metadata |
|
||||
|
||||
所谓“双视图”,特指 canonical 标准结果被进一步分成:
|
||||
|
||||
1. Harness 使用的 Control View;
|
||||
2. 模型使用的 Agent Observation。
|
||||
|
||||
Durable Audit 是第三个持久化面,但它不是 Tool 内容视图,不参与推理或证据验真。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Typed Tool Request"] --> B["ToolBoundary"]
|
||||
B --> C["Backend Raw Response"]
|
||||
C --> D["Tool-specific Projector"]
|
||||
D --> E["Canonical agent_result"]
|
||||
|
||||
B --> F["Redis Canonical Invocation<br/>request + raw + agent_result"]
|
||||
E --> F
|
||||
|
||||
E --> G["Harness Control View<br/>count / status / scope"]
|
||||
E --> H["Agent Observation<br/>白名单、有界、脱敏"]
|
||||
|
||||
B --> I["Durable Audit<br/>identity / status / latency / bytes"]
|
||||
G --> J["重复检测、NO_GAIN、停止"]
|
||||
H --> K["Diagnosis Agent Context"]
|
||||
F --> L["EvidenceGuard / ProgressSnapshot"]
|
||||
```
|
||||
|
||||
## 3. 为什么不能让 Agent Observation 充当真相源
|
||||
|
||||
Agent Observation 是为了控制上下文而生成的投影,它可能:
|
||||
|
||||
- 只保留前 N 条结果;
|
||||
- 截断单条文本;
|
||||
- 对敏感列和日志字段做脱敏;
|
||||
- 把大量事件聚合成模式;
|
||||
- 只暴露粗粒度相关度,不暴露检索轨迹和原始分数。
|
||||
|
||||
这意味着它适合帮助模型推理,却不适合作为“后台真实返回”的完整证明。如果 EvidenceGuard 反过来验证 Agent 自己收到的 observation,就相当于用被审查对象提供的摘要证明其自身真实性。
|
||||
|
||||
Canonical Invocation 解决的正是这个独立性问题。它以 exact `runId + tool_call_id` 建立记录,并保存:
|
||||
|
||||
```text
|
||||
tool_call_id
|
||||
run_id
|
||||
tool_name
|
||||
request
|
||||
raw_response
|
||||
agent_result
|
||||
invocation status
|
||||
evidence status
|
||||
error code
|
||||
started_at / completed_at
|
||||
```
|
||||
|
||||
EvidenceGuard 不信任 Draft 中的引用,也不从模型历史反推 Tool 结果,而是重新按当前 Run 构造 key,读取 canonical record 并验证状态和投影内容。
|
||||
|
||||
## 4. ToolBoundary 为什么必须是统一入口
|
||||
|
||||
RAG、日志和 MySQL 的业务执行方式不同,但以下控制规则完全相同:
|
||||
|
||||
- Run 必须仍然 active;
|
||||
- envelope 的 runId 必须等于当前 Run;
|
||||
- `tool_call_id` 必须合法且不能重复;
|
||||
- Tool 必须已授权并声明只读;
|
||||
- 调用前必须预占 Tool 预算和 request bytes;
|
||||
- raw response 和 agent result 必须分别检查容量;
|
||||
- canonical 状态只能按合法路径迁移;
|
||||
- 对 Agent 只返回稳定错误码;
|
||||
- durable audit 失败不能改变已经得到的 Tool 结果。
|
||||
|
||||
如果把这些逻辑复制到三个 Adapter,任何新增 Tool 都可能漏掉其中一项。因此统一由 `ToolBoundary` 编排一次调用,具体 Adapter 只负责 typed request、backend 和 projector 的连接。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant I as Tool Interceptor
|
||||
participant B as ToolBoundary
|
||||
participant C as Harness Core
|
||||
participant S as Canonical Store
|
||||
participant T as Backend
|
||||
participant P as Projector
|
||||
participant A as Audit Sink
|
||||
|
||||
I->>B: RunContext + ToolCallRequestEnvelope
|
||||
B->>B: run / id / authorization / readonly / JSON preflight
|
||||
B->>C: reserve Tool call + request bytes
|
||||
B->>S: begin PROJECTING
|
||||
B->>T: execute typed request
|
||||
T-->>B: raw response
|
||||
B->>C: reserve raw bytes
|
||||
B->>P: project raw response
|
||||
P-->>B: bounded agent_result + evidence_status
|
||||
B->>C: reserve projection bytes
|
||||
B->>S: mark READY
|
||||
B-->>I: ToolBoundaryResult
|
||||
B-->>A: best-effort metadata audit
|
||||
```
|
||||
|
||||
错误发生时,已经创建的 canonical record 会尽力迁移到 ERROR;如果连 Store 都不可用,则向上只返回 `STORE_ERROR` 等稳定码,不把 Redis 或 backend 异常正文交给模型。
|
||||
|
||||
## 5. 两套状态为什么不能合并
|
||||
|
||||
Tool 调用同时有两个正交维度:
|
||||
|
||||
| 维度 | 状态 | 回答的问题 |
|
||||
|---|---|---|
|
||||
| Invocation lifecycle | `PROJECTING / READY / ERROR` | 这次调用是否完成并形成了可引用记录 |
|
||||
| Evidence semantics | `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | 客观结果中是否存在候选证据 |
|
||||
|
||||
例如日志查询成功返回 0 条:
|
||||
|
||||
```text
|
||||
InvocationStatus = READY
|
||||
EvidenceStatus = NO_EVIDENCE
|
||||
```
|
||||
|
||||
它不是 `ERROR`。这条负向观察可以支持“在指定时间、服务和查询条件下没有匹配日志”,但不能支持“故障不存在”。
|
||||
|
||||
合法组合被类型约束为:
|
||||
|
||||
| InvocationStatus | EvidenceStatus | 是否允许引用 |
|
||||
|---|---|---:|
|
||||
| `PROJECTING` | `null` | 否 |
|
||||
| `READY` | `EVIDENCE_FOUND` | 是 |
|
||||
| `READY` | `NO_EVIDENCE` | 是,但只能作为限定范围负向观察 |
|
||||
| `ERROR` | `ERROR` | 否 |
|
||||
|
||||
把两者合成一个 `SUCCESS / FAILED` 会丢失最重要的信息:技术成功但业务范围内没有证据。
|
||||
|
||||
## 6. 三类 Tool 如何投影
|
||||
|
||||
### 6.1 RAG
|
||||
|
||||
`RagResultProjector` 从 backend 结果中提取有界 evidence block,稳定文档身份,限制 excerpt 数量和长度,并兼容 `relevanceLevel / relevance_level` 后统一为 `relevance_level`。
|
||||
|
||||
这里有一个刻意保留的区分:
|
||||
|
||||
```text
|
||||
evidence 非空 -> EVIDENCE_FOUND
|
||||
relevance_level=REFERENCE -> 相关度一般
|
||||
```
|
||||
|
||||
`EVIDENCE_FOUND` 只说明存在候选内容,`REFERENCE` 也不自动表示 `NO_GAIN`。内容是否推进当前诊断假设,需要模型结合上下文判断。
|
||||
|
||||
### 6.2 Logs
|
||||
|
||||
`QueryLogsResultProjector` 不只是截取前几条日志,它还会:
|
||||
|
||||
- 脱敏 password、token、secret、API key;
|
||||
- 脱敏 pod、host、PID、IP 和 SQL literal;
|
||||
- 去掉堆栈尾部噪声;
|
||||
- 对数字做模式归一化并聚合重复事件;
|
||||
- 对事件做跨范围采样,而不是只保留开头;
|
||||
- 保存实际时间、topic、query scope 和截断标记。
|
||||
|
||||
如果 backend 执行成功且日志数组为空,投影结果是 READY + NO_EVIDENCE。backend 明确返回失败,才是 Tool 执行或投影错误。
|
||||
|
||||
### 6.3 MySQL
|
||||
|
||||
MySQL 在投影之前还有独立的只读安全链:`MysqlSqlValidator` 根据逻辑数据源、schema、table 和 column allowlist 生成 `MysqlQueryPlan`,`JdbcMysqlReadOnlyExecutor` 只执行该 Plan。
|
||||
|
||||
`MysqlResultProjector` 再负责:
|
||||
|
||||
- 限制最大行数、单元格长度和总 bytes;
|
||||
- 保持 number、boolean 和 null 类型;
|
||||
- 对 password、token、secret、credential 等敏感列强制脱敏;
|
||||
- 结果缩减时标记 `truncated=true`。
|
||||
|
||||
Validator 负责“能不能执行”,Projector 负责“模型能看到什么”,两者不能合并。
|
||||
|
||||
## 7. Control View 与 Model Observation 的字段边界
|
||||
|
||||
| 字段 | Harness | Agent | 原因 |
|
||||
|---|---:|---:|---|
|
||||
| `tool_call_id` | 是 | 是 | 引用和上一轮评价都需要 |
|
||||
| 实际 scope | 是 | 是 | Harness 去重;模型理解负向观察边界 |
|
||||
| 有界 evidence/events/rows | 是 | 是 | 模型推理所需事实 |
|
||||
| `evidence_status` | 是 | 是 | 区分候选证据、空结果和错误 |
|
||||
| `relevance_level` | 是 | 是 | 给模型粗粒度检索语义 |
|
||||
| `truncated` | 是 | 是 | 防止模型误以为结果完整 |
|
||||
| `returned_count` | 是 | 否 | Harness 统计,不必消耗模型上下文 |
|
||||
| normalized scope / duplicate identity | 是 | 否 | 内部控制实现 |
|
||||
| 连续 NO_GAIN、阈值和剩余预算 | 是 | 否 | 防止模型围绕限制博弈 |
|
||||
| raw response / 检索轨迹 / 原始分数 | 是 | 否 | 敏感且体积不可控 |
|
||||
|
||||
只有 Harness 必须改变模型行为时,才注入 `STOP_REQUIRED`、错误码或已检查范围等有限控制信息。
|
||||
|
||||
## 8. 存储决策:为什么是 Redis canonical + MySQL metadata
|
||||
|
||||
### 8.1 不把完整 raw 长期写 MySQL
|
||||
|
||||
完整 Tool 结果可能包含日志、SQL 查询结果和内部知识内容。长期保存会扩大泄露半径,也会让 JPA audit 成为第二个事实源。MySQL 只保存运营和对账需要的字段,可以长时间保留而不复制正文。
|
||||
|
||||
### 8.2 Canonical 读取不续期
|
||||
|
||||
Redis record 在创建时设置 TTL,读取或状态更新不恢复初始 TTL。原因是:如果一次历史查询就能续期,敏感 raw 可能因审计访问而永久存在。
|
||||
|
||||
状态更新保留当前剩余 TTL,代价是 read-TTL-write 存在小的并发窗口,但它比无界续期更符合数据治理目标。
|
||||
|
||||
### 8.3 raw 超限为什么不截断
|
||||
|
||||
raw 是内部事实。如果静默截断后仍标记 READY,系统无法区分“backend 只返回这些内容”和“Harness 丢掉了内容”。因此 raw 或完整 record 超限时调用进入 ERROR。
|
||||
|
||||
Agent projection 可以截断,因为它本来就是面向消费的摘要,但必须通过 `truncated=true` 明示不完整。
|
||||
|
||||
## 9. 真实问题如何改变设计
|
||||
|
||||
| 真实问题 | 暴露的错误假设 | 最终修正 |
|
||||
|---|---|---|
|
||||
| Tool raw 直接进入 Agent | backend 输出天然适合模型消费 | 增加 Tool-specific projector 和白名单 observation |
|
||||
| Trace、raw、ContextPack、重复正文同时存在 | 多保存几份可以提高可观测性 | canonical、model view、metadata audit 明确分层 |
|
||||
| Mock 日志 0 命中返回 `success=false` | 没数据等于调用失败 | 技术执行状态与 NO_EVIDENCE 分离 |
|
||||
| RAG 的 `REFERENCE` 在投影中丢失 | 只要 evidence 非空就够了 | 保留 relevance_level,但不提升为根因证据 |
|
||||
| 生产 ObjectMapper 未注册 Java Time 模块,Redis 全部 STORE_ERROR | 单元测试序列化环境等同生产 | 使用生产装配验证 canonical record,并增加 live E2E |
|
||||
| backend 日志打印 raw/rewritten query | 排障信息可以直接进入普通日志 | 普通日志和 durable audit 只保留安全摘要 |
|
||||
|
||||
## 10. 代价与边界
|
||||
|
||||
这项设计不是免费的:
|
||||
|
||||
1. 每增加一种 Tool,都要同时定义 typed request/result、Adapter、Projector 和 EvidenceGuard 读取规则。
|
||||
2. Redis 在 TTL 内成为证据校验依赖;不可用时系统应 fail closed,而不是相信 Draft。
|
||||
3. Agent 看到的是有损投影,Projector 设计不当可能丢掉模型真正需要的诊断信号。
|
||||
4. durable audit 不能替代 canonical replay;TTL 到期后只能解释调用元数据,无法恢复完整正文。
|
||||
|
||||
但这些成本换来了明确的责任:Backend 决定原始事实,Projector 决定模型可见范围,Canonical Store 决定短期验真事实,Audit 决定长期允许保存什么。
|
||||
|
||||
## 11. 如何验证
|
||||
|
||||
| 验证内容 | 代表性测试或证据 |
|
||||
|---|---|
|
||||
| Run mismatch、未授权、非只读、重复 ID、预算和超限 | `ToolBoundaryTest` |
|
||||
| PROJECTING / READY / ERROR 和 TTL 行为 | `CanonicalInvocationStoreTest` |
|
||||
| RAG evidence identity、relevance 和 bytes | `RagResultProjectorTest` |
|
||||
| 日志脱敏、模式聚合、空结果和采样 | `QueryLogsResultProjectorTest` |
|
||||
| MySQL 行列/敏感字段/容量边界 | `MysqlResultProjectorTest` |
|
||||
| typed Tool contract 不漂移 | 三类 `*ToolContractTest` |
|
||||
| 生产 Redis serializer 与 ObjectMapper | single-react cleanup live E2E |
|
||||
|
||||
单元测试只能证明数据变换和状态机;生产 Redis serializer、实际 Tool Calling ID 和 backend 返回格式仍必须通过 live E2E 验证。
|
||||
|
||||
## 12. 与后续机制的关系
|
||||
|
||||
Tool 双视图只解决“什么是可验证的 Tool 事实、模型允许看到什么”,并不解决:
|
||||
|
||||
- 这些事实是否支持最终结论:见[Harness证据安全链-从引用真实到结论可发布.md](Harness证据安全链-从引用真实到结论可发布.md);
|
||||
- Agent 是否应该继续查询:见[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)。
|
||||
@@ -0,0 +1,302 @@
|
||||
# Harness 信息增益停止:让无证据诊断正常收敛
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**主题**:Information Gain、Progress Tracker、STOP_REQUIRED 与过程型 Fallback
|
||||
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
|
||||
**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
|
||||
|
||||
## 1. 预算能限制损失,不能判断何时完成
|
||||
|
||||
在知识库只有通用说明、日志持续为空或查询条件不足时,ReAct Agent 容易出现一种看似合理的行为:不断修改关键词、时间范围或查询表达,再调用一次 Tool。
|
||||
|
||||
每一次调用单独看都可能合法,但整个 Run 没有获得新信息。旧系统只能等到模型次数、Tool 次数、Token 或 deadline 耗尽,再以 `BUDGET_EXHAUSTED` 或 `INTERNAL_FAILURE` 结束。
|
||||
|
||||
这暴露了两个被混淆的问题:
|
||||
|
||||
```text
|
||||
资源预算:这次 Run 最多允许消耗多少?
|
||||
业务收敛:继续查询是否仍可能推进当前诊断?
|
||||
```
|
||||
|
||||
预算是最后一道资源保护,不能承担正常停止策略。否则“当前证据不足”会被错误表达成“系统执行失败”。
|
||||
|
||||
信息增益停止契约的目标是:在硬预算之前识别连续无进展,使 Agent 停止调用 Tool,并把已经完成的有限检查发布为诚实、可验证的业务结果。
|
||||
|
||||
## 2. 先划清三方判断权
|
||||
|
||||
设计停止机制时最危险的做法,是让某一层承担它无法可靠完成的判断。
|
||||
|
||||
| 参与者 | 能可靠判断什么 | 不能判断什么 |
|
||||
|---|---|---|
|
||||
| Tool / Projector | 是否执行成功、结果是否为空、返回数量、实际 scope、是否截断 | 内容是否支持当前诊断假设 |
|
||||
| Diagnosis Agent | 非空内容是否确认、排除或缩小当前假设 | 是否还能绕过预算、重复和饱和门禁 |
|
||||
| Harness | scope 是否重复、连续 NO_GAIN、协议是否合规、是否允许继续 | 业务根因是什么 |
|
||||
| Release | 已有进展可以发布成哪种安全结果 | 是否应该重新调用 Tool |
|
||||
|
||||
最终原则是:
|
||||
|
||||
> Tool 提供客观结果,模型判断语义价值,Harness 拥有最终停止权。
|
||||
|
||||
模型可以选择主动结束,但不能通过继续发 Tool Call 绕过 Harness 已经确定的饱和或预算状态。
|
||||
|
||||
## 3. 为什么信息增益只有两个值
|
||||
|
||||
当前契约只保留:
|
||||
|
||||
```text
|
||||
information_gain = GAINED | NO_GAIN
|
||||
```
|
||||
|
||||
没有 `HIGH / MEDIUM / LOW`,也没有置信分数。停止控制只需要知道结果是否推进了当前诊断;引入更多等级会带来阈值解释、跨 Tool 标定和模型输出不稳定,却不一定改变最终动作。
|
||||
|
||||
判定标准是:
|
||||
|
||||
| 结果 | Information Gain | 生产者 |
|
||||
|---|---|---|
|
||||
| Tool 执行失败 | 不产生 | 进入技术失败流程 |
|
||||
| `evidence_status=NO_EVIDENCE` | `NO_GAIN` | Harness |
|
||||
| 相同 `tool_name + normalized_scope` | `NO_GAIN` | Harness,并拒绝重复执行 |
|
||||
| 成功非空,确认/排除/缩小假设 | `GAINED` | Diagnosis Agent |
|
||||
| 成功非空,但只是通用知识、重复内容或无关事实 | `NO_GAIN` | Diagnosis Agent |
|
||||
|
||||
RAG 的 `REFERENCE` 不能自动映射为 `NO_GAIN`。它只表示检索相关度一般;一段一般相关的资料可能仍排除一个假设,也可能完全无用,需要模型结合诊断上下文判断。
|
||||
|
||||
## 4. 为什么模型在下一次 Tool Call 中评价上一轮
|
||||
|
||||
模型只有在读到 Tool Observation 后,才能判断它是否有语义增益。但如果要求模型单独输出一条 progress 消息,就必须增加新的协议轮次或 Progress Judge。
|
||||
|
||||
当前设计利用模型已经要做的下一步行为:
|
||||
|
||||
- 输出 DiagnosisDraft,表示主动结束;
|
||||
- 发起下一次 Tool Call,表示希望继续。
|
||||
|
||||
当模型选择继续时,下一次 Tool Call Envelope 必须携带对上一轮的评价:
|
||||
|
||||
```json
|
||||
{
|
||||
"previous_observation": {
|
||||
"tool_call_id": "call-123",
|
||||
"information_gain": "NO_GAIN"
|
||||
},
|
||||
"input": {
|
||||
"query": "新的业务查询参数"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Interceptor 在调用业务 Tool 之前完成三件事:
|
||||
|
||||
1. 校验 `previous_observation.tool_call_id` 是否正好是当前 pending 调用;
|
||||
2. 应用 `GAINED / NO_GAIN`,更新连续计数和收集状态;
|
||||
3. 剥离 `previous_observation`,只把 `input` 传给原业务 Tool。
|
||||
|
||||
这是 Agent-facing Tool Schema 的协议变化,但 RAG、日志和 MySQL 的业务 request 并没有被控制字段污染。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as Diagnosis Agent
|
||||
participant I as Tool Interceptor
|
||||
participant P as Progress Tracker
|
||||
participant T as Business Tool
|
||||
|
||||
M->>I: Tool Call #1 + input
|
||||
I->>P: no pending observation
|
||||
I->>T: business input
|
||||
T-->>I: successful non-empty observation
|
||||
I->>P: mark call #1 pending evaluation
|
||||
I-->>M: bounded observation
|
||||
|
||||
M->>I: Tool Call #2 + previous_observation(#1, NO_GAIN)
|
||||
I->>P: validate ID and apply NO_GAIN
|
||||
alt 仍为 COLLECTING
|
||||
I->>T: strip control fields, execute input #2
|
||||
else 达到 SATURATED
|
||||
I-->>M: STOP_REQUIRED
|
||||
end
|
||||
```
|
||||
|
||||
如果模型读完 observation 后直接输出 Draft,就不需要额外评价最后一轮。因为它已经通过实际行为表达“停止”,Harness 也不需要为收集一个统计字段强迫模型再调用 Tool。
|
||||
|
||||
## 5. ProgressTracker 保存什么,不保存什么
|
||||
|
||||
`DiagnosisProgressTracker` 是 `RunContext` 中的线程安全状态句柄,保存:
|
||||
|
||||
- 已完成的 `tool_name + normalized_scope`;
|
||||
- 已完成 Tool Call identity;
|
||||
- 当前等待模型评价的 `pendingToolCallId`;
|
||||
- 连续 `NO_GAIN` 次数;
|
||||
- 连续 progress protocol violation 次数;
|
||||
- `COLLECTING / SATURATED` 状态;
|
||||
- stop reason;
|
||||
- STOP_REQUIRED 是否已经交付。
|
||||
|
||||
它不保存 request、raw response、agent result 或模型 thought。完整 Tool 事实仍属于 Canonical Store。Tracker 只保存作出停止决策需要的最小 identity 和计数,避免出现第二份 Tool 真相。
|
||||
|
||||
结束时,`DiagnosisProgressProjector` 根据 completed identity 回读 canonical READY records,投影成有界 `ProgressSnapshot`。无法验证、不可读取或格式非法的记录不会被发布,只会形成安全 limitation。
|
||||
|
||||
## 6. 重复检测为什么只做参数级
|
||||
|
||||
Harness 使用 `ToolScopeNormalizer` 将业务输入转换成稳定 scope,再比较:
|
||||
|
||||
```text
|
||||
tool_name + normalized_scope
|
||||
```
|
||||
|
||||
这可以识别字段顺序、格式差异下的完全相同查询,并在调用 backend 前拒绝重复执行。
|
||||
|
||||
首版没有做自然语言语义去重,例如以下两条 query 可能语义相同,但不会被代码证明为同一 scope:
|
||||
|
||||
```text
|
||||
“查询支付超时日志”
|
||||
“查找支付请求 timeout 记录”
|
||||
```
|
||||
|
||||
原因是语义去重需要 embedding、模型判断或跨 Tool 指纹,会引入新的不确定性和误杀风险。当前边界选择了可以确定性证明的参数重复;语义近似由模型的 `NO_GAIN` 义务约束。
|
||||
|
||||
## 7. 收集状态机
|
||||
|
||||
连续无增益阈值由配置控制,当前默认值为 2。`GAINED` 会清零连续计数,避免一次早期空查使后续有效取证被过早停止。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> COLLECTING
|
||||
COLLECTING --> COLLECTING: GAINED / consecutiveNoGain=0
|
||||
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
|
||||
COLLECTING --> SATURATED: NO_GAIN / 达到阈值
|
||||
SATURATED --> STOP_CHANCE: 交付一次 STOP_REQUIRED
|
||||
STOP_CHANCE --> RELEASE: 模型输出 Draft
|
||||
STOP_CHANCE --> TERMINATED: 模型再次请求 Tool
|
||||
```
|
||||
|
||||
当某次 READY 结果是 NO_EVIDENCE 时,Harness 可以立即累计 `NO_GAIN`。当成功非空结果需要模型评价时,Tracker 设置 pending ID;上一轮未评价前,新的 Tool Call 不会被执行。
|
||||
|
||||
达到 `SATURATED` 后,Harness 给模型一次合法完成机会,而不是在 Tool response 中立刻抛出通用错误。STOP_REQUIRED 只交付一次;模型仍继续请求 Tool 时,`DiagnosisCollectionStoppedException` 将受控停止穿出框架 ReAct loop。
|
||||
|
||||
## 8. 协议错误为什么不能计为 NO_GAIN
|
||||
|
||||
真实 E2E 曾出现 9 次 `INVALID_PROGRESS_PROTOCOL` Tool 请求拒绝和 13 轮 Diagnosis Agent 模型调用。模型没有正确回传上一轮评价,但这些拒绝也没有增加 NO_GAIN,最终持续空转。
|
||||
|
||||
一种看似简单的修复是把协议错误也计为 NO_GAIN。但这会混淆两个事实:
|
||||
|
||||
- `NO_GAIN` 表示 Tool 结果没有推进诊断;
|
||||
- 协议错误表示模型没有遵守控制契约,Tool 根本没有执行。
|
||||
|
||||
因此引入独立的协议错误计数和 `PROGRESS_PROTOCOL_VIOLATED`:
|
||||
|
||||
1. 第一次错误返回可修正 observation,包含 violation type、缺失字段、期望的上一轮 ID 和允许值;
|
||||
2. 连续错误达到配置阈值后进入 SATURATED;
|
||||
3. 交付一次 `STOP_REQUIRED / PROGRESS_PROTOCOL_VIOLATED`;
|
||||
4. 再次请求 Tool 时受控停止。
|
||||
|
||||
协议错误 Trace 使用 `TOOL_REQUEST_REJECTED`,不能记录成 `TOOL_INVOCATION`,因为 backend 从未被调用,Tool 预算和实际执行数也不应被污染。
|
||||
|
||||
## 9. 三种停止原因必须分开
|
||||
|
||||
| Stop Reason | 含义 | 是否等于技术失败 |
|
||||
|---|---|---:|
|
||||
| `INFORMATION_SATURATED` | 连续结果没有推进诊断 | 否 |
|
||||
| `BUDGET_LIMIT_REACHED` | 达到模型、Tool、Token 或 bytes 预算边界 | 不一定;有安全进展时可发布过程 Fallback |
|
||||
| `PROGRESS_PROTOCOL_VIOLATED` | 模型连续违反 progress Envelope | 协议失败;有安全进展时仍可保留过程价值 |
|
||||
|
||||
信息饱和不能伪装成预算耗尽,否则无法判断阈值是否合理;预算耗尽也不能伪装成信息饱和,因为可能是在持续获得有效证据时资源不足。
|
||||
|
||||
这些 stop reason 是 Harness 内部控制语义,不直接作为第二套公开生命周期。最终仍由 Release 映射为 `SUCCESS / FALLBACK / FAILED / CANCELLED`。
|
||||
|
||||
## 10. 停止以后如何形成用户结果
|
||||
|
||||
停止本身不是答案。系统需要把已经完成的检查转换成可发布内容,又不能依赖预算耗尽后额外调用模型。
|
||||
|
||||
`DiagnosisProgressProjector` 从 canonical READY results 生成:
|
||||
|
||||
- verified sources;
|
||||
- observed facts,包括限定范围的空结果;
|
||||
- 实际查询 scope;
|
||||
- 投影失败、截断或不可读取形成的 limitations;
|
||||
- stop reason。
|
||||
|
||||
Release 根据现有进展决定:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["Agent 主动无结论<br/>或 Harness 受控停止"] --> P["ProgressSnapshot"]
|
||||
P --> Q{"存在已验真 observed facts?"}
|
||||
Q -->|"是"| I["INSUFFICIENT_EVIDENCE<br/>展示已检查内容和下一步"]
|
||||
Q -->|"否"| M{"Draft 声明 missing_info?"}
|
||||
M -->|"是"| C["MISSING_REQUIRED_CONTEXT"]
|
||||
M -->|"否"| F["FAILED / fail closed"]
|
||||
```
|
||||
|
||||
`conclusion=null` 是合法 Draft。它允许 Agent 在缺少企业、时间、服务或错误信息时零次调用 Tool,直接报告 `missing_info`,避免为了表现“已经排查”而执行无明确范围的查询。
|
||||
|
||||
## 11. 为什么没有引入更多控制字段
|
||||
|
||||
### 11.1 不使用 `new_count`
|
||||
|
||||
“新记录数量”需要跨 RAG 文档、日志事件和数据库行建立稳定指纹,而且新数据不等于对当前假设有用。它增加了复杂度,却不能替代语义增益判断。
|
||||
|
||||
### 11.2 不使用 `next_action`
|
||||
|
||||
模型发起 Tool Call已经表示继续,输出 Draft 已经表示结束。再要求 `CONTINUE / STOP` 只会形成一套可能与实际行为冲突的声明状态。
|
||||
|
||||
### 11.3 不增加 Progress Judge
|
||||
|
||||
独立 Judge 会为每轮 Tool 结果增加模型调用、延迟和失败面。空结果和完全重复 scope 本可由代码判断;其他内容由正在做诊断的 Agent 评价即可。
|
||||
|
||||
### 11.4 不向模型公开剩余预算和阈值
|
||||
|
||||
模型只需要知道是否必须停止,不需要围绕“还剩几次”规划消耗。阈值、计数和预算属于 Harness Control View,只有 STOP_REQUIRED 等必要指令进入模型上下文。
|
||||
|
||||
## 12. Prompt 与硬门禁如何分工
|
||||
|
||||
Prompt 仍然需要告诉模型:
|
||||
|
||||
- 不必须得出根因;
|
||||
- `conclusion=null` 是合法完成;
|
||||
- 缺少必要上下文时可以零 Tool 结束;
|
||||
- 正确但不能推进假设的内容也是 NO_GAIN;
|
||||
- 不要通过改写相似关键词重复查询;
|
||||
- 收到 STOP_REQUIRED 后必须停止。
|
||||
|
||||
但 Prompt 只是帮助模型做出正确选择,不构成系统保证。重复 scope、pending evaluation、饱和状态、预算和一次性 STOP_REQUIRED 都由代码门禁执行。
|
||||
|
||||
## 13. 真实问题如何改变设计
|
||||
|
||||
| 真实现象 | 被证伪的假设 | 设计修正 |
|
||||
|---|---|---|
|
||||
| 空日志、通用知识仍不断改写查询 | 模型会自然意识到没有进展 | 明确信息增益义务和 Harness 饱和状态 |
|
||||
| 最终以 BUDGET_EXHAUSTED 结束 | 硬预算可以充当正常停止 | 预算与信息饱和分离 |
|
||||
| `REFERENCE` 非空结果持续触发查询 | 非空候选就是有价值证据 | 检索相关度与诊断增益分离 |
|
||||
| 相同 scope 被反复执行 | Prompt 足以禁止重复 | Harness 参数级去重并在 backend 前拒绝 |
|
||||
| 9 次协议拒绝仍消耗 13 轮模型 | 错误 observation 会让模型自修复 | 可修正反馈 + 独立协议阈值 + STOP_REQUIRED |
|
||||
| 非法 Draft 使已完成检查丢失 | 只有合法最终 Draft 才有用户价值 | 已验真 ProgressSnapshot 可形成过程型 Fallback |
|
||||
| 缺少企业/时间仍被迫调用 Tool | Tool 调用次数大于零才算诊断 | 允许零 Tool、missing context 合法结束 |
|
||||
|
||||
## 14. 代价与当前边界
|
||||
|
||||
1. 模型侧 Tool Schema 增加了 `previous_observation + input`,这是明确的 Agent-facing 协议变化。
|
||||
2. 首版只能确定性识别参数相同的重复 scope,不能阻止所有自然语言近义改写。
|
||||
3. 默认连续 NO_GAIN 阈值 2 是工程起点,需要依靠固定评测集校准;太小会过早停止,太大会增加空转。
|
||||
4. 最后一轮非空 Tool 结果如果模型直接输出 Draft,Tracker 不强制收集其 information gain;这是减少无意义协议轮次的主动取舍。
|
||||
5. ProgressSnapshot 依赖 canonical record 仍在 TTL 内且可解析,无法验真的进展不会被发布。
|
||||
6. 受控预算停止只有在已有安全进展时才能转为 Fallback;没有可验证内容仍然 fail closed。
|
||||
|
||||
## 15. 如何验证
|
||||
|
||||
| 需要证明 | 代表性测试或 E2E |
|
||||
|---|---|
|
||||
| NO_EVIDENCE 自动累计 NO_GAIN,GAINED 清零 | `DiagnosisProgressTrackerTest` |
|
||||
| 重复 scope 在 backend 前被拒绝 | `HarnessToolInterceptorTest`、`ToolScopeNormalizerTest` |
|
||||
| pending evaluation 的缺失、乱序和意外回传被拒绝 | Interceptor protocol focused cases |
|
||||
| 连续协议错误达到阈值并只交付一次 STOP_REQUIRED | Tracker + Interceptor tests |
|
||||
| 模型主动停止、饱和停止和预算停止都能进入 Release | `DiagnosisAgentUseCaseTest`、`DiagnosisReleaseUseCaseTest` |
|
||||
| 非法 Draft 只有在存在安全进展时降级 | `DiagnosisChatExecutorTest` |
|
||||
| Tool 拒绝与实际 Tool 执行分开审计 | exact-run Trace |
|
||||
| 未知 Query 不再以通用内部错误结束 | ISS-016 named SSE E2E |
|
||||
|
||||
其中一条 live E2E 曾准确暴露“协议拒绝不累计 NO_GAIN”的盲区。这说明停止机制不能只验证最终 SSE,还要核对模型轮次、Tool 实际执行数、Tool 拒绝数、Token 和 Timeline 序列。
|
||||
|
||||
## 16. 与另外两项设计的关系
|
||||
|
||||
信息增益依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供客观 evidence status、scope 和有界 observation;停止后的 ProgressSnapshot 和最终 Fallback 依赖[证据安全链](Harness证据安全链-从引用真实到结论可发布.md)确保只发布当前 Run 可验证的事实。
|
||||
|
||||
三者组合后,Harness 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。
|
||||
@@ -0,0 +1,440 @@
|
||||
# Harness 生命周期与状态
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**状态**:当前实现口径
|
||||
**术语前置**:[CONTEXT.md](CONTEXT.md)
|
||||
|
||||
## 1. 先澄清:系统不存在一条包含所有状态的总状态机
|
||||
|
||||
当前 Harness 有多组正交状态:
|
||||
|
||||
- RunState 控制内存执行终态;
|
||||
- ChatApplicationStatus 表示用户可见处理阶段;
|
||||
- InvocationStatus 表示单次 Tool invocation 生命周期;
|
||||
- EvidenceStatus 表示 Tool 客观结果;
|
||||
- CollectionState 表示能否继续收集证据;
|
||||
- StopReason 表示停止收集的内部原因;
|
||||
- SemanticVerdict 表示结论支持度;
|
||||
- ReleaseOutcome 表示最终发布结果;
|
||||
- SSE Session State 表示连接能否继续发送。
|
||||
|
||||
它们在一次请求中并行演进,只在少数边界发生映射。生命周期文档的目标不是把它们合并,而是说明谁先发生、由谁拥有、在哪里汇合。
|
||||
|
||||
## 2. 一次 Run 的主时间线
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client / SSE
|
||||
participant A as ChatApplicationUseCase
|
||||
participant H as Harness Core
|
||||
participant R as Intent Router
|
||||
participant D as Diagnosis Runtime
|
||||
participant L as Release Pipeline
|
||||
participant P as Persistence / Trace
|
||||
|
||||
C->>A: query + optional sessionId
|
||||
A->>A: 读取 routing history / PreviousTurn
|
||||
A->>H: startRun(sessionId)
|
||||
H-->>A: RUNNING RunContext
|
||||
A->>P: diagnosis_run=RUNNING + RUN_STARTED
|
||||
A-->>C: metadata(sessionId, runId)
|
||||
|
||||
A->>R: route(query, bounded history)
|
||||
R-->>A: IntentType
|
||||
|
||||
alt SYSTEM_CHAT
|
||||
A->>A: 单轮受控模型回答
|
||||
else KNOWLEDGE_QUERY
|
||||
A->>A: 一次 RAG + 单轮答案
|
||||
else DIAGNOSIS
|
||||
A->>D: Agent ReAct / Tool / Progress
|
||||
D-->>A: Draft 或 Controlled Stop
|
||||
A->>L: release(Draft/Progress/StopReason)
|
||||
L-->>A: SUCCESS Draft 或 SafeFallback
|
||||
end
|
||||
|
||||
alt 正常完成
|
||||
A->>H: completeSuccess(预算 Fallback 特例除外)
|
||||
A->>P: 持久化 release outcome / safe content / usage
|
||||
A->>P: RUN_FINISHED
|
||||
A-->>C: content + done
|
||||
else 失败
|
||||
A->>H: completeFailure 或读取既有终态
|
||||
A->>P: 持久化 FAILED/CANCELLED
|
||||
A->>P: RUN_FINISHED
|
||||
A-->>C: failure + done(FAILED)
|
||||
end
|
||||
```
|
||||
|
||||
创建顺序很重要:Application 先读取会话上下文,再创建 RunContext、持久化 RUNNING、记录 RUN_STARTED,之后才进入路由和执行。SSE 的 metadata 在 `onStarted` 中发布 exact sessionId/runId,后续结果必须匹配这组身份。
|
||||
|
||||
## 3. Run 生命周期
|
||||
|
||||
### 3.1 状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> RUNNING
|
||||
RUNNING --> SUCCESS: 正常路径完成
|
||||
RUNNING --> FAILED: 不可恢复内部失败
|
||||
RUNNING --> CANCELLED: 客户端断开或用户取消
|
||||
RUNNING --> TIMED_OUT: deadline 到达
|
||||
RUNNING --> BUDGET_EXHAUSTED: 任一硬预算超限
|
||||
|
||||
SUCCESS --> [*]
|
||||
FAILED --> [*]
|
||||
CANCELLED --> [*]
|
||||
TIMED_OUT --> [*]
|
||||
BUDGET_EXHAUSTED --> [*]
|
||||
```
|
||||
|
||||
`RunLifecycle.finish` 使用原子 compare-and-set:只有从“尚无 termination”到某个终态的第一次转换成功。所有终态都不可再次转换。
|
||||
|
||||
### 3.2 状态含义
|
||||
|
||||
| RunState | 含义 | 常见来源 |
|
||||
|---|---|---|
|
||||
| `RUNNING` | 尚未产生 RunTermination | `startRun` 后默认状态 |
|
||||
| `SUCCESS` | Application 已形成正常可持久化结果 | 正常内容,或非预算类 SafeFallback |
|
||||
| `FAILED` | 不可恢复执行失败 | 路由不可用、模型失败、无安全进展的非法 Draft 等 |
|
||||
| `CANCELLED` | 协作取消已成为第一个终态 | 客户端断开、用户请求 |
|
||||
| `TIMED_OUT` | `checkActive` 发现 deadline 已到 | 模型/Tool/Guard 边界前后检查 |
|
||||
| `BUDGET_EXHAUSTED` | 模型、Tool、Token 或 bytes 预算超限 | `DiagnosisHarnessCore.exhaustBudget` |
|
||||
|
||||
### 3.3 RunState 与 ReleaseOutcome 不一一对应
|
||||
|
||||
最重要的反例是 Fallback:
|
||||
|
||||
- 证据不足、缺少上下文、语义不支持等正常降级,RunState 最终为 `SUCCESS`,ReleaseOutcome 为 `FALLBACK`;
|
||||
- 预算耗尽后已经有安全 ProgressSnapshot,RunState 保持 `BUDGET_EXHAUSTED`,但 ReleaseOutcome 可以为 `FALLBACK`;
|
||||
- 超时或预算耗尽且无法形成安全内容时,RunState 分别保持 `TIMED_OUT / BUDGET_EXHAUSTED`,ReleaseOutcome 映射为 `FAILED`。
|
||||
|
||||
所以不能从 ReleaseOutcome 反推出精确 RunState,也不能把 `FALLBACK` 当作 RunState。
|
||||
|
||||
## 4. 取消生命周期
|
||||
|
||||
`RunCancellation` 使用 first-reason-wins。取消原因包括:
|
||||
|
||||
| Reason | RunState 映射 |
|
||||
|---|---|
|
||||
| `CLIENT_DISCONNECTED` | `CANCELLED` |
|
||||
| `USER_REQUESTED` | `CANCELLED` |
|
||||
| `DEADLINE_EXCEEDED` | `TIMED_OUT` |
|
||||
| `BUDGET_EXHAUSTED` | `BUDGET_EXHAUSTED` |
|
||||
| `INTERNAL_FAILURE` | `FAILED` |
|
||||
|
||||
生命周期和取消句柄互相配合:取消回调尝试完成 RunLifecycle;deadline 和预算路径也会先确定终态,再触发取消阻止后续工作。
|
||||
|
||||
取消语义是协作式的:
|
||||
|
||||
1. 后续模型、Tool、Guard 边界调用 `checkActive` 时立即失败;
|
||||
2. SSE 断开后不再发送 content;
|
||||
3. first-terminal-wins 阻止迟到成功覆盖 CANCELLED/TIMED_OUT;
|
||||
4. 已经进入 Provider 的同步调用是否物理停止,取决于底层客户端,Harness 不作虚假保证。
|
||||
|
||||
## 5. Application 阶段不是生命周期状态
|
||||
|
||||
`ChatApplicationStatus` 用于 SSE status 事件:
|
||||
|
||||
```text
|
||||
ROUTING
|
||||
SYSTEM_RESPONDING
|
||||
KNOWLEDGE_SEARCHING
|
||||
KNOWLEDGE_ANSWERING
|
||||
DIAGNOSIS_RUNNING
|
||||
SAFETY_VALIDATING
|
||||
```
|
||||
|
||||
这些值只是用户可见的处理阶段:
|
||||
|
||||
- 不是严格完备的状态机;
|
||||
- 不表示终态;
|
||||
- 不持有取消或预算;
|
||||
- 不保证每个 Run 都经过全部阶段。
|
||||
|
||||
例如 Diagnosis Run 通常经过 `ROUTING -> DIAGNOSIS_RUNNING -> SAFETY_VALIDATING`,System Chat 则经过 `ROUTING -> SYSTEM_RESPONDING`。
|
||||
|
||||
## 6. Diagnosis Agent 执行生命周期
|
||||
|
||||
`DiagnosisAgentExecution` 只有两种合法形态,不额外定义一套枚举:
|
||||
|
||||
| 形态 | 字段 | 含义 |
|
||||
|---|---|---|
|
||||
| Completed | `draft != null, stopReason=null` | Agent 输出严格合法 DiagnosisDraft |
|
||||
| Controlled Stop | `draft=null, stopReason != null` | Tool loop 因信息饱和、预算或协议错误受控停止 |
|
||||
|
||||
其他情况通过异常表达:
|
||||
|
||||
- 空 Draft;
|
||||
- 非法 JSON;
|
||||
- Schema 不合法;
|
||||
- 模型调用失败;
|
||||
- RunAborted。
|
||||
|
||||
Draft 合同失败不是 DiagnosisStopReason。非法 Draft 会被丢弃;只有异常携带的 ProgressSnapshot 已包含可验真 facts,Application 才允许 Release 生成过程型 Fallback。
|
||||
|
||||
## 7. 单次 Tool Invocation 生命周期
|
||||
|
||||
### 7.1 状态转换
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PREFLIGHT
|
||||
PREFLIGHT --> REJECTED: invalid run/id/auth/readonly/request/budget/store
|
||||
PREFLIGHT --> PROJECTING: canonical begin 成功
|
||||
PROJECTING --> READY: backend + projection + store 成功
|
||||
PROJECTING --> ERROR: execution/projection/size/budget/store error
|
||||
READY --> [*]
|
||||
ERROR --> [*]
|
||||
REJECTED --> [*]
|
||||
```
|
||||
|
||||
`PREFLIGHT` 和 `REJECTED` 是本文用于解释流程的阶段,不是 `InvocationStatus` 枚举值。canonical record 只有:
|
||||
|
||||
- `PROJECTING`:已创建,尚未形成可引用结果;
|
||||
- `READY`:执行和投影完成,可以被当前 Run 引用;
|
||||
- `ERROR`:调用失败,不可引用。
|
||||
|
||||
部分 preflight 失败发生在 canonical begin 之前,因此可能只有 `ToolBoundaryResult.ERROR` 和审计事件,没有 canonical ERROR record。
|
||||
|
||||
### 7.2 EvidenceStatus 是另一条轴
|
||||
|
||||
| InvocationStatus | EvidenceStatus | 语义 |
|
||||
|---|---|---|
|
||||
| `PROJECTING` | `null` | 未完成 |
|
||||
| `READY` | `EVIDENCE_FOUND` | 技术完成,存在候选内容 |
|
||||
| `READY` | `NO_EVIDENCE` | 技术完成,当前 scope 为空 |
|
||||
| `ERROR` | `ERROR` | 技术失败 |
|
||||
|
||||
`READY + EVIDENCE_FOUND` 仍不表示证据足以支持结论;后续还要经过 InformationGain、EvidenceGuard 和 SemanticGuard。
|
||||
|
||||
## 8. 信息收集生命周期
|
||||
|
||||
### 8.1 Collection State
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> COLLECTING
|
||||
COLLECTING --> COLLECTING: GAINED / 清零 no-gain
|
||||
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
|
||||
COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值
|
||||
COLLECTING --> SATURATED: 连续 progress protocol violation 达阈值
|
||||
SATURATED --> SATURATED: 终态,不再允许新 evidence Tool
|
||||
```
|
||||
|
||||
`SATURATED` 只表示证据收集不再允许继续。它不是整个 Run 的终态,Agent 仍有一次机会输出 Draft,之后进入 Release。
|
||||
|
||||
预算超限不会把 CollectionState 改成 SATURATED。它将 RunState 置为 `BUDGET_EXHAUSTED`,并把内部 DiagnosisStopReason 标记为 `BUDGET_LIMIT_REACHED`。
|
||||
|
||||
### 8.2 InformationGain
|
||||
|
||||
- `NO_EVIDENCE` 和完全重复的 `tool_name + normalized_scope` 由 Harness 产生 `NO_GAIN`;
|
||||
- 其他成功非空结果由 Diagnosis Agent 评价 `GAINED / NO_GAIN`;
|
||||
- `GAINED` 清零连续 NO_GAIN;
|
||||
- 技术失败和 progress 协议错误不产生 InformationGain。
|
||||
|
||||
### 8.3 StopReason
|
||||
|
||||
| DiagnosisStopReason | 触发条件 | 对 CollectionState 的影响 |
|
||||
|---|---|---|
|
||||
| `INFORMATION_SATURATED` | 连续 NO_GAIN 达阈值 | 进入 SATURATED |
|
||||
| `PROGRESS_PROTOCOL_VIOLATED` | 连续协议错误达阈值 | 进入 SATURATED |
|
||||
| `BUDGET_LIMIT_REACHED` | RunBudget 超限 | 不要求进入 SATURATED;Run 已终止 |
|
||||
|
||||
STOP_REQUIRED 是否已经交付由独立 boolean 记录,它不是第四种 CollectionState。一次指令交付后仍请求 Tool,会抛出受控停止异常。
|
||||
|
||||
## 9. Guard 与 Release 生命周期
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
I["DiagnosisAgentExecution"] --> D{"有合法 Draft?"}
|
||||
D -->|"否,受控停止"| PS["验证 ProgressSnapshot"]
|
||||
D -->|"是,无 conclusion"| NC["验证已有 Tool references"]
|
||||
D -->|"是,有 conclusion"| EG["EvidenceGuard initial"]
|
||||
|
||||
EG --> EV{"evidence valid?"}
|
||||
EV -->|"否"| ER["EvidenceRepair once"]
|
||||
ER --> RE["EvidenceGuard recheck"]
|
||||
EV -->|"是"| SG["SemanticGuard"]
|
||||
RE -->|"valid"| SG
|
||||
RE -->|"invalid"| EF["EVIDENCE_VALIDATION_FAILED"]
|
||||
|
||||
SG -->|"SUPPORTED"| SU["ReleaseOutcome.SUCCESS"]
|
||||
SG -->|"UNSUPPORTED"| SF["SEMANTIC_UNSUPPORTED"]
|
||||
SG -->|"unavailable"| UF["SEMANTIC_UNAVAILABLE"]
|
||||
|
||||
NC -->|"有 progress"| IF["INSUFFICIENT_EVIDENCE"]
|
||||
NC -->|"无 progress,有 missing_info"| MF["MISSING_REQUIRED_CONTEXT"]
|
||||
NC -->|"两者都无"| FAIL["FAILED"]
|
||||
PS -->|"有 verified facts"| IF
|
||||
PS -->|"无 verified facts"| FAIL
|
||||
|
||||
EF --> FB["ReleaseOutcome.FALLBACK"]
|
||||
SF --> FB
|
||||
UF --> FB
|
||||
IF --> FB
|
||||
MF --> FB
|
||||
```
|
||||
|
||||
`DiagnosisReleaseResult` 只表达 `SUCCESS` 或 `FALLBACK`。真正不可恢复的 `FAILED / CANCELLED` 由 Chat Application 异常路径映射。
|
||||
|
||||
## 10. RunState、ReleaseOutcome 和 FallbackType 映射
|
||||
|
||||
| 场景 | RunState | ReleaseOutcome | FallbackType / 内容 |
|
||||
|---|---|---|---|
|
||||
| 有结论且 Guards 通过 | `SUCCESS` | `SUCCESS` | Diagnosis report |
|
||||
| 缺少必要上下文,零 Tool 合法结束 | `SUCCESS` | `FALLBACK` | `MISSING_REQUIRED_CONTEXT` |
|
||||
| 有限检查后证据不足 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| 信息饱和后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| 协议停止后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
|
||||
| SemanticGuard 判定不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
|
||||
| SemanticGuard 技术不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
|
||||
| 预算耗尽但有安全进展 | `BUDGET_EXHAUSTED` | `FALLBACK` | 当前发布 `INSUFFICIENT_EVIDENCE` |
|
||||
| 超时,无法形成安全内容 | `TIMED_OUT` | `FAILED` | failure |
|
||||
| 预算耗尽且无安全进展 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
|
||||
| 不可恢复内部失败 | `FAILED` | `FAILED` | failure |
|
||||
| 客户端断开 | `CANCELLED` | `CANCELLED` | 不公开 CANCELLED done |
|
||||
|
||||
这张表解释了为什么不能只问“最后 status 是什么”。必须先确定是在问内存执行、发布结果还是 Fallback 原因。
|
||||
|
||||
## 11. SSE 连接生命周期
|
||||
|
||||
`ChatSseSession` 有自己的连接状态机:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> NEW
|
||||
NEW --> OPEN: onStarted / metadata
|
||||
NEW --> DISCONNECTED: 客户端提前断开
|
||||
OPEN --> OPEN: status events
|
||||
OPEN --> TERMINAL: content + done
|
||||
OPEN --> TERMINAL: failure + done(FAILED)
|
||||
OPEN --> DISCONNECTED: send failure / disconnect
|
||||
TERMINAL --> [*]
|
||||
DISCONNECTED --> [*]
|
||||
```
|
||||
|
||||
公开事件顺序是:
|
||||
|
||||
```text
|
||||
metadata -> status* -> content | failure -> done
|
||||
```
|
||||
|
||||
边界规则:
|
||||
|
||||
- content 最多发送一次;
|
||||
- result 的 sessionId/runId 必须匹配 metadata;
|
||||
- TERMINAL 或 DISCONNECTED 后拒绝迟到内容;
|
||||
- `ReleaseOutcome.CANCELLED` 不作为公开 done outcome;客户端已经断开时没有可靠发送目标;
|
||||
- 当前 `SseOutcome` 枚举未参与运行时协议,`ChatSseEvent.Done` 使用 `ReleaseOutcome`。
|
||||
|
||||
## 12. 持久化生命周期
|
||||
|
||||
### 12.1 创建
|
||||
|
||||
RunContext 创建后,Application 写入:
|
||||
|
||||
```text
|
||||
diagnosis_run.status = RUNNING
|
||||
session_id / run_id / query
|
||||
```
|
||||
|
||||
之后记录 intent。
|
||||
|
||||
### 12.2 完成映射
|
||||
|
||||
`JpaChatRunStore` 根据 ReleaseOutcome 映射数据库通用 status:
|
||||
|
||||
| ReleaseOutcome | diagnosis_run.status |
|
||||
|---|---|
|
||||
| `SUCCESS` | `SUCCESS` |
|
||||
| `FALLBACK` | `SUCCESS` |
|
||||
| `FAILED` | `FAILED` |
|
||||
| `CANCELLED` | `CANCELLED` |
|
||||
|
||||
这里的 `status=SUCCESS` 表示请求已被正常处理并形成安全内容,不表示一定有诊断 conclusion。
|
||||
|
||||
同时保存:
|
||||
|
||||
- `release_outcome`;
|
||||
- safe answer JSON;
|
||||
- 从 safe content 提取的 conclusion;
|
||||
- duration、Token、Agent step 数和实际 Tool call 数;
|
||||
- 仅在 `DIAGNOSIS + SUCCESS` 时保存 `published_result`。
|
||||
|
||||
Fallback 不进入下一轮 PreviousTurn。
|
||||
|
||||
### 12.3 当前可观测缺口
|
||||
|
||||
内存 `RunTermination.state/reason` 当前没有独立字段直接持久化。`RUN_FINISHED` 主要记录 ReleaseOutcome 和预算对账,精确的 TIMED_OUT/BUDGET_EXHAUSTED 原因需要结合异常路径和其他 Trace 事件判断。
|
||||
|
||||
因此数据库 `status`、ReleaseOutcome 和 Trace 都是必要观察面,任何一个都不是完整替代品。
|
||||
|
||||
## 13. Trace Timeline 如何对应生命周期
|
||||
|
||||
典型 Diagnosis SUCCESS:
|
||||
|
||||
```text
|
||||
RUN_STARTED
|
||||
ROUTING_ATTEMPT
|
||||
ROUTING_DECISION
|
||||
AGENT_MODEL_STEP
|
||||
TOOL_INVOCATION / TOOL_PROGRESS ...
|
||||
EVIDENCE_GUARD_INITIAL
|
||||
SEMANTIC_GUARD_ATTEMPT
|
||||
SEMANTIC_GUARD_DECISION
|
||||
RELEASE_DECISION SUCCESS
|
||||
RUN_FINISHED SUCCESS
|
||||
```
|
||||
|
||||
典型信息不足 Fallback:
|
||||
|
||||
```text
|
||||
RUN_STARTED
|
||||
ROUTING_ATTEMPT
|
||||
ROUTING_DECISION
|
||||
AGENT_MODEL_STEP
|
||||
TOOL_INVOCATION / TOOL_PROGRESS ...
|
||||
COLLECTION_STOP(可选)
|
||||
EVIDENCE_GUARD_INITIAL(无结论引用检查)
|
||||
RELEASE_DECISION FALLBACK
|
||||
RUN_FINISHED FALLBACK
|
||||
```
|
||||
|
||||
`TracePhase` 和 `TraceEventStatus` 用来组织 Timeline。它们描述事件发生在哪一阶段、该事件结果如何,不构成新的 Run 生命周期。
|
||||
|
||||
## 14. 并发和迟到结果规则
|
||||
|
||||
一次 Run 的终止安全依赖三层协作:
|
||||
|
||||
1. `RunLifecycle`:first-terminal-wins,终态不可覆盖;
|
||||
2. `DiagnosisHarnessCore.checkActive`:每个关键边界阻止终止后的新工作;
|
||||
3. `ChatSseSession`:TERMINAL/DISCONNECTED 后拒绝迟到 content。
|
||||
|
||||
这能保证逻辑上的“取消后不再发布”。它不等于强制终止底层线程或 Provider 计算;底层调用返回后仍要经过 active 和 SSE state 检查,迟到结果才会被丢弃。
|
||||
|
||||
## 15. 排障时应该先看哪个状态
|
||||
|
||||
| 问题 | 首先看 | 然后看 |
|
||||
|---|---|---|
|
||||
| 用户为什么收到 Fallback | `release_outcome + FallbackType` | RELEASE/EVIDENCE/SEMANTIC Trace |
|
||||
| Agent 为什么停止调用 Tool | `DiagnosisStopReason` | TOOL_PROGRESS、TOOL_REQUEST_REJECTED、COLLECTION_STOP |
|
||||
| Tool 为什么没有证据 | `InvocationStatus + EvidenceStatus` | canonical record 和 Tool audit error code |
|
||||
| Run 是超时还是预算耗尽 | 内存 termination(运行中)或相关 Trace/异常 | budget usage、collection stop、失败码 |
|
||||
| 数据库为什么 status=SUCCESS 但没有结论 | `release_outcome` | answer 的 content type / FallbackType |
|
||||
| 为什么没有 SSE done | `ChatSseSession.State` | client disconnect/send failure 和 Run cancellation |
|
||||
| 为什么下一轮没有上一轮上下文 | 是否 `DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result` | PublishedResultPolicy |
|
||||
|
||||
## 16. 生命周期不变量
|
||||
|
||||
1. 一个 Run 只能有一个 RunTermination。
|
||||
2. 一个公开 SSE 最多有一次 content/failure 和一次 done。
|
||||
3. Tool Invocation 只有 READY 才能被引用。
|
||||
4. READY 必须同时拥有 `EVIDENCE_FOUND` 或 `NO_EVIDENCE`。
|
||||
5. `NO_EVIDENCE` 不能被提升为全局否定。
|
||||
6. SATURATED 后不再执行新的 evidence Tool。
|
||||
7. StopReason 不直接决定公开内容,必须经过 Release。
|
||||
8. 有 conclusion 才执行完整 EvidenceGuard/Repair/SemanticGuard 链。
|
||||
9. FALLBACK 是正常发布结果,不等于 RunState.FAILED。
|
||||
10. 数据库 status、ReleaseOutcome 和 RunState 不能互相替代。
|
||||
@@ -0,0 +1,374 @@
|
||||
# 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<br/>Run 与公开用例"] --> CORE["core<br/>执行不变量"]
|
||||
APP --> AGENT["agent<br/>ReAct 接入"]
|
||||
AGENT --> PROGRESS["progress<br/>收敛控制"]
|
||||
AGENT --> TOOL["tool<br/>证据边界"]
|
||||
APP --> RELEASE["release<br/>唯一发布"]
|
||||
RELEASE --> GUARD["guard<br/>真实性与支持度"]
|
||||
CORE --> RETRY["retry<br/>显式 attempt"]
|
||||
CORE -.-> AUDIT["audit<br/>可观测账本"]
|
||||
AGENT -.-> AUDIT
|
||||
TOOL -.-> AUDIT
|
||||
RELEASE -.-> AUDIT
|
||||
CONTRACT["contract<br/>类型化语言"] -.-> 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,避免再造全局生命周期。
|
||||
@@ -0,0 +1,503 @@
|
||||
# Harness 设计:非确定性 Agent 的确定性控制边界
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**适用范围**:当前 MVP Chat / Diagnosis Harness
|
||||
**代码基线**:`com.superbiz.agent.harness`
|
||||
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
|
||||
**组件索引**:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md)
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
这个系统真正需要解决的,不是“怎样让模型多调用几个 Tool”,而是:
|
||||
|
||||
> 当业务推理由非确定性模型完成时,怎样保证每一次执行仍然有身份、有边界、有停止条件、有证据闭包,并且只发布系统能够负责的内容。
|
||||
|
||||
当前 Harness 给出的答案是职责分治:
|
||||
|
||||
- Diagnosis Agent 负责提出假设、选择 Tool、理解结果和撰写 Draft。
|
||||
- Harness 负责所有必须确定的事情:Run 生命周期、预算、取消、Tool 授权、结果隔离、停止、验真、发布和审计。
|
||||
- Tool 只报告客观执行结果,不宣称业务根因。
|
||||
- Guard 不重新做诊断,只回答受限的验证问题。
|
||||
- Release 是唯一对外发布决策点,只能发布原始安全 Draft 或确定性 SafeFallback。
|
||||
|
||||
这不是一个新的工作流引擎。Harness 不复制 ReAct 循环,不维护 Planner / Executor / Composer 图,也不替模型判断根因。它包围 ReAct 的不确定部分,在所有外部副作用和最终发布点建立确定性门禁。
|
||||
|
||||
## 2. 根问题:模型能推理,但系统必须能够负责
|
||||
|
||||
### 2.1 旧架构把“推理角色”当成了“安全边界”
|
||||
|
||||
旧方案使用 Planner、Executor、Verifier、Composer 等多个 Agent 串联。表面上每个角色各司其职,实际上每个角色都拥有一部分 Prompt、模型调用、Schema 转换、重试和 Fallback 逻辑。结果是:
|
||||
|
||||
1. 一次诊断的控制权分散在多个模型角色和业务 Service 中。
|
||||
2. 同一种错误可能被不同层重试、改写或吞掉。
|
||||
3. Verifier 既检查引用,又判断语义,还可能重写报告,成为第二个诊断者。
|
||||
4. Tool 原始结果、Agent 上下文和长期审计没有明确的数据边界。
|
||||
5. Run 身份依赖 ThreadLocal 传播,跨线程后无法证明一次调用属于哪个 Run。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["用户请求"] --> P["Planner Agent"]
|
||||
P --> E["Executor Agent"]
|
||||
E --> T["Tool / raw result"]
|
||||
E --> V["Verifier Agent"]
|
||||
V --> C["Composer Agent"]
|
||||
C --> O["公开结果"]
|
||||
|
||||
P -.-> X1["独立 Prompt / retry / schema"]
|
||||
E -.-> X2["独立 Prompt / retry / fallback"]
|
||||
V -.-> X3["验真、语义判断、改写混合"]
|
||||
C -.-> X4["再次生成用户事实"]
|
||||
T -.-> X5["raw、trace、正文边界不清"]
|
||||
|
||||
H["ThreadLocal context"] -.-> P
|
||||
H -.-> E
|
||||
H -.-> V
|
||||
|
||||
classDef risk fill:#fff1f0,stroke:#cf1322,color:#5c0011;
|
||||
class X1,X2,X3,X4,X5,H risk;
|
||||
```
|
||||
|
||||
问题并不在于“Agent 数量多”本身,而在于控制职责没有单一所有者。只要预算、重试、证据和发布仍分散,多 Agent 换成 Graph 也不会自然变得可靠。
|
||||
|
||||
### 2.2 单 Agent 仍然不等于受控系统
|
||||
|
||||
把多 Agent 合并成一个 ReAct Agent,只消除了重复推理链,并没有自动解决以下问题:
|
||||
|
||||
- 模型可能无限改写相似查询;
|
||||
- SDK 可能在 Harness 之外自动重试;
|
||||
- Tool 可能返回过大、敏感或不可引用的原始数据;
|
||||
- 模型可以引用其他 Run 或不存在的 Tool Call;
|
||||
- 证据引用真实,不代表结论被证据支持;
|
||||
- 客户端断开后,迟到的模型结果仍可能覆盖终态;
|
||||
- “没有足够证据”可能被错误地发布为技术失败。
|
||||
|
||||
所以重构的核心不是“单 Agent”,而是“单 Agent + 确定性 Harness”。
|
||||
|
||||
## 3. 设计约束与不变量
|
||||
|
||||
以下不变量是组件拆分的依据,不是实现后的总结。
|
||||
|
||||
| 不变量 | 系统含义 | 由谁保证 |
|
||||
|---|---|---|
|
||||
| 一个 Run 只有一个终态 | 成功、失败、取消、超时、预算耗尽不能相互覆盖 | `RunLifecycle` 的 first-terminal-wins |
|
||||
| 所有边界显式携带 Run 身份 | 不依赖线程绑定的隐式上下文 | `RunContext` 和 framework `tool_call_id` |
|
||||
| 所有模型与 Tool 消耗可计量 | SDK 隐式 retry 不能绕过预算 | Core、Model/Tool interceptor、Harness retry |
|
||||
| Tool raw 不直接进入模型 | 内部数据、体积和控制字段不能污染上下文 | ToolBoundary、canonical store、双视图投影 |
|
||||
| 只有当前 Run 的 READY 调用可被引用 | 防止伪造、串 Run 和引用失败调用 | EvidenceGuard |
|
||||
| 引用真实性与结论支持度分开 | 确定性规则和语义判断不互相冒充 | EvidenceGuard、SemanticGuard |
|
||||
| Guard 不写报告 | 防止验证器成为第二个业务 Agent | Release Policy |
|
||||
| 停止权属于 Harness | Prompt 建议不能替代系统保证 | ProgressTracker、预算和 interceptor |
|
||||
| 没有根因是合法业务结果 | 证据不足不应伪装成内部故障 | `conclusion=null`、ProgressSnapshot、SafeFallback |
|
||||
| 普通审计不保存敏感正文 | 可观测性不能以泄露 Prompt、raw、reasoning 为代价 | metadata-only audit、reasoning 独立存储 |
|
||||
|
||||
## 4. 当前总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Entry["应用入口与 Run 所有权"]
|
||||
APP["ChatApplicationUseCase"]
|
||||
ROUTER["IntentRouter"]
|
||||
EXEC["DiagnosisChatExecutor"]
|
||||
STORE["ChatRunStore"]
|
||||
end
|
||||
|
||||
subgraph Harness["确定性 Harness 边界"]
|
||||
CORE["Core<br/>context / lifecycle / budget / cancel"]
|
||||
MI["Model Interceptor<br/>调用预算与 Token"]
|
||||
TI["Tool Interceptor<br/>协议、重复、停止"]
|
||||
TB["ToolBoundary<br/>授权、执行、canonical"]
|
||||
PROGRESS["ProgressTracker<br/>信息增益与饱和"]
|
||||
EG["EvidenceGuard<br/>引用真实性"]
|
||||
SG["SemanticGuard<br/>结论支持度"]
|
||||
RELEASE["Release Policy<br/>SUCCESS / FALLBACK"]
|
||||
AUDIT["Audit / Trace<br/>metadata-only"]
|
||||
end
|
||||
|
||||
subgraph Nondeterministic["非确定性区域"]
|
||||
AGENT["Diagnosis ReAct Agent"]
|
||||
MODEL["Chat Model"]
|
||||
end
|
||||
|
||||
subgraph Evidence["证据执行与存储"]
|
||||
ADAPTER["RAG / Logs / MySQL Adapter"]
|
||||
BACKEND["Evidence Backend"]
|
||||
CANON["Redis Canonical Invocation"]
|
||||
META["MySQL Durable Metadata"]
|
||||
end
|
||||
|
||||
APP --> CORE
|
||||
APP --> ROUTER
|
||||
ROUTER --> MODEL
|
||||
APP --> EXEC
|
||||
EXEC --> AGENT
|
||||
AGENT --> MI --> MODEL
|
||||
AGENT --> TI
|
||||
TI --> PROGRESS
|
||||
TI --> TB --> ADAPTER --> BACKEND
|
||||
TB --> CANON
|
||||
TB --> META
|
||||
TB --> TI --> AGENT
|
||||
EXEC --> EG --> SG --> RELEASE
|
||||
PROGRESS --> RELEASE
|
||||
RELEASE --> APP --> STORE
|
||||
CORE -.-> MI
|
||||
CORE -.-> TI
|
||||
CORE -.-> TB
|
||||
CORE -.-> EG
|
||||
CORE -.-> SG
|
||||
APP -.-> AUDIT
|
||||
MI -.-> AUDIT
|
||||
TI -.-> AUDIT
|
||||
TB -.-> AUDIT
|
||||
EG -.-> AUDIT
|
||||
SG -.-> AUDIT
|
||||
RELEASE -.-> AUDIT
|
||||
```
|
||||
|
||||
图中的关键边界有三条:
|
||||
|
||||
1. **Agent 之前**:Application 创建 Run,Core 固定身份、预算和终止语义。
|
||||
2. **Tool 前后**:Interceptor 管控制协议,ToolBoundary 管执行与真相,Projector 管模型可见内容。
|
||||
3. **发布之前**:EvidenceGuard 验引用,SemanticGuard 验推论,Release 决定唯一公开内容。
|
||||
|
||||
## 5. 关键决策及设计推导
|
||||
|
||||
### 5.1 决策一:只保留一个拥有 Tool loop 的 Diagnosis Agent
|
||||
|
||||
**问题**:多角色 Agent 把同一份业务上下文在多个模型之间传递,每一跳都可能增加信息损失、重试和幻觉面。
|
||||
|
||||
**候选方案**:
|
||||
|
||||
| 方案 | 优点 | 主要问题 |
|
||||
|---|---|---|
|
||||
| 保留 Planner / Executor / Verifier / Composer | 角色概念直观 | 控制权分散;多套 Prompt 和重试;Verifier/Composer 会产生新事实 |
|
||||
| 外层自建 ReAct / StateGraph | 流程显式 | 与框架 ReAct 重复;状态和异常路径翻倍 |
|
||||
| 单 ReAct Agent + Harness | 推理上下文连续;控制边界集中 | 对 Harness 的契约和门禁设计要求更高 |
|
||||
|
||||
**决策**:Diagnosis Agent 是唯一业务推理者和 Draft 作者,使用框架 `ReactAgent` 自己完成 Thought / Action / Observation。项目不在外层复制循环。
|
||||
|
||||
**代价**:单 Agent 并不能通过“角色互审”获得表面冗余,因此必须把可机械验证的安全要求下沉到 Guard,把语义审查收缩成隔离的单轮判断。
|
||||
|
||||
### 5.2 决策二:RunContext 显式传播,结构不可变、状态句柄线程安全
|
||||
|
||||
**问题**:旧 ThreadLocal 可以在同步调用中工作,但异步 Tool、线程池和取消回调跨线程后,调用方无法证明读到的是当前 Run 的上下文。
|
||||
|
||||
**容易走错的方案**:把所有字段都做成不可变值对象。这样 deadline 和 identity 很干净,但预算、取消和终态不得不在外部另建全局 Map,反而形成第二真相源。
|
||||
|
||||
**决策**:`RunContext` 本身是 record,固定 `sessionId / runId / deadline` 和各状态句柄引用;预算、取消、生命周期、模型账本和进展 Tracker 在各自线程安全对象内变化。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RC["RunContext<br/>结构不可变"] --> ID["sessionId / runId / deadline"]
|
||||
RC --> C["RunCancellation<br/>first-reason-wins"]
|
||||
RC --> B["RunBudget<br/>同步复合计数"]
|
||||
RC --> L["RunLifecycle<br/>first-terminal-wins"]
|
||||
RC --> M["ModelCallLedger<br/>组件/轮次账本"]
|
||||
RC --> P["ProgressTracker<br/>Run 内进展"]
|
||||
```
|
||||
|
||||
**为什么不是全局 Run Registry**:Harness 只控制当前调用,不承担 Run 查询和持久化;持久化真相仍由 `diagnosis_run` 所有,避免 Core 演变成工作流引擎。
|
||||
|
||||
### 5.3 决策三:关闭 SDK 隐式 retry,由 Harness 按失败类型拥有 retry
|
||||
|
||||
**问题**:Spring AI 默认 `maxAttempts=10`。如果 SDK 在模型边界内部自动重试,Harness 看到的一次调用可能对应多个 Provider attempt,预算、延迟、Trace 和取消都失真。
|
||||
|
||||
**决策**:底层 SDK retry 设为 1;仅在 Harness 中使用类型化策略:
|
||||
|
||||
- IntentRouter:超时、传输、非法输出最多 2 次 attempt;
|
||||
- SemanticGuard:超时、传输、解析或 Schema 问题最多 2 次 attempt;
|
||||
- Diagnosis Agent、业务 Tool、EvidenceRepair:1 次,不自动重试;
|
||||
- 取消、预算耗尽、`NO_EVIDENCE`、业务拒绝:从不重试。
|
||||
|
||||
**设计理由**:重试不是通用容错开关。只有调用者知道一次失败是否幂等、是否还在 deadline 内、是否应该再次消耗预算。
|
||||
|
||||
**代价**:Provider 瞬时抖动更容易直接暴露,但真实 attempt 终于可计算、可审计,且不会把业务无证据误判为技术重试条件。
|
||||
|
||||
### 5.4 决策四:Tool 使用 canonical truth 与 Agent projection 双视图
|
||||
|
||||
**问题**:同一份 Tool 返回同时服务三个目标,而三个目标互相冲突:
|
||||
|
||||
- 证据验真需要完整、稳定、按 Run 归属的记录;
|
||||
- Agent 只需要完成下一步推理的最小内容;
|
||||
- 长期审计应保留身份和耗时,但不能长期保存敏感 raw。
|
||||
|
||||
**决策**:把 Tool 数据分成三层,而不是让一个 JSON 到处流转。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
REQ["framework tool_call_id<br/>typed request"] --> B["ToolBoundary"]
|
||||
B --> RAW["backend raw response"]
|
||||
RAW --> CAN["Redis canonical invocation<br/>request + raw + agent_result<br/>短 TTL"]
|
||||
RAW --> PRJ["Tool-specific projector"]
|
||||
PRJ --> CTRL["Harness control view"]
|
||||
PRJ --> OBS["Agent observation<br/>白名单、有界"]
|
||||
B --> META["Durable audit<br/>identity / status / latency / bytes"]
|
||||
CTRL --> STOP["重复、NO_GAIN、停止控制"]
|
||||
OBS --> AGENT["Diagnosis Agent"]
|
||||
CAN --> GUARD["EvidenceGuard"]
|
||||
```
|
||||
|
||||
**关键取舍**:
|
||||
|
||||
- 使用框架 `tool_call_id`,Harness 不生成第二套 ID;
|
||||
- `PROJECTING / READY / ERROR` 表达调用生命周期;
|
||||
- `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` 表达客观结果语义;
|
||||
- raw 超限直接失败,不静默截断;Agent projection 可以有界截断,但必须标记 `truncated`;
|
||||
- Redis 是 TTL 内完整 Tool 真相源,MySQL 只做长期 metadata audit;
|
||||
- Redis 读取不续期,避免一次历史读取无限延长敏感 raw 生命周期。
|
||||
|
||||
**代价**:同一次 Tool 调用需要 projector、store 和 audit 三套表达。但它们不是重复数据模型,而是分别回答“真实发生了什么”“模型允许看到什么”“长期允许保留什么”。
|
||||
|
||||
### 5.5 决策五:EvidenceGuard、SemanticGuard 和 Release 三段分工
|
||||
|
||||
**问题**:“引用是真实的”和“结论被引用支持”是两个不同命题。只做前者会放过牵强推论;都交给模型则无法确定性防止伪造 ID、跨 Run 引用或修改报告。
|
||||
|
||||
**决策**:
|
||||
|
||||
1. `EvidenceGuard` 使用纯代码检查 Draft 结构、analysis ID、Tool Call 当前 Run 所有权、READY 状态、evidence status 和引用闭包。
|
||||
2. 首次引用失败时,`EvidenceRepair` 只允许修复结构和引用;修复前后用户可见语义必须一致,然后重新执行 EvidenceGuard。
|
||||
3. `SemanticGuard` 只接收 query、完整 Draft 和 verified snapshot,进行无 Tool、无记忆的单轮 `SUPPORTED / UNSUPPORTED` 判断。
|
||||
4. `DiagnosisReleaseUseCase` 是唯一发布决策点。Guard 无权改写最终报告。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Diagnosis Agent
|
||||
participant R as Release UseCase
|
||||
participant E as EvidenceGuard
|
||||
participant C as Canonical Store
|
||||
participant X as EvidenceRepair
|
||||
participant S as SemanticGuard
|
||||
participant P as Public Result
|
||||
|
||||
A->>R: DiagnosisDraft
|
||||
R->>E: validate draft references
|
||||
E->>C: read exact run/tool_call_id
|
||||
C-->>E: READY agent_result
|
||||
alt 引用或结构错误
|
||||
E-->>R: violations
|
||||
R->>X: 原 Draft + violations
|
||||
X-->>R: 语义不变的修复 Draft
|
||||
R->>E: revalidate once
|
||||
end
|
||||
alt 证据闭包有效
|
||||
E-->>R: verified snapshot
|
||||
R->>S: query + draft + snapshot
|
||||
S-->>R: SUPPORTED / UNSUPPORTED
|
||||
end
|
||||
alt SUPPORTED
|
||||
R-->>P: 原始安全 Draft
|
||||
else 任一门禁失败
|
||||
R-->>P: deterministic SafeFallback
|
||||
end
|
||||
```
|
||||
|
||||
**为什么 Repair 不能修正文义**:一旦 Repair 可以修改结论,它就成为新的报告作者;此时最终内容不再是 Diagnosis Agent 的 Draft,也无法证明修复只解决了引用问题。
|
||||
|
||||
**为什么 SemanticGuard 不是第二个 Agent**:它没有 Tool、历史记忆或 ReAct loop,只回答一个受约束的二值审查问题,不能探索新事实或生成新结论。
|
||||
|
||||
### 5.6 决策六:资源预算和信息增益是两套停止机制
|
||||
|
||||
**问题**:预算只能回答“还能不能花资源”,不能回答“继续查询还有没有价值”。知识库只有通用资料、日志持续为空时,Agent 可以在预算范围内不断换关键词,最终以 `BUDGET_EXHAUSTED` 结束。用户得到的是技术失败,而系统实际已经知道“当前范围证据不足”。
|
||||
|
||||
**决策**:
|
||||
|
||||
- Tool 负责客观事实:是否执行成功、是否为空、实际 scope;
|
||||
- Harness 机械判定空结果和完全重复的 `tool_name + normalized_scope` 为 `NO_GAIN`;
|
||||
- 其他成功非空结果由模型在下一次 Tool Call Envelope 中声明 `GAINED / NO_GAIN`;
|
||||
- `DiagnosisProgressTracker` 维护连续 `NO_GAIN`,达到阈值后进入 `SATURATED`;
|
||||
- 连续 Envelope 协议错误使用独立计数和 `PROGRESS_PROTOCOL_VIOLATED`,不伪装成无信息增益;
|
||||
- STOP_REQUIRED 只交付一次,再次请求 Tool 直接受控终止;
|
||||
- `INFORMATION_SATURATED` 与 `BUDGET_LIMIT_REACHED` 始终分开记录。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> COLLECTING
|
||||
COLLECTING --> COLLECTING: GAINED / 清零 NO_GAIN
|
||||
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
|
||||
COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值
|
||||
COLLECTING --> SATURATED: 连续协议错误达阈值
|
||||
COLLECTING --> STOPPED: 硬预算到达
|
||||
SATURATED --> DRAFT_CHANCE: 单次 STOP_REQUIRED
|
||||
DRAFT_CHANCE --> STOPPED: 再次请求 Tool
|
||||
DRAFT_CHANCE --> RELEASE: 输出合法 Draft
|
||||
STOPPED --> RELEASE: ProgressSnapshot
|
||||
```
|
||||
|
||||
**为什么不用 `new_count`**:跨 RAG、日志和数据库建立统一内容指纹成本高,而且“新记录”不等于“对假设有价值”。
|
||||
|
||||
**为什么不用独立 Progress Judge**:它会增加模型成本和新的失败点,还会把简单的空结果、重复 scope 判断模型化。
|
||||
|
||||
**首版边界**:重复检测只比较规范化参数,不承诺识别自然语言语义等价查询。
|
||||
|
||||
### 5.7 决策七:`conclusion=null` 是合法完成,不是模型失败
|
||||
|
||||
**问题**:如果成功的唯一含义是“必须给出根因”,Agent 在缺少企业、时间范围或错误信息时只能继续盲查,或者编造结论。
|
||||
|
||||
**决策**:DiagnosisDraft 允许 `conclusion=null`:
|
||||
|
||||
- 缺少开始查询所需信息:`MISSING_REQUIRED_CONTEXT`;
|
||||
- 已完成有限检查但证据不足:`INSUFFICIENT_EVIDENCE`;
|
||||
- 有结论:才进入完整 EvidenceGuard、Repair、SemanticGuard 链。
|
||||
|
||||
最终公开生命周期仍只有 `SUCCESS / FALLBACK / FAILED / CANCELLED`。证据不足是 `FALLBACK` 的原因,不再引入一套与 Run 终态平行的诊断状态机。
|
||||
|
||||
如果模型输出非法 Draft,系统会丢弃非法正文;只有当前 Run 已存在可验真的 ProgressSnapshot,才允许降级成过程型 Fallback,否则保持 fail closed。
|
||||
|
||||
### 5.8 决策八:可观测性记录决策证据,不复制敏感上下文
|
||||
|
||||
**问题**:为了调试 Agent,最直接的做法是保存 Prompt、模型正文、Tool arguments 和 raw response。但这会让普通 Trace 变成敏感数据仓库,也会造成多份事实副本。
|
||||
|
||||
**决策**:
|
||||
|
||||
- `diagnosis_trace_event` 只保存追加式、按 exact Run 排序的事件和有界 metadata;
|
||||
- `ToolInvocation` 长期保存 Tool 身份、状态、耗时和字节数,不保存完整请求/响应;
|
||||
- `AgentStep` 保存步骤元数据和 Token,不保存 Prompt、Tool payload;
|
||||
- Provider reasoning 与 assistant text 放入独立受限审计表和接口;
|
||||
- Trace 写入失败不得改变业务结果;
|
||||
- Model usage 按组件和轮次入账,Run 结束做 Token 对账;
|
||||
- Tool 请求被 Harness 拒绝时记录 `TOOL_REQUEST_REJECTED`,不能伪装成一次真实 `TOOL_INVOCATION`。
|
||||
|
||||
**尚未完成的治理**:Reasoning 的访问控制、保留期限和加密仍由 ISS-015 跟踪。已有隔离不代表完整合规闭环。
|
||||
|
||||
## 6. 一次诊断的完整控制流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Client
|
||||
participant App as Chat Application
|
||||
participant Core as Harness Core
|
||||
participant Agent as Diagnosis Agent
|
||||
participant TI as Tool Interceptor
|
||||
participant TB as ToolBoundary
|
||||
participant Store as Canonical Store
|
||||
participant Guard as Guards
|
||||
participant Release as Release
|
||||
|
||||
U->>App: query + optional sessionId
|
||||
App->>Core: startRun(sessionId)
|
||||
Core-->>App: RunContext(runId, deadline, handles)
|
||||
App->>Agent: query + bounded safe previousTurn
|
||||
|
||||
loop ReAct 由框架拥有
|
||||
Agent->>TI: Tool Call Envelope
|
||||
TI->>TI: 应用上一轮信息增益、检查重复/饱和/协议
|
||||
alt 门禁允许
|
||||
TI->>TB: framework id + business input + RunContext
|
||||
TB->>TB: active / auth / readonly / budget / size
|
||||
TB->>Store: PROJECTING -> READY or ERROR
|
||||
TB-->>TI: bounded canonical agent_result
|
||||
TI-->>Agent: whitelist observation
|
||||
else 信息饱和或协议停止
|
||||
TI-->>Agent: one-shot STOP_REQUIRED
|
||||
end
|
||||
end
|
||||
|
||||
Agent-->>App: DiagnosisDraft 或受控停止
|
||||
App->>Release: Draft + ProgressSnapshot + stop reason
|
||||
alt 有结论
|
||||
Release->>Guard: evidence truth + semantic support
|
||||
Guard-->>Release: verified / unsupported
|
||||
else 无结论或受控停止
|
||||
Release->>Release: 构造确定性 Fallback
|
||||
end
|
||||
Release-->>App: original safe Draft or SafeFallback
|
||||
App->>Core: first terminal wins
|
||||
App-->>U: content/failure + done
|
||||
```
|
||||
|
||||
## 7. 真实问题如何反向修正设计
|
||||
|
||||
这些不是零散 Bug 清单。每个问题都暴露了一个原设计假设不成立,并促成了边界调整。
|
||||
|
||||
| 现场问题 | 被证伪的假设 | 设计修正 | 固化位置 |
|
||||
|---|---|---|---|
|
||||
| Spring AI 默认 10 次 retry | 一次 Harness 模型调用等于一次 Provider attempt | SDK retry=1,retry 所有权上移 | Core / Retry / 配置测试 |
|
||||
| ThreadLocal 跨异步边界不稳定 | 同线程上下文足以表示 Run 所有权 | 显式 `RunContext` 贯穿调用链 | Core / framework metadata |
|
||||
| Tool raw 直接进入 Agent | Tool 返回可同时服务推理、验真和审计 | canonical / control / observation 三层拆分 | ToolBoundary / Projector / Store |
|
||||
| 0 条日志被表达为 `success=false` | 空结果等于技术失败 | 技术执行与 `NO_EVIDENCE` 分离 | Tool contract / Projector |
|
||||
| `REFERENCE` 被当作诊断证据 | 非空候选等于支持结论 | 保留相关度,语义增益交给模型 | RAG projector / Progress |
|
||||
| Redis canonical 全部 `STORE_ERROR` | 单测 ObjectMapper 与生产配置行为一致 | 使用生产 ObjectMapper 能力并增加 live E2E | Store wiring / E2E |
|
||||
| raw/rewritten query 出现在日志 | 可观测性可以直接打印检索输入 | 普通日志和 Trace 仅保存安全 metadata | Audit boundary |
|
||||
| 空查反复改写直到预算耗尽 | 硬预算可以承担正常收敛 | 引入信息增益和饱和停止 | ProgressTracker / Interceptor |
|
||||
| 9 次协议拒绝仍消耗 13 轮模型 | 协议错误会被模型自然修正 | 独立协议错误阈值和一次 STOP_REQUIRED | Progress protocol |
|
||||
| 非法 Draft 导致已完成检查全部丢失 | Draft 失败意味着整个 Run 没有安全价值 | 仅在已有验真进展时发布过程型 Fallback | ProgressSnapshot / Release |
|
||||
|
||||
## 8. 关键决策总表
|
||||
|
||||
| 决策 | 选择 | 放弃的方案 | 获得的能力 | 付出的代价 |
|
||||
|---|---|---|---|---|
|
||||
| 推理拓扑 | 单 Diagnosis ReAct Agent | 多 Agent Graph、外层 ReAct | 上下文连续、唯一 Draft 作者 | Harness 门禁必须完整 |
|
||||
| Run 上下文 | 显式 record + 状态句柄 | ThreadLocal、全局 Registry | 异步可证明、状态所有权清楚 | 参数需要显式传递 |
|
||||
| Tool ID | framework `tool_call_id` | Harness 二次生成 ID | 引用链唯一 | 依赖框架 ID 契约 |
|
||||
| Tool 真相 | Redis canonical,短 TTL | JPA 保存完整 raw | 可验真且限制敏感数据寿命 | Redis 可用性成为验证依赖 |
|
||||
| Agent 输入 | 白名单 projection | raw / 完整 canonical 直传 | 上下文有界、减少泄露 | 需为每类 Tool 维护 projector |
|
||||
| 长期审计 | metadata-only | 永久保存请求和响应 | 降低泄露与重复真相 | 深度回放受 TTL 限制 |
|
||||
| 证据安全 | 确定性 EvidenceGuard + 隔离 SemanticGuard | 单一 Verifier Agent | 分清真实性和支持度 | 两段门禁增加延迟 |
|
||||
| 修复 | 只修引用且语义必须不变 | Guard 重写报告 | 保持唯一作者 | 部分报告只能 Fallback |
|
||||
| 停止 | 预算 + 信息增益双机制 | 只靠 Prompt 或硬上限 | 正常无证据收敛 | 首版只能做参数级重复判断 |
|
||||
| 无结论 | 合法 Draft + SafeFallback | 强制根因 | 避免盲查和编造 | 调用方需理解 FALLBACK 是业务结果 |
|
||||
| 发布 | 唯一 Release Policy | 各层自行 fallback | 对外语义一致 | Release 成为关键集中组件 |
|
||||
|
||||
## 9. Harness 明确不做什么
|
||||
|
||||
边界是否清晰,既看它做什么,也看它拒绝做什么:
|
||||
|
||||
- 不判断业务根因;
|
||||
- 不实现 Planner / Executor / Verifier / Composer 角色图;
|
||||
- 不在框架外复制 ReAct while-loop;
|
||||
- 不让 Tool 声称结果是否支持诊断结论;
|
||||
- 不把检索分数直接提升为结论可信度;
|
||||
- 不依赖 Prompt 作为唯一预算或停止机制;
|
||||
- 不自动重试 Diagnosis Agent 和业务 Tool;
|
||||
- 不允许 Guard 生成新事实或改写结论;
|
||||
- 不把 Redis canonical 变成永久审计库;
|
||||
- 不承诺同步 Provider 调用一定能被立即物理中断;
|
||||
- 不在首版做自然语言语义去重或跨 Tool 内容指纹。
|
||||
|
||||
这些非目标是在防止 Harness 再次长成一套不可维护的业务编排系统。
|
||||
|
||||
## 10. 如何验证设计成立
|
||||
|
||||
验证重点不是某个类是否被调用,而是上述不变量能否在失败和竞态下保持:
|
||||
|
||||
| 验证层 | 需要证明的事实 | 代表性测试/证据 |
|
||||
|---|---|---|
|
||||
| Core 单测 | deadline、取消、预算、first-terminal-wins、异步显式 context | `RunContextTest`、`RunBudgetTest`、`DiagnosisHarnessCoreTest` |
|
||||
| Tool 单测 | cross-run、重复 ID、只读、大小、状态迁移、投影 | `ToolBoundaryTest`、`CanonicalInvocationStoreTest`、各 Projector test |
|
||||
| Agent loop | framework ID、Envelope、STOP_REQUIRED、非法 Draft | `DiagnosisAgentUseCaseTest`、`HarnessToolInterceptorTest` |
|
||||
| Guard / Release | 引用闭包、Repair 语义不变、Unsupported Fallback | `EvidenceGuardTest`、`SemanticGuardTest`、`DiagnosisReleaseUseCaseTest` |
|
||||
| Audit | metadata 边界、Token 对账、拒绝与执行分离 | audit 包 focused tests、exact-run Trace |
|
||||
| Live E2E | 生产序列化、Redis、数据库、模型和 SSE 的组合契约 | `devflow/projects/2026-07-22-single-react-cleanup-e2e/`、ISS-016 evidence |
|
||||
|
||||
单元测试能证明局部状态机,不能证明生产 `ObjectMapper`、Redis serializer、Provider Tool Calling 和 SSE 串联正确。因此 canonical store 的 Java Time 问题、零日志语义和协议空转都必须依靠 live E2E 反证,而不能只看 mock tests。
|
||||
|
||||
## 11. 当前边界与后续治理
|
||||
|
||||
当前设计已经建立可运行的确定性边界,但仍有明确限制:
|
||||
|
||||
1. 重复检测是 `tool_name + normalized_scope` 的参数等价,不识别语义近似查询。
|
||||
2. Redis canonical 受 TTL 约束;TTL 过后只能依赖 metadata audit,不能重建完整证据正文。
|
||||
3. 同步模型请求的取消主要阻止后续边界和迟到发布,不承诺 Provider 已执行计算立即停止。
|
||||
4. SemanticGuard 仍是模型判断,只是被限制在无 Tool、无记忆、二值输出的最小范围内。
|
||||
5. Reasoning 已与普通 Trace 隔离,但访问控制、保留期限和加密仍需完成。
|
||||
6. 信息增益阈值默认为 2,仍需通过固定评测集持续校准,不能通过线上直觉随意调大。
|
||||
|
||||
## 12. 可复用的设计原则
|
||||
|
||||
这套 Harness 最值得复用的不是某个 Java 类,而是以下判断顺序:
|
||||
|
||||
1. 先列出系统必须保证的确定性不变量,再决定组件。
|
||||
2. 将推理权留给模型,将授权、预算、归属、验真和发布权留给代码。
|
||||
3. 不让一个数据表示同时承担内部真相、模型上下文和长期审计。
|
||||
4. 不用资源耗尽代替业务收敛,也不用 Prompt 代替硬门禁。
|
||||
5. 不让验证器成为第二个作者;验证失败时降级,而不是偷偷改写。
|
||||
6. 把“没有足够证据”设计成一等业务结果,系统才不需要用幻觉换取成功率。
|
||||
|
||||
## 13. 代码与文档入口
|
||||
|
||||
- 完整组件说明:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md)
|
||||
- 当前质量门禁:[harness-quality-gates.md](../../architecture/harness-quality-gates.md)
|
||||
- Diagnosis Agent 架构:[agent-orchestration.md](../../architecture/agent-orchestration.md)
|
||||
- 信息增益停止:[diagnosis-information-gain-stop-architecture.md](../../architecture/diagnosis-information-gain-stop-architecture.md)
|
||||
- 核心重构 Issue:[ISS-014-single-react-agent-harness-aci-ptk-refactor.md](../../issues/archived/ISS-014-single-react-agent-harness-aci-ptk-refactor.md)
|
||||
- 信息增益 Issue:[ISS-016-diagnosis-information-gain-stop-contract.md](../../issues/active/ISS-016-diagnosis-information-gain-stop-contract.md)
|
||||
@@ -0,0 +1,276 @@
|
||||
# Harness 证据安全链:从引用真实到结论可发布
|
||||
|
||||
**更新日期**:2026-07-29
|
||||
**主题**:EvidenceGuard、EvidenceRepair、SemanticGuard 与 Diagnosis Release
|
||||
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
|
||||
**前置阅读**:[Harness-Tool双视图-从原始结果到可验证证据.md](Harness-Tool双视图-从原始结果到可验证证据.md)
|
||||
|
||||
## 1. 证据存在,不代表结论成立
|
||||
|
||||
诊断报告至少可能出现三类不同错误:
|
||||
|
||||
1. **伪造引用**:Draft 引用了不存在、失败或属于其他 Run 的 Tool Call。
|
||||
2. **引用断裂**:analysis 有证据,但 conclusion、action plan 或 recommendation 没有绑定对应 analysis。
|
||||
3. **牵强推论**:引用和结构都真实,但证据并不足以支持所写根因。
|
||||
|
||||
例如,系统确实查到了一条支付超时日志,但报告把根因写成“数据库连接池耗尽”。此时 Tool Call 真实、日志真实、引用格式也正确,结论仍然不成立。
|
||||
|
||||
因此“报告是否安全”不能由一个笼统的 Verifier 回答。它至少包含两个不同命题:
|
||||
|
||||
```text
|
||||
P1:引用的证据是否真实、属于当前 Run,并形成结构闭包?
|
||||
P2:这些真实证据是否足以支持报告中的结论?
|
||||
```
|
||||
|
||||
P1 可以由代码确定性证明;P2 是语义判断。把两者都交给模型,会让本可机械验证的身份和状态也变成概率结果;全部交给规则,则规则代码会逐渐变成另一套业务诊断引擎。
|
||||
|
||||
## 2. 设计目标:验证链不能产生新事实
|
||||
|
||||
证据安全链遵守四个约束:
|
||||
|
||||
- Diagnosis Agent 是唯一报告作者;
|
||||
- EvidenceGuard 只做确定性验真;
|
||||
- SemanticGuard 只做受限语义审查;
|
||||
- Release 是唯一公开决策点。
|
||||
|
||||
任何 Guard 都不能调用业务 Tool、探索新证据或重写最终报告。否则验证链会变成第二条隐式 Agent 链,重新引入多 Agent 架构的问题。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D["DiagnosisDraft<br/>唯一业务 Draft"] --> R["DiagnosisReleaseUseCase<br/>发布编排所有者"]
|
||||
R --> E["EvidenceGuard<br/>确定性引用验真"]
|
||||
E -->|"invalid"| X["EvidenceRepair<br/>只修结构/引用"]
|
||||
X --> E2["EvidenceGuard Recheck"]
|
||||
E -->|"valid"| V["VerifiedEvidenceSnapshot"]
|
||||
E2 -->|"valid"| V
|
||||
V --> S["SemanticGuard<br/>SUPPORTED / UNSUPPORTED"]
|
||||
S -->|"SUPPORTED"| OK["发布原始或等义修复 Draft"]
|
||||
E -->|"repair 失败"| F["SafeFallback"]
|
||||
E2 -->|"invalid"| F
|
||||
S -->|"UNSUPPORTED / unavailable"| F
|
||||
```
|
||||
|
||||
## 3. 第一层:EvidenceGuard 证明引用真实性
|
||||
|
||||
`EvidenceGuard` 不调用模型。它从当前 `RunContext` 出发,对 Draft 进行两阶段检查。
|
||||
|
||||
### 3.1 Draft 结构与引用闭包
|
||||
|
||||
首先检查报告自身结构:
|
||||
|
||||
- Draft、analysis、analysis ID、kind 和正文是否存在;
|
||||
- analysis ID 是否重复;
|
||||
- 每条有结论分析是否绑定 Tool Call;
|
||||
- conclusion、action plan 和 recommendation 是否绑定已存在的 analysis ID;
|
||||
- limitations 是否存在并声明报告范围。
|
||||
|
||||
引用链的目标不是让 JSON 看起来完整,而是形成下面的闭包:
|
||||
|
||||
```text
|
||||
Conclusion / Action / Recommendation
|
||||
|
|
||||
v
|
||||
Analysis Item
|
||||
|
|
||||
v
|
||||
framework tool_call_id
|
||||
|
|
||||
v
|
||||
Canonical READY Invocation
|
||||
```
|
||||
|
||||
只要其中一跳缺失,公开报告就不能证明其来源。
|
||||
|
||||
### 3.2 Canonical Invocation 验真
|
||||
|
||||
对每个 `tool_call_id`,Guard 使用当前 `runId` 重新生成 canonical key,然后检查:
|
||||
|
||||
1. ID 格式是否合法;
|
||||
2. canonical record 是否存在;
|
||||
3. record 内 ID 是否与引用一致;
|
||||
4. record 是否属于当前 Run;
|
||||
5. invocation 是否 READY;
|
||||
6. analysis kind 是否接受该 evidence status;
|
||||
7. Tool 类型是否受支持;
|
||||
8. `agent_result` 是否能严格解析为对应 typed result;
|
||||
9. typed result 中的 ID、状态、count 和集合是否自洽。
|
||||
|
||||
因此,即使模型猜中了另一个 Run 的 Tool Call ID,也无法通过当前 Run key 和 ownership 检查。
|
||||
|
||||
### 3.3 正向证据和负向观察不能混用
|
||||
|
||||
`AnalysisKind` 与 `EvidenceStatus` 的匹配是关键约束。
|
||||
|
||||
一条 READY + NO_EVIDENCE 日志结果,只能支持如下陈述:
|
||||
|
||||
> 在企业 A、时间窗 T、服务 S、查询条件 Q 下没有匹配事件。
|
||||
|
||||
它不能支持:
|
||||
|
||||
> 系统没有发生故障。
|
||||
|
||||
EvidenceGuard 会把空结果投影为带 source 和 scope 的负向观察,而不是把它提升为支持任意根因的正向证据。
|
||||
|
||||
## 4. VerifiedEvidenceSnapshot 为什么是必要中间产物
|
||||
|
||||
EvidenceGuard 验证通过后不会把 canonical raw 直接交给 SemanticGuard,而是生成 `VerifiedEvidenceSnapshot`。它保存:
|
||||
|
||||
- analysis ID、正文和 kind;
|
||||
- 已验证证据的 source type、source、scope、timestamp、excerpt;
|
||||
- Tool-specific 的有限结构化 values;
|
||||
- 去重后的 verified sources。
|
||||
|
||||
Snapshot 的作用是形成一条新的最小信任边界:SemanticGuard 不需要访问 Redis,也不能看到 request、raw response 或内部错误。它只判断“这组已经验真的事实是否支持 Draft”。
|
||||
|
||||
这也避免了语义审查阶段重新解释 backend 私有格式。
|
||||
|
||||
## 5. 第二层:EvidenceRepair 只允许修引用
|
||||
|
||||
### 5.1 为什么需要 Repair
|
||||
|
||||
模型可能生成业务语义正确、但引用结构存在局部问题的 Draft,例如:
|
||||
|
||||
- conclusion 漏写 `based_on_analysis_ids`;
|
||||
- analysis 引用了错误的 Tool Call ID;
|
||||
- 输出字段结构不符合严格 Schema。
|
||||
|
||||
如果所有结构错误都直接 Fallback,会丢掉部分可修复结果。但让 Repair 自由重写又会产生更严重的问题:最终结论不再来自 Diagnosis Agent,Repair 事实上成为第二个报告作者。
|
||||
|
||||
### 5.2 Repair 的不变量
|
||||
|
||||
`EvidenceRepair` 只获得:
|
||||
|
||||
- 原始 query;
|
||||
- 原始 Draft;
|
||||
- EvidenceGuard 给出的 violations。
|
||||
|
||||
它没有 Tool,也不能获取新证据。修复输出必须再次严格解析,并满足:
|
||||
|
||||
```text
|
||||
SemanticDraftView(original)
|
||||
==
|
||||
SemanticDraftView(repaired)
|
||||
```
|
||||
|
||||
即用户可见的 analysis、conclusion、action plan、recommendations 和 limitations 语义不变,只允许修复引用和结构。之后必须重新执行 EvidenceGuard,不能因为“已经 Repair”就跳过验真。
|
||||
|
||||
当前策略只给 EvidenceRepair 一次 attempt。Repair 失败、改变语义或复检仍不通过,直接进入 `EVIDENCE_VALIDATION_FAILED` Fallback。
|
||||
|
||||
## 6. 第三层:SemanticGuard 判断结论支持度
|
||||
|
||||
EvidenceGuard 证明的是“证据是真的”,SemanticGuard 判断的是“推论是否成立”。为了不让它变成第二个 Agent,系统主动削减了它的能力:
|
||||
|
||||
| 约束 | 目的 |
|
||||
|---|---|
|
||||
| 无 Tool | 不能自行寻找新事实 |
|
||||
| 无会话记忆 | 不能引入当前输入之外的信息 |
|
||||
| 单轮调用 | 不形成新的 ReAct loop |
|
||||
| 输入固定 | 只接收 query、Draft 和 VerifiedEvidenceSnapshot |
|
||||
| 输出严格 | 只能返回 `verdict + reason` |
|
||||
| 二值 verdict | 只允许 `SUPPORTED / UNSUPPORTED` |
|
||||
| 无发布权限 | 不能改写或直接返回用户报告 |
|
||||
|
||||
技术超时、传输失败、解析失败和 Schema 非法可以按完全相同输入进行一次显式重试。仍失败时不假设“可能支持”,而是 fail closed,发布 `SEMANTIC_UNAVAILABLE` Fallback。
|
||||
|
||||
这里需要准确描述“确定性边界”:SemanticGuard 的语义判断仍然是概率性的;确定的是它的权限、输入、输出、成本、重试次数和失败后的发布行为。
|
||||
|
||||
## 7. Release 为什么必须是唯一发布入口
|
||||
|
||||
如果 Application、EvidenceGuard 和 SemanticGuard 都能构造公开结果,同一种失败会出现多个含义,甚至可能绕过前置校验。`DiagnosisReleaseUseCase` 集中处理以下分支:
|
||||
|
||||
| 输入情况 | 验证路径 | 发布结果 |
|
||||
|---|---|---|
|
||||
| Draft 有 conclusion | EvidenceGuard -> 可选 Repair/Recheck -> SemanticGuard | 原 Draft/等义修复 Draft,或安全 Fallback |
|
||||
| Draft 无 conclusion,有已验真进展 | 只验证其中已有 Tool 引用 | `INSUFFICIENT_EVIDENCE` |
|
||||
| Draft 无 conclusion,无进展但声明缺失信息 | 检查引用边界 | `MISSING_REQUIRED_CONTEXT` |
|
||||
| Harness 受控停止,有已验真进展 | ProgressSnapshot | `INSUFFICIENT_EVIDENCE` |
|
||||
| Draft 非法,但有已验真进展 | 丢弃非法 Draft,使用 ProgressSnapshot | `INSUFFICIENT_EVIDENCE` |
|
||||
| Draft 非法且没有安全进展 | 无可发布事实 | FAILED,保持 fail closed |
|
||||
|
||||
有结论时,只有 `SUPPORTED` 可以发布 Draft。发布的是 Diagnosis Agent 原始 Draft,或者经证明用户可见语义相同的 Repair Draft,不让 SemanticGuard生成“更好的答案”。
|
||||
|
||||
## 8. `conclusion=null` 为什么不进入完整语义审查
|
||||
|
||||
当模型明确表示无法确认根因时,不存在需要验证的根因结论。继续调用 EvidenceRepair 或 SemanticGuard,不仅浪费 Token,还可能让验证模型反向补出一个原本不存在的结论。
|
||||
|
||||
因此无结论路径只做必要的引用真实性检查,然后根据安全进展决定:
|
||||
|
||||
```text
|
||||
有 verified progress
|
||||
-> INSUFFICIENT_EVIDENCE
|
||||
|
||||
没有 progress,但有 limitations.missing_info
|
||||
-> MISSING_REQUIRED_CONTEXT
|
||||
|
||||
两者都没有
|
||||
-> 不是合法业务结果,fail closed
|
||||
```
|
||||
|
||||
这项设计把“没有找到根因”从模型失败中分离出来,但没有降低证据要求。
|
||||
|
||||
## 9. SafeFallback 不是通用错误文案
|
||||
|
||||
SafeFallback 是结构化、确定性的发布结果。它可以包含:
|
||||
|
||||
- `verified_sources`:通过当前 Run 验真的来源;
|
||||
- `observed_facts`:有界、去重后的事实或负向观察;
|
||||
- `limitations`:为什么不能确认根因;
|
||||
- `next_steps`:下一步需要补充的信息或检查;
|
||||
- `validation_issues`:稳定违规码和目标字段;
|
||||
- `failure_stage`:失败发生在收集、证据还是语义阶段。
|
||||
|
||||
它不能包含 Prompt、原始 Draft、raw Tool payload、内部异常或模型 reasoning。Fallback 不是“把错误吞掉”,而是只发布系统已经能够证明的部分。
|
||||
|
||||
## 10. 为什么不采用其他方案
|
||||
|
||||
### 10.1 单个 Verifier Agent
|
||||
|
||||
优点是实现表面简单,缺点是把 ID、Run ownership、Schema 和业务支持度混在同一次模型判断中。本来可以 100% 用代码拒绝的跨 Run 引用,也会变成概率审查。
|
||||
|
||||
### 10.2 Guard 自动改写报告
|
||||
|
||||
可以提高表面成功率,但破坏唯一作者原则。Guard 为了“修好”结论,往往会引入新解释;此时必须重新验证新内容,最终形成循环。
|
||||
|
||||
### 10.3 只做引用存在检查
|
||||
|
||||
能阻止伪造 ID,却无法阻止“真实证据 + 错误根因”。这正是 SemanticGuard 存在的原因。
|
||||
|
||||
### 10.4 语义失败直接技术报错
|
||||
|
||||
丢失了已经验真的事实,也把“结论不受支持”错误表达为基础设施故障。SafeFallback 可以保留过程价值,同时拒绝发布根因。
|
||||
|
||||
## 11. 真实问题如何改变设计
|
||||
|
||||
| 问题 | 暴露的设计缺陷 | 修正 |
|
||||
|---|---|---|
|
||||
| 旧 Verifier 同时验引用、判断语义和改写答案 | 验证职责没有边界 | EvidenceGuard、SemanticGuard、Release 三段拆分 |
|
||||
| Tool 结果面向开发者,含 raw 和重复正文 | Guard 与 Agent 没有独立事实面 | 先建立 canonical truth 和 verified snapshot |
|
||||
| `NO_EVIDENCE` 被当成一般失败或正向证据 | 负向观察没有范围约束 | AnalysisKind 与 EvidenceStatus 匹配校验 |
|
||||
| Repair 可能改变结论 | 修复者变成新的报告作者 | SemanticDraftView 前后等义检查 |
|
||||
| 非法 Draft 使已完成 Tool 检查全部丢失 | Draft 是唯一可发布价值来源 | 只在 ProgressSnapshot 已验真时允许过程型 Fallback |
|
||||
| SemanticGuard 技术失败时行为不一致 | 各层自行决定降级 | Release 统一生成 `SEMANTIC_UNAVAILABLE` |
|
||||
|
||||
## 12. 代价与剩余风险
|
||||
|
||||
1. SemanticGuard 增加一次模型调用和延迟;这是语义安全与成本之间的明确取舍。
|
||||
2. Semantic verdict 不是形式化证明,仍可能误判;当前设计限制的是权限和失败影响,而不是宣称模型绝对正确。
|
||||
3. EvidenceGuard 需要理解每种 Tool 的 typed result;新增 Tool 必须同步增加验证规则。
|
||||
4. Repair 的语义等价由结构化视图定义,无法证明两个自然语言文本在所有解释下完全等价,因此 Repair 能力被刻意限制。
|
||||
5. Canonical record TTL 到期后无法重新完成完整验真,所以公开决策必须在当前 Run 内完成。
|
||||
|
||||
## 13. 如何验证
|
||||
|
||||
| 需要证明 | 代表性验证 |
|
||||
|---|---|
|
||||
| 缺失、重复、未知 analysis 引用被拒绝 | `EvidenceGuardTest` |
|
||||
| cross-run、非 READY、ID/status 不一致被拒绝 | `EvidenceGuardTest` + canonical fixtures |
|
||||
| NO_EVIDENCE 只能形成限定负向观察 | Evidence kind focused cases |
|
||||
| Repair 不得改变用户可见语义 | `DiagnosisReleaseUseCaseTest`、Repair tests |
|
||||
| Semantic 非法输出只有限重试并安全降级 | `SemanticGuardTest` |
|
||||
| Guard 不改写报告,Release 只发布原 Draft 或 Fallback | `DiagnosisReleaseUseCaseTest` |
|
||||
| 无结论、非法 Draft、受控停止正确映射 | `DiagnosisChatExecutorTest`、Release focused cases |
|
||||
| exact-run Trace 能解释每一道门禁 | live E2E Timeline |
|
||||
|
||||
## 14. 与前后链路的关系
|
||||
|
||||
证据安全链依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供独立 canonical truth;它解决的是“哪些内容能够发布”,不负责判断 Agent 是否还应该继续取证。正常收敛由[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)负责。
|
||||
@@ -0,0 +1,77 @@
|
||||
# Harness:先从一次诊断请求开始
|
||||
|
||||
这不是 Harness 的完整说明,而是一页入门导读。
|
||||
|
||||
第一次阅读时,不需要记组件名,不需要看状态枚举,也不需要理解所有边界。先回答一个问题:**为什么 Diagnosis Agent 外面还需要一层 Harness?**
|
||||
|
||||
## 1. 如果只有 Agent,会发生什么
|
||||
|
||||
假设用户问:
|
||||
|
||||
> 为什么支付服务在 10:00 到 10:10 大量超时?
|
||||
|
||||
Diagnosis Agent 会自己决定查什么、调用哪些 Tool、什么时候停止,并根据返回结果写出结论。这里有四个不能只靠 Prompt 解决的问题:
|
||||
|
||||
- 它可能查询过多,耗尽时间和 Token;
|
||||
- Tool 原始结果可能包含敏感或超大内容,不能直接交给模型;
|
||||
- 它引用了某条日志,不代表这条日志真的来自本次查询;
|
||||
- 它写出了一个看似合理的根因,不代表证据足以支持这个根因。
|
||||
|
||||
这些都不是“诊断能力”问题,而是**执行是否受控、结果是否可信**的问题。Harness 就是为此存在的。
|
||||
|
||||
## 2. Harness 在一次请求中做了什么
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["用户提出诊断问题"] --> R["给本次执行建立 Run<br/>限制时间和资源"]
|
||||
R --> A["Agent 分析问题<br/>选择 Tool"]
|
||||
A --> T["Harness 执行 Tool<br/>保存事实,只给 Agent 安全视图"]
|
||||
T --> A
|
||||
A --> D["Agent 写出诊断草稿"]
|
||||
D --> G["Harness 检查<br/>引用是否真实、证据是否支持结论"]
|
||||
G --> P["发布报告<br/>或安全地说明证据不足"]
|
||||
```
|
||||
|
||||
顺着这条线看,Harness 只做三类事情:
|
||||
|
||||
1. **执行前设边界**:为本次请求建立身份、deadline、预算和取消能力。
|
||||
2. **执行中管事实**:所有 Tool 经过统一入口,完整事实由系统保管,模型只看到安全、有限的内容。
|
||||
3. **发布前做验证**:先验证引用,再判断证据是否支持结论;不满足条件就发布 Fallback,而不是让未经验证的结论出去。
|
||||
|
||||
Agent 仍然负责“问题的根因是什么”。Harness 不替 Agent 推理,它负责的是:**让这次推理有边界、有证据、能停止。**
|
||||
|
||||
## 3. 先建立这个最小心智模型
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A["Diagnosis Agent<br/>负责业务推理"]
|
||||
H["Harness<br/>负责确定性控制"]
|
||||
U["用户最终看到的结果"]
|
||||
|
||||
A -->|"提出 Tool 请求和诊断草稿"| H
|
||||
H -->|"返回受控的 Tool 观察"| A
|
||||
H -->|"验证通过后发布,失败则 Fallback"| U
|
||||
```
|
||||
|
||||
到这里,第一次阅读就可以停下。只要能说清下面这句话,就已经抓住了主干:
|
||||
|
||||
> Agent 决定如何诊断,Harness 决定这次诊断可以消耗什么、可以看到什么、何时必须停止,以及什么结果允许发布。
|
||||
|
||||
## 4. 需要时再往下读
|
||||
|
||||
不要按目录顺序阅读。遇到具体问题时,只进入对应文档:
|
||||
|
||||
| 当你想知道 | 再阅读 |
|
||||
|---|---|
|
||||
| 想逐步认识 Harness 的各组组件 | [组件渐进式导读](components/README.md) |
|
||||
| 为什么选这种控制边界,而不是工作流编排 | [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md) |
|
||||
| 一次请求从开始到结束经历什么 | [生命周期与状态](Harness生命周期与状态.md) |
|
||||
| Tool 为什么要保存一份事实、给模型另一份视图 | [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md) |
|
||||
| 如何阻止“引用是真的,但结论是错的” | [证据安全链](Harness证据安全链-从引用真实到结论可发布.md) |
|
||||
| Agent 为什么不会无限查询 | [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md) |
|
||||
| 某个类属于哪里、负责什么 | [组件全景](Harness组件全景-职责-设计原因与边界.md) |
|
||||
| 某个名词或状态是什么意思 | [Context 词典](CONTEXT.md) |
|
||||
|
||||
如果准备系统学习,建议先读组件渐进式导读的四篇文章。每次只读一篇,读到“先记住这些”就可以停止。
|
||||
|
||||
“组件全景”和“Context”都是参考手册,不需要从头读,也不需要背。
|
||||
@@ -0,0 +1,121 @@
|
||||
# 01 运行控制:让一次请求有边界
|
||||
|
||||
这一篇只回答一个问题:**一次诊断请求由谁负责到底?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
用户发起支付超时诊断。请求开始后,Router 调了一次模型,Diagnosis Agent 又调用多轮模型和 Tool,SemanticGuard 最后还要调用一次模型。与此同时,客户端可能断开,某次调用可能超时,两个线程也可能同时报告成功和失败。
|
||||
|
||||
如果没有统一的运行控制,每个组件都会有自己的理解:
|
||||
|
||||
- Controller 认为连接断开了,后台 Agent 却还在继续查询;
|
||||
- Agent 认为还可以调用 Tool,Run 的总预算其实已经耗尽;
|
||||
- 超时线程先写入失败,迟到的模型结果又把它覆盖为成功;
|
||||
- SDK 在内部偷偷重试,系统无法解释多出来的延迟和 Token;
|
||||
- 线上只看到最终失败,却不知道请求在哪一步、因为什么停止。
|
||||
|
||||
所以运行控制不是一个计时器,而是由几个职责不同的组件共同完成。
|
||||
|
||||
## 2. 四组组件怎样协作
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant APP as Application
|
||||
participant CORE as Core / RunContext
|
||||
participant WORK as Agent、Tool、Guard
|
||||
participant RETRY as Retry
|
||||
participant AUDIT as Audit
|
||||
|
||||
APP->>CORE: 创建 RunContext
|
||||
CORE-->>APP: runId、deadline、budget、cancel、lifecycle
|
||||
APP->>WORK: 显式传入 RunContext
|
||||
WORK->>CORE: 每次消耗前检查 active 和预算
|
||||
WORK->>RETRY: 仅允许策略声明的技术重试
|
||||
RETRY->>AUDIT: 记录每个 attempt
|
||||
WORK->>AUDIT: 记录模型、Tool 和阶段元数据
|
||||
APP->>CORE: 尝试写入最终终态
|
||||
CORE-->>APP: first-terminal-wins
|
||||
APP->>AUDIT: 记录公开结果和预算对账
|
||||
```
|
||||
|
||||
第一次阅读可以把它们理解为:
|
||||
|
||||
- **Application 是负责人**:组织一次请求从创建到公开结果。
|
||||
- **Core 是执行规则**:统一管理身份、deadline、预算、取消和唯一终态。
|
||||
- **Retry 是重做规则**:明确什么失败允许再试一次。
|
||||
- **Audit 是运行账本**:记录发生过什么,但不保存敏感正文。
|
||||
|
||||
## 3. Application:负责人,而不是推理者
|
||||
|
||||
Application 负责建立一次请求的应用边界:读取安全历史、创建 Run、判断 intent、调用对应执行分支、持久化结果,并把安全内容交给 SSE。
|
||||
|
||||
设计它的原因,是这些步骤必须由一个地方协调。如果散落在 Controller、Agent 和 Guard 中,会出现多个结果出口和不同的失败语义。
|
||||
|
||||
它不负责判断支付超时的根因,也不负责自己拼一份诊断 Fallback。它只负责把正确的执行组件按顺序接起来,并确保结果经过统一出口。
|
||||
|
||||
主要代码入口:`ChatApplicationUseCase`、`DiagnosisChatExecutor`、`KnowledgeQueryExecutor`、`SystemChatExecutor`、`ChatRunStore`。
|
||||
|
||||
## 4. Core:一次 Run 的共同规则
|
||||
|
||||
Core 创建 `RunContext`。这个上下文显式携带:
|
||||
|
||||
```text
|
||||
这是谁的请求:sessionId + runId
|
||||
最晚执行到何时:deadline
|
||||
还能消耗多少:RunBudget
|
||||
是否要求停止:RunCancellation
|
||||
最终如何结束:RunLifecycle
|
||||
模型消耗如何对账:ModelCallLedger
|
||||
信息收集是否仍有价值:DiagnosisProgressTracker
|
||||
```
|
||||
|
||||
这里最重要的决策是**显式传递 RunContext**,而不是使用 ThreadLocal 或让每个组件自己查询全局状态。原因是模型和 Tool 可能跨线程运行;隐式上下文很容易丢失、串线,也很难在测试中证明归属关系。
|
||||
|
||||
`RunLifecycle` 使用 first-terminal-wins:第一个成功写入的终态不可被迟到结果覆盖。它解决的是并发一致性,不是公开结果的业务含义。
|
||||
|
||||
主要代码入口:`DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunCancellation`、`RunLifecycle`。
|
||||
|
||||
## 5. Retry:不能让“再试一次”藏起来
|
||||
|
||||
重试会增加成本和延迟,也可能重复副作用,因此不能由 SDK、HTTP Client 和各组件各自决定。当前策略是:
|
||||
|
||||
- Router 和 SemanticGuard 的特定技术失败最多尝试两次;
|
||||
- Diagnosis Agent、业务 Tool 和 EvidenceRepair 不做隐藏自动重试;
|
||||
- 取消、预算耗尽和协议错误不能被重试吞掉。
|
||||
|
||||
这里没有设计通用重试 DSL。首版只需要不可变的 `RetryPolicy`、稳定的失败分类和统一的 `HarnessRetryExecutor`,复杂配置系统反而会掩盖真实执行路径。
|
||||
|
||||
## 6. Audit:留下解释,而不是留下全部内容
|
||||
|
||||
线上排障需要知道:调用了哪个组件、用了多少 Token、Tool 是否完成、为什么停止、为什么发布 Fallback。但这不等于长期保存 Prompt、SQL、日志原文、Tool raw response 和模型 reasoning。
|
||||
|
||||
因此 Audit 长期保留的是有界元数据,完整 Tool 真相只在 canonical store 中短期存在。这个决策同时满足可回放和最小暴露原则。
|
||||
|
||||
主要代码入口:`DiagnosisTraceRecorder`、`TraceAuditEvents`、`ToolInvocationAuditSink`、`HarnessAgentAuditHook`、`ModelCallAuditor`。
|
||||
|
||||
## 7. 为什么没有合并成一个 RunManager
|
||||
|
||||
把上述职责都放进一个 `RunManager` 看起来更简单,但它会同时知道 HTTP、路由、预算、模型、Tool、数据库和 SSE,最后变成新的业务工作流引擎。
|
||||
|
||||
当前拆分依据不是“代码越细越好”,而是决策权不同:
|
||||
|
||||
| 组件 | 它拥有的决定 | 它不能决定 |
|
||||
|---|---|---|
|
||||
| Application | 请求走哪条应用分支、何时持久化和返回 | 业务根因是否成立 |
|
||||
| Core | 是否仍可执行、资源是否允许、哪个终态生效 | 返回正常报告还是 Fallback |
|
||||
| Retry | 某类技术失败是否允许下一 attempt | 修改业务结果 |
|
||||
| Audit | 记录哪些安全元数据 | 影响执行结果 |
|
||||
|
||||
## 8. 先记住这些
|
||||
|
||||
第一次阅读只需记住:
|
||||
|
||||
1. Application 对一次请求负责,Core 对执行不变量负责。
|
||||
2. RunContext 必须显式传播,所有模型和 Tool 共用同一预算与取消信号。
|
||||
3. 第一个终态获胜,迟到结果不能翻案。
|
||||
4. Retry 必须显式、分类、可审计。
|
||||
5. Audit 记录控制事实,不长期复制敏感正文。
|
||||
|
||||
下一篇:[02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md)。
|
||||
|
||||
需要查完整状态转换时,阅读[生命周期与状态](../Harness生命周期与状态.md);需要查全部类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
@@ -0,0 +1,148 @@
|
||||
# 02 Agent 接入与收敛:让推理可以工作,也可以停止
|
||||
|
||||
这一篇只回答一个问题:**怎样保留 Agent 的自主推理,同时阻止它重复查询或无限空转?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Diagnosis Agent 第一次查询 10:00 到 10:10 的支付日志,没有发现异常;第二次换了关键词,仍然没有新信息;第三次又提交了与第一次等价的查询。
|
||||
|
||||
仅设置“最多调用 10 次 Tool”只能限制最坏损失,却回答不了:
|
||||
|
||||
- 上一次结果有没有推进诊断?
|
||||
- 这次查询是否已经做过?
|
||||
- 连续多少次没有新信息后应该停止?
|
||||
- Agent 已经收到停止要求,为什么还能继续调用 Tool?
|
||||
- 停止后,怎样安全地向用户说明已经检查过什么?
|
||||
|
||||
这就是 Agent 接入层和 Progress 组件共同解决的问题。
|
||||
|
||||
## 2. 一轮 ReAct 怎样经过 Harness
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Diagnosis Agent
|
||||
participant MI as Model Interceptor
|
||||
participant TI as Tool Interceptor
|
||||
participant P as Progress Tracker
|
||||
participant T as Tool Boundary
|
||||
|
||||
A->>MI: 发起一轮模型调用
|
||||
MI->>MI: 检查 Run、预留预算、记录 Token
|
||||
A->>TI: 请求调用 Tool
|
||||
TI->>P: 评价上一轮是否有信息增益
|
||||
P-->>TI: 可继续 / 重复 / 已饱和 / 协议错误
|
||||
alt 允许继续
|
||||
TI->>T: 执行业务 Tool
|
||||
T-->>TI: Control View + Model Observation
|
||||
TI->>P: 登记已完成调用和 scope
|
||||
TI-->>A: 只返回 Model Observation
|
||||
else 必须停止
|
||||
TI-->>A: STOP_REQUIRED
|
||||
end
|
||||
```
|
||||
|
||||
第一次阅读可以把它们理解为:
|
||||
|
||||
- **Agent 接入层是检查站**:把框架原生 ReAct loop 接入预算、Tool 和审计边界。
|
||||
- **Progress Tracker 是收敛记录器**:判断是否重复、是否连续无增益、是否必须停止。
|
||||
|
||||
## 3. 为什么复用框架 ReAct,而不是自己重写循环
|
||||
|
||||
Diagnosis Agent 需要模型原生 Tool Calling 和多轮 ReAct。项目没有再写一套 `while` 循环,而是通过 Factory、Model Interceptor、Tool Interceptor 和 Audit Hook 接入框架。
|
||||
|
||||
这样做的决策依据是:
|
||||
|
||||
- ReAct 的业务推理由框架和模型负责;
|
||||
- 预算、Tool 授权、事实保存和停止协议由 Harness 负责;
|
||||
- 两者通过明确边界连接,不互相复制实现。
|
||||
|
||||
如果 Harness 自己维护另一套 ReAct 状态机,就会同时出现“框架认为的下一步”和“Harness 认为的下一步”,调试时很难确定谁才是真理源。
|
||||
|
||||
主要代码入口:`DiagnosisAgentFactory`、`DiagnosisAgentUseCase`、`HarnessModelInterceptor`、`HarnessToolInterceptor`、`HarnessEvidenceTools`。
|
||||
|
||||
## 4. Model Interceptor:每一轮模型调用都必须记账
|
||||
|
||||
一个 Diagnosis Agent 执行不等于一次模型调用。ReAct 可能经历多轮思考和 Tool 返回,因此每一轮都要:
|
||||
|
||||
1. 检查 Run 是否仍然 active;
|
||||
2. 在调用前预留模型预算;
|
||||
3. 调用后记录 Provider 返回的 Token usage;
|
||||
4. 再次检查取消或迟到结果。
|
||||
|
||||
Interceptor 不负责重试模型,也不判断输出是否支持根因。它只确保框架内部的每轮调用无法绕过 Harness。
|
||||
|
||||
## 5. Tool Interceptor:先检查进展,再允许查询
|
||||
|
||||
模型提交的 Tool Call 不只是业务参数,还携带对上一轮结果的评价。Tool Interceptor 会依次检查:
|
||||
|
||||
- previous observation 是否完整、顺序是否正确;
|
||||
- 上一轮是 `GAINED` 还是 `NO_GAIN`;
|
||||
- 当前 Tool scope 是否已经完成过;
|
||||
- 收集状态是否已经 `SATURATED`;
|
||||
- 通过检查后,才把业务请求交给 ToolBoundary。
|
||||
|
||||
它不执行后端查询,也不再次扣 Tool 预算。实际执行属于 ToolBoundary;收敛状态属于 ProgressTracker。Interceptor 只是两者与 ReAct 框架之间的接合点。
|
||||
|
||||
## 6. 为什么“有结果”不等于“有信息增益”
|
||||
|
||||
Tool 可以客观判断是否返回候选内容,却不知道这些内容是否推进了当前假设。例如同一条超时日志再次出现:
|
||||
|
||||
```text
|
||||
InvocationStatus = READY
|
||||
EvidenceStatus = EVIDENCE_FOUND
|
||||
InformationGain = NO_GAIN
|
||||
```
|
||||
|
||||
前三个状态回答不同问题:查询是否完成、是否有候选内容、内容是否推进当前诊断。把它们合成一个 `SUCCESS` 会让系统无法正常收敛。
|
||||
|
||||
当前由 Agent 在**下一次 Tool Call** 中评价上一轮的信息增益。这样评价发生在它真正做出下一步行动时,Harness 也能机械检查调用顺序,而不需要再增加一个 Progress Judge 模型。
|
||||
|
||||
## 7. Progress Tracker 保存什么
|
||||
|
||||
Tracker 只保存控制所需的最小状态:
|
||||
|
||||
- 已完成的 `tool_call_id` 和规范化 scope;
|
||||
- 哪个结果仍等待 Agent 评价;
|
||||
- 连续 `NO_GAIN` 次数;
|
||||
- 收集状态和停止原因;
|
||||
- 停止指令是否已经交付。
|
||||
|
||||
它不保存 raw response,也不判断日志是否证明支付线程池耗尽。业务价值判断仍由 Diagnosis Agent 负责,事实内容仍由 canonical store 负责。
|
||||
|
||||
主要代码入口:`DiagnosisProgressTracker`、`ToolScopeNormalizer`、`DiagnosisProgressProjector`。
|
||||
|
||||
## 8. 为什么停止不直接等于失败
|
||||
|
||||
连续无增益后停止,是一次正常的受控收敛,不是技术异常。Harness 会从已经完成的 canonical Tool 记录中投影 `ProgressSnapshot`,只保留可验真的检查范围、观察事实和限制,再形成安全 Fallback。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
N["连续 NO_GAIN 或重复查询"] --> S["CollectionState = SATURATED"]
|
||||
S --> X["拒绝新的证据 Tool"]
|
||||
X --> P["生成安全 ProgressSnapshot"]
|
||||
P --> F["发布证据不足的 Fallback"]
|
||||
```
|
||||
|
||||
因此,“没有找到足够证据”可以正常结束;只有违反进展协议、预算耗尽且没有安全进展等情况,才可能升级为失败。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| 只设 Tool 次数上限 | 只能止损,不能识别查询已经没有价值 |
|
||||
| Harness 根据结果条数判断增益 | 条数是客观统计,不代表是否推进业务假设 |
|
||||
| 增加 Progress Judge Agent | 增加模型成本和新的非确定性判断点 |
|
||||
| 用自然语言相似度判断重复 | 首版难以稳定解释误判,当前只做 typed 参数级 scope 规范化 |
|
||||
| 把剩余预算告诉模型 | 容易让模型围绕阈值博弈,硬限制应由 Harness 保持 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. 框架负责 ReAct loop,Harness 通过 Interceptor 接入控制能力。
|
||||
2. 预算回答“还能不能查”,信息增益回答“继续查有没有价值”。
|
||||
3. Tool 是否有结果与结果是否推进诊断是两件事。
|
||||
4. ProgressTracker 只保存控制状态,不保存完整证据。
|
||||
5. 信息饱和可以正常发布 Fallback,不等于 Run 执行失败。
|
||||
|
||||
下一篇:[03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md)。
|
||||
|
||||
需要深入停止协议时,阅读[信息增益停止专题](../Harness信息增益停止-让无证据诊断正常收敛.md)。
|
||||
@@ -0,0 +1,135 @@
|
||||
# 03 Tool 事实边界:让模型看到必要信息,系统保留真相
|
||||
|
||||
这一篇只回答一个问题:**Tool 返回的数据应该由谁保管,模型究竟可以看到多少?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Agent 查询支付日志,后端一次返回了几千行内容,其中包含重复堆栈、敏感字段和远超上下文窗口的正文。
|
||||
|
||||
直接把原始结果塞回模型会产生几个问题:
|
||||
|
||||
- 敏感数据进入模型上下文;
|
||||
- 大结果挤占 Token,真正关键的证据反而被淹没;
|
||||
- 模型后续引用经过裁剪的文本,系统无法证明它对应哪次真实调用;
|
||||
- 若只保存裁剪结果,EvidenceGuard 又失去了独立验真的事实来源;
|
||||
- 若把完整 raw 长期写入数据库,泄露面和存储成本都会扩大。
|
||||
|
||||
所以 Tool 设计的核心不是“怎样调用后端”,而是**谁拥有原始事实,以及不同消费者应该看到哪一层数据**。
|
||||
|
||||
## 2. 一次 Tool 调用的数据怎样变化
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
REQ["Typed Tool Request"] --> B["ToolBoundary<br/>权限、只读、预算、大小检查"]
|
||||
B --> RAW["Backend Raw Result"]
|
||||
RAW --> P["Tool-specific Projector"]
|
||||
P --> C["Canonical Invocation<br/>短期保存 request、raw、agent_result 和状态"]
|
||||
C --> CV["Control View<br/>供 Harness 判断状态和进展"]
|
||||
C --> MO["Model Observation<br/>供 Agent 推理的白名单内容"]
|
||||
C --> EG["EvidenceGuard<br/>按当前 Run 独立验真"]
|
||||
B -.-> AU["Durable Audit<br/>长期只留有界元数据"]
|
||||
```
|
||||
|
||||
第一次阅读只需区分三份内容:
|
||||
|
||||
- **Canonical Invocation**:系统在当前 Run 内短期保管的完整调用真相。
|
||||
- **Control View**:Harness 用来判断状态、scope 和证据数量的控制字段。
|
||||
- **Model Observation**:模型真正看到的安全、有限内容。
|
||||
|
||||
## 3. ToolBoundary:所有 Tool 的统一入口
|
||||
|
||||
RAG、日志和 MySQL 后端完全不同,但它们都必须遵守相同的不变量:
|
||||
|
||||
1. Run 仍然 active,调用 ID 与当前 Run 匹配;
|
||||
2. Tool 已授权,请求声明和实际行为保持只读;
|
||||
3. 调用前预算允许,结果大小没有越界;
|
||||
4. canonical 状态只能从 `PROJECTING` 进入 `READY` 或 `ERROR`;
|
||||
5. 长期 Audit 不保存完整请求和 raw response。
|
||||
|
||||
如果每个 Adapter 自己实现这些规则,三种 Tool 很快会出现不同的错误码、预算顺序和存储行为。ToolBoundary 集中处理共同规则,Adapter 只处理业务协议。
|
||||
|
||||
ToolBoundary 不判断信息增益,也不判断某条日志是否支持根因。它只保证调用合法、状态可信、结果有界。
|
||||
|
||||
主要代码入口:`ToolBoundary`、`ToolCallRequestEnvelope`、`ToolBoundaryResult`、`ToolBoundaryErrorCode`。
|
||||
|
||||
## 4. Projector:把后端结果变成稳定事实
|
||||
|
||||
不同后端的 raw 数据不能直接成为 Agent 契约:
|
||||
|
||||
- RAG 需要限制证据数量和摘录长度,并保留文档身份;
|
||||
- Logs 需要限制事件、聚合 pattern,明确实际时间范围和是否截断;
|
||||
- MySQL 需要限制行、列、单元格和总 bytes,并保留查询 scope。
|
||||
|
||||
每种 Tool 使用自己的 `ToolResultProjector`。Projector 负责标准化和计算客观 `EvidenceStatus`,但它不调用模型,也不判断这些事实是否足以支持最终结论。
|
||||
|
||||
这是刻意保留的业务差异。强行做一个“万能 JSON Cleaner”,往往会丢掉每类证据真正需要的身份和范围信息。
|
||||
|
||||
## 5. Canonical Store:为什么要保留独立真相
|
||||
|
||||
Agent Observation 是为推理优化的,它会被裁剪和白名单投影,不能反过来充当验真依据。Canonical Store 保存当前调用的 request、raw、标准化 `agent_result`、状态和时间,使 EvidenceGuard 能绕开 Agent 上下文独立读取事实。
|
||||
|
||||
当前选择 Redis TTL,是因为完整调用记录:
|
||||
|
||||
- 只在当前 Run 的验证阶段需要;
|
||||
- 可能包含敏感内容,不应永久保存;
|
||||
- 需要按 `runId + tool_call_id` 快速定位;
|
||||
- 容量必须有硬上限,读取不能自动续期。
|
||||
|
||||
raw 超限时选择失败,而不是悄悄截断。因为一旦截断,canonical record 就不再代表后端真实返回,后续验真会建立在不完整事实之上。
|
||||
|
||||
主要代码入口:`CanonicalInvocationStore`、`RedisCanonicalInvocationStore`、`CanonicalToolInvocation`、`CanonicalInvocationLimits`。
|
||||
|
||||
## 6. Model Observation:模型只拿完成任务所需的内容
|
||||
|
||||
即使 canonical `agent_result` 已经标准化,其中仍可能包含 Harness 控制字段。`ToolResultViewProjector` 会进一步拆成:
|
||||
|
||||
```text
|
||||
Control View:status、evidence count、scope、截断状态等
|
||||
Model Observation:有界证据正文、可读来源和下一步推理所需字段
|
||||
```
|
||||
|
||||
模型不会看到 raw response、Redis key、预算阈值、重复指纹和完整 Harness 状态。这样既减少上下文噪声,也避免模型利用或复述内部控制信息。
|
||||
|
||||
## 7. MySQL 为什么还需要单独的只读沙箱
|
||||
|
||||
`readOnly=true` 只是请求声明,不能证明模型生成的 SQL 安全。MySQL Tool 在进入真实执行前还要经过:
|
||||
|
||||
- JSqlParser AST 解析;
|
||||
- 单条 SELECT 和保守语法子集限制;
|
||||
- 数据源、schema、table、column 精确 allowlist;
|
||||
- 禁止投影通配符和元数据探测;
|
||||
- 只读账号、timeout、LIMIT 与最大行数。
|
||||
|
||||
Validator 产生已经批准的 `MysqlQueryPlan`,Executor 只接受这个 plan,不重新信任原始字符串。这是纵深防御:任何一层都不能单独被当作完整授权。
|
||||
|
||||
## 8. 为什么短期真相和长期审计要分开
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
C["Canonical Store<br/>完整、敏感、短期"] -->|"用于"| V["当前 Run 验真"]
|
||||
A["Durable Audit<br/>有界、元数据、长期"] -->|"用于"| O["排障、统计、成本对账"]
|
||||
```
|
||||
|
||||
把两者合并会走向两个极端:要么长期复制所有敏感正文,要么为了安全只存元数据,导致当前 Run 无法验真。分开后,每种存储只承担自己的责任。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| raw 直接返回 Agent | 泄露、超限、噪声和无法独立验真 |
|
||||
| 只保存 Agent Observation | 投影内容不是完整事实,Guard 会依赖模型看到的版本 |
|
||||
| 完整 raw 永久写 MySQL | 扩大敏感数据暴露面和长期存储成本 |
|
||||
| raw 超限后静默截断 | canonical truth 会变成不完整真相 |
|
||||
| 所有 Tool 共用万能 Projector | 无法保持日志、RAG、表格各自的身份和 scope 语义 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. ToolBoundary 统一执行规则,Adapter 处理具体后端。
|
||||
2. Canonical Invocation 是系统真相,Model Observation 是模型视图。
|
||||
3. Agent 看到的内容不能反过来成为 EvidenceGuard 的事实来源。
|
||||
4. 完整真相短期保存,长期 Audit 只留有界元数据。
|
||||
5. Tool 找到候选内容,不代表它支持最终根因。
|
||||
|
||||
下一篇:[04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md)。
|
||||
|
||||
需要深入字段和存储取舍时,阅读[Tool 双视图专题](../Harness-Tool双视图-从原始结果到可验证证据.md)。
|
||||
@@ -0,0 +1,162 @@
|
||||
# 04 验证与发布:让未经证明的结论无法越过出口
|
||||
|
||||
这一篇只回答一个问题:**Agent 写完诊断草稿以后,为什么还不能直接返回给用户?**
|
||||
|
||||
## 1. 先看一个具体问题
|
||||
|
||||
Agent 在报告中写道:
|
||||
|
||||
> 10:03 出现数据库连接池耗尽,因此支付接口大量超时。
|
||||
|
||||
它同时引用了一次日志 Tool Call。系统即使确认这个调用真实存在、属于当前 Run,而且日志中确实出现“connection timeout”,仍然不能直接发布上述结论。
|
||||
|
||||
因为这里至少有三个不同问题:
|
||||
|
||||
1. Agent 引用的 Tool Call 是不是真的?
|
||||
2. 真实日志是否足以证明“数据库连接池耗尽导致支付超时”?
|
||||
3. 验证失败后,系统最终应该向用户发布什么?
|
||||
|
||||
它们分别属于 EvidenceGuard、SemanticGuard 和 Release。
|
||||
|
||||
## 2. 草稿怎样通过发布链
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D["DiagnosisDraft<br/>Agent 写出的草稿"] --> E["EvidenceGuard<br/>机械验证引用和归属"]
|
||||
E -->|"结构或引用可修复"| R["EvidenceRepair<br/>只修引用,不改语义"]
|
||||
R --> E
|
||||
E -->|"得到已验真证据"| S["SemanticGuard<br/>判断证据是否支持报告"]
|
||||
S -->|"SUPPORTED"| P["Release SUCCESS<br/>发布原始安全 Draft"]
|
||||
E -->|"无法验真"| F["SafeFallback"]
|
||||
S -->|"UNSUPPORTED 或不可用"| F
|
||||
F --> O["Release FALLBACK"]
|
||||
```
|
||||
|
||||
第一次阅读只需记住:
|
||||
|
||||
- **EvidenceGuard 验真**:证明引用确实来自当前 Run 的已完成 Tool 调用。
|
||||
- **SemanticGuard 验义**:判断这些真实证据是否支持报告中的结论。
|
||||
- **Release 决定出口**:只发布验证通过的原 Draft,或者发布确定性的 SafeFallback。
|
||||
|
||||
## 3. EvidenceGuard:代码能证明的事交给代码
|
||||
|
||||
EvidenceGuard 会检查:
|
||||
|
||||
- Draft 结构和 analysis ID 是否合法;
|
||||
- 所有结论分析是否形成完整引用闭包;
|
||||
- `tool_call_id` 是否属于当前 Run;
|
||||
- canonical invocation 是否为 `READY`;
|
||||
- `EvidenceStatus` 与引用类型是否一致;
|
||||
- 引用的 evidence 是否确实存在于标准化 Tool 结果中。
|
||||
|
||||
这些都是确定性问题,不需要模型判断。让模型验证自己的引用,会让同一个非确定性来源同时当作者和裁判。
|
||||
|
||||
验证通过后,EvidenceGuard 生成 `VerifiedEvidenceSnapshot`。它只包含 SemanticGuard 所需的已验真证据、来源和 scope,不包含 Tool raw response。
|
||||
|
||||
主要代码入口:`EvidenceGuard`、`EvidenceGuardResult`、`VerifiedEvidenceSnapshot`、`EvidenceViolation`。
|
||||
|
||||
## 4. EvidenceRepair:为什么允许修,又为什么只能修一次
|
||||
|
||||
有些 Draft 的业务语义可能没有问题,只是引用结构出错,例如漏写一个 analysis ID。直接丢弃会浪费一次昂贵诊断,因此系统允许一次 EvidenceRepair。
|
||||
|
||||
但 Repair 不是第二个报告作者。它必须满足:
|
||||
|
||||
```text
|
||||
修复前用户可见语义 == 修复后用户可见语义
|
||||
```
|
||||
|
||||
它只能修结构和引用,不能新增事实、改变结论或补写建议;修复后还必须重新经过 EvidenceGuard。若语义视图发生变化,Repair 立即失败。
|
||||
|
||||
只允许一次,是为了避免形成“修复失败再修复”的隐式 Agent loop,也让成本和执行路径保持可解释。
|
||||
|
||||
## 5. SemanticGuard:引用真实不等于推论成立
|
||||
|
||||
一条真实的 `connection timeout` 日志,可能来自下游网络问题,也可能只是故障结果,不能自动证明数据库连接池耗尽。
|
||||
|
||||
这类支持关系无法完全用规则判断,因此 SemanticGuard 使用一次隔离的模型调用,只接收:
|
||||
|
||||
- 用户原始问题;
|
||||
- Draft 的用户可见语义;
|
||||
- EvidenceGuard 产生的 verified snapshot。
|
||||
|
||||
它没有 Tool、没有记忆、没有 ReAct loop、不访问 Redis,也不能改写报告。输出只有 `SUPPORTED / UNSUPPORTED` 和审计原因。
|
||||
|
||||
这个设计没有追求一个看似精确的置信度分数。首版真正需要的是发布门禁,而未经校准的 0.73 并不能形成比二元判定更可靠的协议。
|
||||
|
||||
主要代码入口:`SemanticGuard`、`SemanticGuardInput`、`SemanticGuardDecision`、`GuardModelCall`。
|
||||
|
||||
## 6. Release:为什么必须只有一个出口
|
||||
|
||||
如果 Agent、Guard、Application 都能各自构造最终结果,会出现:
|
||||
|
||||
- 同一验证失败被不同层解释成不同文案;
|
||||
- 某个分支忘记经过 SemanticGuard;
|
||||
- 迟到的 Draft 绕过已经确定的取消或失败;
|
||||
- Fallback 混入未经验证的 Agent 内容。
|
||||
|
||||
`DiagnosisReleaseUseCase` 因此拥有唯一发布决策。它协调 EvidenceGuard、可选 Repair、SemanticGuard 和 SafeFallbackFactory,但自己不写新根因。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
V{"可以安全发布正常报告吗?"}
|
||||
V -->|"引用真实且语义受支持"| OK["ReleaseOutcome.SUCCESS<br/>发布原 Draft"]
|
||||
V -->|"证据不足或验证不通过"| FB["ReleaseOutcome.FALLBACK<br/>发布 SafeFallback"]
|
||||
V -->|"无法形成安全业务结果"| ER["ReleaseOutcome.FAILED"]
|
||||
```
|
||||
|
||||
这里必须区分:
|
||||
|
||||
```text
|
||||
RunState.SUCCESS + ReleaseOutcome.FALLBACK
|
||||
```
|
||||
|
||||
这表示系统完整、安全地处理了请求,但证据不足以发布正常诊断结论。Fallback 是安全产品结果,不等于执行失败。
|
||||
|
||||
## 7. SafeFallback:不是让另一个模型重新回答
|
||||
|
||||
验证失败后再次让模型“写得保守一点”,仍然可能产生新事实。SafeFallback 因此由代码确定性构造,只允许使用:
|
||||
|
||||
- 已验真的来源和检查范围;
|
||||
- 可以安全表达的观察事实;
|
||||
- 当前证据的限制;
|
||||
- 面向补充数据的下一步建议;
|
||||
- 稳定、可公开的问题类型。
|
||||
|
||||
它不会包含 Agent 原 Draft 的未验证结论、SemanticGuard 内部原因、Provider 错误或异常栈。
|
||||
|
||||
主要代码入口:`DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory`、`DiagnosisReleaseResult`。
|
||||
|
||||
## 8. Contract:为什么状态和结果必须类型化
|
||||
|
||||
发布链横跨模型 JSON、Java、Redis、数据库和 SSE。如果各层都使用自由字符串 `SUCCESS`,很快就无法区分:
|
||||
|
||||
- Tool 调用完成;
|
||||
- Tool 找到候选证据;
|
||||
- 语义审查通过;
|
||||
- Run 执行成功;
|
||||
- 最终发布正常报告。
|
||||
|
||||
Contract 包用不同类型保持这些问题正交,例如 `InvocationStatus`、`EvidenceStatus`、`SemanticVerdict`、`RunState` 和 `ReleaseOutcome`。类型多不是为了复杂,而是为了阻止一个含糊的 `status` 穿过整个系统。
|
||||
|
||||
## 9. 为什么没有采用其他方案
|
||||
|
||||
| 方案 | 没有采用的原因 |
|
||||
|---|---|
|
||||
| 只检查 Tool Call ID 存在 | 只能证明引用存在,不能证明归属、状态和内容一致 |
|
||||
| 一个 Verifier Agent 同时验引用和语义 | 混合确定性与非确定性判断,失败后难以定位责任 |
|
||||
| Guard 自动改写报告 | Guard 会变成第二个作者,并可能引入未验证事实 |
|
||||
| SemanticGuard 使用 Tool 再查证 | 会形成第二条诊断链,预算和证据归属变复杂 |
|
||||
| 验证失败直接抛技术错误 | 证据不足是正常业务结果,用户仍需要安全说明 |
|
||||
| 用置信度阈值发布 | 未校准分数不能充当可靠安全协议 |
|
||||
|
||||
## 10. 先记住这些
|
||||
|
||||
1. Draft 是候选结果,不是已发布报告。
|
||||
2. EvidenceGuard 用代码证明引用真实,SemanticGuard 判断证据是否支持语义。
|
||||
3. Repair 只能修引用,不能改变用户可见语义,而且只尝试一次。
|
||||
4. Release 是唯一出口,只发布原 Draft 或确定性 SafeFallback。
|
||||
5. `FALLBACK` 可以对应一次正常完成的 Run。
|
||||
|
||||
四篇组件导读到这里结束。需要回看整体路径时返回[组件学习地图](README.md)。
|
||||
|
||||
需要深入验证规则时,阅读[证据安全链专题](../Harness证据安全链-从引用真实到结论可发布.md);需要查所有 contract 类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Harness 组件:从一次请求逐步认识
|
||||
|
||||
这里不按照 Java 包逐个介绍组件,而是沿着一次诊断请求,分四步回答四个问题。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["一个诊断请求来了"] --> C1["01 运行控制<br/>怎样保证这次执行受控"]
|
||||
C1 --> C2["02 Agent 接入与收敛<br/>怎样让 Agent 工作但不空转"]
|
||||
C2 --> C3["03 Tool 事实边界<br/>怎样安全地取得事实"]
|
||||
C3 --> C4["04 验证与发布<br/>怎样决定结果能否交给用户"]
|
||||
```
|
||||
|
||||
| 顺序 | 先回答的问题 | 涉及的职责域 |
|
||||
|---|---|---|
|
||||
| [01 运行控制](01-运行控制-让一次请求有边界.md) | 谁创建 Run,谁限制资源,谁记录它如何结束? | Application、Core、Retry、Audit |
|
||||
| [02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md) | 怎样使用框架 ReAct,同时阻止重复查询和无增益空转? | Agent、Progress |
|
||||
| [03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md) | Tool 原始结果由谁保存,模型究竟能看到什么? | Tool |
|
||||
| [04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md) | 引用真实是否等于结论成立,最终由谁决定发布? | Guard、Release、Contract |
|
||||
|
||||
建议一次只读一篇。每篇读到“先记住这些”就可以停下;类名只在最后用于定位代码。
|
||||
|
||||
需要查全部生产类型时,再使用[组件全景参考手册](../Harness组件全景-职责-设计原因与边界.md)。
|
||||
@@ -4,7 +4,7 @@
|
||||
**前提**:旧 Milvus SDK 直连检索路径后续废弃,不作为长期实现基础
|
||||
**目标**:在现有 `lookup_knowledge` pipeline 上接入 dense + sparse/BM25 混合检索,融合优先走服务端 RRF
|
||||
**关联文档**:
|
||||
- `docs/RAG排序-多路召回与RRF.md`(排序与多路召回判断框架)
|
||||
- `RAG排序-多路召回与RRF.md`(排序与多路召回判断框架)
|
||||
- 本文后续实现讨论以本节 **「交付拆分:分块去重 + Hybrid 同规划」** 为基线
|
||||
|
||||
---
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
|
||||
- ACI 契约:`RagToolResult.relevance_level` / `RagRelevanceLevel`
|
||||
- 计算:`KnowledgeEvidencePostProcessor` → `qualityScore` 阈值
|
||||
- 质量统一:`docs/RAG-Hybrid质量分与后处理.md`
|
||||
- 多路与 RRF:`docs/RAG排序-多路召回与RRF.md`
|
||||
- 质量统一:`RAG-Hybrid质量分与后处理.md`
|
||||
- 多路与 RRF:`RAG排序-多路召回与RRF.md`
|
||||
- **运行时 Trace / 审计**:`mvp/architecture/RAG检索可观测性与审计.md`
|
||||
|
||||
---
|
||||
@@ -9,7 +9,7 @@
|
||||
- `MilvusHybridKnowledgeStore`(dense / hybrid)
|
||||
- `RetrievalScoreNormalizer` / `KnowledgeEvidencePostProcessor`
|
||||
- OpenSpec / devflow:`rag-quality-score-unify`
|
||||
- 前置讨论:`docs/RAG排序-多路召回与RRF.md`
|
||||
- 前置讨论:`RAG排序-多路召回与RRF.md`
|
||||
- 架构:`mvp/architecture/RAG知识检索架构.md` §6
|
||||
|
||||
---
|
||||
@@ -585,7 +585,7 @@ flowchart TB
|
||||
|
||||
| 材料 | 路径 |
|
||||
|------|------|
|
||||
| 多路与 RRF 讨论 | `docs/RAG排序-多路召回与RRF.md` |
|
||||
| 多路与 RRF 讨论 | `RAG排序-多路召回与RRF.md` |
|
||||
| 当前架构 | `mvp/architecture/RAG知识检索架构.md` |
|
||||
| OpenSpec 归档 | `openspec/changes/archive/2026-07-28-rag-quality-score-unify/` |
|
||||
| 主规格 | `openspec/specs/rag-retrieval-quality-score/spec.md` |
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**日期**:2026-07-28
|
||||
**状态**:审计字段 live 验收记录
|
||||
**关联主文档**:[一次诊断到底发生了什么(SUCCESS 全流程)](一次诊断全流程-E2E导读.md)
|
||||
**关联主文档**:[一次诊断到底发生了什么(SUCCESS 全流程)](../diagnosis/一次诊断全流程-E2E导读.md)
|
||||
|
||||
> 主文档只保留 **SUCCESS 完整诊断** 与 **现行审计能力说明**。
|
||||
> 本页单独记录:改造后的一次 live 验收——**审计字段 PASS**,业务因 Milvus 空结果走了 **FALLBACK**。
|
||||
@@ -8,7 +8,7 @@
|
||||
- 目录:`eval/rag-retrieval/`
|
||||
- 脚本:`scripts/eval_rag_retrieval.py`、`generate_rag_lookup_snapshots.ps1`、`prepare_rag_eval_seed.ps1`
|
||||
- OpenSpec / devflow:`rag-eval-hybrid-baseline`(已归档)
|
||||
- 前置:`docs/RAG-Hybrid质量分与后处理.md`、`docs/RAG-Agent如何读relevance_level.md`
|
||||
- 前置:`RAG-Hybrid质量分与后处理.md`、`RAG-Agent如何读relevance_level.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -576,8 +576,8 @@ python scripts\eval_rag_retrieval.py
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| `eval/rag-retrieval/README.md` | 操作说明(以仓库为准) |
|
||||
| `docs/RAG-Hybrid质量分与后处理.md` | quality / 后处理 |
|
||||
| `docs/RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
|
||||
| `docs/RAG排序-多路召回与RRF.md` | 多路与 RRF |
|
||||
| `RAG-Hybrid质量分与后处理.md` | quality / 后处理 |
|
||||
| `RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
|
||||
| `RAG排序-多路召回与RRF.md` | 多路与 RRF |
|
||||
| `devflow/projects/2026-07-28-rag-eval-hybrid-baseline/` | 本 change 档案 |
|
||||
| `openspec/specs/rag-eval-offline-baseline/spec.md` | 主规格 |
|
||||
@@ -1,6 +1,6 @@
|
||||
# MVP Issues 索引
|
||||
|
||||
**更新日期**:2026-07-27
|
||||
**更新日期**:2026-07-28
|
||||
**状态**:按活跃问题、设计笔记、RAG 问题集和已归档问题整理
|
||||
|
||||
## 目录约定
|
||||
@@ -18,6 +18,7 @@
|
||||
|---|---|---|---|---|
|
||||
| ISS-015 | 诊断运行质量与 Reasoning 审计收敛 | 高 | 部分实施 | [active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md](active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md) |
|
||||
| ISS-016 | 诊断 Agent 缺少基于信息增益的停止契约 | 高 | 已实施,待归档 | [active/ISS-016-diagnosis-information-gain-stop-contract.md](active/ISS-016-diagnosis-information-gain-stop-contract.md) |
|
||||
| ISS-017 | RAG L0 过滤收窄与 Fallback 加固 | 中 | 开放,暂缓实施(保持现网) | [active/ISS-017-rag-l0-filter-fallback-hardening.md](active/ISS-017-rag-l0-filter-fallback-hardening.md) |
|
||||
|
||||
## 设计笔记
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# ISS-017 RAG L0 过滤收窄与 Fallback 加固
|
||||
|
||||
**状态**:开放,暂缓实施(保持现网行为)
|
||||
**严重程度**:中
|
||||
**发现时间**:2026-07-28
|
||||
**更新日期**:2026-07-28
|
||||
**来源**:hybrid + qualityScore 收口后的评测/设计讨论;`chat-l0-filter-fallback` golden case
|
||||
**关联**:`LookupKnowledgeTool`、`KnowledgeQueryTransformer`、`KnowledgeEvidencePostProcessor`、`eval/rag-retrieval`、历史 `rag/rag-l0-domain-entity-hint.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前 `lookup_knowledge` 在 L0 命中唯一 domain 时会生成 `categoryFilter`,第一次只在该 category 内检索(`FILTERED_VECTOR`),意图是 **缩小边界、降噪声**。若后处理判定低质,则去掉 filter 用原 query 再搜一次(`UNFILTERED_VECTOR_RETRY`),并记录 `filtered_vector_low_quality` / `filtered_vector_no_evidence`。
|
||||
|
||||
该设计方向正确,但机制偏「硬过滤赌一把 + 失败整页替换」:
|
||||
|
||||
```text
|
||||
唯一 domain → 硬 categoryFilter
|
||||
→ isLowQuality?
|
||||
→ 是:全库 retry,selectedEvidence 直接换成 retry 结果
|
||||
→ 否:采用过滤结果
|
||||
```
|
||||
|
||||
hybrid 与统一 `qualityScore` 上线后,单点 `isLowQuality` 阈值更敏感;中间评测快照曾出现「该 retry 未 retry、decoy 占 top」。随后 fixture/baseline 已在特定种子下恢复 7/7 全绿,**不代表路径已稳健**,只说明当前快照可过。
|
||||
|
||||
**本 Issue 决策(2026-07-28)**:
|
||||
|
||||
- **先保留原样**,不改生产行为。
|
||||
- 记录改进方向,待 RAG 主干稳定、有明确回归动机时再实施。
|
||||
- **不采用**全面「软约束替代硬过滤」(原讨论方案 D)作为近期目标(复杂度高、与 RRF 权威排序边界纠缠)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户 / 系统体验问题
|
||||
|
||||
1. L0 指错 category 时,诊断可能只看到 decoy 或窄池噪声,正确 runbook 进不了上下文。
|
||||
2. 即使触发 retry,第一次过滤结果整页丢弃,窄路里偶然有用的 chunk 无法保留。
|
||||
3. fallback 是否触发高度依赖单一质量闸门,阈值或分数语义一变,行为抖动,golden case 脆弱。
|
||||
4. 审计上 `selectedAttempt` 二元切换,难以表达「窄路 + 宽路共同贡献」。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现状与验证基线
|
||||
|
||||
### 3.1 实现位置
|
||||
|
||||
| 组件 | 职责 |
|
||||
|---|---|
|
||||
| `KnowledgeQueryTransformer` | L0 hint;唯一 domain → `categoryFilter` |
|
||||
| `LookupKnowledgeTool` | `FILTERED_VECTOR` → 可选 `UNFILTERED_VECTOR_RETRY`;retry 时覆盖 `selectedEvidence` |
|
||||
| `KnowledgeEvidencePostProcessor#isLowQuality` | 是否触发 fallback 的主闸门 |
|
||||
| `RetrievalTrace` | `selectedAttempt` / `fallbackReason` / attempts |
|
||||
|
||||
### 3.2 评测
|
||||
|
||||
- Golden:`eval/rag-retrieval/cases/golden-cases.json` → `chat-l0-filter-fallback`
|
||||
- 期望:`UNFILTERED_VECTOR_RETRY` + fallbackReason 之一 + top/source = `rag-l0-filter-fallback`
|
||||
- 种子:`rag-l0-filter-fallback`(真文档)、`rag-l0-filter-decoy`(`category: overfilter-decoy`)
|
||||
- 当前 **accepted baseline**(约 2026-07-28T06:55Z)对该 case 为 pass;仓库内更早的 `baseline-diff.*` 可能仍是中间失败快照,**不以陈旧 diff 为现状**。
|
||||
|
||||
### 3.3 非目标(本 Issue 不解决)
|
||||
|
||||
- 重做 hybrid / RRF / qualityScore 统一(已收口)。
|
||||
- EvidenceGuard / Agent 如何用证据写结论。
|
||||
- 全面取消 L0 或永远全库检索。
|
||||
- **方案 D**:弱化 category 硬过滤、改为 domain 软加权全面替代(明确搁置)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 目标(实施时)
|
||||
|
||||
在 **保留「缩小边界」先验** 的前提下,降低 over-filter 脆性:
|
||||
|
||||
1. 只有 **高置信** L0 才使用硬 `categoryFilter`;中/低置信不锁门。
|
||||
2. 一旦触发宽路 rescue,**合并** 窄路与宽路候选后再后处理,禁止无条件整页替换。
|
||||
3. fallback 触发使用 **多信号**(空/不可用、过少候选、低质且实体重叠差等),避免单阈值独裁。
|
||||
4. Trace/golden 能表达 rescue/merge,而不只绑死某一个 attempt 字符串。
|
||||
5. `chat-l0-filter-fallback` 与主路径 case 在改动后仍可回归;允许演进断言语义(见 §6)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐方案(实施优先级)
|
||||
|
||||
### 5.1 方案 A — 置信度分级收窄(优先,低复杂度)
|
||||
|
||||
| L0 置信 | 行为 |
|
||||
|---|---|
|
||||
| 高(唯一 domain + 强关键词/标题等可标定信号) | 硬 `categoryFilter`(保持收窄) |
|
||||
| 中 | 不硬过滤;domain 仅 hint/审计 |
|
||||
| 低/无 | 直接全库 hybrid |
|
||||
|
||||
可选:多信号触发 retry(空结果、candidateCount 过低、低质等)。
|
||||
|
||||
### 5.2 方案 C — Retry 后 merge(优先,低~中复杂度)
|
||||
|
||||
触发 `UNFILTERED_VECTOR_RETRY` 后:
|
||||
|
||||
- 合并两次候选(按 `evidenceKey` dedup)
|
||||
- 再跑统一 `EvidencePostProcessor` / pack
|
||||
- 不要 `selectedEvidence = retryEvidence` 直接覆盖
|
||||
|
||||
Trace 可增加 `rescue_used` / `merged` 类字段,或扩展 `selectedAttempt` 语义。
|
||||
|
||||
### 5.3 方案 B — 两路并行(可选,中复杂度)
|
||||
|
||||
高置信硬过滤时同时跑窄路 + 较小 topK 宽路,merge 后处理。用延迟换稳定;可在 A+C 之后视延迟预算决定。
|
||||
|
||||
### 5.4 方案 D — 全面软约束(搁置)
|
||||
|
||||
不作为本 Issue 实施范围。仅当知识库噪声结构变化、证明硬过滤长期帮倒忙时再单独立项。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收标准(实施阶段)
|
||||
|
||||
- [ ] 默认/高置信路径仍体现「可收窄」;中低置信不再误锁唯一 category。
|
||||
- [ ] Over-filter 构造下,最终 context **包含** 期望文档(如 `rag-l0-filter-fallback`),且存在可审计的 rescue/merge 痕迹。
|
||||
- [ ] Retry/merge 不丢弃窄路中仍有价值的 `evidenceKey`(在宽路未覆盖时)。
|
||||
- [ ] Offline eval:`chat-l0-filter-fallback` 与其它 golden 全绿或仅含已文档化的 intentional diff。
|
||||
- [ ] 断言可演进为「发生过 rescue + 最终命中真文档」,不必永久绑定旧 attempt 枚举字面量。
|
||||
- [ ] 普通 SUCCESS 诊断路径无回归(时延、top 证据、token 量级可接受)。
|
||||
- [ ] **不**重新引入 keyword boost 颠覆 RRF/qualityScore 主序。
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施阶段建议(未开工)
|
||||
|
||||
| 阶段 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| 0 | 本 Issue 记录;**保持现网原样** | 已完成 |
|
||||
| 1 | 方案 A:高置信才硬过滤 + 触发条件澄清 | 未开始 |
|
||||
| 2 | 方案 C:retry merge + Trace 字段 | 未开始 |
|
||||
| 3 | 更新 golden/fixture/baseline;可选方案 B | 未开始 |
|
||||
| — | 方案 D | 明确不做(本 Issue) |
|
||||
|
||||
每阶段独立 focused tests + offline eval;不与 ISS-015 Repair Schema 捆绑。
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与约束
|
||||
|
||||
- 放宽硬过滤可能增加噪声进入 pack → 需靠 topK/pack 预算与现有 quality 闸门兜住。
|
||||
- Merge 可能略增后处理成本;双路并行增加一次检索延迟。
|
||||
- 改 attempt 语义会动审计/文档/eval,需同步 `RagLookupAuditEnricher` 与 MVP 检索文档。
|
||||
- 在未实施前,运维上依赖:eval 种子 category 正确、decoy 隔离、baseline 以最新 accepted 报告为准。
|
||||
|
||||
---
|
||||
|
||||
## 9. 决议摘要
|
||||
|
||||
| 项 | 决议 |
|
||||
|---|---|
|
||||
| 现网行为 | **保持原样** |
|
||||
| 近期实施 | **否**(开放跟踪) |
|
||||
| 首选方向 | **A + C** |
|
||||
| 可选后续 | B |
|
||||
| 不做 | D(全面软约束) |
|
||||
| 严重程度 | 中(路径敏感,非主链路阻断) |
|
||||
@@ -3,7 +3,7 @@
|
||||
## Context
|
||||
|
||||
- Modular RAG pipeline already exists (`KnowledgeQueryTransformer` → retriever → post-processor → packer → assembler → projector).
|
||||
- Delivery baseline: `docs/Milvus-Hybrid接入清单.md` §1.1 Delivery 1.
|
||||
- Delivery baseline: `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1 Delivery 1.
|
||||
- Historical decision (modular RAG): L0 is hint-only; L1 is fact evidence. This change keeps that boundary.
|
||||
- Legacy Milvus SDK search path will be abandoned later; this change must not thicken SDK-specific logic.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
As a result, L1 can recall multiple useful chunks from one document, but Agent often sees only one. This blocks multi-path / hybrid retrieval benefits and hurts long runbook completeness.
|
||||
|
||||
This change is **Delivery 1** from `docs/Milvus-Hybrid接入清单.md`. Hybrid schema/search is **out of scope** and will be a separate change after this is archived.
|
||||
This change is **Delivery 1** from `mvp/engineering/rag/Milvus-Hybrid接入清单.md`. Hybrid schema/search is **out of scope** and will be a separate change after this is archived.
|
||||
|
||||
## What Changes
|
||||
|
||||
@@ -47,7 +47,7 @@ This change is **Delivery 1** from `docs/Milvus-Hybrid接入清单.md`. Hybrid s
|
||||
- Code: retrieval DTO/services, post-processor, lookup tool config, projector, tests
|
||||
- Agent-visible: more evidence items possible for same logical document when multiple chunks are relevant
|
||||
- Interface level: **L2/L3** — Agent `document_id` semantics become chunk-scoped evidence id (often `docId#chunk-N`); EvidenceGuard still validates against tool projection ids
|
||||
- Docs baseline: `docs/Milvus-Hybrid接入清单.md` §1.1 Delivery 1
|
||||
- Docs baseline: `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1 Delivery 1
|
||||
|
||||
## Risks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user