- README.md is now the product/repo front door (architecture, repo map, local dev, test tiers, delivery flow, doc map). Engine positioning, VedAstro/Codex setup and the oracle/benchmark command reference move verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and docs/benchmark/README.md. Capability badges realigned with the registry (91/78/8/0); tests/test_readme_badges.py was red on staging. - AGENTS.md: Part A (environment truth, delivery, worktrees, record placement, bug workflow, growth freeze, frontend red lines, privacy, pre-work check, test tiers) and Part B (reading-rigor constraints). GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only mirror. All strings locked by tests/ are preserved. - CLAUDE.md added: roles, three working modes, task-brief sections, acceptance criteria, session discipline; imports AGENTS.md. - 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to docs/tasks/ with an index; REPO_LAYOUT.md merged into README. Docs-only change (no gated path touched). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
242 lines
13 KiB
Markdown
242 lines
13 KiB
Markdown
# 任务书 · 个人报告改为分章节生成(2026-08-30)
|
||
|
||
基线:`origin/staging` @ `a96df754`。
|
||
|
||
## 为什么要做
|
||
|
||
现状:`createPersonalReportAgent` 用**一次**模型调用产出整份叙述(`executiveSummary` + 最多 12 个 `thematicNarrative`),结构化输出 + Zod 校验,失败只允许**一次**修复重试。
|
||
|
||
两个问题:
|
||
|
||
1. **没有设输出上限。** 仓库里有现成的 `agentGenerationSettings`(答复预算 16,384 token),`agent-run.ts` 和 `consultation-tools.ts` 都在用,**只有 `src/mastra/personal-report.ts` 没用** —— 它走模型提供方的默认值。而 schema 允许的最坏输出约 224,000 字符(12 章 × 每章上限)。
|
||
2. **失败是全有或全无。** 任何一章写坏或被截断 → JSON 不完整 → schema 不过 → 重试一次 → 整份报告 `failed`。用户看到的是"生成失败",不是"少了两个主题"。
|
||
|
||
**产品已确认:一份缺若干主题、缺失部分明确标注的报告,算合格交付。** 这是本轮降级方案成立的前提。
|
||
|
||
## 这不是"上下文丢失"问题
|
||
|
||
`createPersonalReportAgent` 是结构性无状态的 —— 注释原文:*"chat history and skill text are structurally excluded by the agent definition (no skills, no tools, no memory)"*。prompt 是 `(bundle, plan)` 的纯函数,**现在就没有跨调用的对话上下文可丢**。拆开后每次仍拿到完整证据。
|
||
|
||
真正需要跨章节的只有三件,均在本任务书内解决:摘要要概括全部主题(最后生成)、章节间不重复(传已写标题列表)、断言口径一致(服务端 `claimCards` + `evidenceRefs` 绑定,本就每次相同)。
|
||
|
||
---
|
||
|
||
## 硬红线
|
||
|
||
1. **任务 0 是门控。** 观测必须先上线并收到真实数据,才允许调整输出上限或收紧 schema。**不得凭任务书里的估算直接改参数** —— 那个 224,000 是 schema 上限推算,不是实测。
|
||
2. **章节生成先串行,不得并发。** 章节之间无依赖,理论上可并发,但会放大限流风险。先串行拿数据。
|
||
3. **`report_document` 的语义不得改变**:它只存最终成品。中间结果一律不写进去,避免半成品被当成完整报告读出。
|
||
4. **schema 只可收紧,不可放宽。** 不得为了让截断的输出通过而放松校验。
|
||
5. **任务生命周期在 Postgres 函数里**(`claim_personal_report_job` / `heartbeat_personal_report_job` / `complete_personal_report_job` / `fail_personal_report_job`,见 `supabase/migrations/20260814040000_personal_report_jobs_v2.sql`)。章节级状态**必须沿用同一模式加 SQL 函数**,不得把状态机搬进应用层。
|
||
6. **不得修改既有测试断言** —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
|
||
7. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`。
|
||
8. **数据库测试必须真跑。** 本轮动了表结构,`npm run test:db` 需要 Docker。**没有 Docker 就不要推** —— 把环境缺口写进 `BLOCKED.md` 并停下,不允许"本地跑不了所以跳过"。
|
||
9. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
|
||
|
||
让步顺序:数据不损坏 > 功能与测试不回归 > 可验证的改进 > 成本 > 代码整洁。
|
||
|
||
## 开工前置
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/report-sectioned-20260830 \
|
||
../.worktrees/report-sectioned-20260830 origin/staging
|
||
```
|
||
|
||
基线必须是 `origin/staging`。读 `pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索同类记录。
|
||
|
||
**先读这几个文件再动手**,本任务书的判断都基于它们:
|
||
|
||
- `src/mastra/personal-report.ts` —— agent、prompt、schema、重试
|
||
- `src/lib/personal-report-plan.ts` —— `ReportSectionPlanEntry`,注意它**已经有** `disposition`、`evidenceRefs`、`targetCharacters`
|
||
- `src/lib/personal-report-contract.ts` —— `claimStatus` 含 `blocked`
|
||
- `supabase/migrations/20260814040000_personal_report_jobs_v2.sql` —— 任务表与四个 SQL 函数
|
||
- `src/components/personal-report/personal-report-page.tsx` —— 9 个状态与轮询
|
||
|
||
---
|
||
|
||
## 任务 0(P0,门控)· 先把截断变成可观测的
|
||
|
||
### 事实
|
||
|
||
`src/mastra/personal-report.ts` 的 `logTelemetry` 只记 `resolved` / `failed`,不记 `finishReason` 也不记 token 用量。**现在无法从数据上区分"模型不听话"和"输出被截断"**,也就无法证明后续改动有效。
|
||
|
||
### 做法
|
||
|
||
- `logTelemetry` 增加 `finishReason`(`length` 即截断)与 `usage.outputTokens` / `usage.inputTokens`
|
||
- 两次尝试(首次与修复重试)各记一条,能区分是哪一次
|
||
- 遵守 `AGENTS.md` 的隐私红线:**只记指标名、阶段、数值**,不得记 prompt、bundle、模型原文、用户资料、案例 ID
|
||
|
||
### 验收
|
||
|
||
- 人为构造一次会被截断的生成(例如临时把输出上限调到极小),确认日志出现 `finishReason: "length"`
|
||
- 上线后至少收集一段真实数据,在 PROGRESS 里给出:失败总数、其中 `length` 占比、`outputTokens` 的 p50/p95
|
||
|
||
**这一条必须先单独上线并收到数据**,任务 2 的输出上限取值要用这批数据决定,不许拍脑袋。
|
||
|
||
---
|
||
|
||
## 任务 1(P0)· 章节中间结果落库
|
||
|
||
### 做法
|
||
|
||
新建 `public.personal_report_sections`,**不要**往 `report_document` 里增量写。
|
||
|
||
字段至少包含:`user_id`、`request_id`、`section_id`、`payload jsonb`(单个 thematicNarrative 条目)、`status`(`pending` / `ready` / `blocked`)、`attempt_count`、`max_attempts`、`last_error_code`、`created_at`、`updated_at`。主键或唯一约束用 `(user_id, request_id, section_id)`。
|
||
|
||
选这个方案而不是复用 `report_document` 的三个理由,实现时请守住:
|
||
|
||
1. `report_document` 保持"最终成品"语义
|
||
2. **每个章节有自己的重试预算** —— 一个难写的主题不得烧光整个任务的 `max_attempts`
|
||
3. 续做只需查"哪些 section 还没有 payload"
|
||
|
||
### 迁移要求
|
||
|
||
照 `20260814040000_personal_report_jobs_v2.sql` 的既有写法:
|
||
|
||
- 整个迁移包在 `begin; ... commit;` 里
|
||
- 所有 DDL 用 `if not exists`,可重复执行
|
||
- **必须 `enable row level security`**,并给 `authenticated` 只读自己的行的 policy
|
||
- 显式 `grant`:`authenticated` 只 `select`;`service_role` 才有写权限
|
||
- 章节状态流转写成 SQL 函数(与 `claim/heartbeat/complete/fail_personal_report_job` 同一模式),`security definer` 并显式 `grant execute` 给 `service_role`
|
||
- 文件名沿用 `YYYYMMDDHHMMSS_描述.sql` 格式,时间戳要晚于 `20260830010000`
|
||
|
||
### 验收
|
||
|
||
```bash
|
||
npm run db:migrate:check # 幂等性
|
||
npm run test:db # 需要 Docker
|
||
```
|
||
|
||
新增数据库测试,至少覆盖:`authenticated` 读不到别人的章节;`authenticated` 写不进去;`service_role` 能写;同一 `(user_id, request_id, section_id)` 重复插入不产生第二行。
|
||
|
||
### 止损
|
||
|
||
如果执行环境没有 Docker,**停在这里**,把缺口写进 `BLOCKED.md`,不要带着未验证的迁移继续做任务 2。
|
||
|
||
---
|
||
|
||
## 任务 2(P0)· 把一次生成切成 plan → 章节 → 摘要 → 装配
|
||
|
||
### 阶段模型
|
||
|
||
沿用**已存在但基本没用起来**的 `personal_report_jobs.progress_phase`:
|
||
|
||
```
|
||
plan → section:<id> × N → summary → assemble → ready
|
||
↑确定性 ↑每章节一次调用 ↑最后一次 ↑确定性
|
||
```
|
||
|
||
### 每个章节一次调用
|
||
|
||
输入:
|
||
|
||
- **按该章节 `evidenceRefs` 过滤后的 bundle 子集** —— `ReportSectionPlanEntry.evidenceRefs` 已经有这个字段。**这条是成本关键**:不过滤就是把完整证据包重发 N 次。过滤后既省钱也让模型更聚焦。
|
||
- plan 只保留这一个 section
|
||
- **已完成章节的标题列表**(只要标题,不要正文)——用于避免重复
|
||
- 显式输出上限,用 `agentGenerationSettings`;具体数值由任务 0 的数据决定,并参考该 section 的 `targetCharacters.max`
|
||
|
||
输出 schema 收成**单个 thematicNarrative 条目**,不再是 12 条数组。
|
||
|
||
### 摘要最后生成
|
||
|
||
输入是各已完成章节的**标题 + `claimStatus`**,不是正文全文。这样既概括到全部主题,又不会把 prompt 撑大。
|
||
|
||
### 装配
|
||
|
||
确定性合并成 `report_document`,此时才写 `report_document` 并把 `personal_reports.status` 置 `ready`。
|
||
|
||
### 续做
|
||
|
||
worker 拿到租约后,按 `progress_phase` 与 `personal_report_sections` 的已有行判断从哪继续,**已完成章节不得重算**。
|
||
|
||
### 验收
|
||
|
||
- 单元测试:给定 N 个 write 章节,验证发生 N+1 次调用,且第 k 次的 bundle 只含该章节 `evidenceRefs` 引用到的证据
|
||
- 续做测试:模拟在第 3 章后中断,重新领取任务,验证前 3 章不重算、最终文档完整
|
||
- 端到端至少跑一份真实报告,PROGRESS 给出:章节数、每章 `outputTokens`、总墙钟时间,与改前单次调用对照
|
||
|
||
### 止损
|
||
|
||
- 如果过滤 bundle 导致某类章节缺证据写不出来,**不要退回发全量**:先确认是 plan 的 `evidenceRefs` 不全还是过滤逻辑错了。真是 plan 不全就修 plan,并在 PROGRESS 说明。
|
||
- 如果总墙钟时间超过现有 8 分钟轮询预算(`POLL_BUDGET_MS`),**不要顺手调大预算**,先把数据摆出来再决定。
|
||
|
||
---
|
||
|
||
## 任务 3(P1)· 章节写不出时降级为 blocked,报告照常交付
|
||
|
||
### 前提
|
||
|
||
**产品已确认:缺若干主题、缺失部分明确标注的报告算合格交付。**
|
||
|
||
`blocked` 已经是一等 `claimStatus`(`personal-report-contract.ts`),文档视图已有「阻塞」标签与渲染路径 —— 复用它,不要新造状态。
|
||
|
||
### 做法
|
||
|
||
- 单个章节耗尽自己的 `attempt_count` 后,标记该行 `status = 'blocked'`,记 `last_error_code`,**继续下一章**
|
||
- 装配时把 blocked 章节渲染成明确的"本节未能生成",并说明原因类别(截断 / 校验失败 / 证据不足),**不得静默省略**
|
||
- 只有当**全部** write 章节都 blocked、或摘要本身失败时,整份报告才 `failed`
|
||
|
||
### 明确不要做
|
||
|
||
- 不得因为有 blocked 章节就退积分或改计费 —— 产品已定这算合格交付
|
||
- 不得把 blocked 章节伪装成正常内容
|
||
- 不得为了凑数让模型"随便写点什么"
|
||
|
||
### 验收
|
||
|
||
- 强制某一章连续失败,验证:该章标 blocked、其余章正常、报告 `ready`、页面上该节有明确标注
|
||
- 强制全部章节失败,验证报告 `failed` 且失败原因可读
|
||
|
||
---
|
||
|
||
## 任务 4(P2)· 等待页显示真进度
|
||
|
||
### 事实
|
||
|
||
`personal_report_jobs.progress_percent`(0–100)和 `progress_phase` 都已存在,目前基本没被用起来。等待页只有一个转圈加"已等待 N 分",8 分钟里没有任何进展信号。
|
||
|
||
### 做法
|
||
|
||
- 每完成一个章节更新 `progress_percent`(已完成 / 总 write 章节数)与 `progress_phase`
|
||
- 等待页显示"正在生成第 3/7 个主题"一类的文案
|
||
- 保留既有的 `timed-out` / 「继续等待」/ 「返回报告中心」路径,**不要动那 9 个状态的结构**
|
||
|
||
### 验收
|
||
|
||
生成一份多主题报告,录一段等待过程,说明进度是如何推进的。
|
||
|
||
---
|
||
|
||
## 不在本轮范围
|
||
|
||
- **章节并发生成**:红线 2 已禁止。等串行数据出来再单独立项。
|
||
- **提示缓存**:先做 `evidenceRefs` 过滤,量完成本再谈。
|
||
- **收紧 `thematicNarrative` 的 `max(12)`**:等任务 0 的数据。
|
||
- **换模型或改模型选择逻辑**:不在本轮。
|
||
- 前端 UI 的其它优化:见 `TASK-frontend-interaction-20260830.md`,两份任务书互不冲突。
|
||
|
||
## 收尾
|
||
|
||
- PROGRESS 文件名写 `PROGRESS-report-sectioned-20260830.md`。**不要写成 `PROGRESS.md`** —— 根目录已有受版本控制的 `progress.md`,大小写不敏感文件系统上会互相覆盖。
|
||
- **任务 0 单独一次推送并收数据**;任务 1–4 可合成第二次推送。staging push 触发全量构建+部署,没有路径过滤。
|
||
- 数据库迁移走既有流程(`.gitea/workflows/migrate-staging-database.yml`),**不要手工连库改表**。
|
||
- 推送后核对 `https://staging.jyotisha.chat/api/health` 的 `.deployment.gitCommit`。流水线是 `validate` → `publish` → 才派发 `deploy`,**全链路约 20 分钟**,不要十几分钟就断定失败。
|
||
- 不自行提升 main。
|
||
|
||
## 交付物清单
|
||
|
||
1. `logTelemetry` 记 `finishReason` 与 token 用量,且不含任何隐私字段
|
||
2. 任务 0 上线后的真实数据:失败总数、`length` 占比、`outputTokens` p50/p95
|
||
3. `personal_report_sections` 迁移(事务内、幂等、RLS、显式 grant、SQL 状态函数)
|
||
4. 数据库测试:越权读、越权写、service_role 可写、重复插入幂等
|
||
5. 分章节生成实现:N+1 次调用、按 `evidenceRefs` 过滤 bundle、摘要最后生成
|
||
6. 续做测试:中断后不重算已完成章节
|
||
7. blocked 降级:单章失败报告仍 `ready` 且有明确标注;全部失败才 `failed`
|
||
8. `progress_percent` / `progress_phase` 真实推进 + 等待页文案
|
||
9. 改前改后对照:章节数、每章 `outputTokens`、总墙钟时间
|
||
10. `docs/BUG_HISTORY.md` 条目(编号先对远端确认)
|
||
11. `BLOCKED.md`:任何触发止损的项,尤其是 Docker 缺口
|
||
12. `PROGRESS-report-sectioned-20260830.md`
|
||
13. `tsc --noEmit` / `eslint`(0 error)/ `tsx --test`(失败清单与基线逐条比对)/ `npm run test:db` / `next build` 的实际输出
|