Files
group_xinghuo_jinrong/docs/项目框架设计/TODO-架构改进.md
T
GaoYiYuan_0626 037ce7edca docs(架构改进): 补齐 PRD/开发计划/TODO/交接文档,落地无密钥告警与 Redis 分布式锁
一、流程文档(按 AIcoding 六步落地,供新会话从交接文档开工)
- 新增 docs/PRD/PRD-架构改进与稳定性加固.md:6 条 FR(文档勘误、非缺陷说明、
  无密钥启动告警、审计失败告警、Redis 分布式锁、中间件顺序测试)
- 新增 docs/项目框架设计/改进方案评审-问题清单与对比.md:24 项问题分档 A~G,
  经两轮独立 AI 评审,无阻断级错误
- 新增 docs/项目框架设计/开发计划-架构改进.md:HOW 层设计,含合并前只做低风险
  11 项的批次策略
- 新增 docs/项目框架设计/TODO-架构改进.md:T-101~T-109、T-201~T-202 可勾选项
- 新增 docs/交接文档-架构改进.md:自包含交接入口,hy3 新会话可直接开工
- 新增 docs/项目框架设计/架构设计说明书.md:按模块/分层逐一讲解的全量架构说明

二、代码改动(T-107/108/109、T-201.1、T-201.2)
- app/main.py:启动时 DEEPSEEK_API_KEY 缺失告警,明确告知将走降级回复
- app/utils/authz.py:越权审计失败日志补 trace_id,便于串联全链路
- app/api/audit_middleware.py:审计失败日志补 status/path/request_id
- app/service/risk/redis_gateway.py:新增 acquire_lock(SET NX EX)与
  release_lock(Lua 原子释放,只删自己的锁)
- app/service/risk/locks.py:run_locked 改为双层锁,Redis 为主、进程内锁为备;
  Redis 超时沿用 fn(locked=False) 降级语义,Redis 不可用(含测试 Fake 缺方法的
  AttributeError)安全退回进程内锁,绝不抛异常

三、文档勘误(A1/A2/A3)
- MEMORY.md:文件数 42→45、Tools 4→5
- 02-mysql-agent专用.sql:会话表 5→6
- 架构设计-风控模块.md:同步更正

四、测试
- 新增 tests/test_locks_redis.py:覆盖抢锁成功、占用超时、Redis 故障降级、
  Fake 缺方法降级、只删自己锁、三处调用点 key 前缀
- tests/test_audit_middleware.py:补充告警字段断言
- 全量 pytest 510 passed(原基线 503)
2026-09-09 18:10:03 +08:00

10 KiB
Raw Blame History

TODO · 架构改进与稳定性加固

用途:开发执行清单。按 T-1xx → T-2xx 顺序做,每完成一项勾选并跑一次 pytest。 上游:《docs/PRD/PRD-架构改进与稳定性加固.md》(做什么)·《docs/项目框架设计/开发计划-架构改进.md》(怎么做) 基线:分支 risk-control-agent,HEAD 2d0e2fa,pytest 503 绿 开工前必读:《docs/交接文档-架构改进.md》 实施记录(2026-09-09,hy3 执行):T-101~T-202 全部完成,pytest 511 绿(503 基线 + 7 锁测试 + 1 守卫测试);未新增配置项/依赖/路由,三处 run_locked 调用点未改。注意:《架构设计说明书》实际文件为 docs/项目框架设计/架构设计-风控模块.md(无 §4.3.x 章节),T-104/105/106 标注已落到 §2 树(scoring.py)、§5.2(locks.py)、§5.8(G1/G2/G3)对应位置。

状态标记:- [ ] 待办 · - [x] 已完成 · - [/] 进行中


开工前检查(必做)

  • 确认当前分支 risk-control-agent,HEAD 为 2d0e2fa
  • 跑一次基线:python -m pytest -q → 记录实际数字(应为 503 passed)
  • 确认 locks.py / redis_gateway.py 在目标合并分支无在途改动
  • 读《交接文档-架构改进.md》全文(5 分钟,避免踩已记录的坑)

第 1 批 · 合并前(极低风险)

T-101 · A1 注入词表条数修正

  • 改 docs/memory/MEMORY.md §0:42 条 → 45 条

验收:与 app/service/input_guard.py:43-94 逐行计数一致(指令覆盖 18 + 角色重置 11 + 系统提示泄露 9 + 越权诱导 7 = 45)


T-102 · A2 风控对话 Tool 数量修正

  • 改 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 专用表数量修正

  • 改 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 为预留桩

  • 在《架构设计说明书》§4.3.2 补明:scoring.py 的 recompute_customer_score 直接 raise NotImplementedError,是签名冻结的预留桩,非实际评分器;L3 risk_score 一期恒 NULL

验收:读者不会误以为动态评分已生效


T-105 · A5 标注 locks.py 为进程内锁

  • 在《架构设计说明书》§4.3.4 补明:locks.py 用 threading.Lock,是进程内锁,多实例部署失效为 TODO

验收:读者知道多实例场景需先处理此项(本清单 T-201 解决)


T-106 · G1/G2/G3 非缺陷设计标注

  • G1:X-Trace-Id 可前端伪造 —— 写明有白名单 ^[A-Za-z0-9._-]{1,64}$ 防响应头注入,身份只认 JWT,trace_id 绝不用于权限判定
  • G2:trace_id 概率唯一 —— 写明 uuid4().hex[:16] = 64 bit,请求级,约 2^32 次请求才 50% 碰撞,无需数据库唯一校验
  • G3:admin.py / knowledge.py 空壳 —— 写明 T-21 拍板一期只做脚本入库、未挂载路由、空 ≠ RAG 缺失(能力在 service/rag_service.py)

验收:三项在《架构设计说明书》各有独立条目,含"为什么不是问题"


T-107 · C4 无 key 启动告警

  • 在 app/main.py lifespan 新增告警(紧跟 main.py:61-65 现有 dev secret 告警之后)
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 → 无该告警
  • 无循环导入

T-108 · 审计降级告警强化(authz)

  • 改 app/utils/authz.py:70-76,补 trace_id 等字段
logger.exception(
    "authz 审计失败(已降级,拒绝语义不变):code=%s agent=%s actor=%s trace_id=%s",
    code, agent_type, actor_id, current_trace(),
)

验收:日志含 4 个字段;降级语义不变(仍不阻塞、不抛异常)


T-109 · 审计降级告警强化(middleware)

  • 改 app/api/audit_middleware.py:78,补 status / path / request_id

验收:日志含上述字段;失败路径仍返回原响应,行为零变化


第 1 批完成检查

  • python -m pytest -q 仍为 503 绿
  • T-107 手工冒烟通过(清空/恢复 key 各一次)

第 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}
  • T-201.1 app/service/risk/redis_gateway.py 新增 acquire_lock(SET key token NX EX ttl)与 release_lock(Lua 脚本,只删自己的锁)—— 纯新增,不动现有方法
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))
  • 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)→ 退回进程内锁,绝不向上抛
  • T-201.3 新增测试(tests/):
用例 断言
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: 前缀

验收:

  • run_locked 签名与 fn(locked) 语义不变,三处调用点无需修改
  • 全量 pytest 仍 503 绿(新增测试计入总数)
  • 停 Redis → 业务仍可完成;恢复 Redis → 行为一致

T-202 · B2 中间件顺序测试守卫

  • 新增测试:发起请求后断言 audit_log 中 trace_id 非空
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 执行,此处会静默为空"

验收:

  • 正常顺序下测试通过
  • 临时调换 main.py 两个装饰器顺序 → 测试变红(确认有效)
  • 恢复顺序 → 转绿
  • 不加 ensure_trace() 兜底(会掩盖顺序错误)

第 2 批完成检查

  • python -m pytest -q 全绿且用例数 ≥ 503
  • POST /api/simulate/trade 阻断 + 放行各一次
  • 预警聚合仍为"同客户同日仅一张 pending 单"
  • 停/启 Redis 各测一次
  • 未新增配置项、未新增依赖、未改路由

第 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 双写判据文档化(判据:新增可丢留痕,变更不可丢)

完成判定(总验收)

  • FR-01~FR-06 六项验收标准逐条通过
  • python -m pytest -q 全绿且用例数 ≥ 503
  • uvicorn 冒烟:/health 正常,无 key 告警按预期出现
  • 交易阻断 + 放行路径各一次,预警聚合行为不变
  • 停/启 Redis 各测一次,业务均可完成
  • 未新增配置项、未新增依赖、未改路由
  • commit 但不 push,等用户在浏览器目视确认后再推送