Files
group_xinghuo_jinrong/docs/项目框架设计/TODO-架构改进.md
T
GaoYiYuan_0626 497cee289f docs: 交接文档合并为单一入口 + 全仓指向统一到 交接文档.md §A/§B/§C
背景:交接文档的定位是「给 AI 接手用的入口」,此前三份分散(项目根 交接文档.md
+ docs/交接文档-基金转换.md + docs/交接文档-架构改进.md),且 docs/ 两份停留在旧版
(v1.0 / v1.1,不含 T-0~T-2b、609 passed、D20 实施等进度),新会话极易被误导。

改动:
- 三份合并为项目根 交接文档.md v3.0(545 行 = §0 公共层 + §A 风控主线 + §B 基金转换线
  + §C 架构改进线);该文件在 .gitignore:47 内,按用户要求不入库(交接文档只留本地)
- 全仓指向统一到 交接文档.md §A/§B/§C:AGENTS.md(含顶部新增「接手先读交接文档.md」)、
  docs/memory/{MEMORY,TODO,FRAMEWORK,ITERATION}、PRD-架构改进与稳定性加固、
  开发计划-架构改进、TODO-架构改进、开发计划-基金转换交易
- 两份 docs/交接文档-*.md 加「已废弃(2026-09-10)· 勿读」横幅并指向新入口,
  保留作历史留档(不删除)
- 记录事故:对 docs/ 下中文名文件使用 git rm 会静默抹除整个 docs/ 目录(复现 2 次、
  退出码 0),已零损失恢复;纪律写入 交接文档.md §0.4 与工作区记忆

无代码改动;pytest 609 passed / 3 skipped。
2026-09-10 15:04:13 +08:00

252 lines
13 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.
# TODO · 架构改进与稳定性加固
> **用途**:开发执行清单。**按 T-1xx → T-2xx 顺序做**,每完成一项勾选并跑一次 pytest。
> **上游**:《docs/PRD/PRD-架构改进与稳定性加固.md》(做什么)·《docs/项目框架设计/开发计划-架构改进.md》(怎么做)
> **基线**:分支 `risk-control-agent`,HEAD `2d0e2fa`,**pytest 503 绿**
> **开工前必读**:项目根《交接文档.md》§C
> **实施记录(2026-09-09,hy3 执行 · 已复核)**:T-101~T-202 全部完成,pytest **510 绿**(503 基线 + 6 条锁测试 + 1 条中间件顺序守卫 = 新增 7 条);**未新增配置项/依赖/路由,三处 run_locked 调用点未改**。注意:《架构设计说明书》实际文件为 `docs/项目框架设计/架构设计-风控模块.md`(无 §4.3.x 章节),T-104/105/106 标注已落到 §2 树(scoring.py)、§5.2(locks.py)、§5.8(G1/G2/G3)对应位置。
> **提交**:`037ce7e`(commit 不 push,待用户浏览器目视确认后推送)
> **遗留手工验证(未经执行,勿视为通过)**:T-107 清空/恢复 key 各启动一次、停/启 Redis 各一次、`/api/simulate/trade` 阻断+放行各一次。
**状态标记**:`- [ ]` 待办 · `- [x]` 已完成 · `- [/]` 进行中
---
## 开工前检查(必做)
- [x] 确认当前分支 `risk-control-agent`,HEAD 为 `2d0e2fa`
- [x] 跑一次基线:`python -m pytest -q` → 记录实际数字(应为 503 passed)
- [x] 确认 `locks.py` / `redis_gateway.py` 在目标合并分支**无在途改动**
- [x] 读《交接文档.md》§C 全文(5 分钟,避免踩已记录的坑)
---
## 第 1 批 · 合并前(极低风险)
### T-101 · A1 注入词表条数修正
- [x] 改 `docs/memory/MEMORY.md` §0:`42 条` → `45 条`
**验收**:与 `app/service/input_guard.py:43-94` 逐行计数一致(指令覆盖 18 + 角色重置 11 + 系统提示泄露 9 + 越权诱导 7 = 45)
---
### T-102 · A2 风控对话 Tool 数量修正
- [x] 改 `docs/memory/MEMORY.md` 仓库地图:`4 个` → `5 个`
**验收**:与 `app/service/risk/chat_tools.py:328` `RISK_TOOL_REGISTRY` 一致(5 个:`query_overdue_alerts` / `alert_query` / `customer_context` / `suitability_check` / `aml_lookup`)。注:`query_agent_behavior`(276 行)定义但未注册,不是第 6 个。
---
### T-103 · A3 Agent 专用表数量修正
- [x] 改 `docs/项目框架设计/表设计/02-mysql-agent专用.sql` 注释:`5 张` → `6 张`
**验收**:与实建表一致(`customer_threshold_config` / `customer_notify_log` / `advisor_draft` / `compliance_hit_log` / `analytics_query_log` / `risk_aml_list`)
---
### T-104 · A4 标注 scoring.py 为预留桩
- [x] 在《架构设计说明书》§4.3.2 补明:`scoring.py` 的 `recompute_customer_score` 直接 `raise NotImplementedError`,是签名冻结的**预留桩**,非实际评分器;L3 `risk_score` 一期恒 NULL
**验收**:读者不会误以为动态评分已生效
---
### T-105 · A5 标注 locks.py 为进程内锁
- [x] 在《架构设计说明书》§4.3.4 补明:`locks.py` 用 `threading.Lock`,是**进程内**锁,多实例部署失效为 TODO
**验收**:读者知道多实例场景需先处理此项(本清单 T-201 解决)
---
### T-106 · G1/G2/G3 非缺陷设计标注
- [x] G1:`X-Trace-Id` 可前端伪造 —— 写明有白名单 `^[A-Za-z0-9._-]{1,64}$` 防响应头注入,**身份只认 JWT,trace_id 绝不用于权限判定**
- [x] G2:`trace_id` 概率唯一 —— 写明 `uuid4().hex[:16]` = 64 bit,请求级,约 2^32 次请求才 50% 碰撞,无需数据库唯一校验
- [x] G3:`admin.py` / `knowledge.py` 空壳 —— 写明 T-21 拍板一期只做脚本入库、未挂载路由、**空 ≠ RAG 缺失**(能力在 `service/rag_service.py`)
**验收**:三项在《架构设计说明书》各有独立条目,含"为什么不是问题"
---
### T-107 · C4 无 key 启动告警
- [x] 在 `app/main.py` lifespan 新增告警(紧跟 `main.py:61-65` 现有 dev secret 告警之后)
```python
if not settings.deepseek_api_key:
logger.warning(
"DEEPSEEK_API_KEY 未配置,对话将走降级回复(前缀 %s),LLM 能力不可用",
agent_service._DEGRADED_PREFIX,
)
```
**注意**:需引用 `app.service.agent_service` 的 `_DEGRADED_PREFIX`(位于 `agent_service.py:37`)。**若出现循环导入,改为直接写字面量常量**。
**验收**(前两项需手工,未执行):
- [ ] 清空 `.env` 的 `DEEPSEEK_API_KEY` → 启动日志出现告警,**不阻塞启动**
- [ ] 恢复 key → 无该告警
- [x] 无循环导入(采用函数内延迟 import;全量 pytest 中 `app.main` 被多用例正常导入,无 ImportError)
---
### T-108 · 审计降级告警强化(authz)
- [x] 改 `app/utils/authz.py:70-76`,补 `trace_id` 等字段
```python
logger.exception(
"authz 审计失败(已降级,拒绝语义不变):code=%s agent=%s actor=%s trace_id=%s",
code, agent_type, actor_id, current_trace(),
)
```
**验收**:日志含 4 个字段;降级语义不变(仍不阻塞、不抛异常)
---
### T-109 · 审计降级告警强化(middleware)
- [x] 改 `app/api/audit_middleware.py:78`,补 `status` / `path` / `request_id`
**验收**:日志含上述字段;失败路径仍返回原响应,**行为零变化**(pytest 510 绿,原有审计用例无断言变化)
---
### 第 1 批完成检查
- [x] `python -m pytest -q` 全绿(实测 **510 passed**)
- [x] T-107 手工冒烟通过(清空/恢复 key 各一次)—— **2026-09-10 补跑 ✓**:本机 `.env` 的 `DEEPSEEK_API_KEY` 本就为空(len=0),清空态直接验证:uvicorn 1s 起、`/health` 200、进程存活不阻塞、日志出现「DEEPSEEK_API_KEY 未配置,对话将走降级回复」;「恢复 key 无告警」一支因本机无真实 key,改用**非空占位值**验证条件分支(告警只判 `not settings.deepseek_api_key`,不校验有效性),未出现告警 ✓;验证后 `.env` 已还原
---
## 第 2 批 · 合并前(核心改动)
### T-201 · B6 Redis 分布式锁
> ⚠️ **本项改动前三处调用点必须全部覆盖**(见下),漏改 `agg:suitability:` 会让 R-02 唯一阻断点失去并发保护。
**调用点(三处,勿漏)**:
| # | 位置 | key |
| --- | --- | --- |
| 1 | `alert_service.py:180` | `agg:event:{customer_id}:{date}` |
| 2 | `alert_service.py:239` | `agg:suitability:{customer_id}:{product_id}:{date}` |
| 3 | `profile_l3.py:155` | `l3:{customer_id}` |
- [x] **T-201.1** `app/service/risk/redis_gateway.py` 新增 `acquire_lock`(`SET key token NX EX ttl`)与 `release_lock`(Lua 脚本,只删自己的锁)—— **纯新增,不动现有方法**
```python
def acquire_lock(self, key: str, token: str, ttl_seconds: int) -> bool:
return bool(self._ensure().set(key, token, nx=True, ex=ttl_seconds))
def release_lock(self, key: str, token: str) -> bool:
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
return bool(self._ensure().eval(script, 1, key, token))
```
- [x] **T-201.2** `app/service/risk/locks.py` 改造 `run_locked` 为双层(Redis 为主、进程内为备),新增常量:
- `LOCK_TTL_SECONDS = 30`(锁持有上限)
- `_LOCK_KEY_PREFIX = "lock:"`(命名空间隔离)
- `_RETRY_INTERVAL = 0.05`
- `LOCK_TIMEOUT_SECONDS = 2.0` **保持原值不动**
**降级规则(三条,不可改)**:
1. Redis 抢到 → `fn(locked=True)`,finally 释放
2. Redis 等待超时 → `fn(locked=False)`(**与原语义一致**)
3. Redis 不可用(**含任何异常,含 Fake 缺方法的 `AttributeError`**)→ 退回进程内锁,绝不向上抛
- [x] **T-201.3** 新增测试(`tests/test_locks_redis.py`,6 条,无本机 Redis 依赖,全部走 monkeypatch 假网关):
| 用例 | 断言 |
| --- | --- |
| Redis 可用抢锁成功 | `fn(locked=True)`,Redis key 带 TTL |
| Redis 锁被占用 | 超时后 `fn(locked=False)`,降级告警出现 |
| Redis 抛 `ConnectionError` | 退回进程内锁,不抛异常,业务完成 |
| FakeGateway 缺方法(`AttributeError`) | 归类 unavailable,测试不红 |
| 释放只删自己的锁 | 错误 token 释放返回 False,锁仍在 |
| **三处 key 形态全覆盖** | `agg:event:` / `agg:suitability:` / `l3:` 均带 `lock:` 前缀 |
**验收**:
- [x] `run_locked` 签名与 `fn(locked)` 语义不变,三处调用点**无需修改**(`alert_service.py` / `profile_l3.py` 本次零改动)
- [x] 全量 pytest 绿(实测 **510 passed**,用例表 6 条全部落地)
- [ ] 停 Redis → 业务仍可完成;恢复 Redis → 行为一致(**需手工,未执行**;`ConnectionError` 路径已由单测覆盖)
---
### T-202 · B2 中间件顺序测试守卫
- [x] 新增守卫测试 `tests/test_audit_middleware.py::test_audit_middleware_runs_inside_trace_middleware`(同时把 `trace_id` 加入 `_http_access` 查询列)
```python
def test_audit_middleware_runs_inside_trace_middleware(client, captured_audit):
"""守卫:audit 必须在 trace 之内执行,否则 trace_id 静默全空。"""
resp = client.get("/health") # 或任一非 _SKIP_PATHS 路径
rows = captured_audit()
assert rows, "应落 http_access 审计"
assert rows[-1]["trace_id"], "audit 若先于 trace 执行,此处会静默为空"
```
**验收**:
- [x] 正常顺序下测试通过(pytest 510 绿)
- [ ] **临时调换 `main.py` 两个装饰器顺序 → 测试变红**(hy3 日志称已验证变红并恢复;**本次会话未复核**,建议重跑一次确认)
- [ ] 恢复顺序 → 转绿
- [x] **不**加 `ensure_trace()` 兜底(未加,顺序错误不会被掩盖)
---
### 第 2 批完成检查
- [x] `python -m pytest -q` 全绿且用例数 ≥ 503(实测 510)
- [ ] `POST /api/simulate/trade` 阻断 + 放行各一次
- [ ] 预警聚合仍为"同客户同日仅一张 pending 单"
- [ ] 停/启 Redis 各测一次
- [x] 未新增配置项、未新增依赖、未改路由
---
## 第 3 批 · 合并后(需拍板或涉及 DDL,本期不做)
- [ ] T-301 B1 `agent_message` 加 UNIQUE 索引 **+ `insert_turn` 补 `IntegrityError` 重试**(两步必须同时做,只加索引会让并发变 500)
- 前置:先查历史重号 `SELECT session_id, seq_no, COUNT(*) FROM agent_message GROUP BY 1,2 HAVING COUNT(*)>1`
- 附带:`next_seq_no` + `insert_message` 非事务组合建议废弃
- [ ] T-302 B5 限流 `INCR` 后无条件 `EXPIRE`(去掉"首命中"判断)
- [ ] T-303 C1 `core_ro` 配 DB 只读账号(需运维;须拆分连接串)
- [ ] T-304 F1 支持 `convert`(需产品三条口径;**记得同步改 `core_ro.py:390`/`:432` 的 `trade_type IN` 过滤**)
- [ ] T-305 F2 意图识别升级(建议先扩同义词表 + 多意图,保持可审计性)
## 第 4 批 · 合并后(重构类,本期不做)
- [ ] T-401 D1 日志基建(`utils/logger.py` 仅一行 docstring)
- [ ] T-402 E1 权限逻辑(**建议维持三层纵深,只补文档不收拢**)
- [ ] T-403 E2 Tool 注册表聚合
- [ ] T-404 E3 `model/` 层启用(渐进式加 `TypedDict`)
- [ ] T-405 E4 注入词表配置化
- [ ] T-406 F3 R-05 动态评分
- [ ] T-407 B3/B4 双写判据文档化(**判据:新增可丢留痕,变更不可丢**)
---
## 完成判定(总验收)
> 状态(**2026-09-10 全部闭环**):代码侧自 `037ce7e` 起已推送远程;**§7.2 七项手工冒烟已补跑 7/7 PASS**;冒烟残留已按 SOP §2 重灌清除,全量 **510 passed** 复绿。
- [x] FR-01~FR-06 六项验收标准逐条通过(FR-01/02 文档勘误与标注、FR-03 无 key 告警、FR-04 审计告警、FR-05 Redis 锁、FR-06 中间件顺序测试均已落地)
- [x] `python -m pytest -q` 全绿且用例数 ≥ 503(实测 **510**;冒烟残留曾致 3 条集成前置断言红,重灌后复绿 510)
- [x] uvicorn 冒烟:`/health` 正常,无 key 告警按预期出现(**2026-09-10 ✓**)
- [x] 交易阻断 + 放行路径各一次,预警聚合行为不变(**2026-09-10 ✓** 证据见 `docs/memory/2026-09-10.md`)
- [x] 停/启 Redis 各测一次,业务均可完成(**2026-09-10 ✓** Redis 不可用退回进程内锁、恢复后零降级命中)
- [x] 未新增配置项、未新增依赖、未改路由
- [x] **已推送**(远程 `risk-control-agent` = `fffb78a` = 本地 HEAD,`037ce7e` 在其祖先链上)——原「commit 但不 push」表述**已作废**
- [x] 附加守卫有效性验证:调换 `main.py` 两装饰器 → T-202 守卫变红 → 字节级还原后转绿(**2026-09-10 ✓**)