Files
group_xinghuo_jinrong/docs/项目框架设计/风控Agent模块边界与合并接缝标注.md
T
GaoYiYuan_0626 8c226f0c6d chore: 风控Agent模块自治边界标注 + AL-09 合并预处置
背景:远端 main 新提交 3995cb4(09-07 17:20,交接文档漏记)为 Wave 0 鉴权/
chat/防护平行实现,与已完工 T-01/T-02/T-03/T-06 同名不同路径;试合并实测
20 文件冲突(原记 9 个),另有 13 个 main 新增文件不报冲突会静默并入。
拍板:风控 Agent 按独立封装模块自治,与宿主耦合收敛到 4 个接缝。

1. 《风控Agent模块边界与合并接缝标注》入库存档:A~D 四类文件归属表;
   4 接缝(S1 挂载点 main.py / S2 AuthContext / S3 settings / S4 引擎工厂);
   20 冲突文件逐个裁决(core_ro、model/suitability、conftest、02-seed-base
   以模块版为准;chat/main/settings/agent_service 等公共层以 main 为主);
   三处硬伤处置:issuer 不一致改为适配器映射不统一、STAFF-90001 必保、
   main infer_roles 未知 actor 默认 analyst(fail-open)记宿主侧 P1。
2. app/api/auth_adapter.py:S2 接缝适配器预制件(当前未接线,AL-09 接入)。
   鸭子类型读宿主 ctx 故不依赖宿主文件;sub→actor_id、trace_id→contextvar、
   perm_matches 兼容宿主 `前缀:*` 通配;缺主体即 HostAuthAdapterError,
   fail-closed 不静默降级。
3. tests/test_module_boundary.py:边界防呆 4 类断言——模块私有文件存在、
   禁止跨层 import 宿主私有实现(gateway.*/config.database/middleware.*/
   utils.input_guard/model.schemas)、AuthContext 契约完整(actor_id 与
   has_role 多参)、settings 私有字段与 AGENT_TYPES 四值不漂移。
4. tests/test_auth_adapter.py:适配器 11 例(映射/回退/fail-closed/trace 绑定/通配)。

基线:406 → 436 全绿(演示库已按演练 SOP §2 重灌:AML 8 条 / 演示 7 行 / sync 33 rows)。
2026-09-07 18:28:18 +08:00

14 KiB
Raw Blame History

风控 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 外) 模块测试,406 基线

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。

合并口径:

# 宿主部分保留 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 时接入):

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 私有 以模块版为准(406 用例依赖),再把 main 的 wave0 fixture 增量并入

另有 13 个 main 独有文件不报冲突、静默并入 → 见 §1.4,全部接纳但模块不得 import。


5. 三处硬伤在自治方案下的处置

硬伤 原风险 自治方案下处置
JWT issuer 不一致(idp.jinrong.internal vs .dev) 统一后旧 token 全 401 不统一。模块继续认自己的 issuer;跨层调用走 S2 适配器。省掉一次全量 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 须回 406 + 新增边界用例全绿;
  8. uvicorn 冒烟:三端点 + 模块 4 API 逐个验证(注意 JWT 与 debug 头两条通道都要走一遍)。

7. 待办与遗留

  • AL-09 时实现 setup_risk_module(app)(中间件注册函数),当前 main.py 仍为直接装饰器
  • 中间件执行顺序实测(main add_middleware vs 模块 @app.middleware 双层叠加)
  • 宿主 infer_roles fail-open 缺陷:建议在 AL-09 一并提交宿主侧修复(P1,非模块阻塞项)
  • 模块代码若未来要彻底隔离,可迁至 app/modules/risk/——本次不做(属重构,收益 < 风险)