禅,RSI 与 Agent 最简实现艺术

Note: 这篇文章还在施工中。 [WIP]

2023年6月,OpenAI Head of Safety Systems - Lilian Weng 发表了一篇讨论非常广的文章:LLM Powered Autonomous Agents, 以至于一整年内有关 Agent 组成部分的截图都持续地在各大公司/高校组会/咨询公司或者公众号的材料中出现。

她将 LLM Agent 的核心公式定义为:

\[Agent = LLM + Planning + Memory + Tool Use\]

时过境迁,LLM 本身的输出答案能力和驱动状态机流转的能力,加上设计过的规划与反思(Planning)、长短期记忆(Memory)、各种组件/钩子/Skills相关的工具调用(ToolCall)的能力依然可以概况一个Agent的组成部分。

再后来吴恩达把这几个模块抽象成了构建 Agent 的设计模式,加上了 Multi-agent Collaboration 的一种模式。旨在说明复杂任务分给多个 Agent 的状态。

还有各种各样的公司和角色在之后的时间争夺 Agent 定义的话语权。 笔者的一贯风格是辨别并摒弃不需要了解的无意义内容,比如 Google Vertex AI Agent 重新造了一些词来说明生产环境的 Agent 需要 Runtime Orchestration 和基于真实数据的 grounding 机制。 有些多 Agent 交互(比如 tau-bench,或者其他 HCI 领域的讨论)会提到人本身、人的约束或者LLM约束在 Agent 中的作用,在这个概念的基础上再加一层, 结合 Human-in-the-loop,监控和安全相关的机制做一些文章。但笔者认为这些都属于 Harness 的细节部分,不应该做为核心 Agent 模块在此讨论。

因此,笔者认为 Agent 实际上只需要考虑决策、规划层、存储层、执行层、协同层五个方面的设计即可。 也就是对应 -

\[Agent = LLM + Planning + Memory + Tool Use + Collaboration\]

这样的设计。

本文会谈谈笔者从 Agent 概念大爆发之后,从收集了几十篇 Planning 相关文章使劲读开始,到熟悉社区雨后春笋般出现的 Agent SDK,以及带工程团队手搓业务 Agent, 和最后动手做 long-horizon trajectory SFT & Agentic RL 的全流程里对 Agent 框架设计的一些理解。

*这里预计不含 long-horizon LLM 后训练相关内容,将在其他文章中展开。

1. Memory

我们从 Memory 开始。

先来介绍一下今天(2026年8月)在简洁方面有口皆碑的 Pi Agent 的实践。

没有太多个性化的内容,主要包含会话压缩和持久化。

Pi 的对话压缩与持久化

Compaction

有关压缩的描述可以在这里(packages/coding-agent/docs/compaction.md)找到。 做法和主流 Agent 完全一样,就是 LLM 黑盒压缩 + 凑点东西进新上下文。 具体而言,流程分为 5 步:

  1. 寻找切割点:从最新消息回溯,累积 token 估算直到达到 keepRecentTokens(默认 20k)
  2. 提取消息:收集从上次保留边界到切割点的消息
  3. 生成摘要:调用 LLM 按结构化格式总结,并把上一次摘要作为迭代上下文传入
  4. 追加条目:保存 CompactionEntry,包含摘要和 firstKeptEntryId
  5. 重建上下文:下次请求时用 摘要 + firstKeptEntryId 之后的消息。

所谓 Compaction Entry,是一个包含压缩状态的 JSON,它存储旧消息的摘要,并标记从哪个位置开始的消息要还原到上下文。

interface CompactionEntry<T = unknown> {  
  type: "compaction";  
  id: string;  
  parentId: string;  
  timestamp: number;  
  summary: string;  
  firstKeptEntryId: string;  
  tokensBefore: number;  
  usage?: Usage;  
  fromHook?: boolean;  
  details?: T;  
}

CompactionEntry 数据结构负责记录摘要。

摘要使用固定的结构化格式(Goal / Progress / Key Decisions / Next Steps / Critical Context 等),并在多次压缩时保留之前的信息、更新进展状态。

重建上下文:下次请求时用 摘要 + firstKeptEntryId 之后的消息

Split Turns

A “turn” starts with a user message and includes all assistant responses and tool calls until the next user message. Normally, compaction cuts at turn boundaries.

When a single turn exceeds keepRecentTokens, the cut point lands mid-turn at an assistant message. This is a “split turn”:

Split turn (one huge turn exceeds budget):
 
  entry:  0     1     2      3     4      5      6     7      8
        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐
        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │
        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘
                ↑                                     ↑
         turnStartIndex = 1                  firstKeptEntryId = 7
                │                                     │
                └──── turnPrefixMessages (1-6) ───────┘
                                                      └── kept (7-8)
 
  isSplitTurn = true
  messagesToSummarize = []  (no complete turns before)
  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]

分割 Turn 的处理 有一个细节处理(甚至比 CC 还好):如果单个 turn 本身超出 keepRecentTokens(默认 20k),会产生”split turn”, 此时会生成两份摘要(历史摘要 + turn 前缀摘要)并合并。

也有一些 pi 的分支实现做到了并发地compaction来提升体验;具体在压缩的时候怎么存的问题,TencentDB Memory 有一个存 mermaid 图的设计。 总之,存什么和怎么存是一个重要问题。

Session Persistence

pi 有一个树状的会话历史记录设计。 整个会话以 JSONL 文件树形结构存储在 ~/.pi/agent/sessions/,每个条目有 id/parentId,支持用户原地分支(/tree, /fork, /clone)。

这个压缩是有损的,但是支持完整还原。 完整历史仍保留在 JSONL 文件中,可以通过 /tree 找回。这就是所谓上下文持久化。

记忆与上下文管理

在我参与设计产品里,我给Coding Agent 的问答场景划分了三种上下文管理模式: 上下文隔离上下文整理上下文丰富

  • 上下文隔离:主题检测、SubAgent、沙箱、[Skills的渐进式披露]
  • 上下文整理:黑盒压缩和可逆压缩、多源异构大文件问答、Agent间通信
  • 上下文丰富:to-do list等规划方法、集成拓展(比如环境信息)、动态上下文拼接(*.md,文件引用)

意思是如果真的要展开设计特性,可以围绕这些地方提升能力,但是应该做到哪些?

笔者认为应该把绝对通用的能力留下来。比如我要演进我的WhalePod, 一个重要能力就是多Agent交互时也保证省token、高KVCache命中率。所以多Agent交互一定是我我要考虑的特性,这部分的 memory 管理可能也会更复杂一些。

[WIP]

但上下文管理的做法现在有了一个新思路:

DeepSeek Harness 讨论了一个问题,对带很多需要动态加载的插件的现代复杂系统中,存在两个问题:

  1. 卸载副作用:插件卸载后,其注册的监听器、状态改变或钩子容易遗漏,导致系统状态污染。
  2. 依赖管理混乱:组件间的依赖关系在动态变化时难以自动响应与有序调度。

换句话说,他们把造成的挑战分为了两个维度:

  1. 时间可组合性(Temporal Composability):组件在被移除或替换时,能够完整撤销(Revert)其产生的全部副作用。
  2. 空间可组合性(Spatial Composability):能够声明并响应式地解析不同组件之间的跨模块依赖。

DSH 将编程语言理论中的 Effect(效应) 与 Coeffect(余效应 / 上下文需求) 概念提升至运行时机制,提出了以下核心设计:

  • 可逆效应(Revertible Effects - 解决时间维度) 每个对上下文进行的修改或状态转换,运行时都会自动持有并维护其对应的逆操作。当插件卸载时,运行时会自动按序反向执行逆操作,实现安全卸载。
  • 响应式余效应(Reactive Coeffects - 解决空间维度) 组件以声明式的方式定义自己对外部环境/服务的依赖需求(Coeffect Specification)。当环境中的服务出现、变化或销毁时,系统自动驱动该组件的激活或休眠,保证依赖关系的响应式自治。
  • 上下文范式(The Context Paradigm) 将 Effect 上下文和 Coeffect 上下文统一为单一的 Context 类型。所有组件的副作用和依赖都通过上下文进行调解,确保不同组件交错运行时互不干扰,满足观测等价性(Observational Equivalence)。
  • 动态组合演算(Calculus of Dynamic Composition) 给出了形式化的演算规则与元理论,证明了时空可组合性可以从单一组件无缝扩展到整个由多组件交织构成的复杂系统。

Codex 最近在做实验性的上下文压缩方案:

  1. 完成一个阶段性目标或上下文即将耗尽时,主动调用 new_context 工具开启一个全新的、干净的上下文窗口
  2. 离开旧窗口前,Agent 会总结并记录当前任务的结构化 Notes;旧窗口中的原始用户指令、代码片段、终端返回日志与工具调用输出不再被直接丢弃,而是转为后端归档索引
  3. 旧窗口中的原始用户指令、代码片段、终端返回日志与工具调用输出不再被直接丢弃,而是转为后端归档索引

2. Planning

pi 的文档[2]提到 pi 有意跳过了 sub agents 和 plan mode 这类功能。pi 的核心因此只是一个持续的简单 agent loop:

user query → 输出(可能带工具调用的)assistant message → 执行工具 → observation 给 LLM → 继续下一轮,直到没有工具调用为止。 这套 loop 没规划步骤,只是逐轮响应式地根据 LLM 的工具调用推进,而不是提前生成一个多步计划再执行。

p.s. 这个流程中有钩子(beforeToolCall/afterToolCall/shouldStopAfterTurn)可以介入执行流程。

[WIP]

3. Tool Use

工具的收敛与元工具化

关键是做好反设计。批判为业务定义成百上千个高度特化的 RPC/API 工具,模型不仅难以精准选择,还会炸毁 System Prompt 的 KV Cache。

Remote Procedure Call(远程过程调用)RPC 的核心思想是:“让你像调用本地函数一样,去调用运行在另一台机器或另一个进程中的函数”。底层的网络连接、数据打包(序列化)、发送、等待响应、解包等复杂过程,都被 RPC 框架屏蔽了。 而 API 是一个顶层的大概念,而 RPC 是实现 API 的一种具体模式或技术手段。 一句话总结关系:所有的 RPC 都是一种 API,但并非所有 API 都是 RPC。

4. Collaboration

Pi 原版选择跳过 SubAgent 是明智的,因为它避免了复杂的分布式状态管理。

吴恩达等人推崇的 Multi-agent Collaboration 在工程生产中往往是玩具——多个 Agent 互相客套、圆桌会议导致延迟成倍飙升、死锁震荡以及幻觉级联放大。 Caller-Callee(SubAgent as a Tool)模式

防止循环等协作问题 - 强行引入有向无环图(DAG)的单向推进约束,禁止无条件的双向 Loop。

Reference

[2] https://pi.dev/