docs(design): 补充 summarizer 模块 — view_node/search_similar 的 LLM 两轮摘要

自审发现 TRM4 三个搜索工具全有 question-conditioned 中间摘要层,
原设计遗漏了 summarizer.py。新增:
- §4.4 summarizer API(summarize_node / _children / _nodes_batch)
- anchor 锚模式完整保留
- 6 个额外 prompt 文件
- 更新交互流程图、错误处理、测试策略
This commit is contained in:
2026-07-07 05:22:55 -04:00
parent 11107f5758
commit 5c89ca543a
@@ -7,7 +7,7 @@ date: 2026-07-07
# 搜索 Agent 装配层设计(app/search/
**日期** 2026-07-07 · **状态** 已批准 · **关联** TRM4 `core/search/` + `core/tree/tools.py` + `core/tree/vision.py`
**日期** 2026-07-07 · **状态** 已批准 · **关联** TRM4 `core/search/` + `core/tree/tools.py` + `core/tree/vision.py` + `core/tree/summarizer.py`
---
@@ -23,6 +23,7 @@ date: 2026-07-07
| `core/search/skills.py` | `app/search/skills.py` | 保真迁移 |
| `core/tree/tools.py` | `app/search/tools.py` | 重组为 `SearchToolDispatcher` 类 |
| `core/tree/vision.py` | `app/search/vision.py` | 异步化 + Protocol 注入 |
| `core/tree/summarizer.py` | `app/search/summarizer.py` | 异步化 + Protocol 注入;含 anchor 锚模式 |
| `core/tree/ocr.py` | `adapters/ocr.py` | 异步化 + OCRProvider Protocol |
---
@@ -31,21 +32,28 @@ date: 2026-07-07
```
app/search/
├── __init__.py # 公开 API 重导出
├── prompt.py # PromptManager — prompt 加载与拼装
├── skills.py # SkillRegistry + discover_skills — skill 扫描与注册
├── tools.py # SearchToolDispatcher(实现 ToolDispatcher Protocol
── vision.py # observe_frameVLM 两轮 + OCR 注入
├── __init__.py # 公开 API 重导出
├── prompt.py # PromptManager — prompt 加载与拼装
├── skills.py # SkillRegistry + discover_skills — skill 扫描与注册
├── tools.py # SearchToolDispatcher(实现 ToolDispatcher Protocol
── summarizer.py # question-conditioned 两轮 LLM 摘要(view_node / search_similar 用
└── vision.py # observe_frameVLM 两轮 + OCR 注入)
adapters/
└── ocr.py # MonkeyOCRClient(实现 OCRProvider Protocol
└── ocr.py # MonkeyOCRClient(实现 OCRProvider Protocol
core/protocols.py # 新增 OCRProvider Protocol
core/protocols.py # 新增 OCRProvider Protocol
store/prompts/ # 初始种子(从 TRM4 v2 直接复制,不修改)
store/prompts/ # 初始种子(从 TRM4 v2 直接复制,不修改)
├── system.md
├── observe_frame_extract.md
── observe_frame_verify.md
── observe_frame_verify.md
├── view_node_extract.md
├── view_node_verify.md
├── view_node_children_extract.md
├── view_node_children_verify.md
├── search_similar_extract.md
└── search_similar_verify.md
```
---
@@ -67,6 +75,7 @@ flowchart TB
PROMPT["prompt.py\nPromptManager"]
SKILLS["skills.py\nSkillRegistry"]
TOOLS["tools.py\nSearchToolDispatcher"]
SUMM["summarizer.py\nsummarize_node / _children / _batch"]
VISION["vision.py\nobserve_frame"]
end
@@ -79,9 +88,12 @@ flowchart TB
TOOLS --> ENV
TOOLS --> SKILLS
TOOLS --> VISION
TOOLS --> SUMM
SUMM -->|依赖| PROTO_LLM["core/protocols.py\nLLMProvider"]
VISION -->|依赖| PROTO
PROMPT --> SKILLS
PROMPT -.->|读取| STORE["store/prompts/*.md"]
SUMM -.->|读取| STORE
```
依赖只向内或同层,`core/` 不认识 `app/search/`
@@ -142,6 +154,7 @@ class SearchToolDispatcher:
def __init__(
self,
env: TreeEnvironment,
tool_llm: LLMProvider,
vlm: VLMProvider,
ocr: OCRProvider | None,
prompts_dir: Path,
@@ -149,6 +162,7 @@ class SearchToolDispatcher:
*,
embed_fn: Callable[[str | list[str]], np.ndarray],
verify_vision: bool = True,
anchor: bool = False,
stats_sink: Callable[[dict[str, int]], None] | None = None,
) -> None: ...
@@ -159,8 +173,8 @@ class SearchToolDispatcher:
| 工具 | 实现路径 |
|------|---------|
| `view_node` | → `env.view_node(node_id)` |
| `search_similar` | → `env.search_similar(query, top_k, embed_fn=...)` + 格式化 |
| `view_node` | → `env.view_node(node_id)` 获取原始文本 → `summarizer.summarize_node(...)` 两轮 LLM 摘要 + `summarizer.summarize_children(...)` 子节点标注 |
| `search_similar` | → `env.search_similar(query, top_k, embed_fn=...)``summarizer.summarize_nodes_batch(...)` 并发两轮 LLM 摘要 + 格式化 |
| `observe_frame` | → `env.resolve_frame_paths(...)` + `vision.observe_frame(...)` |
| `submit_answer` | → 返回确认文本 |
| `read_skill` | → `skills.read(name)` |
@@ -169,9 +183,60 @@ class SearchToolDispatcher:
**与 TRM4 有意变更**
- 自由函数 + 大量位置参数 → 类封装(构造时注入依赖)
- 工具描述 `get_tool_descriptions()` 移入此文件
- `search_similar` 结果格式化由 dispatcher 负责(env 返回 `list[tuple[str, float]]`
- LLM 摘要从 environment 拆出到 `summarizer.py`environment 回归纯数据层
- `SearchToolDispatcher.__init__` 新增 `tool_llm: LLMProvider` 参数(工具级 LLMthinking=False,用于 summarizer
### 4.4 observe_framevision.py
### 4.4 summarizersummarizer.py
从 TRM4 `core/tree/summarizer.py` 迁移。三个工具(view_node / search_similar / observe_frame)共享同构的"提取→验证"两轮模式。summarizer 负责前两个工具的文本摘要,vision 负责第三个的视觉摘要。
```python
async def summarize_node(
llm: LLMProvider,
raw_text: str,
question: str,
prompts_dir: Path,
*,
anchor_map: dict[str, str] | None = None,
assemble_mode: str = "ids_expand",
stats_sink: Callable | None = None,
) -> str: ...
async def summarize_children(
llm: LLMProvider,
children_info: list[dict[str, Any]],
question: str,
prompts_dir: Path,
) -> str: ...
async def summarize_nodes_batch(
llm: LLMProvider,
items: list[tuple[str, str, str]],
question: str,
prompts_dir: Path,
) -> list[tuple[str, str]]: ...
```
| 函数 | Prompt 文件 | 输出格式 |
|------|-------------|---------|
| `summarize_node` | `view_node_extract.md` + `view_node_verify.md` | `"[内容摘要] ...\n[核实] ..."` |
| `summarize_children` | `view_node_children_extract.md` + `view_node_children_verify.md` | `"★★/★ 标注\n[核实] ..."` |
| `summarize_nodes_batch` | `search_similar_extract.md` + `search_similar_verify.md` | `[("node_id", "[内容摘要] ..."), ...]` |
**anchor 锚模式**`check_anchors` / `assemble_anchored_output`)保真迁移:给原始文本每行编号(`[c1]` `[s1]`),LLM 摘要引用行号,代码端校验合法性并展开引文。当前生产 `anchor=False`,但代码路径完整保留供后续 A/B 实验。
**与 TRM4 有意变更**
| 项目 | TRM4 | TRM5 |
|------|------|------|
| 归属 | `core/tree/summarizer.py`(嵌入 environment | `app/search/summarizer.py`(独立模块) |
| 异步 | `_call_llm` 同步 | `await llm.chat()` |
| LLM 接口 | 裸 LLMClient | LLMProvider Protocol |
| 并发 | `ThreadPoolExecutor` | `asyncio.gather`(搜索结果批量摘要) |
| Prompt 内容 | store/prompts/v2/ | 原封不动复制 |
### 4.5 observe_framevision.py
(原 §4.4,编号因插入 summarizer 顺移)
```python
async def observe_frame(
@@ -204,7 +269,7 @@ async def observe_frame(
| OCR 接口 | `Callable[[list[Path]], str]` | `OCRProvider` Protocolasync |
| Prompt 内容 | store/prompts/v2/ | 原封不动复制 |
### 4.5 OCRProvider Protocolcore/protocols.py 新增)
### 4.6 OCRProvider Protocolcore/protocols.py 新增)
```python
@runtime_checkable
@@ -213,7 +278,7 @@ class OCRProvider(Protocol):
async def transcribe_frames(self, frame_paths: list[Path]) -> str: ...
```
### 4.6 MonkeyOCRClientadapters/ocr.py
### 4.7 MonkeyOCRClientadapters/ocr.py
```python
class MonkeyOCRClient:
@@ -237,7 +302,9 @@ sequenceDiagram
participant AL as AgentLoop
participant TD as SearchToolDispatcher
participant ENV as TreeEnvironment
participant S as summarizer
participant V as vision.observe_frame
participant LLM as LLMProvider(tool)
participant VLM as VLMProvider
participant OCR as OCRProvider
@@ -247,24 +314,39 @@ sequenceDiagram
PM-->>H: system_prompt
H->>PM: format_user_prompt(question, options, l1_ids)
PM-->>H: user_prompt
H->>TD: 构造(env, vlm, ocr, prompts_dir, registry, embed_fn)
H->>TD: 构造(env, tool_llm, vlm, ocr, prompts_dir, registry, embed_fn)
H->>AL: run(system_prompt, user_prompt, tool_dispatcher)
loop AgentLoop 每步推理
AL->>TD: dispatch("view_node", args, context)
AL->>TD: dispatch("view_node", {node_id, question}, context)
TD->>ENV: view_node(node_id)
ENV-->>TD: 节点文本
TD-->>AL: 输出
ENV-->>TD: 原始 card 文本 + 子节点列表
TD->>S: summarize_node(llm, raw_text, question, ...)
S->>LLM: extract 轮
LLM-->>S: raw_summary
S->>LLM: verify 轮
LLM-->>S: verify_result
S-->>TD: "[内容摘要] ...\n[核实] ..."
TD->>S: summarize_children(llm, children_info, question, ...)
S-->>TD: "★★/★ 标注\n[核实] ..."
TD-->>AL: 完整输出
AL->>TD: dispatch("observe_frame", args, context)
AL->>TD: dispatch("search_similar", {query, question, k}, context)
TD->>ENV: search_similar(query, top_k, embed_fn)
ENV-->>TD: [(node_id, score), ...]
TD->>S: summarize_nodes_batch(llm, items, question, ...)
S-->>TD: 并发两轮摘要结果
TD-->>AL: 格式化输出
AL->>TD: dispatch("observe_frame", {node_ids, question}, context)
TD->>ENV: resolve_frame_paths(node_ids)
ENV-->>TD: list[Path]
TD->>V: observe_frame(vlm, paths, question, ...)
V->>OCR: transcribe_frames(paths)
OCR-->>V: ocr_text
V->>VLM: chat_with_images(extract prompt)
V->>VLM: extract 轮(图片+OCR+问题)
VLM-->>V: raw_evidence
V->>VLM: chat_with_images(verify prompt)
V->>VLM: verify 轮(图片+证据)
VLM-->>V: verify_result
V-->>TD: "[视觉观察] ...\n[验证] ..."
TD-->>AL: 输出
@@ -282,6 +364,9 @@ sequenceDiagram
| 场景 | 处理 | 与 TRM4 一致性 |
|------|------|---------------|
| 节点不存在 | env 抛 KeyErrordispatcher 捕获返回错误文本 | 一致 |
| summarize_node 提取轮失败 | 捕获 Exception,返回 `[摘要错误]` | 一致 |
| summarize_node 验证轮失败 | 降级返回 `[核实] 跳过(调用失败)` | 一致 |
| summarize_children 提取轮失败 | 降级回退原始子节点列表 | 一致 |
| 帧文件不存在 | FileNotFoundErrorvision 返回 `[VL错误]` | 一致 |
| VLM 提取轮失败 | 捕获 Exception,返回 `[VL错误]` | 一致 |
| VLM 验证轮失败 | 降级返回 `[验证] 跳过(调用失败)` | 一致 |
@@ -299,9 +384,10 @@ sequenceDiagram
|----------|------|------|
| `test_search_prompt.py` | PromptManager 加载/拼装/格式化 | 临时目录写 prompt 文件 |
| `test_search_skills.py` | frontmatter 解析、discover_skills 分类 | 临时目录写 .md |
| `test_search_tools.py` | SearchToolDispatcher 5 个工具分发 | 假 env/VLM/OCR 通过 Protocol 注入 |
| `test_search_tools.py` | SearchToolDispatcher 5 个工具分发 + 摘要集成 | 假 env/LLM/VLM/OCR 通过 Protocol 注入 |
| `test_search_summarizer.py` | summarize_node(含 anchor 模式)、summarize_children、summarize_nodes_batch | 假 LLMProvidercheck_anchors / assemble 用真实输入 |
| `test_search_vision.py` | observe_frame 两轮、OCR 注入/降级、stats | 假 VLMProvider + 假 OCRProvider |
| `test_ocr_adapter.py` | MonkeyOCRClient 健康检查/轮询/降级 | responses 库 mock HTTP |
| `test_ocr_adapter.py` | MonkeyOCRClient 健康检查/轮询/降级 | `responses` 库 mock HTTP |
---