Skip to content

perf(agent): 主回复只追加上下文,在上一轮实际发出的请求后面续接 - #1233

Merged
SuInk merged 3 commits into
mainfrom
claude/append-only-context
Oct 27, 2026
Merged

SuInk merged 3 commits into
mainfrom
claude/append-only-context

Conversation

@SuInk

@SuInk SuInk commented Oct 27, 2026

Copy link
Copy Markdown
Owner

主回复改成只追加:不再每轮重新组装请求,而是接在上一轮实际发给模型的请求后面继续发,只追加这期间的新群消息、这一轮完整的尾部和当前消息。默认开启,不设开关。同群连续 8 轮实测,deepseek-flash 命中率从 82.9% 升到 91.9%;gpt-6.1-sol 在早先的去重版本上从 47.4% 升到 90.9%。用 gpt-6.1-sol 回放 15 个轮次,没有出现回错消息。

Why

gpt、swe 主回复的缓存命中率只有 31%~55%,DeepSeek 是 85%。查下来不是切换账号造成的:同一账号、5 分钟内的连续请求,命中也只有约 21%。原因在上游的缓存方式。

经 sub2api 转发的 gpt(上游是 ChatGPT Codex)只在两个位置复用缓存:instructions 加工具定义的末尾,以及上一次完整请求的末尾。用一条 4.7 万 token 的生产请求实测:

新请求和上一次的关系 命中 token
完全相同 / 只在末尾追加两轮 46848
改动 input 第 0 条、80% 或 95% 位置的任意一处 18304

现在每轮都会重新组装请求:尾部每轮都在变,上一轮的当前消息也被改写成历史行,所以新请求从来不是上一条的纯追加。生产上同一会话的连续请求,每条都恰好只命中 18432。DeepSeek 能复用到任意 128 token 的前缀,但历史窗口一往前滑,命中照样会断。

pi-agent 和 DeepSeek Harness 都是只追加的单循环。DSH 遇到系统提示变化时也是把新版追加到已缓存的历史后面,而不是重写开头。

改了什么

  • model/agent/continuation.go 和 runner.go:
    • Request.Continuation 带上一轮的前缀、本轮要追加的消息和校验键。校验键是系统提示、历史前契约段和工具定义的指纹;对得上就在前缀后接着发,Runner 自己的逐轮消息照旧插在当前消息之前,对不上就按 Messages 完整组装。
    • Response.Transcript 是本轮最后一次实际发出的请求(经过 hooks.prepare,包含每步临时插入的工具执行状态),接上模型的回复,每个工具调用都配一条结果,作为下一轮的前缀。
  • model/assistant/append_context.go 和 runtime.go:每个(配置档, 会话)在内存里存一份记录。
    • 头部 system 已经在前缀里,不再追加。
    • 历史区只追加记录里还没有的群消息,机器人自己发过的回复跳过。摘要和媒体索引不再追加,因为续接时历史不滑动。
    • 尾部和当前消息每轮整段原样追加,不按行删掉上一轮出现过的说明,避免删过的尾部缺头少尾、规则离当前消息越来越远。
    • 以下情况会重新开一份记录:头部 system 变了、续接请求超过本轮完整组装的 2 倍、上一轮失败、进程重启。定时查询、事件触发这些后台入口不参与续接。
  • withToolResultReminder:往回找时遇到 user 消息就停。否则续接时会越过当前消息,找到上一轮 send 的结果,误加「刚执行过工具」的提醒。
  • 文档:docs/group-prompt-cache.md 新增「只追加上下文」一节。

影响

  • 所有主回复默认走只追加,后台没有开关。
  • 请求逐轮变长。用 10-27 生产三个群各 12 轮模拟,每轮多 1 万~1.2 万字,大约 6~8 轮碰到两倍上限重开一次,重开那一轮不命中。上面 8 轮实测中,DeepSeek 的总输入是现布局的 1.56 倍,未命中 token 少 26%。被缓存的 token 便宜,按生产数据拟合,每千个缓存 token 首字延迟只增加约 6 毫秒。
  • 模型的上下文里会留着以前的工具结果和回复,旧轮次的「【当前需要回复的消息】」标记也还在。回放中没有看到它因此回错消息。

验证

  • 缓存实测:10-27 生产同群连续 8 轮,尾部整段照发,两组各加随机前缀冷启动,不计第一轮:

    现布局 只追加
    deepseek-flash 命中率 82.9% 91.9%
    deepseek-flash 输入 token 31.5 万 49.1 万
    deepseek-flash 未命中 token 5.4 万 4.0 万

    gpt-6.1-sol 这次网关的账号全部处于限流状态,没能重测。早先尾部按行去重的版本测过:命中率 47.4% → 90.9%,未命中 token 16.1 万 → 4.9 万。

  • 行为回放(gpt-6.1-sol,尾部整段照发):3 个群各取第 3~7 轮,共 15 个轮次,现布局和只追加各生成一次真实回复,逐条对照当前消息。15 轮回的都是当前消息,动作一致:要表情包的 3 轮都调了 sticker,查资料的那轮两边都去搜索,没有重复上一轮的话。

  • 单元测试:

    • TestRunnerContinuationIsPureAppend:第二轮请求逐条以第一轮记录开头。
    • TestRunnerContinuationFallsBackOnKeyMismatch:校验键对不上时退回完整组装。
    • TestAppendContextAppendsNewHistoryAndWholeTail:只追加新历史,尾部整段保留,头部不重复。
    • TestAppendContextRestartsWhenHeadChangesOrGrowsTooLong。
    • TestReplyTurnsContinueAppendOnly:整条回复链路连发两轮,第二次请求以第一次请求加 send 开头,当前消息在最后。
  • gofmt -l、git diff --check 干净;./model/agent、./model/assistant、./webui 全包测试通过。

SuInk added 3 commits October 27, 2026 23:54
经 sub2api 的 gpt 只在 instructions+工具末尾和上一次完整请求末尾复用缓存,逐轮重新组装的请求只能命中开头约 1.8 万 token。续接后同群连续 8 轮实测 gpt-6.1-sol 47.4% 升到 90.9%,deepseek-flash 72.2% 升到 91.2%;按配置档开关,默认关。
只追加不再做成可选项,主回复默认在上一轮实际发出的请求后面续接。尾部每轮整段照发,避免删过的说明读起来缺头少尾;顺带修掉没有群历史时头部 system 被重复追加的问题(之前被按行去重盖住了)。
去掉尾部去重后重测了同群连续 8 轮,文档里的表换成这次的数字,gpt 只有去重版本的数据,单独注明。
@SuInk
SuInk merged commit 0487b02 into main Oct 27, 2026
14 checks passed
@SuInk
SuInk deleted the claude/append-only-context branch October 27, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant