Files
Jyotisha/docs/tasks/TASK-answer-effort-ab-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

17 KiB
Raw Blame History

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. 开工前置命令

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、部署文件、提示词