Goose 上下文压缩机制解析:compaction 提示词、结构化摘要与超窗重试策略
当 Goose 会话触及模型上下文窗口上限时,系统并不会直接报错终止,而是通过 goose-context-management crate 将整段对话历史压缩(compaction)为一条结构化摘要消息,让会话可以跨越上下文窗口继续工作。本篇以该 crate 内嵌的核心提示词 compaction.md 为主体,完整拆解其任务定义、摘要 JSON Schema 与输出规则,并结合 summarize.rs、structured.rs、templates.rs 等源码,说明这条提示词在 Goose 压缩流水线中的真实调用位置、容错解析与超窗降级策略。
compaction.md:压缩提示词的任务设定
compaction.md 是压缩流程的“第一提示词”,它由 templates.rs 中的 include_dir! 宏在编译期打包进二进制(static PROMPTS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/prompts")),运行时由 builtin_template(COMPACTION_TEMPLATE) 取出。其内容可拆为三部分:
任务上下文(Task Context),原文完整保留了以下设定:
- An llm context limit was reached when a user was in a working session with an agent (you)
- Distill the conversation below into a structured summary with only the most verbose parts removed
- Include user requests, your responses, all technical content, and as much of the original context as possible
- This will be used to let the user continue the working session
- The summary will be read by an agent (you) on a next exchange to allow for continuation of the session
要点在于“只删最啰嗦的部分,而不是重写”——摘要的第一读者是 Agent 本身(“you”),而非人类用户,因此它可以保留远比给人看的摘要更多的原始细节。
对话历史注入点:模板中的 {{ messages }} 占位符由 summarize.rs 填充——SummarizeContext { messages } 结构体携带序列化后的历史文本,经 minijinja 渲染(render(&templates.compaction, &context))后作为系统提示词发出,而用户侧消息只有一句固定的 SUMMARIZE_REQUEST_TEXT:"Please summarize the conversation history provided in the system prompt."。
推理草稿区(scratchpad):提示词要求模型先用 <analysis> 标签写时间线复盘(用户目标、方法、关键决策、文件、错误与修复),并明确警告"analysis 会被丢弃,所以保持简短,它只是清单而非细节的存放地"。这个设计与后文的 JSON 提取逻辑强耦合——structured.rs 的 json_candidates 正是以 </analysis> 终止符为锚点向后搜索摘要 JSON。
摘要 JSON Schema 与九个字段的完整语义
提示词要求:在 </analysis> 之后只输出一个 ```json 代码块,严格匹配以下 schema(此处为 compaction.md 原文的完整保留):
{
"user_intent": ["every user goal and request, most important first"],
"technical_concepts": ["all discussed tools, methods, and concepts"],
"files": [
{
"path": "path of a file that was viewed or edited",
"summary": "what was done to it and why",
"key_code": "important code, signatures, or diffs from this file (omit if none)"
}
],
"errors_and_fixes": ["bugs hit, their resolutions, and user-driven changes"],
"problem_solving": ["issues solved or in progress, and key decisions: what was chosen, what was rejected, and why"],
"user_messages": ["all user messages, truncating long tool call arguments or results"],
"pending_tasks": ["all unresolved user requests, most important first"],
"current_work": "active work at summary request time: filenames, code, alignment to latest instruction",
"next_step": "include only if it directly continues a user instruction, otherwise omit"
}
各字段的设计意图(从 structured.rs 的 StructuredSummary 定义与注释可以印证):
| 字段 | 类型 | 设计意图 |
|---|---|---|
user_intent |
字符串列表 | 每个用户目标,最重要的在前——“最重要的在前”意味着消费端可以安全地从尾部截断 |
technical_concepts |
字符串列表 | 讨论过的所有工具、方法与概念 |
files |
FileActivity 对象列表 |
唯一允许嵌套对象的字段:路径、做了什么及为什么、关键代码/签名/diff |
errors_and_fixes |
字符串列表 | 遇到的 bug 与解决方式;提示词要求逐字引用错误信息、panic 文本与失败测试输出 |
problem_solving |
字符串列表 | 已解决/进行中的问题与关键决策:选了什么、拒绝了什么、为什么 |
user_messages |
字符串列表 | 全部用户消息,可截断冗长的工具调用参数与结果 |
pending_tasks |
字符串列表 | 未完成的请求,最重要的在前 |
current_work |
可选字符串 | 发起压缩时的活跃工作:文件名、代码、与最新指令的对应关系 |
next_step |
可选字符串 | 仅在直接延续用户指令时包含,否则省略 |
提示词还给出了一组硬性规则,值得完整引用:
<analysis>块是会被丢弃的草稿纸:只有 JSON 存活,因此 JSON 必须自包含,重复分析中所有对续作有意义的细节;- 每个列表按重要性从高到低排序;
- 除
files(对象数组)外,所有列表条目必须是纯字符串而非嵌套对象; errors_and_fixes中逐字引用错误消息、panic 文本与失败测试输出——包括数字、标识符与路径的精确字符串,不许意译;- 摘要只会被 Agent 自己读取,因此可以比给人看的摘要长得多:把整个长度预算花在 JSON 字段上,大量引用——完整输出块、完整代码片段、用户原话;
- 不要省略任何对继续会话可能重要的信息;
- 字段宁缺毋滥,不要为字段编造内容;
- 除非用户确认,不要引入新想法。
消息如何进入提示词:format_message_for_compacting
进入 {{ messages }} 之前,每条 Message 先经 format.rs 的 format_message_for_compacting 压成纯文本。从源码看,其映射规则是:
- 文本内容原样保留;图片渲染为
[image: {mime}];文档渲染为[document: name (mime)]; - 工具调用渲染为
tool_request({name}): {arguments_json}(参数序列化失败时回退为<<invalid json>>); - 工具结果渲染为
tool_response: {文本拼接},无文本内容时为tool_response: [non-text content]; - 权限确认、elicitation 等
action_required状态、系统通知与错误消息各自有对应的文本形式; Thinking与RedactedThinking内容被直接丢弃(MessageContent::Thinking(_) => None)——推理链对续作会话没有可复用价值,去掉可显著节省 token。
最终每条消息形如 [user]: ... 或 [assistant]: ...,空消息渲染为 [role]: <empty message>,全部以换行拼接后注入模板。
摘要的解析与容错:lenient 反序列化 + 无损失回退
模型并非总能严格遵守 schema。structured.rs 用两层防线保证"压缩永不丢信息":
第一层:宽松反序列化。 StructuredSummary 的每个字段都挂有自定义 deserializer:lenient_string_list、lenient_string、lenient_string_opt、lenient_file_list。注释解释了动机——"模型经常会扩充 schema(例如把 errors_and_fixes 写成 {"error": .., "fix": ..} 对象),单个字段如此不应导致整个好摘要被丢弃"。对象被拼接为 k: v; k: v 形式的字符串、数组被分号连接、数字转字符串;files 里如果模型"过度遵守纯字符串规则"输出了字符串,则降级为只有 path 的 FileActivity 而不是丢弃。未知顶层字段通过 #[serde(flatten)] extra 保留,以便用户自定义提示词新增的字段仍能到达自定义渲染模板。
第二层:JSON 提取策略。 json_candidates 按优先级生成候选序列:以每个 </analysis> 终止符(从后往前)为切点,其后每个 json 围栏(从后往前)再到起始对象;候选必须"紧贴标记且花括号配平"。源码注释解释了为什么不用简单的 `rfind`:摘要 JSON 本身可能引用 `</analysis>` 字符串(例如会话正在编辑压缩提示词的场景),这种候选只有在包含其后所有终止符出现时才被接受。提取用花括号配平而非围栏边界,是因为字符串值里可以合法地包含 ;而被截断的半成品 JSON 永不修复——修复会丢掉结尾的、对续作最关键的章节,而保留原始文本的回退不会。
兜底:raw-text 回退。 summarize.rs 中 apply_structured_summary 的注释直白写明:当模型没遵守结构化输出格式(不认 schema 的模型、用户自定义提示词)时,StructuredSummary::parse 返回 None,原始响应文本被原样保留作为摘要——即无损回退。测试 unusable_responses_fall_back_to_raw_text 覆盖了自由文体、空对象 {}、仅含未知字段、JSON 中途截断、JSON 仅被引用于散文、草稿区内嵌示例 JSON 等 9 种不可用响应,全部断言回退为 raw text。
压缩主流程:summarize 的超窗重试阶梯
summarize.rs 中的 summarize 是整条流水线的核心,返回 Summary { message, usage }。其流程:
- 构造请求:用户侧消息只含
SUMMARIZE_REQUEST_TEXT,全部历史经format_message_for_compacting序列化后渲染进系统提示词; - 超窗重试阶梯:定义
REMOVAL_PERCENTAGES: [u32; 5] = [0, 10, 20, 50, 100]。若模型返回ProviderError::ContextLengthExceeded(连摘要器自己都超窗),则依次移除 10%、20%、50%、100% 的工具响应后重试。filter_tool_responses的删除策略是从中间向两端交替摘除工具响应消息——"从上下文最不可能重要的中间位置开始删",且每轮至少删 1 条; - 失败出口:若历史中根本没有工具响应可删,第一次失败即快速返回,错误信息会给出可操作建议——"换用可用上下文更大的模型或配置、禁用部分 extension 以减小工具 schema 负载、或开新会话";若有工具响应但删完 100% 仍超窗,则报 "context limit exceeded even after removing all tool responses"。summarize.rs 的单元测试 用一个永远报
ContextLengthExceeded的OverflowingModel验证了这两条路径:无工具响应时只发 1 次请求即失败,有工具响应时恰好发 5 次(REMOVAL_PERCENTAGES.len())请求后耗尽。 - usage 归一化:压缩调用本身也消耗 token,
ensure_usage_tokens在响应被改写为更小的渲染摘要之前用TokenEstimator补齐 input/output tokens——注释强调 usage 必须反映"可计费的原始模型输出"。
成功后,摘要消息的 role 被强制改写为 User(response.role = Role::User),以便它以一条用户消息的身份接回原会话历史。
渲染层:compaction_summary.md 与 code_fence 过滤器
结构化摘要最终要变成一条人类可读、Agent 可续读的 Markdown。这一步由 compaction_summary.md 模板完成——它是 minijinja 模板(启用 trim_blocks/lstrip_blocks),按九个字段依次渲染 ## User Intent、## Technical Concepts、## Files + Code、## Errors + Fixes、## Problem Solving、## User Messages、## Pending Tasks、## Current Work、## Next Step 各节,空字段整节省略。
模板头部注释明确说明它是用户可覆盖的:把修改后的副本放到 ~/.config/goose/prompts/compaction_summary.md 即可实验压缩后上下文的内容(例如用 user_intent[:3] 只保留前三条最重要目标),无需重新构建 goose。模板中 key_code 字段经过 code_fence 过滤器处理,该过滤器(templates.rs 的 code_fence 函数)统计代码串内最长反引号连续段,生成比其长 1(且至少 3 个)反引号的围栏,防止嵌入的代码块"逃逸"破坏外层围栏。测试 render_fences_exceed_backtick_runs_in_key_code 验证了含 ```` 的 key_code 会被五反引号围栏包裹。
上层 API:compact、CompactingProvider 与压缩阈值
lib.rs 暴露三层 API,compaction.md 提示词贯穿始终:
summarize:最小层——给定模型与消息切片,产出一条摘要消息;compact:trait 层,供拥有自己会话表示的调用方使用。CompactionInput负责提供消息(可选覆盖Templates),CompactionOutput负责接收摘要与 usage;Vec<Message>已内置实现CompactionInput,简单场景无需包装类型。此 trait API 为 Rust 独有;CompactingProvider:provider.rs 中的Provider装饰器,声明manages_own_context() == true。它在stream和complete中拦截ProviderError::ContextLengthExceeded,自动调用summarize把整段历史压成一条摘要消息,然后用摘要替换原历史重试一次,而不是把错误抛给上层。
此外 README 说明了默认压缩阈值:DEFAULT_COMPACTION_THRESHOLD(0.8)是调用方开始压缩时上下文窗口的默认占比。Python 与 Kotlin 侧通过 goose-sdk 的 UniFFI 绑定访问压缩能力,Templates(compaction + summary 两段提示词文本)允许调用方整体替换内置提示词——这正是 compaction.md 与 compaction_summary.md 分离成两个独立字段的原因:前者控制"怎么总结",后者控制"总结之后长什么样"。
小结
Goose 的上下文压缩机制中,compaction.md 定义了"面向 Agent 自身"的信息保全型摘要契约:九字段 JSON、重要性排序、逐字引用错误、analysis 仅作草稿;而 structured.rs 的宽松解析与 raw-text 回退、summarize.rs 的 0/10/20/50/100% 工具响应重试阶梯、provider.rs 的超窗自动重试,共同保证这条提示词在模型不守格式或历史超长时依然不会丢失会话信息。阅读这两份提示词模板(以及 ~/.config/goose/prompts/compaction_summary.md 的用户覆盖位)是理解并定制 Goose 长会话延续行为的最直接入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00