71 lines
4.3 KiB
Markdown
71 lines
4.3 KiB
Markdown
# Spec-1:Agent 执行环境修复(解析容错 + 步级重试 + 摘要附实体)
|
||
|
||
- **日期**: 2026-07-11
|
||
- **状态**: 已批准(用户确认,步级重试退避改为 20s/40s)
|
||
- **依据**: `research-wiki/findings/2026-07-11-benchmark-failure-taxonomy.md` §四(T8,5 题)与 §二 M1(view_node 摘要吞 entities)
|
||
- **系列**: Spec-1/2/3 三件套之一,见 [2026-07-11-batch-tree-build-design.md]、[2026-07-11-question-gen-v2-design.md]
|
||
|
||
## 1. 问题
|
||
|
||
| # | 缺陷 | 证据 | 影响 |
|
||
|---|------|------|------|
|
||
| A1 | `_parse_response`(`core/agent/loop.py:265-300`)只接受 `action.args` 嵌套结构;deepseek 稳定输出变体(args 平铺 + ```json 围栏)解析三连拒 → 0 步阵亡 | 637-3、615-3 | 整题报废,且 json_repair 修不了结构错位 |
|
||
| A2 | LLM 调用异常(`loop.py:118-129`)直接整题终止,无步级重试 | 796-3(SSL BAD_RECORD_MAC 废掉 13 步上下文) | 一次网络抖动损失全部已积累推理 |
|
||
| B | `summarize_node`(`app/search/summarizer.py`)两轮按题摘要后 entities/visible_text 不保证幸存 | 786-2、872-3、750-1(Agent 站在证据节点上漏读实体) | M1 负证据幻觉的恶化因素 |
|
||
|
||
## 2. 设计
|
||
|
||
### A1 解析容错(结构归一化层)
|
||
|
||
在现有 `repair_json → json.loads → 校验` 之后、返回 None 之前,增加确定性归一化:
|
||
|
||
1. **围栏剥除**:repair_json 前先剥 ```json / ``` 围栏(正则,幂等)。
|
||
2. **args 收拢**:若 `action` 为 dict 且含 `tool` 但缺 `args`,把 action 下除 `tool` 外的所有平铺键收拢为 `args` 嵌套。
|
||
3. 归一化成功 → 照常执行;失败 → 走现有 retry 追问路径(行为不变)。
|
||
|
||
纯函数实现,用 637-3/615-3 的真实坏输出作单测样本。
|
||
|
||
### A2 步级重试
|
||
|
||
`_call_llm` 异常处理改为步级重试循环:
|
||
|
||
| 参数 | 值 | 说明 |
|
||
|------|-----|------|
|
||
| 重试次数 | 2 | 第 3 次失败才整题终止(stop_reason=error 不变) |
|
||
| 退避 | 20s / 40s | 用户指定 |
|
||
| 可重试异常 | 显式类型元组:`ssl.SSLError`、`TimeoutError`、`ConnectionError`、`OSError`、openai SDK 传输类异常(`APIConnectionError`/`APITimeoutError`) | 遵循 CLAUDE.md P5(不做全 Exception 兜底);`asyncio.CancelledError` 绝不吞;其余未知异常 fail-fast 整题终止(现状行为) |
|
||
| 上下文 | 原样保留 | messages 不回滚,重试即重发 |
|
||
| 遥测 | 失败尝试的 error 记录由 `GovernedLLMClient` 内部负责(已有);AgentLoop 侧只以 loguru 记录步级重试事件(不注入 TelemetryRecorder) | AgentLoop 无遥测端口,不越层补写 |
|
||
|
||
### B 摘要附带实体原文
|
||
|
||
在 **dispatcher 侧**(`SearchToolDispatcher._handle_view_node`)由确定性代码(非 LLM)在摘要结果末尾追加节点 card 的字段原文:
|
||
|
||
```
|
||
[实体] <entities + visible_entities 原文>
|
||
[画面文字] <visible_text 原文>
|
||
```
|
||
|
||
- **调用链改动**(Codex 审查修正):`summarize_node` 只收 `raw_text: str`、无结构化 card 访问,且 `_node_full_text` 递归收值不保留字段名——因此在 `TreeEnvironment` 新增结构化字段提取方法(如 `node_entity_fields(node_id) -> dict[str, str]`),dispatcher 调用它并把区块拼接到 summarize_node 返回值之后;summarize_node 本体不改
|
||
- 摘要后追加 → LLM 无法吞掉;字段为空则不加对应区块
|
||
- 对 anchor / 非 anchor 两种模式一致生效
|
||
|
||
## 3. 不做什么(YAGNI)
|
||
|
||
- 不改 GovernedLLMClient 的内部重试栈(已有四层治理)
|
||
- 不改判分协议、不动 prompt 版本化内容
|
||
- 不做异常分型重试策略(统一兜底已覆盖已知案例)
|
||
|
||
## 4. 验证
|
||
|
||
1. 单测:坏输出样本(围栏/平铺/两者叠加)归一化正确;空 content、缺 tool 仍拒
|
||
2. 单测:步级重试计数与退避(mock LLM 抛错)
|
||
3. 单测:summarize_node 追加区块(有/无实体字段两种节点)
|
||
4. 集成:抽 10 道 T2/T8 错题重跑(637-3、615-3、786-2、872-3、750-1 必含),对比修复前后
|
||
5. `make test` 全绿 + 覆盖率不降
|
||
|
||
## 5. 被否方案
|
||
|
||
- **prompt 层要求 LLM 修正输出格式**:治标,deepseek 变体是稳定行为,代码归一化是确定性修复
|
||
- **重试时区分异常类型**(仅网络类重试):已知案例全是穿透型异常,分型收益低且易漏
|