Files
group_xinghuo_jinrong/docs/PRD/PRD-风控监测Agent.md

46 KiB
Raw Permalink Blame History

PRD · 风控监测 Agent 模块

版本:v1.1(已冻结部分 v1.0 原文不动 · 2026-09-07 追加 FR-8/FR-9/FR-10 并入,追加内容经独立 AI 评审→修订→复审通过)
日期:2026-09-06(v1.0 冻结)· 2026-09-07(v1.1 追加并入)
负责人:E(风控/记忆/测试)· 分支 risk-control-agent(旧称 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 → 需求追加 v1.1 独立评审不通过(P0×4)→ 修订 v1.2 → 复审通过 → 并入 v1.1
修订记录: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 文档联动修正已执行;v1.1 追加 FR-8(RISK-006 集中度)/ FR-9(RISK-007 时效升级)/ FR-10(RISK-008 代理人行为链)与 A-10/A-11/A-12 验收,落库映射按真实 DDL 定案(alert_type 复用 pattern + payload.alert_subtype、status 不扩枚举、payload.actor_id 承载代理人)、新增 risk_manager 上级角色(用户拍板 2026-09-07,roles JSON 承载不改表)


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. 系统上下文

                    ┌────────────────────────────────────────────┐
                    │                客户端 / 前端                │
                    └──────┬──────────────────────┬──────────────┘
                           │ 交易请求              │ 风控专员对话
                           ▼                      ▼
              ┌─────────────────────┐   ┌─────────────────────┐
              │ 模拟交易网关(外部替身) │   │  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(C1C5)、core_customer.age、core_product.min_risk_code(R1R5)
  • 规则(冻结,代码硬编码,详见附表):
    • 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)

4A. 追加需求(v1.1 并入 · FR-8 / FR-9 / FR-10)

2026-09-07 经独立 AI 评审(不通过 P0×4)→ 修订 → 复审通过后并入。冻结部分 FR-1~FR-7 原文不动;本节与冻结条款的关系见 §4A.0。

4A.0 与冻结条款的修订关系(不推翻,仅显式扩展)

  1. 升级标记的第二写入方:FR-4 冻结"人工处置是唯一改状态入口"维持不变——RISK-007 升级不修改 status,仅由定时任务写 payload 升级标记(escalation_level/escalated_at),该标记为定时任务独占写入,人工 API 无法触碰
  2. risk_alert.alert_type 不扩枚举:新增两类预警复用冻结枚举值 pattern,以 payload.alert_subtype(concentration / agent_behavior)区分子类型(DDL 核实:ENUM 五值不含新类型,不改表)
  3. 出单维度扩展(非例外):FR-4"同客户同自然日仅一张"继续对客户维度成立;RISK-008 是新增的代理人维度出单线(去重键 = payload.actor_id + 自然日),与客户维度聚合并存、互不影响
  4. risk_alert.status ENUM 扩展(escalated)列入二期改表挂账,本期一律不使用该状态值

4A.1 新增角色:risk_manager(风控专员上级 · 用户拍板 2026-09-07)

  • 角色定位:risk_officer(风控专员)的上级管理岗,升级通知的接收方(系统原先无此角色,通知会推空;用户拍板新增并模拟生成上下级关系)
  • 承载方式(不改表):core_staff.staff_type 为冻结 ENUM(advisor/analyst/risk_officer/compliance/ops)不能加值 → 上级账号 staff_type='risk_officer'、roles=["risk_manager"](roles 为 JSON 字段,不撞红线 7);种子新增 2 个模拟账号 STAFF-31001/STAFF-31002
  • 鉴权判定口径:一律按 JWT roles 数组判定(含 risk_manager 即可),staff_type 不参与鉴权判定;台账查询(GET alerts)新增 risk_manager → 全量只读分支;处置 API(handle)角色白名单维持 roles 含 risk_officer,manager 账号不含该角色自然 403,零代码改动
  • 权限边界:预警台账只读全量(同 risk_officer);RISK-008 行为链明细可见(见 FR-10);无处置权——FR-4"人工处置唯一角色 = risk_officer"冻结条款不变,经理只升级知悉不代为处置
  • 可见渠道:FR-6 对话线入口维持冻结"仅 risk_officer"不变;risk_manager 查看预警台账与行为链明细统一走 HTTP 台账(GET alerts 全量只读分支),不扩展对话线角色
  • 联动清单(C5 开工前执行):scripts/core/02-seed-base.sql 追加账号;JWT 手册 §5.3 角色清单增补;AGENT_ACCESS_MATRIX 增补行
  • 通知升级链总览:预警 → risk_officer(专员);超时 LEVEL_1 → +risk_manager(专员上级);LEVEL_2 → +compliance(合规官知悉)

FR-8 客户资产配置集中度预警(RISK-006)

目标:弥补"单笔合规、整体失衡"盲区。现有 SUIT/RISK-001~005 均聚焦单笔交易,不感知客户持仓结构。

触发时机:交易网关落库后,规则引擎处理完 RISK-001~005 后同步追加本规则;对话线 Tool 实时查询为只读展示,不触发 RISK-006 出单。

规则定义:

字段 定义
规则 ID RISK-006
名称 高风险资产配置集中度预警
触发条件 客户持仓中 R4+R5 产品市值合计 ÷ 总持仓市值 ≥ RISK_CONCENTRATION_THRESHOLD(默认 80%,.env 可配)
计算口径 市值直接取 core_holding.market_value 现成列汇总(与 T-04 core_ro.list_holdings 既有返回一致;不按"份额×最新净值"重算,避免 N+1 调 get_latest_nav 威胁引擎 <100ms 约束);总持仓 = 全产品 market_value 求和;空仓或总持仓为 0 时不触发
优先级 P1
出单类型 落库复用 alert_type='pattern' + payload.alert_subtype='concentration'(不改表);聚合口径归入事件类,与 large_amount/freq_trade/pattern 同口径
评分映射 risk_score = 60(静态映射,R-05 接入后替换)

系统动作:

  1. 引擎调用 core_ro.list_holdings(customer_id) 汇总持仓:按产品风险等级(core_product.min_risk_code)分组求和 R4+R5 市值与总市值。截断防护:list_holdings 默认 limit=500,命中上限即视同达到阈值(保守告警)并在 input_summary 标注 holdings_truncated=true
  2. 命中 → alert_service.agg_upsert 并入当日事件类 pending 单:payload.alert_subtype 追加 "concentration"、triggered_rules 追加 "RISK-006"、risk_score 取 max(60 不覆盖 RISK-001/002 的 70,与 FR-4 聚合语义一致)
  3. profile_l3.upsert:monitor_tier = max(existing, watch),monitor_tags 追加 "high_risk_concentration"
  4. audit_log INSERT(event_type='risk_concentration',input_summary 含脱敏后的 R4+R5 占比与总持仓;金额按 DESENS-005 口径截断展示,customer_id 为内部键不脱敏)
  5. Redis PUBLISH risk:pub:alert,notify_role=["risk_officer"]

API 变更:对话线 Tool customer_context 扩展 concentration_ratio(R4+R5 占比,脱敏后百分比);无新增 HTTP API。

验收标准(A-10):

  • C5 客户持仓 90% 在 R5 产品 → 触发 RISK-006,当日事件类预警单 triggered_rules 含 "RISK-006" 且 payload.alert_subtype 含 "concentration"
  • C2 客户持仓 100% 在 R1 → 不触发
  • 交易落库后 5 秒内完成持仓重算并出单(集成测试以 TRD-TEST- 交易断言)
  • 对话线查询"哪些客户资产配置过于集中"返回正确列表及占比
  • 回归口径:现有用例全量回归;既有断言按"triggered_rules 含新增规则 ID 不判失败"口径预先声明

FR-9 预警单处置时效监控与自动升级(RISK-007)

目标:防止风险事件因人工响应延迟而积压扩大。现有 FR-4 只有生成与处置,无时效监控。

触发时机:定时扫描(非交易事件驱动),每 15 分钟执行一次(RISK_ESCALATION_SCAN_MINUTES 可配)。

规则定义:

字段 定义
规则 ID RISK-007
名称 预警单处置时效监控与升级
触发条件 status='pending_review' 且距 created_at 超过配置时效;每张单独立计时、独立升级;降噪:同一客户多张单在同一扫描周期命中同级别升级时,合并为一次推送
升级策略(阈值全部 settings 注入) LEVEL_1:≥ RISK_ESCALATION_L1_HOURS(默认 4h)未处置 → Redis 升级推送,notify_role 追加 risk_manager(见 4A.1)
LEVEL_2:≥ RISK_ESCALATION_L2_HOURS(默认 24h)未处置 → payload.escalation_level=2 + payload.escalated_at,notify_role 追加 compliance,对话线查询置顶
AML 特殊通道:RISK_ESCALATION_AML_L1_HOURS(默认 1h)/ RISK_ESCALATION_AML_L2_HOURS(默认 4h)
优先级 P0(运营红线)
出单类型 不新建预警单、不改 status(冻结 ENUM 无 escalated,升级信息全部由 payload 承载)
评分映射 不修改 risk_score

系统动作:

  1. 定时任务 scripts/cron/escalation_scan.py(独立脚本优先)查询 risk_alert:status='pending_review'
  2. 按规则计算超时时长,命中升级条件:
    • LEVEL_1:先 UPDATE payload 持久化 escalation_level=1 + escalated_at(幂等依据,与 LEVEL_2 同机制),再 Redis PUBLISH,notify_role 追加 risk_manager
    • LEVEL_2:UPDATE payload 写入 escalation_level=2 + escalated_at,PUBLISH,notify_role 追加 compliance
    • 幂等约束:仅当"本次计算级别 > payload.escalation_level 已记录级别"才执行升级动作;同级别不重复推送(每级别至多推送一次)
  3. 每次升级动作写 audit_log(event_type='alert_escalation',input_summary 含 alert_id、已达级别、升级原因、超时时长);定时任务无 HTTP 上下文,显式 new_trace() 生成 trace_id 并与预警单 payload、推送消息贯通(§7.5 可还原性)
  4. 状态机约束:status 全程保持 pending_review,人工处置照常(pending_review → 三态,冻结不变),处置后单据退出扫描范围;payload 升级标记仅定时任务可写,人工处置 API 不感知、不覆盖(handler_result 枚举外值由 Pydantic 返回 422)
  5. 定时任务对升级决策的全量输入输出写 audit_log,审计写库失败降级不阻塞但本地日志 error 留底(红线 5 口径)

API 变更:新增对话线 Tool query_overdue_alerts(hours?)(返回超期预警列表含 alert_id、created_at、escalation_level(读自 payload,缺省 0)、overdue_hours);无新增 HTTP API。

验收标准(A-11):

  • 免等待测试方案:升级阈值走 settings 注入(测试用小时以下阈值);fixture 支持直接 INSERT created_at 回拨的 pending 单(conftest 扩展)
  • pending 单超 L1 阈值 → 收到升级推送,notify_role 含 risk_manager;同一单重复扫描不重复推送(幂等断言)
  • AML 预警单超 AML L1 阈值 → 触发 LEVEL_1(与普通单阈值不同,两条断言分开)
  • 风控专员在阈值内处置 → 不触发任何升级;处置后超期单不再进入扫描结果
  • 状态机:全程 status='pending_review';handler_result 枚举外值 → 422;payload.escalation_level 与人工处置字段互不覆盖
  • 对话线"今天有哪些超期预警?"返回正确列表及超时时长、升级级别

FR-10 代理人异常行为链识别(RISK-008)

目标:单点拦截(如 T-04 的 AUTH_403_SCOPE)只能阻断单次请求,无法识别代理人本身的操作风险模式。

触发时机:定时扫描(每 30 分钟,RISK_AGENT_BEHAVIOR_SCAN_MINUTES 可配),数据源为 T-02 审计中间件留痕的 audit_log(input_guard_log 不作为数据源:网关路径不写该表且无 code 字段)。

规则定义:

字段 定义
规则 ID RISK-008
名称 代理人异常行为链识别
触发条件(阈值/窗口全部 settings 注入可配) 条件 A(频繁诱导调仓):RISK_AGENT_BEHAVIOR_A_WINDOW_HOURS(默认 24h)内,同一代理人对同一客户发生 ≥ RISK_AGENT_BEHAVIOR_A_COUNT(默认 3)次产品切换操作(赎回某产品后 2 小时内申购另一产品)。归属口径:以 audit_log(event_type='trade_request',网关审计)中的发起人 actor 归属;排除发起人 == 客户本人的交易(防客户正常调仓误报)。系统 core_trade 无 actor 字段、无代理人交易路径(FR-1 鉴权仅 risk_demo/客户本人),演示期由 risk_demo 账号扮演代理人发起(写入演示 SOP);真实代理人交易路径接入时以 Core 提供 actor 字段为准(挂账)
条件 B(试探越权):RISK_AGENT_BEHAVIOR_B_WINDOW_HOURS(默认 72h)内,同一代理人累计 ≥ RISK_AGENT_BEHAVIOR_B_COUNT(默认 5)次 AUTH_403_SCOPE 越权查询。唯一数据口径:audit_log WHERE event_type='authz' AND input_summary 中 code='AUTH_403_SCOPE'(拒绝标记为 decision='forbidden');对话线 Tool blocked 事件经 record_authz_denial 双写后同口径可查
条件 C(高频查询非名下客户):RISK_AGENT_BEHAVIOR_C_WINDOW_HOURS(默认 24h)内,同一代理人查询非名下客户信息被拒 ≥ RISK_AGENT_BEHAVIOR_C_COUNT(默认 10)次。数据源:event_type='customer_query' 在系统中无生产方,条件 C 定义为**"非名下客户查询被拒"事件族**:audit_log WHERE event_type='authz' AND code IN ('AUTH_403_NOT_OWNER','AUTH_403_NOT_ASSIGNED')(API 线归属校验拒绝)∪ 对话线 agent_tool_call blocked 中归属类拒绝码(经 record_authz_denial 双写);customer_query 事件类型补建列为二期 T-02 侧变更(挂账)
优先级 P1
出单类型 agent_behavior 语义预警单:落库 alert_type='pattern' + payload.alert_subtype='agent_behavior';独立出单不按客户聚合(出单维度扩展,见 4A.0 第 3 条):去重键 = payload.actor_id + 自然日,同代理人同日仅一张 pending 单
评分映射 risk_score = 75(静态映射,R-05 评分模型接入后替换)

系统动作:

  1. 定时任务扫描 audit_log 滑动窗口(24h/72h,按上表口径过滤)
  2. 命中任一子条件,生成预警单,落库映射(对照真实 DDL 定案):
    • alert_type='pattern',payload.alert_subtype='agent_behavior'
    • customer_id(NOT NULL 冻结约束)= 行为链中主要涉及客户(按证据条数取众数);payload 显式声明该字段语义为"涉及客户"而非"归属客户"
    • 代理人标识:payload.actor_id + payload.actor_type='agent'(risk_alert 无 actor_id 列)
    • payload 含行为链明细:时间线、涉及客户 ID(内部键,不脱敏)、命中子条件(A/B/C)、证据 trace_id 列表
    • risk_score = 75
  3. 代理画像存储:本期最小实现仅在 risk_alert.payload 承载(不新建 agent_profile_l3 表、不扩展 customer_profile_l3 语义);长期画像追踪 M4 后评估
  4. Redis PUBLISH,notify_role=["risk_officer","risk_manager"]
  5. 行为链明细展示范围:仅 risk_officer / risk_manager 角色可见(其他角色 fail-closed;对话线入口维持仅 risk_officer,经理走 HTTP 台账);对话输出对客户身份信息二次脱敏
  6. 审计:每条命中行为链写 audit_log(event_type='agent_behavior_detected'),显式 new_trace()(同 FR-9 口径)

API 变更:对话线 Tool 扩展 query_agent_behavior(agent_id?)(检索 alert_type='pattern' AND payload.alert_subtype='agent_behavior' 双条件;agent_id 匹配 payload.actor_id;JSON 检索演示规模可接受,性能挂账);无新增 HTTP API。

验收标准(A-12):

  • 代理人在 24h 内对同一客户执行 3 次"赎回→申购" → 触发条件 A,生成代理人维度预警单(payload.actor_id 正确、customer_id = 涉及客户)
  • 客户本人交易不误报:actor_id == customer_id 的正常调仓不计数
  • 代理人在 72h 内累计 5 次 AUTH_403_SCOPE → 触发条件 B
  • 24h 内 10 次 AUTH_403_NOT_OWNER/AUTH_403_NOT_ASSIGNED 被拒 → 触发条件 C(数据经 backdated_audit_event fixture 回拨构造)
  • 风控专员对话查询"代理人 STAFF-20001 最近有没有异常行为" → 返回正确预警及证据链
  • 同代理人同日多次命中不同子条件 → 只一张 pending 单,triggered_rules/payload 追加子条件标识
  • 预警单 payload.actor_id 正确指向代理人而非客户;非授权角色查行为链明细 → fail-closed
  • 测试基建:conftest 扩展 backdated_audit_event fixture(显式 created_at 回拨写入 audit_log,teardown 按 TRD-TEST- 标记清理);窗口边界用例(A:2/3;B:4/5;C:9/10);现有用例全量回归

新增配置项(C4~C6 开工时同步 .env.example + settings.py):

配置项 默认值 用途
RISK_CONCENTRATION_THRESHOLD 0.80 RISK-006 集中度阈值
RISK_ESCALATION_SCAN_MINUTES 15 RISK-007 扫描周期
RISK_ESCALATION_L1_HOURS / L2_HOURS 4 / 24 普通单升级阈值
RISK_ESCALATION_AML_L1_HOURS / AML_L2_HOURS 1 / 4 AML 单升级阈值
RISK_AGENT_BEHAVIOR_SCAN_MINUTES 30 RISK-008 扫描周期
RISK_AGENT_BEHAVIOR_A_WINDOW_HOURS / A_COUNT 24 / 3 条件 A 窗口/次数
RISK_AGENT_BEHAVIOR_B_WINDOW_HOURS / B_COUNT 72 / 5 条件 B 窗口/次数
RISK_AGENT_BEHAVIOR_C_WINDOW_HOURS / C_COUNT 24 / 10 条件 C 窗口/次数

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)

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
A-10 集中度预警(FR-8 · v1.1) C5 客户持仓 90% R5 → 事件类预警单含 RISK-006 + payload.alert_subtype='concentration';C2 全 R1 不触发;落库后 5s 内出单
A-11 时效升级(FR-9 · v1.1) 超阈值 → LEVEL_1 推送含 risk_manager(幂等:重复扫描不重复推);AML 短通道 1h;阈值内处置不升级;全程 status=pending_review;枚举外 handler_result → 422(免等待:阈值 settings 注入 + created_at 回拨 fixture)
A-12 代理人行为链(FR-10 · v1.1) 条件 A(3 次赎回→申购,本人交易排除)/ B(5 次 AUTH_403_SCOPE)/ C(10 次归属拒绝)各自触发;代理人维度独立出单(payload.actor_id);同日多子条件只一张单;行为链明细仅 risk_officer/risk_manager 可见

通用验收:所有预警单初始 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:

-- 演示客户测评日期刷新至演示日前 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.1 追加)scripts/core/02-seed-base.sql 追加 risk_manager 上级角色账号 STAFF-31001/31002(staff_type='risk_officer' + roles=["risk_manager"])——C5 开工前
(v1.1 追加)JWT 手册 §5.3 + AGENT_ACCESS_MATRIX 增补 risk_manager 角色行(台账只读全量、无处置权、对话线不放行)——C5 开工前
(v1.1 追加).env.example + settings.py §4A FR-8/9/10 配置项清单全量登记(集中度阈值/升级双通道/行为链窗口)——C4~C6 开工时
(v1.1 追加)02-redis-keys.md risk:pub:alert payload 增补 escalation_level/alert_subtype 字段说明
(v1.1 追加)演示 SOP 补 risk_demo 扮演代理人发起"赎回→申购"演示步骤——C6 开工前
(v1.1 追加)docs/memory/MEMORY.md / TODO.md / FRAMEWORK.md 登记追加需求 FR-8/9/10 与 C4C6 任务、挂账 #1#9

复审通过并经用户确认后冻结为 v1.0(FR-1~FR-7,2026-09-06);v1.1 追加 FR-8/FR-9/FR-10(2026-09-07 经独立 AI 评审→修订→复审通过后并入)。本 PRD 为风控模块开发唯一依据。