☰
DeepAgents Code 状态与记忆管理体系深度拆解:从 Checkpoint 到文件即记忆
2026/10/1 15:47:50 网站建设 项目流程

1. 引言:为什么 DeepAgents Code 需要专门的状态与记忆设计

传统 Chatbot 是「无状态」的:每次请求把整段对话喂给模型,上下文一长就崩。而 Coding Agent 要长时间运行——读文件、跑命令、改代码、陷入循环再想办法爬出来——它必须回答三个问题:

  1. 上次跑到哪一步了?(状态 State)
  2. 哪些结论值得记住、不需要每次都重新看上下文?(记忆 Memory)
  3. 进程被打断 / 上下文爆炸后,怎么无损恢复?(持久化 Persistence)

DeepAgents Code(LangChain 出品的 CLI 编码智能体)给出的答案非常工程化:

  • 状态:基于 LangGraph 的StateGraph+Reducer,把消息、TODO、结构化中间产物收敛到一个 typed state。
  • 检查点:借助 LangGraph 的Checkpointer把每次invoke的最终状态落盘,实现「中断后可恢复」。
  • 记忆:把「文件系统」当作天然的外置记忆——重要的结论写成brief/*.md,进度写成 TodoList,历史对话用SummarizationMiddleware压缩成摘要。

本文不满足于「讲讲概念」,会用可运行的代码把这三层逐一拆开,最后拼出一个可供生产使用的自定义 coding agent。

2. 先立认知:State、Checkpoint、Memory 是三件事

很多资料把这三个词混着用,先做一次严格区分,后面所有代码都会回到这个框架上:

概念生命周期载体回答的问题
State单次或多次图执行内TypedDict+ Reducer本轮上下文长什么样
Checkpoint跨执行、可持久化序列化到磁盘/数据库断点怎么恢复
Memory跨任务、跨会话文件系统 / 外部存储以前沉淀了什么结论

举个反直觉的例子:LangChain 的Memory类其实只是一坨塞进 prompt 的聊天记录,它既不是状态、也不是可靠记忆——因为一旦上下文压缩它就丢了。而 DeepAgents Code 用文件系统做记忆,天然可持久、可索引、可被下一次新会话读取,这才是真·外置记忆。

3. 状态管理基础:LangGraph StateGraph 与 Reducer

DeepAgents 创建的 agent 本身就是一个StateGraph,它有一个定义好的状态 schema。默认状态大致如下:

fromtypingimportAnnotated,Any,TypedDictfromlangchain_core.messagesimportBaseMessagefromlanggraph.graph.messageimportadd_messagesclassDeepAgentState(TypedDict,total=False):# add_messages 是一个 Reducer:新消息会追加,相同 id 会替换messages:Annotated[list[BaseMessage],add_messages]# 子代理结束后回传的最终结果final_response:Annotated[list[Any],lambdaa,b:b]# 覆盖式# 中间工具产物、待办等,按需挂载todos:list[dict[str,Any]]files:dict[str,str]

关键在于Annotated[..., reducer]这个模式:图的不同分支都可能写入同一个 key,Reducer 决定了并发/多步写入时是「覆盖」还是「累加」还是「合并」。messages用add_messages累加,final_response用覆盖语义,避免旧结果污染。

3.1 手写一个最小可运行的状态示例

fromtypingimportAnnotated,TypedDictfromoperatorimportaddfromlanggraph.graphimportStateGraph,START,ENDclassState(TypedDict):value:Annotated[int,add]# 累加latest:Annotated[int,lambdaold,new:new]# 覆盖defnode_a(state:State)->State:return{"value":1,"latest":"A"}defnode_b(state:State)->State:return{"value":2,"latest":"B"}g=StateGraph(State)g.add_node("a",node_a)g.add_node("b",node_b)g.add_edge(START,"a")g.add_edge("a","b")g.add_edge("b",END)print(g.compile().invoke({"value":0,"latest":""}))# {'value': 3, 'latest': 'B'}

value是 0+1+2=3(累加语义),latest被 B 覆盖。理解这个之后,再看 agent 的messages状态就不神秘了:新增工具消息、删除重试的旧消息,全靠 Reducer 规则在状态层自动完成。

4. 检查点:让 Coding Agent 可以「暂停、恢复、回放」

Coding Agent 最怕的不是跑得慢,而是跑到第 27 步崩了,重启后一切从头再来。DeepAgents 通过 LangGraph 的 Checkpointer 解决这个问题:每个 super-step 结束时,完整 state 会被序列化成一份 checkpoint,按thread_id分组。

4.1 用 SQLite 做持久化 Checkpointer

内存版MemorySaver重启就丢,生产建议用 SQLite:

fromlanggraph.checkpoint.sqliteimportSqliteSaverwithSqliteSaver.from_conn_string("checkpoints.db")ascheckpointer:agent=create_deep_agent(model="anthropic:claude-sonnet-4-20250514",checkpointer=checkpointer,# 关键:可持久化)config={"configurable":{"thread_id":"issue-42"}}# 第一次只运行到某一步就中断(异常、超时、断电都行)agent.invoke({"messages":[{"role":"user","content":"重构 src/auth 模块"}]},config=config,)# 重启后,用同一个 thread_id 恢复上下文,agent 知道自己跑到哪了agent.invoke({"messages":[{"role":"user","content":"继续,并跑一遍测试"}]},config=config,)

要点:

  • thread_id是「会话主键」,不同任务一定要换 id,否则历史会串。
  • Checkpoint 保存的是完整状态,不是压缩摘要——所以恢复是精确的。
  • DeepAgents Code CLI 内部就把「每个 prompt 会话」映射成一个thread_id,配合 cursor 实现--resume式的续跑。

4.2 用 checkpoint id 实现时间旅行

Checkpoint 不只有「最新一份」,而是历史版本都保留。比如 agent 改错了文件想回退到某个时刻:

# 列出该 thread 的所有 checkpointstates=list(agent.get_state_history(config))forsnapshotinstates:print(snapshot.config["configurable"]["checkpoint_id"])# 回到倒数第二个 checkpoint 再分叉agent.update_state(config,values={},checkpoint_id=states[1].config["configurable"]["checkpoint_id"],)# 基于该历史状态继续运行agent.invoke({"messages":[{"role":"user","content":"换一种方案"}],},config=config)

这一步对 Coding Agent 特别重要:git管理的是文件版本,而 checkpoint 管理的是agent 思考与动作的版本,两者维度不同,互补使用。

5. 文件即记忆:FilesystemMiddleware 与 Brief 短时记忆

这是 DeepAgents Code 最核心的设计哲学——不单独发明复杂的内存结构,而是让文件系统本身充当记忆。文件天然支持:可持久化、可 diff、可被人类直接读、可跨会话共享。

5.1 FilesystemMiddleware 默认能力

文件相关的动作全部收敛为工具,中间产物写入工作目录:

fromdeepagentsimportcreate_deep_agentfromdeepagents.middleware.filesystemimportFilesystemMiddleware agent=create_deep_agent(model="anthropic:claude-sonnet-4-20250514",middleware=[FilesystemMiddleware(root_dir="./worktree")],)agent.invoke({"messages":[{"role":"user","content":"在 src 下找找有没有拼写错误"}],})

这个中间件自动向模型暴露ls/read_file/write_file/edit_file/glob/grep六类工具。模型的每一次读写在语义上就是「往记忆中存取」。

5.2 Brief:把「工作记忆」落到磁盘

相比整段对话,一个编码任务中途沉淀的「当前结论」更值得单独存。DeepAgents 用FilesystemBackend提供了write_brief/read_brief,默认写进.dg/briefs/*.md:

fromdeepagents.backendsimportcreate_filesystem_backend backend=create_filesystem_backend(root_dir="./worktree")# 模型在长任务中途写下一份 brief(短时工作记忆)backend.write_brief("refactor-plan",""" 重构 src/auth 的当前结论: 1. TokenService 与 UserRepo 循环依赖,需要引入 AuthContext 破环。 2. 已确认旧接口 /v1/login 保留,新增 /v2/login。 3. 待办:迁移 3 个调用方后删除 LegacyAuth 类。 """)# 新会话开始,模型先读 brief,快速拾起上下文print(backend.read_brief("refactor-plan"))

这种做法的好处:

  • 对话可以被压缩、被遗忘,但 brief 文件还在。
  • 多 subagent 可以通过「读写同名 brief」共享结论(详见第 7 节)。
  • brief 可以用 git 追踪,天然审计。

6. TodoListMiddleware:任务的「程序性记忆」

「我要记住接下来要干什么」是最典型的一种记忆。DeepAgents 把它固化为专门的TodoListMiddleware,而不是塞进系统提示词祈祷模型自觉。

fromdeepagents.middleware.todosimportTodoListMiddleware agent=create_deep_agent("anthropic:claude-sonnet-4-20250514",middleware=[TodoListMiddleware()],)

它给模型一个write_todos工具,模型据此维护结构化待办,诸如:

- [x] 分析 auth 模块依赖 - [ ] 引入 AuthContext 打破循环依赖 - [ ] 迁移 /v1/login 的三个调用方 - [ ] 删除 LegacyAuth 并跑回归测试

Why 这很重要:长任务的「当前目标」一旦被打断,纯靠对话恢复很脆弱,而 TODO 是结构化的、可渲染在 UI 侧边栏的计划。DeepAgents Code CLI 在启动时就强制模型用write_todos先写计划再动手——用结构约束行为。

6.1 把 TODO 放进自定义 State 持久化

如果你需要自己拿 TODO 数据,可以扩展 state:

fromtypingimportAnnotated,TypedDictfromlangchain_core.messagesimportBaseMessageclassMyTaskState(TypedDict,total=False):messages:Annotated[list[BaseMessage],"messages_reducer"]todos:Annotated[list[dict],lambdaold,new:new]# 覆盖式保存最新 todoagent=create_deep_agent("anthropic:claude-sonnet-4-20250514",middleware=[TodoListMiddleware(state_key="todos")],# 指定 state keycheckpointer=checkpointer,)

这样每次 checkpoint 都会带上最新 TODO,恢复会话时 UI 能立刻渲染「上次做到哪了」。

7. 长期对话记忆:上下文压缩与摘要

编码任务动辄几百轮,token 会爆。DeepAgents 的SummarizationMiddleware提供两类策略:

  • 整段摘要:把前N条消息压成一条summary系统消息。
  • 滑动窗口 + 摘要:保留最近 K 条原文,更早的压进摘要,兼顾「近处的细节」与「远处的脉络」。
fromdeepagents.middleware.summarizationimportSummarizationMiddleware agent=create_deep_agent("anthropic:claude-sonnet-4-20250514",middleware=[SummarizationMiddleware(model="anthropic:claude-sonnet-4-20250514",max_tokens_before_summary=64_000,keep_recent_messages=20,)],)

启用后,超阈值时 Middleware 会:

  1. 对老消息调用一次摘要模型。
  2. 把结果写回状态的summary字段。
  3. 从messages中裁剪掉老化部分。

重要:摘要丢的是「对话细节」,不丢「文件内容」。所以第 5 节的 brief 和真实文件系统才是长期记忆的主力,摘要只是辅助。这也是为什么 DeepAgents Code 的记忆体系是「三层金字塔」:

超限

沉淀结论

进度目标

对话消息
完整 State + Checkpoint

摘要层
SummarizationMiddleware

文件记忆
briefs/*.md + 代码文件

程序性记忆
TodoList

跨会话可读
git 可追踪

UI 可渲染
断点可恢复

8. SubAgent 的状态传递与隔离

遇到「去 /services 目录里修 5 个无关的小 bug」这种可并行任务,DeepAgents 会启动 subagent。子代理的状态是隔离的:父代理只拿到子代理返回的 final response,拿不到其内部 messages 洪流。

fromlangchain_core.toolsimporttool@tooldefcompound_analyzer(report:str)->str:"""对一段工程报告做深度分析,返回结论。"""# subagent 内部有自己的 messages 状态与工具sub=create_deep_agent("anthropic:claude-sonnet-4-20250514",system_prompt="你是资深架构师,输出精炼结论。",middleware=[FilesystemMiddleware(root_dir="./srv-tree")],)result=sub.invoke({"messages":[{"role":"user","content":report}]})# 关键:只向上传 "final_response",而不是全部内部消息returnresult["final_response"]main=create_deep_agent("anthropic:claude-sonnet-4-20250514",tools=[compound_analyzer],)

设计要点:

  • 状态隔离避免子代理的几百条中间消息淹没父上下文。
  • 子代理想「记住结果」,必须写文件或 brief,否则其内部状态在返回后就被丢弃。
  • 可并行时,DeepAgents 会把多个 subagent 的调用放进同一个 super-step,父代理再统一合并。

9. 完整实战:一个带持久记忆的自定义 Coding Agent

把前面的零件组装成一个可用骨架:支持 SQLite 检查点、文件记忆、TODO、摘要。

fromlanggraph.checkpoint.sqliteimportSqliteSaverfromdeepagentsimportcreate_deep_agentfromdeepagents.middleware.filesystemimportFilesystemMiddlewarefromdeepagents.middleware.todosimportTodoListMiddlewarefromdeepagents.middleware.summarizationimportSummarizationMiddlewarefromdeepagents.backendsimportcreate_filesystem_backenddefbuild_agent():backend=create_filesystem_backend(root_dir="./repo")withSqliteSaver.from_conn_string("agent.db")ascheckpointer:agent=create_deep_agent(model="anthropic:claude-sonnet-4-20250514",system_prompt="你是一个严谨的编码智能体,先写 TODO 再动手,重要结论写入 brief。",middleware=[FilesystemMiddleware(backend=backend),TodoListMiddleware(),SummarizationMiddleware(model="anthropic:claude-sonnet-4-20250514",keep_recent_messages=30,),],checkpointer=checkpointer,)returnagentif__name__=="__main__":agent=build_agent()cfg={"configurable":{"thread_id":"nightly-refactor"}}# 第一轮:启动长任务agent.invoke({"messages":[{"role":"user","content":"重构 auth 模块并修复循环依赖"}],},config=cfg)# 模拟中途中断后,用同一 thread_id 恢复agent.invoke({"messages":[{"role":"user","content":"继续,把 /v2/login 的调用方迁移完"}],},config=cfg)

验证记忆是否生效,最直接的办法是:开一个新的thread_id,让 agentread_brief("refactor-plan")。如果 brief 是上一次任务写的且能读出来,说明「文件即记忆」跨会话工作正常——这正是 DeepAgents Code 与一次性 Chatbot 的本质区别。

10. 常见陷阱与最佳实践

  1. thread_id忘了隔离:检查点按thread_id分组,多个任务共用一个 id 会导致历史污染。给每个任务/issue 生成唯一 id。
  2. 把 Memory 工具的概念混淆:不要用 LangChain 老的Memory链去「记住」编码状态,它只是 prompt 填充,压缩即丢失。
  3. 过度持久化:不是所有中间消息都值得存进状态。子代理内部消息应通过final_response收敛,只向上抛结论。
  4. Checkpoint 不是 git 替代品:checkpoint 管 agent 执行轨迹,git 管代码版本,两者都要留。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询