研究基于提交
16e84d9(2026-10-06):SDKdeepagents==0.7.22,CLIdeepagents-code==0.1.81。 下文的源码路径都相对于 monorepo 根目录,行号对应这个提交。 事实指在源码中读到、经测试证实或在运行时观察到的内容;解读是我对作者意图的理解。
交互式图表(Archify,每个节点都链接到源码): 架构 · 执行流程 · 核心抽象 · 上下文流 · 记忆流 · 工具运行时 · Agent 循环 · 子 Agent 流程 (图表为英文,中英文版共用)
最小复现:experiments/deepagents/(中文说明)
摘要(TL;DR)
- deepagents 是一层 harness(外壳),不是运行时。
create_deep_agent()构建一个有序的中间件列表和一个后端, 然后把两者交给 LangChain 的create_agent()(libs/deepagents/deepagents/graph.py:989-1011)。 模型→工具的循环、状态、检查点、流式输出和中断,全部属于 LangChain 和 LangGraph。 - 每项能力都是中间件。 三个钩子分别是
before_agent、wrap_model_call和wrap_tool_call。 文件系统工具、子 Agent、摘要压缩、skills、记忆和人工审批(HITL)都以这种方式接入。从提交9340518起,write_todos规划工具改为按需启用。wrap_model_call改写的是单次调用的请求(提示词、消息、工具),不会碰存储的状态。 - 上下文工程是它的核心产品。 有四个机制让提示词保持在上限之内:
- 超过约 2 万 token 的工具结果写入
/large_tool_results/<id>,只在上下文中留下首尾预览; - 超过约 5 万 token 的用户输入也用同样的方式移出;
- 摘要压缩是非破坏性的:它记录一个
_summarization_event,每次调用时重建视图,完整历史仍然留在状态和/conversation_history/*.md里; - 子 Agent 把上下文隔离开。
- 超过约 2 万 token 的工具结果写入
- 后端抽象决定文件放在哪里、有没有 shell。 可选的有图状态、本地磁盘、LangGraph Store、远程沙箱,或在它们之间路由的组合后端。
后端没有 shell 时,
execute工具对模型不可见。 - 记忆是显式的:
AGENTS.md文件被注入系统提示词,由 Agent 通过edit_file自己修改。 运行时已验证:记忆在每个线程只加载一次,线程中途做的修改要到新线程才会重新注入。 - 用真实模型检查过(
deepseek-v4.1-flash,3 轮):模型会用grep和read_file顺着卸载指针取回数据,给子 Agent 写出自包含的任务说明,主动把用户偏好写进AGENTS.md,并能回忆起已被摘要掉的事实。见验证。 dcodeCLI 不在本进程内运行 Agent。它把langgraph dev作为子进程启动, 通过 LangGraph 的RemoteGraph用 HTTP/SSE 与之流式通信。
为什么值得研究(Why This Repository Matters)
- 它把长时间运行的编码 Agent 中常见的许多模式(结果卸载、压缩、子 Agent 隔离、skills、AGENTS.md 记忆、HITL) 做成了可组合、各自单独测试的单元,并建立在主流 Agent 框架之上。
- 提交历史体现了由评测驱动的精简。两个例子:
- 提交
a8d1b32(#4859)删掉了作者手写的基础提示词和工具使用说明: “The System Prompt Experiments found no arm statistically distinguishable, so parsimony argues for the leanest agent.”(系统提示词实验中没有哪个方案在统计上有显著差异,因此按简约原则选最精简的 Agent。) - 提交
9340518(#4929)把write_todos规划工具改为按需启用。
- 提交
- 它是”自己造 Agent 循环”思路的一个有用反例:这个仓库几乎不掌握任何控制流,精力都花在请求塑形和存储上。
仓库概况(Repository Snapshot)
| 项目 | 值(事实) |
|---|---|
| 布局 | uv monorepo:libs/deepagents(SDK)、libs/code(dcode 终端 Agent)、libs/acp(Agent Client Protocol)、libs/talon(实验性的本地运行时宿主)、libs/evals、libs/partners/{daytona,modal,runloop,vercel,quickjs} |
| SDK 规模 | libs/deepagents/deepagents/ 下约 2.96 万行 Python。最大的文件是 middleware/filesystem.py(3.7k 行)、middleware/summarization.py(2.3k 行)和 backends/sandbox.py(2.1k 行) |
| SDK 依赖 | langchain>=1.4.3、langchain-core>=1.6.6、langchain-anthropic、langchain-google-genai、langsmith、wcmatch(libs/deepagents/pyproject.toml) |
| 版本 | .release-please-manifest.json:deepagents 0.7.22、code 0.1.81、acp 0.0.12、talon 0.0.9 |
| 历史 | 4,146 个提交;首个提交在 2025-07-27 |
| 测试 | 在锁定的依赖环境下,3,251 个 SDK 单元测试全部通过(见验证) |
架构(Architecture)
分三层。源码文档这样写,调用关系也印证了这一点:
Deep Agents graph.py + middleware/ + backends/ + profiles/ (组装 + 请求塑形 + 存储)
LangChain langchain.agents.create_agent (模型 -> 工具循环,中间件钩子协议)
LangGraph Pregel 图、channel、checkpointer、Send、interrupt (运行时)
flowchart LR
dev([开发者]) --> tui["dcode TUI<br/>libs/code main.py"]
tui -- "astream(),HTTP+SSE<br/>(RemoteGraph)" --> srv["langgraph dev 服务<br/>server_graph.make_graph"]
srv --> cli["create_cli_agent<br/>agent.py:2524"]
cli --> sdk["create_deep_agent<br/>graph.py:277"]
sdk --> ca["LangChain create_agent<br/>(模型/工具循环)"]
ca --> mw["中间件栈<br/>FS · task · 摘要 · skills · 记忆 · HITL"]
ca --> model[(聊天模型)]
mw --> be["后端<br/>State / FS / LocalShell / Store / Composite"]
be -.->|"沙箱模式"| sbx["BaseSandbox<br/>Daytona · Modal · Runloop · LangSmith"]
be --> disk[(工作区 / ~/.deepagents)]
srv --> db[(sessions.db<br/>SQLite checkpointer)]
带源码链接的完整版:architecture.html。
模块边界(事实):
| 模块 | 负责 | 依赖 |
|---|---|---|
deepagents/graph.py |
组装顺序、子 Agent 栈的构建、profile 的应用、提示词拼装 | middleware、backends、profiles、langchain.agents.create_agent |
deepagents/middleware/ |
所有请求时和工具执行时的行为 | langchain.agents.middleware.types、backends |
deepagents/backends/ |
BackendProtocol 背后的文件存储与命令执行 |
LangGraph 配置内部接口(StateBackend)、LangGraph Store(StoreBackend)、subprocess(LocalShellBackend) |
deepagents/profiles/ |
按提供商和模型调整 harness(提示词后缀、工具描述、排除的工具和中间件、额外中间件) | _models.py 里的模型识别辅助函数 |
libs/code |
CLI 交互、进程拓扑、沙箱提供方、HITL 策略、压缩交互、本地上下文、CLI 专属工具 | 锁定的 SDK deepagents==0.7.22、langgraph-cli、langgraph-checkpoint-sqlite |
libs/partners/* |
BaseSandbox 子类(例如 class DaytonaSandbox(BaseSandbox),libs/partners/daytona/langchain_daytona/sandbox.py:23) |
SDK 的 backends.sandbox |
主要执行流程(Main Execution Flow)
追踪的入口:一次交互式 dcode 对话轮次。只用 SDK 的路径(create_deep_agent(...).invoke(...))从第 6 步起完全相同,只是在本进程内运行。
sequenceDiagram
autonumber
actor U as 开发者
participant T as dcode TUI
participant S as LangGraph 服务
participant G as Agent 图(create_agent)
participant M as 中间件链
participant L as 聊天模型
participant X as 工具 + 后端
U->>T: 提示词
T->>S: astream(messages/updates/custom),经 RemoteGraph
S->>G: 运行 thread_id(加载检查点)
G->>M: before_agent(记忆、skills、修补悬空调用)
loop 直到 AIMessage 不再有 tool_calls
G->>M: 新建 ModelRequest(state)
M->>L: 系统提示词 + 有效消息 + 可见工具
L-->>G: AIMessage(tool_calls)
opt 工具在 interrupt_on 中
G-->>T: __interrupt__(HITL after_model)
T->>S: Command(resume=decisions)
end
G->>X: 每个调用一个 Send("tools", call) → wrap_tool_call 链
X-->>G: ToolMessage(或卸载后的存根)
end
G-->>S: END + 检查点(SQLite)
S-->>T: 流式数据块
T-->>U: 渲染
逐步说明(除非另有标注,每一步都是在源码中读到的事实):
- 控制台入口。
dcode和deepagents-code都映射到deepagents_code:cli_main(libs/code/pyproject.toml:154-156)。cli_main定义在libs/code/deepagents_code/main.py:5294。 参数解析会在三种模式中选一种:交互式 TUI、无界面的-n,或--acp。 - 启动服务进程。 TUI 调用
start_server_and_get_agent。它写出一个脚手架目录,其中langgraph.json指向deepagents_code.server_graph:make_graph(client/launch/server.py:56),然后启动python -m langgraph_cli dev --host 127.0.0.1 ...(server.py:386-392)。客户端是RemoteAgent, 注释原文为 “Client that talks to a LangGraph server over HTTP+SSE”(client/remote_client.py:338-340)。 checkpointer 是AsyncSqliteSaver的子类,带线程归属锁(client/launch/server_manager.py:121-164,thread_ownership.py:238-250)。--acp模式则在本进程内构建 Agent(main.py:3626-3655)。 - 图工厂。
make_graph(server_graph.py:1104)解析模型,构建 CLI 工具 (fetch_url、可选的 Tavilyweb_search、MCP 工具),并按需打开沙箱(server_graph.py:497-503)。 然后调用create_cli_agent(server_graph.py:558-600→agent.py:2524)。 - CLI 组装。
create_cli_agent做了几项选择:- 后端:
LocalShellBackend(root_dir=cwd, virtual_mode=False);禁用 shell 时用普通的FilesystemBackend;或者使用沙箱 (agent.py:3112-3143)。结果再包进一个CompositeBackend,其中带一条持久化的conversation_history路由(agent.py:3229-3297)。 - 记忆: 基于用户级和项目级
AGENTS.md的MemoryMiddleware(agent.py:3056-3093)。 - Skills: 一个
SkillsMiddleware子类。 - HITL: 针对
execute、write_file、edit_file、delete、web_search、fetch_url、task以及异步任务工具的审批策略(agent.py:2278-2372)。 - 调用:
create_deep_agent(...)(agent.py:3649-3661)。
- 后端:
- SDK 组装。
create_deep_agent(libs/deepagents/deepagents/graph.py:277)按以下顺序执行:- 解析模型和 harness profile(
:610-637); - 默认使用
StateBackend()(:653); - 按 USER → profile BASE → profile SUFFIX 的顺序拼装作者编写的提示词(
:655-664); - 为每个声明式子 Agent 编译它自己的中间件栈(
:685-815); - 自动加入
general-purpose子 Agent(:817-886); - 构建主中间件栈(
:888-969); - 调用
create_agent(...).with_config({"recursion_limit": 9_999, ...})(:989-1011)。
- 解析模型和 harness profile(
- 轮次开始。 TUI 调用
agent.astream(stream_input, stream_mode=["messages","updates","custom"], subgraphs=True, durability="exit")(tui/textual_adapter.py:2171-2178)。 before_agent节点(每次调用只执行一次)。 这里运行三个中间件:PatchToolCallsMiddleware为每个没有结果的工具调用补一个报错的ToolMessage(middleware/patch_tool_calls.py:16-52);MemoryMiddleware把AGENTS.md加载到私有字段memory_contents,已存在则跳过(middleware/memory.py:279-311);SkillsMiddleware把 skill 的 frontmatter 加载到skills_metadata(middleware/skills.py:1074-1120)。
- 模型节点。 LangChain 的
model_node(外部依赖:langchain/agents/factory.py,langchain 1.4.3)每次都新建ModelRequest(model, tools, system_message, messages=state["messages"], state, runtime),然后执行组合好的wrap_model_call链。列表里的第一个中间件在最外层。主栈顺序:FilesystemMiddleware:后端没有 shell 时去掉execute,并移出过大的HumanMessage(filesystem.py:3194-3302);SubAgentMiddleware;SummarizationMiddleware:构建有效视图,必要时执行压缩(summarization.py:1487-1623);PatchToolCalls;- 调用方传入的中间件;
- harness profile 的
extra_middleware; - Skills(提示词片段和受控工具的披露);
- 提示词缓存(Anthropic;安装了相应包时还有 Bedrock 和 Fireworks);
- Memory,追加
<agent_memory>; - HITL;
UnsupportedContentMiddleware;_ToolExclusionMiddleware。
最内层的处理函数绑定工具并调用
model.invoke([system, *messages])。 - HITL 关卡。
HumanInTheLoopMiddleware.after_model对匹配的工具调用执行interrupt(...)(外部依赖:langchain/agents/middleware/human_in_the_loop.py:427)。TUI 弹出ApprovalMenu,然后用Command(resume=resume_payload)恢复执行(textual_adapter.py:4096)。 - 路由。 LangChain 的
_make_model_to_tools_edge(外部依赖,factory.py:1991-2042)处理四种情况:- 没有工具调用 → END;
- 有待执行的调用 →
[Send("tools", [call]) for call in pending],因此多个工具调用并行执行; - 只有合成的结果(例如 HITL 拒绝)→ 回到模型;
- 状态里设置了
jump_to时,它优先于以上各种情况。
- 工具节点。 执行组合好的
wrap_tool_call链。FilesystemMiddleware.wrap_tool_call(filesystem.py:3682-3710)做三件事:- 拒绝同一个 AIMessage 中对同一路径的第二次修改;
- 执行工具;
- 卸载过大的结果(
TOOLS_EXCLUDED_FROM_EVICTION中的工具除外)。
task工具会调用一个子 Agent 图(subagents.py:810-838)。 - 观察结果。
ToolMessage被归并进messages。DeltaChannel的 reducer 按 id 去重 (_messages_reducer.py)。控制流回到第 8 步。 - 结束。 当最后一条 AIMessage 不含工具调用时,图执行结束。检查点被持久化(CLI 中是 SQLite),TUI 渲染流式数据。
交互式版本:execution-flow.html 和 agent-loop.html。
核心抽象(Core Abstractions)
classDiagram
direction LR
class create_deep_agent {
+model, tools, subagents, skills, memory
+backend, permissions, interrupt_on
返回 CompiledStateGraph
}
class AgentMiddleware {
<<LangChain 契约>>
+tools
+before_agent(state)
+wrap_model_call(request, handler)
+wrap_tool_call(request, handler)
+state_schema
}
class BackendProtocol {
<<abstract>>
+ls/read/write/edit/delete
+grep/glob
+upload_files/download_files
}
class SandboxBackendProtocol {
+execute(command, timeout)
+id
}
class HarnessProfile {
+base_system_prompt / system_prompt_suffix
+tool_description_overrides
+excluded_tools / excluded_middleware
+extra_middleware
}
class SubAgent {
<<TypedDict>>
name, description, system_prompt
tools, model, middleware, skills
mode: isolated 或 fork
}
class DeepAgentState {
messages: DeltaChannel
files: DeltaChannel,仅 StateBackend
私有字段: _summarization_event, memory_contents...
}
create_deep_agent --> HarnessProfile : 按模型解析
create_deep_agent --> AgentMiddleware : 排定栈顺序
create_deep_agent --> SubAgent : 编译
AgentMiddleware <|-- FilesystemMiddleware
AgentMiddleware <|-- SubAgentMiddleware
AgentMiddleware <|-- SummarizationMiddleware
AgentMiddleware <|-- MemoryMiddleware
AgentMiddleware <|-- SkillsMiddleware
BackendProtocol <|-- SandboxBackendProtocol
BackendProtocol <|-- StateBackend
BackendProtocol <|-- FilesystemBackend
BackendProtocol <|-- CompositeBackend
BackendProtocol <|-- StoreBackend
FilesystemBackend <|-- LocalShellBackend
SandboxBackendProtocol <|-- BaseSandbox
FilesystemMiddleware --> BackendProtocol
SummarizationMiddleware --> BackendProtocol : 卸载历史
SubAgentMiddleware --> SubAgent : task 工具
SummarizationMiddleware --> DeepAgentState : 写入事件
交互式版本:core-abstractions.html。
create_deep_agent(组装器)
- 职责: 把声明式配置转换成一个有序的中间件栈和一个编译好的 LangChain Agent。
- 输入: 模型(字符串或
BaseChatModel)、tools、system_prompt、middleware、subagents、skills、memory、permissions、backend、interrupt_on、response_format、checkpointer、store(graph.py:277-297)。 - 输出:
CompiledStateGraph,配置了recursion_limit=9_999和追踪元数据(graph.py:1002-1011)。 - 生命周期: 只在构建时运行一次,执行期间从不运行。
- 依赖:
resolve_model、_harness_profile_for_model、所有中间件类、create_agent。 - 关键源码:
graph.py:210-244(_apply_custom_middleware:按.name原位替换;新条目插在核心栈之后、尾部之前);graph.py:247-262(_REQUIRED_MIDDLEWARE:profile 不能排除 Filesystem 和 SubAgent)。 - 为什么存在: 顺序本身就是产品,有几项行为只在特定位置才成立。Skills 必须在压缩之后、模型路由之后运行(
graph.py:404-411)。Memory 追加在最后,位于提示词缓存之后。源码注释说 profile 中间件放在“核心中间件和记忆之间,这样记忆更新(会改变系统提示词)不会让 Anthropic 的提示词缓存前缀失效”(graph.py:925-945)。
AgentMiddleware(LangChain 契约,deepagents 的每项功能都实现它)
- 职责: 在循环的固定位置进行拦截。
- 输入与输出:
before_agent(state) -> 状态更新;wrap_model_call(ModelRequest, handler) -> ModelResponse | ExtendedModelResponse(command=Command(update=...));wrap_tool_call(ToolCallRequest, handler) -> ToolMessage | Command;类属性tools和state_schema。 - 生命周期: 每次模型调用或工具调用都会触发钩子。
before_agent每次调用只执行一次。 - 关键源码:
libs/deepagents/deepagents/middleware/__init__.py:1-50说明了为什么功能做成中间件而不是普通工具:普通工具“只会被 LLM 调用,不能在调用 LLM 之前运行”。 - 为什么存在: 钩子让一项功能可以修改工具列表、注入提示词片段、改写消息视图、保存私有状态,这些都是工具函数做不到的。
BackendProtocol / SandboxBackendProtocol
- 职责: 把”文件放在哪里”与”模型看到哪些工具”分开。
- 输入与输出: 绝对虚拟路径,返回
ReadResult、WriteResult、GrepResult等(backends/protocol.py:404-805)。execute(command, timeout) -> ExecuteResponse只存在于沙箱协议上(:886-944)。delete是可选的,通过检测子类是否覆写来判断(:985-1000)。 - 实现(事实):
StateBackend:文件放在图状态里,通过 LangGraph 的CONFIG_KEY_SEND写入(backends/state.py:38-119);FilesystemBackend:本地磁盘,可选virtual_mode虚拟根目录;LocalShellBackend:FilesystemBackend加subprocess,文档写明“没有任何沙箱或隔离”(backends/local_shell.py:1-5);StoreBackend:LangGraph 的BaseStore,跨线程、带命名空间(backends/store.py:90-97);CompositeBackend:按最长前缀路由;execute总是交给默认后端(backends/composite.py:195-283, 814-850);BaseSandbox:所有文件操作都用基于execute()的 shell 或 Python 脚本实现(backends/sandbox.py:1-13, 1536-1569);ContextHubBackend:一个 LangSmith Hub 仓库;LangSmithSandbox。
- 生命周期: 构建一次,由主 Agent、所有子 Agent、摘要压缩、记忆和 skills 共用(
graph.py:653, 718-726)。 - 为什么存在: 同一套工具代码(文件系统工具、结果卸载、记忆加载、历史卸载)因此可以运行在临时状态、磁盘、数据库或远程虚拟机上。
HarnessProfile / ProviderProfile(模型抽象)
- 职责:
ProviderProfile调整模型的构建:OpenAI 默认用 Responses API;NVIDIA 和 OpenRouter 会带上来源标识请求头(_models.py:36-58,profiles/provider/)。HarnessProfile按模型调整 harness:提示词后缀、工具描述覆盖、排除的工具和中间件、额外中间件、general-purpose 子 Agent 的设置(profiles/harness/harness_profiles.py:483)。
- 查找顺序: 先找精确的
provider:model键,再找提供商的默认配置,最后用空 profile(harness_profiles.py:1319-1387)。内置 profile 通过显式 import 注册,第三方通过 entry point 注册(profiles/_builtin_profiles.py:1-60)。 - 例子(事实): Codex profile 重新加回
TodoListMiddleware,因为它的提示词引用了write_todos(profiles/harness/_openai_codex.py:60-88)。Sonnet 4.6 的 profile 只追加了 Anthropic 公开发布的提示词片段(_anthropic_sonnet_4_6.py)。 - 为什么存在: 模型之间的差异用配置数据来处理,代码里没有按模型分支的逻辑。
SubAgent / CompiledSubAgent / AsyncSubAgent
- 职责: 被委派的工作单元。
SubAgent是声明式的,用它自己的create_agent编译(subagents.py:72, 549-604)。CompiledSubAgent是任何返回messages的 runnable(:226)。AsyncSubAgent是在后台运行的远程 Agent Protocol 图(async_subagents.py:34)。
- 生命周期: 在构建时编译(
_build_task_tool→_compile_spec,subagents.py:684-715, 870-874),每次task调用时执行。 - 为什么存在: 隔离上下文、专门化分工;见子 Agent / 工作流。
DeepAgentState
- 职责: 会被写入检查点的状态结构。
- 字段:
messages使用DeltaChannelreducer,每 50 步做一次快照,“把检查点的增长从 O(N²) 降到 O(N)”(graph.py:74-77);- 各中间件贡献的字段有
files(也是DeltaChannel,filesystem.py:1171-1176)、_summarization_event和_summarization_session_id(summarization.py:198-211)、memory_contents、skills_metadata和async_tasks。
- 私有字段: 标注了
PrivateStateAttr的字段会被收集起来(middleware/_state.py,graph.py:970-976),在进出子 Agent 时剔除。 - 为什么存在: 只有当不断增长的日志的检查点仍然便宜时,保留完整历史的非破坏式设计才可行。
Agent 循环(Agent Loop)
事实: deepagents 自己没有循环代码,循环就是 LangChain 编译出的图:
START → [before_agent 节点] → model ⇄ tools → [after_agent 节点] → END
model和tools是 LangGraph 节点。before_model/after_model钩子是额外的节点,wrap_*钩子则在节点内部运行。- 退出条件: 最后一条
AIMessage没有工具调用(factory.py:2016-2019)。 - 并行: 每个待执行的调用对应一个
Send(factory.py:2031-2032)。 - 约束: 用
recursion_limit=9_999(graph.py:1004)而不是轮次计数器。上下文大小靠摘要压缩来控制,而不是靠限制步数。 - 自我修复:
PatchToolCallsMiddleware(patch_tool_calls.py)修补因中断或崩溃而留下未应答工具调用的历史。没有它,下一次请求模型提供商会被拒绝。 - 可选的质量循环:
RubricMiddleware(middleware/rubric.py:1-10)拦截”完成”:一个打分子 Agent 可以注入反馈并让循环继续,最多max_iterations次。
stateDiagram-v2
[*] --> before_agent
before_agent --> model
model --> interrupted: 匹配 interrupt_on(after_model)
interrupted --> route: Command(resume)
model --> route: AIMessage
route --> tools: 有待执行的 tool_calls
route --> model: 只有合成的 ToolMessage(如 HITL 拒绝)
tools --> model: 追加 ToolMessage
route --> [*]: 没有 tool_calls
tools --> [*]: GraphRecursionError(超过 9,999 步)
上下文工程(Context Engineering)
上下文工程是这个仓库最主要的架构关注点。下面每个回答都有源码依据;标注”探针”的,还用真实 SDK 在运行时验证过。
flowchart LR
subgraph Stored["存储(检查点 / 后端)"]
log[(state.messages<br/>完整日志)]
files[(后端文件<br/>/large_tool_results<br/>/conversation_history)]
priv[(私有状态<br/>memory_contents · skills_metadata<br/>_summarization_event)]
end
tr[原始工具结果] -->|"≤ 2 万 token"| log
tr -->|"> 2 万 token:全文"| files
tr -->|"> 2 万 token:指针 + 首尾预览"| log
hm[用户消息] -->|"> 5 万 token:移出"| files
hm --> log
log --> sum{token ≥ 窗口的 85%<br/>或提供商报超长}
sum -->|否| view
sum -->|是:卸载片段,写入事件| files
sum --> view["有效视图<br/>[摘要] + messages[cutoff:]"]
priv --> sp["系统提示词<br/>USER + profile + skill 索引 + 记忆"]
view --> req((ModelRequest))
sp --> req
tools["可见工具<br/>(无 shell 时隐藏 execute,<br/>profile 排除项)"] --> req
req --> llm[(模型)]
交互式版本:context-flow.html。
- 上下文如何构建。 每次调用时由
wrap_model_call链从状态重新构建。除了模型提供商的提示词缓存,调用之间不缓存任何东西(顺序见graph.py:888-969;请求由 LangChain 的model_node构建)。 - 哪些内容进入模型上下文:
- 作者编写的系统提示词(USER → profile BASE → SUFFIX,
graph.py:655-664); - 中间件追加的 Skills、Memory 片段,以及文件系统的宿主路径映射片段;
- 有效消息视图;
- 可见工具的 schema。
内置的工具使用说明被有意排除了,因为它和工具 schema 重复(提交
a8d1b32;注释见graph.py:666-673)。 - 作者编写的系统提示词(USER → profile BASE → SUFFIX,
- 哪些内容被排除:
- skill 的正文,直到模型主动读取;
- 超过 2 万 token 的工具结果和超过 5 万 token 的
HumanMessage(filesystem.py:1771-1772); - 已被摘要掉的片段;
- 后端没有 shell 时的
execute工具(filesystem.py:3194-3240); excluded_tools中列出的工具(_tool_exclusion.py);- 模型不支持的内容块(
unsupported_content.py); - 子 Agent 的对话记录。
- 工具结果如何处理。 结果直接放进
ToolMessage,除非它超过4 × 20,000个字符,且所属工具不在排除名单里。 被排除的工具有:ls/glob/grep(它们会自行截断,而截断意味着”该缩小查询范围”)、read_file(重读一个被卸载的读取结果没有帮助),以及write/edit/delete(输出很小)(filesystem.py:1601-1630)。 工具错误以ToolMessage(status="error")的形式作为观察结果返回,而不是抛出异常(filesystem.py:193-195;execute的参数校验见:3001-3060)。 - 截断与卸载。 两者都有,而且都可以找回原文(代码见本节末尾):
- 大结果(
_message_eviction.py:260-284):写入/large_tool_results/<tool_call_id>,替换为带行号的前 5 行 / 后 5 行预览、一个... [N lines truncated] ...标记和分页读取说明。 探针 P3:一个 169,889 字符的结果到模型那里只剩一个 1,812 字符的存根。 - 大的用户输入: 写入
/conversation_history/<uuid>.md,并给消息打上lc_evicted_to标签。状态里保留全文,只有发给模型的请求副本被截断(filesystem.py:3435-3538)。 - 沙箱里的
execute: “源头捕获”可以把大输出留在沙箱内,只返回预览(sandbox.py:1340-1410, 1589-1625)。它按沙箱类逐个启用(默认enable_capture_offload = False,sandbox.py:1558)。 - 较早的工具调用参数(例如
write_file的内容)会在完整摘要压缩触发之前先被裁剪(TruncateArgsSettings,summarization.py:168-196)。
- 大结果(
- 摘要压缩。 有,而且是非破坏性的(
summarization.py:1487-1623):- 在达到
max_input_tokens的 85% 时触发,保留 10%;模型 profile 未知时,改用 17 万 token / 6 条消息(:262-299); - 选择一个不会把 AIMessage 和它的 ToolMessage 拆开的切分点(LangChain 的
_find_safe_cutoff_point); - 把被移出的片段追加到
/conversation_history/<session>.md; - 请模型生成摘要,并把
{cutoff_index, summary_message, file_path}存为_summarization_event; - 之后每次调用看到的都是
[摘要] + messages[cutoff:](:821-858)。
多次连续压缩时,会把索引换算到原始状态列表上(
:860-886)。遇到提供商的ContextOverflowError时,它先摘要,再裁剪末尾那批 ToolMessage,然后用严格更小的请求最多重试一次;如果输入无法再缩小就直接抛错(:1395-1454;测试见test_compaction_recovery.py)。 探针 P5:状态保留了 44 条消息,模型只看到 6 条。 - 在达到
- 仓库上下文。 不建索引,SDK 里没有 embedding 或检索层。Agent 用
ls/glob/grep/read_file自行探索。read_file默认读 100 行(filesystem.py:951-952),grep按字面匹配,最多返回 1,000 条(:1774)。 CLI 通过LocalContextMiddleware(libs/code/deepagents_code/local_context.py:718)一次性注入本地上下文:git 状态、语言、测试命令、tree -L 3的输出。发生摘要压缩后,只有当这些上下文变化时才发送刷新消息(:826-834)。 - 子 Agent 如何获得上下文。
- 隔离模式:
messages=[HumanMessage(description)],外加父 Agent 中非消息、非私有的状态(subagents.py:776-808)。 mode="fork"(测试阶段):父 Agent 的有效消息(已经过摘要),再加一段禁止继续委派的前言(subagents.py:363-390)。- 探针 P4:子 Agent 的第一次请求恰好是
[SystemMessage, HumanMessage("Write a short report to /report.md")]。
- 隔离模式:
- 上下文隔离如何实现。
- 父 Agent 只收到子 Agent 最后一条非空 AI 文本,作为一条
ToolMessage(subagents.py:721-759)。 _EXCLUDED_STATE_KEYS(messages、todos、structured_response、skills_metadata等)和所有PrivateStateAttr字段在进出时都不会传递(:398-422)。- 子 Agent 没有
task工具,所以委派不会递归。探针 P4 确认子 Agent 的工具列表里没有task。
- 父 Agent 只收到子 Agent 最后一条非空 AI 文本,作为一条
- 长时间任务中如何控制上下文大小。 四个机制配合:
- 每次工具调用时主动卸载;
- 在更低阈值上裁剪工具参数;
- 达到 85% 时摘要压缩;
- 被动的超长恢复。
DeltaChannel让检查点的增长保持线性。解读: 这个设计把提示词当作持久状态之上的缓存,每次移出内容都会留下一个模型能顺着找回去的指针。
第 5 点中的卸载路径,简化自 filesystem.py 和 _message_eviction.py:
# filesystem.py:3682-3710(简化)
def wrap_tool_call(self, request, handler):
if error := _parallel_file_mutation_error(request): # 同一轮中两次修改同一路径
return error
result = handler(request)
if request.tool_call["name"] in TOOLS_EXCLUDED_FROM_EVICTION:
return result
return self._intercept_large_tool_result(result) # > 4 * 20_000 字符 -> 写入后端文件 + 预览存根
记忆(Memory)
| 概念 | 在这里指什么 | 存放位置 | 作用域 | 写入方 | 读取方 |
|---|---|---|---|---|---|
| 对话历史 | state["messages"],完整日志(摘要压缩从不改写它) |
LangGraph 检查点;CLI 用 ~/.deepagents/.state/sessions.db(SQLite) |
线程 | 图节点 | 模型节点(以有效视图的形式) |
| 上下文 | 一个 ModelRequest:系统提示词 + 有效消息 + 工具 schema |
只在内存中,每次调用重建 | 一次模型调用 | wrap_model_call 链 |
模型 |
| 持久记忆 | AGENTS.md 文件 |
后端文件:CLI 中是 ~/.deepagents/<agent>/AGENTS.md,加上项目的 AGENTS.md / .deepagents/AGENTS.md(libs/code/deepagents_code/agent.py:3056-3063,_paths.py:295-297,project_utils.py:151-175) |
用户 / Agent 配置 / 项目 | Agent 自己通过 edit_file 写入,或者由用户写入 |
MemoryMiddleware.before_agent |
| 外部存储 | 卸载的结果、历史归档、工作区文件、Store 命名空间 | 后端:磁盘、状态、StoreBackend(跨线程)、ContextHubBackend(LangSmith Hub 仓库)、沙箱 |
可按路由配置 | 工具和中间件 | Agent 需要时通过 read_file 读取 |
交互式版本:memory-flow.html。
- 记忆是显式的,不是隐式的。 没有自动抽取,也没有向量库。记忆提示词告诉模型什么时候用
edit_file保存学到的东西,以及哪些内容绝不能保存,例如凭据(middleware/memory.py:105-171)。 - 进入上下文:
memory_contents被格式化成<agent_memory>…</agent_memory>,每次调用都追加到系统提示词(memory.py:347-383)。对 Anthropic 模型会加一个cache_control断点。HTML 注释会先被去掉。 - 读取:
before_agent把每个来源下载一次。缺失的文件会被跳过,其他错误直接抛出(memory.py:279-311)。 - 更新与过期(事实,已在运行时验证): 当状态里已有
memory_contents时跳过加载(memory.py:294),而这个字段随线程一起被写入检查点。线程中途做的修改要到新线程才会重新注入。 探针 P6:edit_file把文件改成 “likes rust” 之后,下一轮的系统提示词仍然写着 “likes python”。我在 SDK 里没找到任何让缓存失效的路径。 唯一的缓解措施在提示词里:记忆“可能已过时……以用户和经过验证的证据为准”(memory.py:113-116)。 - 信任: 记忆被定位为数据而不是指令(
memory.py:113-116)。CLI 拒绝指向项目根目录之外的项目级AGENTS.md符号链接(project_utils.py:151-175),并保护一个由程序管理的区块(ManagedMemoryGuardMiddleware,agent.py:3085-3093)。 - Skills 是渐进披露的程序性记忆:
- 只有
SKILL.mdfrontmatter 里的name、description和路径进入提示词(skills.py:797-838); - 正文用
read_file读取; - skill 在
metadata.include_tools中列出的工具,只有在它的SKILL.md被读取之后才会披露给模型(_skill_tools.py:1-60,提交92cd8e7); - 后面的 skill 来源会覆盖前面的(后者优先)。
- 只有
工具(Tools)
- 接口: LangChain 的
BaseTool/ 可调用对象。模型看到的是名称、描述和 JSON schema。有两条注册路径(middleware/__init__.py:1-50):- 中间件的
.tools(文件系统工具、task、异步任务工具、compact_conversation、skill 披露的工具); - 调用方传入的
tools=(CLI 中是fetch_url、web_search、MCP 工具)。
- 中间件的
- 内置工具:
ls、read_file、write_file、edit_file、delete、glob、grep,后端支持时还有execute(filesystem.py:1893-1906);另有task。从9340518起,write_todos改为按需启用,Codex profile 会把它加回来。 - 发现: 每个 Agent 的工具是静态的,但每次调用都会过滤:
FilesystemMiddleware去掉后端不支持的能力对应的工具(filesystem.py:3194-3213);_ToolExclusionMiddleware在请求和执行两个边界上应用 profile 的排除项(_tool_exclusion.py);SkillsMiddleware在对应的SKILL.md被读取后加入受控工具。
- 调用: LangGraph 的
ToolNode通过Send执行每个调用,外面包着组合好的wrap_tool_call链。ToolRuntime会向需要的工具(例如task)注入state、tool_call_id和配置。 - 结果处理: 见上下文工程第 4–5 点。
- 错误处理: 对于用户可纠正的错误(超时参数不对、文件不存在、权限被拒、子 Agent 类型未知),工具返回
status="error"的ToolMessage。例如:TaskToolSchema拒绝未知的参数键,因为模型会把真正的指令放进一个自己编的prompt键里(subagents.py:441-464);- 工具代码本身抛出的异常“按设计不做处理”,直接向上传播(
filesystem.py:3696-3699)。
- 重试: SDK 在工具层没有重试。CLI 为模型节点加了
CodeModelRetryMiddleware(来自 CLI 追踪的结论,没有逐行核实)。 - 权限:
FilesystemPermission(operations, paths, mode=allow|deny|interrupt),按顺序第一条匹配的规则生效,在内置文件系统工具内部执行(filesystem.py:366-412)。interrupt规则会被编译成带when判断条件的HumanInTheLoopMiddleware配置(_fs_interrupt.py:156-183)。- 权限规则和能执行命令的后端同时使用时,除非每条规则都限定在路由范围内,否则会抛出
NotImplementedError(filesystem.py:1849-1856),因为 shell 可以绕过路径规则。
- MCP: 只在 CLI 层(MCP 工具作为普通工具传入)。SDK 里没有 MCP 专用代码。
运行时 / 沙箱(Runtime / Sandbox)
推理与执行的边界在哪里(事实)。
模型只发出 tool_calls、读取 ToolMessage。执行发生在中间件拥有的工具函数里,它们把 I/O 委托给后端。
因此边界就是 BackendProtocol 接口。策略关卡(HITL、权限、修改冲突检查)位于模型和这个接口之间。
交互式版本:tool-runtime.html。
| 运行时 | 隔离程度 | 源码 |
|---|---|---|
StateBackend(默认) |
没有 shell;文件放在会写入检查点的状态里 | backends/state.py |
FilesystemBackend(virtual_mode=True) |
阻止越出 root_dir 的路径;但不是进程级沙箱 |
backends/filesystem.py:132-136, 182-260 |
LocalShellBackend |
没有隔离:以用户权限执行任意命令。文档写明开启 shell 后 virtual_mode“不提供任何安全性” |
backends/local_shell.py |
BaseSandbox 的子类(Daytona、Modal、Runloop、Vercel、LangSmith,以及 CLI 中的 AgentCore) |
远程虚拟机或容器就是隔离边界 | backends/sandbox.py:1536,libs/partners/*,libs/code/.../sandbox_registry.py:334-348 |
BaseSandbox的设计: 子类只需实现execute、upload_files、download_files和id。ls/read/grep/glob/edit都建立在execute()之上,用内嵌的python3 -c和 POSIX shell 脚本实现(sandbox.py:1-13)。一个很小的原语就能让任何带 shell 的虚拟机成为完整的后端。- shell 超时: 每次调用的
timeout会对照max_execute_timeout校验(默认 3600 秒,filesystem.py:1773)。LocalShellBackend默认 120 秒,输出上限 100 kB(local_shell.py)。 - CLI 策略: 本地模式下,
execute、write_file、edit_file以及第 4 步列出的其他工具都需要 HITL 审批,除非用户选择自动批准或 YOLO 模式,或者用 shell 白名单代替 HITL(agent.py:2278-2391)。在--sandbox模式下,auto 模式会被强制改为 manual(来自 CLI 追踪的结论)。 - 网络与浏览器: SDK 没有浏览器运行时。网络访问取决于后端 shell 允许什么,外加 CLI 工具(
fetch_url、Tavily 搜索)。
子 Agent / 工作流(Sub-Agents / Workflow)
sequenceDiagram
participant P as 父模型
participant T as task 工具(subagents.py)
participant C as 子 Agent(独立的 create_agent)
participant B as 后端
P->>T: task(description, subagent_type)
T->>C: invoke({...共享的非私有状态, messages:[Human(description)]})
loop 子 Agent 自己的循环
C->>B: 工具(write_file 等)
end
C-->>T: 最终状态
T-->>P: Command(update={files..., messages:[ToolMessage(最后的 AI 文本)]})
交互式版本:sub-agent-flow.html。
- 注册:
general-purpose会被自动加入,使用父 Agent 的工具、模型和 skills,除非被覆盖或被 profile 禁用(graph.py:817-886;subagents.py:495-501)。自定义子 Agent 的中间件依次是 Filesystem → Summarization → PatchToolCalls → 它自己的中间件 → profile 中间件 → Skills → 缓存(graph.py:714-778)。它们没有SubAgentMiddleware。 - 并行:
task的工具描述告诉模型“任务彼此独立时,在一条消息里发出多个工具调用,同时启动多个 Agent”(subagents.py:467-478)。LangGraph 的Send扇出让它们并行运行。测试覆盖了兄弟子 Agent 之间的私有状态隔离(tests/unit_tests/test_subagents.py:332)。 - 状态回写(事实,外加探针 P7 发现的一个边界情况):
- 子 Agent 中未被排除的状态字段会通过
Command更新返回(subagents.py:731); - 使用
StateBackend时,子 Agent 写的文件会出现在父 Agent 里(探针 P4); - 子 Agent 删除的文件会在父 Agent 里重新出现:子 Agent 的工具返回了
Deleted /old.md,但/old.md仍留在父 Agent 的files里。原因是子 Agent 返回的是完整的files字典,被删的键只是不存在,而不是一个None删除标记,父 Agent 的合并 reducer 于是保留了旧条目。
解读: 这是一个只影响
StateBackend的小正确性缺口。使用磁盘或沙箱后端时,删除发生在真实的文件系统上。 - 子 Agent 中未被排除的状态字段会通过
- 异步子 Agent:
start/check/update/cancel/list_async_task工具通过 LangGraph SDK 驱动远程 Agent Protocol 图(threads.create+runs.create,async_subagents.py:245-340)。任务记录放在async_tasks状态里。这类子 Agent 不继承顶层的interrupt_on。 - 工作流与编排: 没有 DAG 或工作流引擎,编排由模型通过
task调用驱动。可选的例外是RubricMiddleware的”打分—修改”循环和异步任务工具。
重要源码文件(Important Source Files)
| 文件 | 为什么要读 |
|---|---|
libs/deepagents/deepagents/graph.py |
完整的组装顺序;子 Agent 栈;提示词拼装;必需的中间件 |
libs/deepagents/deepagents/middleware/__init__.py |
“中间件还是工具”的简短论证 |
libs/deepagents/deepagents/middleware/filesystem.py |
工具、按能力过滤、权限、大结果与大输入的移出、修改冲突检查 |
libs/deepagents/deepagents/middleware/_message_eviction.py、_overflow_clip.py |
预览存根;超长时的尾部裁剪 |
libs/deepagents/deepagents/middleware/summarization.py |
非破坏性压缩、历史卸载、超长恢复、compact_conversation 工具 |
libs/deepagents/deepagents/middleware/subagents.py |
task 工具、隔离规则、fork 模式 |
libs/deepagents/deepagents/middleware/memory.py、skills.py、_skill_tools.py |
记忆注入;skills 及其工具的渐进披露 |
libs/deepagents/deepagents/backends/{protocol,state,composite,sandbox,local_shell}.py |
存储/执行抽象,以及隔离方面的设计 |
libs/deepagents/deepagents/profiles/harness/harness_profiles.py |
按模型的 harness 配置 |
libs/deepagents/deepagents/_messages_reducer.py |
让永不截断的日志也能以 O(N) 写入检查点 |
libs/code/deepagents_code/agent.py(create_cli_agent) |
一个真实产品如何配置这个 SDK |
libs/code/deepagents_code/client/launch/server.py、client/remote_client.py |
客户端/服务端的进程拆分 |
langchain/agents/factory.py(外部依赖,langchain 1.4.3) |
真正的循环:model_node、_make_model_to_tools_edge |
值得借鉴的 5+ 个实现决策(5+ Implementation Decisions Worth Learning From)
1. 把 harness 做成别人循环上的有序中间件
- 做法:
create_deep_agent构建[Filesystem, SubAgent, Summarization, PatchToolCalls, *caller, *profile, Skills, caching, Memory, HITL, UnsupportedContent, ToolExclusion],然后调用create_agent(graph.py:888-1011)。 调用方的中间件按.name原位替换内置中间件(graph.py:210-244)。核心脚手架不能被排除(graph.py:247-262)。 CLI 就用这个机制把自己的压缩中间件换进摘要压缩的位置:CLICompactionMiddleware.name返回"SummarizationMiddleware"(libs/code/deepagents_code/offload_middleware.py:1017, 1057-1060)。 - 解决的问题: 许多相互正交的关注点(工具、提示词片段、压缩、审批)都要作用于每次模型调用,而又不想分叉出自己的循环。
- 有意思的地方: 几乎不掌握控制流,持久化、流式输出和中断都由 LangGraph 免费提供。按名称替换让默认实现可以被覆盖,不需要插件系统。
- 权衡:
- 顺序带有语义,文档字符串为此写了约 40 行(
graph.py:372-427)。 - 调试时需要知道运行的是四种栈中的哪一种(主 Agent、声明式子 Agent、编译好的子 Agent、异步子 Agent)。
- 存在隐式耦合:一项功能可能依赖于在压缩之后运行。
- 顺序带有语义,文档字符串为此写了约 40 行(
- 源码位置:
graph.py、middleware/__init__.py。 - 可复用之处: 定义一个三钩子契约(
before_run、wrap_model_call、wrap_tool_call),把功能组装成一个有序、可按名称替换的列表,并把少数承重的中间件标为不可移除。
2. 非破坏性压缩:记录事件,重建视图
- 做法:
SummarizationMiddleware从不改写state["messages"]。它保存_summarization_event = {cutoff_index, summary_message, file_path},每次调用时计算[摘要] + messages[cutoff:](summarization.py:821-858, 1487-1623)。 被移出的片段追加到/conversation_history/<session>.md,摘要消息里带着指向它的路径(:772-803)。 工厂函数的文档字符串把这一点和 LangChain 的版本做了对比,后者会“用 RemoveMessage(id=REMOVE_ALL_MESSAGES) 改写消息”(:1802-1807)。 - 解决的问题: 压缩通常会不可逆地丢失信息,破坏重放、评测和共享状态。
- 有意思的地方: 提示词变成了只追加日志之上的一个视图,每个摘要都带着找回原文的指针。
- 权衡:
- 状态会无限增长,靠
DeltaChannel检查点来负担这部分开销(graph.py:74-77)。 - 连续多次压缩需要做索引换算(
:860-886)。 - 每次调用都要计算 token。
- 保留窗口必须明显小于触发阈值。我的复现显示,
保留量 ≥ 触发阈值时每次调用都会重新摘要。
- 状态会无限增长,靠
- 源码位置:
middleware/summarization.py;测试tests/unit_tests/middleware/test_summarization_middleware.py、test_compaction_recovery.py。 - 可复用之处: 原始对话记录只追加;把压缩存为元数据,按需渲染模型视图;永远把被移出的片段写到 Agent 读得到的地方。
3. 把文件系统当作上下文的溢出阀
- 做法: 超过 2 万 token 的工具结果和超过 5 万 token 的用户消息被写入后端,替换为带分页读取说明的首尾预览(
filesystem.py:3359-3433, 3509-3538;_message_eviction.py:115-284)。 排除名单是刻意挑选的:会自行截断的 grep 和 glob 输出,应该促使模型缩小查询范围,而不是被卸载(filesystem.py:1601-1630)。 沙箱可以在源头捕获输出,让大输出根本不经过网络(sandbox.py:1340-1410)。 - 解决的问题: 一次
cat或一次测试运行就可能撑爆上下文窗口。 - 有意思的地方: 它复用 Agent 已有的工具(
read_file、grep)作为检索机制,不需要新的”记忆 API”。 - 权衡: 要靠模型自己决定去分页读取;预览可能藏住了中间的关键部分;卸载的文件会在存储中不断累积。
- 源码位置:
middleware/filesystem.py、middleware/_message_eviction.py、backends/sandbox.py。 - 可复用之处: 给每个观察结果设上限,把完整内容存到以
tool_call_id为键的确定路径下,并展示首尾内容和准确的分页读取说明。
4. 统一的存储抽象,按能力过滤工具
- 做法: 所有文件 I/O 都经过
BackendProtocol。execute只存在于SandboxBackendProtocol上。 后端不支持时,FilesystemMiddleware从请求中去掉execute(以及delete)(filesystem.py:3194-3240)。探针 P2 确认使用StateBackend时没有execute。CompositeBackend按最长前缀路由,但execute交给默认后端(composite.py:195-283, 814-850)。StateBackend通过 LangGraph 的CONFIG_KEY_SEND写入图状态,同一步内可以读到自己刚写的内容(state.py:81-119)。 - 解决的问题: 同一个 Agent 需要不改代码就能运行在临时状态(测试、SaaS)、本地磁盘(CLI)和远程虚拟机(生产环境)上。
- 有意思的地方: 模型永远看不到一个注定失败的工具,存储选择变成了部署配置。
- 权衡:
StateBackend导入了私有 APIlanggraph._internal._constants。- 权限在工具里执行,而不在后端:“直接使用后端时目前不会应用 permissions”(
graph.py:501-504)。 - 路径路由约束不了 shell。
- 源码位置:
backends/、middleware/filesystem.py。 - 可复用之处: 把运行环境建模成按能力分类的接口,每次请求时根据能力推导出可见的工具列表。
5. 子 Agent 作为一个工具,并隔离上下文
- 做法: 只有一个
task(description, subagent_type)工具。- 子 Agent 从
[HumanMessage(description)]加上共享的非私有状态开始。 - 它只返回最后一条非空 AI 文本,或者序列化成 JSON 的结构化结果(
subagents.py:721-838)。 - 未知的参数键会被拒绝,指令无法藏在自编的字段里(
:441-464)。 - 子 Agent 不能继续委派。
- 测试阶段的
mode="fork"把父 Agent 摘要后的历史交给子 Agent(:363-390)。
- 子 Agent 从
- 解决的问题: 探索性工作(搜索、读大量文件)会污染父 Agent 的上下文;长任务也能从并行的独立工作者中受益。
- 有意思的地方: 隔离是在结构上强制实现的(状态过滤、私有字段、禁止递归),而不是靠提示词。
- 权衡: 父 Agent 必须写出完整的任务说明;子 Agent 的工作过程对父 Agent 不可见(只在追踪中可见);探针 P7 发现了删除不回传的缺口;每个子 Agent 都要重复承担系统提示词和工具 schema 的开销。
- 源码位置:
middleware/subagents.py;测试tests/unit_tests/test_subagents.py(例如:130、:332、:415)。 - 可复用之处: 把委派做成一个工具,输入是自包含的任务说明,输出是一份报告;进出时剔除历史和私有状态。
6. 记忆即可编辑的文件,并注入提示词
- 做法:
AGENTS.md来源被一次性加载到私有状态,每次调用都追加到系统提示词(memory.py:279-383)。Agent 用普通的edit_file更新记忆。记忆提示词把这些内容定位为可能过时的数据,并列出绝不能保存的内容(memory.py:105-171)。 - 解决的问题: 不需要额外基础设施就能跨会话学习,而且记忆对人来说可查看、可 diff。
- 有意思的地方: 它复用文件系统工具,而”记忆是数据不是指令”的定位,应对的是借记忆注入恶意提示词的风险。
- 权衡: 同一线程内会过期(探针 P6);没有检索或筛选,整份文件都被注入;质量取决于模型是否主动写入;提示词会随记忆增长。
- 源码位置:
middleware/memory.py;CLI 的接线在agent.py:3056-3093。 - 可复用之处: 先用 Agent 可编辑的普通文件加上提示词定位。如果按会话缓存记忆,就要在记忆路径被写入时让缓存失效,这正是这个设计留下的缺口。
7. 由评测驱动的提示词精简,规划改为按需启用
- 做法: 删掉了作者手写的基础提示词和内置的工具使用说明(提交
a8d1b32:“没有哪个方案在统计上有显著差异,因此按简约原则选最精简的 Agent”),并把TodoListMiddleware改为按需启用(提交9340518)。针对特定模型的指导移到了 harness profile 里(profiles/harness/_*.py)。 - 解决的问题: 提示词膨胀:和工具 schema 重复、消耗 token,还会和工具的实际行为渐渐脱节。
- 有意思的地方: 这些决定是靠测量做出的,旧提示词仍然可以通过一个弃用兼容层取到(
graph.py:125-141)。 - 权衡: 较弱的模型可能需要更多引导,这部分被推给了 profile;行为更依赖工具描述的质量。
- 源码位置:
graph.py:666-673;profiles/;提交历史。 - 可复用之处: 把每段提示词都当作需要评测验证的假设,把针对模型的调优放进数据。
8. 防御性的历史修复,以及考虑缓存的排序
- 做法:
PatchToolCallsMiddleware在每次运行前为悬空或无效的工具调用补上结果(patch_tool_calls.py:16-52)。- 同路径修改冲突检查拒绝同一轮中相互竞争的编辑(
filesystem.py:198-224)。 - 超长恢复最多只发一次严格更小的重试(
summarization.py:1415-1454)。 - 最可能变化的提示词片段 Memory 被追加在最后,profile 中间件放在“核心中间件和记忆之间,这样记忆更新(会改变系统提示词)不会让 Anthropic 的提示词缓存前缀失效”(
graph.py:925-927)。 - CLI 只在本地上下文变化时才重新发送它(
local_context.py:826-834)。
- 解决的问题: 被中断的运行、并行工具调用和提供商的限制,会造成无效的对话记录、重试风暴和缓存未命中。
- 可复用之处: 每次运行前检查每个工具调用是否都有结果;拒绝相互冲突的并行写入;限制恢复重试的次数;按从最稳定到最易变的顺序排列提示词片段。
最小复现(Minimal Reproduction)
experiments/deepagents/ 是一个只依赖标准库的 Python 包 minideep,约 1,200 行。它复现的是架构:一个普通循环 + 有序中间件 + 可插拔后端。
用 ScriptedModel 代替 LLM,因此每个属性都能被精确断言。
| minideep | 复现的内容 |
|---|---|
loop.py::Agent |
LangChain create_agent 的控制流:每步新建请求;洋葱式组合的 wrap_model_call / wrap_tool_call;没有工具调用时退出;错误作为观察结果 |
graph.py::create_deep_agent |
栈顺序;自动加入 general-purpose 子 Agent;子 Agent 没有 task |
backends.py |
通过运行上下文写入的 StateBackend(类似 CONFIG_KEY_SEND)、DirBackend、ShellBackend(无隔离)、CompositeBackend 路由、supports_execution |
filesystem.py |
execute 的能力过滤、2 万 token 卸载与首尾预览、read_file 分页、修改冲突检查、与上游一致的排除名单 |
summarization.py |
基于事件的非破坏性压缩、历史 .md 卸载、安全切分点、连续事件、ContextOverflowError 回退、trim_tokens_to_summarize |
subagents.py |
带隔离的 task 工具、只返回最终文本、共享 files 回写、剔除私有字段 |
memory.py |
AGENTS.md 记忆,每个线程只加载一次(复现了过期问题);只披露元数据的 skills |
演示(./run.sh demo)模拟排查一套失败的测试。过程中它会:
- 卸载一个 170 KB 的
execute输出; - 用 grep 搜索被卸载的文件;
- 按需读取一个 skill;
- 委派给一个隔离的子 Agent;
- 分页读取一份日志,直到摘要压缩触发两次;
- 把一条偏好写进由状态承载的
/memories/路由。
最后检查 10 条不变式。
验证(Verification)
所有命令都在本次会话的容器中运行(Python 3.13.16,uv 0.11.32)。
1. 上游测试套件(固定提交,锁定依赖):
cd libs/deepagents && uv sync --group test --frozen
uv run --frozen --group test pytest -q -n auto --disable-socket --allow-unix-socket tests/unit_tests
# → 3251 passed, 118 skipped, 3 xfailed in 25.68s
第一次用未锁定版本的 pip 安装时有 51 个失败,主要集中在 skill 工具载荷和视频相关测试。换成 uv.lock 后这些失败全部消失,说明它们是环境造成的,不是代码问题。
2. 用真实 SDK 做的运行时探针(experiments/deepagents/upstream_probe/probe_deepagents.py)。它用一个脚本化的 BaseChatModel 驱动 create_deep_agent,不需要网络。
分别在锁定环境(langgraph 1.2.12、langchain-core 1.6.6)和最新发布版本(langgraph 1.2.14、langchain-core 1.6.7)下运行,除一个随机的会话 id 外输出完全相同。
| 探针 | 预期(来自源码) | 观察结果 |
|---|---|---|
| P1 默认中间件栈 | Filesystem、SubAgent、Summarization、PatchToolCalls、缓存、UnsupportedContent | 一致 |
P2 StateBackend 下的 execute |
隐藏 | 隐藏 |
| P3 17 万字符的工具结果 | 被卸载,留下存根 | /large_tool_results/call_big_1;存根 1,812 字符 |
| P4 子 Agent 的输入/输出 | 输入 [System, Human(description)],输出最终文本,文件共享,没有 task |
全部确认 |
| P5 摘要压缩 | 状态完整,视图很小 | 状态 44 条消息,最后一次视图 6 条,cutoff_index=39 |
| P6 线程中途修改记忆 | (推断)过期 | 过期:第二轮的提示词仍是旧内容 |
| P7 子 Agent 内删除文件 | (推断)不回传 | 不回传:父 Agent 里仍有 /old.md |
3. 复现:
cd experiments/deepagents && ./run.sh
# [1/4] venv + pip install -e .[test] [2/4] 构建 wheel:minideep-0.1.0-py3-none-any.whl
# [3/4] 21 passed in 0.07s [4/4] 演示:10/10 项检查通过,exit 0
4. 变异检查(测试真的测到架构了吗?) 逐个注入四个架构层面的回归:
| 变异 | 结果 |
|---|---|
| 子 Agent 收到父 Agent 的历史 | 1 个测试失败 |
| 摘要压缩丢掉了事件 | 3 个测试失败 |
| 关闭卸载 | 2 个测试失败 |
| 每次运行都重新加载记忆 | 1 个测试失败 |
恢复代码后,21 个测试再次全部通过。
5. 真实模型运行(experiments/deepagents/real_model/run_ark.py)。用 deepseek-v4.1-flash(服务端显示为 deepseek-v4-1-flash-260910)经火山引擎方舟的 OpenAI 兼容接口驱动固定提交的真实 SDK,模型对象为 ChatOpenAI(use_responses_api=False)。四个场景各跑 3 轮,每轮完整运行耗时 46–63 秒。
| 场景 | 检验什么 | 结果(3/3 轮) |
|---|---|---|
| S1 大结果 | 真实模型会不会顺着卸载指针取回数据? | ✅ 它用多个关键词(FAILED、ERROR、failed 等)对 /large_tool_results/<tool_call_id> 执行 grep,确认只有一个失败;然后带偏移量调用 read_file 查看上下文(两轮的偏移是 1730,一轮是日志末尾的 2995),并正确给出 tests/test_1734.py::test_case |
| S2 委派 | 它怎样给隔离的子 Agent 写任务说明? | ✅ task 的说明分别为 706 / 772 / 941 字符,内容自包含(内联了文件内容、指定了输出格式,还写了”你是无状态的,看不到我的对话”)。子 Agent 写出了 /summary.md |
| S3 记忆 | 它会不会主动保存用户声明的偏好? | ✅ 它对 /AGENTS.md 调用 edit_file,新增一条”用户偏好”:”always provide them in Rust” |
| S4 摘要压缩 | 已被摘要掉的片段里的事实还能用吗? | ✅ max_input_tokens=12000 强制触发压缩。包含事实的片段是状态中的第 2 条消息,位于 cutoff_index=11 之前,因此被摘要掉了。这个事实保留在摘要文本里,模型没有读取 /conversation_history/*.md 就答对了 |
这能说明什么、不能说明什么:上下文设计对模型的几个假设——取回卸载的数据、写出完整的任务说明、主动写记忆——在一个当前模型、几个简单明确的任务上成立了。S4 没有走到回退路径(摘要丢了某个细节、模型必须打开历史文件),所以这条路径还没有经过真实模型的检验。n=3、只有一个模型,这是佐证,不是基准测试。
6. 图表。 8 张 Archify 图都通过了 finalize --quality showcase --repo-root <clone>,其中包含 schema 校验、带校验的交付、严格的来源检查,以及无头 Chromium 浏览器检查。1440×900 的截图都人工看过。见 assets/deepagents/archify/README.md。
验证的已知局限:
- 真实模型的证据只覆盖一个模型(
deepseek-v4.1-flash)、四个简单场景、各 3 轮。长而模糊的任务,以及摘要有损后模型必须读回历史文件的路径,都还没有测试。 - 没有运行 CLI 的终端界面(
dcode),CLI 路径只通过读源码验证。让dcode对接 OpenAI 兼容接口需要额外配置,因为字符串形式的openai:模型规格默认使用 Responses API(profiles/provider/_openai.py)。 minideep顺序执行工具调用,也没有 checkpointer。
我会复用什么(What I Would Reuse)
- 三钩子中间件契约,加上有序、可按名称替换的中间件栈,作为 Agent 系统的核心扩展点。
- 只追加的对话记录 + 压缩事件 + 渲染出的视图。 永远不要让摘要压缩破坏日志,并且总要留一条找回原文的路。
- 给观察结果设上限,超出就卸载并留下指针,以
tool_call_id为键,使用模型能据此行动的预览格式。 - 按能力分类的运行环境接口,每次请求时由它决定工具的可见性。
- 把委派做成一个工具,在结构上隔离,约定只返回一份报告。
- 每次运行都修复历史(悬空的工具调用),以及考虑缓存的提示词排序。
- 我会改的地方:
- 记忆路径被写入时,让缓存的记忆失效;
- 把子 Agent 的
files合并回父 Agent 时,发出显式的删除标记; - 在后端层执行权限检查。
局限与开放问题(Limitations / Open Questions)
- 记忆过期(探针 P6):按线程缓存是有意为之(比如为了提示词缓存的稳定),还是疏漏?记忆提示词要求模型”及时”保存学到的东西,但这些内容在下一个线程之前都不会出现在提示词里。作者意图不确定。
- 子 Agent 删除不回传(探针 P7):只影响
StateBackend。我没有找到对应的上游 issue。尚未提交,值得和维护者确认。 - 与 LangGraph 内部实现耦合:
StateBackend使用langgraph._internal._constants.CONFIG_KEY_READ/SEND。LangGraph 升级时这是一个稳定性风险。 - 权限与 shell: 除非每条规则都限定在路由范围内,否则路径权限无法和能执行命令的后端一起使用。因此真正的隔离取决于选择沙箱后端。
- 没有检索层: 对仓库的理解依赖 Agent 自己用
grep/glob/read_file导航,以及摘要。超大仓库的效果取决于模型能力和子 Agent 扇出。 - 没有深入验证的部分: 实验性的
talon运行时宿主、acp、评测框架、QuickJS 的js_eval解释器、视频/多模态路径,以及 CLI 的重试和成本中间件。
延伸阅读(Further Reading)
- 源码:langchain-ai/deepagents @ 16e84d9,尤其是
libs/ARCHITECTURE.md(概览;其中的说法已对照上文的代码核实)和libs/code/ARCHITECTURE.md。 - 提交:
a8d1b32(精简提示词)、9340518(todo 列表改为按需启用)、92cd8e7(skill 工具在读取后才披露)、6b4427f(子 Agent fork)。 - LangChain
create_agent与中间件文档:https://docs.langchain.com/oss/python/langchain/middleware/overview - Deep Agents 文档:https://docs.langchain.com/oss/python/deepagents/overview
- 代码中引用的 Agent Skills / AGENTS.md 约定:https://agents.md/