M08. 上下文:有限窗口如何承载长任务#
上下文工程不是“把更多资料塞进 prompt”。真正的问题是:在窗口有限、工具输出可能爆炸、项目规则需要始终生效、历史又不断增长的情况下,哪些信息应该被保留,哪些应该延迟加载,哪些必须被压缩。
1. 两个方向的防护#
Pi 的上下文处理可以按两个方向理解:
- 输入侧:控制一次工具结果和动态提示词的大小。
- 历史侧:控制长期对话、分支和摘要如何进入当前路径。
它们不能互相替代。压缩解决历史增长,不能阻止一条异常大的 bash 输出瞬间占满窗口;截断工具结果也不能替代跨几十轮对话的摘要。
2. 工具输出截断#
限制工具输出时,通常同时看行数和字节数,先达到的限制生效。头部截断适合保留命令开头的错误和环境信息,尾部截断适合保留最新结果或末尾摘要。
function truncateTail(text: string, maxBytes: number) {
const bytes = Buffer.byteLength(text, "utf8");
if (bytes <= maxBytes) return text;
return text.slice(-Math.floor(maxBytes / 2)) + "\n[truncated]";
}真实实现还要处理 UTF-8 多字节边界、单行超限和准确的提示语。示意代码的重点是:截断后必须明确告诉模型发生过截断,否则模型会把不完整结果当完整事实。
3. 动态系统提示词#
项目规则、当前工作目录、可用工具和 skills 往往需要动态拼接。稳定的系统骨架可以保持简洁,把项目指令用清楚的边界包装起来,把技能列表放进提示词,把技能正文留到按需读取时再加载。
静态 Agent 规则
+ 工作目录与环境摘要
+ 当前工具说明
+ 项目上下文文件
+ 可用 skills 的名称和用途这是一种渐进式披露:路由信息常驻,详细知识按需进入。它同时降低 token 成本和错误路由概率。
4. transformContext 是扩展点,不是银弹#
宿主可以通过 transformContext 对待发送的 AgentMessage 做裁剪、补充或重排。这个扩展点适合加入项目特定的上下文策略,但不应该在里面悄悄删除用户约束,也不应该把安全策略交给模型自己推断。
上下文变换应可解释:记录哪些消息被过滤、哪些工具结果被截断、压缩从哪里开始。否则用户看到 Agent 忘记某件事时,你无法判断是模型问题还是 projection 问题。
5. 分支摘要与普通压缩#
普通 compaction 处理的是“当前历史太长”;branch summarization 处理的是“从另一条分支跳回来后,旧路径的信息怎样带回来”。两者都使用摘要,但触发原因和语义不同:前者是窗口管理,后者是分支切换时的语义桥。
6. 一次上下文处理链#
Session Tree 当前路径
→ 取出可见 entry
→ 处理 compaction / branch summary
→ 生成 AgentMessage
→ transformContext
→ convertToLlm
→ provider adapter任何一步都可能改变模型最终看到的内容。因此调试上下文时,要同时保存“原始历史”和“最终请求”,不能只看会话文件或只看模型请求中的最后几条消息。
7. 工具调用也是按需加载#
当模型决定调用 read、grep 或某个 skill 工具时,它是在选择一块上下文载体。工具输出应足够支持下一步决策,但不应把整个仓库一次性搬进窗口。好的工具接口本身就是上下文预算策略。
8. 上下文工程的判断标准#
任何上下文变换都应该能回答三个问题:删掉了什么,为什么可以删;补进了什么,来源是否可信;模型是否知道当前内容经过了截断或摘要。上下文不是越多越好,而是要让下一步决策所需的信息密度更高。把原始大结果保存到文件、把短摘要放进消息,正是把存储和推理两个问题分开。
资料
本文依据 Pi SDK 文档和 Compaction 文档重新组织。