---
id: main-inference-entry
title: main.py 推理入口 + 初始 Prompt 集设计
type: design
status: approved
created: 2026-07-09
---

# main.py 推理入口 + 初始 Prompt 集设计

## 1. 目标

完成第一版 900 道题推理基线，具体交付：

| 交付物 | 说明 |
|--------|------|
| `main.py` | CLI Composition Root，本次仅实现 infer 模式 |
| `app/harness/deps_router.py` | 按 video_id 懒加载 InferenceDeps 的路由器 |
| `app/ports.py` 扩展 | 新增 `ToolDispatchFactory` / `PromptBuilderFactory` Protocol |
| `app/harness/runner.py` 小改 | `__init__` 注入 factory，替换 noop 占位 |
| `store/skills/v1/` | 13 个 skill（从 TRM4 v1 精简 + 注入 TRM5 card 字段） |
| `store/prompts/v1/` | 目录重组（从 `store/prompts/` 扁平结构迁入） |
| 配置变更 | concurrency=24, max_steps=40, breaker_threshold=48 |

## 2. 架构

### 2.1 依赖流（Clean Architecture Composition Root）

```
main.py (Composition Root — 最外层)
  │
  ├─ 构建 adapters ──────────────────────────────┐
  │   CircuitBreaker                             │
  │   RedisResponseCache (可选，降级为 None)       │ adapters/
  │   SQLiteTelemetryRecorder                    │
  │   GovernedLLMClient (search)                 │
  │   GovernedLLMClient (evolve, 本次传同一实例)   │
  │   GovernedVLMClient                          │
  │   LocalEmbeddingProvider                     │
  │   MonkeyOCRClient (可选)                      │
  │                                              │
  ├─ 构建 InferenceDepsRouter ───────────────────┤
  │   接收: store_dir, embed, llm, vlm, ocr      │ app/harness/deps_router.py
  │   复用: factory.build_inference_deps()        │
  │                                              │
  ├─ 构建 Runner ────────────────────────────────┤
  │   注入: config, llm, evolve_llm, vlm,        │ app/harness/runner.py
  │         telemetry,                           │
  │         tool_dispatch_factory,               │
  │         prompt_builder_factory               │
  │                                              │
  └─ asyncio.run(runner.infer()) ────────────────┘
```

依赖只向内：`main.py` → `adapters/` + `app/` → `core/`。

### 2.2 InferenceDepsRouter

**位置**：`app/harness/deps_router.py`

**职责**：将 Runner 的全局统一 dispatch/prompt_builder 接口路由到 per-video 的 `InferenceDeps`。

```
prompt_builder(qa)          ← 先调用，注册 qid→vid 映射
    ↓
AgentLoop.run()
    ↓
dispatch(tool, args, ctx)   ← 后调用，通过 ctx["session_id"] 查映射
```

核心伪代码：

```python
class InferenceDepsRouter:
    _deps_cache: dict[tuple, InferenceDeps]   # (vid, skills_dir, prompts_dir) → deps
    _qid_to_vid: dict[str, str]               # question_id → video_id

    def create_dispatch(self, *, skills_dir=None):
        async def _dispatch(tool_name, args, *, context):
            vid = self._qid_to_vid[context["session_id"]]
            deps = self._ensure_deps(vid, skills_dir, ...)
            return await deps.tool_dispatch_fn(tool_name, args, context=context)
        return _dispatch

    def create_prompt_builder(self, *, skills_dir=None, prompts_dir=None):
        def _builder(qa):
            self._qid_to_vid[qa.question_id] = qa.video_id  # 注册映射
            deps = self._ensure_deps(qa.video_id, skills_dir, prompts_dir)
            return deps.prompt_builder(qa)
        return _builder

    def _ensure_deps(self, video_id, skills_dir, prompts_dir):
        key = (video_id, str(skills_dir or ""), str(prompts_dir or ""))
        if key not in self._deps_cache:
            self._deps_cache[key] = build_inference_deps(...)
        return self._deps_cache[key]
```

**时序保证**：`run_inference()` 中每道题先调 `prompt_builder(qa)` 构建 prompt，再启动 AgentLoop（调 dispatch）。映射注册总先于使用。

### 2.3 Runner 改动

`__init__` 新增 2 个可选参数，按 mode 做 fail-fast 校验：

```python
def __init__(self, config, *, llm, evolve_llm, vlm, telemetry,
             tool_dispatch_factory=None,
             prompt_builder_factory=None):
    # mode in {"infer", "eval", "train"} 时 factory 为 None → 立即 ValueError
    if config.mode in {"infer", "eval", "train"}:
        if tool_dispatch_factory is None or prompt_builder_factory is None:
            raise ValueError(
                f"mode={config.mode} 需要 tool_dispatch_factory 和 prompt_builder_factory"
            )
```

`_make_tool_dispatch_fn` / `_make_prompt_builder` 优先用注入值。测试场景可传 mock factory。

### 2.4 Protocol 定义

`app/ports.py` 新增精确类型的 Protocol：

```python
class ToolDispatchFn(Protocol):
    """工具调度函数签名。"""
    async def __call__(
        self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any]
    ) -> str: ...

class ToolDispatchFactory(Protocol):
    """per-version 工具调度工厂。"""
    def __call__(self, *, skills_dir: Path | None = None) -> ToolDispatchFn: ...

class PromptBuilderFn(Protocol):
    """Prompt 构建函数签名。"""
    def __call__(self, qa: GeneratedQuestion) -> tuple[str, str]: ...

class PromptBuilderFactory(Protocol):
    """per-version prompt 构建工厂。"""
    def __call__(self, *, skills_dir: Path | None = None,
                 prompts_dir: Path | None = None) -> PromptBuilderFn: ...
```

### 2.5 InferenceDepsRouter dispatch 防御

Router 的 dispatch 函数对缺失/未知 `session_id` 显式 raise 带诊断信息的 `KeyError`：

```python
async def _dispatch(tool_name, args, *, context):
    session_id = context.get("session_id")
    if not session_id or session_id not in self._qid_to_vid:
        raise KeyError(
            f"未注册的 session_id={session_id!r}，"
            f"已注册 {len(self._qid_to_vid)} 条映射"
        )
    ...
```

### 2.6 main.py 结构

使用 argparse（复用 `RunConfig` + `load_config()` 的 YAML/CLI 三层合并逻辑）。

适配器参数通过 `InfraSettings(BaseSettings)` 从 `.env` 加载（遵循 CLAUDE.md §4.5 pydantic-settings 规范），禁止 main.py 直接散读环境变量：

```python
class InfraSettings(BaseSettings):
    """工程配置（少变/敏感），从 .env 加载。"""
    search_llm_model: str
    search_llm_base_url: str
    search_llm_api_key: str
    vl_llm_model: str
    # ... 其余 LLM/VLM/Redis/OCR/timeout/breaker 字段
    model_config = SettingsConfigDict(env_file=".env")

def _build_adapters(settings: InfraSettings) -> _Adapters:
    # 从 settings 注入构建全套 adapters
    ...

def _log_result(result):   # 输出推理结果摘要
def main():                # Composition Root：load_config → settings → adapters → router → runner
```

本次只实现 `mode == "infer"` 分支，其余模式 `raise SystemExit("尚未实现")`。

> **Supersede 声明**：本设计的 `store/prompts/v1/` 版本化目录结构替代了 `2026-07-07-search-module-design.md` 中 `store/prompts/` 扁平结构的约定。`PromptManager(prompts_dir)` 今后总接收具体版本目录（`store/prompts/v1` 或 workspace 内 `prompts/vN`），不再接收 `store/prompts`。

## 3. 初始 Prompt 集

### 3.1 设计原则

```
进化能学到的 → v1 留框架、去细节（给进化留空间）
进化学不到的 → v1 用人类版（否则永远缺失）
```

依据 TRM4 进化历史（v1→v40）：skills 进化 39 轮（主力），system prompt 仅 1 轮，tool prompts 未变。

### 3.2 三类 Prompt 处理

| 类别 | 是否进化 | v1 来源 | 处理 |
|------|---------|---------|------|
| system.md | 微量 | TRM5 现有版本 | 保留（已含 card 字段说明，是基础设施） |
| skills/v1/ (13个) | 主力 | TRM4 v1 精简 + TRM5 注入 | 见 §3.3 |
| extract/verify (8个) | 不变 | TRM5 现有版本 | 保留（已适配 card 结构） |

### 3.3 Skills v1 内容策略

每个 skill 文件的改造规则：

| 内容层次 | 进化能学到？ | v1 处理 |
|----------|------------|---------|
| YAML frontmatter（task_type） | 否 | 保留原版 |
| Step 标题 + 一句话基本意图 | 否 | 保留原版 |
| reflect/plan/action JSON schema | 否 | 保留原版 |
| **TRM5 card 字段索引** | **否** | **新增**（注入各层字段名及适用场景） |
| 数据驱动统计 | 是 | 移除 |
| 步骤间精确转换条件 | 是 | 移除 |
| 详细操作性规则 | 是 | 移除 |
| 大多数自检信号 | 是 | 移除（保留最基本 1 条） |
| 特定失败模式陷阱 | 是 | 移除（保留 1-2 条通用警告） |

**card 字段索引表**（注入每个 skill）：

| 层级 | 字段 | 适用场景 |
|------|------|---------|
| L1 | scene_summary | 整体概况 |
| L1 | key_entities | 查找人物/物体 |
| L1 | main_actions | 主要动作 |
| L1 | temporal_flow | 时间线概览 |
| L1 | topic_keywords | 主题定位 |
| L2 | event_description | 事件因果 |
| L2 | entities / actions | 实体和动作细节 |
| L2 | state_changes | 状态转变 |
| L2 | spatial_relations | 空间关系变化 |
| L3 | frame_summary | 精确视觉证据 |
| L3 | visible_entities | 具体物体确认 |
| L3 | ongoing_actions | 正在发生的动作 |
| L3 | spatial_layout | 精确空间位置 |
| L3 | visual_attributes | 光照、色调、机位 |
| 全层 | visible_text | 画面文字（OCR） |
| 全层 | subtitle | 字幕转写 |

### 3.4 store/ 目录重组

```
store/
  prompts/
    v1/                          ← 新建（从 store/prompts/*.md 移入）
      system.md
      observe_frame_extract.md
      observe_frame_verify.md
      search_similar_extract.md
      search_similar_verify.md
      view_node_extract.md
      view_node_verify.md
      view_node_children_extract.md
      view_node_children_verify.md
  skills/
    v1/                          ← 新建（13 个文件）
      default-strategy.md
      action-reasoning.md
      action-recognition.md
      attribute-perception.md
      counting-problem.md
      information-synopsis.md
      object-reasoning.md
      object-recognition.md
      ocr-problems.md
      spatial-perception.md
      spatial-reasoning.md
      temporal-perception.md
      temporal-reasoning.md
```

## 4. 配置变更

| 参数 | 原值 | 新值 | 位置 |
|------|------|------|------|
| concurrency | 12 | 24 | config/default.yaml |
| max_steps | 15 | 40 | config/default.yaml |
| breaker_threshold | 5 | 48 | .env |

熔断器行为：所有 24 并发 agent 共享同一 CircuitBreaker 实例，`record_success()` 归零计数。48 = 连续 2 波全失败才触发开路，间歇抖动不会误触。

## 5. 拒绝的方案

| 方案 | 拒绝原因 |
|------|---------|
| Runner 子类覆盖 `_make_*` | Template Method 反模式，违反依赖反转 |
| 绕过 Runner 直接调 `run_inference` | 不复用 workspace 管理、日志、报告逻辑 |
| 照搬 TRM4 v1 skills | 进化空间不足，且缺少 TRM5 card 字段知识 |
| 极简骨架 skills | 基线质量过低，可能不收敛 |
| typer CLI | 需重写 `RunConfig` 的 YAML/CLI 合并逻辑，ROI 不值 |
