Skip to content

refactor(agent): 收紧上下文准备,每轮只读一次历史,历史窗口只留一条查询 - #1724

Open
chen-ran wants to merge 6 commits into
mainfrom
refactor/context-prep
Open

chen-ran wants to merge 6 commits into
mainfrom
refactor/context-prep

Conversation

@chen-ran

@chen-ran chen-ran commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Author

  • Human
  • Agent

Type

  • bug
  • feat
  • test

Summary

收紧 chat / Discuss 的上下文准备:每轮只读一次历史,历史窗口只留一条遍历查询,并修掉两个生产问题(Memoh-Cloud #685、#697)。Discuss 仍在 Channel 组装、chat 仍在 Server 组装,两边改为共用同一套加载与转换代码;不改跨进程协议。

1. Discuss 凭据在派发时签发(#685)

Channel 收到消息时签发 5 分钟有效的 SessionToken / ChatToken,存在 Discuss worker 的长寿命配置里,等历史加载、排队、重组之后才用于 StartTurn;超过 5 分钟就报 invalid session credential for resume。现在 DiscussSessionConfig 携带 Credentials(绑定唤醒会话的那条入站消息的身份),worker 在每次 runner.Run 前签发新令牌。签发逻辑和有效期不变。

2. 历史窗口只留一条遍历查询,删掉重复和死代码

  • ListHistoryMessagesAfterWithinBytes 与 ListTurnResponseSourcesAfterWithinBytes 是同一段递归遍历的两份拷贝。合成一条只返回 id 和 truncated 的 ListHistoryWindow;完整行与 turn-response 投影各自按 id 取(turn-response 仍不传完整 metadata)。
  • MeasureHistoryMessagesAfter 只给 Discuss 打一行日志,窗口自带的 truncated 已经回答"有没有被截断",一并删除。
  • 删掉没有生产调用方的消息服务方法和查询(List、ListSince、ListBefore、ListActiveSinceBySession、ReplaceTurn、GetVisibleHistoryTurnRequestMessageIDByTurn 等),以及只测这些死代码的测试。

3. 每轮只读一次历史

  • 记忆召回不再自己读历史:原生轮次从 loadChatHistory 第一次加载的原始用户行(只读拷贝)或 pipeline 已合成的消息生成查询,压缩后重读也沿用第一次的查询;先确认 bot 配了记忆提供方再生成,没配就不读。记忆消息照旧追加在历史之后,已编排好的历史不变。外部 runtime 没有可复用的历史,仍自己读一次,但只读最近 4k token。
  • continuation 不再手写"加载 → 截断 → 压缩 → 重读"和半套过滤链,改走 loadChatHistory。
  • ResolveRunConfig 不再调用 prepareRunConfig:它在消息填入前跑一次、continuation 随后又跑一次,提示词 Hook 触发两次。现在 continuation 只在消息就绪后跑一次;执行审批工具只需要工具,不再构建系统提示词;Discuss 保持原样。

4. 窗口遍历每一步只取键(#697)

每一步原来在 LATERAL 子查询里就算 octet_length(content::text)。规划器只要不沿有序索引走、改成"扫一遍再排序取一行",每一步都会把窗口里每一行解开转成文本,毫秒级变成十几秒。现在每一步只取主键和排序键,取到那一行后按主键量它的内容;返回的行不变。

5. 其他收紧

Discuss 消息转 SDK 只保留一份(contextview.DiscussMessageToSDK);checkpoint 没有摘要时不查摘要表;去掉不会触发的防御判断(记忆消息的工具修剪、ContextLifecycle / 时区的 nil 判断、空 RC 的重复判断、准入上限的重复兜底)和只被测试调用的 injectImagePartsIntoLastUserMessage;合并重复测试。

行为变化

  • continuation 也会去掉被 Hook 拒绝的输入和空失败行,并按本轮上下文预算判断截断(原来按绝对上限加载、按本轮预算压缩)。
  • 外部 runtime 的记忆查询只读最近 4k token 的历史,原来按绝对上限(约 800 KB)。
  • 执行审批工具不再触发提示词 Hook。

不在这个 PR 里

  • 让 Discuss 在历史被截断时像 chat 一样先压缩:试过后撤回。turn-response 窗口的字节预算也算入用户行,而 Discuss 的用户侧来自时间线,truncated 会在 Discuss 什么都没丢时触发(TestPostgresPreTurnCompaction 因此失败)。
  • 梳理中发现的疑似问题:被 Hook 拒绝后重试时,ensureRequiredHistoryMessage 可能把被拒的输入加回来,和新的 query 一起发给模型。尚未用测试确认。
  • 生产上为什么会选到退化计划仍未确认,需要在生产对慢会话跑一次普通 EXPLAIN;第 4 点让退化计划也不再爆炸。

Related Issues

Refs felinics/Memoh-Cloud#685
Refs felinics/Memoh-Cloud#697

Validation

测试(当前 head,基于 main 524b73439)

  • go test ./internal/agent/... ./internal/channel/... ./internal/chat/... ./internal/contextview/... ./internal/db/... 通过。
  • 本地 PostgreSQL 18(按本分支迁移建库)上跑 TestPostgres*:internal/chat/...、internal/agent/application、cmd/agent(含 Discuss 与 continuation 端到端)、internal/agent/context/compaction 全部通过。
  • 历史窗口的对拍测试(与参考 SQL 逐行比较)和计划守卫(沿索引走、访问行数受限)改为验证 ListHistoryWindow,均通过。
  • golangci-lint 对改动的包没有新增告警。

窗口查询耗时(本地 PostgreSQL 18;合成会话 3,384 行、9.7 MB,预算 800 KB,取 272 行)

正常计划 禁用索引扫描
main 4.6 ms 6.3 s
本 PR(窗口 + 按 id 取行) 3.5 ms + 按 id 取行约 1–2 ms 0.21 s

合成数据上的结果,没有在生产数据和 Cloud 的 RLS 上测过。

Screenshots / Recordings

尚未完成。开发环境和真实 UI 验证还没有做:本地的隔离开发环境在与其他工作负载共用 Docker 时被清掉,需要重建后再验证 chat、Discuss(Telegram)、continuation 三条路径并补截图。因此这是 draft,验证完成前不应合并。

Human QA

  • Human QA passed

🤖 Generated with Claude Code

chen-ran and others added 6 commits November 2, 2026 07:08
Channel 在收到消息时签发 SessionToken / ChatToken(5 分钟有效),存进
Discuss worker 的长寿命配置,等历史加载、排队和重组之后才拿去 StartTurn。
间隔超过 5 分钟,Agent 校验凭据失败,普通入轮报 "invalid session
credential for resume"(Memoh-Cloud #685)。

DiscussSessionConfig 改为携带 Credentials:绑定唤醒会话的那条入站消息的
身份,worker 在每次 runner.Run 之前调用它签发新令牌。签发逻辑和有效期不变,
bot 所有者在签发时解析。

Refs felinics/Memoh-Cloud#685

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ListHistoryMessagesAfterWithinBytes 与 ListTurnResponseSourcesAfterWithinBytes
  是同一段递归遍历的两份拷贝,只是投影不同。合成一条只返回 id 和 truncated 的
  ListHistoryWindow;完整行和 turn-response 投影各自用已有的按 id 查询取,
  turn-response 仍不传完整 metadata。
- MeasureHistoryMessagesAfter 只给 Discuss 打一行日志,窗口自己的 truncated
  已经回答"有没有被截断",删掉查询、MeasureHistory 和 ActiveMessagesMeasure。
- 删掉没有生产调用方的消息服务方法和查询:List、ListSince、ListBefore、
  ListActiveSinceBySession、ReplaceTurn、GetVisibleHistoryTurnRequestMessageIDByTurn,
  以及 ListMessagesSince、ListMessagesBefore、ListMessagesBeforeMessageBySession、
  ListActiveMessagesSinceBySession。只测这些死代码的测试一并删除,两条遍历的
  对拍测试改为直接验证这一条查询。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 记忆召回不再自己读历史:原生轮次从 loadChatHistory 第一次加载的原始用户行
  (只读拷贝)或 pipeline 已合成的消息生成查询;压缩后重读也沿用第一次的查询。
  先确认 bot 配了记忆提供方再生成查询,没配就完全不读。记忆消息照旧追加在历史
  之后。外部 runtime 没有可复用的历史,仍自己读一次,但只读最近 4k token。
- continuation 不再手写"加载 → 截断 → 压缩 → 重读"和半套过滤链,改走
  loadChatHistory(显式传入它自己的 scope)。从此 continuation 也会去掉被 Hook
  拒绝的输入和空失败行,并按本轮上下文预算判断截断。
- ResolveRunConfig 不再调用 prepareRunConfig:它在消息填入之前跑,continuation
  随后又跑一遍,提示词 Hook 触发两次。continuation 只在消息就绪后跑一次;
  执行审批工具只需要工具,不再构建系统提示词;Discuss 保持原样。
- 删掉 resolve 里不会触发的防御:对记忆消息的工具修剪、ContextLifecycle 和
  时区的 nil 判断。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Discuss 消息转 SDK 只保留一份实现(contextview.DiscussMessageToSDK),
  collector 与 native Discuss 共用。
- checkpoint 没有摘要时不再查询摘要表。
- 去掉 worker 对空 RC 的重复判断、driver 对准入上限的重复兜底,以及只被测试
  调用的 injectImagePartsIntoLastUserMessage。
- 删除重复测试:WS 版"截断后先压缩"(与 StreamChat 版走同一条加载路径)、
  手工复刻过滤链的 TestHistoryRecordPathPreservesLegacyServiceMessagePipeline、
  与 TestDedupePersistedCurrentUserMessageUsesHistoryRecordProvenance 重复的
  去重测试,以及随之无人使用的辅助函数。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ListHistoryWindow 每一步原来在 LATERAL 子查询里就算 octet_length(content::text)。
规划器只要不沿有序索引走、改成"扫一遍再排序取一行",每一步都会把窗口里每一行
解开转成文本,同样的数据从毫秒级变成十几秒(Memoh-Cloud #697 的生产慢样本、
#1710 开发环境的 2×8 s 都是这个量级)。

现在每一步只取主键和排序键,取到那一行后按主键量它的内容。返回的行与原来逐行
一致;合成会话(3,384 行 / 9.7 MB,预算 800 KB)上正常计划 4.6 ms → 3.5 ms,
禁用索引扫描时 6.3 s → 0.21 s。

Refs felinics/Memoh-Cloud#697

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added bug Fixes a bug or unexpected behavior change:server Changes backend code, configuration, or API contracts size:L PR size uses the larger of added or deleted lines, excluding generated files labels Nov 1, 2026
@chen-ran
chen-ran marked this pull request as ready for review November 2, 2026 05:59

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Fixes a bug or unexpected behavior change:server Changes backend code, configuration, or API contracts size:L PR size uses the larger of added or deleted lines, excluding generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant