9 min read教程

PiAgent 08|上下文工程:有限窗口如何承载长任务

从资源加载、工具输出截断到历史投影,理解上下文不是“塞更多文本”,而是持续做信息选择

PiAgent 08|上下文工程:有限窗口如何承载长任务#

从资源加载、工具输出截断到历史投影,理解上下文不是“塞更多文本”,而是持续做信息选择

上下文工程不是把更多资料拼进 prompt。真正的问题是:模型窗口有限,工具输出可能爆炸,项目规则需要长期生效,历史又不断增长。Pi 通过资源加载、消息投影、工具输出限制和压缩等机制,把不同时间尺度的信息分层管理。

1. 当前请求的上下文由什么组成#

一次发送给模型的 Context 通常包括:

text
系统提示词
  + 当前工作目录与上下文文件
  + skills / prompt templates 的按需内容
  + 会话历史的当前分支
  + 最近的工具调用与结果
  + 可用工具定义
  + 本轮用户输入

这些内容的来源不同、稳定性不同:系统规则通常长期稳定,工具结果短期有效,skills 可能按任务按需加载,历史则需要随着窗口压力被压缩。把它们全部以同样优先级拼接,会导致真正重要的约束被噪声淹没。

2. 输入侧和历史侧是两类问题#

Pi 的上下文处理可以分成两个方向:

  • 输入侧:控制单条工具结果、动态提示词和外部资料的大小。
  • 历史侧:控制长期对话、分支和摘要如何进入当前路径。

压缩解决历史增长,不能阻止一次异常大的 bash 输出瞬间占满窗口;截断工具结果也不能替代跨几十轮对话的摘要。两者需要同时存在。

3. ResourceLoader 是上下文入口之一#

在 Coding Agent 层,ResourceLoader 负责发现和加载:

  • 系统提示词。
  • AGENTS.md 等上下文文件。
  • skills。
  • extensions。
  • prompt templates 和主题等资源。

这解释了一个常见的定位误区:如果默认 Agent 的行为规则不符合预期,不一定要修改 Agent 类;更可能应该检查 ResourceLoader 最终生成了什么系统提示词,以及资源的优先级和覆盖关系。

4. 工具输出必须有边界#

限制工具输出时,通常同时考虑行数和字节数,先达到的限制生效:

ts
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 成本,也会让稳定规则在长任务中被淹没。

可以把信息分成三层:

  1. 不变量:安全规则、输出约束、工具边界,始终保留。
  2. 当前任务状态:目标、已完成事项、待处理事项,随着运行更新。
  3. 按需资料:某个 skill、文件或外部文档,只在相关阶段加载。

这是一种信息生命周期设计:不是所有资料都应该同时处于“模型可见”状态。

6. 历史投影要尊重当前分支#

会话树支持回退和分支,因此当前上下文不是 JSONL 文件所有行的简单拼接,而是从当前 leaf 向父节点回溯得到的一条路径:

text
root → user A → assistant A → tool A
                    └→ user B → assistant B   ← 当前 leaf

如果回退到 assistant A 再输入新消息,旧的 user B 不应继续出现在当前上下文,但它也不应该从文件中被物理删除。上下文构建需要区分“历史事实存在”和“当前分支可见”。

7. 技能是延迟加载的上下文#

Skill 的价值不只是保存一段提示词,而是把一组专业规则按需挂到资源系统上。加载过早会污染所有任务的上下文;加载过晚则可能让模型在关键决策前缺少约束。

一个合理的生命周期是:

text
发现 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

相关文章