From 1ac331b281312af56bbbfe57f8f6d67c5649c811 Mon Sep 17 00:00:00 2001 From: YUAN Date: Mon, 7 Sep 2026 18:01:16 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=98=B6=E6=AE=B5=E4=B8=80=20AL-01~08?= =?UTF-8?q?=20=E5=AE=8C=E6=88=90=E8=90=BD=E8=B4=A6=20+=20=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=90=8C=E6=AD=A5=E2=80=94=E2=80=94=E5=85=AD=E4=BB=B6?= =?UTF-8?q?=E5=A5=97=E5=8F=8D=E6=98=A0=E6=8D=A2=E6=A0=B8=E5=AE=8C=E6=88=90?= =?UTF-8?q?(406=20=E7=BB=BF=E5=9F=BA=E7=BA=BF=20425=E2=86=92406/SUIT-001~0?= =?UTF-8?q?08=20=E9=80=80=E5=BD=B9=E6=94=B9=20core=5Fro.check=5Fsuitabilit?= =?UTF-8?q?y=20C=C3=97R=20=E7=9F=A9=E9=98=B5+JR-AST/FM=20=E5=A5=91?= =?UTF-8?q?=E7=BA=A6/risk=5Fsuitability=5Flog=2021=20=E5=88=97/=E7=A7=8D?= =?UTF-8?q?=E5=AD=90=2033=20=E5=AE=A2=E6=88=B7=2014=20=E4=BA=A7=E5=93=81/r?= =?UTF-8?q?isk-m1=20=E5=B7=B2=E8=A1=A5=E6=89=93=E6=8C=87=E5=90=91=203c07de?= =?UTF-8?q?6)=EF=BC=9BTODO=20=E8=BF=9B=E8=A1=8C=E4=B8=AD=E6=94=B9=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E4=B8=80=E9=AA=8C=E6=94=B6=E9=97=A8(=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E5=8D=A1=E7=82=B9=3D=E7=94=A8=E6=88=B7=E7=9B=AE?= =?UTF-8?q?=E8=A7=86=E7=A1=AE=E8=AE=A4)+AL-09/10/11=20=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=EF=BC=9BITERATION=20=E8=BF=BD=E5=8A=A0=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E4=B8=80=E5=AE=8C=E6=88=90=E8=A1=8C=EF=BC=9BREQUIREMENTS=20R-0?= =?UTF-8?q?2=20=E6=8D=A2=E6=A0=B8=E6=A0=87=E6=B3=A8=EF=BC=9B=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=AE=A1=E5=88=92=20M1=20=E6=94=B9=E5=B7=B2=E8=A1=A5?= =?UTF-8?q?=E6=89=93=EF=BC=9B=E5=85=A5=E5=BA=93=E5=AD=98=E6=A1=A3=E3=80=8A?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8A=A5=E5=91=8A-=E5=AF=B9=E9=BD=90main?= =?UTF-8?q?=E5=9F=BA=E5=87=86.md=E3=80=8B(=E9=98=B6=E6=AE=B5=E4=B8=80?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E4=BE=9D=E6=8D=AE)=E4=B8=8E=E3=80=8A?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=96=B9=E6=A1=88-=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E9=9C=80=E6=B1=82v1.1-C4C6.md=E3=80=8B(?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E4=BA=8C=E7=BC=96=E7=A0=81=E4=BE=9D=E6=8D=AE?= =?UTF-8?q?)=EF=BC=9B=E5=90=B8=E6=94=B6=E4=B8=8A=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E9=81=97=E7=95=99=E5=90=8C=E6=AD=A5(=E5=88=86=E6=94=AF?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=20risk-control-agent/PRD=20v1.1=20=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E6=A0=87=E6=B3=A8/T-21=20=E8=AF=84=E5=AE=A1=E9=97=AD?= =?UTF-8?q?=E7=8E=AF/T-03/C1~C3=20=E8=90=BD=E8=B4=A6/=E4=BA=A4=E6=8E=A5?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=8C=87=E5=BC=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +- docs/PRD/PRD-风控监测Agent.md | 2 +- docs/memory/ENVIRONMENT.md | 2 +- docs/memory/FLOW.md | 12 +- docs/memory/FRAMEWORK.md | 23 +- docs/memory/ITERATION.md | 4 + docs/memory/MEMORY.md | 29 +- docs/memory/REQUIREMENTS.md | 15 +- docs/memory/TODO.md | 23 +- docs/项目框架设计/修改报告-对齐main基准.md | 162 ++++++ .../实现方案-风控追加需求v1.1-C4C6.md | 543 ++++++++++++++++++ docs/项目框架设计/开发计划-风控模块.md | 8 +- docs/项目框架设计/架构设计-风控模块.md | 2 +- 13 files changed, 795 insertions(+), 47 deletions(-) create mode 100644 docs/项目框架设计/修改报告-对齐main基准.md create mode 100644 docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md diff --git a/README.md b/README.md index ee0f6f5..6556566 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,20 @@ | 目录 | 内容 | | --- | --- | | [docs/memory/MEMORY.md](docs/memory/MEMORY.md) | 项目记忆入口(**新 Agent 读 §0 交接清单**) | -| [docs/需求拆解/](docs/需求拆解/) | 业务场景、数据矩阵、合规 | -| [docs/项目框架设计/](docs/项目框架设计/) | 表设计、技术选型、JWT 手册 | +| [docs/PRD/](docs/PRD/) | 风控监测 Agent PRD(v1.1)+ 风控规则表 | +| [docs/项目框架设计/](docs/项目框架设计/) | 表设计、技术选型、JWT 手册、开发计划、实现方案 | +| [交接文档.md](交接文档.md) | 最新进度速览(本地保留,不入库) | ## 后端结构 ```text app/ -├── api/ # 路由:chat、knowledge、admin -├── service/ # Agent、RAG、记忆 -├── tool/ # 解析、Embedding、Milvus +├── api/ # 路由:chat、risk、simulate、knowledge、admin + audit_middleware、deps +├── service/ # Agent 编排、风控服务(risk/*)、RAG、记忆、鉴权、输入防护 +├── tool/ # Core 只读 Tool、知识库 kb_tools、解析/Embedding/Milvus +├── gateway/ # 模拟交易网关(唯一可写 core_trade) ├── model/ # Pydantic + ORM -├── repository/ # Core 只读 core_ro(已实现) +├── repository/ # core_ro(Core 只读)、risk_repository、session_repository ├── config/ # settings、database ├── utils/ └── main.py @@ -26,7 +28,7 @@ app/ ## 环境要求 -- Python 3.13+、MySQL 8.0、Redis 8、Neo4j Desktop、Ollama(bge-m3) +- Python 3.13+(pytest 依赖装在系统 Python)、MySQL 8.0、Redis 8、Neo4j Desktop、Ollama(bge-m3) - Windows 原生部署(见技术选型文档) ## 快速启动 @@ -36,6 +38,7 @@ cp .env.example .env pip install -r requirements.txt # 首次:灌 Core 模拟库与同步 — 见 docs/memory/FLOW.md §0 uvicorn app.main:app --reload +python -m pytest # 406 用例 ``` 健康检查:`GET http://127.0.0.1:8000/health` diff --git a/docs/PRD/PRD-风控监测Agent.md b/docs/PRD/PRD-风控监测Agent.md index 44a2c18..e0db683 100644 --- a/docs/PRD/PRD-风控监测Agent.md +++ b/docs/PRD/PRD-风控监测Agent.md @@ -2,7 +2,7 @@ > 版本: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(风控/记忆/测试)· 分支 `feature/risk` +> 负责人: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 承载不改表)** diff --git a/docs/memory/ENVIRONMENT.md b/docs/memory/ENVIRONMENT.md index f825e37..d3e37c5 100644 --- a/docs/memory/ENVIRONMENT.md +++ b/docs/memory/ENVIRONMENT.md @@ -25,7 +25,7 @@ - **内部分析**:SQL 或 IT 导表。 - **风控**:事后/T+1 发现异常;适当性靠交易前规则;AML 批处理或人工。 -Agent **尚未上线**;后端风控事件线已实现(`feature/risk` 分支,B1~B7),其余 Agent 仍为脚手架;无生产接入。 +Agent **尚未上线**;后端风控事件线 + 对话线 + 知识库已实现(`risk-control-agent` 分支,B1~B9b + 阶段 C + T-03/T-21 + **阶段一对齐 main 基准 AL-01~08**,406 测试绿),其余 Agent 仍为脚手架;无生产接入。 ------ diff --git a/docs/memory/FLOW.md b/docs/memory/FLOW.md index 55357fe..e5dab26 100644 --- a/docs/memory/FLOW.md +++ b/docs/memory/FLOW.md @@ -38,7 +38,7 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md ``` -**尚未自动化:** Milvus 建 Collection(T-21);agent 专用表已有 `02-mysql-agent专用.sql`(手工执行,含 risk_aml_list)。JWT 中间件已完成(T-01:`app/service/auth_service.py` + `deps.py` 工厂;dev 联调签发 `scripts/dev/issue_dev_token.py`,debug 头兜底仅限 development 且未配 RS256 公钥)。 +**尚未自动化:** agent 专用表已有 `02-mysql-agent专用.sql`(手工执行,含 risk_aml_list);Milvus 建 Collection 已随 T-21 自动化(`scripts/kb/build_kb.py` 可重灌)。JWT 中间件已完成(T-01:`app/service/auth_service.py` + `deps.py` 工厂;dev 联调签发 `scripts/dev/issue_dev_token.py`,debug 头兜底仅限 development 且未配 RS256 公钥)。 ------ @@ -48,9 +48,9 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md Client → Gateway(JWT/RBAC) → api/chat → agent_service(LangGraph) → Tools → 存储 → 响应 + audit_log ``` -当前:**风控事件线 + Wave 0 共用底座 + T-04 Tool 节点完成**(交易网关 → 规则引擎 → 预警/AML/L3 → 4 API,`main.py` 路由挂载 + trace 中间件(X-Trace-Id/X-Request-Id + 500 兜底)+ audit 中间件(http_access)+ lifespan 完成,B1~B8;T-01 JWT 鉴权(Bearer 全环境 + dev debug 头兜底 + X-Agent-Type 准入矩阵)/ T-02 审计与 input_guard_log 双写 / T-06 chat 最小闭环(`POST /api/chat` 会话+窗口+落盘)/ T-07 LangGraph 骨架+DeepSeek(无 key 降级)均于 2026-09-07 完成并经独立 AI 评审闭环;M2 tag risk-m2 + M4 复核闭环;T-04 Core RO Tool 节点(customer/advisor 意图触发 + 归属校验 + agent_tool_call 落库 + 公共鉴权双写 `utils/authz.py` + 入参白名单,2026-09-07,两轮评审闭环);321 测试绿);阶段 B 完结;**对话链路已含只读查数**(chat → LangGraph(tool→llm→guard) → 免责声明 → 落库),风控四 Tool 与 RAG 待 C1/T-21、输入防护待 T-03。 +当前:**风控事件线(B1~B9b)+ Wave 0 共用底座 + 风控阶段 C(C1~C3 对话线,tag risk-m2/risk-m3)+ T-04 Tool 节点 + T-03 输入防护 + T-21 知识库全部完成**(交易网关 → 规则引擎 → 预警/AML/L3 → 4 API,`main.py` 路由挂载 + trace 中间件 + audit 中间件 + lifespan;T-01/T-02/T-06/T-07/T-04/T-03/T-21 均经独立 AI 评审闭环)。**阶段一「对齐 main 基准」AL-01~AL-08 已完成(2026-09-07,406 测试绿;适当性判定换核 core_ro.check_suitability,risk_suitability_log 21 列,Core 表 is_hnw/风评 expires_at 新列,种子 33 客户/14 产品)**。**下一步:阶段一验收门(用户目视确认)→ AL-09 合并 main → 阶段二 C4~C6 编码**(FR-8 集中度/FR-9 时效升级/FR-10 代理人行为链,实现方案《docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md》已定稿并经独立 AI 评审修订闭环,含定时任务 scripts/cron/* 与新增对话 Tool;编码未开始)。 -**本机已就位状态(2026-09-07 更新 · 已完成上述 ①~⑤,无需重做):** `.env` 已配置(学习项目,`MYSQL_PASSWORD=123456`;NEO4J/DEEPSEEK 留空——DEEPSEEK_API_KEY 空时 chat 走降级回复,不阻塞演示;`.env` 无 JWT 项,签发走默认 `JWT_DEV_SECRET`,**启动时出现默认密钥 warning 属预期**,生产形态须配 `JWT_PUBLIC_KEY_PATH`);`jinrong_core` + `jinrong_agent` 已灌库(28 客户 / AML 名单 8 条 / 演示测评已刷新 / 归属同步 28 行);`python -m pytest` 321 用例全绿(集成测试真连本机 MySQL,未灌库的机器自动 skip 集成模块);**演示库已重灌并清理冒烟数据(2026-09-07),全量测试可直接跑,下次演示/走查后再按 SOP §2 重灌**;本机 Redis 服务在跑(`redis://127.0.0.1:6379/0`,B9a 订阅脚本已验证)。 +**本机已就位状态(2026-09-07 更新 · 已完成上述 ①~⑤,无需重做):** `.env` 已配置(学习项目,`MYSQL_PASSWORD=123456`;NEO4J/DEEPSEEK 留空——DEEPSEEK_API_KEY 空时 chat 走降级回复,不阻塞演示;`.env` 无 JWT 项,签发走默认 `JWT_DEV_SECRET`,**启动时出现默认密钥 warning 属预期**,生产形态须配 `JWT_PUBLIC_KEY_PATH`);`jinrong_core` + `jinrong_agent` 已灌库(**阶段一 AL-01/AL-08 后为 33 客户/14 产品/矩阵 25 行/风评 33** / AML 名单 8 条 / 演示测评 expires_at 新口径已刷新 / 归属同步 33 行);`python -m pytest` **406** 用例全绿(集成测试真连本机 MySQL,未灌库的机器自动 skip 集成模块;pytest 依赖装在系统 Python 3.13.14);**演示库已按阶段一新口径重灌并清理冒烟数据(2026-09-07),全量测试可直接跑,下次演示/走查后再按 SOP §2 重灌**;本机 Redis 服务在跑(`redis://127.0.0.1:6379/0`,B9a 订阅脚本已验证)。 **本机已知坑:** `mysql.exe` 不在 PATH(位于 `C:\Program Files\MySQL\MySQL Server 8.0\bin`);`reset.ps1` 的交互式 `-p` 在自动化执行时会卡死——脚本化重灌用 `MYSQL_PWD` 环境变量传密码(交互执行不受影响,不把密码写进仓库脚本);**`redis` 与 `python-jose[cryptography]` 包均出现过 requirements 有而环境漏装(B9a 补装 redis、Wave 0 补装 jose,2026-09-07),重装环境后先 `python -c "import jose, redis"` 自检**。 @@ -63,15 +63,17 @@ Client → Gateway(JWT/RBAC) → api/chat → agent_service(LangGraph) → Tools ↓ Gateway:验签、角色准入、注入 AuthContext 【T-01 已做】(JWT 验签/吊销 + AGENT_ACCESS_MATRIX) ↓ +输入防护:限流(429)→ 注入/超长(400) 【T-03 已做】(Redis 固定窗口 30 次/分 fail-open;被拒 fail-fast 不建会话,blocked 落 input_guard_log) +↓ api/chat:SessionGuard;创建/续 agent_session 【T-06 已做】(actor/agent_type 一致校验) ↓ memory_service:Redis 读最近 N 轮;MySQL 落盘 agent_message 【T-06 已做】(窗口 TTL2h N≤20,miss 回源;同步落盘) ↓ -agent_service:LangGraph StateGraph + DeepSeek;tool(意图→Tool) → llm → guard 【T-04 已做(tool 节点:关键词意图仅 customer/advisor;risk/analyst 空转);LLM intent 归 C2】 +agent_service:LangGraph StateGraph + DeepSeek;tool(意图→Tool) → llm → guard 【T-04 + C2 已做(分组关键词意图 customer/advisor/risk;risk 分支接 C1 四 Tool)】 ↓ Tool(tool_service.run_tool:白名单 → 归属校验 → 执行 → agent_tool_call 落库): - CoreReadOnlyRepository:持仓/流水/L0(customer_id 由会话注入,不来自 LLM) 【T-04 已做 app/tool/core_tools.py 三 Tool】 - - 风控四 Tool:alert_query/customer_context/suitability_check/aml_lookup 【待做 C1 chat_tools,复用 run_tool】 + - 风控四 Tool:alert_query/customer_context/suitability_check/aml_lookup 【C1 已做 chat_tools(RISK_TOOL_REGISTRY)】 - milvus_tool:产品规则 RAG + source_refs 【已做 T-21(app/tool/kb_tools.py + rag_service/milvus_service;仅 customer/advisor 意图开放)】 - memory_service:读/写 L1/L2/L3(ProfileGuard) 【窗口读写已做;画像读写待做】 ↓ diff --git a/docs/memory/FRAMEWORK.md b/docs/memory/FRAMEWORK.md index 2b398ad..607f105 100644 --- a/docs/memory/FRAMEWORK.md +++ b/docs/memory/FRAMEWORK.md @@ -38,10 +38,10 @@ | 客户财富 Agent | L1 画像、事实查询、阈值提醒 | Core RO、Milvus 产品库 | 空壳 service(chat 骨架已通) | | 代理人助手 Agent | L2 画像、RAG、草稿 | L1 只读、Milvus | 空壳 service(chat 骨架已通) | | 数据分析 Agent | NL→SQL→解读 | Core RO、画像只读 | 空壳 service(chat 骨架已通) | -| 风控监测 Agent | 预警、L3、R-02 适当性 | 交易事件、AML 名单 | **已实现**(B1~B7:rules/alert_service/profile_l3/aml_service/engine/scoring 占位 + 4 API + suitability + main 集成与挂账①~⑦) | +| 风控监测 Agent | 预警、L3、R-02 适当性 | 交易事件、AML 名单 | **已实现 B1~B7 + C1~C3(对话线四 Tool + risk 分支 StateGraph,A-6 验收)**;追加 FR-8/9/10(C4~C6:集中度/时效升级/行为链)方案已定稿待编码 | | Core 只读层 | L0 事实查询 | `jinrong_core` | **已实现 + 已接对话 Tool(T-04)**:core_ro 经 app/tool/core_tools.py 三只读 Tool(L0/持仓/流水)进 chat;风控扩展查询照旧 | | 共用底座 | 会话、审计、输入防护 | MySQL 11 表 + Redis | **已接入(2026-09-07)**:会话(T-06 session_repository + memory_service 窗口)、审计中间件(T-02 http_access + input_guard_log 双写)、agent_tool_call Tool 留痕(T-04)、输入防护(T-03 input_guard:注入词表纯函数检测 + oversize + Redis 固定窗口限流,chat 链路 限流→注入/超长→归属) | -| 对话编排 | LangGraph StateGraph + DeepSeek | langgraph/langchain-openai | **已实现(T-07 骨架 + T-04 Tool 节点,2026-09-07)**:tool(关键词意图→Tool)→llm→guard(免责声明);LLM intent 与风控四 Tool 归 C1/C2 | +| 对话编排 | LangGraph StateGraph + DeepSeek | langgraph/langchain-openai | **已实现(T-07 骨架 + T-04 Tool 节点 + C1 风控四 Tool + C2 risk 分支,2026-09-07)**:tool(分组关键词意图→Tool,customer/advisor/risk)→llm→guard(免责声明);analyst 分支与 LLM intent 待后续 | | 同步脚本 | 归属、Neo4j | Core → agent / 图库 | **sync_*.py 已实现** | ------ @@ -50,12 +50,13 @@ ```text api/ → risk / simulate / chat / deps / audit_middleware 已实现;knowledge、admin 空壳 【大部分】 -service/ → risk/*(rules/alert/aml/engine/l3/scoring/locks/redis_gateway)+ suitability + - auth_service(T-01 JWT)+ agent_service(T-07 图 + T-04 tool 节点)+ memory_service(T-06)+ - tool_service(T-04 对话 Tool 编排:意图/归属校验/run_tool 落库)已实现; - rag_service 空壳 【大部分】 -tool/ → core_tools(T-04:L0/持仓/流水只读 Tool + TOOL_REGISTRY 白名单)已实现; - document_parser、embedding_tool、milvus_tool 【部分;RAG 三件空壳】 +service/ → risk/*(rules/alert/aml/engine/l3/scoring/locks/redis_gateway/chat_tools)+ suitability + + auth_service(T-01 JWT)+ agent_service(T-07 图 + T-04 tool 节点 + C2 risk 分支)+ memory_service(T-06)+ + tool_service(T-04 对话 Tool 编排:意图/归属校验/run_tool 落库)+ input_guard(T-03)+ + embedding/milvus_service/rag_service(T-21,已实现) 【大部分】 +tool/ → core_tools(T-04:L0/持仓/流水只读 Tool + TOOL_REGISTRY 白名单)+ + kb_tools(T21-5:search_knowledge,RISK/KB 注册表第三层)已实现; + document_parser、embedding_tool、milvus_tool(早期占位壳,T-21 实现在 service 层) 【部分】 repository/ → core_ro(Core 只读)+ risk_repository(风控四表+审计双写)+ session_repository(agent_session/agent_message/agent_tool_call,T-06/T-04) 【已实现】 gateway/ → 模拟交易网关(trade_gateway + gateway_repository 仅 INSERT core_trade) 【已实现 B5】 @@ -75,9 +76,11 @@ main.py → 路由挂载 + trace 中间件(双 ID 贯通 + 500 兜底) ```text scripts/core/ → jinrong_core DDL + 种子 + reset.ps1 scripts/agent/ → jinrong_agent 种子(risk_aml_list AML 名单等) -scripts/demo/ → 演示数据准备(prepare_risk_demo.sql,reset 后重跑) +scripts/demo/ → 演示数据准备(prepare_risk_demo.sql,reset 后重跑)+ subscribe/rebuild_alerts scripts/sync/ → sync_advisor_rel.py、sync_neo4j.py -scripts/dev/ → rbac-seed-reference.md(联调账号) +scripts/dev/ → issue_dev_token.py(JWT 签发)+ rbac-seed-reference.md(联调账号) +scripts/kb/ → build_kb.py(知识库入库,T-21) +scripts/cron/ → (C5/C6 规划)escalation_scan.py / agent_behavior_scan.py 定时任务 ``` ------ diff --git a/docs/memory/ITERATION.md b/docs/memory/ITERATION.md index c80ae1a..2dcd385 100644 --- a/docs/memory/ITERATION.md +++ b/docs/memory/ITERATION.md @@ -8,3 +8,7 @@ | 2026-09-05 | Core 模拟库 jinrong_core + 扩大种子 + Neo4j 同步 | 无真实 Core | FRAMEWORK / FLOW / ENVIRONMENT | | 2026-09-05 | Agent 编排依赖改为 LangGraph 为主 | 状态图 + Tool 节点 | FRAMEWORK | | 2026-09-05 | memory 全量刷新:§0 新 Agent 交接清单 + 实现状态表 + bootstrap | 脚手架已落地,便于无上下文交接 | MEMORY / FRAMEWORK / FLOW / TODO / REQUIREMENTS / ENVIRONMENT | +| 2026-09-07 | 追加需求 FR-8/FR-9/FR-10 经独立 AI 评审→修订→复审并入 PRD v1.1 + 规则表 v1.1 + 开发计划 v1.2(C4~C6 任务 + 挂账 #1~#9) | 弥补单笔合规盲区/处置时效红线/代理人操作风险 | PRD / 附-风控规则表 / 开发计划 | +| 2026-09-07 | 《实现方案-风控追加需求v1.1-C4C6.md》定稿:独立 AI 评审 FAIL(P0×1/P1×4/P2×6)→ 修订闭环;C4~C6 编码依据可照抄 | 开发流程第 2 步(另一 AI 验证挑毛病) | 实现方案文档 | +| 2026-09-07 | 全库文档同步:分支口径修正(`feature/risk` → 实况 `risk-control-agent`,与 main 分叉 67/4)+ T-21 评审闭环 / 425 绿 / T-03 / C1~C3 状态落账 + 新建项目根交接文档.md | 文档与实况不一致(用户排查要求) | MEMORY / TODO / REQUIREMENTS / FRAMEWORK / FLOW / ENVIRONMENT / ITERATION / PRD / 架构设计 / 开发计划 / README / 交接文档 | +| 2026-09-07 | **阶段一「对齐 main 基准」AL-01~AL-08 完成**(逐项独立 commit bb244f4~b5fd52e):SUIT-001~008 换核为 core_ro.check_suitability(C×R 矩阵 + match_result 五值 + JR-AST/FM 契约)· risk_suitability_log 重建 21 列 · Core 表 is_hnw/风评 expires_at 七新列 · 种子 33 客户/14 产品 · 全量 **406 passed 0 failed 0 skipped**(425→406,SUIT 测试重写)· uvicorn 冒烟三端点通过 · risk-m1 补打(3c07de6)· 六项拍板全落定 | 阶段一执行(用户拍板 main = 需求唯一基准);基线变更与 SUIT 体系退役需落账 | MEMORY / TODO / REQUIREMENTS / FRAMEWORK / FLOW / ENVIRONMENT / ITERATION / README / 交接文档 | diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index dbd7c08..0c7d8e8 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -9,7 +9,7 @@ **项目是什么:** 金融四 Agent(客户财富 / 代理人 / 数据分析 / 风控)共用数据层与合规底座;**不**互调 LLM,跨 Agent 走 L1/L2/L3 画像与预警表。 -**当前进度:** 需求与表设计已定 · **风控模块 B1~B9b 全部完成(M2 tag risk-m2),M4 复核已闭环(2026-09-07),风控阶段 B 正式完结** · **Wave 0 已完成(2026-09-07,经独立 AI 评审闭环)**:T-01 JWT 鉴权(auth_service Auth SDK + deps 工厂替换 + X-Agent-Type 准入矩阵)/ T-02 审计中间件(http_access + 独立 request_id + 4xx/500 统一错误体 + input_guard_log 双写)/ T-06 chat 最小闭环(POST /api/chat + 会话落库 + Redis 窗口)/ T-07 LangGraph StateGraph 骨架 + DeepSeek(无 key 降级)· **T-04 Core RO Tool 节点已完成(2026-09-07)**:app/tool/core_tools.py 三只读 Tool + tool_service(意图/归属校验/run_tool)+ 图 tool 节点 + agent_tool_call 落库 + `utils/authz.py` 公共鉴权留痕,**321 测试绿(首评+复审双闭环)**。**下一步:T-21 Milvus / T-03 输入防护 / 前端 React 多 Agent 入口(HashRouter `web/` init)。风控阶段 C 已完成(2026-09-07,345 绿,tag `risk-m3`,A-6 对话线验收通过,独立 AI 评审 PASS P0=0)。**T-03 输入防护已完成(2026-09-07,378 绿,独立 AI 评审 PASS with findings P0=0):app/service/input_guard.py 注入词表 42 条纯函数检测 + oversize 4000 + actor 级 Redis 固定窗口限流 30 次/分(fail-open);chat 链路顺序 = 鉴权→准入→空白→限流 429→注入/超长 400→归属→会话,被拒 fail-fast 不建会话,blocked 落 input_guard_log(ENUM 四值已用满)。****开发在分支 `feature/risk`(未合入 main)。** +**当前进度:** 需求与表设计已定 · **风控模块 B1~B9b 全部完成(M2 tag risk-m2),M4 复核已闭环(2026-09-07),风控阶段 B 正式完结** · **Wave 0 已完成(2026-09-07,经独立 AI 评审闭环)**:T-01 JWT 鉴权(auth_service Auth SDK + deps 工厂替换 + X-Agent-Type 准入矩阵)/ T-02 审计中间件(http_access + 独立 request_id + 4xx/500 统一错误体 + input_guard_log 双写)/ T-06 chat 最小闭环(POST /api/chat + 会话落库 + Redis 窗口)/ T-07 LangGraph StateGraph 骨架 + DeepSeek(无 key 降级)· **T-04 Core RO Tool 节点已完成(2026-09-07)**:app/tool/core_tools.py 三只读 Tool + tool_service(意图/归属校验/run_tool)+ 图 tool 节点 + agent_tool_call 落库 + `utils/authz.py` 公共鉴权留痕,**321 测试绿(首评+复审双闭环)**。风控阶段 C 已完成(2026-09-07,345 绿,tag `risk-m3`,A-6 对话线验收通过,独立 AI 评审 PASS P0=0)。T-03 输入防护已完成(2026-09-07,378 绿,独立 AI 评审 PASS with findings P0=0):app/service/input_guard.py 注入词表 42 条纯函数检测 + oversize 4000 + actor 级 Redis 固定窗口限流 30 次/分(fail-open);chat 链路顺序 = 鉴权→准入→空白→限流 429→注入/超长 400→归属→会话,被拒 fail-fast 不建会话,blocked 落 input_guard_log(ENUM 四值已用满)。**阶段一「对齐 main 基准」AL-01~AL-08 已完成(2026-09-07,逐项独立 commit bb244f4~b5fd52e):适当性判定换核为 main 的 core_ro.check_suitability(C×R 矩阵表数据驱动,match_result 五值/JR-AST-012/FM-01/FM-03/JR-AST-PRO 契约,SUIT-001~008 退役);risk_suitability_log 重建 21 列;Core 表加 is_hnw/风评七新列(expires_at);种子 33 客户/14 产品;全量 406 passed 0 failed 0 skipped + uvicorn 冒烟三端点通过;risk-m1 已补打(指向 3c07de6)**。**下一步:阶段一验收门(用户浏览器目视确认)→ AL-09 合并 main → AL-10 PRD v1.2 → AL-11 docx 登记 → 阶段二 C4~C6(实现方案已定稿并经独立 AI 评审修订闭环)/ 前端 React 多 Agent 入口(HashRouter `web/` init)。****开发在分支 `risk-control-agent`(与 main 已分叉:领先 75 提交 / main 独有 4 提交;旧文档中的 `feature/risk` 为过时口径)。** **仓库地图:** @@ -22,21 +22,22 @@ | `app/service/embedding.py` `milvus_service.py` `rag_service.py` | **已实现(T21-1/2/4)** | Ollama bge-m3 1024 维(失败不静默降级)/ kb_product_rules 建集合+upsert+合规过滤检索 / search_knowledge→chunks+source_refs 溯源 | | `app/tool/kb_tools.py` | **已实现(T21-5)** | search_knowledge 对话 Tool(skip_access_check 公开知识;仅 customer/advisor 意图开放,risk 不开放);kb_product_rules 首批 24 块已入库(data/kb 6 产品手册,scripts/kb/build_kb.py 可重灌) | | `app/service/auth_service.py` | **已实现(T-01)** | Auth SDK:JWT 验签(HS256 dev/RS256 生产)、必填 claims、jti 吊销(Redis fail-open);签发 CLI `scripts/dev/issue_dev_token.py` | -| `app/service/risk/*` + `service/suitability.py` | **已实现** | RISK-001~005 规则 / 预警聚合 / L3 写入 / AML / 引擎编排 / SUIT-001~008 / locks+redis_gateway 公共原语(B7) | +| `app/service/risk/*` + `service/suitability.py` | **已实现** | RISK-001~005 规则 / 预警聚合 / L3 写入 / AML / 引擎编排 / suitability(**阶段一换核:内核改调 core_ro.check_suitability,SUIT-001~008 已退役**)/ locks+redis_gateway 公共原语(B7) | | `app/service/agent_service.py` `memory_service.py` | **已实现(T-07/T-06/T-04/C2)** | LangGraph StateGraph(tool→llm→guard)+ DeepSeek(无 key 降级);T-04 tool 节点(关键词意图 customer/advisor/risk 分组 + tool_service.run_tool);C2 tool_node 守卫放宽(requires_customer=False 允许无绑定客户,支撑 A-6 全量待审);Redis 会话窗口 + MySQL 回源 | | `app/service/tool_service.py` `app/tool/core_tools.py` `service/risk/chat_tools.py` | **已实现(T-04/C1)** | 对话 Tool 编排(统一注册表 get_registered_tool:core+risk / 归属校验 / agent_tool_call 落库)+ Core RO 三只读 Tool(L0/持仓/流水)+ C1 风控四只读 Tool(alert_query/customer_context/suitability_check/aml_lookup,RISK_TOOL_REGISTRY) | | `app/repository/session_repository.py` | **已实现(T-06/T-04)** | agent_session / agent_message / agent_tool_call 读写 | | `app/gateway/` | **已实现** | 模拟交易网关(仅 gateway_repository 可 INSERT core_trade,B5) | -| `app/repository/core_ro.py` | **已实现** | Core 只读 SELECT(含风控扩展 sum_trades_on_date / list_trades_range / list_active_customers) | +| `app/repository/core_ro.py` | **已实现** | Core 只读 SELECT(含风控扩展 sum_trades_on_date / list_trades_range / list_active_customers);**阶段一吸收 main:check_suitability(C×R 矩阵判定,CURDATE() 改 Python 端 `_is_expired`)/ list_products_for_customer / list_holdings 合并(limit=500+新列)/ list_trades / get_customer_l0 扩列版** | | `app/repository/risk_repository.py` | **已实现** | risk_alert / risk_suitability_log / L3 / risk_aml_list / audit_log / input_guard_log 读写 | | `app/utils/` | **基本就绪** | trace(trace_id+request_id 双 contextvar)/ desensitize / db(引擎工厂)/ response(统一错误体+4xx/500 handler)/ exceptions(含 ApiError)已实现;logger 占位 | | `app/config/settings.py` | **已实现** | 双库 + risk_* 阈值 + JWT 配置(jwt_public_key_path/jwt_dev_secret/jwt_issuer/jwt_audience) | | `scripts/core/*.sql` + `reset.ps1` | **已实现** | Core 模拟库 DDL + 种子 | | `scripts/agent/` `scripts/demo/` `scripts/dev/` | **已实现** | AML 名单种子 + 风控演示数据 + subscribe_alerts/rebuild_alerts + issue_dev_token(JWT 签发) | | `scripts/sync/*.py` | **已实现** | 归属同步 + Neo4j 全图 | -| `tests/` | **已实现** | 22 个测试文件 321 用例(sqlite 隔离;DDL 单一事实源 `_ddl.py`;`test_integration_risk.py` 走真 MySQL + TRD-TEST- 前缀隔离) | +| `tests/` | **已实现** | 30 个测试模块 406 用例(sqlite 隔离;DDL 单一事实源 `_ddl.py`;`test_integration_risk.py` 走真 MySQL + TRD-TEST- 前缀隔离;pytest 依赖装在系统 Python 3.13.14;阶段一 test_suitability 按 main 契约重写后基线 425→406) | | `docs/需求拆解/` | 已定 | 场景 P0、矩阵、合规原文 | -| `docs/PRD/PRD-风控监测Agent.md` | **已冻结** | 风控 PRD v1.0 + 规则表附录 | +| `docs/PRD/PRD-风控监测Agent.md` | **已冻结(v1.1)** | 风控 PRD v1.0 + v1.1 追加 FR-8/9/10(§4A)+ 规则表附录 | +| `docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md` | **已定稿** | C4~C6 编码依据(经独立 AI 评审修订闭环);分支/进度速览另见项目根 `交接文档.md` | | `docs/项目框架设计/表设计/` | 已定 | Agent 共用 11 表 + agent 专用 SQL | | `docs/项目框架设计/Core模拟底座/` | 已定 | 无真实 Core 时的 L0 方案 | | `web/` | **不存在** | 前端 React 待 init | @@ -52,23 +53,33 @@ 5. mysql … < scripts/agent/seed-aml-list.sql # AML 名单种子 (风控演示:scripts/demo/prepare_risk_demo.sql,reset 后重跑) 6. python scripts/sync/sync_advisor_rel.py && python scripts/sync/sync_neo4j.py -7. uvicorn app.main:app --reload → GET /health;python -m pytest(321 绿) +7. uvicorn app.main:app --reload → GET /health;python -m pytest(406 用例,系统 Python 3.13.14) ``` > **本机 ①~⑤已执行、`.env` 已配置,勿重做**;本机状态与已知坑(mysql.exe 路径 / reset.ps1 交互式 -p / `redis`、`python-jose` 包漏装已补)见 `FLOW.md` §0 尾注。 > **演示库状态(2026-09-07)**:已按演示 SOP §2 重灌并清理冒烟数据(alerts 0 / AML 8 / 会话 0),pytest 全量可直接跑;**下次演示/走查后再按 SOP §2 重灌**(核查单⑥前置断言会对走查残留显式 fail,属防护行为)。 -**下一步开发(见 TODO):** ~~T-21 Milvus~~(已完成 2026-09-07,425 绿,待独立 AI 评审)/ 前端 React 多 Agent 入口。演示走查按 `docs/项目框架设计/演示SOP-风控模块.md`(debug 头通道仍有效;JWT 通道签发用 `scripts/dev/issue_dev_token.py`)。知识库入库:`python scripts/kb/build_kb.py`(先启 Ollama;**Milvus 数据路径必须纯英文**——faiss 不支持中文路径,本机 .env 已配 C:/Users/YUAN/.jinrong/milvus/)。 +**下一步开发(见 TODO):** 阶段一验收门(用户浏览器目视确认,pytest 406 绿 + 冒烟已通过)→ AL-09 合并 main → AL-10 PRD v1.2 → AL-11 docx 登记 → 阶段二 C4~C6 追加需求编码(FR-8/9/10 已并入 PRD v1.1 · 实现方案《docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md》定稿,AL-05 换核后 SuitabilityResult 字段名需同步核对)/ 前端 React 多 Agent 入口。演示走查按 `docs/项目框架设计/演示SOP-风控模块.md`(debug 头通道仍有效;JWT 通道签发用 `scripts/dev/issue_dev_token.py`;**演示库已按 AL-08 expires_at 新口径重灌**)。知识库入库:`python scripts/kb/build_kb.py`(先启 Ollama;**Milvus 数据路径必须纯英文**——faiss 不支持中文路径,本机 .env 已配 C:/Users/YUAN/.jinrong/milvus/)。 **禁止(改代码前必记):** Core 正式 C1~C5 不可被画像覆盖 · 审计表只 INSERT · 代理人草稿不外发 · 仅 R-02 可阻断交易 · 四 Agent 不互调 LLM。 +**里程碑 tag 中文对照(tag 本体保留英文原名——git 技术标识符需跨平台/脚本稳定引用,符合「技术字面量保留英文」约定;中文名经对照表统一):** + +| tag | 中文名 | 状态 | +| --- | --- | --- | +| `risk-m1` | 阶段A · 适当性校验可用 | 已补打(2026-09-07,指向阶段 A 完成提交 `3c07de6`,历史归档) | +| `risk-m2` | 阶段B · 事件线全链路 | 已打(2026-09-07,附注含验收走查记录) | +| `risk-m3` | 阶段C前半 · 对话线闭环 | 已打(2026-09-07,指向 C1~C3 完成提交) | +| `risk-m4` | 追加需求 · C4~C6(FR-8/9/10) | 拟(完成后打,名待拍板) | +| `risk-align1`(拟) | 阶段一 · 对齐 main 基准 | 待定(AL-01~08 已完成,是否为此打 tag 由用户拍板;当前仅 AL 逐项 commit) | + ------ ## 1. 项目简介 - **名称:** JinRong 金融四 Agent 智能管家 -- **当前阶段:** **T-21 知识库入库已完成(2026-09-07:embedding/milvus_service/rag_service/kb_tools 四层 + 6 产品手册 24 块真库入库,search_knowledge("基金申购") TopK 验收通过,425 绿,待独立 AI 评审)**;风控阶段 C 完成(M3 tag risk-m3,345 绿);T-03 输入防护完成(378 绿,评审 PASS with findings P0=0);下一批 = 前端 React 入口 -- **当前优先级:** 前端 React 入口 → Wave 1(T-21 已完成) +- **当前阶段:** **阶段一「对齐 main 基准」AL-01~AL-08 已完成(2026-09-07,commit bb244f4~b5fd52e,406 绿 + 冒烟通过,待用户目视确认验收门)**;T-21 知识库入库已完成并评审闭环(2026-09-07,425 绿基线,commit 9cc2e59);风控阶段 C 完成(M3 tag risk-m3,345 绿);T-03 输入防护完成(378 绿,评审 PASS with findings P0=0);追加需求 FR-8/9/10 已并入 PRD v1.1,实现方案(C4~C6)已定稿并经独立 AI 评审修订闭环,编码未开始 +- **当前优先级:** 阶段一验收门(用户目视确认)→ AL-09/10/11 → 阶段二 C4~C6 → 前端 React 入口 ------ diff --git a/docs/memory/REQUIREMENTS.md b/docs/memory/REQUIREMENTS.md index f63bf79..395feac 100644 --- a/docs/memory/REQUIREMENTS.md +++ b/docs/memory/REQUIREMENTS.md @@ -37,15 +37,18 @@ ## Wave 2 · 风控 P0 -> 详细需求见 **`docs/PRD/PRD-风控监测Agent.md`(v1.0 已冻结)**;范围含 R-04(P1 顺手做)、R-05 仅预留 L3 接口。 +> 详细需求见 **`docs/PRD/PRD-风控监测Agent.md`(v1.0 冻结 + v1.1 追加 FR-8/9/10)**;范围含 R-04(P1 顺手做)、R-05 仅预留 L3 接口。 | ID | 需求 | 验收对照 | 状态 | TODO | | --- | --- | --- | --- | --- | -| R-01 | 大额预警 | risk_alert pending_review | **已实现**(B1~B6,A-1/A-2 用例绿) | T-30 | -| R-02 | 适当性阻断请求 | 唯一阻断场景 | **已实现**(suitability SUIT-001~008 + check API 带鉴权 + 网关阻断,B5/B6;MySQL 手工 SQL 对照挂账 B9b) | T-31 | -| R-03 | AML 命中通知 | 不自动冻户 | **已实现**(aml_service + scan API + L3 联动;对话通知线待 T-32 阶段 C) | T-32 | -| R-04 | 频繁/试探模式预警 | 聚合出单 + 去重 | **已实现**(B1 规则 + B2 聚合去重) | T-30 | -| R-05 | L3 画像写入 | 最小写入(最高档合并) | **已实现**(profile_l3 防降级;`risk_score` 一期保持 NULL 归评分模型首写) | T-30 | +| R-01 | 大额预警 | risk_alert pending_review | **已实现**(B1~B6,A-1/A-2 用例绿) | ~~T-30~~ | +| R-02 | 适当性阻断请求 | 唯一阻断场景 | **已实现**(~~SUIT-001~008~~ **阶段一换核**:main 的 core_ro.check_suitability C×R 矩阵 + match_result 五值/JR-AST-012/FM-01/FM-03/JR-AST-PRO 契约 + check API 带鉴权 + 网关阻断;risk_suitability_log 21 列落库;MySQL 手工 SQL 对照挂账 B9b) | ~~T-31~~ | +| R-03 | AML 命中通知 | 不自动冻户 | **已实现**(aml_service + scan API + L3 联动;对话线随 C1~C3 完成,A-6 验收通过) | ~~T-32~~ | +| R-04 | 频繁/试探模式预警 | 聚合出单 + 去重 | **已实现**(B1 规则 + B2 聚合去重) | ~~T-30~~ | +| R-05 | L3 画像写入 | 最小写入(最高档合并) | **已实现**(profile_l3 防降级;`risk_score` 一期保持 NULL 归评分模型首写) | ~~T-30~~ | +| FR-8 | 客户资产配置集中度预警(RISK-006) | A-10:R4+R5 占比 ≥ 阈值出单(并入事件类聚合);对话线只读展示 | **待编码**(实现方案已定稿,任务 C4) | C4 | +| FR-9 | 预警单处置时效监控与自动升级(RISK-007) | A-11:超时分级升级(payload 承载不改 status)+ 幂等 + 降噪 | **待编码**(P0 红线;任务 C5,前置 = risk_manager 角色联动) | C5 | +| FR-10 | 代理人异常行为链识别(RISK-008) | A-12:条件 A/B/C 触发 + 代理人维度出单(payload.actor_id)+ 明细仅 officer/manager 可见 | **待编码**(任务 C6,前置 = trade_gateway actor 透传) | C6 | ## Wave 3 · 客户 P0 diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index 0017ad7..cd77d56 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -5,12 +5,29 @@ ## 进行中 -(无——T-21 已完成待独立 AI 评审;评审通过后下一项 = 前端 React 多 Agent 入口) +**阶段一「对齐 main 基准」AL-01~AL-08 已全部完成(2026-09-07,逐项独立 commit bb244f4~b5fd52e)**,验收门进度:pytest 全量 **406 passed 0 failed 0 skipped**(含真库集成)✓ · uvicorn 冒烟三端点(/health · /api/simulate/trade SUIT_RISK_MISMATCH+rule_refs · /api/risk/suitability/check 新契约)✓ · risk-m1 补打 ✓(指向 3c07de6)——**唯一未完成项:用户浏览器目视确认 UI**。确认通过后按 AL-09 → AL-10 → AL-11 收尾,再进阶段二 C4~C6。 ## 待办(推荐顺序) +- [ ] **【阶段一验收门】用户浏览器目视确认 UI**(唯一卡点;pytest 406 绿 + 冒烟均已自动通过) +- [ ] **【阶段一收尾】AL-09 合并 main 分支**(9 文件冲突按修改报告 AL-09 口径解决:memory 文档以我方为准,吸收 main 新增资料;02-seed-base 两方账号并存) +- [ ] **【阶段一收尾】AL-10 PRD v1.2 修订**(SUIT-001~008 编号体系退役登记,用户拍板②授权) +- [ ] **【阶段一收尾】AL-11 `01-客户.docx` 11~16 条读取登记**(用户拍板⑤:用工具读 docx) +- [ ] **【阶段二】C4~C6 追加需求编码**(实现方案已定稿;AL-05 重构后 SuitabilityResult 字段名需同步核对)——前置:阶段一验收门通过 +- [ ] **【阶段二前置】**`python -m pytest` 全量(系统 Python 3.13.14)确认基线绿 +- [ ] **【阶段二】C5 前置联动**(挂账 #6):seed STAFF-31001/31002 + AGENT_ACCESS_MATRIX 增补 risk_manager + chat.py 显式 deny(对话线维持仅 risk_officer)+ risk.py 台账 manager 只读分支 + JWT 手册 §5.3 +- [ ] **【阶段二】C4 RISK-006 集中度**(FR-8):rules(rule_concentration + RuleHit.alert_subtype)+ core_ro.concentration_profile + engine 接入 + alert_service(锚点排除 agent_behavior 单【评审 P0-1】+ subtype 合并 + alert_type 重算)+ customer_context 扩展 + settings 11 项配置;验收 A-10 +- [ ] **【阶段二】C5 RISK-007 时效升级**(FR-9):escalation_service + scripts/cron/escalation_scan.py + query_overdue_alerts Tool + conftest 回拨 fixture;验收 A-11 +- [ ] **【阶段二】C6 RISK-008 代理人行为链**(FR-10):trade_gateway actor_id 透传(前置)+ agent_behavior_service + scripts/cron/agent_behavior_scan.py + query_agent_behavior Tool;验收 A-12 +- [ ] **【阶段二收尾】**演示 SOP 补代理人扮演步骤 + 02-redis-keys.md 增补 + 挂账 #1~#9 核对;tag 名待拍板(risk-m3 已被 C0~C3 占用,建议 risk-m4) +- [ ] 前端 React 多 Agent 入口(HashRouter,`web/` init) + +- [x] **【阶段一】对齐 main 基准 AL-01~AL-08 —— 完成(2026-09-07,逐项独立 commit `bb244f4`~`b5fd52e`)**:AL-01 Core 表结构+矩阵表(core_suitability_rule 25 行 + is_hnw + 风评七新列 expires_at/investor_category + 33 客户/14 产品种子,保留 STAFF-90001)· AL-02 risk_suitability_log 重建 21 列(match_result 五值/mismatch_type 七值/block_response_code 八值/check_source 四值/rule_refs JSON + 6 索引;tests/_ddl.py 同步)· AL-03 core_ro 融合(check_suitability C×R 数据驱动 + list_products_for_customer + list_holdings 合并 + list_trades;CURDATE() 改 Python 端 `_is_expired`/`_as_date` sqlite 兼容;保留我方骨架与风控扩展)· AL-04 model/suitability 引入(build_suitability_log_row + compute_rule_refs 公共函数防漂移)· AL-05 suitability 服务换核(SUIT-001~008 判定内核 → check_suitability;判定链 not_found→FM-03 过期→JR-AST-PRO 豁免→JR-AST-012 矩阵 forbidden→披露→FM-01 网点确认;SuitabilityResult 扩新字段+兼容层;settings.risk_assessment_valid_days 退役改 expires_at;insert_suitability_log 21 列;check_source r02_trade/manual + actor_id 透传;NotFoundError 改结构化)· AL-06 chat_tools.suitability_check 返回体新枚举 + tool_service 文案重写(check_source='r02_chat')· AL-07 test_suitability 按 main 契约重写(七判定路径正反例 + compute_rule_refs 四类组合 + 21 列落库端到端 + MySQL 方言冒烟)· AL-08 演示 SQL expires_at 口径改写(测评 90 天前/剩余 275 天,CUST-1004 保持过期)+ SOP 同步 + 网关阻断响应补全契约 + 双库重灌。**验证:406 passed 0 failed 0 skipped(sqlite + 真 MySQL 集成);uvicorn 冒烟三端点通过;真库 risk_suitability_log 按 21 列 DDL 重建(旧表 RENAME 备份 bak_20260907 零数据丢失)** +- [x] **【阶段一拍板项】六项待确认全部落定(2026-09-07 用户拍板)**:①SUIT-006 退役以 main FM-01 为准 ✓ ②SUIT-001~008 编号体系退役授权 PRD v1.2 ✓ ③risk_assessment_valid_days 退役改 expires_at ✓ ④risk-m1 补打 ✓(已指向 3c07de6)⑤docx 用工具读取 ✓ ⑥合并 main 时机=阶段一验收门后 ✓ +- [ ] 待办提醒:chat 链路 risk_suitability_log.actor_id 暂落 SYSTEM(run_tool 统一透传需动 core_tools/kb_tools 冻结 func 契约,发起者留痕已由 agent_tool_call 覆盖)——AL-06 遗留,阶段二顺带评估 + - [x] T-04 Core RO 封装为 Tool 节点;A-01 归属校验(chat Tool 接入 + agent_tool_call 落库)—— **完成(2026-09-07)**:`app/tool/core_tools.py`(L0/持仓/流水三只读 Tool + TOOL_REGISTRY 白名单)+ `app/service/tool_service.py`(关键词意图仅 customer/advisor;归属校验对齐 deps.assert_customer_access 口径——Tool 层拒绝不抛 403 而 blocked 留痕,纵深防御;run_tool 执行+落库,落库失败降级)+ agent_service 图 tool 节点(START→tool→llm→guard;结果注入 LLM 上下文,降级回复带摘要)+ session_repository.insert_tool_call(message_id 一期 NULL)。**A-01 口径:本任务只做 Tool 层归属校验语义(customer 本人/advisor assigned/risk_officer 全量/其余拒),A-01 完整场景(代理人画像)仍归 T-20**;risk 分支四 Tool 归 C1 复用 run_tool。**评审闭环(2026-09-07):首评 P1×2+P2×3+P3×2 → 修复 `40a9ee2` → 复审 P2×1+P3×4 → 修复 `1216d26`,pytest 321 绿;入参白名单+days 钳制、公共鉴权双写 `utils/authz.py`、list_holdings LIMIT 均落地** -- [x] T-21 Milvus Lite + kb_product_rules 首批入库 —— **完成(2026-09-07,commits e34973f~535c8a0 共 6 个)**:`app/service/embedding.py`(Ollama bge-m3 批量 /api/embed 1024 维,四类失败一律 EmbeddingError 不静默降级)+ `app/service/milvus_service.py`(kb_product_rules 幂等建集合+upsert+search_kb 溯源/effective_date 合规过滤)+ `data/kb/` 6 只种子产品手册(prospectus/fee/rule/risk 四节 24 块)+ `scripts/kb/build_kb.py`(--dry-run/--kb-dir)+ `app/service/rag_service.py` 落地(search_knowledge→chunks+source_refs,异常上抛防 LLM 编造)+ `app/tool/kb_tools.py` 挂注册表第三层(**ToolSpec 新增 skip_access_check**:公开知识无归属语义,advisor 无绑定客户可查;意图层限定 customer/advisor,risk 不开放——用户拍板)+ tool_node 按 spec 白名单注入 query + summarize 溯源口径。**真库验收:24 块入库,search_knowledge("基金申购") Top3 命中申赎规则(score 0.73+),溯源完整**。三坑收口:Milvus Lite 跨进程重开须显式 load / faiss 不支持中文路径(本机 .env 改 C:/Users/YUAN/.jinrong/milvus/)/ build_kb sys.path 引导。知识库 API(上传/重建端点)一期不做(knowledge.py 仍空壳,用户拍板),425 绿 +- [x] T-21 Milvus Lite + kb_product_rules 首批入库 —— **完成(2026-09-07,commits e34973f~535c8a0 共 6 个 + 评审闭环 9cc2e59)**:`app/service/embedding.py`(Ollama bge-m3 批量 /api/embed 1024 维,四类失败一律 EmbeddingError 不静默降级)+ `app/service/milvus_service.py`(kb_product_rules 幂等建集合+upsert+search_kb 溯源/effective_date 合规过滤)+ `data/kb/` 6 只种子产品手册(prospectus/fee/rule/risk 四节 24 块)+ `scripts/kb/build_kb.py`(--dry-run/--kb-dir)+ `app/service/rag_service.py` 落地(search_knowledge→chunks+source_refs,异常上抛防 LLM 编造)+ `app/tool/kb_tools.py` 挂注册表第三层(**ToolSpec 新增 skip_access_check**:公开知识无归属语义,advisor 无绑定客户可查;意图层限定 customer/advisor,risk 不开放——用户拍板)+ tool_node 按 spec 白名单注入 query + summarize 溯源口径。**真库验收:24 块入库,search_knowledge("基金申购") Top3 命中申赎规则(score 0.73+),溯源完整**。三坑收口:Milvus Lite 跨进程重开须显式 load / faiss 不支持中文路径(本机 .env 改 C:/Users/YUAN/.jinrong/milvus/)/ build_kb sys.path 引导。知识库 API(上传/重建端点)一期不做(knowledge.py 仍空壳,用户拍板),425 绿 - [x] T-03 输入防护规则(F-03:prompt_injection/oversize/限流)—— **完成(2026-09-07,commits 497554d/65b45aa/cc6f34f/207fc79)**:`app/service/input_guard.py`(注入短语词表 42 条四类纯函数检测 + `inspect_message` oversize 4000 先于 injection + `check_rate_limit` Redis INCR 固定窗口 actor 级 30 次/分 fail-open)+ chat 路由接入(顺序:鉴权→准入→空白→限流 429→注入/超长 400→归属→会话;被拒 fail-fast 不建会话不落消息,blocked 落 input_guard_log,留痕降级不改拒绝语义;Pydantic 硬顶 20000,业务限 4000 移 guard 层换留痕)+ redis_gateway.incr + settings.guard_rate_limit_*;拍板:命中即拒不清洗、仅 blocked 留痕、词表代码常量可审计。**独立 AI 评审 PASS with findings(P0=0;P2×3 已收口:词表断言修正/补 7 条同构变体/窗口 TTL 断言+EXPIRE 失败取舍锁定;P3 已收 session_id 截断,挂账对抗变体扩表)**,378 绿 - [x] 风控阶段 C(C0 pytest-asyncio / C1 chat_tools / C2 risk 分支 StateGraph / C3 A-6 验收)—— **完成(2026-09-07)**:`app/service/risk/chat_tools.py` 四只读 Tool(alert_query/customer_context/suitability_check/aml_lookup)挂 `RISK_TOOL_REGISTRY`;`tool_service.run_tool` 改查 core+risk 统一注册表 `get_registered_tool` 且恒传 `risk_repo`;`_normalize_params` 读 spec 白名单;`match_intent` 按 agent 分组扩 risk 关键词;`agent_service.tool_node` 守卫放宽(`requires_customer=False` 允许无绑定客户,支撑 A-6 全量待审);A-6 验收+诱导处置边界(无处置 Tool、系统提示禁自动处置、诱导只触发只读且预警状态不变);**独立 AI 评审 PASS(P0=0,P2 补 advisor 归属/空 product_id 单测已收口),345 绿,tag `risk-m3`** - [ ] 前端 React 多 Agent 入口(HashRouter,`web/` init) @@ -18,7 +35,7 @@ ### 风控模块(PRD v1.0 已冻结 · `docs/PRD/PRD-风控监测Agent.md`,事件驱动线不依赖 T-07 可先行) - [x] T-30 风控事件线 —— **全部完成(B1~B9b,M2 tag risk-m2)**:规则引擎/预警聚合/L3/AML/交易网关/4 API/演示走查 11/11 PASS + 核查单①~⑥收口,均经独立 AI 评审闭环;scan_all 幂等读后写无锁留痕(接批量时 run_locked) -- [x] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` —— **交付=suitability 服务函数+单测(77 例;check API 随 B6 带鉴权+直调审计);MySQL 手工 SQL 对照(阶段 A 评审 P2-8)2026-09-07 收口:4 组演示组合真库 SQL 推导与函数判定逐项一致(演示 SOP 核查单附行)** +- [x] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` —— **交付=suitability 服务函数+单测(77 例;check API 随 B6 带鉴权+直调审计);MySQL 手工 SQL 对照(阶段 A 评审 P2-8)2026-09-07 收口:4 组演示组合真库 SQL 推导与函数判定逐项一致(演示 SOP 核查单附行)。【2026-09-07 阶段一退役】SUIT-001~008 编号体系已随 AL-05 换核退役,判定内核改为 main 的 core_ro.check_suitability(C×R 矩阵 + JR-AST/FM 契约),测试按 main 契约重写(原 77 例作废,全量基线 425→406)** - [x] T-32 预警台账与人工处置 API + 对话线 —— **台账/处置 API 已由 B6 覆盖并复审通过(A-7/A-9 用例绿);交付=对话线,归阶段 C(C1~C3,T-07 已就绪)** ## 已完成 diff --git a/docs/项目框架设计/修改报告-对齐main基准.md b/docs/项目框架设计/修改报告-对齐main基准.md new file mode 100644 index 0000000..9ba0ac9 --- /dev/null +++ b/docs/项目框架设计/修改报告-对齐main基准.md @@ -0,0 +1,162 @@ +# 修改报告 · 以 main 仓库为需求唯一基准的差距比对与修改方案 + +> 版本:v1.0(2026-09-07)· 依据用户拍板:**main 项目仓库内容 = 项目需求的唯一基准** +> 分支:`risk-control-agent`(当前 67 提交领先)· 基线:425 用例(collect 已验证) +> 比对方法:`git merge-base`(`1ddd44a`,09-05 骨架点)→ main 侧 47 文件 diff 逐一核读 → 与本分支实现逐项对照 +> **执行铁律:本报告阶段一(AL-01~AL-09,对齐 main 基准)全部完成并全量验证通过后,才允许进入阶段二(C4~C6 新增需求编码)。两阶段不得交叉进行。** + +--- + +## 0. 基准概览:main 侧定义了什么 + +main 侧提交(Andrew 3fb0ceb「Enhance suitability assessment and documentation」+ 2c5440b 行业文档 + zhangyong a9521bc 用户手册)重新定义了 **R-02 适当性的完整契约**: + +| 基准载体 | 内容 | +| --- | --- | +| `app/repository/core_ro.py`(+211 行) | `check_suitability()`:SQL 一次查(客户+产品+矩阵表)+ Python 判定;`_suitability_result()` 19 字段返回;`list_products_for_customer()`(C-11);`list_trades()`;`get_customer_l0` 扩列版 | +| `app/model/suitability.py`(新增 55 行) | `build_suitability_log_row()`:check 结果 → risk_suitability_log INSERT 行映射(R-02 P0 契约),rule_refs 体系 JR-AST-012 / FM-01 / FM-03 / JR-AST-PRO | +| `scripts/core/01-ddl.sql` | 新表 `core_suitability_rule`(C×R 矩阵,L0 权威);`core_customer.is_hnw`;`core_customer_risk` 加 investor_category/questionnaire_score/max_loss_tolerance_pct/investment_goal/investment_horizon/professional_approved_at/expires_at | +| `docs/项目框架设计/表设计/01-mysql-共用底座.sql` | `risk_suitability_log` 重建:21 列(加 product_name/investor_category/match_result/mismatch_type/requires_disclosure/needs_branch_confirm/risk_was_expired/block_response_code/check_source/actor_id/rule_refs JSON)+ 6 索引 | +| `docs/项目框架设计/表设计/07-risk_suitability_log说明.md` | P0 字段契约:match_result 五值 / mismatch_type 七值 / block_response_code 八值 / check_source 四值 / §8 P0 验收四条 | +| 种子 SQL 03~06 | 33 客户 / 14 产品 / KYC / C×R 矩阵数据 | +| `docs/memory/REQUIREMENTS.md` | 客户 Agent 允许「适当性匹配说明」(C-07 P1 / C-11 P2);「营销式推荐」细化不做 | +| `docs/需求拆解/用户故事/01-客户.docx` | 用户咨询需求补至 16 条(二进制,待人工读取登记) | + +**main 判定流程(基准语义,替换我方 SUIT-001~008):** +``` +客户/产品不存在 → forbidden / not_found / blocked(SUIT_NOT_FOUND) +→ 风评过期(expires_at < CURDATE(),FM-03)→ risk_expired / blocked(SUIT_RISK_EXPIRED) +→ 专业投资者(investor_category='professional')→ professional_exempt / 放行(SUIT_PROFESSIONAL_EXEMPT) +→ C×R 矩阵查 core_suitability_rule:forbidden 或无记录 → forbidden / blocked(SUIT_RISK_MISMATCH) +→ allowed_with_disclosure 或产品 requires_disclosure → 需披露(SUIT_NEED_DISCLOSURE) +→ 年龄 ≥70 且产品 R3+ → needs_branch_confirm / blocked(SUIT_AGE_CONFIRM,FM-01 网点当面确认) +``` + +--- + +## 1. 差距总览(main 基准 vs 当前实现) + +| # | 维度 | main 基准 | 当前实现(risk-control-agent) | 差距级别 | +| --- | --- | --- | --- | --- | +| 1 | 匹配规则来源 | `core_suitability_rule` 矩阵表(数据驱动、L0 权威) | 代码硬编码「C 序号 ≥ R 序号」(SUIT-001~005) | **重构** | +| 2 | 判定结果粒度 | match_result 五值 + mismatch_type 七值 + requires_disclosure + needs_branch_confirm | 二值 is_matched/blocked | **重构** | +| 3 | 风评有效期 | `expires_at` 列显式判定(FM-03) | `evaluated_at + 365 天`计算(SUIT-008,settings.risk_assessment_valid_days) | **口径替换** | +| 4 | 专业投资者豁免 | professional 全豁免(JR-AST-PRO) | 无此概念 | **新增** | +| 5 | 高龄处理 | age≥70 买 R3+ → **阻断**需网点确认(FM-01) | SUIT-006 age≥70 按 C3 封顶(继续判定) | **语义替换** | +| 6 | 风险揭示书 | allowed_with_disclosure + product.requires_disclosure | 无此概念 | **新增** | +| 7 | 起购金额 | min_subscribe_amount(mismatch_type=min_subscribe,P0 预留) | 无 | **预留** | +| 8 | 机器响应码 | block_response_code 八值 | rule_id(SUIT-00n/SUIT-PASS) | **替换** | +| 9 | 规则编号体系 | JR-AST-012 / FM-01 / FM-03 / JR-AST-PRO(rule_refs JSON 数组) | SUIT-001~008 单值 rule_id | **替换** | +| 10 | risk_suitability_log | 21 列 + 6 索引 | 10 列 | **重建** | +| 11 | 判定落库入口 | model/build_suitability_log_row 统一映射(check_source 四值) | service 内联 10 字段 dict | **重构** | +| 12 | NotFound 语义 | 返回 forbidden/not_found 结构(不抛异常) | 抛 NotFoundError | **行为变更** | +| 13 | 新查询能力 | list_products_for_customer(C-11)、list_trades | 无 | **新增** | +| 14 | Core 种子 | 33 客户 / 14 产品 / KYC / 矩阵数据 | 28 客户 / 12 产品 | **更新** | +| 15 | get_customer_l0 | `c.*` + 风评新列 + risk_is_expired | 固定 8 列 | 扩列(向后兼容) | +| 16 | list_holdings | 无 limit,加 min_subscribe_amount/term_days 列 | limit=500 截断防护(T-04 评审) | 合并两方 | + +--- + +## 2. 修改任务清单(阶段一:对齐 main 基准) + +> 依赖顺序即编号顺序;每项含方案 / 文件 / 优先级 / 验收标准。 + +### AL-01 Core 库表结构对齐(P0 · 阻塞一切) + +- **方案**:`scripts/core/01-ddl.sql` 采纳 main 版(core_customer.is_hnw、core_customer_risk 七新列、新表 core_suitability_rule + FK);`02~06-seed*.sql` 采纳 main 版(33 客户/14 产品/KYC/矩阵);**追加我方已有内容**:core_staff 的 STAFF-90001(risk_demo)等我方种子(main 侧 02-seed-base.sql 无此账号)。 +- **涉及**:`scripts/core/01-ddl.sql`、`02-seed-base.sql`、`03-seed-customers.sql`、`04-seed-holdings.sql`、`05-seed-trades.sql`、`06-seed-nav.sql`、`scripts/core/README.md`。 +- **验收**:`reset.ps1` 全新灌库成功;`SELECT * FROM core_suitability_rule` 有 C×R 25 行矩阵;STAFF-90001/31001 存在性符合预期。 + +### AL-02 agent 库 risk_suitability_log 重建(P0) + +- **方案**:`docs/项目框架设计/表设计/01-mysql-共用底座.sql` 中该表采纳 main 版 21 列定义;同步 `docs/项目框架设计/表设计/07-risk_suitability_log说明.md`(main 版已完整,直接采用)。 +- **涉及**:上两文件 + `tests/_ddl.py`(sqlite 测试库 DDL 同步——**注意 sqlite 无 ENUM,用 VARCHAR + CHECK 约束替代,与现有 _ddl 风格一致**)。 +- **验收**:sqlite 建表成功;手工 INSERT 一行含全部新列成功。 + +### AL-03 core_ro.py 融合(P0 · 冲突主战场) + +- **方案**:以**我方文件为骨架**(保留 get_trade_by_id/sum_trades_on_date/list_trades_range/list_active_customers/concentration_profile 及 utils/db 引擎工厂——main 版自带 create_engine 工厂会破坏统一引擎管理,不采纳),吸收 main 四项: + 1. `get_customer_l0` 换 main 扩列版(c.* + 风评新列 + risk_is_expired) + 2. 新增 `check_suitability` + `_suitability_result`(main 原样引入,**但 SQL 中 `(r.expires_at < CURDATE())` 改为取回 expires_at 后 Python 端计算**——CURDATE() 是 MySQL 专属函数,sqlite 测试库会直接报错;`JOIN core_customer_risk` 保持 main 的 INNER JOIN 语义) + 3. 新增 `list_products_for_customer`(C-11) + 4. `list_holdings` 合并:保留我方 limit=500 截断防护 + 加 main 的 min_subscribe_amount/term_days 列 +- **涉及**:`app/repository/core_ro.py`(唯一文件,两方改动都在此)。 +- **验收**:单测通过(调用方 engine/chat_tools 零改动仍工作);check_suitability 对 sqlite 与 MySQL 双方言可执行。 + +### AL-04 引入 model/suitability.py(P0) + +- **方案**:main 的 `app/model/suitability.py` 原样引入(build_suitability_log_row,55 行,无依赖冲突)。 +- **验收**:单测覆盖四类 mismatch 的 rule_refs 生成(JR-AST-012/FM-01/FM-03/JR-AST-PRO)。 + +### AL-05 service/suitability.py 重构(P0 · 行为核心变更) + +- **方案**:判定内核从「纯函数矩阵」改为「调 core_ro.check_suitability」: + - `suitability_check(customer_id, product_id, ...)` 保持**签名不变**(trade_gateway/risk.py/chat_tools 三个调用方入参不动),内部改走 check_suitability + - 返回类型 `SuitabilityResult` 扩展:新增 match_result/mismatch_type/requires_disclosure/needs_branch_confirm/block_response_code/rule_refs 字段;保留 is_matched/blocked/reasons/block_reason(映射 reason)供旧调用方读取 + - `check_core`/`cap_by_age`/`match_by_matrix` **删除**(SUIT-006 封顶语义被 FM-01 网点确认替换;SUIT-001~005 被 JR-AST-012 矩阵替换;SUIT-008 被 FM-03 expires_at 替换) + - `settings.risk_assessment_valid_days` 退役(改由 core_customer_risk.expires_at 数据驱动);`.env.example` 同步注释 + - NotFoundError 不再抛出:客户/产品缺失 → 返回 forbidden/not_found 结构(main 语义);trade_gateway 的 try/except NotFoundError 相应调整 + - 写库改 `build_suitability_log_row`:check_source 参数化(网关传 r02_trade、Tool 传 r02_chat);actor_id 透传(网关传 "svc-trade-suitability" 或 auth.actor_id) +- **涉及**:`app/service/suitability.py`、`app/config/settings.py`、`.env.example`、`app/gateway/trade_gateway.py`(仅 NotFound 分支 + actor_id/check_source 传参)、`app/api/risk.py`(suitability_check_api 的响应字段扩展:match_result/block_response_code 对外)。 +- **验收**:单测覆盖 main 基准全部判定分支(not_found / risk_expired / professional_exempt / 矩阵 forbidden / allowed / allowed_with_disclosure / age_branch_confirm 七路径);A-1/A-2 等价的阻断场景全部仍阻断。 + +### AL-06 对话 Tool 与 API 响应对齐(P1) + +- **方案**:`chat_tools.suitability_check` 返回体扩展 match_result/mismatch_type/requires_disclosure/needs_branch_confirm/block_response_code(替代现 rule_id/blocked 展示);`tool_service.summarize` 的 suitability 分支文案按新枚举重写(如「需网点当面确认(FM-01)」「需签署风险揭示书」);`risk.py::suitability_check_api` 响应体同步。 +- **涉及**:`app/service/risk/chat_tools.py`、`app/service/tool_service.py`、`app/api/risk.py`。 +- **验收**:对话问「能不能买」返回新枚举语义;A-8 单测更新后绿。 + +### AL-07 测试重建(P0 · 原 77 例断言作废) + +- **方案**:`tests/test_suitability*.py` 按 main 契约重写:七判定路径各正反例 + build_suitability_log_row 逐字段映射断言 + rule_refs 组合(过期+等级不匹配 → ["JR-AST-012","FM-03"])+ sqlite/MySQL 双方言(fixture 注入);`tests/_ddl.py` 同步 AL-01/02 新表;`test_trade_gateway.py`/`test_risk_api.py`/`test_chat_tools.py` 中涉及旧 rule_id(SUIT-00n/SUIT-PASS)的断言改为新枚举。 +- **验收**:`python -m pytest` 全量绿(系统 Python 3.13.14);集成测试真库走新表结构。 + +### AL-08 演示库重灌与种子核对(P0 · 依赖 AL-01/02) + +- **方案**:按新 SQL 全量重灌 jinrong_core + jinrong_agent(演示 SOP §2 流程);prepare_risk_demo.sql 核对(CUST-4001 测评等断言在新列下仍成立——**expires_at 取代 evaluated_at+365 后,演示 SQL 的测评日期写法要改写 expires_at**);AML 名单 8 条保持。 +- **验收**:演示 SOP 核查单①~⑥通过;集成测试不 skip。 + +### AL-09 文档基准同步(P1 · 合并后立即做) + +- **方案**:与 main 分支实际合并时(git merge main)按以下口径解决 9 个冲突文件:memory 六件套 + AGENTS.md **以我方为准**(我方是 425 绿现状的权威记录),把 main 侧新增实质内容吸收进 REQUIREMENTS(C-07/C-11 行、营销式推荐细化、33 客户/14 产品口径);`02-seed-base.sql` 手工融合两方账号;`core_ro.py` 按 AL-03 融合结果落定。PRD/规则表修订单列 AL-10。 +- **验收**:merge 完成后工作区 clean;pytest 全量绿;文档 grep 无 feature/risk 残留、无 SUIT-006 残留口径。 + +### AL-10 PRD/规则表修订(P1 · 需用户确认后执行) + +- **方案**:PRD v1.1 → v1.2:FR-2/§7 的 SUIT-001~008 体系替换为 main 契约(矩阵表驱动 + match_result 五值 + FM-01/FM-03/JR-AST-PRO/JR-AST-012 + block_response_code);§8 验收表 A-1/A-2/A-8 的 rule_id 断言改为新枚举;附-风控规则表 §1「SUIT-001~008」整节替换(SUIT-006 封顶 → FM-01 网点确认;SUIT-008 过期 → FM-03)。**此为需求基准变更,需用户逐条确认后落笔。** +- **验收**:PRD v1.2 冻结;规则表 v1.2;A-1/A-2/A-8 测试与文档断言一致。 + +### AL-11 新增需求登记(P2) + +- **方案**:REQUIREMENTS Wave 3 补 C-07(T-41)/C-11(T-42)两行(main 口径:客户 Agent 适当性匹配说明,须联动 R-02,非投顾推荐);`docs/需求拆解/用户故事/01-客户.docx` 的 11~16 条需求人工读取后登记进 Wave 3 或 P1 清单(docx 为二进制,需人工打开确认)。 +- **验收**:REQUIREMENTS 与 main 基准无需求遗漏。 + +--- + +## 3. 执行顺序(强制) + +```text +阶段一 · 对齐 main 基准(先完成已实现功能的全部修改并确认无误) + AL-01 表结构 → AL-02 agent 表 → AL-03 core_ro 融合 → AL-04 model 引入 + → AL-05 suitability 重构 → AL-06 Tool/API 对齐 → AL-07 测试重建 + → AL-08 重灌演示库 → 【阶段一验收门】python -m pytest 全量绿 + + uvicorn 冒烟(/health + /api/simulate 阻断路径 + /api/risk/suitability/check) + + 用户浏览器目视确认 → git commit(每个 AL 独立 commit) +阶段二 · 新增需求(阶段一验收门通过后才允许开始) + C5 前置联动 → C4(RISK-006)→ C5(RISK-007)→ C6(RISK-008) + (按《实现方案-风控追加需求v1.1-C4C6.md》执行,方案需按本报告微调处: + ① C4 concentration_profile 基于 AL-03 融合后的 list_holdings(含新列); + ② AL-05 重构后 suitability_check 返回结构变化,C4/C6 方案中引用 SuitabilityResult + 字段名的地方同步核对) +``` + +**注意**:AL-09(分支合并)建议在阶段一验收门之后、阶段二之前执行——先在我方分支完成适配实现并验证,再合 main 的文档/资料,避免双线作战。若团队要求先合并,则 AL-03 的融合工作在合并冲突解决中一并完成。 + +## 4. 待人工确认事项 + +1. **SUIT-006 封顶语义正式退役**(改 main 的 FM-01「阻断+网点确认」)——两个口径对 70 岁客户结论相反(我方放行 C3 内产品,main 一律阻断待确认),确认以 main 为准。 +2. **SUIT-001~008 编号体系退役**——PRD/规则表/测试断言全面改 JR-AST/FM 体系,PRD 升 v1.2 需授权。 +3. **settings.risk_assessment_valid_days 退役**——365 天有效期改为 core_customer_risk.expires_at 数据驱动(.env 不再可配)。 +4. risk-m1 是否补打(阶段 A 完成时漏打)。 +5. `docs/需求拆解/用户故事/01-客户.docx` 11~16 条需人工打开读取后登记。 +6. 实际合并 main 的时机(建议阶段一验收门后)。 diff --git a/docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md b/docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md new file mode 100644 index 0000000..f24c25b --- /dev/null +++ b/docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md @@ -0,0 +1,543 @@ +# 实现方案 · 风控追加需求 v1.1(FR-8 / FR-9 / FR-10 → C4~C6) + +> 依据:PRD v1.1 §4A(已冻结并入)· 附-风控规则表 v1.1 · 开发计划 v1.2 C4~C6 任务行 + 挂账 #1~#9 +> 分支 `risk-control-agent`(旧称 feature/risk 已过时)· 基线:T-21 完成后(425 用例 collect 已验证,全量执行绿待跑) +> 本文为编码依据,函数签名/文件路径可直接照抄;与 PRD 冲突时以 PRD 为准。 + +> **评审修订记录(v1.1 · 2026-09-07)**:经独立 AI 评审 FAIL → 修订本文。P0-1 C4 聚合锚点与 C6 行为链单碰撞(§2.1/§2.5 锚点排除 agent_behavior);P1-1 升级 payload 写入统一读改写(§3.2);P1-2 回归机制改 autouse monkeypatch settings + `from_settings` 补读字段(§2.3/§6.2);P1-3 追加时 alert_type 按合并后规则重算(§2.5);P1-4 L2 推送通知链累积(§3.3);P2-1~P2-6 顺手收口(§2.2/§3.5/§4.2/§4.3/§6.2)。 + +--- + +## 0. 三个需求一句话 + 落库前提核实结论 + +| 需求 | 规则 | 一句话 | 触发方式 | 优先级 | +| --- | --- | --- | --- | --- | +| FR-8 | RISK-006 | 客户持仓 R4+R5 市值占比 ≥ 阈值(默认 80%)→ 并入当日事件类预警单 | 交易落库后**同步**(引擎内追加) | P1 | +| FR-9 | RISK-007 | pending 单超时(4h/24h,AML 1h/4h)→ payload 写升级标记 + 推送升级,**不改 status** | **定时扫描** 15 分钟 | **P0** | +| FR-10 | RISK-008 | 代理人行为链(A 诱导调仓 / B 越权试探 / C 越权查询)→ 代理人维度独立预警单 | **定时扫描** 30 分钟 | P1 | + +**DDL 核实结论(决定方案形态,均已确认):** + +| 项 | 结论 | 对方案的影响 | +| --- | --- | --- | +| `risk_alert.alert_type` | ENUM 五值,无 concentration/agent_behavior | 一律落 `pattern` + `payload.alert_subtype` 区分 | +| `risk_alert.status` | ENUM 四值,无 `escalated` | 升级信息全由 `payload.escalation_level/escalated_at` 承载 | +| `risk_alert.payload` | JSON NOT NULL | 直接写 dict(repo `_dump_alert` 已序列化) | +| `audit_log.event_type` | **VARCHAR(64)**(非 ENUM) | `risk_concentration`/`alert_escalation`/`agent_behavior_detected` 三个新事件类型**可直接写入,无需改表** | +| `core_product.min_risk_code` | CHAR(2),取值 R1~R5 | RISK-006 判定 `min_risk_code in ("R4","R5")`(sqlite 测试 DDL 为 VARCHAR(8),同口径兼容) | +| `agent_session`/Agent 准入 | `AGENT_ACCESS_MATRIX`["risk"].roles 现为 ("risk_officer","service_risk") | risk_manager 需增补(见 §3.3,注意对话线例外) | + +--- + +## 1. 模块与文件划分总表 + +### 1.1 新增文件(6 个) + +| 文件 | 职责 | 所属任务 | +| --- | --- | --- | +| `app/service/risk/escalation_service.py` | RISK-007 扫描 + 升级决策 + 幂等 payload 写入 + 降噪推送(纯 service,可被脚本/测试直调) | C5 | +| `app/service/risk/agent_behavior_service.py` | RISK-008 滑动窗口聚合 audit_log + 行为链判定 + 出单(纯 service) | C6 | +| `scripts/cron/escalation_scan.py` | 定时任务壳:sys.path 引导 → `new_trace()` → 调 escalation_service → 打印摘要(15min 周期挂系统 cron) | C5 | +| `scripts/cron/agent_behavior_scan.py` | 同上(30min 周期) | C6 | +| `tests/test_escalation_service.py` | C5 单测(含幂等/边界/降噪) | C5 | +| `tests/test_agent_behavior_service.py` | C6 单测(A/B/C 边界 + 本人排除 + 去重) | C6 | + +### 1.2 修改文件(10 个) + +| 文件 | 改动 | 所属任务 | +| --- | --- | --- | +| `app/config/settings.py` | 追加 11 个配置项(§5) | C4~C6 | +| `.env.example` | 同步登记(含注释) | C4~C6 | +| `app/service/risk/rules.py` | `RuleHit` 加 `alert_subtype` 字段;新增 `rule_concentration` 纯函数;`RiskThresholds` 加 `concentration_threshold` | C4 | +| `app/service/risk/engine.py` | `process_trade_event` 追加持仓规则分支(RISK-001~005 之后、AML 之前) | C4 | +| `app/service/risk/alert_service.py` | `record_trade_alerts` 维护 `payload.alert_subtype`;`_publish_alert` 支持覆盖 notify_role/附带 subtype | C4(C5/C6 复用) | +| `app/repository/risk_repository.py` | 新增 `append_alert_subtypes` / `update_alert_escalation` / `list_pending_alerts_all` / `find_agent_behavior_alert` / `list_audit_events` | C5/C6 | +| `app/repository/core_ro.py` | 新增 `concentration_profile(customer_id, limit=500)`(R4+R5 聚合一次 SQL 搞定,避免 Python 端 N+1) | C4 | +| `app/api/deps.py` | `AGENT_ACCESS_MATRIX["risk"]["roles"]` 增补 `risk_manager` | C5 | +| `app/api/risk.py` | `list_alerts_api` 加 risk_manager 全量只读分支;`chat` 相关不动 | C5 | +| `app/api/chat.py` | risk 分支准入后加显式守卫:roles 含 `risk_manager` → deny(对话线维持仅 risk_officer) | C5 | +| `app/service/risk/chat_tools.py` | `customer_context` 扩展 `concentration_ratio`;新增 `query_overdue_alerts` / `query_agent_behavior` 两个 Tool + 注册表条目 | C4/C5/C6 | +| `app/service/tool_service.py` | `_INTENT_KEYWORDS["risk"]` 追加两条意图词组;`summarize` 加三个 Tool 摘要分支 | C4/C5/C6 | +| `app/gateway/trade_gateway.py` | `submit_trade` 加可选参数 `actor_id`,透传到 `_audit`(C6 条件 A 数据源前提) | C6 | +| `app/api/simulate.py` | 调 `submit_trade` 时传 `actor_id=auth.actor_id` | C6 | +| `scripts/core/02-seed-base.sql` | 追加 STAFF-31001/31002(staff_type='risk_officer',roles='["risk_manager"]') | C5 前置 | +| `tests/conftest.py` | 新增 `backdated_alert` / `backdated_audit_event` fixture(免等待测试基建) | C5/C6 | + +> `chat_tools.py` / `tool_service.py` 三个任务都碰,按 C4→C5→C6 顺序合并提交,避免冲突。 + +--- + +## 2. C4 · RISK-006 集中度预警(FR-8) + +### 2.1 数据流转 + +```text +交易落库 → engine.process_trade_event + ├─ run_rules(RISK-001~005) (现有,不动) + ├─ [新增] core.concentration_profile(customer_id) + │ → rule_concentration(profile, th) → RuleHit(alert_subtype="concentration") + │ 命中 → 并入 hits,走既有 record_trade_alerts(聚合锚点 = find_pending_event_alert + │ **排除 payload.alert_subtype 含 'agent_behavior' 的单**——客户维度事件单与 + │ 代理人维度行为链单是两条出单线,聚合并存、互不影响,PRD 4A.0 #3; + │ 评审 P0-1 收口,见 §2.5 锚点过滤) + │ + L3:upsert_profile_l3(customer_id, "pattern", monitor_tags=["high_risk_concentration"]) + │ + 审计:event_type='risk_concentration'(金额 DESENS-005 截断) + └─ match_customer(AML) (现有,不动) +``` + +关键点:RISK-006 **不新建出单路径**——并入 `record_trade_alerts` 既有聚合,`risk_score=60` 经 `max()` 语义不覆盖 RISK-001/002 的 70;`alert_type` 随最高分规则动态更新的现有逻辑不变。 + +### 2.2 core_ro 新增:`concentration_profile`(挂账 #1 收口) + +```python +def concentration_profile(self, customer_id: str, limit: int = 500) -> dict[str, Any]: + """持仓集中度画像(RISK-006 专用,一次 SQL 聚合,避免 N+1)。 + + 返回 {r45_value: Decimal, total_value: Decimal, holdings_truncated: bool, rows: [...]}。 + rows 仅保留 market_value 降序前 limit 条明细(进审计 input_summary 用摘要,不进 payload 全量)。 + 触及 limit → holdings_truncated=True(调用方按保守口径视同达标,PRD FR-8 截断防护)。 + """ + # SQL:SELECT h.market_value, p.min_risk_code FROM core_holding h + # JOIN core_product p ... WHERE h.customer_id=:cid + # ORDER BY h.market_value DESC LIMIT :lim+1 ← 多取 1 行判定截断 + # Python 端按 min_risk_code in ("R4","R5") 分组求和(Decimal) +``` + +> 采用「SQL 取明细 + Python 聚合」而非 SUM(CASE):明细行还要进审计摘要,且 sqlite/MySQL DDL 差异下 Python 判 `min_risk_code` 最稳。limit+1 探测截断,避免二次 COUNT 查询。 +> 注:PRD FR-8 字面为「调用 `core_ro.list_holdings`」;本方法为挂账 #1 的收口(聚合封装,内部同源查询,市值口径不变),偏离字面已在方法 docstring 注明。 + +### 2.3 rules.py 改动 + +```python +@dataclass(frozen=True) +class RuleHit: + rule_id: str + alert_type: str + risk_score: int + detail: str + alert_subtype: str | None = None # 新增:RISK-006→"concentration",RISK-008→"agent_behavior",RISK-001~005 恒 None + +RULE_SCORES["RISK-006"] = 60 +RULE_ALERT_TYPES["RISK-006"] = "pattern" + +@dataclass(frozen=True) +class RiskThresholds: + ... + concentration_threshold: float = 0.80 # settings.risk_concentration_threshold + +# from_settings() 必须同步补读:concentration_threshold=s.risk_concentration_threshold +# (评审 P1-2:引擎/网关测试全部走 from_settings 默认路径,漏读字段会导致 +# autouse 回归 fixture 的 monkeypatch 失效) + +def rule_concentration(profile: dict[str, Any], th: RiskThresholds) -> RuleHit | None: + """RISK-006 集中度:R4+R5 市值占比 ≥ 阈值(纯函数)。 + + total_value == 0(空仓)不触发;holdings_truncated=True 视同达标(保守告警)。 + detail 含 r45_value/total_value/占比(Decimal→str)。 + """ +``` + +> **不动 `run_rules` 签名**——RISK-001~005 吃 trades、RISK-006 吃持仓画像,输入域不同;引擎分别调用后合并 hits,比改 run_rules 兼容面小(现有 13 处 run_rules 调用/断言零改动)。 + +### 2.4 engine.py 接入点 + +```python +# process_trade_event 内,hits = run_rules(...) 之后: +profile = core.concentration_profile(trade["customer_id"]) # 截断防护在 rule 内判定 +conc = rule_concentration(profile, th) +if conc: + hits = [*hits, conc] # 并入后走 record_trade_alerts 既有聚合/审计/推送 + upsert_profile_l3(trade["customer_id"], "pattern", + monitor_tags=["high_risk_concentration"], + last_alert_id=... ) # 聚合出单后补(同现有 best 分支) + _audit_concentration(repo, trade, profile, conc) # event_type='risk_concentration' +``` + +> 注意顺序:RISK-006 分支放在 `run_rules` 之后、`record_trade_alerts` 之前,使 `record_trade_alerts` 一次调用覆盖合并;L3 upsert 与专属审计放在出单之后(拿 alert_id)。命中 RISK-006 但 RISK-001~005 未命中时,`record_trade_alerts` 同样正常建单(hits 非空即走聚合分支),**无需**为「仅集中度命中」写独立分支。 + +### 2.5 alert_service.py 改动 + +**① 聚合锚点排除代理人维度单(评审 P0-1 收口,必须做)** + +`RiskRepository.find_pending_event_alert` 改为「查候选 + Python 过滤」: + +```python +def find_pending_event_alert(self, customer_id: str, day_start: datetime) -> dict | None: + """当日该客户的事件类 pending 单(聚合锚点)——**排除代理人维度行为链单**。 + + LIMIT 1 会取到最新 pattern 单;若它恰是 agent_behavior 单而更早还有客户维度 + 事件单,会误判无锚点导致当日出第二张客户维度单。故候选取 LIMIT 50 再过滤: + SELECT * FROM risk_alert + WHERE customer_id=:cid AND status='pending_review' + AND alert_type IN (事件类) AND created_at>=:day_start + ORDER BY created_at DESC LIMIT 50 + → Python 过滤:'agent_behavior' not in (payload.get("alert_subtype") or []) → 取最新 + """ +``` + +> 依据 PRD §4A.0 #3:FR-4「同客户同自然日仅一张」只对**客户维度**事件单成立,RISK-008 行为链单(同 pattern 但 `payload.actor_type='agent'`)是独立出单线,两者不得互相并入。C6 的 `find_agent_behavior_alert` 已按 subtype 过滤,天然安全(单向污染只在 C4 侧)。 + +**② alert_subtype 集合维护** + +```python +subtypes = sorted({h.alert_subtype for h in hits if h.alert_subtype}) +# 新建单:仅当 subtypes 非空才写 payload["alert_subtype"] = subtypes(空集不注入该键,评审 P2-3) +# 追加单(锚点命中):risk_repo.append_alert_event(..., extra_subtypes=subtypes) +``` + +**③ 追加时 alert_type 按合并后规则重算(评审 P1-3,既有缺陷顺带修正)** + +现状缺陷:`append_alert_event` 用「本批 best.alert_type」更新——既有 large_amount(70) 单,本批仅命中 RISK-006(60) 时会被错误翻转为 pattern。修法(alert_service 追加分支): + +```python +merged_rules = sorted(set(pending["triggered_rules"]) | {h.rule_id for h in hits}) +best_rule = max((r for r in merged_rules if r in RULE_SCORES), key=lambda r: RULE_SCORES[r]) +risk_repo.append_alert_event(..., risk_score=max(old, 本批max), alert_type=RULE_ALERT_TYPES[best_rule]) +``` + +(`RULE_SCORES`/`RULE_ALERT_TYPES` 从 rules.py 导入——alert_service 已依赖 rules.RuleHit,无新增分层问题。编码后跑全量回归确认无现有断言依赖旧翻转行为。) + +- `RiskRepository.append_alert_event` 加可选参数 `extra_subtypes: list[str] | None = None`:合并进 `payload.setdefault("alert_subtype", [])`(sorted set 并集),老单无该字段时首次追加自动创建。**向后兼容**:不传时行为与现在完全一致,现有用例不受影响。 +- `_publish_alert` 加可选参数 `notify_role: list[str] | None = None, extra: dict | None = None`:缺省保持 `["risk_officer"]`(+aml compliance) 现状;`extra` 合并进推送体(C5 放 `escalation_level`,C4/C6 放 `alert_subtype`)。`02-redis-keys.md` 同步增补字段说明(PRD §13 联动清单)。 + +### 2.6 对话线:`customer_context` 扩展 + +`chat_tools.customer_context` 返回体追加: + +```python +"profile": {"concentration_ratio": 0.9, "r45_value": "...", "total_value": "...", + "holdings_truncated": False} # 复用 core_ro.concentration_profile,脱敏后百分比 +``` + +调用同一聚合函数,不重复实现口径。`tool_service.summarize` 的 customer_context 分支加一句「高风险持仓占比 90%(仅供参考)」。意图词无需新增(customer_context 词组已有)。 + +--- + +## 3. C5 · RISK-007 时效升级(FR-9) + +### 3.1 鉴权与角色联动(**C5 开工前置**,挂账 #6) + +1. **种子**:`scripts/core/02-seed-base.sql` 追加: + +```sql +INSERT INTO core_staff VALUES ('STAFF-31001','风控经理甲','risk_officer','["risk_manager"]',1,...); +INSERT INTO core_staff VALUES ('STAFF-31002','风控经理乙','risk_officer','["risk_manager"]',1,...); +``` + +2. **`app/api/deps.py`**:`AGENT_ACCESS_MATRIX["risk"]["roles"]` → `("risk_officer", "risk_manager", "service_risk")`。理由:HTTP 台账请求经 `get_auth_context` 的交叉校验,不加则 manager 连 GET /api/risk/alerts 都过不去(401 通道 AUTH_403_AGENT_MISMATCH)。 +3. **`app/api/chat.py`(对话线例外)**:risk 分支在 `assert_agent_access` 之后加守卫: + +```python +if "risk_manager" in auth.roles: + deny(auth, "AUTH_403_ROLE", repo, message="对话线仅限 risk_officer,请走 HTTP 台账") +``` + +> 这是对 PRD 4A.1「AGENT_ACCESS_MATRIX 增补行」与「对话线不放行 risk_manager」两条的联合落地:矩阵放行解决 HTTP 通道,chat 层显式拒绝保住 FR-6 冻结口径。Tool 层 `assert_tool_access` 不动(manager 根本进不了对话线,天然 fail-closed)。 + +4. **`app/api/risk.py::list_alerts_api`**: + +```python +if auth.has_role("risk_officer"): + ... # 现有全量 +elif auth.has_role("risk_manager"): + ... # 全量只读(同 risk_officer 查询路径,无处置入口) +elif auth.has_role("compliance"): + ... # 现有 aml 强制过滤 +else: + deny(...) +``` + +`handle_alert_api` 白名单**不动**(manager 无 risk_officer 角色 → 自然 403,PRD「零代码改动」)。JWT 手册 §5.3 增补为文档动作,随 C5 提交。 + +### 3.2 仓储新增(risk_repository.py) + +```python +def list_pending_alerts_all(self, page_size: int = 1000) -> list[dict]: + """全部 pending_review 单(C5 扫描输入;演示规模一次取回,Python 端判级)。 + 复用 _parse_alert。""" + +def update_alert_escalation(self, alert_id: str, escalation_level: int, + escalated_at: datetime, trace_id: str) -> bool: + """payload 升级标记独占写入(RISK-007;status/handler_* 列一律不碰)。 + + 实现:**统一读改写**(评审 P1-1 定案)——SELECT payload → Python 合并 + escalation_level/escalated_at/escalation_trace_id → UPDATE payload。 + 与现有 append_alert_event 同模式(跨 MySQL/sqlite 已验证可行),不引入 + JSON_SET 方言分支;并发窗口由「单发定时脚本 + 幂等闸门(仅升不降)」兜底。 + WHERE alert_id=:aid,返回 rowcount==1。""" +``` + +> 人工处置 API 走 `handle_alert_with_audit`(只写 status/handler_*/handled_at 列),与 `update_alert_escalation` **写集不相交**,互不覆盖——对应 A-11「payload 与人工处置字段互不覆盖」断言。 + +### 3.3 escalation_service.py(核心职责) + +```python +"""预警处置时效升级(C5 · PRD FR-9 / 规则表 v1.1 RISK-007 补充约束)。""" + +def scan_and_escalate( + now: datetime | None = None, + risk_repo: RiskRepository | None = None, + thresholds: EscalationThresholds | None = None, +) -> dict[str, Any]: + """单次扫描入口(定时脚本/测试/手动演示共用)。 + + EscalationThresholds(frozen dataclass, from_settings()): + l1_hours=4, l2_hours=24, aml_l1_hours=1, aml_l2_hours=4 + + 流程: + 1. rows = repo.list_pending_alerts_all() + 2. 逐单计算 computed_level: + overdue_h = (now - created_at).total_seconds()/3600 + aml 单用 aml_l1/l2,普通单用 l1/l2;level = 2 if overdue>=l2 else 1 if overdue>=l1 else 0 + 3. 幂等闸门:computed_level > (payload.escalation_level or 0) 才动作; + 同级/降级一律跳过(每级别至多推送一次,A-11 幂等断言锚点) + 4. 动作顺序(LEVEL_1 与 LEVEL_2 同机制,PRD 拍板「先持久化再推送」): + a. repo.update_alert_escalation(alert_id, level, now, trace_id) + b. 审计 event_type='alert_escalation'(input_summary 含 alert_id/已达级别/ + 超时时长/升级原因;trace 贯通) + 5. 降噪合并:按 (customer_id, level) 分组,同组多单合并为一次推送 + (alert_ids 列表进推送体;审计仍逐单落,不合并) + 6. 推送:redis_gateway.publish("risk:pub:alert", {..., "escalation_level": lvl, + "notify_role": 通知链**累积**(评审 P1-4 修正): + L1 → ["risk_officer", "risk_manager"] + L2 → ["risk_officer", "risk_manager", "compliance"] ← manager 不因升到 L2 而移出 + → 复用 alert_service._publish_alert 的 extra/notify_role 扩展 + 7. 任务级审计一条:decision='scan_completed',input_summary={scanned, escalated, merged, skipped} + 返回 {"scanned": n, "escalated": [...], "merged_notices": m, "skipped": k} + """ +``` + +**边界细则(编码时执行):** +- `created_at` 为 naive datetime(repo 直返),`now` 缺省 `datetime.now()`,同口径相减。 +- AML 判定按 `alert_type == "aml"`;suitability 单**同样纳入**扫描(属 pending_review,超时同样积压——PRD 未排除,从「时效监控」本义)。 +- 升级后单据 status 保持 `pending_review`,仍在下轮扫描范围(level 已达 2,幂等闸门自动跳过);人工处置后自然退出(status ≠ pending_review)。 +- `escalation_level` 读缺省 0:`int((alert["payload"] or {}).get("escalation_level") or 0)`。 + +### 3.4 定时任务脚本 + +`scripts/cron/escalation_scan.py`(结构对齐 `scripts/demo/rebuild_alerts.py`): + +```python +"""RISK-007 处置时效升级扫描(建议 15min 周期;RISK_ESCALATION_SCAN_MINUTES 可配)。 +初期独立脚本 + 系统 cron;内嵌 lifespan 归 M4 评估(开发计划挂账 #2)。""" +# ① sys.path 引导项目根(rebuild_alerts 先例) +# ② from app.utils.trace import new_trace; new_trace() ← 显式生成 trace(无 HTTP 上下文) +# ③ scan_and_escalate() → print JSON 摘要(供 cron 日志/演示走查) +# 审计写库失败:service 内降级 + logger.error 留底(红线 5 口径,不阻塞下次扫描) +``` + +### 3.5 对话 Tool:`query_overdue_alerts` + +```python +def query_overdue_alerts(customer_id, core_ro=None, risk_repo=None, **params): + """超期 pending 单列表(只读;FR-9 API 变更)。 + hours = params.get("hours") # 缺省取 settings L1 阈值 + → repo.list_pending_alerts_all() → 过滤 overdue_h >= hours + → [{alert_id, customer_id, alert_type, created_at, escalation_level(读 payload,缺省0), + overdue_hours(round 1位), risk_score}],按 overdue_hours 降序(对话线查询置顶语义) + """ +# 注册表条目:requires_customer=False(risk_officer 无绑定客户可查全量,C2 守卫先例) +# param_whitelist=("hours",), int_bounds={"hours": (1, 720)} +``` + +意图词组:`("query_overdue_alerts", ("超期", "超时", "逾期", "多久没处理", "处置时效"))`——**置于 risk 组 alert_query 之前**(评审 P2-4:「超时预警」类话术不得误命中台账)。 + +--- + +## 4. C6 · RISK-008 代理人行为链(FR-10) + +### 4.0 前置改造:交易审计 actor 透传(条件 A 数据源前提) + +现状:`trade_gateway._audit` 写 `actor_id="SYSTEM"`(B6 挂账注释),条件 A 无法归属发起人。 + +```python +# trade_gateway.submit_trade 加参数:actor_id: str | None = None +# _audit(..., actor_id=actor_id or "SYSTEM") ← 缺省 SYSTEM,现有测试/脚本零改动 +# app/api/simulate.py:submit_trade(req, ..., actor_id=auth.actor_id) +``` + +演示期 risk_demo 账号(roles 含 risk_demo)扮演代理人发起 → audit_log.trade_request 行的 actor_id = STAFF-90001;客户本人发起时 actor_id == customer_id,扫描时**排除**(防误报,A-12 断言)。 + +### 4.1 仓储新增 + +```python +def list_audit_events(self, event_type: str, since: datetime, actor_id: str | None = None, + decision: str | None = None, limit: int = 2000) -> list[dict]: + """audit_log 滑窗查询(RISK-008 数据源;input_summary 解析回 dict)。 + WHERE event_type=:et AND created_at>=:since [AND actor_id=:aid] [AND decision=:d] + ORDER BY created_at ASC。条件 B/C 用 actor_id+decision='forbidden'; + 条件 A 用 event_type='trade_request' 全量(actor 过滤在 Python 端做,排除 SYSTEM)。""" + +def find_agent_behavior_alert(self, actor_id: str, day_start: datetime) -> dict | None: + """同代理人当日 pending 行为链单(去重锚点)。 + SQL:SELECT * FROM risk_alert WHERE status='pending_review' AND alert_type='pattern' + AND created_at>=:day_start ORDER BY created_at DESC(演示规模可接受); + Python 过滤 payload.alert_subtype=='agent_behavior' and payload.actor_id==actor_id。 + 不用 payload LIKE:JSON 键值歧义 + 序列化空格差异两坑(挂账 #9 登记性能,一期 Python 过滤最稳)。""" +``` + +### 4.2 agent_behavior_service.py(核心职责) + +```python +"""代理人异常行为链识别(C6 · PRD FR-10 / 规则表 v1.1 RISK-008 补充约束)。 + +数据源(定案口径,input_guard_log 不作为数据源): + 条件 A:audit_log(event_type='trade_request') ∩ core_trade 时间线——**join 键 = trade_id** + (评审 P2-2 补细):审计行 input_summary.trade_id 归属发起人 actor_id; + 扫描先取 trade_request 行 → 过滤 actor_id=='SYSTEM'(历史无归属)与 + actor_id==customer_id(本人交易)→ 以行内 trade_id 集合从 core_trade + (list_trades_range 当日窗 + get_trade_by_id 补漏)取 traded_at/trade_type/ + product_id 组装时间线 → 模式判定只在「有归属 actor 的 trade_id 子集」内做 + 条件 B:audit_log(event_type='authz', decision='forbidden') 且 input_summary.code=='AUTH_403_SCOPE' + 条件 C:同 B 族,code IN ('AUTH_403_NOT_OWNER','AUTH_403_NOT_ASSIGNED') + (对话线 blocked 经 record_authz_denial 双写 audit_log,天然同口径可查) +""" + +@dataclass(frozen=True) +class BehaviorThresholds: + a_window_hours: int = 24; a_count: int = 3 + b_window_hours: int = 72; b_count: int = 5 + c_window_hours: int = 24; c_count: int = 10 + # from_settings() 对齐 settings.risk_agent_behavior_* + +def detect_hits(now, core, repo, th) -> dict[str, dict[str, Any]]: + """按代理人分组聚合三条件证据。返回 {actor_id: {"A": [...], "B": [...], "C": [...]}}, + 每条证据含 {trace_id, at, customer_id, detail}。纯查询,无副作用(单测友好)。""" + +def scan_and_alert(now=None, core_ro=None, risk_repo=None, thresholds=None) -> dict: + """扫描入口(脚本/测试共用;模式对齐 escalation_service)。 + 1. detect_hits 收集证据 + 2. 逐 actor: + existing = repo.find_agent_behavior_alert(actor_id, day_start) + ├─ 有 → 新子条件才 append_alert_event(triggered_rules 追加 "RISK-008", + │ payload.events 追加证据、alert_subtype 集合并集——同日多子条件只一张单) + └─ 无 → 出单: + alert_type='pattern', triggered_rules=["RISK-008"], risk_score=75 + customer_id = 证据条数众数客户(PRD:涉及客户语义,payload 显式声明) + payload = {alert_subtype:'agent_behavior', actor_id, actor_type:'agent', + subtypes_hit:['A'|'B'|'C'], timeline:[...], customers:[...], + evidence_trace_ids:[...], note:'customer_id 为涉及客户非归属客户'} + L3 不写(代理人画像本期仅 payload 承载,挂账 #3) + 3. 推送 notify_role=["risk_officer","risk_manager"](_publish_alert extra 带 alert_subtype) + 4. 审计 event_type='agent_behavior_detected'(每 actor 每次命中一条;显式 new_trace 由脚本入口给) + 返回 {"scanned_windows": ..., "hits": [...], "created": [...], "appended": [...]} + """ +``` + +**边界细则:** +- 条件 A 模式判定:归属映射 audit 行 → (actor_id, customer_id, trade_id),trade_id 经 core_trade 取时间线;同 (actor, customer) 组内,对每个 redeem 事件找 2h 内该 customer 的 subscribe 且 product_id 不同 → 计一次诱导调仓;同一 redeem 不重复配对(消费制)。窗口按 core_trade.traded_at(事件时点,非扫描墙钟,与 RISK-004 rebuild 口径一致)。 +- 窗口均为滑动窗口 `created_at >= now - window`,自然日去重键只用于出单(PRD 明确两套口径并存)。 +- 行为链明细展示脱敏:payload 存 customer_id(内部键不脱敏);Tool 输出层对 display_name 走 `utils/desensitize` 二次脱敏(PRD 系统动作 6)。 + +### 4.3 对话 Tool:`query_agent_behavior` + +```python +def query_agent_behavior(customer_id, core_ro=None, risk_repo=None, **params): + """代理人行为链查询(只读)。agent_id = params.get("agent_id")(可缺省=全部)。 + rows, _ = repo.list_alerts(alert_type='pattern', status=None, page_size=100) # 注意返回 tuple(评审 P2-1) + → Python 过滤 payload.alert_subtype=='agent_behavior'(+ actor_id==agent_id 若传) + → [{alert_id, actor_id, subtypes_hit, customers(脱敏展示), created_at, + risk_score, status, evidence_trace_ids[:5]}] + 可见性:对话线准入已限 risk_officer(manager 走 HTTP 台账),Tool 层天然 fail-closed。 + """ +# 注册表:requires_customer=False, param_whitelist=("agent_id",), int_bounds={} +# 意图词组:("query_agent_behavior", ("代理人", "行为链", "异常行为", "诱导", "越权记录")) +``` + +> 意图词注意与现有 risk 组内 `alert_query`("预警")词序:`query_agent_behavior` 放 alert_query **之前**("代理人预警"应命中行为链而非台账);"预警"仍是 alert_query 专属词,无冲突。 + +### 4.4 定时脚本 + +`scripts/cron/agent_behavior_scan.py`:结构同 escalation_scan.py(sys.path 引导 → `new_trace()` → `scan_and_alert()` → JSON 摘要打印)。 + +--- + +## 5. 配置项清单(settings.py + .env.example,C4~C6 开工时一次性加齐) + +```python +# ===== 风控追加 v1.1(FR-8/9/10 · PRD §4A)===== +risk_concentration_threshold: float = 0.80 # FR-8 R4+R5 占比阈值 +risk_escalation_scan_minutes: int = 15 # FR-9 扫描周期(cron 侧参考) +risk_escalation_l1_hours: int = 4 # FR-9 普通单 LEVEL_1 +risk_escalation_l2_hours: int = 24 # FR-9 普通单 LEVEL_2 +risk_escalation_aml_l1_hours: int = 1 # FR-9 AML 通道 L1 +risk_escalation_aml_l2_hours: int = 4 # FR-9 AML 通道 L2 +risk_agent_behavior_scan_minutes: int = 30 # FR-10 扫描周期 +risk_agent_behavior_a_window_hours: int = 24 # FR-10 条件 A +risk_agent_behavior_a_count: int = 3 +risk_agent_behavior_b_window_hours: int = 72 # FR-10 条件 B +risk_agent_behavior_b_count: int = 5 +risk_agent_behavior_c_window_hours: int = 24 # FR-10 条件 C +risk_agent_behavior_c_count: int = 10 +``` + +--- + +## 6. 测试方案(A-10 / A-11 / A-12) + +### 6.1 conftest 新增 fixture(免等待基建) + +```python +@pytest.fixture() +def backdated_alert(sqlite_engine): + """注入 created_at 回拨的 pending 单(C5);teardown 按 alert_id 精确删除。 + 用法:alert_id = backdated_alert(alert_id="ALT-TEST-...", customer_id=..., hours_ago=5, payload={...})""" + +@pytest.fixture() +def backdated_audit_event(sqlite_engine): + """注入 created_at 回拨的 audit_log 行(C6);trace_id 用 'TEST-TRACE-' 前缀, + teardown DELETE WHERE trace_id LIKE 'TEST-TRACE-%'(不污染正常 trace 空间)。""" +``` + +> 真 MySQL 集成测试沿用 `risk_demo_env`;回拨行 teardown 注意 `_cleanup_test_rows` 按 `created_at >= started_at` 清不掉回拨行 → 两个 fixture **必须自带精确清理**(alert_id / trace 前缀),这是评审必查点。 + +### 6.2 用例清单 + +| 文件 | 覆盖 | +| --- | --- | +| `tests/test_rules.py`(扩) | rule_concentration:90% R5 命中 / 100% R1 不触发 / 空仓不触发 / 截断视同达标 / 阈值边界(79.9% 不触发 / 80% 触发) | +| `tests/test_engine.py`(扩) | 引擎合并:RISK-006 与 RISK-001 同单聚合、risk_score=max(60,70)=70、payload.alert_subtype 含 concentration、L3 tag 追加、risk_concentration 审计 | +| `tests/test_alert_service.py`(扩) | append_alert_event extra_subtypes 合并(老单无字段首次创建);**P0-1 回归:当日已有 agent_behavior 单后再触发 RISK-006 → 出第二张客户维度单,两单 payload 互不污染**;alert_type 合并重算(large_amount 单 + 仅 RISK-006 追加 → 保持 large_amount 不翻转);现有断言回归 | +| `tests/test_escalation_service.py`(新) | 3h59m 不升 / 4h 升 L1 / 24h 升 L2 / AML 1h 短通道(两条断言分开)· 同单重复扫描不重复推送(幂等)· 同客户多单同级别合并一次推送 · 阈值内处置不升级 · 处置后退出扫描 · status 全程 pending_review · notify_role 断言(L1 含 risk_manager / L2 含 compliance)· update_alert_escalation 不碰 handler 列 | +| `tests/test_agent_behavior_service.py`(新) | 条件 A:2 次不触发 / 3 次触发 / 本人交易(actor==customer)不计数 / 跨产品 2h 窗口边界 / SYSTEM actor 排除;条件 B:4/5 边界 + code 精确匹配;条件 C:9/10 边界 + 双 code 并集;同日多子条件只一张单(subtypes_hit 并集);customer_id=众数;payload.actor_id 正确指向代理人 | +| `tests/test_chat_tools.py`(扩) | customer_context 带 concentration_ratio;query_overdue_alerts 边界与缺省 hours;query_agent_behavior 过滤与 agent_id 匹配 | +| `tests/test_risk_api.py`(扩) | risk_manager:GET alerts 200 全量 / handle 403 / **suitability/check 403 / aml/scan 403(评审 P2-5:防未来新增端点只依赖矩阵漏加角色校验的回归断言)** / chat risk 入口 deny(AUTH_403_ROLE)/ debug 头与 JWT 通道各一条 | +| 集成(risk_demo_env) | A-10(TRD-TEST- 交易落库后 5s 内出单)/ A-11 / A-12 按验收表逐条 | + +**全量回归口径(PRD A-10 预声明 · 评审 P1-2 修订)**:conftest 增加自动生效的回归隔离 fixture(autouse,作用于全部单测): + +```python +@pytest.fixture(autouse=True) +def _disable_concentration_rule(monkeypatch): + """RISK-006 阈值推到不可达,现有用例断言零改动;RISK-006 专测内再手动改回真实阈值。""" + monkeypatch.setattr(settings, "risk_concentration_threshold", 1.01) +``` + +前置条件:`RiskThresholds.from_settings()` **必须补读** `concentration_threshold` 字段(引擎/网关测试全部走 `from_settings()` 默认路径,漏读则 monkeypatch 失效、425 绿被 RISK-006 打穿)。RISK-006 专属测试显式传 `RiskThresholds(...)` 或再次 monkeypatch 真实阈值 0.80。 + +--- + +## 7. 实现优先级与执行顺序 + +| 步骤 | 内容 | 理由 | +| --- | --- | --- | +| 0 | C5 前置联动:seed 两个 manager 账号 + deps 矩阵 + chat 守卫 + risk.py 台账分支 + JWT 手册文档 | PRD「C5 开工前执行」;独立可测(不依赖 C4) | +| 1 | **C4**(rules + core_ro + engine + alert_service + customer_context + settings) | 改动最小、复用度最高、无新基建;先拿下一个完整验收(A-10) | +| 2 | **C5**(repo 方法 + escalation_service + cron 脚本 + Tool + conftest fixture) | 建立定时任务基建与 fixture 模式,C6 直接复用;P0 红线规则 | +| 3 | **C6**(trade_gateway actor 透传 + repo 审计查询 + agent_behavior_service + Tool + 脚本) | 前置改造最多(actor 链路),基建已由 C5 备好 | +| 4 | 演示 SOP 补章节(risk_demo 扮演代理人步骤)+ 02-redis-keys.md 增补 + MEMORY/TODO 收口 | 随各任务顺手,最后统一核对挂账 #1~#9 | + +每个任务独立 commit(C4 → `feat: T-C4 ...`),全量 `python -m pytest` 绿后再进下一步;C4~C6 全部完成后按开发计划 M3 口径验收 A-10/A-11/A-12 并打 tag。 + +### 红线自查(每步提交前过一遍) + +- 不改表结构(alert_type/status 复用 payload 承载;audit_log.event_type 为 VARCHAR 可直接扩)✓ +- 升级标记仅定时任务可写(update_alert_escalation 只有 cron 链路调用;handle API 不触碰 payload)✓ +- 人工处置唯一入口 risk_officer(manager 全链路无处置权)✓ +- 四 Agent 不互调 LLM(C5/C6 定时任务无 LLM;新 Tool 全部只读)✓ +- 审计只 INSERT(三个新 event_type 走 insert_audit_log)✓ +- 客户 Agent 边界不涉及(三个需求全部收口在 risk Agent + 定时任务)✓ + +### 已登记挂账(不属本期,编码时勿"顺手"处理) + +#2 定时任务内嵌 lifespan、#3 agent_profile_l3 建表、#4 escalated 状态改表、#5 alert_type ENUM 扩展、#7 customer_query 事件类型补建、#8 core_trade actor 字段、#9 payload JSON 检索性能。 diff --git a/docs/项目框架设计/开发计划-风控模块.md b/docs/项目框架设计/开发计划-风控模块.md index b8bcf03..c724005 100644 --- a/docs/项目框架设计/开发计划-风控模块.md +++ b/docs/项目框架设计/开发计划-风控模块.md @@ -1,6 +1,6 @@ # 开发计划 · 风控监测 Agent 模块 -> 版本:v1.2(2026-09-07 追加 C4~C6 任务并入 · v1.1 为 AI 评审通过版:修复 P1×2——假 AuthContext 规范落 deps.py、conftest 测试前置;P2×7 顺手收敛)· 分支 `feature/risk` · 负责人 E +> 版本:v1.2(2026-09-07 追加 C4~C6 任务并入 · v1.1 为 AI 评审通过版:修复 P1×2——假 AuthContext 规范落 deps.py、conftest 测试前置;P2×7 顺手收敛)· 分支 `risk-control-agent`(旧称 feature/risk 已过时)· 负责人 E > 依据:PRD v1.1(含追加 FR-8/9/10)+ 架构设计 v1.1 > 铁律:**每步完成即测,测试通过才进下一步**;每个任务至少一个 commit,里程碑打 tag;涉及表结构/接口协议的步骤已获 PRD 批准 > 任务映射:T-31 = 阶段 A(**交付=suitability 服务函数+单测**;check API 随 B6)· T-30 = 阶段 B · T-32 = 阶段 C(**交付=对话线**;台账/处置 API 随 B6)——此边界已按 M4 复核(2026-09-07)同步修订 TODO 完成 @@ -78,9 +78,9 @@ | 里程碑 | 内容 | 提交 | | --- | --- | --- | -| M1 | 阶段 A 完成(suitability 可用) | 逐任务 commit + 打 tag `risk-m1` | -| M2 | 阶段 B 完成(事件线全链路,验收 **A-1~A-5、A-7~A-9**) | 逐任务 commit + 打 tag `risk-m2` | -| M3 | 阶段 C 完成(对话线 C0~C3 + 追加任务 C4~C6,验收 **A-6 / A-10 / A-11 / A-12**;C4~C6 各自验收含全量回归) | 逐任务 commit + 打 tag `risk-m3` | +| M1 | 阶段 A 完成(suitability 可用) | 逐任务 commit + 打 tag `risk-m1`(**已补打 2026-09-07**,指向阶段 A 完成提交 3c07de6,历史归档) | +| M2 | 阶段 B 完成(事件线全链路,验收 **A-1~A-5、A-7~A-9**) | 逐任务 commit + 打 tag `risk-m2`(**已打**,中文名:阶段B·事件线全链路) | +| M3 | 阶段 C 完成(对话线 C0~C3 + 追加任务 C4~C6,验收 **A-6 / A-10 / A-11 / A-12**;C4~C6 各自验收含全量回归) | C0~C3 部分已于 2026-09-07 打 tag `risk-m3`(中文名:阶段C前半·对话线闭环);**C4~C6 完成后补打 `risk-m4`(中文名拟:追加需求·集中度/时效/行为链;tag 名待用户拍板,避免与已有 tag 冲突)** | | M4 | 收尾:MEMORY/TODO 状态复核、挂账收口(含追加任务挂账 #1~#9)、边界文字对齐 | `docs: 风控模块验收记录` | **M4 完成(2026-09-07 · 复核记录):** diff --git a/docs/项目框架设计/架构设计-风控模块.md b/docs/项目框架设计/架构设计-风控模块.md index dc90acc..4c48c29 100644 --- a/docs/项目框架设计/架构设计-风控模块.md +++ b/docs/项目框架设计/架构设计-风控模块.md @@ -1,7 +1,7 @@ # 架构设计说明书 · 风控监测 Agent 模块 > 版本:v1.1(AI 评审通过:修复 P1×5——Redis 同步选型、并发首单锁方案、contextvars 实现约束、测试基建、rebuild 降级为脚本;P2×7 顺手收敛)· 2026-09-06 -> 分支:`feature/risk` · 负责人:E +> 分支:`risk-control-agent`(旧称 feature/risk 已过时)· 负责人:E > 上游:`docs/PRD/PRD-风控监测Agent.md`、`docs/PRD/附-风控规则表.md`、`docs/memory/FRAMEWORK.md` > 技术选型不变(FastAPI + SQLAlchemy + MySQL 双库 + Redis + LangGraph + DeepSeek),本文只定义**目录划分、模块职责、核心时序、关键技术决策**。