|
@@ -0,0 +1,333 @@
|
|
|
|
|
+# 寻找 Agent v2 当前实现流程
|
|
|
|
|
+
|
|
|
|
|
+本文档描述 `find_agent_v2` 当前代码的真实执行路径、LLM 与宿主程序的职责边界、工具权限、
|
|
|
|
|
+数据表和状态流转。文档基于 2026-08-12 的实现。
|
|
|
|
|
+
|
|
|
|
|
+## 1. 总体架构
|
|
|
|
|
+
|
|
|
|
|
+v2 使用“Python 外层业务循环 + 单轮固定 DAG + 节点内 ReAct”的结构。业务数据全部写入
|
|
|
|
|
+`find_agent_v2_*` 新表,和旧寻找 Agent 的运行表、工具和日志隔离。
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+flowchart TD
|
|
|
|
|
+ A["创建 v2 run<br/>status=running"] --> B["启动 Obagent run"]
|
|
|
|
|
+ B --> C["读取轮次开始快照"]
|
|
|
|
|
+ C --> D["创建 round<br/>phase=planning"]
|
|
|
|
|
+
|
|
|
|
|
+ subgraph G["单轮固定 DAG"]
|
|
|
|
|
+ D --> P["Planner<br/>生成搜索计划"]
|
|
|
|
|
+ P --> S["Search<br/>搜索并写入候选"]
|
|
|
|
|
+ S --> Q{"存在 pending_evaluation?"}
|
|
|
|
|
+ Q -- 否 --> X["结束本轮"]
|
|
|
|
|
+ Q -- 是 --> E["Evidence<br/>补详情和画像"]
|
|
|
|
|
+ E --> V["Evaluator<br/>评分并申请分池"]
|
|
|
|
|
+ V --> R{"pending 已清零?"}
|
|
|
|
|
+ R -- 否 --> V
|
|
|
|
|
+ R -- 是 --> 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`。
|
|
|
|
|
+
|
|
|
|
|
+## 3. 单轮节点和工具权限
|
|
|
|
|
+
|
|
|
|
|
+每个节点都会创建一个新的 `supply_agent.Agent` 实例,只注册本节点允许的工具,并移除通用
|
|
|
|
|
+`load_skill` 工具。节点之间通过数据库状态和轻量内存状态衔接,不共享一个长期 ReAct 会话。
|
|
|
|
|
+
|
|
|
|
|
+| 节点 | LLM 职责 | 可用工具 | 上下文范围 | 最大 ReAct 轮数 |
|
|
|
|
|
+|---|---|---|---|---:|
|
|
|
|
|
+| Planner | 生成最多 3 个互补搜索方向 | 无 | 全量 run 状态 | 2 |
|
|
|
|
|
+| Search | 执行搜索计划,不补证、不评分 | `search_videos_v2`、`query_find_agent_v2_state` | 全量状态 | 10 |
|
|
|
|
|
+| Evidence | 为候选补视频详情和双侧年龄画像 | `fetch_candidate_details_v2`、`fetch_candidate_portraits_v2`、`query_pending_candidates_v2` | 仅 pending 候选 | 12 |
|
|
|
|
|
+| Evaluator | 给出 R/E/S/V,申请 `primary` 或 `rejected` | `evaluate_candidates_v2`、`query_pending_candidates_v2` | 仅 pending 候选 | 每次 12 |
|
|
|
|
|
+| Report | 汇总最终结果 | `query_find_agent_v2_state` | 全量最终状态 | 4 |
|
|
|
|
|
+
|
|
|
|
|
+Evaluator 支持分批消费:宿主最多重新进入 Evaluator 64 次,直到 pending 清零。如果连续 3 次
|
|
|
|
|
+调用后 pending 数量都没有下降,则判定为技术失败,防止模型空响应造成无限循环。
|
|
|
|
|
+
|
|
|
|
|
+## 4. 搜索与证据链路
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+flowchart LR
|
|
|
|
|
+ SP["Planner 搜索计划"] --> 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()` 当前以固定数量 `5` 判断 `goal_met`,而外层 Agent 的
|
|
|
|
|
+`target_primary_count` 可以在构造时配置。默认值一致;如果将目标数配置为其他值,两处判断可能
|
|
|
|
|
+产生不一致,后续应统一为同一个规则快照字段。
|
|
|
|
|
+
|
|
|
|
|
+## 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
|
|
|
|
|
+ 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["planner"]
|
|
|
|
|
+ G1 --> S1["search"]
|
|
|
|
|
+ G1 --> E1["evidence"]
|
|
|
|
|
+ G1 --> V1["evaluator"]
|
|
|
|
|
+ G2 --> P2["planner/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` | 单轮固定 DAG 和 Evaluator 消费保护 |
|
|
|
|
|
+| `runtime.py` | 每节点新建 ReAct Agent,执行物理工具隔离 |
|
|
|
|
|
+| `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` | 轻量流程状态和返回结构 |
|
|
|
|
|
+
|