# find_agent 工具能力与接口 PRD

> 文档版本：v1.0  
> 代码基线：2026-08-03  
> 文档性质：As-Is 工具产品说明与接口契约  
> 适用对象：产品、算法、Agent Prompt、后端、测试与运维人员

## 1. 文档目标

本文说明“寻找视频 Agent”`find_agent` 当前真实可调用的工具，包括：

- 工具清单、定位和推荐调用阶段；
- 每个工具对模型暴露的参数 Schema；
- 参数的业务含义、默认值、枚举、数量限制和组合规则；
- 成功、部分成功和失败时的返回结构；
- 搜索、详情、画像、候选更新和运行结束之间的数据衔接；
- 当前接口限制、风险和验收口径。

本文以运行时 `create_find_agent().tools` 生成的 Schema 和当前实现代码为准，不把仅为旧调用方
保留、但未注册给 Agent 的函数算作可用工具。

## 2. 产品定位

`find_agent` 面向一条已经分级并完成视频点位拓展的需求，使用多个抖音数据来源完成：

1. 搜索候选视频；
2. 保存每次搜索及其候选；
3. 拉取视频详情和受众画像；
4. 判断相关性 `R`、老年受众倾向 `E`、分享价值 `S` 和综合价值 `V`；
5. 将候选放入 `primary` 或 `rejected`；
6. 完成运行并输出 `goal_met`、`partial` 或 `no_match`。

工具服务于模型的 ReAct 决策，但搜索记录、证据、候选门禁和最终运行结果均由代码持久化，
不能只依赖模型最终文本。

## 3. 工具总览

Agent 初始化后共有 13 个可调用工具：1 个框架内置工具和 12 个 `find_agent` 专属工具。

| # | 工具 | 类型 | 主要用途 | 是否写库 |
|---:|---|---|---|---:|
| 1 | `load_skill` | 框架内置 | 按名称加载 Skill 指令 | 否 |
| 2 | `batch_search_and_record` | 批量搜索 | 一次提交最多 6 个关键词任务，每项最多 2 页 | 是 |
| 3 | `douyin_search` | 关键词搜索 | 使用内部搜索源获取一页视频 | 是 |
| 4 | `douyin_search_tikhub` | 关键词搜索 | 使用 TikHub 获取一页视频和完整分页状态 | 是 |
| 5 | `douyin_user_videos` | 作者扩展 | 按作者 `sec_uid` 获取一页最热或最新作品 | 是 |
| 6 | `douyin_detail` | 详情证据 | 批量获取视频详情；新模式自动保存证据并重算门禁 | 新模式是 |
| 7 | `get_content_fans_portrait` | 单条画像 | 获取视频点赞用户画像 | 否 |
| 8 | `get_account_fans_portrait` | 单条画像 | 获取作者粉丝画像 | 否 |
| 9 | `batch_fetch_portraits` | 批量画像 | 批量获取视频/作者画像；新模式自动保存并重算门禁 | 新模式是 |
| 10 | `normalize_age_portraits` | 证据处理 | 将两侧年龄桶标准化为统一证据 | 否 |
| 11 | `batch_update_video_discovery_candidates` | 候选决策 | 按 `candidate_id` 更新评分、理由和最终分池 | 是 |
| 12 | `update_video_discovery_run_status` | 运行控制 | 更新运行状态、意图摘要和停止原因 | 是 |
| 13 | `query_video_discovery_state` | 状态查询 | 读取运行、搜索轨迹和候选快照 | 否 |

以下文件存在，但当前没有注册给 `find_agent`：

- `create_video_discovery_run`：运行由调度器在模型循环前预创建；
- `qwen_video_analyze`：当前不开放视频理解；
- `batch_save_video_candidate_evaluations`、`audit_video_discovery_run` 等旧接口：不在实际工具表中。

## 4. 推荐调用链

```mermaid
flowchart LR
    A["调度器预创建 run_id"] --> B["批量或单页搜索"]
    B --> C["返回 search_id 和 candidate_id"]
    C --> D["douyin_detail 拉详情"]
    D --> E["batch_fetch_portraits 拉双侧画像"]
    E --> F["候选 R/E/S/V 判断"]
    F --> G["batch_update... 写入分池"]
    G --> H["query... 核对最终状态"]
    H --> I["update... status=finished"]
    I --> J["输出业务结果"]
```

推荐优先使用带 `run_id + candidate_ids` 的详情和批量画像新模式。该模式由工具从数据库读取
`aweme_id` 和作者 ID，自动保存原始证据、刷新候选字段并重算门禁，减少模型搬运大 JSON 的错误。

## 5. 全局接口约定

### 5.1 返回值类型

所有工具在 Python 层都返回 `str`。除 `load_skill` 成功时返回 Skill 上下文文本外，其余工具
返回值均为 JSON 字符串，调用方必须先解析 JSON，不能把它当成已经反序列化的对象。

通用成功字段通常包括：

| 字段 | 类型 | 含义 |
|---|---|---|
| `title` | string | 结果标题，主要用于日志和模型阅读 |
| `output` | string | 人类可读摘要，不是最终业务事实的唯一来源 |

通用错误字段：

| 字段 | 类型 | 含义 |
|---|---|---|
| `error` | string | 参数、上游接口、网络或数据库错误文本 |
| `input_error` | boolean，可选 | `true` 表示调用参数错误；缺失不代表成功 |
| `title` | string，可选 | 错误场景标题 |
| `errors` | array，可选 | 批量调用中的逐项错误 |

框架通过返回内容中是否存在错误字段识别工具错误，但不会自动重试、回滚整条 Agent 运行或切换
备用工具。模型必须读取返回结果后决定下一步。

### 5.2 核心 ID

| ID | 来源 | 用途 | 约束 |
|---|---|---|---|
| `run_id` | 调度器预创建 | 贯穿一次需求的视频发现过程 | 所有写库工具必须复用同一个值 |
| `search_id` | 搜索工具返回 | 标识一页搜索记录 | 翻页/标签/作者扩展可作为 `parent_search_id` |
| `candidate_id` | 搜索工具返回 | 标识一次搜索命中的候选记录 | 详情、画像、评分和分池更新的首选主键 |
| `aweme_id` / `content_id` | 抖音内容 | 标识视频本身 | 同一视频被多个搜索命中时可对应多个 `candidate_id` |
| `author.sec_uid` / `account_id` | 搜索或详情结果 | 标识抖音作者 | 作者画像及作者作品扩展使用完整 `sec_uid` |

### 5.3 当前 Schema 能力边界

工具框架可从 Python 类型注解生成顶层 JSON Schema，但当前不生成枚举、取值范围、字符串格式，
也不展开 `list[dict]` 内部对象字段。因此模型可见 Schema 中：

- `searches` 和 `items` 只显示为“对象数组”；
- `source_type`、`status`、排序方式等只显示为普通字符串；
- 单批上限、字段组合关系只存在于工具描述和运行时代码校验中。

本文后续表格补齐这些语义约束，但它们尚未全部成为可被模型供应方提前校验的 JSON Schema。

## 6. 搜索类工具

### 6.1 `batch_search_and_record`

#### 功能

顺序执行多个关键词搜索任务。每完成一页立即新增搜索记录，并为该页每条有效结果新增候选。
适合启动阶段一次性提交 2～3 个语义不同的根搜索，也可用于受控翻页。

#### 顶层参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 预创建的运行 ID，不能为空 |
| `searches` | array<object> | 是 | - | 搜索任务数组，必须非空，最多 6 项 |

#### `searches[]` 对象

| 字段 | 类型 | 必填 | 默认/取值 | 说明 |
|---|---|---:|---|---|
| `keyword` | string | 是 | - | 实际搜索词，不能为空 |
| `query_reason` | string | 是 | - | 为什么执行该搜索，不能为空 |
| `source_type` | string | 是 | `demand` / `seed` / `point` / `tag` / `pagination` / `mixed` | 搜索来源类型 |
| `provider` | string | 否 | `internal_keyword` | 可选 `internal_keyword`、`tikhub` |
| `max_pages` | integer | 否 | `1` | 自动限制到 `1～2`；超范围不会报错而会被截断 |
| `cursor` | string 或 integer | 否 | 内部源为 `"0"`，TikHub 为 `0` | 起始游标 |
| `search_id` | string | 否 | `""` | TikHub 翻页状态，不是数据库 `search_id` |
| `backtrace` | string | 否 | `""` | TikHub 翻页回溯状态 |
| `source_value` | string | 否 | `null` | 来源值，如点位文本、标签词 |
| `parent_search_id` | integer | 否 | `null` | 父搜索页的数据库 ID |
| `content_type` | string | 否 | `视频` | 传给底层搜索源 |
| `sort_type` | string | 否 | `综合排序` | 传给底层搜索源 |
| `publish_time` | string | 否 | `不限` | 发布时间筛选 |
| `filter_duration` | string | 否 | `不限` | 仅 TikHub 使用 |
| `min_duration_seconds` | integer | 否 | `30` | 实际最小值固定不低于 30 秒 |

#### 返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 固定为“批量搜索并自动落库” |
| `run_id` | string | 本次运行 ID |
| `task_count` | integer | 通过顶层校验并进入结果的任务数；不一定等于输入项数 |
| `saved_page_count` | integer | 已成功持久化的搜索页数 |
| `new_candidate_count` | integer | 所有成功页新增候选总数 |
| `error_count` | integer | 任务校验或分页执行错误数 |
| `errors` | array<string> | 带任务下标、关键词和页码的错误摘要 |
| `tasks` | array<object> | 各搜索任务的执行结果 |
| `tasks[].index` | integer | 原始输入数组下标 |
| `tasks[].keyword` | string | 搜索词 |
| `tasks[].provider` | string | 实际搜索源 |
| `tasks[].pages` | array<object> | 各页结果 |
| `pages[].page_no` | integer | 页码 |
| `pages[].results_count` | integer | 该页结果数 |
| `pages[].has_more` | boolean | 是否还有下一页 |
| `pages[].next_cursor` | string 或 integer | 下一页游标 |
| `pages[].search_id` | integer | 数据库搜索记录 ID |
| `pages[].new_candidate_count` | integer | 该页新增候选数 |
| `pages[].candidates` | array<object> | 该页候选摘要，结构见 6.5 |
| `pages[].persisted` | boolean | 是否成功落库 |
| `pages[].error` | string/null | 该页错误 |
| `output` | string | 批量结果摘要 |
| `error` | string，可选 | 所有页均未成功保存时出现 |

执行是串行的，不并发。单个任务某页报错后停止该任务后续翻页，但会继续处理后面的任务；因此
返回结果可能是部分成功。

### 6.2 `douyin_search`

#### 功能

通过内部关键词搜索源获取一页抖音视频，并自动保存搜索页、原始搜索证据和候选记录。

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 运行 ID |
| `keyword` | string | 是 | - | 搜索词 |
| `query_reason` | string | 是 | - | 搜索原因 |
| `source_type` | string | 是 | - | 可选 `demand/seed/point/tag/author/pagination/mixed` |
| `source_value` | string/null | 否 | `null` | 来源原值 |
| `parent_search_id` | integer/null | 否 | `null` | 父搜索记录 ID |
| `page_no` | integer | 否 | `1` | 小于 1 时按 1 保存；大于 1 自动改为 `pagination` |
| `content_type` | string | 否 | `视频` | 上游支持 `视频/图文` |
| `sort_type` | string | 否 | `综合排序` | 上游支持 `综合排序/最新发布/最多点赞` |
| `publish_time` | string | 否 | `不限` | 上游支持 `不限/一天内/一周内/半年内` |
| `cursor` | string | 否 | `"0"` | 翻页游标 |
| `account_id` | string | 否 | `771431222` | 内部搜索接口账号 ID |
| `min_duration_seconds` | integer | 否 | `30` | 客户端短视频过滤阈值，实际不低于 30 秒 |
| `timeout` | number/null | 否 | `null` | 请求超时；`null` 时底层默认 60 秒 |

#### 返回结果

成功后返回“搜索页公共结构”，见 6.5。内部源特有或常用字段还包括：

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | `抖音搜索: {keyword}` |
| `output` | string | 含标题、作者、点赞、评论、分享的文本摘要 |
| `filtered_count` | integer | 因时长不足被过滤的数量 |
| `duration_ms` | integer | 上游调用耗时 |

### 6.3 `douyin_search_tikhub`

#### 功能

使用 TikHub 独立搜索源获取一页抖音视频，提供收藏、播放、话题以及完整分页状态，并自动落库。

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 运行 ID |
| `keyword` | string | 是 | - | 搜索词，不能为空 |
| `query_reason` | string | 是 | - | 搜索原因 |
| `source_type` | string | 是 | - | 可选值同单页内部搜索 |
| `source_value` | string/null | 否 | `null` | 来源原值 |
| `parent_search_id` | integer/null | 否 | `null` | 父搜索记录 ID |
| `page_no` | integer | 否 | `1` | 大于 1 自动记录为 `pagination` |
| `content_type` | string | 否 | `视频` | `不限/视频/图片/图文/文章`，也兼容 `0/1/2/3` |
| `sort_type` | string | 否 | `综合排序` | `综合排序/最多点赞/最新发布`，也兼容 `0/1/2` |
| `publish_time` | string | 否 | `不限` | `不限/一天内/一周内/半年内`，也兼容 `0/1/7/180` |
| `cursor` | integer | 否 | `0` | 首次为 0；翻页原样使用上次 `next_cursor` |
| `filter_duration` | string | 否 | `不限` | `不限/一分钟内/1-5分钟/5分钟以上`，兼容供应商代码 |
| `search_id` | string | 否 | `""` | 供应商分页状态；翻页必须原样回传 |
| `backtrace` | string | 否 | `""` | 供应商回溯状态；翻页必须原样回传 |
| `min_duration_seconds` | integer | 否 | `30` | 客户端二次过滤，实际不低于 30 秒 |
| `timeout` | number/null | 否 | `null` | `null` 时默认 60 秒 |

#### 返回结果

除 6.5 的搜索页公共结构外，还返回：

| 字段 | 类型 | 说明 |
|---|---|---|
| `provider` | string | `tikhub` |
| `request_params` | object | 传给 TikHub 的规范化请求参数 |
| `filtered_count` | integer | 被时长过滤的数量 |
| `provider_search_id` | string，可选 | 供应商返回的分页 `search_id`；避免与数据库 `search_id` 混淆 |
| `backtrace` | string | 下一页回传值 |
| `duration_ms` | integer | 上游调用耗时 |

分页时必须同时复用上一页的 `next_cursor`、`provider_search_id` 和 `backtrace`。数据库
`search_id` 只能用于父子搜索关系，不能作为 TikHub 的供应商分页参数。

### 6.4 `douyin_user_videos`

#### 功能

按作者完整 `sec_uid` 获取一页最热或最新作品，适合从高潜作者扩展更多候选；结果自动落库。

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 运行 ID |
| `account_id` | string | 是 | - | 作者 `sec_uid` |
| `query_reason` | string | 是 | - | 作者扩展原因 |
| `source_value` | string/null | 否 | `null` | 缺省时使用 `account_id` |
| `parent_search_id` | integer/null | 否 | `null` | 触发作者扩展的父搜索页 ID |
| `page_no` | integer | 否 | `1` | 页码 |
| `sort_type` | string | 否 | `最热` | 仅支持 `最热`、`最新` |
| `cursor` | string | 否 | `""` | 作者作品分页游标 |
| `min_duration_seconds` | integer | 否 | `30` | 实际不低于 30 秒 |
| `timeout` | number/null | 否 | `null` | `null` 时默认 60 秒 |

#### 返回结果

使用 6.5 的搜索页公共结构，并保留：

| 字段 | 类型 | 说明 |
|---|---|---|
| `account_id` | string | 作者 `sec_uid` |
| `sort_type` | string | 实际排序方式 |
| `filtered_count` | integer | 被时长过滤的数量 |
| `duration_ms` | integer | 上游调用耗时 |

持久化返回前会移除原始 `user_videos` 和 `search_results` 大数组，只保留数据库生成的候选摘要。

### 6.5 搜索页公共返回结构

三个单页搜索工具成功持久化后共同返回：

| 字段 | 类型 | 说明 |
|---|---|---|
| `run_id` | string | 运行 ID |
| `search_id` | integer | 数据库搜索页 ID |
| `keyword` | string | 本页搜索词；作者作品为 `author:{account_id}` |
| `query_reason` | string | 搜索原因 |
| `source_type` | string | 实际保存的来源类型 |
| `source_value` | string/null | 来源值 |
| `parent_search_id` | integer/null | 父搜索页 ID |
| `provider` | string | `internal_keyword` / `tikhub` / `internal_blogger` |
| `provider_state` | object | 保存的供应商分页状态 |
| `content_type` | string | 内容类型 |
| `sort_type` | string | 排序方式 |
| `publish_time` | string | 发布时间筛选 |
| `cursor` | string | 本页请求游标 |
| `page_no` | integer | 规范化页码 |
| `results_count` | integer | 上游本页结果数 |
| `new_candidate_count` | integer | 本页实际新增候选数 |
| `has_more` | boolean | 是否有下一页 |
| `next_cursor` | string 或 integer/null | 下一页游标 |
| `status` | string | `success` 或 `failed` |
| `error_message` | string/null | 持久化的搜索错误 |
| `raw_evidence_id` | integer/null | 原始搜索证据 ID |
| `candidates` | array<object> | 本页新建候选摘要 |
| `persisted` | boolean | 成功落库时为 `true` |

`candidates[]` 摘要字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `candidate_id` | integer | 后续详情、画像和更新使用的数据库 ID |
| `search_id` | integer | 所属搜索页 |
| `aweme_id` | string | 抖音视频 ID |
| `title` | string/null | 标题或描述 |
| `author_name` | string/null | 作者名 |
| `author_sec_uid` | string/null | 作者完整 `sec_uid` |
| `publish_at` | string/null | 发布时间 |
| `duration_seconds` | number/null | 视频时长，秒 |
| `share_count` | integer/null | 分享数 |
| `gate_status` | string | 初始通常为 `pending` |

相同 `aweme_id` 被不同搜索命中会创建不同 `candidate_id`，不会按视频 ID 合并覆盖。

## 7. 详情工具

### 7.1 `douyin_detail`

#### 功能

批量获取视频详情、真实播放链接、作者、互动、话题和时间信息。单次最多处理 8 个去重后的视频。

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `content_ids` | array<string>/null | 否 | `null` | 旧模式视频 ID 列表；新流程不推荐 |
| `timeout` | number/null | 否 | `null` | 单请求超时，`null` 时默认 60 秒 |
| `run_id` | string/null | 条件必填 | `null` | 使用 `candidate_ids` 时必填 |
| `candidate_ids` | array<integer>/null | 条件必填 | `null` | 新模式候选 ID，必须属于同一 `run_id` |

组合规则：

- 有 `candidate_ids` 时忽略 `content_ids`，从数据库读取 `aweme_id`；
- `candidate_ids` 模式缺少 `run_id` 返回 `input_error=true`；
- ID 不存在或不属于本次运行时整次输入失败；
- 去重后为空会报错；超过 8 条会报错；
- 新模式会追加保存每条成功或失败的原始证据，并重算对应候选门禁。

#### 新模式返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 成功数/请求数摘要 |
| `output` | string | 详情与失败项文本摘要 |
| `results_count` | integer | 请求视频数 |
| `success_count` | integer | 成功详情数 |
| `failed_count` | integer | 失败数 |
| `errors` | array<object> | `content_id`、`error`；错误文本最多保留 500 字符 |
| `duration_ms` | integer | 整批耗时 |
| `candidates` | array<object> | 刷新后的候选门禁摘要 |
| `candidates[].candidate_id` | integer | 候选 ID |
| `candidates[].aweme_id` | string | 视频 ID |
| `candidates[].gate_status` | string | `pending/pass/fail` |
| `candidates[].failed_reason_codes` | array<string> | 未通过门禁原因 |
| `candidates[].evidence_version` | integer | 当前证据版本 |

#### 旧模式额外返回

旧模式不自动关联候选，返回 `details` 数组；单条成功时额外返回 `detail`。每条详情常用字段包括：

- `content_id`、`channel_content_id`、`content_link`；
- `title`、`body_text`、`content_type`、`topic_list`；
- `video_url`、`video_duration`、`duration_seconds`、`cover_url`；
- `channel_account_id`、`channel_account_name`、`channel_account_avatar`；
- `play_count/view_count`、`like_count`、`comment_count`、`collect_count`、`share_count`；
- `publish_at` 及源数据中的创建、发布、修改时间；
- `image_url_list`、`video_url_list`、`multi_bitrate`、`bgm_data`、`is_original`。

## 8. 画像与标准化工具

### 8.1 `get_content_fans_portrait`

获取单个视频的点赞用户画像。`content_id` 必须为 15～25 位纯数字 `aweme_id`。

### 8.2 `get_account_fans_portrait`

获取单个作者的粉丝画像。`account_id` 必须是完整 `sec_uid`，并以 `MS4wLjABAAAA` 开头。

### 8.3 两个单条画像工具的公共参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `content_id` / `account_id` | string | 是 | - | 内容或作者 ID |
| `need_province` | boolean | 否 | `false` | 是否返回省份分布 |
| `need_city` | boolean | 否 | `false` | 是否返回城市分布 |
| `need_city_level` | boolean | 否 | `false` | 是否返回城市等级 |
| `need_gender` | boolean | 否 | `false` | 是否返回性别分布 |
| `need_age` | boolean | 否 | `true` | 是否返回年龄分布 |
| `need_phone_brand` | boolean | 否 | `false` | 是否返回手机品牌 |
| `need_phone_price` | boolean | 否 | `false` | 是否返回手机价格 |
| `timeout` | number/null | 否 | `null` | `null` 时默认 60 秒 |

### 8.4 两个单条画像工具的公共返回

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 内容或账号画像标题 |
| `output` | string | 各画像维度的人类可读摘要；省份和城市只展示 TOP5 |
| `has_portrait` | boolean | 是否存在任一非空画像维度 |
| `portrait_data` | object | 画像维度对象；常见维度为年龄、性别、地域等 |
| `raw_data` | object | 上游原始完整响应 |
| `duration_ms` | integer | 调用耗时 |

单条工具不自动写入候选证据。需要持久化时应优先使用 `batch_fetch_portraits` 新模式。

### 8.5 `batch_fetch_portraits`

#### 功能

按输入顺序逐条获取内容点赞画像；`fetch_account_portrait=true` 时同时获取作者粉丝画像，
否则仅在内容画像缺失且允许兜底时请求作者画像。单次最多 8 项。

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `candidates_json` | string | 否 | `""` | 旧模式 JSON 数组字符串 |
| `fetch_account_portrait` | boolean | 否 | `false` | 是否始终同时取作者画像；老年受众判断建议为 `true` |
| `need_province` | boolean | 否 | `false` | 同单条画像 |
| `need_city` | boolean | 否 | `false` | 同单条画像 |
| `need_city_level` | boolean | 否 | `false` | 同单条画像 |
| `need_gender` | boolean | 否 | `false` | 同单条画像 |
| `need_age` | boolean | 否 | `true` | 同单条画像 |
| `need_phone_brand` | boolean | 否 | `false` | 同单条画像 |
| `need_phone_price` | boolean | 否 | `false` | 同单条画像 |
| `timeout` | number/null | 否 | `null` | `null` 时默认 60 秒 |
| `run_id` | string/null | 条件必填 | `null` | 使用 `candidate_ids` 时必填 |
| `candidate_ids` | array<integer>/null | 条件必填 | `null` | 新模式候选 ID，最多 8 条 |

旧模式 `candidates_json` 每项：

| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `aweme_id` | string | 是 | - | 视频 ID；也兼容字段名 `content_id` |
| `author_sec_uid` | string | 否 | - | 作者 ID；也兼容字段名 `account_id` |
| `try_account_fallback` | boolean | 否 | `true` | 内容画像缺失时是否尝试作者画像 |

#### 新模式返回

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 批量完成标题 |
| `output` | string | 各候选画像获取摘要 |
| `count` | integer | 处理项数 |
| `duration_ms` | integer | 整批耗时 |
| `candidates` | array<object> | 刷新后的候选门禁摘要 |
| `candidates[].candidate_id` | integer | 候选 ID |
| `candidates[].aweme_id` | string | 视频 ID |
| `candidates[].gate_status` | string | `pending/pass/fail` |
| `candidates[].failed_reason_codes` | array<string> | 门禁失败原因 |
| `candidates[].evidence_version` | integer | 证据版本 |

#### 旧模式返回

旧模式返回 `results`，每项包含：

- `aweme_id`、`author_sec_uid`、`try_account_fallback`、`fetch_account_portrait`；
- `content`：`ok/error/has_portrait/portrait_data/output`；
- `account`：`attempted/error/skipped_reason/has_portrait/portrait_data/output`；
- `age_normalization`：两侧年龄标准化结果；
- `error`：条目级错误。

批量画像允许逐项失败，不会因为一条上游失败而丢弃整批结果。

### 8.6 `normalize_age_portraits`

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `content_portrait` | object | 是 | - | 视频 `portrait_data` 或其中的年龄字典 |
| `account_portrait` | object/null | 否 | `null` | 作者 `portrait_data` 或其中的年龄字典 |

#### 返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | “年龄画像标准化” |
| `content` | object | 视频侧标准化结果 |
| `account` | object | 作者侧标准化结果 |
| `consistency` | string | `aligned/conflict/content_only/account_only/missing` |
| `elder_score_cap` | number | 当前证据条件下建议的 E 分数上限 |
| `output` | string | 两侧强度、一致性与上限摘要 |

`content`、`account` 公共字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `has_age_portrait` | boolean | 是否识别到年龄维度 |
| `older_ratio` | number | 50+ 桶占比，范围 0～1 |
| `older_tgi` | number/null | 50+ 桶按占比加权 TGI |
| `mature_ratio` | number | 40～50 等成熟代理桶占比 |
| `strength` | string | `strong/moderate/weak/missing` |
| `buckets` | array<object> | 原桶的 `label/kind/ratio/tgi` |

当前 `elder_score_cap` 规则：

| 证据情况 | 上限 |
|---|---:|
| 内容与作者两侧都有画像 | `1.0` |
| 仅内容侧 | `1.0` |
| 仅作者侧且强证据 | `0.85` |
| 仅作者侧且非强证据 | `0.75` |
| 两侧均缺失 | `0.50` |

此上限是返回给模型的决策建议，候选更新工具不会硬校验 `elder_score <= elder_score_cap`。

## 9. 候选更新工具

### 9.1 `batch_update_video_discovery_candidates`

#### 功能

严格按 `candidate_id` 批量更新候选详情、画像证据、R/E/S/V 评分、理由和分池。该工具只更新
候选，不新增候选、不修改搜索记录，也不结束运行。

#### 顶层参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `run_id` | string | 是 | 运行 ID |
| `items` | array<object> | 是 | 非空候选更新数组 |

#### `items[]` 字段

| 字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `candidate_id` | integer | 是 | 必须大于 0、存在且属于 `run_id` |
| `decision_bucket` | string | 是 | 仅 `primary` 或 `rejected` |
| `title` | string | 否 | 标题，最多 512 字符 |
| `content_link` | string | 否 | 内容链接，最多 1024 字符 |
| `author_name` | string | 否 | 作者名，最多 256 字符 |
| `author_sec_uid` | string | 否 | 作者 ID，最多 256 字符 |
| `tags` | array/string | 否 | 标签；与已有标签合并 |
| `publish_at` | string/number | 否 | ISO 时间、日期或秒/毫秒时间戳 |
| `duration_seconds` | number | 否 | 秒；也兼容 `video_duration` |
| `play_count` | integer | 否 | 播放数，负值归零 |
| `like_count` | integer | 否 | 点赞数，负值归零 |
| `comment_count` | integer | 否 | 评论数，负值归零 |
| `collect_count` | integer | 否 | 收藏数，负值归零 |
| `share_count` | integer | 否 | 分享数，负值归零 |
| `content_age_evidence` | object | 否 | 视频侧原始/结构化年龄证据 |
| `account_age_evidence` | object | 否 | 作者侧原始/结构化年龄证据 |
| `age_normalization` | object | 否 | 标准化结果；兼容包在 `result` 等字段内 |
| `content_50_plus_ratio` | number | 否 | 视频侧 50+ 占比，必须在 0～1 |
| `content_50_plus_tgi` | number | 否 | 视频侧 50+ TGI |
| `account_50_plus_ratio` | number | 否 | 作者侧 50+ 占比，必须在 0～1 |
| `account_50_plus_tgi` | number | 否 | 作者侧 50+ TGI |
| `temporal_type` | string | 否 | 建议 `evergreen/event/seasonal/festival/daypart` |
| `temporal_status` | string | 否 | 显式 `fail/unknown` 会参与时间门禁；其他值由规则重算 |
| `temporal_evidence` | object | 否 | 时效判断证据 |
| `reject_reason_code` | string | 否 | 拒绝原因码，最多 64 字符 |
| `relevance_score` | number | 否 | `R`，保存 6 位小数 |
| `elder_score` | number | 否 | `E`，保存 6 位小数 |
| `share_score` | number | 否 | `S`，保存 6 位小数 |
| `value_score` | number | 否 | `V`，保存 2 位小数 |
| `decision_reason` | string | 否 | 最终判断理由 |

#### 校验和重分类

- 任一 `items[]` 不是对象、缺少有效 `candidate_id` 或分池非法，整批在写库前失败；
- 不存在或不属于本次运行的 `candidate_id` 会使数据库更新失败；
- `content/account_50_plus_ratio` 被硬校验为 0～1；
- R/E/S/V 当前只做数字转换和精度处理，不硬校验 0～1，也不自动计算 `V`；
- 请求 `primary` 但证据未齐时保存为 `pending_evaluation`；
- 请求 `primary` 但 P0 门禁失败时自动保存为 `rejected`；
- 请求 `rejected` 时即使门禁通过也仍保存为 `rejected`。

默认 P0 规则快照包含：时效有效、时长至少 30 秒、分享至少 1000、视频或作者任一侧 50+ 占比
至少 20%。部分缺失项可由代码中的高质量信号补偿；运行使用预创建时保存的规则快照，环境变量
可修改阈值。

#### 返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | “候选已更新” |
| `run_id` | string | 运行 ID |
| `updated_count` | integer | 成功更新候选数 |
| `reclassified_count` | integer | 门禁导致分池变化的数量 |
| `reclassified` | array<object> | `candidate_id/requested_bucket/saved_bucket/failed_reason_codes` |
| `candidates` | array<object> | 更新后候选摘要 |
| `candidates[].candidate_id` | integer | 候选 ID |
| `candidates[].aweme_id` | string | 视频 ID |
| `candidates[].decision_bucket` | string | 实际保存的分池 |
| `candidates[].gate_status` | string | `pending/pass/fail` |
| `candidates[].failed_reason_codes` | array<string> | 门禁失败码 |
| `candidates[].pending_reason_codes` | array<string> | 尚缺证据的原因码 |
| `candidates[].evidence_version` | integer | 候选证据版本 |
| `candidates[].gate_evidence_version` | integer/null | 门禁使用的证据版本 |
| `candidates[].latest_gate_evaluation_id` | integer/null | 最近门禁评估记录 ID |
| `output` | string | 更新与重分类摘要 |

## 10. 运行控制与查询工具

### 10.1 `update_video_discovery_run_status`

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 运行 ID |
| `status` | string | 是 | - | 仅 `running/finished/failed` |
| `intent_summary` | string/null | 否 | `null` | Agent 对需求真实意图的总结 |
| `stop_reason` | string/null | 否 | `null` | 停止搜索或失败原因 |

#### 行为

- `finished`：把所有剩余 `pending_evaluation/unreviewed` 候选归档为 `rejected`，刷新计数并计算业务结果；
- `failed`：设置 `outcome_status=failed`；
- `running`：只更新运行状态及可选说明，不清空既有搜索和候选。

业务结果：有效且按 `aweme_id` 去重的 `primary` 至少 5 条为 `goal_met`，1～4 条为 `partial`，
0 条为 `no_match`。

#### 返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 状态更新标题 |
| `run` | object | 完整运行摘要，见 10.3 |
| `output` | string | `run_id/status/primary_count` 摘要 |

### 10.2 `query_video_discovery_state`

#### 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---|---|
| `run_id` | string | 是 | - | 运行 ID |
| `include_rejected` | boolean | 否 | `true` | 是否包含 `rejected`；为 false 时仍包含 `primary` 和待评估候选 |
| `limit` | integer | 否 | `100` | 候选返回上限，代码约束为 `1～500` |

#### 返回结果

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 查询标题 |
| `run` | object | 运行摘要 |
| `searches` | array<object> | 所有搜索页，结构见 10.4 |
| `candidates` | array<object> | 候选详情，结构见 10.5 |
| `output` | string | 搜索页数和主推荐数摘要 |

### 10.3 `run` 对象

| 字段 | 类型 | 说明 |
|---|---|---|
| `run_id` | string | 运行 ID |
| `biz_dt` | string/null | 业务日期 |
| `demand_grade_id` | integer/null | 需求分级记录 ID |
| `demand_word` | string | 需求词 |
| `seed_video_id` | string/null | 兼容旧流程的主参考视频 ID |
| `seed_video_title` | string/null | 兼容旧流程的主参考视频标题 |
| `intent_summary` | string/null | 意图总结 |
| `status` | string | `running/finished/failed` |
| `search_count` | integer | 已保存搜索页数 |
| `primary_count` | integer | 当前 primary 记录数 |
| `valid_primary_count` | integer | 门禁通过且按视频去重后的 primary 数 |
| `outcome_status` | string/null | `goal_met/partial/no_match/failed` |
| `attempt_count` | integer | 执行次数 |
| `stop_reason` | string/null | 停止或失败原因 |
| `rule_version` | string | P0 规则版本 |
| `rule_config` | object | 本次运行冻结的门禁配置和评估时间 |

### 10.4 `searches[]` 对象

字段与 6.5 搜索页公共结构一致，但不含 `run_id`、`candidates` 和 `persisted`。查询态包含：
`search_id`、关键词与原因、来源和父子关系、供应商及分页状态、筛选参数、结果数、候选数、
`has_more/next_cursor`、执行状态、错误信息和原始证据 ID。

### 10.5 `candidates[]` 完整对象

| 字段组 | 字段 |
|---|---|
| 标识与来源 | `candidate_id`、`search_id`、`aweme_id`、`source_keywords`、`source_search_ids` |
| 内容与作者 | `title`、`content_link`、`author_name`、`author_sec_uid`、`tags`、`publish_at`、`duration_seconds` |
| 互动数据 | `play_count`、`like_count`、`comment_count`、`collect_count`、`share_count` |
| 模型评分 | `relevance_score`、`elder_score`、`share_score`、`value_score`、`decision_reason` |
| 分池 | `decision_bucket`、`reject_reason_code` |
| 年龄证据 | `content_age_evidence`、`account_age_evidence`、`age_normalization`、两侧 `50_plus_ratio/tgi` |
| 画像状态 | `content_portrait_status`、`account_portrait_status`、`portrait_conflict` |
| 时效门禁 | `temporal_type`、`temporal_status`、`temporal_evidence` |
| 门禁结果 | `gate_status`、`gate_results`、`rule_version` |
| 证据引用 | 四类 `latest_*_evidence_id`、三类 `*_fetch_status` |
| 版本审计 | `evidence_version`、`gate_evidence_version`、`latest_gate_evaluation_id`、`gate_evaluated_at` |

数据库旧值 `unreviewed` 在返回时统一映射为 `pending_evaluation`。

## 11. 内置工具 `load_skill`

### 参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `name` | string | 是 | Skill 名称 |

### 返回

- 找到 Skill：返回包含 Skill 名称、说明和完整指令的上下文文本；
- 未找到：返回 JSON 字符串 `{"error": "Skill '...' not found. Available: ..."}`。

当前 `find_agent` 没有预加载 Skill，项目 `skills/` 下也没有可发现的 `SKILL.md`，因此正常视频
发现流程不依赖此工具。

## 12. 非功能要求

### 12.1 性能与限流

- 内部关键词搜索：进程内相邻请求至少间隔约 10.1 秒；
- 作者作品：进程内相邻请求至少间隔约 10.1 秒；
- 视频详情：进程内逐条请求至少间隔约 10.1 秒；
- TikHub 搜索：进程内相邻请求至少间隔 1 秒；
- 批量搜索和批量画像均按顺序执行，不承诺并发；
- 单次 Agent 运行默认总超时 600 秒，工具自身默认上游超时通常为 60 秒。

### 12.2 可追溯性

- 每次搜索页都新增记录，不覆盖同参数历史页；
- 搜索、详情和批量画像新模式保存原始证据；
- 候选门禁记录证据版本和最近评估 ID；
- 运行完成后以数据库 `outcome_status` 和 `valid_primary_count` 为准，不能仅看最终文案。

### 12.3 数据安全与体积

- 单条画像工具返回 `raw_data`，可能体积较大；Agent 主流程应优先使用新模式批量画像的候选摘要；
- 搜索落库返回会移除原始响应和统一搜索结果大数组，但原始响应已保存到证据表；
- 错误文本可能包含上游响应内容，展示到前台前应做长度控制和敏感信息过滤。

## 13. 已知问题与产品改进项

| 优先级 | 问题 | 影响 | 建议 |
|---|---|---|---|
| P0 | `list[dict]` 内部结构未进入 JSON Schema | 模型容易漏填 `keyword/query_reason/candidate_id/decision_bucket` | 使用 Pydantic 模型或 TypedDict 生成嵌套 Schema |
| P0 | 多数枚举和范围未进入 JSON Schema | 非法 `source_type/status/sort_type` 只能运行时发现 | 为参数增加 enum、minimum、maximum |
| P1 | 所有工具都返回字符串化 JSON | 调用方易发生二次解析遗漏 | 在框架层明确结构化工具结果协议 |
| P1 | 搜索工具的数据库 `search_id` 与 TikHub `search_id` 同名 | 翻页时容易传错 ID | 对外统一命名为 `db_search_id/provider_search_id` |
| P1 | R/E/S/V 不做范围与公式校验 | 可保存越界分数或错误 V | 强制 0～1 并由代码计算 V |
| P1 | `elder_score_cap` 只提示、不执行 | 模型可能给出高于证据上限的 E | 更新候选时加入硬校验或自动截断 |
| P1 | 单条画像工具不写证据 | 使用旧模式会出现结果存在但不可追溯 | Prompt 和调用层只开放批量新模式，或给单条工具增加候选模式 |
| P2 | 批量搜索只返回部分 TikHub 分页字段 | 外部调用方难以手动接管中途分页 | 在 `pages[]` 明确返回 `provider_search_id/backtrace` |
| P2 | 错误结构不完全统一 | 调用方需兼容 `error/errors/error_message` | 定义统一 `ok/code/error/data` 包装 |

其中年龄上限应以本文件记录的当前代码值为准：仅作者强证据 `0.85`、仅作者非强证据 `0.75`、
两侧缺失 `0.50`。旧文档中的 `0.65/0.35` 已不符合当前实现。

## 14. 验收标准

### 14.1 文档验收

- 工具数量与 `create_find_agent().tools.list_tools()` 一致；
- 每个工具的顶层必填项、类型和默认值与运行时 Schema 一致；
- 搜索任务、候选更新项等嵌套字段与运行时代码校验一致；
- 返回字段可在对应实现或 Service 序列化函数中逐项定位；
- 明确区分数据库 ID、平台 ID 和供应商分页 ID。

### 14.2 调用验收

- 搜索成功后必须得到 `search_id` 和至少零个 `candidate_id`，且 `persisted=true`；
- 详情/画像新模式必须使用同一 `run_id` 下的 `candidate_ids`；
- 候选更新后必须读取实际 `decision_bucket` 和 `gate_status`，不能假设请求分池等于保存分池；
- 结束前必须调用状态查询核对搜索数、候选分池和门禁；
- 正常业务结束必须调用 `status=finished`，并以返回的 `outcome_status` 为最终结果；
- 任一返回含 `error` 时都必须显式处理，不得视为成功继续。

## 15. 代码依据

| 范围 | 代码位置 |
|---|---|
| Agent 注册工具 | `agents/find_agent/tools/__init__.py` |
| 运行时 Tool Schema | `supply_agent/tools/base.py` |
| 内置 `load_skill` | `supply_agent/agent/core.py` |
| 搜索工具 | `agents/find_agent/tools/` 与 `agents/find_agent/support/douyin_*.py` |
| 搜索持久化 | `agents/find_agent/support/search_persistence.py` |
| 详情与画像 | `agents/find_agent/support/douyin_detail.py`、`portrait.py`、`age_portrait.py` |
| 候选与运行工具 | `agents/find_agent/support/video_discovery.py` |
| 返回对象序列化 | `supply_infra/services/video_discovery_service.py` |
| P0 门禁 | `supply_infra/video_discovery_gates.py` |

