文档版本:v2.0
代码基线:2026-07-29
文档性质:As-Is 现状说明
本文只描述 find_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 和已注册工具。
单次调度运行对应一个 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 |
参考视频对应的有效拓展点位 |
点位只接受 inspiration、purpose、key 三种类型。空点位被丢弃,同一视频内按
(point_type, expanded_text) 去重。没有有效拓展点位的参考视频不会进入上下文;没有
有效参考视频的需求不会生成运行上下文。
上下文加载后的实际排序键为:
score 降序 → grade(S 在 A 前)→ demand_name → demand_grade_id
因此当前实现先比较 score,只有同分时才比较 S/A 等级。
进入模型循环前,系统先按 (biz_dt, demand_grade_id) 预创建或复用
video_discovery_run:
run_id,初始状态为 running;force 时:
finished,跳过;run_id,并将运行状态重置为 running;force 时复用已有 run_id 并重置运行输入,不新建第二条同需求运行。复用或强制运行不会清空原 run_id 下的搜索页和候选,也没有“执行代次”字段。新一次
模型执行会继续读写同一组持久化记录。
模型收到的用户消息包含:
run_id;demand_grade_id;demand_word;reference_videos 及各自点位;run_id 开始搜索、更新候选和管理状态的指令。relevant_points 不传给模型,也不要求模型生成。调度程序在预创建运行时已经完成点位
展平,并保存展平结果、完整参考视频快照以及兼容旧表结构的主参考视频字段。
| 阶段 | 进入条件 | 内部动作 | 退出条件 |
|---|---|---|---|
CONTEXT_READY |
已构造有效上下文 | 生成输入快照 | 准备运行记录 |
RUNNING |
运行记录已预创建或重置 | 启动日志与 ReAct 循环 | 模型请求工具或直接回答 |
SEARCHING |
模型调用召回工具 | 关键词、翻页、标签或作者扩展 | 搜索结果返回 |
PERSISTING_SEARCH |
搜索页已返回 | 自动新增搜索记录,并为本页每条结果新增候选记录 | 候选进入待评估态 |
EVIDENCE_GATHERING |
模型选择高潜候选 | 获取详情、视频画像、作者画像并标准化 | 模型认为证据足够 |
EVALUATING |
候选具备可用证据 | 生成 R/E/S/V、理由和最终分池 | 评估写入数据库 |
FINAL_QUERY |
候选与运行状态已保存 | 重新读取数据库最终状态 | 模型生成最终文本 |
LOOP_DONE |
模型返回不含工具调用的消息 | 结束 ReAct 循环 | 进入运行结果判定 |
FAILED |
外层异常、超时或结果侧失败 | 将运行标记为 failed |
本次执行结束 |
逻辑阶段没有单独持久化字段。数据库只持久化运行、搜索页和候选三个层级的状态。
运行状态:
runningfinishedfailed搜索页状态:
successfailed候选状态:
pending_evaluationprimaryrejected数据库兼容读取旧值 unreviewed,读取时统一映射为 pending_evaluation。
每轮执行顺序为:
AgentResult.content,循环结束;tool 消息;同一轮中的多个工具调用不是并行执行,而是顺序执行。工具返回 JSON 中即使包含
error,循环框架也只把它标记为工具错误并交还模型,不自动重试、不自动失败,也不
自动切换替代工具。
模型供应方返回 MALFORMED_FUNCTION_CALL 时:
0,关闭并行工具调用,并附加“只调用一个必要工具”的修正指令;MalformedFunctionCallError;完成 60 次迭代后,框架追加“立即给出当前最佳答案”的消息,再发起一次不携带工具定义的 模型请求。该请求只能生成文本,不能继续完成搜索、保存或最终状态查询。
框架不会在模型输出最终文本前检查运行状态或数据库一致性。
搜索词、搜索顺序和是否扩展由模型决定。系统 Prompt 当前要求:
demand、seed、point 或 mixed;tag、author 或 pagination;decision_bucket=primary 的通过视频;以上搜索策略由 Prompt 驱动,AgentLoop 不包含固定搜索计划器。
| 来源 | 当前行为 |
|---|---|
douyin_search |
内部关键词搜索;返回标题、作者、点赞、评论、分享和分页游标 |
douyin_search_tikhub |
TikHub 独立搜索;额外返回话题、收藏、播放、时长及完整分页状态 |
douyin_user_videos |
按作者 sec_uid 获取最热或最新作品 |
内部关键词搜索和作者作品接口在进程内分别执行至少 10.1 秒的调用间隔控制;TikHub
搜索执行至少 1 秒的调用间隔控制。
TikHub 翻页需要原样复用上一页的 next_cursor、search_id、backtrace。三个来源的
游标彼此独立。
搜索工具在外部接口返回后自动完成持久化。当前处理规则为:
video_discovery_search,相同参数重复执行也不会覆盖;page_no > 1 时强制将来源类型改为 pagination;parent_search_id;failed 和原始错误,不新增候选;video_discovery_candidate;search_id 直接关联本次搜索;aweme_id 被不同搜索命中时允许重复插入,每条记录拥有独立 candidate_id;pending_evaluation;search_id/candidate_id 拼入结果。douyin_detail 单次最多处理 8 个视频 ID,用于更新:
详情工具返回证据,不直接修改候选数据库。模型需要再次调用候选评估保存工具才能写入。
年龄证据包含两个独立来源:
batch_fetch_portraits 单次最多处理 8 个候选,按输入顺序逐条请求内容画像;设置
fetch_account_portrait=true 时同时请求作者画像。每条结果自动生成标准化年龄结果。
单条画像工具只返回原始画像,需要额外调用 normalize_age_portraits。
年龄标准化把画像桶归为:
older:明确覆盖 50 岁及以上;mature:覆盖 40 岁以上但不能作为直接老年桶;younger;unknown。每侧画像根据老年占比、老年 TGI 和成熟人群占比确定:
strongmoderateweakmissing双侧结果再生成:
aligned:两侧强弱方向一致;conflict:两侧强弱方向冲突;content_only;account_only;missing。标准化工具还返回 elder_score_cap:双侧、仅内容侧为 1.0,仅账号侧为 0.65,两侧
均缺失为 0.35。该上限只作为返回给模型的决策信息,保存工具不会执行分数上限校验。
模型分别判断:
R:候选是否满足需求真实意图;E:视频点赞用户和作者粉丝画像是否支持较高年龄受众倾向;S:候选是否同时具备传播行为信号和可解释分享动机。最终目标为 R ∩ E ∩ S。相关性是准入闸门,分享规模和年龄倾向不能弥补低相关。
当前 Prompt 要求:
Prompt 定义联合价值关系:
V = R^0.40 × E^0.35 × S^0.25
数据库中的 R/E/S/V 都使用 0~1 小数。当前保存实现不会计算 V、不会校验分数
范围,也不会根据分数调整分池。R/E/S 最多保留 6 位小数,V 最多保留 2 位小数;
无法转换为数字的值按缺失处理。
最终分池只接受:
primaryrejected分池完全由模型决定。更新层仅校验候选 ID 和枚举合法性,不校验:
R/E/S/V 是否齐全或在 0~1 范围;V 是否符合公式;primary 是否满足证据条件;候选更新必须使用搜索工具返回的 candidate_id。不存在或不属于当前 run_id 时整批
失败,不允许补建候选,也不允许用 aweme_id 更新同视频的其他搜索记录。候选更新工具
只修改 video_discovery_candidate;运行状态由独立工具修改。
Prompt 规定的正常结束流程为:
停止搜索 → 获取必要证据 → 按 candidate_id 更新已作出的候选判断
→ 将运行设为 finished → 输出
query_video_discovery_state 保留为按需恢复和查看已持久化状态的工具,不是正常结束的
强制步骤。AgentLoop 没有完成守卫:模型任何一轮只要返回不含工具调用的普通消息,就会
立即结束。
模型返回普通消息后生成 AgentResult,其中记录:
随后关闭异步 LLM 客户端并清理未完成异步任务。
运行日志和可视化产物在核心循环结束后发布。发布等待设置为 120 秒;超时或异常只记录
日志,不改变 AgentResult。当前使用线程池上下文执行发布,超时被捕获后退出线程池时
仍可能继续等待发布线程真正结束,因此 120 秒不是完整调用链的硬停止时间。
超过单次运行总超时后:
TimeoutError;failed,保存超时原因。Agent 运行或结果判定期间出现未处理异常时,调度包装层把运行状态标记为 failed,
保存异常文本并继续向上抛出。
工具返回的结构化错误不是未处理异常,不会自动将运行标记为 failed。是否停止、切换
工具或输出“任务未完成(工具故障)”由模型判断。
模型循环返回后,调度侧的当前成功判定只检查:
该 run_id 下是否存在至少一条候选记录
只要存在任意候选,即判定本次执行成功。该候选可以是:
pending_evaluation;primary;rejected;成功判定不检查:
finished;primary;如果一条候选都不存在,调度侧将运行标记为 failed,失败原因为 no_candidates。模型
最终文本只作为失败原因预览附加保存,不参与成功判定。
| 约束 | Prompt 驱动 | 代码硬约束 |
|---|---|---|
所有存储和状态工具复用预创建 run_id |
是 | 是,相关工具均要求 run_id |
| 形成 2~3 个根搜索词 | 是 | 否 |
| 每次搜索新增并保存搜索页 | 否 | 是 |
| 每条搜索结果新增独立候选并返回 ID | 否 | 是 |
| 页码大于 1 自动归为翻页 | 否 | 是 |
| 根搜索不保留父搜索 ID | 否 | 是 |
| 详情单批最多 8 条 | 否 | 是 |
| 画像单批最多 8 条 | 否 | 是 |
| 不使用视频理解 | 是 | 是,工具未注册 |
评分使用 0~1 |
是 | 否 |
V 按公式计算 |
是 | 否 |
最终分池仅 primary/rejected |
是 | 是 |
primary 具备足够详情和双画像证据 |
是 | 否 |
| 运行最大 60 次模型迭代 | 否 | 是 |
| 单次运行总超时 | 否 | 是 |
| 调度成功必须存在候选 | 否 | 是 |
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