1. 引言为什么 DeepAgents Code 需要专门的状态与记忆设计传统 Chatbot 是「无状态」的每次请求把整段对话喂给模型上下文一长就崩。而 Coding Agent 要长时间运行——读文件、跑命令、改代码、陷入循环再想办法爬出来——它必须回答三个问题上次跑到哪一步了状态 State哪些结论值得记住、不需要每次都重新看上下文记忆 Memory进程被打断 / 上下文爆炸后怎么无损恢复持久化 PersistenceDeepAgents CodeLangChain 出品的 CLI 编码智能体给出的答案非常工程化状态基于 LangGraph 的StateGraphReducer把消息、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 与 ReducerDeepAgents 创建的 agent 本身就是一个StateGraph它有一个定义好的状态 schema。默认状态大致如下fromtypingimportAnnotated,Any,TypedDictfromlangchain_core.messagesimportBaseMessagefromlanggraph.graph.messageimportadd_messagesclassDeepAgentState(TypedDict,totalFalse):# 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]这个模式图的不同分支都可能写入同一个 keyReducer 决定了并发/多步写入时是「覆盖」还是「累加」还是「合并」。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}gStateGraph(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是 0123累加语义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重启就丢生产建议用 SQLitefromlanggraph.checkpoint.sqliteimportSqliteSaverwithSqliteSaver.from_conn_string(checkpoints.db)ascheckpointer:agentcreate_deep_agent(modelanthropic:claude-sonnet-4-20250514,checkpointercheckpointer,# 关键可持久化)config{configurable:{thread_id:issue-42}}# 第一次只运行到某一步就中断异常、超时、断电都行agent.invoke({messages:[{role:user,content:重构 src/auth 模块}]},configconfig,)# 重启后用同一个 thread_id 恢复上下文agent 知道自己跑到哪了agent.invoke({messages:[{role:user,content:继续并跑一遍测试}]},configconfig,)要点thread_id是「会话主键」不同任务一定要换 id否则历史会串。Checkpoint 保存的是完整状态不是压缩摘要——所以恢复是精确的。DeepAgents Code CLI 内部就把「每个 prompt 会话」映射成一个thread_id配合 cursor 实现--resume式的续跑。4.2 用 checkpoint id 实现时间旅行Checkpoint 不只有「最新一份」而是历史版本都保留。比如 agent 改错了文件想回退到某个时刻# 列出该 thread 的所有 checkpointstateslist(agent.get_state_history(config))forsnapshotinstates:print(snapshot.config[configurable][checkpoint_id])# 回到倒数第二个 checkpoint 再分叉agent.update_state(config,values{},checkpoint_idstates[1].config[configurable][checkpoint_id],)# 基于该历史状态继续运行agent.invoke({messages:[{role:user,content:换一种方案}],},configconfig)这一步对 Coding Agent 特别重要git管理的是文件版本而 checkpoint 管理的是agent 思考与动作的版本两者维度不同互补使用。5. 文件即记忆FilesystemMiddleware 与 Brief 短时记忆这是 DeepAgents Code 最核心的设计哲学——不单独发明复杂的内存结构而是让文件系统本身充当记忆。文件天然支持可持久化、可 diff、可被人类直接读、可跨会话共享。5.1 FilesystemMiddleware 默认能力文件相关的动作全部收敛为工具中间产物写入工作目录fromdeepagentsimportcreate_deep_agentfromdeepagents.middleware.filesystemimportFilesystemMiddleware agentcreate_deep_agent(modelanthropic: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/*.mdfromdeepagents.backendsimportcreate_filesystem_backend backendcreate_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 agentcreate_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 数据可以扩展 statefromtypingimportAnnotated,TypedDictfromlangchain_core.messagesimportBaseMessageclassMyTaskState(TypedDict,totalFalse):messages:Annotated[list[BaseMessage],messages_reducer]todos:Annotated[list[dict],lambdaold,new:new]# 覆盖式保存最新 todoagentcreate_deep_agent(anthropic:claude-sonnet-4-20250514,middleware[TodoListMiddleware(state_keytodos)],# 指定 state keycheckpointercheckpointer,)这样每次 checkpoint 都会带上最新 TODO恢复会话时 UI 能立刻渲染「上次做到哪了」。7. 长期对话记忆上下文压缩与摘要编码任务动辄几百轮token 会爆。DeepAgents 的SummarizationMiddleware提供两类策略整段摘要把前N条消息压成一条summary系统消息。滑动窗口 摘要保留最近 K 条原文更早的压进摘要兼顾「近处的细节」与「远处的脉络」。fromdeepagents.middleware.summarizationimportSummarizationMiddleware agentcreate_deep_agent(anthropic:claude-sonnet-4-20250514,middleware[SummarizationMiddleware(modelanthropic:claude-sonnet-4-20250514,max_tokens_before_summary64_000,keep_recent_messages20,)],)启用后超阈值时 Middleware 会对老消息调用一次摘要模型。把结果写回状态的summary字段。从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.toolsimporttooltooldefcompound_analyzer(report:str)-str:对一段工程报告做深度分析返回结论。# subagent 内部有自己的 messages 状态与工具subcreate_deep_agent(anthropic:claude-sonnet-4-20250514,system_prompt你是资深架构师输出精炼结论。,middleware[FilesystemMiddleware(root_dir./srv-tree)],)resultsub.invoke({messages:[{role:user,content:report}]})# 关键只向上传 final_response而不是全部内部消息returnresult[final_response]maincreate_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():backendcreate_filesystem_backend(root_dir./repo)withSqliteSaver.from_conn_string(agent.db)ascheckpointer:agentcreate_deep_agent(modelanthropic:claude-sonnet-4-20250514,system_prompt你是一个严谨的编码智能体先写 TODO 再动手重要结论写入 brief。,middleware[FilesystemMiddleware(backendbackend),TodoListMiddleware(),SummarizationMiddleware(modelanthropic:claude-sonnet-4-20250514,keep_recent_messages30,),],checkpointercheckpointer,)returnagentif__name____main__:agentbuild_agent()cfg{configurable:{thread_id:nightly-refactor}}# 第一轮启动长任务agent.invoke({messages:[{role:user,content:重构 auth 模块并修复循环依赖}],},configcfg)# 模拟中途中断后用同一 thread_id 恢复agent.invoke({messages:[{role:user,content:继续把 /v2/login 的调用方迁移完}],},configcfg)验证记忆是否生效最直接的办法是开一个新的thread_id让 agentread_brief(refactor-plan)。如果 brief 是上一次任务写的且能读出来说明「文件即记忆」跨会话工作正常——这正是 DeepAgents Code 与一次性 Chatbot 的本质区别。10. 常见陷阱与最佳实践thread_id忘了隔离检查点按thread_id分组多个任务共用一个 id 会导致历史污染。给每个任务/issue 生成唯一 id。把 Memory 工具的概念混淆不要用 LangChain 老的Memory链去「记住」编码状态它只是 prompt 填充压缩即丢失。过度持久化不是所有中间消息都值得存进状态。子代理内部消息应通过final_response收敛只向上抛结论。Checkpoint 不是 git 替代品checkpoint 管 agent 执行轨迹git 管代码版本两者都要留。