π Pi 轻松学
首页 / 第 4 篇

记忆 —— 消息、上下文,和遗忘的艺术

30 秒版本: 大模型没有记忆——每次调用都是从零开始,Pi 必须把整个对话重新发一遍。所以上下文是 Pi 最金贵的资源,它也像管理金库一样管理它:裁减输入、精心组装提示词,历史太重了就压缩成摘要,而不是直接扔掉。


金鱼问题

大模型有个根本性的尴尬:它们什么都不记得。每一次模型调用,都像一个陌生人走进来,需要你把整个故事从头讲一遍——你的请求、之前说过的所有话、每一次工具结果。你体验到的"对话",其实是外壳维持的幻觉:每轮都把完整记录重新发送一遍。

唯一限制这份记录的是上下文窗口——每次调用的硬性 token 预算(比如 Claude 的 200k token)。装满了,API 直接拒绝:prompt is too long。游戏结束。

而编码 Agent 偏偏是工具结果特别巨大的工种(想想 npm install 的输出,或者读一个 3000 行的文件),随时生活在溢出的威胁之下。Pi 的应对是三道防线。

防线一:进门先裁

最省钱的 token,是压根不发的那个。Pi 在工具输出进入历史之前就动手裁:

  • bash 输出只保留最后约 2000 行或 50 KB(哪个先到算哪个)——因为报错和结果都住在日志的末尾。完整输出会存进一个临时文件,截断提示会告诉模型去哪找——它真需要的时候可以自己 read
  • 对大文件的 read 同样有上限,但保留开头——因为 import、类型、结构都住在源码文件的头部。

注意这里的优雅:截断不是偷偷丢数据,而是一张字条——"这是概要,档案在那边,想要自己去拿。"模型依然掌握着自己的好奇心。

防线二:把提示词打包成一份爱心便当

每次调用模型,都会收到一份精心打包的上下文:

系统提示词      你是谁、基本规矩、工具清单
上下文文件      你项目里的 AGENTS.md(以及父目录里的)
技能索引        可用技能的名字 + 一句话描述
对话记录        到目前为止的完整记录(裁减过、翻译过)

有两个细节值得品味。

上下文文件是递归收集的。 Pi 会收集工作目录、每一级父目录的 AGENTS.md / CLAUDE.md,外加 ~/.pi/agent/ 里的全局版本。monorepo?组织规范、团队规范、项目规范自动层层叠加,最具体的排最后。项目知识零硬编码。

技能用"图书馆目录卡"的套路。 Pi 可能知道几十个技能,但它们的全文不进提示词——只有一张小索引进去(名字 + 一句话描述 + 文件路径)。任务匹配时,模型自己用 read 工具去读技能全文。与其预载所有可能的能力(昂贵且大部分浪费),不如让模型按需取用。用多少付多少。

防线三:窗口快满了——压缩

裁减有帮助,但长会话还是会涨。当记录逼近窗口边缘,Pi 执行压缩(compaction):请模型总结较早部分的对话,然后用摘要换掉原文。

触发条件是一个简单的不等式:

contextTokens  >  contextWindow − reserveTokens     (reserve 默认 16,384)

那个 reserve 是给模型回复留的喘气空间——窗口永远不能装到沿上。

有意思的是从哪切。Pi 从最新的消息往回走,一路累计 token,攒够约 20k(可配置)的近期历史为止——因为近期上下文是模型正在干活用的材料。切点之前的所有东西,成为摘要的原材料。

而且不是自由发挥的摘要——Pi 要求结构化输出,固定的章节:目标、约束、进度(已完成 / 进行中 / 受阻)、关键决策、下一步、关键上下文。末尾还附上读过和改过的文件清单,跨多次压缩累计。会话压缩了三轮,Pi 依然记得它碰过的每一个文件。

压缩后模型看到的:

┌────────┬──────────────┬───────────────────────────────────┐
│ 系统    │    摘要       │ 近期消息,原样保留                  │
│ 提示词  │ (被压缩的    │ (正在工作的现在)                  │
│        │   过去)      │                                   │
└────────┴──────────────┴───────────────────────────────────┘

原始消息并没有在任何地方被删除——记住,会话是 append-only 的——它们只是不再被发送。压缩是一个视图,不是碎纸机。

还有一个我们很喜欢的小细节:这些一次性的总结调用会用全新的路由 ID,并且在提供方允许时跳过 prompt cache 写入——一个小小的体贴,意思是"这个提示词永远不会复用,别让它污染缓存。"

底层的管道:两种消息

简短看一眼机器内部。Pi 内部的对话记录装的是 AgentMessage——一个灵活的大家族,包含 LLM 听得懂的三种(userassistanttoolResult),加上应用特有的类型:压缩摘要、分支摘要、纯 UI 记录。

但 LLM 只听那三种标准话。所以每次调模型前的最后一刻,一个翻译函数(convertToLlm)把丰富的内部格式转成严格的外部格式。内里丰富,门口严格——和 Pi 其他一切设计同款套路。

这也是"用户跑了条命令,输出永远不该让模型看到"这类戏法的实现方式:记录留在 UI 里,翻译函数只是不翻译它而已。

带走的话

上下文工程听着高深,在 Pi 里就是三个朴素的习惯:

  1. 不需要的别发(裁输出、技能只放索引不嵌全文)。
  2. 需要的发好(分层上下文文件、结构化提示词)。
  3. 过去太重时,总结它——永远不要直接截肢。

一个健忘的模型,加上纪律严明的笔记习惯,就无限接近于记忆。


资料来源: Compaction & Branch Summarization · pi-agent-core 消息流