标签归档:harness engineering

聊聊 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]

以上。