# 寻找 Agent v2 当前实现流程

本文档描述 `find_agent_v2` 当前代码的真实执行路径、LLM 与宿主程序的职责边界、工具权限、
数据表和状态流转。文档基于 2026-08-12 的实现。

## 1. 总体架构

v2 使用“Python 外层业务循环 + 单轮真实 LangGraph + 节点内 LangChain ReAct”的结构。业务数据全部写入
`find_agent_v2_*` 新表，和旧寻找 Agent 的运行表、工具和日志隔离。
V2 表不使用任何数据库外键，表间 ID 仅作为应用层逻辑引用。

```mermaid
flowchart TD
    A["创建 v2 run<br/>status=running"] --> B["启动 Obagent run"]
    B --> C["读取轮次开始快照"]
    C --> D["创建 round<br/>phase=planning"]

    subgraph G["单轮受控自主图"]
        D --> P["Supervisor<br/>提议 next_action / worker_count / evidence_scope"]
        P --> Q["Policy Guard<br/>依据真实 DB 状态与预算审核"]
        Q --> S["Search<br/>搜索并写入候选"]
        Q --> E["Evidence Workers<br/>按建议范围补证"]
        Q --> V["Evaluator Workers<br/>评分并申请分池"]
        S --> P
        E --> P
        V --> P
        Q --> X["结束本轮"]
    end

    X --> T{"valid primary ≥ 目标数?"}
    T -- 是 --> F["finalize: goal_met"]
    T -- 否 --> N{"仍有 pending?"}
    N -- 是 --> Z["finalize: failed"]
    N -- 否 --> I{"本轮有新增候选且未达最大轮数?"}
    I -- 是 --> C
    I -- 否 --> O["finalize: partial 或 no_match"]
    F --> RP["Report 只读生成报告"]
    O --> RP
    Z --> END["结束，不执行 Report"]
    RP --> END
```

默认参数：

- 最大业务轮数：`2`
- 目标有效 Primary：`5`
- 默认模型：`google/gemini-3-flash-preview`
- 节点温度：`0.2`

## 2. 调用入口

```mermaid
sequenceDiagram
    participant Caller as 调用方
    participant Runner as runner.py
    participant Service as FindAgentV2Service
    participant Agent as FindAgentV2
    participant Ob as Obagent

    Caller->>Runner: create_find_agent_v2_run(...)
    Runner->>Service: create_run(...)
    Service-->>Caller: run_id
    Caller->>Runner: run_find_agent_v2(user_input, run_id)
    Runner->>Agent: arun(run_id, user_input)
    Agent->>Service: require_run(status=running)
    Agent->>Ob: observe.run(project=find_agent_v2)
    Agent->>Service: 保存 obagent_run_uid
    Agent->>Agent: 执行业务轮次和单轮 DAG
    Agent->>Service: finalize(...)
    Agent->>Ob: finish(final_output)
    Agent-->>Caller: FindAgentResult
```

已有 run 也可以通过 `run_prepared_find_agent_v2(run_id)` 执行。此入口从
`find_agent_v2_run.input_json` 读取创建时保存的 `user_input`。

终态 run 使用 `resume=True` 时不会删除历史 round、候选或证据，而是从 `current_round + 1`
继续，创建新的 Obagent run，并把本次 Token/费用累加到历史运行用量。

## 3. 单轮节点和工具权限

每个节点都会通过 LangChain `create_agent` 创建新的 ReAct Agent，只注册本节点允许的工具。
节点之间通过数据库状态和轻量内存状态衔接，不共享一个长期 ReAct 会话。模型调用接入重试
middleware，工具调用也接入独立重试；可通过 `FIND_AGENT_V2_FALLBACK_MODEL` 配置备用模型。

| 节点 | LLM 职责 | 可用工具 | 上下文范围 | 业务迭代提示值 |
|---|---|---|---|---:|
| Supervisor | 提议下一动作、搜索计划、补证范围和 Worker 数 | 无 | 全量 run 状态 | 2 |
| Search | 执行搜索计划，不补证、不评分 | `search_videos_v2`、`query_find_agent_v2_state`、`delegate_agents_v2` | 全量状态 | 10 |
| Evidence | 为候选补视频详情和双侧年龄画像 | `fetch_candidate_details_v2`、`fetch_candidate_portraits_v2`、`query_pending_candidates_v2`、`delegate_agents_v2` | 仅 pending 候选 | 12 |
| Evaluator | 给出 R/E/S/V，申请 `primary` 或 `rejected` | `evaluate_candidates_v2`、`query_pending_candidates_v2`、`delegate_agents_v2` | 仅 pending 候选 | 每次 12 |
| Report | 汇总最终结果 | `query_find_agent_v2_state` | 全量最终状态 | 4 |

每个业务动作执行后都会回到 Supervisor。默认单轮最多 16 个业务动作、最多 3 次搜索动作；
Worker 并发建议会被限制为 1~8。Evaluator 连续 3 次没有减少 pending 时判定技术失败。
当动作预算耗尽，框架会强制补证或评估以安全收敛，仍无进展才失败。

LangChain `create_agent` 的图超步预算与上表业务迭代提示值分离，当前为
`max(64, 业务迭代提示值 × 6)`；模型节点、工具节点和 middleware 重试都会消耗超步。

Search、Evidence、Evaluator 还可以通过 `delegate_agents_v2` 将最多 8 个相互独立的任务并发
委派给同阶段 Worker。Worker 拥有与父节点相同的业务工具，但不能继续委派，避免递归失控；每路
使用独立模型上下文，并在 Obagent 中通过 `branch_key` 展示。

## 4. 搜索与证据链路

```mermaid
flowchart LR
    SP["Supervisor 搜索计划"] --> ST["search_videos_v2"]
    ST -->|provider=tikhub| TK["TikHub 搜索接口"]
    ST -->|其他 provider| IK["内部关键词搜索接口"]
    TK --> SR["find_agent_v2_search"]
    IK --> SR
    SR --> C["find_agent_v2_candidate<br/>pending_evaluation"]

    C --> DT["fetch_candidate_details_v2<br/>每批最多 8 条"]
    C --> PT["fetch_candidate_portraits_v2<br/>每批最多 8 条"]
    DT --> DE["详情接口"]
    PT --> CP["内容画像接口"]
    PT --> AP["账号画像接口"]
    DE --> C
    CP --> NM["v2 年龄画像归一化"]
    AP --> NM
    NM --> C
    DE --> EV["find_agent_v2_evidence"]
    NM --> EV
```

证据阶段写入的主要字段：

- 详情：发布时间、时长、播放、点赞、评论、收藏、分享、作者和链接。
- 画像：内容侧与账号侧的原始画像，以及归一化后的
  `content_50_plus_ratio`、`account_50_plus_ratio`。
- 状态：`detail_status`、`portrait_status`，取值通常为 `pending/success/failed`。

LLM 在 Evaluator 阶段拿到归一化后的真实值和证据状态，不只拿门禁 reason code。原始接口响应
保存在候选 JSON 字段和 `find_agent_v2_evidence.raw_json` 中。

## 5. 评估、确定性门禁和分池

LLM 给每个候选提供：

- `relevance_score`（R）
- `elder_score`（E）
- `share_score`（S）
- `value_score`（V）
- 申请的 `decision_bucket`：`primary` 或 `rejected`
- 决策说明和可选淘汰原因

LLM 的 `primary` 只是申请。`FindAgentV2Service.evaluate()` 会在写库前调用 v2 自有的
`evaluate_candidate_gate()`，由程序强制执行门禁。

```mermaid
flowchart TD
    L["LLM 输出 R/E/S/V<br/>并申请 primary/rejected"] --> G["确定性门禁"]
    G --> TM["时效检查"]
    G --> DU["时长检查<br/>默认 ≥ 30 秒"]
    G --> SH["分享量检查<br/>默认 ≥ 1000"]
    G --> PO["50+画像检查<br/>内容侧或账号侧默认 ≥ 20%"]
    TM --> ALL{"全部门禁通过?"}
    DU --> ALL
    SH --> ALL
    PO --> ALL
    ALL -- 否 --> RJ["强制 rejected<br/>gate_status=fail"]
    ALL -- 是且LLM申请primary --> PR["primary<br/>gate_status=pass"]
    ALL -- 是但LLM申请rejected --> RJ2["rejected<br/>gate_status=pass"]
```

最终通过视频的查询条件是：

```sql
SELECT *
FROM find_agent_v2_candidate
WHERE run_id = :run_id
  AND decision_bucket = 'primary'
  AND gate_status = 'pass';
```

### 当前补偿规则

当前门禁不是“证据必须全部成功”的严格模式。强评分候选可以对部分缺失事实进行补偿：

- V ≥ 0.65，或 R/E/S 分别达到 0.70/0.70/0.65，视为强候选。
- 时长缺失可由强评分或高分享量补偿。
- 分享量缺失可由强评分、高点赞或高播放补偿。
- 内容侧和账号侧画像都缺失时，可由强评分补偿。

因此当前实现中可能出现：

```text
decision_bucket = primary
gate_status = pass
detail_status = pending
portrait_status = pending
```

这属于现行规则允许的结果，并非状态写入错误。如果业务要求“详情、画像都成功才可通过”，需要
额外增加 `detail_status=success` 和 `portrait_status=success` 的硬门禁，并取消相应补偿。

常见门禁原因码包括：

| 原因码 | 含义 |
|---|---|
| `TEMPORAL_UNKNOWN` | 发布时间缺失且未满足补偿 |
| `RELATIVE_DATE_EXPIRED` | 标题含相对日期，但内容已非当天 |
| `EVENT_EXPIRED` | 事件型内容超出时效 |
| `SEASONAL_EXPIRED` | 季节型内容超出时效 |
| `DURATION_UNKNOWN` | 时长缺失且未满足补偿 |
| `DURATION_TOO_SHORT` | 时长低于阈值 |
| `SHARE_COUNT_UNKNOWN` | 分享量缺失且未满足补偿 |
| `SHARE_COUNT_TOO_LOW` | 分享量低于阈值 |
| `CONTENT_PORTRAIT_MISSING` | 内容侧和账号侧都没有画像，且未满足补偿 |
| `PORTRAIT_50_PLUS_TOO_LOW` | 至少一侧有画像，但内容侧和账号侧 50+ 比例均未达阈值 |

## 6. 轮次停止与运行终态

每轮完成后，宿主按以下顺序判断：

```mermaid
flowchart TD
    A["单轮结束"] --> B{"valid_primary_count ≥ target?"}
    B -- 是 --> G["goal_met / finished"]
    B -- 否 --> C{"pending_count > 0?"}
    C -- 是 --> F["failed / failed"]
    C -- 否 --> D{"candidate_count 有增长?"}
    D -- 否 --> S["停止扩展"]
    D -- 是 --> E{"达到 max_rounds?"}
    E -- 否 --> NR["进入下一轮"]
    E -- 是 --> S
    S --> H{"有效 primary 数量"}
    H -- 大于 0 --> P["partial / finished"]
    H -- 等于 0 --> N["no_match / finished"]
```

技术异常统一进入 `failed/failed`。非技术失败的终态会继续运行只读 Report 节点；Report 失败只
记录到内存 `state.failures`，不会反向改变已完成的业务终态。

`finalize()` 使用与外层 Agent 相同的 `target_primary_count` 判断 `goal_met`，默认值为 5。

## 7. 数据表和状态归属

```mermaid
erDiagram
    FIND_AGENT_V2_RUN ||--o{ FIND_AGENT_V2_ROUND : has
    FIND_AGENT_V2_RUN ||--o{ FIND_AGENT_V2_SEARCH : has
    FIND_AGENT_V2_RUN ||--o{ FIND_AGENT_V2_CANDIDATE : has
    FIND_AGENT_V2_SEARCH ||--o{ FIND_AGENT_V2_CANDIDATE : first_discovers
    FIND_AGENT_V2_CANDIDATE ||--o{ FIND_AGENT_V2_EVIDENCE : has

    FIND_AGENT_V2_RUN {
        string run_id
        string status
        string outcome_status
        int current_round
        int valid_primary_count
        int total_tokens
        decimal cost_usd
        string obagent_run_uid
    }
    FIND_AGENT_V2_ROUND {
        string run_id
        int round_index
        string phase
        string status
    }
    FIND_AGENT_V2_SEARCH {
        string run_id
        string keyword
        string provider
        string status
    }
    FIND_AGENT_V2_CANDIDATE {
        string aweme_id
        string detail_status
        string portrait_status
        string decision_bucket
        string gate_status
        string reject_reason_code
    }
    FIND_AGENT_V2_EVIDENCE {
        bigint candidate_id
        string evidence_type
        string provider
        string status
    }
```

状态归属：

| 层级 | 字段 | 主要取值 |
|---|---|---|
| Run 技术状态 | `find_agent_v2_run.status` | `running`、`finished`、`failed` |
| Run 业务结果 | `outcome_status` | `goal_met`、`partial`、`no_match`、`failed` |
| Round 阶段 | `find_agent_v2_round.phase` | `planning`、`searching`、`evidence`、`evaluating`、`done` |
| Round 状态 | `find_agent_v2_round.status` | `open`、`done`、`failed` |
| 候选证据状态 | `detail_status/portrait_status` | `pending`、`success`、`failed` |
| 候选分池 | `decision_bucket` | `pending_evaluation`、`primary`、`rejected` |
| 候选门禁 | `gate_status` | `pass`、`fail`，未评估时为 `NULL` |

## 8. Obagent 可视化映射

v2 只接入新的 Obagent 可视化，不使用旧项目 JSONL、OSS 或本地日志可视化。

```mermaid
flowchart TD
    R["observe.run<br/>project=find_agent_v2<br/>agent=find_agent_v2"]
    R --> G1["graph · 第 1 轮"]
    R --> G2["graph · 第 2 轮（如需要）"]
    G1 --> P1["supervisor（可重复进入）"]
    G1 --> S1["search"]
    G1 --> E1["evidence"]
    G1 --> V1["evaluator"]
    S1 --> SW["search worker 扇出（可选）"]
    E1 --> EW["evidence worker 扇出（可选）"]
    V1 --> VW["evaluator worker/批次扇出"]
    G2 --> P2["supervisor/search/evidence/evaluator"]
    R --> RP["report"]
```

每个节点记录：

- 稳定的输入槽：当前轮次、原始任务、数据库快照、本轮搜索计划。
- 实际 system prompt、模型和工具列表。
- 完整 LLM 消息链、usage、工具参数、工具返回、异常和迭代次数。
- 节点最终输出。

`find_agent_v2_run.obagent_run_uid` 保存可视化 run UID，控制台地址格式为：

```text
{OBAGENT_ENDPOINT}/#client_uid={obagent_run_uid}
```

## 9. 代码索引

| 文件 | 职责 |
|---|---|
| `runner.py` | 创建 run，提供同步/异步/已准备任务入口 |
| `agent.py` | 外层业务轮次、停止条件、终态和 Report |
| `graph.py` | 编译真实 StateGraph、条件边和 Evaluator 消费保护 |
| `runtime.py` | LangChain create_agent、重试/fallback、usage 和受控委派 |
| `prompts.py` | 节点提示词和公共业务规则 |
| `tools.py` | v2 工具定义和节点 allowlist |
| `providers.py` | 搜索、详情、画像接口与响应归一化 |
| `gates.py` | v2 确定性门禁和规则快照 |
| `service.py` | v2 表事务、快照、评估和 finalize |
| `models.py` | 五张独立 ORM 表 |
| `observability.py` | Obagent run、graph、node 和 ReAct 轨迹 |
| `state.py` | 轻量流程状态和返回结构 |
