# TASK · 普通对话答题步推理强度:开关 + A/B(issue #待定) - 日期:2026-10-13 - 来源:「2026-10-13 全仓屎山与性能排查」P1 · 答题步推理强度 - 执行方:**Grok(Windows + Docker)**,分支 `codex/answer-effort-ab-20261013`,只推这个分支 - 验收:Claude(Linux) - BUG 编号:**无 BUG 编号**(研究 + 开关,记录写 PROGRESS 与 A/B 记录)。若 T0 证实「既有的 `reasoningEffort` 管道从未送到供应商」需要立 Bug,开工时核对 `docs/BUG_HISTORY.md` 最大号(写作时最大号 BUG-1317,1335~1339 已分配)顺延取号,并在 PROGRESS 写明。(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起) - 开工前置:**`consult-prep-parallel` 已合入 `origin/staging`**(两单都改 `frontend/src/mastra/consultation-tools.ts`)。 ## 1. 基线 - 基线 commit:`origin/staging` `77057a819`(开工时以合入 `consult-prep-parallel` 之后的 staging 为准) - 涉及文件: - `frontend/src/mastra/consultation-tools.ts::consultationGenerationSettings`(第 224 行)、`consultationSliceGenerationSettings`(第 232 行)、`consultationNatalPrepareStep` - `frontend/src/lib/agent-generation-settings.ts::agentGenerationSettings` - `frontend/src/app/api/consult/route.ts`(第 1214 行 `...consultationGenerationSettings(selectedModel.model)`,`natalStreamOptions` / `windowStreamOptions`) - `frontend/scripts/research/consult-biography-backtest.mts`(AGENTS.md §10 指定的名人生平回测,A/B 的底座) ## 2. 事故实证 ### 2.1 推理占答题时间的大头(既有实测,非本轮) 10-04 咨询耗时审计(Claude 记忆与 `docs/testing/consult-plain-answer-20261001-model-runs.md`、`docs/testing/prompt-slim-20261007-model-runs.md`):答题步推理 token 中位 9.5k–11.9k,约占答题步输出 90%,约 200 tok/s,答题步 45–60 s,占整轮 75–85%。10-07 精简提示词后:deepseek-flash 10 题平均思考 5,683 token、30.3 s;deepseek-v4-pro 3 题平均思考 3,588 token、33.6 s。这些数字本轮未重测。 ### 2.2 普通对话答题从不传推理强度 `consultationGenerationSettings(model)` = `agentGenerationSettings(model, { thinking: "enabled" })`,没有 `reasoningEffort`。route 第 1214 行把它摊进同一轮的 `streamOptions`,第 0 步(决定调工具)与第 1 步(写答案)用同一份设置。 ### 2.3 既有的 `reasoningEffort: "low"` 管道很可能从未到达供应商(起草时读依赖源码得出,需 T0 用真实请求证实) 1. `consultationSliceGenerationSettings`(唯一传 `reasoningEffort: "low"` 的地方)**在产品代码里没有调用方**:`grep -rn consultationSliceGenerationSettings frontend/src` 只有定义;只有 `frontend/tests/consultation-agentic-runtime.test.ts:2460` 与 `consultation-workflow-contract.test.ts:210` 断言它的形状。 2. 形状放错了位置:`agentGenerationSettings` 产出 `providerOptions[providerId] = { thinking: { type: "enabled", reasoningEffort: "low" } }`,强度嵌在 `thinking` 里。 3. 产品的模型走哪条通道:`model-catalog.ts::resolveRow` 对非 openai / anthropic 的供应商给 `{ providerId: <供应商 code>, modelId, url, apiKey }`;Mastra 1.50.1(`package-lock.json`)在 `config.url` 存在时一律 `createOpenAICompatible({ name: providerId })`(`@mastra/core/dist/chunk-AR6WPTSV.js` 约 518 行)。 4. OpenAI 兼容通道的取参(`@mastra/core/dist/chunk-DRULWNQ6.js` 约 3225–3290 行):只认 `providerOptions[name]` **顶层**的 `reasoningEffort`(字符串)→ 请求体 `reasoning_effort`;其余键原样摊进请求体。所以今天的形状会发出 `"thinking": {"type": "enabled", "reasoningEffort": "low"}`、**没有** `reasoning_effort` 字段——DeepSeek 大概率忽略这个嵌套键。 5. 同版本 Mastra 内置的 DeepSeek 原生通道(`chunk-YJB7RPO7.js` `deepseekChatOptions`,约 1515 行)的注释是「Controls the thinking strength for DeepSeek V4 reasoning models」,取值只有 `"high" | "max"`。也就是说 **DeepSeek V4 可能根本不接受 `low` / `medium`**,产品要的「low / medium / 不设」三组可能有一组或两组发不出去。 上面 3~5 是读打包后源码的推断,必须 T0 用真实请求证实,不得直接当结论。 ### 2.4 以前的 A/B 做法 `docs/testing/prompt-slim-20261007-model-runs.md`:同一套拼法(系统提示 + 用户轮指令 + 工具调用 + 工具结果)直连 DeepSeek,公开名人 golden 盘造工具结果,`max_tokens` 与线上一致(24,576;8,000 会让 4/10 空答),thinking 模式下 assistant 的 tool_call 消息必须带 `reasoning_content`。那次脚本没入库;现在仓库里已有等价且更完整的 `frontend/scripts/research/consult-biography-backtest.mts`(真实 `getJyotishAgent`、真实 `consultationGenerationSettings`、真实 `streamAgentResponse`,引擎调用换成 `frontend/tests/fixtures/consult-biography-backtest-golden.json`,9 张公开 AA 盘 × 4 领域,确定性检查对照 `docs/research/consult_biography_backtest_rubric.json`),本单在它上面加参数,不另写一套拼提示词的脚本。 ## 3. 根因 - 2.2:从未设计。BUG-1142 把答题钟从 70 s 放到 300 s 时记录了「推理 token 也从同一只钟里扣」,但没有人去调强度。 - 2.3:`consultationSliceGenerationSettings` 加入时只写了形状测试(断言 `providerOptions.deepseek` 等于嵌套形状),没有对真实请求体做断言,所以「传了 low」看上去成立。这是测试只验源码/对象形状、不验线上请求的老问题(同 BUG-148 防复发「不得用只匹配源码字符串的断言替代该回归」)。 - 关联:BUG-1142(答题钟只防卡死)、BUG-1051(截断不扣点)、BUG-1232(`modelSteps`、`answer.reasoning_ms` 埋点,本单 A/B 用它)、BUG-1256(提示词精简 A/B)、BUG-282 / BUG-937(第 0 步关思考的风险,本单不碰第 0 步)。 ## 4. 决策记录 - **产品负责人 2026-10-14:模型 key 在本单交付验收时由产品临时提供给 Claude**。Grok 无 key 时只交付开关、请求体拦截测试与 A/B 脚本,T0 第 2 步与 T2 的真实运行由 Claude 验收时执行;回报里写「A/B 未跑:等产品提供 key」,不得写通过。 - 产品授权:2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行,并**已同意做答题步推理强度 A/B**;默认值等 A/B 结论交产品拍板。 - 本单默认行为**不变**:开关不设时,请求体与 `77057a819` 逐字节相同。 - `consultationSliceGenerationSettings` 的嵌套形状与它的两条断言**本单不改**(没有调用方,改它不影响线上;是否删除或修正交产品,写进 PROGRESS「需拍板」)。 - 「答题步关思考」(`thinking: disabled`)**不作为候选默认**:只在 T0 证实 DeepSeek 不接受降档时,作为对照组 D 跑数据供产品参考;若要采用需另行拍板(BUG-282 / 937 记录过关思考的质量风险)。 - 不推翻任何既有红线。 ## 5. 硬红线 1. 开关未设置时:发给供应商的请求体逐字节不变(T1 用拦截 `fetch` 的测试证明)。 2. 只影响**答题步**(拿到计算结果之后写答案的那一步,及其续写/重试——续写本来就是 `thinking: disabled`,不动)。第 0 步(决定调工具)的请求体不变。若 Mastra 的 `prepareStep` 返回 `providerOptions` 不是按步生效,则在 PROGRESS 写明并改为整轮生效,A/B 照常,但要把第 0 步耗时单列。 3. 不改答题钟(300 s)、工具钟(110 s)、`AGENT_ANSWER_OUTPUT_TOKENS` / `AGENT_THINKING_OUTPUT_TOKENS`、`max_tokens`。 4. 不改系统提示、Skill、证据卡、回答形状(改了就要另跑生平回测,超出本单)。 5. 模型 key 只从环境变量读(`DS_KEY` / `BACKTEST_MODEL_KEY`),不得写进任何文件、日志、提交、A/B 记录;回答原文不入库,A/B 记录只写数字、条数与短评。 6. 不动表结构与迁移;不改 `scripts/jyotish_api_server.py`;不碰 `frontend/src/app/(app)/page.tsx`;不改 `.gitea/workflows/**`;不升级依赖(包括 `@mastra/core`);不顺手修 warning。 7. 改既有断言写「原值 / 新值 / 原因」三栏。开工先 grep:`grep -rn "consultationGenerationSettings\|agentGenerationSettings\|reasoningEffort\|thinking: { type" frontend/tests`。 ## 6. 任务分解 ### T0 · 先证实:供应商收到了什么、接受什么(需 key;Grok 无 key 则只交付脚本,由 Claude 跑) - 写 `frontend/scripts/research/answer-effort-probe.mts`: 1. **请求体拦截**(不需要 key):用 Mastra 的 `createOpenAICompatible` 通道(与产品同一构造方式:`{ providerId: "deepseek", modelId, url, apiKey: "dummy" }`)配一个假 `fetch`,分别以 (a) 今天的 `consultationGenerationSettings`、(b) `consultationSliceGenerationSettings`、(c) T1 的新形状 `low` / `medium` / `high` 发一次 `agent.stream`,打印实际请求体里的 `thinking`、`reasoning_effort`、`max_tokens`。预期 (b) 只有嵌套键、无 `reasoning_effort`——证实或推翻 §2.3。 2. **供应商接受度**(需 key):对 `deepseek-flash` 与 `deepseek-v4-pro`(以 `docs/testing/prompt-slim-20261007-model-runs.md` 用过的模型 id 为准;若产品库里的 id 不同,以产品负责人提供的为准),各发一个短问题,`reasoning_effort` 依次 不设 / `low` / `medium` / `high` / `max`,记录 HTTP 状态、错误码、`completion_tokens_details.reasoning_tokens`。每档 3 次。 - 判定: - 某档 HTTP 4xx → 该档「不支持」,A/B 去掉这档; - 某档 200 但推理 token 与「不设」差异 < 10%(中位数)→ 视为「被忽略」,同样去掉; - `low` 与 `medium` 都不支持或被忽略 → 推理强度这条路不通:只跑对照组 D(答题步 `thinking: disabled`)供参考,结论写「供应商不支持降档」,交产品。 - 产出写进 `docs/testing/answer-effort-ab-20261013.md` 第一节。 ### T1 · 环境变量开关(Grok 自跑 / Claude 验收) - 新开关 `CONSULT_ANSWER_REASONING_EFFORT`:取值 `low` / `medium` / `high` / `max` / 空;空或非法 = 不设(今天的行为),非法值 `console.warn` 一次。 - `agent-generation-settings.ts::agentGenerationSettings` 增加一个**新**选项(例如 `providerReasoningEffort`),把强度写到 `providerOptions[providerId].reasoningEffort` **顶层**(OpenAI 兼容通道据此发 `reasoning_effort`;`providerOptions.openai` 同样顶层),不改现有 `reasoningEffort`(嵌套)选项的行为——slice 的断言因此不需要改。 - 生效位置:`consultationNatalPrepareStep` / `consultationWindowPrepareStep` 在答题步(`stepNumber ≥ 1` 且计算结果已在手——与 `createConsultationRunClock` 的 `answerReady` 同一判据)返回带强度的 `providerOptions`;第 0 步不带。无工具的公开日历路线(`usesPublicDailyGeneralAgent`,`answerFromStart`)从第一步起就是答题步,同样带。 - 验收: - 新测试 `frontend/tests/answer-effort-switch-20261013.test.ts`:拦截 `fetch`,开关不设时第 0、1 步请求体与改前逐字节一致(快照取自改前代码运行结果,`fixtureProvenance` 写明命令);设 `low` 时只有答题步出现 `"reasoning_effort":"low"`、第 0 步没有;续写请求仍是 `thinking: disabled` 且无 `reasoning_effort`。 - `tsc --noEmit` 0 错、`npm run lint` 0 error。 ### T2 · A/B 脚本(Grok 写;有 key 才跑) - 在 `consult-biography-backtest.mts` 上加最少的参数,不复制提示词: - `--cases figure:domain,…`:固定题集(取代 `--figures × --domains` 的笛卡尔积); - 强度由环境变量 `CONSULT_ANSWER_REASONING_EFFORT` 传入(脚本走的就是产品的 `consultationGenerationSettings` 与 prepareStep),summary 里记下当次取值; - 每行额外记录:端到端秒数、第 0 / 1 步秒数与推理 token(复用 BUG-1232 的 `modelStepTimings` / `answerReasoningMs`)、输出 token、`finish`、是否空答、是否截断、正文字数。 - 固定 10 题(9 张公开 AA 盘均在 golden 里): | # | 盘 | 领域 | | --- | --- | --- | | 1 | steve_jobs | career | | 2 | barack_obama | parents | | 3 | elizabeth_taylor | marriage | | 4 | marilyn_monroe | parents | | 5 | judy_garland | health | | 6 | edith_piaf | marriage | | 7 | frida_kahlo | health | | 8 | george_w_bush | career | | 9 | zinedine_zidane | career | | 10 | marilyn_monroe | marriage | - 组别:A = 不设;B1 = `low`;B2 = `medium`(T0 去掉的档不跑);D = 答题步关思考(只在 T0 判定降档不通时跑)。每组 10 题 × 3 次,模型先 `deepseek-flash`;`deepseek-v4-pro` 抽 1、4、6 三题 × 3 次。 - 批量跑时 `--concurrency` 不超过 4,组与组交错跑(同一题 A、B1、B2 相邻),避免供应商时段波动全落在某一组。 ### T3 · 质量对比与判定(Claude 验收时做;产品拍板) - 自动项(脚本出):rubric `must_not` 命中数、空答数、截断数、`finish` 非正常数、英文术语/后台词等 `recheck` 已有检查。 - 人工项(Claude 读,同题 A/B 正文并排):每题记 缺事实(证据卡里有、与问题直接相关却没说)/ 跑题 / 重复 / 空答 的条数,三次运行都读。 - 判定门槛(某档 B 可提议为默认,须**全部**满足): 1. 答题步推理 token 中位数降 ≥ 30%,且端到端耗时中位数降 ≥ 20%; 2. 空答、截断均为 0(A 若有,B 不得多于 A); 3. rubric `must_not` 命中总数 ≤ A; 4. 人工四项每一项的总条数 ≤ A(30 份回答合计); 5. `deepseek-v4-pro` 抽样不出现 A 没有的空答或截断。 满足者取降幅最大的一档提议为默认;都不满足 → 默认维持「不设」,开关保留。结论只是**提议**,由产品负责人拍板后另一个小提交改默认值(`CONSULT_ANSWER_REASONING_EFFORT` 的部署默认写在哪里也由那次决定,本单不改部署文件)。 ### T4 · 记录(Grok) - `docs/testing/answer-effort-ab-20261013.md`:T0 结果(请求体实拍、各档接受度)、T2/T3 的表(耗时、推理 token、质量条数),不贴回答原文。 - `docs/tasks/PROGRESS-answer-effort-ab-20261013.md`:开关实现、三栏改断言表(若有)、「需拍板」清单(默认值;`consultationSliceGenerationSettings` 删除或修正)。 - 若 T0 证实 §2.3,按 §头部规则取一个 BUG 号,记录「既有低强度设置从未送达供应商」,状态 `investigating`(slice 无调用方,线上无影响)。 - `CHANGELOG.md`:本单默认不变,不写;产品拍板改默认那次再写。 - `frontend/DESIGN.md`:不改。 - 无 key 时在 `BLOCKED.md` 记「A/B 未跑:缺模型 key」。 ## 7. 让步顺序 T0 第 1 步(请求体拦截)与 T1 不可砍。时间不够先砍:`deepseek-v4-pro` 抽样 → 组 D → B2(`medium`)。 ## 8. 验收命令分工 | 谁 | 命令 | | --- | --- | | Grok 自跑 | `cd frontend && ./node_modules/.bin/tsc --noEmit && npm run lint`;`./node_modules/.bin/tsx --test tests/answer-effort-switch-20261013.test.ts tests/consultation-agentic-runtime.test.ts tests/consultation-workflow-contract.test.ts tests/consult-answer-clock-20261001.test.ts tests/consult-latency-timing-20261005.test.ts`(`# fail` 与 `# cancelled` 都为 0);`./node_modules/.bin/tsx scripts/research/answer-effort-probe.mts --dry-run`(只做请求体拦截)。有 key 才跑 T0 第 2 步与 T2;**没有 key 就回报「A/B 未跑:缺模型 key」,不得写通过** | | Claude 验收 | 上述全部 + 前端全量 `npm test` 与基线逐名比对 + `next build`(`/` 仍 Static,首屏 gzip ±2%)+ 用产品负责人提供的临时 key 跑 T0、T2,按 T3 读稿判定 | | 不需要 | `test:db`、Python 快速门(不动 Python) | ## 9. 开工前置命令 ```bash git fetch origin --prune # 前置单必须已在 staging 上: git log origin/staging --oneline | grep -i "consult-prep-parallel\|BUG-1337" | head -3 # 为空则停,回报「前置未合入」 git worktree add -b codex/answer-effort-ab-20261013 .worktrees/answer-effort-ab-20261013 origin/staging cd .worktrees/answer-effort-ab-20261013 grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 grep -rn "consultationSliceGenerationSettings" frontend/src # 起草时只有定义,没有调用方 grep -rn "consultationGenerationSettings\|agentGenerationSettings\|reasoningEffort\|thinking: { type" frontend/tests ``` ## 10. BUG 编号起点 无(研究 + 开关)。T0 若证实 §2.3 需立 Bug,开工时取当前最大号 + 1。 ## 11. 交付物清单 - 代码:`agent-generation-settings.ts`(新选项)、`consultation-tools.ts`(开关读取 + prepareStep) - 测试:`frontend/tests/answer-effort-switch-20261013.test.ts`(新) - 脚本:`frontend/scripts/research/answer-effort-probe.mts`(新)、`consult-biography-backtest.mts`(加 `--cases` 与计时字段) - 文档:`docs/testing/answer-effort-ab-20261013.md`、`docs/tasks/PROGRESS-answer-effort-ab-20261013.md`、视 T0 结果 `docs/BUG_HISTORY.md`、无 key 时 `BLOCKED.md` - 不改:`CHANGELOG.md`、`frontend/DESIGN.md`、部署文件、提示词