2026-09-06 15:09:13 +08:00
|
|
|
|
# 开发计划 · 风控监测 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 |
|
2026-09-06 15:37:42 +08:00
|
|
|
|
| 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 |
|
2026-09-06 17:04:38 +08:00
|
|
|
|
| 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 |
|
2026-09-06 17:47:30 +08:00
|
|
|
|
| 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 接入前统一** |
|
2026-09-06 15:09:13 +08:00
|
|
|
|
| B5 | `app/gateway/`(trade_gateway + gateway_repository 仅 INSERT core_trade)+ `api/simulate.py` 薄路由 | 网关 | 集成:convert 400、阻断不落 trade | A4、B4 |
|
2026-09-06 15:37:42 +08:00
|
|
|
|
| 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) |
|
2026-09-06 19:21:15 +08:00
|
|
|
|
| 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 |
|
2026-09-06 23:06:32 +08:00
|
|
|
|
|
|
|
|
|
|
**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-1~A-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=`profile:l3:{customer_id}` 与降级路径);P3-2 locks 降级文案中性化顺手改** | 测试套件 + fixture | `pytest` 全绿 | B7 |
|
2026-09-06 15:09:13 +08:00
|
|
|
|
| B9a | 演示/运维脚本开发:`scripts/demo/subscribe_alerts.py`(订阅演示)+ `scripts/demo/rebuild_alerts.py`(按 trade_id 幂等重放补偿) | 2 个脚本 | 手工执行验证 | B2、B4(可与 B5~B8 并行) |
|
2026-09-06 23:52:59 +08:00
|
|
|
|
|
2026-09-07 00:43:25 +08:00
|
|
|
|
**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 尾注)。
|
|
|
|
|
|
|
2026-09-07 01:02:12 +08:00
|
|
|
|
**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 路径真库复验后现场清理。
|
|
|
|
|
|
|
2026-09-06 23:52:59 +08:00
|
|
|
|
**B8 复审(独立 AI 评审 · 2026-09-06):有条件通过 → 已闭环。** 开发计划 B8 行 a/b/c + DDL 单一事实源/localtime + TRD-TEST- 注入 + A-1~A-5/A-7/A-9/trace/审计 JSON 全部达成,196 绿实跑,B7 复审 P2-1/P3-2 落地。**P1-1 `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-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 复审观察③)⑤ 生产/演示机 `app_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 |
|
2026-09-06 15:09:13 +08:00
|
|
|
|
|
2026-09-07 01:36:44 +08:00
|
|
|
|
**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-1~A-5/A-7~A-9 验收),commit 打 tag risk-m2(M1 时点未打 tag,留痕)**。
|
|
|
|
|
|
|
2026-09-07 01:59:15 +08:00
|
|
|
|
**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 测试绿。
|
|
|
|
|
|
|
2026-09-06 15:09:13 +08:00
|
|
|
|
## 阶段 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~A-5、A-7~A-9**) | 逐任务 commit + 打 tag `risk-m2` |
|
|
|
|
|
|
| M3 | 阶段 C 完成(对话线验收 A-6) | 逐任务 commit + 打 tag `risk-m3` |
|
|
|
|
|
|
| M4 | 收尾:MEMORY/TODO 状态复核与边界文字对齐 | `docs: 风控模块验收记录` |
|
|
|
|
|
|
|
|
|
|
|
|
**Commit 规范**(自《03-团队分工》内联,该文档当前在仓库外):
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
格式: <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)
|
|
|
|
|
|
|
|
|
|
|
|
## 已知依赖风险
|
|
|
|
|
|
|
|
|
|
|
|
1. **T-05 未执行**:A2/B8 前需本机灌库(`reset.ps1` + 共用底座 SQL,FLOW §0 ③④,一次性本机操作)
|
|
|
|
|
|
2. **T-01 未做 → 假 AuthContext 过渡规范**(B6 落地,零返工):
|
|
|
|
|
|
- `AuthContext` Pydantic 模型(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 头执行,可完整验证权限矩阵
|
|
|
|
|
|
3. **Ollama 未启动**:不影响风控模块(无 Embedding 依赖)
|