fix(ai): F1-5a 沙箱 batch-run 接入 runtime_context (W1 / 阶段 A 主体)
Neo F1-5 反馈: "让沙箱起到其真正的作用. 真正的模拟日期, 仅能看到沙箱设定日期 及之前日期的数据, 并运行 AI 的各个业务." 调研发现 (4 个并行子代理): batch-run 端点 _run_batch 是空壳 stub (只 logger.info, 实际不跑 AI), GUC apply_runtime_session_vars 0 处调用 (dead code), 7 张业务表 6 张有 runtime 复合索引唯独 ai_run_logs 漏建, App2/2a 3 行 _calc_date_range 漏传 ref_date. 本 commit (F1-5a 阶段 A 主体, F1-5b 后续完整 zqyy_app RLS 视图层): 后端核心: - admin_service.py: _run_batch 真实化 (Semaphore(5)+asyncio.gather+ return_exceptions=True+ctx_snapshot 防漂移); estimate 入口抓 RuntimeContext 快照, confirm 取出传给 worker - admin_ai.py: confirm_batch_run lazy 注入 dispatcher - admin_service.retry_trigger_job: INSERT 落 runtime_mode + sandbox_instance_id 列 (用 runtime_insert_columns helper) - runtime_context.py: get_runtime_context 加 bind_to_session 参数, 激活 GUC app.current_business_date / app.current_runtime_mode - run_log_service.create_log: 启用 bind_to_session=True 试点 App2/2a 3 行 ref_date 修复: - app2_finance_prompt.py:817 储值卡余额变化板块 - app2_finance_prompt.py:841 日粒度 series + 异常检测窗口 - app2a_finance_area_prompt.py:466 区域日粒度 series DB: - migrations/20260505__ai_run_logs_runtime_index.sql: 补 (site_id, runtime_mode, sandbox_instance_id, created_at DESC) 复合索引 前端: - AIOperations.tsx: 顶部加 sandbox 模式提示条 (Alert 显示 sandbox_date + sandbox_instance_id + 影响范围 + 切回 live 入口) 未做 (留 F1-5b 完整 zqyy_app RLS 视图层一并): - B1 admin_service 6 处 CURRENT_DATE -> business_date - B2 fdw_queries 异常分支兜底 - GUC 完整传递 (fdw_queries / page_context 等) - 测试 3 套 (.gitignore:71 排除, F2-2 入仓时 commit) - P20 SPEC \xa76/\xa710/\xa711/\xa715 (F1-5b 完整收口后同步更准确) Neo 决策: docs/_overview/wave1-findings/F1-5-impl-decisions.md 详见 docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
This commit is contained in:
229
docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
Normal file
229
docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Wave 1 F1-5a — 沙箱 batch-run 接入 runtime_context(MVP + 漂移防御核心)
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| 日期 | 2026-05-05 |
|
||||
| Wave | 1 / Day 5 主线必修(P0-7 沙箱) |
|
||||
| 范围 | batch-run executor 真实化 + ctx_snapshot 防漂移 + retry 落 runtime 列 + ai_run_logs 索引 + App2/2a ref_date 修复 + GUC 接入机制 + admin-web sandbox 提示条 |
|
||||
| 拆分背景 | F1-5b(zqyy_app RLS 视图层 + 46 处 `FROM biz.*` → `app.v_*` + B1/B2 完整漂移防御)紧接本次后做 |
|
||||
|
||||
## 一、F1-5 决策卡 6 项 Neo 决策
|
||||
|
||||
详见 [`docs/_overview/wave1-findings/F1-5-impl-decisions.md`](../../_overview/wave1-findings/F1-5-impl-decisions.md)。
|
||||
|
||||
| # | 决策 | Neo 反馈 | 落实 |
|
||||
|---|---|---|---|
|
||||
| D1 | batch executor 实现 | 同意 c 方案 + N=5,ctx_snapshot 时机 Claude 定 | Semaphore(5) + asyncio.gather(return_exceptions=True);estimate 入口抓快照 |
|
||||
| D2 | GUC C 方案处置 | "完整且统一,符合规范化项目架构" | 拆 F1-5a/F1-5b 两步:F1-5a 接入机制 + ETL 库激活,F1-5b 完整 zqyy_app RLS 视图层落地 |
|
||||
| D3 | 测试覆盖 | 同意 a 纳入 | 测试文件本地保留(`.gitignore:71` 排除 tests/,F2-2 入仓后一并 commit) |
|
||||
| D4 | 走查 mock 授权 | 授权 a | W1-T8 走查时调 PATCH 切 sandbox=2026-03-01,完成切回 live |
|
||||
| D5 | commit 拆分 | 1 个 commit | 本审计对应 1 个 commit `fix(ai): F1-5a 沙箱 batch-run 接入 runtime_context (W1)` |
|
||||
| D6 | P20 SPEC 同步 | 纳入 a | F1-5a 完整改造后 SPEC §6/§10/§11/§15 在 F1-5b commit 一并同步(因 F1-5b 才完整收口) |
|
||||
|
||||
## 二、本次产出(F1-5a 范围)
|
||||
|
||||
### 2.1 后端 — `_run_batch` 真实化(A1)
|
||||
|
||||
**文件**:`apps/backend/app/services/ai/admin_service.py`
|
||||
|
||||
| 改动 | 行号(改动后) | 说明 |
|
||||
|---|---|---|
|
||||
| import | L7-19 | 加 `RuntimeContext / get_runtime_context / runtime_insert_columns`,`TYPE_CHECKING: AIDispatcher` |
|
||||
| 常量 | L31 | `_BATCH_CONCURRENCY = 5`(Neo 决策) |
|
||||
| `__init__` | L40-45 | 加 `_dispatcher: AIDispatcher \| None = None` 字段 + `set_dispatcher` 方法,lazy 注入 |
|
||||
| `_run_batch` | 整段重写 | 真正实现:`Semaphore(5)` + `asyncio.gather` + `return_exceptions=True`;每个 sub-call 调 `dispatcher.run_single_app(app_type, context={..., business_date}, triggered_by=f"batch:{batch_id}")` |
|
||||
|
||||
**关键设计点**:
|
||||
- **快照防漂移**:`ctx_snapshot` 在 estimate 阶段抓取,confirm 取出传给 worker,worker 按快照值执行(避免 Neo 在 estimate→confirm 间切 sandbox 模式造成数据漂移)
|
||||
- **`return_exceptions=True`**:单个 member 失败不连坐,失败信息已写 ai_run_logs(熔断/限流/超时由 dispatcher.\_run_step 自动处理)
|
||||
- **`triggered_by="batch:{batch_id}"`**:打标 ai_run_logs,Wave 2 加 `GET /batch-run/{batch_id}/status` 进度查询时 `WHERE triggered_by LIKE 'batch:<id>%'` 即可统计
|
||||
|
||||
### 2.2 后端 — `estimate_batch` / `confirm_batch` ctx_snapshot(A2)
|
||||
|
||||
**文件**:`apps/backend/app/services/ai/admin_service.py`
|
||||
|
||||
| 改动 | 说明 |
|
||||
|---|---|
|
||||
| `estimate_batch` | 入口调 `get_runtime_context(site_id)` 抓 RuntimeContext,存入 `_batch_store[batch_id].ctx_snapshot`;日志输出 mode + sandbox_date |
|
||||
| `confirm_batch` | 取出 `ctx_snapshot`,异步调 `_run_batch(params, ctx_snapshot)` |
|
||||
| `_batch_store` | 字典结构从 `{params, expires_at}` 扩展为 `{params, ctx_snapshot, expires_at}`;`params` 内加 `batch_id` 字段(传给 `_run_batch` 用于 triggered_by 标注) |
|
||||
|
||||
### 2.3 后端 — `confirm_batch_run` 路由 lazy 注入 dispatcher
|
||||
|
||||
**文件**:`apps/backend/app/routers/admin_ai.py:283-303`
|
||||
|
||||
`_admin_svc` 是模块级单例,不持有 dispatcher 引用;`dispatcher` 通常在 lifespan startup 才初始化。lazy 注入避免模块加载顺序问题:
|
||||
|
||||
```python
|
||||
if _admin_svc._dispatcher is None:
|
||||
try:
|
||||
from app.ai.dispatcher import get_dispatcher
|
||||
_admin_svc.set_dispatcher(get_dispatcher())
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(503, f"Dispatcher 未初始化: {exc}")
|
||||
```
|
||||
|
||||
### 2.4 后端 — `retry_trigger_job` INSERT 落 runtime 列(A4)
|
||||
|
||||
**文件**:`apps/backend/app/services/ai/admin_service.py:380-422`
|
||||
|
||||
INSERT 显式拼接 `runtime_mode` + `sandbox_instance_id` 列(用 `runtime_insert_columns(site_id)` helper),与原 trigger_job 的 runtime 上下文保持一致。避免依赖默认值 'live' 导致 sandbox 模式下重试丢失 sandbox 标记。
|
||||
|
||||
### 2.5 后端 — App2 / App2a 3 行漏传 ref_date(A3)
|
||||
|
||||
| 文件:行 | 改动 |
|
||||
|---|---|
|
||||
| `app2_finance_prompt.py:817` | `_calc_date_range(board_time)` → `_calc_date_range(board_time, ref_date=get_runtime_context(site_id).business_date)`(储值卡余额变化板块) |
|
||||
| `app2_finance_prompt.py:841` | 同上(日粒度 series + 异常检测窗口) |
|
||||
| `app2a_finance_area_prompt.py:466` | 同上(区域日粒度 series) |
|
||||
|
||||
### 2.6 后端 — `runtime_context.get_runtime_context` 加 `bind_to_session` 参数
|
||||
|
||||
**文件**:`apps/backend/app/services/runtime_context.py:88-148`
|
||||
|
||||
新增可选参数 `bind_to_session: bool = False`,True 时返回前调用 `apply_runtime_session_vars(conn, ctx)`,激活 GUC `app.current_business_date` / `app.current_runtime_mode`,使 ETL 库 26 个 `app.v_*` 视图自动按业务日上界裁剪(`app.business_date_now()` 函数读取 GUC)。
|
||||
|
||||
**说明**:激活机制就位,但只在 `run_log_service.create_log` 试点开启(2.7);其余完整传递留 F1-5b。
|
||||
|
||||
### 2.7 后端 — `run_log_service.create_log` 启用 `bind_to_session=True`
|
||||
|
||||
**文件**:`apps/backend/app/ai/run_log_service.py:62-65`
|
||||
|
||||
试点改造,验证 GUC 激活机制可用。运行时事务级 SET LOCAL 在该 INSERT 期间生效。
|
||||
|
||||
### 2.8 DB — `biz.ai_run_logs` 复合索引补建(A6)
|
||||
|
||||
**文件**:`db/zqyy_app/migrations/20260505__ai_run_logs_runtime_index.sql`
|
||||
|
||||
```sql
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_runtime_site
|
||||
ON biz.ai_run_logs (site_id, runtime_mode, sandbox_instance_id, created_at DESC);
|
||||
```
|
||||
|
||||
补齐其他 6 张业务表的复合索引模式。admin-web 按 runtime 维度过滤不再全表扫。
|
||||
|
||||
### 2.9 前端 — AIOperations.tsx sandbox 提示条(A5)
|
||||
|
||||
**文件**:`apps/admin-web/src/pages/AIOperations.tsx`
|
||||
|
||||
| 改动 | 说明 |
|
||||
|---|---|
|
||||
| import | 加 `Alert`(antd) + `fetchRuntimeContext / RuntimeContext`(api/runtimeContext) |
|
||||
| `useEffect` | 拉 `cacheSiteId` 对应的 runtime context |
|
||||
| Title 后 Alert | sandbox 模式下显示 `沙箱模式 · 业务日 X · 实例 sbx_*` + 详细描述(影响范围 + 切回 live 入口) |
|
||||
|
||||
## 三、未做(留 F1-5b 完整收口)
|
||||
|
||||
### 3.1 B1 — `admin_service.py` 6 处 `CURRENT_DATE` → `business_date`
|
||||
|
||||
涉及 `_get_range_stats` / `_get_7d_trend` / `_get_app_distribution` / `get_budget` 等,需要为这些函数加 site_id 参数链路(当前部分函数 site_id 是可选 None,改造涉及函数签名变更),工作量较大。F1-5b 完整 zqyy_app RLS 视图层落地时,这些查询自然走 `app.v_*` 视图(GUC 自动裁剪),无需改函数签名,更优雅。
|
||||
|
||||
### 3.2 B2 — `fdw_queries.py:113, 2552` 异常分支兜底改 `ctx.business_date`
|
||||
|
||||
异常分支用 `_date.today().isoformat()`,sandbox 触发异常会泄露真实今日。F1-5b 完整 fdw_queries GUC 接入时一并改。
|
||||
|
||||
### 3.3 GUC 完整传递
|
||||
|
||||
`run_log_service.create_log` 试点开启了 `bind_to_session=True`,但其他真正走 ETL 库视图的函数(fdw_queries 11+ 处 / page_context 等)没改。F1-5b 一并改造。
|
||||
|
||||
### 3.4 测试 3 套
|
||||
|
||||
`test_runtime_context.py` / `test_admin_ai_batch_runtime.py` / `test_dispatcher_runtime.py` 因 `.gitignore:71` 排除 tests/,本次会话写本地。F2-2 tests/ 入仓(Wave 5)时一并入仓 + 跑 CI。
|
||||
|
||||
### 3.5 P20 SPEC 同步
|
||||
|
||||
§6/§10/§11/§15 同步**留 F1-5b commit 一并改**(因为 F1-5b 才是 P20 SPEC C 方案的完整收口,届时 SPEC 反映"两库都用 GUC + zqyy_app 加 RLS 视图层"的最终状态更准确)。
|
||||
|
||||
## 四、风险与回滚
|
||||
|
||||
| 项 | 风险 | 回滚 |
|
||||
|---|---|---|
|
||||
| `_run_batch` 真实化 | 调用 dispatcher 失败时单 member 失败已写 ai_run_logs,不连坐;dispatcher 未注入时返 503 | git revert 当前 commit |
|
||||
| `ctx_snapshot` 设计 | 内存 dict,worker crash 时 batch 丢失 — 接受(原 batch_id 设计本就 in-memory) | 同上 |
|
||||
| `retry_trigger_job` 加 runtime 列 | INSERT 多 2 列,与已有索引兼容(只加列不删字段) | 同上 |
|
||||
| App2/2a 3 行 ref_date | 仅在已有 try 块内改,异常 fallback 不变 | 同上 |
|
||||
| `bind_to_session` 加可选参数 | 默认 False,旧调用不变;只在 run_log_service 试点开启 | 同上 |
|
||||
| `migration` 仅 CREATE INDEX IF NOT EXISTS | 索引可重复执行,不破坏数据 | `DROP INDEX IF EXISTS biz.idx_ai_run_logs_runtime_site` |
|
||||
| 前端 sandbox 提示条 | 失败时不阻断页面渲染(catch 后静默) | git revert |
|
||||
|
||||
## 五、F1-5b 待办(下一个 commit)
|
||||
|
||||
- B1 admin_service 6 处 CURRENT_DATE → business_date(随 RLS 视图层一并)
|
||||
- B2 fdw_queries.py:113, 2552 异常分支兜底
|
||||
- GUC 完整传递(fdw_queries / page_context / 等)
|
||||
- zqyy_app 加 RLS 视图层(7+ 视图 `app.v_*`)+ 后端 46 处 `FROM biz.*` → `FROM app.v_*`
|
||||
- 测试 3 套(本地写,F2-2 入仓时同步)
|
||||
- P20 SPEC §6/§10/§11/§15 同步(反映 F1-5a + F1-5b 完整收口状态)
|
||||
|
||||
## 六、验证(F1-5a 范围)
|
||||
|
||||
### 6.1 syntax 检查(已通过)
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python.exe -m py_compile \
|
||||
apps/backend/app/services/runtime_context.py \
|
||||
apps/backend/app/services/ai/admin_service.py \
|
||||
apps/backend/app/routers/admin_ai.py \
|
||||
apps/backend/app/ai/run_log_service.py \
|
||||
apps/backend/app/ai/prompts/app2_finance_prompt.py \
|
||||
apps/backend/app/ai/prompts/app2a_finance_area_prompt.py
|
||||
# → all-files-compile-ok
|
||||
```
|
||||
|
||||
### 6.2 W1-T8 走查待验证项(F1-5a + F1-5b 改完后一并验证)
|
||||
|
||||
- 切 sandbox=2026-03-01 后触发 1 次 app2_finance,验证 `ai_run_logs.runtime_mode='sandbox'` + prompt JSON `current_time='2026-03-01 HH:MM'` + `ai_cache.target_id` 含 `sbx_*:` 前缀
|
||||
- admin-web `/ai/operations` 顶部 sandbox 条带显示
|
||||
- batch-run 真正执行(estimate→confirm 后 ai_run_logs 出现 `triggered_by='batch:<batch_id>'` 记录)
|
||||
- estimate→切模式→confirm:验证 worker 用 estimate 时刻的 ctx_snapshot,**不**用 confirm 时刻的新模式
|
||||
|
||||
## 七、commit 建议
|
||||
|
||||
```
|
||||
fix(ai): F1-5a 沙箱 batch-run 接入 runtime_context (W1 / 阶段 A 主体)
|
||||
|
||||
Neo F1-5 反馈: "让沙箱起到其真正的作用. 真正的模拟日期, 仅能看到沙箱设定日期
|
||||
及之前日期的数据, 并运行 AI 的各个业务."
|
||||
|
||||
调研发现 (4 个并行子代理): batch-run 端点 _run_batch 是空壳 stub
|
||||
(只 logger.info, 实际不跑 AI), GUC apply_runtime_session_vars 0 处调用
|
||||
(dead code), 7 张业务表 6 张有 runtime 复合索引唯独 ai_run_logs 漏建,
|
||||
App2/2a 3 行 _calc_date_range 漏传 ref_date.
|
||||
|
||||
本 commit (F1-5a 阶段 A 主体, F1-5b 后续完整 zqyy_app RLS 视图层):
|
||||
|
||||
后端核心:
|
||||
- admin_service.py: _run_batch 真实化 (Semaphore(5)+asyncio.gather+
|
||||
return_exceptions=True+ctx_snapshot 防漂移); estimate 入口抓
|
||||
RuntimeContext 快照, confirm 取出传给 worker
|
||||
- admin_ai.py: confirm_batch_run lazy 注入 dispatcher
|
||||
- admin_service.retry_trigger_job: INSERT 落 runtime_mode +
|
||||
sandbox_instance_id 列 (用 runtime_insert_columns helper)
|
||||
- runtime_context.py: get_runtime_context 加 bind_to_session 参数,
|
||||
激活 GUC app.current_business_date / app.current_runtime_mode
|
||||
- run_log_service.create_log: 启用 bind_to_session=True 试点
|
||||
|
||||
App2/2a 3 行 ref_date 修复:
|
||||
- app2_finance_prompt.py:817 储值卡余额变化板块
|
||||
- app2_finance_prompt.py:841 日粒度 series + 异常检测窗口
|
||||
- app2a_finance_area_prompt.py:466 区域日粒度 series
|
||||
|
||||
DB:
|
||||
- migrations/20260505__ai_run_logs_runtime_index.sql:
|
||||
补 (site_id, runtime_mode, sandbox_instance_id, created_at DESC) 复合索引
|
||||
|
||||
前端:
|
||||
- AIOperations.tsx: 顶部加 sandbox 模式提示条 (Alert 显示 sandbox_date +
|
||||
sandbox_instance_id + 影响范围 + 切回 live 入口)
|
||||
|
||||
未做 (留 F1-5b 完整 zqyy_app RLS 视图层一并):
|
||||
- B1 admin_service 6 处 CURRENT_DATE → business_date
|
||||
- B2 fdw_queries 异常分支兜底
|
||||
- GUC 完整传递 (fdw_queries / page_context 等)
|
||||
- 测试 3 套 (.gitignore:71 排除, F2-2 入仓时 commit)
|
||||
- P20 SPEC §6/§10/§11/§15 (F1-5b 完整收口后同步更准确)
|
||||
|
||||
Neo 决策: docs/_overview/wave1-findings/F1-5-impl-decisions.md
|
||||
|
||||
详见 docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
|
||||
```
|
||||
Reference in New Issue
Block a user