diff --git a/README.md b/README.md index 7124359..2a2b1a8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ | Wave 0 / T-04 / T-03 / T-21 | JWT 鉴权 · 审计中间件 · chat 闭环 · LangGraph+DeepSeek · Core 只读 Tool · 输入防护 · Milvus 知识库 | | `risk-m1` | 适当性校验(已换核为 C×R 矩阵契约:match_result 五值 + JR-AST/FM 规则编号) | | `risk-m2` | 事件线全链路(RISK-001~005 规则引擎 / 预警聚合 / L3 / AML / 交易网关 / 4 个 API) | -| `risk-m3` | 对话线闭环(风控四只读 Tool + StateGraph risk 分支) | +| `risk-m3` | 对话线闭环(风控四只读 Tool + StateGraph risk 分支;现为五只读 Tool,C5 追加 `query_overdue_alerts`) | | 阶段一 | 对齐 main 基准 AL-01~08(SUIT-001~008 退役 → core_ro.check_suitability 矩阵判定) | | `risk-m4` | 追加需求:FR-8 集中度预警 RISK-006 / FR-9 处置时效升级 RISK-007 / FR-10 代理人行为链 RISK-008 | @@ -133,7 +133,7 @@ dev 环境无 Bearer 时可用 `X-Debug-Role` / `X-Debug-Actor` 头兜底(仅 | `POST /api/risk/suitability/check` | 适当性校验(C×R 矩阵,G-01 归属) | customer 本人 / advisor 名下 / risk_officer | | `POST /api/risk/aml/scan` | 手动全量 AML 扫描(FR-5) | risk_officer | | `POST /api/simulate/trade` | 模拟交易(适当性阻断 + 规则引擎) | risk_demo 或客户本人 | -| `POST /api/chat` | 对话(X-Agent-Type 分流四 Agent;风控线四只读 Tool) | 按准入矩阵 | +| `POST /api/chat` | 对话(X-Agent-Type 分流四 Agent;风控线五只读 Tool) | 按准入矩阵 | | `GET /api/chat/sessions` | 会话列表(本人 + 本 Agent 线,分页) | 按准入矩阵 | | `GET /api/chat/sessions/{id}/messages` | 会话历史消息(seq_no 升序分页) | 仅本人会话 | | `POST /api/chat/sessions/{id}/close` | 关闭会话(active→closed) | 仅本人会话 | diff --git a/docs/memory/FLOW.md b/docs/memory/FLOW.md index e5dab26..73f9c31 100644 --- a/docs/memory/FLOW.md +++ b/docs/memory/FLOW.md @@ -73,7 +73,7 @@ agent_service:LangGraph StateGraph + DeepSeek;tool(意图→Tool) → llm ↓ 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(RISK_TOOL_REGISTRY)】 + - 风控五 Tool:query_overdue_alerts(C5 追加)/alert_query/customer_context/suitability_check/aml_lookup 【RISK_TOOL_REGISTRY 现 5 个】 - 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 607f105..ce84b97 100644 --- a/docs/memory/FRAMEWORK.md +++ b/docs/memory/FRAMEWORK.md @@ -41,7 +41,7 @@ | 风控监测 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 节点 + C1 风控四 Tool + C2 risk 分支,2026-09-07)**:tool(分组关键词意图→Tool,customer/advisor/risk)→llm→guard(免责声明);analyst 分支与 LLM intent 待后续 | +| 对话编排 | LangGraph StateGraph + DeepSeek | langgraph/langchain-openai | **已实现(T-07 骨架 + T-04 Tool 节点 + C1 风控四 Tool + C5 追加 query_overdue_alerts 共五只 + C2 risk 分支)**:tool(分组关键词意图→Tool,customer/advisor/risk)→llm→guard(免责声明);analyst 分支与 LLM intent 待后续 | | 同步脚本 | 归属、Neo4j | Core → agent / 图库 | **sync_*.py 已实现** | ------ diff --git a/docs/memory/REQUIREMENTS.md b/docs/memory/REQUIREMENTS.md index 395feac..f8fe616 100644 --- a/docs/memory/REQUIREMENTS.md +++ b/docs/memory/REQUIREMENTS.md @@ -19,14 +19,14 @@ | --- | --- | --- | --- | --- | | F-01 | JWT + RBAC + 数据归属 | 越权 403 + audit;JWT 手册 §13 | **已完成(T-01,2026-09-07)**:auth_service 验签/吊销 + deps 工厂(Bearer 全环境 + dev debug 头兜底)+ X-Agent-Type 准入矩阵 + 归属断言(B6 已有);越权 403 + audit + input_guard_log 双写 | ~~T-01~~ | | F-02 | 全量审计留痕 | trace_id 可还原 | **已完成(T-02,2026-09-07)**:http_access 审计中间件 + X-Trace-Id/X-Request-Id 双贯通 + 4xx/500 统一错误体;业务判定审计 B6 已接 | ~~T-02~~ | -| F-03 | 输入防护 | input_guard_log | **完成(2026-09-07,T-03)**:prompt_injection(词表 42 条纯函数检测,命中即拒)/ oversize(4000,Pydantic 硬顶 20000)/ rate_limit(Redis 固定窗口 actor 级 30 次/分 fail-open);chat 链路 blocked 全留痕,fail-fast 不建会话 | — | +| F-03 | 输入防护 | input_guard_log | **完成(2026-09-07,T-03)**:prompt_injection(词表纯函数检测,T-03 初版 42 条、2026-09-09 实测扩至 **45 条** = 18+11+9+7,命中即拒)/ oversize(4000,Pydantic 硬顶 20000)/ rate_limit(Redis 固定窗口 actor 级 30 次/分 fail-open);chat 链路 blocked 全留痕,fail-fast 不建会话 | — | | F-04 | Core 只读层 | 不改 Core 账;Repository 只 SELECT | **已完成(T-04,2026-09-07)**:core_ro(含风控扩展)经 app/tool/core_tools.py 三只读 Tool 接入 chat(L0/持仓/流水);Tool 层归属校验(A-01 语义:customer 本人/advisor assigned/risk_officer 全量)+ agent_tool_call 全留痕;Core 侧仍仅 SELECT | ~~T-04~~ | | R0-DB | MySQL 共用 11 表 + Redis | 01-mysql-共用底座.sql | SQL 已定;本机已灌库(B9b 走查验证) | T-05(本机已完成) | | R0-CORE | Core 模拟 + 同步 | reset.ps1 + sync 脚本 | **脚本已落地** | T-05 | **F-01 部分完成说明:** B6 已落地 `app/api/deps.py`(AuthContext Pydantic + `get_auth_context()` 工厂,dev 从 `X-Debug-Role`/`X-Debug-Actor` 头构造;T-01 就绪后仅替换工厂内部为 JWT 解析)。 -**F-04 完成说明(T-04,2026-09-07):** `app/repository/core_ro.py`(含风控扩展)仅 SELECT 不变;对话接入经 `app/tool/core_tools.py`(query_customer_profile / query_holdings / query_recent_trades,TOOL_REGISTRY 白名单)+ `app/service/tool_service.py`(run_tool:归属校验→执行→agent_tool_call 落库);agent_service 图为 START→tool→llm→guard,customer_id 由会话注入不接受 LLM 生成。风控四 Tool 归 C1 复用同一 runner。 +**F-04 完成说明(T-04,2026-09-07):** `app/repository/core_ro.py`(含风控扩展)仅 SELECT 不变;对话接入经 `app/tool/core_tools.py`(query_customer_profile / query_holdings / query_recent_trades,TOOL_REGISTRY 白名单)+ `app/service/tool_service.py`(run_tool:归属校验→执行→agent_tool_call 落库);agent_service 图为 START→tool→llm→guard,customer_id 由会话注入不接受 LLM 生成。风控五 Tool(C1 四只 + C5 追加 query_overdue_alerts,RISK_TOOL_REGISTRY 实 5 个)复用同一 runner。 ## Wave 1 · 内部 Agent P0 diff --git a/docs/项目框架设计/架构设计说明书.md b/docs/项目框架设计/架构设计说明书.md index 799b068..d16a072 100644 --- a/docs/项目框架设计/架构设计说明书.md +++ b/docs/项目框架设计/架构设计说明书.md @@ -1,6 +1,6 @@ # XingHuo 智能财富管家 · 架构设计说明书 -> **代码基线**:分支 `risk-control-agent`,HEAD `2d0e2fa`(2026-09-08),全量 pytest 503 绿。 +> **代码基线**:分支 `risk-control-agent`(2026-09-09 架构改进 T-101~T-202 落地后),全量 pytest **510 绿**(503 + 锁测试 6 例 + 中间件顺序守卫 1 例)。 > **编写原则**:本文所有论断均落到具体文件、类、函数、行号;凡文档口径与代码实测不一致处,以代码为准并在 §9.4 列出。 > **与既有文档的关系**:`FRAMEWORK.md` 是选型与状态的速查表,`FLOW.md` 是端到端链路与 bootstrap,本文是**设计决策与理由的系统阐述**——回答"为什么是这样"以及"换成别的会怎样"。 @@ -99,7 +99,7 @@ ServerErrorMiddleware → trace_middleware → audit_middleware → ExceptionMid 代码自证:`audit_middleware.py:9` 注释写"注册顺序:trace 中间件之后注册(即执行序在 trace 之内),保证审计时 trace_id/request_id 已绑定"。 -> ⚠️ **这是一处隐式依赖**:audit 的正确性完全依赖注册顺序,而顺序由"谁写在文件后面"决定。目前靠注释固化(两处都有),但**没有测试守卫**——若有人调换两个装饰器的位置,审计会静默丢失 trace_id,且不报错。 +> ⚠️ **这是一处隐式依赖**:audit 的正确性完全依赖注册顺序,而顺序由"谁写在文件后面"决定。原靠注释固化(两处都有),**现已由测试守卫锁定**:`tests/test_audit_middleware.py::test_audit_middleware_runs_inside_trace_middleware` 断言 `audit_log.trace_id` 非空——调换两个装饰器的注册顺序该测试即变红(已实测验证)。测试即文档,比注释更可靠。 ### 2.2 四个 ID,各管一层 @@ -296,7 +296,7 @@ def build_graph(): > **多解说明**:LangGraph 支持 `add_conditional_edges` 做真正的图分支。本项目**选择了数据驱动而非图分支**——理由是四 Agent 共享相同的"取数 → 生成 → 加免责声明"骨架,差异只在提示词与可用工具集,抽成一张图比维护四张子图成本低。代价是**无法表达"风控对话需要额外的复核节点"这类结构性差异**,若将来某 Agent 需要独立节点拓扑,需拆图。 -**无 key 降级**(`:148-150`):`if not settings.deepseek_api_key: reply = _degraded_reply(state)`,降级回复携带 Tool 查询摘要并加 `_DEGRADED_PREFIX`,不抛异常。好处是演示链路不断;风险是**生产漏配 key 会"看起来正常"**(§7.3)。 +**无 key 降级**(`:148-150`):`if not settings.deepseek_api_key: reply = _degraded_reply(state)`,降级回复携带 Tool 查询摘要并加 `_DEGRADED_PREFIX`,不抛异常。好处是演示链路不断;原风险是**生产漏配 key 会"看起来正常"**——**该风险已由 T-107 闭环**:`main.py` 的 `lifespan` 在 `DEEPSEEK_API_KEY` 缺失时打印显式告警(函数内延迟导入 `_DEGRADED_PREFIX` 防循环导入),启动日志即提示将走降级回复,不再是静默失败。改进已完成,见 §7.3 P0-3。 #### 4.2.2 流式执行时序 @@ -416,7 +416,7 @@ app/gateway/trade_gateway.py:86 submit_trade **并发控制**:进程内锁 + 乐观锁 `update_l3(expected_computed_at)` + 3 次重试。 -> ⚠️ **`service/risk/locks.py` 不是分布式锁**:`run_locked:31` 用的是**进程内 `threading.Lock`**,按 key 串行,`LOCK_TIMEOUT_SECONDS = 2.0` 超时降级 `fn(locked=False)`。文件注释明确"多进程部署换 Redis SET NX,接口不变"——**这是 TODO,当前无分布式实现**。单进程演示成立,多实例部署会失效(§7.2)。 +> ⚠️ **`service/risk/locks.py` 已完成「双层锁」改造(T-201)**:`run_locked(key, fn)` 优先用 **Redis `SET NX EX`** 抢分布式锁(`redis_gateway.acquire_lock` / `release_lock` 由 Lua 原子释放、只删自己的锁),抢到则 `fn(locked=True)`;**Redis 等待超时(2.0s)→ `fn(locked=False)`** 沿用原降级语义(绝不抛异常);**Redis 不可用(含测试 Fake 缺方法的 `AttributeError`)→ 安全退回进程内 `threading.Lock`**,最坏退化为单进程行为,不会更差。常量:`LOCK_TTL_SECONDS=30` / `LOCK_TIMEOUT_SECONDS=2.0` / `_LOCK_KEY_PREFIX="lock:"` / `_RETRY_INTERVAL=0.05`。**三处调用点(alert_service 聚合 ×2 含 R-02 阻断点、profile_l3 ×1)签名与调用方式不变**,多实例部署不再失效(§7.2 已更新)。 #### 4.3.5 C4 / C5 / C6 追加需求 @@ -546,7 +546,7 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON 2. **新增风控规则**:`rules.py` 加纯函数 + `run_rules` 编排 + `RULE_SCORES` 映射 3. **接入动态评分**:实现 `scoring.py:recompute_customer_score`(签名已冻结),同步放开 L3 `risk_score` 4. **替换真实交易系统**:整体退役 `app/gateway/`,实现 `submit_trade` 契约 -5. **切分布式锁**:`locks.py` 换 Redis SET NX(**接口不变**,注释已预留) +5. **切分布式锁**:`locks.py` 换 Redis SET NX(**接口不变**,注释已预留)—— **T-201 已完成**:现为 Redis 双层锁,三处调用点未改动(见 §4.3.4) 6. **前端接入**:后端接入面已就绪(方案 B 三端点 + 方案 C SSE),`web/` 待 init --- @@ -564,7 +564,7 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON | 6.7 | 限流降级 | fail-open | fail-closed | Redis 故障不阻断业务 | 故障时恶意用户可绕过 | | 6.8 | Embedding 降级 | fail-closed(抛错) | 返回空结果 | 避免 LLM 编造产品规则 | 依赖可用性,Ollama/Milvus 故障直接报错 | | 6.9 | 会话落库 | 整轮一次性落(同事务) | 边生成边落 | 无半截历史 | 断连/异常整轮丢失(Tool 留痕仍在) | -| 6.10 | 锁 | 进程内 `threading.Lock` | Redis SET NX | 零依赖、单进程够用 | 多实例部署失效(TODO 未做) | +| 6.10 | 锁 | 双层:Redis `SET NX EX` 为主(TTL 30s、超时 2s 降级 `fn(locked=False)`),进程内 `threading.Lock` 为备(Redis 不可用时退回) | 纯进程内锁 | 多实例部署不再失效,Redis 故障不阻塞业务、不抛异常 | 引入 Redis 依赖;需防临界区超 TTL 自动解锁(已用 30s TTL 缓解) | --- @@ -572,7 +572,7 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON ### 7.1 前提假设(不成立则设计失效) -1. **单进程部署**——`locks.py` 进程内锁、`insert_turn` 的 `MAX(seq_no)+1` 竞态(注释承认"并发写锁归后续")均以此为前提 +1. **单进程部署**——`locks.py` 已升级为 Redis 双层锁(多实例不再失效),但 `insert_turn` 的 `MAX(seq_no)+1` 竞态(注释承认"并发写锁归后续")**仍以此为前提** 2. **Core 是模拟库且无人写入**——只读靠约定,无 DB 级账号保护 3. **用户用语高度收敛**——关键词意图识别的前提 4. **演示/内网环境**——dev debug 通道、`_degraded_reply` 静默降级均在公网生产下变危险 @@ -582,7 +582,7 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON | 能力 | 边界 | | --- | --- | -| 分布式锁 | ❌ 多实例部署失效 | +| 分布式锁 | ✅ 双层(Redis 主 + 进程内备,T-201 已落地);⚠️ `insert_turn` 的 seq 竞态仍在(§7.3 P0-2) | | 动态风险评分 | ❌ `scoring.py` 是桩,L3 `risk_score` 恒 NULL | | 跨库事务 | ❌ 双库最终一致,靠 `rebuild_alerts.py` 补偿 | | 知识库管理 API | ❌ 一期只做脚本入库 | @@ -596,12 +596,12 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON 1. **为 `core_ro` 配 DB 级只读账号**:当前红线仅靠约定,配只读账号是从根基卡死(§4.6.1) 2. **`agent_message` 并发重号**:索引非唯一 + SELECT 无锁 → 并发同会话静默重号。修法:加 `UNIQUE(session_id, seq_no)` **并同步给 `insert_turn` 加 `IntegrityError` 重试**(只加索引会让并发变 500) -3. **无 key 降级改显式告警**:`_degraded_reply` 静默返回,生产漏配会"看似正常" +3. ✅ **无 key 降级改显式告警**:`_degraded_reply` 静默返回,生产漏配会"看似正常" —— **T-107 已完成**:`main.py` 启动即打印告警(见 §4.2.1) 4. **审计/限流 fail-open 策略可配置**:合规强场景应能切 fail-closed **P1 — 可维护性** -5. **中间件顺序加测试守卫**:当前靠注释固化,调换装饰器会静默丢 trace_id(§2.1) +5. ✅ **中间件顺序加测试守卫**:原靠注释固化,调换装饰器会静默丢 trace_id —— **T-202 已完成**:`test_audit_middleware_runs_inside_trace_middleware` 断言 `audit_log.trace_id` 非空,调换注册顺序即变红(§2.1) 6. **权限逻辑收拢**:C5 红线分散在矩阵 / `_assert_chat_entry` / `assert_tool_access` 三处 7. **注册表合并**:`TOOL_REGISTRY` 与 `KB_TOOL_REGISTRY` 分裂 8. **`model/` 层启用**:逐步用 Pydantic / `TypedDict` 约束仓储入参 @@ -617,7 +617,6 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON ### 7.4 已知待决事项 - 审计降级 fail-open 是否切 fail-closed(已登记) -- 进程内锁换 Redis SET NX 的时机(接口已预留) - `chat` 链路 `risk_suitability_log.actor_id` 落 `SYSTEM` 待评估 - 前端 React 多 Agent 入口 `web/` 归属待拍板 @@ -674,9 +673,9 @@ SQL 取回 `core_suitability_rule` 的 `matrix_match_result`(`LEFT JOIN ... ON | 风控对话 Tool | 4 个 | **5 个**(`chat_tools.py:328`,另有 1 个定义未注册) | | Agent 专用表 | 5 张 | **6 张**(SQL 注释写 5,实建 6) | | `scoring.py` | 未明确 | **`NotImplementedError` 预留桩**,非实际评分器 | -| `locks.py` | 未明确 | **进程内 `threading.Lock`**,非分布式锁 | +| `locks.py` | 未明确 | **双层锁(T-201 已落地)**:Redis `SET NX EX` 为主、进程内 `threading.Lock` 为备,三处调用点不变 | | `convert` 交易 | 未提及 | DB ENUM 支持,**网关主动拒绝** | --- -*本文基于 `risk-control-agent` 分支 `2d0e2fa` 的代码实测撰写。后续改动请先更新 `docs/memory/` 再同步本文。* +*本文基于 `risk-control-agent` 分支的代码实测撰写,已同步 2026-09-09 架构改进(T-101~T-202:双层锁 / 无 key 告警 / 中间件顺序守卫 / 口径勘误,pytest 510 绿)。后续改动请先更新 `docs/memory/` 再同步本文。*