
让 Agent 连续做几次同类任务,很容易看出两种不同的遗忘情况。
一种是背景丢了。上次已经说过"用中文回答""示例必须能追溯到官方源码",新会话还得再交代。另一种是方法没有复用:明明做过一次完整的报告生成,下次又从头摸索资料、整理结构、检查格式。
把这些内容全放进 system prompt,确实能让模型看到。但项目规范、个人偏好和几十份操作手册混在一起,任何一次简单提问都要带上整套材料。
DeepAgent 把这两类信息放进了不同的通道:Memory 默认把配置的记忆正文带入模型请求;Skills 先放技能目录,正文等任务需要时再读取。
它们都通过 middleware 工作。不过,读文件、把内容交给模型、把新经验写回文件,是三件发生在不同位置的事。沿着源码把这三件事分开,才能解释"为什么改了记忆却没生效",也能判断"装了一个 skill,是否就一定会执行"。
01 从 create_deep_agent() 看,两条通道在哪里装进去
先看一段配置。下面沿用官方 Skills 文档 5 的文件后端用法,显式开启虚拟路径,并同时配置 memory:
from deepagents import create_deep_agentfrom deepagents.backends import FilesystemBackend agent = create_deep_agent( model="anthropic:claude-sonnet-4-6", backend=FilesystemBackend( root_dir="./agent-data", virtual_mode=True, ), memory=["/AGENTS.md"], skills=["/skills/"],)
运行前,磁盘上要有对应文件,模型服务也要配置好凭据。例如:
agent-data/ AGENTS.md skills/ langgraph-docs/ SKILL.md
这里的 /AGENTS.md 是 backend 中的虚拟路径,对应 ./agent-data/AGENTS.md;/skills/ 对应技能集合目录。配置路径不会自动创建这些文件。
在官方 graph.py 1 中,主 Agent 的组装逻辑会根据参数加入两个中间件:传入 skills 时加入 SkillsMiddleware,传入 memory 时加入 MemoryMiddleware。两者拿到同一个 backend 配置,最终随 middleware 列表交给 LangChain 的 create_agent()。
创建 Agent 对象,主要是在组装grap;文件加载发生在grap执行时。这一区别在远程存储或启动排障时很有用:对象创建成功,不代表记忆文件存在,也不代表技能已经发现。

两个中间件都使用了两类 hook:
before_agent:一次 invocation 开始时,检查内部 state,必要时加载材料。
wrap_model_call:每次模型调用前,从 state 组织本次请求需要的提示内容。
异步执行有对应的 abefore_agent 和 awrap_model_call。因此,一次任务里即使调用模型多次,也不需要在每次请求前重新扫描所有文件。
这里说的"常驻",是指默认会反复进入模型请求的那部分内容。它没有改变模型权重,也不代表文件在进程启动时就已经读好。
02 Memory 加载的是一份快照
MemoryMiddleware.before_agent() 中有一个值得停下来看的判断。下面两行摘自官方 memory.py 3:
if "memory_contents" in state: return None
它检查的是字段是否存在,而不是内容是否为空。即使 memory_contents 是空字典,也会跳过这次加载。
字段不存在时,中间件通过 backend 的 download_files() 读取配置的文件,将路径和 UTF-8 文本保存到 memory_contents。不存在的文件会被跳过;其他后端读取错误会抛出异常。
到了模型调用前,wrap_model_call() 委托 modify_request(),把这份 state 中的内容格式化,再追加到当前请求的 system message。多个 memory source 按配置顺序拼接,保留文件路径作为来源标记。
这不是"后面的记忆覆盖前面的记忆"。两份文件如果写了互相矛盾的约定,源码不会做语义去重或冲突合并;两段文字都会出现。应用需要提前整理冲突,不能把配置顺序当成一套可靠的规则优先级。
注入前,格式化函数还会剥离 HTML 注释。因此,文件中的 不进入这段 memory prompt。但原文仍保存在加载快照里,模型如果通过其他文件工具读取原文件,也不能假定注释继续隐藏。这项处理方便维护文档,不能用来藏凭据。
memory_contents 带有 PrivateStateAttr 标记。LangChain 将它定义为从输入、输出 schema 中省略的字段。"Private"描述的是接口可见性,不是加密,也不是"不会进入 checkpoint"。它仍属于图内部状态;是否随会话保存,要看图与 checkpointer 的配置。13
03 Memory 会引导写回,但不会替你完成整套记忆治理
只把 Memory 描述成一个"读文件、拼 prompt"的中间件,会漏掉它对写回行为的引导。
默认模板除了 ,还有一大段 。它会引导模型从用户反馈中提炼长期偏好,通过 edit_file 及时更新记忆,同时列出临时事项不应保存、凭据不应记录等规则。
模板也明确提醒:记忆内容是可能过时或有误的文件数据,不应当作隐藏的系统指令。即使放进 system message,文件里的一条约定也不能因此获得绕过用户要求和运行时权限的资格。
所以,DeepAgent提供了学习与写回的默认行为指引。但这是提示词引导模型决策,再由模型调用实际工具;并不是中间件里有一个每轮必跑的结构化提取器,也没有在这里实现去重数据库、过期调度或效果评估。
写回能否发生,至少取决于三件事:模型是否判断值得记、相关工具是否可用、后端是否允许写入。工具被限制或文件只读时,提示词不会凭空获得写权限。

还有一个更容易踩到的细节:后端文件改了,当前 state 里的快照不一定跟着变。
例如,某次执行已经把"优先简洁回答"加载进 memory_contents。之后 Agent 用 edit_file 把文件改成"复杂问题请展开解释"。文件写入成功,并不意味着这个中间件会自动重读文件、更新 memory_contents。后续调用仍然从 state 取内容。
新用户指令或工具结果可能已让模型知道这次修改,但那与"注入的 memory block 已更新"是两回事。沿用保存过的 thread state 时,下一次 invocation 也可能继续命中"字段已存在"的判断。
源码没有在这里实现文件监听或自动失效策略。需要刷新时,应用要安排内部状态更新或重新加载;从不含旧加载字段的新状态开始执行,才会重新走读取流程。仅把字段改成空字典或 None,仍然满足"字段存在"的跳过条件。
同样的边界也适用于 Skills 的元数据快照:给磁盘新增一份 skill,不意味着已有会话下一轮就会重新扫描。
这是一种读缓存的取舍。好处是少做重复 I/O,同一段执行使用一致的加载结果;代价是应用需要决定何时让旧快照失效。
04 Skills 的"按需加载",省的是哪一层
Skills 最容易被讲错的地方,是把"模型没有看到正文"说成"后端还没有读正文"。
在这个版本的 skills.py 2 中,发现过程相当具体:对每个 source 调用 backend.ls(),取它的直接子目录,拼出各目录下的 SKILL.md 路径,再用 download_files() 下载文件内容,解析 YAML frontmatter。
为了提取元数据,发现阶段实际上已经下载了各份 SKILL.md。只是正文没有因此被全部放进模型上下文;state 保存的是提取后的技能元数据。
也就是说,这套渐进披露首先优化的是模型输入的体积,并不意味着免去了发现阶段的文件读取成本。技能放在远程 backend 时,目录扫描与下载延迟仍需要测量。

默认情况下,模型先看到名称、描述和完整路径,还可能看到工具建议、兼容性信息等元数据。描述的作用,是给模型判断相关性提供依据;它不是一个 SDK 内置分类器的路由配置。
觉得某项技能相关时,模型按提示调用 read_file 读取对应 SKILL.md,返回的正文以工具结果进入上下文。它不会因为"激活技能"而自动变成另一段常驻 system prompt。
正文再指向参考材料、脚本或模板,Agent 才继续通过可用工具读取或执行。单独存在一个 scripts/ 目录,不代表运行环境就能执行脚本;文件读取工具和命令执行能力也不能混为一谈。
一个 skill 因此更接近可发现、可读取的操作说明。它可以描述流程,却没有像 LangGraph 节点那样,把每一步变成必须经过的状态转换。模型可能漏选、误选,也可能读完后没有遵守某一步。需要强制顺序的流程,应落实到程序控制和工具约束中。
发现规则会直接影响技能是否可用
source 要指向技能集合目录。如果传入 /skills/langgraph-docs/,而 SKILL.md 就在这个目录本身,这份 skill 不会按预期加载,因为扫描器寻找的是 source 的子目录。
frontmatter 的 name 和 description 不能为空。无效 YAML、缺少必需字段等情况会被跳过;但名字不符合规范,在 deepAgent 0.6.7 版本 中只是警告后继续加载,过长的描述则被截断。规范要求与当前实现的兼容行为,应分开看。2
多个 source 中出现同名技能时,合并代码是:
for skill in source_skills: all_skills[skill["name"]] = skill
这一次,后面的 source 确实会覆盖前面的同名条目,包括最终展示给模型的文件路径。比如 skills="/skills/base/", "/skills/project/",两个来源都包含 code-review 时,项目版本会成为目录中的那一项。它是按名称替换元数据,不是把两个技能正文合并。
这也解释了 Memory 和 Skills 在来源合并上的差异:
| 配置 | 多来源的实际处理 |
|---|---|
| memory=... | 按顺序拼接各文件正文 |
| skills=... | 按名称合并,后来源覆盖同名技能 |
还有一个不能只凭名字理解的字段:allowed-tools。这个版本会解析它,并在技能目录中展示工具建议;SkillsMiddleware 没有据此构造一个禁止其他工具执行的拦截器。安全规则需要落在真正的执行路径上,不能只写在 frontmatter 里。
05 中间件顺序影响什么
在主 Agent 的默认装配中,Skills 位于基础栈前部;用户中间件与 profile 扩展之后,是 Anthropic prompt caching,再之后才是 Memory 和可选的人工审批中间件。profile 排除配置还可能进一步改变最终栈。1
这个顺序不意味着模型"先认识 Skills,过一会儿再看到 Memory"。模型调用发生在请求组装之后,拿到的是已经处理过的完整请求。
顺序真正影响的是:哪个中间件先修改 request,后面的中间件基于什么内容继续修改,以及缓存标记落在哪里。

以 ChatAnthropic 请求为例,前部中间件已经追加的提示内容,先经过 AnthropicPromptCachingMiddleware。它会在当时 system message 的末尾打缓存标记,也处理工具及模型设置中的缓存配置。
之后,MemoryMiddleware 追加记忆正文和默认指引。工厂传入的 add_cache_control=True 使它在符合模型类型条件时,再给末尾内容块加一个断点。4
这样做的意义是保留一个 Memory 之前的前缀边界。Memory 更新时,前面未变的内容仍有机会复用缓存;不是说修改任何一段内容,都能保证前部命中。上游前缀变化、长度门槛、TTL 和服务端缓存规则,仍会影响结果。
这项优化也没有减少请求包含的逻辑上下文长度。Memory 和技能目录仍会占据上下文;缓存主要影响重复前缀的处理成本与延迟。换成不支持该中间件的模型,工厂配置会让缓存中间件忽略处理,Memory 和 Skills 的基本注入机制仍然可以工作。
更不能用缓存断点解释记忆刷新。prompt cache 是模型服务端对请求前缀的复用;memory_contents 是应用图里的加载快照。前者命中了没有,决定不了后者是否重新读取文件。
06 长期保存、共享范围和权限,分别由谁负责
一份文件被命名为 AGENTS.md,不会自动让它变成跨会话的长期记忆。MemoryMiddleware 负责读取和注入,文件的保存方式由 backend 决定。

StateBackend 的文件在图 state 里。配置 checkpointer,可以让同一 thread 的状态被保存和恢复;这与多个 thread 自动共享一份记忆不同。
FilesystemBackend 的文件在磁盘上。新会话如果访问同一份磁盘文件,可以重新加载已有经验。但目录权限、容器磁盘是否持久、是否多个用户共用目录,都是部署需要解决的问题。virtual_mode=True 限制这个 backend 的路径解析范围,不提供进程级沙箱。
StoreBackend 通过 namespace 组织文件。它可以让多个 thread 访问同一逻辑命名空间;具体数据能否跨进程保留,取决于传入的 Store。InMemoryStore 适合演示,进程退出后不会替你留下数据。
官方平台示例会使用 rt.server_info.assistant_id 作为 namespace 的组成部分。这需要相应的运行时信息。普通本地 Python 程序不能把它当成必然存在的默认值,还要显式配置可用的 Store。
共享范围同样需要认真选择。把所有用户放进一个 assistant 级 namespace,就意味着他们可能在维护同一份记忆;要做用户隔离,应从可信的应用身份确定作用域,再配合后端访问控制。不能让模型自己指定一个 user ID,就把它当成授权依据。
CompositeBackend 能按路径前缀把请求分发到不同后端,但路由本身不等于只读权限或租户授权。要让团队技能只读、个人记忆可写,需要相应的文件权限、后端策略或工具治理。Skills 中写着"提交前审批",也不能代替实际的审批拦截。
07 放进一个任务里看,哪些事情需要验证
假设 Agent 要定期整理技术资料。
"引用要能追溯到原始来源"是贯穿任务的约定,可以放进 Memory。怎样筛选资料、怎样核对版本、怎样排版,是一套按任务展开的方法,可以放进 Skill。两者可以协同,也可以引用同一份领域材料,没有一条技术规则禁止内容交叉;划分的依据是信息出现的频率与读取成本。
真正接入后,我会先看几次实际调用,而不是只看最终文章写得顺不顺。
换一个不含旧加载快照的新会话,检查记忆是否从正确的后端加载;给出相关任务,检查技能目录是否出现、模型是否读了正确版本的 SKILL.md;给出不相关任务,观察是否仍无谓地展开整份技能。
再改一次文件,分别观察"后端内容""内部快照""模型请求"这三个位置。它们不一致时,问题通常就能缩小到持久化、刷新或提示注入中的某一段。
例如,文件里已经更新了回答偏好,旧 thread 的 memory block 却还是原文。此时先查 memory_contents,往往比继续修改 system prompt 更能解释这次行为。
资料与源码
以下地址可复制到浏览器访问。源码固定到本文版本,官方指南用于说明使用方式;编号与正文引用对应。
1 create_deep_agent 主 Agent 装配
DeepAgent 0.6.7,graph.py:
2 Skills 加载、解析与提示注入
DeepAgent 0.6.7,skills.py:
3 Memory 加载、默认写回指引与内部状态
DeepAgent 0.6.7,memory.py:
LangChain 1.3.4,PrivateStateAttr 定义:
4 Memory 缓存标记与 Anthropic prompt caching
DeepAgent 0.6.7,Memory 缓存标记:
langchain-anthropic 1.4.4,缓存中间件:
Anthropic prompt caching 官方说明:
https://platform.claude.com/docs/en/build-with-claude/prompt-caching
5 DeepAgent 官方使用指南
Memory 指南:
https://docs.langchain.com/oss/python/deepagents/memory
Skills 指南:
https://docs.langchain.com/oss/python/deepagents/skills
Backend 指南:
https://docs.langchain.com/oss/python/deepagents/backends
6 文件后端源码与 Agent Skills 规范
DeepAgent 0.6.7,FilesystemBackend:
DeepAgent 0.6.7,StoreBackend:
Agent Skills 规范: