PRD.md 16 KB

find_agent 当前执行逻辑 PRD

文档版本:v2.0
代码基线:2026-07-29
文档性质:As-Is 现状说明

1. 文档范围

本文只描述 find_agent 当前已经存在的内部执行逻辑,包括:

  • Agent 实例配置;
  • 单次运行的输入构造;
  • 模型循环与工具调用;
  • 搜索、候选、证据和评估状态;
  • 持久化状态变化;
  • 正常结束、超时和失败判定;
  • Prompt 约束与代码硬约束的实际边界。

2. Agent 实例状态

项目 当前值
Agent 名称 find_agent
默认模型 google/gemini-3-flash-preview
模型覆盖 创建或运行时显式传入 model 可覆盖默认模型
温度 0.2
最大模型迭代 60
单次运行总超时 默认 600
超时配置 FIND_AGENT_TIMEOUT_SECONDS,最小 60 秒,最大 7200
执行方式 异步 ReAct 循环;同步入口负责创建并关闭事件循环
视频理解 不可用;qwen_video_analyze 未注册到 Agent

Agent 初始化时注册一个内置 load_skill 工具和 12 个 find_agent 专属工具。当前 find_agent 没有预加载 Skill,正常执行依赖系统 Prompt 和已注册工具。

3. 单次运行输入状态

3.1 上下文对象

单次调度运行对应一个 FindDemandContext

字段 含义
biz_dt 业务日期,格式为 YYYYMMDD
demand_grade_id 本次需求记录 ID
demand_name 传给 Agent 的 demand_word
grade 当前需求等级
videos 该需求下的参考视频集合
videos[].video_id 参考视频 ID
videos[].title 参考视频标题;缺失时使用 (无标题\|video_id)
videos[].points 参考视频对应的有效拓展点位

点位只接受 inspirationpurposekey 三种类型。空点位被丢弃,同一视频内按 (point_type, expanded_text) 去重。没有有效拓展点位的参考视频不会进入上下文;没有 有效参考视频的需求不会生成运行上下文。

上下文加载后的实际排序键为:

score 降序 → grade(S 在 A 前)→ demand_name → demand_grade_id

因此当前实现先比较 score,只有同分时才比较 S/A 等级。

3.2 运行预创建

进入模型循环前,系统先按 (biz_dt, demand_grade_id) 预创建或复用 video_discovery_run

  1. 新任务生成随机 run_id,初始状态为 running
  2. 已有记录且未启用 force 时:
    • 状态为 finished,跳过;
    • 或该运行下已经存在任意候选记录,跳过;
  3. 已有记录但不满足跳过条件时,复用原 run_id,并将运行状态重置为 running
  4. 启用 force 时复用已有 run_id 并重置运行输入,不新建第二条同需求运行。

复用或强制运行不会清空原 run_id 下的搜索页和候选,也没有“执行代次”字段。新一次 模型执行会继续读写同一组持久化记录。

3.3 模型用户消息

模型收到的用户消息包含:

  • 预创建的 run_id
  • demand_grade_id
  • demand_word
  • 全部 reference_videos 及各自点位;
  • 直接复用给定 run_id 开始搜索、更新候选和管理状态的指令。

relevant_points 不传给模型,也不要求模型生成。调度程序在预创建运行时已经完成点位 展平,并保存展平结果、完整参考视频快照以及兼容旧表结构的主参考视频字段。

4. 内部执行状态机

4.1 逻辑阶段

阶段 进入条件 内部动作 退出条件
CONTEXT_READY 已构造有效上下文 生成输入快照 准备运行记录
RUNNING 运行记录已预创建或重置 启动日志与 ReAct 循环 模型请求工具或直接回答
SEARCHING 模型调用召回工具 关键词、翻页、标签或作者扩展 搜索结果返回
PERSISTING_SEARCH 搜索页已返回 自动新增搜索记录,并为本页每条结果新增候选记录 候选进入待评估态
EVIDENCE_GATHERING 模型选择高潜候选 获取详情、视频画像、作者画像并标准化 模型认为证据足够
EVALUATING 候选具备可用证据 生成 R/E/S/V、理由和最终分池 评估写入数据库
FINAL_QUERY 候选与运行状态已保存 重新读取数据库最终状态 模型生成最终文本
LOOP_DONE 模型返回不含工具调用的消息 结束 ReAct 循环 进入运行结果判定
FAILED 外层异常、超时或结果侧失败 将运行标记为 failed 本次执行结束

逻辑阶段没有单独持久化字段。数据库只持久化运行、搜索页和候选三个层级的状态。

4.2 持久化状态

运行状态:

  • running
  • finished
  • failed

搜索页状态:

  • success
  • failed

候选状态:

  • pending_evaluation
  • primary
  • rejected

数据库兼容读取旧值 unreviewed,读取时统一映射为 pending_evaluation

5. ReAct 循环逻辑

每轮执行顺序为:

  1. 将系统 Prompt、历史消息和当前工具定义发送给模型;
  2. 模型返回普通消息或一个及以上工具调用;
  3. 无工具调用时,当前普通消息立即成为 AgentResult.content,循环结束;
  4. 有工具调用时,按模型返回顺序逐个执行;
  5. 每个工具结果写入运行日志并追加为 tool 消息;
  6. 下一轮模型基于完整消息历史继续决策。

同一轮中的多个工具调用不是并行执行,而是顺序执行。工具返回 JSON 中即使包含 error,循环框架也只把它标记为工具错误并交还模型,不自动重试、不自动失败,也不 自动切换替代工具。

5.1 非法工具参数恢复

模型供应方返回 MALFORMED_FUNCTION_CALL 时:

  1. LLM 客户端最多进行 3 次内部重试,即一次原请求加 3 次重试;
  2. 重试时温度降为 0,关闭并行工具调用,并附加“只调用一个必要工具”的修正指令;
  3. 连续失败后向 AgentLoop 抛出 MalformedFunctionCallError
  4. AgentLoop 追加一条参数修正消息,然后消耗下一次 Agent 迭代继续执行。

5.2 最大迭代结束

完成 60 次迭代后,框架追加“立即给出当前最佳答案”的消息,再发起一次不携带工具定义的 模型请求。该请求只能生成文本,不能继续完成搜索、保存或最终状态查询。

框架不会在模型输出最终文本前检查运行状态或数据库一致性。

6. 搜索与候选召回逻辑

6.1 搜索计划

搜索词、搜索顺序和是否扩展由模型决定。系统 Prompt 当前要求:

  • 从需求、参考标题和相关点形成 2~3 个语义不同的根搜索;
  • 根搜索来源使用 demandseedpointmixed
  • 标签、作者和翻页扩展使用 tagauthorpagination
  • 尽量形成 5 条 decision_bucket=primary 的通过视频;
  • 优先验证语义不同的有效根搜索,并按信息价值决定是否翻页或扩展标签;
  • 剩余前沿不再可能改变候选判断、排序或置信度时停止。

以上搜索策略由 Prompt 驱动,AgentLoop 不包含固定搜索计划器。

6.2 搜索来源

来源 当前行为
douyin_search 内部关键词搜索;返回标题、作者、点赞、评论、分享和分页游标
douyin_search_tikhub TikHub 独立搜索;额外返回话题、收藏、播放、时长及完整分页状态
douyin_user_videos 按作者 sec_uid 获取最热或最新作品

内部关键词搜索和作者作品接口在进程内分别执行至少 10.1 秒的调用间隔控制;TikHub 搜索执行至少 1 秒的调用间隔控制。

TikHub 翻页需要原样复用上一页的 next_cursorsearch_idbacktrace。三个来源的 游标彼此独立。

6.3 搜索页持久化

搜索工具在外部接口返回后自动完成持久化。当前处理规则为:

  1. 每次搜索、每一页都新增一条 video_discovery_search,相同参数重复执行也不会覆盖;
  2. page_no > 1 时强制将来源类型改为 pagination
  3. 根来源自动清空 parent_search_id
  4. 搜索失败也新增搜索记录,保存 failed 和原始错误,不新增候选;
  5. 搜索成功后,本页每条有效结果都新增一条 video_discovery_candidate
  6. 候选通过 search_id 直接关联本次搜索;
  7. 同一 aweme_id 被不同搜索命中时允许重复插入,每条记录拥有独立 candidate_id
  8. 新候选状态统一为 pending_evaluation
  9. 搜索工具返回视频基础信息,并把数据库生成的 search_id/candidate_id 拼入结果。

7. 证据获取逻辑

7.1 详情核验

douyin_detail 单次最多处理 8 个视频 ID,用于更新:

  • 标题和描述;
  • 作者信息;
  • 话题标签;
  • 页面链接;
  • 播放、点赞、评论、收藏和分享数据;
  • 发布时间和视频时长。

详情工具返回证据,不直接修改候选数据库。模型需要再次调用候选评估保存工具才能写入。

7.2 年龄画像

年龄证据包含两个独立来源:

  • 视频点赞用户画像:内容侧直接证据;
  • 作者粉丝画像:账号侧先验。

batch_fetch_portraits 单次最多处理 8 个候选,按输入顺序逐条请求内容画像;设置 fetch_account_portrait=true 时同时请求作者画像。每条结果自动生成标准化年龄结果。

单条画像工具只返回原始画像,需要额外调用 normalize_age_portraits

7.3 年龄标准化

年龄标准化把画像桶归为:

  • older:明确覆盖 50 岁及以上;
  • mature:覆盖 40 岁以上但不能作为直接老年桶;
  • younger
  • unknown

每侧画像根据老年占比、老年 TGI 和成熟人群占比确定:

  • strong
  • moderate
  • weak
  • missing

双侧结果再生成:

  • aligned:两侧强弱方向一致;
  • conflict:两侧强弱方向冲突;
  • content_only
  • account_only
  • missing

标准化工具还返回 elder_score_cap:双侧、仅内容侧为 1.0,仅账号侧为 0.65,两侧 均缺失为 0.35。该上限只作为返回给模型的决策信息,保存工具不会执行分数上限校验。

8. 候选判断逻辑

8.1 三项命题

模型分别判断:

  • R:候选是否满足需求真实意图;
  • E:视频点赞用户和作者粉丝画像是否支持较高年龄受众倾向;
  • S:候选是否同时具备传播行为信号和可解释分享动机。

最终目标为 R ∩ E ∩ S。相关性是准入闸门,分享规模和年龄倾向不能弥补低相关。

8.2 证据使用顺序

当前 Prompt 要求:

  1. 先用搜索标题、描述、话题和互动数据进行低成本预筛;
  2. 默认最多选 8 条高潜候选进入详情和双画像阶段;
  3. 视频画像优先于作者画像;
  4. 双侧一致时增强置信度,冲突时优先视频画像并降低置信度;
  5. 数据缺失视为未知,不作为负证据;
  6. 高度重复候选只保留证据更强的一条;
  7. 价值相近时优先覆盖不同需求点或分享动机。

8.3 评分与分池

Prompt 定义联合价值关系:

V = R^0.40 × E^0.35 × S^0.25

数据库中的 R/E/S/V 都使用 0~1 小数。当前保存实现不会计算 V、不会校验分数 范围,也不会根据分数调整分池。R/E/S 最多保留 6 位小数,V 最多保留 2 位小数; 无法转换为数字的值按缺失处理。

最终分池只接受:

  • primary
  • rejected

分池完全由模型决定。更新层仅校验候选 ID 和枚举合法性,不校验:

  • R/E/S/V 是否齐全或在 0~1 范围;
  • V 是否符合公式;
  • primary 是否满足证据条件;
  • 最终理由是否齐全。

候选更新必须使用搜索工具返回的 candidate_id。不存在或不属于当前 run_id 时整批 失败,不允许补建候选,也不允许用 aweme_id 更新同视频的其他搜索记录。候选更新工具 只修改 video_discovery_candidate;运行状态由独立工具修改。

9. 结束与输出流程

Prompt 规定的正常结束流程为:

停止搜索 → 获取必要证据 → 按 candidate_id 更新已作出的候选判断 → 将运行设为 finished → 输出

query_video_discovery_state 保留为按需恢复和查看已持久化状态的工具,不是正常结束的 强制步骤。AgentLoop 没有完成守卫:模型任何一轮只要返回不含工具调用的普通消息,就会 立即结束。

10. 结束、超时与失败状态

11.1 模型循环结束

模型返回普通消息后生成 AgentResult,其中记录:

  • 最终文本;
  • 完整运行消息;
  • 已执行迭代数;
  • 工具调用数;
  • 已加载 Skill。

随后关闭异步 LLM 客户端并清理未完成异步任务。

运行日志和可视化产物在核心循环结束后发布。发布等待设置为 120 秒;超时或异常只记录 日志,不改变 AgentResult。当前使用线程池上下文执行发布,超时被捕获后退出线程池时 仍可能继续等待发布线程真正结束,因此 120 秒不是完整调用链的硬停止时间。

11.2 超时

超过单次运行总超时后:

  1. 取消 Agent 核心协程;
  2. 关闭异步客户端;
  3. 最多等待 5 秒清理后台异步任务,仍未完成的任务被取消;
  4. 抛出 TimeoutError
  5. 调度包装层将对应运行标记为 failed,保存超时原因。

11.3 异常

Agent 运行或结果判定期间出现未处理异常时,调度包装层把运行状态标记为 failed, 保存异常文本并继续向上抛出。

工具返回的结构化错误不是未处理异常,不会自动将运行标记为 failed。是否停止、切换 工具或输出“任务未完成(工具故障)”由模型判断。

11.4 当前成功判定

模型循环返回后,调度侧的当前成功判定只检查:

该 run_id 下是否存在至少一条候选记录

只要存在任意候选,即判定本次执行成功。该候选可以是:

  • pending_evaluation
  • primary
  • rejected
  • 仅由搜索页自动创建、尚未补证的候选。

成功判定不检查:

  • 运行状态是否为 finished
  • 是否存在 primary
  • 最终文本是否符合输出契约。

如果一条候选都不存在,调度侧将运行标记为 failed,失败原因为 no_candidates。模型 最终文本只作为失败原因预览附加保存,不参与成功判定。

11. 当前约束归属

约束 Prompt 驱动 代码硬约束
所有存储和状态工具复用预创建 run_id 是,相关工具均要求 run_id
形成 2~3 个根搜索词
每次搜索新增并保存搜索页
每条搜索结果新增独立候选并返回 ID
页码大于 1 自动归为翻页
根搜索不保留父搜索 ID
详情单批最多 8 条
画像单批最多 8 条
不使用视频理解 是,工具未注册
评分使用 0~1
V 按公式计算
最终分池仅 primary/rejected
primary 具备足够详情和双画像证据
运行最大 60 次模型迭代
单次运行总超时
调度成功必须存在候选

12. 当前执行伪代码

load contexts
  -> 过滤无有效点位或无有效参考视频的需求
  -> 按 score、grade、名称、ID 排序

for each context:
  run_id = prepare_or_reuse_run(status="running")
  if finished or has_any_candidate and not force:
      skip

  user_input = build_agent_input(context, run_id)
  agent = create_find_agent(model, temperature=0.2, max_iterations=60)

  try:
      repeat up to 60 iterations:
          response = LLM(system_prompt + message_history + tool_schemas)
          if response has no tool_calls:
              agent_result = response
              break

          for tool_call in response.tool_calls:
              tool_result = await execute(tool_call)
              append tool_result to message_history

      if 60 iterations exhausted:
          agent_result = LLM("provide best answer", tools=None)

      success = database.has_any_candidate(run_id)
      if not success:
          mark_run_failed("no_candidates")
  except timeout or exception:
      mark_run_failed(error)
      raise
  finally:
      close async resources

  publish run logs
    -> report timeout after 120 seconds
    -> thread-pool shutdown may still wait for the publisher to exit