Files
Jyotisha/docs/tasks/TASK-consultation-context-and-cache-20260906.md
T

17 KiB
Raw Blame History

TASK · 普通对话上下文窗口与模型缓存(2026-09-06)

  • 基线:origin/staging @ d8a0f615
  • 分支:codex/consultation-context-and-cache-20260906
  • 执行方:coding agent;验收:Claude
  • 涉及文件:frontend/src/lib/consultation-session-history.tsfrontend/src/app/api/consult/route.tsfrontend/src/lib/agent-generation-settings.tsfrontend/src/lib/model-catalog.tsfrontend/src/lib/consultation-methodology.tsfrontend/src/mastra/skill-binding.tsfrontend/src/mastra/index.tsfrontend/src/app/api/admin/usage/aggregate/route.tsfrontend/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-555BUG-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.mdBUG-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 KBskill-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 排盘 + methodologyMETHODOLOGY_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_windowapi/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_eventsusage_ledger.metadataconsult 路由 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_exceededmaximum context lengthprompt is too longinput is too longtoo many tokensmax_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_checklistmastra/index.ts 中"methodology 段落已交付"的措辞相应改为"共享基线在系统块、领域清单随工具结果"。把整份 strict-workflow-router.md22.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.tsadmin-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.sqlalter 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.tsqueryCatalog 选出 v.context_windowResolvedLanguageModel.contextWindow: number | null
  • consultation-session-history.ts:新导出 consultationHistoryWindow(messages, summary, { contextWindow, excludeRequestId }){ tail: ConsultationHistoryMessage[], summaryText: string | null, droppedCount }historyBudgetChars(contextWindow);截断标记;旧函数保留为兼容包装或删除(删除则更新调用方与测试)。
  • 新文件 lib/session-context-summary.tsshouldCheckpoint(messages, summary)buildSummaryPrompt(previous, messages)generateSessionContextSummary(...)(ref 计时器超时、清洗、长度上限)、writeSessionContextSummary(...)(乐观并发 SQL)。
  • consult/route.ts:读 chat_sessions.context_summarybaseMessages 用尾巴 + 摘要块;completeResponsecomplete_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_00032_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:dbDocker)通过,或 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.tsjyotishSkillMethodBlock 追加 <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. 开工前置命令

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 --noEmitnpm run lint0 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 一起)。