四级上下文压缩
按成本从低到高管理信息生命周期,让大结果落盘、旧消息降级、摘要可追溯。
先看它怎样跑起来
这一章不是几个孤立知识点,而是一条会产生结果的因果链。
四级上下文压缩
亮起的是当前动作,留下的是已经满足的前置条件。点击任意一步,可以从那里继续。
- 压缩必须按完整消息组进行,不能拆开 tool_call 与 tool_result。
- 便宜策略先执行,昂贵摘要后执行。
- 落盘路径是追溯原始结果的索引。
顺着原文把边界看清
01导读:问题背景与本章目标上一章解决了 Skill 按需加载的问题:Agent 启动时只看到技能目录,需要哪项能力,再通过 loadskill 把正文放进当前会话。⌄
上一章解决了 Skill 按需加载的问题:Agent 启动时只看到技能目录,需要哪项能力,再通过 load_skill 把正文放进当前会话。
但目录瘦下来,只解决了初始上下文。任务一旦开始,读文件、跑命令、调用工具,历史仍会不断增长。尤其是一轮里出现多个大工具结果时,几次调用就可能把真正有用的目标、约束和最新进展淹没。
加大上下文窗口只能推迟问题。真正要做的是管理信息的生命周期:大结果放到哪里,旧内容怎样降级,哪些消息绝不能拆开,以及压缩后如何追溯原始记录。
02核心原则:先便宜,后昂贵2. snipCompactHistory() 按完整消息组裁掉中段。⌄
当前第 8 章把压缩分成四层,并严格按成本从低到高执行:
- 大工具结果落盘,只把路径和有界预览留在历史里。
snipCompactHistory()按完整消息组裁掉中段。microCompactHistory()把较早工具组的结果替换为占位符。- 前三层之后仍超过阈值,才调用模型生成结构化摘要。
前三层不增加模型请求。第四层才通过同一个 ModelClient 发起一次不带工具的摘要请求。
这里还要说明一个时间点:压缩不是在一次请求内部修改上下文,而是在两次模型调用之间预处理 request history。System Prompt 与工具定义保持静态前缀;request history 中从替换点开始的缓存会失效,之前的前缀仍可复用。所以 P08 把大结果集中在整轮结束时落盘,把模型摘要推迟到超过字节阈值后,而不是每轮都做模型摘要。ai-agent-book 第 2 章把这称为“批量压缩比频繁压缩更有利于 KV Cache”。
实际实现集中在 [features/compaction.ts](code/chapters/ch08/src/features/compaction.ts),接线位于 [bootstrap.ts](code/chapters/ch08/src/bootstrap.ts) 和共享的 [core/loop.ts](code/chapters/ch08/src/core/loop.ts)。第 8 章入口 [chapters/ch08.ts](code/chapters/ch08/src/chapters/ch08.ts) 只选择固定的 P08 Profile,不复制 Agent Loop。
组合根创建同一个 CompactionManager,把它同时接到两个明确边界:historyProcessor 在模型请求前准备 request history;toolResultProcessor 在整轮工具执行完成后处理结果。P08 没有复制循环,也没有注册额外的 compact 工具。
03先分清三种历史和一种产物压缩最容易出错的地方,是把所有 messages 都当成同一份可变列表。当前实现明确区分四个对象:⌄
压缩最容易出错的地方,是把所有 messages 都当成同一份可变列表。当前实现明确区分四个对象:
| 对象 | 当前实现中的含义 | 会不会被请求压缩改写 |
|---|---|---|
| canonical history | AgentRunner.#history 保存的会话事实;RunResult.history 返回它的不可变快照 | 不会 |
| request history | 每次模型调用前,从 canonical history 复制出的请求快照,再交给 CompactionManager.prepare() | 会,但只影响这次及后续可复用的请求快照 |
| transcript | 进入模型摘要前保存的 canonical history JSONL 快照 | 不会;文件独占发布,冲突不覆盖 |
| tool-result artifact | 大工具结果的完整 UTF-8 内容 | 不会;canonical history 中保存的是路径和预览 |
这里有个看似细小、实际上非常关键的边界:大工具结果是在写入 canonical history 之前 处理的。所以 canonical history 里不会保留那段超大正文,而会保留 <persisted-tool-result> 引用。完整正文在独立 artifact 中。
而 snip、micro 和模型摘要都发生在请求快照上。它们可以让模型下一次看到更短的历史,却不会倒过来删除 AgentRunner.#history 中已经存在的会话消息。
当前请求路径可以概括为:
一整轮 ToolResult
-> 大结果落盘,生成路径与预览
-> 作为 tool messages 追加到 canonical history
-> 复制 request history
-> snip(完整消息组)
-> micro(完整工具组)
-> 规范 JSONL 的 UTF-8 bytes 是否超过 50,000?
-> 否:直接请求模型
-> 是:保存 canonical transcript,再摘要 request history04消息原子组:不能只看相邻两条消息OpenAI 的一条 assistant 消息可以同时包含多个 toolcalls。这时合法历史不是简单的“一条调用配一条结果”,而是一个不可拆分原子组:⌄
OpenAI 的一条 assistant 消息可以同时包含多个 tool_calls。这时合法历史不是简单的“一条调用配一条结果”,而是一个不可拆分原子组:
assistant(tool_calls=[call_a, call_b])
tool(tool_call_id=call_a)
tool(tool_call_id=call_b)当前内部的 validatedGroups() 先调用 validateToolPairing(),再把历史划成两类组:
- 普通消息单独构成一组。
- 一条带工具调用的 assistant 消息,加上它的全部 tool results,共同构成一个工具交换组。
snip 的保留边界、micro 的替换单位、响应式压缩保留的尾部,都使用这个组,而不是原始消息下标。因此一轮包含两个、三个甚至更多工具调用时,也不会留下孤儿 tool 消息,或缺失结果的 assistant 消息。
这项约束不是 Prompt 建议,而是运行时契约。压缩前后都会再次验证配对;非法历史会在写 transcript 或调用摘要模型之前失败。
05第一层:大工具结果按 UTF-8 bytes 落盘CompactionManager.compactToolResults() 接收的是一整轮已经执行完的 ToolResult。共享 Loop 会等这一轮所有 handler 和 PostToolUse 都完成后,再把整批结果交给它。随后,结果才按原 toolcallid 顺序写入历史。⌄
CompactionManager.compactToolResults() 接收的是一整轮已经执行完的 ToolResult。共享 Loop 会等这一轮所有 handler 和 PostToolUse 都完成后,再把整批结果交给它。随后,结果才按原 tool_call_id 顺序写入历史。
阈值计算使用真实 UTF-8 字节数:
const contentBytes = Buffer.from(result.content, "utf8");
const sizeBytes = contentBytes.byteLength;默认规则是:
- 单项结果大于
30_000bytes,直接落盘;恰好等于阈值仍保留在消息中。 - 未落盘结果的合计若仍大于
200_000bytes,就按原始字节数从大到小继续落盘,直到剩余结果进入预算。 - 完整内容写入
.agent_tutorial/artifacts/tool-result-<id>.txt。 - 消息中保留相对路径、
original_bytes、头部最多 2,000 bytes 和尾部最多 2,000 bytes 的预览。
注意,这里的 2,000 是 bytes,不是字符。UTF-8 中文通常占多个字节;如果边界刚好切在一个多字节字符中间,预览会丢弃那个不完整字符,所以实际显示的字符数可能更少。
产物先写临时文件并同步,再独占发布最终路径。ID 冲突不会覆盖旧文件;同一批写入中途失败时,已经创建的本批产物会清理。共享 Loop 会把整批结果改成对应 ID 的 tool_result_processing_error,从而继续满足“每个调用恰好一个结果”的消息契约。
06第二层:snip 按组裁掉中段默认超过 50 个消息组才触发 snipCompactHistory()。触发后保留:⌄
默认超过 50 个消息组才触发 snipCompactHistory()。触发后保留:
- 最前面的 3 个完整组;
- 一条
[Compacted: N message groups omitted]system 标记; - 最后面的 46 个完整组。
这里统计的是消息组,不是 ChatMessage 条数。一个包含两次工具调用的工具交换组实际占三条消息,但在 snip 预算里只算一组,也只能整组保留或整组省略。
旧示例通过检查截断点前后各一条消息来修补边界,这对同轮多个工具调用并不可靠。当前实现先建组再切片,边界天然不会落在 assistant 和它的任一 tool result 之间。
07第三层:micro 只降级旧工具结果microCompactHistory() 默认完整保留最近 3 个工具交换组。更早工具组中的所有结果内容都替换为:⌄
microCompactHistory() 默认完整保留最近 3 个工具交换组。更早工具组中的所有结果内容都替换为:
[Earlier tool result compacted. Re-run if needed.]它不会删除 assistant 的调用,也不会改动 tool_call_id。同一 assistant 里有多个调用时,这些结果一起替换,整个原子组仍然合法。
当前实现没有“结果超过 120 字符才替换”的分支。只要工具组属于最近 3 组之前,它的结果就进入 micro compact。这个规则更简单,也让缓存结果保持确定性。
snip 先执行,micro 后执行。两者都基于新的不可变数组快照生成 request history,不会原地修改 canonical history。
08第四层:超过 50,000 bytes 才调用摘要模型前三层结束后,historyUtf8Bytes() 会把请求快照转换为规范化 OpenAI JSONL,再计算编码后的 UTF-8 bytes。默认只有结果大于 50000 bytes 时,才进入模型摘要。⌄
前三层结束后,historyUtf8Bytes() 会把请求快照转换为规范化 OpenAI JSONL,再计算编码后的 UTF-8 bytes。默认只有结果大于 50_000 bytes 时,才进入模型摘要。
因此,这个阈值不是:
- JavaScript 字符串的
length; - 工具正文长度之和;
- 模型 tokenizer 给出的 token 数;
- 模型供应商承诺的上下文窗口。
它是当前实现选择的一条确定性字节预算,包含序列化后的 role、tool_calls、tool_call_id、JSON 标点和换行。它能稳定触发压缩,但不能保证 50,000 bytes 对所有模型都对应相同 token 数。
进入第四层时,顺序也很重要:
- 先把未经过 snip、micro 或摘要的 canonical 快照写成 transcript。
- 再让 summarizer 读取已经经过便宜层处理的 request history。
- 摘要模型请求不携带任何工具。
- 只接受
finishReason === "stop"、无toolCalls、非空且字段精确的 JSON object。 - 成功后,请求历史变成一条 system 消息,其中包含 transcript 路径和五类继续工作信息。
五类字段分别是 current_goal、key_findings、files_read_or_changed、remaining_work 和 user_constraints。如果 transcript 写入失败,摘要模型不会被调用。如果摘要失败,canonical history 仍不改变,已经成功保存的 transcript 会留下供排查。
自动 prepare() 摘要后不会额外拼接最近几条消息。保留最近完整组是响应式恢复 API 的单独行为,不能混为一谈。
09transcript 保存的到底是什么transcript 位于 .agenttutorial/artifacts/transcript-<id>.jsonl。每行是内部 ChatMessage 转成 OpenAI wire shape 后的规范 JSON,使用 UTF-8、保留中文,并以换行结尾。⌄
transcript 位于 .agent_tutorial/artifacts/transcript-<id>.jsonl。每行是内部 ChatMessage 转成 OpenAI wire shape 后的规范 JSON,使用 UTF-8、保留中文,并以换行结尾。
它保存的是模型摘要发生前的 canonical history 快照,而不是已经 snip/micro 的请求历史。因此可以还原“请求级压缩之前”的消息序列。
但 transcript 不是所有原始字节的总备份。超过工具结果阈值的正文早已在进入 canonical history 前写入单独的 tool-result artifact;transcript 记录的是这个 artifact 的路径和预览。要恢复完整现场,需要同时保留 transcript 和它引用的产物文件。
默认 ID 使用 UUID,文件通过独占发布创建。若生成器给出重复 ID,第二次写入明确失败,不会覆盖第一次 transcript。
实现里还有两个容易被忽略的细节:serializeTranscript() 使用键排序后的稳定 JSON,所以相同历史无论字段声明顺序如何都得到同一行文本。historiesEqual() 和 historyStartsWith() 才能可靠识别“完全相同”和“纯追加”两类缓存场景。writeExclusiveAtomic() 则先写临时文件并 sync(),再用硬链接独占发布最终路径;最终路径只在成功时出现,重复 ID 会以 EEXIST 失败而不是覆盖。目录侧由 ensureSafeDirectory() 在创建后再次 lstat()/realpath(),拒绝符号链接或指向 workspace 外的 artifact 目录。
10缓存:相同历史不重复压缩,只追加时复用前缀CompactionManager 在当前 Agent 实例内保存最近一次 #preparedSource 和 #preparedHistory:⌄
CompactionManager 在当前 Agent 实例内保存最近一次 #preparedSource 和 #preparedHistory:
- canonical 快照完全相同时,直接返回上一次准备好的请求历史,不再次 snip、micro、写 transcript 或调用摘要模型。
- 新 canonical history 只是给旧历史追加消息时,复用已经压缩的前缀,只把新增后缀接上,再重新执行有界检查。
- 历史不是纯追加关系时,从当前 canonical 快照重新准备。
如果追加内容让请求再次超过阈值,summarizer 看到的是“缓存后的压缩前缀 + 新增后缀”,避免反复把旧长历史送进摘要模型。这一次写出的 transcript 仍然来自完整的新 canonical history。
缓存只存在于这个 CompactionManager 实例中,不跨进程,也不持久化到磁盘。它优化请求准备成本,不是另一套会话事实来源。
11主动压缩 API 不是 compact 工具CompactionManager.compactProactively() 提供一个独立 API:调用者可以显式要求“先保存完整 transcript,再把给定历史变成结构化摘要”。聚焦测试覆盖了成功、摘要失败、写入失败和 ID 冲突。⌄
CompactionManager.compactProactively() 提供一个独立 API:调用者可以显式要求“先保存完整 transcript,再把给定历史变成结构化摘要”。聚焦测试覆盖了成功、摘要失败、写入失败和 ID 冲突。
但当前 P08 工具注册表没有 compact 工具,模型不能在对话里主动调用它。P08 相比 P07 新增的是运行时 ARTIFACTS/COMPACTION 能力,而不是一个新工具名。实际 Loop 接入的是每轮请求前自动执行的 prepare(),以及整批工具结果处理器。
因此,下面这种说法不符合当前实现:“模型觉得上下文快满时会调用 compact 工具”。目前能保证的是自动请求级压缩和一个可由外层代码调用的主动压缩 API。
12prompt-too-long:恢复原语已经有了,Loop 信号还没有compactOnPromptTooLong(history, { retryCount: 0 }) 是一个独立的响应式恢复原语。它会:⌄
compactOnPromptTooLong(history, { retryCount: 0 }) 是一个独立的响应式恢复原语。它会:
- 保存传入历史的完整 transcript;
- 生成结构化摘要;
- 在摘要后保留最近 5 个完整消息组;
- 当
retryCount > 0时抛出PromptTooLongRetryError,阻止同一恢复窗口再次压缩。
但是,当前 P08 AgentRunner 尚未接入供应商无关的“输入 prompt 过长”错误信号。也就是说,P08 Loop 不会自动调用这个方法。它需要未来的模型错误分类边界先确认“请求因为输入过长而被拒绝”,再把明确的重试计数交给恢复原语。
尤其不能把内部的 finishReason === "length" 当成 prompt-too-long:
| 信号 | 含义 | 当前 P08 行为 |
|---|---|---|
finishReason === "length" | 请求已经被模型接受,但输出达到生成上限,响应不完整 | AgentRunner 抛出 IncompleteModelReplyError,不做响应式压缩 |
| 输入 prompt-too-long | 请求的输入上下文被供应商拒绝 | P08 尚无统一 typed signal,也没有自动恢复接线 |
compactOnPromptTooLong(..., { retryCount: 0 }) | 调用者已经完成错误分类后可使用的恢复原语 | 单独可测;只允许当前恢复窗口的一次尝试 |
也不能靠异常字符串里是否包含 prompt_too_long 或 too many tokens 来冒充通用信号。不同 OpenAI 兼容端点的异常类型、状态码和响应体并不一致。错误分类应该在模型适配器或恢复层完成。
这一边界解释了为什么第 8 章测试能直接证明“第二次响应式压缩会被拒绝”,却不能据此宣称 P08 的真实 Agent Loop 已经会自动处理输入过长。
13与 Claude Code 的差异本章的上下文压缩是教学子集。Claude Code 的压缩体系同样遵循“先便宜,后昂贵”的原则,但在实现上有以下显著差异。官方文档见:https://code.claude.com/docs/en/context-window,https://code.claude.com/docs/en/commands,https://code.claude.com/docs/en/agent-sdk/hooks。参考仓库:learn-claude-code/s08contextcompact/README.md。⌄
本章的上下文压缩是教学子集。Claude Code 的压缩体系同样遵循“先便宜,后昂贵”的原则,但在实现上有以下显著差异。官方文档见:https://code.claude.com/docs/en/context-window,https://code.claude.com/docs/en/commands,https://code.claude.com/docs/en/agent-sdk/hooks。参考仓库:learn-claude-code/s08_context_compact/README.md。
- 精确度不同。Claude Code 使用精确 token 阈值(
contextWindow - maxOutputTokens - 13,000)。P08 使用 UTF-8 字节估算,省略了精确 tokenizer 的引入成本。 - 压缩排序不同。Claude Code 的五级管线为
budget → snip → micro → contextCollapse → auto(query.ts:379-468)。P08 没有contextCollapse层,执行顺序为budget → snip → micro → auto。 - contextCollapse 与 sessionMemoryCompact 不同。Claude Code 源码包含独立的
contextCollapse上下文管理系统(启用时抑制 proactive autocompact)和sessionMemoryCompact(compact 前先用 session memory 尝试轻量摘要)。P08 两者均不涉及。 - 摘要格式不同。Claude Code 的摘要 prompt 包含 9 个信息部分,模型输出使用
<analysis>/<summary>双标签,首尾双重防呆禁止调用工具。P08 使用严格 5 字段 JSON(current_goal、key_findings、files_read_or_changed、remaining_work、user_constraints),没有防呆标签。 - 主动压缩与钩子不同。Claude Code 内置
/compact命令和SnipTool工具,并且有完整的PreCompact/PostCompact钩子生命周期。P08 未将compactProactively()暴露为工具,CompactionManager不发射钩子事件。 - 后压缩恢复不同。Claude Code 在 compact 后自动重新读取最近修改的文件(最多 5 个文件、每个 5K token、总预算 50K token),并维护
readFileState避免重复读取未修改文件。P08 只保留摘要,不自动恢复文件内容。 - 响应式重试不同。Claude Code 有更精细的分级重试与模型错误分类。P08 的
compactOnPromptTooLong()是一个可测试的被动原语,但 P08 Loop 尚未接入供应商无关的 prompt-too-long 信号。
注意事项:本章不实现 contextCollapse 和 sessionMemoryCompact,不暴露 compact 工具,也不自动恢复文件内容。如果需要用摘要后的剩余 token 预算恢复最新读过的文件,需要在 CompactionManager 外层叠加状态恢复逻辑,但这不属于本章的压缩模块边界。教学版的 microCompactHistory() 对所有工具结果统一作占位符替换;Claude Code 则按工具类型(read_file 可恢复、其他工具则直接替换)有更细致的保留策略。
14从 ai-agent-book 学到什么:压缩不是省 token,而是整理知识ai-agent-book 第 2 章“上下文压缩策略”提出压缩有两个动机:第一是长度与成本约束,第二是思考质量。后者更容易被忽略。即使上下文窗口没满,原始信息散落在多轮工具结果里时,模型仍要从数万 token 中反复“检索”相关片段;当无关内容占到大头,关键信息会被稀释,这就是 Context Rot。压缩不是简单地把内容丢掉,而是把“需要模型现场归纳的原始记录”换成“可以直接检索的结构化结论”。⌄
ai-agent-book 第 2 章“上下文压缩策略”提出压缩有两个动机:第一是长度与成本约束,第二是思考质量。后者更容易被忽略。即使上下文窗口没满,原始信息散落在多轮工具结果里时,模型仍要从数万 token 中反复“检索”相关片段;当无关内容占到大头,关键信息会被稀释,这就是 Context Rot。压缩不是简单地把内容丢掉,而是把“需要模型现场归纳的原始记录”换成“可以直接检索的结构化结论”。
它的实验 2-9 用同一个研究任务对比六种策略,结果很值得对照:
| 策略 | 实测结果 | 暴露的问题 |
|---|---|---|
| 无压缩 | 7 次工具调用约 367,000 字符;第 5 次迭代约 165K token 超过 128K 预算 | 只靠大窗口不可行 |
| 个体摘要 | 压缩率 10.9%,12 次迭代,276,608 token | 页面重复信息碎片化 |
| 组合摘要 | 压缩率 4.3%,10 次迭代,93,449 token | 超长输入截断可能丢尾部 |
| 上下文感知摘要 | 压缩率约 3.0%,7 次迭代,40,157 token;一次把 147,877 字符压到 1,963 字符 | 需要把当前目标和已有结论放进摘要请求 |
| 带引用摘要 | 222,992 token | 可溯源,但引用元数据有明显成本 |
| 自适应窗口化 | 174,601 token;接近 80% 阈值时批量压缩全部未标记结果 | 前几轮保留原始信息,后期集中降级 |
对照后,P08 的设计取舍更清晰:
- 先便宜后昂贵是生产原则。
ai-agent-book把生产级压缩分成五层:工具结果预算控制、噪声删除、API 层微压缩、归档式摘要、全量压缩与熔断。P08 的四层是教学子集:落盘对应第一层,snip 近似删除中段,micro 用本地占位符近似 API 层微压缩,模型摘要作为最后手段。P08 没有实现服务端上下文编辑,也没有连续失败熔断器。 - 结构化摘要优于把原文留给模型自行归纳。 实验里上下文感知摘要的 token 与迭代次数都明显低于个体摘要。P08 的 5 字段 JSON 把
current_goal、key_findings、files_read_or_changed、remaining_work、user_constraints变成可校验状态。transcript 和 artifact 又保留原始现场,对应书中“有损压缩 + 无损索引”的取舍。 - 当前 P08 还不是上下文感知压缩。 书中最优策略会把当前查询意图和已有结论写进摘要 prompt。P08 的
SUMMARY_SYSTEM_PROMPT是固定 JSON schema,不接收当前 goal 参数。因此不能拿 40,157 token 这类目标指标宣称 P08 已达到相同效果。 - 标记让压缩结果可读,但不等于持久防重复机制。 书中自适应窗口化用
[COMPRESSED]标记防止同一批结果重复压缩。P08 的[Compacted: ...]和[Earlier tool result compacted...]让 request history 可检查,但缓存判断由historiesEqual()/historyStartsWith()完成,不是扫描消息正文。 - 保留优先级和 P08 五字段一致。 书建议保留架构决策、关键约束、已修改文件、验证状态和未解决 TODO。P08 的摘要字段虽然没有专门的“验证状态”,但
key_findings/remaining_work可承载 pass/fail 与回滚信息,canonical transcript 保证压缩前现场不丢。
| 实现 | 职责与边界 |
|---|---|
DEFAULT_* 常量 | 集中保存 30_000、200_000、2_000、3、5、50_000、50 等默认预算;构造时可注入测试小值 |
CompactionManager.prepare() | 模型请求前从 canonical snapshot 生成 request history:snip -> micro -> 超过 50_000 bytes 才摘要;只影响本次及后续可复用的请求快照 |
compactToolResults() | 整轮工具结果回填 canonical history 前按单项阈值和批次预算落盘,保留 error metadata,返回结果与 artifact 引用 |
validatedGroups() / validateToolPairing() | 先验证 assistant 工具调用与 tool results 的配对,再切成普通组和工具交换组;所有裁剪边界都基于组 |
snipCompactHistory() | 超过 50 个消息组时保留前 3 组、省略标记和后 46 组,整组省略 |
microCompactHistory() | 保留最近 3 个工具交换组,更早工具结果替换为占位符,不删除 assistant 调用和 tool_call_id |
historyUtf8Bytes() / serializeTranscript() | 把历史转成稳定 JSONL 后计算 UTF-8 bytes,作为摘要阈值和 transcript 的同一份规范化输出 |
ModelHistorySummarizer / parseCompactionSummary() | 发起无工具摘要请求,要求 stop 且输出精确 5 字段的非空 JSON |
historiesEqual() / historyStartsWith() | 用稳定 JSON 识别“完全相同”和“纯追加”,复用已压缩前缀,避免重复送模型 |
ensureSafeDirectory() / writeExclusiveAtomic() / removeCreatedArtifacts() | 复查真实目录、临时文件加硬链接独占发布、批内失败清理已创建产物 |
compactProactively() / compactOnPromptTooLong() | 外层显式压缩 API 与响应式恢复原语;后者摘要后保留最近 5 个完整组,并用 PromptTooLongRetryError 限制一次重试 |
AgentRunner.#processToolResults() | 结果处理器失败时把整批结果替换为 tool_result_processing_error,仍与原工具调用数一一对应 |
15运行第 8 章npm run ch08 -- --prompt "读取当前目录中的 Markdown 文件并总结关键信息"⌄
可以任选一个入口:
npm run ch08 -- --prompt "读取当前目录中的 Markdown 文件并总结关键信息"
npm run agent-tutorial -- run --chapter 8 --prompt "读取当前目录中的 Markdown 文件并总结关键信息"16当前边界这一章当前能直接保证的是:工具结果按真实 UTF-8 bytes 有界落盘;所有裁剪都保持 OpenAI 工具消息原子组;请求级压缩不改写 canonical history;模型摘要前保存可追溯 transcript;相同历史和纯追加历史可以复用缓存。⌄
这一章当前能直接保证的是:工具结果按真实 UTF-8 bytes 有界落盘;所有裁剪都保持 OpenAI 工具消息原子组;请求级压缩不改写 canonical history;模型摘要前保存可追溯 transcript;相同历史和纯追加历史可以复用缓存。
它没有承诺精确 token 计数,也没有把 compactProactively() 暴露成工具,更没有在 P08 Loop 中自动识别 prompt-too-long。产物目录目前也没有自动清理、敏感信息脱敏或加密能力;这些不能只靠压缩模块隐式解决。
上下文压缩解决的是当前会话“装不下”的问题。下一章会在这个边界之上讨论另一件事:哪些信息值得跨会话保留,以及文件级记忆如何在失败时仍不丢数据。
换个场景,你还会判断吗?
每题只测一个边界。先做决定,再看解释。
准备开始