Files
Jyotisha/docs/tasks/TASK-consult-prep-parallel-20261013.md
T
Jesse_ChenandClaude Opus 5.5 2994588ad4 docs(tasks): fold product decisions into audit briefs (BUG-1344..1346)
- dasha-unify: fix Ashtottari birth balance (BUG-1344)
- rect-scoring-perf: server-side candidate cap 241 (BUG-1345)
- node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346)
- consult-prep-parallel: classifier/reserve concurrency declined
- answer-effort-ab: model key supplied by product at acceptance
- drop stricter-than-test line-count red lines (AGENTS.md section 6)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-14 00:41:02 +08:00

243 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 普通对话发问前的准备步骤改为并行(issue #待定)
- 日期:2026-10-13
- 来源:「2026-10-13 全仓屎山与性能排查」P2 · 咨询前置并行 / 多领域并发 / 报告章节并发
- 执行方:**Grok(Windows + Docker)**,分支 `codex/consult-prep-parallel-20261013`,只推这个分支
- 验收:Claude(Linux)
- BUG 编号:**BUG-1337~1339**(开工时核对 `docs/BUG_HISTORY.md` 最大号;写作时最大号 BUG-1317,1335/1336 已被其它单占用)(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
- 开工前置:无(第一批)。**本单之后**还有两单改同一批文件,必须串行:`answer-effort-ab`(改 `consultation-tools.ts`)→ `consult-route-split`(搬 `route.ts`)。本单不要顺手搬 `route.ts` 的其它段落。
## 1. 基线
- 基线 commit:`origin/staging` `77057a819`
- 涉及文件:
- `frontend/src/app/api/consult/route.ts::POST`(1,840 行文件,`POST` 从第 296 行起)
- `frontend/src/lib/model-catalog.ts::readVersion` / `resolveSessionLanguageModel` / `invalidateLanguageModelCatalog`
- `frontend/src/lib/moderation/index.ts::moderate`
- `frontend/src/mastra/consultation-tools.ts::createConsultationTools`(多领域循环)
- `frontend/src/lib/personal-report-generation.ts::generateSectionedPersonalReport`(只核实,不改,见 §4 决策 4)
## 2. 事故实证
### 2.1 开流前的串行等待(`route.ts::POST`)
开流(`streamFirstResponse`,第 1821 行)之前,下面每一步都是 `await` 完上一步才开始:
| 顺序 | 行(`77057a819`) | 步骤 | 依赖 |
| --- | --- | --- | --- |
| 1 | 301–316 | `createServerSupabaseClient` + `supabase.auth.getUser()` | — |
| 2 | 318 | `refuseWhenAccountDeletionPending(user.id)`(一次 service client RPC) | 只要 `user.id` |
| 3 | 321 | `request.json()` + zod | 无 |
| 4 | 331 | 读 `chat_sessions`(含整段 `messages`) | 只要 `user.id` + 请求体的 `sessionId` |
| 5 | 365 | `resolveSessionLanguageModel(model_id, model_config_version)` | 第 4 步 |
| 6 | 424 | `moderate(visibleQuestion, "input")`:本地词表恒跑;`MODERATION_PROVIDER=aliyun` 时再远程,上限 `MODERATION_REMOTE_TIMEOUT_MS = 1_500` | 第 4、5 步通过 |
第 2 步与第 3~5 步互不依赖;第 6 步排在首帧之前,开了阿里云时**首帧最多晚 1.5 s**(BUG-1074 的「先建流」只把第 444 行以后搬进了流里)。
流内(`runTurn`)还有两处串行:
- 第 674 行 `classifyConsultationTurn`(`SMALLTALK_TIMEOUT_MS = 3_000`)排在 `prepareConsultationRoute`(资料/时区/候选区间读取 + `reserve_consultation_usage`)与 `append_consultation_question` 之后。
- 第 741 行分类完成后才再读一次 `chat_sessions.title`(只读,不依赖分类结果)。
### 2.2 `readVersion` 每轮查库 + 解密 + DNS(`model-catalog.ts`)
`resolveSessionLanguageModel` 在会话绑定了版本号时(普通对话都绑定)走 `readVersion` → `queryCatalog`(一次联表 SQL)→ `resolveRow`:`decryptModelProviderApiKey` + 对 openai-compatible 供应商 `assertAllowedModelProviderUrl`(`dns.lookup` 全部地址并逐个判公网)。**没有缓存**;同文件的 `loadLanguageModelCatalog` 对**同样的**解析结果(含同一个 DNS 校验)已经缓存 15 s(`cacheTtlMs = 15_000`)。
核实「版本不可变」:**只对了一半**。
- 不可变:已发布/已退役版本所用供应商的 `code`、`provider_type`、`base_url`(触发器 `prevent_published_model_provider_runtime_mutation`,`frontend/supabase/migrations/20260808010000_model_provider_emergency_disable.sql`;管理端报 `model_provider_runtime_immutable`)。
- **可变**:`model_providers.enabled`(紧急停用)、`model_config_versions.enabled`、供应商密钥(重新启用时可换 key)。`readVersion` 的 where 条件里有 `v.enabled and p.enabled`,所以缓存必须有 TTL 且能被管理端操作清掉。
核实 SSRF(AGENTS.md §8.3):`assertAllowedModelProviderUrl` 是**解析模型配置时**的公网地址校验;之后真正发模型请求的是 Mastra 的 `createOpenAICompatible`(`baseURL` 是主机名,`fetch` 会重新解析 DNS,不是 `pinnedAddressLookup` 固定地址)。所以今天这道 DNS 校验也不是逐请求绑定,目录路径已经把它缓存 15 s。结论:**按(模型, 版本)缓存、TTL 与目录相同(15 s)、管理端变更时一并清除,不削弱现有防护**;TTL 更长或缓存失败结果则会削弱,不做。
### 2.3 多领域逐个 `await`(`consultation-tools.ts::createConsultationTools`)
第 1230–1280 行附近 `for (let index = 0; index < domains.length; …)` 逐个 `await runConsultationWorkflow`(`POST /api/consultation_workflow`)。`MAX_CONSULTATION_DOMAINS = floor((110_000 − 45_000) / 31_000) = 2`。`/api/consultation_workflow` 在 `scripts/api_heavy_compute_gate.py::HEAVY_COMPUTE_PATHS` 里,`JYOTISH_HEAVY_COMPUTE_CONCURRENCY` 默认 2,满了直接 429;前端 `consultation-workflow.ts::httpFailureCode` 把 429 记成 `workflow_rate_limited`,**不退避重试**,整次计算失败。
本机实测(起草时,`77057a819`,系统 python3.13,无 vedastro SDK,本地起 `ThreadingHTTPServer`,Steve Jobs 公开 AA 盘,事业 + 婚恋两领域,每组 3 次取中位数):
| 情形 | 串行 | 两路并发 |
| --- | --- | --- |
| 盘已缓存 | 0.47 s | 0.60 s(**更慢**) |
| 盘未缓存(每轮换出生分钟) | 2.25 s(第一域约 2.0 s,第二域吃到排盘缓存 0.29 s) | 2.03 s(两路各自冷算约 2.0 s) |
复现:`scratchpad/bench-prep/bench2.py`、`bench3.py`(起草会话临时目录,执行方按 §6 T5 重写进 `frontend/scripts/research/`)。
解读:Python 服务是 `ThreadingHTTPServer`,排盘吃 GIL;串行时第二个领域复用第一个领域的排盘缓存,并发时两路各算一遍。**本机并发只省约 10%,CPU 翻倍、一次占满两个重计算名额。** 生产容器装了 vedastro SDK,每域约 1.66 s 里有 1.33 s 是 BUG-301 保留的前台网络等待(`consult-latency-audit` 10-05 记录),网络等待会释放 GIL,所以生产上的收益可能比本机大——要靠 A/B 实测,不能推断。
另:同一盘、同一分钟,「串行时第二个领域(命中缓存)」与「冷启动单算该领域」的响应逐字段比对,除计时/缓存字段(`consult_card_facts.timing.*`、`chart.runtime_cache.*`)外,还有 `module_execution_audit[*].module_id` 与 `consultation_native_layers.d9_summary.vargottama` 两处**列表顺序**不同(集合遍历顺序,ERR-111:需 `PYTHONHASHSEED=0`)。所以 §6 T5 的差分脚本必须固定 `PYTHONHASHSEED=0` 并列出忽略字段,不能直接 `cmp`。
### 2.4 报告章节并发:目标代码已退役
`generateSectionedPersonalReport`(`personal-report-generation.ts` 第 3510 行)确实逐章串行,但它只在 `PERSONAL_REPORT_WRITER_ENABLED` 为真时被 worker 调用;`frontend/src/lib/personal-report-writer-flag.ts` 写死 `false`(注释:「Writer narrative pipeline is retired for new reports. Keep the code; do not call it.」,`cfcd369d4`)。新报告走 `generateLongformReport`(Python 完整数据版模板渲染,不逐章调模型)。`/api/reports` 的 `resolveReportCreate` 有 `jobs` 时只入队。**改它对线上零影响**,见 §4 决策 4。
## 3. 根因
- 2.1:不是回归。各步按当时目的串行写成;BUG-1074 只把「会等模型或数据库的大头」搬进流里,开流前那几步与流内的读标题没有人回头看能否并行;输入审核 09-30 加在开流前。
- 2.2:从未设计缓存。目录缓存(15 s)只服务「没绑版本」的旧路径,绑版本的 `readVersion` 是后加的。
- 2.3:BUG-257 的防复发明确写了「任何领域级并行提议必须先证明后端能承接并发,判据是 Python 侧的进程模型、GIL 约束与有界队列」。本单只做开关和证明,不默认开启。
关联记录(均已读完并核对防复发仍在):
| BUG | 与本单的关系 | 必须保留的防线 |
| --- | --- | --- |
| BUG-1074 | 先建流、首帧是确定性进度句 | 开流前只留鉴权/请求体/会话/会话模型(`consult-first-frame-and-pacing-20260928.test.tsx` 两张清单);流内拒绝必须能表达成 `request_rejected` |
| BUG-1075 | 放字节奏 | 不碰 |
| BUG-976 / 977 | 寒暄分类 | 「reserve 仍在分类前,零点无订阅仍被拦」——分类器不得早于预留 |
| BUG-1142 | 答题钟 300 s / 工具钟 110 s / 领域预算 65 s、2 个 | 领域上限公式不变,并发不得改 `MAX_CONSULTATION_DOMAINS` |
| BUG-257 / 945 | 领域上限由时钟推导;计划长度与执行长度两个常数 | 并发仍按 `executableDomainPlan` 截断、仍披露 `omitted_domains`、仍降级 |
| BUG-723 | 引擎 429 单独成档 | 并发版本对 429 不得压平;并发导致的 429 必须在 A/B 里计数 |
| BUG-1231 / 1232 | 分段计时(`modelSteps`、`answer.reasoning_ms`、分类耗时进用量) | 不删字段;本单新增的准备阶段计时走同一个 `logAgentObservability` |
| BUG-148 | DNS 固定地址请求 | `requestAllowedModelProvider` 不动 |
| BUG-496 | 报告分章失败不得中止其余章 | 本单不改报告 |
## 4. 决策记录
产品授权:2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行;**P1/P2 改动必须做 A/B、做成开关,默认值按 A/B 定**。
1. 开流前的独立读取并行(删号检查 ∥ 读请求体 + 读会话 → 解析模型):做。
2. 输入审核:**仍在任何模型调用和预留之前生效**,但不再挡首帧——开流后与资料读取并行,在 `reserve` 之前等它的结论;被拦时仍不扣点、不进模型;报错优先级与今天一致(见 T2)。不推翻 BUG-1074 的开流前清单(审核本来就不在那张清单里)。
3. `readVersion` 缓存:做,TTL 与目录缓存相同(15 s),不缓存失败和 `null`,管理端模型/供应商变更时清除。不延长 TTL。
4. **报告章节并发:本单不做**。目标函数已退役(§2.4),改了不会让任何用户的报告变快;排查报告里这一条作废。若产品以后重新启用 writer 管线,另立单。
5. **分类器与预留并发:产品负责人 2026-10-14 决定不做。** 它会让零点用户的问题在预留失败前先送进模型(违反 BUG-976 边界「reserve 仍在分类前」),且分类调用产生供应商成本。可选折中「预留成功后,分类与 `append_consultation_question` 并行;append 失败则丢弃分类结果并取消」也涉及计费顺序,同样留待拍板。
6. 多领域并发:做成开关,**默认关**(串行),A/B 达标才改默认。不改领域上限、不改时钟。
7. 不推翻任何既有红线。
## 5. 硬红线
1. 被审核拦下的问题:不扣点(不得调用 `reserve_consultation_usage`)、不进任何模型(分类器、Agent、标题生成都不得先于审核结论)。
2. 开流前的 HTTP 状态集合与先后顺序不变:401 → 423 → 400(请求体)→ 503(读会话失败)→ 404 → 409(`session_not_consultation`)→ 409(会话模型已变)→ 503(模型不可用)→ 409(旧校正入口)→ 400(`blocksPromptExtraction`)。并行后**按这个顺序**判定返回,不得因为谁先回来就先报谁。
3. 删号冻结(423)必须在任何扣点、写库之前判定(`account-deletion.test.tsx` 的 `if (frozen) return frozen;`)。
4. `readVersion` 缓存:只缓存成功解析的结果;TTL ≤ 15 s;`invalidateLanguageModelCatalog()` 同时清它;不缓存 DNS 失败或非公网地址的结果;不改 `assertAllowedModelProviderUrl` 与 `requestAllowedModelProvider`。
5. 多领域并发开关打开时:`executions` 顺序必须等于计划顺序(不得按完成先后);任一领域失败的处理与串行一致(整次计算失败、错误码不变);429 不得被吞或改码;`MAX_CONSULTATION_DOMAINS`、`domainFitsRunBudget` 的公式与常量不变。
6. 不动表结构、不加迁移;不改 `scripts/jyotish_api_server.py`;不碰 `frontend/src/app/(app)/page.tsx`;不改 `.gitea/workflows/**`;不升级依赖;不顺手修 warning。
7. 改既有断言写「原值 / 新值 / 原因」三栏;不得为让测试过而弱化断言。开工先 grep(AGENTS.md §7.8):
```bash
grep -rn "await moderate(visibleQuestion\|await resolveSessionLanguageModel\|return await readVersion\|refuseWhenAccountDeletionPending\|const runTurn = async\|for (let index = 0; index < domains.length" frontend/tests tests
```
已知会受影响:`frontend/tests/content-moderation.test.ts`(约 242–243 行:`await moderate(visibleQuestion, "input"` 存在且在 `reserve_consultation_usage` 之前)、`frontend/tests/model-catalog.test.ts`(约 48–49 行:`return await readVersion(...)`、`await resolveSessionLanguageModel(`)、`frontend/tests/application-billing-contract.test.ts`(约 160 行)、`frontend/tests/consult-first-frame-and-pacing-20260928.test.tsx`(256–289 行两张清单)、`frontend/tests/account-deletion.test.tsx`(约 111–122 行)、`frontend/tests/consultation-agentic-runtime.test.ts`(领域循环相关用例)。
8. 输出逐字节一致:开关全关时,`/api/consult` 的 NDJSON 事件序列与 `77057a819` 相同;开关全开时,除新增的 `consultation-prep-v1` 可观测日志外,响应体与落库内容与全关相同(T7 差分验证)。
9. 公开仓库:A/B 记录、PROGRESS 只写虚构盘或公开名人盘;不写真实用户资料、key、日志原文。
## 6. 任务分解
所有新开关在 `frontend/src/lib/consult/prep-flags.ts`(新建目录 `frontend/src/lib/consult/`,后续 `consult-route-split` 单沿用)集中读取,每个请求读一次 `process.env`,非法值按默认处理并 `console.warn` 一次。
| 开关 | 取值 | 本单代码默认 | A/B 后的默认 |
| --- | --- | --- | --- |
| `CONSULT_PREP_PARALLEL` | `on` / `off` | `off` | 按 T7 判定 |
| `MODEL_VERSION_CACHE_TTL_MS` | `0`~`15000` 的整数,`0` 关 | `0` | 按 T7 判定(开则 15000) |
| `CONSULT_DOMAIN_CONCURRENCY` | `1` / `2` | `1` | 按 T7 判定 |
> 默认值的改动(若 A/B 达标)只改 `prep-flags.ts` 里的常量,单独一个提交,便于回滚。
### T1 · 开流前并行(BUG-1337 的一半)(Grok 自跑 / Claude 验收)
- 新建 `frontend/src/lib/consult/turn-prep.ts`,导出 `loadConsultTurnHead(deps)`:输入为注入的依赖(`getUser`、`readPendingDeletion`、`readBody`、`readSession`、`resolveModel`),返回 `{ ok: false, response }` 或 `{ ok: true, user, parsed, chatSession, sessionModel }`。`route.ts::POST` 用它替换第 301–377 行的判定段,`NextResponse.json(...)` 的文案、状态码、`code` 原样搬过去(逐字)。
- `CONSULT_PREP_PARALLEL=on`:拿到 `user` 后同时启动「删号检查」与「读请求体 → 读会话 → 解析模型」两条链;然后**按 §5.2 的顺序**依次检查结果再返回。`off`:保持今天的串行顺序(同一个函数里两条代码路径,便于 A/B)。
- 注意:并行时请求体非法、会话不存在等情形下仍会多跑一次删号检查(只读),可接受;但**不得**在冻结账户上调用任何写操作。
- 验收:新增 `frontend/tests/consult-turn-prep-20261013.test.ts`:用假依赖(每个依赖带可控延迟和返回值)覆盖 §5.2 每一种返回,并断言两种开关下状态码与 body **逐字段相同**;`on` 时总耗时 ≈ max(删号, 会话 + 模型) 而非相加(用假时钟或延迟 50/80/80 ms 断言 < 230 ms);冻结账户 + 非法请求体时返回 423。
- 命令:`./node_modules/.bin/tsx --test tests/consult-turn-prep-20261013.test.ts`
### T2 · 输入审核不挡首帧,仍在预留前生效(BUG-1337 的另一半)(Grok 自跑 / Claude 验收)
- `CONSULT_PREP_PARALLEL=on`:在第 424 行原位置(所有开流前检查都通过之后)**启动** `moderate(...)` 但不 `await`,得到 `inputModerationPending`;进入 `runTurn` 后:
- `prepareConsultationRoute` 的 `beforeReserve` 回调第一行 `await inputModerationPending`,非 `pass` 时 `recordModerationEvent`(只记一次),`block` 时抛专用错误 `ConsultationInputBlockedError`;
- `prepareConsultationRoute` 的 `catch` 里**先**看审核结论:若为 `block`,无论 prepare 抛的是什么,都返回今天那个 400(`code: "content_blocked"`、`inputBlockedCopy(...)`)——保持「审核拦截优先于资料错误」的旧优先级;
- legacy 运行时(`shouldUseAgenticRuntime` 为假)同样走这段,仍是直接 HTTP 400。
- `off`:维持第 424 行 `await` 原样。
- 首帧之后被拦的请求,客户端收到 `run.failed` / `request_rejected` / `status: 400` / `reason: "content_blocked"`,`use-consultation-run.ts` 的 4xx 回滚已有,不改客户端。自伤类拦截文案(`MODERATION_SELF_HARM_COPY`)必须原样到达用户(断言事件 `message`)。
- 验收:
- 新测试:审核挂起 1.5 s 时首帧已写出(参照 `consult-first-frame-and-pacing-20260928.test.tsx` 的 `onFirstFrame` 用例);审核 `block` 时 `reserve` 假函数调用 0 次、模型假函数调用 0 次;审核 `block` + 资料读取抛 `ConsultationProfileTruthError` 时返回 `content_blocked`;自伤文案逐字到达。
- `content-moderation.test.ts` 约 242–243 行按三栏改:原值「源码含 `await moderate(visibleQuestion, "input"` 且位置早于 `reserve_consultation_usage`」→ 新值「`beforeReserve` 内 `await inputModerationPending` 早于 `input.reserve()`(`consultation-route-service.ts` 里 `beforeReserve` 先于 `reserve` 已有保证)+ 行为测试:block 时 reserve 0 次」。原因:审核从开流前挪到流内、预留前。**新值必须比原值更强(有行为断言)**。
### T3 · `readVersion` 缓存(BUG-1338)(Grok 自跑 / Claude 验收)
- `model-catalog.ts`:新增模块级 `Map<"modelId:version", { expiresAt, model }>`(挂 `globalThis`,同 `jyotishaModelCatalogCache` 写法)。`resolveSessionLanguageModel` 绑版本分支:TTL > 0 且命中未过期 → 直接返回;否则 `readVersion`,**只有非 null 成功结果**才写缓存。`invalidateLanguageModelCatalog()` 同时清空它。
- 不缓存:`readVersion` 抛错(`return null` 路径)、`resolveRow` 返回 null(解不开 key)、`assertAllowedModelProviderUrl` 失败。
- 验收:`frontend/tests/model-catalog.test.ts` 加行为测试(注入假 `queryCatalog` 与假 DNS):TTL 内第二次不查库不解析 DNS;过期后重查;`invalidateLanguageModelCatalog` 后重查;失败不缓存;TTL=0 每次都查。约 48 行 `return await readVersion(modelId, configVersion)` 按三栏改(原值 / 新值 `readVersion` 结果经缓存写入 / 原因)。管理端 `model-mutation-handler.ts` 的 `invalidateCatalog` 调用点(保存供应商、发布、回滚)不变,断言它们仍在。
- 说明 SSRF 结论(§2.2)写进 PROGRESS 与 BUG-1338「防复发」:TTL 不得超过目录缓存。
### T4 · 流内读标题与分类并行(BUG-1337)(Grok 自跑 / Claude 验收)
- 第 741 行读 `chat_sessions.title` 改为在 `append_consultation_question` 成功后立即启动,与 `classifyConsultationTurn` 并行,在原使用点 `await`。分类本身的位置**不动**(仍在预留和 append 之后,§4 决策 5)。
- 验收:源码断言读标题在 append 之后启动;行为不变(`session-title-persist-contract.test.ts`、`consultation-smalltalk.test.ts` 全绿)。
### T5 · 多领域并发开关(BUG-1339)(Grok 自跑 / Claude 验收)
- `createConsultationTools` 的领域循环:`CONSULT_DOMAIN_CONCURRENCY=2` 时,对 `executableDomainPlan` 截断后的领域同时发起 `runWorkflow`,用 `Promise.all` 收齐后**按计划顺序**组装 `executions`;`chart-calculation` 活动句按今天的 `chartCalculationProgressLabel(index + 1, domains.length)` 在发起时依次写出。任一失败 → 与串行一样整次失败、错误码不变。`=1` 时代码路径与今天逐字相同。
- 并发时 `domainFitsRunBudget` 不再逐个判断(所有领域同时开始),但领域数仍受 `MAX_CONSULTATION_DOMAINS` 截断;在 PROGRESS 里写明这一点。
- 验收:`consultation-agentic-runtime.test.ts` 加用例:假 `runWorkflow` 让第二个领域先完成,`executions` / 证据卡 / `workflowReceipt` 与串行逐字段相同;一个领域 429 时错误码为 `workflow_rate_limited`(与串行同);开关为 1 时调用顺序严格串行。
- 写 `frontend/scripts/research/consult-domain-concurrency-ab.py`(或 `.mts`):本地起 Python API(`PYTHONHASHSEED=0`,带 vedastro SDK 的 Docker 镜像一组、不带一组),公开 AA 盘 3 张 × 领域对 2 组(`career,marriage`、`parents,health`)× 冷/热缓存,串行 vs 两路并发,每格 3 次取中位数;另跑「两个用户同时问两领域」的争用场景,统计 429 次数。差分:同盘同分钟两种方式的领域响应,忽略字段清单 = `consult_card_facts.timing.*`、`chart.runtime_cache.*`,并对 `module_execution_audit`、`vargottama` 等集合派生列表排序后比较,要求其余逐字节一致。
### T6 · 准备阶段计时(Grok 自跑 / Claude 验收)
- 在 `route.ts` 现有 `logAgentObservability` 体系里加一个事件 `agentVersion: "consultation-prep-v1"`,`contractPhases` 记:`prep.auth`、`prep.deletion`、`prep.session`、`prep.model`、`prep.moderation`、`prep.subject`(prepare 到 beforeReserve)、`prep.reserve`、`prep.append`、`prep.classify`、`prep.title`,以及开关取值(写进 `phase` 名或已有可用字段,不新增 schema 字段)。只记毫秒和状态,**不含问题原文、出生资料、用户标识以外的任何字段**(用户标识沿用现有规则)。
- 验收:测试断言事件通过 `agentObservabilityEventSchema`,且植入的问题原文不出现在日志里(参照 BUG-1232 的写法)。
### T7 · A/B 验证(Grok 跑能跑的;需要 staging 的由 Claude)
记录写 `docs/testing/consult-prep-parallel-ab-20261013.md`。
| 项 | A | B | 输入 | 度量 | 判定门槛(默认开启的条件) |
| --- | --- | --- | --- | --- | --- |
| 开流前并行 + 审核挪后(`CONSULT_PREP_PARALLEL`) | `off` | `on` | 进程内基准:用 T1/T2 的假依赖,延迟取真实中位数(Grok 在本机 Docker 里对本地 Postgres 量 `getUser`/删号 RPC/读会话/`readVersion` 各 30 次取中位数;阿里云审核无 key 时按 300 ms 与 1,500 ms 两档模拟);每组 30 次 | 首帧毫秒(`onFirstFrame`)、准备总毫秒、各段毫秒 | 首帧中位数降 ≥ 20% 且 §5.2 全部返回逐字段一致、被拦请求 reserve 0 次 → 默认 `on`;否则保持 `off` |
| `readVersion` 缓存 | TTL 0 | TTL 15000 | 同上进程内基准 + 本地 Postgres 真查;每组 30 次 | `prep.model` 毫秒、SQL 次数、DNS 解析次数 | `prep.model` 中位数降 ≥ 50%、TTL 内 SQL 与 DNS 次数为 0、失效/停用 15 s 内生效(测试证明)→ 默认 15000 |
| 多领域并发(`CONSULT_DOMAIN_CONCURRENCY`) | 1 | 2 | T5 的脚本 | 两领域墙钟中位数、Python 进程 CPU 秒、峰值内存、争用场景 429 次数、差分结果 | 带 vedastro SDK 的镜像里墙钟中位数降 ≥ 25%,**且**争用场景 429 次数不多于 A,**且**差分一致 → 默认 2;任何一条不满足保持 1。本机起草实测只降约 10%(§2.3),预计不达标,如实记录即可 |
| staging 实测(Claude 验收时) | 部署前 7 天 `consultation-stream-first-v1` `first_byte` 中位数 | 部署后同口径 + `consultation-prep-v1` 各段 | 真实流量或产品负责人用测试账号 10 轮 | `first_byte`、`prep.*` | 只做核对,不改默认;数字写进同一份记录 |
每组至少重复 3 次取中位数(上表已写更多次的按表);正确性部分全部要求差分一致,不是「看起来一样」。
### T8 · 记录(Grok)
- `docs/BUG_HISTORY.md`:
- **BUG-1337**:普通对话开流前与流内的准备步骤无依赖却串行(删号检查、读会话、解析模型、开了阿里云时审核挡首帧最多 1.5 s、读标题)。
- **BUG-1338**:会话模型按版本解析每轮查库 + 解密 + DNS,无缓存(目录路径早有 15 s 缓存)。写清「版本不可变只对了一半」与 SSRF 结论。
- **BUG-1339**:多领域串行;并发开关 + A/B 结论(若不达标,状态写 `closed_by_design` 或 `investigating` 并附数字,不写 resolved)。
- `docs/tasks/PROGRESS-consult-prep-parallel-20261013.md`:每个 T 的结果、三栏改断言表、A/B 数字、开关最终默认值及依据。
- `CHANGELOG.md`:若有默认开启的开关,写一句「发问后更快出现第一行」之类用户可感知变化;全部默认关则不写。
- `frontend/DESIGN.md`:不改(无 UI 变化)。
- `BLOCKED.md`:缺阿里云 key、缺 staging 实测时各记一条。
## 7. 让步顺序
时间不够时按此顺序砍(先砍最后的):T5(多领域并发,预计不达标)→ T4 → T6 的细分段(保留 `prep.moderation` 与 `prep.model`)。T1、T2、T3、T7 中对应 A/B 与 T8 不可砍。
## 8. 验收命令分工
| 谁 | 命令 |
| --- | --- |
| Grok 自跑 | `cd frontend && ./node_modules/.bin/tsc --noEmit && npm run lint`;单文件:`./node_modules/.bin/tsx --test tests/consult-turn-prep-20261013.test.ts tests/content-moderation.test.ts tests/model-catalog.test.ts tests/application-billing-contract.test.ts tests/consult-first-frame-and-pacing-20260928.test.tsx tests/account-deletion.test.tsx tests/consultation-agentic-runtime.test.ts tests/session-title-persist-contract.test.ts tests/consultation-smalltalk.test.ts`(看 `# fail` 与 `# cancelled` 都为 0、退出码 0);T7 进程内基准与 T5 脚本。前端全量、Python 快速门、整站构建**不跑** |
| Claude 验收 | 上述全部 + 前端全量 `npm test` 与基线逐名比对 + `next build`(`/` 仍 Static,首屏 gzip ±2%)+ 复跑 T7 基准与 T5 脚本 + staging 部署后 `first_byte` 对比 |
| `test:db` | 不需要(不动表、不改数据库访问层查询语句);若 T3 改了 `queryCatalog` 的 SQL 文本则必须跑并按 `tools/agent-loop/windows-known-failures.txt` 比对 |
## 9. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/consult-prep-parallel-20261013 .worktrees/consult-prep-parallel-20261013 origin/staging
cd .worktrees/consult-prep-parallel-20261013
git log -1 --format=%h # 应为 77057a819 或其后代
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号,本单用 1337~1339
cat frontend/src/lib/personal-report-writer-flag.ts # 确认仍为 false(决策 4 的前提)
grep -rn "await moderate(visibleQuestion\|await resolveSessionLanguageModel\|return await readVersion\|refuseWhenAccountDeletionPending\|const runTurn = async\|for (let index = 0; index < domains.length" frontend/tests tests
```
## 10. BUG 编号起点
BUG-1337(写作时最大号 BUG-1317;1335、1336 已分配给其它单)。开工时若 1337~1339 已被占用,顺延并在 PROGRESS 写明。
## 11. 交付物清单
- 代码:`frontend/src/lib/consult/prep-flags.ts`、`frontend/src/lib/consult/turn-prep.ts`(新)、`route.ts`、`model-catalog.ts`、`consultation-tools.ts`
- 测试:`frontend/tests/consult-turn-prep-20261013.test.ts`(新)及 §5.7 列出文件的三栏修改
- 脚本:`frontend/scripts/research/consult-domain-concurrency-ab.*`、T7 进程内基准脚本(同目录)
- 文档:`docs/testing/consult-prep-parallel-ab-20261013.md`、`docs/tasks/PROGRESS-consult-prep-parallel-20261013.md`、`docs/BUG_HISTORY.md`(BUG-1337~1339)、视结果 `CHANGELOG.md`、`BLOCKED.md`
- 不改:`frontend/DESIGN.md`、数据库迁移、`scripts/jyotish_api_server.py`
## 12. 环境缺口(预告)
- 阿里云审核真实延迟需 key,无 key 用 300 / 1,500 ms 模拟,写进 `BLOCKED.md`。
- staging 前后对比只能在部署后由 Claude 做。
- 多领域并发的生产收益取决于 vedastro SDK 的网络等待,Grok 的 Docker 镜像要与生产同版本才算数。