---
type: plan
node_id: plan:2026-07-07-search-module
title: "app/search/ 搜索 Agent 装配层实现计划"
date: 2026-07-07
---

# app/search/ 搜索 Agent 装配层实现计划

> **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 完整迁移 TRM4 搜索 Agent 装配层到 TRM5 app/search/，包含 prompt 管理、skill 注册、工具分发、LLM 两轮摘要、VLM 视觉观察和 OCR 支持。

**Architecture:** 方案 A 平铺模块（6 个文件 + 1 个 adapter + 1 个 Protocol）。所有 LLM/VLM 调用通过 Protocol 注入，environment 保持纯数据层。详见 `research-wiki/designs/2026-07-07-search-module-design.md`。

**Tech Stack:** Python 3.11, asyncio, pluggy, loguru, requests, numpy, pytest

**核心算法保真声明：** 本计划不涉及 ARCHITECTURE.md §6 核心算法保真清单中的 13 项关键算法迁移。

---

## 文件结构总览

| 操作 | 文件 | 职责 |
|------|------|------|
| Create | `app/search/__init__.py` | 公开 API 重导出 |
| Create | `app/search/skills.py` | SkillRegistry + discover_skills |
| Create | `app/search/summarizer.py` | 两轮 LLM 摘要 + anchor 锚模式 |
| Create | `app/search/vision.py` | observe_frame（VLM 两轮 + OCR） |
| Create | `app/search/tools.py` | SearchToolDispatcher + 工具描述 |
| Create | `app/search/prompt.py` | PromptManager |
| Create | `adapters/ocr.py` | MonkeyOCRClient |
| Modify | `app/ports.py` | 新增 OCRProvider Protocol |
| Modify | `app/tree/environment.py` | 新增 get_node_text / get_children_info |
| Copy | `store/prompts/*.md` × 9 | 从 TRM4 v2 字节级复制 |

---

### Task 1: 复制 Prompt 种子文件

**Files:**
- Copy: `store/prompts/` (9 files from TRM4 `store/prompts/v2/`)

- [ ] **Step 1: 复制全部 prompt 文件**

```bash
mkdir -p store/prompts
for f in system.md observe_frame_extract.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; do
  cp /home/iomgaa/Projects/Video-Tree-TRM4/store/prompts/v2/$f store/prompts/$f
done
```

- [ ] **Step 2: 字节级校验**

```bash
for f in system.md observe_frame_extract.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; do
  diff /home/iomgaa/Projects/Video-Tree-TRM4/store/prompts/v2/$f store/prompts/$f
done
```

Expected: 无输出（全部一致）

- [ ] **Step 3: Commit**

```bash
git add store/prompts/ && git commit -m "chore: 复制 TRM4 v2 prompt 种子文件（9 个，字节级一致）"
```

---

### Task 2: OCRProvider Protocol + MonkeyOCRClient

**Files:**
- Modify: `app/ports.py` — 新增 OCRProvider
- Create: `adapters/ocr.py` — MonkeyOCRClient
- Create: `tests/unit/test_ocr_adapter.py`

- [ ] **Step 1: 写 OCR 测试**

`tests/unit/test_ocr_adapter.py`。测试 Protocol 合规性、单帧转录、失败降级、健康检查、轮询、行去重过滤。使用 `responses` 库 mock HTTP。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_ocr_adapter.py -v
```

Expected: ImportError

- [ ] **Step 3: 实现 OCRProvider Protocol**

`app/ports.py` 新增 `OCRProvider(Protocol)` + `async def transcribe_frames(self, frame_paths: list[Path]) -> str`。

- [ ] **Step 4: 实现 MonkeyOCRClient**

`adapters/ocr.py` 从 TRM4 `core/tree/ocr.py` 迁移。公开方法改 async（`asyncio.to_thread` 包装同步 HTTP）。构造函数 `ValueError` 替代 `assert`。逻辑与 TRM4 完全一致：多端点轮询、线程安全 Session、单帧失败降级、行去重过滤。

- [ ] **Step 5: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_ocr_adapter.py -v
```

Expected: 全部 PASS

- [ ] **Step 6: Commit**

```bash
git add app/ports.py adapters/ocr.py tests/unit/test_ocr_adapter.py
git commit -m "feat(adapters): OCRProvider Protocol + MonkeyOCRClient 异步实现"
```

---

### Task 3: TreeEnvironment 扩展

**Files:**
- Modify: `app/tree/environment.py` — 新增 get_node_text + get_children_info
- Modify: `tests/unit/test_tree_environment.py`

- [ ] **Step 1: 写测试**

追加 `TestGetNodeText`（正常/anchor 模式/不存在节点）和 `TestGetChildrenInfo`（L1 有子节点/L3 空/不存在节点）到现有测试文件。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_tree_environment.py::TestGetNodeText -v
```

Expected: AttributeError

- [ ] **Step 3: 实现**

`get_node_text(node_id, *, anchor=False) -> tuple[str, dict[str, str] | None]`：复用已有 `_node_full_text` / `_node_anchored_text`，anchor 模式解析行号构建 anchor_map。

`get_children_info(node_id) -> list[dict[str, Any]]`：复用 `_get_children` + `_node_description` + `_format_time_range`。

- [ ] **Step 4: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_tree_environment.py -v
```

Expected: 全部 PASS

- [ ] **Step 5: Commit**

```bash
git add app/tree/environment.py tests/unit/test_tree_environment.py
git commit -m "feat(tree): TreeEnvironment.get_node_text + get_children_info 结构化查询"
```

---

### Task 4: app/search/skills.py

**Files:**
- Create: `app/search/skills.py`
- Create: `tests/unit/test_search_skills.py`

- [ ] **Step 1: 写测试**

测试 `parse_frontmatter`（正常/缺结束符/无 frontmatter）、`strip_frontmatter`、`SkillRegistry.read`（正常/未注册 KeyError）、`discover_skills`（always/task_type/catalog 分类 + 空目录）。使用 `tmp_path` 创建临时 .md 文件。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_skills.py -v
```

- [ ] **Step 3: 实现**

从 TRM4 `core/search/skills.py` 保真迁移。逻辑完全一致。

- [ ] **Step 4: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_skills.py -v
```

- [ ] **Step 5: Commit**

```bash
git add app/search/skills.py tests/unit/test_search_skills.py
git commit -m "feat(search): SkillRegistry + discover_skills — skill 扫描与注册"
```

---

### Task 5: app/search/summarizer.py

**Files:**
- Create: `app/search/summarizer.py`
- Create: `tests/unit/test_search_summarizer.py`

- [ ] **Step 1: 写 anchor 工具测试**

测试 `check_anchors`（合法锚保留/非法锚删除/范围展开/声明句不计数）和 `assemble_anchored_output`（ids/ids_expand/expand_only 三种模式 + 封顶逻辑）。纯函数，无需 mock。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_summarizer.py -v
```

- [ ] **Step 3: 实现 anchor 工具**

从 TRM4 `core/tree/summarizer.py` 保真迁移：`_expand_anchor_ids`, `check_anchors`, `_cited_anchor_ids`, `assemble_anchored_output` + 全部正则常量。逻辑完全一致。

- [ ] **Step 4: 运行 anchor 测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_summarizer.py -v
```

- [ ] **Step 5: 写 summarize_* 测试**

测试 `summarize_node`（两轮正常/提取失败/验证失败降级/anchor 模式）、`summarize_children`（正常/失败回退原始列表）、`summarize_nodes_batch`（并发多节点）。使用 FakeLLMProvider mock。

- [ ] **Step 6: 实现 summarize_node / summarize_children / summarize_nodes_batch**

从 TRM4 迁移。有意变更：同步→async；`_call_llm` → `await llm.chat()`（返回 `response.content`）；`ThreadPoolExecutor` → `asyncio.gather`；透传 `session_id` / `parent_call_id`。

- [ ] **Step 7: 运行全部 summarizer 测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_summarizer.py -v
```

- [ ] **Step 8: Commit**

```bash
git add app/search/summarizer.py tests/unit/test_search_summarizer.py
git commit -m "feat(search): summarizer — 两轮 LLM 摘要 + anchor 锚模式"
```

---

### Task 6: app/search/vision.py

**Files:**
- Create: `app/search/vision.py`
- Create: `tests/unit/test_search_vision.py`

- [ ] **Step 1: 写测试**

测试 `observe_frame`：两轮正常、verify=False 仅提取、OCR 注入、OCR 失败降级、OCR 为 None、VLM 提取失败、VLM 验证失败降级、帧文件不存在、stats 键完整性。使用 FakeVLMProvider + FakeOCRProvider mock。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_vision.py -v
```

- [ ] **Step 3: 实现**

从 TRM4 `core/tree/vision.py` 迁移。有意变更：`await vlm.chat_with_images(messages, images)` 替代手动 base64 + 同步 `_call_vl`；images 传 Path 列表；OCR `await ocr.transcribe_frames()`；透传遥测字段。输出格式与 TRM4 完全一致。

- [ ] **Step 4: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_vision.py -v
```

- [ ] **Step 5: Commit**

```bash
git add app/search/vision.py tests/unit/test_search_vision.py
git commit -m "feat(search): vision.observe_frame — VLM 两轮 + OCR 异步实现"
```

---

### Task 7: app/search/tools.py

**Files:**
- Create: `app/search/tools.py`
- Create: `tests/unit/test_search_tools.py`

- [ ] **Step 1: 写测试**

测试 `get_tool_descriptions`（含/不含 read_skill）、`SearchToolDispatcher.dispatch` 五个工具（view_node 调 env+summarizer、search_similar 调 env+summarize_batch、observe_frame 调 env+vision+subtitle 拼接、submit_answer 返回文本、read_skill 调 registry）+ 未知工具 ValueError + 节点不存在错误文本。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_tools.py -v
```

- [ ] **Step 3: 实现**

`get_tool_descriptions()` 工具描述文本与 TRM4 完全一致。`SearchToolDispatcher` 类封装，构造注入全部依赖，`dispatch` 按工具名路由到 `_handle_view_node` / `_handle_search_similar` / `_handle_observe_frame` 私有方法。`ValueError` 直接抛出（未知工具），其他异常捕获返回错误文本。

- [ ] **Step 4: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_tools.py -v
```

- [ ] **Step 5: Commit**

```bash
git add app/search/tools.py tests/unit/test_search_tools.py
git commit -m "feat(search): SearchToolDispatcher — 5 工具分发 + 摘要集成"
```

---

### Task 8: app/search/prompt.py

**Files:**
- Create: `app/search/prompt.py`
- Create: `tests/unit/test_search_prompt.py`

- [ ] **Step 1: 写测试**

测试 `__init__`（加载 system.md / 不存在抛错）、`build_inference_prompt`（auto/manual/none 三种 skill_mode）、`format_user_prompt`（含/不含 task_type）、`load`（正常/不存在）。使用 `tmp_path` 写入 prompt 文件。

- [ ] **Step 2: 运行测试确认失败**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_prompt.py -v
```

- [ ] **Step 3: 实现**

从 TRM4 `core/search/prompt.py` 迁移。有意变更：工具描述从 `app.search.tools.get_tool_descriptions` 获取；`format_user_prompt` 参数显式化（question/options/l1_node_ids/task_type）。

- [ ] **Step 4: 运行测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_prompt.py -v
```

- [ ] **Step 5: Commit**

```bash
git add app/search/prompt.py tests/unit/test_search_prompt.py
git commit -m "feat(search): PromptManager — prompt 加载与拼装"
```

---

### Task 9: app/search/__init__.py + Lint + 全量测试

**Files:**
- Create: `app/search/__init__.py`

- [ ] **Step 1: 创建 __init__.py**

```python
"""搜索 Agent 装配层 — prompt 管理、skill 注册、工具分发、LLM 摘要、视觉观察。"""
from app.search.prompt import PromptManager
from app.search.skills import SkillRegistry, discover_skills
from app.search.tools import SearchToolDispatcher, get_tool_descriptions

__all__ = [
    "PromptManager",
    "SkillRegistry",
    "SearchToolDispatcher",
    "discover_skills",
    "get_tool_descriptions",
]
```

- [ ] **Step 2: Lint**

```bash
conda activate Video-Tree-TRM & ruff check app/search/ adapters/ocr.py --fix && ruff format app/search/ adapters/ocr.py
```

- [ ] **Step 3: 全量 search 测试**

```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_search_prompt.py tests/unit/test_search_skills.py tests/unit/test_search_tools.py tests/unit/test_search_summarizer.py tests/unit/test_search_vision.py tests/unit/test_ocr_adapter.py -v
```

Expected: 全部 PASS

- [ ] **Step 4: 回归测试**

```bash
conda activate Video-Tree-TRM & pytest tests/ -v --tb=short
```

Expected: 全部 PASS

- [ ] **Step 5: Commit**

```bash
git add app/search/__init__.py && git commit -m "feat(search): __init__.py 公开 API + lint 通过"
```

---

### Task 10: 更新 ARCHITECTURE.md

**Files:**
- Modify: `research-wiki/ARCHITECTURE.md`

- [ ] **Step 1: 更新 §2.3 目录树中 app/search/**

替换为实际 6 个文件。

- [ ] **Step 2: 更新 §3.3 ToolDispatcher 实现映射**

`app/search/skills.py SkillRegistry` → `app/search/tools.py SearchToolDispatcher`。

- [ ] **Step 3: 更新 §3.2 OCRProvider 方法签名**

`recognize(image_path)` → `transcribe_frames(frame_paths: list[Path]) -> str`。

- [ ] **Step 4: Commit**

```bash
git add research-wiki/ARCHITECTURE.md && git commit -m "docs: 同步 app/search/ + OCRProvider 到 ARCHITECTURE.md"
```
