Files
group_xinghuo_jinrong/docs/项目框架设计/改进方案评审-问题清单与对比.md
T
GaoYiYuan_0626 037ce7edca docs(架构改进): 补齐 PRD/开发计划/TODO/交接文档,落地无密钥告警与 Redis 分布式锁
一、流程文档(按 AIcoding 六步落地,供新会话从交接文档开工)
- 新增 docs/PRD/PRD-架构改进与稳定性加固.md:6 条 FR(文档勘误、非缺陷说明、
  无密钥启动告警、审计失败告警、Redis 分布式锁、中间件顺序测试)
- 新增 docs/项目框架设计/改进方案评审-问题清单与对比.md:24 项问题分档 A~G,
  经两轮独立 AI 评审,无阻断级错误
- 新增 docs/项目框架设计/开发计划-架构改进.md:HOW 层设计,含合并前只做低风险
  11 项的批次策略
- 新增 docs/项目框架设计/TODO-架构改进.md:T-101~T-109、T-201~T-202 可勾选项
- 新增 docs/交接文档-架构改进.md:自包含交接入口,hy3 新会话可直接开工
- 新增 docs/项目框架设计/架构设计说明书.md:按模块/分层逐一讲解的全量架构说明

二、代码改动(T-107/108/109、T-201.1、T-201.2)
- app/main.py:启动时 DEEPSEEK_API_KEY 缺失告警,明确告知将走降级回复
- app/utils/authz.py:越权审计失败日志补 trace_id,便于串联全链路
- app/api/audit_middleware.py:审计失败日志补 status/path/request_id
- app/service/risk/redis_gateway.py:新增 acquire_lock(SET NX EX)与
  release_lock(Lua 原子释放,只删自己的锁)
- app/service/risk/locks.py:run_locked 改为双层锁,Redis 为主、进程内锁为备;
  Redis 超时沿用 fn(locked=False) 降级语义,Redis 不可用(含测试 Fake 缺方法的
  AttributeError)安全退回进程内锁,绝不抛异常

三、文档勘误(A1/A2/A3)
- MEMORY.md:文件数 42→45、Tools 4→5
- 02-mysql-agent专用.sql:会话表 5→6
- 架构设计-风控模块.md:同步更正

四、测试
- 新增 tests/test_locks_redis.py:覆盖抢锁成功、占用超时、Redis 故障降级、
  Fake 缺方法降级、只删自己锁、三处调用点 key 前缀
- tests/test_audit_middleware.py:补充告警字段断言
- 全量 pytest 510 passed(原基线 503)
2026-09-09 18:10:03 +08:00

30 KiB
Raw Blame History

XingHuo 架构改进方案评审 · 问题清单与方案对比

文档性质:送审材料。供另一个 AI 在全新上下文中独立审核。 代码基线:分支 risk-control-agent,HEAD 2d0e2fa(2026-09-08),全量 pytest 503 绿。 本次改动清单:无(零代码改动)。 本轮仅产出评审材料,未修改任何业务代码。 配套文档:《docs/项目框架设计/架构设计说明书.md》(本次评审的事实来源,§7.3 与 §9.4 为本清单的初始输入)。

审核记录

第一轮(2026-09-09,独立 AI 全新上下文审核)

总体结论:需修改后实施(小改)。 六项核心事实断言(A1 / A2 / B1 / B2 / C1 / F1)全部属实,证据确凿、推导正确,无阻断级事实错误。红线相关实现(审计只 INSERT、R-02 唯一阻断路径、Core 只读)在代码中均有落点,未发现颠覆性安全或正确性遗漏。

审核提出 3 项重要修正 + 2 项可选修正,本轮已全部吸收,修订点如下:

# 审核意见 档位 本轮处理 位置
1 只加 UNIQUE 不加重试,并发会从静默脏数据变成 500 硬失败 重要 已补充实施配套要求 §3 B1
2 §5 缺"生产部署拓扑(单进程 vs 多实例)"决策项 重要 已新增待确认第 7 项 §5
3 放开 convert 后,聚合层两处 SQL 过滤会让它"隐形" 重要 已补充并标为易漏点 §3 F1
4 next_seq_no + insert_message 是更危险的并行取号路径 可选 已补充警示 §3 B1
5 C1 严重度略夸大(方法全为固定 SELECT,非纯约定) 可选 已校正表述 §3 C1

未吸收意见:无。


0. 结论速览

状态 数量 说明
未修复 · 建议修 9 正确性隐患与合规风险,见 §3 B/C 类
未修复 · 文档口径错误 5 改文档即可,零代码风险,见 §3 A 类
待确认 · 需人工拍板 4 涉及接口协议或架构选型,见 §5
非缺陷 · 仅需文档标注 3 设计正确但易被误读,见 §3 G 类
合计 21

三条最需要优先处理的(按"后果严重性 × 触发概率"排序):

  1. B1 agent_message 的 (session_id, seq_no) 是普通索引而非唯一索引 → 并发同会话会静默产生重号消息(本轮新发现,此前未记录)
  2. C1 core_ro 只读纯靠约定,无 DB 级账号保护 → 项目五条红线之一是"Core C1~C5 不可被覆盖",当前无任何强制手段
  3. B2 中间件执行顺序无测试守卫 → 调换两个装饰器会静默丢失全站 trace_id,且不报错

1. 本次改动清单

无。

原因:本轮会话(2026-09-09)全程为架构讲解与术语答疑,未形成任何经确认的改动方案。用户于本轮末尾选定"先出方案对比文档,不改码",故本轮仅产出本材料,等待拍板后再实施。

文件 改动点 原因
— — 本轮零代码改动

2. 问题汇总表

ID 问题 类别 状态 影响范围 优先级
A1 注入词表文档写 42 条,实测 45 条 文档口径 未修复 MEMORY.md §0 低
A2 风控对话 Tool 文档写 4 个,实测 5 个 文档口径 未修复 MEMORY.md 仓库地图 低
A3 Agent 专用表 SQL 注释写 5 张,实建 6 张 文档口径 未修复 02-mysql-agent专用.sql 低
A4 scoring.py 实为 NotImplementedError 桩,文档未标明 文档口径 未修复 L3 risk_score 恒 NULL 中
A5 locks.py 实为进程内锁,文档未标明 文档口径 未修复 多实例部署 中
B1 agent_message 的 seq 索引非唯一,并发会静默重号 正确性 未修复 消息顺序 / LLM 上下文 高
B2 中间件执行顺序无测试守卫 正确性 未修复 全站 trace_id 高
B3 deny() 双写无事务,可能只写一半 正确性 未修复 安全台账完整性 中
B4 record_trade_alerts 出单双写无事务 正确性 未修复 审计完整性 中
B5 限流 INCR + 首命中 EXPIRE 非原子 正确性 未修复 限流窗口 中
B6 locks.py 进程内锁,多实例失效 正确性 未修复 预警聚合 / L3 中
C1 core_ro 无 DB 级只读账号 安全合规 未修复 Core 红线 高
C2 dev debug 通道误配即放开无签名身份 安全合规 未修复 生产环境 高
C3 审计 / 限流 fail-open 策略不可配置 安全合规 未修复 合规强场景 中
C4 无 DeepSeek key 时静默降级,无告警 安全合规 未修复 生产可观测性 中
D1 utils/logger.py 仅一行 docstring,日志无落盘 可观测性 未修复 全站排障能力 中
E1 权限逻辑分散在矩阵 / chat / tool 三处 可维护性 未修复 改权限需三处同步 中
E2 Tool 注册表分裂为三套 可维护性 未修复 新增 Tool 低
E3 model/ 层闲置,跨层全用 dict 可维护性 未修复 类型安全 中
E4 注入词表硬编码 45 条,扩展需改代码 可维护性 未修复 对抗性 prompt 低
F1 不支持 convert 交易类型(DB 支持,网关拒绝) 能力 待确认 接口协议 待确认
F2 意图识别为关键词匹配,换说法即漏触 能力 待确认 对话命中率 待确认
F3 R-05 动态评分未实现 能力 未修复 L3 评分 低
G1 trace_id 可前端伪造 非缺陷 设计如此 需文档标注 —
G2 trace_id 概率唯一,无冲突检测 非缺陷 设计如此 需文档标注 —
G3 admin.py / knowledge.py 空壳 非缺陷 一期范围 需文档标注 —

说明:F1~F3 属能力增强而非缺陷,是否做取决于产品排期;G 类为"看起来像问题但设计正确",只需在文档中写明,避免后续被重复提出。


3. 逐条方案对比

A 类 · 文档口径错误(5 项,改文档即可)

A1 input_guard.py:43-94 实测 45 条(指令覆盖 18 + 角色重置 11 + 系统提示泄露 9 + 越权诱导 7),MEMORY.md §0 写 42 条。

  • 方案 A:改 MEMORY.md 为 45 条 —— 零风险,10 秒完成
  • 方案 B:不改 —— 后续读者按 42 条核对会误判
  • 建议:A。同源问题 A2/A3 一并改。

A2 chat_tools.py:328 RISK_TOOL_REGISTRY 实际注册 5 个(query_overdue_alerts / alert_query / customer_context / suitability_check / aml_lookup),另有 query_agent_behavior:276 定义但未注册。文档写"四个"。

A3 02-mysql-agent专用.sql 注释写 5 张,实建 6 张(多出 risk_aml_list)。与 entities.py 注释"jinrong_agent 17 张"一致(11+6)。

A4 service/risk/scoring.py:18 recompute_customer_score 直接 raise NotImplementedError,是签名冻结的预留桩,不是实际评分器。L3 的 risk_score 一期恒 NULL。文档应明示,否则读者会以为评分已生效。

A5 service/risk/locks.py:31 用 threading.Lock,是进程内锁,注释自陈"多进程部署换 Redis SET NX,接口不变"——是 TODO,非已完成。


B 类 · 正确性隐患

B1 agent_message 并发会静默产生重号消息(本轮新发现)

证据链:

  1. session_repository.py:241-252:insert_turn 在事务内取 MAX(seq_no)+1
  2. 该事务的 SELECT 是非锁定读,MySQL InnoDB 默认 REPEATABLE READ 下两个并发事务仍可读到相同的 MAX 值
  3. 01-mysql-共用底座.sql 中 agent_message 只有 KEY idx_session_seq (session_id, seq_no) —— 普通索引,非 UNIQUE
  4. 代码注释自陈:"并发同会话不重号(并发写锁归后续)"——承认未做完

后果:两个事务算出同一个 seq,各自 INSERT 成功,产生两条 seq_no 相同的消息。list_messages 按 seq_no 排序时,同号两条的相对顺序由物理存储决定 → LLM 读到的上下文可能乱序("AI 先答、用户后问")。

触发条件:同一会话并发请求——多端同时发问、前端重复提交、SSE 断连重试。单用户串行对话不会触发。

方案 做法 收益 代价
A(推荐) 给 (session_id, seq_no) 加 UNIQUE 索引 重号从"静默脏数据"变为"数据库报错",可被 insert_turn 捕获后重试;同时提升查询性能 需先核查历史数据有无重号,有则先清洗;DDL 变更需确认
B 会话级 Redis 锁串行化 彻底避免并发 引入新依赖点,Redis 挂时退化;需设计锁粒度与超时
C 改用 AUTO_INCREMENT 的 id 排序 零冲突 改变现有 seq 语义,影响 list_messages_page 分页与前端

建议:优先 A。它是唯一能让"问题显式暴露"的方案——B/C 只是绕开,A 是把隐性错误变成显性失败,符合项目"宁多勿漏"的一贯口径。

⚠️ 实施 A 的必要配套(第一轮审核补充) 当前 insert_turn 没有任何 IntegrityError 捕获与重试逻辑。若只加 UNIQUE 索引而不加重试,并发同会话请求会从"静默脏数据"直接变成 500 硬失败——问题同样没解决,只是换了个表现形式。 实施时必须同步补:捕获 IntegrityError → 重读 MAX(seq_no) → 重试(建议 3 次,与 L3 乐观锁的重试次数保持一致)。

⚠️ 另有一条更危险的并行取号路径(第一轮审核补充) session_repository.py:134 next_seq_no() 用非事务的 .connect() 取号,:142 insert_message() 单独 INSERT——取号与写入分离,连事务都没有,竞态比 insert_turn 更严重(连"同事务"这层缓解都没有)。 insert_turn 是后来为修方案 C 评审 P0/P1 才新增的原子路径,旧的组合方式仍留在代码里。建议统一走 insert_turn,废弃 next_seq_no + insert_message 组合,或至少在方法 docstring 加警示。

B2 中间件执行顺序无测试守卫

现状:main.py 中 audit 先注册(81 行)、trace 后注册(87 行)。Starlette add_middleware 用 insert(0) 且 build_middleware_stack 反向包裹 → 数组越靠前越外层 → trace 在外层先执行,audit 在内层,与其注释"执行序在 trace 之内"吻合。

风险:调换两个装饰器位置后,audit 会在 trace 之前执行,current_trace() 返回空串,全站审计静默丢失 trace_id,且不报错、不失败。

方案 做法 收益 代价
A(推荐) 加断言测试:发起请求后校验 audit_log.trace_id 非空 零业务代码改动,回归即发现 需访问数据库断言,或 mock 仓储捕获入参
B 在 audit_middleware 入口显式 ensure_trace() 兜底生成,不依赖顺序 掩盖顺序错误,trace_id 与响应头不一致

建议:A。B 会让"顺序错了"这件事从可见故障变成隐性不一致,反而更难查。

B3 / B4 两处双写无事务

  • B3 utils/authz.py:44-76:两次 insert 在同一 try 内但无事务 → 可能 audit_log 成功、input_guard_log 失败
  • B4 alert_service.py:174-176:insert_alert 与 _audit 两次独立 INSERT,无事务 → 可能出单成功、审计丢失

对照:handle_alert_with_audit(risk_repository.py:331)是同事务的,注释明确"审计失败整体回滚,不产生无痕状态变更"。

方案 做法 收益 代价
A 三处统一改同事务 语义一致,审计完整 审计故障会连带回滚业务单据,与当前 fail-open 口径冲突,需先定 C3
B 维持现状,但加"审计写入失败"计数指标与告警 保留可用性优先,同时让丢失可见 需引入指标设施(当前无)
C(建议) 维持现状 + 在文档中固化判据:"新增可丢留痕,变更不可丢" 零改动,消除后续争议 无

建议:C 为当前最优。B3/B4 都是"新增"场景,最坏少一条留痕,业务单据仍在,与 §3 中"处置双写必须同事务"形成清晰判据。但这条判据目前只存在于代码注释里,应写进文档。

B5 限流 INCR + 首命中 EXPIRE 非原子

input_guard.py 的 check_rate_limit:首次命中时先 INCR 再 EXPIRE,两步之间进程崩溃会留下无 TTL 的计数键 → 该 actor 被永久限流。

方案 做法 代价
A 改用 Lua 脚本或 SET key 1 EX 60 NX + INCR 组合 需改 Redis 调用方式
B 用 Redis 的 EXPIRE 幂等重试 / 每次 INCR 后无条件 EXPIRE 极小改动:每次都发 EXPIRE,不做"首命中"判断

建议:B,改动一行级别,可接受。

B6 进程内锁多实例失效

locks.py:31 用 threading.Lock,key 为 agg:event:{customer_id}:{date} / l3:{customer_id}。多进程/多实例部署时,两个实例各自持锁,锁形同虚设。

缓解现状:L3 更新另有乐观锁(computed_at 比对 + 3 次重试)兜底,跨进程天然有效,所以最坏是"多出一张重复单"而非数据错乱。

方案 做法 代价
A 换 Redis SET key val NX EX ttl,接口 run_locked 不变 引入对 Redis 的强依赖,Redis 挂时退化为无锁(当前已 fail-open)
B 维持现状,文档标注"单进程部署前提" 零成本,但多实例时失效

建议:B(标注前提)+ 待多实例部署时再做 A。注释已预留接口,切换成本可控。


C 类 · 安全与合规

C1 core_ro 无 DB 级只读账号(红线相关)

项目五条红线之一:"Core 正式 C1~C5 不可被画像覆盖"。当前保障手段仅为:双库物理隔离 + core_ro.py 类注释"仅 SELECT"。

实际风险:任何持有 mysql_core_database engine 的代码都能执行 INSERT/UPDATE,无 SQL 层拦截、无 DB 只读账号。唯一允许写 Core 的 gateway_repository.insert_trade 与 core_ro 相比只是"另一个类",无任何机制性区分。

审核校正(第一轮):core_ro 的方法全部是硬编码的固定 SELECT,要破坏它需要主动新增写方法,因此比"纯君子协定"略强。但本质上仍是"靠人不犯错",而非机制保障。严重度维持高,理由表述以本段为准——不夸大,也不因"暂时没出事"而降级。

方案 做法 收益 代价
A(推荐) 为 core_ro 配置独立的 MySQL 只读用户(GRANT SELECT ONLY) 从根基卡死,代码怎么写都写不进去 需运维配合;gateway_repository 必须用另一个有写权限的账号,连接串要拆分
B 代码层加 SQL 白名单校验(只允许 SELECT 开头) 不依赖运维 易被绕过(注释、子查询、存储过程);有性能开销
C 维持现状 零成本 红线无强制保障

建议:A。这是唯一"不可逆"的保障方式——B 是软约束,C 无保障。注意 A 需要拆连接串:get_engine(settings.mysql_core_database) 目前只有一个账号,读写共用。

C2 dev debug 通道误配风险

deps.py:258:仅当 app_env == "development" 且 jwt_public_key_path 为空时才走 X-Debug-Role / X-Debug-Actor 兜底。

风险:生产环境若误配 app_env=development 且未设公钥 → 任何人都能用 X-Debug-Role: risk_officer 头伪造身份,且无签名。

现有缓解:main.py:49-57 的 lifespan 在非 development 环境强制校验 JWT 就绪,且 AUTH_FACTORY_IS_DEBUG 双保险。

方案 做法 代价
A 维持现状,在 .env.example 与部署文档加粗警示 零成本
B 启动时若 app_env=development 打 WARNING(已有类似:dev secret 默认值告警) 一行代码

建议:A + B 组合。B 可复用 main.py:61-65 现有的 dev secret 告警模式,改动极小。

C3 审计 / 限流 fail-open 策略不可配置

utils/authz.py:11-12 注释自陈"降级口径与 T-02 审计一致……生产可切 fail-closed(待决,已登记)"。

当前所有审计失败都是 fail-open(仅 logger.exception),业务语义不变。合规强场景可能需要 fail-closed。

建议:纳入 §5 待确认事项,需要业务方明确"审计丢失"与"业务中断"哪个更不可接受。技术上是加一个配置项的事,但这是业务决策不是技术决策。

C4 无 DeepSeek key 时静默降级

agent_service.py:148-150:if not settings.deepseek_api_key: reply = _degraded_reply(state),返回带 _DEGRADED_PREFIX 的摘要,不抛异常、不告警。

风险:生产漏配 key 时,系统"看起来正常",只是所有回答都是降级内容。用户和运维都难以察觉。

方案 做法 代价
A(推荐) 启动时若 key 缺失,打 WARNING 日志(可复用 lifespan 里 jwt_ready() 的模式) 一行代码
B 降级回复中明确告知用户"当前为降级模式" 影响演示体验
C 生产环境 key 缺失直接启动失败 影响演示/CI

建议:A。main.py 已有 jwt_ready() 的启动告警先例,风格统一。


D 类 · 可观测性

D1 utils/logger.py 完全占位

文件内容仅 """日志模块。""" 一行。全站 logger.warning/exception 走 Python 默认配置,只输出到控制台,无格式化、无落盘、无轮转、未接 trace_id。

对比:审计(audit_log)做得相当扎实——六处落点、只 INSERT、结构化可查。日志侧则是空白。

方案 做法 收益 代价
A 配置 logging.basicConfig + 文件 handler + 格式化串带 trace_id 排障能力质变 需注入 trace_id 到 LogRecord(可用 filter)
B 引入 structlog / loguru 结构化日志 新增依赖,需确认(技术选型硬阀门)
C 维持现状 零成本 线上问题基本无法追溯

建议:A 起步。用 logging.Filter 把 current_trace() 注入每条日志记录,是收益/成本比最高的改动,且无需新增依赖。


E 类 · 可维护性

ID 问题 方案 代价
E1 权限逻辑分散三处(矩阵 deps.py:46 / chat 显式 deny chat.py:122 / tool fail-closed tool_service.py:138) A:维持三层纵深,但在文档固化"三层各自职责"
B:收拢到一处配置
A 零成本
B 改动大,且会削弱纵深防御
E2 Tool 注册表分裂为 core / risk / kb 三套 A:抽统一 ALL_TOOLS 聚合器 低风险,但收益有限
E3 model/ 闲置(31 个 ORM 类不用),跨层全 dict 传参 A:逐步用 TypedDict 约束仓储入参
B:维持现状
A 渐进式,无编译期风险
B 保持敏捷但拼写错误运行时才暴露
E4 注入词表硬编码 45 条 A:配置化(.env 或 DB)
B:维持现状 + 持续红队补充
A 需设计加载与热更新

E1 建议:A。三层不是冗余,是纵深防御——① 是能力声明、② 是业务线约束、③ 是兜底。收拢反而危险。只需写清文档避免后人"好心合并"。

E3 建议:A 渐进式。优先给 insert_alert / insert_audit_log 这类高频入口加 TypedDict。


F 类 · 能力增强(非缺陷)

F1 支持 convert 转换交易(用户指定纳入)

现状:

  • scripts/core/01-ddl.sql:152:trade_type ENUM('subscribe','redeem','convert') —— 数据库支持
  • trade_gateway.py:36:SUPPORTED_TRADE_TYPES = ("subscribe", "redeem")
  • trade_gateway.py:112-113:if trade_type == "convert": raise UnsupportedTradeType(CONVERT_MESSAGE)
  • 提示语:"转换交易暂不支持,请分别发起申购/赎回"

即:Core(模拟真实交易系统)有这个能力,网关主动收窄了。

为什么当前不支持(代码层面的硬约束):

  1. 请求体 req 只有 {customer_id, product_id, trade_type, amount} —— 单个 product_id
  2. check_suitability(customer_id, product_id) 只接受一个产品 —— 转换需要校验转入那只是否匹配客户风险等级
  3. amount 语义会分裂:申购/赎回时是金额,转换时是份额

方案对比:

方案 做法 收益 代价 / 风险
A 请求体加 target_product_id,convert 时校验转入产品适当性,转出产品走持仓校验 业务完整,符合真实交易系统能力 改接口协议(需确认);引擎规则要重新定义(转换算不算当日累计?算几笔?);审计口径、前端都要跟着改
B 维持不支持,仅优化提示语与文档说明 零风险 业务缺口保留
C 网关层把 convert 拆解为 redeem + subscribe 两笔内部交易 不改接口协议,复用现有校验 语义变化大(变成两笔交易),trade_id 与审计口径都要变;金额换算复杂

建议:本轮不做,列入待确认。若要做,方案 A 是唯一正确的——C 会把一笔业务变成两笔,在金融审计上是灾难。

⚠️ 除网关外还有两处硬编码过滤(第一轮审核补充,最易漏) core_ro.py:390(list_trades_range)与 :432(sum_trades_on_date)的 SQL 里写死:

AND trade_type IN ('subscribe', 'redeem')

即便网关放开 convert,这两处不改的话,convert 交易在 RISK-001(大额)/ RISK-002(当日累计)/ RISK-003(频繁交易)的聚合统计中会完全"隐形"——规则引擎压根看不到它,等于给大额与累计规则开了一个绕过口子。 这是放行 convert 时最容易漏掉的一处,且漏了不会报错,只会静默少算。

前置依赖:需产品明确三件事:① 转换的适当性校验对象是转入产品还是两只都校验;② 转换金额是否计入 risk_daily_total 当日累计;③ 转换在 RISK-003 频繁交易规则中算一笔还是两笔。

F2 意图识别为关键词匹配

tool_service.py:100-112 match_intent:按 agent 分组遍历关键词,any(k in message for k in kws),命中即停(单意图)。

  • 换说法即漏触("我的基金" vs "持仓")
  • 不支持多意图("我的持仓和适当性"只取首条)
方案 收益 代价
A:换 LLM function calling 支持多意图、泛化好 增加一次 LLM 调用延迟与成本;判定不可复现,不利于审计
B:扩充同义词表 + 支持多意图 保持零延迟、可复现、可单测 仍需人工维护词表
C:混合(关键词优先,未命中降级 LLM) 兼顾 实现复杂度最高

建议:若命中率是痛点,先做 B(成本低、保持可审计性);A 的"不可复现"在金融审计场景下是明显劣势。

F3 R-05 动态评分未实现

scoring.py 抛 NotImplementedError,签名已冻结。L3 risk_score 一期恒 NULL。是否做取决于产品排期。


G 类 · 非缺陷(设计正确,仅需文档标注)

ID 事项 为什么不是问题
G1 X-Trace-Id 可前端伪造 设计如此:有格式白名单 ^[A-Za-z0-9._-]{1,64}$ 防响应头注入;身份只认 JWT,trace_id 从不用于权限判定。需在文档写明"不得用作权限凭据"
G2 trace_id 概率唯一(uuid4().hex[:16] = 64 bit),无冲突检测 请求级 ID,按生日悖论约 2^32 次请求才有 50% 碰撞;真撞了只是两条日志串一起,不影响业务数据。无需数据库唯一校验
G3 admin.py / knowledge.py 空壳 T-21 拍板"一期只做脚本入库"。二者未 include_router,不影响启动。注意 knowledge.py 空 ≠ RAG 缺失(能力在 service/rag_service.py)

4. 风险点与回归验证步骤

本节针对"若决定实施上述改动"给出。本轮未改动任何代码,以下为预案。

4.1 通用风险

风险 说明 缓解
测试基线漂移 当前基线 503 绿 任何改动前先跑全量确认基线,改动后对比
路由清单失配 tests/test_main.py::test_all_routers_mounted 硬编码路由清单 增改路由必须同步更新该清单,否则必红
SQLite / MySQL 差异 单测用 SQLite,生产 MySQL 涉及 DDL / 日期函数的改动需双跑
双库事务 跨库无法 JOIN、无分布式事务 任何跨库改动只能最终一致,需补偿机制

4.2 分类回归清单

若改 A 类(文档口径)

  • 无代码改动 → 无需回归测试
  • 验证:人工核对 MEMORY.md 与代码计数一致

若改 B1(加 UNIQUE 索引)

  1. 先执行 SELECT session_id, seq_no, COUNT(*) FROM agent_message GROUP BY session_id, seq_no HAVING COUNT(*) > 1 确认历史无重号
  2. 有重号 → 先清洗再建索引
  3. 建索引后跑全量 pytest
  4. 并发用例:模拟同会话并发 insert_turn,确认抛异常而非静默重号
  5. 验证 list_messages_page 分页不受影响

若改 B2(加中间件顺序测试)

  1. 新增测试:发起任意请求 → 断言 audit_log 中该行 trace_id 非空
  2. 反向验证:临时调换装饰器顺序 → 测试应变红(确认测试有效)
  3. 恢复顺序 → 测试转绿

若改 B5(限流 EXPIRE)

  1. 单测覆盖:首次请求后键存在且带 TTL
  2. 模拟 EXPIRE 失败 → 确认不产生无 TTL 键

若改 C1(只读账号)

  1. 拆分连接串:core_ro 用只读账号,gateway_repository 用可写账号
  2. 验证 submit_trade 仍能写 core_trade
  3. 验证 core_ro 所有查询正常
  4. 尝试用 core_ro 执行 INSERT → 应被数据库拒绝
  5. 全量 pytest + uvicorn 冒烟三端点

若改 C4(启动告警)

  1. 清空 .env 的 DEEPSEEK_API_KEY → 启动应打 WARNING
  2. 填回 → 无告警

若改 D1(日志配置)

  1. 验证日志落盘
  2. 验证 trace_id 出现在每条记录中
  3. 验证并发请求下 trace_id 不串号(关键:contextvar 传播)

若改 F1(支持 convert)

  1. 接口协议变更需先经确认
  2. 单测:convert 正常路径、转入产品适当性不匹配路径、持仓不足路径
  3. 引擎回归:确认转换对 RISK-001/002/003 的计数影响符合产品定义
  4. 审计回归:确认 convert 落审计的 decision 取值正确
  5. 前端/演示脚本同步

4.3 最小回归命令

# 1. 建立基线
python -m pytest -q                      # 期望 503 passed

# 2. 启动冒烟
uvicorn app.main:app --reload
curl http://127.0.0.1:8000/health        # 期望 {"status":"ok",...}

# 3. 接口实调(按演示 SOP)
#    - POST /api/simulate/trade  阻断路径 + 放行路径
#    - POST /api/chat            同步对话
#    - POST /api/chat/stream     SSE 流式(确认 [DONE] 与落库)
#    - GET  /api/risk/alerts     台账只读

# 4. 改动后重跑 1~3,对比基线

5. 待确认事项(需人工拍板,本轮不擅自决定)

# 事项 为什么需要拍板 关联 ID
1 审计失败是否从 fail-open 切 fail-closed 业务决策:审计丢失 vs 业务中断,哪个更不可接受 C3
2 是否支持 convert 交易类型 改接口协议;需产品定义三条业务口径(见 F1) F1
3 是否为 core_ro 建 DB 只读账号 需运维配合;连接串要拆分 C1
4 意图识别是否升级为 LLM 命中率 vs 可审计性的取舍 F2
5 是否引入日志库(structlog / loguru) 涉及技术选型硬阀门,新增依赖需确认 D1
6 insert_turn 并发控制:加唯一索引 or 引入会话锁 方案选择,涉及 DDL 变更;且须同步加重试(见 B1) B1
7 生产部署拓扑:单进程 or 多实例 locks.py 进程内锁、insert_turn 取号竞态均以单进程为前提;多实例时 B6 锁直接失效、B1 触发概率显著上升。此项不拍板,B1/B6 的方案选择无从谈起(第一轮审核补充) B1 / B6

6. 给审核 AI 的检查清单

请逐项核验并在审核意见中给出结论:

事实核验(对照代码,勿信本文断言)

  • §2 表中每一项的状态(未修复 / 待确认 / 非缺陷)是否与代码一致
  • A1~A5 的"文档口径 vs 实测"数值是否属实(尤其是 A1 的 45 条、A2 的 5 个 Tool)
  • B1 的并发重号问题:确认 agent_message 索引确为普通 KEY 而非 UNIQUE,确认 insert_turn 的 SELECT 未加锁
  • B2 的中间件栈序推导是否正确(Starlette add_middleware 的 insert(0) + build_middleware_stack 的 reversed)
  • C1 是否真的无法写 Core(确认无 DB 级只读账号、无 SQL 层拦截)
  • F1 中"DB 支持 convert 但网关拒绝"是否属实

方案评估

  • 每条"建议"是否与其代价分析自洽(有无只谈收益不谈代价)
  • B1 推荐方案 A(加唯一索引)是否会破坏现有数据或测试
  • C1 推荐方案 A(只读账号)是否与 gateway_repository 需写 Core 冲突,拆分连接串是否可行
  • E1 建议"维持三层权限纵深"是否合理,还是确实应该收拢

遗漏检查

  • 是否存在本文未覆盖的正确性或安全性问题
  • §4 回归清单是否覆盖了所有高风险改动
  • §5 待确认事项是否有遗漏(尤其是需要业务方而非技术方决策的)

结论要求

  • 明确给出:可实施 / 需修改后实施 / 不建议实施
  • 若发现本文事实性错误,请直接指出并给出正确结论