模块 1 · 风控能力

预警、适当性、AML
与模拟交易

风控专员 Demo STAFF-30001 带 risk_officer + risk_demo 角色: 能看预警台账、处置、跑 AML 扫描,还能在模拟交易页触发规则引擎。 本地登录页一键切换,改 JWT 角色后需重新登录。 仓库基线 merger · python -m pytest → 825 passed。

四条能力线(REST + 对话 Tool 共用底座)

预警台账 FR-4

GET /api/risk/alerts 分页查询;POST .../handle 人工处置(仅 risk_officer)。响应固定带 disclaimer。

适当性校验 R-02 / FR-2

POST /api/risk/suitability/check 与网关共用 Core 只读层 的 check_suitability(旧 SUIT-001~008 已退役)。客户本人 / 理财师名下 / 风控全量。

AML 扫描 FR-5

POST /api/risk/aml/scan:仅 risk_officer 触发全量扫描,生成 aml 类预警。

模拟交易 FR-1

POST /api/simulate/trade:risk_demo 或客户本人可提交;走 模拟写 Core 网关(trade_gateway.py) → 适当性 → 规则引擎(risk_engine.py)。

鉴权与问数不同: 风控 REST 走 get_auth_context,前端 risk.ts 必须带 X-Agent-Type: risk + Bearer。缺头会 401 AUTH_401_MISSING_AGENT_TYPE。

群聊:风控专员打开预警页

浏览器通过 riskHeaders 声明自己在敲风控窗:

CODE · 模拟交易入口(simulate.py)鉴权
if not (auth.has_role("risk_demo")
        or (auth.is_customer()
            and auth.customer_id == req.customer_id)):
    deny(auth, "AUTH_403_ROLE", ...)
return submit_trade(req.model_dump(), actor_id=auth.actor_id)
白话

模拟交易不是谁都能点:要么 risk_demo 演示号,要么客户给自己下单。

STAFF-30001 有 risk_demo 权限,可在 RiskSimulatePage 代客触发规则。

越权会 deny 并写审计,不会悄悄放行。

STAFF-30001 能在模拟交易页提交申购吗?

模块 2 · 模拟交易链路

一笔演示交易
怎么长出预警单?

RiskSimulatePage 提交表单 → 模拟交易入口(simulate.py)验权 → 模拟写 Core 网关(trade_gateway.py)写库 → 规则引擎(risk_engine.py)扫规则 → 聚合进 risk_alert。 像演示用的「假收银台」:钱是假的,但风控流程是真的。

四段链路(动画)

🧪模拟交易 UI
🚪模拟交易入口
⚙模拟写 Core 网关
🛡规则引擎

Preset A-3:CUST-3001 · 50 万 → 大额预警 · Preset A-1:适当性阻断

R-02 阻断 vs R-01 大额待审(同网关、不同结果)

模拟写 Core 网关(trade_gateway.py)里先跑适当性:不过 → 交易写不进去(R-02);过了 → 写入 core_trade → 规则引擎再扫 R-01 大额等 → 出 pending_review 预警。

A-1 · R-02 阻断

CUST-1001 + PROD-161725 · 风评与产品 R 档不匹配 → 网关拒绝 · 可有 suitability 类预警 · 无成交

A-3 · R-01 大额待审

CUST-3001 · 50 万申购 → 成交写入 Core → 引擎出大额类 pending_review · 台账可见

指挥 AI 时: 别把「阻断交易」和「出预警待审」混成一个状态——前者在网关,后者在引擎聚合。

代码地图

模拟交易入口(app/api/simulate.py)
薄路由:鉴权 + 调 submit_trade
模拟写 Core 网关(app/gateway/trade_gateway.py)
适当性 → 写 core_trade → 调 risk 引擎
app/service/risk/
规则引擎(risk_engine.py) · alert_service.py · 规则聚合
web/src/pages/risk/RiskSimulatePage.tsx
预设 A-1 / A-3 一键填表
CODE · 模拟交易入口(simulate.py)
if not (auth.has_role("risk_demo")
        or (auth.is_customer() and
            auth.customer_id == req.customer_id)):
    deny(auth, "AUTH_403_ROLE", ...)
return submit_trade(req.model_dump(), ...)
白话

能点「提交模拟交易」的只有两类人:带 risk_demo 的风控演示号,或客户本人买自己的单。

STAFF-30001 在 jwt_service 里配了 risk_demo,所以能造 A-3 大额演示数据。

Demo 里谁可以把新成交写进 core_trade?

模块 3 · REST 与对话 Tool

台账 REST 要带 risk 头
对话靠六只 Tool

风控REST 走模块鉴权工厂(deps.py 的 get_auth_context),前端必须带 X-Agent-Type: risk。 风控对话走顾问通用编排(agent_service.py)的 risk 分支 + SSE;Tool 由风控对话 Tool 注册表(chat_tools.py)注册 六只只读 Tool,经对话 Tool 编排(tool_service.py)分发。

预警 REST 两端点

GET /api/risk/alerts

risk_officer / risk_manager 全量;compliance 强制只看 aml;响应含 disclaimer

POST /api/risk/alerts/{id}/handle

仅 risk_officer;handler_result 三枚举 + 审计;409 状态冲突

曾踩过的坑: web/src/api/risk.ts 若漏 X-Agent-Type: risk,JWT 正确也 401。 问数 API 则相反——不要带这个头。

风控对话 Tool(chat_tools.py)注册表(C1 + 扩展)

风控对话 Tool(app/service/risk/chat_tools.py)
alert_query — 客户或全量待审预警
customer_context — L0 + L3 + 待审预警
suitability_check — 只读校验 + 审计落库
aml_lookup — 名单匹配
query_overdue_alerts — 超期未处置(FR-9)
query_agent_behavior — 代理人行为链(FR-10)

红线:Tool 只读,不生成预警单、不处置、不改正式风险等级。

群聊:风控问「今天多少待审预警」

前端 · risk.ts + simulate
apiFetch('/api/risk/alerts', {
  token, agentType: 'risk',  // 必须
})
// RiskSimulatePage → /api/simulate/trade 同样带 risk
白话

台账页、模拟交易页、风控对话——凡走 get_auth_context 的都要声明 risk 线。

处置预警用 handle REST;日常问数用对话 Tool,两者不要混接口。

风控对话里问「代理人 STAFF-Q 有没有越权记录」,应触发哪个 Tool?

仓库现状(2026-09-10):STAFF-30001 含 risk_demo;python -m pytest → 825 passed。

模块 4 · 演示与缺口

后端有、前端没有
cron 和空台账怎么理解

风控引擎和 REST 大多已实现,但演示运维、cron 升级、部分前端按钮仍开放。 reset 后台账为空是正常现象——不是「风控坏了」。

后端有 · 前端未全接(2026-09)

AML scan

POST /api/risk/aml/scan — API 有,web/ 无专用操作页。

适当性双 URL

平台 canonical /api/compliance/suitability-check vs 风控 /api/risk/suitability/check — 别再加第三条。

台账筛选

类型/客户/日期高级筛选 — 后端能力部分有,UI 简版。

FR-9/10:引擎在,cron 无 UI

💬风控 Chat
⏰cron 脚本
📊MySQL 台账

点击「下一步」

reset 后台账空: reset.ps1 只重建 jinrong_core;risk_alert 要跑 prepare_risk_demo.sql + AML 种子才有演示数据。完整步骤见 docs/项目框架设计/演示SOP-风控模块.md §2。

合规硬边界(再强调)

预警默认 pending_review · 仅风控专员 handle 改状态 · 不自动冻户 · 不自动上报监管 · 不改正式 C1~C5。

风控专员说「在 Chat 里说一句就自动跑超期升级」,能实现吗?

模块 5 · FR-8/9/10

集中度、超期升级
与代理人行为链

PRD v1.1 追加的三条能力(FR-8~10)已在 merger 落地。 指挥 AI 改代码时要分清:规则引擎 / cron 脚本负责「出单、升级」;对话 Tool 只读查库,不能替运维跑定时任务。

三条规则 · 各管什么

FR-8 · RISK-006 集中度

客户持仓 R4+R5 占比 ≥ 阈值 → 并入事件类预警聚合(同客户同日一张事件单)。演示 A-10。

FR-9 · RISK-007 超期升级

pending_review 超阈值未处置 → 超期升级 cron(escalation_scan.py)写 payload 分级 · status 不变。

FR-10 · RISK-008 行为链

代理人 A/B/C 条件命中 → 按 actor_id 维度出单;明细仅 officer/manager 可见。演示 A-12。

数据流:引擎出单 vs Chat 只读查

⚙引擎 / cron
💬风控 Chat Tool
🗄risk_alert 表

点击「下一步」

对话 Tool · 超期查询(chat_tools.py)
# query_overdue_alerts — 只读 FR-9 入口
# query_agent_behavior — 只读 FR-10 入口
# 无 Tool 封装 escalation_scan / agent_behavior_scan
白话

想让超期单「升级」→ 运维跑 scripts/cron/escalation_scan.py,不是改 prompt。

想让行为链出单 → 先积累 audit 痕迹,再跑 agent_behavior_scan.py。

Chat 里问「有哪些超期单」→ query_overdue_alerts 查现有数据即可。

FR-9 超期升级后,预警单的 status 会怎样?

模块 6 · L3 与角色

监测档怎么写
谁能看谁能处置

L3 监测画像写入服务(profile_l3.py)把 AML 命中、规则结果写入 customer_profile_l3(防降级合并)。 模块鉴权工厂(deps.py)按 JWT 角色决定台账 REST 与对话 Tool 能看到什么——接前端或新 Tool 前先对这张矩阵。

L3 监测档 · 谁写、谁读

①

写入 · AML 扫描服务(aml_service.py)命中 → 写 L3 monitor_tier=high + 预警出单(R-03 通知不冻户)

②

合并 · 只升不降(normal < watch < high);禁止 Agent 把 high 改回 normal

③

读取 · customer_context Tool:Core L0 + L3 + 待审预警摘要(只读,供风控 Chat 解读)

铁律: L3 是 enrich 监测标签,不覆盖 Core 正式 C1~C5 风评;审计表只 INSERT。

角色矩阵(REST + 对话数据面)

risk_officer

台账全量 · POST .../handle 处置 · AML scan · 模拟 risk_demo · 行为链明细可见

risk_manager

台账全量 · 一般不 handle(按矩阵)· 行为链明细可见

compliance

台账强制 aml 域 · 无 simulate · 对话 Tool 仍只读

群聊:AML 命中后 L3 与台账

L3 防降级 · profile_l3.py 口径
# monitor_tier: normal < watch < high
# 合并时取 max(tier),禁止写低档覆盖高档
# risk_score 一期 NULL,归评分模型首写
白话

指挥 AI 加「自动降档」= 违规;L3 只能抬监测强度或保持。

客户 Agent 不可读 L3 全文念给客户;只有风控/顾问 Tool 在 RBAC 内读。

处置预警用 REST handle,不是让 LLM 在对话里改库。

AML 扫描命中 CUST-1002 后,系统会做什么?

模块 7 · 写侧并发

同一天两笔交易
为什么还是一条预警?

进程内聚合锁原语(locks.py)把「同一客户、同一聚合 key」串行化;L3 监测画像写入服务(profile_l3.py)用 computed_at 乐观锁防丢更新。指挥 AI 改写路径时,先分清悲观锁(串行)和乐观锁(版本戳比对)各管哪一段。

两层锁 · 各管什么

职责代码入口机制
预警同日聚合串行 预警处置与聚合服务(alert_service.py)+ 锁原语(locks.py · run_locked) 单进程 threading.Lock;拿锁超时降级执行,冲突由锁内重查兜底
L3 写侧防覆盖 L3 写入(profile_l3.py)+ 仓储更新(risk_repository.update_l3) 乐观锁:WHERE computed_at = :expected,失败重读重试(最多 3 次)
处置 + 审计同事务 人工处置(handle_alert)+ 风控仓储(risk_repository) 状态变更与 audit 同连接提交(B7 挂账⑦ 已收口)
不做: 风控线不接 L1/L2 画像 Redis 热缓存(归客服/顾问);L3 读侧已有 profile:l3:{customer_id} cache-aside + 写后 DEL。
锁原语 · 预警聚合 key 示例
# alert_service:同日同客户事件类预警
run_locked(
  f"agg:event:{customer_id}:{today}",
  lambda locked: _merge_or_insert(...),
)

# profile_l3:同一客户 L3 upsert
run_locked(f"l3:{customer_id}", _write_with_optimistic_retry)
白话

想象收银台叫号器:同一客户、同一天的「首单聚合」必须排队进窗口,窗口里再查「今天是否已有单」——两笔并发进来也不会开出两张重复首单。

L3 则是贴便签前先对表上的时间戳:若别人刚改过,你的 UPDATE 打不中,就重读再合并(只升不降监测档)。

群聊:并发两笔 · 聚合与 L3

数据流:写侧三条线

🔒聚合锁
📋risk_alert
📊L3 + audit

点击「下一步」

指挥 AI「给风控加 L1 Redis 热读」合理吗?