18 KiB
开发计划 · 风控监测 Agent 模块
版本:v1.1(AI 评审通过:修复 P1×2——假 AuthContext 规范落 deps.py、conftest 测试前置;P2×7 顺手收敛)· 2026-09-06 · 分支
feature/risk· 负责人 E
依据:PRD v1.0 + 架构设计 v1.1
铁律:每步完成即测,测试通过才进下一步;每个任务至少一个 commit,里程碑打 tag;涉及表结构/接口协议的步骤已获 PRD 批准
任务映射:T-31 = 阶段 A(交付=suitability 服务函数+单测;check API 随 B6)· T-30 = 阶段 B · T-32 = 阶段 C(交付=对话线;台账/处置 API 随 B6)——此边界说明用于对齐docs/memory/TODO.md文字,M4 时同步修订 TODO
阶段 A · 基建与公共校验(不依赖 T-01/T-07,可立即开工)
| # | 任务 | 产出 | 验证方式 | 依赖 |
|---|---|---|---|---|
| A1 | utils/trace.py + utils/desensitize.py + settings.py 扩展(risk_* 阈值)+ .env.example 同步 |
3 个工具模块 + 配置 | 单测:脱敏 DESENS-001~004 四规则(005 资产金额归前端组件,后端不做)、trace set/get | 无 |
| A2 | core_ro.py 扩展 sum_trades_on_date(其余复用现有方法) |
Repository 方法 | 单测/手工 SQL 对照 | 前置:本机已执行 reset.ps1 灌库(FLOW §0 ③,一次性本机操作,非等待 T-05 任务) |
| A3 | repository/risk_repository.py(risk_alert / risk_suitability_log / l3 / aml_list 读写) |
Repository | 手工 SQL 对照 | agent 库建表(含 risk_aml_list) |
| A4 | service/suitability.py:SUIT-001~008 纯函数 + check 服务(落日志)+ AuthContext Pydantic 模型与 get_auth_context 工厂签名占位(实现归 B6) |
公共校验服务 + AuthContext 接口冻结 | 单测:矩阵 25 组合 + 69/70 岁 + NULL 年龄 + 364/365 天(验收 A-8) | A1、A2、A3 |
阶段 B · 事件驱动线(核心,不依赖 T-07)
| # | 任务 | 产出 | 验证方式 | 依赖 |
|---|---|---|---|---|
| B1 | service/risk/rules.py:RISK-001~005 纯函数 |
规则函数 | 单测:各规则命中/不命中 + RISK-004 窗口 + RISK-005 非连续 | A1 |
| B2 | alert_service.py:聚合去重(进程内锁 + 锁内 check-insert)+ 审计落库 + risk:pub:alert PUBLISH(同步 Redis 单例)。备注:merge 原语(append_alert_event)已下沉 repo(A3),本任务只做聚合决策与编排(评审 P2-5 口径) |
预警服务 | 单测:聚合合并、risk_score 取 max、去重追加;并发冒烟:两线程同客户同日首单 → 预警单数=1 且 events[] 含两笔 | A3 |
| B3 | profile_l3.py:get_l3 → 最高档合并 → insert_l3/update_l3。备注:非原子,并发首单需 catch IntegrityError 转更新(或复用 B2 锁)(评审 P2-7③);B3 评审后口径(P3-4 · 用户拍板 2026-09-06):L3 risk_score 一期不写(保持 NULL,归 R-05 评分模型首写),tier/tags/last_alert_id/computed_at 照 FR-7 |
L3 写入 | 单测:normal→high 不降级、AML 后大额不回落 | A3 |
| B4 | aml_service.py(归一化+相似度匹配、scan_all)+ engine.py(process_trade_event 组装 + 预留客户事件钩子)+ scoring.py 占位签名(FR-7 预留) |
引擎完整 | 单测:AML 阈值边界;引擎集成冒烟 | B1、B2、B3。B4 评审修复(2026-09-06):FR-4 payload 客户上下文(L0+近 30 天统计)补组装;core_ro 增 list_trades_range(升序)/list_active_customers;种子名单 3 条改名维持唯一演示命中;core_cash_flow 上下文一期未接(PRD「如有」),B8 前评估;真实名单数据接入时 payload/审计出口接 utils/desensitize(B9b 前核查项);复审观察项:30 天窗口口径(现 31 自然日)/申赎混合求和方向,R-05 接入前统一 |
| B5 | app/gateway/(trade_gateway + gateway_repository 仅 INSERT core_trade)+ api/simulate.py 薄路由 |
网关 | 集成:convert 400、阻断不落 trade | A4、B4 |
| B6 | app/api/deps.py:AuthContext(actor_id/roles/customer_id,字段按 JWT 手册冻结)+ get_auth_context() 工厂——dev 模式从 X-Debug-Role/X-Debug-Actor 请求头构造、app_env != development 启动时检测 debug 头直接拒绝;T-01 就绪后仅替换工厂内部为 JWT 解析,签名不变。另:api/risk.py 4 个 API(GET alerts / POST handle / POST suitability/check / POST aml/scan)+ 归属校验(含 compliance 强制 aml 过滤)。备注:依赖层须校验 handler_result 枚举(repo 不校验);本阶段顺手统一 NotFoundError 异常(utils/exceptions.py 现为占位)(评审 P2-7①②) |
鉴权依赖 + 4 个 API | Swagger 手测 + 权限矩阵(按 debug 头切换角色/身份执行 A-7/A-9 用例) | A4、B2、B4(aml/scan 依赖 scan_all) |
| B7 | main.py 集成:路由挂载 + lifespan(双 Engine 单例注入 + Redis 单例 + trace 中间件)。备注:顺手提取 utils/db.py 引擎工厂收敛 core_ro/risk_repository 双份 _default_engine(评审 P2-6);B3 挂账(B3 评审 P2-5):① _run_locked 锁原语公共化(alert_service/profile_l3 现复用私有实现)② L3 写侧 Redis 缓存 DEL 钩子(PRD §5.1 profile:l3:{customer_id} 更新时 DEL,profile_l3.upsert_profile_l3 已留痕)③ 删除 core_ro.list_trades 死代码(B4 改用 list_trades_range 后无调用方,复审 N3)④ 统一响应外壳落地(utils/response.py 现占位,simulate/risk 路由届时一并包裹,B5 评审 P2-2;错误体对齐手册 §10 error_code/message/trace_id,B6 评审 P3-2);B6 挂账(B6 复审):⑤ lifespan 启动期检测 app_env != development + debug 头依赖直接拒绝启动(现为请求时 RuntimeError,B6 评审遗漏①)⑥ 引擎工厂须覆盖 deps/simulate/risk 三处每请求 RiskRepository()/CoreReadOnlyRepository() 实例化点并 dispose(现复用 _default_engine 不释放,B6 复审 P3)⑦ handle_alert 的 update_alert_status 与 insert_audit_log 两事务非原子——统一事务或补偿记录(B6 复审 P3)⑧ input_guard_log 双写缺口(手册 P-05 要求 audit_log+input_guard_log,表归 T-03 底座,B7 接 T-02 审计中间件时统一补) |
可运行应用 | uvicorn 启动 + /health + 全路由可达 |
B5、B6 |
B7 复审(独立 AI 评审 · 2026-09-06):有条件通过 → 已闭环。 挂账①②③④⑤⑦证实落地、⑧确认随 T-02 无烂尾、红线全守住。P1-1 dispose_engines 仅 cache_clear 未真 dispose——已修(utils/db.py 手写单例字典 + 显式 Engine.dispose(),新增 tests/test_db.py 3 例,183 绿)。挂账:P2-1 L3 DEL 钩子无行为断言随 B8;P2-2 未捕获异常 500 无 trace 头回写 + 422/404/405 错误码补齐归 T-02;P3-1 update_alert_status 建议标注 deprecated(防绕过同事务审计)、P3-2 locks 降级文案中性化(可随 B8 顺手);P3-3 生产误配 app_env=development 检查归 B9b SOP;P3-4 独立 request_id(现沿用 trace_id)归 T-02。偏差留痕:成功响应不包裹外壳 = B5 P2-2「返回体不变」演进口径,M4 复核时对齐开发计划文本。
| B8 | tests/conftest.py:a) session fixture 启动校验演示数据就位(CUST-4001 测评 <365 天、risk_aml_list ≥8),缺失则中止并提示先跑 FLOW §0 ③④;b) fixture 幂等代跑 prepare_risk_demo.sql;c) teardown 按 TRD-TEST- 清 core_trade + 关联 risk_alert/risk_suitability_log/audit_log + 还原 L3 行。顺手集中 sqlite 测试 DDL 为单一事实源(B4 评审 P3-12,各测试文件手写 DDL 收敛;CURRENT_TIMESTAMP 改 localtime 或 fixture 固定时间,防 UTC/本地日界错位——B5 评审 P3-4);集成测试交易统一走 trade_id_factory 注入 TRD-TEST- 前缀(trade_gateway 已留参数,B5 评审 P3-2)。集成测试:A-1A-5、A-7(状态机/compliance 403/GET 强制 aml)、A-9 越权、trace 一致性断言;补 platform 审计 input_summary 的 JSON 解析断言(含引擎输出/阻断 reasons,B5 复审 L1);B7 复审 P2-1:补 L3 DEL 钩子行为断言(fake 记录 deletes,断言 key=B8 并行) |profile:l3:{customer_id} 与降级路径);P3-2 locks 降级文案中性化顺手改 | 测试套件 + fixture | pytest 全绿 | B7 |
| B9a | 演示/运维脚本开发:scripts/demo/subscribe_alerts.py(订阅演示)+ scripts/demo/rebuild_alerts.py(按 trade_id 幂等重放补偿) | 2 个脚本 | 手工执行验证 | B2、B4(可与 B5
B9a 完成(2026-09-07): 两脚本 + repo 只读扩展(core_ro.get_trade_by_id / risk_repo.find_alerts_by_trade:payload LIKE 查已入单,含 aml/已处置单——aml 出单路径本身无去重,幂等检查是其防重放双单的唯一闸门)+ tests/test_demo_scripts.py 5 例,201 绿。真库手工验证:rebuilt(RISK-001/002 出单)→ 再跑 skipped → missing exit 1;订阅端到端收假消息 + 真交易推送(trace 贯通、notify_role 正确),验证现场已清理。本机补装 redis 包(requirements 有而环境漏装,见 FLOW §0 尾注)。
B9a 复审(独立 AI 评审 · 2026-09-07):有条件通过 → 已闭环。 幂等选型(payload LIKE)经穷举核实无误匹配/漏匹配面、三条出单路径全覆盖、已处置单覆盖、红线全守、风格与文档注记准确。闭环:P2-1 部分失败中间态补偿盲区——skipped 分支新增 has_engine_error_audit 检测(audit_log 无 trade_id 列,input_summary LIKE 同口径),命中输出 warning 提示人工核对 aml/审计/L3 缺口 + docstring 局限声明(聚合锚点按执行日/编排非原子);P2-2 补「同日第二笔 append 进同单后 rebuild 非首笔 trade 必 skip 且不重复 append」用例。P3 顺手落:连接自检(subscribe 的 ImportError 分支 + rebuild 双库探针)、非 dict payload 防护(raw 兜底)、get_trade_by_id 加 confirmed 过滤(对齐引擎统计口径)、docstring 勿并行声明。P3 留痕:find_alerts_by_trade 全表扫 + 跨进程并发重放无防护(演示规模可接受,生产化改 JSON_CONTAINS/events 明细表)。204 绿;warning 路径真库复验后现场清理。
B8 复审(独立 AI 评审 · 2026-09-06):有条件通过 → 已闭环。 开发计划 B8 行 a/b/c + DDL 单一事实源/localtime + TRD-TEST- 注入 + A-1A-5/A-7/A-9/trace/审计 JSON 全部达成,196 绿实跑,B7 复审 P2-1/P3-2 落地。P1-1 A-5、A-7~A-9(A-6 归 M3)**。B6 挂账核查单:① 预警类 API 响应体含固定 disclaimer「本预警由系统自动生成,最终判定需经风控专员人工审核」(PRD §6/规则表 §5,B6 复审 P3-7)② aml/scan 幂等防护(重复扫描同命中客户重复出单,演示点击即复现,B6 评审 P3-6)③ B6 时代码以 TestClient 独立挂 router 等价验证,本次补一次真 Swagger 手测(B6 复审遗漏⑤)④ analyst 台账只读权限扩展待 Wave 3 分析 Agent 接入时定权限矩阵(手册 §5.3 有 risk:alert:read,现 fail-closed 拒绝,B6 复审观察③)⑤ 生产/演示机 conftest.py except pytest.SkipRequested 引用不存在属性,演示数据缺失时 skip 路径反噬整个 pytest 收集(未灌库机器单测全跑不了)——已修(Skipped 继承 BaseException 直传,故障注入验证:错密码收集期 skip 不 error);P3 顺手项已落:teardown l3_snapshot 哨兵(防 setup 失败掩盖原始异常)、A-1 补 trace 环①响应头与环②suitability_log.trace_id。挂账:P2-1 集成测试对演示库当日状态隐式耦合(A-4 依赖 CUST-9527 当日无历史交易、A-5 aml 单查询未按本测试 trade_id 定位、A-1 同日已有 pending 单会并入旧单)→ B9b 核查单⑥,演示与 pytest 同日交叉前必须处理;P3-3 _state 模块级顺序耦合(单跑 A-7 等 KeyError,文件头已声明保序,可接受留痕);P3-4 阈值冒烟用例在 .env 覆盖阈值时会红(注释已声明前提);P3-5 teardown 时间窗隐含 MySQL 时钟==本机时钟(docstring 已声明单机约定)。
| B9b | 演示链路走查:reset.ps1 → prepare_risk_demo.sql → agent 库建表 → seed-aml-list.sql → Swagger 逐条过 **A-1app_env=development 误配检查(B7 复审 P3-3:误配时 debug 头可达且启动校验放行,列入演示 SOP)⑥ B8 复审 P2-1:集成测试对演示库当日状态的隐式耦合前置断言(A-4 前断言 CUST-9527 当日事件计数==0、A-5 aml 单按本测试 trade_id 定位、A-1 同日 pending 单并入防护),演示与 pytest 同日交叉执行前必须处理 | 演示 SOP | 按 PRD §8 验收表逐条打勾 | B8、B9a |
B9b 完成(2026-09-07): 走查前置代码三件:①预警类 API(GET alerts/POST handle/POST aml/scan)响应体加固定 disclaimer(核查单①)②scan_all 幂等防护——新增 find_recent_aml_alert,同客户当日已有 aml 单(含交易触发/已处置)不再出单,summary 增 skipped_existing(核查单②)③集成测试前置断言(核查单⑥):A-1/A-3/A-4 清洁检查(交叉时 fail+重灌提示)、A-5 aml 单按本笔 trade_id 定位。208 测试绿(+4:scan 幂等×2/disclaimer/交易触发后 scan skip)。演示 SOP 落地 docs/项目框架设计/演示SOP-风控模块.md(含脚本化重灌 MYSQL_PWD 方式)。真服务走查(uvicorn 8123 + 真库):A-1~A-5/A-7/A-9 + 核查单①②④⑤ 全 PASS(11/11),核查单⑥防护价值现场复现(走查残留数据致 A-1 定位失效 → 重灌后过,正是 P2-1 预警的场景)。核查单③=本次真服务手测(区别于 B6 TestClient 等价验证);⑤本机 APP_ENV=development 确认。走查演示数据按 SOP 保留,pytest 集成测试前需重灌。里程碑 M2 达成(阶段 B 全链路 + A-1A-5/A-7A-9 验收),commit 打 tag risk-m2(M1 时点未打 tag,留痕)。
B9b 复审(独立 AI 评审 · 2026-09-07):有条件通过 → 已闭环。 核查单②幂等口径(不限 status/按日/覆盖交易触发单)、①disclaimer 落点三处与文案逐字、⑥前置断言与聚合锚点口径一致(LIKE 为保守超集 fail-closed)、红线全守、SOP 与 PRD §8 逐项一致,均在审查中证实。闭环三项(全为文档/账务):P2-1 SOP 交互方式重灌补 DROP DATABASE jinrong_agent(reset.ps1 只重建 core + 裸 CREATE TABLE 会 1050 中止);P2-2 T-31 名下阶段 A 挂账 P2-8 遗漏——已补做收口(4 组演示组合真库手工 SQL 推导与函数判定逐项一致,登记 SOP 核查单附行);P2-3 SOP 走查范围与 11/11 口径统一(A-6 归 M3、A-8 由单测矩阵 + P2-8 对照覆盖,§4 补映射交代)。P3 落地:A-4/A-9 COUNT 断言加当日窗(跨日残留不再裸失败)、scan→handle→再 scan 幂等用例(防 status 过滤回归)、suitability/check 阻断响应补 G-08 两要素 advice/notice(PRD §6 阻断类口径)、MEMORY tests 行/FLOW 日期戳同步。留痕(P3-2):scan_all 幂等为读后写(无锁无约束),手动单发演示场景可接受;接定时批量/多操作员时以 run_locked 包住 check+insert。209 测试绿。
阶段 C · 对话线(依赖 Wave 0 的 T-01 JWT / T-03 输入防护 / T-07 LangGraph)
| # | 任务 | 产出 | 验证方式 | 依赖 |
|---|---|---|---|---|
| C0 | 阶段 C 开工前置:requirements 增补 pytest-asyncio(异步 Tool 单测需要) |
依赖落地 | pytest --version 正常 |
T-07 临近 |
| C1 | service/risk/chat_tools.py:alert_query / customer_context / suitability_check / aml_lookup 四个只读 Tool |
Tool 集 | 单测:各 Tool 输出结构 + alert_id 溯源 | B6、T-07 |
| C2 | agent_service.py 注册 risk 分支 StateGraph(intent → tool → respond)+ DeepSeek + 输出规范(alert_id、仅供参考标注) |
对话闭环 | A-6:"今天有多少待审预警" | C1、T-01、T-03 |
| C3 | A-6 验收 + 边界测试(诱导处置 → 拒绝并引导 API) | 验收记录 | 手测 | C2 |
里程碑与提交策略
| 里程碑 | 内容 | 提交 |
|---|---|---|
| M1 | 阶段 A 完成(suitability 可用) | 逐任务 commit + 打 tag risk-m1 |
| M2 | 阶段 B 完成(事件线全链路,验收 A-1 |
逐任务 commit + 打 tag risk-m2 |
| M3 | 阶段 C 完成(对话线验收 A-6) | 逐任务 commit + 打 tag risk-m3 |
| M4 | 收尾:MEMORY/TODO 状态复核与边界文字对齐 | docs: 风控模块验收记录 |
Commit 规范(自《03-团队分工》内联,该文档当前在仓库外):
格式: <type>: <描述>
type: feat 新功能 / fix 修复 / docs 文档 / refactor 重构 / test 测试 / chore 辅助
粒度: 每个任务至少一个 commit(A1、A2…各自独立可回溯);里程碑完成时打 tag
示例: feat: suitability 公共校验服务(SUIT-001~008)
- 每任务完成 = 代码 + 测试通过 + 验证方式打勾 + 即时更新
docs/memory/TODO.md勾选,方可进下一任务;M4 仅做最终复核 - 阶段 B 期间若 T-01/T-02 就绪,B7 的 lifespan 立即接审计中间件(T-02)
已知依赖风险
- T-05 未执行:A2/B8 前需本机灌库(
reset.ps1+ 共用底座 SQL,FLOW §0 ③④,一次性本机操作) - T-01 未做 → 假 AuthContext 过渡规范(B6 落地,零返工):
AuthContextPydantic 模型(actor_id / roles / customer_id)与get_auth_context()工厂签名在 A4 冻结- dev 模式(
app_env=development)从X-Debug-Role/X-Debug-Actor请求头构造 AuthContext;生产环境(app_env != development)启动时检测到 debug 依赖注册直接拒绝启动 - T-01 就绪后仅替换工厂内部为 JWT 解析,接口签名与调用方零改动,无双份维护
- B8 权限类用例(A-7/A-9)通过切换 debug 头执行,可完整验证权限矩阵
- Ollama 未启动:不影响风控模块(无 Embedding 依赖)