- 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
243 lines
28 KiB
Markdown
243 lines
28 KiB
Markdown
# 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 镜像要与生产同版本才算数。
|