# SupplyAgent

一个现代、可扩展的 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 循环：模型推理 → 工具调用 → 观察结果 → 重复 |

## 快速开始

### 1. 安装依赖

```bash
pip install -e ".[dev]"
```

### 2. 配置环境变量

```bash
cp .env.example .env
# 编辑 .env，填入你的 OpenRouter API Key
```

### 3. 运行示例

```bash
# 基础对话
python examples/basic_agent.py

# 带自定义工具
python examples/with_tools.py

# 带 Skills 技能
python examples/with_skills.py

# 流式事件
python examples/streaming.py
```

## 使用指南

### 创建 Agent

```python
from supply_agent import Agent

# 使用默认配置（从 .env 读取）
agent = Agent()

# 指定模型
agent = Agent(model="openai/gpt-4o")

# 运行时切换模型
agent.model = "google/gemini-2.5-pro-preview"
```

### 注册 Tools

```python
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

在 `skills/` 目录下创建 `SKILL.md` 文件（兼容 Cursor Skills 格式）：

```
skills/
└── my-skill/
    └── SKILL.md
```

Agent 会自动发现技能。模型可通过内置的 `load_skill` 工具按需加载专业技能指令。

### 流式事件

```python
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"])
```

### 异步 API

```python
result = await agent.arun("Your question")

async for event in agent.astream("Your question"):
  ...
```

## 支持的模型

通过 OpenRouter 可使用任意支持的模型，例如：

- `google/gemini-2.5-flash`（默认）
- `anthropic/claude-sonnet-5`
- `openai/gpt-4o`
- `google/gemini-2.5-pro-preview`
- `meta-llama/llama-4-maverick`

完整列表见 [OpenRouter Models](https://openrouter.ai/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`。

手动生成可视化页面：

```bash
# 无参数：为 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）、思考过程、模型输出、工具调用的输入与输出。

## 全局分类树（Web）

后端 FastAPI（端口 **8080**）一次性返回 `global_tree_category` 整棵树；前端在仓库根目录 `web/`（Vue 3）。

```bash
# 后端 API
.venv/bin/python -m api
# 或: .venv/bin/supply-api

# 前端（另开终端）
cd web && npm install && npm run dev
```

- API: `GET http://127.0.0.1:8080/api/category-tree`
- 前端: http://127.0.0.1:5173 （开发代理 `/api` → 8080）
- 默认展开 3 层，可选择展开层数，支持节点手动展开/收起

### 登录与权限

Web 控制台使用本地账号和服务端 Session。系统包含两个固定角色：

- `admin`：全部页面和 API 权限，并可在“用户管理”中创建、禁用和重置账号；
- `user`：仅可访问“全局需求地图”和“需求汇总”及其只读 API。

每次成功登录都会创建一条独立、不可刷新的会话 Token 记录，同一账号可同时在任意数量
的浏览器或设备登录。退出当前设备只删除当前 Token；管理员修改账号角色、禁用账号或
重置密码时，会统一使该账号的旧 Token 失效。

首次部署前先执行数据库迁移，并通过环境变量创建初始管理员：

```bash
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`。

## 运行测试

```bash
pytest
```

## License

MIT
# SupplyAgent 业务框架设计

## 1. 项目定位

SupplyAgent 是一个面向平台内容供给的需求汇总工具。

它要解决的核心问题不是“从数据中找出若干热门词”，而是：

> 汇总来自不同渠道、不同时间尺度和不同验证阶段的需求信号，将其统一挂靠到一棵全局分类树上，形成一张可追溯、可解释、可持续反馈的需求关系图，最终输出数百条平台级需求。

最终结果同时服务于两种业务视角：

1. 从全局分类树和关系图观察平台需求版图、层级、覆盖和交织关系。
2. 从排序后的需求清单直接开展内容发现、内容生产、供给调度和效果验证。

项目最终交付的不是一张扁平需求表，而是：

- 一棵稳定的全局分类树；
- 数百条挂靠在树上的平台需求；
- 需求与多个树节点之间的关系线；
- 需求背后的多维数据证据；
- 需求与内容、线上表现之间的反馈闭环。

---

## 2. 对现有项目业务结构的理解

当前项目已经形成了需求汇总的基础业务链路。

### 2.1 上游需求池

项目从上游策略需求池接收不同类型的需求信号。目前已经体现出的主要维度包括：

- 外部热度；
- 平台持续热度；
- 平台去年同期热度；
- 平台近期供需缺口；
- 近 7 日真实 ROV；
- 近 7 日真实 VOV。

前四类数据主要回答“什么需求可能值得做”，属于先验信号。

真实 ROV、VOV 主要回答“需求被内容承接并上线后是否真的有效”，属于后验反馈。

### 2.2 全局分类树

`global_tree_category` 所表达的是一棵统一的全局语义分类树，而不是多棵相互独立的人物树、事件树或情感树。

分类树从左向右按层级展开，例如：

```text
L1        L2        L3          L4          L5
知识  →   历史  →   历史时期  →  古代史

事件  →   社会事件  →  人物故事  →  个人经历
                              →  名人故事
```

树上的正式节点是稳定、抽象、可治理的业务分类。具体的人物、事件、情感和需求表达，不一定都要成为正式树节点。

### 2.3 需求词归类

上游需求中的词语或短语会被挂靠到全局分类树的合适节点上。

归类的业务意义是为需求建立稳定坐标，使需求可以：

- 沿树向上汇总；
- 在同类需求间比较；
- 观察不同分支的需求覆盖；
- 计算分类节点在不同信号维度下的强度；
- 支撑后续平台需求生成。

### 2.4 分类节点强度

当前项目会把需求词级别的数据向分类树节点及其祖先聚合。

因此分类树不仅承担知识组织，还承担需求统计和强度观察：

- 叶子或挂载节点反映局部、具体需求；
- 中间节点反映某个业务方向的整体强度；
- 高层节点反映平台需求版图中的大方向。

### 2.5 平台需求生成

现有需求生成 Agent 已经体现了以下层次：

```text
来源维度 → 整体方向 → 汇总事件 → 原始需求名称
```

这个结构可以作为初始生成方式，但最终业务模型需要进一步升级为：

```text
全局分类树 + 需求节点 + 跨分支关系 + 多维证据 + 线上反馈闭环
```

---

## 3. 核心设计原则

### 3.1 一棵树，而不是多棵业务树

全局分类树是整个系统唯一的正式分类骨架。

人物、事件、情感、知识、历史、行为等概念分布在同一棵树的不同分支中。平台需求通过同时连接多个分支，表达真实用户兴趣的交织关系。

### 3.2 以树确定归属，以图表达关系

树内父子关系负责表达：

- 层级；
- 上下位关系；
- 统计汇总路径；
- 业务覆盖；
- 正式分类治理。

图上的跨节点关系负责表达：

- 一个需求同时涉及哪些分类分支；
- 人物、事件、行为、情感等元素如何共同组成需求；
- 不同需求之间是否存在重合、包含、相似或关联；
- 哪些上游信号共同支持同一个需求。

### 3.3 每条需求必须挂树

每条正式平台需求至少挂靠一个真实存在的分类树节点。

一条需求可以同时挂靠多个正式树节点。多个挂靠点共同定义需求，不要求在业务语义上强制区分主次。

例如同一条需求可以同时挂靠：

- 人物故事；
- 个人经历；
- 历史解读；
- 诗词创作；
- 与具体战争时期相关的历史分类。

如果某些统计、展示或资源分配场景必须使用唯一口径，可以额外指定一个“统计主归属节点”。统计主归属只用于避免重复计数，不代表其他挂靠点在业务语义上更次要。

### 3.4 用户意图优先

需求的多个挂靠点应共同表达“用户为什么对此感兴趣”，而不是只机械选择最具体的对象节点。

例如需求：

> 毛泽东在抗日战争和解放战争时期的诗词创作

可能涉及：

- 人物对象：毛泽东；
- 历史背景：抗日战争、解放战争；
- 行为：写诗、诗词创作；
- 内容意图：人物经历、作品背景、历史解读。

需求可以同时挂靠“人物故事”“个人经历”“历史解读”或与诗词创作相关的正式节点，具体取决于上游数据实际支持了哪些用户意图。

“毛泽东”“抗日战争”“解放战争”“诗词创作”等相关语义通过多个挂靠点、需求元素和关系线共同表达。

### 3.5 数据驱动为主，自由推演为辅

平台需求必须主要来源于上游数据。

建议的总体比例是：

- 80%～90% 为上游数据直接支持的核心需求；
- 10%～20% 为基于已有数据、节点和关联关系形成的邻近机会需求。

自由推演不得：

- 凭空创造没有来源的人物、事件、情感或主题；
- 用模型常识替代上游证据；
- 将弱关联包装成确定关系；
- 与数据支持的需求混淆展示。

推演需求必须单独标记，并能说明它是从哪些已有数据和关系扩展而来。

### 3.6 原始事实与语义推断分离

上游目前主要提供“关联”关系。

系统必须永久保留原始“关联”边，不得覆盖或篡改。

系统可以在有依据时，把普通关联扩展解释为新的语义关系，例如：

- 人物参与事件；
- 事件发生于某个历史时期；
- 人物在某个时期进行诗词创作；
- 作品表达某种精神或情感；
- 某个历史背景影响作品主题。

所有推断语义关系必须附带：

- 推断说明；
- 原始关联证据；
- 数据来源；
- 置信度；
- 是否经过人工确认；
- 创建或更新时间。

证据不足时继续保留“关联”，不强行解释。

---

## 4. 整体业务图结构

整张图由六类核心业务对象组成：

- 分类树节点；
- 细节元素；
- 视频或内容实例；
- 明确语言观点与长段讨论；
- 平台需求；
- 证据、状态与反馈。

### 4.1 分类树节点

正式、稳定、可治理的分类节点。

分类树节点之间保留原始父子关系，并从左向右按层级展开。

### 4.2 需求元素节点

从上游需求中识别出的具体业务元素，例如：

- 人物：毛泽东；
- 历史事件：抗日战争、解放战争；
- 行为：写诗；
- 作品或内容：战争时期诗词；
- 情感或精神：革命乐观主义、家国情怀。

需求元素首先来自上游数据。系统只负责归一别名、识别类型并保留来源。

### 4.3 平台需求节点

平台需求是最终业务输出的核心实体，不等同于某个分类节点，也不等同于某个原始需求词。

例如：

> 毛泽东在抗日战争时期创作过哪些诗词

> 毛泽东的战争诗词如何反映当时的历史环境

> 从抗日战争到解放战争，毛泽东诗词主题发生了什么变化

这些需求可以共同涉及“毛泽东、抗日战争、解放战争、写诗”等元素，但它们表达的是不同用户意图，因此应当是不同的平台需求节点。

### 4.4 内容节点

能够承接某条需求的真实内容，包括：

- 已有内容；
- 搜索发现的内容；
- 新生产的内容；
- 已上线并获得真实反馈的内容。

需求和内容之间允许多对多关系：

- 一条需求可以由多条内容承接；
- 一条内容也可能同时承接多个需求。

但需要区分内容的主需求和辅助需求，避免效果归因失真。

### 4.5 证据与状态节点

记录需求在各个独立维度上的证据、状态和变化，包括：

- 外部热度；
- 平台持续热度；
- 去年同期热度；
- 近期供需缺口；
- 真实 ROV、VOV；
- 样本量；
- 数据日期；
- 来源可信度；
- 生命周期状态；
- 推演标记；
- 风险或抑制原因。

### 4.6 分类节点下的证据子图

正式分类树不是信息下钻的终点。

每一个分类节点下面，还可以继续挂载与它相关的细节元素、真实视频和语言化内容，形成一套“节点下证据子图”：

```text
正式分类节点
   ↓
细节元素
   ↓
真实视频实例
   ↓
明确语言观点或问题
   ↓
长段讨论
   ↓
平台需求
```

这里的“继续下钻”是产品浏览和证据展开，不是继续增加正式分类树层级。

元素、视频、短观点和长段讨论不应被强行定义成 L6、L7、L8 分类节点。它们挂在正式分类节点下面，但属于不同类型的业务对象。

这样可以同时保证：

- 全局分类树结构稳定、纯净；
- 节点可以持续承载越来越丰富的细节信息；
- 用户能够从抽象分类一路下卷到真实内容；
- 平台需求能够追溯到具体视频和语言证据；
- 真实内容表现能够逐层回流到元素、需求和分类节点。

### 4.7 细节元素

细节元素是分类节点下面更具体的语义索引。

例如某个“人物故事”或“历史人物”分类节点下面，可以挂载：

- 毛泽东；
- 抗日战争；
- 解放战争；
- 写诗；
- 诗词创作；
- 战争时期诗词；
- 革命乐观主义；
- 家国情怀。

元素来自真实上游数据、视频解析、内容标签或人工治理。

一个元素可以：

- 挂在多个正式分类节点下；
- 与其他元素保留原始“关联”关系；
- 连接多条真实视频；
- 被多个平台需求共同引用；
- 根据视频数量、出现次数和线上表现形成元素强度。

分类节点下的元素列表应展示：

- 元素名称；
- 元素类型；
- 出现次数；
- 关联视频数量；
- 支持的平台需求数量；
- 线上表现摘要；
- 数据来源；
- 更新时间。

### 4.8 视频实例

元素可以继续下钻到具体视频。

视频不是单纯的播放链接，而是需求提取和效果反馈的事实实例。每条视频应尽量保留：

- 视频标题；
- 视频链接；
- 作者和发布时间；
- 视频转写或内容描述；
- 命中的分类节点；
- 命中的细节元素；
- 对应的平台需求；
- 点赞、评论、分享、收藏等表现；
- 真实 ROV、VOV；
- 内容质量或解析可信度；
- 是否属于主承接内容。

同一条视频可以连接多个元素和需求，但需要区分主要承接和辅助承接。

### 4.9 明确语言观点

视频解析后，可以从内容中提取能够独立表达的短语言单元，例如：

- 一个明确事实；
- 一个观点；
- 一个判断；
- 一个问题；
- 一句可用于需求命名的话；
- 一条用户容易理解和传播的内容结论。

例如：

> 战争环境并未中断诗词创作，反而强化了作品的历史表达。

> 毛泽东为什么在战争时期持续进行诗词创作？

短语言单元可以用于：

- 快速理解视频贡献了什么；
- 比较不同视频是否表达同一观点；
- 形成需求候选名称；
- 聚合同义需求；
- 支撑需求关系说明。

所有语言提取都要保留对应视频、原始片段和提取说明，避免模型生成的语言脱离真实内容。

### 4.10 长段讨论

多个视频、观点和元素还可以进一步形成长段讨论。

长段讨论用于表达短句无法承载的内容，例如：

- 一个问题的完整背景；
- 多个视频观点之间的共同点和差异；
- 历史过程和人物行为之间的联系；
- 一个需求为什么值得形成；
- 某个需求存在什么争议；
- 内容供给可以从哪些角度展开；
- 线上反馈为什么支持或否定该需求。

例如可以围绕以下主题形成讨论：

> 从抗日战争到解放战争，毛泽东的诗词既记录时代环境，也呈现政治理想、个人情感和历史判断。不同视频分别提供作品背景、创作动机、表达方式和受众理解方面的证据。

长段讨论不是脱离数据的自由文章。它必须引用实际元素、视频和明确观点，并标识：

- 讨论依据；
- 主要证据；
- 推断内容；
- 不确定点；
- 支持或关联的平台需求。

### 4.11 节点下钻的产品形态

用户从全局分类树继续下卷时，建议按以下顺序展开：

```text
分类节点概览
→ 高频或高价值元素
→ 元素关联的视频
→ 视频转写与明确观点
→ 多视频形成的长段讨论
→ 从证据中提取的平台需求
→ 需求对应的线上验证结果
```

每一级都要能够返回上一级，并保留：

- 来源；
- 出现次数；
- 贡献度；
- 时间；
- 置信度；
- 真实线上表现；
- 与需求之间的关系。

节点下钻同时服务两个目的：

1. 展示：让业务人员理解这个分类节点下具体有什么。
2. 提取：让系统从元素、视频和语言证据中发现、生成、合并和验证需求。

---

## 5. 图中的关系类型

### 5.1 树内父子关系

正式分类树原有的层级关系，是全图最稳定的结构。

### 5.2 需求多挂靠关系

每条平台需求可以同时挂靠多个正式分类节点。每条挂靠关系都应记录：

- 挂靠节点；
- 挂靠理由；
- 支持该挂靠的上游数据；
- 关系置信度；
- 是否属于正式挂靠或待审核挂靠；
- 生效和更新时间。

多个挂靠点共同表达需求的完整语义和跨分支交织关系。

例如“毛泽东的战争诗词如何反映当时的历史环境”可以同时挂靠：

- 历史解读；
- 人物故事；
- 个人经历；
- 与诗词创作相关的正式节点；
- 与抗日战争、解放战争所处历史时期相关的正式节点。

当主题统计需要避免重复计数时，可以为需求设置一个“统计主归属节点”，或者按明确的分摊规则将需求计入多个节点。该统计属性与业务挂靠关系分开管理。

### 5.4 原始关联关系

来自上游数据的事实关系，关系类型统一为“关联”。

原始关联边必须保留完整的来源和时间信息。

### 5.5 推断语义关系

系统根据多个原始关联、树上位置和上下文推断出的补充关系。

推断边只用于：

- 增强解释；
- 帮助聚类；
- 提供低权重排序增益；
- 辅助发现邻近机会需求。

推断边不能取代原始关联边。

### 5.6 需求与内容关系

表达内容承接了哪个需求，以及承接程度：

- 主承接；
- 辅助承接；
- 部分覆盖；
- 待验证；
- 已上线；
- 已形成有效样本。

### 5.7 内容表现回流关系

表达某批内容的真实线上表现如何反向影响需求强度。

### 5.8 分类节点与元素关系

表达某个细节元素下挂在哪些正式分类节点下。

该关系应记录元素为何属于该分类、来源和出现次数。一个元素允许同时下挂多个分类节点。

### 5.9 元素与视频关系

表达某条视频包含、体现或讨论了哪些元素。

上游只有“关联”时保留原始关联；需要扩展为“人物出现、事件涉及、行为发生、观点表达”等语义时，必须附带说明和置信度。

### 5.10 视频与语言关系

表达明确观点、问题和长段讨论是从哪些视频或视频片段中提取出来的。

语言内容必须能回看原始视频或转写依据。

### 5.11 语言与需求关系

表达某个明确观点、问题或长段讨论支持、形成或验证了哪些平台需求。

同一个需求可以由多个语言证据共同支持，同一个语言观点也可以被多个需求引用。

---

## 6. 需求汇总业务流程

整体不是一次性的线性任务，而是持续循环的业务系统。

```text
上游需求信号
    ↓
数据归一与来源保留
    ↓
需求元素识别
    ↓
挂靠全局分类树
    ↓
保留原始关联并构建关系图
    ↓
连接真实视频并解析内容
    ↓
提取明确观点和长段讨论
    ↓
合并为唯一平台需求
    ↓
计算先验需求强度
    ↓
形成主题方向与需求池
    ↓
搜索或生产承接内容
    ↓
内容上线并产生真实表现
    ↓
归因到平台需求
    ↓
更新需求验证强度与生命周期
    ↓
影响下一周期的需求排序和内容供给
```

### 6.1 数据归一

统一处理：

- 同义词；
- 简称与全称；
- 人物别名；
- 事件的不同表达；
- 错别字；
- 中英文表达；
- 时间和周期口径。

归一不等于覆盖原始数据。每个标准表达都必须能追溯到原始名称和来源。

### 6.2 意图挂树

优先挂靠现有正式树节点。

如果现有树无法准确表达需求：

1. 挂到当前最可靠的上级节点；
2. 标记表达缺口；
3. 进入候选节点治理区；
4. 由人工审核是否需要增加正式节点。

Agent 不直接修改正式分类树。

### 6.3 需求合并

多种来源命中同一个需求时，不按来源拆成重复需求，而是合并为一个平台需求节点。

建议以以下组合判断需求是否相同：

```text
核心用户意图 + 关键对象 + 适用范围或约束
```

例如：

- “毛泽东抗战时期写的诗”
- “毛泽东在抗日战争阶段创作的诗词”

可以合并为同一平台需求，并保留两个原始表达。

但下面两条不应简单合并：

- “毛泽东在抗日战争时期创作过哪些诗词”
- “毛泽东的抗战诗词表达了什么情感”

前者关注作品事实，后者关注作品解读，用户意图不同。

### 6.4 少量邻近推演

只有在已有数据能够支持时，才允许生成邻近机会需求。

例如上游同时反复出现：

- 毛泽东；
- 抗日战争；
- 解放战争；
- 诗词创作；

系统可以提出：

> 从抗日战争到解放战争，毛泽东诗词主题发生了什么变化

但必须说明：

- 它由哪些上游关联组合而来；
- 上游是否直接出现过该完整需求；
- 哪些部分属于模型推断；
- 当前属于核心需求还是机会需求。

### 6.5 从视频和语言证据中提取需求

需求不仅可以直接来自上游需求名称，也可以从节点下挂的真实内容中被发现。

提取过程应遵循：

1. 从分类节点下的高频、高表现元素开始；
2. 找到承载这些元素的真实视频；
3. 从视频转写和解析结果中抽取明确事实、观点和问题；
4. 对多个视频的语言单元进行归并和比较；
5. 形成有真实内容依据的需求候选；
6. 将候选需求挂回一个或多个正式分类节点；
7. 与已有平台需求去重或合并；
8. 标记数据来源、视频证据和推断部分。

例如，“毛泽东、抗日战争、解放战争、诗词创作”这些元素分别在多条视频中共同出现，并形成以下语言证据：

- 毛泽东为何在战争时期持续进行诗词创作；
- 战争环境如何影响诗词主题；
- 抗日战争和解放战争时期的作品表达有何变化。

系统可以据此提取平台需求，但不能只凭模型常识生成。每一条需求都必须能够回到具体视频和语言证据。

---

## 7. 需求—内容—线上表现闭环

真实线上反馈不是普通的附属指标，而是需求汇总系统的核心后验闭环。

```text
平台需求
   ↓
找到或生产内容
   ↓
内容上线和分发
   ↓
获得真实线上表现
   ↓
校正并归因到需求
   ↓
更新需求强度
   ↓
调整下一周期供给
```

### 7.1 建立需求与内容的映射

如果需求和内容之间没有明确映射，线上表现就无法准确回流。

每条上线内容至少需要说明：

- 主要承接哪个平台需求；
- 是否辅助承接其他需求；
- 对需求的覆盖程度；
- 内容上线时间；
- 当前是否形成有效样本。

### 7.2 线上表现不能直接等同于需求表现

内容表现还会受以下因素影响：

- 内容本身质量；
- 标题、封面和表达方式；
- 分发流量；
- 发布时间；
- 平台环境；
- 同类内容竞争；
- 供给数量；
- 样本量是否充足。

因此需要把真实表现经过归因校正后，再反向更新需求强度。

不能因为一条质量较差的内容表现不好，就直接判定需求不存在。

### 7.3 反馈影响需求强度

真实线上反馈应当能够：

- 验证一个新需求是否真实成立；
- 提高持续表现良好需求的强度；
- 降低持续表现较差需求的优先级；
- 判断某个需求是否已经过度供给；
- 发现先验热度不高但真实表现很好的潜在需求；
- 影响分类树中相关节点的整体强度；
- 影响下一周期的主题配额和供给策略。

---

## 8. 需求强度模型

需求不能只保留一个不可解释的总分。

每条需求应至少保留“四类分数 + 一个综合等级”。

### 8.1 先验需求分

回答：

> 在内容上线验证之前，这个需求有多值得尝试？

主要来源：

- 外部热度；
- 平台近期供需缺口；
- 平台持续热度；
- 去年同期或周期性热度；
- 上游来源数量；
- 样本量和数据时效性。

这些维度应保持正交，分别展示，然后再形成先验综合判断。

### 8.2 线上验证分

回答：

> 内容实际承接该需求后，这个需求是否真实成立？

主要来源：

- 真实 ROV；
- 真实 VOV；
- 消费、互动或转化表现；
- 多条内容的一致性；
- 多个周期的稳定性；
- 归因校正结果。

### 8.3 关系增益分

回答：

> 图上的关系是否进一步增强了该需求成立的可能性？

关系增益只作为低权重补充，不能代替原始数据。

### 8.4 风险抑制分

主要风险包括：

- 后验效果持续较差；
- 数据维度之间明显冲突；
- 样本量不足；
- 数据过期；
- 内容承接失败；
- 同类需求过度重复；
- 某个主题过度集中；
- 需求主要来自自由推演；
- 关系推断置信度较低。

### 8.5 综合需求强度

业务上可以将其理解为：

```text
最终需求强度
= 先验需求强度
+ 经归因校正的线上验证强度
+ 低权重关系增益
- 风险抑制
```

具体权重不应在业务框架阶段固定死，应根据历史验证逐步校准。

无论最终采用什么权重，都必须能够展开查看各个组成部分，避免形成黑盒分数。

---

## 9. 需求生命周期

需求是动态实体，不是一经生成就永久有效。

建议设置以下生命周期状态。

### 9.1 新机会

先验信号明显，但尚未有足够内容和线上反馈。

### 9.2 待验证

已经找到或安排了承接内容，正在等待有效线上样本。

### 9.3 增长需求

先验信号增强，或者真实线上表现持续改善。

### 9.4 已验证需求

已经由足量内容、有效样本或多个周期的表现证明。

### 9.5 观察需求

存在一定信号，但样本较少、数据冲突或结论不稳定。

### 9.6 衰退需求

需求热度、真实表现或用户兴趣持续下降。

### 9.7 抑制需求

经过验证后确认当前不值得继续投入，或供给已经明显过量。

### 9.8 周期需求

当前强度下降，但预计会在特定日期、节日、纪念日或社会周期重新激活。

### 9.9 新老需求公平

没有后验数据不等于后验表现差。

为了避免旧需求永久占据高位：

- 新需求主要依据先验分进入探索区；
- 老需求的历史表现需要时间衰减；
- 每个主题保留一定探索额度；
- 只有形成有效样本后才提高后验权重；
- 已验证需求也要接受持续复验。

---

## 10. 主题方向与数百条需求的组织

最终输出采用两层结构。

### 10.1 上层：主题方向

主题方向不是另建一棵脱离现有分类树的新树。

它应主要来自：

- 全局分类树的中高层节点；
- 某个树分支下的需求聚类；
- 多个相关分支共同形成的稳定业务方向。

例如：

- 历史人物故事；
- 历史人物个人经历；
- 战争历史与人物；
- 历史人物作品解读；
- 历史事件影响；
- 人物精神与情感。

每个主题方向应包含：

- 主题名称；
- 对应的分类树范围；
- 覆盖的用户意图；
- 需求数量；
- 各生命周期数量；
- 整体需求强度；
- 主要数据来源；
- 趋势；
- 需求集中度；
- 探索需求占比。

### 10.2 下层：平台需求

每条平台需求应包含：

- 唯一需求 ID；
- 标准需求名称；
- 原始需求表达；
- 多个正式挂靠节点；
- 可选的统计主归属节点；
- 每个挂靠点的理由和证据；
- 关联需求元素；
- 原始关联边；
- 推断语义边及说明；
- 所属主题方向；
- 各维先验信号；
- 线上验证结果；
- 关联内容及归因情况；
- 需求强度；
- 生命周期；
- 是否属于推演需求；
- 风险和抑制原因；
- 数据更新时间；
- 强度变化历史。

### 10.3 需求数量分配

数百条需求不按主题平均分配，也不能完全被少数热门主题占满。

采用：

> 数据驱动为主，最低覆盖和最高集中度约束为辅。

具体原则：

- 强信号主题可以拥有更多需求；
- 弱主题允许较少需求；
- 无真实信号的主题可以暂时为空；
- 重要主题保留最低覆盖；
- 单一主题设置最高集中度；
- 每个主题保留少量探索位；
- 推演需求总量保持在受控范围。

---

## 11. 最终展示方式

最终使用同一份需求数据提供两个入口。

### 11.1 全局树图入口

从左向右浏览：

```text
L1 → L2 → L3 → L4 → L5 → 挂载需求
```

树图中需要保留：

- 原有树内父子线；
- 分类节点的需求数量和强度；
- 平台需求与多个正式树节点之间的挂靠线；
- 可选的统计主归属标记；
- 跨分支关联线；
- 原始关联与推断关系的区别；
- 需求与内容的承接关系；
- 线上反馈回流关系。

用户点击某条需求后，应能看到完整追溯链路：

```text
上游数据
→ 原始需求表达
→ 标准化过程
→ 多个挂靠节点及各自理由
→ 关联元素
→ 原始及推断关系
→ 关联内容
→ 线上表现
→ 需求强度变化
```

### 11.2 需求清单入口

以业务执行为目的，支持按以下维度查看：

- 主题方向；
- 需求强度；
- 生命周期；
- 多个挂靠节点；
- 可选的统计主归属节点；
- 数据来源；
- 是否已经验证；
- 是否已经有内容承接；
- 是否存在供需缺口；
- 是否属于推演需求；
- 更新时间。

树图入口和清单入口指向同一批需求实体，不维护两套独立结果。

---

## 12. 分类树治理

现有分类树是基础权威骨架，但不是永远不变。

### 12.1 正式节点

已经审核并进入全局分类树，可以作为需求挂靠节点和统计归属节点。

### 12.2 候选节点

当大量上游需求反复出现，而现有树无法准确表达其核心意图时，可以提出候选节点。

候选节点必须说明：

- 哪些需求无法被现有节点准确表达；
- 当前临时挂靠在哪里；
- 出现频率和持续周期；
- 预期父节点；
- 与现有节点的差异；
- 新增后能解决什么业务问题。

### 12.3 人工审核

Agent 只能提出候选节点，不能直接修改正式树。

人工审核后可以：

- 接受为正式节点；
- 与现有节点合并；
- 继续观察；
- 拒绝新增。

---

## 13. 业务质量控制

### 13.1 可追溯

任何平台需求必须能够追溯到上游数据。

### 13.2 可解释

必须解释：

- 为什么形成该需求；
- 为什么挂在这些节点；
- 为什么与其他表达合并或不合并；
- 为什么获得当前强度和生命周期；
- 线上反馈如何影响了它。

### 13.3 防止过度聚合

一个平台需求应表达一个清晰的用户意图。

不能将大量弱相关人物、事件、作品和情感塞入一个“大合集需求”。

### 13.4 防止过度拆分

同一用户意图仅因为来源、措辞或时间不同，不应被拆成多个重复需求。

### 13.5 防止自由推演失控

所有推演需求必须：

- 有数据起点；
- 有推演路径；
- 有置信度；
- 有独立标识；
- 有比例限制；
- 可以被人工拒绝。

### 13.6 防止后验误判

线上反馈必须经过归因和样本校正，避免将内容质量、流量不足或供给不足误认为需求无效。

---

## 14. 项目业务模块的总体框架

从业务职责看，整个项目可以划分为九个部分。

### 14.1 信号接入

负责接收内外部、近期远期、周期性和真实反馈数据。

### 14.2 需求理解

负责标准化原始表达、识别需求元素、保留来源和原始关联。

### 14.3 分类树挂靠

负责为需求建立正式分类坐标，并发现分类树表达缺口。

### 14.4 树图汇总

负责把分类树、需求元素、需求节点和关系边组织成统一业务图。

### 14.5 元素与内容下钻

负责展示分类节点下的细节元素，并将元素连接到真实视频、内容标签、转写和线上表现。

### 14.6 语言观点与讨论提取

负责从真实视频中提取明确事实、观点、问题和长段讨论，并完整保留视频依据和推断说明。

### 14.7 需求生成与合并

负责形成唯一平台需求，控制需求粒度，避免重复和过度聚合。

### 14.8 需求决策

负责计算需求强度、生命周期、主题配额和内容供给优先级。

### 14.9 内容反馈闭环

负责把需求与内容连接起来，并将内容真实线上表现回流到需求和分类节点。

整体业务关系是：

```text
信号接入
   ↓
需求理解
   ↓
分类树挂靠
   ↓
树图汇总
   ↓
元素与视频下钻
   ↓
语言观点与讨论提取
   ↓
需求生成与合并
   ↓
需求决策
   ↓
内容承接和线上反馈
   └──────────────→ 回到需求决策和树图强度
```

---

## 15. 一句话总结

SupplyAgent 的最终业务框架是：

> 以一棵全局分类树作为稳定骨架，把上游多源需求统一挂树；以平台需求作为独立图节点连接多个分类分支和具体需求元素；以原始数据决定需求是否成立，以低权重语义推断补充关系，以内容真实线上表现持续反向更新需求强度，最终形成几十个主题方向和数百条可执行、可解释、可追溯的平台需求。

---

## 16. 业务可视化资料与服务

本次业务框架讨论过程中形成的全部可视化材料统一保存在：

```text
visualization/
```

目录中同时包含：

- 业务图结构方案；
- 从左向右的树图示意；
- 业务节点和关系模型；
- 需求—内容—线上表现反馈闭环；
- 最终主题和平台需求输出结构；
- 对话过程中的阶段性可视页面；
- 用户提供的全局分类树局部参考图；
- 用于本地浏览全部图稿的可视化服务。

目录结构如下：

```text
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
```

### 16.1 启动可视化服务

在项目根目录执行：

```bash
bash visualization/run.sh
```

服务默认使用：

```text
http://localhost:8765/
```

需要指定端口时：

```bash
bash visualization/run.sh --port 9000
```

如果不希望自动打开浏览器：

```bash
python3 visualization/service/serve.py
```

服务只依赖 Python 标准库，不要求安装额外前端依赖。

### 16.2 图稿与最终业务口径的关系

`visualization/pages/` 保留了整个讨论过程，因此早期图稿可能包含后来被修正的中间方案。

最终应以本文档的业务定义为准：

- 整体只有一棵全局分类树；
- 分类树从左向右按层级展开；
- 平台需求是独立的图节点；
- 一条需求可以同时挂靠多个正式树节点；
- 多个挂靠点共同定义需求，不强制区分业务主次；
- 如统计需要唯一口径，可以另设“统计主归属节点”；
- 上游原始“关联”边永久保留；
- 推断语义关系必须附带说明、来源和置信度；
- 内容的真实线上表现必须反向更新需求强度。

### 16.3 各图稿用途

| 图稿 | 主要用途 |
|---|---|
| `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` | 作为元素扩展为明确语言、长段讨论和最终选题的参考 |


# SupplyAgent 代码仓库结构总结

## 1. 阅读基线

本文档完全基于当前代码仓库重新阅读后形成，不继承此前对项目的业务推演或可视化设计理解。

代码快照：

- 分支：`feature/zhangbo`
- 阅读时提交：`2ab77ce`
- Python 要求：`>= 3.11`
- 后端：FastAPI + SQLAlchemy 2.x + PyMySQL
- Agent：OpenRouter/OpenAI 兼容 API + 自研 Tool/Skill/ReAct 框架
- 数据源：ODPS（MaxCompute）
- 数据库：MySQL
- 对象存储：阿里云 OSS
- 前端：Vue 3 + TypeScript + Vite

`.env` 已被 `.gitignore` 忽略。本文不会记录其中的密码、密钥、数据库地址或 Token。

---

## 2. 当前项目的实际定位

当前 SupplyAgent 由两部分组成：

1. 一套通用 AI Agent 运行框架；
2. 一套围绕“需求词、全局分类树、热度、视频和需求生成”的业务系统。

业务系统当前主要完成：

- 从 ODPS 同步全局分类树和树下元素；
- 从 ODPS 同步多策略需求池；
- 将需求词挂到分类树节点；
- 汇总四项先验热度和真实 ROV/VOV；
- 把词级热度沿分类树向上聚合；
- 按单一热度维度生成并保存平台需求；
- 把需求词连接到真实视频和视频最终选题；
- 通过 API 和 Vue 页面展示分类树、热力图和下钻证据；
- 记录 Agent 完整执行过程，生成 HTML，上传 OSS 并在 MySQL 留档。

当前系统的核心业务链路是：

```text
ODPS 全局分类与元素
        ↓
MySQL 全局分类树
        ↑
需求池词语 → 归类 Agent → 需求词挂靠
        ↓
四项先验 + 真实 ROV/VOV
        ↓
词级统计 → 分类树节点聚合 → 四维全局排名
        ↓
需求生成 Agent → generated_demand
        ↓
FastAPI → Vue 分类树/热力图/证据下钻
```

---

## 3. 仓库分层

```text
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 ...`）。

模块之间的依赖方向：

```text
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
```

---

## 4. 通用 Agent 框架：`supply_agent/`

### 4.1 `supply_agent/config.py`

负责 Agent 侧配置：

- OpenRouter API Key、模型和 Base URL；
- Agent 最大迭代次数和温度；
- Skills 目录；
- 日志目录和日志开关。

配置来自环境变量或项目根目录 `.env`，相对路径会解析到项目根目录。

### 4.2 `supply_agent/llm/client.py`

基于 OpenAI Python SDK 连接 OpenRouter，提供：

- 同步对话 `chat`；
- 异步对话 `achat`；
- 同步文本流 `stream`；
- 异步文本流 `astream`；
- Tool Calling；
- OpenRouter reasoning effort；
- LLM 输入和输出日志。

模型可以在运行时切换。

### 4.3 `supply_agent/tools/`

`base.py` 提供：

- `@tool` 装饰器；
- 从 Python 函数签名生成 JSON Schema；
- Optional 和基础类型转换；
- 同步/异步工具执行；
- 异常转为 JSON 错误结果。

`registry.py` 提供：

- 工具注册、删除和查询；
- OpenAI ToolDefinition 列表；
- 根据模型返回的工具名执行工具；
- 同步和异步执行；
- 批量注册被 `@tool` 装饰的函数。

### 4.4 `supply_agent/skills/`

Skill 机制读取指定目录下的 `SKILL.md`：

- 支持 YAML frontmatter 的名称和描述；
- 启动时把 Skill 目录作为目录索引加入系统提示词；
- Agent 通过内置 `load_skill` 工具按需加载全文；
- Skill 加载后会重建系统消息并注入当前上下文。

当前仓库根目录的 `skills/` 被 `.gitignore` 忽略，因此代码支持 Skill，但仓库没有可随代码分发的业务 Skill 内容。

### 4.5 `supply_agent/agent/`

`core.py` 的 `Agent` 负责组装：

- LLMClient；
- ToolRegistry；
- SkillRegistry；
- AgentLoop；
- AgentLogger。

对外提供：

- `run`；
- `arun`；
- `stream`；
- `astream`。

`loop.py` 实现标准 ReAct/Tool Calling 循环：

```text
系统消息 + 对话历史
        ↓
调用 LLM
        ↓
有工具调用？──否──→ 返回最终结果
        │是
        ↓
执行工具并写入 Tool Message
        ↓
下一轮 LLM
```

达到最大迭代次数时，同步和普通异步运行会追加一条用户消息，要求模型给出当前最佳答案；流式路径则直接发出 Max iterations reached。

### 4.6 `supply_agent/logging/`

每次 Agent 运行生成：

- 人类可读 `.log`；
- 结构化 `.jsonl`。

日志记录：

- run_start；
- 完整 LLM 输入；
- 模型输出和 reasoning；
- 工具调用参数和结果；
- Skill 加载；
- run_end。

运行结束后：

1. 将日志渲染成 HTML；
2. 上传 `.log`、`.jsonl`、`.html` 到 OSS；
3. 把 HTML 公网地址写入 `oss_logs`；
4. 上传失败只记录错误，不影响 Agent 主结果。

---

## 5. 三个业务 Agent

## 5.1 `demand_belong_category_agent`

目标：把需求词挂到真实存在的全局分类树节点。

工具：

- `query_global_tree_category`：读取整棵树或指定子树；
- `batch_insert_demand_belong_category`：批量写入需求词、分类 ID 和原因。

业务流程：

1. 查看顶层分类；
2. 选择最相关分支；
3. 逐层下钻；
4. 不确定时停在更可靠的上层节点；
5. 将结果写入 `demand_belong_category`。

系统提示允许一个词选择一个或多个高置信节点，但当前数据库和写入工具以 `name` 唯一：

- `demand_belong_category.name` 有唯一约束；
- 同一次请求也按 name 去重；
- 因此当前实现实际上只能保存“一词一个分类节点”；
- 多挂靠尚未在当前表结构中实现。

定时任务会把新需求词按 100 个一批交给该 Agent。

## 5.2 `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`。

输出结构固定为：

```text
source_dim
  └── overall_direction
        └── summary_event
              └── demand_name
```

重要约束：

- `demand_name` 必须原样来自 `demand_belong_category.name`；
- Agent 不能新造需求词；
- `summary_event` 尽量对应一个需求，最多轻量合并 2～3 个强相关需求；
- 写入时保存类目、路径、维度 avg/count 快照、reason、业务日和 run_id；
- 同一次工具调用按 `(source_dim, demand_name)` 去重；
- 当前 `generated_demand` 没有数据库唯一约束，不同 run 可以重复生成相同需求。

真实 ROV/VOV 当前不属于该 Agent 的 `source_dim`，也没有参与其单维生成工具链。

## 5.3 `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`：视频详情、双侧画像、标签、评分、分池与人工审核状态。

---

## 6. 基础设施：`supply_infra/`

### 6.1 配置

`supply_infra/config.py` 管理：

- MySQL；
- ODPS；
- APScheduler；
- 阿里云 OSS；
- Agent 日志上传开关。

配置文件固定从项目根目录 `.env` 读取，不依赖当前工作目录。

### 6.2 数据库会话

`supply_infra/db/session.py`：

- 懒加载全局 SQLAlchemy Engine；
- 使用连接池和 `pool_pre_ping`；
- `get_session()` 自动提交；
- 异常时自动回滚；
- `init_db()` 通过 ORM metadata 创建缺失表。

### 6.3 Repository 规则

业务 Agent、API 和定时任务原则上不直接执行 MySQL SQL，而是通过 Repository：

- 基础 CRUD；
- 批量插入；
- insert ignore；
- upsert；
- 条件查询；
- 批量更新。

ODPS 查询仍在 `supply_infra/odps/client.py` 内直接组织 SQL。

### 6.4 ODPS 客户端

当前读取的主要 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。

### 6.5 OSS 客户端

负责：

- 生成对象路径；
- 上传本地文件；
- 返回 CDN 公网地址。

---

## 7. MySQL 数据模型

当前 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 查询 |

### 7.1 关键关系

```text
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。

---

## 8. 热度数据的真实代码口径

### 8.1 策略到四项先验的映射

代码把需求池策略映射为：

| ODPS strategy | 统计字段 | 页面含义 |
|---|---|---|
| `新热事件` | `ext_pop` | 外部热度 |
| `逐月` | `plat_sust_pop` | 平台持续热度 |
| `去年同期阳历` | `plat_ly_pop` | 去年同期热度 |
| `去年同期阴历` | `plat_ly_pop` | 去年同期热度 |
| `当下供需gap` | `recent_pop` | 近期热度 |

### 8.2 真实 ROV/VOV

从近 7 日 `dwd_video_produce_plan_stat_hour` 获取人工 AGC 和自动 AGC 数据：

- ROV = 回流 UV / 分发曝光 PV；
- VOV = 拉回曝光 PV / 分发曝光 PV；
- 同一特征值存在多行时，优先保留 `rov_diff`、`vov_diff` 较高的一行；
- 按特征值与 `demand_name` 精确匹配，回填 `multi_demand_pool_di`。

### 8.3 词级统计

对每个 `demand_belong_category.name`：

1. 在当日 `multi_demand_pool_di.demand_name` 中执行子串 LIKE 匹配；
2. 按策略收集非零 weight；
3. 分别计算四项先验的 avg/count；
4. 汇总匹配行的真实 ROV/VOV avg/count；
5. 写入 `demand_popularity_stats`。

因此当前词与需求池的匹配核心是字符串包含关系，不是分词索引、向量匹配或显式语义关系。

### 8.4 分类树节点聚合

分类节点聚合六个独立维度：

- 外部热度；
- 平台持续热度；
- 去年同期热度；
- 近期热度；
- 真实 ROV；
- 真实 VOV。

每个节点统计其整个子树内有需求词挂靠的节点：

```text
节点维度 avg = Σ(挂靠点 avg × count) / Σ(count)
节点维度 count = Σ(count)
```

结果写入 `category_tree_weight`。

### 8.5 四维排名分和全局热度

`demand_pool/tree_weight.py` 在写入权重后会对四项先验排名：

- 各维只让 `count > 0` 的节点参与；
- 按 avg 降序排名；
- 同分取平均名次；
- 映射到 `(0, 1]`；
- 无数据节点为 0；
- `total_score` 是四个排名分直接相加，范围 `[0, 4]`。

真实 ROV/VOV 会被聚合并通过 API 返回，但当前不进入 `total_score`。

前端显示“全局热度”时再使用 `total_score / 4` 映射为 `0～1` 色阶。

单一维度热度则由前端根据该维所有有数据节点的 avg 做百分位色阶，不直接使用数据库中的四维 rank score 字段。

---

## 9. 定时任务和数据流水线

### 9.1 已注册的定时任务

`supply_infra/scheduler/app.py` 当前只注册一条任务：

1. 每天 `15:00`（`SCHEDULER_TIMEZONE`）：串行执行全链路
   `run_supply_pipeline`（全局树 → 需求池 → 分级 → 拓展 → 找片 → AIGC 发布）。

### 9.2 全局树同步

`sync_global_tree_odps_to_mysql`：

1. 拉取有效元素；
2. 拉取全部分类；
3. 从元素命中的分类向上补齐祖先；
4. 按 ODPS source_id 去重；
5. 已有分类保持不变，只插入新分类；
6. 给新分类分配 MySQL ID 并转换 parent_id；
7. 写入元素及其 MySQL category_id。

该任务是增量插入，不会根据 ODPS 当前状态自动删除或更新历史分类。

### 9.3 策略需求池完整流水线

`supply_infra.scheduler.jobs.demand_pool`（`sync_multi_demand_pool_odps_to_mysql`）实际执行顺序：

1. 比较 ODPS 与 MySQL 当日去重行数；
2. 行数不同时执行差异同步；
3. 对当日所有 demand_name 按空格拆词；
4. 调用归类 Agent 处理未出现过的新词；
5. 建立需求词与需求池的子串匹配边，并回填词级 video_list；
6. 获取并回填近 7 日真实 ROV/VOV；
7. 计算词级六维 avg/count；
8. 计算整棵分类树六维聚合；
9. 计算四项先验全局排名分和 total_score；
10. 同步全部待处理视频的标题和最终选题。

当前有一个同步判断风险：只要 ODPS 和 MySQL 行数相同，就跳过需求池差异拉取；如果内容发生变化但总行数不变，该轮不会发现这些变化。

### 9.4 手动任务入口

用 `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`）；
- 以及 global_tree / expand / discover / publish 各步骤模块。

也可通过 API `POST /api/scheduler/jobs/{job_id}/run` 手动触发。

---

## 10. FastAPI：`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 查询，没有分页、鉴权或缓存。

---

## 11. Vue 前端：`web/`

### 11.1 路由

| 路由 | 页面 |
|---|---|
| `/` | 平台全局需求地图 |
| `/demand-tree` | 传统横向分类树 |
| `/demand-process` | 需求归类 Agent 日志 |
| `/demand-map` | 重定向到 `/` |

当前顶部导航只显示“平台全局需求地图”，另外两个页面有路由但没有导航入口。

### 11.2 平台全局需求地图

`GlobalDemandMapView.vue` 并行读取：

- 完整分类树和热度；
- 全部需求词挂靠。

`IcicleHeatTree.vue` 使用 Canvas 绘制从左到右的冰柱树：

- 每层固定 `200px` 宽；
- 全局状态适配视口高度；
- 选择层级或聚焦节点后按可读高度纵向扩展；
- 最大逻辑画布高度 `24000px`；
- 实际 Canvas 始终只有视口大小，只绘制可见区域；
- 普通滚轮滚动；
- 拖拽平移；
- `Ctrl/Command + 滚轮` 缩放；
- 点击节点聚焦子树；
- 支持层级起点和祖先面包屑。

热力标签：

- 全局树结构；
- 全局热度 `total_score`；
- 外部热度；
- 平台持续热度；
- 去年同期热度；
- 近期热度。

API 虽然返回真实 ROV/VOV，但当前冰柱图标签没有提供真实 ROV/VOV 的独立切换入口。

### 11.3 节点证据下钻

分类节点存在需求词时显示搜索标识。点击后通过 `DemandPathPanel.vue` 展开：

```text
当前分类节点
  → 细节元素/需求词
  → 真实视频实例
  → 最终选题 JSON
```

这里展示的是 `demand_belong_category` 需求词，不是 `generated_demand` 中由生成 Agent 产出的四层平台需求。

当前前端没有读取或展示 `generated_demand` 的 API。

### 11.4 传统分类树页面

`CategoryTree.vue` 使用 Vue DOM 递归组件展示：

- 默认展开 3 层；
- 支持选择展开深度；
- 支持手动展开和收起；
- 支持按维度过滤有数据的节点；
- 支持拖动画布；
- 支持导出包含数据的独立 HTML；
- 同样可以下钻需求词、视频和最终选题。

### 11.5 需求归类过程页面

读取 `demand_belong_category_agent` 的 `oss_logs`，按时间倒序显示，点击打开 Agent 运行过程 HTML。

---

## 12. 部署和运行

### 12.1 Python 命令入口

`pyproject.toml` 注册：

- `supply-api`；
- `supply-visualize`。

### 12.2 本地开发

后端：

```bash
python -m api
```

前端：

```bash
cd web
npm install
npm run dev
```

Vite 在 `5173`，将 `/api` 代理到 `127.0.0.1:8080`。

### 12.3 Docker

Docker 使用两阶段构建：

1. Node 20 构建 Vue；
2. Python 3.11 安装项目和 ODPS 可选依赖；
3. 把 `web/dist` 放入 Python 镜像；
4. 通过 `python -m api` 启动 8080。

`scripts/docker-deploy.sh` 可以构建并向指定镜像仓库推送时间戳标签和 `latest`。

---

## 13. 当前代码已经实现的能力

- 通用同步/异步 Agent 和 Tool Calling；
- 动态 Skill 加载机制；
- 三个业务 Agent；
- 全量 Agent 日志和 OSS 发布；
- ODPS 到 MySQL 的全局树同步；
- 多策略需求池增量同步；
- 新词自动归类；
- 需求词与需求池匹配边；
- 真实 ROV/VOV 回填；
- 词级六维统计；
- 分类树六维向上聚合；
- 四项先验排名和 total_score；
- 单维度平台需求生成与落库；
- 视频标题和最终选题同步；
- FastAPI 查询接口；
- Vue 全局冰柱热力图；
- 分类节点到需求词、视频和最终选题的下钻；
- Docker 构建和部署脚本。

---

## 14. 当前代码未完成或存在偏差的部分

### 14.1 需求多挂靠尚未实现

`demand_belong_category.name` 唯一，当前一个需求词只能保存一个 category_id。系统提示中的“一词多个节点”无法真实落库。

### 14.2 `generated_demand` 未进入产品展示

需求生成 Agent 已能写 `generated_demand`，但：

- 没有对应 API；
- 前端没有需求清单；
- 没有与内容、反馈的展示闭环；
- 当前全局图下钻的是需求词，不是最终生成需求。

### 14.3 后验没有进入全局综合分

真实 ROV/VOV 已采集、聚合并由 API 返回，但：

- 不进入 `total_score`；
- 不进入 generate_demand_agent 的 source_dim；
- 当前主热力图没有 ROV/VOV 标签；
- 尚未形成“后验反向调整最终需求排序”的完整实现。

### 14.4 `find_agent` 的外部证据仍可增强

当前已持久化搜索轨迹、正式推荐和人工备选，但画像接口提供的是点赞用户而非真实转发
用户。后续若能补充转发用户画像、分年龄观看留存、相似视频和批量搜索接口，可进一步
提高老年分享判断的直接性与多词多页探索效率。

### 14.5 关系语义较弱

当前主要关系是：

- 树父子；
- 需求词到单一分类；
- 需求词和需求池的字符串子串匹配；
- 需求词到视频列表。

尚没有独立的语义关系表、关系类型、关系 reason、置信度和人工审核状态。

### 14.6 同步完整性风险

需求池同步先比较总行数。总行数相同但内容变化时，会跳过 ODPS 明细同步。

### 14.7 测试体系缺失

当前仓库没有 `tests/`，并且 `.gitignore` 直接忽略 `tests/`，会阻止正常提交测试目录。

### 14.8 文档已滞后

现有 `README.md`、`ARCHITECTURE.md` 和 `agents/README.md` 包含已经不存在或未实现的结构，例如：

- `video_content` 模型/Repository；
- find_agent 的保存和历史查询工具；
- 旧的 Agent 数量和数据流。

后续应以本文和源码为准，并更新旧文档。

---

## 15. 当前本地运行环境与数据库状态

### 15.1 本地 Python 环境不匹配

当前机器默认环境：

- Python `3.10.19`；
- SQLAlchemy `1.4.51`；
- 项目根目录没有 `.venv`。

源码要求：

- Python `>=3.11`；
- SQLAlchemy `>=2.0`。

因此直接使用当前默认 `python3` 导入数据库层会在 `DeclarativeBase` 处失败。应建立 Python 3.11+ 虚拟环境后安装项目依赖。

### 15.2 `.env` 的 MySQL 配置问题

只读连接检查发现：

1. `MYSQL_HOST` 的值开头多了一个 `=`，会导致主机名解析失败；
2. 临时去掉该字符后可以到达 MySQL 服务，但返回错误 `1045 Access denied`；
3. 需要核对用户名/密码、RDS 白名单或该用户允许连接的 Host 范围。

由于未通过鉴权，本次无法确认数据库真实表数量、行数和最新业务日。本文的数据结构来自当前 ORM 和 Repository 源码，不冒充线上运行态数据。

### 15.3 前端依赖未安装

当前没有 `web/node_modules`。Node `20.11.1`、npm `10.2.4` 已存在，执行前需要先在 `web/` 运行 `npm install` 或 `npm ci`。

---

## 16. 建议的后续阅读顺序

如果继续开发，建议按以下顺序进入代码：

1. `supply_infra/scheduler/jobs/demand_pool/`：理解需求池同步主业务；
2. `supply_infra/db/models/`：理解真实数据对象；
3. `supply_infra/scheduler/jobs/demand_pool/tree_weight.py`：理解树上热度；
4. `agents/demand_belong_category_agent/`：理解需求词挂树；
5. `agents/generate_demand_agent/`：理解平台需求生成；
6. `api/services/category_tree.py`：理解后端对前端的数据形态；
7. `web/src/components/IcicleHeatTree.vue`：理解全局热力图；
8. `web/src/components/DemandPathPanel.vue`：理解节点证据下钻；
9. `supply_agent/agent/`：理解底层 Agent 执行机制；
10. `supply_agent/logging/`：理解可追溯运行日志。

---

## 17. 一句话总结

当前 SupplyAgent 是一套以 ODPS 和 MySQL 为数据底座、以全局分类树为组织骨架、以自研 Agent 框架完成需求词归类和单维需求生成、并通过 FastAPI/Vue 展示分类热度与视频证据的业务系统；核心数据流水线已经形成，但多挂靠、最终需求产品化、后验反馈进入综合决策、语义关系图和自动化测试仍未完成。
