|
|
45 minuti fa | |
|---|---|---|
| agents | 45 minuti fa | |
| alembic | 7 ore fa | |
| api | 7 ore fa | |
| deploy | 22 ore fa | |
| prd | 5 ore fa | |
| problem | 5 ore fa | |
| scripts | 1 giorno fa | |
| sql | 7 ore fa | |
| supply_agent | 1 giorno fa | |
| supply_infra | 1 ora fa | |
| tests | 1 ora fa | |
| visualization | 1 settimana fa | |
| web | 22 ore fa | |
| .dockerignore | 4 giorni fa | |
| .env.example | 7 ore fa | |
| .gitignore | 22 ore fa | |
| ARCHITECTURE.md | 2 giorni fa | |
| Dockerfile | 2 giorni fa | |
| PRD.md | 22 ore fa | |
| README.md | 5 ore fa | |
| alembic.ini | 4 giorni fa | |
| pyproject.toml | 4 giorni fa | |
| requirements.txt | 4 giorni fa |
一个现代、可扩展的 Python AI Agent 框架,支持 OpenRouter 多模型、Tools 工具调用和 Skills 技能系统。
supply_agent/
├── config.py # 配置管理(环境变量 / .env)
├── types.py # 核心类型定义
├── llm/
│ └── client.py # OpenRouter LLM 客户端(OpenAI 兼容 API)
├── tools/
│ ├── base.py # @tool 装饰器 & Tool 类
│ └── registry.py # 工具注册表 & 执行器
├── skills/
│ ├── loader.py # SKILL.md 加载器(兼容 Cursor 格式)
│ └── registry.py # 技能注册表
├── logging/
│ ├── logger.py # 运行日志(.log + .jsonl)
│ ├── parser.py # 日志解析
│ └── visualize.py # HTML 可视化生成
└── agent/
├── core.py # Agent 主类
└── loop.py # ReAct 循环(Reason → Act → Observe)
| 模块 | 职责 |
|---|---|
| LLM Client | 通过 OpenRouter 调用任意模型,支持同步/异步/流式 |
| Tool Registry | 注册工具、自动生成 JSON Schema、执行工具调用 |
| Skill Registry | 从 SKILL.md 加载专业技能指令,按需注入上下文 |
| Agent Loop | ReAct 循环:模型推理 → 工具调用 → 观察结果 → 重复 |
pip install -e ".[dev]"
cp .env.example .env
# 编辑 .env,填入你的 OpenRouter API Key
# 基础对话
python examples/basic_agent.py
# 带自定义工具
python examples/with_tools.py
# 带 Skills 技能
python examples/with_skills.py
# 流式事件
python examples/streaming.py
from supply_agent import Agent
# 使用默认配置(从 .env 读取)
agent = Agent()
# 指定模型
agent = Agent(model="openai/gpt-4o")
# 运行时切换模型
agent.model = "google/gemini-2.5-pro-preview"
from supply_agent.tools import tool
@tool
def search(query: str, limit: int = 10) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent()
agent.tools.from_decorated(search)
result = agent.run("Search for Python tutorials")
print(result.content)
在 skills/ 目录下创建 SKILL.md 文件(兼容 Cursor Skills 格式):
skills/
└── my-skill/
└── SKILL.md
Agent 会自动发现技能。模型可通过内置的 load_skill 工具按需加载专业技能指令。
from supply_agent.types import AgentEventType
for event in agent.stream("Your question"):
if event.type == AgentEventType.TOOL_CALL:
print(f"Calling: {event.data['name']}")
elif event.type == AgentEventType.MESSAGE:
print(event.data["content"])
result = await agent.arun("Your question")
async for event in agent.astream("Your question"):
...
通过 OpenRouter 可使用任意支持的模型,例如:
google/gemini-2.5-flash(默认)anthropic/claude-sonnet-5openai/gpt-4ogoogle/gemini-2.5-pro-previewmeta-llama/llama-4-maverick完整列表见 OpenRouter Models。
| 变量 | 说明 | 默认值 |
|---|---|---|
OPENROUTER_API_KEY |
OpenRouter API 密钥 | (必填) |
OPENROUTER_MODEL |
默认模型 | google/gemini-2.5-flash |
OPENROUTER_TIMEOUT_SECONDS |
单次模型请求超时秒数 | 120 |
AGENT_MAX_ITERATIONS |
最大循环次数 | 20 |
AGENT_TEMPERATURE |
生成温度 | 0.7 |
SKILLS_DIR |
Skills 目录 | skills |
LOGS_DIR |
Agent 运行日志目录 | logs |
LOG_ENABLED |
是否写入运行日志 | true |
find_agent 本地:FIND_AGENT_TIMEOUT_SECONDS(默认 600,即 10 分钟)由 agents/find_agent/runtime.py 读取,不属于通用 supply_agent 配置。
每次 agent.run() 会在 logs/ 下写出:
| 文件 | 说明 |
|---|---|
run_<agent>_<id>.log |
人类可读的完整日志 |
run_<agent>_<id>.jsonl |
结构化事件流(推荐用于可视化) |
运行结束后会自动:生成 .html 可视化页 → 上传 .log / .jsonl / .html 到 OSS(supply_agent/<agent_name>/)→ 写入 MySQL oss_logs。
可用 LOG_OSS_UPLOAD_ENABLED=false 关闭上传。
事件类型:run_start → llm_input → llm_output(含 reasoning)→ tool_call(完整入参/返回)→ … → run_end。
手动生成可视化页面:
# 无参数:为 logs/ 下全部运行生成可视化页面
python scripts/visualize_run.py
# 指定某次运行
python scripts/visualize_run.py logs/run_20260714_134901_9daf6fe1.jsonl
# 最新一次运行,并打开浏览器
python scripts/visualize_run.py --latest --open
# 安装后也可用
supply-visualize --open
页面按步骤展示:LLM 输入(messages / tools)、思考过程、模型输出、工具调用的输入与输出。
后端 FastAPI(端口 8080)一次性返回 global_tree_category 整棵树;前端在仓库根目录 web/(Vue 3)。
# 后端 API
.venv/bin/python -m api
# 或: .venv/bin/supply-api
# 前端(另开终端)
cd web && npm install && npm run dev
GET http://127.0.0.1:8080/api/category-tree/api → 8080)Web 控制台使用本地账号和服务端 Session。系统包含两个固定角色:
admin:全部页面和 API 权限,并可在“用户管理”中创建、禁用和重置账号;user:仅可访问“全局需求地图”和“需求汇总”及其只读 API。每次成功登录都会创建一条独立、不可刷新的会话 Token 记录,同一账号可同时在任意数量 的浏览器或设备登录。退出当前设备只删除当前 Token;管理员修改账号角色、禁用账号或 重置密码时,会统一使该账号的旧 Token 失效。
首次部署前先执行数据库迁移,并通过环境变量创建初始管理员:
alembic upgrade head
export AUTH_BOOTSTRAP_ADMIN_USERNAME=admin
export AUTH_BOOTSTRAP_ADMIN_PASSWORD='replace-with-a-strong-password'
python -m api
初始管理员只会在该用户名不存在时创建,修改环境变量不会重置已有密码。生产 HTTPS
环境必须设置 AUTH_COOKIE_SECURE=true。
pytest
MIT
SupplyAgent 是一个面向平台内容供给的需求汇总工具。
它要解决的核心问题不是“从数据中找出若干热门词”,而是:
汇总来自不同渠道、不同时间尺度和不同验证阶段的需求信号,将其统一挂靠到一棵全局分类树上,形成一张可追溯、可解释、可持续反馈的需求关系图,最终输出数百条平台级需求。
最终结果同时服务于两种业务视角:
项目最终交付的不是一张扁平需求表,而是:
当前项目已经形成了需求汇总的基础业务链路。
项目从上游策略需求池接收不同类型的需求信号。目前已经体现出的主要维度包括:
前四类数据主要回答“什么需求可能值得做”,属于先验信号。
真实 ROV、VOV 主要回答“需求被内容承接并上线后是否真的有效”,属于后验反馈。
global_tree_category 所表达的是一棵统一的全局语义分类树,而不是多棵相互独立的人物树、事件树或情感树。
分类树从左向右按层级展开,例如:
L1 L2 L3 L4 L5
知识 → 历史 → 历史时期 → 古代史
事件 → 社会事件 → 人物故事 → 个人经历
→ 名人故事
树上的正式节点是稳定、抽象、可治理的业务分类。具体的人物、事件、情感和需求表达,不一定都要成为正式树节点。
上游需求中的词语或短语会被挂靠到全局分类树的合适节点上。
归类的业务意义是为需求建立稳定坐标,使需求可以:
当前项目会把需求词级别的数据向分类树节点及其祖先聚合。
因此分类树不仅承担知识组织,还承担需求统计和强度观察:
现有需求生成 Agent 已经体现了以下层次:
来源维度 → 整体方向 → 汇总事件 → 原始需求名称
这个结构可以作为初始生成方式,但最终业务模型需要进一步升级为:
全局分类树 + 需求节点 + 跨分支关系 + 多维证据 + 线上反馈闭环
全局分类树是整个系统唯一的正式分类骨架。
人物、事件、情感、知识、历史、行为等概念分布在同一棵树的不同分支中。平台需求通过同时连接多个分支,表达真实用户兴趣的交织关系。
树内父子关系负责表达:
图上的跨节点关系负责表达:
每条正式平台需求至少挂靠一个真实存在的分类树节点。
一条需求可以同时挂靠多个正式树节点。多个挂靠点共同定义需求,不要求在业务语义上强制区分主次。
例如同一条需求可以同时挂靠:
如果某些统计、展示或资源分配场景必须使用唯一口径,可以额外指定一个“统计主归属节点”。统计主归属只用于避免重复计数,不代表其他挂靠点在业务语义上更次要。
需求的多个挂靠点应共同表达“用户为什么对此感兴趣”,而不是只机械选择最具体的对象节点。
例如需求:
毛泽东在抗日战争和解放战争时期的诗词创作
可能涉及:
需求可以同时挂靠“人物故事”“个人经历”“历史解读”或与诗词创作相关的正式节点,具体取决于上游数据实际支持了哪些用户意图。
“毛泽东”“抗日战争”“解放战争”“诗词创作”等相关语义通过多个挂靠点、需求元素和关系线共同表达。
平台需求必须主要来源于上游数据。
建议的总体比例是:
自由推演不得:
推演需求必须单独标记,并能说明它是从哪些已有数据和关系扩展而来。
上游目前主要提供“关联”关系。
系统必须永久保留原始“关联”边,不得覆盖或篡改。
系统可以在有依据时,把普通关联扩展解释为新的语义关系,例如:
所有推断语义关系必须附带:
证据不足时继续保留“关联”,不强行解释。
整张图由六类核心业务对象组成:
正式、稳定、可治理的分类节点。
分类树节点之间保留原始父子关系,并从左向右按层级展开。
从上游需求中识别出的具体业务元素,例如:
需求元素首先来自上游数据。系统只负责归一别名、识别类型并保留来源。
平台需求是最终业务输出的核心实体,不等同于某个分类节点,也不等同于某个原始需求词。
例如:
毛泽东在抗日战争时期创作过哪些诗词
毛泽东的战争诗词如何反映当时的历史环境
从抗日战争到解放战争,毛泽东诗词主题发生了什么变化
这些需求可以共同涉及“毛泽东、抗日战争、解放战争、写诗”等元素,但它们表达的是不同用户意图,因此应当是不同的平台需求节点。
能够承接某条需求的真实内容,包括:
需求和内容之间允许多对多关系:
但需要区分内容的主需求和辅助需求,避免效果归因失真。
记录需求在各个独立维度上的证据、状态和变化,包括:
正式分类树不是信息下钻的终点。
每一个分类节点下面,还可以继续挂载与它相关的细节元素、真实视频和语言化内容,形成一套“节点下证据子图”:
正式分类节点
↓
细节元素
↓
真实视频实例
↓
明确语言观点或问题
↓
长段讨论
↓
平台需求
这里的“继续下钻”是产品浏览和证据展开,不是继续增加正式分类树层级。
元素、视频、短观点和长段讨论不应被强行定义成 L6、L7、L8 分类节点。它们挂在正式分类节点下面,但属于不同类型的业务对象。
这样可以同时保证:
细节元素是分类节点下面更具体的语义索引。
例如某个“人物故事”或“历史人物”分类节点下面,可以挂载:
元素来自真实上游数据、视频解析、内容标签或人工治理。
一个元素可以:
分类节点下的元素列表应展示:
元素可以继续下钻到具体视频。
视频不是单纯的播放链接,而是需求提取和效果反馈的事实实例。每条视频应尽量保留:
同一条视频可以连接多个元素和需求,但需要区分主要承接和辅助承接。
视频解析后,可以从内容中提取能够独立表达的短语言单元,例如:
例如:
战争环境并未中断诗词创作,反而强化了作品的历史表达。
毛泽东为什么在战争时期持续进行诗词创作?
短语言单元可以用于:
所有语言提取都要保留对应视频、原始片段和提取说明,避免模型生成的语言脱离真实内容。
多个视频、观点和元素还可以进一步形成长段讨论。
长段讨论用于表达短句无法承载的内容,例如:
例如可以围绕以下主题形成讨论:
从抗日战争到解放战争,毛泽东的诗词既记录时代环境,也呈现政治理想、个人情感和历史判断。不同视频分别提供作品背景、创作动机、表达方式和受众理解方面的证据。
长段讨论不是脱离数据的自由文章。它必须引用实际元素、视频和明确观点,并标识:
用户从全局分类树继续下卷时,建议按以下顺序展开:
分类节点概览
→ 高频或高价值元素
→ 元素关联的视频
→ 视频转写与明确观点
→ 多视频形成的长段讨论
→ 从证据中提取的平台需求
→ 需求对应的线上验证结果
每一级都要能够返回上一级,并保留:
节点下钻同时服务两个目的:
正式分类树原有的层级关系,是全图最稳定的结构。
每条平台需求可以同时挂靠多个正式分类节点。每条挂靠关系都应记录:
多个挂靠点共同表达需求的完整语义和跨分支交织关系。
例如“毛泽东的战争诗词如何反映当时的历史环境”可以同时挂靠:
当主题统计需要避免重复计数时,可以为需求设置一个“统计主归属节点”,或者按明确的分摊规则将需求计入多个节点。该统计属性与业务挂靠关系分开管理。
来自上游数据的事实关系,关系类型统一为“关联”。
原始关联边必须保留完整的来源和时间信息。
系统根据多个原始关联、树上位置和上下文推断出的补充关系。
推断边只用于:
推断边不能取代原始关联边。
表达内容承接了哪个需求,以及承接程度:
表达某批内容的真实线上表现如何反向影响需求强度。
表达某个细节元素下挂在哪些正式分类节点下。
该关系应记录元素为何属于该分类、来源和出现次数。一个元素允许同时下挂多个分类节点。
表达某条视频包含、体现或讨论了哪些元素。
上游只有“关联”时保留原始关联;需要扩展为“人物出现、事件涉及、行为发生、观点表达”等语义时,必须附带说明和置信度。
表达明确观点、问题和长段讨论是从哪些视频或视频片段中提取出来的。
语言内容必须能回看原始视频或转写依据。
表达某个明确观点、问题或长段讨论支持、形成或验证了哪些平台需求。
同一个需求可以由多个语言证据共同支持,同一个语言观点也可以被多个需求引用。
整体不是一次性的线性任务,而是持续循环的业务系统。
上游需求信号
↓
数据归一与来源保留
↓
需求元素识别
↓
挂靠全局分类树
↓
保留原始关联并构建关系图
↓
连接真实视频并解析内容
↓
提取明确观点和长段讨论
↓
合并为唯一平台需求
↓
计算先验需求强度
↓
形成主题方向与需求池
↓
搜索或生产承接内容
↓
内容上线并产生真实表现
↓
归因到平台需求
↓
更新需求验证强度与生命周期
↓
影响下一周期的需求排序和内容供给
统一处理:
归一不等于覆盖原始数据。每个标准表达都必须能追溯到原始名称和来源。
优先挂靠现有正式树节点。
如果现有树无法准确表达需求:
Agent 不直接修改正式分类树。
多种来源命中同一个需求时,不按来源拆成重复需求,而是合并为一个平台需求节点。
建议以以下组合判断需求是否相同:
核心用户意图 + 关键对象 + 适用范围或约束
例如:
可以合并为同一平台需求,并保留两个原始表达。
但下面两条不应简单合并:
前者关注作品事实,后者关注作品解读,用户意图不同。
只有在已有数据能够支持时,才允许生成邻近机会需求。
例如上游同时反复出现:
系统可以提出:
从抗日战争到解放战争,毛泽东诗词主题发生了什么变化
但必须说明:
需求不仅可以直接来自上游需求名称,也可以从节点下挂的真实内容中被发现。
提取过程应遵循:
例如,“毛泽东、抗日战争、解放战争、诗词创作”这些元素分别在多条视频中共同出现,并形成以下语言证据:
系统可以据此提取平台需求,但不能只凭模型常识生成。每一条需求都必须能够回到具体视频和语言证据。
真实线上反馈不是普通的附属指标,而是需求汇总系统的核心后验闭环。
平台需求
↓
找到或生产内容
↓
内容上线和分发
↓
获得真实线上表现
↓
校正并归因到需求
↓
更新需求强度
↓
调整下一周期供给
如果需求和内容之间没有明确映射,线上表现就无法准确回流。
每条上线内容至少需要说明:
内容表现还会受以下因素影响:
因此需要把真实表现经过归因校正后,再反向更新需求强度。
不能因为一条质量较差的内容表现不好,就直接判定需求不存在。
真实线上反馈应当能够:
需求不能只保留一个不可解释的总分。
每条需求应至少保留“四类分数 + 一个综合等级”。
回答:
在内容上线验证之前,这个需求有多值得尝试?
主要来源:
这些维度应保持正交,分别展示,然后再形成先验综合判断。
回答:
内容实际承接该需求后,这个需求是否真实成立?
主要来源:
回答:
图上的关系是否进一步增强了该需求成立的可能性?
关系增益只作为低权重补充,不能代替原始数据。
主要风险包括:
业务上可以将其理解为:
最终需求强度
= 先验需求强度
+ 经归因校正的线上验证强度
+ 低权重关系增益
- 风险抑制
具体权重不应在业务框架阶段固定死,应根据历史验证逐步校准。
无论最终采用什么权重,都必须能够展开查看各个组成部分,避免形成黑盒分数。
需求是动态实体,不是一经生成就永久有效。
建议设置以下生命周期状态。
先验信号明显,但尚未有足够内容和线上反馈。
已经找到或安排了承接内容,正在等待有效线上样本。
先验信号增强,或者真实线上表现持续改善。
已经由足量内容、有效样本或多个周期的表现证明。
存在一定信号,但样本较少、数据冲突或结论不稳定。
需求热度、真实表现或用户兴趣持续下降。
经过验证后确认当前不值得继续投入,或供给已经明显过量。
当前强度下降,但预计会在特定日期、节日、纪念日或社会周期重新激活。
没有后验数据不等于后验表现差。
为了避免旧需求永久占据高位:
最终输出采用两层结构。
主题方向不是另建一棵脱离现有分类树的新树。
它应主要来自:
例如:
每个主题方向应包含:
每条平台需求应包含:
数百条需求不按主题平均分配,也不能完全被少数热门主题占满。
采用:
数据驱动为主,最低覆盖和最高集中度约束为辅。
具体原则:
最终使用同一份需求数据提供两个入口。
从左向右浏览:
L1 → L2 → L3 → L4 → L5 → 挂载需求
树图中需要保留:
用户点击某条需求后,应能看到完整追溯链路:
上游数据
→ 原始需求表达
→ 标准化过程
→ 多个挂靠节点及各自理由
→ 关联元素
→ 原始及推断关系
→ 关联内容
→ 线上表现
→ 需求强度变化
以业务执行为目的,支持按以下维度查看:
树图入口和清单入口指向同一批需求实体,不维护两套独立结果。
现有分类树是基础权威骨架,但不是永远不变。
已经审核并进入全局分类树,可以作为需求挂靠节点和统计归属节点。
当大量上游需求反复出现,而现有树无法准确表达其核心意图时,可以提出候选节点。
候选节点必须说明:
Agent 只能提出候选节点,不能直接修改正式树。
人工审核后可以:
任何平台需求必须能够追溯到上游数据。
必须解释:
一个平台需求应表达一个清晰的用户意图。
不能将大量弱相关人物、事件、作品和情感塞入一个“大合集需求”。
同一用户意图仅因为来源、措辞或时间不同,不应被拆成多个重复需求。
所有推演需求必须:
线上反馈必须经过归因和样本校正,避免将内容质量、流量不足或供给不足误认为需求无效。
从业务职责看,整个项目可以划分为九个部分。
负责接收内外部、近期远期、周期性和真实反馈数据。
负责标准化原始表达、识别需求元素、保留来源和原始关联。
负责为需求建立正式分类坐标,并发现分类树表达缺口。
负责把分类树、需求元素、需求节点和关系边组织成统一业务图。
负责展示分类节点下的细节元素,并将元素连接到真实视频、内容标签、转写和线上表现。
负责从真实视频中提取明确事实、观点、问题和长段讨论,并完整保留视频依据和推断说明。
负责形成唯一平台需求,控制需求粒度,避免重复和过度聚合。
负责计算需求强度、生命周期、主题配额和内容供给优先级。
负责把需求与内容连接起来,并将内容真实线上表现回流到需求和分类节点。
整体业务关系是:
信号接入
↓
需求理解
↓
分类树挂靠
↓
树图汇总
↓
元素与视频下钻
↓
语言观点与讨论提取
↓
需求生成与合并
↓
需求决策
↓
内容承接和线上反馈
└──────────────→ 回到需求决策和树图强度
SupplyAgent 的最终业务框架是:
以一棵全局分类树作为稳定骨架,把上游多源需求统一挂树;以平台需求作为独立图节点连接多个分类分支和具体需求元素;以原始数据决定需求是否成立,以低权重语义推断补充关系,以内容真实线上表现持续反向更新需求强度,最终形成几十个主题方向和数百条可执行、可解释、可追溯的平台需求。
本次业务框架讨论过程中形成的全部可视化材料统一保存在:
visualization/
目录中同时包含:
目录结构如下:
visualization/
├── README.md
├── run.sh
├── assets/
│ └── tree.jpeg
├── pages/
│ ├── graph-structure-options.html
│ ├── graph-structure-left-to-right.html
│ ├── business-graph-model.html
│ ├── demand-feedback-loop.html
│ ├── final-output-structure.html
│ ├── node-drilldown-evidence-chain.html
│ ├── waiting-business-scope.html
│ ├── waiting-demand-pipeline.html
│ └── waiting-strength-lifecycle.html
└── service/
└── serve.py
在项目根目录执行:
bash visualization/run.sh
服务默认使用:
http://localhost:8765/
需要指定端口时:
bash visualization/run.sh --port 9000
如果不希望自动打开浏览器:
python3 visualization/service/serve.py
服务只依赖 Python 标准库,不要求安装额外前端依赖。
visualization/pages/ 保留了整个讨论过程,因此早期图稿可能包含后来被修正的中间方案。
最终应以本文档的业务定义为准:
| 图稿 | 主要用途 |
|---|---|
graph-structure-options.html |
记录单一大树、自由图和树骨架关系图三种方案的比较 |
graph-structure-left-to-right.html |
确认整体从左向右的阅读方式 |
business-graph-model.html |
说明分类节点、需求元素、平台需求、关系和证据 |
demand-feedback-loop.html |
说明需求如何找到或生产内容,以及线上表现如何回流 |
final-output-structure.html |
说明主题方向、数百条平台需求和证据卡如何组织 |
node-drilldown-evidence-chain.html |
说明分类节点如何继续下挂元素、视频、明确观点、长段讨论并提取需求 |
waiting-*.html |
保留对话过程中的阶段性页面 |
assets/tree.jpeg |
作为现有全局分类树结构和阅读方式的参考 |
assets/tree2.png |
作为分类节点下挂大量细节元素的参考 |
assets/case.jpeg |
作为元素扩展为明确语言、长段讨论和最终选题的参考 |
本文档完全基于当前代码仓库重新阅读后形成,不继承此前对项目的业务推演或可视化设计理解。
代码快照:
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 业务前端
├── scripts/ 部署、日志上传、日志可视化脚本
├── visualization/ 早期/独立的业务设计可视化材料
├── Dockerfile 前端构建 + Python 运行镜像
├── pyproject.toml Python 包、依赖和命令入口
└── requirements.txt 另一份运行依赖清单
任务 CLI 已并入 supply_infra/scheduler/jobs/(python -m ...)。
模块之间的依赖方向:
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。
demand_pool/tree_weight.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 当前只注册一条任务:
15:00(SCHEDULER_TIMEZONE):串行执行全链路
run_supply_pipeline(全局树 → 需求池 → 分级 → 拓展 → 找片 → AIGC 发布)。sync_global_tree_odps_to_mysql:
该任务是增量插入,不会根据 ODPS 当前状态自动删除或更新历史分类。
supply_infra.scheduler.jobs.demand_pool(sync_multi_demand_pool_odps_to_mysql)实际执行顺序:
当前有一个同步判断风险:只要 ODPS 和 MySQL 行数相同,就跳过需求池差异拉取;如果内容发生变化但总行数不变,该轮不会发现这些变化。
用 python -m 调用(与定时流水线同源):
python -m supply_infra.db:初始化数据库;python -m supply_infra.scheduler.jobs.run_supply_pipeline:全链路;python -m supply_infra.scheduler.jobs.demand_pool:需求池同步(含内部阶段);python -m supply_infra.scheduler.jobs.grade_demand_pool:分级(可加 --retry-failed);也可通过 API POST /api/scheduler/jobs/{job_id}/run 手动触发。
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-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/demand_pool/:理解需求池同步主业务;supply_infra/db/models/:理解真实数据对象;supply_infra/scheduler/jobs/demand_pool/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 展示分类热度与视频证据的业务系统;核心数据流水线已经形成,但多挂靠、最终需求产品化、后验反馈进入综合决策、语义关系图和自动化测试仍未完成。