Files
meida/docs/开发任务包/T1-四档熔断降级.md
T
xsl e511b6a66f 收尾两批并行开发的文档回写与任务包
- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
2026-08-14 21:07:20 +08:00

107 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T1 · 全局日成本四档熔断降级
> 对应任务:`T-P1-25b` | 需求:`NFR-COST-02`、架构 §13.3 | 分支:`task/t1-tiered-budget` | worktree:`/home/xsl/meida_backend-t1`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
把现有"单一日预算阈值"(`DailyBudgetGuard`:到 100% 拒绝非 critical 场景)升级为架构 §13.3 的**四档分级降级**,并补上 T-P1-25b 的验收要求:**被熔断拒绝的请求配额净扣减 = 0(须有集成断言)**。
四档(阈值可配置,百分比相对 `LLM_DAILY_BUDGET_CENTS`):
| 档位 | 阈值 | 动作 |
| --- | --- | --- |
| tier 1 `warn` | ≥60% | 只告警(日志 + 指标),不影响任何请求 |
| tier 2 `degrade_secondary` | ≥80% | 拒绝 blocked 列表中的场景(默认 `hd_export`、`report_regen`) |
| tier 3 `degrade_generation` | ≥90% | 追加拒绝(默认 `hairstyle`、`tryon`) |
| tier 4 `block_all` | ≥100% | 拒绝所有非 critical 场景,**但 allow 名单内的场景放行**(默认 `agent_chat`——架构原文"仅保留在途任务与纯文本对话") |
规则:
- **critical 场景永远豁免**(现有 `SceneConfig.critical`,如 `INTENT_CLASSIFY`),但照常计入成本;
- **手动干预**:Redis 键可强制指定当前档位(运营解除/加严),优先级高于成本水位;
- 在途任务放行 = 熔断只拦网关**新请求**(现有行为已满足,不要在 worker 侧加检查)。
## 2. 已就绪的契约(协调提交已提供,直接用,禁止改动)
### 配置(`packages/shared/config.py` 已加字段 + `.env.example` 已示例)
```python
llm_budget_tiers: list[dict[str, Any]] # env: LLM_BUDGET_TIERS_JSON,**JSON 是一个列表**(无外层 key)
```
默认 JSON(元素结构:`{"threshold": 0.6, "name": "warn", "blocked": []}`;`blocked` 为字符串列表,`"*"` 表示全部;`allow` 仅 `block_all` 档有意义):
```json
{"tiers":[
{"threshold":0.6,"name":"warn","blocked":[]},
{"threshold":0.8,"name":"degrade_secondary","blocked":["hd_export","report_regen"]},
{"threshold":0.9,"name":"degrade_generation","blocked":["hairstyle","tryon"]},
{"threshold":1.0,"name":"block_all","blocked":["*"],"allow":["agent_chat"]}
]}
```
### 错误码(`packages/shared/errors.py` 已加)
- `LLM_SCENE_DEGRADED = 60006`,meta:http 503,message「该功能今日暂时关闭」,hint「AI 服务今日已达服务上限,明天恢复」。
### 现有代码事实(先读这些文件再动手)
- `packages/llm/budget.py`:`DailyBudgetGuard(counter, *, daily_budget_cents, enabled, key_prefix, tz_offset_hours, clock)`;方法 `current_key()`(`llm:cost:daily:{UTC+8日期}`)、`spent_today()`、`ensure_within_budget(scene, critical)`、`add_cost(cost_cents)`;`CostCounter` Protocol(`InMemoryCostCounter`/`RedisCostCounter`)。
- `packages/llm/gateway.py:110`:`await self._budget.ensure_within_budget(scene=scene.value, critical=config.critical)` —— 你要替换的就是这一处调用点。
- `packages/llm/errors.py`:已有 `DailyBudgetExceededError`。
- `apps/api/routers/agent.py:38`:已 catch `DailyBudgetExceededError → LLM_DAILY_BUDGET_EXCEEDED`。
- `packages/agent/service.py`:reserve → commit → gateway 异常时**先 refund 再 raise**(配额净扣减=0 的机制已存在,你只需用集成测试证明它)。
## 3. 实现要点
全部改动限定在:`packages/llm/budget.py`、`packages/llm/errors.py`、`packages/llm/gateway.py`(仅调用点与构造参数)、`apps/api/routers/agent.py`(异常映射)、`apps/api/deps.py` **不许改**——预算档配置从 `Settings.llm_budget_tiers` 读取的地方在哪?`build_llm_gateway()` 已在 deps.py(禁改)。**因此**:`DailyBudgetGuard.__init__` 增加关键字参数 `tiers: list[dict[str, Any]] | None = None`(默认 None = 内置默认四档),deps.py 已在协调提交里改为传入 `tiers=list(settings.llm_budget_tiers)`。你只管实现。
1. **budget.py**:
- 新增 pydantic `BaseModel`:`BudgetTier(threshold: float, name: str, blocked: list[str], allow: list[str] = [])`(llm 层允许 pydantic);
- `DailyBudgetGuard` 扩展:`current_tier() -> int`(0=未触发,1~4 如上)、`tier_name()`、`blocked_scenes() -> frozenset[str] | "*"`;
- 新方法 `ensure_scene_allowed(*, scene: str, critical: bool = False) -> None`:
- `critical` → 直接返回;
- 手动 override(Redis `llm:budget:override`,值为 "1"~"4",**读不到/值非法则忽略**)优先于水位;
- 按档位判定:tier2/3 且 scene ∈ blocked → 抛 `SceneDegradedError(scene, tier_name)`(新异常,加到 `errors.py`);tier4 且 `blocked=="*"` 且 scene ∉ allow → 抛 `DailyBudgetExceededError`(**保持 100% 档的既有语义与 60003 错误码不变**——现有测试依赖它);
- 保留 `ensure_within_budget()` 兼容(内部转调 `ensure_scene_allowed`),或直接替换调用点后删除——二选一,保持 `gateway.py` 只调一个方法;
- 新增 `set_override(tier: int)` / `clear_override()`(写 Redis,TTL 到当日 UTC+8 24 点),供运营/演练用;
- **指标**:`prometheus_client` 的 `Gauge("llm_budget_tier", ...)` 与 `Counter("llm_budget_blocked_total", ..., ["scene"])`(参考 `packages/llm/recording.py` 里 Prometheus 的用法);档位上升时 `logger.warning("budget_tier_raised", tier=..., spent=..., budget=...)`(仅上升沿打一次,避免每请求刷日志——实例内记 last_tier 即可)。
2. **gateway.py**:把 `ensure_within_budget` 调用换成 `ensure_scene_allowed(scene=scene.value, critical=config.critical)`;构造函数若需传 tiers,加默认参数(deps 已传)。
3. **agent.py**:新增 `except SceneDegradedError as exc: raise AppError(ErrorCode.LLM_SCENE_DEGRADED) from exc`。
4. **mypy strict**:budget.py 在 `packages/llm` 内,全量标注。
## 4. 测试要求
单测(`tests/unit/test_tiered_budget.py` 新文件;现有 `test_daily_budget.py` 保持通过,允许因 API 扩展做**增量**修改):
- 四档边界:59.9%/60%/79.9%/80%/89.9%/90%/99.9%/100% 时 `current_tier()` 正确;
- tier2 拒绝 `hd_export` 放行 `agent_chat`;tier4 拒绝非 allow 非 critical;critical 全档豁免;
- override 强制 tier4 / 清除后回落;
- `allow=["agent_chat"]` 在 tier4 生效;
- 计数用 `InMemoryCostCounter` + 注入 clock(参考现有测试)。
集成测试(**编辑 `tests/integration/test_vertical_slice.py`**,你是唯一允许编辑它的任务):
- 用 `monkeypatch.setenv` 把 `LLM_DAILY_BUDGET_CENTS` 设为极小值(如 "1")、`LLM_BUDGET_TIERS_JSON` 把 `block_all.blocked` 设为 `["agent_chat"]`(即在本测试里连纯文本对话也拦),`get_settings.cache_clear()` 后 `create_app`;
- 先用 Redis 客户端把当日成本键 `INCRBYFLOAT` 到预算以上(键格式照 `DailyBudgetGuard.current_key()` 计算);
- `POST /v1/agent/chat`(FakeProvider,参考现有 fixture)→ 断言:响应 code 为 60003(或 60006,取决于你把 agent_chat 归到哪个异常——按上面设计应为 60003)、**且 `bl_ledger` 中该用户当日 `consume` 流水净和为 0(reserved 后 refund)**——这正是 T-P1-25b 的验收"被熔断拒绝的请求配额净扣减 = 0,须有集成断言覆盖 reserve 之后才触发熔断这条路径";
- 测试结束 `get_settings.cache_clear()` 恢复,避免污染其它用例。
## 5. 禁改清单(通用禁改之外)
`packages/agent/**`、`packages/billing/**`、`packages/identity/**`、`packages/analytics/**`、`packages/storage/**`、`apps/api/routers/{auth,quota,tasks,events,uploads}.py`、`apps/api/schemas.py`。
## 6. 验收标准
1. 四档降级可由配置驱动,`warn` 档无用户可感知变化;
2. 手动 override 可加严/解除(redis-cli 可操作,写清键名与取值到 budget.py docstring);
3. 集成断言证明"reserve 之后熔断 → refund → 净扣减 0";
4. §3 检查全绿(mypy 范围:`packages/llm` strict + `apps packages/shared packages/identity packages/billing packages/task packages/agent packages/compliance packages/analytics packages/storage`)。
## 7. 提交信息
```
feat(llm): four-tier daily budget degradation with manual override
四档熔断(warn/secondary/generation/block_all)+ critical 豁免 + Redis 手动 override
+ Prometheus 指标 + 集成断言:被拒请求配额净扣减为 0(T-P1-25b)。
偏差说明:<如有写在这里,无则写"无">
```