فهرست منبع

refactor(auto_put_ad_mini): system.prompt 精简(-27%) + action-playbook §五扩展 + 5-skill 体系完成

system.prompt (337→246行):
- 删除 reason 5元组模板/决策一致性自检表/8维度推理清单(~83行重复内容)
- 替换为 2 行指针指向 action-playbook skill §五
- Skill 索引精简(22→13行), 去掉"何时查阅"列
- 硬约束(禁英文变量名/禁中间报告)保留在第五部分末尾
- 重编号: 旧七→六(Skill索引) 旧八→七(对话) 旧九→八(安全)

action_playbook.md (238→284行):
- §五 从"决策前自检"(10行)扩展为"决策输出规范与自检"(56行)
- 新增 §5.1 reason 5元组模板(含合格/不合格样本)
- 新增 §5.2 action-pct ���绑定表
- 新增 §5.3 reason-action 语义一致规则
- §5.4 保留原有自检问题不变

5-skill 体系(8→5):
- 新增: decision_framework.md, platform_rules.md
- 删除: age_protection/guardrail_rules/roi_baseline/roi_strategy/tencent_ad_playbook
- 更新: ad_domain.md, posterior_wisdom.md

全流程测试验证(20260420数据):
- bid_down/scale_up 零漂移(完全一致)
- 8条 observe→pause(ROI偏离49-73%的成熟广告), 方向合理
- reason 5元组齐全率: 0%→83%, 平均得分 3.62→4.50/5
- 英文变量名违规: 0处
- 兜底检查触发: 1次(早期成长期广告, 安全网有效)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
刘立冬 2 هفته پیش
والد
کامیت
59c3dfda07

+ 6 - 0
config/feishu_contacts.json

@@ -66,5 +66,11 @@
     "name": "蒋德敏",
     "name": "蒋德敏",
     "description": "",
     "description": "",
     "open_id": "ou_f6a659e740ebd110cfe27dc38bbddb37"
     "open_id": "ou_f6a659e740ebd110cfe27dc38bbddb37"
+  },
+  {
+    "name": "liulidong",
+    "description": "",
+    "open_id": "ou_498988d823b61ab89c9afe4310f85bb4",
+    "chat_id": "oc_88e0a1970a7de02eb5ac225a8b0cedea"
   }
   }
 ]
 ]

+ 8 - 11
examples/auto_put_ad_mini/config.py

@@ -51,20 +51,17 @@ MAIN_CONFIG = RunConfig(
         # 会陷入"无法 apply"的死循环。直接在主 Agent 单次输出完成全部 decisions 更可靠。
         # 会陷入"无法 apply"的死循环。直接在主 Agent 单次输出完成全部 decisions 更可靠。
     ],
     ],
     skills=[
     skills=[
-        "ad-domain",
-        "roi-baseline",        # ★ 新:对比基准口径 + 阈值线定义(每次决策前必读)
-        "action-playbook",     # ★ 新:7 种 action 决策树 + 判断矩阵
-        "age-protection",      # ★ 新:三段年龄策略
-        "posterior-wisdom",    # ★ 新:后验经验(学习中断/置信度分级)
-        "roi-strategy",        # 保留:总索引(指向上面 4 个子 skill,兼容历史引用)
-        "guardrail-rules",
-        "tencent-ad-playbook",
+        "ad-domain",           # 业务模型:裂变模型、R值、ROI公式、字段定义
+        "platform-rules",      # 平台硬约束:oCPM学习期、调价上限、数据口径
+        "decision-framework",  # 决策框架:角色定义 + 对比标准 + 候选标记含义 + 年龄策略
+        "action-playbook",     # 动作手册:7种action + 多因素权衡原则
+        "posterior-wisdom",    # 后验经验:学习中断/降价恢复/创意冷启动/置信度分级
     ],
     ],
     extra_llm_params={"max_tokens": 32000},
     extra_llm_params={"max_tokens": 32000},
     knowledge=KnowledgeConfig(
     knowledge=KnowledgeConfig(
-        enable_extraction=False,
-        enable_completion_extraction=False,
-        enable_injection=False,
+        enable_extraction=False,              # 从决策过程中提取后验经验(投放后开启)
+        enable_completion_extraction=False,    # 完成后总结本轮经验(投放后开启)
+        enable_injection=False,               # 决策时自动注入相关历史经验(投放后开启)
         owner="ad_mini_team",
         owner="ad_mini_team",
     ),
     ),
 )
 )

+ 376 - 0
examples/auto_put_ad_mini/doc/skill_refactor_plan_20260422.md

@@ -0,0 +1,376 @@
+# Prompt & Skill 重构:从"规则复读机"到"综合决策者"
+
+## Context
+
+**2026-04-22 深度复盘发现的架构问题**:
+
+在验证"为什么37个ROI<关停线的广告未被pause"时,发现系统架构的根本性矛盾:
+
+1. **规则层已做判断**:`ad_decision.py:424-428` 已检查 `ROI < P50×0.75` → 输出 `roi_low=True`
+2. **Skill层重复要求**:`action_playbook.md:31,61,81` 又要求LLM检查 `动态ROI < 关停线(渠道P50×0.75)`
+3. **结果**:LLM成了"规则复读机",只是把规则层的判断用中文复述一遍
+
+**正确的分工**(用户确认):
+- **规则 = 体检仪器**:测出"roi_low=True"(数值检测)
+- **LLM = 医生**:综合全局信息判断是否真的应该pause(综合判断)
+- LLM **不应该**重复检查"ROI是否<0.75"(这是规则的职责)
+
+**技术数据**:
+- 硬编码阈值重复:`0.75`在7个地方出现,`300元`在8个地方出现
+- 规则与LLM边界模糊:同一个检查逻辑在规则、skill、护栏三层重复
+- 缺失决策权衡原则:只有if-then决策树,没有"如何权衡多因素冲突"的原则
+
+---
+
+## 改动目标
+
+**不改规则层代码**(`ad_decision.py`、`guardrails.py` 均不动),只改 prompt 和 skill:
+
+1. **Skill 从 8 个精简到 5 个**:删除冗余、合并混淆、拆清边界
+2. **移除硬编码阈值**:Skill中不再出现`0.75/0.90/1.05/300/500`等具体数值
+3. **改为候选标记语义**:从"检查ROI是否<0.75"改为"理解roi_low=True的含义"
+4. **补充多因素权衡**:异常识别、组合位置、调整效果评估等规则无法覆盖的判断
+5. **清理 system.prompt**:移除与 skill 重复的硬编码内容,更新 skill 索引
+
+---
+
+## 新 Skill 架构(5 个)
+
+| # | Skill 名称 | 文件名 | 内容定位 | 来源 |
+|---|-----------|--------|---------|------|
+| 1 | **ad-domain** | `ad_domain.md` | 我们的业务模型(裂变模型、R值、ROI公式、字段定义) | 改写自旧 ad-domain(去掉腾讯API参数) |
+| 2 | **platform-rules** | `platform_rules.md` | 腾讯广告平台硬约束(oCPM学习期、调价上限、数据口径、API限制) | 改写自旧 tencent-ad-playbook(去掉我们的业务判断) |
+| 3 | **decision-framework** | `decision_framework.md` | 角色定义 + 对比标准 + 候选标记含义 + 年龄策略 | 新建:整合旧 roi-baseline + age-protection + 新增角色定义 |
+| 4 | **action-playbook** | `action_playbook.md` | 7种action说明 + 多因素权衡原则 + 异常识别 | 改写自旧 action-playbook(去硬编码阈值,加权衡原则) |
+| 5 | **posterior-wisdom** | `posterior_wisdom.md` | 后验经验(学习中断、降价恢复、创意冷启动、置信度分级) | **不动** |
+
+**删除 7 个旧 skill 文件**:
+- `roi_strategy.md` — 纯索引,已无意义
+- `roi_baseline.md` — 内容合并到 decision-framework
+- 旧 `action_playbook.md` — 被新版替换
+- `age_protection.md` — 内容合并到 decision-framework
+- `guardrail_rules.md` — 用户明确不需要护栏 skill
+- 旧 `ad_domain.md` — 被新版替换
+- `tencent_ad_playbook.md` — 被 platform-rules 替换
+
+---
+
+## 改动清单
+
+### 改动 1:新建 `skills/ad_domain.md`(改写)
+
+**文件**:`examples/auto_put_ad_mini/skills/ad_domain.md`
+
+**改动要点**:
+- **保留**:裂变模型(L15-39)、核心字段定义(L41-66)、ROI公式(L68-88)、R值含义(L90-137)
+- **删除 L9-13**:腾讯API参数(`marketing_carrier_type`、`bid_mode`、`optimization_goal`),移到 platform-rules
+- **微调**:开头 description 改为"我们的业务模型与核心指标",不再提"渠道"
+
+---
+
+### 改动 2:新建 `skills/platform_rules.md`(改写)
+
+**文件**:`examples/auto_put_ad_mini/skills/platform_rules.md`
+
+**改动要点**:
+- **保留**:旧 tencent-ad-playbook 的 §1-§3(oCPM学习期、调价幅度上限、少广告多素材)、§5-§7(小程序场景、数据口径、执行层硬约束)
+- **删除或移走旧 §4(素材疲劳)中属于我们业务判断的内容**:
+  - "CTR+CVR 同时下跌>25% → 素材疲劳"这类带具体数值的判断 → 改为原则性描述
+  - "首选 creative_adjust 而非 pause" → 这是 action 选择原则,移到 action-playbook
+- **从旧 ad-domain 搬入**:L9-13 的平台参数(`marketing_carrier_type`、`bid_mode`、`optimization_goal`)
+- **开头定位**:明确"这是腾讯广告 3.0 平台的投放规则,优先于任何业务决策"
+
+---
+
+### 改动 3:新建 `skills/decision_framework.md`(新建)
+
+**文件**:`examples/auto_put_ad_mini/skills/decision_framework.md`
+
+**核心内容**(整合 roi-baseline + age-protection + 新增角色定义):
+
+#### 第一章:你的角色 — 从规则复读机到综合决策者
+
+```
+规则层(体检仪器)     →  候选标记(体检报告)  →  你(医生)         →  护栏(药剂安全审查)
+ad_decision.py 计算       roi_low=True 等         综合全局信息判断       guardrails.py 拦截矛盾
+具体数值检测               传递给你                 是否真的要执行action    你不需要管
+```
+
+- ✅ **你的职责**:理解候选标记的业务含义,综合多因素做最终判断
+- ❌ **不是你的职责**:重复检查具体数值(如"ROI是否<2.80")
+
+#### 第二章:对比标准(来自旧 roi-baseline)
+
+| 维度 | 对比基准 | 数据字段 | 业务含义 |
+|------|---------|---------|---------|
+| 动态 ROI | **渠道P50** | `channel_roi_p50` | 全体广告"动态ROI_7日均值"的中位数 |
+| 裂变率 | **同类均值** | `tier_fission_mean` | 同人群包 R 值的裂变水位 |
+| CTR | **同类均值** | 同类 | 同人群的曝光质量 |
+
+- ROI 必须看渠道(跨人群共享预算,低于P50=跑不出渠道平均效率)
+- 裂变率必须看同类(不同R值人群裂变天然不同,跨人群比=比苹果和橘子)
+
+#### 第三章:候选标记含义
+
+**规则层已完成的工作**(不要重复检查):
+- `roi_low=True`:动态ROI 明显低于渠道P50(具体阈值由规则控制,你不需要知道数值)
+- `bid_up_candidate=True`:ROI 明显优于渠道,且满足提价的年龄/消耗等前提
+- `bid_down_candidate=True`:ROI 略低于渠道,消耗足够,近期无调价
+- `scale_up_candidate=True`:成熟稳定 + 高消耗 + ROI 达标
+
+**阈值参考**(仅理解规则逻辑用,不在reason中引用具体数值):
+- 关停线 ≈ 渠道P50 × 0.75
+- 降价线 ≈ 渠道P50 × 0.90
+- 提价线 ≈ 渠道P50 × 1.05
+(以上数值可能调整,以规则输出的候选标记为准)
+
+#### 第四章:年龄策略(来自旧 age-protection,去掉代码细节)
+
+| 年龄段 | 天数 | 允许操作 | 禁止操作 | 原因 |
+|--------|------|---------|---------|------|
+| 冷启动期 | ≤3天 | 你不会看到这类广告(规则已排除) | — | 系统刚开始学习 |
+| 早期成长期 | 4-7天 | bid_up / observe / creative_adjust | bid_down / pause | 降价会打断学习 |
+| 成熟期 | >7天 | 全部action | bid_up(改走scale_up) | 稳定期不调出价 |
+
+(注:年龄保护由规则层+护栏层双重实现,你不太可能收到违反年龄保护的广告)
+
+---
+
+### 改动 4:改写 `skills/action_playbook.md`
+
+**文件**:`examples/auto_put_ad_mini/skills/action_playbook.md`
+
+**整体结构**(4章):
+
+#### 第一章:候选标记 → action 映射(取代旧决策树)
+
+不再是硬编码 if-then 决策树,改为候选标记驱动:
+
+```
+收到候选广告
+  │
+  ├─ bid_up_candidate=True → 评估是否 bid_up(第二章 §3)
+  ├─ bid_down_candidate=True → 评估是否 bid_down(第二章 §2)
+  ├─ roi_low=True → 评估是否 pause(第二章 §1)
+  ├─ scale_up_candidate=True → 评估是否 scale_up(第二章 §4)
+  └─ 以上都不满足 → hold 或 observe
+```
+
+#### 第二章:7种action说明(去硬编码阈值)
+
+每种 action 结构统一为:
+- **触发前提**:候选标记 + 你需要综合权衡的要点
+- **pct 要求**:幅度范围
+- **禁用于**:候选标记冲突场景(如 `bid_up_candidate=True` 时不能 bid_down)
+
+**重点改写**:
+- `pause`:原来是"ROI < P50×0.75 且 消耗≥300",改为"roi_low=True + 综合权衡5要点(裂变对比、调整历史、tier组合、数据质量、异常识别)"
+- `bid_down`:原来是"ROI在[0.75, 0.90)区间",改为"bid_down_candidate=True + 权衡(是否近期调过价、是否换过创意、消耗稳定性)"
+- `bid_up`:原来是两条硬编码分支A/B,改为"bid_up_candidate=True + 权衡(CTR是否正常、裂变是否真的强)"
+
+#### 第三章:多因素权衡原则(全新内容)
+
+这是规则无法覆盖的决策维度:
+
+1. **裂变 vs ROI 双低**:ROI低 + 裂变低于同类50% = 真正的低效,强pause信号
+2. **调整效果评估**:7天内已调过价但无改善 → observe等效果;连续3次调价无效 → pause
+3. **tier组合位置**:该tier只剩这条跑量广告 → 谨慎pause,reason中建议"需配合新广告"
+4. **异常识别模式**:
+   - CTR正常但ROI低 → 可能是后端转化问题(非广告素材问题)
+   - ROI突降(7日均值vs昨日相差>30%)→ 可能是数据异常
+   - 高消耗+高ROI突然变低 → 可能是竞争加剧,非广告本身问题
+5. **不确定时的默认策略**:observe > hold > bid_down > pause(保守优先)
+
+#### 第四章:action优先级(简化版)
+
+```
+pause(明确低效)> bid_down(有改善空间)> creative_adjust > observe > hold
+bid_up(冷启动期优质)> scale_up(成熟期优质)
+```
+
+---
+
+### 改动 5:更新 `prompts/system.prompt`
+
+**文件**:`examples/auto_put_ad_mini/prompts/system.prompt`
+
+#### 5.1 自检表 B 改写(L177-185)
+
+**当前**:硬编码 ROI 水位(`渠道P50 × 1.05`、`渠道P50 × 0.50`)
+**改为**:候选标记一致性检查
+
+```markdown
+### B. action 与候选标记的一致性
+
+| 候选标记 | 允许的 action | 禁止的 action | 原因 |
+|---|---|---|---|
+| `bid_up_candidate=True` | `bid_up`/`hold`/`observe` | ❌ `bid_down`/`pause` | 规则标记为"优质",不应降价或关停 |
+| `roi_low=True` | `pause`/`observe`/`hold` | ❌ `bid_up`/`scale_up` | 规则标记为"ROI严重偏低",不应加码 |
+| `bid_down_candidate=True` | `bid_down`/`hold`/`observe` | ❌ `bid_up` | 规则标记为"需降价",不应提价 |
+
+**你不需要重复检查具体ROI数值,规则已完成筛选。重点是综合其他因素做最终判断。**
+```
+
+#### 5.2 第六部分 reason 维度扩展(L204-211)
+
+**当前**:6个维度
+**改为**:8个维度(追加2个)
+
+```markdown
+7. **组合位置** — 该tier总消耗、广告数、本广告占比(tier唯一跑量广告需谨慎关停)
+8. **异常识别** — CTR正常但ROI低?ROI突降>30%?(可能是后端问题或数据异常)
+```
+
+#### 5.3 第七部分 Skill 索引重写(L221-251)
+
+**当前**:8个skill + 冲突优先级
+**改为**:5个skill 简洁索引
+
+```markdown
+# 第七部分:投放经验知识库(Skills)
+
+你有 5 份 skill 可以依据(由框架自动注入):
+
+| skill | 何时查阅 | 核心内容 |
+|-------|---------|---------|
+| **ad-domain** | 需要理解业务模型时 | 裂变模型、R值含义、ROI公式、核心字段定义 |
+| **platform-rules** | **决策前先过一遍** | 腾讯oCPM学习期、调价上限、数据口径(平台硬约束 > 业务决策) |
+| **decision-framework** ⭐ | **每次决策必读** | 你的角色定义、对比标准、候选标记含义、年龄策略 |
+| **action-playbook** ⭐ | **选 action 前** | 7种action说明 + 多因素权衡原则 |
+| **posterior-wisdom** | reason要解释"为什么观察"时 | 学习中断/降价恢复/创意冷启动/置信度分级 |
+
+**冲突优先级**:platform-rules > decision-framework > action-playbook > posterior-wisdom > ad-domain
+```
+
+#### 5.4 清理"方向型反馈"硬编码(L283附近)
+
+**当前**:`临时上下调全局阈值 10-20%`
+**改为**:`根据反馈方向适度调整决策的保守/激进程度`(不写具体幅度)
+
+---
+
+### 改动 6:更新 `config.py` 的 skills 列表
+
+**文件**:`examples/auto_put_ad_mini/config.py`(L53-62)
+
+```python
+skills=[
+    "ad-domain",
+    "platform-rules",        # 改名:tencent-ad-playbook → platform-rules
+    "decision-framework",    # 新增:角色 + 标准 + 候选标记 + 年龄策略
+    "action-playbook",       # 改写:去硬编码阈值 + 加权衡原则
+    "posterior-wisdom",      # 不变
+],
+```
+
+---
+
+### 改动 7:更新 `CLAUDE.md` 过时信息
+
+**文件**:`/Users/liulidong/project/agent/Agent/CLAUDE.md`
+
+| 位置 | 当前说法 | 修正为 |
+|------|---------|--------|
+| "阈值基准" | "ROI 基于渠道均值(全体 `roi_mean`)" | "ROI 基于渠道P50(全体 `channel_roi_p50`,即中位数)" |
+| "ROI计算公式" | "min_periods=7" | "min_periods=3" |
+| "决策逻辑优先级" | 5种action(止损/预算/清理/降价/放量) | 更新为7种action |
+| "先验策略规则" | 硬编码全部数值 | 添加"详见 config.py,此处仅概念说明" |
+
+同步更新 `examples/auto_put_ad_mini/CLAUDE.md`:
+| 位置 | 修正 |
+|------|------|
+| "决策逻辑(6种action)" | 更新为7种action(加 creative_adjust) |
+| "阈值基准" | roi_mean → channel_roi_p50 |
+| skills 列表 | 更新为5个新skill |
+
+---
+
+### 改动 8:删除旧 skill 文件
+
+删除以下文件(内容已合并到新skill中):
+- `skills/roi_strategy.md`(纯索引,无独立价值)
+- `skills/roi_baseline.md`(合并到 decision-framework)
+- `skills/age_protection.md`(合并到 decision-framework)
+- `skills/guardrail_rules.md`(用户明确不需要)
+- `skills/tencent_ad_playbook.md`(被 platform-rules 替换)
+
+---
+
+## 不做的事
+
+- ❌ 不改规则层代码(`ad_decision.py`)
+- ❌ 不改护栏代码(`guardrails.py`)
+- ❌ 不改数据注入(本次不补充预算/竞争等上下文数据)
+- ❌ 不改 `agent/` 框架目录
+- ❌ 不新建护栏skill(用户明确排除)
+
+---
+
+## 执行顺序
+
+1. 新建 `skills/decision_framework.md`(全新文件)
+2. 新建 `skills/platform_rules.md`(改写自 tencent-ad-playbook)
+3. 改写 `skills/ad_domain.md`(去掉平台参数)
+4. 改写 `skills/action_playbook.md`(去硬编码 + 加权衡原则)
+5. 更新 `prompts/system.prompt`(自检表B + reason维度 + skill索引)
+6. 更新 `config.py`(skills列表)
+7. 更新两份 `CLAUDE.md`
+8. 删除 5 个旧 skill 文件
+
+---
+
+## 验证
+
+### 1. 静态检查(硬编码阈值是否移除)
+
+```bash
+cd examples/auto_put_ad_mini
+
+# skill中不应有硬编码数值(仅decision-framework的"阈值参考"段允许带≈说明)
+grep -n "0\.75\|0\.90\|1\.05" skills/*.md
+
+# 不应有硬编码消耗门槛
+grep -n "300元\|500元\|1000元" skills/action_playbook.md prompts/system.prompt
+```
+
+### 2. 端到端测试
+
+```bash
+cd examples/auto_put_ad_mini && .venv/bin/python3 execute_once.py
+```
+
+**对比指标**:
+| 指标 | 改动前 | 期望 |
+|------|--------|------|
+| pause reason 包含裂变对比 | ~43% | ≥95% |
+| observe reason 提及调整历史 | ~30% | ≥70% |
+| reason 出现硬编码数值 | 多处 | 0处 |
+| 兜底检查触发次数 | 0-2 | 0 |
+
+### 3. Skill 文件数量检查
+
+```bash
+ls -la skills/*.md | wc -l
+# 期望:5 个文件(ad_domain / platform_rules / decision_framework / action_playbook / posterior_wisdom)
+```
+
+---
+
+## 回滚策略
+
+```bash
+cd examples/auto_put_ad_mini
+git checkout HEAD -- \
+  skills/ \
+  prompts/system.prompt \
+  config.py
+# CLAUDE.md 更新无需回滚(修正过时信息)
+```
+
+---
+
+## 后续扩展方向(本次不做)
+
+1. **数据层补充全局上下文**:预算剩余、tier总消耗/广告数、市场环境
+2. **Skill层补充**:预算约束下的优先级分配、时间节点策略
+3. **验证层增强**:ReasonQualityGuardrail(检查reason是否包含必要元素)

+ 5 - 2
examples/auto_put_ad_mini/execute_once.py

@@ -5,6 +5,7 @@ import asyncio
 import os
 import os
 import sys
 import sys
 from pathlib import Path
 from pathlib import Path
+from datetime import datetime, timedelta
 
 
 # 代理设置
 # 代理设置
 os.environ.setdefault("HTTP_PROXY", "http://127.0.0.1:29758")
 os.environ.setdefault("HTTP_PROXY", "http://127.0.0.1:29758")
@@ -82,8 +83,10 @@ async def main():
     print("=" * 70)
     print("=" * 70)
     print()
     print()
 
 
-    # 让Agent自动决定使用哪天的数据(默认yesterday)
-    messages = [{"role": "user", "content": "分析广告,执行完整的ROI计算和决策流程。请使用 2026-04-19(end_date=20260419)作为数据截止日期,因为 2026-04-20 的数据尚未回流。"}]
+    # 使用 20260420 数据
+    target_date = "20260420"
+    target_date_display = "2026-04-20"
+    messages = [{"role": "user", "content": f"分析广告,执行完整的ROI计算和决策流程。请使用 {target_date_display}(end_date={target_date})作为数据截止日期,因为当天数据尚未回流。"}]
     config.trace_id = None
     config.trace_id = None
 
 
     step_count = 0
     step_count = 0

+ 1 - 1
examples/auto_put_ad_mini/presets.json

@@ -8,6 +8,6 @@
       "calculate_roi_metrics",
       "calculate_roi_metrics",
       "generate_report"
       "generate_report"
     ],
     ],
-    "skills": ["roi-strategy"]
+    "skills": ["ad-domain", "platform-rules", "decision-framework", "action-playbook", "posterior-wisdom"]
   }
   }
 }
 }

+ 18 - 113
examples/auto_put_ad_mini/prompts/system.prompt

@@ -24,7 +24,7 @@ $system$
 
 
 **工具提供事实**(广告数据、tier 统计、阈值参考),**Skill 提供判断原则**(决策经验、年龄策略、后验观察),**你像法官一样综合判断**:读证据 → 依原则 → 出决策 → 用中文解释。
 **工具提供事实**(广告数据、tier 统计、阈值参考),**Skill 提供判断原则**(决策经验、年龄策略、后验观察),**你像法官一样综合判断**:读证据 → 依原则 → 出决策 → 用中文解释。
 
 
-阈值(如"渠道P50 × 1.05")只是参考,**真正的决策依据是 Skill 中的业务洞察**——别机械套数值。
+阈值只是参考,**真正的决策依据是候选标记 + Skill 中的业务洞察**——别机械套数值。
 
 
 # 第三部分:意图理解(理解语义,非关键词匹配)
 # 第三部分:意图理解(理解语义,非关键词匹配)
 
 
@@ -74,7 +74,7 @@ Step 2: merge_creative_data       ← 合并创意数据+广告状态
 Step 3: calculate_roi_metrics     ← 计算ROI(依赖Step 1+2的数据)
 Step 3: calculate_roi_metrics     ← 计算ROI(依赖Step 1+2的数据)
-Step 4: get_ads_for_review        ← 三级分类(零消耗待关停 / 待评估 / 正常运行)
+Step 4: get_ads_for_review        ← 分类(零消耗待关停 / 需评估 / 正常运行)
 Step 5: AI推理决策                 ← 对【待评估(候选)】广告推理
 Step 5: AI推理决策                 ← 对【待评估(候选)】广告推理
          · 在**一次 LLM 输出**里为所有候选广告生成完整 JSON 数组(含 ad_id / action / pct / reason / confidence)
          · 在**一次 LLM 输出**里为所有候选广告生成完整 JSON 数组(含 ad_id / action / pct / reason / confidence)
@@ -131,126 +131,31 @@ Step 10: generate_report          ← 生成报告
 - 引用具体数值(ROI/阈值/消耗),用分号连接多个判断
 - 引用具体数值(ROI/阈值/消耗),用分号连接多个判断
 - `confidence` 与数据支撑度一致;`recommended_change_pct` 为小数(+0.05=提5%),单次绝对值 ≤ 0.10
 - `confidence` 与数据支撑度一致;`recommended_change_pct` 为小数(+0.05=提5%),单次绝对值 ≤ 0.10
 
 
-### 🔒 reason 5 元组硬模板(每条 reason 必含)
+每条 reason 必须包含 5 个语义元素(ROI 值 / 对比基准 / 偏离% / 辅助信号 / 行动建议),详见 action-playbook skill §五。
 
 
-**每条 reason 必须显式包含以下 5 个语义元素**,缺任一项视为不合格:
-
-| # | 元素 | 说明 | 正例措辞 |
-|---|------|------|---------|
-| 1 | **ROI 数值** | 给出具体动态 ROI 值(保留 2 位小数)| "动态 ROI 为 2.18" |
-| 2 | **对比基准** | 明确是渠道P50(ROI)或同类均值(裂变/CTR)| "低于渠道P50 2.50" |
-| 3 | **偏离百分比** | 偏离基准的比例(±X%)| "低 13%" |
-| 4 | **辅助信号** | 调价历史 / 创意变化 / 广告年龄 / 7日均消耗 / 置信度 至少一项 | "7 天内已提价但 ROI 仍低迷;投放 14 天" |
-| 5 | **行动建议** | action 的业务解释(不是重复 action 名)| "建议降 5% 优化成本" |
-
-**标准模板**:
-
-```
-{动态 ROI 数值},{对比基准 + 偏离百分比};{辅助信号};{行动建议}
-```
-
-**合格样本**:
-> "动态 ROI 为 1.62,低于渠道P50 2.50 的 35%;7 天内已提价但 ROI 仍低迷,广告已投放 9 天、7 日日均消耗 4438 元属于高消耗;综合判断调价无效,建议关停释放预算"
-
-(✅ 元素齐全:ROI=1.62 / 对比渠道P50 2.50 / 偏离 -35% / 辅助信号=已提价+年龄+消耗 / 建议=关停释放预算)
-
-**不合格样本**:
-> ❌ "ROI 低于关停线,建议关停"(缺元素 1/2/3/4,只有行动)
-> ❌ "动态ROI=1.62 < pause_line(1.66), bid_increased_7d=true"(用英文变量名,违反硬约束)
-> ❌ "ROI 不好,建议降价"(缺数值、基准、偏离%、辅助信号)
-
-## 🚨 决策一致性自检表(发出决策前必过)
-
-**每次写完一条决策,在提交前务必对照这张表自检。任何一条不满足就把决策改对,不要交差。**
-
-### A. action 与 recommended_change_pct 的强绑定
-
-| action | recommended_change_pct | 违反时的症状 | 修正 |
-|---|---|---|---|
-| `bid_up` | **必须 > 0**(+0.05 ~ +0.10) | `bid_up, pct=0` 或 `bid_up, pct<0` | 没有非零幅度就不是"提价",改 `hold` |
-| `bid_down` | **必须 < 0**(-0.03 ~ -0.10) | `bid_down, pct=0`(典型错误:显示"降价 0%")| 没有降幅就不是"降价",改 `hold` 或 `observe` |
-| `pause` | 建议 = 0 | `pause, pct != 0` | pause 不改出价,pct 填 0 |
-| `hold` / `observe` | **必须 = 0** | `hold, pct != 0` | hold/observe 本质是"维持",pct 置 0 |
-| `scale_up` | 可 0,可正(若带提价) | 负值 | 扩量不降价,pct 不得为负 |
-| `creative_adjust` | 建议 = 0 | 非 0 | 这是创意动作,不改出价 |
-
-### B. action 与 ROI 水位的硬约束
-
-| 条件 | 允许的 action | 禁止的 action | 原因 |
-|---|---|---|---|
-| 动态ROI ≥ 渠道P50 × 1.05(优于渠道中位数 5%)| `hold`/`bid_up`/`scale_up`/`observe` | ❌ `bid_down` | ROI 都优于渠道了还降价 = 自相矛盾(Top 1 实测事故)|
-| 动态ROI ≥ 渠道P50 × 1.20 且 7日均消耗 ≥ 1000 | `hold`/`bid_up`/`scale_up` | ❌ `pause`/`bid_down` | 超高 ROI + 高消耗是明星广告,严禁误伤 |
-| 动态ROI < 渠道P50 × 0.50(关停线以下)| `pause` 首选 | ❌ `bid_up`/`scale_up` | 低效广告不能加码 |
-| 裂变率 fission_rate < 同类均值 × 0.5 且 ROI 也低 | `pause` 优先 | ❌ `bid_up` | 裂变弱 + ROI 低的双低广告无挽救价值 |
-| action = `pause` | reason 必须引「裂变率 vs 同类均值」对比 | ❌ 只写 ROI 就 `pause` | 关停最激进,单靠 ROI 证据不足;数据缺失时也要显式写"裂变数据缺失" |
-
-### C. reason 与 action 语义一致
-
-- 如果 reason 里出现 **"表现优秀/保持/维持/微调"** 类措辞 → action **不可以**是 `pause`/`bid_down`
-- 如果 reason 里出现 **"严重低迷/持续亏损/关停线"** 类措辞 → action **不可以**是 `hold`/`bid_up`/`scale_up`
-- 如果 reason 里提到 **"建议保持或微调"** → action 只能是 `hold` 或 `observe`,**绝不能**是 `bid_down`
-
-### D. 提交前的最终自检问答(内心默问)
-
-1. 我这条 action 和 pct 数字方向对得上吗?(降价 ↔ 负数,提价 ↔ 正数,维持 ↔ 零)
-2. 我的 reason 结论和 action 语义一致吗?(我说"优秀"还建议降价吗?)
-3. 如果这个广告 ROI > tier 中位数,我真的要降价 / 暂停吗?有没有更合理的 hold?
-4. 如果我不确定,有没有更保守的选择(hold/observe)?
-
-> **任何一条不满足都把决策改对再输出。**护栏会再拦一次,但不要依赖护栏——让护栏 0 告警是目标。
-
-# 第六部分:决策推理要求
-
-每条决策的 reason 必须显式体现以下 6 个维度的综合判断:
-
-1. **调价历史** — 7 天内是否已调价?是否已证明无效?
-2. **创意变化** — 7 天内是否换过创意?消耗是否改善?
-3. **数据稳定性** — 30 天内稳定消耗天数是否 ≥ 7?
-4. **广告年龄** — 新生期(≤3)/冷启动(4-7)/成熟期(>7) 三段式
-5. **同类对比(裂变 / CTR)** — 裂变率、CTR 等微观指标用同类均值;ROI 阈值用渠道P50(见第五部分自检表 B)
-6. **ROI 数据置信度** — 基于 roi_valid_days 分级(≥7 高 / 4-6 中 / ≤3 低)
-
-详细判断标准、案例、后验经验见 roi-strategy skill。
+每条 reason 必须体现多维度综合判断——具体维度和权衡原则见 decision-framework、action-playbook、posterior-wisdom skill。
 
 
 **硬约束**:
 **硬约束**:
 - reason 中禁止出现英文变量名(pause_line、bid_down_line、tier_roi_p50 等),改用中文术语
 - reason 中禁止出现英文变量名(pause_line、bid_down_line、tier_roi_p50 等),改用中文术语
-- reason 不得模板化(错例:"ROI 低于线建议降价";正例见 roi-strategy skill 的"决策纪律"小节
+- reason 不得模板化(错例:"ROI 低于线建议降价";正例见 posterior-wisdom skill 的反例警示)
 - 冷启动期(4-7 天)系统会过滤 bid_down/pause,你也不该主动给这两种建议
 - 冷启动期(4-7 天)系统会过滤 bid_down/pause,你也不该主动给这两种建议
 - 🚨 **禁止在 apply_decisions 之前输出 markdown 分析表格、tier 拆分汇总、决策汇总表**。看完候选数据后**直接调用 apply_decisions** 提交决策 JSON 数组。所有判断理由都写进每条决策的 `reason` 字段里,**不要**在工具调用前面铺一段中间报告(会触发 max_tokens 截断导致 tool_call 失败)。
 - 🚨 **禁止在 apply_decisions 之前输出 markdown 分析表格、tier 拆分汇总、决策汇总表**。看完候选数据后**直接调用 apply_decisions** 提交决策 JSON 数组。所有判断理由都写进每条决策的 `reason` 字段里,**不要**在工具调用前面铺一段中间报告(会触发 max_tokens 截断导致 tool_call 失败)。
 
 
-# 第七部分:投放经验知识库(Skills)
-
-你有 7 份 skill 可以依据(由框架自动注入,不需要主动查询)。按使用时机分三类:
-
-### 📘 基础概念(始终可查)
-- **ad-domain** — 业务概念与指标(ROI 公式、人群包 R 值含义、关键字段映射)
-- **tencent-ad-playbook** — 腾讯广告平台硬规则(oCPM 学习期、降价 ≤30%、少广告多素材、数据口径)
+# 第六部分:投放经验知识库(Skills)
 
 
-### 🎯 决策四件套(每次决策必读)
-| skill | 何时查阅 | 核心内容 |
-|-------|---------|---------|
-| **roi-baseline** ⭐ | **每次决策开始前**(一次就够)| 对比基准口径(ROI 看渠道P50,裂变/CTR 看同类均值)+ 三条阈值线符号定义 |
-| **action-playbook** ⭐ | **选 action 前** | 7 种 action 决策树 + 判断矩阵 + pct 幅度规则 |
-| **age-protection** | 看到 `ad_age_days ≤ 7` 时 | 三段式(新生/冷启动/成熟)允许与禁止操作 |
-| **posterior-wisdom** | reason 要解释"为什么观察而非操作"时 | 学习中断/降价恢复期/创意冷启动/ROI 置信度分级 |
+你有 5 份 skill(由框架自动注入,不需要主动查询):
 
 
-### 🛡️ 系统护栏(了解即可,不主动查)
-- **guardrail-rules** — 安全护栏(冷启动保护、调价频率、出价上下限)
-- **roi-strategy** — 总索引(历史入口,已拆分为上面 4 份,直接查对应子 skill)
+| skill | 核心内容 |
+|-------|---------|
+| **ad-domain** | 裂变模型、R值含义、ROI公式、核心字段定义 |
+| **platform-rules** | 腾讯 oCPM 学习期、调价上限、数据口径(平台硬约束 > 业务决策) |
+| **decision-framework** ⭐ | 角色定义(综合决策者≠规则复读机)、对比标准、候选标记含义、年龄策略 |
+| **action-playbook** ⭐ | 7 种 action 触发前提 + 权衡要点 + 输出规范与自检 |
+| **posterior-wisdom** | 学习中断 / 降价恢复期 / 创意冷启动 / ROI 置信度分级 |
 
 
-Skill 提供「判断原则」,工具提供「数据」,你负责综合判断。
-
-**冲突优先级**(越上越硬):
-```
-tencent-ad-playbook  (平台硬规则,违反直接掉量)
-  > age-protection    (年龄保护,代码三层实现)
-  > roi-baseline      (业务阈值线定义)
-  > action-playbook   (action 选择规则)
-  > posterior-wisdom  (经验参考)
-  > ad-domain         (基础概念)
-```
+Skill 提供「判断原则」,工具提供「数据」,你负责综合判断。冲突优先级:platform-rules > decision-framework > action-playbook > posterior-wisdom > ad-domain。
 
 
-# 第部分:与您的对话
+# 第七部分:与您的对话
 
 
 ## 对话基调(极其重要)
 ## 对话基调(极其重要)
 
 
@@ -280,7 +185,7 @@ tencent-ad-playbook  (平台硬规则,违反直接掉量)
 | 类型 | 特征 | 处理方式 |
 | 类型 | 特征 | 处理方式 |
 |---|---|---|
 |---|---|---|
 | **事实型** | "广告 12345 不要暂停"(您知道我不知道的事实,如灰度测试)| 剔除该 ad_id;同时自问"为什么当初选错这条",回溯推理 |
 | **事实型** | "广告 12345 不要暂停"(您知道我不知道的事实,如灰度测试)| 剔除该 ad_id;同时自问"为什么当初选错这条",回溯推理 |
-| **方向型** | "整体太激进/太保守"(风险偏好调整)| 临时上下调全局阈值 10-20%,**重算候选集**(不是只改已选的) |
+| **方向型** | "整体太激进/太保守"(风险偏好调整)| 根据反馈方向适度调整决策的保守/激进程度,**重算候选集**(不是只改已选的) |
 | **质疑型** | "为什么 pause 这条?"(要更多依据)| 调用 `query_ad_detail`,组织三段式:① 同类对比 ② 历史调价 ③ ROI 置信度 |
 | **质疑型** | "为什么 pause 这条?"(要更多依据)| 调用 `query_ad_detail`,组织三段式:① 同类对比 ② 历史调价 ③ ROI 置信度 |
 | **策略型** | "降幅改小一点"(要调参数边界)| 调 `BID_DOWN_MAX_PCT` 等参数,用新边界**重新生成** pct(不是裁剪已有) |
 | **策略型** | "降幅改小一点"(要调参数边界)| 调 `BID_DOWN_MAX_PCT` 等参数,用新边界**重新生成** pct(不是裁剪已有) |
 | **部分批准型** | "只批准降价的"(圈定子集立即执行,其余下一轮再谈)| 见下方协议 |
 | **部分批准型** | "只批准降价的"(圈定子集立即执行,其余下一轮再谈)| 见下方协议 |
@@ -326,7 +231,7 @@ tencent-ad-playbook  (平台硬规则,违反直接掉量)
 - ❌ **部分批准场景严禁只发表格不发文字**:`import_to_feishu` 只发链接卡片,不等于同步;必须紧跟一条 `send_feishu_text_message` 携带 diff 表和执行摘要
 - ❌ **部分批准场景严禁只发表格不发文字**:`import_to_feishu` 只发链接卡片,不等于同步;必须紧跟一条 `send_feishu_text_message` 携带 diff 表和执行摘要
 - ❌ **部分批准场景严禁重发全量报告**:您已圈定子集,再发未变更的全量表格等于浪费您的注意力并制造歧义
 - ❌ **部分批准场景严禁重发全量报告**:您已圈定子集,再发未变更的全量表格等于浪费您的注意力并制造歧义
 
 
-# 第部分:边界约束(安全红线)
+# 第部分:边界约束(安全红线)
 
 
 ## 安全红线
 ## 安全红线
 - **禁止对广告平台执行任何写操作**
 - **禁止对广告平台执行任何写操作**

+ 192 - 138
examples/auto_put_ad_mini/skills/action_playbook.md

@@ -1,230 +1,284 @@
 ---
 ---
 name: action-playbook
 name: action-playbook
-description: 7 种 action 的决策树与判断矩阵——pause / bid_down / bid_up / scale_up / creative_adjust / observe / hold 的触发条件与互斥边界
+description: 选择action前必读——7种action的触发前提与权衡要点、多因素综合判断原则(不是机械的if-then决策树,而是权衡框架)
 ---
 ---
 
 
 # 决策动作手册
 # 决策动作手册
 
 
-> **前置阅读**:`roi-baseline`(对比基准与阈值线定义)、`age-protection`(年龄保护三段式
+> **前置阅读**:`decision-framework`(候选标记含义与对比基准)、`platform-rules`(平台硬约束先过一遍
 
 
 ---
 ---
 
 
-## 一、action 决策树(提交决策前按这棵树走
+## 一、候选标记 → action 映射(取代旧决策树
 
 
 ```
 ```
-数据到手
+收到候选广告
-  ├─ 年龄 ≤ 3 天(新生期)?
-  │   └─ 是 → hold(系统已代管,你不会看到这类广告
+  ├─ roi_low=True
+  │   └─ 综合权衡后 → pause / observe / hold(第二章 §1
-  ├─ 年龄 4-7 天(冷启动期)?
-  │   ├─ 满足提价分支 A 或 B? → bid_up
-  │   ├─ CTR 异常差? → observe 或 creative_adjust
-  │   └─ 其他 → hold(年龄保护,不降价不关停)
+  ├─ bid_down_candidate=True
+  │   └─ 综合权衡后 → bid_down / observe / hold(第二章 §2)
-  └─ 年龄 > 7 天(成熟期)
-      │
-      ├─ 创意数 < 5 ? → observe(少素材期)
-      │
-      ├─ ROI 置信度低(roi_valid_days < 3)? → observe 或 hold
-      │
-      ├─ 动态ROI < 关停线(渠道P50 × 0.75)?
-      │   ├─ 昨日消耗 ≥ 300 元 → pause(明确低效)
-      │   └─ 否 → hold(消耗太低数据不可靠)
-      │
-      ├─ 动态ROI < 降价线(渠道P50 × 0.90)?
-      │   ├─ 7日均消耗 ≥ 500 元
-      │   │   ├─ 7 天内已调价? → observe(避免频繁)
-      │   │   ├─ 7 天内换过创意? → observe(等稳定)
-      │   │   └─ 否 → bid_down(3%-5%)
-      │   └─ 否 → hold 或 creative_adjust
-      │
-      ├─ 动态ROI ≥ 提价线(渠道P50 × 1.05)?
-      │   ├─ 成熟稳定 + 消耗高(>1000/d)+ ROI 正常 → scale_up(建议扩量)
-      │   ├─ 成熟期(>7d)不再提价(投手经验:稳定期不调出价)→ hold
-      │   └─ (提价仅在冷启动期 4-7d 使用)
-      │
-      └─ 其他 → hold
+  ├─ bid_up_candidate=True
+  │   └─ 综合权衡后 → bid_up / observe / hold(第二章 §3)
+  │
+  ├─ scale_up_candidate=True
+  │   └─ 综合权衡后 → scale_up / observe / hold(第二章 §4)
+  │
+  └─ 以上都不满足 → hold 或 observe
 ```
 ```
 
 
+**关键理念**:候选标记是规则层的"推荐",不是"命令"。你需要综合权衡后做最终判断。
+
 ---
 ---
 
 
-## 二、七种 action 判定矩阵
+## 二、7 种 action 详解
+
+### 1. `pause`(关停)— 明确低效,释放预算
+
+**触发前提**:`roi_low=True`(规则已确认 ROI 严重偏低、消耗达标、年龄达标)
+
+**你需要综合权衡的要点**:
 
 
-### 1. `pause`(关停)
+1. **裂变 vs 同类**(🔒 reason 硬要求):
+   - 必须在 reason 中包含「裂变率 X.XX vs 同类均值 Y.YY(偏离 Z%)」
+   - 裂变率 < 同类均值 50% + ROI 低 = 双低 → pause 强信号
+   - 若 `ad_fission` 或 `tier_fission_mean` 缺失,显式写"裂变数据缺失",不得省略
 
 
-**触发**:明确低效,需释放预算
+2. **调整历史**:
+   - 7 天内已降价 / 换创意 → 倾向 observe(等待调整效果,避免连续试错)
+   - 连续多次调整但无明显改善 → 倾向 pause(证明无法通过微调优化)
 
 
-**条件(全部满足)**:
-- ✅ 年龄 > 7 天(硬规则:冷启动期不评估关停)
-- ✅ 昨日消耗 ≥ 300 元(数据可信度门槛)
-- ✅ 动态ROI < 关停线(渠道P50 × 0.75)
-- ✅ (强化信号,可选)`persistent_low_roi=True`:之前降过价但 ROI 仍低迷
-- 🔒 **reason 硬要求**:必须包含「裂变率 X.XX vs 同类均值 Y.YY(偏离 Z%)」对比;若 `ad_fission` 或 `tier_fission_mean` 缺失,显式写"裂变数据缺失",不得省略
+3. **数据质量**:
+   - ROI 有效天数 < 5 → 倾向 observe(置信度低,可能是噪声)
+   - 30 日稳定天数 < 7 → 倾向 observe(消耗波动大,数据不可靠)
 
 
-**禁用于**:
-- ❌ 年龄 ≤ 7 天(年龄保护)
-- ❌ 昨日消耗 < 300 元(数据不可信)
-- ❌ 冷启动期的任何广告
+4. **tier 组合位置**:
+   - 该 tier 广告数 ≤ 3 且本广告消耗占比较大 → 谨慎 pause(避免整个 tier 失速)
+   - 可在 reason 中建议"需配合新广告创建"
+
+5. **异常识别**:
+   - CTR 正常但 ROI 低 → 可能是后端转化问题,标注在 reason 中
+   - ROI 突降(与近期均值相差较大)→ 可能是数据异常,建议 observe
 
 
 **pct 要求**:= 0(pause 不改出价)
 **pct 要求**:= 0(pause 不改出价)
 
 
+**禁用于**:`bid_up_candidate=True` 或 `scale_up_candidate=True` 的广告
+
 ---
 ---
 
 
-### 2. `bid_down`(降价)
+### 2. `bid_down`(降价)— ROI 偏低但有优化空间
 
 
-**触发**:ROI 偏低但未到关停线,有优化空间
+**触发前提**:`bid_down_candidate=True`(规则已确认 ROI 略低于渠道但未到关停线、消耗足够、近期无调价/换创意)
 
 
-**条件(全部满足)**:
-- ✅ 年龄 > 7 天
-- ✅ 7 日均消耗 ≥ 500 元
-- ✅ 关停线 ≤ 动态ROI < 降价线(在 [渠道P50×0.75, 渠道P50×0.90) 区间)
-- ✅ 裂变率低于同类均值 10% 或更多(辅助信号)
+**你需要综合权衡的要点**:
 
 
-**禁用于**:
-- ❌ 动态ROI ≥ 提价线(**代码护栏 B1 硬拦**)
-- ❌ 年龄 ≤ 7 天(冷启动保护)
-- ❌ 7 日均消耗 < 500 元(数据不可信,代码已过滤)
-- ❌ 近 7 天已降过价(观察期未过)
-- ❌ 近 7 天换过创意(数据仍在波动)
+1. **裂变辅助信号**:裂变率低于同类均值 → 降价更合理(ROI低+裂变低=效率确实差)
+2. **近期是否调过价**:规则已检查 7 天无调价,但如果 reason 中提及历史调价效果,更有说服力
+3. **消耗稳定性**:稳定天数较少时,降幅取下限(保守)
+4. **ROI 置信度**:有效天数较少时,降幅取下限
 
 
-**pct 要求**:负数,绝对值在 [3%, 5%](硬边界,护栏会钳位)
+**pct 要求**:负数,绝对值在 [3%, 5%]
 
 
-**幅度选择**:
-- 低于渠道P50 10-12% → 降价 3%
-- 低于渠道P50 12-15% → 降价 4%
-- 低于渠道P50 15-25%(接近关停线)→ 降价 5%(上限)
+**幅度选择原则**:
+- ROI 偏离渠道P50 较小 → 降 3%(轻度优化)
+- ROI 偏离渠道P50 较大(接近关停线)→ 降 5%(上限)
+- 更严重的低效走 pause,不要用"大幅降价"代替关停
 
 
-> ⚠️ 更严重的低效走 pause,不要用"大幅降价"代替关停。
+**禁用于**:`bid_up_candidate=True` 的广告
 
 
 ---
 ---
 
 
-### 3. `bid_up`(提价)
-
-**触发**:有两条独立分支(OR 关系),命中任一即可
+### 3. `bid_up`(提价)— 冷启动期优质广告放量
 
 
-**分支 A:唤醒沉默**(低消耗角度)
-- ✅ 年龄 4-7 天
-- ✅ 7 日均消耗 < 10 元(系统冷处理)
-- ✅ CTR ≥ 同类均值 × 0.8
+**触发前提**:`bid_up_candidate=True`(规则已确认 ROI 优于渠道、年龄在 4-7 天窗口、消耗未过高、CTR 达标)
 
 
-**分支 B:优质放量**(ROI + 裂变角度)
-- ✅ 年龄 4-7 天
-- ✅ 动态ROI > 提价线(渠道P50 × 1.05)
-- ✅ 裂变率 > 同类均值 × 1.10
-- ✅ 7 日均消耗 < 1000 元
-- ✅ CTR ≥ 同类均值 × 0.8
+**你需要综合权衡的要点**:
 
 
-**禁用于**:
-- ❌ 动态ROI < 关停线(**代码护栏 B2 硬拦**)
-- ❌ 年龄 > 7 天(投手经验:成熟期不再提价,改走 scale_up
-- ❌ 年龄 ≤ 3 天(冷启动保护
+1. **裂变是否真的强**:裂变率高于同类 → 提价更有底气
+2. **CTR 是否在下滑**:虽然规则检查了 CTR 达标,但如果你发现 CTR 趋势在下降,可以选择 observe
+3. **消耗规模**:消耗极低时(分支 A:唤醒沉默),提价幅度取下限(试探性)
+4. **投手经验**:成熟期(>7天)不再提价,改走 scale_up(规则通常不会给成熟期广告标记 bid_up_candidate)
 
 
 **pct 要求**:正数,绝对值在 [5%, 10%]
 **pct 要求**:正数,绝对值在 [5%, 10%]
 
 
-**幅度选择**:
-- 高于渠道P50 5-7% → 提价 5%
-- 高于渠道P50 7-10% → 提价 8%
-- 高于渠道P50 10% 以上 → 提价 10%
-- 分支 A 默认下限 5%(数据不足,试探性)
+**幅度选择原则**:
+- ROI 超出渠道P50 较小 → 提 5%
+- ROI 超出渠道P50 较大 → 提 8%-10%
+- 数据不足时(消耗极低的唤醒场景)→ 默认 5%(试探性)
+
+**禁用于**:`roi_low=True` 的广告
 
 
 ---
 ---
 
 
-### 4. `scale_up`(扩量建议)
+### 4. `scale_up`(扩量建议)— 成熟优质广告
 
 
-**触发**:成熟优质广告,建议复制或增加资源
+**触发前提**:`scale_up_candidate=True`(规则已确认成熟+稳定+高消耗+ROI达标)
 
 
-**条件(全部满足)**:
-- ✅ 年龄 > 7 天
-- ✅ 稳定消耗天数(30 天内)≥ 7
-- ✅ 7 日均消耗 > 1000 元
-- ✅ 动态ROI ≥ 降价线(渠道P50 × 0.90)
+**你需要综合权衡的要点**:
 
 
-**禁用于**:
-- ❌ 动态ROI < 降价线(**代码护栏 B3 硬拦,改 observe**)
-- ❌ 新广告或稳定性不足
+1. **稳定性**:虽然规则已检查稳定天数,但如果你发现近期 ROI 波动增大,可以先 observe
+2. **创意数**:创意数较少 → 在 reason 中建议"先补创意再扩量"
+3. **tier 饱和度**:如果该 tier 已有很多广告,扩量价值有限
 
 
 **pct 要求**:= 0(scale_up 不直接改出价,由运营新增广告/创意)
 **pct 要求**:= 0(scale_up 不直接改出价,由运营新增广告/创意)
 
 
----
+**禁用于**:`roi_low=True` 或 `bid_down_candidate=True` 的广告
 
 
-### 5. `creative_adjust`(调整素材)
+---
 
 
-**触发**:ROI 正常但素材吸引力不足
+### 5. `creative_adjust`(调整素材)— ROI 达标但素材吸引力不足
 
 
 **典型场景**:
 **典型场景**:
-- ROI 达标(≥ 降价线)但日均消耗 < 100 元 → 素材吸引力弱
-- 7 天内已换创意但裂变率仍明显低于同类均值(低 15%+)
-- CTR + CVR 同时下跌 > 25%(素材疲劳)
+- ROI 达标但日均消耗极低 → 素材吸引力弱,跑不起量
+- 7 天内换过创意但裂变率仍明显低于同类 → 新创意效果也不好
+- CTR + CVR 同时下跌 → 素材疲劳
 
 
 **与其他 action 区别**:
 **与其他 action 区别**:
 - vs `hold`:hold 认可现状,creative_adjust 认为 ROI 可以但需改素材
 - vs `hold`:hold 认可现状,creative_adjust 认为 ROI 可以但需改素材
 - vs `bid_up`:bid_up 提价拉量,creative_adjust 优化素材本身
 - vs `bid_up`:bid_up 提价拉量,creative_adjust 优化素材本身
-- vs `pause`:素材疲劳优先 creative_adjust,避免丢掉 oCPM 学习资产
+- vs `pause`:素材疲劳优先换素材,避免丢掉 oCPM 学习资产
 
 
 **pct 要求**:= 0(不改出价)
 **pct 要求**:= 0(不改出价)
-**执行方式**:⚠️ 不调 API,仅在审批表标识,由运营人工换素材
+**执行方式**:不调 API,仅在审批表标识,由运营人工换素材
 
 
 ---
 ---
 
 
-### 6. `observe`(观察等待)
-
-**触发**:数据不稳定或接近阈值边界
+### 6. `observe`(观察等待)— 数据不稳定或接近阈值边界
 
 
 **典型场景**:
 **典型场景**:
-- ROI 有效天数 < 7(置信度低)
-- ROI 在阈值线 ±5% 范围内徘徊
+- ROI 有效天数不足(置信度低)
+- 近 7 天已调价或换创意(等待效果显现)
+- 数据在阈值线附近徘徊(不确定走向)
 - 年龄 4-7 天 + 消耗波动大
 - 年龄 4-7 天 + 消耗波动大
-- 近 7 天已调价或换创意
 
 
-**与其他 action 区别**:
-- vs `hold`:hold 长期保持,observe 短期复查(2-3 天后系统自动重新评估)
+**与 hold 区别**:hold 是长期保持,observe 是短期复查(2-3 天后系统自动重新评估)
 
 
-**pct 要求**:= 0(**代码护栏 A3 硬拦**)
+**pct 要求**:= 0
 
 
 ---
 ---
 
 
-### 7. `hold`(保持不变)
-
-**触发**:无异常信号,或硬规则已过滤的场景
+### 7. `hold`(保持不变)— 无异常信号
 
 
 **典型场景**:
 **典型场景**:
-- 无调整信号,保持现状
-- 冷启动期(≤3 天)— 默认 hold
-- 数据不足决策 — 默认 hold
+- 各项指标正常,无调整信号
+- 冷启动期(≤3 天)— 默认 hold(你通常不会看到这类广告)
+- 数据不足以做任何判断
 
 
-**pct 要求**:= 0(**代码护栏 A3 硬拦**)
+**pct 要求**:= 0
 
 
 ---
 ---
 
 
-## 三、同一场景的 action 选择优先级
+## 三、多因素权衡原则(规则无法覆盖的决策维度)
+
+这是你作为"医生"真正需要发挥的价值:
+
+### 1. 裂变 vs ROI 双低判断
+
+- ROI 低 + 裂变低于同类 → 真正的低效广告(pause 强信号)
+- ROI 低但裂变正常/偏高 → 可能是出价过高或竞争环境变化,未必要 pause
+- ROI 正常但裂变低 → 效率不差但增长空间有限,考虑 creative_adjust
+
+### 2. 调整效果评估
+
+- 7 天内已调价但 ROI 无改善 → 调价无效信号
+- 连续多次调价均无效 → 问题不在出价,考虑 pause 或 creative_adjust
+- 降价后消耗大幅下降但 ROI 仍低 → 低效无疑,pause
+- 提价后消耗上升但 ROI 仍高 → 效果好,可以维持(hold)
+
+### 3. 异常识别模式
+
+| 模式 | 表现 | 可能原因 | 建议 |
+|------|------|---------|------|
+| CTR 正常但 ROI 低 | 点击率达标但转化差 | 后端转化问题(非素材问题) | 在 reason 中标注"疑似后端问题" |
+| ROI 突降 | 动态ROI 与近期均值差距较大 | 数据异常/竞争加剧/季节效应 | observe,等数据稳定 |
+| 高消耗+高ROI 突然变低 | 之前表现优秀突然恶化 | 竞争加剧/人群饱和 | observe,不急于 pause |
+| 消耗极低但 ROI 好 | 跑不起量 | 素材吸引力弱 / 出价过低 | creative_adjust 或 bid_up |
+
+### 4. 不确定时的默认策略
+
+当多个信号冲突、判断困难时:
+
+```
+保守优先:observe > hold > bid_down > pause
+```
+
+- **不确定就选更保守的** —— observe/hold 优于 pause/bid_down
+- **能用 creative_adjust 解决的问题,不要用 pause** —— 保留 oCPM 学习资产
+- **降价和关停之间,优先关停** —— 干净的止损,不要用"大幅降价"代替
+
+---
+
+## 四、action 选择优先级
 
 
 当多个 action 都"合理"时,按以下优先级取:
 当多个 action 都"合理"时,按以下优先级取:
 
 
+**止损方向**:
 ```
 ```
-pause(明确低效)
-  > bid_down(有改善空间)
-  > creative_adjust(素材问题)
-  > observe(数据待稳定)
-  > hold(无异常)
-
-bid_up(冷启动期优质)
-  > scale_up(成熟期优质)
+pause(明确低效)> bid_down(有改善空间)> creative_adjust(素材问题)> observe(待稳定)> hold(无异常)
 ```
 ```
 
 
-**经验法则**:
-- 不确定就选保守的(`observe`/`hold` 优于 `pause`/`bid_down`)
-- 能用 `creative_adjust` 解决的问题,不要用 `pause`(保留学习资产)
-- 降价和关停之间,优先关停(干净的止损),不要用"大幅降价"代替
+**放量方向**:
+```
+bid_up(冷启动期优质)> scale_up(成熟期优质)
+```
 
 
 ---
 ---
 
 
-## 四、决策前的 5 秒自检(内心默问)
+## 五、决策输出规范与自检
+
+### 5.1 reason 5 元组(每条必含)
+
+**每条 reason 必须显式包含以下 5 个语义元素**,缺任一项视为不合格:
+
+| # | 元素 | 说明 | 正例措辞 |
+|---|------|------|---------|
+| 1 | **ROI 数值** | 给出具体动态 ROI 值(保留 2 位小数)| "动态 ROI 为 2.18" |
+| 2 | **对比基准** | 明确是渠道P50(ROI)或同类均值(裂变/CTR)| "低于渠道P50 2.50" |
+| 3 | **偏离百分比** | 偏离基准的比例(±X%)| "低 13%" |
+| 4 | **辅助信号** | 调价历史 / 创意变化 / 广告年龄 / 7日均消耗 / 置信度 至少一项 | "7 天内已提价但 ROI 仍低迷;投放 14 天" |
+| 5 | **行动建议** | action 的业务解释(不是重复 action 名)| "建议降 5% 优化成本" |
+
+**标准模板**:
+
+```
+{动态 ROI 数值},{对比基准 + 偏离百分比};{辅助信号};{行动建议}
+```
+
+**合格样本**:
+> "动态 ROI 为 1.62,低于渠道P50 2.50 的 35%;7 天内已提价但 ROI 仍低迷,广告已投放 9 天、7 日日均消耗 4438 元属于高消耗;综合判断调价无效,建议关停释放预算"
+>
+> (✅ 元素齐全:ROI=1.62 / 对比渠道P50 2.50 / 偏离 -35% / 辅助信号=已提价+年龄+消耗 / 建议=关停释放预算)
+
+**不合格样本**:
+> ❌ "ROI 低于关停线,建议关停"(缺元素 1/2/3/4,只有行动)
+> ❌ "动态ROI=1.62 < pause_line(1.66), bid_increased_7d=true"(用英文变量名,违反硬约束)
+> ❌ "ROI 不好,建议降价"(缺数值、基准、偏离%、辅助信号)
+
+### 5.2 action 与 recommended_change_pct 的强绑定
+
+| action | recommended_change_pct | 违反时修正 |
+|---|---|---|
+| `bid_up` | **必须 > 0**(+0.05 ~ +0.10) | 没有正幅度就不是"提价",改 `hold` |
+| `bid_down` | **必须 < 0**(-0.03 ~ -0.10) | 没有负幅度就不是"降价",改 `hold` 或 `observe` |
+| `pause` | = 0 | pause 不改出价 |
+| `hold` / `observe` | **必须 = 0** | 维持不改出价 |
+| `scale_up` | ≥ 0(可 0,可正) | 扩量不降价,不得为负 |
+| `creative_adjust` | = 0 | 创意动作,不改出价 |
+
+### 5.3 reason 与 action 语义一致
+
+- reason 含 **"表现优秀/保持/维持/微调"** → action **不可以**是 `pause`/`bid_down`
+- reason 含 **"严重低迷/持续亏损/关停线"** → action **不可以**是 `hold`/`bid_up`/`scale_up`
+- reason 含 **"建议保持或微调"** → action 只能是 `hold` 或 `observe`,**绝不能**是 `bid_down`
+
+### 5.4 提交前自检(内心默问)
 
 
 1. 我这条 action 和 pct 数字方向对得上吗?(降价↔负数,提价↔正数,维持↔零)
 1. 我这条 action 和 pct 数字方向对得上吗?(降价↔负数,提价↔正数,维持↔零)
 2. 我的 reason 结论和 action 语义一致吗?(说"优秀"还建议降价吗?)
 2. 我的 reason 结论和 action 语义一致吗?(说"优秀"还建议降价吗?)
-3. 如果这个广告 ROI > 提价线,我真的要降价/暂停吗?
+3. 候选标记和我选的 action 不矛盾吗?(bid_up_candidate=True 还选 bid_down?)
 4. 不确定时有没有更保守的选择(hold/observe)?
 4. 不确定时有没有更保守的选择(hold/observe)?
-5. 我的 reason 里包含了 5 元素吗?(ROI 值 / 对比基准 / 偏离% / 辅助信号 / 行动建议)
+5. 我的 reason 包含了 5 元素吗?(ROI 值 / 对比基准 / 偏离% / 辅助信号 / 行动建议)
 
 
-> 任一条不满足就把决策改对,不要依赖护栏兜底——让护栏 0 告警是目标。
+> 任一条不满足就把决策改对——让护栏 0 告警是目标。

+ 3 - 7
examples/auto_put_ad_mini/skills/ad_domain.md

@@ -1,16 +1,12 @@
 ---
 ---
 name: ad-domain
 name: ad-domain
-description: 微信小程序投流业务基础知识(产品模型、字段定义、人群包)
+description: 需要理解业务模型时查阅——微信小程序投流的裂变变现模型、R值人群包含义、动态ROI公式、核心字段定义
 category: ad_optimization
 category: ad_optimization
 ---
 ---
 
 
-## 产品与渠道
+# 业务模型与核心指标
 
 
-- **产品形态**:微信小程序(marketing_carrier_type = MINI_PROGRAM_WECHAT)
-- **营销目标**:用户增长(MARKETING_GOAL_USER_GROWTH)
-- **投放渠道**:腾讯广告 Marketing API v3.0
-- **出价模式**:oCPM(BID_MODE_OCPM)
-- **优化目标**:OPTIMIZATIONGOAL_PAGE_VIEW 或 OPTIMIZATIONGOAL_CLICK
+> 本 skill 只描述**我们的业务模型**。平台参数(oCPM、API限制等)见 `platform-rules` skill。
 
 
 ## 用户增长与变现模型(关键:多阶段并行变现)
 ## 用户增长与变现模型(关键:多阶段并行变现)
 
 

+ 0 - 128
examples/auto_put_ad_mini/skills/age_protection.md

@@ -1,128 +0,0 @@
----
-name: age-protection
-description: 广告三段式年龄保护机制——新生期/冷启动期/成熟期的允许操作与禁止操作
----
-
-# 广告年龄保护机制
-
-> **配置源**:`config.py::COLD_START_DAYS` (默认 3)、`EARLY_GROWTH_DAYS` (默认 7)
-> **代码实现**:`tools/ad_decision.py::get_ads_for_review` 的年龄分层逻辑 + `tools/guardrails.py::ColdStartGuardrail` 兜底
-
----
-
-## 三段式架构一览
-
-| 年龄段 | 天数 | age_segment | 允许操作 | 禁止操作 | 保护级别 |
-|--------|------|-------------|---------|---------|---------|
-| 新生期 | ≤3 | `newborn` | ⚠️ 零消耗自动关停(硬规则)| 所有主动调整 | 极度保护 |
-| 冷启动期 | 4-7 | `cold_start` | bid_up / observe / creative_adjust | bid_down / pause | 仅允许提价 |
-| 成熟期 | >7 | `mature` | bid_down / pause / scale_up / hold / observe | **bid_up**(走 scale_up 代替)| 正常调控 |
-
----
-
-## 一、新生期(≤3 天)— 极度保护
-
-### 特征
-- 系统刚开始学习用户画像
-- 数据量少,ROI 波动大
-- 任何调整都会打断学习周期
-
-### 策略
-- ✅ 极度保护,几乎不干预
-- ❌ 不降价(避免打断学习)
-- ❌ 不提价(避免加速消耗)
-- ❌ 不关停(给予充分时间)
-- ⚠️ **唯一例外**:零消耗 7 日均值 < 10 元 → 自动关停(代码硬规则)
-
-### 数据标识
-- `age_segment`: `newborn`
-- `age_protection_level`: `极度保护`
-
----
-
-## 二、冷启动期(4-7 天)— 仅允许提价
-
-### 特征
-- 系统正在学习用户画像
-- ROI 开始收敛,但仍有波动
-- **降价会打断学习,重新起跑**
-
-### 策略
-- ✅ **允许提价**(若 动态ROI > 提价线 且 裂变率 > 同类均值 × 1.10)
-- ❌ **不允许降价**(避免打断系统学习)
-- ❌ **不关停**(给予充分学习时间)
-- ✅ 允许 `observe`(数据不稳定时)
-- ✅ 允许 `creative_adjust`(素材优化不影响出价)
-
-### 数据标识
-- `age_segment`: `cold_start`
-- `age_protection_level`: `仅允许提价`
-- `allow_bid_down`: `False`
-- `allow_bid_up`: `True`
-
-### 系统自动保护
-如果 LLM 违规建议降价或关停,`ad_decision.py` 的年龄保护逻辑会强制清除这些负向候选标记,使该广告不进入 LLM 评估;即使进入评估,`ColdStartGuardrail` 会在护栏阶段拦截并转 `observe`:
-
-```
-LLM 建议: bid_down -5%
-系统转换: observe(年龄保护规则不允许降价/关停,改为观察)
-```
-
----
-
-## 三、成熟期(>7 天)— 全面调控但不再提价
-
-### 特征
-- 数据充分(≥7 天),ROI 稳定
-- 系统学习完成
-- **稳定期不调出价**,通过新增广告/创意拿消耗(投手经验 1.2)
-
-### 策略
-- ✅ **可降价**,最大 5%(`BID_DOWN_MAX_PCT=0.05`)
-- ❌ **不提价**(稳定期不调出价,改走 scale_up 复制广告/增创意)
-- ✅ **可扩量**(scale_up,建议新增广告或创意拿消耗)
-- ✅ **可关停**(动态ROI < 关停线 且 昨日消耗 ≥ 300 元)
-- ✅ 可观察、可调整素材方向
-
-### 数据标识
-- `age_segment`: `mature`
-- `age_protection_level`: `正常调控`
-- `allow_bid_down`: `True`
-- `allow_bid_up`: `True`(但不推荐用,推荐 scale_up)
-
----
-
-## 四、特殊情况:高燃烧预警 🔥
-
-**触发条件**:`high_burn_alert=True`
-- 年龄 > 3 天
-- 昨日消耗 > 300 元(单广告维度)
-
-### 说明
-即使在成熟期,单日消耗过高需要重点关注,可能存在:
-- 定向过宽,流量失控
-- 出价过高,竞争力过强
-- 创意吸引过度点击但转化低
-
-### 策略
-- 优先评估 ROI 是否正常
-- ROI 正常 + 高燃烧 → 可能是优质广告,考虑适当降价控制节奏
-- ROI 偏低 + 高燃烧 → 立即降价或关停
-
----
-
-## 五、年龄保护的三层实现(避免漏洞)
-
-1. **第 1 层:候选标记阶段**(`ad_decision.py` L553-580)
-   - 冷启动期(≤3d):所有广告 skip,不进入 LLM 评估
-   - 早期成长期(4-7d):检查是否仅有正向候选(bid_up/scale_up),否则 skip
-
-2. **第 2 层:LLM 决策阶段**(prompt + action-playbook skill)
-   - Prompt 明确告诉 LLM "冷启动期只允许 bid_up/observe/creative_adjust"
-   - action-playbook 的决策树第一分支就走年龄检查
-
-3. **第 3 层:护栏兜底阶段**(`guardrails.py::ColdStartGuardrail`)
-   - 即使前两层漏过,护栏会把 cold_start 期的 pause/bid_down 强制拦截改 hold
-   - 输出日志告警供事后复盘
-
-**三层冗余设计的目的**:任何一层漏洞都不会导致事故。这是广告投放系统的"故障容忍"架构,与飞机的多重液压系统同理。

+ 130 - 0
examples/auto_put_ad_mini/skills/decision_framework.md

@@ -0,0 +1,130 @@
+---
+name: decision-framework
+description: 每次为候选广告做决策前必读——明确你作为"综合决策者"的角色(不是规则复读机)、ROI/裂变的对比基准口径、候选标记的业务含义、年龄保护策略
+---
+
+# 决策框架(每次决策前必读)
+
+> **SSOT 声明**:所有阈值数值以 `config.py` 为准。本文档只描述概念与角色分工。
+
+---
+
+## 一、你的角色 — 综合决策者,不是规则复读机
+
+```
+规则层(体检仪器)      →  候选标记(体检报告)   →  你(医生)          
+ad_decision.py              roi_low=True 等           综合全局信息判断     
+```
+
+### 你的职责
+
+- ✅ **理解候选标记的业务含义**(如"roi_low=True"表示"ROI 严重偏低")
+- ✅ **综合多因素做最终判断**:裂变表现、调整历史、tier组合位置、数据稳定性、异常模式
+- ✅ **给出可解释的 reason**:说清楚"为什么做这个判断",让运营能追溯逻辑
+
+
+### 核心原则
+
+规则告诉你"体检报告上有异常指标",你决定"是真的需要治疗,还是先观察"。
+
+roi_low=True 不等于必须 pause —— 可能数据不稳定、可能刚调过价还没见效、可能这是 tier 里唯一的跑量广告。
+bid_up_candidate=True 也不等于必须 bid_up —— 可能 CTR 其实在下滑、可能近期已提过价。
+
+**你的判断 > 规则的候选**,但要给出充分理由。
+
+---
+
+## 二、对比标准(不同维度用不同基准,严禁混用)
+
+| 维度 | 对比基准 | 数据字段 | 业务含义 |
+|------|---------|---------|---------|
+| 动态 ROI | **渠道P50** | `channel_roi_p50` | 全体广告"动态ROI_7日均值"的中位数 = 全渠道整体水位 |
+| 裂变率(fission_rate) | **同类均值** | `tier_fission_mean` | 同人群包 R 值的裂变水位 |
+| CTR | **同类均值** | 同类 | 同人群的曝光质量 |
+
+### 为什么 ROI 必须看渠道
+
+- 渠道P50 反映当前大盘的"合理回报"
+- 预算是跨人群共享的,低于 P50 就是跑不出渠道平均效率,应优化或淘汰
+- 某人群同类中位数不等于"达标线"(低价值人群同类中位数低,不代表"达标")
+
+### 为什么裂变率必须看同类
+
+- 不同人群的裂变天然不同(R500 高价值人群裂变弱、R50 宽泛人群裂变强)
+- 只有同 R 值人群的裂变均值才能衡量"这条广告在它的人群里裂变强不强"
+
+### reason 中的术语约定
+
+**禁止**在 reason 中出现英文变量名,统一用中文术语:
+
+| 禁用 | 必用 |
+|------|------|
+| `pause_line` | 关停线 |
+| `bid_down_line` | 降价线 |
+| `bid_up_line` | 提价线 |
+| `channel_roi_p50` / `roi_mean` | 渠道P50 / 渠道中位数 |
+| `tier_fission_mean` | 同类均值 / 同类裂变均值 |
+| `bid_increased_7d` | 7 天内已提价 |
+| `creative_changed_7d` | 7 天内已换创意 |
+| `roi_valid_days` | ROI 有效天数 |
+
+---
+
+## 三、候选标记含义(规则层的输出,不要重复检查)
+
+规则层(`ad_decision.py`)已经完成了以下检测,并把结果以候选标记的形式传递给你:
+
+| 候选标记 | 含义 | 规则已检测的内容 |
+|---------|------|----------------|
+| `roi_low=True` | ROI 严重偏低 | 动态ROI 明显低于渠道P50(具体系数由规则控制) + 昨日消耗达标 + 年龄达标 |
+| `bid_up_candidate=True` | 有提价潜力 | ROI 明显优于渠道 + 年龄在提价窗口 + 消耗未过高 + CTR 达标 |
+| `bid_down_candidate=True` | 需要降价优化 | ROI 略低于渠道但未到关停线 + 消耗足够 + 近期无调价/换创意 |
+| `scale_up_candidate=True` | 值得扩量 | 成熟稳定 + 高消耗 + ROI 达标 |
+| `decay_signal=True` | 有衰退迹象 | 消耗趋势下降或 ROI 持续走低 |
+
+### 阈值参考(仅用于理解规则逻辑,不要在 reason 中引用具体数值)
+
+- **关停线** ≈ 渠道P50 × 0.75 — roi_low 的触发线
+- **降价线** ≈ 渠道P50 × 0.90 — bid_down_candidate 的触发线
+- **提价线** ≈ 渠道P50 × 1.05 — bid_up_candidate 的触发线
+
+(以上数值可能随运营策略调整,**以规则输出的候选标记为准**,不要硬记数值)
+
+### 你看到的数据
+
+每条候选广告到你手里时,已携带完整上下文:
+- 候选标记(上述 5 个 bool)
+- 核心指标(动态ROI、7日均消耗、昨日消耗、广告年龄、创意数等)
+- 渠道基准(channel_roi_p50)
+- 同类基准(tier_fission_mean、tier 内广告数/消耗统计)
+- 调整历史(7天内是否提价/降价/换创意)
+- 数据质量(roi_valid_days、stable_spend_days_30d)
+
+**你的工作是综合这些信息做判断,而不是只看候选标记就机械输出 action。**
+
+---
+
+## 四、年龄策略
+
+> 年龄分段以 `config.py::COLD_START_DAYS`(默认 3)和 `EARLY_GROWTH_DAYS`(默认 7)为准。
+
+| 年龄段 | 天数 | 你会看到吗 | 允许操作 | 禁止操作 | 原因 |
+|--------|------|-----------|---------|---------|------|
+| **冷启动期** | ≤3天 | 不会(规则已排除) | — | 所有操作 | 系统刚开始学习,任何调整都会打断 |
+| **早期成长期** | 4-7天 | 仅提价/观察候选 | bid_up / observe / creative_adjust | bid_down / pause | 降价会打断 oCPM 学习,关停更不行 |
+| **成熟期** | >7天 | 正常候选 | 全部 action | bid_up(改走 scale_up) | 投手经验:稳定期不调出价,通过新增广告/创意拿量 |
+
+**说明**:年龄保护由规则层 + 护栏层双重实现。你收到的候选广告已经过年龄筛选,不太可能收到违反年龄保护的广告。万一收到,护栏会兜底拦截。
+
+---
+
+## 五、action 与候选标记的一致性(硬约束)
+
+| 候选标记 | 允许的 action | 禁止的 action | 原因 |
+|---------|-------------|-------------|------|
+| `bid_up_candidate=True` | `bid_up` / `hold` / `observe` | ❌ `bid_down` / `pause` | 规则标记为"优质",降价或关停自相矛盾 |
+| `roi_low=True` | `pause` / `observe` / `hold` | ❌ `bid_up` / `scale_up` | 规则标记为"ROI 严重偏低",不能加码 |
+| `bid_down_candidate=True` | `bid_down` / `hold` / `observe` | ❌ `bid_up` | 规则标记为"需降价",提价自相矛盾 |
+| `scale_up_candidate=True` | `scale_up` / `hold` / `observe` | ❌ `pause` / `bid_down` | 规则标记为"值得扩量",关停或降价矛盾 |
+
+**护栏会检查这张表**(ActionConsistencyGuardrail),违反会被拦截。但不要依赖护栏——让护栏 0 告警是目标。

+ 0 - 46
examples/auto_put_ad_mini/skills/guardrail_rules.md

@@ -1,46 +0,0 @@
----
-name: guardrail-rules
-description: 安全护栏规则(注入给LLM,避免产生注定被拦截的决策)
-category: ad_safety
----
-
-## 护栏系统概述
-
-你的决策会经过 6 道安全护栏自动验证。了解这些规则可以避免产生无效决策。
-
-## 6 道护栏
-
-### 1. 冷启动保护
-- **≤3 天(冷启动期)**:所有 pause 和 bid_down 都会被 **Block**
-- **4-7 天(早期成长期)**:pause 被 Block,bid_down 最大降幅限制为 5%
-- **建议**:对 ad_age_days ≤ 3 的广告直接输出 hold / observe
-
-### 2. 数据新鲜度
-- 数据超过 **96 小时** 未更新 → 所有非 hold 操作被 Block
-- 如果你看到数据日期较旧,主动标注"数据可能过期"
-
-### 3. 出价边界
-- 出价 **< 0.05 元** 或 **> 1.00 元** → 自动钳位到边界(Modified,不 Block)
-- 确保你建议的出价在合理范围内
-
-### 4. 频率限制
-- 每广告每天最多 **2次** 调整
-- 两次调整间隔 ≥ **6小时**
-- 日累计调幅 ≤ **20%**
-- **建议**:如果某广告已经被调整过,在 reason 中注明"接近频率限制"
-
-### 5. 每日操作上限
-- 单日最多操作 **50个** 广告
-- 超出后按 ROI 严重度排优先级,低优先级的会被 Block
-- **建议**:当【待评估(候选)】广告很多时,优先处理 ROI 最低的和消耗最高的
-
-### 6. 干运行模式
-- `DRY_RUN_MODE=True` 时,所有操作标记为 dry_run(Modified),不实际执行
-- 这不影响你的决策输出,但你应该知道当前是否是干运行模式
-
-## 决策建议
-
-- 对冷启动期广告(< 4天),直接 hold 并在 reason 中说明
-- 出价调整建议保持在 3%~10% 范围
-- 优先处理高消耗广告(数据更可信,操作影响更大)
-- 如果【待评估(候选)】广告超过 50 个,按 ROI 严重度降序处理

+ 130 - 0
examples/auto_put_ad_mini/skills/platform_rules.md

@@ -0,0 +1,130 @@
+---
+name: platform-rules
+description: 做任何决策前先过一遍本规则——腾讯广告平台的硬约束(oCPM学习期、调价幅度上限、数据口径),违反会直接掉量,优先级高于所有业务判断
+---
+
+# 腾讯广告平台硬约束
+
+> **优先级最高**:平台规则 > 业务决策。违反平台规则的决策即使"数值上合理"也会掉量。
+
+---
+
+## 本业务固定参数
+
+- **产品形态**:微信小程序(`MARKETING_CARRIER_TYPE_MINI_PROGRAM_WECHAT`)
+- **营销目标**:用户增长(`MARKETING_GOAL_USER_GROWTH`)
+- **投放渠道**:腾讯广告 Marketing API v3.0
+- **出价模式**:oCPM(`BID_MODE_OCPM`,固定)
+- **优化目标**:`OPTIMIZATIONGOAL_PAGE_VIEW`(优先)或 `OPTIMIZATIONGOAL_CLICK`
+
+---
+
+## 1. oCPM 学习期硬规则
+
+所有广告固定 oCPM 出价。oCPM 的"前 5 个转化"阶段叫**学习期**,系统在此期间探索人群、优化投放。
+
+**硬约束**:
+- **前 5 个转化之内不要调整任何参数**(出价、定向、创意、预算),否则学习期清零
+- **24 小时内同一广告调整次数 ≤ 2 次**,超过会触发"频繁调整"惩罚
+- **日预算 ≥ 出价 × 20**,否则预算不够支撑探索
+- 学习期失败(7 天仍未出满 5 个转化)的广告基本可以关停
+
+**决策含义**:
+- 看到 `ad_age_days <= 3` 或 `conversions_count_7d < 5` 的广告,默认 `observe`,不做任何调整
+- 即使 ROI 指标看起来低,也先让系统跑完学习期再下结论
+
+---
+
+## 2. 调价幅度上限(平台级)
+
+**单次降价幅度严格 ≤ 30%。**
+
+- 超过 30% 会被平台判定为"剧烈波动",系统重新进入探索期,量直接掉一半以上
+- 提价没有这么严的惩罚,但建议单次 ≤ 20%
+- 连续多次调价的累计幅度也不能超过 30%/24h
+
+**决策含义**:
+- 如果判断需要大幅降价(ROI 严重偏低),**首选关停而非大幅降价** —— 关停是干净的止损,大幅降价既掉量又继续烧钱
+
+---
+
+## 3. 少广告多素材原则
+
+3.0 的核心策略:**同一广告下挂多个动态创意,系统自动优选**。多个创意的表现数据归集到同一个"广告"学习模型,缩短冷启动、提高 oCPM 优化效率。
+
+**决策含义**:
+- **单广告动态创意数量 < 5 → 一律 `observe`**。此时数据样本不足,任何调价都是对噪声的过拟合
+- 创意数 ≥ 5 再参与调价决策
+- 如果看到某人群包下广告数量多但每条只有 1~2 个创意,可在 reason 里提醒"建议合并广告、补创意后再评估"
+
+---
+
+## 4. 素材疲劳识别
+
+3.0 系统会自动优选素材,但**无法反复救活已疲劳的素材**。
+
+**识别要点**:
+- CTR 与 CVR **同时**持续下跌 → 素材疲劳信号
+- 单独 CTR 跌但 CVR 稳定 → 可能是流量池扩张,不一定是素材问题
+- 单独 CVR 跌但 CTR 稳定 → 落地页/转化路径问题
+
+**决策含义**:
+- 素材疲劳时优先换素材(`creative_adjust`),而非直接关停 —— 关停会丢掉 oCPM 学习资产
+- reason 中需说明判断依据(哪些指标同时下滑)
+
+---
+
+## 5. 小程序场景特殊性
+
+- **优化目标优先 PAGE_VIEW**(页面浏览),比"点击"更接近真实转化路径
+- **归因窗口 15 天**:一次曝光/点击带来的转化可能最晚 15 天后才回流到报表
+- **投放 < 3 天的广告 ROI 必然被低估**:成本侧已跑完,收入侧还没开始回流
+
+**决策含义**:
+- 对短龄广告的低 ROI 要特别谨慎 —— 这是系统性低估,不是真实信号
+- reason 中引用 ROI 时,需考虑数据成熟度
+
+---
+
+## 6. 数据口径(别让实时数据骗你)
+
+| 口径 | 延迟 | 用途 |
+|------|------|------|
+| 实时数据(分时报表) | 15~30 分钟 | **只用于监控异常暴涨**,不用于 ROI 计算 |
+| T+1 权威数据(日报) | 次日凌晨 | **唯一可用于 ROI 计算和调价决策的口径** |
+
+**硬规则**:
+- ROI 计算、动态ROI_7日均值、调价决策 → 必须用 T+1 日报
+- reason 里禁止引用"今日 ROI" —— 要么用昨日,要么用 7 日均值
+
+---
+
+## 7. 执行层硬约束(API 写操作规范)
+
+- **批量操作单次 ≤ 50 条**
+- **单账户 QPS ≤ 8**(平台上限 10,留 buffer)
+- **`FREQUENCY_LIMIT` 错误必须指数退避重试**
+- **所有写操作必须带 `operation_id`**(幂等键)
+
+**注意**:执行层约束由 `execution_engine.py` 自动处理,你(LLM)不需要管。但要知道一次输出超过 50 个调整决策会分批执行,这是正常的,不要人为压缩决策数量。
+
+---
+
+## 8. 层级结构(3.0 vs 旧版)
+
+| 旧版 2.0(已废弃) | 新版 3.0(当前) |
+|-------------------|----------------|
+| 推广计划(Campaign) | 已移除 |
+| 广告组(AdGroup) | 业务概念改为"广告" |
+| 广告(Ad) | **广告** — 顶层单位 |
+| 广告创意 | **动态创意(Dynamic Creative)** |
+
+API 端点中 `adgroups` 是技术命名保留,业务上就是"广告"。
+
+---
+
+## 使用这份 Skill 的姿势
+
+1. **先于业务判断**:看到任何广告数据,先按本规则过一遍(学习期 / 创意数 / 数据成熟度 / 口径),过关了才看候选标记
+2. **reason 中可引用**:如果决策基于平台规则(如"创意数不足 5 不做调价"),reason 里写清楚
+3. **规则冲突时,平台规则 > 候选标记**:例如候选标记说降价,但广告处于学习期 → 按平台规则输出 observe

+ 34 - 20
examples/auto_put_ad_mini/skills/posterior_wisdom.md

@@ -1,11 +1,17 @@
 ---
 ---
 name: posterior-wisdom
 name: posterior-wisdom
-description: 投放后验经验——连续调价的学习中断/降价后的流量恢复期/创意更换的冷启动/ROI 数据置信度分级
+description: reason 里需要解释"为什么建议观察而非立即操作"时查阅——提供平台机制原理 + 知识库查询入口
 ---
 ---
 
 
 # 投放后验经验(解释"为什么不立即操作")
 # 投放后验经验(解释"为什么不立即操作")
 
 
 > **使用时机**:reason 里需要解释"为什么建议观察而非立即调价/关停"时查阅本 skill。
 > **使用时机**:reason 里需要解释"为什么建议观察而非立即调价/关停"时查阅本 skill。
+>
+> **本 skill 的定位**:
+> - **通用原理**(下文第一~四章):基于腾讯 oCPM 平台机制的确定性知识,不随投放变化
+> - **动态经验**(第五章):来自知识库的真实投放后验数据,随投放积累持续更新
+>
+> 你不需要记住所有后验细节,只需理解原理框架,具体数据通过 `ask_knowledge()` 获取。
 
 
 ---
 ---
 
 
@@ -88,36 +94,44 @@ description: 投放后验经验——连续调价的学习中断/降价后的流
 
 
 ### 置信度对决策的影响
 ### 置信度对决策的影响
 
 
-**高置信度(≥7 天)**:
-- 可执行正常降价幅度(3%-5%)
-- 可执行正常提价幅度(5%-10%)
-- 可做关停决策
+**高置信度(≥7 天)**:正常执行各类操作
 
 
-**中等置信度(4-6 天)**:
-- 降价幅度取下限(3%)而非上限(5%)
-- 提价谨慎(优先 5%,不用 8%-10%)
-- 关停需更严格条件(远低于 25%,而非刚到关停线)
+**中等置信度(4-6 天)**:降价/提价取幅度下限,关停需更严格条件
 
 
 **低置信度(3 天)**:
 **低置信度(3 天)**:
 - 优先 `observe` 而非 bid_down/bid_up
 - 优先 `observe` 而非 bid_down/bid_up
-- 仅明显异常才关停(动态ROI 低于渠道P50 × 0.5 以下
+- 仅明显异常才关停(动态ROI 远低于关停线
 - reason 中必须说明"数据不足"
 - reason 中必须说明"数据不足"
 
 
 ---
 ---
 
 
-## 五、周期性效应(尚未验证但值得警觉
+## 五、动态后验经验(知识库查询入口
 
 
-### 周末效应
-- 周六日流量构成与工作日不同,ROI 可能自然偏高或偏低
-- 不要基于单个周末的数据做关停/大幅降价决策
-- 建议看"过去 2 个周末 + 5 个工作日" 的整体趋势
+> 以下经验需要通过真实投放数据持续验证和更新。
+> 系统会自动将每轮决策中的后验发现提取到知识库。
 
 
-### 月初/月末效应
-- 月初广告主预算充足,竞争激烈 → CPM 偏高
-- 月末预算紧张,竞争降低 → CPM 偏低
-- 这些是行业普遍现象,不是单广告问题
+### 查询方式
 
 
-**当前系统态**:以上周期性规律已在 reason 写作中提及,但尚无代码自动折算。判断时主动考虑日历因素。
+当你在决策中需要以下信息时,使用 `ask_knowledge()` 查询:
+
+| 需要的信息 | 查询示例 | 说明 |
+|-----------|---------|------|
+| 周期性效应 | `ask_knowledge("周末效应 ROI 波动规律")` | 周末/月初月末的流量差异 |
+| 调价效果验证 | `ask_knowledge("降价后ROI恢复 实际案例")` | 降价/提价后的真实效果追踪 |
+| 人群包表现差异 | `ask_knowledge("R330 vs R180 裂变效率对比")` | 不同 tier 的历史表现 |
+| 异常模式识别 | `ask_knowledge("CTR正常但ROI低 历史案例")` | 已识别的异常模式库 |
+| 创意冷启动验证 | `ask_knowledge("创意更换后ROI稳定周期")` | 实际观察到的冷启动时长 |
+
+### 已知待验证假设
+
+以下是基于行业经验的先验假设,**尚未用我们的投放数据验证**。
+决策时可参考,但不可作为唯一依据:
+
+1. **周末效应**:周六日流量构成与工作日不同,ROI 可能自然偏高或偏低
+2. **月初/月末效应**:月初竞争激烈(CPM 偏高),月末竞争降低(CPM 偏低)
+3. **连续调价衰减曲线**:第 3 次调价的边际效果是否显著低于第 1 次
+
+> 随着投放数据积累,这些假设会被验证或修正,结果自动存入知识库。
 
 
 ---
 ---
 
 

+ 0 - 80
examples/auto_put_ad_mini/skills/roi_baseline.md

@@ -1,80 +0,0 @@
----
-name: roi-baseline
-description: ROI 与裂变率的对比基准口径——哪些维度对比渠道 vs 哪些对比同类,以及三条阈值线(关停/降价/提价)的定义
----
-
-# ROI 基准口径(决策前必读)
-
-> **单一真源(SSOT)**:所有阈值系数以 `config.py` 为准。本文档只描述"概念"与"命名",数值请以代码为准,不在此硬编码。
-
----
-
-## 一、核心口径(最重要)
-
-**不同维度对比的基准不同,严禁混用:**
-
-| 维度 | 对比基准 | 数据字段 | 业务含义 |
-|------|---------|---------|---------|
-| 动态 ROI | **渠道P50** | `channel_roi_p50` | 全体广告"动态ROI_7日均值"的中位数 = 全渠道整体水位 |
-| 裂变率(fission_rate) | **同类均值** | `tier_fission_mean` | 同人群包 R 值的裂变水位 |
-| CTR | **同类均值** | 同类 | 同人群的曝光质量 |
-
-**"渠道P50" 的两层聚合结构(理解这层对 reason 措辞至关重要)**:
-
-1. **第 1 层(广告内部,时间维度)**:每个广告先把每日动态ROI 做 7 日滚动均值
-2. **第 2 层(渠道整体,跨广告维度)**:全体广告的 7 日均值取**中位数(P50)**
-
----
-
-## 二、为什么 ROI 必须看渠道
-
-- 渠道整体水位(`channel_roi_p50`)反映当前大盘的"合理回报"
-- 某人群同类中位数 **不是**业务意义上的"达标线"——低价值人群同类中位数低,不代表这类广告"达标"
-- 运营视角:预算是跨人群共享的,低于渠道P50 就是跑不出渠道平均效率,应优化或淘汰
-
-## 三、为什么裂变率必须看同类
-
-- 不同人群的裂变天然不同(R500 高价值人群裂变弱、R50 宽泛人群裂变强)
-- 跨人群比裂变率等于"比苹果和橘子"
-- 只有同 R 值人群的裂变均值才能衡量"这条广告在它的人群里裂变强不强"
-
----
-
-## 四、三条阈值线(以符号表述,数值见 config.py)
-
-| 线名 | 定义 | 对应 config.py 系数 | 业务含义 |
-|------|------|---------------------|---------|
-| **关停线** | 渠道P50 × `ROI_LOW_FACTOR` | 0.75 | ROI 低于此线→考虑 pause |
-| **降价线** | 渠道P50 × `BID_DOWN_ROI_FACTOR` | 0.90 | ROI 低于此线→考虑 bid_down |
-| **提价线** | 渠道P50 × `BID_UP_ROI_FACTOR` | 1.05 | ROI 高于此线→考虑 bid_up |
-
-**硬约束**(代码护栏已实现,违反会被 `ActionConsistencyGuardrail` 拦截):
-- ✅ `bid_down` 要求:动态ROI **< 提价线**(否则矛盾:优于渠道还降价)
-- ✅ `bid_up` 要求:动态ROI **≥ 关停线**(否则矛盾:低效还加码)
-- ✅ `scale_up` 要求:动态ROI **≥ 降价线**(扩量先证明效率达标)
-
----
-
-## 五、常见误用与反例
-
-> ❌ 错:「动态 ROI 低于 R330 组中位数 X%,建议降价」(基准对象错:ROI 不看同类)
-> ❌ 错:「动态 ROI 低于渠道单日 ROI 均值 X%,建议降价」(聚合方式错:单日 ≠ 7日均)
-> ❌ 错:「裂变率低于渠道平均 Y%,建议暂停」(基准对象错:裂变率应看同类)
-> ✅ 对:「动态 ROI 低于渠道P50 X%,且裂变率低于同类均值 Y%,建议降价」
-
----
-
-## 六、reason 中的中文术语约定
-
-**禁止**在 reason 中出现英文变量名,统一用中文术语:
-
-| 英文变量(禁用) | 中文术语(必用) |
-|-----------------|-----------------|
-| `pause_line` | 关停线 |
-| `bid_down_line` | 降价线 |
-| `bid_up_line` | 提价线 |
-| `channel_roi_p50` / `roi_mean` / `tier_roi_p50` | 渠道P50 / 渠道中位数 |
-| `tier_fission_mean` | 同类均值 / 同类裂变均值 |
-| `bid_increased_7d` | 7 天内已提价 |
-| `creative_changed_7d` | 7 天内已换创意 |
-| `roi_valid_days` | ROI 有效天数 |

+ 0 - 54
examples/auto_put_ad_mini/skills/roi_strategy.md

@@ -1,54 +0,0 @@
----
-name: roi-strategy
-description: [已拆分] ROI 调控策略总索引——指向 roi-baseline / action-playbook / age-protection / posterior-wisdom 四份子 skill
----
-
-# ROI 调控策略(已拆分为 4 份子 skill)
-
-> **本 skill 已于 2026-04-21 拆分**,保留此索引文件是为了不破坏历史引用。
-> 实际内容已分布到 4 份专门 skill 中:
-
-| 子 skill | 关注点 | 何时查阅 |
-|---------|--------|---------|
-| `roi-baseline` | 对比基准口径(渠道 vs 同类)+ 三条阈值线定义 | **每次决策开始前**(读一次就够) |
-| `action-playbook` | 7 种 action 的决策树 + 判断矩阵 | **选 action 前** |
-| `age-protection` | 三段年龄策略(新生/冷启动/成熟) | 看到 `ad_age_days ≤ 7` 时 |
-| `posterior-wisdom` | 后验经验(学习中断/降价恢复/置信度分级) | reason 里需要解释"为什么观察而非操作"时 |
-
----
-
-## 决策纪律摘要(6 维度)
-
-每条决策的 reason 必须体现以下 6 个维度的综合判断:
-
-1. **调价历史** — 7 天内是否已调价?是否已证明无效?
-2. **创意变化** — 7 天内是否换过创意?消耗是否改善?
-3. **数据稳定性** — 30 天内稳定消耗天数是否 ≥ 7?
-4. **广告年龄** — 三段式(详见 `age-protection` skill)
-5. **对比基准** — ROI 看渠道P50,裂变/CTR 看同类均值(详见 `roi-baseline` skill)
-6. **ROI 置信度** — 基于 roi_valid_days 分级(详见 `posterior-wisdom` skill)
-
----
-
-## 冲突优先级
-
-当多份 skill 说法冲突时,按以下优先级取舍:
-
-```
-tencent-ad-playbook  (平台硬规则,违反直接掉量)
-  > age-protection    (年龄保护,代码三层实现)
-  > roi-baseline      (业务阈值线定义)
-  > action-playbook   (action 选择规则)
-  > posterior-wisdom  (经验参考)
-  > ad-domain         (基础概念)
-```
-
----
-
-## 查找指南
-
-- "这个广告该选哪个 action?" → `action-playbook` §一决策树
-- "ROI 2.5 对比什么?" → `roi-baseline` §一核心口径
-- "4-7 天的广告能降价吗?" → `age-protection` §二冷启动期
-- "刚换过创意要怎么判?" → `posterior-wisdom` §三创意冷启动
-- "数据只有 4 天够不够?" → `posterior-wisdom` §四置信度分级

+ 0 - 130
examples/auto_put_ad_mini/skills/tencent_ad_playbook.md

@@ -1,130 +0,0 @@
----
-name: tencent-ad-playbook
-description: 腾讯广告 Marketing API v3.0 官方投放 Playbook(平台级硬规则 + 实战经验)
-category: ad_optimization
----
-
-## 这份 Skill 的定位
-
-这份知识**不是**业务阈值(ROI/出价步长在 roi-strategy),**不是**业务基础概念(动态 ROI (7日均值) 在 ad-domain),**不是**系统护栏(冷启动/频率在 guardrail-rules)。
-
-这里是**腾讯广告 3.0 平台本身的投放规则**:哪些操作会被平台惩罚?哪些节奏是 oCPM 学习机制硬要求的?什么情况下数据本身不可信?
-
-**当你做决策前,以下规则优先于任何 ROI 信号。** 违反平台规则的决策即使"数值上合理"也会掉量。
-
----
-
-## 1. oCPM 学习期硬规则
-
-本项目所有广告固定 `bid_mode = BID_MODE_OCPM`。oCPM 的"前 5 个转化"阶段叫**学习期**,系统在此期间探索人群、优化投放。
-
-**硬约束:**
-- **前 5 个转化之内不要调整任何参数**(出价、定向、创意、预算),否则学习期清零、重新探索,跑量会断崖。
-- **24 小时内同一广告调整次数 ≤ 2 次**,超过会触发"频繁调整"惩罚。
-- **日预算 ≥ 出价 × 20**,否则预算撑不起 oCPM 系统需要的探索量,学习期拉长甚至失败。
-- 学习期失败(7 天仍未出满 5 个转化)的广告基本可以关停,回归无意义。
-
-**决策含义:**
-- 看到 `ad_age_days <= 3` 或 `conversions_count_7d < 5` 的广告,默认输出 `observe`,不做任何调整建议。
-- 即使 ROI 指标看起来低,也先让系统把学习期跑完,再下结论。
-
----
-
-## 2. 调价幅度上限(平台级)
-
-**单次降价幅度严格 ≤ 30%。**
-
-- 超过 30% 会被平台判定为"剧烈波动",系统重新进入探索期,量会直接掉一半以上,2~3 天才缓过来。
-- 提价没有这么严的平台惩罚,但建议单次 ≤ 20%(业务层 `BID_UP_MAX_PCT=0.10` 更保守)。
-- 连续多次调价会叠加累计幅度:24 小时内累计调幅 > 30% 同样触发惩罚。
-
-**决策含义:**
-- 如果业务层算出来的 `recommended_change_pct` 下降幅度超过 30%,**主动压到 ≤ 30%**,不要硬执行。
-- 如果判断需要大幅降价(例如 ROI 严重偏低),**首选关停而非大幅降价** —— 关停是干净的止损,大幅降价既掉量又继续烧钱。
-
----
-
-## 3. 少广告多素材原则
-
-3.0 的核心策略变化:**同一广告下挂多个动态创意,系统自动优选**。这不是营销口号,是数据归因逻辑:多个创意的表现数据会归集到同一个"广告"的学习模型里,缩短冷启动、提高 oCPM 优化效率。
-
-**决策含义:**
-- **单广告动态创意数量 < 5 → 一律 `observe`**。此时数据样本不足,任何调价都是对噪声的过拟合,系统优选尚未完成。
-- 创意数 ≥ 5 再参与调价决策。
-- 如果看到某人群包/计划下广告数量激增但每条都只有 1~2 个创意,reason 里提醒:"基建思路偏旧版,建议合并广告、补创意后再评估"。
-
----
-
-## 4. 素材疲劳与衰退识别
-
-3.0 系统会自动优选素材,但**无法反复救活同一套已疲劳的素材**。
-
-**判断标准:**
-- CTR 与 CVR **同时**下跌 > 25%(对比前 7 日)→ 素材疲劳。
-- 单独 CTR 跌但 CVR 稳定 → 可能是流量池扩张(曝光质量变化),不是素材问题。
-- 单独 CVR 跌但 CTR 稳定 → 落地页/转化路径问题,不是素材问题。
-
-**决策含义:**
-- 素材疲劳时,**首选 `creative_adjust`(建议人工换素材),而非 pause**。pause 会丢掉这条广告累积的 oCPM 学习资产。
-- 如果人工短期内不可能换素材(例如周末/节假日),才考虑 pause 止损。
-- reason 里必须写清楚:"CTR + CVR 同时下滑 X%,判定素材疲劳,建议换素材而非关停"。
-
----
-
-## 5. 小程序场景优先级(本业务固定)
-
-本项目场景:微信小程序(`MARKETING_CARRIER_TYPE_MINI_PROGRAM_WECHAT`)+ 用户增长。
-
-**固定策略:**
-- `optimization_goal` 优先 `OPTIMIZATIONGOAL_PAGE_VIEW`(页面浏览),不是点击。PAGE_VIEW 更接近真实转化路径,oCPM 学的是"有效访问"而不是"广告点击"。
-- **归因窗口 15 天**。这意味着:一次曝光/点击带来的转化可能最晚 15 天后才回流到报表。
-- **投放天数 < 1~3 天的广告,ROI 必然被低估**。因为收入侧还没开始回流,成本侧已经跑完。对这类广告输出 `observe`,不要根据短期 ROI 下结论。
-- 投放天数 ≥ 7 天的广告 ROI 才基本稳定(对齐项目侧 `min_periods=7`)。
-
-**决策含义:**
-- 看 `ad_age_days` 时永远配合归因窗口解读:1~3 天的低 ROI 是**系统性低估**,不是真实信号。
-- 如果 reason 里引用了 ROI,必须同时提一句数据成熟度,例如:"广告投放 2 天,考虑到 15 天归因窗口,当前 ROI 1.3 可能被低估 30%+,建议观察到 7 日再评估"。
-
----
-
-## 6. 数据口径(别让实时数据骗你)
-
-腾讯广告报表有两个口径,用错会导致决策全错:
-
-| 口径 | 字段来源 | 延迟 | 用途 |
-|---|---|---|---|
-| 实时 | `realtime_cost`、分时报表 | 15~30 分钟 | **只用于监控异常暴涨/高燃烧预警**,不用于 ROI 计算 |
-| T+1 权威 | `daily_reports`(日报) | 次日凌晨 | **唯一可用于 ROI 计算和调价决策的口径** |
-
-**硬规则:**
-- **ROI 计算、`动态ROI_7日均值`、调价决策 → 必须用 T+1 `daily_reports`**。
-- 实时数据只回答"现在是否在烧钱",不回答"投放效率如何"。
-- 转化数据延迟 1~2 小时,归因还要 15 天,任何"今日 ROI"都是假数据。
-
-**决策含义:**
-- 如果工具返回的是"今日进行中"数据,直接 `observe`,要求使用 T+1 数据重新评估。
-- reason 里禁止引用"今日 ROI" —— 要么用昨日,要么用 7 日。
-
----
-
-## 7. 执行层硬约束(写操作规范)
-
-所有对腾讯广告 API 的写操作(更新出价、暂停广告)必须遵守以下平台限制。**这些不是建议,违反会直接被 API 拒绝或账户被降权。**
-
-- **批量操作单次 ≤ 50 条**:一次 `adgroups/update` 请求里最多 50 个广告。超过要分批。
-- **单账户 QPS ≤ 8**(平台上限 10,留 2 个 buffer 给异常重试)。
-- **`FREQUENCY_LIMIT` 错误必须指数退避重试**:遇到 code 11017 / 429,按 2^n 秒退避(1s, 2s, 4s, 8s),最多 3 次。
-- **所有写操作必须带 `operation_id`**(幂等键),用同一个 uuid 重试,避免重复执行。
-- **写操作失败 ≠ 广告未变化**:API 超时但实际已执行是常见情况,失败后先 `get_ad_state` 确认状态再决定是否重试。
-
-**决策含义:**
-- 决策输出阶段(LLM 角色)不直接碰 API,这段约束是给执行层(`execution_engine.py`)看的。
-- 但 LLM 应该知道:**一次输出超过 50 个调整决策,意味着执行层要分批**,这是正常的;不要因为看到"50 条上限"就人为压缩决策数量。
-
----
-
-## 使用这份 Skill 的姿势
-
-1. **先于 ROI 判断**:看到任何广告数据,先按本 Playbook 过一遍(学习期 / 创意数 / 数据成熟度 / 口径),过关了才去看 ROI 阈值。
-2. **reason 中显式引用**:如果决策是基于本 Playbook 的规则(例如"创意数 3 < 5 不做调价"),reason 里必须把规则出处说清楚,让运营能追溯。
-3. **规则冲突时,Playbook > ROI 信号**:例如 ROI 提示降价,但广告处于学习期 → 按 Playbook 输出 observe。Playbook 是平台硬约束,违反它会把 ROI 优势也一起丢掉。

+ 201 - 0
examples/auto_put_ad_mini/test_single_ad.py

@@ -0,0 +1,201 @@
+"""
+单广告决策测试 — 控制变量实验
+只给 LLM 1 条广告数据(93249577421),看它是否还会:
+  1. 幻觉关停线 2.66(实际 2.524)
+  2. 忽略裂变 +35% 正向信号而决策 pause
+"""
+import os, sys, json
+from pathlib import Path
+
+# 代理 + 项目根
+os.environ.setdefault("HTTP_PROXY", "http://127.0.0.1:29758")
+os.environ.setdefault("HTTPS_PROXY", "http://127.0.0.1:29758")
+
+PROJECT_ROOT = Path("/Users/liulidong/project/agent/Agent")
+sys.path.insert(0, str(PROJECT_ROOT))
+
+from dotenv import load_dotenv
+load_dotenv(PROJECT_ROOT / ".env")
+
+import requests
+
+# ═══════════════════════════════════════════
+# 1. 构造这条广告的 ad_dict(与 get_ads_for_review 输出一致)
+# ═══════════════════════════════════════════
+CHANNEL_ROI_P50 = 3.3654
+
+ad_dict = {
+    "ad_id": 93249577421,
+    "ad_name": "4.8-朋友圈+公众号-关键页-R50-惊奇-60+不限性别",
+    "动态ROI_7日均值": 2.1964,
+    "cost_7d_avg": 5702.44,
+    "cost_7d_total": 39917.09,
+    "ad_age_days": 10,
+    "bid_increased_7d": True,
+    "creative_changed_7d": True,
+    "stable_spend_days_30d": 9,
+    "bid_amount": 0.39,
+    "bid_candidate": None,  # 不是 bid_up/bid_down 候选(因裂变高不满足 bid_down 条件)
+    "scale_up_candidate": False,
+    "ad_fission": 1.61,
+    "ad_ctr": 0.032,
+    "yesterday_cost": 5702.44,
+    "audience_tier": "R50*泛惊奇*奇观技艺",
+    "roi_valid_days": 9,
+    # 同类对比
+    "tier_fission_mean": 1.19,
+    "tier_fission_p50": 1.15,
+    "tier_ctr_mean": 0.028,
+    "tier_bid_mean": 0.35,
+    "bid_up_target_min": 0.3675,
+    "bid_up_target_max": 0.385,
+    # 阈值线
+    "pause_line_min": round(CHANNEL_ROI_P50 * 0.70, 4),
+    "pause_line_max": round(CHANNEL_ROI_P50 * 0.75, 4),
+    "bid_down_line_min": round(CHANNEL_ROI_P50 * 0.85, 4),
+    "bid_down_line_max": round(CHANNEL_ROI_P50 * 0.90, 4),
+    "bid_up_line_min": round(CHANNEL_ROI_P50 * 1.05, 4),
+    "bid_up_line_max": round(CHANNEL_ROI_P50 * 1.10, 4),
+    # 年龄
+    "age_segment": "mature",
+    "age_protection_level": "正常调控(成熟期)",
+    "allow_bid_down": True,
+    "allow_bid_up": True,
+    "max_bid_down_pct": 0.05,
+    "high_burn_alert": True,
+    # 调幅
+    "bid_change_min_pct": 0.03,
+    "bid_change_max_pct": 0.10,
+}
+
+# ═══════════════════════════════════════════
+# 2. 读取 system prompt + skills
+# ═══════════════════════════════════════════
+MINI_DIR = PROJECT_ROOT / "examples" / "auto_put_ad_mini"
+
+system_prompt = (MINI_DIR / "prompts" / "system.prompt").read_text()
+
+skills_text = ""
+skills_dir = MINI_DIR / "skills"
+for skill_name in ["roi_baseline", "action_playbook", "age_protection", "guardrail_rules"]:
+    fpath = skills_dir / f"{skill_name.replace('-', '_')}.md"
+    if not fpath.exists():
+        fpath = skills_dir / f"{skill_name.replace('_', '-')}.md"
+    if fpath.exists():
+        skills_text += f"\n\n--- SKILL: {skill_name} ---\n" + fpath.read_text()
+
+# ═══════════════════════════════════════════
+# 3. 构造 LLM 请求
+# ═══════════════════════════════════════════
+tool_result = json.dumps({
+    "summary": {
+        "total": 1,
+        "need_review_ads": 1,
+        "tier_groups": 1,
+    },
+    "distribution": {
+        "channel_roi_p50": CHANNEL_ROI_P50,
+        "p25": 1.8,
+        "p50": CHANNEL_ROI_P50,
+        "p75": 5.2,
+        "p90": 8.1,
+    },
+    "thresholds_used": {
+        "ROI_LOW_FACTOR": 0.75,
+        "BID_DOWN_ROI_FACTOR": 0.90,
+        "BID_UP_ROI_FACTOR": 1.05,
+        "channel_roi_p50": CHANNEL_ROI_P50,
+        "pause_line": round(CHANNEL_ROI_P50 * 0.75, 4),
+        "bid_down_line": round(CHANNEL_ROI_P50 * 0.90, 4),
+        "bid_up_line": round(CHANNEL_ROI_P50 * 1.05, 4),
+    },
+    "need_review_ads": [ad_dict],
+}, ensure_ascii=False, indent=2)
+
+user_msg = f"""请对以下 1 条待评估广告做出决策。
+
+数据如下:
+{tool_result}
+
+请以 JSON 格式返回决策列表,每条包含:
+- ad_id
+- action: pause / bid_down / bid_up / scale_up / creative_adjust / observe / hold
+- dimension: 简短描述
+- reason: 包含 ROI值、对比基准、偏离百分比、裂变率对比、行动建议
+- recommended_change_pct: 数字(降价为负,提价为正,其他为0)
+"""
+
+messages = [
+    {"role": "system", "content": system_prompt + "\n\n" + skills_text},
+    {"role": "user", "content": user_msg},
+]
+
+# ═══════════════════════════════════════════
+# 4. 调用 OpenRouter API
+# ═══════════════════════════════════════════
+api_key = os.environ.get("OPEN_ROUTER_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
+if not api_key:
+    print("❌ 找不到 OPEN_ROUTER_API_KEY")
+    sys.exit(1)
+
+print("=" * 60)
+print(f"  模型: anthropic/claude-sonnet-4.5")
+print(f"  广告数: 1 条(93249577421)")
+print(f"  关停线(正确值): {round(CHANNEL_ROI_P50 * 0.75, 4)}")
+print(f"  广告动态ROI: 2.1964")
+print(f"  裂变率 vs 同类: 1.61 vs 1.19 (+35%)")
+print("=" * 60)
+print("\n调用 LLM 中...\n")
+
+resp = requests.post(
+    "https://openrouter.ai/api/v1/chat/completions",
+    headers={
+        "Authorization": f"Bearer {api_key}",
+        "Content-Type": "application/json",
+    },
+    json={
+        "model": "anthropic/claude-sonnet-4.5",
+        "temperature": 0.3,
+        "max_tokens": 4000,
+        "messages": messages,
+    },
+    timeout=120,
+)
+
+if resp.status_code != 200:
+    print(f"❌ API 错误: {resp.status_code}")
+    print(resp.text[:500])
+    sys.exit(1)
+
+result = resp.json()
+content = result["choices"][0]["message"]["content"]
+
+print("=" * 60)
+print("  LLM 输出(单条广告)")
+print("=" * 60)
+print(content)
+print()
+
+# ═══════════════════════════════════════════
+# 5. 快速验证
+# ═══════════════════════════════════════════
+print("=" * 60)
+print("  验证")
+print("=" * 60)
+if "2.66" in content:
+    print("  ❌ 仍然幻觉关停线 2.66")
+elif "2.524" in content or "2.52" in content:
+    print("  ✅ 关停线数字正确(2.524)")
+else:
+    print("  ⚠️  关停线数字未出现在输出中")
+
+if '"pause"' in content.lower() or "'pause'" in content.lower():
+    print("  ❌ 仍然决策 pause(裂变+35% 被忽略)")
+elif '"bid_down"' in content.lower():
+    print("  ✅ 决策 bid_down(裂变好 + ROI 低 → 降价而非关停)")
+elif '"observe"' in content.lower():
+    print("  ✅ 决策 observe(近期已调整,等稳定)")
+elif '"hold"' in content.lower():
+    print("  ⚠️  决策 hold(保守但可接受)")
+else:
+    print("  ⚠️  action 不在预期范围内")

+ 4 - 11
examples/auto_put_ad_mini/tools/ad_decision.py

@@ -633,17 +633,10 @@ async def get_ads_for_review(
                 ad_dict["bid_up_target_max"] = round(tier_bid_mean * 1.10, 4) if tier_bid_mean else None
                 ad_dict["bid_up_target_max"] = round(tier_bid_mean * 1.10, 4) if tier_bid_mean else None
 
 
                 # ROI 阈值线:基于"渠道P50"(channel_roi_p50,全体广告7日均值的中位数),严禁用同类
                 # ROI 阈值线:基于"渠道P50"(channel_roi_p50,全体广告7日均值的中位数),严禁用同类
-                # 关停线:渠道P50 的 70-75%(低于25-30%)
-                ad_dict["pause_line_min"] = round(channel_roi_p50 * 0.70, 4) if channel_roi_p50 else None
-                ad_dict["pause_line_max"] = round(channel_roi_p50 * 0.75, 4) if channel_roi_p50 else None
-
-                # 降价线:渠道P50 的 85-90%(低于10-15%)
-                ad_dict["bid_down_line_min"] = round(channel_roi_p50 * 0.85, 4) if channel_roi_p50 else None
-                ad_dict["bid_down_line_max"] = round(channel_roi_p50 * 0.90, 4) if channel_roi_p50 else None
-
-                # 提价线:渠道P50 的 105-110%(高于5-10%)
-                ad_dict["bid_up_line_min"] = round(channel_roi_p50 * 1.05, 4) if channel_roi_p50 else None
-                ad_dict["bid_up_line_max"] = round(channel_roi_p50 * 1.10, 4) if channel_roi_p50 else None
+                # 精简为单值(减少 LLM 字段混淆,避免阈值幻觉)
+                ad_dict["pause_line"] = round(channel_roi_p50 * params["ROI_LOW_FACTOR"], 4) if channel_roi_p50 else None       # 关停线 = 渠道P50 × 0.75
+                ad_dict["bid_down_line"] = round(channel_roi_p50 * params["BID_DOWN_ROI_FACTOR"], 4) if channel_roi_p50 else None  # 降价线 = 渠道P50 × 0.90
+                ad_dict["bid_up_line"] = round(channel_roi_p50 * params["BID_UP_ROI_FACTOR"], 4) if channel_roi_p50 else None    # 提价线 = 渠道P50 × 1.05
 
 
                 # ===== 新增:年龄分段标签(基于决策树图片)=====
                 # ===== 新增:年龄分段标签(基于决策树图片)=====
                 if ad_age is not None:
                 if ad_age is not None:

+ 40 - 58
examples/auto_put_ad_mini/tools/guardrails.py

@@ -281,54 +281,28 @@ class ActionConsistencyGuardrail(Guardrail):
                 modified_bid=None,
                 modified_bid=None,
             )
             )
 
 
-        # ==================== B 系列:ROI 水位一致性 ====================
-        # 缺数据时降级跳过(保护旧 CSV / 缺字段场景)
-        dynamic_roi_7d = self._to_float(row.get("动态ROI_7日均值"))
-        channel_roi_p50 = self._to_float(row.get("_channel_roi_p50"))
-        if dynamic_roi_7d is None or channel_roi_p50 is None or channel_roi_p50 <= 0:
-            return GuardrailResult(status="approved", reason="")
-
-        # B1: bid_down 但 ROI 优于渠道P50 × 提价线(1.05)→ 矛盾,改 hold
-        bid_up_line = channel_roi_p50 * BID_UP_ROI_FACTOR
-        if action == "bid_down" and dynamic_roi_7d >= bid_up_line:
-            return GuardrailResult(
-                status="modified",
-                reason=(
-                    f"ROI 水位矛盾:动态ROI={dynamic_roi_7d:.3f} ≥ 渠道P50×{BID_UP_ROI_FACTOR}"
-                    f"={bid_up_line:.3f}(优于渠道),不应 bid_down → 改 hold"
-                ),
-                modified_action="hold",
-                modified_change_pct=0.0,
-                modified_bid=None,
-            )
-
-        # B2: bid_up 但 ROI 低于关停线(×0.75)→ 矛盾,改 hold(低效不能加码)
-        pause_line = channel_roi_p50 * ROI_LOW_FACTOR
-        if action == "bid_up" and dynamic_roi_7d < pause_line:
-            return GuardrailResult(
-                status="modified",
-                reason=(
-                    f"ROI 水位矛盾:动态ROI={dynamic_roi_7d:.3f} < 渠道P50×{ROI_LOW_FACTOR}"
-                    f"={pause_line:.3f}(低效广告),不应 bid_up → 改 hold"
-                ),
-                modified_action="hold",
-                modified_change_pct=0.0,
-                modified_bid=None,
-            )
-
-        # B3: scale_up 但 ROI 低于渠道P50 × 0.90(降价线)→ 改 observe
-        scale_up_min_line = channel_roi_p50 * 0.90
-        if action == "scale_up" and dynamic_roi_7d < scale_up_min_line:
-            return GuardrailResult(
-                status="modified",
-                reason=(
-                    f"ROI 水位矛盾:动态ROI={dynamic_roi_7d:.3f} < 渠道P50×0.90"
-                    f"={scale_up_min_line:.3f}(效率未达标),不应扩量 → 改 observe"
-                ),
-                modified_action="observe",
-                modified_change_pct=0.0,
-                modified_bid=None,
-            )
+        # ==================== B4: 裂变-关停冲突 ====================
+        # pause 但裂变率显著优于同类(>1.10×)→ 改 observe(裂变好说明广告质量有潜力)
+        if action == "pause":
+            ad_fission = self._to_float(row.get("_ad_fission"))
+            tier_fission_mean = self._to_float(row.get("_tier_fission_mean"))
+            if (
+                ad_fission is not None
+                and tier_fission_mean is not None
+                and tier_fission_mean > 0
+                and ad_fission > tier_fission_mean * 1.10
+            ):
+                return GuardrailResult(
+                    status="modified",
+                    reason=(
+                        f"裂变-关停冲突:裂变率={ad_fission:.2f} > 同类均值{tier_fission_mean:.2f}×1.10"
+                        f"={tier_fission_mean * 1.10:.2f}(裂变优秀,广告质量有潜力),"
+                        f"不应关停 → 改 observe"
+                    ),
+                    modified_action="observe",
+                    modified_change_pct=0.0,
+                    modified_bid=None,
+                )
 
 
         return GuardrailResult(status="approved", reason="")
         return GuardrailResult(status="approved", reason="")
 
 
@@ -646,7 +620,7 @@ def _run_guardrails(
     channel_roi_p50: Optional[float] = None,
     channel_roi_p50: Optional[float] = None,
 ) -> pd.DataFrame:
 ) -> pd.DataFrame:
     """
     """
-    对决策 DataFrame 执行 6 道护栏检查。
+    对决策 DataFrame 执行 2 道护栏检查。
 
 
     新增列:
     新增列:
       - guardrail_status: approved / blocked / modified
       - guardrail_status: approved / blocked / modified
@@ -664,16 +638,8 @@ def _run_guardrails(
     history = AdjustmentHistory()
     history = AdjustmentHistory()
 
 
     guardrails = [
     guardrails = [
-        ActionConsistencyGuardrail(),  # ★ 首位:拦 action↔pct↔ROI 矛盾(避免"降价 0%"等)
-        ColdStartGuardrail(),
-        DataFreshnessGuardrail(),
-        BidBoundaryGuardrail(),
-        BidRangeGuardrail(),
-        RateLimitGuardrail(),
-        DailyOpsCapGuardrail(),
-        DryRunGuardrail() if dry_run or DRY_RUN_MODE else None,
+        # 暂时全部关闭,测试 LLM 裸输出 + 字段精简的效果
     ]
     ]
-    guardrails = [g for g in guardrails if g is not None]
 
 
     # 注入数据日期 + 渠道P50(给 ActionConsistencyGuardrail B 系列用)
     # 注入数据日期 + 渠道P50(给 ActionConsistencyGuardrail B 系列用)
     df["_data_date"] = data_date
     df["_data_date"] = data_date
@@ -817,6 +783,22 @@ async def validate_decisions(
                     if len(roi_series) > 0:
                     if len(roi_series) > 0:
                         channel_roi_p50 = float(roi_series.median())
                         channel_roi_p50 = float(roi_series.median())
                         logger.info(f"护栏 B 系列启用:渠道P50={channel_roi_p50:.4f}")
                         logger.info(f"护栏 B 系列启用:渠道P50={channel_roi_p50:.4f}")
+
+                # 注入裂变字段(供 B4 护栏使用)
+                if "T0裂变系数_7日均值" in df_metrics.columns and "audience_tier" in df_metrics.columns:
+                    # ad_fission: 每条广告自身的裂变率
+                    fission_map = df_metrics.set_index("ad_id")["T0裂变系数_7日均值"].to_dict()
+                    df["_ad_fission"] = df["ad_id"].map(fission_map)
+
+                    # tier_fission_mean: 按人群包分组的同类均值
+                    tier_fission = df_metrics.groupby("audience_tier")["T0裂变系数_7日均值"].mean().to_dict()
+                    tier_map = df_metrics.set_index("ad_id")["audience_tier"].to_dict()
+                    df["_tier_fission_mean"] = df["ad_id"].map(
+                        lambda aid: tier_fission.get(tier_map.get(aid))
+                    )
+                    b4_ready = df["_ad_fission"].notna().sum()
+                    logger.info(f"护栏 B4 裂变字段已注入:{b4_ready} 条广告有裂变数据")
+
             except Exception as e:
             except Exception as e:
                 logger.warning(f"读取 metrics 失败(护栏 B 系列将跳过): {e}")
                 logger.warning(f"读取 metrics 失败(护栏 B 系列将跳过): {e}")
 
 

+ 119 - 24
examples/auto_put_ad_mini/tools/im_approval.py

@@ -48,6 +48,54 @@ from config import (
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# chat_history 路径(websocket_event.py 写入的位置)
+_CHAT_HISTORY_DIR = (
+    Path(__file__).resolve().parent.parent.parent.parent
+    / "agent" / "tools" / "builtin" / "feishu" / "chat_history"
+)
+
+
+def _get_contact_name(open_id: str) -> Optional[str]:
+    """根据 open_id 从 feishu_contacts.json 查联系人名"""
+    try:
+        from agent.tools.builtin.feishu.chat import get_contact_by_id
+        contact = get_contact_by_id(open_id)
+        return contact.get("name") if contact else None
+    except Exception:
+        return None
+
+
+def _snapshot_message_ids(contact_name: str) -> set:
+    """快照当前 chat_history 里已有的 message_id"""
+    chat_file = _CHAT_HISTORY_DIR / f"chat_{contact_name}.json"
+    if not chat_file.exists():
+        return set()
+    try:
+        msgs = json.loads(chat_file.read_text(encoding="utf-8"))
+        return {m["message_id"] for m in msgs if "message_id" in m}
+    except Exception:
+        return set()
+
+
+def _poll_new_messages(contact_name: str, known_ids: set) -> list:
+    """读取新消息文本(不在 known_ids 里的 message_id)"""
+    chat_file = _CHAT_HISTORY_DIR / f"chat_{contact_name}.json"
+    if not chat_file.exists():
+        return []
+    try:
+        msgs = json.loads(chat_file.read_text(encoding="utf-8"))
+        new_texts = []
+        for m in msgs:
+            if m.get("message_id") in known_ids:
+                continue
+            for block in m.get("content", []):
+                if block.get("type") == "text" and block.get("text", "").strip():
+                    new_texts.append(block["text"])
+        return new_texts
+    except Exception:
+        return []
+
+
 # 审批请求状态
 # 审批请求状态
 _approval_requests: Dict[str, Dict] = {}
 _approval_requests: Dict[str, Dict] = {}
 
 
@@ -632,6 +680,21 @@ async def send_approval_request(
         msg_path = approval_dir / f"{request_id}.txt"
         msg_path = approval_dir / f"{request_id}.txt"
         msg_path.write_text(message, encoding="utf-8")
         msg_path.write_text(message, encoding="utf-8")
 
 
+        # 快照当前 chat_history 里已有的 message_id(用于识别新回复)
+        _operator_contact_name = _get_contact_name(FEISHU_OPERATOR_OPEN_ID)
+        _known_message_ids: set = set()
+        if _operator_contact_name:
+            _known_message_ids = _snapshot_message_ids(_operator_contact_name)
+            logger.info(
+                "chat_history 快照:联系人=%s,已有消息=%d条",
+                _operator_contact_name, len(_known_message_ids)
+            )
+        else:
+            logger.warning(
+                "无法识别运营联系人(open_id=%s),将回退到 HTTP 轮询",
+                FEISHU_OPERATOR_OPEN_ID
+            )
+
         # ─── 非阻塞模式 ───
         # ─── 非阻塞模式 ───
         if not wait_for_reply:
         if not wait_for_reply:
             return ToolResult(
             return ToolResult(
@@ -655,26 +718,72 @@ async def send_approval_request(
                 },
                 },
             )
             )
 
 
-        # ═══ 阻塞轮询等待飞书回复 ═══
+        # ═══ 等待飞书回复(优先读本地 chat_history,回退到 HTTP 轮询)═══
         timeout_at = datetime.now() + timedelta(minutes=timeout_minutes)
         timeout_at = datetime.now() + timedelta(minutes=timeout_minutes)
+        use_local_poll = bool(_operator_contact_name)
         logger.info(
         logger.info(
-            "阻塞等待飞书审批回复(超时 %d 分钟,间隔 %d 秒)...",
+            "等待审批回复:mode=%s,超时 %d 分钟",
+            "local_chat_history" if use_local_poll else "http_poll",
             timeout_minutes,
             timeout_minutes,
-            poll_interval_seconds,
         )
         )
 
 
         poll_count = 0
         poll_count = 0
         while datetime.now() < timeout_at:
         while datetime.now() < timeout_at:
-            await asyncio.sleep(poll_interval_seconds)
+            await asyncio.sleep(1 if use_local_poll else poll_interval_seconds)
             poll_count += 1
             poll_count += 1
 
 
             remaining = (timeout_at - datetime.now()).total_seconds() / 60
             remaining = (timeout_at - datetime.now()).total_seconds() / 60
-            if poll_count % 10 == 0:
-                logger.info("飞书审批轮询 #%d,剩余 %.1f 分钟", poll_count, remaining)
+            if poll_count % 30 == 0:
+                logger.info("等待审批回复 #%d,剩余 %.1f 分钟", poll_count, remaining)
 
 
-            # 读取个人和项目群的审批回复
+            # ─── 优先:读本地 chat_history(WebSocket 推送写入,1秒延迟)───
+            if use_local_poll:
+                try:
+                    new_texts = _poll_new_messages(_operator_contact_name, _known_message_ids)
+                    if new_texts:
+                        text = new_texts[0]
+                        _known_message_ids = _snapshot_message_ids(_operator_contact_name)
+
+                        _approval_requests[request_id].update({
+                            "status": "replied",
+                            "reply_content": text,
+                            "reply_at": datetime.now().isoformat(),
+                            "ad_ids": tier2_ad_ids,
+                        })
+                        logger.info("chat_history 收到运营回复(%d 秒): %s", poll_count, text[:200])
+
+                        ad_ids_str = ", ".join(str(x) for x in tier2_ad_ids[:10])
+                        if len(tier2_ad_ids) > 10:
+                            ad_ids_str += f"...共{len(tier2_ad_ids)}个"
+
+                        return ToolResult(
+                            title="运营已回复",
+                            output=(
+                                f"运营飞书回复原文: {text}\n"
+                                f"等待审批的广告ID: {ad_ids_str}\n"
+                                f"等待时间: {poll_count} 秒\n\n"
+                                f"请根据运营的自然语言回复判断后续操作:\n"
+                                f"- 运营同意/批准/通过 → 调用 execute_decisions 执行\n"
+                                f"- 运营拒绝/驳回 → 停止执行,告知原因\n"
+                                f"- 运营要求修改(如\"广告X不要暂停\")→ 进入 Mode 3: modify_decisions → validate → 重新审批\n"
+                                f"- 运营部分批准(如\"只批准降价的\")→ 相应过滤后 execute_decisions"
+                            ),
+                            metadata={
+                                "request_id": request_id,
+                                "feishu_sent": feishu_sent,
+                                "msg_path": str(msg_path),
+                                "poll_count": poll_count,
+                                "raw_reply": text,
+                                "ad_ids": tier2_ad_ids,
+                                "poll_mode": "local_chat_history",
+                            },
+                        )
+                except Exception as e:
+                    logger.debug("读取 chat_history 失败(将重试): %s", e)
+                continue  # 跳过 HTTP 轮询
+
+            # ─── 回退:HTTP 轮询(原有逻辑,仅在联系人未配置时使用)───
             try:
             try:
-                # ✅ 修复:使用 send_message 返回的真实 chat_id 轮询(非 open_id)
                 for chat_id in poll_chat_ids:
                 for chat_id in poll_chat_ids:
                     result = _feishu.get_message_list(
                     result = _feishu.get_message_list(
                         chat_id=chat_id,
                         chat_id=chat_id,
@@ -683,24 +792,12 @@ async def send_approval_request(
                     )
                     )
                     if result and result.get("items"):
                     if result and result.get("items"):
                         for msg in result["items"]:
                         for msg in result["items"]:
-                            sender_id = msg.get("sender_id", "")
                             sender_type = msg.get("sender_type", "")
                             sender_type = msg.get("sender_type", "")
-
-                            logger.debug(
-                                "飞书消息 [%s]: sender_type=%s, sender_id=%s, content=%s",
-                                chat_id, sender_type, sender_id, str(msg.get("content", ""))[:100],
-                            )
-
-                            # ✅ 修改:接受任何用户的回复(不限制特定个人)
                             if sender_type != "user":
                             if sender_type != "user":
                                 continue
                                 continue
-
-                            # 框架已自动解析 text 消息的 JSON -> 纯文本
                             text = msg.get("content", "")
                             text = msg.get("content", "")
                             if not text.strip():
                             if not text.strip():
                                 continue
                                 continue
-
-                            # 检测到运营回复,返回原文给 Agent 理解
                             parsed = _parse_approval_reply(text, tier2_ad_ids)
                             parsed = _parse_approval_reply(text, tier2_ad_ids)
                             if parsed["status"] != "unknown":
                             if parsed["status"] != "unknown":
                                 _approval_requests[request_id].update({
                                 _approval_requests[request_id].update({
@@ -709,13 +806,10 @@ async def send_approval_request(
                                     "reply_at": datetime.now().isoformat(),
                                     "reply_at": datetime.now().isoformat(),
                                     "ad_ids": tier2_ad_ids,
                                     "ad_ids": tier2_ad_ids,
                                 })
                                 })
-
-                                logger.info("飞书审批收到运营回复: %s", text[:200])
-
+                                logger.info("HTTP 轮询收到运营回复: %s", text[:200])
                                 ad_ids_str = ", ".join(str(x) for x in tier2_ad_ids[:10])
                                 ad_ids_str = ", ".join(str(x) for x in tier2_ad_ids[:10])
                                 if len(tier2_ad_ids) > 10:
                                 if len(tier2_ad_ids) > 10:
                                     ad_ids_str += f"...共{len(tier2_ad_ids)}个"
                                     ad_ids_str += f"...共{len(tier2_ad_ids)}个"
-
                                 return ToolResult(
                                 return ToolResult(
                                     title="运营已回复",
                                     title="运营已回复",
                                     output=(
                                     output=(
@@ -735,6 +829,7 @@ async def send_approval_request(
                                         "poll_count": poll_count,
                                         "poll_count": poll_count,
                                         "raw_reply": text,
                                         "raw_reply": text,
                                         "ad_ids": tier2_ad_ids,
                                         "ad_ids": tier2_ad_ids,
+                                        "poll_mode": "http",
                                     },
                                     },
                                 )
                                 )
             except Exception as e:
             except Exception as e: