SKILL.md 技能文档
能力目标
给智能体装上跨会话记忆:约定 /memories/ 前缀下的文件走持久后端、其余路径仍走临时状态,换一个全新会话标识后智能体仍能自发读回并答对;并能按用户隔离记忆、在本地内存实现与数据库实现之间平滑切换。
前置
- 已能构造智能体、并知道它带
write_file、read_file、ls这套虚拟文件工具(见 bootstrapping-deepagents-env)。 - 两个必须先分清的对象:检查点保存器按会话标识存取状态快照、只管同一会话内的连续性;键值仓库独立于任何会话、是跨会话记忆的载体。两者是正交的两条轴,不能互相替代。
- 不配任何持久化时用的是默认的状态后端,文件存进绑定单次会话的状态通道,换会话就没了。
实操流程
先复现一次失忆,建立基线。同一个智能体,第一次调用写文件、第二次换一个会话标识再问:
agent.invoke( {"messages": [{"role": "user", "content": "请把这个事实记到 /notes/user.md:……"}]}, config={"configurable": {"thread_id": "thread-1"}}, ) result_2 = agent.invoke( {"messages": [{"role": "user", "content": "读 /notes/user.md,告诉我……"}]}, config={"configurable": {"thread_id": "thread-2"}}, )第二次会拿到「文件不存在」,且
result_2["files"]为空——换会话标识就是换了一块独立状态。建一个键值仓库实例,全程复用同一个对象:
from langgraph.store.memory import InMemoryStore store = InMemoryStore()配组合后端的路由:只让约定前缀走持久后端,其余仍走临时后端:
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend backend = CompositeBackend( default=StateBackend(), routes={ "/memories/": StoreBackend(namespace=lambda rt: ("demo-user",)), "/cache/": StateBackend(), }, ) agent = create_deep_agent(model=model, backend=backend, store=store)兜底后端接管所有没命中前缀的路径;命名空间决定数据存进仓库的哪个隔离区。
两次会话调用,只换会话标识:
agent.invoke( {"messages": [{"role": "user", "content": "请把这个事实写入文件 /memories/user.md,内容就是:……"}]}, config={"configurable": {"thread_id": "thread-A"}}, ) result_2 = agent.invoke( {"messages": [{"role": "user", "content": "请读取文件 /memories/user.md,告诉我……"}]}, config={"configurable": {"thread_id": "thread-B"}}, )要多用户隔离就把命名空间换成区分用户的值。本地开发用固定元组,上生产再换成从运行时上下文取身份的可调用写法:
routes={"/memories/": StoreBackend(namespace=lambda rt: ("user-123",))}要扛住进程重启就换持久化仓库实现,其余代码不动:
from langgraph.store.postgres import PostgresStore store = PostgresStore(conn_string="postgresql://...")迁到自己的场景只改三处:写入的内容字符串、文件名(保持在约定前缀下)、命名空间取值。架构代码一行不动。
校验回路
- 第二次会话的回复里包含第一次写入的事实,且这一轮的工具调用记录是
['read_file']——说明智能体是自发去取文件的,不是被系统硬塞的。这条行为层证据比「文件字节还在仓库里」更硬。 - 第一次会话打印的
files键里看不到你写的那个路径,这不矛盾:持久后端的文件进的是键值仓库、不进状态通道,正因如此它才能跨会话存活。 - 直接查仓库做存储层断言:
list(store.search(("demo-user",))),每条的键是剥掉前缀后的路径。 - 做四档对照确认两条轴正交:不配检查点保存器时同一会话第二次调用就读不到;配上后同一会话读得到、换会话读不到;用组合后端把约定前缀路由到键值仓库后,不配检查点保存器换会话也读得到。
- 同一个智能体下,写到未命中前缀的路径换会话读不到、写到约定前缀下换会话读得到——路径前缀本身就是持久开关。
常见陷阱
- 中途新建了仓库实例:最常见的「明明配对了却读不回」。内存实现的数据就在那一块进程内存里,新建一个等于开了一块空内存。全程复用同一个对象。
- 抄官方的命名空间写法在本地跑:从运行时上下文取用户身份的写法在本地会报
AttributeError: 'NoneType' object has no attribute 'user',因为本地没有服务端上下文。本地一律用固定元组,生产再换回去。还有一条更早的报错路径:完全脱离图执行去访问命名空间会报「运行时不可用」,两条要分清、别排查错方向。 - 以为仓库里有目录树:底层是一组扁平的键值对,键是剥掉路由前缀后的路径。
ls能列出子目录是把所有键按分隔符切分临时合成的视图,仓库里并没有目录实体。设计记忆结构时还要知道搜索是先拉全量再本地过滤,同一命名空间下条目越多越重。 - 写路径时漏掉尾部斜杠:写成不带尾斜杠的前缀本身算精确命中路由根目录,剥前缀后路径变成根。
- 配了多条重叠前缀不知道谁优先:按前缀长度倒序匹配、最长的先命中。前缀互不重叠时这条规则永远轮不到出场;需要「子目录走更细的后端」时才配重叠前缀。
- 把跨会话当成永久:内存实现指的是进程内内存,进程重启数据清零。要扛重启换持久化实现。
- 写错后端参数名:本地磁盘后端的目录参数名是
root_dir,写成别的会报unexpected keyword argument。 - 把它和常驻注入型记忆混为一谈:后端持久化是「文件存哪 + 按需读回」,智能体要主动调
read_file才拿得到;另有一类中间件在每次运行开始前就把约定文件的内容注入系统提示、零工具调用就带着记忆开局。一个可见信号就能区分两者:这一轮有没有read_file调用。两者适用场景不同,不是替代关系。
适用范围与前置条件
- 已能构造智能体、并知道它带
write_file、read_file、ls这套虚拟文件工具(见 bootstrapping-deepagents-env)。 - 两个必须先分清的对象:检查点保存器按会话标识存取状态快照、只管同一会话内的连续性;键值仓库独立于任何会话、是跨会话记忆的载体。两者是正交的两条轴,不能互相替代。
- 不配任何持久化时用的是默认的状态后端,文件存进绑定单次会话的状态通道,换会话就没了。
怎么使用
使用步骤
先复现一次失忆,建立基线。同一个智能体,第一次调用写文件、第二次换一个会话标识再问:
agent.invoke( {"messages": [{"role": "user", "content": "请把这个事实记到 /notes/user.md:……"}]}, config={"configurable": {"thread_id": "thread-1"}}, ) result_2 = agent.invoke( {"messages": [{"role": "user", "content": "读 /notes/user.md,告诉我……"}]}, config={"configurable": {"thread_id": "thread-2"}}, )第二次会拿到「文件不存在」,且
result_2["files"]为空——换会话标识就是换了一块独立状态。建一个键值仓库实例,全程复用同一个对象:
from langgraph.store.memory import InMemoryStore store = InMemoryStore()配组合后端的路由:只让约定前缀走持久后端,其余仍走临时后端:
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend backend = CompositeBackend( default=StateBackend(), routes={ "/memories/": StoreBackend(namespace=lambda rt: ("demo-user",)), "/cache/": StateBackend(), }, ) agent = create_deep_agent(model=model, backend=backend, store=store)兜底后端接管所有没命中前缀的路径;命名空间决定数据存进仓库的哪个隔离区。
两次会话调用,只换会话标识:
agent.invoke( {"messages": [{"role": "user", "content": "请把这个事实写入文件 /memories/user.md,内容就是:……"}]}, config={"configurable": {"thread_id": "thread-A"}}, ) result_2 = agent.invoke( {"messages": [{"role": "user", "content": "请读取文件 /memories/user.md,告诉我……"}]}, config={"configurable": {"thread_id": "thread-B"}}, )要多用户隔离就把命名空间换成区分用户的值。本地开发用固定元组,上生产再换成从运行时上下文取身份的可调用写法:
routes={"/memories/": StoreBackend(namespace=lambda rt: ("user-123",))}要扛住进程重启就换持久化仓库实现,其余代码不动:
from langgraph.store.postgres import PostgresStore store = PostgresStore(conn_string="postgresql://...")迁到自己的场景只改三处:写入的内容字符串、文件名(保持在约定前缀下)、命名空间取值。架构代码一行不动。