收尾两批并行开发的文档回写与任务包

- 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 中相应引用改为留痕表述
This commit is contained in:
xsl
2026-08-14 21:07:20 +08:00
parent 2934954eb2
commit e511b6a66f
22 changed files with 1407 additions and 461 deletions
@@ -0,0 +1,106 @@
# 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)。
偏差说明:<如有写在这里,无则写"无">
```