- 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
17 KiB
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/staging77057a819(开工时以合入consult-prep-parallel之后的 staging 为准) - 涉及文件:
frontend/src/mastra/consultation-tools.ts::consultationGenerationSettings(第 224 行)、consultationSliceGenerationSettings(第 232 行)、consultationNatalPrepareStepfrontend/src/lib/agent-generation-settings.ts::agentGenerationSettingsfrontend/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 用真实请求证实)
consultationSliceGenerationSettings(唯一传reasoningEffort: "low"的地方)在产品代码里没有调用方:grep -rn consultationSliceGenerationSettings frontend/src只有定义;只有frontend/tests/consultation-agentic-runtime.test.ts:2460与consultation-workflow-contract.test.ts:210断言它的形状。- 形状放错了位置:
agentGenerationSettings产出providerOptions[providerId] = { thinking: { type: "enabled", reasoningEffort: "low" } },强度嵌在thinking里。 - 产品的模型走哪条通道:
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 行)。 - OpenAI 兼容通道的取参(
@mastra/core/dist/chunk-DRULWNQ6.js约 3225–3290 行):只认providerOptions[name]顶层的reasoningEffort(字符串)→ 请求体reasoning_effort;其余键原样摊进请求体。所以今天的形状会发出"thinking": {"type": "enabled", "reasoningEffort": "low"}、没有reasoning_effort字段——DeepSeek 大概率忽略这个嵌套键。 - 同版本 Mastra 内置的 DeepSeek 原生通道(
chunk-YJB7RPO7.jsdeepseekChatOptions,约 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. 硬红线
- 开关未设置时:发给供应商的请求体逐字节不变(T1 用拦截
fetch的测试证明)。 - 只影响答题步(拿到计算结果之后写答案的那一步,及其续写/重试——续写本来就是
thinking: disabled,不动)。第 0 步(决定调工具)的请求体不变。若 Mastra 的prepareStep返回providerOptions不是按步生效,则在 PROGRESS 写明并改为整轮生效,A/B 照常,但要把第 0 步耗时单列。 - 不改答题钟(300 s)、工具钟(110 s)、
AGENT_ANSWER_OUTPUT_TOKENS/AGENT_THINKING_OUTPUT_TOKENS、max_tokens。 - 不改系统提示、Skill、证据卡、回答形状(改了就要另跑生平回测,超出本单)。
- 模型 key 只从环境变量读(
DS_KEY/BACKTEST_MODEL_KEY),不得写进任何文件、日志、提交、A/B 记录;回答原文不入库,A/B 记录只写数字、条数与短评。 - 不动表结构与迁移;不改
scripts/jyotish_api_server.py;不碰frontend/src/app/(app)/page.tsx;不改.gitea/workflows/**;不升级依赖(包括@mastra/core);不顺手修 warning。 - 改既有断言写「原值 / 新值 / 原因」三栏。开工先 grep:
grep -rn "consultationGenerationSettings\|agentGenerationSettings\|reasoningEffort\|thinking: { type" frontend/tests。
6. 任务分解
T0 · 先证实:供应商收到了什么、接受什么(需 key;Grok 无 key 则只交付脚本,由 Claude 跑)
- 写
frontend/scripts/research/answer-effort-probe.mts:- 请求体拦截(不需要 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。 - 供应商接受度(需 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 次。
- 请求体拦截(不需要 key):用 Mastra 的
- 判定:
- 某档 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 --noEmit0 错、npm run lint0 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 可提议为默认,须全部满足):
- 答题步推理 token 中位数降 ≥ 30%,且端到端耗时中位数降 ≥ 20%;
- 空答、截断均为 0(A 若有,B 不得多于 A);
- rubric
must_not命中总数 ≤ A; - 人工四项每一项的总条数 ≤ A(30 份回答合计);
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. 开工前置命令
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、部署文件、提示词