《Deep Agents 实战笔记》第 8 篇 | 基于 deepagents (Python) 官方文档 原文:https://docs.langchain.com/oss/python/deepagents/memory 原文:https://docs.langchain.com/oss/python/deepagents/skills

上一篇我们解决了"长任务跑得完"的问题。但还有个更根本的缺憾:Agent 跑完就忘。你告诉它"我喜欢简洁的回复",下次对话它照样长篇大论;你们团队打磨出一套写作规范,每开一个新会话就得从头交代一遍。

Deep Agents 的答案朴素得让人意外:记忆和技能,都是文件系统里的 Markdown 文件。 记忆让 Agent 记住"你是谁",技能让它学会"怎么做"。没有向量数据库,没有微调,就是纯文本。这一篇拆开看这两套机制,最后动手写一个"团队写作规范"技能。

一、记忆的工作方式:读、写、沉淀

Deep Agents 把记忆作为一等公民,采用基于文件系统的记忆,三步循环:

  1. 指向记忆文件:创建代理时把路径传给 memory=,比如 /memories/AGENTS.md;
  2. 代理读取记忆:记忆文件始终加载进系统提示词(与技能的按需加载不同,记忆没有渐进式披露——所以务必精简);
  3. 代理更新记忆:学到新信息时,用内置的 edit_file 工具更新文件,变更持久化,下次对话生效。

按更新时机分两种模式:

方式优点缺点
热路径(对话期间写入,默认)记忆立即可用,对用户透明增加延迟,代理必须多任务处理
后台整合(对话之间写入)不产生面向用户的延迟,可跨多个对话综合要到下次对话才可用,需要第二个代理

后台整合的做法很工程化:部署一个独立的整合代理,读取最近的对话历史、提取关键事实、合并进记忆 store,用 cron 按时间表触发。文档里有个值得抄的细节:cron 间隔必须与整合代理的回看窗口同步——示例里 cron 每 6 小时跑一次,工具就回看 6 小时;跑得太频繁会重复处理,跑得太疏会漏记忆。

另外还有情节记忆:深度代理本来就在用 checkpointer,每次对话作为一个被 checkpoint 的线程持久化——把线程搜索包装成工具,代理就能回看"这个问题当初是怎么解决的",而不只是"学到了什么结论"。

二、记忆放哪:文件态与 Store 态

记忆是文件,但文件存在哪,决定了它的生命周期。这是理解记忆机制的关键一刀:

  • 默认 StateBackend(文件态):工作记忆文件存在代理状态里,状态只在单个线程内持久化——会话结束就没了,这是短期记忆;
  • CompositeBackend + StoreBackend(Store 态):把特定路径(通常是 /memories/)路由到 LangGraph Store,实现跨线程持久化——这才是长期记忆。
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()  # 部署到 LangSmith 时使用平台 store

agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    memory=["/memories/preferences.md"],
    skills=["/skills/"],
    backend=CompositeBackend(
        default=StateBackend(),
        routes={
            "/memories/": StoreBackend(
                namespace=lambda rt: (rt.server_info.user.identity,),
            ),
        },
    ),
)

Store 的**命名空间(namespace)**就是记忆的作用域,不同的 namespace 函数划出完全不同的记忆边界:

flowchart LR
    AG["同一个深度代理"] --> NS1
    AG --> NS2
    AG --> NS3
    subgraph NS1["命名空间:assistant_id"]
        M1["代理级记忆<br/>所有用户共享"]
    end
    subgraph NS2["命名空间:user_id"]
        M2["用户 A 的记忆"]
        M3["用户 B 的记忆"]
    end
    subgraph NS3["命名空间:org_id"]
        M4["组织策略与合规规则<br/>通常设为只读"]
    end
    style AG fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9
    style NS1 fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
    style NS2 fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
    style NS3 fill:#fdeef0,color:#701d2a,stroke:#c94a5e

正在渲染图表…

代理级记忆(assistant_id)让代理拥有随时间演进的持久身份;用户级记忆(user_id)保证用户 A 的偏好绝不泄漏到用户 B 的对话;组织级记忆放合规规则,通常设为只读——这里有个安全考量值得记住:如果一个用户可以写入另一个用户会读取的记忆,恶意用户就能向共享状态注入指令。缓解手段是默认用户作用域、共享策略只读、写共享记忆前加人机协同审批。

还有个现实问题:多个线程并行写同一份记忆文件会"后写胜出"。文档的建议很务实:代理级或组织级记忆用后台整合来串行化写入,或者把记忆按主题拆成独立文件减少竞争。单次写入丢失也不用慌——LLM 通常足够聪明,能重试或优雅恢复。

三、技能:SKILL.md 与渐进式披露

如果说记忆是"始终在场的背景知识",技能就是"按需取用的操作手册"。一个技能是一个包含 SKILL.md 的目录,遵循 Agent Skills 规范:frontmatter 里写 name 和 description,正文是技能被激活时代理要遵循的指令,目录里还可以带脚本、参考文档和模板。

它解决的是上下文膨胀的老问题:把所有指令塞进系统提示词,会在与当前任务无关的信息上浪费 token。Deep Agents 的解法是渐进式披露(progressive disclosure)——分三级加载,每一级只在任务需要时才引入更多细节:

级别加载内容时机
1. 元数据frontmatter 中的 name 和 description代理启动时,对所有技能
2. 指令完整的 SKILL.md 正文技能被调用时
3. 资源scripts/、references/、assets/ 辅助文件指令引用到它们时
flowchart TB
    subgraph L1S["第 1 级:启动时"]
        A["扫描技能路径,解析 frontmatter<br/>只注入 name 和 description"]
    end
    subgraph L2S["第 2 级:技能被激活"]
        B["read_file 读取完整 SKILL.md 正文"]
    end
    subgraph L3S["第 3 级:指令需要时"]
        C["按需读取 scripts / references / assets"]
    end
    A -->|"任务与某技能描述匹配"| B
    B -->|"指令引用到辅助文件"| C
    style A fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
    style B fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
    style C fill:#f4effb,color:#4a2170,stroke:#8b52c9

正在渲染图表…

启动时,系统提示词里只有每个技能的一行摘要;任务与某个技能的描述匹配时,代理才 read_file 读全文;辅助文件更是直到指令点名才加载。加载越深,细节越多,上下文却始终只装当前需要的东西。

这也解释了为什么描述字段是技能的生死线:发现阶段,代理对每个技能唯一能看到的就是 description。官方给的好坏对比非常直白:

# 好:明确说明做什么、何时使用,带可匹配的关键词
description: >-
  Extract text and tables from PDF files, fill PDF forms, and merge
  multiple PDFs. Use when working with PDF documents or when the user
  mentions PDFs, forms, or document extraction.

# 差:过于模糊,难以可靠匹配
description: Helps with PDFs.

写技能的几条硬规矩(都来自官方建议):frontmatter 简洁,正文控制在 5000 token 以内(规范建议 500 行以内);描述相近的多个技能会让代理选错或犹豫,用途相近就合并;详细参考资料移入辅助文件、在正文中引用,引用距 SKILL.md 不超过一层,避免深层嵌套的引用链。

四、技能组合与动态技能列表

技能不是孤立的文件,它有一整套"组合拳":

多来源叠加,后者优先。 skills 数组可以传多个路径,同名技能靠后的来源覆盖靠前的——用项目专属版本覆盖基础技能,分层叠加:

agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    skills=["/skills/org/", "/skills/team/", "/skills/request/"],
)

按上下文动态选择。 技能列表可以在创建代理之前由应用逻辑决定——按角色、租户或请求类型给不同的技能集:

SKILLS_BY_ROLE = {
    "engineering": ["/skills/engineering/"],
    "data": ["/skills/data/"],
    "support": ["/skills/support/"],
}

def create_agent_for_user(user_role: str):
    return create_deep_agent(
        model="anthropic:claude-sonnet-4-6",
        skills=SKILLS_BY_ROLE.get(user_role, []),
    )

用 Backend 隔离技能库。 更彻底的做法是代理配置不动,让 CompositeBackend 把 /skills/ 路由到以当前身份为范围的 store——同一个 skills=["/skills/"],不同用户解析出不同的技能库。还可以组合共享库和个人库:/skills/approved/ 路由到组织级(配合权限规则设为只读),/skills/editable/ 路由到用户级(可写),让代理既能用审批技能、又能沉淀自己的技能。

线程内只加载一次。 技能在每个线程中只加载一次并保存在代理状态里,此后新增、修改、删除都不会到达模型。要让变更生效,把 "skills_metadata": None 作为运行输入传入(需要 deepagents>=0.7.16),或用 agent.update_state(config, {"skills_metadata": None}) 重置线程。注意是 None 而不是空列表——空列表意味着"来源已加载但没有任何技能",代理整个线程都看不到任何技能。

用户点名时固定技能。 当应用里用户输入 /langgraph-docs 这种点名语法,把名称解析出来传进 pinned_skills(需要 deepagents>=0.7.23),中间件会把该技能的指令直接追加进对话,代理无需自己选。

五、技能 × 工具 × 沙箱 × 子代理

技能真正的威力在于和其他机制联动。

技能自带工具。 在 frontmatter 的 metadata.include_tools 里列出工具名(注意是空格分隔的字符串,YAML 列表形式永远匹配不到),代理读了技能之后这些工具才可见——schema 在任务需要之前不进提示词(需要 deepagents>=0.7.22)。而且工具是受门控的:没读 SKILL.md 就调用会失败,即使模型猜对了名称和参数。还能传一个解析器函数做名称映射,比如把 linear 一个名字映射到 Linear MCP 服务器的全部工具,甚至按运行时上下文为每个用户返回不同的工具。

技能调用沙箱。 代理可以从任何 Backend 读取脚本,但要执行脚本就需要能访问 shell——只有沙箱 Backend 才提供。沙箱跑在隔离容器里,容器外的技能文件默认不可用,所以要配一个同步中间件:运行前把技能文件从 store 复制进沙箱,运行后把更新写回:

sequenceDiagram
    participant App as 应用代码
    participant ST as Store 技能库
    participant MW as 同步中间件
    participant SB as 沙箱
    participant AG as 代理
    App->>ST: 部署时播种共享技能
    App->>MW: 每次运行前触发
    MW->>ST: 读取共享技能文件
    MW->>SB: 上传到沙箱
    AG->>SB: 执行技能脚本
    AG->>MW: 运行结束
    MW->>ST: 回写更新或新建的技能文件

正在渲染图表…

子代理的技能隔离。 规则很简单:通用子代理自动继承主代理的技能;自定义子代理不会继承,需要在每个子代理定义里显式加 skills 参数。而且技能状态完全隔离——主代理的技能对子代理不可见,反之亦然。给研究员子代理配专属技能、写报告子代理配写作技能,互不干扰。

六、实战作业:写一个"团队写作规范"技能

把机制落到手上。假设你们团队有一套博客写作规范,每个新会话都要口头交代——这正是技能的教科书场景。建一个技能目录:

skills/
└── team-writing-style/
    ├── SKILL.md
    └── references/
        └── tone-examples.md

SKILL.md 的内容(注意 description 的写法——做什么、何时用、带关键词):

---
name: team-writing-style
description: >
  Apply our team's Chinese tech-blog writing style when drafting,
  reviewing, or editing any blog post or technical article. Use when
  the user asks to write, revise, or polish a blog post, article,
  or any publishable technical content.
---

# team-writing-style

写任何可发布的技术内容前,遵循以下规范:

## 硬性规则

1. 开头第一段不超过 3 句话,直接给出文章要解决什么问题;
2. 每个章节配一个可运行的代码示例,禁止只讲概念不给代码;
3. 全文使用"你"称呼读者,禁止使用"我们建议"这类句式;
4. 术语中英对照首次出现时标注英文原文,之后只用中文。

## 参考资料

- 历史优秀文风示例见 [tone-examples.md](references/tone-examples.md),
  动笔前先读一遍校准语感。

然后用 FilesystemBackend 挂载并验证(技能存在磁盘上时无需上传即可加载):

from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend

backend = FilesystemBackend(root_dir="./my-project")

agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    backend=backend,
    skills=["./my-project/skills/"],
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "帮我写一篇介绍 LangGraph 的博客开头"}]},
    config={"configurable": {"thread_id": "1"}},
)

怎么验证技能生效了? 三个观察点:第一,开一个 LangSmith trace,启动时的系统提示词里应该只有 team-writing-style 的 name 和 description,而没有正文——这就是第 1 级;第二,看 trace 里有没有一次针对 SKILL.md 的 read_file 调用——这是第 2 级被激活的标志;第三,输出应该符合规范(第一段三句话以内、用了"你"),而当你问一个和写作完全无关的问题(比如"今天天气怎么样")时,不应该出现那次 read_file——按需加载没有误触发。如果技能没被激活,先回头检查 description 是不是太模糊。

七、本篇踩坑点

  • 记忆写得太肥:记忆没有渐进式披露,始终注入系统提示词。把流程细节塞进记忆而不是技能,等于每轮对话都在为它付 token。
  • description 写成"Helps with X":发现阶段全靠它匹配,模糊描述 = 技能永远不被激活。技能没生效时,第一个该查的就是 description。
  • 路径层级搞错:skills 的每一项要指向包含若干技能目录的目录。直接指向某个带 SKILL.md 的技能目录,什么也不会发生,而且不报错。
  • 改了技能不生效:技能每线程只加载一次。部署环境有持久化 checkpointer,整个线程生命周期内都持有旧技能列表——记得重置 skills_metadata。
  • include_tools 写成 YAML 列表:必须是空格分隔的字符串,这是文档里明确点名的高频错误。

记忆和技能都靠文件系统承载,但有一类工作它们罩不住:Agent 要写代码、跑数据分析、执行技能附带的脚本——这些操作放出去跑,怎么保证安全?下一篇我们讲代码执行:本地 shell、远程沙箱、轻量解释器三种姿势,以及各自的选型逻辑。


参考资料: