找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3558

积分

0

好友

466

主题
发表于 前天 23:55 | 查看: 18| 回复: 0

我在公众号已经陆陆续续写过好几篇 图解 Claude Code 原理 的文章,基本每篇都是万字长文,配上几十张图解。

图解Agent系列文章截图

说实话,开写第一篇之前我心里是打鼓的。这种硬核到要啃源码的技术文,真的会有人愿意读吗?

结果挺出乎我意料。这么硬核的图解原理文,竟然有好几篇阅读量冲到了 5w+,最高的一篇直接到了 10w+ 阅读了。

Claude Code 上下文窗口管理面试题帖子截图

Claude Code 多Agent实现机制面试题帖子截图

不是,林友们,你们一个个都这么硬核的吗?

既然大家爱看,那就接着写。

一个黄色的卡通狗头表情,眼神斜视

刚好,最近陆续收到不少读者留言,好几位读者点名想看一篇深度讲 Claude Code 记忆机制的文章

理由出奇地一致:面试被问太多次了。

这事我太能理解了。

Agent 的记忆机制,如今已经是个不折不扣的面试热点。

只要你简历上挂着一个 agent 项目,面试官大概率会追着问一句:「你这个 agent 的记忆机制,到底是怎么做的?」

面试官拿着简历问记忆机制,应聘者慌张的插画

那这一篇,我们就把 Claude Code 的记忆机制,从 源码分析 层面翻个底朝天。

照例,开写之前先把几个问题摆出来,这篇文章就是带你一个一个把它们想明白:

  • LLM 明明是「无状态」的,那它跟你聊到第 50 轮还像记得你,记忆到底存在哪?
  • 滑动窗口、对话摘要、向量检索这些主流方案,听上去都挺合理,到底差在哪?
  • Claude Code 不上向量数据库,那它究竟拿什么来存记忆?
  • 为什么一个 CLAUDE.md 不够用,非得拆成六个层级?
  • Claude Code 怎么做到让 agent「自己学、自己写、自己用」记忆这一整个闭环?
  • 这套设计里,到底有哪几条原则能直接抄进你自己的 agent 项目?
  • 面试官再问起记忆机制,怎么答才能让对方眼前一亮?

七个问题逐个想透,这道面试题你就稳了。

正文字数和阅读时间统计信息图片


一、先聊聊「LLM 其实没记忆」

聊 Claude Code 怎么做之前,我们先退一步,把「LLM 没记忆」这件事彻底说清楚。如果你已经很熟悉,可以直接跳到第二节。

LLM 的「金鱼记忆」是怎么回事

我们先做一个小实验。

你跟 ChatGPT 聊到第 50 轮的时候,它好像还记得你前面说过的话。

请你停一下,先猜一下:这个「记得」,到底是怎么发生的?

是模型本身把你说的话存进脑子里了?还是别的什么机制?

……

答案可能跟你想的不太一样。LLM 本身根本「记不住」任何东西,它是彻头彻尾无状态的。

每次你按下回车,对它来说都是「从头看一遍」:把系统提示词(system prompt)、所有历史对话、当前问题,全部塞进去,然后输出一个回复。

大模型无状态调用机制对比图:人脑记忆 vs 大模型每次重新看

你以为它记得,其实是因为你的客户端(比如 ChatGPT 的网页前端)偷偷把历史消息又一起发了过去。所谓「上下文窗口」,就是塞这些消息的最大长度。

打个比方,LLM 就像一只金鱼,记忆只有 7 秒。你跟它说完话,它转个身就忘。如果你想让它记得,得每次开口前先把过往对话再念一遍。

聊天场景下,这套办法还能凑合。毕竟聊天就是「你一句我一句」,每轮也就几百个 token(token 你可以粗略理解成模型眼里的「字」,一个汉字大概算一到两个 token),把历史全念一遍也撑得住。但放到 agent 上,就完全不够用了。

agent 真正缺的是哪种记忆

agent 跟聊天最大的不同,是它在长跑

调工具、读文件、调 API、再调工具、再读文件……跑着跑着上下文就爆窗口了。你不能指望它「每次都把昨天的对话全念一遍」,那点窗口光塞历史都不够。

更关键的是,agent 想记住的东西,跟「历史对话」其实不是一回事。

四格漫画:用户告诉Agent自己是十年Go后端,新会话后Agent忘记了,需要记住的是事实而非对话本身

你昨天可能说过一句「我是十年 Go 后端,第一次接触 React」。今天打开新会话问它一个前端问题,你希望它记住的是「这个用户后端经验丰富但前端是新手」这个事实本身,而不是「你昨天说过那句话」。这两者天差地别。

再举个例子。你跟 agent 强调过「测试别用 mock,要打真实数据库」。下次写测试你希望它直接照做,而不是你重新强调一遍。这种「规则类」的记忆,跟「对话历史」更是完全两码事。

所以你看,agent 真正想记住的,大概是这几类东西:

  • 用户画像:你是谁、擅长什么、知识水平如何
  • 行为偏好:你不喜欢什么,喜欢什么
  • 项目动态:当前项目要干啥、有什么截止日期
  • 外部指针:去哪查什么信息

光靠把对话历史存起来,再搞个 RAG 检索(RAG 即「检索增强生成」,先去资料库里查到相关内容、再拼进上下文给模型参考),根本解决不了上面这些问题

AI Agent 真正需要的四类长期记忆:用户画像、行为偏好、项目动态、外部指针

我觉得一个更贴切的比喻,是把 LLM 想象成一个得了失忆症的实习生

健忘的实习生坐在办公桌前,工位上贴满了各种便签,表现信息过载的状态

这个实习生很聪明,写代码、查资料、做调研样样都行。但他每天早上一来上班,啥都不记得

你必须在他工位上贴满便签:「你叫某某某」「这是你正在做的项目」「老板不喜欢 PPT 用斜体」……否则他每天都从零开始,干一天还干不出昨天的进度。

那「记忆机制」要解决的核心问题,就变成了:这些便签贴在哪、谁来贴、什么时候撕掉

在盘点之前,还得再说清一件事,免得后面看糊涂:记忆其实分两种。

一种是短期记忆,说白了就是上下文窗口本身,装着当前这轮对话;窗口快满了,就把旧消息压缩(compaction)一下腾地方。前面讲的「把历史重新念一遍」,管的就是它。

另一种是长期记忆,持久化存到磁盘、能跨会话活下来;前面说 agent 真正缺的那四类东西(用户画像、偏好、项目动态、外部指针),全属于这一类。

这篇文章重点拆的 Claude Code 记忆机制,是长期记忆这一半。至于短期那套怎么压缩上下文,我之前专门写过一篇(点这里查看),这篇就不展开了。

记好这条坐标轴。

业界给 agent 做记忆这件事,实打实研究了很久,方案五花八门,可惜大部分都不太够看。

下面就来盘点一下,你会看到有的方案在管短期、有的在管长期,但没一个真正够用。


二、业界主流的记忆方案为什么不够看?

讲 Claude Code 之前,我想先让你做一件事

假设老板现在拍着桌子让你给一个 agent 加记忆机制,你会怎么设计?闭上眼想 30 秒。

我猜你脑子里大概会冒出这几个思路:

  • 把最近几轮对话直接存下来?
  • 太多了就用 LLM 总结一下?
  • 上向量数据库做相似度检索?
  • 学操作系统,分热的常驻、冷的归档?

一个人坐在书桌前思考如何设计Agent记忆系统,周围有四个方案气泡

很正常,你能想到的,业界全部都试过。GitHub 上扒一扒开源 agent 框架,记忆机制大概就这四类。

我们一个一个过一遍,过完你会发现一件挺有意思的事:这些「看上去都很合理」的方案,真用起来全都不太够

方案一:滑动窗口 Memory

最直观、最暴力的一种。

原理是把最近 N 轮对话原样保留,超过 N 轮的就丢掉。LangChain 早期那个 buffer window memory 就是这思路(在新版本里这套已经被官方标记弃用了,但思路依然散落在很多框架里)。

滑动窗口记忆示意图:只保留固定大小的最近消息,旧消息被丢弃

听上去合理对吧?反正窗口有限,丢就丢呗。

但有个问题,你丢的不一定是没用的

举个例子。用户在第 1 轮说「我是后端工程师」,然后跑了 50 轮各种问答,第 51 轮问「这段 useState 怎么用」。滑动窗口一砍,「我是后端工程师」这句早就不在窗口里了。agent 给你按零基础前端讲,你还得再花两轮告诉它「我有十年开发经验」。

关键信息和无关信息混在一起被丢,是滑动窗口的硬伤。

方案二:对话摘要 Memory

那再聪明点。不直接砍,而是定期把旧对话用 LLM 总结一下,把摘要塞回上下文。对应 LangChain 早期的 summary memory。

对话摘要记忆机制示意图:将长对话压缩为简短摘要,但细节会丢失

这样原文丢了,但「精华」保留下来了。

听起来还不错?我们看个真实场景。

假设用户在 20 轮前说过一句「我们的 API gateway 用的是 Kong,不是 nginx」。LLM 做摘要的时候觉得这句不太重要,就压成了「讨论了一些技术栈细节」。等下一次你问「我们的 API gateway 怎么排查 502」,agent 完全不知道你用的是 Kong,按 nginx 给你答了一通。

重要的细节被压糊,这是摘要 Memory 的硬伤。而且 LLM 做摘要本身要耗 token、有延迟,每隔几轮就摘一次,成本不低。

方案三:向量检索 Memory

这是目前最热的方案。Mem0、Letta(原名 MemGPT)、Zep,甚至大部分自己搓的 agent 都是这思路。

向量检索记忆架构图:将对话转为向量存储,对新问题进行相似度检索

先说清楚 embedding 是什么。你可以把它理解成,给每段文字在一个高维空间里标一个坐标,意思相近的两段话,坐标也会靠得近。

向量检索就是基于这个坐标做文章。它把每条对话或每条记忆转成 embedding 向量(也就是上面说的那个坐标),存进向量数据库。

每次新对话来,先把用户当前的问题(query)也转成向量,然后跟数据库里所有记忆比一比坐标远近,召回最相似的前 K 条(业界叫 top-K)塞回上下文。

Mem0 是这条路上最响的选手,按他们 2025 年 4 月那篇论文(arxiv 2504.19413)的数据,在 LoCoMo 这个长对话记忆基准上能打到 91.6 分,落地框架也覆盖了一大票。

听上去这方案是不是无懈可击?我给你泼盆冷水。

第一个问题,相似不等于相关。你问「这段代码有没有 bug」,向量检索可能把过去所有讨论 bug 的对话都召回来,但其中只有一两条跟当前代码真正相关,剩下的全是噪音。模型一被噪音淹没,就开始胡言乱语。

第二个问题,召回不稳定。embedding 模型换一个,召回结果差别巨大。你今天用 OpenAI 的 embedding 表现好好的,明天换成开源的,记忆系统可能从「能用」变「不能用」。

第三个问题,维护成本高。要部署向量数据库(pinecone、qdrant、milvus 任选一个),要选 embedding 模型,要管 chunk 大小,要管索引更新,要管冲突合并……上线一套向量记忆系统,工程量比写 agent 主流程还大。

第四个问题最致命,用户没法看。你存进向量数据库的记忆是一堆 768 维浮点数,人脑根本读不懂。哪天 agent 给你召回了一条错的记忆,你想去看看是哪条记忆引起的?对不起,先把向量反查回原文,还要 debug 一通索引。

向量库 vs 明文便签:一堆浮点数难排查 vs 内容清晰易懂

方案四:分层存储 Memory

MemGPT(现在叫 Letta)那一派的方案。

原理是把记忆分成几层:core memory(常驻上下文)、recall memory(最近对话可召回)、archival memory(远期归档)。LLM 自己当操作系统,用工具调用主动在不同层之间搬数据

MemGPT 三层记忆架构:core memory 常驻、recall memory 按需召回、archival memory 冷归档

这套设计学术上确实漂亮,但工程上落地的反馈是「概念太多、迁移成本太大」。让 agent 自己管理三层记忆,意味着每个 prompt 都要训练它什么时候搬、搬哪条,复杂度直接翻倍。

而且分层归分层,搬数据的依据本质上还是「相关性匹配」,依然要靠 embedding 召回,前面那几个硬伤一个都没躲过去。

这四类方案的共同病根

你把四个方案摆一起,会发现它们其实有几个共同的硬伤。

第一,自由文本无约束。存什么、不存什么没规则,结果记忆库迅速膨胀成垃圾堆。

第二,不区分类型。「用户画像」「项目动态」「外部指针」全部一锅炖、用同一种方式检索,最后哪个都查不准。

第三,没有老化机制。一条记忆存进去就是永久的。今天你跟 agent 说「我们项目用 Kong」,半年后换成了 nginx,旧记忆还在告诉它你用 Kong。这种「权威的错误」比没记忆还糟糕。

第四,重检索、轻写入。所有方案都把精力花在「怎么查到」,但「该不该存」「存什么」这一步基本是放任的,导致垃圾进、垃圾出。

四类记忆方案缺陷总结图:都在和“上下文长度”这道天花板作斗争

带着这四个病根,我们来看 Claude Code 是怎么治的。


三、Claude Code 的两层记忆架构鸟瞰

那 Claude Code 是怎么避开这些坑的?

你可能会期待一个特别炫的方案,比如自研一个混合存储、训一个专门的记忆模型、上一套分布式索引。

但答案恰恰相反。Claude Code 走了一条「土到反直觉」的路。

它没用向量数据库,没用 embedding,没用任何复杂的存储引擎。它用的是磁盘上的 markdown 文件

是不是想笑?markdown 文件能管好记忆?

别急。等你看完整套机制,你会明白为什么这个看着「土」的方案,反而把向量检索那一套比了下去

讲细节之前,先给你一张地图,否则一头扎进源码容易迷路。

Claude Code 的记忆机制其实是两条独立的线,并行工作:

Claude Code 两层记忆架构:静态层 CLAUDE.md 六层级 和 动态层自动记忆系统

静态层是 CLAUDE.md 体系,本质是「声明式指令」。你写好放那里,agent 启动时全量加载。这一层解决的是「我们怎么协作」「这个项目要遵守什么规则」这种确定性的事情

动态层是自动记忆系统,本质是「学习式偏好」。

agent 在跟你互动的过程中,把它认为「值得记下来的事」自动写成记忆文件存到磁盘上,下次对话再按需检索。这一层解决的是「我从跟你的互动中学到了什么」这种不确定的事情

打个比方,静态层就像你工位上的「公司员工手册」,每个新员工入职都得看一遍;动态层就像你工位旁那本「自己的工作笔记」,写的是「老板不喜欢 PPT 用斜体」「张三的需求经常变」这种你慢慢摸索出来的东西。

静态层员工手册与动态层工作笔记对比插画

两层一起用,才是 Claude Code 记忆机制的完整答案。

接下来两节我们各自展开,先看静态层。


四、静态层:CLAUDE.md 的六个层级

很多人对 CLAUDE.md 的印象停留在「项目根目录放一个 md 文件,写点项目说明」。

但你打开 Claude Code 源码就会发现,CLAUDE.md 这套体系远比你想的复杂

为什么不能只有一个 CLAUDE.md

我们先想一个问题,如果只有一个 CLAUDE.md,会发生什么?

你很快会发现,想往里塞的「规则」根本不是同一个来源。公司级的强制策略(比如「禁止 commit secrets」)得全员生效、谁也改不得;你个人的习惯(比如 commit message 用中文)希望跨所有项目通用;项目自己的规则要签入 git、给团队共享。

还有几类更微妙的。本地调试用的约定不想签入 git,只想在自己机器上读到;团队一起摸索出来的经验,希望同步给所有成员;以及 Claude Code 自己从对话里学到的你的偏好,也总得有个地方落下来。

这六种来源,可见范围不同、谁能改也不同,硬塞进一个 CLAUDE.md 只会要么打架要么混乱。所以 Claude Code 索性把它拆成了六个层级

六个层级各管一摊

按加载顺序从低到高,长这样:

Claude Code 记忆加载 6 层级总览图

每一层的定位都很明确:

  • Managed:放在系统级路径,只有管理员能改。公司级强制策略走这层
  • User:放在用户家目录下,你自己的全局偏好走这层,无论在哪个项目都生效
  • Project:项目根目录的 CLAUDE.md.claude/CLAUDE.md,项目层规则,签入 git 让团队共享
  • Local:项目根目录的 CLAUDE.local.md,默认不签入 git,你自己用
  • Auto(源码标识符 AutoMem):项目级的自动记忆目录,Claude Code 自动写入的偏好,下一节单独展开
  • Team(源码标识符 TeamMem):在 Auto 目录下再开一个 team/ 子目录,团队共享的 AI 学到的偏好(需要 feature flag 开启)

Claude Code 六层记忆加载流程图:按顺序加载,逐层拼接

你可能注意到了,后两层 Auto 和 Team 存的其实是 MEMORY.md 文件,不是 CLAUDE.md。这里把六层统称「层级」,是因为它们同属一套加载体系、最后都拼进 system prompt,不必纠结文件名的差别。

六层之间是叠加关系不是覆盖关系。Claude Code 启动时把它们全部拼接进 system prompt,让模型一起看到。

CLAUDE.md 六层记忆叠加插画:层层叠加形成一份完整指令

@include:让 CLAUDE.md 互相引用

光分层还不够,还有个实际问题。

假设你们公司有一份「通用安全规范」,每个项目都得遵守,难道每个项目的 CLAUDE.md 都把它复制一遍吗?

太蠢了。

Claude Code 给的方案是 @include 指令。

你在 CLAUDE.md 里写一行 @~/company/security-rules.md,加载的时候它就自动把那个文件的内容读进来拼上,思路跟 C 语言的 #include 一模一样。

当然,背后还得防循环引用、防路径遍历这些工程坑,这里就不展开了。

条件规则:编辑 .tsx 才加载前端规范

层级和 @include 都讲完了,再看一个挺有意思的设计

如果你给项目写了一份很长的前端规范:React Hooks 用法、CSS 命名规则、Tailwind 配置原则……几百行。

但你只在编辑前端代码的时候才需要这套规范,编辑后端代码也加载,纯粹浪费 token。

Claude Code 在 .claude/rules/ 这个目录下支持条件规则。每条规则是一个独立的 md 文件,文件 frontmatter(md 文件开头用 --- 框起来的那段元数据)里可以写一个 paths 字段,用 glob 通配符(就是 *.tsx 这种写法)匹配:

---
name: 前端规范
description: React + Tailwind 项目规范
paths: ["**/*.tsx", "**/*.jsx"]
---

# 前端规范
...(规则正文)

加载的时候,Claude Code 会比对当前编辑的文件路径,只有匹配上的规则才会被拼进 system prompt,匹配不上的就跳过。

这个设计的妙处在于,它让 CLAUDE.md 不再是「一次性全塞」,而是「按需注入」。一个大项目可以有几十条规则,每条只在它真正需要的时候才占用 token,整体上下文窗口就省下来了。

规则文件路径匹配机制示意图:只加载匹配的规则

截断双保险:防长行索引炸弹

最后聊一个跟安全有关的小设计,挺巧的。

MEMORY.md 索引文件(下一节会讲是什么)要塞进 system prompt,所以必须有大小上限,否则索引膨胀就把上下文撑爆了。

按常规思路,限制行数就行了,比如「最多 200 行」。但 Claude Code 团队发现,有时候一条索引就是一行超长字符串。代码里有句注释:

p100 observed: 197KB under 200 lines

什么意思?他们观察到的极端情况是,一个 MEMORY.md 只有不到 200 行,但加起来 197KB。光看行数完全察觉不到,但塞进 system prompt 就是个灾难。

所以 Claude Code 用了双保险

export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000

两个限制任意一个先触发,就截断。截断的时候还会主动追加一条警告告诉模型「这个索引被截过了,部分内容没加载进来」,让模型自己有数。

这种「行数 + 字节」的双截断,本质上是在防御「长行索引炸弹」这种特殊形态的上下文溢出。

在做 agent 项目的时候,凡是用户可控的文本要进 system prompt,都建议参考这种双保险设计。

文件大小检查机制卡通插画:行数与字节大小双重限制,任一触达即拦截

静态层讲完了,但真正有意思的是动态层,下一节是文章的重头戏。


五、动态层:自动记忆系统的完整闭环

如果说静态层是「框架」,那动态层就是 Claude Code 的真正灵魂。

为什么还需要动态记忆

CLAUDE.md 这套体系再完善,也有一个根本局限:得你主动写

你昨天跟 Claude 抱怨「这个 mock 测试又骗过 CI 了」,今天换会话再写测试,它依然得你重新强调「别用 mock」。CLAUDE.md 又不会自己长出新内容。

理想的状态应该是:Claude 在跟你聊天的过程中,自动把它学到的东西记下来,下次你不用再说一遍。

这就是动态层要解决的问题,让 agent 自己学、自己写、自己用

动态记忆循环示意图:从对话中学习、写入记忆文件、下次检索使用

听起来像 RAG 那一套对吧?

但 Claude Code 做得很不一样。它没有用向量数据库、没有用 embedding,而是用了一套结构化的文件系统 + LLM 选择

我们一步步拆。

四种类型为什么这么分

要让 agent 自己写记忆,第一个问题就是:让它写什么?

你可能会想,「记下来对我有用的就行了呗」。

但「对我有用的」是个模糊到没法落地的标准。我们设想一下,如果你放任 agent 自由发挥,它一天能写出哪些东西?

它可能会记「用户今天问了 React 的问题」(流水账没用)、「这个函数用了 useMemo」(代码里 grep 一下就有)、「上次帮用户改了 5 行代码」(活动日志没用)……

三天下来,记忆库就成了一个啥都有、啥都查不准的垃圾桶。

那怎么办?

Claude Code 的答案干脆利落:只允许四种类型,其他一律不许写

export const MEMORY_TYPES = [
  'user',
  'feedback',
  'project',
  'reference',
] as const

短短四个词,背后的设计哲学非常重,我们来拆。

四种记忆类型图标:user 用户画像、feedback 偏好反馈、project 项目动态、reference 外部指针

user 用户画像,记的是「你是谁」。比如「这位用户写了十年 Go,刚接触 React」。这类记忆让 agent 的回答因人而异,对老兵和新手用完全不同的解释方式。

feedback 行为偏好,记的是「你不喜欢什么 / 你确认有效的做法」。比如「这位用户不希望每次回复后做总结,diff 就够了」。这类记忆是 Claude Code 最看重的一种,因为它直接决定了 agent 下一次行为的对不对

feedback 类型有一个强制结构,正文必须包含三段:

规则本身

**Why:** 用户为什么这么要求(往往是踩过的坑)
**How to apply:** 什么情况下生效

反馈记忆的三层结构图:规则本身、Why 为什么、How to apply 何时生效

为什么这么严?因为只记规则不记原因,遇到边界情况就抓瞎。

比如「不要用 mock 测试」,单纯一句话不够,加上「上季度 mock 测试通过但 prod 迁移挂了」这个 Why,agent 在边界情况下就能自己判断这个 case 该不该破例

project 项目动态,记的是「项目正在发生什么」。比如「移动端 3 月 5 号开始合并冻结」。

这类记忆其实跟 feedback 一样吃同一套强制结构:开头是事实/决定,然后 Why(这事为什么发生,是哪条约束或截止日期推动的)、How to apply(这件事应该怎么影响 agent 的建议)。

Claude Code 把这套约束同时套在 feedback 和 project 上,是因为这两类记忆最容易过期、最需要让 agent 自己判断「现在还该不该信」。

除此之外,project 还有个怪要求:必须把相对日期转成绝对日期

用户说「周四之前冻结」,存进去要变成「2026-03-05 之前冻结」。原因很简单,「周四」过几天就过期了,「2026-03-05」永远准确。

reference 外部指针,记的是「去哪查什么」。

比如「pipeline bug 都在 Linear 的 INGEST 项目里追踪」。agent 不需要知道外部系统的具体内容,只需要知道去哪里找

四种类型限定死了,记忆系统的「信息形态」就有了纪律。每次想存一条记忆,agent 必须先想清楚「这属于哪一类」,而不是一股脑往里塞。

该存什么 vs 不该存什么

跟「该存什么」同样重要的,是「不该存什么」。Claude Code 在系统提示词里明确列了一份禁令清单。

该存与不该存的对比信息图:明确列出了四类应保留和五类不应保存的信息

不该存的有这些:

  • 代码模式、架构、文件路径、项目结构(用 grep / CLAUDE.md 就能得到,存了反而和实际状态不一致)
  • Git 历史和最近改动(git log / git blame 是权威,记忆只会落后于真相)
  • 调试方案和修复方法(fix 已经在代码里,commit 已经记录了上下文)
  • CLAUDE.md 里已经写过的内容
  • 临时任务状态和当前对话上下文

为什么这条「不该存」清单这么重要?

因为代码是「活的」,记忆是「死的」。代码随时在变,但一条记忆存进去就定格了。如果记忆说「AuthService 在某个具体路径第 42 行」,但代码已经重构了,这条记忆就变成了一个「权威的错误」,比没有记忆还糟糕。

代码迭代造成记忆过时的教育类插画:重构后,旧记忆变成权威的错误

所以记忆系统的纪律是:只记代码推不出来的东西。这个原则贯穿了整套设计。

存储设计:单文件 + 索引

类型定下来了,下一个问题来了。

假设你已经积攒了 50 条记忆,它们放哪里,怎么让模型知道

最直觉的做法,全部塞进 system prompt。但你算一下,50 条每条 200 字,就是 1 万字、几千 token,system prompt 直接被记忆撑爆

那反过来呢,完全不塞?那模型根本不知道有这些记忆可用,写了等于白写。

两难。

我们先看 Claude Code 怎么平衡。每条记忆是一个独立的 .md 文件,文件头有一段 YAML frontmatter:

---
name: 不要用 mock 数据库
description: 集成测试必须连真实数据库
type: feedback
---

集成测试必须连真实数据库,不要用 mock。

**Why:** 上季度 mock 测试通过了但 prod 迁移挂了
**How to apply:** 所有标了「集成测试」的 case 都适用

frontmatter 里的三个字段是「身份证」:name 是标识,description 是「这条记忆是啥」(这一句话非常重要,决定了它能不能被检索到),type 是四种类型之一。

所有记忆文件放在一个目录下,目录里还有一个特殊文件 MEMORY.md,是所有记忆的索引清单。

记忆文件索引结构示意图:MEMORY.md 作为索引文件,列出所有子文件及其用途

这里有一个很关键的设计,回到刚才那个两难问题,Claude Code 的答案是:只塞「目录」,不塞「正文」

MEMORY.md 索引 → 始终加载进 system prompt
独立记忆文件 → 按需加载

是不是有点像翻一本厚厚的工具书?你不需要把整本书背下来塞脑子里,但至少得知道目录里都有哪几章,需要某章再翻到那一页。

落到 Claude Code 上就是:

  • agent 看到索引就知道有哪些记忆可选(看 name 和 description 就够)
  • 真正需要某条的时候,再把完整内容加载进来

这个设计还顺带解决了上一节提到的「截断双保险」问题:索引始终常驻,所以索引的大小必须严格控制,行 + 字节双限制就是这么来的。

对比插画:全塞模式撑爆上下文 vs 索引常驻+按需加载模式节省Token

写入:Extract Memories 代理

光设计好类型和存储格式还不够。记忆是怎么写进去的?

最朴素的做法你应该想到了,让主对话自己写呗。每轮结束让模型「想想这次有啥该记的」,然后自己写文件。

听上去合理。但你再仔细想想,会不会有坑?

至少两个坑很明显

第一个,模型分心。主任务都做不好,还要分一脑子去想「这条要不要记、属于哪类、放哪里」,回复质量会打折。

第二个,token 浪费。每轮都得在 system prompt 里塞一段「记一下偏好啊」的指令,每次都进、每次都算钱,全是冗余。

那 Claude Code 怎么躲开这两个坑?

它的方案是:每轮对话结束后,后台单独跑一个代理来抽取记忆

这个代理叫 extractMemories,触发时机是每轮 query loop 完整结束(也就是模型给出最终回复、没有任何 tool call 了),通过一个 stopHook 钩子触发。

Extract Memories 触发时机流程图:主对话循环结束后,stopHook 触发,后台 fork 代理开跑

源码注释把它的精髓讲得很清楚:

// Uses the forked agent pattern (runForkedAgent) — a perfect fork of the main
// conversation that shares the parent's prompt cache.

这里有个很妙的小心思。Extract Memories 代理不是从零启动一个新对话,而是「完美 fork(复刻一份)主对话」,复用主对话的 prompt cache(把已经算过的 system prompt 缓存下来,下次不用重算)。

这意味着什么?

意味着它不用重新加载几千 token 的 system prompt,只需要看着对话历史,决定「这次有没有值得记的东西」就行。整个抽取过程多花的钱很少。

提示词缓存复用机制插画:抽取代理复用主对话的 prompt cache,省时省钱又高效

抽取代理的逻辑大致是:

  • 扫一遍这一轮对话里用户的反馈、纠正、信息
  • 跟现有记忆比对,看有没有重复
  • 如果有新的值得记的内容,按四种类型分类,写一个新文件

注意「跟现有记忆比对」这步。代理会主动检测 hasMemoryWritesSince过滤掉它刚刚写过的内容,避免对同一件事反复写记忆。

还记得第二章那四个病根吗?

其中一个是「重检索、轻写入」,所有方案都在琢磨「怎么查到」,却放任「该不该存、存什么」。

Claude Code 专门派一个后台代理来干「写入」这件事,本身就是在治这个病根。

检索:用 Sonnet 选 top-5

存储讲完了,下一个问题来了。

下次对话来的时候,假设你有 100 条记忆,怎么挑出最相关的 5 条塞给主模型

你脑子里第一反应,大概是向量检索对吧?给每条记忆做个 embedding,给 query 也做个 embedding,算相似度、召回 top-5,干脆利落。

合情合理。但 Claude Code 偏偏不这么干

它的做法反直觉到你可能根本没往这上面想过,让一个小模型来挑

具体怎么挑?逻辑写在 findRelevantMemories 里。

第一步,扫描所有记忆文件的「头部信息」。它只读每个文件前 30 行,足够提取 frontmatter,不会读取记忆完整内容。这样即使有 200 个记忆文件,扫描开销也很小。

第二步,把所有记忆的「标题清单」拼成一段文本,发给 Sonnet:

Query: 用户当前的问题

Available memories:
- user_role.md — 后端工程师,新接触 React
- feedback_no_mock.md — 测试不要用 mock
- project_freeze.md — 3 月 5 号开始合并冻结
...

第三步,让 Sonnet 用 JSON schema 返回 top-5 文件名。系统提示词写得非常严苛:

Only include memories that you are certain will be
helpful based on their name and description.
Be selective and discerning.

意思就是「不确定的就别选」。宁可少选,不可错选。

为什么是 Sonnet 不是 Haiku?Haiku 更便宜啊。

我的理解是:Sonnet 比 Haiku 准很多,记忆相关性判错的代价远大于多花的那点钱。一旦把错的记忆塞进上下文,整个回复都会被污染。这道选择 面经 题的容错率非常低,所以宁可贵一点也要选准。

Sonnet 选择器工作流程示意图:扫描、甄别、返回最多 Top-5

还有几个细节值得讲。

  • alreadySurfaced 过滤:上一轮对话已经露过脸的记忆,这次直接排除。让 Sonnet 把 5 个名额花在新候选上,而不是反复挑同样的。

  • recentTools 过滤:最近用过的工具的「用法参考文档」不要选。因为 agent 当前对话里已经在实际使用这些工具了,再塞一份文档纯粹是噪音。但工具的「警告、坑点、已知问题」记忆要保留,「正在用」的时候恰好是这些警告最该出现的时候。

这两个细节看着小,但反映的是一个非常成熟的产品思维:检索不只是匹配,还要看「当前上下文已经有了什么」

信息筛选流程示意图:候选记忆经过已有记忆和近期工具两层过滤后,再由模型选择器选择

注入:system-reminder 包裹 + 老化警告

记忆被选出来之后,怎么塞进对话?

直接拼到 user message 里?不行,模型可能把它当成「用户刚说的话」。

Claude Code 的做法是用 <system-reminder> 标签包裹后注入:

<system-reminder>
This memory was saved 5 days ago. Verify it
's still accurate before acting on it.

[记忆内容]
</system-reminder>

系统提醒注入流程图:将记忆包装成&lt;system-reminder&gt;,并附带“年龄”和“验证”提示

注意上面那句「This memory was saved 5 days ago」。这就是记忆老化的设计:

今天 / 昨天 → 不警告
2 天以前 → 主动加警告,提醒模型「这是过去的快照」

为什么 2 天就警告?

因为软件开发节奏快,两天前的「项目正在做 X」可能今天就已经改了。让模型见到记忆的时候,自动带着「这是历史,不是现状」的心态去用。

这一条警告解决了向量检索方案最致命的问题,「权威的错误」。当一条记忆过时了,agent 不是闭着眼睛信,而是会主动去验证(比如 grep 一下、读一下当前文件状态),发现冲突就更新或忽略它。

Claude Code 还有专门一段「Before recommending from memory」的提示词,明确告诉模型:

  • 如果记忆里写了文件路径,先检查文件是否存在
  • 如果记忆里写了函数名或 flag,先 grep 一下
  • 如果用户要照你的建议动手了,先验证再说

「记忆说 X 存在」不等于「X 现在存在」,这句话写得非常重。

完整闭环图

到这里,自动记忆系统的完整闭环就讲完了。我们把它串成一张图:

Agent 记忆系统完整工作流程图:展示从用户对话到模型生成的完整闭环

抽取在右边、检索在左边,一写一读,记忆系统转起来。

而最妙的是,整套机制没有一行 embedding、没有一个向量数据库,全是结构化文件 + 一个小模型当选择器。简单,但比向量检索好用得多


六、几个值得借鉴的设计选择

读到这里,你不妨再停一下

把上面 Claude Code 的整套机制盖住,闭眼回想一下:你能从里头抽出几条「可以抄到自己 agent 项目」的原则吗

试着自己列一列,再往下看。

我自己看完源码,抽出了四条。你可以对照着看,跟你想的一不一样。

第一条:结构化优于自由文本

Claude Code 不让记忆是「自由文本流」,而是强制四种类型 + 强制 frontmatter。

为什么这么重要?因为自由文本无约束 = 垃圾堆。三个月后回头看自由文本的记忆库,你会发现里面什么都有,什么都查不准。

强制类型的本质是逼 agent 在写之前先做一次「分类决策」:这条信息属于「用户画像」还是「行为偏好」?是「项目动态」还是「外部指针」?想清楚才能写,写下来的东西就有用。

自由文本 vs 结构化对比插画:分类前先想清楚

迁移到自己的项目,这条原则可以变成:给记忆定一个 schema,强制每条都填齐。哪怕只是 4 个字段,比啥都不要强一百倍。

第二条:索引常驻 + 内容按需

这是我最喜欢的一个设计。

「全塞进 system prompt」会爆窗口,「完全不塞」agent 又不知道有什么。中间这道平衡,Claude Code 用一个 MEMORY.md 索引解决了:

  • 索引始终在 system prompt 里(让 agent 知道有什么
  • 完整内容按需加载(不浪费 token)

这个思路不只适用于记忆系统。任何「内容总量大但只有少数需要展开」的场景都可以套:长 RAG 文档、知识库、tool 列表、历史 PR……都可以做成索引 + 按需展开。

可复用的索引模式示意图:索引+按需,适用于文档、代码、团队知识等多个场景

第三条:廉价模型做选择题

向量检索流派把检索当成「数学题」(相似度计算)来做。Claude Code 把检索当成「选择题」来做,让小模型挑。

这是一个非常重要的思维转换

向量相似度是个数值,它告诉你「这条记忆跟 query 有 0.87 的相似度」。但 0.87 是相关吗?模型不知道,得靠阈值。阈值难定,错召回了你都不知道是哪一步出错了。

让 Sonnet 当选择器,它给你的是自然语言判断:「这条该选,那条不该选」。判错了也是模型可以解释的错,调起来比调阈值容易得多。

成本上,Sonnet 一次选择几百 token,比维护一个向量数据库的人力 + 算力便宜得多。

迁移到自己的项目:只要候选集合不大(几百以内),用小模型选择 > 向量检索

对比插画:算相似度做数学题 vs 做选择题,用便宜模型做选择题更划算

第四条:时间感知 + 主动验证

最后一条,是给「权威的错误」上锁。

记忆系统最大的风险,不是「召回不准」,而是「召回了一条已经过时的记忆,agent 闭着眼睛信」。这种错误特别隐蔽,因为模型把记忆当 ground truth,错也不知道是错。

Claude Code 的两道防线是:

  • 时间感知:2 天前的记忆主动加 stale 警告
  • 主动验证:记忆里写了文件路径 / 函数 / flag,使用前先 grep 一下

这两道防线背后是同一个心法:记忆不是真理,是历史快照。模型对待记忆的姿态应该像对待 git log,「这是过去发生的事」,不是「这是现在的状态」。

记忆安全的双重防线示意图:时间感知 + 主动验证,防止过期记忆误导代理


七、这道面试题该怎么答?

回到开头那个面试问题:

你的 agent 项目记忆机制是怎么做的?

先别急着往下看。请你把书合上,自己心里默答一遍。

如果你只能答出「上向量数据库存 embedding 做相似度检索」,那是 60 分答案,跟开头那位被问懵的读者一个水平。

如果你想答 95 分,可以这样组织:

第一步,先指出 LLM 是无状态的。记忆机制的本质是「在工位上贴便签」,所以核心问题是「贴在哪、谁来贴、什么时候撕」。

第二步,指出业界主流方案都有共同短板。滑动窗口、对话摘要、向量检索、分层存储这四类方案的共同病根,是自由文本无约束、不区分类型、没有老化机制、重检索轻写入。

第三步,举 Claude Code 作为反例。它没用向量数据库,而是用了「结构化文件 + LLM 选择」的设计。

展开来说:

  • 两层架构:CLAUDE.md 六层级(声明式)+ 自动记忆系统(学习式)
  • 四种类型:user / feedback / project / reference,强制分类决策
  • 索引常驻:MEMORY.md 索引始终加载、内容按需加载
  • 小模型选择:用 Sonnet 当选择器选 top-5,过滤已露脸 + 过滤工具文档
  • 老化警告:2 天前的记忆主动加 stale 提醒,逼模型主动验证

第四步,给出可迁移的设计原则:结构化优于自由文本、索引常驻 + 内容按需、廉价模型做选择题、时间感知 + 主动验证。

到这里,面试官不光会觉得你看过 Claude Code 源码,还会觉得你能把这套思路用在自己的 agent 项目里

如果你能答到这个程度,下次再被问到记忆机制这道题,大概率能拿下 offer。

面试反杀场景插画:应聘者自信讲解Agent记忆机制,面试官听得很满意


最后

最后说一句,记忆机制这套设计的精髓。

其实跟 Claude Code 的整体哲学高度一致:不堆复杂度,把已经成熟的简单组件(文件系统 + LLM)组合出比花哨方案更好用的东西

这套设计哲学,正是 云栈社区 上许多资深开发者推崇的——用第一性原理思考,而非盲目追新。




上一篇:MybatisPlus Pro实战:一行代码搞定SpringBoot增删改查与分页
下一篇:HTTP/2炸弹漏洞:NGINX等5大服务器中招,家用带宽即可打崩站点
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-4 02:51 , Processed in 0.781506 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表