179 lines
7.1 KiB
Markdown
179 lines
7.1 KiB
Markdown
# 04 · 多 Agent 预测架构
|
|
|
|
Profeto 的核心预测路径是 **5 个领域专家 Agent 并行分析 + 1 个终裁 Agent 汇总决策**。
|
|
每个专家只拿到自己维度的数据切片,输出结构化 JSON;终裁综合 5 份报告给出最终预测。
|
|
|
|
## Agent 一览
|
|
|
|
| Agent | 职责 | 数据切片 | 输出核心 |
|
|
|---|---|---|---|
|
|
| `form` 近期状态 | 分析比分与关键事件,判断近期走势 | 两队近 N 场赛果(含 xG) | `home_edge` + 走势判断 |
|
|
| `stats` 攻防数据 | 评估进球、射门与控球,量化攻防强度 | 近 N 场进球/射门/控球/xG 统计 | `home_edge` + 攻防强度 |
|
|
| `home_away` 主客因素 | 对比主场与客场表现,评估地理优势影响 | 主队主场战绩 + 客队客场战绩 | `home_edge` + 地理优势 |
|
|
| `injuries` 阵容完整性 | 汇总伤停与停赛名单,评估战力缺失程度 | 伤停数据(当前无源 → no_data 门控) | `home_edge` 或 `no_data` |
|
|
| `h2h` 历史交锋 | 分析过去数年以及近期的交手数据,提取交手规律 | 近 N 次交锋(含主客方向 + 总计统计) | `home_edge` + 交手规律 |
|
|
| `aggregator` 终裁 | 权衡 5 份报告 → 最终结论 | 5 份结构化报告 + 比赛头信息 | 最终预测 + 各报告采信度 |
|
|
|
|
## 数据流
|
|
|
|
```
|
|
POST /predict {match_id, mode: "multi"}
|
|
│
|
|
├─ load_match_header ──► MatchHeader(比赛基础信息,所有 agent 共享)
|
|
│
|
|
├─ asyncio.gather(并行执行 5 专家, before=match_date 防未来信息泄漏)
|
|
│ ├─ form agent ─┐
|
|
│ ├─ stats agent │ 每个 agent 拿到专属数据切片
|
|
│ ├─ home_away agent │ → no_data 门控 → 调 LLM → 输出 JSON 报告
|
|
│ ├─ injuries agent │ (无数据 → 跳过 LLM,返回 stub)
|
|
│ └─ h2h agent ─┘
|
|
│
|
|
├─ aggregator agent(5 份报告 + 比赛头 → 最终 JSON)
|
|
│
|
|
└─ 存 predictions(mode="multi", agent_outputs JSONB)
|
|
```
|
|
|
|
## 执行语义
|
|
|
|
### 1. 并行执行
|
|
5 个专家通过 `asyncio.gather` 并发,总延迟 ≈ `max(专家延迟) + 终裁延迟` ≈ 2 次串行 LLM 调用。
|
|
|
|
### 2. no_data 门控(省 token、防幻觉)
|
|
数据切片为空时(如伤停数据源未接入),**跳过 LLM 调用**,直接返回:
|
|
```json
|
|
{"agent": "injuries", "status": "no_data", "data_sufficiency": "none",
|
|
"analysis": "该维度无数据,跳过分析。"}
|
|
```
|
|
终裁 Agent 会看到这个 `no_data` 状态,不会编造伤停分析。
|
|
|
|
### 3. fail-open(单专家失败不阻断)
|
|
单个专家 LLM 调用失败 → 其报告标记 `status: error`,其余 4 份 + 终裁照常执行。
|
|
只有**终裁 Agent 失败**才会整体返回 502。
|
|
|
|
### 4. 防未来信息泄漏
|
|
所有切片查询都带 `before=match_date`,确保只用比赛**之前**的数据。
|
|
这对历史回测(对已完赛比赛跑预测)尤其重要。
|
|
|
|
## 输出契约
|
|
|
|
### 专家 Agent 统一输出(JSON)
|
|
|
|
```json
|
|
{
|
|
"agent": "h2h",
|
|
"status": "ok",
|
|
"data_sufficiency": "high",
|
|
"analysis": "近 5 次交锋主队 3 胜 1 平 1 负,主场交锋 3 连胜……",
|
|
"home_edge": 0.4,
|
|
"subjective_confidence": 0.7,
|
|
"key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"]
|
|
}
|
|
```
|
|
|
|
| 字段 | 说明 |
|
|
|---|---|
|
|
| `status` | `ok` / `no_data` / `error` / `parse_error` |
|
|
| `data_sufficiency` | `high` / `medium` / `low` / `none` |
|
|
| `home_edge` | -1.0 ~ 1.0,正数=利主队,负数=利客队 |
|
|
| `subjective_confidence` | 0.0 ~ 1.0,该专家对自己分析的主观信心(非概率) |
|
|
| `key_evidence` | 关键证据列表(最多 5 条) |
|
|
|
|
### 终裁 Agent 输出
|
|
|
|
在现有最终预测 schema 基础上新增 `agent_weights`:
|
|
|
|
```json
|
|
{
|
|
"pred_home_goals": 2.1,
|
|
"pred_away_goals": 1.0,
|
|
"1x2": "1",
|
|
"subjective_confidence": 0.68,
|
|
"reasoning": "综合 stats 报告的攻防强度与 form 报告的三连胜势头……",
|
|
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "injuries": 0.0, "h2h": 0.8}
|
|
}
|
|
```
|
|
|
|
`agent_weights` 体现终裁对各专家报告的采信度(0–1),可用于后续分析"哪个维度对预测贡献大"。注意:这是 LLM 主观权重,非统计权重。
|
|
|
|
## 模型分档配置
|
|
|
|
专家用便宜快模型,终裁用强模型,各自回落 `LLM_MODEL`:
|
|
|
|
| 环境变量 | 说明 | 回落 |
|
|
|---|---|---|
|
|
| `LLM_SPECIALIST_MODEL` | 5 个专家共用模型 | `LLM_MODEL` |
|
|
| `LLM_AGGREGATOR_MODEL` | 终裁模型 | `LLM_MODEL` |
|
|
|
|
示例(专家用 gpt-4o-mini,终裁用 gpt-4o):
|
|
```bash
|
|
LLM_MODEL=gpt-4o
|
|
LLM_SPECIALIST_MODEL=gpt-4o-mini
|
|
LLM_AGGREGATOR_MODEL=gpt-4o
|
|
```
|
|
|
|
## Prompt 版本化
|
|
|
|
每个 Agent 有独立 prompt 文件,位于 `src/llm/prompts/agents/`:
|
|
|
|
```
|
|
agents/
|
|
├── form_v1.md # 近期状态专家
|
|
├── stats_v1.md # 攻防数据专家
|
|
├── home_away_v1.md # 主客因素专家
|
|
├── injuries_v1.md # 阵容完整性专家
|
|
├── h2h_v1.md # 历史交锋专家
|
|
└── aggregator_v1.md # 终裁
|
|
```
|
|
|
|
调用时传 `prompt_version: "v1"` 即加载所有 `*_v1.md`。
|
|
迭代 prompt 时:复制 `h2h_v1.md` → `h2h_v2.md`,改内容,传 `prompt_version: "v2"`。
|
|
`predictions.prompt_version` 存的是 `multi_v2`,与 single 模式的 `v1`/`v2` 天然分组,可在 eval summary 中 A/B 对比。
|
|
|
|
### 版本纪律(强制)
|
|
|
|
> **改 prompt 内容必须 bump 版本号(v2→v3),禁止默默修改 `*_v1.md` 内容却不改版本。**
|
|
|
|
原因:
|
|
1. **可复现性**:`predictions.prompt_version` 决定哪份 prompt 产生了历史预测;篡改 v1 会让历史预测的 prompt 来源失真,eval 对比失效。
|
|
2. **A/B 可信度**:`get_eval_summary` 按 `(provider, model, prompt_version)` 分组。若 v1 内容在不同时间指向不同 prompt,则 v1 桶内数据不可比。
|
|
3. **缓存一致性**:模板内容 hash 写入缓存键(`_prompt_template_hash`),版本不变则 hash 不变,命中旧缓存。bump 版本自动让旧缓存失效。
|
|
|
|
**流程**:改 prompt → 新建 `*_v{N+1}.md` → 新请求传 `prompt_version=v{N+1}` → 旧版本文件保持不变(供历史复现)。
|
|
|
|
代码保证:写入 DB 的 `prompt_version` 与 `_load_prompt_template(version)` / `load_agent_prompt(name, version)` 加载的文件**严格一致**,不会漂移。
|
|
|
|
## 如何新增一个专家 Agent
|
|
|
|
三步:
|
|
|
|
**1. 写切片函数**(`src/llm/context_builder.py`):
|
|
```python
|
|
async def weather_slice(header: MatchHeader, *, before=None) -> str:
|
|
"""天气切片示例。"""
|
|
return "── 天气 ──\n 比赛日: 小雨 15°C"
|
|
```
|
|
|
|
**2. 注册 AgentSpec**(`src/llm/agents/orchestrator.py` 的 `SPECIALIST_SPECS`):
|
|
```python
|
|
AgentSpec(name="weather", system_prompt="你是足球天气影响分析专家。只输出 JSON。",
|
|
slice_fn=weather_slice)
|
|
```
|
|
|
|
**3. 写 prompt 文件**(`src/llm/prompts/agents/weather_v1.md`):
|
|
```markdown
|
|
你是足球天气影响分析专家。分析以下天气数据对比赛的影响。
|
|
{{context}}
|
|
严格按此 JSON 输出……
|
|
```
|
|
|
|
重启服务即生效,终裁会自动收到第 6 份报告。
|
|
|
|
## 与单 Agent 模式的关系
|
|
|
|
`mode: "single"` 走原有单次调用路径(一个大 context + 一个 prompt),用于:
|
|
- 与 multi 模式做 A/B 基线对比
|
|
- 快速验证(省 token)
|
|
- 调试单个 prompt
|
|
|
|
eval summary 按 `prompt_version` 分组:`v1`/`v2` 是 single,`multi_v1`/`multi_v2` 是 multi,可直接对比准确率。
|