PiAgent 08|上下文工程:有限窗口如何承载长任务#
从资源加载、工具输出截断到历史投影,理解上下文不是“塞更多文本”,而是持续做信息选择
上下文工程不是把更多资料拼进 prompt。真正的问题是:模型窗口有限,工具输出可能爆炸,项目规则需要长期生效,历史又不断增长。Pi 通过资源加载、消息投影、工具输出限制和压缩等机制,把不同时间尺度的信息分层管理。
1. 当前请求的上下文由什么组成#
一次发送给模型的 Context 通常包括:
系统提示词
+ 当前工作目录与上下文文件
+ skills / prompt templates 的按需内容
+ 会话历史的当前分支
+ 最近的工具调用与结果
+ 可用工具定义
+ 本轮用户输入这些内容的来源不同、稳定性不同:系统规则通常长期稳定,工具结果短期有效,skills 可能按任务按需加载,历史则需要随着窗口压力被压缩。把它们全部以同样优先级拼接,会导致真正重要的约束被噪声淹没。
2. 输入侧和历史侧是两类问题#
Pi 的上下文处理可以分成两个方向:
- 输入侧:控制单条工具结果、动态提示词和外部资料的大小。
- 历史侧:控制长期对话、分支和摘要如何进入当前路径。
压缩解决历史增长,不能阻止一次异常大的 bash 输出瞬间占满窗口;截断工具结果也不能替代跨几十轮对话的摘要。两者需要同时存在。
3. ResourceLoader 是上下文入口之一#
在 Coding Agent 层,ResourceLoader 负责发现和加载:
- 系统提示词。
AGENTS.md等上下文文件。- skills。
- extensions。
- prompt templates 和主题等资源。
这解释了一个常见的定位误区:如果默认 Agent 的行为规则不符合预期,不一定要修改 Agent 类;更可能应该检查 ResourceLoader 最终生成了什么系统提示词,以及资源的优先级和覆盖关系。
4. 工具输出必须有边界#
限制工具输出时,通常同时考虑行数和字节数,先达到的限制生效:
function truncate(text: string, maxBytes: number) {
const bytes = Buffer.byteLength(text, "utf8");
if (bytes <= maxBytes) return { text, truncated: false };
return {
text: text.slice(-Math.floor(maxBytes / 2)) + "\n[output truncated]",
truncated: true,
};
}真实实现还要处理 UTF-8 多字节边界、单行超限、头部或尾部保留策略,以及完整输出的外部存储路径。截断后必须明确告诉模型发生过截断,否则模型可能把不完整结果当成完整事实。
头部还是尾部
- 头部适合保留命令、环境信息和最早出现的错误。
- 尾部适合保留最新状态、汇总和最后一段诊断信息。
- 结构化数据最好按记录裁剪,不要从 JSON 中间截断成非法格式。
5. 动态系统提示词不能无限增长#
系统提示词经常包含当前目录、工具说明、资源清单和业务约束。每轮都把完整环境重新展开,会增加 token 成本,也会让稳定规则在长任务中被淹没。
可以把信息分成三层:
- 不变量:安全规则、输出约束、工具边界,始终保留。
- 当前任务状态:目标、已完成事项、待处理事项,随着运行更新。
- 按需资料:某个 skill、文件或外部文档,只在相关阶段加载。
这是一种信息生命周期设计:不是所有资料都应该同时处于“模型可见”状态。
6. 历史投影要尊重当前分支#
会话树支持回退和分支,因此当前上下文不是 JSONL 文件所有行的简单拼接,而是从当前 leaf 向父节点回溯得到的一条路径:
root → user A → assistant A → tool A
└→ user B → assistant B ← 当前 leaf如果回退到 assistant A 再输入新消息,旧的 user B 不应继续出现在当前上下文,但它也不应该从文件中被物理删除。上下文构建需要区分“历史事实存在”和“当前分支可见”。
7. 技能是延迟加载的上下文#
Skill 的价值不只是保存一段提示词,而是把一组专业规则按需挂到资源系统上。加载过早会污染所有任务的上下文;加载过晚则可能让模型在关键决策前缺少约束。
一个合理的生命周期是:
发现 skill 元信息
→ 根据任务判断是否相关
→ 只加载相关正文或资源
→ 在当前运行中保持可见
→ 压缩时保留已采用的关键规则Skill 不应成为绕过工具权限的后门。它能改变模型的说明和行为倾向,但真正的权限仍应在工具执行层判断。
8. 上下文预算应该可观测#
每轮至少估算并记录:
- system prompt 占用。
- 历史消息占用。
- 工具定义占用。
- 最近工具结果占用。
- 预留给模型输出和下一轮工具结果的空间。
当上下文接近上限时,要提前决定是截断、压缩、拒绝新输入还是减少工具集合。等 Provider 返回上下文超限错误再处理,通常已经失去了本轮的恢复空间。
常见误区
- 以为上下文越长,Agent 就越可靠。
- 只做历史压缩,不限制单次工具输出。
- 截断结果却不标记,导致模型误读不完整事实。
- 每轮重复注入完整 skills 和项目规则。
- 回退会话后仍然把旧分支消息拼进当前上下文。
小结
上下文工程的本质是信息选择和生命周期管理:资源加载决定规则从哪里来,消息投影决定哪些事实进入模型,工具截断控制瞬时峰值,会话分支控制历史路径,压缩则处理长期增长。把这些机制拆开,才能知道上下文问题究竟是“太多”“太旧”“太杂”还是“缺少关键事实”。
源码定位
- 资源加载:
packages/coding-agent/src/core/resource-loader.ts - 上下文与消息转换:
packages/agent/src/agent.ts - 工具结果处理:
packages/agent/src/agent-loop.ts - skills 与上下文文件:
packages/coding-agent/src/core/ - 压缩入口:
packages/coding-agent/src/agent-session.ts