338 lines
27 KiB
Markdown
338 lines
27 KiB
Markdown
# PRD · 风控监测 Agent 模块
|
||||
|
|
|
|||
|
|
> 版本:v1.0(**已冻结** · 2026-09-06 用户确认,风控模块开发唯一依据)
|
|||
|
|
> 日期:2026-09-06
|
|||
|
|
> 负责人:E(风控/记忆/测试)· 分支 `feature/risk`
|
|||
|
|
> 上游依据:`docs/需求拆解/Agent风险与合规约束汇总.md` §5、`docs/memory/REQUIREMENTS.md` Wave 2、`docs/memory/FRAMEWORK.md`、`docs/项目框架设计/表设计/`(表结构 · Redis key)、规则冻结版 `docs/PRD/附-风控规则表.md`
|
|||
|
|
> 状态流转:草稿 → AI 评审 → 修订 v0.2 → 复审 → 修订 v0.3 → **用户确认冻结 v1.0**
|
|||
|
|
> 修订记录:v0.2 修复首轮评审 P0×2、P1×9、P2×10;v0.3 修复复审 P1×3 与 P2×3;v1.0 用户确认三个新增项(risk_aml_list 表、STAFF-90001 演示账号、scripts/demo/prepare_risk_demo.sql),§13 文档联动修正已执行
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 背景与目标
|
|||
|
|
|
|||
|
|
风控监测 Agent 是四 Agent 体系中**唯一可阻断交易请求**的模块(其余一律只监测不拦截)。当前后端仅有脚手架,本 PRD 定义风控模块从零到可演示的完整需求。
|
|||
|
|
|
|||
|
|
**目标(一句话)**:交易事件进来能实时预警,适当性不匹配能在交易前拦住,AML 命中能紧急通知人工,风控专员能对话查台账——全程留痕、全部待人工审核。
|
|||
|
|
|
|||
|
|
**本期不做**:自动冻户、自动上报监管、自动改正式风险等级 C1~C5、限制客户交易次数/金额、R-05 完整评分模型(只预留)、真实支付/TA 清算对接、"仅提示不阻断"的放行路径(R-02 一期所有不匹配一律阻断,放行+提示留二期)、AML 开户/信息变更触发(模拟环境无开户流程,仅预留事件入口定义,见 FR-5)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 已拍板的决策(不重议)
|
|||
|
|
|
|||
|
|
| # | 决策 | 内容 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| D1 | Agent 划分 | 以主分支新四 Agent 为准(客户财富/代理人助手/数据分析/风控监测);本地旧文档中"客服/投顾/业务操作"命名废弃 |
|
|||
|
|
| D2 | 推荐边界(全局修订) | Agent **可以推荐产品并说明理由**,但:①推荐前必须过适当性校验(SUIT-007);②输出附免责声明;③标注"需经持证投顾审核";④**禁止具体操作指令**("现在买入 X 万元""我帮您下单"类表述一律拦截) |
|
|||
|
|
| D3 | 冻结边界 | 任何 Agent 不得自动冻结账户;AML 命中只做"最高级预警 + 多渠道通知 + 标记待复核",处置由**风控专员**人工完成,合规官**知悉**(见 FR-5 通知机制) |
|
|||
|
|
| D4 | 范围 | R-01、R-02、R-03、R-04 全做;R-05 只预留 L3 写入接口,评分模型后置 |
|
|||
|
|
| D5 | 事件来源 | **方案 A:模拟交易网关**(扮演外部 Core 交易系统,独立于 Agent 分层)——交易请求先过 R-02 校验(阻断点),通过后写 `core_trade` 并触发规则引擎 |
|
|||
|
|
| D6 | 模块形态 | 两条线并存:事件驱动线(无对话)+ 对话线(风控专员 chat) |
|
|||
|
|
|
|||
|
|
**D2 的文档联动修正**(PRD 冻结后执行,见 §13)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 系统上下文
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
┌────────────────────────────────────────────┐
|
|||
|
|
│ 客户端 / 前端 │
|
|||
|
|
└──────┬──────────────────────┬──────────────┘
|
|||
|
|
│ 交易请求 │ 风控专员对话
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────────────────┐ ┌─────────────────────┐
|
|||
|
|
│ 模拟交易网关(外部替身) │ │ api/chat (risk) │
|
|||
|
|
│ app/api/simulate.py │ │ X-Agent-Type=risk │
|
|||
|
|
│ → app/gateway/* │ └──────────┬──────────┘
|
|||
|
|
└──────┬──────────────┘ │ LangGraph
|
|||
|
|
│ ①R-02 适当性校验 ▼
|
|||
|
|
│ ├─不匹配→阻断(不落 trade) service/agent_service
|
|||
|
|
│ └─匹配→②INSERT core_trade │
|
|||
|
|
▼ │
|
|||
|
|
┌─────────────────────┐ │
|
|||
|
|
│ 风控规则引擎 │◄────────────────┘
|
|||
|
|
│ service/risk/* │ R-01/R-03/R-04
|
|||
|
|
└──────┬──────────────┘
|
|||
|
|
│ ③写预警单 risk_alert(单事件聚合出单)
|
|||
|
|
│ ④写 risk_suitability_log
|
|||
|
|
│ ⑤写 L3(取最高档合并)
|
|||
|
|
│ ⑥PUBLISH risk:pub:alert
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────┐ ┌──────────────┐
|
|||
|
|
│ 人工处置 API │ │ Redis 通知 │
|
|||
|
|
│ 风控专员改状态 │ │ risk:pub:alert│
|
|||
|
|
└─────────────────────┘ └──────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**角色澄清(红线自证)**:模拟交易网关是**外部 Core 交易系统的替身**(生产环境由真实交易系统回调替代),它写 `core_trade` 是扮演 L0 系统的角色,**不属于 Agent 写 Core**;Agent 侧代码依旧只读 `jinrong_core`。
|
|||
|
|
|
|||
|
|
**分层落地(对照 FRAMEWORK.md)**:网关独立为 `app/gateway/` 包(`trade_gateway.py` 业务 + `gateway_repository.py` **仅 INSERT core_trade**),不在 api/service/tool/repository 四层之内,声明为"模拟外部系统模块";`app/api/simulate.py` 仍为薄路由。§13 联动修正 FRAMEWORK.md 增补该分层例外说明。规则引擎、校验服务、预警处置全部遵守"repository 不写 Core"约束。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 功能需求
|
|||
|
|
|
|||
|
|
### FR-1 模拟交易网关(D5)
|
|||
|
|
|
|||
|
|
- `POST /api/simulate/trade`:入参 `customer_id, product_id, trade_type, amount`;服务端生成 `trade_id` + `trace_id`
|
|||
|
|
- `trade_type` 一期仅接受 `subscribe/redeem`;**`convert` 显式拒绝(400,文案"转换交易暂不支持,请分别发起申购/赎回")**;规则引擎入口对未知 trade_type 抛错兜底(真实交易系统接入时再定义 convert 拆算规则)
|
|||
|
|
- 流程:
|
|||
|
|
1. 调用适当性校验服务(FR-2);**不匹配 → 返回阻断响应(HTTP 200,`blocked=true` + 阻断文案),交易不落 `core_trade`**,但落 `risk_suitability_log`(`is_blocked=1`)+ 生成 suitability 预警单(同客户+产品+日去重,见 FR-4)
|
|||
|
|
2. 匹配 → INSERT `core_trade`(`trade_status='confirmed'`)→ 同步调用规则引擎处理本笔交易(FR-3)→ 返回 `blocked=false` + `trade_id`
|
|||
|
|
- 阻断文案要素(G-08 + 合规 §5.3):**不匹配原因(客户等级 vs 产品等级)+ 引导"请联系持证投资顾问" + "本次请求已记录"声明**
|
|||
|
|
- 鉴权:依赖 T-01 JWT。一期网关接受两类身份:①`roles` 含 `risk_demo` 的演示账号;②客户本人(`customer_id == JWT subject`,客户 JWT 方案由 T-01 提供,未就绪前仅接受 risk_demo)。演示账号方案见 §10
|
|||
|
|
- 阻断/放行的全量输入输出写 `audit_log`(`agent_type='platform'`)
|
|||
|
|
|
|||
|
|
### FR-2 适当性校验服务(R-02 · 全系统唯一阻断点)
|
|||
|
|
|
|||
|
|
- 公共函数 `service/suitability.py :: suitability_check(customer_id, product_id) -> SuitabilityResult`
|
|||
|
|
- 数据源(全只读,经扩展后的 `core_ro`):`core_customer_risk.risk_code`(C1~C5)、`core_customer.age`、`core_product.min_risk_code`(R1~R5)
|
|||
|
|
- 规则(冻结,代码硬编码,详见附表):
|
|||
|
|
- SUIT-001~005:客户 C 级 ≥ 产品 R 级方可购买(C1↔仅R1 … C5↔R1-R5)
|
|||
|
|
- SUIT-006:年龄 ≥70 岁的客户按最高 C3 处理(即使测评得 C4/C5);**`age IS NULL` 时跳过本条,`reasons` 标注"年龄缺失,建议人工复核"**(不单独构成阻断)
|
|||
|
|
- SUIT-008:风险测评有效期默认 **365 天**(`.env: RISK_ASSESSMENT_VALID_DAYS` 可配),`evaluated_at` 超期 → **等级矩阵照常计算(`is_matched` 不受影响),但最终 `blocked=true`**,阻断原因"风险测评已过期,请重新测评"
|
|||
|
|
- 返回结构语义(两字段不冗余):`is_matched` = 纯等级矩阵结果;`blocked` = 最终是否阻断(= `NOT is_matched` **或** 测评过期);`reasons[]` 列明每条规则的判定与封顶说明(如"C4 因年龄≥70 按 C3 处理")
|
|||
|
|
- `risk_suitability_log.customer_risk_level` 记录**原测评等级**(SUIT-006 封顶只进 `reasons`,不篡改落库值)
|
|||
|
|
- 每次校验落 `risk_suitability_log`(含 `trace_id`、`profile_l1_version` 可空)
|
|||
|
|
- **数据归属校验(G-01)**:customer → 仅本人;advisor → 经 `customer_advisor_rel` 归属校验;risk_officer → 全部;越权 403 + audit
|
|||
|
|
- **复用方**:模拟交易网关(FR-1)、客户财富/代理人助手的推荐前校验(D2)、风控对话线(FR-6)
|
|||
|
|
|
|||
|
|
### FR-3 风控规则引擎(R-01 / R-04,事件驱动)
|
|||
|
|
|
|||
|
|
- 入口:网关交易落库后**进程内同步调用**(不用消息队列保证阻断演示可靠性);每个事件处理 <100ms
|
|||
|
|
- 规则清单(阈值支持 `.env` 覆盖,默认值冻结):
|
|||
|
|
|
|||
|
|
| 规则 ID | 名称 | 触发条件 | 优先级 | 出单类型 |
|
|||
|
|
| --- | --- | --- | --- | --- |
|
|||
|
|
| RISK-001 | 单笔大额 | `amount ≥ 500,000` | P0 | large_amount |
|
|||
|
|
| RISK-002 | 单日累计大额 | 同一客户当日 subscribe+redeem 合计 `≥ 500,000`(含本笔) | P0 | large_amount |
|
|||
|
|
| RISK-003 | 频繁交易 | 同一客户同一产品当日申赎合计 `≥ 3 笔` | P1 | freq_trade |
|
|||
|
|
| RISK-004 | 接近阈值试探 | 5 分钟内 `≥3 笔` 且每笔 `≥400,000` | P1 | pattern |
|
|||
|
|
| RISK-005 | 先小后大 | 当日**时间序上首次大额之前**已存在 `≥3 笔 ≤10,000`(不要求连续、中间可穿插其他金额) | P0 | pattern |
|
|||
|
|
|
|||
|
|
- **累计口径统一为 `core_trade` 当日流水**;`core_cash_flow` 不参与触发计算(仅作预警单 payload 上下文展示,避免与 trade 双计)
|
|||
|
|
- "当日"口径:**服务器本地时区自然日(YYYYMMDD)**,以 `traded_at` 为准
|
|||
|
|
- 风险评分一期为**静态映射**:aml=95、suitability=90、pattern=80、large_amount=70、freq_trade=50(R-05 时替换为动态评分)
|
|||
|
|
- **单事件聚合出单**:同一笔交易命中的全部规则合并进**一张**预警单(`triggered_rules` JSON 数组、`risk_score` 取命中规则映射的最高值、`alert_type` 取分值最高规则对应的类型);详见 FR-4 去重与聚合
|
|||
|
|
- 触发数据源:`core_trade` 当日流水 + `core_customer`/`core_customer_risk`(上下文)
|
|||
|
|
|
|||
|
|
### FR-4 预警单生命周期
|
|||
|
|
|
|||
|
|
- 生成:`risk_alert`,`status='pending_review'`,**`triggered_rules` 字段写规则 ID 数组**;`payload` 含脱敏交易明细、客户画像摘要(L0 事实 + 可得的 L1/L2 只读)、近 30 天交易统计、`core_cash_flow` 上下文(如有)
|
|||
|
|
- **聚合与去重**:
|
|||
|
|
- **事件类(large_amount/freq_trade/pattern)视作同一类**:同客户同自然日仅**一张**事件类 pending 单——`alert_type` 随当前命中分值最高的规则**动态更新**,新事件命中的规则一律追加进 `payload.events[]` 与 `triggered_rules`、`risk_score` 取 max(不按类型分单,避免类型漂移产生多单);无 pending 单则新建
|
|||
|
|
- suitability:同客户+产品+自然日仅一张 pending 单(防反复重试刷单),同上追加
|
|||
|
|
- aml:**独立出单不聚合**(最高级,单事件单张)
|
|||
|
|
- Redis 去重键 `risk:dedup:{customer_id}:{rule_id}:{date}` 仅作规则级防重入辅助(24h TTL)
|
|||
|
|
- 通知:`PUBLISH risk:pub:alert`,payload=`{alert_id, alert_type, customer_id_mask, risk_score, trace_id, notify_role}`;以 PRD 版本为准,§13 联动修正 02-redis-keys.md §2.4
|
|||
|
|
- 人工处置(唯一允许改状态的角色 = 风控专员 `risk_officer`,经 JWT):
|
|||
|
|
- `GET /api/risk/alerts`:参数 `status, alert_type, customer_id, start_date, end_date, page, page_size`
|
|||
|
|
- `POST /api/risk/alerts/{alert_id}/handle`:`{handler_result: confirmed_normal|confirmed_suspicious|reported, handler_comment?}`;写 `handler_id/handled_at`,状态机只允许 `pending_review → 其余三种`,禁止跳改已处置单
|
|||
|
|
- 处置动作全量写 `audit_log`(`agent_type='risk'`)
|
|||
|
|
|
|||
|
|
### FR-5 AML 名单监测(R-03)
|
|||
|
|
|
|||
|
|
- 新增表 `risk_aml_list`(agent 库,风控专用,DDL 见 §6.2)
|
|||
|
|
- 种子数据:≥8 条假名单,其中**故意包含 1 条与种子客户 `display_name` 同名的记录**用于演示命中
|
|||
|
|
- 触发时机(P0 做前两个;开户/信息变更触发因模拟环境无开户流程**本期不做**,仅在规则引擎预留 `on_customer_created/on_customer_updated` 事件入口定义):
|
|||
|
|
1. **交易事件触发**:规则引擎处理每笔交易时,比对该客户姓名(同步,<100ms)
|
|||
|
|
2. **手动全量扫描**:`POST /api/risk/aml/scan`(模拟每日批量;P1 起挂定时任务)
|
|||
|
|
- **匹配算法(一期降级)**:仅 `display_name` 归一化(去空格、大小写折叠)+ 相似度 ≥ `match_threshold`(默认 0.85);表结构保留 `id_no`/`bank_card_no` 字段,**证件/银行卡匹配待 Core 提供证件数据后启用**(当前 `core_customer` 无证件字段,不修改 Core 表结构)
|
|||
|
|
- 命中动作:生成 `alert_type='aml'` 独立预警单(risk_score=95,每事件一张)+ Redis 紧急推送(`notify_role=["risk_officer","compliance"]`)+ **标记客户 L3 `monitor_tier='high'` + `monitor_tags` 追加 `"aml_hit_pending_review"`**;**不冻结、不自动上报**
|
|||
|
|
- **合规官知悉路径**:①`risk:pub:alert` payload 带 `notify_role`,风控工作台按角色过滤展示;②compliance 账号(STAFF-40001/40002)登录预警列表可见 aml 类型预警单(只读)
|
|||
|
|
- 命中记录入 `risk_alert.payload`(含名单类型、匹配字段、相似度、名单版本),全量审计
|
|||
|
|
|
|||
|
|
### FR-6 对话线(风控专员 chat)
|
|||
|
|
|
|||
|
|
- 入口:`POST /api/chat`,`X-Agent-Type=risk`,JWT 角色 `risk_officer`;LangGraph StateGraph + DeepSeek(依赖 T-01/T-03/T-07)
|
|||
|
|
- Tool 节点(只读 + 专用查询):
|
|||
|
|
1. 查预警台账(按状态/类型/客户/日期统计,含"今日新增/待审数",复用 FR-4 查询参数)
|
|||
|
|
2. 查客户风险上下文:L0(只读 core_ro)+ L1/L2(只读画像表)+ L3(自有)+ 近 30 天交易摘要
|
|||
|
|
3. 发起适当性校验并解读结果(调 FR-2)
|
|||
|
|
4. AML 名单查询(某客户是否命中过)
|
|||
|
|
- **对话线只读不处置**:改预警状态必须走 FR-4 结构化 API,对话中输出"处置建议"但不执行——防止 LLM 误操作,审计口径清晰
|
|||
|
|
- 输出规范:引用预警单必须带 `alert_id`;两级文案——**面向专员的对话输出不含 G-08 客户免责声明,但评分/分层必须标注"仅供参考,不自动决策"**;系统级预警 `disclaimer`("本预警由系统自动生成,最终判定需经风控专员人工审核")固定出现在预警 API 响应体中,两者是不同文案、各自适用
|
|||
|
|
|
|||
|
|
### FR-7 L3 画像写入(R-05 预留接口)
|
|||
|
|
|
|||
|
|
- 本期仅实现最小写入:预警单生成时 UPSERT `customer_profile_l3`
|
|||
|
|
- **合并规则(防降级)**:`monitor_tier` 取**最高档**(`new_tier = max(existing_tier, mapped_tier)`,normal < watch < high;映射:aml→high、pattern→watch、large_amount→watch、freq_trade→watch、suitability→normal);`monitor_tags` **追加合并**不覆盖;`computed_at` 写当前时间(字段 NOT NULL 必须显式赋值);`last_alert_id` 联动
|
|||
|
|
- `risk_score` 动态评分、定期批量重算、监测报告:**本期不做**,接口签名预留 `service/risk/scoring.py :: recompute_customer_score(customer_id)`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 数据设计
|
|||
|
|
|
|||
|
|
### 5.1 读写权限总表
|
|||
|
|
|
|||
|
|
| 表 / Key | 操作 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `jinrong_core.core_trade` | **INSERT(仅网关 gateway_repository)** | 网关扮演外部交易系统 |
|
|||
|
|
| `jinrong_core.core_*` 其余 | SELECT | 经 `CoreReadOnlyRepository`(需扩展方法),Agent 只读 |
|
|||
|
|
| `jinrong_agent.risk_alert` | INSERT + UPDATE(状态机) | 风控写 |
|
|||
|
|
| `jinrong_agent.risk_suitability_log` | INSERT | 每次校验落一条 |
|
|||
|
|
| `jinrong_agent.customer_profile_l3` | UPSERT(最高档合并) | 风控独写 |
|
|||
|
|
| `jinrong_agent.risk_aml_list` | SELECT + 种子脚本维护 | 风控读,admin 维护 |
|
|||
|
|
| `jinrong_agent.customer_profile_l1/l2` | SELECT | 只读,辅助判断 |
|
|||
|
|
| `jinrong_agent.audit_log` | INSERT | 只增 |
|
|||
|
|
| Redis `risk:pub:alert` | PUBLISH | 预警通知广播(payload 含 `notify_role`) |
|
|||
|
|
| Redis `risk:dedup:{customer_id}:{rule_id}:{date}` | SET EX 24h | 规则级防重复预警 |
|
|||
|
|
| Redis `profile:l3:{customer_id}` | SET EX 5m | L3 热缓存,MySQL 更新时 DEL |
|
|||
|
|
|
|||
|
|
### 5.2 新增表 DDL(需用户确认后并入 `02-mysql-agent专用.sql`)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE risk_aml_list (
|
|||
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|||
|
|
list_id VARCHAR(64) NOT NULL,
|
|||
|
|
list_type ENUM('sanction','terror','pep') NOT NULL,
|
|||
|
|
full_name VARCHAR(128) NOT NULL COMMENT '与 core_customer.display_name 同为脱敏展示名口径',
|
|||
|
|
id_no VARCHAR(32) NULL COMMENT '预留:待 Core 提供证件数据后启用匹配',
|
|||
|
|
bank_card_no VARCHAR(32) NULL COMMENT '预留:同上',
|
|||
|
|
match_threshold DECIMAL(3,2) NOT NULL DEFAULT 0.85,
|
|||
|
|
source VARCHAR(64) NOT NULL,
|
|||
|
|
list_version VARCHAR(16) NOT NULL,
|
|||
|
|
effective_date DATE NOT NULL,
|
|||
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|||
|
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|||
|
|
UNIQUE KEY uk_list_id (list_id),
|
|||
|
|
KEY idx_name (full_name),
|
|||
|
|
KEY idx_active (is_active)
|
|||
|
|
) ENGINE=InnoDB COMMENT='【风控专用】AML 名单本地镜像';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
其余复用底座已有表,**不修改任何现有表结构**(含 Core)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. API 清单汇总
|
|||
|
|
|
|||
|
|
| 方法 | 路径 | 角色 | 说明 |
|
|||
|
|
| --- | --- | --- | --- |
|
|||
|
|
| POST | `/api/simulate/trade` | risk_demo 演示账号 / customer 本人 | 模拟交易网关(R-02 阻断点) |
|
|||
|
|
| GET | `/api/risk/alerts` | risk_officer 全量;**compliance 只读且服务端强制 `alert_type=aml`** | 预警台账分页查询 |
|
|||
|
|
| POST | `/api/risk/alerts/{id}/handle` | risk_officer | 人工处置(唯一改状态入口) |
|
|||
|
|
| POST | `/api/risk/suitability/check` | customer(本人)/advisor(名下)/risk_officer | 适当性校验(推荐前复用;用 POST 保幂等语义——每次调用落 suitability_log) |
|
|||
|
|
| POST | `/api/risk/aml/scan` | risk_officer | 手动全量 AML 扫描 |
|
|||
|
|
| POST | `/api/chat`(X-Agent-Type=risk) | risk_officer | 对话线 |
|
|||
|
|
|
|||
|
|
统一响应格式复用 `utils/response.py`;预警类 API 响应体含固定 `disclaimer`:"本预警由系统自动生成,最终判定需经风控专员人工审核。"(阻断类 API 响应体含 G-08 客户免责声明。)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 合规与审计(红线)
|
|||
|
|
|
|||
|
|
1. **唯一阻断点**:全系统仅 R-02 交易前校验可阻断**请求**;其余场景只预警不拦截
|
|||
|
|
2. **不自动冻户、不自动上报监管、不自动改 C1~C5**(L3 只是监测标签)
|
|||
|
|
3. **审计字段对齐 `audit_log` 实际表结构**:`trace_id, created_at, agent_type, rule_id, customer_id, event_type, input_summary(脱敏), decision, risk_score, handler_id, handler_result, handler_comment`——任何风控判定(校验/预警/阻断/通知)全落 `audit_log`,只 INSERT;**`customer_id` 为系统内部代理键,不脱敏**(保证按客户检索审计的索引有效性)
|
|||
|
|
4. **脱敏规则**:权威定义见附表 `docs/PRD/附-风控规则表.md` §4——身份证/手机号前 3 后 4、姓名留姓氏、银行卡留后 4;`customer_id` 内部键不脱敏;姓名/证件/手机号在 `input_summary`、预警 payload、通知、对话输出中脱敏;LLM 上下文传入前必须已脱敏
|
|||
|
|
5. **trace_id 贯通**:网关生成 → suitability_log → risk_alert → audit_log → Pub/Sub 消息,同一链路可还原
|
|||
|
|
6. **处置人工化**:预警状态仅 `risk_officer` 经结构化 API 变更;compliance 只读;对话线不处置
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 验收标准(对照 REQUIREMENTS Wave 2 + 评分项)
|
|||
|
|
|
|||
|
|
> 前置:先执行 §10 演示数据准备。
|
|||
|
|
|
|||
|
|
| # | 演示场景 | 预期结果 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| A-1 | C1 客户(测评有效期内,CUST-1001)申购 R4 产品(PROD-161725 科技成长主题) | 响应 `blocked=true`,`block_reason` 命中 SUIT-001(C1 仅可购 R1);`core_trade` 无记录;`risk_suitability_log.is_blocked=1`;suitability 预警单生成;audit 可查 |
|
|||
|
|
| A-2 | CUST-4001(70 岁、测评 C5、已刷新有效)购买 R4 产品(PROD-161725) | `blocked=true`,`reasons[]` 含 SUIT-006 封顶说明("C5 因年龄≥70 按 C3 处理")与 SUIT-003 不匹配,**无 SUIT-008 干扰**;其余同 A-1 |
|
|||
|
|
| A-3 | CUST-3001(C3、已刷新有效)单笔申购 50 万 PROD-510300 沪深300指数(R3,等级匹配) | `core_trade` 落库;事件类预警单 `pending_review`(`triggered_rules` 含 RISK-001+RISK-002,`risk_score=70`);Redis 收到 `risk:pub:alert` 推送 |
|
|||
|
|
| A-4 | 同产品当日第 3 笔申赎 | freq_trade 规则并入当日预警单(不另开新单);当日去重不重复报 |
|
|||
|
|
| A-5 | AML 名单命中客户交易 | aml 独立预警单(score=95)+ L3 `monitor_tier='high'` + 紧急推送(`notify_role` 含 compliance)+ compliance 账号可见该单;**账户未被冻结** |
|
|||
|
|
| A-6 | 风控专员对话 | "今天有多少待审预警" 返回正确统计 + alert_id 可溯源 |
|
|||
|
|
| A-7 | 人工处置 | 状态 `pending_review→confirmed_suspicious`;compliance 调 `/handle` → 403(其调 GET 仅返回 aml 单);audit 留痕 |
|
|||
|
|
| A-8 | `suitability_check` 单元级验收 | 脚本/单测直接调用,断言 C1+R4 → `is_matched=false, blocked=true` 且返回结构完整;"不匹配拒绝生成推荐语"的端到端验收归客户/代理人 Agent 各自 PRD(见 §9 依赖) |
|
|||
|
|
| A-9 | 越权 | customer 查他人 suitability、advisor 查非名下客户 → 403 + audit |
|
|||
|
|
|
|||
|
|
**通用验收**:所有预警单初始 `pending_review`;审计链路任取一条 `trace_id` 可还原全流程;越权 403。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 依赖与前置
|
|||
|
|
|
|||
|
|
| 依赖 | 状态 | 影响 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| T-01 JWT/RBAC 中间件 | 未做 | 所有 /api/risk 接口角色校验 + 客户本人 JWT + 最简签发端点 |
|
|||
|
|
| T-02 audit 中间件 + trace_id | 未做 | §7 审计要求 |
|
|||
|
|
| T-03 输入防护(F-03) | 未做 | 对话线接收专员输入的全员底线 |
|
|||
|
|
| T-04 core_ro 扩展 | **需扩展** | 现仅有部分方法;需补 `get_customer_risk`(含 evaluated_at)、当日交易统计、近 N 天流水等查询(含 SUIT-008 所需 `evaluated_at`) |
|
|||
|
|
| T-05 Core 模拟库灌库 | 脚本已有未执行 | 演示数据 |
|
|||
|
|
| T-07 LangGraph + DeepSeek | 未做 | 仅对话线 FR-6 依赖;事件驱动线不依赖 |
|
|||
|
|
| 共用底座 SQL 灌库 | 待执行 | risk_alert 等表 |
|
|||
|
|
| 客户/代理人 Agent | Wave 1/3 | A-8 端到端部分归各自 PRD;本期只交付 `suitability_check` 公共函数 + 单测 |
|
|||
|
|
|
|||
|
|
**排期策略**:事件驱动线(FR-1~FR-5)不依赖 T-07,可先行;对话线(FR-6)等 Wave 0 底座就绪后接入。具体切任务见后续开发计划文档。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 演示账号与演示数据准备
|
|||
|
|
|
|||
|
|
### 10.1 演示账号(需确认后并入 seed)
|
|||
|
|
|
|||
|
|
| 项 | 值 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 账号 | `STAFF-90001`(display_name=风控演示账号) | `staff_type='risk_officer'`、`roles=["risk_officer","risk_demo"]` |
|
|||
|
|
| 网关鉴权 | roles 含 `risk_demo` 即可发起任意客户的模拟交易 | 演示期口径;生产由真实交易系统回调替代,无此角色 |
|
|||
|
|
| AML 演示名单 | 种子含 1 条与某种子客户 `display_name` 同名的 sanction 记录 | 演示命中 |
|
|||
|
|
|
|||
|
|
### 10.2 测评有效期演示数据(修复 SUIT-008 与种子冲突)
|
|||
|
|
|
|||
|
|
种子库 28 位客户的 `evaluated_at` 均已过期(最晚 2025-09-01),若不处理,**所有交易都会被 SUIT-008 阻断,A-3/A-4/A-5 无法走通**。演示前执行 `scripts/demo/prepare_risk_demo.sql`:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 演示客户测评日期刷新至演示日前 90 天内(仅演示用例涉及的客户)
|
|||
|
|
UPDATE jinrong_core.core_customer_risk SET evaluated_at = CURDATE() - INTERVAL 90 DAY
|
|||
|
|
WHERE customer_id IN ('CUST-1001','CUST-1002','CUST-1003','CUST-3001','CUST-4001','CUST-9527');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 该脚本与 `reset.ps1` 分离(reset 重建全库后需重跑),并在 FLOW.md bootstrap 补一步
|
|||
|
|
- 同时保留 **1 位测评过期客户**(CUST-1004,C4,不刷新)用于演示 SUIT-008 阻断路径:"测评过期 → 等级匹配仍阻断 → 提示重新测评"
|
|||
|
|
- A-1/A-2/A-3 的断言均写明命中的具体规则编号与 `block_reason` 内容,避免规则叠加时断言含糊
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 性能与非功能
|
|||
|
|
|
|||
|
|
- 规则引擎单事件处理 <100ms(含 AML 比对)
|
|||
|
|
- 适当性校验 P95 <300ms(两次索引查询)
|
|||
|
|
- 预警通知从交易落库到 Pub/Sub 推送 <1s(演示用"实时感")
|
|||
|
|
- AML 全量扫描(28 客户 × 8 名单)<3s
|
|||
|
|
- 所有金额用 `Decimal`,禁止 float
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. 边界与不做清单(复述强约束)
|
|||
|
|
|
|||
|
|
- 不自动冻结账户、不自动上报监管、不自动调整正式风险等级、不代替客户重测
|
|||
|
|
- 不拦截已发生交易(只拦请求)
|
|||
|
|
- 不限制客户交易次数/金额
|
|||
|
|
- 不生成投资建议/收益承诺
|
|||
|
|
- 不做"仅提示不阻断"的放行路径(R-02 一期全部阻断)
|
|||
|
|
- 不做 AML 开户/信息变更触发(无开户流程,仅预留事件入口)
|
|||
|
|
- 数据分析 Agent 只能统计预警台账,不能处置
|
|||
|
|
- 不做:真实支付对接、TA 清算、毫秒级行情、多租户、convert 交易类型
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. 文档联动修正(PRD 冻结后执行)
|
|||
|
|
|
|||
|
|
| 文档 | 修正内容 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `docs/需求拆解/Agent风险与合规约束汇总.md` | "产品推荐:禁止"→"允许:须过 R-02 校验 + 免责声明 + 标注需持证审核;禁止具体操作指令"(§1 G-08 附近及各 Agent 边界表) |
|
|||
|
|
| `docs/PRD/附-风控规则表.md`(已入库) | 后续仅此表维护规则;原本地《02-业务规则表》停止维护(本地工作区文件可在冻结后删除) |
|
|||
|
|
| 本地《03-团队分工》 | Git 分支策略对齐实际仓库(main + feature/*,无 develop) |
|
|||
|
|
| `docs/memory/FRAMEWORK.md` | §3 分层增补例外说明:`app/gateway/` 为模拟外部系统模块,仅 `gateway_repository` 可 INSERT `core_trade`;§3 脚本清单增补 `scripts/demo/`(演示数据准备)与 `scripts/agent/`(agent 库种子:risk_aml_list 名单、演示交易数据) |
|
|||
|
|
| `docs/项目框架设计/表设计/02-redis-keys.md` | §2.4 `risk:pub:alert` payload 口径对齐本 PRD(`{alert_id, alert_type, customer_id_mask, risk_score, trace_id, notify_role}`) |
|
|||
|
|
| `docs/项目框架设计/表设计/02-mysql-agent专用.sql` | 追加 `risk_aml_list`(经用户确认) |
|
|||
|
|
| `scripts/core/02-seed-base.sql` | 追加演示账号 `STAFF-90001`(staff_type='risk_officer',roles=["risk_officer","risk_demo"]) |
|
|||
|
|
| `docs/memory/REQUIREMENTS.md` / `TODO.md` / `FLOW.md` | Wave 2 增加 T-30~T-32 细化任务与 PRD 链接;bootstrap 补演示数据准备步骤(`scripts/demo/prepare_risk_demo.sql`) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*复审通过并经用户确认后冻结为 v1.0,作为风控模块开发唯一依据。*
|