本文档完全基于当前代码仓库重新阅读后形成,不继承此前对项目的业务推演或可视化设计理解。
代码快照:
feature/zhangbo2ab77ce>= 3.11.env 已被 .gitignore 忽略。本文不会记录其中的密码、密钥、数据库地址或 Token。
当前 SupplyAgent 由两部分组成:
业务系统当前主要完成:
当前系统的核心业务链路是:
ODPS 全局分类与元素
↓
MySQL 全局分类树
↑
需求池词语 → 归类 Agent → 需求词挂靠
↓
四项先验 + 真实 ROV/VOV
↓
词级统计 → 分类树节点聚合 → 四维全局排名
↓
需求生成 Agent → generated_demand
↓
FastAPI → Vue 分类树/热力图/证据下钻
SupplyAgent/
├── supply_agent/ 通用 Agent 框架
├── agents/ 业务 Agent 和专属工具
├── supply_infra/ MySQL、ODPS、OSS、定时任务
├── api/ FastAPI 查询接口和静态前端托管
├── web/ Vue 3 业务前端
├── jobs/ 数据任务的手动 CLI 入口
├── scripts/ 部署、日志上传、日志可视化脚本
├── visualization/ 早期/独立的业务设计可视化材料
├── Dockerfile 前端构建 + Python 运行镜像
├── pyproject.toml Python 包、依赖和命令入口
└── requirements.txt 另一份运行依赖清单
模块之间的依赖方向:
web
↓ HTTP
api
↓
supply_infra.db.repositories
↓
MySQL
agents
↓
supply_agent(Agent 框架)
↓
supply_infra(数据库/ODPS/OSS)
jobs / scheduler
↓
supply_infra.scheduler.jobs
↓
ODPS + MySQL + 业务 Agent
supply_agent/supply_agent/config.py负责 Agent 侧配置:
配置来自环境变量或项目根目录 .env,相对路径会解析到项目根目录。
supply_agent/llm/client.py基于 OpenAI Python SDK 连接 OpenRouter,提供:
chat;achat;stream;astream;模型可以在运行时切换。
supply_agent/tools/base.py 提供:
@tool 装饰器;registry.py 提供:
@tool 装饰的函数。supply_agent/skills/Skill 机制读取指定目录下的 SKILL.md:
load_skill 工具按需加载全文;当前仓库根目录的 skills/ 被 .gitignore 忽略,因此代码支持 Skill,但仓库没有可随代码分发的业务 Skill 内容。
supply_agent/agent/core.py 的 Agent 负责组装:
对外提供:
run;arun;stream;astream。loop.py 实现标准 ReAct/Tool Calling 循环:
系统消息 + 对话历史
↓
调用 LLM
↓
有工具调用?──否──→ 返回最终结果
│是
↓
执行工具并写入 Tool Message
↓
下一轮 LLM
达到最大迭代次数时,同步和普通异步运行会追加一条用户消息,要求模型给出当前最佳答案;流式路径则直接发出 Max iterations reached。
supply_agent/logging/每次 Agent 运行生成:
.log;.jsonl。日志记录:
运行结束后:
.log、.jsonl、.html 到 OSS;oss_logs;demand_belong_category_agent目标:把需求词挂到真实存在的全局分类树节点。
工具:
query_global_tree_category:读取整棵树或指定子树;batch_insert_demand_belong_category:批量写入需求词、分类 ID 和原因。业务流程:
demand_belong_category。系统提示允许一个词选择一个或多个高置信节点,但当前数据库和写入工具以 name 唯一:
demand_belong_category.name 有唯一约束;定时任务会把新需求词按 100 个一批交给该 Agent。
generate_demand_agent目标:从分类树局部热度和已挂靠需求词中,按单一维度生成平台需求结果。
支持的来源维度只有四项先验:
ext_pop:外部热度;plat_sust_pop:平台持续热度;plat_ly_pop:平台去年同期热度;recent_pop:近期热度。一次运行只允许处理一个维度。
工具:
query_latest_biz_dt:取两张统计表都有数据的最新日期;query_category_tree_by_dim:查看指定维度有数据的树;query_category_leaves_by_dim:定位有数据的叶子节点;query_demand_words_by_category:查询节点或子树下的真实需求词;query_category_path:补充根到节点的路径;batch_save_generated_demands:写入 generated_demand。输出结构固定为:
source_dim
└── overall_direction
└── summary_event
└── demand_name
重要约束:
demand_name 必须原样来自 demand_belong_category.name;summary_event 尽量对应一个需求,最多轻量合并 2~3 个强相关需求;(source_dim, demand_name) 去重;generated_demand 没有数据库唯一约束,不同 run 可以重复生成相同需求。真实 ROV/VOV 当前不属于该 Agent 的 source_dim,也没有参与其单维生成工具链。
find_agent目标:根据需求词、参考视频标题和相关点,寻找老年受众更可能观看和分享的抖音视频。
当前注册的业务工具包括:
douyin_search:调用外部抖音关键词搜索服务;douyin_search_tikhub:调用 TikHub 搜索并保留 search_id/backtrace 分页状态;douyin_user_videos:按作者 sec_uid、排序和游标扩展历史作品;douyin_detail:按 content_id 获取视频详情和可播放地址;get_content_fans_portrait:获取视频点赞用户画像;get_account_fans_portrait:获取作者粉丝画像;batch_fetch_portraits:批量获取视频画像,并可同时获取作者画像;normalize_age_portraits:标准化 50- 等年龄桶及双侧证据;audit_video_discovery_process:结束前审计多词、翻页、扩展、证据和双池分流;qwen_video_analyze:调用千问视频模型解析视频;create_video_discovery_run:创建可追踪的找片运行;record_video_search_page:保存 Agent 自主搜索词、扩展来源、游标与本页结果;batch_save_video_candidate_evaluations:保存证据、评分并分为正式推荐/人工备选/淘汰;query_video_discovery_state:查询搜索树和候选分池;review_video_discovery_candidate:记录用户对推荐或备选的人工选择结果。搜索和详情接口有约 10 秒的请求间隔限制。
Agent 以需求相关性、老年受众倾向和分享价值的联合目标做判断。系统提示明确区分 “分享量高”和“受众偏老”两类证据,使用视频点赞画像作为内容侧证据、作者粉丝画像 作为账号先验,并对画像缺失或冲突降低置信度。搜索词由 Agent 根据需求、参考标题、 相关点和途中发现的有效标签自主决定;支持多关键词、游标翻页和标签扩展。与需求无关 但老年倾向、分享价值双高的视频保存在人工备选池。
搜索过程持久化到:
video_discovery_run:任务输入、意图、状态与计数;video_discovery_search:逐关键词、逐游标页的搜索轨迹;video_discovery_candidate:视频详情、双侧画像、标签、评分、分池与人工审核状态。supply_infra/supply_infra/config.py 管理:
配置文件固定从项目根目录 .env 读取,不依赖当前工作目录。
supply_infra/db/session.py:
pool_pre_ping;get_session() 自动提交;init_db() 通过 ORM metadata 创建缺失表。业务 Agent、API 和定时任务原则上不直接执行 MySQL SQL,而是通过 Repository:
ODPS 查询仍在 supply_infra/odps/client.py 内直接组织 SQL。
当前读取的主要 ODPS 数据:
public_pattern_mining_category:全局分类;pattern_mining_element:树下元素;dwd_multi_demand_pool_di:多策略需求池;dwd_topic_decode_result_di:视频解析和最终选题;dwd_video_produce_plan_stat_hour:真实 ROV/VOV。负责:
当前 ORM 定义 10 张表。
| 表 | 作用 | 关键唯一性/关联 |
|---|---|---|
global_tree_category |
全局分类树节点 | source_id 唯一;parent_id 形成树 |
global_tree_element |
ODPS 元素与分类挂靠 | (name, category_id) 唯一 |
multi_demand_pool_di |
按日同步的策略需求池 | 代码按 (strategy, demand_id, biz_dt) 管理差异 |
demand_belong_category |
需求词归属分类 | name 唯一;当前一词只能保存一个分类 |
demand_belong_pool_rel |
需求词与需求池行的匹配边 | (demand_belong_category_id, multi_demand_pool_di_id) 唯一 |
demand_popularity_stats |
词级六维 avg/count | (demand_category_id, biz_dt) 唯一 |
category_tree_weight |
分类树节点六维聚合和四维排名分 | (category_id, biz_dt) 唯一 |
multi_demand_video_detail |
视频标题与最终选题 JSON | vid 唯一 |
generated_demand |
需求生成 Agent 输出 | 按 run_id 记录,无数据库唯一约束 |
oss_logs |
Agent 日志 HTML 的 OSS 地址 | 按 agent_name 查询 |
global_tree_category.id
├── global_tree_category.parent_id
├── global_tree_element.category_id
├── demand_belong_category.category_id
├── category_tree_weight.category_id
└── generated_demand.category_id
demand_belong_category.id
├── demand_popularity_stats.demand_category_id
├── demand_belong_pool_rel.demand_belong_category_id
└── generated_demand.demand_belong_id
multi_demand_pool_di.id
└── demand_belong_pool_rel.multi_demand_pool_di_id
这些关联在 ORM 中主要以整数 ID 表达,没有使用 SQLAlchemy relationship,也没有显式数据库 ForeignKey。
代码把需求池策略映射为:
| ODPS strategy | 统计字段 | 页面含义 |
|---|---|---|
新热事件 |
ext_pop |
外部热度 |
逐月 |
plat_sust_pop |
平台持续热度 |
去年同期阳历 |
plat_ly_pop |
去年同期热度 |
去年同期阴历 |
plat_ly_pop |
去年同期热度 |
当下供需gap |
recent_pop |
近期热度 |
从近 7 日 dwd_video_produce_plan_stat_hour 获取人工 AGC 和自动 AGC 数据:
rov_diff、vov_diff 较高的一行;demand_name 精确匹配,回填 multi_demand_pool_di。对每个 demand_belong_category.name:
multi_demand_pool_di.demand_name 中执行子串 LIKE 匹配;demand_popularity_stats。因此当前词与需求池的匹配核心是字符串包含关系,不是分词索引、向量匹配或显式语义关系。
分类节点聚合六个独立维度:
每个节点统计其整个子树内有需求词挂靠的节点:
节点维度 avg = Σ(挂靠点 avg × count) / Σ(count)
节点维度 count = Σ(count)
结果写入 category_tree_weight。
update_category_tree_rank_scores.py 只对四项先验排名:
count > 0 的节点参与;(0, 1];total_score 是四个排名分直接相加,范围 [0, 4]。真实 ROV/VOV 会被聚合并通过 API 返回,但当前不进入 total_score。
前端显示“全局热度”时再使用 total_score / 4 映射为 0~1 色阶。
单一维度热度则由前端根据该维所有有数据节点的 avg 做百分位色阶,不直接使用数据库中的四维 rank score 字段。
supply_infra/scheduler/app.py 当前只注册:
02:30:同步昨天的全局树和元素;12:00:同步当天的策略需求池并执行后续完整流水线。时区来自 SCHEDULER_TIMEZONE。
sync_global_tree_odps_to_mysql:
该任务是增量插入,不会根据 ODPS 当前状态自动删除或更新历史分类。
sync_multi_demand_pool_odps_to_mysql 实际执行顺序:
当前有一个同步判断风险:只要 ODPS 和 MySQL 行数相同,就跳过需求池差异拉取;如果内容发生变化但总行数不变,该轮不会发现这些变化。
jobs/ 提供:
api/服务端口:8080。
| 接口 | 作用 |
|---|---|
GET /health |
健康检查 |
GET /api/category-tree?biz_dt=YYYYMMDD |
返回嵌套分类树、六维 avg/count、total_score 和挂靠词数 |
GET /api/demand-belong-category |
返回所有有效需求词挂靠 |
GET /api/demand-belong-category/{id}/videos |
返回需求词关联的视频标题和最终选题 JSON |
GET /api/demand-belong-oss-logs |
返回归类 Agent 的 OSS 日志列表 |
如果 web/dist 存在,FastAPI 会把它挂到 /,同一个 8080 服务同时提供 API 和生产前端。
API 当前是同步 SQLAlchemy 查询,没有分页、鉴权或缓存。
web/| 路由 | 页面 |
|---|---|
/ |
平台全局需求地图 |
/demand-tree |
传统横向分类树 |
/demand-process |
需求归类 Agent 日志 |
/demand-map |
重定向到 / |
当前顶部导航只显示“平台全局需求地图”,另外两个页面有路由但没有导航入口。
GlobalDemandMapView.vue 并行读取:
IcicleHeatTree.vue 使用 Canvas 绘制从左到右的冰柱树:
200px 宽;24000px;Ctrl/Command + 滚轮 缩放;热力标签:
total_score;API 虽然返回真实 ROV/VOV,但当前冰柱图标签没有提供真实 ROV/VOV 的独立切换入口。
分类节点存在需求词时显示搜索标识。点击后通过 DemandPathPanel.vue 展开:
当前分类节点
→ 细节元素/需求词
→ 真实视频实例
→ 最终选题 JSON
这里展示的是 demand_belong_category 需求词,不是 generated_demand 中由生成 Agent 产出的四层平台需求。
当前前端没有读取或展示 generated_demand 的 API。
CategoryTree.vue 使用 Vue DOM 递归组件展示:
读取 demand_belong_category_agent 的 oss_logs,按时间倒序显示,点击打开 Agent 运行过程 HTML。
pyproject.toml 注册:
supply-api;supply-scheduler;supply-visualize。后端:
python -m api
前端:
cd web
npm install
npm run dev
Vite 在 5173,将 /api 代理到 127.0.0.1:8080。
Docker 使用两阶段构建:
web/dist 放入 Python 镜像;python -m api 启动 8080。scripts/docker-deploy.sh 可以构建并向指定镜像仓库推送时间戳标签和 latest。
demand_belong_category.name 唯一,当前一个需求词只能保存一个 category_id。系统提示中的“一词多个节点”无法真实落库。
generated_demand 未进入产品展示需求生成 Agent 已能写 generated_demand,但:
真实 ROV/VOV 已采集、聚合并由 API 返回,但:
total_score;find_agent 的外部证据仍可增强当前已持久化搜索轨迹、正式推荐和人工备选,但画像接口提供的是点赞用户而非真实转发 用户。后续若能补充转发用户画像、分年龄观看留存、相似视频和批量搜索接口,可进一步 提高老年分享判断的直接性与多词多页探索效率。
当前主要关系是:
尚没有独立的语义关系表、关系类型、关系 reason、置信度和人工审核状态。
需求池同步先比较总行数。总行数相同但内容变化时,会跳过 ODPS 明细同步。
当前仓库没有 tests/,并且 .gitignore 直接忽略 tests/,会阻止正常提交测试目录。
现有 README.md、ARCHITECTURE.md 和 agents/README.md 包含已经不存在或未实现的结构,例如:
video_content 模型/Repository;后续应以本文和源码为准,并更新旧文档。
当前机器默认环境:
3.10.19;1.4.51;.venv。源码要求:
>=3.11;>=2.0。因此直接使用当前默认 python3 导入数据库层会在 DeclarativeBase 处失败。应建立 Python 3.11+ 虚拟环境后安装项目依赖。
.env 的 MySQL 配置问题只读连接检查发现:
MYSQL_HOST 的值开头多了一个 =,会导致主机名解析失败;1045 Access denied;由于未通过鉴权,本次无法确认数据库真实表数量、行数和最新业务日。本文的数据结构来自当前 ORM 和 Repository 源码,不冒充线上运行态数据。
当前没有 web/node_modules。Node 20.11.1、npm 10.2.4 已存在,执行前需要先在 web/ 运行 npm install 或 npm ci。
如果继续开发,建议按以下顺序进入代码:
supply_infra/scheduler/jobs/sync_multi_demand_pool_odps_to_mysql.py:理解主业务流水线;supply_infra/db/models/:理解真实数据对象;supply_infra/scheduler/jobs/compute_category_tree_weight.py:理解树上热度;agents/demand_belong_category_agent/:理解需求词挂树;agents/generate_demand_agent/:理解平台需求生成;api/services/category_tree.py:理解后端对前端的数据形态;web/src/components/IcicleHeatTree.vue:理解全局热力图;web/src/components/DemandPathPanel.vue:理解节点证据下钻;supply_agent/agent/:理解底层 Agent 执行机制;supply_agent/logging/:理解可追溯运行日志。当前 SupplyAgent 是一套以 ODPS 和 MySQL 为数据底座、以全局分类树为组织骨架、以自研 Agent 框架完成需求词归类和单维需求生成、并通过 FastAPI/Vue 展示分类热度与视频证据的业务系统;核心数据流水线已经形成,但多挂靠、最终需求产品化、后验反馈进入综合决策、语义关系图和自动化测试仍未完成。