# 风控 Agent 模块边界与合并接缝标注 > 版本:v1.0 · 2026-09-07 18:20 > 适用分支:`risk-control-agent`(当前唯一开发分支) > 定位:**给 AL-09 合并 main 用的裁决依据**,也供日常改码判断"这个文件动了会不会影响宿主"。 > 关联:《修改报告-对齐main基准.md》(阶段一执行依据)、《实现方案-风控追加需求v1.1-C4C6.md》(阶段二编码依据) --- ## 0. 一句话结论 **风控 Agent 按"独立封装模块"自治:模块内部(含自有鉴权)自己说了算,与宿主的耦合收敛到 4 个接缝;合并 main 时不逐文件融合,只在接缝处接线。** 采用本方案后,main 侧 `gateway/*`、`middleware/trace.py`、`utils/input_guard.py` 等文件与模块私有实现**允许并存**——它们不是"重复实现",而是"宿主层"与"模块层"各自的实现,只要接缝不串、模块不被宿主覆盖即可。 **前提(三条,缺一不可)**: 1. 模块私有文件不被宿主同名文件覆盖(合并时以模块版本为准); 2. 模块对宿主的依赖只走 §2 的 4 个接缝,不得直接 import 宿主私有实现; 3. 接缝由 `tests/test_module_boundary.py` 锁死,宿主一改破坏契约即红灯。 --- ## 1. 模块边界(文件归属表) ### 1.1 A 类 · 模块私有(风控业务,合并时全量保留,不与 main 融合) | 路径 | 说明 | | --- | --- | | `app/service/risk/` | 风控引擎、规则、预警服务、对话 Tool、L3 画像、AML(阶段 A/B/C 全部成果) | | `app/service/suitability.py` | 适当性服务(AL-05 换核后为 main 契约 + 我方兼容层) | | `app/service/tool_service.py` | Tool 编排与文案汇总 | | `app/repository/risk_repository.py` | 风控读写仓储 | | `app/repository/core_ro.py` | Core 只读(AL-03 已与 main 融合,归模块私有) | | `app/repository/session_repository.py` | 会话仓储(模块自用) | | `app/api/risk.py`、`app/api/simulate.py` | 风控 4 API + 交易网关模拟入口 | | `app/gateway/trade_gateway.py` | 交易网关(C6 需透传 actor_id) | | `app/model/suitability.py` | 适当性落库行构造(AL-04 引入,与 main 同名 → **冲突时以模块版为准**) | | `scripts/demo/*`、`scripts/core/*` | 演示与 Core 种子脚本 | | `tests/`(除 test_module_boundary 外) | 模块测试;**AL-09 当时 530 passed** · **当前全量基线见 MEMORY §0** | ### 1.2 B 类 · 模块私有基建(与宿主同类但模块内自用,**允许与 main 并存**) | 模块侧(我方) | 宿主侧(main) | 并存是否安全 | 说明 | | --- | --- | --- | --- | | `app/service/auth_service.py` + `app/api/deps.py` | `app/gateway/{jwt_service,auth_deps,rbac,ownership}.py` | ✅ 安全 | 模块内 API 一律 `Depends(get_auth_context)` 走 deps.py,不碰 gateway | | `app/utils/trace.py` + `app/api/audit_middleware.py` | `app/middleware/trace.py` | ✅ 安全 | 模块用 contextvar 取 trace;宿主用 TraceMiddleware,互不读写 | | `app/service/input_guard.py` | `app/utils/input_guard.py` | ✅ 安全 | 模块版含 42 词表 + oversize 4000 + Redis 限流 + 留痕;宿主版仅 SQL 注入校验 | | `app/utils/db.py`(`get_engine(db)`) | `app/config/database.py`(`get_agent_engine/get_core_engine`) | ⚠️ 有条件 | 二者都是双库,功能等价;但**连接池分裂**——模块 Side 不得 import `config.database`,见 §2-S4 | > ⚠️ 唯一实质风险:B 类第 4 行。其余三行只要"各用各的"就不冲突。 ### 1.3 C 类 · 公共层/接缝(合并时以 main 为主,模块做适配) | 接缝 | 文件 | 处置 | | --- | --- | --- | | S1 | `app/main.py` | 以 main 骨架为主,补挂模块 router 与中间件(见 §2-S1) | | S2 | `app/config/settings.py` | 以 main 为主 + **必须保留模块私有字段**(见 §2-S3) | | S3 | `app/utils/exceptions.py`、`app/utils/response.py` | 以 main 为主,补齐模块用到的错误码与响应字段 | | S4 | `app/service/agent_service.py`、`app/service/memory_service.py` | 以 main 为主(chat 主链路),模块 Tool 通过注册表挂载 | ### 1.4 D 类 · main 独有新增(合并时静默并入,模块不依赖) `app/api/auth.py`、`app/gateway/{auth_deps,jwt_service,ownership,rbac}.py`、`app/middleware/{__init__,trace}.py`、 `app/repository/{advisor_rel,agent,audit}_repository.py`、`app/utils/input_guard.py`、`app/model/schemas.py`、 `app/config/database.py`、`tests/test_wave0_*.py` 处置:**全部接纳,不删除**(它们是宿主 Wave 0 的正当实现),但模块代码**不得 import** 其中任何一个——由 §3 防呆测试锁死。 --- ## 2. 四个接缝(合并时唯一需要人工接线的地方) ### S1 · 挂载点(`app/main.py`) 我方现状:挂 `risk` / `simulate` / `chat` 三个 router;两层 `@app.middleware("http")`(audit + trace)。 main 现状:`add_middleware(TraceMiddleware)` + 挂 `auth` / `chat`。 合并口径: ```python # 宿主部分保留 main 写法 app.add_middleware(TraceMiddleware) app.include_router(auth_router) app.include_router(chat_router) # 模块部分:追加挂载,中间件改为模块内注册(见下) app.include_router(risk_router) app.include_router(simulate_router) setup_risk_module(app) # AL-09 已完成:注册 audit + trace 两层 http 中间件 ``` **注意**:main 用 `add_middleware`,我方用 `@app.middleware("http")`——两种注册方式可共存,但中间件执行顺序需实测确认(trace 必须在 audit 内层,保证 audit 能读到 trace_id)。这列为 AL-09 必测项。 ### S2 · AuthContext 契约(**最关键的接缝**) | 字段/方法 | 模块侧(deps.AuthContext) | 宿主侧(schemas.AuthContext) | 差异后果 | | --- | --- | --- | --- | | 主体标识 | `actor_id` | `sub` | 字段名不同,全量 `auth.actor_id` 引用会 AttributeError | | trace | 无(走 contextvar `current_trace`) | `trace_id`(必填) | 模块需从 contextvar 或适配器补齐 | | agent_type | 无(单独传参) | `agent_type`(必填,4 值) | 取值集合**双方一致**:`customer/advisor/analyst/risk` ✅ | | `has_role` | `has_role(*roles)` 多参 | `has_role(role)` 单参 | 多参调用会 TypeError | | 权限判断 | `has_permission(perm)` | `has_perm(perm)`(支持 `xx:*` 通配) | 方法名不同 | | 其余 | `roles/customer_id/token_type/permissions/tenant_id/jti` | 同左(tenant_id 默认 `default`,jti 非空) | 基本对齐 | **处置:写适配器,不改模块内部。** 已预制 `app/api/auth_adapter.py`(当前未接线,AL-09 时接入): ```python from app.api.auth_adapter import from_host_auth # 宿主 AuthContext(sub/trace_id/agent_type) → 模块 AuthContext(actor_id/...) ctx = from_host_auth(host_ctx) ``` 适配器保证 `sub→actor_id`、`trace_id→contextvar`、`has_role` 多参语义、权限前缀通配到 `permissions` 的展开。 ### S3 · settings 字段 - **main 新增需吸收**:`jwt_dev_algorithm`(HS256)、`jwt_dev_expire_hours`(8)。`jwt_dev_secret` 双方同名同值,无冲突。 - **模块私有,合并时一个都不能丢**: - 双库:`mysql_core_database` - 中间件:`redis_url` - 知识库:`milvus_uri`、`ollama_base_url`、`embed_model`、`embed_dim`、`embed_timeout_seconds` - LLM:`deepseek_api_key`、`deepseek_base_url` - 风控阈值 11 项:`risk_large_amount`、`risk_daily_total`、`risk_freq_count`、`risk_probe_window_minutes`、`risk_probe_count`、`risk_probe_amount`、`risk_small_amount`、`risk_small_count`、`risk_aml_default_threshold`、`guard_rate_limit_max`、`guard_rate_limit_window_seconds` - **⚠️ issuer 硬伤**:模块 `jwt_issuer = https://idp.jinrong.internal`,宿主 `https://idp.jinrong.dev`。 自治方案下**不改任何一方**,由 S2 适配器在模块入口完成映射;已签发 token 不受影响(模块继续认自己的 issuer)。 ### S4 · 引擎工厂 模块统一用 `app/utils/db.py:get_engine(database)`(含 `dispose_engines` 生命周期)。 宿主 `app/config/database.py` 提供 `get_agent_engine()` / `get_core_engine()`。 **红线**:模块代码一律不得 `from app.config.database import ...`,否则连接池分裂、测试 monkeypatch 失效。由防呆测试锁死。 AL-09 可选优化:让 `config.database` 内部转调 `utils.db.get_engine`,保留宿主 API 外观、共用一处连接池。 --- ## 3. 防呆机制(`tests/test_module_boundary.py`) 新增 4 组断言,任何一条被破坏即测试失败: 1. **模块私有文件存在性**:A 类关键文件不得被误删。 2. **禁止跨层 import**:模块文件(A/B 类)中不得出现 `from app.gateway.auth_deps`、`from app.config.database`、`from app.middleware.trace`、`from app.utils.input_guard`(宿主版)的导入。 3. **AuthContext 契约**:模块 `AuthContext` 必须保留 `actor_id/roles/customer_id/token_type/permissions/tenant_id/jti` 字段与 `has_role(多参)/has_permission/is_customer` 方法——防合并时被宿主类替换。 4. **settings 私有字段**:§2-S3 列出的模块私有配置必须齐全;`AGENT_TYPES` 必须等于 `("customer","advisor","analyst","risk")`。 > 用法:AL-09 合并后立刻跑 `pytest tests/test_module_boundary.py`,全绿才说明模块没被宿主侵蚀。 --- ## 4. 冲突裁决表(AL-09 用,试合并实测 20 个冲突文件) > 实测命令:`git merge-tree --write-tree --name-only risk-control-agent 3995cb4`(只读,可随时复算) | # | 冲突文件 | 归属 | 裁决 | | --- | --- | --- | --- | | 1 | `app/api/chat.py` | C 公共 | 以 main 为主;模块对话能力经 Tool 注册表与 `agent_service` 挂载,不占用此文件 | | 2 | `app/main.py` | C 接缝 S1 | 以 main 骨架为主 + 追加模块 router/中间件(见 §2-S1) | | 3 | `app/config/settings.py` | C 接缝 S3 | 以 main 为主 + 保留模块私有字段(见 §2-S3 清单) | | 4 | `app/service/agent_service.py` | C 公共 | 以 main 为主;注意入口函数差异:main `run_chat(ctx,msg,cust)->(str,bool)` vs 模块 `chat(...)`,**由适配层统一,勿直接替换** | | 5 | `app/service/memory_service.py` | C 公共 | 以 main 为主,补齐模块会话窗口所需方法 | | 6 | `app/utils/exceptions.py` | C 公共 | 以 main 为主,补齐模块错误码(`AUTH_*`、`STATE_CONFLICT` 等) | | 7 | `app/utils/response.py` | C 公共 | 以 main 为主,补齐模块响应字段 | | 8 | `app/repository/core_ro.py` | A 私有 | **以模块版为准**(AL-03 已融合 main 四项 + 我方风控扩展 5 项,main 版缺风控扩展) | | 9 | `app/model/suitability.py` | A 私有 | **以模块版为准**(AL-04 引入;main 版为纯落库映射,缺模块兼容层) | | 10 | `app/gateway/__init__.py` | B/D 混合 | 合并两侧导出:保留模块 `trade_gateway` 导出,不引入 main 的 gateway 鉴权导出 | | 11 | `AGENTS.md` | 文档 | 以我方为准(含 memory 六件套指引),吸收 main 新增条目 | | 12~18 | `docs/memory/{MEMORY,TODO,REQUIREMENTS,FRAMEWORK,FLOW,ENVIRONMENT,ITERATION}.md` | 文档 | **以我方为准**(六件套是我方进度事实源),逐份吸收 main 新增实质内容 | | 19 | `scripts/core/02-seed-base.sql` | A 私有 | 手工融合:main 账号(STAFF-10086~50001 等)+ 模块独有 **STAFF-90001**(风控演示账号,main 无!)+ C5 前置 **STAFF-31001/31002** | | 20 | `tests/conftest.py` | A 私有 | **以模块版为准**(502 用例依赖),再把 main 的 wave0 fixture 增量并入 | **另有 13 个 main 独有文件不报冲突、静默并入** → 见 §1.4,全部接纳但模块不得 import。 --- ## 5. 三处硬伤在自治方案下的处置 | 硬伤 | 原风险 | 自治方案下处置 | | --- | --- | --- | | JWT issuer 不一致(`idp.jinrong.internal` vs `.dev`) | 统一后旧 token 全 401 | **AL-09 已统一**:`/api/auth/login` → `issue_dev_token`;`gateway/jwt_service` issuer/audience 读 `settings`;旧 dev token 需重签 | | main 无 STAFF-90001(风控演示账号) | 演示账号变 401 | 模块账号由模块 seed 维护(`02-seed-base.sql` 融合时保留),不走宿主 `DEFAULT_ROLES_BY_ACTOR` | | main `infer_roles` 未知 actor 默认 `["analyst"]`(fail-open,含 `sql:execute:readonly`) | 越权风险 | 模块鉴权不采用 `infer_roles`;**若宿主侧修复前上线,须在 AL-09 记录为待办 P1**——模块自身为 fail-closed(缺失 X-Agent-Type 即 401) | --- ## 6. AL-09 执行步骤(按本方案修订) 1. `git merge main`(预期 20 冲突,按 §4 表逐个裁决,禁止 `git checkout --ours/theirs` 批量解决); 2. 跑 `pytest tests/test_module_boundary.py`,红了先修边界再继续; 3. 接 S2 适配器:`app/api/auth_adapter.py` 接线到模块入口; 4. 接 S1 挂载点:`main.py` 补 router + 两层中间件,实测中间件顺序(trace 在内层); 5. 处理 S4:确认模块无 `config.database` 导入(防呆测试已覆盖); 6. 融合 `02-seed-base.sql`(保留 STAFF-90001 / 补 STAFF-31001、31002); 7. SOP §2 重灌演示库 → 全量 `pytest` 须 **530 passed 0 skipped** + 边界用例全绿; 8. uvicorn 冒烟:三端点 + 模块 4 API 逐个验证(注意 JWT 与 debug 头两条通道都要走一遍)。 --- ## 7. 待办与遗留 - [x] AL-09 实现 `setup_risk_module(app)` 与中间件注册(2026-09-08) - [x] 中间件执行顺序实测;trace 对 ApiError 等 re-raise(2026-09-08) - [ ] 宿主 `infer_roles` fail-open 缺陷:建议宿主侧修复(P1,非模块阻塞项) - [ ] 模块代码若未来要彻底隔离,可迁至 `app/modules/risk/`——本次**不做**(属重构,收益 < 风险)