标签归档:harness engineering

从 Pi 到 DSH:Agent Harness 如何从「可扩展」走向「自生长」

大模型能力持续提升之后,Agent 系统的竞争焦点正在发生变化。

过去,我们主要关心模型能不能理解任务、调用工具、编写代码。现在,更重要的问题逐渐变成:模型之外的系统,能否为 Agent 提供一个可以持续变化的运行环境?

这个运行环境通常被称为 Agent Harness

Harness 负责把模型、提示词、工具、记忆、上下文、权限和外部环境连接起来。它决定模型能够看到什么、调用什么、保存什么,以及一次行动会产生怎样的系统影响。

从这个角度看,Agent 的能力并不只来自模型,用一个公式来表达,大概如下:

Agent 能力
=
模型能力
× 上下文组织
× 工具系统
× 运行时结构
× 生命周期治理

如果 Harness 是固定的,那么即使模型越来越强,Agent 的能力边界仍然主要由开发者预先决定。

如果 Harness 可以扩展,用户和开发者就能不断为 Agent 增加工具、技能和工作流。

但如果我们希望 Agent 进一步具备「自生长」能力,仅仅提供扩展接口还不够。系统还必须允许 Agent:

  1. 发现自己的能力缺口;
  2. 生成或引入候选能力;
  3. 在运行时安全地激活这些能力;
  4. 验证变化是否有效;
  5. 在失败时完整回滚;
  6. 将成功经验沉淀为可复用组件;
  7. 淘汰已经失效或长期无用的能力。

Pi 与 DSH 可以被视为这条演进路径上的两个阶段。

Pi 回答的是:

如何构建一个足够小、足够开放,可以持续扩展的 Agent Harness?

DSH 进一步追问:

如何让 Agent 不只是使用外部提供的扩展,而是安全地参与自身能力结构的形成?

这不是从一种插件格式切换到另一种插件格式,而是 Agent Harness 从开放扩展平台能力生长环境的转变。

1. 什么是 Agent Harness

一个最简单的 Agent,可以被表示为:

Model
  ↓
Prompt
  ↓
Tool Call
  ↓
Environment

模型接收上下文,生成工具调用,再根据工具结果继续推理。

但在真实系统里,Agent 还需要处理更多问题:

  • 当前有哪些工具可用?
  • 工具由谁注册?
  • 不同任务应该使用什么模型?
  • 哪些信息应该进入上下文?
  • 哪些记忆需要跨会话保存?
  • 如何限制文件、网络和进程权限?
  • 工具调用失败后如何恢复?
  • 新能力如何加载?
  • 组件移除后如何清理副作用?
  • 不同组件之间的依赖如何表达?

于是,Agent 的实际结构更接近于:

                ┌─────────────┐
                │    Model    │
                └──────┬──────┘
                       │
                ┌──────▼──────┐
                │ Agent Loop  │
                └──────┬──────┘
                       │
       ┌───────────────┼────────────────┐
       │               │                │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│    Tools    │ │    Memory   │ │   Context   │
└─────────────┘ └─────────────┘ └─────────────┘
       │               │                │
       └───────────────┼────────────────┘
                       │
                ┌──────▼──────┐
                │ Environment │
                └─────────────┘

Harness 并不是模型外面的一层简单包装。

它实际上规定了 Agent 的:

  • 能力边界;
  • 行为边界;
  • 状态边界;
  • 权限边界;
  • 演化边界。

因此,当我们讨论 Agent 是否可以扩展、重构甚至自生长时,本质上讨论的不是模型参数如何变化,而是 Harness 能否安全地改变自身结构

2. Pi:以最小内核换取最大扩展空间

Pi 的核心思想非常明确:

不要预先替用户决定完整工作流,而是提供足够小的基础能力,让用户自己构建需要的 Harness。

Pi 将自己定义为一个最小化的 Agent Harness,并通过 Extensions、Skills、Prompt Templates、Themes 和 Packages 提供扩展能力。它允许用户修改工具、命令、模型提供商、工作流、上下文处理和终端界面,也支持将这些资源打包后通过 npm 或 Git 分发。(pi.dev)

这背后是一种「原语优先」的设计哲学:

不是:
内置所有功能

而是:
提供少量原语
+
开放扩展接口
+
允许用户自行组合

2.1 最小化不是能力不足

传统 Agent 产品通常倾向于内置更多功能:

  • 规划模式;
  • 子 Agent;
  • 权限确认;
  • MCP 集成;
  • 后台任务;
  • 长期记忆;
  • 沙箱;
  • Todo 系统。

Pi 选择了另一条路线:这些能力不一定需要成为内核的一部分,它们可以通过扩展或外部环境实现。

Pi 当前提供文件读取、文件修改、内容搜索和命令执行等内置工具,同时允许通过命令行配置工具集合,也允许 Extension 注册新的工具或覆盖现有工具。(pi.dev)

这种设计的意义把稳定的内核和变化的能力分开:

稳定部分:
模型调用、Agent Loop、会话、基础工具、扩展 API

变化部分:
工具、技能、上下文、工作流、权限策略、界面、模型路由

只要扩展接口足够开放,功能就不必全部固化在核心代码中。

2.2 Pi 的四类扩展资源

从 Harness 的角度看,Pi 的可扩展性主要体现在以下几个层面。

2.2.1 Extensions:改变运行时行为

Extension 是可以进入 Agent 运行时的代码模块。

它可以:

  • 注册工具和命令;
  • 监听工具调用;
  • 拦截或修改行为;
  • 改变上下文;
  • 切换模型;
  • 增加权限检查;
  • 实现自定义 UI;
  • 覆盖内置工具;
  • 连接外部系统。

因此,Extension 改变的不只是模型“知道什么”,还可以直接改变 Harness“如何运行”。

2.2.2 Skills:注入按需能力

Skill 更接近一种可复用的过程知识。

它可以告诉 Agent:

  • 在什么情况下使用某种方法;
  • 如何执行特定工作流;
  • 如何调用外部命令;
  • 如何处理某类项目;
  • 如何检查输出质量。

Pi 将 Skill 设计为按需加载的能力包,使 Agent 不必在每次会话开始时把所有知识都放进上下文。(pi.dev)

2.2.3 Prompt Templates:固化交互模式

Prompt Template 将常见任务封装成可复用提示词,例如:

  • 代码审查;
  • 架构分析;
  • 发布检查;
  • 测试生成;
  • 故障排查。

它改变的是任务入口和交互结构。

2.2.4 Packages:分发完整能力

Package 可以将 Extensions、Skills、Prompt Templates 和 Themes 组合起来,通过 npm、Git 或本地路径安装。Pi 还支持临时加载 Package,使组件只对当前运行生效。(github.com)

这使 Pi 不只是一个 Agent,也成为一个 Harness 能力分发平台:

Pi Package
├── Extensions
├── Skills
├── Prompts
└── Themes

2.3 Pi 的关键突破:Agent 可以参与修改 Harness

Pi 最值得关注的地方,不只是「插件多」。

它的官方描述明确鼓励用户让 Pi 编写自己需要的扩展,并在修改后通过重新加载继续当前工作。(pi.dev)

这意味着一种新的工作方式正在出现:

Agent 发现缺少某项能力
        ↓
Agent 编写 Extension
        ↓
用户检查或确认
        ↓
重新加载 Harness
        ↓
Agent 使用新增能力继续任务

过去,Agent 与开发工具之间的关系通常是:

人类开发 Harness
Agent 使用 Harness

在 Pi 中,它开始变成:

人类定义目标
Agent 修改 Harness
Agent 使用修改后的 Harness

这是从「可扩展」走向「自生长」的重要一步。

但是,这还不等于完整的自生长。

3. 行为自治不等于结构自治

理解 Pi 与 DSH 的关系,需要区分两种不同的自治。

3.1 行为自治

行为自治指 Agent 可以在已有能力范围内自主决定:

  • 下一步做什么;
  • 调用哪个工具;
  • 如何拆解任务;
  • 是否修改文件;
  • 是否运行测试;
  • 如何根据结果调整计划。

此时,Agent 的选择空间是动态的,但能力集合通常是预先确定的。

固定工具集合
    ↓
Agent 自主选择工具
    ↓
完成任务

3.2 结构自治

结构自治指 Agent 可以改变自己的能力结构:

  • 创建新工具;
  • 安装新组件;
  • 替换 Memory;
  • 修改上下文策略;
  • 调整模型 Router;
  • 增加权限规则;
  • 组合新的执行流程;
  • 卸载无效组件。
当前能力结构
    ↓
Agent 发现能力缺口
    ↓
生成候选组件
    ↓
改变 Harness
    ↓
形成新的能力结构

Pi 已经为结构自治打开了入口。

但它的扩展机制主要回答的是:

如何把一项新能力加入系统?

真正的自生长还必须回答:

  • 新能力应该在什么条件下激活?
  • 它依赖哪些已有能力?
  • 它失败后能否完整退出?
  • 它注册的工具和监听器由谁撤销?
  • 如何判断变化带来了正收益?
  • 如何防止不同扩展相互冲突?
  • 如何把一次临时变化沉淀成长期能力?
  • 如何淘汰不再适用的组件?

这些问题将我们从扩展系统带到了组件运行时。

4. DSH:为 Harness 自生长建立运行时基础

本文讨论的 DSH,不只是另一个插件系统。

它试图将 Agent Harness 表达为一个可以在运行中不断组合和重组的组件系统。

其基本结构可以概括为:

Component
=
Context
+
Effect
+
Cleanup
+
Coeffects

这四个概念分别回答四个问题:

概念 回答的问题
Context 组件可以看到和使用什么?
Effect 组件加入系统后会产生什么影响?
Cleanup 组件退出时如何撤销影响?
Coeffects 组件依赖哪些外部能力或其他组件?

DSH 的目标不是让所有模块都能任意修改系统,而是让变化成为一种显式、可追踪、可逆的运行时操作

4.1 Context:为能力提供明确边界

一个组件不能直接访问所有系统状态。

它应该通过 Context 获取所需能力,例如:

Context
├── Model Registry
├── Tool Registry
├── Memory
├── Event Bus
├── Session
├── Logger
├── Storage
├── Permission
└── Environment

Context 的第一层作用是解耦。

组件不需要知道某个能力的具体实现,只需要知道运行环境向它提供了什么。

例如,一个模型路由组件可能只依赖:

Model Registry
Task Metadata
Usage Metrics

一个长期记忆组件可能只依赖:

Session Events
Embedding Service
Storage
Context Injector

一个工具组件则可能只依赖:

Tool Registry
Credentials
Network
Logger

Context 的第二层作用是限制能力边界。

如果组件只能通过 Context 访问环境,那么系统就可以决定:

  • 哪些资源向它开放;
  • 哪些能力只读;
  • 哪些调用需要权限;
  • 哪些状态不能跨组件共享;
  • 哪些操作只能在 Sandbox 中执行。

因此,Context 不只是依赖注入,也是一种能力安全模型:

组件能做什么
≈
Context 向它暴露什么

4.2 Effect 与 Cleanup:让变化可以被撤销

当组件加入 Harness 时,通常会产生副作用。

例如:

  • 注册一个工具;
  • 添加一个事件监听器;
  • 打开数据库连接;
  • 启动后台任务;
  • 修改模型路由;
  • 向上下文注入内容;
  • 挂载文件目录;
  • 注册快捷键;
  • 创建缓存;
  • 改变权限策略。

这些变化可以统一理解为 Effect:

Component
    ↓
Effect
    ↓
Harness 状态发生变化

问题在于,大多数插件系统只强调如何创建 Effect,却没有把撤销 Effect 当作同等重要的能力。

于是,组件卸载后可能出现:

  • 工具仍然留在 Registry 中;
  • 事件监听器重复注册;
  • 后台任务继续运行;
  • 网络连接没有关闭;
  • 临时文件没有删除;
  • Context 中残留失效状态;
  • 新旧路由规则同时生效。

在 DSH 中,Effect 应该返回对应的 Cleanup:

Effect(Context) → Cleanup

例如:

function activate(context{
  const unregister = context.tools.register(myTool)

  return function cleanup() {
    unregister()
  }
}

更复杂的组件可能产生多个副作用:

function activate(context{
  const stopTool = registerTool(context)
  const stopListener = registerListener(context)
  const stopWorker = startWorker(context)
  const closeConnection = connectDatabase(context)

  return async function cleanup() {
    await stopWorker()
    stopListener()
    stopTool()
    await closeConnection()
  }
}

这使组件具备完整生命周期:

创建
  ↓
激活
  ↓
运行
  ↓
停用
  ↓
清理

4.3 时间可组合性:让 Agent 有资格试错

Effect 与 Cleanup 提供的是一种时间可组合性

一个组件可以在某个时刻进入系统,也可以在另一个时刻完整退出:

t₀:Harness 原始状态

t₁:组件进入
    → 注册工具
    → 启动服务
    → 注入上下文

t₂:组件运行

t₃:组件退出
    → 移除工具
    → 停止服务
    → 撤销上下文

t₄:Harness 恢复到可预测状态

这对普通插件系统很重要,但对自生长系统更重要。

因为自生长必然包含试错。

Agent 创建的新工具、新路由器、新记忆策略,不可能每次都正确。如果一次失败的尝试不能被完整撤销,那么每次实验都会向系统中留下残余状态。

最终,所谓“生长”就会退化成不可控的复杂度积累。

因此,自生长的基本过程不应该是:

生成组件
  ↓
永久安装

而应该是:

生成候选组件
      ↓
执行受控 Effect
      ↓
验证运行结果
   ┌──┴──┐
 成功    失败
  │       │
保留   Cleanup

Cleanup 的意义不只是“支持热卸载”。

它赋予 Agent 一种更关键的能力:

在不永久污染系统的前提下试验新的自身结构。

4.4 Coeffects:让依赖关系成为运行时的一部分

时间可组合性解决的是组件何时进入、何时退出。

但 Agent Harness 的组件并不是彼此孤立的。

如果依赖关系只是组件内部的隐式假设,运行时就无法回答:

  • 组件现在能否启动?
  • 它缺少什么能力?
  • 某个依赖消失后,哪些组件应该停用?
  • 替换一个 Provider 会影响哪些组件?
  • 哪些组件可以被安全卸载?
  • 一个候选组件是否与当前环境兼容?

Coeffects 用于显式表达组件所需的外部条件:

Effect:
组件对系统做什么

Coeffects:
组件需要系统提供什么

例如:

const vectorMemory = {
  coeffects: [
    "embedding-model",
    "vector-storage",
    "session-events"
  ],

  effect(context) {
    // 激活 Memory
    return cleanup
  }
}

运行时可以根据当前 Context 判断组件是否具备激活条件:

依赖全部满足
    ↓
组件激活

依赖不完整
    ↓
组件保持未激活

依赖运行中消失
    ↓
执行 Cleanup

这使 Harness 不再只是一个插件列表,而是一个动态依赖图:

Credential
    ↓
GitHub Provider
    ↓
GitHub Tool
    ↓
Repository Agent

或者:

Embedding Model ─┐
                 ├→ Vector Memory → Context Injector
Vector Storage ──┘

4.5 空间可组合性:让生长不是无序堆积

Coeffects 提供的是一种空间可组合性

组件不是按照固定顺序硬编码在系统中,而是根据能力和依赖形成运行时拓扑。

当一个能力出现时,依赖它的组件可以被激活;当这个能力消失时,相关组件可以被停用:

Sandbox 出现
    ↓
Code Executor 激活
    ↓
Test Runner 激活
    ↓
Coding Agent 获得安全执行能力

如果 Sandbox 被移除:

Sandbox 消失
    ↓
Code Executor Cleanup
    ↓
Test Runner Cleanup
    ↓
相关工具从 Agent 能力集合中撤销

这与传统插件系统有一个关键区别。

传统插件系统更像:

安装了什么
=
系统拥有什么

而动态组件运行时更像:

当前可用能力
=
当前组件
× 当前依赖
× 当前环境
× 当前权限

这使 Harness 的结构可以随着任务和环境变化,而不是不断向全局系统中永久添加模块。

真正的生长,不是组件数量持续增加,而是系统能够形成更适合当前任务的能力结构。

4.6 从动态重组到自生长闭环

Context、Effect、Cleanup 和 Coeffects 为动态重组提供了基础。

但必须强调:

动态重组是自生长的必要条件,不是自生长的全部。

完整的自生长至少需要五个阶段。

4.6.1 第一阶段:发现能力缺口

Agent 首先要判断自己缺少什么。

例如:

  • 当前没有访问某类数据的工具;
  • 现有 Tool 无法处理特定协议;
  • Memory 无法支持长期任务;
  • 当前模型不适合某类输入;
  • 缺少项目专用的验证流程;
  • 当前执行环境不满足安全要求;
  • 工具调用成本或延迟过高。

能力缺口不能只由「工具调用失败」判断。

它还可能来自:

  • 任务规划;
  • 运行指标;
  • 用户反馈;
  • 测试结果;
  • 历史失败记录;
  • 现有能力清单;
  • 环境变化。

因此,自生长的起点不是生成代码,而是:

目标要求
-
当前能力
=
能力缺口

4.6.2 第二阶段:生成或引入候选能力

识别缺口后,Agent 可以选择不同方式补齐能力:

  • 编写新的 Tool;
  • 生成新的 Skill;
  • 安装已有 Package;
  • 包装外部 API;
  • 组合已有组件;
  • 替换 Memory Provider;
  • 调整模型 Router;
  • 创建新的工作流;
  • 启动一个专用子 Agent;
  • 为现有组件增加适配层。

这里的关键词是「候选」。

Agent 生成的组件不能因为能够编译或加载,就自动成为系统的永久能力。

生成成功
≠
能力有效
≠
适合长期保留

4.6.3 第三阶段:在受控环境中激活

候选组件应该先进入受限 Context。

系统需要明确:

  • 它可以访问哪些目录;
  • 可以调用哪些网络服务;
  • 是否可以读取凭证;
  • 可以注册哪些工具;
  • 能否修改全局状态;
  • 资源消耗上限是多少;
  • 运行时间是否受限;
  • 哪些副作用必须登记。

这一点尤其重要,因为扩展代码通常拥有较高权限。

以 Pi 为例,其官方安全文档明确说明,内置工具和 Extension 默认继承启动 Pi 的进程权限;第三方 Package 可能执行代码或指示模型执行操作,因此需要代码审查,并在需要时依赖容器或虚拟化提供真正的隔离边界。(github.com)

如果 Agent 开始自行生成和加载组件,权限问题只会更加突出。

所以,自生长必须建立在受控运行时上,而不能建立在「默认信任所有新代码」之上。

4.6.4 第四阶段:验证、保留或回滚

候选能力激活后,系统需要对它进行验证。

验证可以包括:

  • 能否完成目标任务;
  • 是否通过单元测试;
  • 是否破坏已有功能;
  • 是否产生未声明的副作用;
  • 是否扩大了不必要的权限;
  • 是否降低了任务成本;
  • 是否提高了成功率;
  • 是否造成明显的上下文膨胀;
  • 是否与现有组件冲突;
  • Cleanup 是否完整。

验证结果通常有三种:

验证通过
    ↓
保留或固化

部分通过
    ↓
修改后重新试验

验证失败
    ↓
Cleanup + 回滚

因此,自生长不是一次性的代码生成,而是一个选择过程:

Generate
→ Activate
→ Observe
→ Evaluate
→ Select

没有评估的生成,只是变化。

经过评估和选择的变化,才可能成为生长。

4.6.5 第五阶段:沉淀、复用与淘汰

一次任务中的成功,不代表组件值得永久保留。

系统还需要决定:

  • 这项能力是否跨会话有效?
  • 它适用于哪些任务?
  • 应该在什么条件下重新激活?
  • 它依赖哪些版本?
  • 它的权限范围是什么?
  • 它由谁生成?
  • 经过了哪些测试?
  • 是否允许自动升级?
  • 何时应该重新评估?
  • 何时应该被淘汰?

因此,一个可复用组件除了代码,还需要附带元数据:

Component
├── Implementation
├── Capability Description
├── Coeffects
├── Permissions
├── Validation Records
├── Version
├── Provenance
├── Metrics
├── Effect
└── Cleanup

真正的自生长闭环应当是:

发现缺口
  ↓
生成候选能力
  ↓
隔离激活
  ↓
观察与验证
  ↓
保留或回滚
  ↓
沉淀为可复用组件
  ↓
持续评估
  ↓
升级或淘汰

5. Pi 与 DSH 的核心差异

Pi 与 DSH 并不是简单的替代关系。

它们关注的是两个不同层次的问题。

维度 Pi DSH
核心目标 构建开放、可定制的 Agent Harness 构建支持动态变化的组件运行时
能力来源 Extensions、Skills、Prompts、Packages 运行时组件及其依赖关系
主要变化发起者 用户、开发者,也可以由 Agent 辅助 用户、系统或 Agent
组件组织方式 扩展资源与 Package 动态依赖图
激活方式 启动加载、临时加载或重新加载 根据 Context 与 Coeffects 激活
生命周期 以扩展加载和配置为中心 Effect 与 Cleanup 对称管理
依赖治理 主要由扩展代码和包依赖处理 将运行时能力依赖显式化
失败处理 依赖扩展实现、进程或会话边界 通过 Cleanup 支持组件级回滚
演化方向 从固定产品走向开放平台 从开放平台走向受控自生长
核心价值 让 Harness 容易被改变 让 Harness 可以安全地持续改变

可以把两者的关系概括为:

Pi:
稳定核心
+
开放扩展点
+
能力分发机制

DSH:
组件化 Context
+
显式 Coeffects
+
可逆 Effect
+
动态生命周期

Pi 解决了「能力能否加入」的问题。

DSH 更关注「能力如何进入、如何协作、如何退出,以及如何在运行时形成新的结构」。

6. 从可扩展到自生长,中间还差什么

可以把 Agent Harness 的演进分为三个阶段。

6.1 第一阶段:固定能力

模型
+
固定提示词
+
固定工具

系统能够执行任务,但能力边界在设计阶段已经确定。

如果缺少能力,只能由开发者修改产品代码并重新发布。

6.2 第二阶段:开放扩展

稳定核心
+
Extensions
+
Skills
+
Packages
+
开放 API

用户和开发者可以持续增加能力。

Agent 也可以帮助编写扩展,但扩展的发现、安装、验证和维护通常仍然依赖人工流程。

Pi 代表了这一阶段的重要方向:Harness 不再是一个封闭产品,而成为可以根据个人工作流持续定制的平台。

6.3 第三阶段:受控自生长

稳定内核
+
能力缺口识别
+
候选组件生成
+
动态组件运行时
+
显式依赖
+
可逆副作用
+
隔离验证
+
自动回滚
+
能力沉淀
+
持续淘汰

Agent 不再只是等待外部为它安装能力。

它可以根据任务识别不足,提出新的能力结构,在受控环境中试验,并根据结果决定保留、修改还是撤销。

这时,Agent 的角色从能力消费者变成了能力形成过程的参与者。

7. 自生长不等于无限增长

「自生长」并不是 Agent 不断为自己增加工具和代码。

无限增加不是生长,而是熵增。

一个只会添加能力、不会清理能力的系统,最终会出现:

  • 工具数量不断膨胀;
  • 相似能力重复实现;
  • 组件版本相互冲突;
  • 权限范围持续扩大;
  • 上下文被大量描述占据;
  • 路由规则越来越难以理解;
  • 无效监听器和后台任务持续运行;
  • Agent 无法判断应该使用哪个工具;
  • 系统行为逐渐不可预测。

因此,自生长必须同时包含四种能力:

增加
+
选择
+
清理
+
遗忘

可以进一步表示为:

可持续生长
=
生成新能力
-
淘汰无效能力
+
重组已有能力

这也是 Cleanup 和生命周期管理的重要性所在。

真正成熟的 Agent 不仅要知道「我还需要什么」,还要知道:

  • 什么已经不需要了;
  • 什么正在产生负面影响;
  • 什么应该暂时停用;
  • 什么可以被更简单的组件替代;
  • 什么经验只适用于过去的环境。

遗忘不是生长的反面。

受控遗忘本身就是生长的一部分。

9. Pi 与 DSH 不是替代,而是演进

Pi 的价值在于证明了一件事:

Agent Harness 不需要是一个功能固定的封闭产品。

通过开放 Extension、Skill、Prompt 和 Package,Harness 可以被用户重新塑造,也可以让 Agent 辅助编写自身需要的扩展。

这已经比传统 Agent 产品向前迈出了一大步。

DSH 所代表的方向,则是在此基础上进一步抽象:

如果 Agent 可以创建新能力,那么这些新能力应该如何成为运行时的一等公民?

答案不能只是把更多代码放进扩展目录。

系统还需要:

  • 明确组件所处的 Context;
  • 显式声明组件依赖的 Coeffects;
  • 管理组件产生的 Effect;
  • 在组件退出时执行 Cleanup;
  • 根据环境变化重新计算能力拓扑;
  • 对候选能力进行验证;
  • 将有效能力沉淀下来;
  • 将失效能力安全淘汰。

因此,二者可以形成一种递进关系:

Pi:
让 Harness 可以被修改

        ↓

DSH:
让修改具有显式生命周期

        ↓

自生长 Agent:
让 Agent 可以发起、验证并沉淀修改

从这个角度看,DSH 不是对 Pi 的否定。

它是在 Pi 所代表的开放扩展思路上,将“变化”进一步提升为运行时的核心抽象。

10. Harness 正在成为 Agent 的生长环境

如果说 Pi 的核心贡献,是把 Agent 从一个封闭产品变成一个可编程、可扩展的平台,那么 DSH 所探索的下一步,就是让 Agent 逐渐成为自身能力变化的参与者。

它不只是使用已有工具,还可以:

  • 发现能力缺口;
  • 创建候选组件;
  • 调整能力组合;
  • 在受控环境中试验;
  • 根据结果保留或回滚;
  • 将成功经验沉淀为长期能力。

但自生长不等于无限增加。

一个只会安装新工具、写入新记忆、叠加新策略的 Agent,最终只会被自己积累的复杂度拖垮。

真正可持续的生长,必须同时包含:

发现
→ 生成
→ 激活
→ 验证
→ 选择
→ 清理
→ 沉淀
→ 淘汰

Pi 回答了:

如何让 Agent Harness 可以被持续扩展?

DSH 则进一步追问:

如何让 Agent 安全地参与自身能力的形成?

从 Pi 到 DSH,变化的不只是扩展机制,而是 Harness 的角色。

它正在从承载工具与插件的工程框架,转变为支持能力试验、筛选、重组和沉淀的运行环境。

未来的 Agent Harness 可能不再只是模型与工具之间的连接层。

它还将成为 Agent 管理自身变化的基础设施:

  • 允许新能力出现;
  • 阻止错误变化扩散;
  • 保存经过验证的经验;
  • 清除已经失效的结构;
  • 在保持稳定内核的同时持续重组外围能力。

只有当 Agent 既能增加能力,也能验证、回滚、清理和遗忘时,它才真正具备从「可扩展的软件」走向「可持续生长的系统」的可能。

以上。

聊聊 Harness:从 Agent 到组织

我们在落地 Agent 时面临的核心矛盾,是大模型的概率生成机制与工程系统所需的绝对确定性存在天然冲突。要获取大规模、可维护且值得信赖的代码,必须在系统外围构建 Harness。Harness 的本质是将不确定性转化为确定性。

提高信任度和可靠性需要极度压缩 Agent 的解决方案空间。我们必须放弃让模型「生成任何内容」的灵活性,转而采用包含大量技术细节的提示、规则和框架。特定的架构模式、强制执行的边界以及标准化的结构,构成了这套护栏的物理基础。

当前越来越多的团队在持续快速的产生代码,而这些演示很好看,当真的进入整个软件生命周期中,就会产生混乱,当越来越多的人随着时间的推移在仓库中堆砌代码,组织就开始堆人进行 review、反复返工,最后 AI 的吞吐量被人类注意力卡死,表面上用了 Agent,实际产能没上去,维护成本还更高。

当然,这是一种结果,也有人在过程中不停的构建基建,做 Harness 工程,整个代码不再是无序的扩张。从这个逻辑来讲,harness 的作用是把大模型输出从概率事件压回工程确定性的系统设计

Agent 的 Harness

harness 是什么

很多人把 harness 比作一个操作系统,模型是 CPU,但我觉得并不是。

如果模型真的是 CPU,那它接收指令后的执行结果应该是绝对严格且可预测的;但大模型本质上是一个概率引擎,它在潜空间里做的是模式匹配与概率生成。因此,harness 并不是像操作系统那样去调度底层硬件资源或分配内存,它更像是一套概率过滤器和对齐机制。它依靠纯粹的工程手段,把模型那种发散的、充满不确定性的「创造力」或「幻觉」,强行压缩进一条狭窄、严谨且符合人类预期的流水线里。

这种工程逻辑在实践中,体现为无处不在的防御性设计和反馈闭环。当模型吐出一串代码或一个决策时,harness 并不负责直接「运行」它,而是负责「质检」和「纠偏」。它通过静态检查、架构规则扫描、自动化测试和沙箱验证,把模型给出的「大概率正确」转化为工程上非黑即白的「通过或驳回」。正是这种让概率不断撞击确定性规则的过程,才使得最终沉淀到代码库里的产物是安全、可控且符合系统长期利益的。

harness 解决确定性问题的终极目的,是为了在系统中建立无需人工干预的信任,从而真正释放 AI 的吞吐量。如果没有这套逻辑,模型生成的代码越多,人类审查的负担就越重,整个组织的运转速度依然会被人类的注意力瓶颈卡死。

Martin Fowler 的博客中发表了 Thoughtworks 的技术专家的一篇文章,将 OpenAI 文章中所描述的 harness 分为三个方面:

上下文工程

上下文工程需要做到动态与静态的交织

单纯依赖超长 Prompt 无法解决复杂工程问题。上下文工程的核心在于构建代码库中持续增强的知识库,并打通 Agent 对动态上下文的访问路径。

静态知识库定义了系统的基础法则。我们将领域模型、API 契约和历史架构决策文档化,作为 Agent 初始化的基线上下文。动态上下文决定了 Agent 在运行时的决策质量。系统需要将实时的可观测性数据、测试覆盖率报告甚至浏览器导航状态,实时注入到 Agent 的工作流中。缺乏动态上下文的 Agent 就像蒙眼狂奔的打字机,产出的代码在语法上完美,在逻辑上完全脱离系统现状。

架构约束

架构约束是确定性的防线。

完全依赖 LLM 进行自我反思和代码审查,在生产环境中极度危险。架构约束必须由确定性的自定义代码检查器和结构测试来强制执行。

我们通过静态分析工具拦截不合规的依赖调用,利用 AST(抽象语法树)解析确保代码分层符合规范。当 Agent 试图在 UI 层直接发起数据库连接时,确定性的检查器会立即阻断该行为,并将具体的错误堆栈和修复路径作为反馈输入给 Agent。这种混合架构确保了系统的底线由死板的规则守卫,Agent 的创造力被严格限制在安全的沙盒内。

垃圾回收

垃圾回收主要是用于对抗代码熵增。

完全自主的智能体引入了代码库衰败的新问题。Agent 会精准且不知疲倦地复现代码仓库中已存在的模式,包含那些不均衡或不够理想的遗留设计。随着时间的推移,这种行为不可避免地导致系统架构漂移。

最初,人类开发者试图手动处理这个问题。团队过去每周五要花费 20% 的时间清理「AI 残渣」。这种依赖人力的做法毫无可扩展性。

我们将资深工程师的主观品味转化为机械规则,提炼为「黄金原则」并直接编码到代码仓库中,建立了一个循环清理流程。我们强制要求使用共享的实用程序包,禁止手工编写零散的辅助工具,确保不变式集中管理。我们严禁使用猜测性的数据探测,强制验证边界或依赖类型化的 SDK,防止 Agent 基于虚幻的结构进行构建。

系统定期运行一组后台 Agent 任务,扫描代码库中的偏差、更新质量等级,并发起有针对性的重构 Pull Request。这些 PR 大多可以在一分钟内完成审查并自动合并。这套机制的功能等同于内存管理中的垃圾回收。技术债务如同高息贷款,通过高频的微小重构不断偿还,远胜过让债务累积到系统崩溃。人类的架构品味一旦被捕获并规则化,就会无情地应用于每一行代码,每天自动发现并消灭不良模式。

AI Agent Harness 的工程化落地

从几个流行的框架来看,主要是从流程强化、规格沉淀、任务编排等逻辑上来做事情。

将这些逻辑拆开可以分为四个维度:

上下文工程

上下文工程主要是在规范层解决问题,其主要解决的「规则文件失控」的问题,实现规格沉淀与对齐,以及上下文工程的可控。

之前,我们习惯把所有规范塞进类似于单个 .cursorrules 文件,导致 AI 上下文过载且容易忽略细节。这一层落地的第一步是建立结构化、按需加载的规范体系。主要做到如下的点:

  • 规范模块化:将系统架构、数据库规范、错误处理等拆分为独立的结构化文档。
  • 按需检索:AI 不需要每次都通读所有规范,而是根据当前所处的任务阶段,动态检索并加载所需的上下文。
  • 任务记忆隔离:为每个独立任务建立物理隔离的工作区和日志。AI 每次开启新会话时,只读取当前任务的精确记忆,既解决了“跨会话失忆”,又屏蔽了无关信息的干扰。

以 Trellis 框架为例,Trellis 摒弃了单一庞大的全局提示词文件,而是采用 spec/ 目录将规范模块化(如拆分为 database-guidelines.md)。在执行任务时,它利用 tasks/ 目录下的 JSONL 配置文件,让 Agent 动态检索并按需加载上下文。

架构约束

架构约束的核心逻辑:用代码约束代码,实现闭环自愈。 口头约定或纯文本规范在 AI 面前是脆弱的,它极易为了「跑通逻辑」而破坏架构分层。

  • 规则代码化:将核心的架构依赖规则(例如“前端组件严禁直接调用数据库”)编写为静态分析脚本或自定义 Linter。
  • 带解释的强阻断:在代码提交或验证阶段强制执行这些拦截器。关键在于,报错信息不能仅仅是「检查失败」,必须输出高度结构化的指导:明确告诉 AI“为什么违反了规则”以及“正确的做法是什么”。
  • 自动修复:AI 读取到结构化的报错指导后,能够自动理解并修正代码,形成无需人类介入的自愈闭环。

反馈循环

核心逻辑:降噪处理,防范死循环。 LLM 的注意力会被长篇大论的日志(如几千行的覆盖率输出)稀释注意力,从而忽略真正致命的错误。 因此我们需要做到:

  • 零输出原则:改造验证脚本。如果测试通过,脚本应保持完全沉默;如果失败,只输出精简的错误堆栈和失败原因。
  • 强制验收清单:在 AI 试图标记任务「已完成」之前,系统应强制拦截,要求其对照需求文档逐项确认边界条件。
  • 防死循环干预:设定重试阈值。如果 AI 对同一文件连续修改多次且测试依然失败,系统应主动中断并强制其回滚代码、重新审视需求,防止 AI 陷入无效的「幻觉修 Bug」循环。

熵管理

熵管理主要是阻断「坏模式」的指数级扩散

核心逻辑:快速偿还技术债。 AI 复制坏代码的速度是指数级的。一旦允许一个临时的妥协方案合入主分支,AI 会在极短时间内将其复制到整个代码库。

  • 高频垃圾收集:彻底放弃“集中清技术债”的传统做法。每天必须安排固定时间,专门 Review AI 生成的代码(人工或 AI 自动),及时识别新引入的坏模式。
  • 规范资产的动态演进:一旦发现坏模式,立即让 AI 深度分析根因,并自动将正确的防范规则更新到规范库中
  • 团队级免疫:由于规范库与代码同源管理(存在于 Git 仓库中),当这段新规则被提交后,团队其他成员拉取代码时,他们的 AI 助手就能立刻“学会”这个新技能。这把偿还技术债的动作,变成了每天自动化、可积累的系统进化。

以 Cursor 为例,可以更新 Team Rules

组织级 Harness

聊完 Agent 的 Harness,再聊一下组织的。

人的角色已经变了

大家都知道康威定律,简单来说就是:设计系统的组织,其产生的设计受限于这些组织的沟通结构。

而系统设计到最后,也一定会遇到一个问题:谁来定义规则,谁来解释例外,谁来承担后果。

以前的软件开发分工相对稳定。PM 写需求,设计出稿,前后端分别实现,测试验证,运维发布。大家各自占一段链路,边界虽然有摩擦,但总体清楚。

AI 进来以后,边界开始模糊。

PM 已经可以直接产出前端原型,很多时候产出的还不是静态图,而是真能跑的页面代码。设计师也不再只是给稿子,很多交互和组件约束可以直接沉淀成生成资产。前端工程师花在纯页面搭建上的时间下降,开始更多介入状态管理、交互抽象、可维护性收拢。后端和算法也更早被拉进来,因为很多 AI 生成的原型一开始就会碰到真实数据和能力边界。

这是现在很多团队正在进行的转型。

如果组织还按旧的分工运转,Agent 会把协作缝隙快速放大。

组织级 Harness 要管什么

我理解的组织级 harness,重点在三件事:

  1. 定义新的协作接口
  2. 重新分配注意力
  3. 把责任从「谁写了代码」改成「谁定义了系统」

协作接口要前移

以前很多问题可以留到开发阶段再对齐。现在不行。

因为 PM 通过 AI 已经能直接产出前端代码,需求不再是文字说明,而可能是一个可交互原型;设计规范也不再只是 Figma 标注,而是可以半自动映射到组件约束;后端接口能力如果不提前讲清楚,前面的生成很容易一路偏到错误方向。

所以组织里的评审必须前移,重点也得改。

过去的需求评审,很多时候在讨论功能要不要做。现在要多讨论三件事:

  • 验收标准到底是什么
  • 哪些边界不能突破
  • 哪些部分允许先用原型推进,哪些必须工程化收拢后才能上线

这几个东西不提前定,后面会出现一个很常见的问题:原型阶段看起来进展飞快,进入工程化后才发现返工巨大。

注意力要重新分配

我现在越来越少鼓励资深工程师花时间逐行抠低风险代码。

这不是说 review 不重要,而是注意力要贵着用。

在 Agent 环境里,重要的工作变成了:

  • 定义验收标准
  • 设计架构边界
  • 提炼黄金原则
  • 识别系统性失败信号
  • 决定哪些异常值得阻塞主流程
  • 审核高风险改动和高影响面重构

反过来,低风险、重复性、局部性的东西,应该尽量交给自动化校验和后台清理任务。

如果一个组织还在让最贵的人力去看大批格式化差异、小工具改名、重复样板代码,那 harness 基本等于没有。

责任归属要重写

在 AI-Native 组织里,谁对结果负责?

PM 产出了页面代码,前端做了工程化收拢,Agent 自动补了测试,清理 Agent 又改了一轮共享工具。最后线上出问题,算谁的?

如果这个问题没有明确答案,团队会很快进入防御状态。每个人都怕接 AI 产出的锅,于是流程开始重新变重,所有人都试图把责任往后传。

所以组织级 harness 一定要明确责任模型。

可以按三层分:

  • 需求责任:谁定义了目标与验收标准,谁负责需求正确性
  • 架构责任:谁定义了边界、模式和约束,谁负责系统一致性
  • 发布责任:谁决定进入生产环境,谁负责风险接受

不要再执着于「谁手写了这行代码」。就像团队管理一样,最后拍板的人担责。

可落地的 AI-Native 研发流程

以下为我们当前在跑的流程:

需求生成

第一步由 PM 主导,但交付物不再只是 PRD,而是带验收标准的可运行原型

但是,原型代码不等于可直接上线代码。它的价值是澄清需求、暴露分歧、提前感知交互复杂度。

所以 PM 可以生成,但不能默认拥有工程决策权。最终所有的代码都需要前端工程师构建的工具链条,以及 AI 和人工的审核及合入。

联合评审

第二步是全员参与的需求评审与架构设计。设计、前端、后端、算法都要尽早介入。

这个阶段重点不是抠实现细节,而是确定:

  • 用户路径是否成立
  • 数据流怎么走
  • 状态边界怎么划
  • 哪些能力用现有服务承接
  • 哪些模块需要新增抽象
  • 风险点在哪
  • 验收怎么自动化

这这一步的产出结构化进仓库,因为后面它会直接成为 Agent 的约束输入。

工程收拢

第三步是工程化整合。这个阶段前端、后端、算法开始把前面的原型和需求收敛进正式系统。

这里 Agent 会大量参与,但人类不能退出。重点工作包括:

  • 把原型重构进现有组件和模块体系
  • 校正状态管理、错误处理、埋点、权限、监控
  • 对接真实接口和算法能力
  • 补齐类型、边界验证和回归测试
  • 处理跨模块影响面

这一段最考验 harness,因为原型代码最容易带着局部最优、全局失真、风格漂移的问题冲进主仓。

自动验证与灰度

最后一步是自动化测试、灰度发布和反馈回收。

这一步先由专门的工程团队来负责,加入部分的 AI 成分,固化系统。

从 Agent 到组织,真正难的是控制系统

很多人以为 AI 落地的核心挑战在模型能力、成本或者工具接入。我现在看,最大挑战更集中在三个词:环境、反馈回路、控制系统。

环境决定 Agent 看到了什么、能做什么、不能做什么。

反馈回路决定错误会被放大,还是会被系统吸收成改进信号。

控制系统决定生成能力增长之后,组织是变得更稳,还是更乱。

这三个东西做不好,模型再强也只是更快地产生问题。

做得好,哪怕模型能力没到最顶尖,系统一样能稳定进化。因为工程上真正稀缺的,从来不是一次惊艳输出,而是长期重复地产出靠谱结果。

组织的 AI-Native 化

组织的 AI-Native 化也是慢慢进货,逐步推进的,先从小范围试起,再根据结果不断调整规则和流程。并且各家有各家的风格和气质。

第一,选一条链路打透。不要一开始就全组织铺开。先找一个协作关系清楚、反馈周期短、风险相对可控的场景,比如中后台、运营工具、内部系统,或者低风险服务改造。重点不是让 AI 多写代码,而是先验证:信息怎么给、边界怎么定、错误怎么发现、问题怎么清理。

第二,先改规则,再谈效率。很多团队一上来就问产能能提升多少,但更重要的是:规则有没有沉淀下来,错误能不能回流,坏模式能不能及时发现并清掉。如果这些没做好,所谓提效往往只是把问题推后,甚至把混乱放大。

第三,把人的位置往上移。资深工程师要逐渐从大量写代码,转向定规则、画边界、看反馈;技术管理者要从盯人和排期,转向设计流程、分层风险、明确责任;产品可以更早参与原型,但不能越过工程判断。

组织真正变成 AI-Native,不是因为每个人都在用 Agent,而是协作方式已经围绕 Agent 被重新设计过。

模型当然重要,但不是决定性因素。真正拉开差距的,是谁先意识到:Agent 不是一个更快的开发者,而是一个高吞吐的生产单元。它会放大环境本身。规则清楚,它就放大规则;流程混乱,它就放大混乱。

所以到最后,harness 这件事谈的根本不只是 AI。

谈的是工程纪律怎么重新编码。

谈的是组织协作怎么重新布线。

谈的是我们怎么把概率生成系统,放进一个仍然要求长期维护、长期演进、长期负责的软件世界。

这是我理解的「从 Agent 到组织」。

以上。

深入 Claude Code 源码了解其记忆系统

最近做 Agent 的同学应该大部分都有研读 Claude Code 泄漏的源码,网上出了各种 AI 加持下的各种解读,教程,细节分析,甚至包括换了一种语言实现的版本,如 Python,Go,Rust 等等。感觉有点「一鲸落,万物生」的感觉。

之前学习了 Claude Code 的系统提示词,写了一篇关于记忆系统的提示词。今天我们再深入其源码,看看其实现的细节。

从其源码来看,

Claude Code 这套记忆系统把几类完全不同的问题拆开处理了:长期记忆、当前轮相关记忆、会话压缩摘要、子代理独立记忆。和 OpenClaw 不同,OpenClaw 使用了统一的 Memory Service,加上一个向量库做检索,Claude Code 走的是另一条路:文件系统优先,分层清晰,召回时机明确,代价可控

1. Claude Code 到底要记了什么

在 memoryTypes.ts#L14-L31 里,长期记忆的类型是的四类:

  • user
  • feedback
  • project
  • reference

这四类东西有一个共同点:它们都不容易从当前代码状态直接推导出来。用户习惯、项目背景、团队约束、外部系统入口,这些信息不写下来,下次对话就丢了。反过来,代码结构、文件路径、Git 历史、当前临时任务,这些内容源码里明确要求不要进长期记忆,因为它们本来就有权威来源。memoryTypes.ts#L183-L195

记忆系统只该保存「代码外的信息」和「会跨轮次继续影响决策的信息」。以编程为例,当我们把代码事实也塞进去,后面一定会出现双份真相。你会遇到一个很尴尬的局面:代码说 A,memory 说 B,模型开始摇摆。

2. 四层分工

第一层是 auto memory / team memory。这是长期记忆,负责跨会话保存信息。目录逻辑在 paths.ts#L79-L259 和 teamMemPaths.ts#L66-L94。

第二层是 relevant memories。这一层不关心长期存储,它只负责一件事:用户当前这一问,应该把哪几条历史记忆临时塞进上下文。入口在 findRelevantMemories.ts#L39-L141 和 attachments.ts#L2197-L2425。

第三层是 session memory。这层服务的是长会话压缩,不负责跨会话记忆。位于当前 session 下的 summary.md。sessionMemory.ts#L183-L350

第四层是 agent memory。子代理如果要持久化自己的经验,可以放 user/project/local 三种 scope 的独立目录。agentMemory.ts#L12-L177

这四层拆开之后,很多设计选择就顺了:

  • 长期记忆用文件,便于审计和手工修复
  • 当前轮召回走轻量检索,减少 prompt 污染
  • 长会话压缩用单独 summary,避免每次 compact 都从头总结
  • 子代理隔离状态,减少串味

3. 长期记忆为什么选文件

Claude Code 的长期记忆是使用的 Markdown 文件。每条记忆一个文件,外加一个 MEMORY.md 入口索引。这部分规则在 memdir.ts#L199-L316 里写得很明白。

源码里的写入约束是这样的:

'## How to save memories',
'',
'Saving a memory is a two-step process:',
'',
'**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
'',
...MEMORY_FRONTMATTER_EXAMPLE,
'',
`**Step 2** — add a pointer to that file in \`${ENTRYPOINT_NAME}\`. \`${ENTRYPOINT_NAME}\` is an index, not a memory — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. It has no frontmatter. Never write memory content directly into \`${ENTRYPOINT_NAME}\`.`,

实现位置见 memdir.ts#L219-L230。

还有有三个工程判断。

第一,MEMORY.md 只是索引,不承载正文。如果把所有记忆都堆到一个大文件里,前期简单,后期灾难。Claude Code 从一开始就做拆分,每条记忆单文件,这样更新一条信息时不会引起全量重写。

第二,frontmatter 强制有 description 字段,这个字段后面要参与召回。很多团队做知识条目,只写正文,不写检索摘要,最后靠 embedding 硬扛。Claude Code 反过来,它要求记忆写入阶段就产出一条高质量摘要。召回质量在写入那一刻就埋下去了。

第三,完全基于文件系统,调试成本低。你可以直接去目录里看文件,团队同步时还能走 Git 或远端同步链路。数据库方案最大的问题不在性能,在可观察性。出了问题你要查 schema、查索引、查 embedding 版本、查写入日志,排障很慢。

文件方案当然也有代价。文件一多,目录扫描成本会上升;MEMORY.md 入口过长也会逼近 prompt token 上限。Claude Code 后面靠动态召回机制兜住了这个问题,这个设计是连起来看的。

4. 写入链路

它怎么把记忆真正落盘

4.1 主模型直接写

长期记忆的第一条写入链路,是主模型自己写。系统 prompt 里已经告诉它记忆目录在哪、允许写什么、怎么写文件、什么时候写。

loadMemoryPrompt() 会把 memory rules 注入系统提示词,入口在 [loadMemoryPrompt] memdir.ts#L419-L507。这一段 prompt 并没有替模型做决策,它只是把写入协议放进脑子里:目录、类型、索引格式、读取时机、失效校验。

这意味着 Claude Code 对模型的假设很明确:模型可以自己判断「这条信息值不值得保存」,然后调用写文件工具去落盘。写入不是一个外置 API,写入就是普通文件操作。

这条路有个好处:反馈延迟很低。用户刚说完「记住这个偏好」,主模型当轮就能写,不用等后台任务。

4.2 后台抽取器补写

如果主模型这一轮没动手写,系统会在 turn end 触发后台抽取器。stop hook 在 stopHooks.ts#L141-L156:

if (
  feature('EXTRACT_MEMORIES') &&
  !toolUseContext.agentId &&
  isExtractModeActive()
) {
  void extractMemoriesModule!.executeExtractMemories(
    stopHookContext,
    toolUseContext.appendSystemMessage,
  )
}

真正逻辑在 extractMemories.ts#L329-L567。

这条链路里最重要的一段判断是:

if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
  logForDebugging(
    '[extractMemories] skipping — conversation already wrote to memory files',
  )
  ...
  return
}

位置见 extractMemories.ts#L345-L360。

它防的是双写。主模型已经写过,后台抽取器就别再重做一遍。很多系统做异步归档时忘了这件事,最后要么生成重复记忆,要么覆盖用户刚刚确认的内容。

后台抽取器的权限也非常收敛。createAutoMemCanUseTool() 明确规定,只准:

  • Read / Grep / Glob
  • 只读 Bash
  • memory 目录内的 Edit / Write

实现见 [createAutoMemCanUseTool] extractMemories.ts#L166-L222。

extractor 的职责:它只做归档,不许顺手验证代码,不许顺手修改业务文件,不许借机跑工具链。权限如果不锁死,后台代理迟早会从归档器膨胀成第二个主代理。

4.3 KAIROS 下的写法

KAIROS 模式更有意思。它不要求模型实时维护 MEMORY.md,新记忆先按天追加到日志文件里。规则在 [buildAssistantDailyLogPrompt] memdir.ts#L318-L370。

"This session is long-lived. As you work, record anything worth remembering by **appending** to today's daily log file:",
` \`${logPathPattern}\` `,
'Write each entry as a short timestamped bullet. Create the file (and parent directories) on first write if it does not exist. Do not rewrite or reorganize the log — it is append-only. A separate nightly process distills these logs into `MEMORY.md` and topic files.',

这条策略很适合长驻 Agent。会话存活时间长时,频繁重写 topic files 和索引很贵,冲突也多。先写 append-only 日志,夜间再蒸馏,吞吐更稳,模型也更不容易在白天工作时把记忆目录写乱。

5. 召回链路

它怎么决定哪段记忆该进来

Claude Code 的召回要分成两种看。

一种是静态注入,也就是固定随上下文加载的那些东西。另一种是动态召回,根据当前 query 临时挑选最相关的记忆文件。

很多系统只做前者,结果上下文越来越肥。很多系统只做后者,结果基本行为约束丢了。Claude Code 两条都做。

5.1 静态注入

getUserContext() 会构造一个 claudeMd 字段,位置在 context.ts#L155-L188。

核心调用是:

const claudeMd = shouldDisableClaudeMd
  ? null
  : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))

getMemoryFiles() 的实现很长,在 claudemd.ts#L790-L1075。它会统一加载:

  • Managed 指令
  • User 指令
  • Project 指令
  • Local 指令
  • AutoMem 的 MEMORY.md
  • TeamMem 的 MEMORY.md

然后 getClaudeMds() 把这些文件串成提示词内容,[getClaudeMds] claudemd.ts#L1153-L1195。

它给模型一个稳定的全局工作框架。它会知道项目规则、用户偏好、团队共享记忆索引。它适合放那些「大方向会持续生效」的内容。

5.2 动态召回

静态注入解决不了所有问题。长期记忆正文一多,全部塞进 prompt 代价太高。Claude Code 的处理方式,是每轮用户发言后启动一个相关记忆预取。

入口在 query.ts#L297-L304:

using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
  state.messages,
  state.toolUseContext,
)

这个预取不会阻塞主流程。到后面条件满足时再消费,query.ts#L1595-L1617。

真正检索逻辑在 attachments.ts#L2197-L2425。它会先决定搜索哪个目录:如果用户显式提到某个 agent,就搜 agent memory;否则搜 auto memory。

然后调用 [findRelevantMemories] findRelevantMemories.ts#L39-L141。

这里最有意思的点在于,它没有用向量库。它的步骤是:

  1. scanMemoryFiles() 扫 memory 目录里的 .md 文件,读 frontmatter,产出一个 manifest
    见 memoryScan.ts#L35-L94

  2. 用户 query + manifest 发给一个 sideQuery 模型
    见 findRelevantMemories.ts#L77-L141

  3. 让这个模型返回最多 5 个文件名

  4. 再去读取这些文件正文,截断到限定行数和字节数
    见 [readMemoriesForSurfacing] attachments.ts#L2280-L2333

这套方案的好处:

  • 没有 embedding 构建成本
  • 没有索引维护复杂度
  • manifest 很小,side query 很快
  • 召回逻辑对开发者可见,容易调

缺点也明确。召回质量强依赖 frontmatter 的 description。写入时 description 写差了,后面召回一定差。

6. 记忆怎么进入上下文

不是一处注入,是四处入口

很多人看 Agent 源码时老在问「上下文是在什么地方拼进去的」。这个问题本身就有误导性。Claude Code 里记忆的注入入口不止一个。

6.1 system prompt 入口

第一处是 system prompt。这里进来的内容主要是「记忆系统的使用规则」,比如什么时候读、什么时候存、什么时候验证失效。对应实现是 prompts.ts#L492-L527 调 loadMemoryPrompt()

这是行为层指令。

6.2 user context 入口

第二处是 getUserContext() 构造的 claudeMd。这里进来的是 CLAUDE.md、rules、MEMORY.md 这种比较稳定的文本。context.ts#L155-L188

这是稳定背景层。

6.3 attachment 入口

第三处是 relevant memory attachment。被召回的正文不会直接拼到 claudeMd,而是先变成 attachment,再由 messages.ts#L3743-L3756 包装成 <system-reminder>

return wrapMessagesInSystemReminder(
  attachment.memories.map(m => {
    const header = m.header ?? memoryHeader(m.path, m.mtimeMs)
    return createUserMessage({
      content: `${header}\n\n${m.content}`,
      isMeta: true,
    })
  }),
)

这意味着这些记忆是临时的、按轮次加载的、带 freshness header 的系统提醒。它的优先级和普通用户消息不同。

6.4 compact summary 入口

第四处是 session memory compact。上下文过长后,系统会把会话前半段替换为一条 summary message,summary 内容来自 summary.md 的裁剪版。sessionMemoryCompact.ts#L437-L503

这是上下文续命层。

四处入口分工以后,就能看明白为什么 Claude Code 的行为相对稳定:规则、稳定背景、临时相关信息、压缩摘要,各走各的通道,互相不抢角色。

7. 真正的压缩发生在哪里

很多人一听「记忆系统」,第一反应是长期 memory 压缩。Claude Code 里最成熟的压缩逻辑,实际上落在 session memory 上。

前面说过,session memory 是 summary.md,它本身就是会话结构化摘要。维护逻辑在 sessionMemory.ts#L272-L350。

当上下文真的不够时,系统优先尝试 trySessionMemoryCompaction(),sessionMemoryCompact.ts#L514-L619。

它的动作顺序:

  1. 先确认 session memory 功能和 compact 功能都开着
  2. 等待正在进行中的 session memory 抽取结束
  3. 读取 summary.md
  4. 如果还是空模板,放弃,退回传统 compact
  5. 计算需要保留的 recent messages 窗口
  6. summary.md 的裁剪版构造 compact summary
  7. 组装 boundary + summary + recent messages + attachments + hooks

这里的「保留 recent messages」特别关键。作者没有图省事把所有旧消息都抹掉,而是保留一段最近窗口。窗口大小由 [DEFAULT_SM_COMPACT_CONFIG] sessionMemoryCompact.ts#L56-L66 定义,默认:

  • minTokens = 10000
  • minTextBlockMessages = 5
  • maxTokens = 40000

保留窗口的计算在 [calculateMessagesToKeepIndex] sessionMemoryCompact.ts#L324-L397。

这个策略解决的问题是:摘要永远会损失细节,最近一段工作现场最好保留原始消息,模型续做时不至于失真。要是所有内容都只剩 summary,模型会失去工具调用上下文、局部错误信息、最近的计划变更。

更细的一层防御在 adjustIndexToPreserveAPIInvariants()。 sessionMemoryCompact.ts#L232-L314

它干的事情很硬核,也很必要:如果最近保留窗口里出现了 tool_result,系统必须把匹配的 tool_use 也补进来;如果 assistant 消息因为流式输出被拆成多个共享 message.id 的块,thinking 和 tool_use 也要一起补齐。否则 compact 后发给 API 的消息链会断,直接报错。

这一段代码说明作者踩过坑,或者至少认真想过 API 侧不变量。很多开源 Agent 框架在消息压缩这里写得很草,最后线上 bug 都长一个样:tool result 找不到 parent,thinking 丢了,message 合并失败。

8. session memory 自己也会被裁剪

就算 summary.md 已经是摘要,compact 时还会再做一次 section 级裁剪。逻辑在 [truncateSessionMemoryForCompact] prompts.ts#L249-L295。

过程如下:

  • # section 拆段
  • 每个 section 允许的大小用 MAX_SECTION_LENGTH * 4 粗略换算成字符数
  • 超过就按行保留前半部分
  • 最后插入 [... section truncated for length ...]

实际截断函数见 [flushSessionSection] prompts.ts#L298-L324。

这套逻辑谈不上优雅,语义理解也谈不上深入,但它有一个优点:非常稳。系统真的到了上下文极限时,保底截断总比把整个 compact 失败掉强。工程里很多时候要的是「退化可接受」,不是「完美压缩」。

然后 createCompactionResultFromSessionMemory() 把裁剪后的 session memory 包成 summary message。 sessionMemoryCompact.ts#L437-L503

这里还有一个细节:如果发生过裁剪,它会额外附一句话,告诉模型和人类完整 session memory 文件路径在哪。排障时你可以直接打开原始 summary.md,不用猜裁掉了什么。

9. KAIROS 的压缩逻辑和普通模式不一样

KAIROS 里还有另一种「压缩」,它压的不是当前上下文,而是长期事件流。

在 memdir.ts#L321-L349 里能看到,KAIROS 模式下白天写的是 append-only daily log。到夜间,/dream 流程会把这些日志蒸馏成 topic files 和 MEMORY.md

这是另一类压缩:

  • 输入是时间顺序日志
  • 输出是主题化长期记忆

session memory compact 处理的是「上下文窗口」问题。KAIROS dream 处理的是「长期事件沉淀」问题。这两类压缩混在一起看会非常乱,源码里其实已经把它们分得很开。

10 小结

这套设计的工程代价与收益

10.1 一些值得学习的点

第一,分层彻底。长期记忆、当前轮召回、会话摘要、子代理记忆,各自有自己的存储形态和注入入口。系统复杂度是被隔离开的。

第二,文件优先。排查方便,审计方便,人工纠错方便。很多团队高估了数据库和向量库的必要性,低估了可观察性的重要性。

第三,动态召回走轻量 manifest + side query。对 CLI Agent 这种高频交互场景,这个方案的性价比很高。它把复杂度留给模型的小规模选择,而不是重型检索基础设施。

第四,压缩时保 recent window,并修补 tool_use/tool_result 不变量。这一点极少有团队一开始就写对。

10.2 一些代价

第一,frontmatter 的 description 质量变成关键依赖。这个字段一旦写烂,召回效果会大幅波动。它省掉了 embedding 的复杂度,也把一部分压力前置给写入质量。

第二,双通道写入意味着状态机会更复杂。主模型可以写,后台 extractor 也能写。虽然代码里有跳过逻辑,但这类架构天然比单通道更需要小心。

第三,session memory 的 section 截断是粗粒度的。它靠字符数近似 token,再按行截断,这属于保底工程,不属于精细压缩。能用,谈不上漂亮。

第四,MEMORY.md 仍然有索引容量压力。即便动态召回已经分担了很大一部分负担,入口索引的组织质量依然重要。

10.3 我们能用什么

如果把这套思路迁移到我们自己的 Agent 系统,可以借鉴(抄):

第一,先拆问题,再选技术。你要先决定自己在解哪件事:跨会话长期记忆、当前轮检索、超长对话压缩、团队共享经验。不要一上来就建一个统一 Memory API。

第二,先用文件,再考虑数据库。只要你的系统规模还没逼到那个份上,文件系统几乎总是更划算。它便宜、透明、好调试。很多团队用数据库,是因为觉得那样「更像正经系统」,这个判断没什么含金量。

第三,把召回质量的责任前移到写入阶段。Claude Code 用 description 做 manifest 检索这件事,给我的启发很大。与其指望后面靠复杂召回算法弥补,不如要求写入时就产出高质量摘要和类型信息。

如果你们团队正在做本地化的企业内 Agent,我甚至建议先抄一版这种架构原型:
长期记忆用 Markdown + frontmatter,当前轮召回走 manifest + 小模型筛选,会话压缩单独维护结构化 summary。三周内就能跑起来。比起一开始堆向量库、事件总线、关系数据库,这条路短得多。

11. 其它

Claude Code 的记忆系统没有神秘技术。它的难点不在某个单点算法,在边界控制和时机设计。什么时候写,写到哪里,什么时候读,读多少,压缩后保留什么,这些问题都比「用什么模型做召回」更重要。

如果你把这篇文章里的结论压成一句工程建议,那就是:
先把长期记忆、短期相关记忆、会话压缩拆开,再谈检索和存储。

源码入口可以优先读这几组文件:

  • 长期记忆规则与路径:
    [memdir.ts] [paths.ts] [memoryTypes.ts]

  • 记忆静态注入与动态召回:
    [claudemd.ts] [attachments.ts] [findRelevantMemories.ts]

  • 会话记忆与压缩:
    [sessionMemory.ts] [prompts.ts] [sessionMemoryCompact.ts]

  • 团队共享记忆:
    [teamMemPaths.ts] [teamMemorySync/index.ts]

以上。