会话摘要(Summary)
概述
随着对话持续增长,维护完整的事件历史可能会占用大量内存,并可能超出 LLM 的上下文窗口限制。会话摘要功能使用 LLM 自动将历史对话压缩为简洁的摘要,在保留重要上下文的同时显著降低内存占用和 token 消耗。
核心特性
- 自动触发:在执行摘要检查时,根据事件数量、token 数量或时间阈值自动生成摘要
- 增量处理:只处理自上次摘要以来的新事件,避免重复计算
- LLM 驱动:使用任何配置的 LLM 模型生成高质量、上下文感知的摘要
- 非破坏性:原始事件完整保留,摘要单独存储
- 异步处理:后台异步执行,不阻塞对话流程
- 灵活配置:支持自定义触发条件、提示词和字数限制
基础配置
步骤 1:创建摘要器
使用 LLM 模型创建摘要器并配置触发条件:
步骤 2:配置会话服务
将摘要器集成到会话服务(内存或 Redis):
WithAsyncSummaryNum 只控制后台异步摘要 worker 的并发数,不是同步/异步模式开关,也不是关闭摘要功能的开关。如果不需要生成摘要,不配置 WithSummarizer 即可;如果需要在同一次 Run 的长 ReAct loop 中让下一次 LLM 调用前立刻看到最新摘要,应在 Agent 侧使用 llmagent.WithSyncSummaryIntraRun(true)。
步骤 3:配置 Agent 和 Runner
创建 Agent 并配置摘要注入行为:
主示例保持默认异步摘要路径。只有在长 ReAct loop 需要同一次 Run 内下一次 LLM 调用前立刻看到最新摘要时,才显式添加同步 intra-run 选项:
完成以上配置后,摘要功能即可自动运行。
Cache-Safe 摘要 Forking
摘要器有两种请求构造模式。
独立摘要请求 是默认模式。框架会先选出需要摘要的 events,把它们转换成
conversation text;如果配置了 WithPreSummaryHook(...),还会先执行这个
hook。随后摘要模型收到的请求由下面两部分组成:
- 可选的 system message,来自
WithSystemPrompt(...)。 - 一条 user message,来自
WithPrompt(...);其中{conversation_text}会被替换为提取出的对话文本。自定义 prompt 还可以使用{previous_summary},把上一版滚动摘要与本次新增的对话事件分别放置。
这条请求和主 agent 的请求相互独立,因此同步摘要、异步摘要、手动调用摘要接口 都能使用。
如果长会话场景对 prompt cache 命中率比较敏感,可以显式开启 cache-safe forking:
在普通 LLM flow 里触发 context compaction 时,框架已经构造好了当前主 agent
调用的父 model.Request。开启 WithCacheSafeForking(true) 后,摘要请求会按
下面的方式构造:
- 克隆这个父请求,保留它对模型可见的 prefix,包括 system context、已注入的 summary、session history、用户输入、工具定义、headers、extra fields 和 generation settings。
- 在末尾追加一条 user message,内容来自
WithCacheSafeForkPrompt(...)。 - 强制摘要调用使用非流式输出,并清掉 structured output,因为摘要调用只需要返回普通摘要文本。
这样摘要请求和父请求拥有相同的前缀,支持 prompt cache 的模型服务就能复用更多 已缓存输入。如果当前没有父请求,例如手动或外部调用摘要接口,摘要器会自动 回退到独立摘要请求。
无论最终使用哪种请求,发送前都会按摘要模型的有效输入预算做准入检查:如果模型
能够提供 provider-specific input budget,框架会取它与“模型 context window 的
70%”这层保守上限中的较小值。fork 请求超预算时,框架只修改 clone,不会污染父
请求:先移除摘要调用不会使用的 tool schemas,再按完整 source round 从旧到新
缩减并保护最新一轮,必要时替换较大的 tool arguments/results payload。如果仍然
放不下,再重建为 bounded standalone 请求。standalone fallback 会对
{conversation_text} 和 {previous_summary} 两块 payload 做首尾保留截断,
固定的 system prompt 和 user prompt 模板不会被截坏。
预算适配和 fork → standalone 的选择发生在 BeforeModel callback 之前,因此
callback 看到并修改的就是最终准备送模的请求。callback 返回后框架会再次计数;
如果 callback 自己把请求扩到超预算,会明确失败,而不是再次换请求并静默丢失
callback 的修改。如果 provider 仍返回 context-length error,或者非 custom 的
模型调用返回空 summary,摘要器会用第一次输入预算的一半再做一次 bounded
standalone 重试。
这里有一个重要的 branch 摘要行为:开启 WithCacheSafeForking(true) 后,非空
branch 触发摘要时,可以用当前父请求 fork 来生成 branch 摘要;但同一轮 summary
pass 不会再跑级联出来的全量会话摘要。框架会直接跳过这个全量摘要目标,而不是
回退到独立的全量摘要 prompt,也不会复用这个 branch 视角的 fork request。如果
需要覆盖所有 branch 的全量摘要,需要单独触发一次全量会话摘要。
Prompt 规则:
WithPrompt(...)配置独立摘要请求的 user prompt,必须包含{conversation_text},并可选包含{previous_summary}。使用该可选占位符时,{previous_summary}是上一版滚动摘要,{conversation_text}只包含摘要边界后 新增的事件;不使用时,上一版摘要继续合并在{conversation_text}中以保持兼容。 如果配置了WithMaxSummaryWords(...),{max_summary_words}必须出现在WithPrompt(...)或WithSystemPrompt(...)其中之一。WithSystemPrompt(...)配置独立摘要请求里可选的 system message,不能包含{conversation_text}或{previous_summary},可以包含{max_summary_words}。WithCacheSafeForkPrompt(...)只配置 fork 模式下追加的 user message,不能 包含{conversation_text}或{previous_summary},因为克隆出来的父请求里已经有 对话内容和已注入的摘要;它可以包含{max_summary_words}。
即使开启了 cache-safe forking,也要保持独立摘要 prompt 有效,因为 fallback 路径仍然会使用它。自定义 fork prompt 时,建议明确要求模型“总结上面的对话, 供后续继续对话使用”,并保留用户目标、决策、约束、未完成事项、工具结果和重要 事实;同时要求模型不要调用工具、不要直接回答最新用户请求,也不要把 system 和 tool-use 指令当作事实写进摘要。
WithPreSummaryHook(...) 仍然会在摘要模型调用前执行。独立摘要模式下,hook
修改后的文本会渲染进 {conversation_text}。当 prompt 使用
{previous_summary} 时,hook 的 Events 和 Text 是本次新增对话,
PreviousSummary 则是可以单独修改的上一版摘要。如果 fork 模式拿到了父请求,
这些 payload 修改不会再被塞进摘要请求,因为对话内容已经在克隆的父请求里。
此时 hook 仍可用于更新 context、做副作用处理,以及服务 fallback 到独立摘要请求的场景。
在 fork 模式下,WithPreSummaryHook(...) 对 text 或 events 的修改不会对克隆
出来的父请求做脱敏、redaction 或 filtering。如果这个 hook 用于在摘要前做脱敏
或过滤,请让这类流程使用独立摘要模式,或确保父 model.Request 在被克隆前已经
完成脱敏。
Cache-safe forking 控制的是“生成摘要那次请求”的构造方式。摘要已经生成以后, 下一次普通对话请求如果也希望更利于 prompt cache,建议把摘要注入为 user message,而不是合并进 system prompt:
摘要 + 渐进式披露
当摘要注入和 prompt 侧的上下文压缩一起工作时,旧细节可能不再直接出现在 模型可见的请求里。如果你希望 Agent 只在需要时再把这些细节取回来,可以启用 会话历史的渐进式披露。
启用条件与行为:
WithEnableOnDemandSession(true)会按后端能力暴露按需 session 工具: 后端实现session.SearchableService时暴露session_search,实现session.WindowService时暴露session_load。后端可以只支持其中一个, 也可以同时支持两者。session/pgvector同时支持语义发现和精确加载。普通 session 后端只要实现了WindowService,即使没有语义session_search,也可以暴露精确的session_load恢复能力。current_hidden会严格搜索当前 session 中、位于summary:last_included_ts之前的历史内容。summary:last_included_ts是摘要中记录的last_included_ts时间戳,表示该摘要覆盖到的最后一个事件时间。current_session会搜索整个当前 session,不受 summary cutoff 限制。 当请求投影或 context compaction 把当前 session 的细节裁掉时,这个 scope 最有用。other_sessions会搜索同一<appName, userID>下的其他 session。all_sessions会合并current_hidden和other_sessions。
当前可召回的内容:
- 用户消息和助手消息。
- 历史 tool result,包括那些因为上下文压缩而没有直接出现在 prompt 里的工具输出。
当前不会索引的内容:
- 原始 tool call 请求本身不会被索引。
- partial event 不会被索引。
推荐使用方式:
- 先让模型基于当前可见 prompt、summary 和最近历史正常回答。
- 如果
session_search可用且缺少旧细节,再先调用session_search。 - 当已经有
event_id且需要周边原始历史或精确 tool result 时,调用session_load;这同样适用于没有语义搜索能力的后端。 - 取回的内容应视为历史上下文,而不是当前轮的主动指令。
迁移提示:早期版本只有在 session_search 和 session_load 同时存在时,
才认为按需 session 能力可用。现在工具面按能力分别暴露,因此 search-only
集成可以只暴露 session_search,load-only 集成可以只暴露 session_load。
SessionSummarizer 接口
上下文感知的摘要检查
已发布的 SessionSummarizer 接口保持不变。
如果摘要触发条件依赖请求上下文,可以直接使用 ContextChecker
以及带 context 的检查选项:
框架本身不会为摘要触发方式预留 context key。如果业务需要区分不同的
摘要入口,可以在调用 session API 之前自行往 ctx 写入标记,并在
ContextChecker 中读取。
动态摘要器
当会话服务需要复用,但摘要模型、提示词或检查条件需要按请求变化时,可以
使用 NewDynamicSummarizer。这适合多租户系统、自定义模型路由等场景。
对于 MySQL 等数据库会话服务,建议保持 session service 长生命周期复用,
从而复用底层连接池,而不是为了更换摘要器按请求新建 service。
请求执行前,把本次摘要配置放到 ctx:
对于同一个 ctx 和 session,resolver 应尽量保持轻量且确定。非强制摘要
场景下,它可能在摘要检查阶段调用一次,在实际生成摘要阶段再调用一次。如果
构造摘要器成本较高,可以把已构造好的摘要器放到 ctx,resolver 只负责读取。
resolver 返回 nil 会跳过自动摘要检查;如果直接调用 Summarize,或在没有
解析到真实摘要器时强制摘要,会返回错误。如果 resolver 在
ShouldSummarizeWithContext 执行自动、非强制摘要检查时返回错误,gate 会将其
当作 false 并跳过摘要生成;直接调用 Summarize 时会把 resolver 错误返回给调用方。
摘要器选项
触发条件
| 选项 | 说明 |
|---|---|
WithEventThreshold(eventCount int) |
当自上次摘要后的事件数量超过阈值时触发 |
WithTokenThreshold(tokenCount int) |
当自上次摘要后的 token 数量超过阈值时触发 |
WithContextThreshold(opts ...ContextThresholdOption) |
当自上次摘要后的 token 数量超过当前模型 context window 的指定比例时触发 |
WithTimeThreshold(interval time.Duration) |
Runner 路径按当前顶层 request 到来前的空闲间隔判断;standalone 调用保留“最后事件距现在多久”的兼容行为 |
如果你希望使用固定的业务阈值,例如“不管当前使用什么模型,只要新增
4000 token 就摘要”,使用 WithTokenThreshold。这个阈值会固化在摘要器配置里,
应用切换模型时不会自动变化。
如果摘要触发条件应该跟随当前模型的 context window,使用
WithContextThreshold。对于会在同一 session 中切换模型的 agent,这是更推荐的配置。
每次摘要检查时,框架会按以下顺序解析 context window:
- 单次运行覆盖值:
agent.WithModelContextWindow(tokens) - 模型实例配置:例如
openai.WithContextWindow(tokens)或provider.WithContextWindow(tokens) - 进程级模型名注册表:
model.RegisterModelContextWindow(name, tokens)
然后按 contextWindow * ratio 计算阈值(默认 50%)。为了避免在极短上下文中
过早摘要,WithContextThreshold 默认还会施加 2000 token 的最小触发阈值。
也就是说,实际阈值是 max(contextWindow * ratio, minTokenThreshold),且内置
checker 只有在估算 token 数大于阈值时才触发。如果把 ratio 设置得很小,
例如 0.001,但希望 1000 token 左右就开始摘要,需要显式传入
summary.WithContextThresholdMinTokens(0),或设置成业务希望的最小值。
触发和调用上报
如果需要观察“为什么触发 summary”以及“summary 模型请求实际用了多少 token”,
可以配置 summary.WithReportHook:
Report 会把两个 token 口径拆开:
report.Trigger.Value:触发 checker 使用的值,例如上次 summary 之后增量事件的估算 token 数report.Call.EstimatedPromptTokens:框架在发起 summary 模型请求前,对完整请求做的本地估算report.Call.PromptTokens:summary 模型返回的官方usage.prompt_tokens
开启 cache-safe forking 时,report.Call.Mode 为 cache_safe_fork,请求估算值来自 fork
后的父请求加上追加的 summary 指令。普通独立 summary prompt 模式下,mode 为 standalone。
如果 BeforeModel callback 返回 custom response,实际没有发送 summary 模型请求,mode 为
custom_response,prompt 估算值保持为 0。
高级集成如果要在高层 summary 流程前放入同一个 report,可以使用
summary.ContextWithReport(ctx, report),需要从 context 取出时使用
summary.ReportFromContext(ctx)。单一路径会复用这个 report;cascade 并行生成多个
summary 时,框架会给每个 worker 克隆一份 report,避免不同分支同时写同一个对象。
这些 fork 出来的 report 会通过各自调用的 hook 发出,不会再合并回 root report。
对于私有部署、endpoint ID、微调模型、新模型或多租户自定义模型配置,优先使用模型实例或单次运行 option, 避免不同用户覆盖同一个进程级注册表:
只有当模型名在当前进程中有稳定的全局含义时,才建议使用全局注册:
常用 ContextThresholdOption:
| 选项 | 说明 |
|---|---|
WithContextThresholdRatio(ratio float64) |
设置 context window 的触发比例,默认 0.5 |
WithContextThresholdMinTokens(tokens int) |
设置绝对最小触发 token 数,默认 2000;传入 0 可以取消这层下限 |
WithContextThresholdFallbackWindow(tokens int) |
设置 summary checker 的兜底 context window,默认 8192。在 WithContextThreshold 路径中,不传这个选项时,框架会尽量从 summarizer model 推导 fallback;显式传入后会使用你的值,并跳过 summarizer model fallback。实际检查时,只有当运行上下文、模型实例和注册表都无法解析 context window 时才使用该 fallback;这不同于 token tailoring 对未知模型使用的 128000 fallback |
组合条件
| 选项 | 说明 |
|---|---|
WithChecksAll(checks ...Checker) |
要求所有条件都满足(AND 逻辑),使用 Check* 函数 |
WithChecksAny(checks ...Checker) |
任何条件满足即触发(OR 逻辑),使用 Check* 函数 |
WithChecksAllContext(checks ...ContextChecker) |
要求所有带请求上下文的条件都满足(AND 逻辑) |
WithChecksAnyContext(checks ...ContextChecker) |
任一带请求上下文的条件满足即触发(OR 逻辑) |
ContextChecker 的签名为 (ctx context.Context, sess *session.Session)。
注意:在 WithChecksAll 和 WithChecksAny 中使用 Check* 函数(如 CheckEventThreshold),而不是 With* 函数。
摘要生成
| 选项 | 说明 |
|---|---|
WithMaxSummaryWords(maxWords int) |
限制摘要的最大字数,包含在提示词中指导模型生成 |
WithPrompt(prompt string) |
自定义摘要提示词,必须包含 {conversation_text},可选包含 {previous_summary} |
WithSystemPrompt(prompt string) |
为摘要额外添加独立的 system message 指令;不能包含 {conversation_text} 或 {previous_summary} |
WithCacheSafeForking(enable bool) |
在有父请求可用时,启用 cache-safe 摘要请求 forking。默认关闭 |
WithCacheSafeForkPrompt(prompt string) |
自定义 cache-safe fork 模式下追加的压缩 user message。可包含 {max_summary_words},但不能包含 {conversation_text} 或 {previous_summary} |
WithSkipRecent(skipFunc SkipRecentFunc) |
自定义函数跳过最近事件 |
Hook 选项
| 选项 | 说明 |
|---|---|
WithPreSummaryHook(h PreSummaryHook) |
摘要前的 Hook,可修改输入文本 |
WithPostSummaryHook(h PostSummaryHook) |
摘要后的 Hook,可修改输出摘要 |
WithSummaryHookAbortOnError(abort bool) |
Hook 报错时是否中断,默认 false(忽略错误) |
工具调用格式化
默认情况下,摘要器会将工具调用和工具结果包含在发送给 LLM 进行总结的对话文本中。默认格式为:
- 工具调用:
[Called tool: toolName with args: {"arg": "value"}] - 工具结果:
[toolName returned: result content]
| 选项 | 说明 |
|---|---|
WithToolCallFormatter(f ToolCallFormatter) |
自定义工具调用在摘要输入中的格式。返回空字符串可排除该工具调用 |
WithToolResultFormatter(f ToolResultFormatter) |
自定义工具结果在摘要输入中的格式。返回空字符串可排除该结果 |
模型回调(Before/After Model)
summarizer 在调用底层 model.GenerateContent 前后支持模型回调,可用于修改请求、短路返回自定义响应、或在摘要请求上做埋点。
| 选项 | 说明 |
|---|---|
WithModelCallbacks(callbacks *model.Callbacks) |
为摘要器的底层模型调用注册 Before/After 回调 |
Checker 函数
Checker 是用于判断是否需要触发摘要的函数类型:
内置 Checker
| Checker | 说明 |
|---|---|
CheckEventThreshold(eventCount int) |
当自上次摘要以来的增量事件数大于阈值时返回 true |
CheckTimeThreshold(interval time.Duration) |
Runner 摘要路径检查当前顶层 request 到来前的空闲间隔;没有 Runner observation 的直接调用保留 last-event-age 行为 |
CheckTokenThreshold(tokenCount int) |
当自上次摘要以来的增量事件提取的对话文本估算 token 数大于阈值时返回 true(通过 TokenCounter 估算,而非 event.Response.Usage.TotalTokens) |
ChecksAll(checks []Checker) |
组合多个 Checker,所有都返回 true 时才返回 true(AND) |
ChecksAny(checks []Checker) |
组合多个 Checker,任一返回 true 时返回 true(OR) |
自定义提示词
Prompt 占位符:
{conversation_text}:必须包含,会被对话内容替换{previous_summary}:可选,用于把上一版滚动摘要与摘要边界后的新增事件分开; 第一次摘要时为空。不使用该占位符时,上一版摘要仍会合并进{conversation_text},保持原有行为{max_summary_words}:当maxSummaryWords > 0时,必须包含在WithPrompt(...)或WithSystemPrompt(...)其中之一
如果希望在增量摘要中单独放置上一版摘要,可以这样写:
{previous_summary} 适用于 standalone 请求和 cache-safe fallback 请求。
cache-safe fork 成功时会直接使用克隆的父请求,摘要在父请求中的位置不会由该占位符改变。
如果希望把摘要指令放到独立的 system message,可以组合使用
WithSystemPrompt 和一个更轻量的 user prompt:
说明:
WithPrompt仍然渲染到 user messageWithSystemPrompt会渲染到独立的 system messageWithSystemPrompt不能包含{conversation_text}或{previous_summary};对话内容必须保留在 user prompt 中
Token 计数器配置
默认情况下,CheckTokenThreshold 使用内置的 SimpleTokenCounter 基于文本长度估算 token 数量。如果需要自定义 token 计数行为,可以使用 summary.SetTokenCounter 设置全局 token 计数器:
SimpleTokenCounter 的 WithApproxRunesPerToken(v) 表示约 v 个 UTF-8 字符对应 1 个 token,估算公式是 estimatedTokens = countedUTF8Runes / v。例如 v=1.5 表示约 1.5 字符/token;不要把它当成 token 乘数。
Token 估算取舍
内置
SimpleTokenCounter只按 UTF-8 字符数做轻量估算,默认4.0字符/token 更接近英文文本的经验值。中文、日文、韩文以及中英文混合内容通常会偏离这个比例, 建议结合业务压测或线上观测按模型和语料校准,例如把WithApproxRunesPerToken调整到更保守的1.2到2.0区间。框架没有默认调用模型厂商的精确 token API:很多模型的 tokenizer 并未开源, tokenizer 也会随模型版本演进;如果在每次摘要检查中远程调用 token 统计接口, 会引入额外延迟、成本和限流风险,而且不同 provider 的接口能力也不一致。 因此 summary checker 使用可替换的本地估算器作为快速 gate。如果业务需要更高精度, 建议实现
model.TokenCounter,并在应用初始化时通过summary.SetTokenCounter统一设置。
注意:
- 全局影响:
SetTokenCounter会影响当前进程中所有的CheckTokenThreshold评估,建议在应用初始化时一次性设置 - 默认计数器:如果不设置,将使用默认的
SimpleTokenCounter(约每 token 对应 4 个字符) - 参数语义:
WithApproxRunesPerToken(v)中的v是字符/token。传入2.0/3.0表示约0.67字符/token,等价于约1.5token/字符
跳过最近事件
使用 WithSkipRecent 可以在摘要时跳过最近的事件:
摘要 Hook
PreSummaryHook
在摘要生成前调用,可以修改输入文本或事件:
PostSummaryHook
在摘要生成后调用,可以修改输出摘要:
使用示例
摘要触发机制
自动触发(推荐)
Runner 在每次对话完成后自动检查触发条件,满足条件时在后台异步生成摘要。
如果启用了 WithSyncSummaryIntraRun(true),Flow 会在同一次 Run 的 LLM 迭代之间同步调用 CreateSessionSummary(...),确保下一次 LLM 调用前可以使用最新摘要。中间 tool result 的冗余异步入队会被跳过;最终 assistant response 仍然可以入队一个摘要任务,用于刷新本轮结束后的摘要。在 async worker 可用且队列有容量时,该任务会在后台执行;如果没有配置 async worker 或队列已满,EnqueueSummaryJob 可能 fallback 到同步摘要创建,因此这不是严格的非阻塞保证。同步路径与异步 worker 共享同一套 boundary/delta 判断和进程内 session/filterKey 级别的串行控制,在单进程内通常可以避免对同一批事件重复发起昂贵的 LLM 摘要,但它不是跨实例分布式锁。
触发时机:
- 事件数量超过阈值(
WithEventThreshold) - Token 数量超过阈值(
WithTokenThreshold) - Token 数量超过当前模型 context window 的指定比例(
WithContextThreshold) - 当前顶层 request 到来前的空闲间隔超过阈值(Runner 路径中的
WithTimeThreshold) - 满足自定义组合条件(
WithChecksAny/WithChecksAll)
WithTimeThreshold 不是后台定时器。Runner 自动路径会在顶层 request 到达时固定记录时间,并将它与同一摘要 scope 中的上一条相关事件比较。例如,5*time.Minute 表示:“下一次顶层 request 在该 scope 静默超过 5 分钟后到达时,由这次 request 引发的摘要检查可以触发。”模型响应耗时和异步 worker 排队时间不会计入 gap。没有 Runner request observation 的直接 checker 或摘要 API 调用保留原有的 last-event-age 行为。
同轮同步摘要(长 ReAct loop)
默认自动触发路径是异步的:Runner 追加符合条件的完整响应事件后(例如
tool result 或最终 assistant response)入队 summary job,由后台 worker
再按触发条件决定是否生成摘要。user message、tool-call response、无效内容、
SkipSummarization 事件,以及 sync summary 下的中间 tool result 不会入队异步
summary job。这个模式能保护主链路延迟,但在同一次 Run 包含多轮 LLM/tool
迭代时,后台摘要可能来不及在下一次 LLM 调用前完成。
如果你的 agent 经常在同一次 Run 内连续调用工具,并且工具结果可能快速推高
上下文长度,可以开启同轮同步摘要:
开启后,Flow 会在同一次 Run 的 LLM loop 之间同步执行一次摘要检查。它调用
CreateSessionSummary(..., force=false),因此不会绕过摘要器配置的事件数、token
数、时间或 context window 阈值;只有条件满足时才会真正生成摘要。配合
WithAddSessionSummary(true) 时,下一次 LLM 请求会注入刚刷新的 summary;对于普通
已完成历史,通常只拼接 summary 边界之后的增量事件。但在同一次 Run 内,请求组装仍
可能保留或压缩边界前必要的 tool-call/tool-result 消息,以维持当前 ReAct 工具链的合法
结构。
为避免重复工作,开启同轮同步摘要后,中间 tool result 事件会跳过冗余的异步
summary 入队;最终 assistant response 仍然可以触发异步 job,让本轮结束后的
session summary 保持最新。这个选项不会替代跨 Run 的默认异步摘要行为。
同轮同步摘要会把一次可能的 summary LLM 调用放进主链路,适合长 ReAct loop、coding agent、连续大工具输出或接近 context window 的场景;一般在线问答和低延迟场景仍建议 优先使用默认异步摘要。
手动触发
某些场景下,你可能需要手动触发摘要:
API 说明:
-
EnqueueSummaryJob:异步摘要(推荐)- 后台处理,不阻塞当前操作
- 失败时自动回退到同步处理
- 适合生产环境
CreateSessionSummary:同步摘要- 立即处理,会阻塞当前操作
- 直接返回处理结果
- 适合调试或需要立即获取结果的场景
参数说明:
- filterKey:
session.SummaryFilterKeyAllContents表示对完整会话生成摘要 - force 参数:
false:遵守配置的触发条件(事件数、token 数、时间阈值等),只有满足条件才生成摘要true:强制生成摘要,完全忽略所有触发条件检查,无论会话状态如何都会执行
使用场景:
| 场景 | 推荐 API | force 参数 |
|---|---|---|
| 正常对话流程 | 自动触发(无需调用) | - |
| 后台批量处理 | EnqueueSummaryJob |
false |
| 用户主动请求 | EnqueueSummaryJob |
true |
| 调试/测试 | CreateSessionSummary |
true |
| 会话结束时 | EnqueueSummaryJob |
true |
上下文注入机制
框架提供两种模式来管理发送给 LLM 的对话上下文:
在选择模式前,先区分三类上下文减载机制:
| 机制 | 所在层 | 改动对象 | 典型用途 |
|---|---|---|---|
| Summary | Session Service + prompt assembly | 用 LLM 将历史事件生成可持久化摘要;开启 WithAddSessionSummary(true) 后,请求中注入摘要,并只拼接摘要时间点之后的增量事件 |
长会话保留语义连续性,减少反复发送完整历史 |
| Context compaction | Agent prompt assembly | 不调用 LLM,不删除整轮消息;只在请求投影阶段改写 tool result 内容,例如旧结果替换为占位符、超大结果首尾保留截断 |
工具输出很长,但希望尽量保留对话结构和当前轮工具链路 |
| Token tailoring | Model provider | 模型调用前按 token budget 删除或保留消息轮次,默认策略会尽量保留系统消息和最新轮次,但最终仍受可用预算约束 | 最后一层兜底,保证请求落入模型 context window |
正常调用链路大致是:先由 agent 组装 prompt;如果启用了
WithAddSessionSummary(true),则注入 summary;随后按需压缩 tool result。
如果开启了摘要注入且压完后仍接近 context window,会在 LLM 调用前同步尝试
刷新一次 summary 并重建请求;最后模型层的 token tailoring 再按预算裁剪
消息列表。也就是说,context compaction 和 token tailoring 都能减少 prompt
体积,但前者缩小消息内部的工具输出,后者删减消息轮次;summary 则是用新的
语义摘要替代一段历史。
模式 1:启用摘要注入(推荐)
工作方式:
- 会话摘要合并到已有的系统消息中(如果存在),否则作为新的系统消息插入到开头
- 这确保了与要求单条系统消息位于开头的模型兼容(如 Qwen3.5 系列)
- 包含摘要时间点之后的所有增量事件。如果同步 intra-run summary 在当前 invocation 内推进了 boundary,重建请求时还会保留当前 user message,以及 cutoff 前最新一个完整 tool round 作为有界 resume tail
- 通过浓缩历史、cutoff 后事件和当前 invocation 的有界 resume tail 保持语义连续;更早且已被覆盖的 tool rounds 只由 summary 表达
WithMaxHistoryRuns参数被忽略
摘要注入模式
默认情况下,摘要以 system message 的方式注入(合并到已有 system prompt 中)。这种方式下,摘要会被 token tailoring 的 preserved head 保护,不会被滑动窗口裁剪掉。
如果希望摘要能参与 token 预算裁剪,形成真正的滑动窗口效果,可以将注入模式切换为 user:
两种注入模式的区别:
| 模式 | 注入位置 | Token Tailoring 行为 | 适用场景 |
|---|---|---|---|
SessionSummaryInjectionSystem(默认) |
合并到 system message | 摘要在 preserved head 中,不会被裁剪 | 需要摘要始终存在的场景 |
SessionSummaryInjectionUser |
优先合并到第一条 user history/current message;否则在靠近 history 的位置注入 | 摘要参与普通轮次裁剪,可被滑动窗口淘汰;也更利于保持稳定 system 前缀 | 超长对话、prompt cache 敏感场景 |
Memory preload 和 session recall preload 有各自独立的 placement 设置。为了兼容 已有用户,它们默认仍注入 system context,因此会留在 token tailoring 的 preserved head 中。如果 cache-sensitive 场景希望把它们也放到 user/history 路径,需要显式 opt in:
User placement 能让稳定 system 前缀更利于 prompt cache,但 preloaded memory 和 recalled session events 会参与 token tailoring,可能被裁剪。
User 模式的消息结构:
当 history 第一条消息为 user role 时,摘要会自动合并进去:
当 history 第一条消息不是 user role 时,摘要作为独立 user message 插入:
注意事项:
- User 模式下,processor 会优先把摘要合并到第一条 user history/current message,让摘要贴近当前生效的 user 轮次
- 如果没有可合并的 user history/current message,但 prompt 前缀最后一条已经是 user message(例如 injected context),则会回退合并到那条 user message,避免额外再插入一条相邻的 user block
- User 模式使用更中性的默认文案("Context from previous interactions"),避免以系统指令的语气出现在 user role 中
- 自定义的
WithSummaryFormatter同样对 user 模式生效 - 摘要的生成链路不受影响——注入模式只影响 prompt assembly 层,不影响 summarizer 本身
提示:如果你的场景是超长对话(数百轮),且希望旧摘要能被自然淘汰(被新的摘要替代),建议使用
SessionSummaryInjectionUser模式。
Context Compaction 细节
Context compaction 不是 summary 的同义词,也不是 token tailoring。它只处理
tool result 这种容易异常膨胀的内容,不会把普通 user/assistant 消息做
LLM 摘要,也不会像 token tailoring 那样直接丢弃完整消息轮次。
命名说明:
WithEnableContextCompaction(true)中的 "compaction" 指 prompt-side tool result compaction/pruning;如果需要语义摘要,仍然由WithAddSessionSummary(true)和会话摘要器负责。
当开启 WithEnableContextCompaction(true) 时,框架会在真正调用模型前按配置执行下面几类 tool result 压缩:
Pass 0 — 按工具名强制占位替换(ForceCleanToolNames,默认未配置):
- 只作用于命中
ForceCleanToolNames的历史tool result,不需要超过ContextCompactionToolResultMaxTokens阈值 - 当前 request、recent 保护集合中的 request/invocation 不受影响;
KeepToolNames优先级更高 - 适合清理 shell、grep、日志抓取等高噪声工具的历史输出
Pass 1 — 历史 tool result 占位替换(ContextCompactionToolResultMaxTokens,默认 1024 tokens):
- 只作用于旧 request 中超过阈值的
tool result,将其内容整体替换为简短占位符,但保留ToolID和ToolName - current/recent 保护集合不受影响;这个集合包含当前 request、最近
ContextCompactionKeepRecentRequests个已完成 request,以及ToolResultCompactionConfig.SkipRecentFunc返回的尾部 event 所属 request/invocation SkipRecentFunc与ContextCompactionKeepRecentRequests是并集关系;如果希望完全由自定义 recent 逻辑控制,可将ContextCompactionKeepRecentRequests设为0- 适合清理已不重要的历史工具输出
Pass 2 — 超大 tool result 截断(ContextCompactionOversizedToolResultMaxTokens,默认 0 / 关闭):
- 作用于几乎所有 tool result,包括当前 request 的;
session_load自身返回的恢复结果会被跳过,避免恢复片段再次被压缩 - 超过阈值的 tool result 会使用首尾保留策略截断:保留内容的开头和结尾,中间插入
[...N characters truncated...]标记 - 这是防止单个超大 tool result 直接撑爆 context window 的安全网(例如
web_fetch返回 800K+ 字符的 HTML)
几类压缩的定位不同:Pass 0 是显式工具名策略;Pass 1 低阈值、全量替换,激进清理旧历史;Pass 2 高阈值、只在极端情况触发,也可能作用于当前 request。
同步 intra-run summary 还有一条专门的请求投影规则。如果新 summary boundary 覆盖到了当前 invocation 的 events,那么普通已覆盖历史以 boundary 为硬边界, 但重建后的主 agent 请求仍会保留:
- 当前 invocation 的 user message。
- cutoff 前最新一个完整 tool round;并行 tool calls 及其匹配 results 会整批保留。
- cutoff 后的全部增量 events。
框架只恢复这一个最新的完整 tool round;更早的已覆盖 tool rounds 只由 summary
表达。这个小型 resume tail 能避免主模型把已完成的工具步骤误判为“尚未执行”,
进而重复有副作用的调用。开启 context compaction 后,恢复出来的每个 tool-call
arguments payload,以及每个未被 keep 规则保护的 tool result,都会分别与
ContextCompactionToolResultMaxTokens 比较;只替换单项超限的内容,并保留
tool ID、名称和 call/result 配对。关闭 context compaction 时,框架不会改写这些
payload。如果 cutoff 正好落在 tool call 和 result 中间,原有的配对修复仍会补齐
协议结构,但不会带回无关的已覆盖历史。
Pass 2 默认是关闭的(0),需要满足两个条件才会生效:(1) WithEnableContextCompaction(true) 总开关已打开;(2) ContextCompactionOversizedToolResultMaxTokens > 0(推荐显式传入 8192,可读取常量 processor.DefaultContextCompactionOversizedToolResultMaxTokens)。这样 EnableContextCompaction=false 在语义上始终等于"框架不会修改任何 tool result"。
如果需要按工具名控制行为,可以使用 WithToolResultCompactionConfig(...):
ForceCleanToolNames:这些 tool 的历史结果在 context compaction 开启、且 current/recent 保护生效之后会通过 Pass 0 直接替换为策略占位符,适合 shell、grep、日志抓取等高噪声工具KeepToolNames:这些 tool 的结果不会被 context compaction 清理,适合session_load、session_search这类模型可能需要逐字读取的恢复工具SkipRecentFunc:自定义尾部多少个 event 视为 recent,和ContextCompactionKeepRecentRequests一起组成 recent 保护集合,影响 Pass 0 强制清理和 Pass 1 的"历史"判定;Pass 2 仍会处理 recent/current 中的超大 tool result
如果同一个 tool name 同时出现在 ForceCleanToolNames 和 KeepToolNames 中,KeepToolNames 优先。
当 Pass 1 占位符或 Pass 2 截断标记对应的事件有 event_id 时,会携带 event_id、tool_call_id、tool_name 等恢复线索;Pass 0 的策略占位符不携带这些恢复线索。开启 WithEnableOnDemandSession(true) 且后端实现 session.WindowService 后,模型可以调用 session_load,用 content_offset / content_limit 精确加载原始 tool result 的小片段。session_load 的返回大小由它自己的窗口参数和 content_limit 控制;读取超大结果时建议分片加载,而不是一次请求全文。
此外:
- 如果同时开启了
WithAddSessionSummary(true),并且压完后请求仍接近 context window,会在 LLM 调用前同步执行一次CreateSessionSummary(...)并重建 request - 模型层的 token tailoring 仍然作为最后兜底。它按消息轮次裁剪,因此恢复片段应保持足够小,避免在最后的模型请求中被整体挤出
- Context compaction 默认使用
SimpleTokenCounter估算 token。如果业务使用了针对中文 或特定 provider 的自定义 counter,建议同时通过WithContextCompactionTokenCounter(...)传入同一个 counter,让 Pass 1 判断和 Pass 2 截断与模型层 token tailoring 使用一致的估算口径。
完整示例见
examples/context_compaction。
该示例会调用真实模型,默认通过 -debug=true 打印每次实际发送给模型的
request,用来检查历史大 tool result 是否按预期被替换为占位符。
上下文结构:
模型兼容性:
部分 LLM 提供商对系统消息的位置和数量有严格要求:
- Qwen3.5 系列等模型要求系统消息必须位于对话开头,且不支持多条系统消息
- 默认的合并行为可避免
System message must be at the beginning等错误 - 预加载的内存内容也会通过相同机制合并到系统消息中
模式 2:不使用摘要
工作方式:
- 不添加摘要消息
- 只包含最近
MaxHistoryRuns轮对话 MaxHistoryRuns=0时不限制,包含所有历史- 如果开启
WithEnableContextCompaction(true),保留下来的旧 request 中超长tool result会在 request projection 阶段被压缩(Pass 1);如果同时显式设置WithContextCompactionOversizedToolResultMaxTokens(8192)(或其他正值),任意 request 中的超大 tool result 会被首尾保留截断(Pass 2)。两者都需要EnableContextCompaction=true总开关 - 这个模式下不会触发 pre-LLM 的同步摘要重试
上下文结构:
模式选择建议
| 场景 | 推荐配置 | 说明 |
|---|---|---|
| 长期会话(客服、助手) | AddSessionSummary=true |
保持完整上下文,优化 token |
| 短期会话(单次咨询) | AddSessionSummary=falseMaxHistoryRuns=10 |
简单直接,无需摘要开销 |
| 调试测试 | AddSessionSummary=falseMaxHistoryRuns=5 |
快速验证,减少干扰 |
| 高并发场景 | AddSessionSummary=true增加 worker 数量 |
异步处理,不影响响应速度 |
如果你的长会话里经常出现搜索结果、日志、代码扫描输出这类长 tool result,建议开启 EnableContextCompaction=true。如果你还希望在接近 context window 时多一次同步摘要兜底,再配合 AddSessionSummary=true 一起使用。
提示:如果你的 agent 使用了
web_fetch等可能单次返回超大结果的工具,ContextCompactionOversizedToolResultMaxTokens尤为重要——它能防止单个 tool result 吃光整个 context window,即使该 result 属于当前正在处理的(受保护的)request。它默认关闭,需要显式开启WithEnableContextCompaction(true)并设置一个正阈值(推荐8192)才会生效。
摘要格式自定义
默认情况下,会话摘要会以包含上下文标签和关于优先考虑当前对话信息的提示进行格式化:
默认格式:
您可以使用 WithSummaryFormatter 来自定义摘要格式:
使用场景:
- 简化格式:使用简洁的标题和最少的上下文提示来减少 token 消耗
- 语言本地化:将上下文提示翻译为目标语言
- 角色特定格式:为不同的 Agent 角色提供不同的格式
- 模型优化:根据特定模型的偏好调整格式
获取摘要
Filter Key 支持:
- 不提供选项时,返回全量会话摘要(
SummaryFilterKeyAllContents) - 提供特定 filter key 但未找到时,回退到全量会话摘要
- 如果都不存在,兜底返回任意可用的摘要
按事件类型生成摘要
在实际应用中,你可能希望为不同类型的事件生成独立的摘要。
使用 AppendEventHook 设置 FilterKey
FilterKey 前缀规范
⚠️ 重要:FilterKey 必须添加 appName + "/" 前缀。
原因:Runner 在过滤事件时使用 appName + "/" 作为过滤前缀,如果 FilterKey 没有这个前缀,事件会被过滤掉。
为不同类型生成摘要
限制摘要目标
默认情况下,当某个非空分支 FilterKey 触发摘要时,session service 会同时
刷新该分支摘要和全量会话摘要(SummaryFilterKeyAllContents)。如果某些分支
不需要摘要,可以通过 allowlist 控制范围,并按需关闭全量摘要级联:
行为说明:
WithSummaryFilterAllowlist(...)只控制非空分支摘要目标,不会阻止session.SummaryFilterKeyAllContents这个全量摘要目标。WithCascadeFullSessionSummary(...)控制非空分支触发摘要时,是否同时刷新 全量会话摘要。- 开启
WithCacheSafeForking(true)后,如果当前有父请求可 fork,branch 触发的 summary pass 只会生成 branch 摘要;级联出来的全量会话摘要目标会被跳过,不会 回退到独立的全量摘要 prompt,也不会复用这个 branch 视角的 fork request。如果 确实需要覆盖所有 branch 的全量摘要,请单独触发一次全量会话摘要。 - 如果只想保留 branch 触发出来的全量摘要,不写任何 branch 摘要,可以显式传入 空 allowlist,并保持默认 cascade 开启:
mysql.WithSummaryFilterAllowlist("")和mysql.WithSummaryFilterAllowlist()都表示“不允许任何 branch key”; 在默认 cascade 行为下,仍然会刷新全量会话摘要。- 如果同时设置
mysql.WithCascadeFullSessionSummary(false),非空 branch 触发时 就没有任何摘要目标,因此不会生成摘要。 - allowlist 使用带分隔符的层级匹配,不是原始字符串前缀匹配。框架内部会先给
两边补上 filter key 分隔符(
"/"),再判断两者是否处于同一条祖先/子孙 层级路径上。 - 例子:
- 放行
my-app/tool时,会匹配my-app/tool和my-app/tool/search。 - 放行
my-app/tool/search时,也会匹配my-app/tool。 - 放行
my-app/tool时,不会匹配my-app/toolbox。 - 放行
my-app/tool时,不会匹配other-app/tool。
- 放行
- 即使配置了 allowlist,
session.SummaryFilterKeyAllContents仍然可以被直接 用于生成全量会话摘要。 - 不配置 allowlist 时会保持兼容行为,所有分支
FilterKey都可以触发摘要。 - 显式传入空 allowlist 会阻止 branch 摘要目标;如果 cascade 开启,branch 触发时仍会刷新全量会话摘要。
工作原理
- 增量处理:摘要器跟踪每个会话的上次摘要时间,后续运行只处理上次摘要后发生的事件
- 增量摘要:新事件与先前的摘要组合,生成一个既包含旧上下文又包含新信息的更新摘要
- 触发条件评估:在生成摘要之前,评估配置的触发条件。如果条件未满足且
force=false,则跳过摘要 - 异步 Worker:摘要任务使用基于哈希的分发策略分配到多个 worker goroutine,确保同一会话的任务按顺序处理
- 回退机制:如果异步入队失败(队列已满、上下文取消或 worker 未初始化),系统会自动回退到同步处理
最佳实践
- 选择合适的阈值:如果 agent 运行时可能切换模型,优先使用
WithContextThreshold;如果你明确需要固定 token 预算,再使用WithTokenThreshold。对于自定义模型或租户提供的模型,优先使用模型级WithContextWindow或单次运行的agent.WithModelContextWindow;只有稳定的进程级模型名才使用全局注册 - 使用异步处理:在生产环境中始终使用
EnqueueSummaryJob而不是CreateSessionSummary,以避免阻塞对话流程 - 监控队列大小:如果频繁看到"queue is full"警告,请增加
WithSummaryQueueSize或WithAsyncSummaryNum - 自定义提示词:根据应用需求定制摘要提示词。例如,如果你正在构建客户支持 Agent,应关注关键问题和解决方案
- 平衡字数限制:设置
WithMaxSummaryWords以在保留上下文和减少 token 使用之间取得平衡。典型值范围为 100-300 字 - 测试触发条件:尝试不同的
WithChecksAny和WithChecksAll组合,找到摘要频率和成本之间的最佳平衡
性能考虑
- LLM 成本:每次摘要生成都会调用 LLM,监控触发条件以平衡成本和上下文保留
- 内存使用:摘要与事件一起存储,配置适当的 TTL 以管理长时间运行会话中的内存
- 异步 Worker:更多 worker 会提高吞吐量但消耗更多资源,从 2-4 个 worker 开始,根据负载进行扩展
- 队列容量:根据预期的并发量和摘要生成时间调整队列大小
完整示例
以下是演示所有组件如何协同工作的完整示例: