docs(tasks): consultation context window + prompt cache brief (BUG-555/556)

This commit is contained in:
Jesse_Chen
2026-09-06 03:53:58 +00:00
parent d8a0f615c7
commit d2a209d326
2 changed files with 114 additions and 0 deletions
+1
View File
@@ -58,6 +58,7 @@
| `TASK-composer-live-input-and-stop-20260906.md` | `PROGRESS-composer-live-input-and-stop-20260906.md` | 生成中输入框整个禁用致焦点丢失、回车丢消息(改为永不禁用 + 排队发送);生时校正停止呈现为红色告警且选择题等待中停止无效(改为中性停止态、所有 fetch 挂 abort | 待执行 | `codex/composer-live-input-and-stop-20260906`BUG-551、552 |
| `TASK-session-list-title-and-order-20260906.md` | `PROGRESS-session-list-title-and-order-20260906.md` | 历史对话标题改为首轮模型总结(一次、不扣点数、校正/今日运势保持日期标题)、侧栏去资料前缀;排序只按置顶排且元数据 PATCH 推进 `updated_at`(改为置顶 + 活动时间,改名/收藏/换模型不动顺序);历史区加 今天/昨天/7天/30天/更早 分组;列表按游标分页(每页 40、置顶首页全量、滚到底静默续取) | 待执行(串行:在 composer 单之后) | `codex/session-list-title-and-order-20260906`BUG-553 |
| `TASK-settings-dialog-and-billing-pane-20260906.md` | `PROGRESS-settings-dialog-and-billing-pane-20260906.md` | 设置弹窗尺寸随分区跳变、星盘资料格无条件铺开整张添加表单(改为固定尺寸四分区、列表→详情);「账户与点数」成为弹窗分区,删除 `/membership``/membership/orders` 页面并重定向,七处入口改回调 | 待执行(串行:在会话列表单之后) | `codex/settings-dialog-and-billing-pane-20260906`BUG-554 |
| `TASK-consultation-context-and-cache-20260906.md` | `PROGRESS-consultation-context-and-cache-20260906.md` | 普通对话历史只取每条前 4,000 字(报告结论被砍、无标记)、历史窗口不看模型 `context_window`、无溢出识别;改为检查点式会话摘要 + append-only 尾巴 + 按模型预算 + 一次降级重试;Anthropic 历史断点;共享方法段进系统块;后台用量页显示缓存命中率 | 待执行(串行:在会话列表单之后;动表需迁移) | `codex/consultation-context-and-cache-20260906`BUG-555/556 |
| `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`2026-09-05staging 部署仍停在 `afd14948``deploy-staging``c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`BUG-550 |
| `TASK-api-not-configured-mislabel-20260904.md` | — | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待执行 | `codex/api-not-configured-mislabel-20260904`BUG-542 起) |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505509 |
@@ -0,0 +1,113 @@
# TASK · 普通对话上下文窗口与模型缓存(2026-09-06)
- 基线:`origin/staging` @ `d8a0f615`
- 分支:`codex/consultation-context-and-cache-20260906`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/consultation-session-history.ts``frontend/src/app/api/consult/route.ts``frontend/src/lib/agent-generation-settings.ts``frontend/src/lib/model-catalog.ts``frontend/src/lib/consultation-methodology.ts``frontend/src/mastra/skill-binding.ts``frontend/src/mastra/index.ts``frontend/src/app/api/admin/usage/aggregate/route.ts``frontend/src/components/admin/pricing-simulator.tsx`(或承载用量聚合表的组件)、新文件 `frontend/src/lib/session-context-summary.ts`、新迁移 `frontend/supabase/migrations/20260906010000_chat_session_context_summary.sql`。**不改**生时校正链路(`rectification-agentic/**``rectification-v9-tools.ts`)、不改 Skill 正文、不改引擎、不改 `page.tsx`
- BUG 编号起点:**BUG-555**BUG-551/552 归 composer 单、553 归会话列表单、554 归设置弹窗单、542 归 api-not-configured 单;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
- 串行:本单与 `TASK-session-list-title-and-order-20260906.md`BUG-553)都改 `consult/route.ts` 的结算路径与 `chat_sessions` 行,**在 BUG-553 合入 staging 之后开工**;与 composer 单、设置弹窗单无文件交集。
- 本单动表,必须真跑 `npm run test:db --prefix frontend`;本机无 Docker 时按 §8 写 `BLOCKED.md`
## 1. 事实(基线代码实测,按字节估算 token,未用分词器)
| 部分 | 普通对话每轮 | 生时校正每轮(对照,本单不动) |
| --- | --- | --- |
| 系统提示 | 产品指令约 10.7 KB + Skill 运行时节选约 28 KB`skill-binding.ts::boundMethod` 从 51 KB 的 `SKILL.md` 绑 9 段) | Skill 10.0.14 全文约 18.5 KB |
| 聊天历史 | `consultationHistoryFromStoredMessages`:最近 `CONSULTATION_HISTORY_LIMIT = 12` 条,每条 `slice(0, 4_000)` | 不带;模型调 `rectification-read-case` 读投影(最近 8 轮 × 1,600 字 + 最近 20 条证据) |
| 工具结果 | `run-jyotish-consultation`:每领域约 15–18 KB 排盘 + `methodology``METHODOLOGY_TOTAL_MAX_CHARS = 24_000` | 档案投影 |
| 动态内容位置 | 时间、称呼、模式说明都在最后一条用户消息(`consult/route.ts` L735800 | 时间、Case ID 在用户消息 |
| 最坏总量 | 约 89 万 tokens | 约 34 万 tokens |
四个问题:
1. **报告尾巴被砍且无标记**`slice(0, 4_000)` 取每条消息开头。Level 2 报告通常远超 4,000 字,模型在下一轮看不到自己上一轮的应期、综合判断与技法审计表,也没有任何"已截断"提示。用户追问"你上次说的应期"时模型是真的没看到。
2. **没有溢出保护**:后台模型配置有 `model_config_versions.context_window``api/admin/models/route.ts` L47/L96/L141),但 `model-catalog.ts::resolveRow` 不读它,`ResolvedLanguageModel` 没有这个字段,历史窗口是写死的常数;超限时供应商报错会走普通失败路径,没有任何地方识别 `context_length_exceeded` 一类错误。
3. **缓存只对 Anthropic 显式生效,且不覆盖历史**`agent-generation-settings.ts::cachedSystemMessage` 只在 `providerId === "anthropic"` 时加 `cacheControl: ephemeral`,位置在系统块之后(指令 + Skill + 工具定义命中),历史消息不在任何断点内,每轮全价重算。OpenAI / OpenAI 兼容供应商靠自动前缀缓存,代码已把动态内容放在最后一条消息,前缀稳定——这部分是对的,但滑窗一旦启动(第 13 条起)历史前缀每轮变化,只剩系统块能命中。
4. **命中率无人可见**`complete_usage` RPC 把 `metadata.cache {readTokens, writeTokens, noCacheTokens, hit}` 写进 `usage_events``usage_ledger.metadata`consult 路由 L572、rectification 路由 L682),但 `api/admin/usage/aggregate` 与用量列表都不读它,后台没有任何一列显示缓存。另外 `costMicrousd` 按全价输入单价计算,不区分缓存读价——这是账务口径问题,本单不改(见 §7)。
另有一处已经做对、本单要保住的:`skill-binding.ts` 已把 129 KB 的 Skill 激活清单压成 28 KB 运行时节选(注释写明),是当前最大的一次上下文优化,不得回退。
## 2. 根因
- 历史窗口是"固定条数 × 固定头部字数"的一刀切,既不知道模型有多大,也不保留结论。
- 缓存能力只对一家供应商写了显式标记,其余靠供应商默认;没有数据回流到后台,无法判断是否真的命中。
- `methodology` 里跨领域不变的两段(Full-spectrum invocation 约 1.9 KB、Event judgment skeleton 约 4.5 KB)每次随工具结果重发,工具结果不在任何缓存前缀里。
## 3. 决策记录(产品已授权,2026-09-06"都要"
1. **会话滚动摘要,按检查点而不是按轮**`chat_sessions` 新增 `context_summary jsonb null`,形状 `{ version: 1, text, throughRequestId, throughMessageIndex, messageCount, updatedAt }`。历史 = **摘要之后的全部消息(append-only 尾巴)**;只有当尾巴总字数超过 `CONSULTATION_HISTORY_TAIL_MAX_CHARS = 16_000` 时,在本轮结算后做一次检查点:把"旧摘要 + 尾巴中除最后一对问答外的消息"压成新摘要,`through*` 前移到最后一对之前。两个检查点之间尾巴只增不减,所以前缀稳定、自动前缀缓存与 Anthropic 增量缓存都能延续命中;这是选"检查点"而不是"每轮摘要"的唯一理由。
2. **摘要放在最后一条用户消息里**(与时间、称呼同位置,标题固定为 `【会话摘要(服务端维护)】`),不放在历史前面——摘要一变就会让它后面的所有历史失去前缀,放在末尾则只影响本轮动态部分。
3. **每条消息的截断上限从 4,000 提到 `CONSULTATION_HISTORY_MESSAGE_CHARS = 12_000`,超出时在截断处写明** `……(以下省略 N 字,结论已并入会话摘要)`,不再静默。
4. **摘要生成不扣用户点数、不阻塞回答**:在 `complete_consultation_response` 成功之后触发,15 s 超时用 ref 的计时器(不得用裸 `AbortSignal.timeout()`,它的计时器始终 unref,空事件循环会在 abort 前排空——BUG-523),失败只 `console.warn`,下一轮再试。模型用目录默认模型(`defaultModelId`),默认模型不可用时用会话模型。摘要输出 ≤ 800 汉字,固定四段:已问过的问题 / 已给出的结论(含应期、置信度、blocked 项)/ 用户补充的事实 / 未决与待追问;不得包含出生日期、出生时间、出生地、姓名、邮箱(服务端只传问答文本,不传资料;生成后正则清洗日期时间形态的字段属于二道保险)。
5. **写入用乐观并发**`update … set context_summary = $new where id = $id and (context_summary is null or context_summary->>'updatedAt' = $seenUpdatedAt)`;两轮并发结算时后到者放弃。
6. **按模型上下文算预算**:目录读 `context_window`(列已存在,不动表);`context_window` 为空时视作 128k。历史预算 `historyBudgetChars = clamp((context_window 60_000) × 1.5, 4_000, 40_000)`——60k 为系统块 + 三领域工具结果 + 输出预算的保留量,1.5 为汉字/token 粗系数;尾巴超预算时**从最旧一端整条丢**(不砍单条)直到不超,摘要始终保留。128k 模型结果与决策 1 一致;64k 模型历史约 6,000 字;32k 模型只剩摘要 + 最后一对。
7. **溢出识别与一次降级重试**:把供应商错误文本 / code 中的 `context_length_exceeded``maximum context length``prompt is too long``input is too long``too many tokens``max_tokens` 与上下文相关的 400 归为 `context_overflow`;命中时**同一请求内**把历史缩到"摘要 + 最后一对"重试一次(复用现有 `attempt.reset` 事件,不新增等待态),仍失败才按现有失败路径结算。用户看到的仍是一次等待一次揭幕。
8. **Anthropic 第二个缓存断点**:历史尾巴非空时,在最后一条历史消息上加 `providerOptions.anthropic.cacheControl`(保持系统块断点不变,总断点数 ≤ 4)。OpenAI / 兼容供应商不加任何标记(它们的自动缓存无需也不接受标记),继续靠前缀稳定。
9. **`methodology` 中跨领域不变的两段搬进系统块**Full-spectrum invocation 与 Event judgment skeleton 由 `skill-binding.ts` 绑进 `jyotishSkillMethodBlock`(可缓存),工具结果的 `methodology.sections` 只保留领域专属段(strict 路由 + 对应 `event_judgment_*.md`)与 `further_reading` / `domains_without_strict_checklist``mastra/index.ts` 中"methodology 段落已交付"的措辞相应改为"共享基线在系统块、领域清单随工具结果"。**不**把整份 `strict-workflow-router.md`22.6 KB)与三份 event judgment(约 15 KB)都搬进系统块——那会让无缓存供应商每轮多付 12–27 KB,是否值得等 §5.2 的命中率数据出来再定。
10. **后台可见**:用量聚合接口按 `actual_model_id` × 近 7 天 / 近 30 天给出:有缓存数据的运行数、命中率(`readTokens > 0` 的运行 ÷ 有缓存数据的运行)、`readTokens` / `writeTokens` / `noCacheTokens` 合计、缓存占比(read ÷ (read + write + noCache));用量列表加一列"缓存读 tokens"。全部从 `usage_ledger.metadata->'cache'` 读,不动表、不加索引(30 天窗口走现有 `usage_ledger_created_idx`)。
11. 生时校正链路一行不动:它已是档案 + 投影的正确形态。
## 4. 硬红线
- `jyotishSkillMethodBlock` 的 9 段节选与 `providesSkillDiscovery: "on-demand"` 机制不得回退;系统块字节数变化写进进度记录(预期 +6.4 KB)。
- 摘要与历史都只含问答文本;出生资料仍只在服务端工具内部,不进 prompt。摘要文本不进 `usage_ledger.metadata`、不进日志。
- 现有 `consultation-session-history.test.ts``admin-usage-aggregate-contract.test.ts` 的断言若改,三栏(原值 / 新值 / 原因)说明;测试总数不低于开工时 `origin/staging` 实测。
- 不改 `page.tsx`;客户端零改动(溢出重试复用服务端既有事件)。
- 不给摘要、缓存加任何开关或环境变量;不加"加载中"态。
- 不改 `costMicrousd` 的计价口径(§7)。
- 迁移只加一列,无回填、无索引;`npm run test:db` 真跑或写 `BLOCKED.md`
## 5. 任务分解
### 5.1 摘要 + 尾巴历史 + 按模型预算(决策 1–7)
- 迁移 `20260906010000_chat_session_context_summary.sql``alter table public.chat_sessions add column if not exists context_summary jsonb null check (context_summary is null or jsonb_typeof(context_summary) = 'object')`
- `model-catalog.ts``queryCatalog` 选出 `v.context_window``ResolvedLanguageModel.contextWindow: number | null`
- `consultation-session-history.ts`:新导出 `consultationHistoryWindow(messages, summary, { contextWindow, excludeRequestId })``{ tail: ConsultationHistoryMessage[], summaryText: string | null, droppedCount }``historyBudgetChars(contextWindow)`;截断标记;旧函数保留为兼容包装或删除(删除则更新调用方与测试)。
- 新文件 `lib/session-context-summary.ts``shouldCheckpoint(messages, summary)``buildSummaryPrompt(previous, messages)``generateSessionContextSummary(...)`(ref 计时器超时、清洗、长度上限)、`writeSessionContextSummary(...)`(乐观并发 SQL)。
- `consult/route.ts`:读 `chat_sessions.context_summary``baseMessages` 用尾巴 + 摘要块;`completeResponse``complete_consultation_response` 成功后 `void checkpoint()``context_overflow` 识别与一次降级重试;`sliced` / `continue` / 重试路径复用同一窗口。
- 验收:
- `tests/consultation-session-history.test.ts`:① 20 条消息、无摘要、128k → 尾巴按预算从最旧丢整条;② 有摘要 `throughMessageIndex = 14` → 尾巴只含 15 起;③ 单条 20,000 字 → 12,000 字 + 省略标记且标记含省略字数;④ `contextWindow = 64_000``32_000` 的预算数值;⑤ `contextWindow = null` 等价 128k。
-`tests/session-context-summary.test.ts`:① 尾巴 15,999 字不触发、16,001 字触发;② 触发时最后一对不进摘要输入;③ 摘要清洗掉 `1990-01-01` / `08:30` / 邮箱形态;④ 空事件循环下超时仍会 abort、用例不得被 `cancelledByParent`(照 BUG-523 的验收口径看 `# cancelled` 与退出码);⑤ 并发写 `updatedAt` 不匹配时放弃。
- 消息装配测试(现有 consult 路由合同测试或新增):摘要块出现在最后一条用户消息、位于时间行之后问题之前;`context_overflow` 错误触发一次重试且重试消息只含摘要 + 最后一对;第二次失败按原路径结算。
- `npm run test:db`Docker)通过,或 `BLOCKED.md` 记录 + 迁移 SQL 在 psql 语法检查(`psql --set ON_ERROR_STOP=1 -f` 对空库)通过。
### 5.2 后台缓存可见(决策 10)
- `api/admin/usage/aggregate/route.ts`:新增 `cache` 段(按 `actual_model_id`,两个窗口),SQL 只用 `metadata->'cache'``api/admin/usage/route.ts` 列表加 `cacheReadTokens`
- 前端聚合表加"缓存命中"表格(模型 / 运行数 / 命中率 / 缓存占比 / 读 / 写 / 未命中),列表加一列。
- 验收:`tests/admin-usage-aggregate-contract.test.ts` 扩展——metadata 无 `cache` 的行不计入分母;`readTokens = 0` 计为未命中;两个窗口互不串;权限仍为 `billing.orders.read`
### 5.3 Anthropic 历史断点(决策 8
- `agent-generation-settings.ts` 新导出 `cachedHistoryMessage(message, model)`(非 Anthropic 返回原消息);`consult/route.ts` 尾巴最后一条套用。
- 验收:单测——Anthropic 模型时最后一条历史带 `providerOptions.anthropic.cacheControl`,其余历史与 OpenAI / 兼容模型的所有消息都不带;系统块断点仍在。
### 5.4 共享方法段搬进系统块(决策 9)
- `skill-binding.ts``jyotishSkillMethodBlock` 追加 `<jyotish-shared-method>`Full-spectrum invocation + Event judgment skeleton,仍从包内文件实时读取);`consultation-methodology.ts::consultationMethodologyForDomains` 不再 push 这两段;`mastra/index.ts` 指令措辞同步。
- 验收:现有 methodology 合同测试更新(三栏说明);新断言——单领域 `methodology.sections` 只含该领域两段;系统块含两段且与包内文件逐字一致;进度记录写系统块前后字节数与单领域 / 三领域工具结果前后字节数。
### 5.5 记录
- `docs/BUG_HISTORY.md`:BUG-555(历史头部截断丢结论、无溢出识别)、BUG-556(缓存命中数据写库但后台不可见;Anthropic 历史无断点)。
- `CHANGELOG.md` 一条;`docs/tasks/PROGRESS-consultation-context-and-cache-20260906.md`(含 §4 要求的字节数表);`docs/testing/consultation-context-and-cache-20260906.md`(真实环境:同一会话问 6 轮以上后追问"你前面说的应期是哪年",答案与前文一致;后台用量页缓存表在 staging 跑过 3 轮后命中率 > 0DeepSeek / OpenAI / Anthropic 各一条对照)。
- `BLOCKED.md`:无 Docker 时 `test:db` 条目。
## 6. 让步顺序
5.1 → 5.2 → 5.3 → 5.4 → 5.5。5.1 内部:迁移 + 尾巴历史 + 截断标记不可拆;摘要生成与溢出重试可以各自后置但必须在同一分支完成后再验收。5.5 不可省。
## 7. 不在本单
- **缓存读折扣计价**`costMicrousd` 目前按全价输入单价算,缓存读实际按供应商折扣计费(Anthropic / DeepSeek 约 0.1×,OpenAI 约 0.5×)。要改需在 `model_config_versions` 加缓存读单价列并改后台模型表单,产品另定。
- 整份 strict router / event judgment 搬进系统块:等 5.2 的命中率数据。
- 生时校正链路的缓存断点(工具结果在 agent 循环内,AI SDK 不便打断点)。
## 8. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/consultation-context-and-cache-20260906 .worktrees/consultation-context-and-cache-20260906 origin/staging
cd .worktrees/consultation-context-and-cache-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && npx tsx --test tests/consultation-session-history.test.ts tests/admin-usage-aggregate-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok"
npx tsx --test tests/*.test.ts 2>&1 | grep -E "^# (tests|pass|fail)" # 记下总数作为下限
```
收尾:`tsc --noEmit``npm run lint`0 error)、`npm test` fail=0 且总数不低于开工值、`npm run build``/` 仍 Static,首屏 gzip ±2%)、`npm run test:db`(或 `BLOCKED.md`)。部署前产品负责人需先跑 Gitea `Migrate Staging Database`(本迁移与已积压的 `5010000_personal_report_longform_appendices.sql` 一起)。