refactor(harness): freeze single-agent contracts

This commit is contained in:
zhuyongxin
2026-07-21 17:33:25 +08:00
parent 30d3296043
commit 58c39107c5
47 changed files with 2607 additions and 24 deletions
+56
View File
@@ -0,0 +1,56 @@
结合你在前几轮对话中梳理出的“过度设计”痛点,既然你已经决定回归单 Agent(ReAct)+ 强 Harness 架构,接下来你需要做一次彻底的“架构物理重构”。
以下是为你量身定制的5步落地行动指南,按优先级从高到低执行:
第一步:链路合并,砍掉“伪 Graph”节点
目标:把被拆散的推理逻辑还给单一的 ReAct 循环。
删除节点:直接在 Graph/状态机中抹掉 Planner、Executor、Composer 以及串联它们的边。
合并为单一 Agent:创建一个 DiagnosisAgent。在它的 System Prompt 中明确:“你需要自行规划排查路径,调用工具获取证据,并在证据充分后输出最终的结构化诊断报告。”
保留机制:该 Agent 内部运行一个带最大步数限制的 While 循环(如 max_iterations=10),防止死循环。
第二步:实施“上下文卸载”与“工具清洗”(解决7次调用膨胀问题)
目标:确保单 Agent 在多轮工具调用后,上下文依然干净,从源头切断幻觉。
工具输出清洗(RTK 机制):改造你的所有工具(如 queryLogs, queryDB)。在工具的 Callback 层加代码,将原始的大段返回结果(如 1000 行日志)强制过滤、聚合,只把最核心的 5-10 行 ERROR 或统计指标返回给 Agent。
上下文卸载:如果某些原始数据必须保留,在工具返回时,将全量数据写入本地文件(如 refs/log_001.md),上下文里只注入一行:[发现5个504错误,详见 refs/log_001.md]。
第三步:下沉确定性逻辑,用代码替代 LLM 节点
目标:将你之前用 Gatekeeper 和 VerifiedInput 做的事,降级为零 LLM 调用的代码拦截器。
前置拦截(Pre-Tool Hook):Agent 发起工具调用时,Harness 代码用 JSON Schema 校验参数格式。不合法直接报错打回,不执行工具。
后置断言(Post-Tool Hook):Agent 输出最终诊断报告时,代码层强制校验报告中引用的 evidence_id 和 raw_path 是否真实存在于历史记录或文件系统中。不合法直接拒绝输出,发回重试。
第四步:锁死输出契约
目标:防止 Executor 过度输出和发散。
在 System Prompt 中强制规定 Agent 的中间思考步和最终输出步必须符合严格的 JSON 结构。
例如,中间步必须是 {"thought": "<不超过50字>", "action": "queryLogs", "parameters": {...}}。Harness 代码检查字数,超长直接打回。
第五步:剥离异步验证(保留你最初的“防幻觉”初衷)
目标:在不增加主链路复杂度的前提下,保留交叉验证能力。
主 ReAct Agent 输出报告后,不要在主 Graph 里串行接一个 Verifier 节点。
改为异步触发一个轻量级 LLM(或小模型),只传入“压缩后的证据摘要 + 草稿结论”。让它判断时间线与逻辑是否一致。如果不一致,在最终输出上加“低置信度警告”;如果一致,直接放行。
总结:你的重构后架构全景图
重构后,你的代码结构应该极其清爽,大致如下:
[用户输入]
│
▼
[Diagnosis ReAct Agent] (唯一的 LLM 推理节点,自带规划、执行、总结)
│
├── Tool: queryLogs
│ └── [Harness 代码]: 过滤 INFO,提取 ERROR,写入 refs,返回摘要
├── Tool: queryMetrics
│ └── [Harness 代码]: 聚合统计值,返回 3 行核心指标
│
▼ (Agent 输出最终 JSON 报告)
[Output Schema Linter] (纯代码层,0 LLM)
│
├─ 校验失败 ──> 返回错误给 [Agent] 重新生成
│
▼ (校验通过)
[Async Verifier] (异步轻量 LLM,只做时间线/逻辑一致性校验)
│
▼
[最终输出 / 带警告输出]
现在你应该做的第一件事:打开你的代码,把主链路上除 Diagnosis Agent 以外的所有 LLM 编排节点全部注释掉,然后按照上面的结构,给工具加上 Pre-Tool 和 Post-Tool 的代码拦截器。把精力从“画 Graph”转移到“写工具清洗代码”上。
+4 -1
View File
@@ -1,6 +1,6 @@
# MVP Issues 索引
**更新日期**:2026-07-10
**更新日期**:2026-07-20
**状态**:按活跃问题、设计笔记、RAG 问题集和已归档问题整理
## 目录约定
@@ -18,6 +18,9 @@
|---|---|---|---|---|
| ISS-003 | MVP 设计与实现 Review 收敛 | 高 | 待规划 | [active/ISS-003-mvp-design-implementation-review.md](active/ISS-003-mvp-design-implementation-review.md) |
| ISS-004 | Executor 域级检索水位控制 | 低 | 待规划 | [active/ISS-004-executor-domain-hard-limit.md](active/ISS-004-executor-domain-hard-limit.md) |
| ISS-012 | Executor Token 预算与上下文膨胀 | 高 | 待规划 | [active/ISS-012-executor-token-budget-and-context-growth.md](active/ISS-012-executor-token-budget-and-context-growth.md) |
| ISS-013 | Chat 入口解耦与真正 SSE 收敛 | 高 | 待规划 | [active/ISS-013-chat-entry-decoupling-and-sse.md](active/ISS-013-chat-entry-decoupling-and-sse.md) |
| ISS-014 | 单体 ReAct Agent、Harness 与 ACI 工具瘦身 | 高 | 待阶段 0 冻结 | [active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md](active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md) |
| executor-evidence-attribution-hallucination | Executor 证据归因幻觉 | 高 | 待规划 | [active/executor-evidence-attribution-hallucination.md](active/executor-evidence-attribution-hallucination.md) |
| rag-refactor-plan | RAG 检索重构计划 | 高 | 待规划 | [active/rag-refactor-plan.md](active/rag-refactor-plan.md) |
@@ -0,0 +1,119 @@
# ISS-012 Executor Token 预算与上下文膨胀
**状态**:待规划
**严重程度**:高
**发现时间**:2026-07-20
**关联**:ISS-002、ISS-004、ISS-011
---
## 背景
ISS-011 完成 StateGraph 切换后,复杂 Chat 链路已经具备显式 Node、Gatekeeper、Verifier、Composer 和 Run 级 Trace。但最终 live E2E 暴露出 Executor 的 Token 成本和上下文增长问题:一次只返回安全 Fallback 的请求消耗了超过 11 万 Token。
## 现象
最终验收 Run:
- `runId`:`run-808ac38f-3ad0-4462-a6d0-ed50d8686473`
- 总耗时:`75964ms`
- 最终答案:109 字符,Run 为 `CHAT/SUCCESS`,`degraded=true`
- AgentStep:8 条,其中 Planner 1 次、Executor 7 次
- ToolInvocation:12 次
- Run `total_token_count`:`111802`
Executor 每次模型调用的 Token 逐步上升:
```text
6396 -> 11265 -> 12786 -> 16774 -> 17965 -> 19340 -> 24236
```
工具调用包括 4 次 `lookup_knowledge`、6 次 `query_logs`、1 次 `query_metrics` 和 1 次 `get_available_log_topics`。最终因模型输出缺少 `source_invocation_id`,Gatekeeper 将结果降为 `LOW_CONFID` 并进入安全 Fallback。
## 已确认事实
1. 数据库中 8 条 `agent_step` 均为不同记录,不存在重复插入;`111802` 等于各步骤 `token_count` 的求和。
2. `TokenTrackingChatModel` 当前只保存供应商返回的 `usage.totalTokens`,没有拆分输入、输出、缓存和推理 Token。
3. `ChatService.backfillRunMetrics` 直接累加每条 AgentStep 的 `token_count`。
4. `AgentLoggingHook` 只把模型输入截断为 500 字符写入审计表,无法从当前 Trace 还原模型实际发送的完整 Prompt。
5. Executor 当前没有独立的模型调用次数、工具调用次数或 Token 预算;Graph recursion limit 不能限制 ReactAgent 内部工具循环。
## 初步根因假设
- 每次 Executor 模型调用都会重新携带 Planner 结果、历史消息和之前的工具返回,导致输入上下文随工具循环增长。
- 工具返回内容包含较多日志、知识库结果和检索明细,完整结果被反复带入后续模型请求。
- 工具结果没有稳定返回 `source_invocation_id`,模型无法可靠生成精确证据引用,导致高成本检索后仍然进入 Fallback。
- 当前只能看到 `totalTokens`,尚未确认供应商 usage 中 input/output/cached/reasoning 的精确占比。
## 影响
- 单次诊断成本和延迟不可控,复杂问题可能继续超过模型上下文窗口。
- Token 消耗与最终答案质量不匹配,出现“高成本检索 + 安全降级”的低收益路径。
- 缺少 Token 分项指标,无法建立成本预算、P95 延迟和 degraded rate 门禁。
- Executor 可能重复查询相同或相近的知识域、日志主题和指标。
## 目标
1. 建立按 Run/AgentStep 的 input、output、cached、reasoning Token 可观测性。
2. 为 Executor 增加硬性模型轮数、工具调用和 Token 预算。
3. 将完整工具结果留在 Run Trace/数据库中,模型上下文只接收有界证据投影。
4. 让工具结果直接携带可引用的 `source_invocation_id` 和紧凑 `evidence_refs`。
5. 在预算耗尽时安全结束并明确记录原因,不绕过 Gatekeeper、Verifier 或 Run Trace。
## 建议方案
### 1. Token 统计拆分
- 从 ChatModel usage 中记录 `input_tokens`、`output_tokens`、`cached_tokens`、`reasoning_tokens`(供应商提供时)。
- 保留 `total_token_count` 作为汇总字段,但明确其计算口径。
- 在 `orchestration_trace` 中记录每个 Agent 的累计 Token 和预算命中情况。
### 2. Executor 硬预算
初版建议从以下上限开始,并通过固定 E2E 调整:
- Executor 模型调用最多 4 次。
- 工具调用最多 8 次。
- `lookup_knowledge` 最多 2 次。
- `query_logs` 默认最多返回 5 条日志,并限制单次输出长度。
- 达到预算后停止扩展检索,基于已验真证据输出,或进入带原因的安全 Fallback。
### 3. 有界证据上下文
- 工具完整原始结果继续写入 `tool_invocation`,不直接作为下一轮完整上下文。
- 返回模型的工具视图只保留 invocation ID、工具名、查询条件、有限 evidence refs、excerpt 和 no-evidence 状态。
- 同一工具数组项禁止重复绑定;相同知识域和日志主题不重复查询。
### 4. 证据引用闭环
- 每次 evidence tool 返回结果时直接包含 `source_invocation_id`。
- Executor 输出必须引用该 ID;Gatekeeper 不再依赖事后猜测或唯一候选补全。
- 由于引用失败进入 Fallback 时,Trace 必须记录具体缺失字段和预算消耗。
## 验收标准
- [ ] 每个 AgentStep 可查看 input/output/total Token,供应商支持时可查看 cached/reasoning Token。
- [ ] 固定 `payment-timeout` E2E 的 Token 上限、工具调用上限和最大延迟已定义并通过回归。
- [ ] 连续至少 10 次相同 fixture 运行,Token 和延迟 P95 不超过定义的预算。
- [ ] Executor 预算耗尽时只走安全 Fallback,不绕过 Gatekeeper、Verifier 或 Trace 持久化。
- [ ] 工具返回包含真实 `source_invocation_id`;正常证据链不再因缺少该字段而无谓降级。
- [ ] 12 个 diagnosis eval fixture、Graph workflow/node contract、Trace ownership 回归全部通过。
- [ ] E2E 日志和数据库能按 exact `sessionId + runId` 对齐 Token、工具调用、Fallback 原因和最终状态。
## 非目标
- 不删除 Gatekeeper、Verified Input 或 Verifier。
- 不以降低模型 `maxTokens` 代替上下文治理。
- 不恢复 Sequential/StateGraph 双轨或旧兼容协议。
- 不在本 Issue 中物理删除数据库中的历史 `diagnosis_session` 表。
## 相关文件
- `src/main/java/com/superbiz/agent/hook/TokenTrackingChatModel.java`
- `src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java`
- `src/main/java/com/superbiz/agent/service/ChatService.java`
- `src/main/java/com/superbiz/agent/graph/diagnosis/ReactAgentDiagnosisInvoker.java`
- `src/main/java/com/superbiz/agent/service/ToolInvocationRecorder.java`
- `src/main/resources/prompts/chat-executor-prompt.md`
- `mvp/architecture/stategraph-runtime-architecture.md`
- `devflow/projects/2026-07-17-chat-diagnosis-stategraph-cleanup-docs/evidence.md`
@@ -0,0 +1,81 @@
# ISS-013 Chat 入口解耦与真正 SSE 收敛
**状态**:待规划
**严重程度**:高
**发现时间**:2026-07-20
**关联**:ISS-011、ISS-012
---
## 背景
当前 Chat 入口同时提供 `/api/chat` 和 `/api/chat_stream`。`ChatController` 不仅处理 HTTP/SSE 协议,还直接承担会话创建、历史读取与回写、模型和工具获取、执行策略调用以及异常响应组装,入口职责已经明显超出协议适配层。
现有 `/api/chat_stream` 会等待完整答案生成后再按固定长度切片发送,并不是真正的流式生成。同步和伪流式入口还复制了大部分业务流程,增加了维护成本和行为不一致风险。
## 已确认问题
1. Chat 的会话、历史、模型、工具、执行和结果回写功能耦合在 `ChatController` 中,HTTP 层与应用用例边界不清晰。
2. `/api/chat` 与 `/api/chat_stream` 重复编排同一套 Chat 流程。
3. `/api/chat_stream` 只是对完整答案做事后分块,不具备模型生成过程中的真实增量输出能力。
4. Controller 直接获取 `ChatModel` 和 `ToolCallbackProvider`,将模型基础设施细节暴露到入口层。
5. SSE 使用 Controller 自建的无界缓存线程池,缺少统一生命周期和容量治理。
6. 当前入口错误响应存在 HTTP 状态、外层 `ApiResponse` 与内层 `ChatResponse` 状态不一致的问题。
## 目标
1. 将会话生命周期、历史管理、执行调用和结果回写从 Controller 分离,形成单一 Chat 应用用例入口。
2. 只保留一个 `/api/chat` 接口,并将其协议改为真正的 SSE。
3. SSE 在模型或诊断链路产生内容时增量发送,而不是等待完整答案后再切片。
4. 保留 `sessionId + runId` 作为一次 Chat Run 的稳定关联契约。
5. 在满足入口职责分离的前提下使用最少组件,不引入没有实际职责的接口、工厂或适配层。
## 设计约束
- Controller 只负责请求校验、协议转换和 SSE 生命周期,不负责选择模型、组装工具、管理历史或编排诊断流程。
- 同一次请求只能进入一个应用用例入口,禁止同步和流式路径各自维护一套业务逻辑。
- 真正 SSE 至少需要区分元数据、内容增量、完成和错误事件。
- `sessionId`、`runId` 必须在内容事件之前可获得,并用于日志、数据库和 Trace 对齐。
- 客户端断开、超时和执行失败必须显式终止后台执行并完成 Run 状态记录。
- 不保留旧 `/api/chat_stream` 或同步 `/api/chat` 的兼容分支,直接以新协议为准。
- 优先使用 Spring 管理的执行设施和现有服务能力,不创建无界线程池。
## 建议的最小边界
```text
POST /api/chat (SSE)
-> ChatController:请求与 SSE 协议
-> Chat 应用用例:会话、Run、历史和执行生命周期
-> 现有 Chat 执行能力:简单回答或诊断编排
```
这里的“应用用例”是职责边界,不要求预先拆出多层接口。只有出现独立变化原因或明确复用需求时才增加新组件。
## 验收标准
- [ ] 对外只保留一个 `POST /api/chat`,响应类型为 `text/event-stream`。
- [ ] 删除 `/api/chat_stream` 及同步 Chat 兼容路径。
- [ ] 首个内容事件在完整答案生成完成前发送,禁止通过固定字符切片伪造流式输出。
- [ ] SSE 事件包含稳定的 metadata、content、error、done 契约。
- [ ] Controller 不再直接依赖 `ChatModel`、`ToolCallbackProvider`,也不管理会话历史和 Run 持久化。
- [ ] 同一请求的 `sessionId + runId` 在 SSE、应用日志、`diagnosis_run`、`agent_step` 和 `tool_invocation` 中一致。
- [ ] 客户端断开、超时、模型失败和工具失败都有明确的资源清理与 Run 终态。
- [ ] 不存在 Controller 自建的无界线程池。
- [ ] 单元测试覆盖入口校验和 SSE 事件契约;端到端测试验证真实增量输出、断开清理及 Trace 对齐。
## 非目标
- 不在本 Issue 中重新设计 StateGraph 节点、Gatekeeper、Verifier 或证据协议。
- 不为未来可能出现的其他传输协议预建通用框架。
- 不引入多套 Command、Handler、Adapter、Factory 只为形式上的分层。
- 不保留旧同步接口或 `/api/chat_stream` 的兼容逻辑。
- 不以“完整答案分块发送”作为 SSE 验收通过条件。
## 相关文件
- `src/main/java/com/superbiz/agent/controller/ChatController.java`
- `src/main/java/com/superbiz/agent/service/ChatService.java`
- `src/main/java/com/superbiz/agent/service/session/SessionManager.java`
- `src/test/java/com/superbiz/agent/controller/ChatControllerTest.java`
- `src/test/java/com/superbiz/agent/service/ChatServiceGraphIntegrationTest.java`
- `mvp/architecture/current-mvp-architecture.md`
File diff suppressed because it is too large Load Diff