diff --git a/TASK-report-section-writer-failure-20260902.md b/TASK-report-section-writer-failure-20260902.md new file mode 100644 index 00000000..55385e74 --- /dev/null +++ b/TASK-report-section-writer-failure-20260902.md @@ -0,0 +1,146 @@ +# 任务书 · 个人报告写作阶段 report_schema_invalid 修复(2026-09-02) + +基线:`origin/staging` 最新(开工时 `git fetch` 后以 `origin/staging` HEAD 为准;本任务书出稿时为 `d9404976` 之后)。 + +## 事故实证 + +blocked-repairs(`7faf8555`)部署后,2026-09-02 21:19(UTC 13:19,requestId `77b79067-bd70-465e-ac23-adc4500d4738`)真实生成 personal_full(career/marriage/wealth/timing)**failed,failureCode = `report_schema_invalid`**。历史同码失败另有 4 次(08-30 三次、09-02 05:10 一次)。 + +**证据链已收窄到模型写作路径**:委托方已在本地对最新 staging 代码做过确定性全管线复现——真实引擎四主题响应 → `buildReportEvidenceBundleV2`(4 claim cards / 0 blocked)→ `generatePersonalReport`(注入 schema 合规的确定性假 writer + 内存 sectionService)→ **PIPELINE-READY,4 章节装配、guard、最终解析全部通过**。即 bundle 校验、plan 校验、按章过滤(`filterReportEvidenceBundleForSection` 的 finalize 校验)、装配与文档合同都没有确定性缺陷;失败只可能来自真实模型输出未通过校验的路径:每章两次尝试均失败 → 章节 blocked → 四章全 blocked → `all_sections_blocked`,或摘要生成失败。 + +## 两个候选根因(按嫌疑排序,任务 0 定案) + +**主嫌 · 输出预算的中文 token 口径反了。** +`frontend/src/lib/personal-report-generation.ts`: + +```ts +const SECTION_OUTPUT_TOKEN_BUDGET = 3072; +const SUMMARY_OUTPUT_TOKEN_BUDGET = 1536; +// 每章: +maxOutputTokens: Math.min(3072, Math.max(1024, Math.ceil(entry.targetCharacters.max / 2))) +``` + +standard 深度 `targetCharacters.max = 1200`(中文字符)→ 每章预算 `max(1024, 600) = 1024` tokens。`/2` 隐含"2 字符 ≈ 1 token",这是英文口径;**中文约 1 字 ≈ 1 token 或更多**。writer 被系统提示与 plan 要求写 260–1200 字中文 narrative + title + actions + caveats + evidenceRefs 的完整 JSON,写入目标区间上半段必然超 1024 → `finishReason: "length"` → JSON 截断 → schema 校验失败 → 修复重试**预算不变**再截断 → 耗尽 2 次 → 章节 blocked。四章同构 → `all_sections_blocked`。摘要同理:schema 允许 `summary` 2000 字符中文,1536 预算同样可截断。 +(这正是 `TASK-report-sectioned-generation-20260830.md` 红线"输出上限取值要用数据决定,不许拍脑袋"要防的事故。) + +**次嫌 · evidenceRefs 精确回显失败。** +`assertWriterOutput` 要求 `candidate.evidenceRefs` 与 `entry.evidenceRefs` **集合相等**。按章过滤后的 bundle `evidenceRefs` = `entry.evidenceRefs ∪ 全局解读层 refs`(yoga/functional 回执随章下发),模型若照抄 bundle 的 refs 而不是 plan entry 的 refs → mismatch → 失败。修复重试的提示词只说"未通过校验",不说差在哪。 + +## 决策记录 + +- 产品方向不变:writer 输出 schema 不放宽;`assertWriterOutput` 的 id/theme/evidenceRefs 精确匹配语义不改(服务器主权);章节串行不变。 +- 授权:按任务 0 实测数据调整输出 token 预算公式与 repair 提示词;在报告失败记录/详情中透出失败细节(仅错误码级别,不含内容)。 + +## 硬红线 + +1. **任务 0 是门控。** 必须先从 staging 拿到 21:19 这次失败的实证(telemetry `finishReason` 分布 + `personal_report_sections.last_error_code`),确认主嫌/次嫌,再改数值。**预算新值必须由数据 + 中文 token 口径推导,在 PROGRESS 写出推导过程**,不得再拍脑袋。 +2. writer 输出 schema 只可收紧;`assertWriterOutput` 语义不改(允许把失败原因喂给 repair 提示词,不允许放松校验)。 +3. 隐私红线延续:日志/PROGRESS 只引用错误码、token 数、finishReason;不得摘录 prompt、bundle、模型输出、用户资料。 +4. 不改数据库结构(`personal_report_sections` 已有 `last_error_code`,透出走应用层)。 +5. 上两轮任务书的其余红线继续有效(隔离、assertionLevel、最低证据组、串行、`./node_modules/.bin/tsc` 而非 `npx tsc`、不改 `.gitea/workflows/**`、不提升 main)。 + +## 开工前置 + +```bash +git fetch origin --prune +git worktree add -b codex/report-writer-failure-20260902 \ + ../.worktrees/report-writer-failure-20260902 origin/staging +``` + +读 `pre_work_error_ledger.md`、`frontend/AGENTS.md`;`docs/BUG_HISTORY.md` 检索(本轮与 8-30 sectioned 轮的任务 0 观测、BUG-486 系列相邻)。 + +**先读**:`personal-report-generation.ts` 的 `generateSectionedPersonalReport`(预算、重试、blocked 判定)、`src/mastra/personal-report.ts` 的 `runStructured` / `REPAIR_PROMPT_SUFFIX` / telemetry、`personal-report-worker-core.ts` 的失败映射、`PROGRESS-report-sectioned-20260830.md` 的任务 0 数据。 + +--- + +## 任务 0(P0,门控)· 用 staging 实证定案 + +### 做法 + +执行环境有 staging 部署访问权,取证 requestId `77b79067-bd70-465e-ac23-adc4500d4738`: + +1. 服务器日志中该时段的 `[personal-report-agent]` telemetry 行:每次尝试的 `outcome` / `finishReason` / `outputTokens`,与 `[personal-report] generation_failed` 的 `innerReason`。 +2. `personal_report_sections` 表该 requestId 的行:`section_id` / `status` / `attempt_count` / `last_error_code`。 +3. 汇总成表写进 PROGRESS(只有错误码与数值)。 + +### 判定 + +- `finishReason: "length"` 占多数 / `last_error_code = section_output_truncated` → 主嫌成立,任务 1 为主修。 +- `section_output_invalid` 且 telemetry `finishReason: "stop"` → 次嫌成立,任务 2 为主修。 +- 两者混合 → 两任务都做(大概率如此,都在本轮范围内)。 +- 若实证指向第三种原因(如 summary 失败、worker 层指纹错配),停下,把数据写进 PROGRESS/BLOCKED 再定方案,不得按本任务书猜测硬改。 + +--- + +## 任务 1(P0)· 输出预算按中文口径重算 + +### 做法 + +- 每章预算改为以中文口径推导:narrative 上限 `targetCharacters.max` 字 ≈ `max × 1.3` tokens,再加 title/actions/caveats/refs 与 JSON 结构余量(按任务 0 实测 outputTokens 校准),向上取整后 cap 在 `SECTION_OUTPUT_TOKEN_BUDGET`(3072;若实测 research 深度需要更高,单独说明再调)。**禁止再出现"字符 ÷ 2"式换算**。 +- 摘要预算同口径重算(schema 允许 headline 200 + summary 2000 + priorities,1536 明显不足)。 +- 修复重试遇 `finishReason: "length"` 时,repair 提示词明确要求压缩篇幅至 `targetCharacters.min` 附近,且重试预算不得低于首次。 +- telemetry 已记 `finishReason` 与 tokens,保持。 + +### 验收 + +- 单元测试锁新预算公式(各深度的每章/摘要预算值表)。 +- staging 部署后真实生成一份 standard personal_full:四章有正文;telemetry 无 `length`;PROGRESS 给每章实测 `outputTokens` 与预算余量。 + +--- + +## 任务 2(P1)· evidenceRefs 回显加固 + +### 做法 + +- `sectionPrompt` 明确一句:`evidenceRefs 必须逐字复制 plan.evidenceRefs 数组,不得增删`(模型已能看到 plan entry)。 +- 校验失败时把**失败类别**(identity mismatch / refs mismatch / schema invalid)拼进 repair 提示词(只有类别词,不带内容),让第二次尝试有的放矢。 +- `assertWriterOutput` 语义不变。 + +### 验收 + +- 单元测试:refs mismatch 的 repair 提示词包含类别提示;断言校验语义未变(原有 mismatch 用例全保留)。 + +--- + +## 任务 3(P1)· 失败可观测透出 + +### 做法 + +- 报告失败时把 `innerReason`(如 `all_sections_blocked`)与各章 `last_error_code` 汇总透出到报告详情 API(错误码级别;`personal_report_sections` 已有数据,应用层聚合即可,不改表)。 +- 报告中心失败卡片显示可读的失败摘要(如"4 个主题中 4 个写作失败:输出被截断"),替代裸码 `report_schema_invalid`。 + +### 验收 + +- 单元测试 + 一次人为构造的失败在页面上可读。 + +--- + +## 任务 4(P1)· 真实验证与上两轮欠账 + +staging 部署后真实生成 standard personal_full(四主题): + +1. 报告 ready、四章有正文、摘要非降级文案。 +2. writer 输出回溯抽查 ≥3 处(叙事中的 yoga/大运/强弱表述能对回 bundle 条目)。 +3. 每章实测 `inputTokens` / `outputTokens` / 墙钟,对照 `PROGRESS-report-skill-parity-20260901.md` 的 2 倍线裁决(产品已接受估算 2.28×,以本次实测为准;实测超线把数据摆进 PROGRESS,不自行裁决)。 + +## 不在本轮范围 + +- writer schema、assert 语义、最低证据组、bundle 内容的任何改动。 +- 章节并发、模型更换、提示缓存策略调整。 +- 数据库结构。 + +## 收尾 + +- `PROGRESS-report-writer-failure-20260902.md`;`docs/BUG_HISTORY.md` 条目(编号对远端确认;与 8-30 任务 0 观测、`final_parse_rejected` 历史关联)。 +- 建议推送顺序:任务 0 取证先写 PROGRESS(可不推代码);任务 1–3 一次推送;任务 4 部署后补记。 +- 推送后核对 `https://staging.jyotisha.chat/api/health` 的 `.deployment.gitCommit`;流水线约 20 分钟。 +- 不自行提升 main。 + +## 交付物清单 + +1. 任务 0 实证表(telemetry finishReason/outputTokens + 各章 last_error_code)与定案结论 +2. 新预算公式 + 推导过程 + 各深度预算表 + 单元测试 +3. length 重试的压缩提示 + repair 类别提示 + 测试 +4. 失败详情透出(API + 报告中心文案)+ 测试 +5. 真实报告验收:四章正文、telemetry 无 length、回溯抽查 ≥3 处、实测 tokens/墙钟与 2 倍线对照 +6. 全套质量门实际输出、BUG_HISTORY、BLOCKED(如有止损)、PROGRESS