模块 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)。
get_auth_context,前端 risk.ts 必须带 X-Agent-Type: risk + Bearer。缺头会 401 AUTH_401_MISSING_AGENT_TYPE。
群聊:风控专员打开预警页
浏览器通过 riskHeaders 声明自己在敲风控窗:
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。
像演示用的「假收银台」:钱是假的,但风控流程是真的。
四段链路(动画)
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 · 台账可见
代码地图
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 只读,不生成预警单、不处置、不改正式风险等级。
群聊:风控问「今天多少待审预警」
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
点击「下一步」
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 只读查
点击「下一步」
# 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 解读)
角色矩阵(REST + 对话数据面)
risk_officer
台账全量 · POST .../handle 处置 · AML scan · 模拟 risk_demo · 行为链明细可见
risk_manager
台账全量 · 一般不 handle(按矩阵)· 行为链明细可见
compliance
台账强制 aml 域 · 无 simulate · 对话 Tool 仍只读
群聊:AML 命中后 L3 与台账
# 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 挂账⑦ 已收口) |
profile:l3:{customer_id} cache-aside + 写后 DEL。
# 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
数据流:写侧三条线
点击「下一步」