DeepAgents 框架介绍与应用实战
一、DeepAgents 框架定位与核心价值
1.1 什么是 DeepAgents ?
DeepAgents(代码库名为 deepagents)是一个基于 LangChain 和 LangGraph 构建的企业级高级智能体框架。它建立在 LangGraph(底层运行时)和 LangChain(工具/模型层)之上,是一个高阶的Agent Harness(智能体装备/套件)。
-
定位:它旨在简化长运行自主智能体 (Long-running Autonomous Agents) 的开发过程,通过内置的最佳实践和中间件,解决复杂任务中的规划、记忆、工具使用和环境交互问题。
-
核心理念:如果说 LangChain 提供了积木,LangGraph 提供了地基,那么 DeepAgents 就是一套成品级的框架。它预设了最佳实践(规划、文件系统、子智能体),让你能快速构建类似 “OpenAI Deep Research” 或 “Claude Code” 的应用。
论文地址:https://arxiv.org/pdf/2510.21618
DeepAgents 开源地址:https://github.com/langchain-ai/deepagents


1.2 解决的核心问题
传统的 Agent 开发通常运行一个简单的循环:思考→调用工具→观察→重复。这种模式在处理多小时或多天的任务时,容易遇到以下"浅层陷阱"(Shallow Agent Problem)
- 规划能力缺失:原生 Agent 倾向于“走一步看一步”,缺乏全局视角的任务拆解,容易在多步任务中迷失方向。
- 遗忘与混乱:在执行超过 10-20 步的长任务时,由于 Context Window(上下文窗口)限制,传统 Agent 容易忘记初始目标或陷入死循环。
- 环境交互困难:文件系统操作、代码执行环境(沙箱)的配置和安全管理复杂。
- 上下文污染:所有工具返回结果都堆积在一个对话历史中,导致噪声过大。
- 协作编排复杂:多智能体(Multi-Agent)之间的任务分发和上下文隔离难以实现。
DeepAgents 通过引入"类人"的工作流解决了这些问题:先做计划(Plan),再执行,利用文件系统管理记忆,遇到复杂子任务时"外包"给子 Agent。将规划工具、文件系统访问、子代理和详细提示词等关键机制整合在一起,以支持复杂的深度任务 。

import deepagents # List all the functions print(dir(deepagents))
输出:

1.3 应用场景
DeepAgents 不适合用来做简单的聊天机器人(Chatbot),它是为重任务设计的,它适用于任务需规划、上下文海量、需多专家协作、要求持久记忆的场景,将LangChain生态从单步响应提升至自主完成复杂项目的高度:
- 深度调研 (Deep Research):自动进行多轮网页搜索、阅读文档、整理笔记并生成长篇报告(如分析某个行业的市场格局)。
- 全栈代码生成 (Coding Assistant):类似 Claude Code,在沙箱环境中编写、运行、测试和修复代码,甚至重构整个代码库。
- 复杂数据分析:自动连接数据库,生成 SQL,执行查询,将中间数据存为 CSV 文件(在虚拟文件系统中),最后生成图表。
- 自动化运维 (DevOps Automation):操作文件系统、执行 Shell 命令、管理服务器状态。
- 复杂工作流编排:需要多角色协作(如产品经理-程序员-测试员)的复杂业务流程。
当然这个描述相对来说比较抽象,因此我们这里对适用于DeepAgents的场景进行一个总结:
从技术定位看,LangChain 适用于需要自定义提示与工具的基础智能体搭建;LangGraph 更适合构建复杂的多智能体系统与工作流;而 DeepAgents 面向希望省去底层开发、直接采用深度自主机制的用户,可快速实现 AutoGPT 类应用。因此,DeepAgents 本质上是基于 LangChain 的深度模式封装——它并非替代 LangChain 或 LangGraph,而是将其常用抽象与运行时封装为开箱即用的组件,可视为一层“开发加速器”。

LangChain:提供 Prompt, Models, Tools 等积木。
LangGraph:提供 State, Nodes, Edges 等地基和连接逻辑。
DeepAgents:LangGraph 的一种“最佳实践实现”。它底层使用 LangGraph 来管理状态和循环,但向上提供了更高级的 API (`create_deep_agent`),隐藏了底层的图构建细节。
三、DeepAgents 核心功能介
安装deepagents,这里python使用3.12.13版本:
pip install deepagents
查看依赖

发现底层会自动安装langchain、langgraph相关组件:

测试:
import os import dotenv from langchain_deepseek import ChatDeepSeek # 1.加载.env环境变量 dotenv.load_dotenv() # 2.初始化模型 model = ChatDeepSeek(model="deepseek-v4-pro", api_base="https://api.deepseek.com",api_key=os.getenv('api_key'), temperature=0.7) # 调用模型 print(model.invoke("农副产品批发行业市场研究分析?"))

3.1 核心入口:create_deep_agent()
这是整个框架的核心函数,它创建了一个功能完整的深度智能体。
默认配置:
- 使用 Claude Sonnet 4 或 GPT-4o 作为默认模型(推荐)。
- 集成 7 个核心文件操作工具。
- 提供待办事项管理功能。
- 支持子代理调用。
关键参数:
model: 支持自定义语言模型。tools: 自定义工具集。system_prompt: 系统提示词subagents: 子代理配置。backend: 文件存储后端。interrupt_on: 人机交互配置 (Human-in-the-Loop)。允许在特定节点暂停 Agent 执行,等待人工干预。这对于安全审核(删除文件)、成本控制(调用昂贵 API)和质量保证至关重要。
源码如下:
def create_deep_agent( model: str | BaseChatModel | None = None, tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None, *, system_prompt: str | None = None, middleware: Sequence[AgentMiddleware] = (), subagents: list[SubAgent | CompiledSubAgent] | None = None, response_format: ResponseFormat | None = None, context_schema: type[Any] | None = None, checkpointer: Checkpointer | None = None, store: BaseStore | None = None, backend: BackendProtocol | BackendFactory | None = None, interrupt_on: dict[str, bool | InterruptOnConfig] | None = None, debug: bool = False, name: str | None = None, cache: BaseCache | None = None, ) -> CompiledStateGraph:
案例:实现网络搜索案例
pip install langchain-tavily
案例代码:
import os import dotenv from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_tavily import TavilySearch from langgraph.checkpoint.memory import InMemorySaver dotenv.load_dotenv() # 1.初始化Tavily搜索工具 tavily = TavilySearch(max_results=5) # 2.准备提示词 prompt = """ 你是一位资深的研究人员。你的工作是进行深入的研究,然后撰写一份精美的报告。 你可以通过互联网搜索引擎作为主要的信息收集工具。 ## 可用工具 ### `互联网搜索` 使用此功能针对给定的查询进行互联网搜索。您可以指定要返回的最大结果数量、主题以及是否包含原始内容。 ### `写入本地文件` 使用此功能将研究报告保存到本地文件。当您完成研究并生成报告后,请使用此工具将完整的报告内容保存到文 件中。 - 文件路径建议使用 .md 格式(Markdown),例如 "research_report.md" 或 "./reports/报告名 称.md" - 请确保报告内容完整、结构清晰,包含所有章节和引用来源 ## 工作流程 在进行研究时: 1. 首先将研究任务分解为清晰的步骤 2. 使用互联网搜索来收集全面的信息 3. 将信息整合成一份结构清晰的报告 4. **重要**:完成报告后,务必使用 `写入本地文件` 工具将完整报告保存到本地文件 5. 务必引用你的资料来源 **注意**:请确保在完成研究后,将完整的报告内容保存到文件中,这样用户可以方便地查看和保存报告。 """ # 3.初始化大模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider='deepseek', base_url="https://api.deepseek.com", api_key=os.getenv('api_key'), temperature=0.7, max_tokens=10000, ) # 4.创建agent agent = create_deep_agent( model=deepseek_v4_pro, # 使用DeepSeek模型 tools=[tavily], # 使用Tavily搜索工具 system_prompt=prompt, # 使用自定义提示词 checkpointer=InMemorySaver() # 使用内存作为短期记忆 ) resp = agent.invoke( {"messages": [{"role": "user", "content": "请使用互联网搜索引擎进行搜索,并编写一份关于“如何实现一个企业智能知识库的”的详细报告。"}]}, config={"configurable": {"thread_id": "1"}} ) print(resp["messages"][-1].content)

3.2 create_deep_agent内部结构
源码参数说明:

安装美化代码库Rich
pip install rich
代码:
from rich.console import Console from rich.table import Table from rich.panel import Panel from create_demo_deep_agent.serarch_agent import agent RICH_AVAILABLE = True console = Console() def print_agent_tools(agent): """ 打印 Agent 中加载的所有工具 包括用户自定义工具、文件系统工具、系统工具等 """ # 获取 agent 的 nodes (LangGraph 的节点) if hasattr(agent, 'nodes') and 'tools' in agent.nodes: tools_node = agent.nodes['tools'] # tools_node 是 PregelNode,真正的 ToolNode 在 bound 属性中 if hasattr(tools_node, 'bound'): tool_node = tools_node.bound # 从 ToolNode 获取工具 if hasattr(tool_node, 'tools_by_name'): tools = tool_node.tools_by_name # 分类工具 user_tools = [] filesystem_tools = [] system_tools = [] for tool_name, tool in tools.items(): tool_info = { 'name': tool_name, 'description': getattr(tool, 'description', '无描述') } # 分类 if tool_name in ['ls', 'read_file', 'write_file', 'edit_file', 'glob', 'grep', 'execute']: filesystem_tools.append(tool_info) elif tool_name in ['write_todos', 'task']: system_tools.append(tool_info) else: user_tools.append(tool_info) # 打印加载工具的输出 _print_tools_rich(user_tools, filesystem_tools, system_tools) else: print("无法获取工具列表 (tools_by_name 不存在)") else: print("无法获取工具列表 (bound 属性不存在)") else: print("无法获取工具列表 (nodes 结构不符合预期)") def _print_tools_rich(user_tools, filesystem_tools, system_tools): """使用 Rich 库美化打印工具列表""" console.print() # 创建表格 table = Table(title="Agent 加载的工具列表", show_header=True, header_style="bold magenta") table.add_column("类别", style="cyan", width=20) table.add_column("工具名称", style="green", width=20) table.add_column("描述", style="white", width=60) # 添加用户工具 for i, tool in enumerate(user_tools): category = "用户工具" if i == 0 else "" desc = tool['description'][:80] + "..." if len(tool['description']) > 80 else tool['description'] table.add_row(category, tool['name'], desc) # 添加文件系统工具 for i, tool in enumerate(filesystem_tools): category = "文件系统工具" if i == 0 else "" desc = tool['description'][:80] + "..." if len(tool['description']) > 80 else tool['description'] table.add_row(category, tool['name'], desc) # 添加系统工具 for i, tool in enumerate(system_tools): category = "系统工具" if i == 0 else "" desc = tool['description'][:80] + "..." if len(tool['description']) > 80 else tool['description'] table.add_row(category, tool['name'], desc) console.print(table) # 打印统计 total = len(user_tools) + len(filesystem_tools) + len(system_tools) console.print(Panel( f"[bold green]共计 {total} 个工具[/bold green]\n\n" f"• 用户工具: {len(user_tools)} 个\n" f"• 文件系统工具: {len(filesystem_tools)} 个\n" f"• 系统工具: {len(system_tools)} 个", title="统计信息", border_style="green" )) console.print() if __name__ == '__main__': print_agent_tools(agent)


这里可以看到,除了自己定义的工具(如 Tavily 搜索),DeepAgents 还默认添加了一些其他工具:
-
文件系统中间件(FileSystemMiddleware): 用于读写、查询、执行文件系统中的文件。
-
待办事项中间件(TodoListMiddleware): write_todos 用于写入待办事项,task 用于创建子agent来执行待办事项。
这些都是 DeepAgents 特有的功能,用于支持智能体在实际应用中的各种场景。那么,接下来我们来看看这些功能的具体应用。
Langgraph Studio 中可视化结构图:能看到
- PatchToolCallsMiddleware用于 自动检测并修复“悬空”的工具调用的关键中间件,确保工具调用的完整性和正确性。
- SummarizationMiddleware上下文压缩中间件,防止上下文过长
四、四大核心内置工具与组件详解
DeepAgents 通过中间件 (Middleware) 的形式,为智能体注入了四项核心能力,构成了框架的四大支柱(Four Pillars):

DeepAgents四大内置工具通过角色分离、状态贯通、成本优化的设计哲学,将长周期Agent的开发复杂度降低70%以上,同时通过LangGraph运行时保障生产级可靠性。其核心价值在于将原本需要手动编排的规划-存储-委托-执行流程,固化为中心化、可复用、可观测的中间件体系,标志着AI Agent从"脚本化"向"产品化"的关键演进。
| 维度 | 系统提示词(System Prompt) | 规划工具(Planning Tool) | 文件系统(File System) | 子代理(Sub Agents) |
|---|---|---|---|---|
| 角色定位 | 行为总导演:定义Agent的"世界观"与工具使用范式,确保三大中间件协同不偏离目标 | 任务架构师:将模糊需求转化为可执行、可追踪、可动态调整的结构化任务蓝图 | 上下文仓库:虚拟化存储引擎,解决长任务中的信息溢出与状态持久化难题 | 执行特派员:实现上下文隔离与专业分工,防止主Agent因深层递归导致状态混乱 |
| 核心功能 | 内置 Claude Code 风格指令,涵盖规划逻辑、文件操作规范、子代理调用协议,支持场景化自定义覆盖 |
write_todos:生成带优先级/依赖关系的 JSON 任务列表 read_todos:实时查询任务执行进度与状态 |
ls/glob:文件浏览与模式匹配 read/write/edit:CRUD 操作 grep:内容检索 execute:沙箱命令执行 |
task:动态生成同构或异构子Agent支持独立上下文窗口与工具集配置结果通过文件系统回传 |
| 技术实现 | 字符串模板,在 create_deep_agent 时注入;默认提示词约 2000 tokens,包含 ReAct 循环与三大中间件调用示例 |
TodoListMiddleware:拦截 LLM 输出中的 todo_list 字段,解析为 agent_state.todos 字典,状态变更触发持久化 |
FilesystemMiddleware:基于 LangGraph State 的 files 字段实现内存级虚拟文件系统,大工具结果(>2KB)自动放置 file 消息 |
SubAgentMiddleware:将 task 调用编译为独立的 StateGraph 子图,通过命名空间隔离状态,仅图通过 file 读取子图输出 |
| 状态管理 | 静态配置,单次会话内不可变;可通过 on_configurable_agent 实现热更新 |
动态状态机:每个 todo 包含 id / description / status / priority / dependencies 字段,执行后状态从 pending → completed,支持 update_todos 动态调整 |
持久化存储:默认存储在 LangGraph State,支持切换 StateBackend(内存/Redis/Postgres)实现跨会话文件共享 | 完全隔离:子Agent拥有独立的 messages 与 files 命名空间,异常不会污染父Agent状态;支持 max_iterations 限制防止无限递归 |
| 使用场景 | ① 垂直领域定制:金融研究/医疗诊断等需强化专业约束的场景② 多Agent协作:统一多个子Agent的行为规范③ 安全合规:注入数据脱敏、权限检查等硬性规则 |
① 长周期研究:自动拆解为文献检索→数据收集→分析→撰写的阶段性任务 ② 故障恢复:崩溃后通过 ③ 动态重规划:执行中发现信息不足时新增补充任务 |
① 大结果处理:搜索返回 100KB 内容自动落盘,避免上下文溢出 ② 知识沉淀:中间分析结果写入文件供后续步骤复用 ③ 多Agent数据共享:父Agent与子Agent通过文件交换数据,无需序列化传递 |
① 高风险操作隔离:网页抓取、代码执行等易失败任务交由子Agent处理 ② 专业化分工:主Agent负责总体编排,子Agent专注领域执行(如搜索、代码、分析) ③ 资源优化:子Agent可使用轻量模型,降低整体 Token 成本 |
4.1 系统提示词 (System Prompts)
功能:定义 Agent 的“人设”、行为准则和核心目标。
机制:框架会自动将用户定义的 system_prompt 与内置的 BASE_AGENT_PROMPT 结合。
作用:确保 Agent 始终遵循指令,理解其可用的工具集,并保持一致的输出风格。
角色本质:系统提示词是DeepAgents三大中间件协同的"契约",其默认版本包含:
- 规划指令:要求LLM在任务开始前必须调用write_todos,输出格式为JSON Schema;
- 文件操作规范:明确write_file用于新内容,edit_file用于局部修改,避免覆盖冲突;
- 子代理调用协议:规定task工具的参数结构及结果通过/subagent_results/ .md回传;
- 安全底线:禁止直接执行删除、格式化等危险命令,必须通过execute沙箱;
1.文件系统中间件提示词(FileSystemMiddleWare)
文件路径:deepagents/middleware/filesystem.py
- FileSystem_System_prompt 文件系统提示词

- Execute_Tool_Descriptition 执行工具提示词

2.TodoList中间件提示词(TodoListMiddleware)
文件路径:deepagents/middleware/todo.py
- WRITE_TODOS_SYSTEM_PROMPT写执行任务提示词

3.子智能体中间件提示词(SubAgentMiddleware)
文件路径:deepagents/middleware/subagent.py
- TASK_SYSTEM_PROMPT Task执行器提示词

4.2 规划工具 (Planning System / Todo List)
组件:TodoListMiddleware
工具名:write_todos
功能:Agent 在行动前先生成 Markdown 格式的待办事项列表 (Todo List),并在执行过程中更新状态(完成/进行中)。
工作流:
- Agent 接收复杂任务(简单短期的任务不会触发todolist)。
- 调用 write_todos 将任务拆解为子步骤 (Pending)。
- 每完成一步,更新状态为 (Completed)。
- 自我反思:在每一步行动前,Agent 都会看到当前的 Todo List,从而避免迷失方向。这强制模型进行"思维链"的显性化管理。
类似编程工具中的待执行项:

import os import dotenv from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_tavily import TavilySearch from langgraph.checkpoint.memory import InMemorySaver dotenv.load_dotenv() # 1.初始化Tavily搜索工具 tavily = TavilySearch(max_results=5) # 2.准备提示词 prompt = """ 你是一位资深的研究人员。你的工作是进行深入的研究,然后撰写一份精美的报告。 你可以通过互联网搜索引擎作为主要的信息收集工具。 ## 可用工具 ### `互联网搜索` 使用此功能针对给定的查询进行互联网搜索。您可以指定要返回的最大结果数量、主题以及是否包含原始内容。 ### `写入本地文件` 使用此功能将研究报告保存到本地文件。当您完成研究并生成报告后,请使用此工具将完整的报告内容保存到文 件中。 - 文件路径建议使用 .md 格式(Markdown),例如 "research_report.md" 或 "./reports/报告名 称.md" - 请确保报告内容完整、结构清晰,包含所有章节和引用来源 ## 工作流程 在进行研究时: 1. 首先将研究任务分解为清晰的步骤 2. 使用互联网搜索来收集全面的信息 3. 将信息整合成一份结构清晰的报告 4. **重要**:完成报告后,务必使用 `写入本地文件` 工具将完整报告保存到本地文件 5. 务必引用你的资料来源 **注意**:请确保在完成研究后,将完整的报告内容保存到文件中,这样用户可以方便地查看和保存报告。 """ # 3.初始化大模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider='deepseek', base_url="https://api.deepseek.com", api_key=os.getenv('api_key'), temperature=0.7, max_tokens=10000, ) # 4.创建agent(供 langgraph.json / Studio 导入) # 注意:langgraph dev / LangGraph API 会自行管理持久化,不能传自定义 checkpointer # agent = create_deep_agent( # model=deepseek_v4_pro, # 使用DeepSeek模型 # tools=[tavily], # 使用Tavily搜索工具 # system_prompt=prompt, # 使用自定义提示词 # ) # 本地脚本直接跑时,再挂内存 checkpointer local_agent = create_deep_agent( model=deepseek_v4_pro, tools=[tavily], system_prompt=prompt, checkpointer=InMemorySaver(), ) if __name__ == "__main__": # 本地脚本直接跑时,再挂内存 checkpointer local_agent = create_deep_agent( model=deepseek_v4_pro, tools=[tavily], system_prompt=prompt, checkpointer=InMemorySaver(), ) resp = local_agent.invoke( { "messages": [ { "role": "user", "content": "请使用互联网搜索引擎进行搜索,并编写一份关于“如何实现一个企业智能知识库的”的详细报告。", } ] }, config={"configurable": {"thread_id": "1"}}, ) print(resp["messages"][-1].content)
测试代码:
这段在做的事可以概括成一条流水线,每个 stream 步骤只看 messages 里最后一条,然后按消息类型分别打印:
| 步骤 | 看什么 | 打印什么 |
|---|---|---|
| 1 | messages[-1] |
本步新产生的那条消息 |
| 2 | tool_calls |
模型是否要调工具 |
| 3 | 有 content 且无 tool_calls | 记为候选「最终回答」 |
| 4 | content | 绿色面板:AI 思考/正文(长文截断) |
| 5 | tool_calls 列表 |
蓝色面板:工具名 + 参数 |
| 6 | 消息有 .name(ToolMessage) |
紫色面板:工具返回(截断) |
| 步骤 | 看什么 | 打印什么 |
|---|---|---|
| 1 | messages[-1] |
本步新产生的那条消息 |
| 2 | tool_calls |
模型是否要调工具 |
| 3 | 有 content 且无 tool_calls | 记为候选「最终回答」 |
| 4 | content | 绿色面板:AI 思考/正文(长文截断) |
| 5 | tool_calls 列表 |
蓝色面板:工具名 + 参数 |
| 6 | 消息有 .name(ToolMessage) |
紫色面板:工具返回(截断) |
对应一次典型运行顺序:
用户提问 → AI(可能带 tool_calls)→ 蓝色「工具调用」 → ToolMessage(工具结果)→ 紫色「工具响应」 → AI 再思考 / 再调工具 … → 最后一次纯文本 AI → final_response
如下:
import json from rich.console import Console from rich.json import JSON from rich.panel import Panel from create_demo_deep_agent.serarch_agent import local_agent RICH_AVAILABLE = True console = Console() def debug_agent(query: str, agent, save_to_file: str = None): """ 运行智能体并打印中间过程,使用Rich美化输出 :param query: 用于查询 :param save_to_file: 保存最终输出到文件(可选) :return: 最终的研究报告 """ console.print(Panel.fit(f"[bold cyan]查询:[/bold cyan] {query}", border_style="cyan")) step_num = 0 final_response = None for chunk in agent.stream( {"messages": [{"role": "user", "content": query}]}, config={"configurable": {"thread_id": "2"}}, stream_mode="values", # stream_mode="values" 时,每个 chunk 是完整 agent 状态字典 ): console.print(f"\n[bold yellow]{'-' * 80}[/bold yellow]") console.print(f"[bold yellow]步骤 {step_num} [/bold yellow]") console.print(f"[bold yellow]{'-' * 80}[/bold yellow]") step_num += 1 # stream_mode="values" 时,每个 chunk 通常是完整 agent 状态字典,例如: # {"messages": [HumanMessage, AIMessage, ToolMessage, ...], ...} if "messages" in chunk: # 取出截至当前步骤的全部消息列表(越往后越长,因为是累计状态) messages = chunk["messages"] if messages: # ---------- 1) 只关心「本步新增」的那条:取列表最后一条 ---------- # HumanMessage: 用户输入 # AIMessage: 模型回复(可能带 tool_calls,表示要调工具) # ToolMessage: 某个工具执行完后的返回结果 last_message = messages[-1] # ---------- 2) 安全读取 tool_calls ---------- # getattr(obj, "attr", default): 没有该属性时返回 default,避免 AttributeError # AIMessage 常有 tool_calls;Human/ToolMessage 通常没有 # `or []`:若属性存在但值为 None / 空,统一当成空列表,后面 for 不会报错 tool_calls = getattr(last_message, "tool_calls", None) or [] # ---------- 3) 记录「可能的最终回答」---------- # 条件:有正文 content,且没有待执行的工具调用 # 含义:模型这轮是在「直接回答用户」,而不是「先去调工具」 # 注意:中间过程也可能满足条件,所以用赋值覆盖;流结束后留下的就是最后一次纯文本回答 if getattr(last_message, "content", None) and not tool_calls: final_response = last_message.content # ---------- 4) 打印 AI 正文(思考/回答预览)---------- # 只要当前这条消息带 content,就展示(AI 常见;Tool 也会有 content,会走到下面工具响应分支) if getattr(last_message, "content", None): content = last_message.content # 长文本且不是「正在发起工具调用」的 AI 回复:只预览前 300 字,避免刷屏 # (真正完整内容会在函数返回的 final_response / 主程序 print 里看到) if len(content) > 300 and not tool_calls: preview = content[:300] + "..." console.print( Panel( # [dim]...[/dim] 是 Rich 标记:把提示文字显示成灰色弱化样式 f"{preview}\n\n[dim](内容较长,完整内容将在最后显示)[/dim]", title="[bold green]AI思考[/bold green]", border_style="green", ) ) else: # 短文本,或带 tool_calls 的 AI(content 可能为空/很短):完整打印 console.print( Panel(content, title="[bold green]AI思考[/bold green]", border_style="green") ) # ---------- 5) 打印「模型决定调用哪些工具」---------- # tool_calls 示例结构(dict 时)大致为: # [{"name": "tavily_search", "args": {"query": "..."}, "id": "call_xxx"}, ...] # 一条 AIMessage 里可以一次请求多个工具 for tool_call in tool_calls: # LangChain 里 tool_call 有时是 dict,有时是对象,两种都兼容 if isinstance(tool_call, dict): # dict:用 .get,缺 key 时给默认值 name = tool_call.get("name", "unknown") args = tool_call.get("args", {}) else: # 对象:用 getattr 读属性 name = getattr(tool_call, "name", "unknown") args = getattr(tool_call, "args", {}) # 整理成中文 key,方便在终端阅读 tool_info = {"工具名称": name, "参数": args} console.print( Panel( # json.dumps → 普通 JSON 字符串;ensure_ascii=False 保留中文 # rich.json.JSON → 终端里带语法高亮的 JSON 展示 JSON(json.dumps(tool_info, ensure_ascii=False)), title="[bold blue]工具调用[/bold blue]", border_style="blue", ) ) # ---------- 6) 打印「工具执行结果」(ToolMessage)---------- # ToolMessage 特有字段 name:被调用的工具名(如 tavily_search / write_file) # AIMessage/HumanMessage 一般没有有意义的 .name,这里用 name 是否存在来区分 if getattr(last_message, "name", None): # 工具返回可能很长(整页搜索结果),只展示前 500 字符 resp = str(last_message.content)[:500] if len(str(last_message.content)) > 500: # 提示被截断,并告诉原始总长度 resp += f"\n...(共{len(str(last_message.content))}字符)" console.print( Panel( resp, # 标题里带上工具名,一眼看出是哪个工具回的 title=f"[bold magenta]工具响应:{last_message.name}[/bold magenta]", border_style="magenta", ) ) # for 流式循环结束:agent 跑完所有步骤(思考 → 调工具 → 再思考 → … → 最终回答) console.print("\n[bold green]任务完成![/bold green]\n") # 返回过程中不断覆盖得到的「最后一次无 tool_calls 的 AI 正文」 return final_response print("调用函数已创建") if __name__ == '__main__': query = "详细调研关于如何使用deepagents框架实现一个爬取政策新闻,生成总结报告的助手,写一份结构化的总结报告。" resp = debug_agent(query, local_agent) print(resp)
输出如下:
D:\anaconda3\envs\deepagents\python.exe E:\code\InsightFlow\create_demo_deep_agent\demo_planning_sysytem_todo_list.py 调用函数已创建 ┌─────────────────────────────────────────────────────────────────────────────┐ │ 查询: │ │ 详细调研关于如何使用deepagents框架实现一个爬取政策新闻,生成总结报告的助手 │ │ ,写一份结构化的总结报告。 │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 0 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ 详细调研关于如何使用deepagents框架实现一个爬取政策新闻,生成总结报告的助手 │ │ ,写一份结构化的总结报告。 │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 1 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ 详细调研关于如何使用deepagents框架实现一个爬取政策新闻,生成总结报告的助手 │ │ ,写一份结构化的总结报告。 │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 2 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "write_todos", │ │ "参数": { │ │ "todos": [ │ │ { │ │ "content": "调研 deepagents 框架的基本概念、架构与核心特性", │ │ "status": "in_progress" │ │ }, │ │ { │ │ "content": "调研 deepagents 框架的安装与使用方法", │ │ "status": "pending" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现网页爬取功能", │ │ "status": "pending" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现政策新闻摘要与报告生成", │ │ "status": "pending" │ │ }, │ │ { │ │ "content": "整合信息,撰写结构化总结报告", │ │ "status": "pending" │ │ }, │ │ { │ │ "content": "将报告保存到本地文件", │ │ "status": "pending" │ │ } │ │ ] │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 3 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'in_progress'}, {'content': │ │ '调研 deepagents 框架的安装与使用方法', 'status': 'pending'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'pending'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'pending'}, {'content': ... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────── 工具响应:write_todos ───────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'in_progress'}, {'content': │ │ '调研 deepagents 框架的安装与使用方法', 'status': 'pending'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'pending'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'pending'}, {'content': '整合信息,撰写结构化总结报告', 'status': │ │ 'pending'}, {'content': '将报告保存到本地文件', 'status': 'pending'}] │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 4 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents framework 介绍 架构 使用方法", │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents LangGraph agent framework tutorial", │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents web scraping crawling agent", │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 5 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ {"query": "deepagents web scraping crawling agent", "follow_up_questions": │ │ null, "answer": null, "images": [], "results": [{"url": │ │ "https://docs.langchain.com/oss/python/integrations/tools/hyperbrowser_web_ │ │ scraping_tools", "title": "Hyperbrowser web scraping integration - Docs by │ │ LangChain", "conten... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────── 工具响应:tavily_search ──────────────────────────┐ │ {"query": "deepagents web scraping crawling agent", "follow_up_questions": │ │ null, "answer": null, "images": [], "results": [{"url": │ │ "https://docs.langchain.com/oss/python/integrations/tools/hyperbrowser_web_ │ │ scraping_tools", "title": "Hyperbrowser web scraping integration - Docs by │ │ LangChain", "content": "> ## Documentation Index\n>\n> Fetch the complete │ │ documentation index at:</llms.txt>\n>\n> Use this file to discover all │ │ available pages before exploring further.\n\nInterrupt is coming to NYC an │ │ ...(共11127字符) │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 6 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents Python create_deep_agent 代码示例 爬虫 报告生成", │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents GitHub repository documentation tools file system │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "langchain deepagents 爬取新闻 政策 摘要报告 实现方案", │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 7 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ {"query": "langchain deepagents 爬取新闻 政策 摘要报告 实现方案", │ │ "follow_up_questions": null, "answer": null, "images": [], "results": │ │ [{"url": │ │ "https://www.linkedin.com/posts/vivek-trivedy-433509134_at-langchain-we-hav │ │ e-a-ton-of-fun-building-activity-7438979196586168320-vMYM", "title": │ │ "LangChain Deepagents Lib... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────── 工具响应:tavily_search ──────────────────────────┐ │ {"query": "langchain deepagents 爬取新闻 政策 摘要报告 实现方案", │ │ "follow_up_questions": null, "answer": null, "images": [], "results": │ │ [{"url": │ │ "https://www.linkedin.com/posts/vivek-trivedy-433509134_at-langchain-we-hav │ │ e-a-ton-of-fun-building-activity-7438979196586168320-vMYM", "title": │ │ "LangChain Deepagents Library for Efficient Agent Building | Vivek Trivedy │ │ posted on the topic | LinkedIn", "content": "Is this the end of the \"Black │ │ Box\" AI Coding Agent? 🛠️💀\nIn case you missed it, Chorouk Malmoum just dr │ │ ...(共8801字符) │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 8 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents create_deep_agent tools tavily web_search subagent │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents Python middleware skills custom tool crawl news po │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 9 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ {"query": "deepagents Python middleware skills custom tool crawl news │ │ policy summarization 2025", "follow_up_questions": null, "answer": null, │ │ "images": [], "results": [{"url": │ │ "https://www.youtube.com/watch?v=AZ6257Ya_70", "title": "Rewriting Deep │ │ Agents on top of LangChain 1.0", "content": "# Rewr... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────── 工具响应:tavily_search ──────────────────────────┐ │ {"query": "deepagents Python middleware skills custom tool crawl news │ │ policy summarization 2025", "follow_up_questions": null, "answer": null, │ │ "images": [], "results": [{"url": │ │ "https://www.youtube.com/watch?v=AZ6257Ya_70", "title": "Rewriting Deep │ │ Agents on top of LangChain 1.0", "content": "# Rewriting Deep Agents on top │ │ of LangChain 1.0\n## LangChain\n194000 subscribers\n457 likes\n\n### │ │ Description\n19485 views\nPosted: 24 Sep 2025\nIn this video, we will walk │ │ through how we rebuilt deepagen │ │ ...(共11429字符) │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 10 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents Firecrawl BeautifulSoup web crawling tool integrat │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents policy news crawler agent subagent delegation plan │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 11 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ {"query": "deepagents policy news crawler agent subagent delegation planned │ │ task architecture", "follow_up_questions": null, "answer": null, "images": │ │ [], "results": [{"url": │ │ "https://www.linkedin.com/posts/vivek-trivedy-433509134_at-langchain-we-hav │ │ e-a-ton-of-fun-building-activity-74389791965861683... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────── 工具响应:tavily_search ──────────────────────────┐ │ {"query": "deepagents policy news crawler agent subagent delegation planned │ │ task architecture", "follow_up_questions": null, "answer": null, "images": │ │ [], "results": [{"url": │ │ "https://www.linkedin.com/posts/vivek-trivedy-433509134_at-langchain-we-hav │ │ e-a-ton-of-fun-building-activity-7438979196586168320-vMYM", "title": │ │ "LangChain Deepagents Library for Efficient Agent Building | Vivek Trivedy │ │ posted on the topic | LinkedIn", "content": "Is this the end of the \"Black │ │ Box\" AI Coding Agent? 🛠️💀\nIn │ │ ...(共9918字符) │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 12 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "site:dev.to \"Building Advanced AI Agents with LangChain's De │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "tavily_search", │ │ "参数": { │ │ "query": "deepagents crawler news article extract content tool implemen │ │ "search_depth": "advanced" │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 13 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ {"query": "deepagents crawler news article extract content tool │ │ implementation python code", "follow_up_questions": null, "answer": null, │ │ "images": [], "results": [{"url": │ │ "https://stackoverflow.com/questions/30356069/extract-news-article-content- │ │ from-stored-html-pages", "title": "python - Extract N... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────── 工具响应:tavily_search ──────────────────────────┐ │ {"query": "deepagents crawler news article extract content tool │ │ implementation python code", "follow_up_questions": null, "answer": null, │ │ "images": [], "results": [{"url": │ │ "https://stackoverflow.com/questions/30356069/extract-news-article-content- │ │ from-stored-html-pages", "title": "python - Extract News article content │ │ from stored .html pages - Stack Overflow", "content": "Explore Stack │ │ Internal\n\n# Extract News article content from stored .html pages\n\nAsk │ │ Question\n\nAsked\n\nModified 4 years │ │ ...(共9507字符) │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 14 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "write_todos", │ │ "参数": { │ │ "todos": [ │ │ { │ │ "content": "调研 deepagents 框架的基本概念、架构与核心特性", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研 deepagents 框架的安装与使用方法", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现网页爬取功能", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现政策新闻摘要与报告生成", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "整合信息,撰写结构化总结报告", │ │ "status": "in_progress" │ │ }, │ │ { │ │ "content": "将报告保存到本地文件", │ │ "status": "pending" │ │ } │ │ ] │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 15 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'completed'}, {'content': '调研 │ │ deepagents 框架的安装与使用方法', 'status': 'completed'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'completed'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'completed'}, {'conten... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────── 工具响应:write_todos ───────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'completed'}, {'content': '调研 │ │ deepagents 框架的安装与使用方法', 'status': 'completed'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'completed'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'completed'}, {'content': '整合信息,撰写结构化总结报告', 'status': │ │ 'in_progress'}, {'content': '将报告保存到本地文件', 'status': 'pending'}] │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 16 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "write_file", │ │ "参数": { │ │ "file_path": "/deepagents_policy_news_agent_report.md", │ │ "content": "# 基于 DeepAgents 框架构建政策新闻爬取与总结报告助手 —— 结 │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 17 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ Updated file /deepagents_policy_news_agent_report.md │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────── 工具响应:write_file ────────────────────────────┐ │ Updated file /deepagents_policy_news_agent_report.md │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 18 ------------------------------------------------------------------------------- - ┌───────────────────────────────── 工具调用 ──────────────────────────────────┐ │ { │ │ "工具名称": "write_todos", │ │ "参数": { │ │ "todos": [ │ │ { │ │ "content": "调研 deepagents 框架的基本概念、架构与核心特性", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研 deepagents 框架的安装与使用方法", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现网页爬取功能", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "调研如何使用 deepagents 实现政策新闻摘要与报告生成", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "整合信息,撰写结构化总结报告", │ │ "status": "completed" │ │ }, │ │ { │ │ "content": "将报告保存到本地文件", │ │ "status": "completed" │ │ } │ │ ] │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 19 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'completed'}, {'content': '调研 │ │ deepagents 框架的安装与使用方法', 'status': 'completed'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'completed'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'completed'}, {'conten... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────── 工具响应:write_todos ───────────────────────────┐ │ Updated todo list to [{'content': '调研 deepagents │ │ 框架的基本概念、架构与核心特性', 'status': 'completed'}, {'content': '调研 │ │ deepagents 框架的安装与使用方法', 'status': 'completed'}, {'content': │ │ '调研如何使用 deepagents 实现网页爬取功能', 'status': 'completed'}, │ │ {'content': '调研如何使用 deepagents 实现政策新闻摘要与报告生成', 'status': │ │ 'completed'}, {'content': '整合信息,撰写结构化总结报告', 'status': │ │ 'completed'}, {'content': '将报告保存到本地文件', 'status': 'completed'}] │ └─────────────────────────────────────────────────────────────────────────────┘ ------------------------------------------------------------------------------- - 步骤 20 ------------------------------------------------------------------------------- - ┌────────────────────────────────── AI思考 ───────────────────────────────────┐ │ 报告已完成并保存到 │ │ `/deepagents_policy_news_agent_report.md`。以下是报告的核心内容摘要: │ │ │ │ --- │ │ │ │ ## 📋 报告概览 │ │ │ │ 该结构化报告共 **12 个章节**,涵盖以下核心内容: │ │ │ │ ### 🔑 关键发现 │ │ │ │ **1. DeepAgents 是什么?** │ │ - LangChain 官方推出的"电池已装好"的 Agent 框架,基于 LangGraph 构建 │ │ - 灵感来自 Claude Code、Manus、Deep Research 等产品,将其共性能力抽象封装 │ │ │ │ **2. 四大核心支柱** │ │ - **规划工具**(`write_todos`):行动前先制定... │ │ │ │ (内容较长,完整内容将在最后显示) │ └─────────────────────────────────────────────────────────────────────────────┘ 任务完成! 报告已完成并保存到 `/deepagents_policy_news_agent_report.md`。以下是报告的核心内容摘要: --- ## 📋 报告概览 该结构化报告共 **12 个章节**,涵盖以下核心内容: ### 🔑 关键发现 **1. DeepAgents 是什么?** - LangChain 官方推出的"电池已装好"的 Agent 框架,基于 LangGraph 构建 - 灵感来自 Claude Code、Manus、Deep Research 等产品,将其共性能力抽象封装 **2. 四大核心支柱** - **规划工具**(`write_todos`):行动前先制定结构化 TODO 列表 - **虚拟文件系统**:`write_file`/`read_file`/`edit_file`/`ls`/`glob`/`grep` 全套工具,实现上下文管理和子代理协作 - **子代理机制**(`task` 工具):独立上下文窗口,爬取/分析/审校分工协作 - **系统提示词**:内置 + 用户自定义双层提示词设计 **3. 政策新闻助手的推荐架构** ``` 主代理 → 爬虫子代理(搜索+爬取全文) → 分析子代理(提取要点+撰写初稿) → 审校子代理(检查准确性+反馈) → 主代理定稿 → final_report.md ``` **4. 报告包含完整可运行代码**,涵盖工具定义、子代理配置、主代理创建、流式输出、多轮对话支持等所有关键实现。 **5. 提供了调试方案**:LangGraph Studio 可视化调试、Deep Agents UI、LangSmith 追踪。 Process finished with exit code 0
4.3 子代理 (Sub-Agent Delegation)
组件:SubAgentMiddleware
工具名:task (delegate_task)
核心概念:
- 任务隔离:每个子代理有独立的上下文窗口。
- 并行执行:支持同时启动多个子代理。
- 结果聚合:智能整合多个子代理的输出。
机制:
- 当任务过于具体(如"爬取并分析这篇长论文")时,主 Agent 会生成一个隔离环境的子 Agent 去执行。
- 子 Agent 启动时,只继承必要的环境配置,但拥有全新的、空白的消息历史。
- 子 Agent 执行完毕后只返回一个总结性的结果。这保证了主 Agent 的时间线(Context)保持干净,极大地节省了 Token。
自动触发默认的 SubAgentMiddleware
import asyncio import os import dotenv from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_core.messages import BaseMessage, ToolMessage from langchain_tavily import TavilySearch from rich.console import Console from rich.panel import Panel from rich.tree import Tree # 1.加载环境变量 dotenv.load_dotenv() # 2.配置Rich Console console = Console() async def run_auto_subagent(): """ 不显示传入subagents参数,自动触发默认的SubAgentMiddleware :return: """ console.print( Panel.fit( "[bold magenta]DeepAgents自动subAgent中间件演示[/bold magenta]", border_style="magenta", ) ) console.print( "[dim]本演示验证:即使不传入subagents参数,Agent默认也会启动 'general-purpose' 子 Agent [/dim]" ) # 1.初始化模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) # 2.初始化Tavily搜索工具 tavily = TavilySearch(max_results=5) # 3.创建agent,但是不传入subagents参数 # create_deep_agent默认会加载SubAgentMiddleware(), 这就意味着Agent会自动获取一个名为task的工具,可以调用 'general-purpose' 子 Agent console.print("[bold cyan]正在创建Agent(subagents=None)...[/bold cyan]") agent = create_deep_agent( model=deepseek_v4_pro, tools=[tavily], # subagents=[], # 故意不传或者传入为空 system_prompt="""你是一个能高效处理并发任务的智能助手,对于包含多个部分的复杂任务, 你必须使用 'task'工具来创建'general-purpose' 子Agent进行处理。 不要自己在主线程中串行执行所有操作。利用子Agent来隔离上下文并提高效率。""", ) # 4.定义一个合适 并行/隔离 的任务 task = """ 请同时调研一下四个完全不同的主题:并分别给出简单总结 1. 请说明claude code的技术架构说明 2. 请你帮我说明python的多线程机制 3. python的性能为什么很差? 4. open claude 小龙虾的技术实现架构说明 """ # 5.运行并可视化 step = 0 console.print("[dim]开始流式输出...[/dim]") try: # astream:异步流式跑图。每跑完「一个节点」就 yield 一个 chunk(默认类似 updates 模式) # 输入格式和 invoke 一样:{"messages": [用户消息, ...]} async for chunk in agent.astream({"messages": [{"role": "user", "content": task}]}): step += 1 # 每收到一个 chunk 算一步(含后面不打印的静默更新,所以 Step 可能跳号) # ---------- chunk 长什么样? ---------- # 当前写法没传 stream_mode 时,astream 默认一般是 "updates"。若要显式写清楚: # chunk 是一个 dict,键 = LangGraph 节点名,值 = 该节点本次产出的状态增量。 # 例子: # {"model": {"messages": [AIMessage(...)]}} # {"tools": {"messages": [ToolMessage(...), ToolMessage(...)]}} # 同一轮也可能只有一个键。 # .items():把 dict 拆成 (key, value) 对,便于 for 解包。 # node_name → 如 "model" / "tools" # node_data → 如 {"messages": [...]},也可能是 todos 等其它字段 for node_name, node_data in chunk.items(): # 个别更新可能是空的,跳过 if node_data is None: continue # 本演示只关心「带 messages 的更新」; # 若节点只改了 todos 等,没有 messages,整段 if 不进 → 终端上就像「少了 Step」 if "messages" in node_data: # 取出本节点新增/带回的消息列表(updates 模式下通常是本步新消息,不是全历史) msgs = node_data["messages"] # 防御:偶发单条消息而不是 list,先包成 list,后面统一 for 遍历 if not isinstance(msgs, list): msgs = [msgs] for msg in msgs: # 0) 只要 LangChain 消息对象(HumanMessage / AIMessage / ToolMessage…) # 其它奇怪类型直接跳过 if not isinstance(msg, BaseMessage): continue # 安全读 tool_calls: # - AIMessage 可能带 tool_calls(模型决定调哪些工具) # - HumanMessage / ToolMessage 通常没有该属性 # getattr(..., None) 避免 AttributeError;or [] 把 None/空统一成空列表 tool_calls = getattr(msg, "tool_calls", None) or [] # ---------- 分支 1:模型发起工具调用(常见于 Node=model)---------- # tool_calls 是一个列表,例如模型一次要调 4 个工具: # [ # {"name": "task", "args": {"subagent_type": "general-purpose", "description": "调研 Claude Code"}}, # {"name": "task", "args": {"subagent_type": "general-purpose", "description": "调研 Python 多线程"}}, # {"name": "write_todos", "args": {...}}, # ] if tool_calls: # Rich Tree:树形打印「本步调了哪些工具」 tree = Tree( f"[bold yellow]Step {step}: 决策与调用 (Node: {node_name}) [/bold yellow]" ) # 一条 AIMessage 里可以并行请求多个工具(本 demo 里常见一次 4 个 task) # 取出工具名和参数(兼容两种格式) # tc 是字典 tc["name"]、tc["args"](用 .get 防缺 key) # tc 是对象 tc.name、tc.args(用 getattr 防缺属性) for tc in tool_calls: # LangChain 里 tool_call 有时是 dict,有时是对象,两种都兼容 if isinstance(tc, dict): tool_name = tc.get("name", "未知工具") # 如 "task" / "write_todos" tool_args = tc.get("args", {}) # 调用参数字典 else: tool_name = getattr(tc, "name", "未知工具") tool_args = getattr(tc, "args", {}) or {} if tool_name == "task": # task = SubAgentMiddleware 注入的「派生子 Agent」工具 # 典型 args:subagent_type="general-purpose", description="调研xxx" # Step 7: 决策与调用 (Node: model) ← 这是外面已建好的 tree 根 # └── 成功触发 'task' 工具 (Sub-Agent) ← branch = tree.add(...) # ├── 子Agent类型: general-purpose ← branch.add(...) # └── 任务指令: 调研 xxx ← branch.add(...) branch = tree.add( "[bold red]成功触发 'task' 工具 (Sub-Agent)[/bold red]" ) # tree.add 返回子节点,可再 .add 挂详情 branch.add( f"[cyan]子Agent类型:[/cyan] {tool_args.get('subagent_type', '未知')}" ) branch.add( f"[cyan]任务指令:[/cyan] {tool_args.get('description', '未知')}" ) else: # 其它工具(write_todos、tavily_search…)一律标成普通工具 tree.add(f"[bold blue]普通工具调用:[/bold blue] {tool_name}") console.print(tree) # 把整棵树打到终端 # ---------- 分支 2:工具执行结果(常见于 Node=tools)---------- # ToolMessage:某个工具跑完后的返回;.name 是工具名,.content 是返回正文 # 判断当前消息是不是[工具回执],只有工具跑完后,图才会产生ToolMessage elif isinstance(msg, ToolMessage): content = str(msg.content) # 统一转成字符串再展示 # task表示是子Agent的结果 if msg.name == "task": # 子 Agent 整段任务做完后,只把「最终摘要/报告」回传给主 Agent # (子 Agent 内部搜索过程默认不会全部冒泡到这里) console.print( Panel( content, title=f"[bold magenta]Sub-Agent 完成任务 (Node: {node_name})[/bold magenta]", border_style="magenta", ) ) else: # 非 task 工具(如 write_todos):内容可能很长,只预览前 100 字 preview = content[:100] + ("..." if len(content) > 100 else "") console.print(f"[dim]Tool Output ({msg.name}): {preview}[/dim]") # ---------- 分支 3:模型纯文本回复(无工具调用)---------- # 条件:有 content,且 tool_calls 为空 → 多半是对用户的最终/中间文字回答 elif getattr(msg, "content", None) and not tool_calls: title = f"[bold green]Agent回复 (Node: {node_name})[/bold green]" console.print(Panel(msg.content, title=title, border_style="green")) except Exception as e: console.print(f"[bold red]运行时错误:{e}[/bold red]") console.print("\n[bold magenta]演示结束[/bold magenta]") if __name__ == "__main__": # 模块顶层不能直接 await,要用 asyncio.run 驱动协程 asyncio.run(run_auto_subagent())
输出:
D:\anaconda3\envs\deepagents\python.exe E:\code\InsightFlow\create_demo_deep_agent\demo_sub_agent_middleware.py ┌──────────────────────────────────┐ │ DeepAgents自动subAgent中间件演示 │ └──────────────────────────────────┘ 本演示验证:即使不传入subagents参数,Agent默认也会启动 'general-purpose' 子 Agent 正在创建Agent(subagents=None)... 开始流式输出... Step 3: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '调研 Claude Code 技术架构', 'status': 'in_progress'}, {'content': '调研 P... Step 7: 决策与调用 (Node: model) ├── 成功触发 'task' 工具 (Sub-Agent) │ ├── 子Agent类型: general-purpose │ └── 任务指令: Claude Code 技术架构调研 ├── 成功触发 'task' 工具 (Sub-Agent) │ ├── 子Agent类型: general-purpose │ └── 任务指令: Python 多线程机制调研 ├── 成功触发 'task' 工具 (Sub-Agent) │ ├── 子Agent类型: general-purpose │ └── 任务指令: Python 性能差原因调研 └── 成功触发 'task' 工具 (Sub-Agent) ├── 子Agent类型: general-purpose └── 任务指令: Open Claude 小龙虾技术架构调研 ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ # 🔍 Python 性能差原因深度调研报告 │ │ │ │ --- │ │ │ │ ## 一、核心架构层面(语言设计决定的天花板) │ │ │ │ ### 1. 解释执行(Interpreted Execution) │ │ │ │ 这是 Python 慢的**根本原因**。Python 代码的执行路径比 C/C++ │ │ 等编译语言多了一个中间层: │ │ │ │ ``` │ │ Python: 源码 → 字节码(.pyc) → PVM解释器逐条解释 → CPU执行 │ │ C/C++: 源码 → 直接编译为机器码 → CPU直接执行 │ │ ``` │ │ │ │ 每次运行 Python 代码时,解释器都要做 **"读取字节码 → 解析 → 派发 → 执行"** │ │ 的循环,这个 **dispatch loop** 本身消耗巨大。而 C 代码编译后直接就是 CPU │ │ 指令,无需中间转换。 │ │ │ │ ### 2. 动态类型(Dynamic Typing) │ │ │ │ 这是 Python 最大的性能杀手之一: │ │ │ │ | 对比维度 | 静态语言 (C/C++/Rust) | Python | │ │ |---------|----------------------|--------| │ │ | 类型确定时机 | 编译期 | 运行期 | │ │ | `a + b` 操作 | 编译成单条 CPU 加法指令 | 运行时查找 │ │ `a.__add__`,检查类型,再执行 | │ │ | 编译优化 | 可以做大量优化(内联、向量化) | 几乎无法做编译期优化 | │ │ │ │ ```python │ │ a = 1 │ │ b = 2 │ │ c = a + b # 运行时要做:type(a) → int, type(b) → int, 查找 __add__, 调用, │ │ 返回新对象 │ │ ``` │ │ │ │ 同一个 `+` 操作,Python 背后要经历:类型检查 → 方法查找 → 函数调用 → │ │ 新对象分配,而 C 只是一条 CPU 指令。 │ │ │ │ > 一项微基准测试显示:在 Python 的执行时间中,约 **6%** │ │ 花在动态类型检查上(这还只是类型检查本身,不含连锁影响)。 │ │ │ │ --- │ │ │ │ ## 二、内存模型层面 │ │ │ │ ### 3. 万物皆对象(Everything is a PyObject) │ │ │ │ 在 C 中,一个 `int` 就是 4 或 8 字节的一块内存。而在 Python │ │ 中,即使是最简单的整数,也是一个复杂的结构体: │ │ │ │ ``` │ │ C int: [4 bytes 存储值] │ │ Python int: [PyObject_HEAD (refcount + type指针)] + [实际数值] │ │ ↑ 至少 28 字节 (64位系统) │ │ ``` │ │ │ │ **内存开销对比**: │ │ - C 语言 `int`:4 字节 │ │ - Python `int`:**28+ 字节**(包含引用计数、类型指针、值本身) │ │ │ │ 这意味着: │ │ - 同样的数据,Python 占用 5~7 倍内存 │ │ - CPU 缓存命中率大幅降低 │ │ - 每次操作都要分配/释放堆内存 │ │ │ │ ### 4. 引用计数垃圾回收(Reference Counting GC) │ │ │ │ 每个 Python 对象的 `PyObject_HEAD` 中都包含一个 `refcount` │ │ 字段。**每一次**引用变化都会触发原子增减操作: │ │ │ │ ```python │ │ a = obj # obj->refcount += 1 (原子操作,有开销) │ │ b = a # obj->refcount += 1 │ │ del a # obj->refcount -= 1 │ │ ``` │ │ │ │ 在一段密集型计算中,引用计数的原子操作可能占据 **10-30%** │ │ 的执行时间。这也是为什么 Gilectomy 项目(移除 GIL)在单线程场景下反而慢了约 │ │ 30% 的原因——原子引用计数操作太昂贵。 │ │ │ │ --- │ │ │ │ ## 三、并发层面 │ │ │ │ ### 5. 全局解释器锁(GIL - Global Interpreter Lock) │ │ │ │ GIL 是 CPython 中最大的并发瓶颈: │ │ │ │ > **GIL 是一把互斥锁,保证同一时刻只有一个线程在执行 Python 字节码。** │ │ │ │ ``` │ │ 多核 CPU: [Core 0: 执行] [Core 1: 等待GIL] [Core 2: 等待GIL] [Core 3: │ │ 等待GIL] │ │ ↑ 只有一个是真正在工作! │ │ ``` │ │ │ │ **GIL 的影响**: │ │ │ │ | 任务类型 | 受 GIL 影响 | 说明 | │ │ |---------|------------|------| │ │ | CPU 密集型(计算、图像处理) | ❌ **严重影响** | │ │ 多线程反而可能比单线程更慢 | │ │ | I/O 密集型(网络请求、文件读写) | ✅ 影响较小 | I/O 操作时会释放 GIL | │ │ │ │ **绕开 GIL 的方式**: │ │ - `multiprocessing`:多进程(每个进程有自己独立的 GIL) │ │ - C 扩展:在 C 代码中可手动释放 GIL │ │ - 替代实现:PyPy、Jython、IronPython 无 GIL │ │ - Python 3.13+:PEP 703 引入了可选的 **GIL-free 模式** │ │ │ │ > ⚠️ 注意:GIL 只在 **CPython**(最主流实现)中存在,是 CPython │ │ 为了简化内存管理(避免给每个对象加锁)所做的权衡。 │ │ │ │ --- │ │ │ │ ## 四、运行时开销 │ │ │ │ ### 6. 属性/方法查找开销 │ │ │ │ Python 每个 `.` 操作符都意味着运行时的**字典查找**: │ │ │ │ ```python │ │ result = [] │ │ for word in words: │ │ result.append(word.upper()) # 每次循环:查找 result.append → 查找 │ │ word.upper → 两次dict查找 │ │ ``` │ │ │ │ 对于 100 万次循环,这就是 **200 万次字典查找**!优化方式是缓存方法引用: │ │ │ │ ```python │ │ append = result.append │ │ upper = str.upper │ │ for word in words: │ │ append(upper(word)) # 直接引用,无查找开销 │ │ ``` │ │ │ │ ### 7. 函数调用开销巨大 │ │ │ │ 每次 Python 函数调用都涉及: │ │ - 创建栈帧(stack frame) │ │ - 参数打包与解包(打包成 tuple,被调用方再解析) │ │ - 递归深度检查 │ │ │ │ 一项分析显示,在典型的 Python 代码中,**参数传递**占用了约 **31%** │ │ 的执行时间,与"真正干活的时间"几乎持平。 │ │ │ │ ### 8. 没有 JIT 编译器(CPython) │ │ │ │ | 语言/实现 | 执行方式 | │ │ |----------|---------| │ │ | **CPython** | 纯解释执行字节码 | │ │ | **Java (JVM)** | JIT 编译热点代码为机器码 | │ │ | **JavaScript (V8)** | JIT 编译 + 内联缓存 | │ │ | **PyPy** | 跟踪 JIT(Tracing JIT),比 CPython 快 4-7 倍 | │ │ | **Python 3.14 (PEP 744)** | 正在引入 Copy-and-Patch JIT | │ │ │ │ CPython 作为参考实现,选择了**简单性优先于性能**,一直未内置 JIT。 │ │ │ │ --- │ │ │ │ ## 五、开发者常见误区(可避免的性能坑) │ │ │ │ | 问题 | 慢的做法 | 优化做法 | │ │ |------|---------|---------| │ │ | 字符串拼接 | `s += substr` 在循环中(每次创建新对象) | `''.join(list)` | │ │ | 查找操作 | 用 `list` 做成员检查 O(n) | 用 `set`/`dict` O(1) | │ │ | 全局变量 | 频繁访问 `global` 变量 | 用局部变量缓存 | │ │ | 循环方式 | `range()` 返回完整列表(Python 2) | `xrange()` 或 Python 3 的 │ │ `range()` | │ │ | 函数调用 | 循环内频繁调用小函数 | 内联或批量处理 | │ │ │ │ --- │ │ │ │ ## 六、总结:各因素影响权重 │ │ │ │ 基于实际的微基准测试分析: │ │ │ │ ``` │ │ 参数传递/解析 ████████████████████████████ 31% │ │ 真正计算工作 ████████████████████████████ 31% │ │ 整数装箱拆箱 ████████████ 13% │ │ 解释器 dispatch 开销 ███████████ 11% │ │ 递归限制检查 ████████ 8% │ │ 动态类型检查 ██████ 6% │ │ ``` │ │ │ │ > *数据来源:kmod 的 Python 性能分析博客* │ │ │ │ --- │ │ │ │ ## 七、关键结论 │ │ │ │ 1. **Python 慢不是 bug,是设计取舍** —— 用性能换取开发效率、灵活性和可读性 │ │ 2. │ │ **核心矛盾**:动态特性(动态类型、动态属性、运行时修改)从根本上限制了编译 │ │ 优化的可能性 │ │ 3. **通用规律**:Python 比 C 慢 **10~100 倍**不等,但开发效率通常高出 │ │ **3~10 倍** │ │ 4. **未来方向**:Python 3.13 的 GIL-free 模式 + Python 3.14 的 JIT │ │ 编译器正在逐步改善局面 │ │ 5. **实战策略**:CPU 密集型部分用 C 扩展 / Cython / Rust (PyO3) │ │ 实现,Python 做胶水层 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ 现在我已经收集了足够的信息,下面为你整理一份 Claude Code │ │ 技术架构的全面调研报告。 │ │ │ │ --- │ │ │ │ # Claude Code 技术架构调研报告 │ │ │ │ ## 一、概述 │ │ │ │ Claude Code 是 Anthropic 推出的**终端优先(Terminal-first)的自主编程 Agent │ │ 工具**,它不是一个简单的 CLI │ │ 包装器,而是一个围绕"单线程主循环"构建的六层架构系统。其核心理念是 **"Less │ │ Scaffolding, More │ │ Model"**——让模型自身承担推理和决策,而非依赖复杂的硬编码编排逻辑。 │ │ │ │ --- │ │ │ │ ## 二、技术栈 │ │ │ │ | 层级 | 技术选型 | 选型理由 | │ │ |------|----------|----------| │ │ | **语言** | TypeScript | Claude 模型对 TS │ │ 极其精通,处于模型的"分布内(on-distribution)" | │ │ | **终端 UI** | React + **Ink** | Ink 将 React │ │ 组件模型带到命令行,提供类浏览器组件化体验 | │ │ | **布局引擎** | Yoga(Meta 开源) | 基于 Flexbox │ │ 的约束布局,解决终端窗口尺寸适配问题 | │ │ | **构建/打包** | Bun | 相比 Webpack/Vite 等构建速度更快 | │ │ | **分发** | npm | Node 生态最大包管理器,兼容性最好 | │ │ | **AI 模型** | Claude Sonnet(默认) | API 级调用 Anthropic 模型 | │ │ | **自举率** | ~90% 代码由 Claude Code 自己编写 | │ │ 技术栈刻意选择模型擅长的领域 | │ │ │ │ --- │ │ │ │ ## 三、核心架构:六层体系 │ │ │ │ Claude Code 的系统架构分为**六个层次**,以单线程主循环(Master │ │ Loop)为核心: │ │ │ │ ``` │ │ ┌──────────────────────────────────────────────┐ │ │ │ Layer 1: 输入层 (Input Layer) │ │ │ │ Layer 2: 知识层 (Knowledge Layer) │ │ │ │ Layer 3: 执行层 (Execution Layer) │ │ │ │ Layer 4: 集成层 (Integration Layer) │ │ │ │ Layer 5: 多智能体层 (Multi-Agent Layer) │ │ │ │ Layer 6: 模型层 (Model Layer) │ │ │ │ ★ 主循环 (Master Loop) 贯穿始终 │ │ │ └──────────────────────────────────────────────┘ │ │ ``` │ │ │ │ ### 3.1 主循环(Master Loop / "nO")— 架构核心 │ │ │ │ 这是整个系统的"心脏",代码逻辑极其简单,Anthropic 称之为 **"Dumb │ │ Loop"(哑循环)**: │ │ │ │ ``` │ │ while (claude_response.has_tool_call): │ │ result = execute_tool(tool_call) │ │ claude_response = send_to_claude(result) │ │ return claude_response.text │ │ ``` │ │ │ │ **设计哲学:** │ │ - **没有** Intent Classifier(意图分类器) │ │ - **没有** Task Router(任务路由器) │ │ - **没有** RAG/Embedding Pipeline │ │ - **没有** DAG Orchestrator │ │ - **没有** Planner/Executor Split │ │ │ │ > │ │ 模型自己决定何时调用工具、调用哪个工具、何时结束。所有智能都在模型权重中, │ │ 循环只负责调度。 │ │ │ │ ### 3.2 Layer 1 — 输入层(Input Layer) │ │ │ │ - **会话管理**:管理多轮对话的生命周期 │ │ - **权限门控**:在请求到达模型前的权限检查 │ │ - **YAML 信任等级**:基于 YAML 配置的多级信任策略 │ │ - **消息队列 "h2A"**:异步事件处理 │ │ │ │ ### 3.3 Layer 2 — 知识层(Knowledge Layer) │ │ │ │ 这是 Claude Code "超越模型权重"的智能所在: │ │ │ │ - **Skill Registry(技能注册表)**:可扩展的技能系统 │ │ - **Task Graph(任务图)**:跟踪任务依赖和状态 │ │ - **Cross-Session Memory(跨会话记忆)**: │ │ - `CLAUDE.md`(~500 行限制)— 项目级持久记忆 │ │ - `agent_memory.md` — 跨会话持久化 │ │ - **Context Compressor(上下文压缩器)**: │ │ - **三层压缩体系**:92% 作为压缩触发阈值 │ │ - Tier 1:截断旧消息 │ │ - Tier 2:用结构化摘要替换冗长输出 │ │ - Tier 3:LLM 驱动的上下文总结(9 │ │ 个维度:当前状态、目标意图、近期变更、关键决策、活跃工作、关键文件、经验教 │ │ 训、重要上下文、可选后续步骤) │ │ - **关键约束**:压缩时禁止调用工具;绝不拆分 `tool_use` / `tool_result` │ │ 配对 │ │ │ │ ### 3.4 Layer 3 — 执行层(Execution Layer) │ │ │ │ **工具系统(Tool Arsenal):** │ │ │ │ | 工具类别 | 工具 | 说明 | │ │ |----------|------|------| │ │ | **读取发现** | `View`(Read) | 文件读取,默认 ~2000 行 | │ │ | | `LS` | 目录列表 | │ │ | | `Glob` | 通配符文件搜索 | │ │ | | `GrepTool` | 全正则搜索(类 ripgrep),**不使用向量数据库** | │ │ | **编辑操作** | `Edit` | 精确字符串替换(old_string → new_string) | │ │ | | `Write` | 创建新文件 | │ │ | **执行** | `Bash` | Shell 命令执行 | │ │ | **规划** | `write_todos` | TODO 任务规划与跟踪 | │ │ | **子代理** | `Task` | 生成子代理处理独立任务 | │ │ │ │ **核心设计决策:不使用向量数据库/RAG**。原因:Claude │ │ 模型对代码结构的理解能力极强,可以直接生成复杂的正则表达式,无需维护搜索索 │ │ 引的运维开销。 │ │ │ │ **编辑工具的独特设计:** │ │ - 使用 `old_string → new_string` 的精确替换,而非行号编辑 │ │ - 避免行号漂移问题 │ │ │ │ ### 3.5 Layer 4 — 集成层(Integration Layer) │ │ │ │ - **MCP Runtime**:连接外部 MCP 服务器(文件系统、Git、Jira、GitHub 等) │ │ - **Hook │ │ 系统**:在生命周期事件触发(工具执行前后、会话边界、权限请求、压缩等) │ │ - **流式运行时**:处理并行执行和 token 流 │ │ - **Prompt Cache**:复用稳定前缀,成本降至约 10% │ │ │ │ ### 3.6 Layer 5 — 多智能体层(Multi-Agent Layer) │ │ │ │ 支持**两级并行**: │ │ │ │ | 级别 | 说明 | │ │ |------|------| │ │ | **Subagents(子代理)** | 独立上下文窗口,严格深度限制,不继承会话历史 | │ │ | **Agent Teams(代理团队)** | 更高层级的协调与任务分发 | │ │ │ │ **子代理设计原则:** │ │ - **隔离上下文**:子代理拥有独立上下文窗口,不污染主会话 │ │ - **深度限制**:防止无限递归代理生成 │ │ - **受约束的工具集**:子代理只能使用指定的工具子集 │ │ - **权衡**:总 Token 消耗会增加(因代理间通信),但可显著减少主会话压缩次数 │ │ │ │ ### 3.7 Layer 6 — 模型层(Model Layer) │ │ │ │ - **Claude Sonnet** 作为默认推理引擎 │ │ - 模型只产生 token 流和工具调用决策,**不直接执行任何操作** │ │ - 职责分离: │ │ - 模型 → 决定**做什么** │ │ - 权限层 → 决定**是否允许** │ │ - 工具层 → 决定**如何执行** │ │ │ │ --- │ │ │ │ ## 四、权限与安全模型 │ │ │ │ 三级权限门控(Deny-First 设计): │ │ │ │ | 级别 | 行为 | │ │ |------|------| │ │ | **Tier 1** | 默认拒绝,逐步授权 | │ │ | **Tier 2** | 基于规则的自动允许(如安全文件路径) | │ │ | **Tier 3** | 完全信任模式(用户显式配置) | │ │ │ │ **架构级安全原则:** 将所有外部输入(MCP │ │ 结果、文件内容、工具返回值)视为不可信。 │ │ │ │ --- │ │ │ │ ## 五、上下文管理与压缩 │ │ │ │ ### 核心挑战 │ │ 编码过程中大量文件读取、工具输出、日志很容易耗尽 200K 上下文窗口。 │ │ │ │ ### 三层压缩策略 │ │ │ │ ``` │ │ Tier 1: 裁剪(Truncation) │ │ └─ 直接丢弃最旧的消息块 │ │ │ │ Tier 2: 结构化替换 │ │ └─ 用结构化摘要替换冗长的工具输出 │ │ │ │ Tier 3: LLM 摘要(触发阈值 ~92%) │ │ └─ 调用 Claude Haiku 生成 9 维度结构化摘要 │ │ └─ 关键:tools: [] 禁用工具调用,纯摘要模式 │ │ └─ 绝不拆分 tool_use/tool_result 配对 │ │ ``` │ │ │ │ ### 子代理策略 │ │ 将独立任务委托给子代理是**保持主会话上下文干净**的关键手段。子代理拥有独立 │ │ 上下文,完成后只返回结果摘要。 │ │ │ │ --- │ │ │ │ ## 六、扩展体系(Extension Stack) │ │ │ │ Claude Code 提供六层扩展体系: │ │ │ │ | 层级 | 特性 | 类型 | │ │ |------|------|------| │ │ | 1. CLAUDE.md | 项目记忆文件 | 确定性 | │ │ | 2. Slash Commands | 自定义斜杠命令 | 确定性 | │ │ | 3. Skills | 可组合的技能模块 | 概率性 | │ │ | 4. Hooks | 生命周期事件钩子 | 概率性 | │ │ | 5. Subagents | 子代理委托 | 概率性 | │ │ | 6. Agent Teams | 多代理协调 | 概率性 | │ │ │ │ --- │ │ │ │ ## 七、关键设计哲学总结 │ │ │ │ 1. **极简核心**:主循环简单到被称为"哑循环",智能完全来自模型 │ │ 2. **模型驱动**:每个新模型发布后,团队会**删除**模型不再需要的脚手架代码 │ │ 3. **可调试性优先**:扁平历史 + 单线程 >> │ │ 多智能体并发(在没有强大可观测性工具的情况下) │ │ 4. **安全默认拒绝**:权限系统采用 deny-first 策略 │ │ 5. **去复杂化**:不使用 RAG/向量数据库/意图分类器等常见 Agent 组件 │ │ 6. **自我迭代**:技术栈刻意选择模型擅长的 TypeScript,使 ~90% 代码由 Claude │ │ Code 自身编写 │ │ │ │ --- │ │ │ │ ## 八、参考资料 │ │ │ │ 1. [How Claude Code is built — Pragmatic │ │ Engineer](https://newsletter.pragmaticengineer.com/p/how-claude-code-is-bui │ │ lt) │ │ 2. [Claude Code Agent Architecture — │ │ ZenML](https://www.zenml.io/llmops-database/claude-code-agent-architecture- │ │ single-threaded-master-loop-for-autonomous-coding) │ │ 3. [Claude Code's Architecture Explained Visually — Daily Dose of Data │ │ Science](https://blog.dailydoseofds.com/p/claude-codes-architecture-explain │ │ ed) │ │ 4. [System Architecture — │ │ DeepWiki](https://deepwiki.com/anthropics/claude-code/1.1-system-architectu │ │ re) │ │ 5. [How Claude Code Works: Architecture & Internals — │ │ GitHub](https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/ │ │ main/guide/core/architecture.md) │ │ 6. [The System Design of Claude Code Agent Explained — │ │ Medium](https://medium.com/@milesk_33/the-system-design-of-claude-code-agen │ │ t-explained-318d17496534) │ │ 7. [Claude Code Official │ │ Docs](https://code.claude.com/docs/en/features-overview) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ # Python 多线程机制深度调研报告 │ │ │ │ --- │ │ │ │ ## 一、核心概念:GIL(全局解释器锁) │ │ │ │ ### 1.1 什么是 GIL? │ │ │ │ **GIL(Global Interpreter Lock,全局解释器锁)** 是 CPython │ │ 解释器中的一个互斥锁(mutex),它确保**同一时刻只有一个线程在执行 Python │ │ 字节码**。这意味着即使在多核 CPU 上,Python │ │ 多线程程序也无法实现真正的并行执行。 │ │ │ │ ``` │ │ ┌──────────────────────────────────────────────────┐ │ │ │ CPython 进程 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ Thread1 │ │ Thread2 │ │ Thread3 │ │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ └────────────┼────────────┘ │ │ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ │ GIL │ ← 同一时刻仅一个线程 │ │ │ │ │ 持有锁 │ 能执行字节码 │ │ │ │ └─────┬──────┘ │ │ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ │ Python │ │ │ │ │ │ 解释器 │ │ │ │ │ └────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ ``` │ │ │ │ ### 1.2 为什么需要 GIL? │ │ │ │ | 原因 | 说明 | │ │ |------|------| │ │ | **内存管理简化** | CPython 使用引用计数进行垃圾回收,GIL │ │ 避免了多线程并发修改引用计数的竞争条件 | │ │ | **C 扩展兼容性** | 大量 C 扩展库假定单线程访问,GIL 保证了向后兼容 | │ │ | **单线程性能** | 去掉 GIL │ │ 后需要细粒度锁,会显著拖慢单线程程序性能(历史实验显示下降约 30%) | │ │ │ │ ### 1.3 GIL 的调度机制 │ │ │ │ Python 使用 **"check and balance"** 机制: │ │ │ │ ```python │ │ # 伪代码:GIL 的工作方式 │ │ # 在 Python 3.2+ 中,GIL 使用固定的时间片(默认 5ms) │ │ # 当线程的时间片耗尽或遇到 I/O 操作时,会释放 GIL │ │ │ │ while True: │ │ acquire_gil() # 获取 GIL │ │ execute_bytecode() # 执行字节码(最多 5ms 或直到 I/O) │ │ release_gil() # 释放 GIL(给其他线程机会) │ │ ``` │ │ │ │ --- │ │ │ │ ## 二、`threading` 模块 — 核心 API │ │ │ │ ### 2.1 创建线程的两种方式 │ │ │ │ ```python │ │ import threading │ │ import time │ │ │ │ # 方式一:传入目标函数 │ │ def worker(name, delay): │ │ """工作线程函数""" │ │ for i in range(3): │ │ print(f"[{name}] iteration {i}") │ │ time.sleep(delay) │ │ │ │ t = threading.Thread(target=worker, args=("Thread-1", 0.5), name="Worker") │ │ t.start() # 启动线程 │ │ t.join() # 等待线程结束 │ │ │ │ # 方式二:继承 Thread 类 │ │ class MyThread(threading.Thread): │ │ def __init__(self, name, delay): │ │ super().__init__(name=name) │ │ self.delay = delay │ │ │ │ def run(self): │ │ for i in range(3): │ │ print(f"[{self.name}] iteration {i}") │ │ time.sleep(self.delay) │ │ │ │ t2 = MyThread("CustomThread", 0.3) │ │ t2.start() │ │ t2.join() │ │ ``` │ │ │ │ ### 2.2 Thread 对象关键方法 │ │ │ │ | 方法 | 说明 | │ │ |------|------| │ │ | `start()` | 启动线程(只能调用一次) | │ │ | `join(timeout=None)` | 阻塞等待线程结束 | │ │ | `is_alive()` | 线程是否还在运行 | │ │ | `name` / `getName()` | 获取线程名称 | │ │ | `daemon` | 设为守护线程(主线程退出时自动终止) | │ │ | `ident` | 线程唯一标识符 | │ │ | `native_id` | 操作系统原生线程 ID | │ │ │ │ ### 2.3 守护线程 vs 非守护线程 │ │ │ │ ```python │ │ import threading │ │ import time │ │ │ │ def background_task(): │ │ while True: │ │ print("守护线程工作中...") │ │ time.sleep(1) │ │ │ │ # daemon=True: 主线程结束时,该线程被强制终止 │ │ t = threading.Thread(target=background_task, daemon=True) │ │ t.start() │ │ │ │ time.sleep(3) │ │ print("主线程结束,守护线程自动终止") │ │ ``` │ │ │ │ --- │ │ │ │ ## 三、线程同步原语(Synchronization Primitives) │ │ │ │ ### 3.1 Lock(互斥锁) │ │ │ │ 最基本的同步原语,确保共享资源的互斥访问: │ │ │ │ ```python │ │ import threading │ │ │ │ counter = 0 │ │ lock = threading.Lock() │ │ │ │ def increment(n): │ │ global counter │ │ for _ in range(n): │ │ with lock: # 上下文管理器自动 acquire/release │ │ counter += 1 │ │ │ │ # 无锁情况下的竞态条件示例 │ │ def unsafe_increment(n): │ │ global counter │ │ for _ in range(n): │ │ counter += 1 # 非原子操作:读-改-写,可能产生竞态 │ │ ``` │ │ │ │ ### 3.2 RLock(可重入锁) │ │ │ │ 允许同一线程多次获取同一把锁(常用于递归场景): │ │ │ │ ```python │ │ rlock = threading.RLock() │ │ │ │ def recursive_func(n): │ │ with rlock: │ │ if n > 0: │ │ recursive_func(n - 1) # 同一线程可再次获取锁 │ │ # 否则普通 Lock 会死锁! │ │ ``` │ │ │ │ ### 3.3 Semaphore(信号量) │ │ │ │ 控制同时访问资源的线程数量: │ │ │ │ ```python │ │ # 最多允许 3 个线程同时访问资源 │ │ semaphore = threading.Semaphore(3) │ │ │ │ def limited_access(): │ │ with semaphore: │ │ print(f"{threading.current_thread().name} 正在访问资源") │ │ time.sleep(2) │ │ # 离开 with 块时自动释放 │ │ │ │ # BoundedSemaphore: 防止 release() 被调用超过 acquire() 次数 │ │ bounded_sem = threading.BoundedSemaphore(3) │ │ ``` │ │ │ │ ### 3.4 Event(事件) │ │ │ │ 线程间通信的简单机制,一个线程等待信号,另一个线程发送信号: │ │ │ │ ```python │ │ event = threading.Event() │ │ │ │ def waiter(): │ │ print("等待事件...") │ │ event.wait() # 阻塞直到 event.set() │ │ print("事件已触发!") │ │ event.clear() # 重置事件 │ │ event.wait(timeout=5) # 超时等待 │ │ │ │ def setter(): │ │ time.sleep(2) │ │ event.set() # 触发事件,唤醒所有等待线程 │ │ │ │ # Event 方法:set(), clear(), wait(), is_set() │ │ ``` │ │ │ │ ### 3.5 Condition(条件变量) │ │ │ │ 比 Event 更强大,支持 notify/wait 模式: │ │ │ │ ```python │ │ condition = threading.Condition() │ │ items = [] │ │ │ │ def producer(): │ │ for i in range(5): │ │ with condition: │ │ items.append(f"item-{i}") │ │ print(f"生产: item-{i}") │ │ condition.notify() # 唤醒一个等待的消费者 │ │ # condition.notify_all() # 唤醒所有等待的消费者 │ │ │ │ def consumer(): │ │ while True: │ │ with condition: │ │ while not items: │ │ condition.wait() # 等待生产者通知 │ │ item = items.pop(0) │ │ print(f"消费: {item}") │ │ ``` │ │ │ │ ### 3.6 Barrier(屏障) │ │ │ │ 让多个线程互相等待,直到所有线程都到达屏障点: │ │ │ │ ```python │ │ # 3 个线程都到达后才能继续 │ │ barrier = threading.Barrier(3) │ │ │ │ def worker(): │ │ print(f"{threading.current_thread().name} 阶段1完成") │ │ barrier.wait() # 等待所有线程 │ │ print(f"{threading.current_thread().name} 阶段2开始") │ │ ``` │ │ │ │ ### 3.7 Timer(定时器) │ │ │ │ 延迟执行任务: │ │ │ │ ```python │ │ def delayed_task(): │ │ print("延迟执行的任务") │ │ │ │ # 5 秒后执行 │ │ timer = threading.Timer(5.0, delayed_task) │ │ timer.start() │ │ timer.cancel() # 可以取消 │ │ ``` │ │ │ │ ### 3.8 同步原语对比总结 │ │ │ │ | 原语 | 用途 | 特点 | │ │ |------|------|------| │ │ | **Lock** | 互斥访问 | 不可重入,简单高效 | │ │ | **RLock** | 递归互斥 | 同一线程可多次获取 | │ │ | **Semaphore** | 资源池控制 | 限制并发数 N | │ │ | **Event** | 信号通知 | 一对多广播 | │ │ | **Condition** | 生产者-消费者 | 支持 notify/wait | │ │ | **Barrier** | 阶段同步 | N 个线程相互等待 | │ │ | **Timer** | 延迟执行 | 定时触发 | │ │ │ │ --- │ │ │ │ ## 四、`concurrent.futures` — 高层接口 │ │ │ │ ### 4.1 ThreadPoolExecutor │ │ │ │ ```python │ │ from concurrent.futures import ThreadPoolExecutor, as_completed │ │ import time │ │ import urllib.request │ │ │ │ def fetch_url(url): │ │ """模拟 I/O 密集型任务""" │ │ start = time.time() │ │ with urllib.request.urlopen(url) as response: │ │ data = response.read() │ │ elapsed = time.time() - start │ │ return f"{url}: {len(data)} bytes in {elapsed:.2f}s" │ │ │ │ urls = [ │ │ "https://www.python.org", │ │ "https://www.github.com", │ │ "https://www.stackoverflow.com", │ │ ] │ │ │ │ # 使用线程池 │ │ with ThreadPoolExecutor(max_workers=5) as executor: │ │ # 方式1: submit + as_completed(按完成顺序获取结果) │ │ futures = {executor.submit(fetch_url, url): url for url in urls} │ │ for future in as_completed(futures): │ │ print(future.result()) │ │ │ │ # 方式2: map(保持输入顺序) │ │ # results = executor.map(fetch_url, urls) │ │ # for result in results: │ │ # print(result) │ │ ``` │ │ │ │ ### 4.2 Future 对象方法 │ │ │ │ | 方法 | 说明 | │ │ |------|------| │ │ | `result(timeout=None)` | 获取返回值(阻塞等待) | │ │ | `exception(timeout=None)` | 获取异常(如有) | │ │ | `done()` | 任务是否完成 | │ │ | `running()` | 是否正在执行 | │ │ | `cancel()` | 尝试取消任务 | │ │ | `cancelled()` | 是否被取消 | │ │ | `add_done_callback(fn)` | 完成时回调 | │ │ │ │ --- │ │ │ │ ## 五、I/O 密集型 vs CPU 密集型 │ │ │ │ ### 5.1 核心区别 │ │ │ │ ``` │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 任务类型对比 │ │ │ ├───────────────┬─────────────────────┬───────────────────────┤ │ │ │ 场景 │ I/O 密集型 │ CPU 密集型 │ │ │ ├───────────────┼─────────────────────┼───────────────────────┤ │ │ │ 典型操作 │ 网络请求、文件读写 │ 数学计算、图像处理 │ │ │ │ │ 数据库查询、爬虫 │ 加密、模型训练 │ │ │ ├───────────────┼─────────────────────┼───────────────────────┤ │ │ │ GIL 影响 │ ★ 影响小 │ ★★★★★ 影响巨大 │ │ │ │ │ (I/O 等待时释放 GIL) │ (计算时不释放 GIL) │ │ │ ├───────────────┼─────────────────────┼───────────────────────┤ │ │ │ 推荐方案 │ threading / asyncio │ multiprocessing │ │ │ ├───────────────┼─────────────────────┼───────────────────────┤ │ │ │ 性能提升 │ 线性 ~ N 倍 │ 几乎无提升 │ │ │ │ │ │ (甚至变慢) │ │ │ └───────────────┴─────────────────────┴───────────────────────┘ │ │ ``` │ │ │ │ ### 5.2 性能对比实验 │ │ │ │ ```python │ │ import threading │ │ import time │ │ import math │ │ │ │ # === CPU 密集型任务 === │ │ def cpu_bound(n=10_000_000): │ │ """纯计算,持续持有 GIL""" │ │ result = 0 │ │ for i in range(n): │ │ result += math.sqrt(i) │ │ return result │ │ │ │ # === I/O 密集型任务 === │ │ def io_bound(): │ │ """网络/磁盘 I/O,等待时释放 GIL""" │ │ import urllib.request │ │ with urllib.request.urlopen("https://www.python.org") as r: │ │ return len(r.read()) │ │ │ │ # 单线程 baseline │ │ def run_sequential(task, times=4): │ │ start = time.perf_counter() │ │ for _ in range(times): │ │ task() │ │ return time.perf_counter() - start │ │ │ │ # 多线程 │ │ def run_threaded(task, times=4): │ │ start = time.perf_counter() │ │ threads = │ │ for t in threads: │ │ t.start() │ │ for t in threads: │ │ t.join() │ │ return time.perf_counter() - start │ │ │ │ # 结果对比(典型数据): │ │ # CPU密集型: sequential=10s, threaded=11s (几乎无提升,甚至变慢) │ │ # IO密集型: sequential=8s, threaded=2s (接近 4x 提升) │ │ ``` │ │ │ │ --- │ │ │ │ ## 六、`multiprocessing` — 绕过 GIL 的替代方案 │ │ │ │ ### 6.1 核心思想 │ │ │ │ 每个进程拥有独立的 Python 解释器和 GIL,真正实现并行: │ │ │ │ ```python │ │ from multiprocessing import Process, Pool, cpu_count │ │ import time │ │ │ │ def cpu_intensive(n): │ │ """CPU 密集型计算""" │ │ result = 0 │ │ for i in range(n): │ │ result += i ** 2 │ │ return result │ │ │ │ # 方式1: Process(类似 Thread) │ │ processes = [] │ │ for i in range(cpu_count()): │ │ p = Process(target=cpu_intensive, args=(5_000_000,)) │ │ p.start() │ │ processes.append(p) │ │ for p in processes: │ │ p.join() │ │ │ │ # 方式2: Pool(推荐) │ │ with Pool(processes=cpu_count()) as pool: │ │ results = pool.map(cpu_intensive, [5_000_000] * cpu_count()) │ │ ``` │ │ │ │ ### 6.2 进程间通信 │ │ │ │ ```python │ │ from multiprocessing import Queue, Pipe, Value, Array, Manager │ │ │ │ # Queue: 进程安全队列 │ │ q = Queue() │ │ q.put("data") │ │ item = q.get() │ │ │ │ # Pipe: 双向管道 │ │ parent_conn, child_conn = Pipe() │ │ parent_conn.send("hello") │ │ msg = child_conn.recv() │ │ │ │ # Value / Array: 共享内存(C 类型) │ │ counter = Value('i', 0) # int 类型 │ │ arr = Array('d', [1.0, 2.0]) # double 类型 │ │ │ │ # Manager: 高级共享对象 │ │ with Manager() as manager: │ │ shared_list = manager.list() │ │ shared_dict = manager.dict() │ │ ``` │ │ │ │ ### 6.3 threading vs multiprocessing 对比 │ │ │ │ | 维度 | threading | multiprocessing | │ │ |------|-----------|-----------------| │ │ | **并行能力** | 受 GIL 限制 | 真正的并行 | │ │ | **内存开销** | 低(共享内存) | 高(独立内存空间) | │ │ | **启动速度** | 快(毫秒级) | 慢(秒级) | │ │ | **通信方式** | 共享变量 + 锁 | Queue/Pipe/共享内存 | │ │ | **适用场景** | I/O 密集型 | CPU 密集型 | │ │ | **调试难度** | 中等 | 较高 | │ │ │ │ --- │ │ │ │ ## 七、`queue.Queue` — 线程安全队列 │ │ │ │ ```python │ │ from queue import Queue, LifoQueue, PriorityQueue │ │ import threading │ │ │ │ # FIFO 队列(默认) │ │ q = Queue(maxsize=10) # 有界队列 │ │ q.put(item, block=True, timeout=5) # 阻塞放入 │ │ item = q.get(block=True, timeout=5) # 阻塞取出 │ │ q.task_done() # 标记任务完成 │ │ q.join() # 等待所有任务完成 │ │ │ │ # LIFO 队列(栈) │ │ stack = LifoQueue() │ │ │ │ # 优先级队列 │ │ pq = PriorityQueue() │ │ pq.put((2, "low priority")) │ │ pq.put((1, "high priority")) # 数字越小,优先级越高 │ │ ``` │ │ │ │ ### 生产者-消费者模式 │ │ │ │ ```python │ │ import threading │ │ from queue import Queue │ │ import time │ │ import random │ │ │ │ def producer(queue, n_items): │ │ for i in range(n_items): │ │ item = f"data-{i}" │ │ queue.put(item) │ │ print(f"生产: {item}") │ │ time.sleep(random.uniform(0.1, 0.5)) │ │ queue.put(None) # 哨兵值,通知消费者停止 │ │ │ │ def consumer(queue, name): │ │ while True: │ │ item = queue.get() │ │ if item is None: # 收到哨兵 │ │ queue.put(None) # 传递给下一个消费者 │ │ break │ │ print(f"[{name}] 消费: {item}") │ │ time.sleep(random.uniform(0.2, 0.8)) │ │ │ │ q = Queue(maxsize=5) │ │ prod = threading.Thread(target=producer, args=(q, 10)) │ │ cons = │ │ │ │ prod.start() │ │ for c in cons: │ │ c.start() │ │ prod.join() │ │ for c in cons: │ │ c.join() │ │ ``` │ │ │ │ --- │ │ │ │ ## 八、Python 3.13 Free-Threading(重大变革) │ │ │ │ ### 8.1 历史性突破 │ │ │ │ Python 3.13(2024 年 10 月发布)引入了 **free-threaded CPython │ │ 构建**,首次在官方版本中允许禁用 GIL。这是 PEP 703 的实现成果。 │ │ │ │ ### 8.2 如何启用 │ │ │ │ ```bash │ │ # 安装 free-threaded 版本(独立的可执行文件) │ │ # 通常命名为 python3.13t 或 python3.13t.exe │ │ │ │ # 检查 GIL 状态 │ │ python3.13t -c "import sys; print(sys._is_gil_enabled())" # False │ │ │ │ # 临时启用 GIL │ │ PYTHON_GIL=1 python3.13t script.py │ │ # 或 │ │ python3.13t -X gil=1 script.py │ │ ``` │ │ │ │ ### 8.3 性能提升实测 │ │ │ │ 来自社区 benchmark(CPU 密集型多线程任务,4 核环境): │ │ │ │ | 模式 | 执行时间 | 加速比 | │ │ |------|---------|--------| │ │ | 单线程(GIL 启/禁) | ~7.5s | 1x | │ │ | 多线程(GIL 启用) | ~7.5s | **1x**(无提升) | │ │ | 多线程(GIL 禁用) | ~1.4s | **~5.4x** | │ │ | 多进程(GIL 启/禁) | ~1.5s | ~5x | │ │ │ │ **关键发现**:GIL │ │ 禁用后,多线程性能从几乎无提升跃升至接近多进程水平,且内存开销更低。 │ │ │ │ ### 8.4 注意事项 │ │ │ │ 1. **向后兼容**:普通 Python 3.13 构建仍默认启用 GIL,free-threaded │ │ 是独立构建 │ │ 2. **C 扩展兼容性**:部分 C 扩展尚未适配无 GIL 环境,可能自动重新启用 GIL │ │ 3. **生态过渡期**:NumPy、pandas 等核心库正在适配中 │ │ 4. **不建议生产使用**:3.13 的 free-threaded 仍标记为实验性功能 │ │ 5. **长期目标**:PEP 703 计划在未来版本中将 free-threaded 作为默认模式 │ │ │ │ --- │ │ │ │ ## 九、协程(asyncio)— 另一种并发模型 │ │ │ │ 虽然不是多线程,但 asyncio 是 Python 解决 I/O 密集型并发的现代方案: │ │ │ │ ```python │ │ import asyncio │ │ import aiohttp │ │ │ │ async def fetch(session, url): │ │ async with session.get(url) as response: │ │ return await response.text() │ │ │ │ async def main(): │ │ urls = ["https://python.org", "https://github.com"] │ │ async with aiohttp.ClientSession() as session: │ │ tasks = │ │ results = await asyncio.gather(*tasks) │ │ return results │ │ │ │ asyncio.run(main()) │ │ ``` │ │ │ │ | 方案 | 并发模型 | 适用场景 | 内存开销 | │ │ |------|---------|----------|----------| │ │ | **threading** | 抢占式多线程 | I/O 密集型 | 低 | │ │ | **multiprocessing** | 多进程 | CPU 密集型 | 高 | │ │ | **asyncio** | 协作式协程 | 高并发 I/O | 极低 | │ │ │ │ --- │ │ │ │ ## 十、最佳实践与选型建议 │ │ │ │ ### 10.1 决策流程图 │ │ │ │ ``` │ │ 需要并发/并行? │ │ │ │ │ ├── I/O 密集型? │ │ │ ├── 需要极高并发(万级连接)? → asyncio │ │ │ ├── 使用已有同步库? → threading / ThreadPoolExecutor │ │ │ └── 简单并发请求? → ThreadPoolExecutor │ │ │ │ │ ├── CPU 密集型? │ │ │ ├── Python 3.13t 可用? → threading(GIL 禁用) │ │ │ └── 否则 → multiprocessing / ProcessPoolExecutor │ │ │ │ │ └── 混合型? │ │ └── multiprocessing + threading 嵌套使用 │ │ ``` │ │ │ │ ### 10.2 常见坑与解决方案 │ │ │ │ | 问题 | 解决方案 | │ │ |------|----------| │ │ | **竞态条件** | 使用 Lock / RLock 保护共享数据 | │ │ | **死锁** | 统一加锁顺序;使用 `RLock`;设置超时 │ │ `lock.acquire(timeout=...)` | │ │ | **线程泄露** | 使用 `ThreadPoolExecutor` 自动管理生命周期 | │ │ | **GIL 瓶颈** | CPU 密集用 multiprocessing;I/O 密集用 threading/asyncio | │ │ | **过度上下文切换** | 线程数 ≈ CPU 核心数 × (1 + 等待时间/计算时间) | │ │ | **守护线程数据丢失** | 关键任务使用非守护线程 + `join()` | │ │ │ │ ### 10.3 线程池大小经验公式 │ │ │ │ ```python │ │ import os │ │ │ │ # I/O 密集型:可以开较多线程 │ │ io_workers = min(32, os.cpu_count() + 4) │ │ │ │ # CPU 密集型(传统 GIL 模式):多线程无效,使用多进程 │ │ cpu_workers = os.cpu_count() │ │ │ │ # CPU 密集型(Free-Threaded 模式): │ │ free_thread_workers = os.cpu_count() │ │ ``` │ │ │ │ --- │ │ │ │ ## 十一、总结 │ │ │ │ | 维度 | 总结 | │ │ |------|------| │ │ | **GIL 本质** | CPython │ │ 的内存管理简化机制,限制同一时刻仅一个线程执行字节码 | │ │ | **threading 适用** | I/O 密集型任务(网络、磁盘、数据库),通过时间片和 │ │ I/O 释放 GIL | │ │ | **multiprocessing 适用** | CPU 密集型任务,每个进程独立 GIL,实现真正并行 │ │ | │ │ | **同步原语** | Lock/RLock/Semaphore/Event/Condition/Barrier │ │ 覆盖各类并发控制场景 | │ │ | **高层接口** | `concurrent.futures` 提供 ThreadPoolExecutor / │ │ ProcessPoolExecutor | │ │ | **Python 3.13 变革** | Free-Threaded 构建允许禁用 GIL,CPU │ │ 密集型多线程性能提升 5x+ | │ │ | **未来趋势** | Free-Threaded 是长期方向,但生态完全适配仍需数年 | │ │ │ │ --- │ │ │ │ > **参考资料**:Python 官方文档 (docs.python.org)、PEP 703、Python 3.13 │ │ Release Notes、社区 benchmark 报告 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ --- │ │ │ │ 以上就是 OpenClaw(小龙虾)技术架构的完整调研报告。核心要点总结: │ │ │ │ 1. **六层架构**:渠道适配 → Gateway控制平面 → Agent运行时(ReAct) → │ │ 模型抽象层 → 能力触角(Skills+沙箱) → 数据持久层 │ │ 2. **Gateway 是心脏**:Node.js 进程,负责会话路由、Lane Queue │ │ 序列化队列、定时任务 │ │ 3. **ReAct 循环**:边推理边行动,不是预先规划、死板执行 │ │ 4. **Skills 按需加载**:解决 context 膨胀和 token 浪费的关键设计 │ │ 5. **双源记忆**:Markdown 文件(长期)+ JSONL 会话日志(动态)+ SQLite │ │ 向量搜索 │ │ 6. **Heartbeat 主动唤醒**:区别于所有其他 Agent 框架的最大创新 │ │ 7. **安全多层防御**:网络隔离、工具白名单、Docker 沙箱、硬拦截危险指令 │ │ │ │ 如果你对某个具体组件(如记忆系统、Skills 开发、Gateway │ │ 实现细节等)想深入了解,可以继续提问。 │ └─────────────────────────────────────────────────────────────────────────────┘ Step 14: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '调研 Claude Code 技术架构', 'status': 'completed'}, {'content': '调研 Pyt... Step 18: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '调研 Claude Code 技术架构', 'status': 'completed'}, {'content': '调研 Pyt... ┌────────────────────────── Agent回复 (Node: model) ──────────────────────────┐ │ --- │ │ │ │ # 四主题调研报告 │ │ │ │ --- │ │ │ │ ## 1. Claude Code 技术架构 │ │ │ │ **Claude Code** 是 Anthropic 推出的**终端优先(Terminal-first)的自主编程 │ │ Agent**,采用 TypeScript + React(Ink 终端 UI 库)构建,通过 npm 分发。 │ │ │ │ ### 核心架构:六层体系 + 单线程主循环 │ │ │ │ | 层级 | 职责 | │ │ |------|------| │ │ | Layer 1 输入层 | 会话管理、权限门控、消息队列 | │ │ | Layer 2 知识层 | Skill注册表、跨会话记忆(CLAUDE.md)、上下文压缩器 | │ │ | Layer 3 执行层 | 工具系统(Read/Write/Edit/Bash/Grep/Glob/Task/todos) | │ │ | Layer 4 集成层 | MCP运行时、Hook系统、Prompt Cache | │ │ | Layer 5 多Agent层 | 子代理隔离分发、任务并行 | │ │ | Layer 6 模型层 | Claude Sonnet API 推理 | │ │ │ │ **核心设计哲学**:主循环是极简的 "Dumb Loop"——模型输出工具调用 → 执行 → │ │ 结果返回模型 → │ │ 循环,直到模型认为任务完成。**不使用向量数据库/RAG、意图分类器等传统 Agent │ │ 组件**,智能完全来自模型权重。约 90% 代码由 Claude Code 自身编写。 │ │ │ │ --- │ │ │ │ ## 2. Python 多线程机制 │ │ │ │ ### GIL(全局解释器锁) │ │ CPython 中的互斥锁,**同一时刻只允许一个线程执行 Python │ │ 字节码**。存在原因:简化内存管理(引用计数安全)、保证 C 扩展兼容性。 │ │ │ │ ### threading 模块核心组件 │ │ │ │ | 同步原语 | 用途 | │ │ |----------|------| │ │ | **Lock** | 互斥访问共享资源 | │ │ | **RLock** | 允许同一线程多次获取 | │ │ | **Semaphore** | 限制并发数 N | │ │ | **Event** | 线程间一对多信号通知 | │ │ | **Condition** | 生产者-消费者 notify/wait | │ │ | **Barrier** | N 个线程互相等待到齐 | │ │ │ │ ### 关键结论 │ │ │ │ - **I/O 密集型** → `threading` / `ThreadPoolExecutor`,GIL 影响小(I/O │ │ 等待时释放),性能 ~N 倍提升 │ │ - **CPU 密集型** → `multiprocessing` / `ProcessPoolExecutor`,绕过 │ │ GIL,每进程独立解释器 │ │ - **高并发 I/O**(万级连接)→ `asyncio` 协程,内存开销极低 │ │ - **Python 3.13 突破**:引入 free-threaded 模式,可禁用 GIL,CPU │ │ 密集型多线程性能提升 5x+ │ │ │ │ --- │ │ │ │ ## 3. Python 性能差的原因 │ │ │ │ ### 六大根因 │ │ │ │ | 原因 | 影响 | 占比 | │ │ |------|------|------| │ │ | **解释执行** | 字节码 → 解释器 dispatch loop → CPU,多一层转换 | ~11% | │ │ | **动态类型** | 运行时类型检查替代编译期优化,`a+b` 需查找 `__add__` | ~6% │ │ | │ │ | **万物皆对象** | 一个 int 占 28+ 字节(C仅4字节),内存开销大5-7倍 | ~13% │ │ | │ │ | **引用计数GC** | 每次赋值/传递都触发原子操作,CPU密集时占 10-30% | — | │ │ | **GIL 锁** | 多核 CPU 密集型任务无法真正并行 | — | │ │ | **无 JIT(CPython)** | 纯解释执行,无热点编译优化 | — | │ │ │ │ ### 优化方向 │ │ - **PyPy**:追踪 JIT,比 CPython 快 4-7 倍 │ │ - **Cython**:编译为 C 扩展 │ │ - **Numba**:JIT 编译数值计算 │ │ - **Python 3.14**:正在引入 Copy-and-Patch JIT(PEP 744) │ │ │ │ > **核心矛盾**:Python 用性能换开发效率。比 C 慢 10-100 倍,但开发效率高 │ │ 3-10 倍。CPU 密集用 C/Rust 扩展,Python 做胶水层。 │ │ │ │ --- │ │ │ │ ## 4. OpenClaw(小龙虾)技术架构 │ │ │ │ 调研发现的相关项目是 **OpenClaw**(小龙虾),一个开源的多渠道 AI Agent │ │ 聊天机器人框架。 │ │ │ │ ### 核心架构:六层体系 │ │ │ │ | 层级 | 组件 | │ │ |------|------| │ │ | Channel Adapters | 多渠道适配(WhatsApp/TG/Discord/Web等) | │ │ | Gateway 控制平面 | Node.js 进程,负责会话路由、Lane Queue │ │ 序列化队列、定时任务 | │ │ | Agent 运行时 | **ReAct 循环**(边推理边行动),非预规划执行 | │ │ | 模型抽象层 | 统一模型调用接口 | │ │ | 能力触角 | Skills 按需加载 + Docker 沙箱执行 | │ │ | 数据持久层 | Markdown 长期记忆 + JSONL 会话日志 + SQLite 向量搜索 | │ │ │ │ ### 关键创新 │ │ │ │ - **Heartbeat 主动唤醒**:区别于被动等待的 Agent 框架,可定时主动触发对话 │ │ - **Skills 按需加载**:解决 context 膨胀和 token 浪费 │ │ - **双源记忆**:Markdown(长期)+ JSONL(动态)+ SQLite 向量检索 │ │ - **多层安全**:网络隔离 + 工具白名单 + Docker 沙箱 + 硬拦截危险指令 │ │ │ │ --- │ │ │ │ 以上是四个主题的调研总结,如需深入了解某个方向的更多细节,可以进一步提问! │ └─────────────────────────────────────────────────────────────────────────────┘ 演示结束 Process finished with exit code 0
显示传入subAgent参数
安装 MCP 适配器(关键依赖)\MCP 服务器开发库(如需自定义工具)
pip install langchain-mcp-adapters mcp
检查 Node.js
node –-version
检查 npm/npx
npx –version
手动安装 MCP 服务器包
npm install -g @amap/amap-maps-mcp-server
案例:
import asyncio import os import dotenv from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_core.messages import BaseMessage, ToolMessage from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_tavily import TavilySearch from rich.console import Console from rich.panel import Panel from rich.tree import Tree # 1.加载环境变量 dotenv.load_dotenv() # 2.配置Rich Console console = Console() # 3.配置 Context7 MCP # Windows 上 stdio + npx 在并行子 Agent 里会反复拉起/关闭 Node 进程, # 容易触发: Assertion failed: !(handle->flags & UV_HANDLE_CLOSING) # 因此默认用远程 HTTP;失败则回退 Tavily。 async def setup_mcp_tools(): console.print("[dim]正在连接 Context7(streamable_http)...[/dim]") context7_headers = {} api_key = os.getenv("CONTEXT7_API_KEY") if api_key: context7_headers["CONTEXT7_API_KEY"] = api_key try: mcp_client = MultiServerMCPClient({ "context7": { "transport": "streamable_http", "url": "https://mcp.context7.com/mcp", **({"headers": context7_headers} if context7_headers else {}), }, }) tools = await mcp_client.get_tools() console.print(f"[bold green]成功加载 {len(tools)} 个 MCP 工具(HTTP)[/bold green]") return mcp_client, tools except Exception as e: console.print(f"[bold yellow]Context7 HTTP 连接失败:{e}[/bold yellow]") return None, [] # 4.定义子Agent配置 # 注意:不要写 model="deepseek-v4-pro" 字符串 —— create_agent 会再 init_chat_model, # 并要求环境变量 DEEPSEEK_API_KEY。应传入已配置好的模型实例,或省略 model 继承主 Agent。 def get_subagent_config(mcp_tools, model): # 每个子 Agent 用独立工具实例,避免并行时共享同一 MCP session 出问题 community_search = TavilySearch(max_results=3) if mcp_tools: doc_tools = list(mcp_tools) docs_prompt = ( "你是一名专门查询官方文档的技术专家。" "请优先使用 Context7 MCP 工具获取准确的技术细节。不要猜测。" ) console.print("[dim]DocsResearcher 使用 Context7 MCP 工具[/dim]") else: doc_tools = [TavilySearch(max_results=5)] docs_prompt = ( "你是一名专门查询官方文档的技术专家。" "请用搜索工具查找 docs.langchain.com / deepagents 官方文档,只依据检索结果作答,不要猜测。" ) console.print("[dim]DocsResearcher 使用 Tavily(未启用 MCP)[/dim]") docs_researcher = { "name": "DocsResearcher", "description": "负责查阅官方文档和技术规范的专家Agent。", "system_prompt": docs_prompt, "tools": doc_tools, "model": model, } community_researcher = { "name": "CommunityResearcher", "description": "负责搜索社区博客、教程和最佳实践的专家Agent。", "system_prompt": "你是一名关注社区动态的开发者,请搜索博客、论坛和GitHub讨论。", "tools": [community_search], "model": model, } return [docs_researcher, community_researcher] # 5.主逻辑 async def run_parallel_subagents(): console.print(Panel.fit("[bold blue]DeepAgents并行子Agent演示[/bold blue]", border_style="blue")) # 先初始化模型(子 Agent 也要用同一套配置) deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) # 获取MCP工具 mcp_client, mcp_tools = await setup_mcp_tools() # 获取子Agent配置(传入同一 model,避免 DEEPSEEK_API_KEY 校验失败) subagent_config = get_subagent_config(mcp_tools, deepseek_v4_pro) agent = create_deep_agent( model=deepseek_v4_pro, tools=[], subagents=subagent_config, system_prompt=""" 你是一名技术总监。你的任务是协调DocsResearcher和CommunityResearcher完成调研任务。 请你根据用户需求,将任务拆解分发给这两个子Agent。 调用 task 工具时,subagent_type 必须是 DocsResearcher 或 CommunityResearcher。 如果任务允许,请务必并行调用它们以提高效率。 最后汇总它们的报告。 """, ) task = ( "请详细调研 'LangChain DeepAgents' 框架。" "我需要官方的技术架构说明(来自文档)以及社区最佳实践(来自博客、论坛、GitHub讨论)。请对比两者" ) console.print(f"[bold green]任务指令:[/bold green] {task}\n") # 运行并可视化 step = 0 try: # astream:异步流式跑图。每跑完「一个节点」就 yield 一个 chunk(默认类似 updates 模式) # 输入格式和 invoke 一样:{"messages": [用户消息, ...]} async for chunk in agent.astream({"messages": [{"role": "user", "content": task}]}): step += 1 # 每收到一个 chunk 算一步(含后面不打印的静默更新,所以 Step 可能跳号) # ---------- chunk 长什么样? ---------- # 当前写法没传 stream_mode 时,astream 默认一般是 "updates"。若要显式写清楚: # chunk 是一个 dict,键 = LangGraph 节点名,值 = 该节点本次产出的状态增量。 # 例子: # {"model": {"messages": [AIMessage(...)]}} # {"tools": {"messages": [ToolMessage(...), ToolMessage(...)]}} # 同一轮也可能只有一个键。 # .items():把 dict 拆成 (key, value) 对,便于 for 解包。 # node_name → 如 "model" / "tools" # node_data → 如 {"messages": [...]},也可能是 todos 等其它字段 for node_name, node_data in chunk.items(): # 个别更新可能是空的,跳过 if node_data is None: continue # 本演示只关心「带 messages 的更新」; # 若节点只改了 todos 等,没有 messages,整段 if 不进 → 终端上就像「少了 Step」 if "messages" in node_data: # 取出本节点新增/带回的消息列表(updates 模式下通常是本步新消息,不是全历史) msgs = node_data["messages"] # 防御:偶发单条消息而不是 list,先包成 list,后面统一 for 遍历 if not isinstance(msgs, list): msgs = [msgs] for msg in msgs: # 0) 只要 LangChain 消息对象(HumanMessage / AIMessage / ToolMessage…) # 其它奇怪类型直接跳过 if not isinstance(msg, BaseMessage): continue # 安全读 tool_calls: # - AIMessage 可能带 tool_calls(模型决定调哪些工具) # - HumanMessage / ToolMessage 通常没有该属性 # getattr(..., None) 避免 AttributeError;or [] 把 None/空统一成空列表 tool_calls = getattr(msg, "tool_calls", None) or [] # ---------- 分支 1:模型发起工具调用(常见于 Node=model)---------- # tool_calls 是一个列表,例如模型一次要调 4 个工具: # [ # {"name": "task", "args": {"subagent_type": "general-purpose", "description": "调研 Claude Code"}}, # {"name": "task", "args": {"subagent_type": "general-purpose", "description": "调研 Python 多线程"}}, # {"name": "write_todos", "args": {...}}, # ] if tool_calls: # Rich Tree:树形打印「本步调了哪些工具」 tree = Tree( f"[bold yellow]Step {step}: 决策与调用 (Node: {node_name}) [/bold yellow]" ) # 一条 AIMessage 里可以并行请求多个工具(本 demo 里常见一次 4 个 task) # 取出工具名和参数(兼容两种格式) # tc 是字典 tc["name"]、tc["args"](用 .get 防缺 key) # tc 是对象 tc.name、tc.args(用 getattr 防缺属性) for tc in tool_calls: # LangChain 里 tool_call 有时是 dict,有时是对象,两种都兼容 if isinstance(tc, dict): tool_name = tc.get("name", "未知工具") # 如 "task" / "write_todos" tool_args = tc.get("args", {}) # 调用参数字典 else: tool_name = getattr(tc, "name", "未知工具") tool_args = getattr(tc, "args", {}) or {} if tool_name == "task": # task = SubAgentMiddleware 注入的「派生子 Agent」工具 # 典型 args:subagent_type="general-purpose", description="调研xxx" # Step 7: 决策与调用 (Node: model) ← 这是外面已建好的 tree 根 # └── 成功触发 'task' 工具 (Sub-Agent) ← branch = tree.add(...) # ├── 子Agent类型: general-purpose ← branch.add(...) # └── 任务指令: 调研 xxx ← branch.add(...) branch = tree.add( "[bold red]成功触发 'task' 工具 (Sub-Agent)[/bold red]" ) # tree.add 返回子节点,可再 .add 挂详情 branch.add( f"[cyan]子Agent类型:[/cyan] {tool_args.get('subagent_type', '未知')}" ) branch.add( f"[cyan]任务指令:[/cyan] {tool_args.get('description', '未知')}" ) else: # 其它工具(write_todos、tavily_search…)一律标成普通工具 tree.add(f"[bold blue]普通工具调用:[/bold blue] {tool_name}") console.print(tree) # 把整棵树打到终端 # ---------- 分支 2:工具执行结果(常见于 Node=tools)---------- # ToolMessage:某个工具跑完后的返回;.name 是工具名,.content 是返回正文 # 判断当前消息是不是[工具回执],只有工具跑完后,图才会产生ToolMessage elif isinstance(msg, ToolMessage): content = str(msg.content) # 统一转成字符串再展示 # task表示是子Agent的结果 if msg.name == "task": # 子 Agent 整段任务做完后,只把「最终摘要/报告」回传给主 Agent # (子 Agent 内部搜索过程默认不会全部冒泡到这里) console.print( Panel( content, title=f"[bold magenta]Sub-Agent 完成任务 (Node: {node_name})[/bold magenta]", border_style="magenta", ) ) else: # 非 task 工具(如 write_todos):内容可能很长,只预览前 100 字 preview = content[:100] + ("..." if len(content) > 100 else "") console.print(f"[dim]Tool Output ({msg.name}): {preview}[/dim]") # ---------- 分支 3:模型纯文本回复(无工具调用)---------- # 条件:有 content,且 tool_calls 为空 → 多半是对用户的最终/中间文字回答 elif getattr(msg, "content", None) and not tool_calls: title = f"[bold green]Agent回复 (Node: {node_name})[/bold green]" console.print(Panel(msg.content, title=title, border_style="green")) except Exception as e: console.print(f"[bold red]运行时错误:{e}[/bold red]") console.print(f"[bold green]运行结束退出。[/bold green]") if __name__ == '__main__': # 模块顶层不能直接 await,要用 asyncio.run 驱动协程 asyncio.run(run_parallel_subagents())
输出:
D:\anaconda3\envs\deepagents\python.exe E:\code\InsightFlow\create_demo_deep_agent\demo_mulit_server.py ┌───────────────────────────┐ │ DeepAgents并行子Agent演示 │ └───────────────────────────┘ 正在连接 Context7(streamable_http)... 成功加载 2 个 MCP 工具(HTTP) DocsResearcher 使用 Context7 MCP 工具 任务指令: 请详细调研 'LangChain DeepAgents' 框架。我需要官方的技术架构说明(来自文档)以及社区最佳实践(来自博客、论坛、Git Hub讨论)。请对比两者 Step 3: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '通过 DocsResearcher 调研 LangChain DeepAgents 官方文档和技术架构', 'status': '... Step 7: 决策与调用 (Node: model) ├── 成功触发 'task' 工具 (Sub-Agent) │ ├── 子Agent类型: DocsResearcher │ └── 任务指令: 调研 LangChain DeepAgents 官方文档和架构 └── 成功触发 'task' 工具 (Sub-Agent) ├── 子Agent类型: CommunityResearcher └── 任务指令: 调研 DeepAgents 社区最佳实践 ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ --- │ │ │ │ # DeepAgents 社区最佳实践调研报告 │ │ │ │ > 综合整理了 GitHub Discussions、Reddit │ │ (r/LangChain)、官方文档、技术博客及社区文章中的最佳实践。 │ │ │ │ --- │ │ │ │ ## 一、项目定位与架构概况 │ │ │ │ **DeepAgents** 是 LangChain 官方出品的 "batteries-included agent │ │ harness"(开箱即用的 Agent 骨架),构建于 LangGraph 之上。它从 Claude │ │ Code、Deep Research │ │ 等生产级系统中提炼出常见模式,内置了规划、文件系统、子代理委托、上下文管理 │ │ 和技能加载等能力。 │ │ │ │ **三层架构:** │ │ ``` │ │ LangGraph (图运行时) → create_agent (轻量 harness) → DeepAgents (重度 │ │ harness) │ │ ``` │ │ │ │ **核心原则:** │ │ - **Opinionated** — 默认调优为长时间、多步骤任务 │ │ - **Extensible** — 可覆盖或替换任何组件,无需 fork │ │ - **Model-agnostic** — 任何支持 tool calling 的模型均可使用 │ │ - **Production-ready** — 内置 streaming、persistence、checkpointing,可与 │ │ LangSmith 深度集成 │ │ │ │ > 📦 **安装:** `pip install deepagents` 或 `uv add deepagents` │ │ │ │ --- │ │ │ │ ## 二、使用场景选择(什么时候该用 DeepAgents) │ │ │ │ 根据社区和官方文档的一致共识: │ │ │ │ | 场景 | 推荐方案 | │ │ |---|---| │ │ | 多步骤任务需要规划 | ✅ **DeepAgents** | │ │ | 超大上下文需要文件管理 | ✅ **DeepAgents** | │ │ | 需要专业子代理分工 | ✅ **DeepAgents** | │ │ | 跨会话持久记忆 | ✅ **DeepAgents** | │ │ | 简单单次工具调用 | ❌ `create_agent` 即可 | │ │ | 上下文可放入单次 prompt | ❌ `create_agent` 即可 | │ │ | 需要完全自定义图结构 | ❌ 直接用 LangGraph | │ │ │ │ > 💡 **社区建议:** "If starting in 2026, use deepagents (built on │ │ LangGraph) and transition to raw LangGraph for finer control." │ │ │ │ --- │ │ │ │ ## 三、中间件 (Middleware) 最佳实践 │ │ │ │ ### 3.1 默认中间件栈及顺序 │ │ │ │ ``` │ │ TodoListMiddleware → SkillsMiddleware → FilesystemMiddleware │ │ → SubAgentMiddleware → SummarizationMiddleware │ │ ``` │ │ │ │ 顺序很重要!中间件按顺序执行,这是社区反复强调的。 │ │ │ │ ### 3.2 覆盖中间件 —— 社区核心讨论 │ │ │ │ 来自 [GitHub Discussion │ │ #655](https://github.com/langchain-ai/deepagents/discussions/655) │ │ 的激烈讨论: │ │ │ │ **关键机制:** 通过 `.name` │ │ 匹配来替换默认中间件实例,而非追加。即同名中间件会被你的自定义版本**替换** │ │ 而非合并。 │ │ │ │ ```python │ │ agent = create_deep_agent( │ │ model="claude-sonnet-4-5-20250929", │ │ middleware=[ │ │ SummarizationMiddleware(trigger=("tokens", 80000), │ │ keep=("messages", 20)), │ │ ], │ │ ) │ │ ``` │ │ │ │ **⚠️ 社区踩坑:** │ │ - 覆盖 `FilesystemMiddleware` 时必须手动传入 `backend` 和 `permissions` │ │ - `SubAgentMiddleware` 是**承重中间件**(load-bearing),不可移除 │ │ - `FilesystemMiddleware` 同样被多处依赖,移除会触发 `ValueError` │ │ - 通用子代理**默认不继承**主代理的自定义中间件([Issue │ │ #2744](https://github.com/langchain-ai/deepagents/issues/2744)) │ │ │ │ **排除不需要的中间件(社区贡献方案):** │ │ ```python │ │ HarnessProfile( │ │ excluded_middleware=frozenset({ │ │ "TodoListMiddleware", │ │ "FilesystemMiddleware", │ │ "SummarizationMiddleware", │ │ }), │ │ ) │ │ ``` │ │ │ │ > 引用 Reddit 用户评论:「我尝试基于 deepagents │ │ 构建,但发现它相当受限,开源 LLM 对一些工具有困难。最后回归到常规 │ │ agent,只使用 deepagents 包中的部分中间件。」 │ │ │ │ --- │ │ │ │ ## 四、系统提示词与指令设计 │ │ │ │ ### 4.1 最佳实践 │ │ │ │ 来自多个博客和官方文档的一致建议: │ │ │ │ ```python │ │ # ❌ 不好的做法 │ │ system_prompt = "You are a helpful assistant." │ │ │ │ # ✅ 好的做法 │ │ system_prompt = """ │ │ You are a research analyst. When given a topic: │ │ 1. Use write_todos to plan your approach │ │ 2. Delegate research to the general-purpose subagent via task() │ │ 3. Read findings from the filesystem │ │ 4. Synthesize into a structured report with clear sections │ │ 5. Save the final report to /reports/{topic}.md │ │ """ │ │ ``` │ │ │ │ - **具体胜过笼统**:告诉 agent 期望的输出格式、质量标准、操作步骤 │ │ - **自定义指令会被前置**到内置系统提示词之前 │ │ - **不超过必要长度**:冗长的指令会挤占上下文窗口 │ │ │ │ --- │ │ │ │ ## 五、工具 (Tools) 设计最佳实践 │ │ │ │ ### 5.1 文档字符串与类型提示 │ │ │ │ 这是社区最一致的建议之一: │ │ │ │ ```python │ │ # ❌ 不好的工具设计 │ │ @tool │ │ def search(q: str) -> str: │ │ """search something""" │ │ return ... │ │ │ │ # ✅ 好的工具设计 │ │ @tool │ │ def web_search(query: str) -> str: │ │ """Search the web for information on a given topic. │ │ │ │ Use this tool when you need up-to-date information or │ │ facts that may not be in your training data. │ │ │ │ Args: │ │ query: A specific, detailed search query string. │ │ Use natural language and be as precise as possible. │ │ │ │ Returns: │ │ Search results with titles, URLs, and snippets. │ │ """ │ │ return ... │ │ ``` │ │ │ │ **核心原则:** │ │ - LLM 通过 docstring 和参数描述决定何时/如何使用工具 —— **清晰文档 = │ │ 更好工具调用** │ │ - 使用 `Literal` 类型、清晰的参数名(`query: str` 而非 `q: str`) │ │ - 设计用于**深入研究**而非简单回答的工具 │ │ │ │ ### 5.2 安全模型 │ │ │ │ > 来自 Flowtivity 博客:「Deep Agents 遵循 "trust the tools, not the model" │ │ 哲学。Agent │ │ 可以做其工具允许的任何事情。你在工具层面而非期望模型自我约束来实施边界。」 │ │ │ │ - ✅ 对敏感操作添加 `HumanInTheLoopMiddleware` │ │ - ✅ 在工具级别做沙箱限制 │ │ - ❌ 不要依赖 prompt 层面的护栏(脆弱且不可靠) │ │ │ │ --- │ │ │ │ ## 六、Skills(技能)最佳实践 │ │ │ │ ### 6.1 渐进式能力暴露 │ │ │ │ Skills 是 DeepAgents 独有的模式:agent 启动时只看到 skill │ │ 的名称和描述(frontmatter),仅在需要时才加载完整内容。 │ │ │ │ ``` │ │ .deepagents/skills/ │ │ ├── research/SKILL.md │ │ ├── deploy/SKILL.md │ │ └── review-pr/SKILL.md │ │ ``` │ │ │ │ **社区推荐:** │ │ │ │ | 建议 | 原因 | │ │ |---|---| │ │ | frontmatter 保持简洁 | 所有 frontmatter 在发现阶段注入系统提示词 | │ │ | `SKILL.md` body 控制在 5000 token 以内 | body 只在激活时加载 | │ │ | `description` 要具体且包含触发词 | 这是 agent 选择 skill 的唯一依据 | │ │ | 每个子代理配置独立的 skills | 通用子代理会自动继承;自定义子代理不会 | │ │ │ │ **description 示例:** │ │ ``` │ │ ✅ "Use when the user asks to research a topic, find information, │ │ or gather data from external sources. Handles web search, │ │ source verification, and structured note-taking." │ │ │ │ ❌ "Does research" │ │ ``` │ │ │ │ ### 6.2 Skills 创建流程(社区提炼) │ │ │ │ 1. **Execute Task** — 手动让 agent 执行一次任务 │ │ 2. **Reflect** — 让 agent 分析自己的执行轨迹 │ │ 3. **Create Skill** — 用 `skill-creator` 固化流程为 skill │ │ │ │ > 来自 Reddit 用户:「我建议深入研究 skills.md,将每个子代理的 skills │ │ 分开配置。这看起来很有前景,正在取得更好的结果,但通过 skills.md │ │ 是一个逐步的迭代过程。」 │ │ │ │ --- │ │ │ │ ## 七、子代理 (Sub-Agents) 最佳实践 │ │ │ │ ### 7.1 设计原则 │ │ │ │ 来自 LangChain 官方博客 "Building Multi-Agent Applications with Deep │ │ Agents": │ │ │ │ **描述质量决定委托质量:** │ │ │ │ ```python │ │ # ✅ 好的子代理描述 │ │ subagents=[ │ │ { │ │ "name": "financial-analyst", │ │ "description": "Analyzes financial data and generates investment │ │ insights │ │ with confidence scores and risk assessments", │ │ "system_prompt": "...", │ │ "tools": , │ │ }, │ │ ] │ │ │ │ # ❌ 不好的描述 │ │ subagents=[{"name": "finance", "description": "Does finance stuff"}] │ │ ``` │ │ │ │ ### 7.2 分工模式 │ │ │ │ 社区验证的有效模式: │ │ │ │ ``` │ │ Main Agent (Planner + Orchestrator) │ │ ├── General-purpose Subagent (research, web search — 自动添加) │ │ ├── Custom Subagent A (专业领域 X) │ │ └── Custom Subagent B (专业领域 Y) │ │ ``` │ │ │ │ > Reddit 用户实践:「我在构建生产级 deepagents,子代理工具通过 FastMCP │ │ Gateway 调用。灵活性方面建议深入研究 skills.md。」 │ │ │ │ **关键决策:** │ │ - 子代理不应承担不必要的中间件负担(GitHub #655 的讨论核心) │ │ - 通用子代理自动继承主代理的 skills;自定义子代理需要显式配置 │ │ - 让子代理做单一职责的事情,避免"大而全" │ │ │ │ --- │ │ │ │ ## 八、上下文与记忆管理 │ │ │ │ ### 8.1 文件系统后端选择 │ │ │ │ | 后端 | 用途 | 场景 | │ │ |---|---|---| │ │ | `StateBackend`(默认) | 单线程内临时文件 | 中间结果、草稿 | │ │ | `StoreBackend` | 跨线程/跨会话持久化 | 长期记忆、用户偏好 | │ │ | `FilesystemBackend` | 直接磁盘访问 | CLI 本地开发 | │ │ | `CompositeBackend` | 混合路由 | `/memories/` → Store, 其余 → State | │ │ │ │ **社区推荐的多租户记忆模式:** │ │ │ │ ```python │ │ CompositeBackend( │ │ default=StateBackend(), │ │ routes={"/memories/": StoreBackend(store=store)}, │ │ ) │ │ ``` │ │ │ │ Namespace 模式:按 `user_id` 隔离,每个用户拥有私有记忆空间。 │ │ │ │ ### 8.2 上下文窗口管理 │ │ │ │ > "Context management remains a first-class engineering concern regardless │ │ of window size, because attention quality degrades long before the hard │ │ limit is reached." — abvijaykumar.medium.com │ │ │ │ **社区的实战教训:** │ │ - 自动上下文压缩 (`SummarizationMiddleware`) │ │ 的触发阈值和保留量需要根据任务调试 │ │ - 大型搜索结果自动卸载到文件系统,agent 通过 `read_file` 按需读取 │ │ - 使用 `thread_id` 保持跨调用的会话连续性 │ │ │ │ --- │ │ │ │ ## 九、评估与测试 │ │ │ │ ### 9.1 LangChain 官方五大评估模式 │ │ │ │ 来自 [LangChain 博客 "Evaluating Deep Agents: Our │ │ Learnings"](https://www.langchain.com/blog/evaluating-deep-agents-our-learn │ │ ings): │ │ │ │ | 模式 | 说明 | │ │ |---|---| │ │ | **1. Bespoke test logic per datapoint** | │ │ 每个测试用例有独立的断言逻辑,检查 agent 轨迹和状态 | │ │ | **2. Single-step evaluations** | 验证特定决策点(如是否正确调用了 │ │ `edit_file`) | │ │ | **3. Full agent turn testing** | 端到端测试完整行为 | │ │ | **4. Multi-turn conversations** | 模拟带条件逻辑的真实多轮交互 | │ │ | **5. Environment setup** | 干净、可复现的测试环境 | │ │ │ │ **核心洞察:** Deep Agents 打破了传统 LLM 评估的假设 —— │ │ 不是每个测试用例都能用相同的应用逻辑运行和相同的评估器打分。 │ │ │ │ ### 9.2 实际应用示例 │ │ │ │ ```python │ │ @pytest.mark.langsmith │ │ def test_memory_update(): │ │ result = agent.invoke({"messages": [{"role": "user", "content": │ │ "..."}]}) │ │ │ │ # 检查轨迹中的特定工具调用 │ │ assert any(call["name"] == "edit_file" and "/memories/" in │ │ call["args"]["path"] │ │ for call in result["tool_calls"]) │ │ │ │ # 检查最终回复包含记忆更新确认 │ │ assert "updated" in result["messages"][-1].content.lower() │ │ ``` │ │ │ │ --- │ │ │ │ ## 十、生产部署 │ │ │ │ ### 10.1 PII 与安全 │ │ │ │ ```python │ │ from langchain.agents.middleware import PIIMiddleware │ │ │ │ agent = create_deep_agent( │ │ model="google_genai:gemini-3.5-flash", │ │ middleware=[ │ │ PIIMiddleware("email", strategy="redact", apply_to_input=True), │ │ PIIMiddleware("credit_card", strategy="mask", apply_to_input=True), │ │ ], │ │ ) │ │ ``` │ │ │ │ 策略选项:`redact`、`mask`、`hash`、`block` │ │ │ │ ### 10.2 人机协作 (Human-in-the-Loop) │ │ │ │ ```python │ │ agent = create_deep_agent( │ │ interrupt_on={"write_file": True, "execute": True}, │ │ checkpointer=MemorySaver(), # 必须!interrupt 需要 checkpointer │ │ ) │ │ ``` │ │ │ │ ### 10.3 模型选择 │ │ │ │ | 阶段 | 推荐 | │ │ |---|---| │ │ | 原型开发 | Groq 等快速模型 | │ │ | 生产环境 | GPT-4o、GPT-5、Claude Sonnet 等强模型 | │ │ │ │ > "Deep agents make many sequential decisions. More capable models │ │ generally plan and execute better than smaller models." │ │ │ │ --- │ │ │ │ ## 十一、常见坑与社区经验教训 │ │ │ │ ### 11.1 社区最常踩的坑 │ │ │ │ 1. **忘了传 `checkpointer`** — 使用 `interrupt_on` 时必须配合 │ │ checkpointer,否则报错 │ │ 2. **覆盖中间件丢失配置** — 覆盖 `FilesystemMiddleware` 时忘记传入 │ │ `backend` 和 `permissions` │ │ 3. **子代理继承问题** — 自定义子代理不会自动继承主代理的 middleware 和 │ │ skills │ │ 4. **开源模型兼容性** — 部分开源 LLM 对某些 built-in 工具有困难 │ │ 5. **跳过 `write_todos`** — 很多开发者初期跳过规划步骤,导致混乱执行 │ │ 6. **工具描述过于简略** — "Does finance stuff" 式的描述导致 agent │ │ 无法正确路由 │ │ 7. **线程 ID 不一致** — 每次调用使用不同 `thread_id` 导致记忆丢失 │ │ │ │ ### 11.2 Reddit 社区的真实反馈 │ │ │ │ > 👍 "Tracing/visibility as a first class concern is really nice — I can │ │ see what's going on and fix/tweak the agent very easily." │ │ │ │ > 👍 "The abstraction handles context window shrinking and provides easy │ │ ways to customize agents. It's been successful bringing people not so │ │ familiar with LangGraph into building backend agents." │ │ │ │ > 👎 "I found it really restrictive, and open source LLMs had trouble with │ │ some of the tools." │ │ │ │ > 💡 "If you want more customizability, also check out OpenHarness." │ │ │ │ --- │ │ │ │ ## 十二、快速上手清单 │ │ │ │ 按照社区推荐顺序: │ │ │ │ 1. **从简单开始** — 一个工具 + 简单 prompt,跑通后再加复杂度 │ │ 2. **用 `write_todos`** — 显式规划比让 agent "自己想办法" 效果好得多 │ │ 3. **写好工具 docstring** — LLM 通过它们决定调用策略 │ │ 4. **启用 LangSmith** — 没有可观测性,调试几乎不可能 │ │ 5. **选对模型** — 原型用快速模型,生产用强模型 │ │ 6. **迭代 skills.md** — 逐步将成功的工作流程固化为 skills │ │ 7. **设计专用子代理** — 一个研究、一个审查、一个执行,分离关注点 │ │ 8. **配置记忆后端** — 短期用 StateBackend,长期用 StoreBackend,混合用 │ │ CompositeBackend │ │ │ │ --- │ │ │ │ ## 参考来源 │ │ │ │ - [DeepAgents GitHub](https://github.com/langchain-ai/deepagents) │ │ - [官方文档 - │ │ Customization](https://docs.langchain.com/oss/python/deepagents/customizati │ │ on) │ │ - [官方文档 - │ │ Skills](https://docs.langchain.com/oss/python/deepagents/skills) │ │ - [官方文档 - Going to │ │ Production](https://docs.langchain.com/oss/python/deepagents/going-to-produ │ │ ction) │ │ - [LangChain Blog - Evaluating Deep │ │ Agents](https://www.langchain.com/blog/evaluating-deep-agents-our-learnings │ │ ) │ │ - [LangChain Blog - Building Multi-Agent │ │ Applications](https://www.langchain.com/blog/building-multi-agent-applicati │ │ ons-with-deep-agents) │ │ - [Reddit r/LangChain - Anyone building on top of │ │ DeepAgents?](https://www.reddit.com/r/LangChain/comments/1s8t4tv/) │ │ - [GitHub Discussions #655 - Overriding │ │ Middleware](https://github.com/langchain-ai/deepagents/discussions/655) │ │ - [GitHub Issue #2744 - Subagent Middleware │ │ Inheritance](https://github.com/langchain-ai/deepagents/issues/2744) │ │ - [Krish Naik - Building Deep Agents with │ │ LangChain](https://krishcnaik.substack.com/p/building-deep-agents-with-lang │ │ chain) │ │ - [Flowtivity - Deep Agents Framework │ │ Review](https://flowtivity.ai/blog/langchain-deep-agents-framework-review) │ │ - [Analytics Vidhya - Deep Agents │ │ Tutorial](https://www.analyticsvidhya.com/blog/2025/11/langchains-deep-agen │ │ t-guide) │ │ - [ZenML - Evaluation Patterns for Deep │ │ Agents](https://www.zenml.io/llmops-database/evaluation-patterns-for-deep-a │ │ gents-in-production) │ │ - [DEV.to - Building Advanced AI Agents with │ │ DeepAgents](https://dev.to/samadhi_patil_294a4ff7fea/building-advanced-ai-a │ │ gents-with-langchains-deepagents-a-hands-on-guide-1bk4) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────── Sub-Agent 完成任务 (Node: tools) ──────────────────────┐ │ --- │ │ │ │ # LangChain DeepAgents 官方文档与架构调研报告 │ │ │ │ ## 1. 概述 │ │ │ │ **DeepAgents** 是 LangChain 生态中的一个开源 Agent │ │ 框架,专门用于构建**处理复杂、长时间运行任务**的智能体。它基于 │ │ **LangGraph** 图编排引擎构建,提供了一套"电池包含"(batteries-included)的 │ │ Agent 脚手架。 │ │ │ │ > 官方定义:*"Deep Agents is a framework for building agents designed for │ │ complex, long-running tasks. It includes features like planning, subagents, │ │ a virtual filesystem, and long-term memory."* │ │ │ │ ### 核心仓库 │ │ │ │ | 仓库 | 说明 | │ │ |------|------| │ │ | `deepagents` (Python 包) | 核心框架,安装后导入使用 | │ │ | `langchain-ai/deepagents-quickstarts` | 官方示例集(如 Deep Research │ │ Agent) | │ │ | `shkarupa-alex/deepagents-opensandbox` | 沙箱执行后端(社区) | │ │ │ │ --- │ │ │ │ ## 2. 核心架构 │ │ │ │ DeepAgents 的架构围绕以下几个核心概念构建: │ │ │ │ ``` │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ create_deep_agent() │ │ │ │ (主入口函数) │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ │ │ Model │ │ Tools │ │ System Prompt │ │ │ │ │ └──────────┘ └──────────┘ └──────────────────┘ │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ Middleware 层 │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ │ │ Filesystem │ │ SubAgent │ │ Memory │ │ │ │ │ │ Middleware │ │ Middleware │ │ Middleware │ │ │ │ │ ├──────────────┤ ├──────────────┤ ├─────────────┤ │ │ │ │ │ Summarization│ │ Planning │ │ AsyncSubAg..│ │ │ │ │ │ Middleware │ │ (via LangCh) │ │ Middleware │ │ │ │ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ Backend 层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ │ │ │ │ State │ │ Store │ │ CompositeBackend │ │ │ │ │ │ Backend │ │ Backend │ │ (路由混合) │ │ │ │ │ ├──────────┤ ├──────────┤ ├────────────────────┤ │ │ │ │ │ Filesystem│ │ Sandbox │ │ ProtocolBackend │ │ │ │ │ │ Backend │ │ Backend │ │ (自定义实现) │ │ │ │ │ └──────────┘ └──────────┘ └────────────────────┘ │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ LangGraph StateGraph │ │ │ │ (编译为 CompiledStateGraph 运行) │ │ │ └─────────────────────────────────────────────────────┘ │ │ ``` │ │ │ │ ### 2.1 主入口:`create_deep_agent()` │ │ │ │ 这是构建 DeepAgent 的核心工厂函数,返回一个 `CompiledStateGraph`(LangGraph │ │ 编译后的图)。 │ │ │ │ ```python │ │ from deepagents import create_deep_agent │ │ │ │ agent = create_deep_agent( │ │ model: str | BaseChatModel | None = None, # LLM 模型 │ │ tools: Sequence[BaseTool | Callable | dict] = None, # 工具列表 │ │ *, │ │ system_prompt: str | SystemMessage | None = None, # 系统提示词 │ │ middleware: Sequence[AgentMiddleware] = (), # 中间件列表 │ │ subagents: Sequence[SubAgent | CompiledSubAgent] = None, # 子 Agent │ │ skills: list | None = None, # 技能 │ │ memory: list | None = None, # 记忆配置 │ │ permissions: list[FilesystemPermission] | None = None, # 文件权限 │ │ backend: BackendProtocol | BackendFactory | None = None, # 后端存储 │ │ interrupt_on: dict = None, # 人工介入 │ │ response_format: ResponseFormat | None = None, # 输出格式 │ │ state_schema: type[DeepAgentState] | None = None, # 自定义状态 │ │ context_schema: type[ContextT] | None = None, # 上下文Schema │ │ checkpointer: Checkpointer | None = None, # 检查点(持久化) │ │ store: BaseStore | None = None, # 长期存储 │ │ debug: bool = False, │ │ name: str | None = None, │ │ cache: BaseCache | None = None, │ │ ) -> CompiledStateGraph │ │ ``` │ │ │ │ --- │ │ │ │ ## 3. Middleware(中间件)体系 │ │ │ │ Middleware 是 DeepAgents 最核心的扩展机制。每种 Middleware 都实现了 │ │ `AgentMiddleware` 基类,可以 hook 到 Agent 生命周期的各个阶段: │ │ │ │ | Hook 方法 | 触发时机 | │ │ |-----------|----------| │ │ | `before_agent()` / `abefore_agent()` | Agent 开始执行前 | │ │ | `before_model()` / `abefore_model()` | 每次模型调用前 | │ │ | `after_model()` / `aafter_model()` | 每次模型调用后 | │ │ | `wrap_model_call()` / `awrap_model_call()` | 拦截/包装模型调用 | │ │ | `wrap_tool_call()` / `awrap_tool_call()` | 拦截/包装工具调用 | │ │ | `after_agent()` / `aafter_agent()` | Agent 执行结束后 | │ │ │ │ ### 3.1 DeepAgents 内置 Middleware │ │ │ │ | Middleware | 功能 | │ │ |------------|------| │ │ | **FilesystemMiddleware** | 提供虚拟文件系统工具:`ls`, `read_file`, │ │ `write_file`, `edit_file`, `glob`, `grep`, `execute` | │ │ | **SubAgentMiddleware** | 子 Agent 委托管理——主 Agent 可将任务委派给子 │ │ Agent 并行执行 | │ │ | **AsyncSubAgentMiddleware** | 支持远程 LangGraph 部署作为子 Agent(通过 │ │ HTTP/ASGI) | │ │ | **MemoryMiddleware** | 长期记忆——从 `~/.deepagents/AGENTS.md` │ │ 等文件加载/保存知识 | │ │ | **SummarizationMiddleware** | 自动摘要——当上下文超过阈值(如 token 数达 │ │ 85%)时自动压缩对话 | │ │ | **SummarizationToolMiddleware** | 提供手动触发摘要的工具 | │ │ | **Planning Middleware** (via LangChain) | 规划与任务管理中间件 | │ │ | **ToolRetryMiddleware** (via LangChain) | 工具调用失败自动重试 | │ │ │ │ ### 3.2 FilesystemMiddleware 详解 │ │ │ │ 文件系统中件间提供了完整的虚拟文件系统,Agent 可以像在真实 OS │ │ 中一样操作文件: │ │ │ │ ```python │ │ # 工具列表 │ │ tools = [ │ │ self._create_ls_tool(), # 列出目录 │ │ self._create_read_file_tool(), # 读取文件 │ │ self._create_write_file_tool(),# 写入文件 │ │ self._create_edit_file_tool(), # 编辑文件 │ │ self._create_glob_tool(), # 通配符查找 │ │ self._create_grep_tool(), # 内容搜索 │ │ self._create_execute_tool(), # 执行命令 (需沙箱) │ │ ] │ │ ``` │ │ │ │ --- │ │ │ │ ## 4. Backend(后端存储)体系 │ │ │ │ Backend │ │ 负责文件存储和状态持久化。选择取决于对**持久化**和**隔离性**的需求。 │ │ │ │ ### 4.1 内置 Backend 类型 │ │ │ │ | Backend | 说明 | │ │ |---------|------| │ │ | **StateBackend** | 临时/内存存储(默认,不持久化) | │ │ | **StoreBackend** | 基于 LangGraph Store 的持久化存储 | │ │ | **FilesystemBackend** | 真实文件系统存储(需 sandbox 或 HIL 批准) | │ │ | **CompositeBackend** | 路由混合后端——不同路径映射到不同后端 | │ │ | **自定义 Backend** | 实现 `BackendProtocol` 接口 | │ │ │ │ ### 4.2 CompositeBackend 示例 │ │ │ │ ```python │ │ from deepagents.backends import StateBackend, StoreBackend, │ │ CompositeBackend │ │ │ │ backend = CompositeBackend( │ │ default=StateBackend(), # 默认临时存储 │ │ routes={ │ │ "/memories/": StoreBackend(), # /memories/ 路径持久化 │ │ } │ │ ) │ │ ``` │ │ │ │ --- │ │ │ │ ## 5. SubAgent(子 Agent)体系 │ │ │ │ DeepAgents 支持**层级委托**——主 Agent 可以将子任务分派给专门的子 Agent。 │ │ │ │ ### 5.1 子 Agent 配置 │ │ │ │ ```python │ │ research_sub_agent = { │ │ "name": "research-agent", │ │ "description": "Delegate research to the sub-agent researcher.", │ │ "system_prompt": RESEARCHER_INSTRUCTIONS, │ │ "model": "openai:gpt-5.5", # 可独立指定模型 │ │ "tools": , # 独立工具集 │ │ } │ │ │ │ agent = create_deep_agent( │ │ model=model, │ │ tools=[...], │ │ system_prompt=INSTRUCTIONS, │ │ subagents=, # 注入子 Agent │ │ ) │ │ ``` │ │ │ │ ### 5.2 两种子 Agent 类型 │ │ │ │ | 类型 | 说明 | │ │ |------|------| │ │ | **SubAgent** (本地) | 在同一进程中编译运行,共享状态 Schema | │ │ | **AsyncSubAgent** (远程) | 通过 HTTP 调用远程 LangGraph 部署,支持 │ │ LangSmith 部署 | │ │ │ │ ```python │ │ # 远程子 Agent │ │ from deepagents.middleware.async_subagents import AsyncSubAgentMiddleware │ │ │ │ middleware = AsyncSubAgentMiddleware( │ │ async_subagents=[{ │ │ "name": "researcher", │ │ "description": "Remote research agent", │ │ "url": "https://my-deployment.langsmith.dev", │ │ "graph_id": "research_agent", │ │ }] │ │ ) │ │ ``` │ │ │ │ ### 5.3 并行执行 │ │ │ │ DeepAgents 支持**并行执行**多个子 Agent——主 Agent 可在单次响应中发起多个 │ │ `task` 调用,子 Agent 独立返回结果。 │ │ │ │ --- │ │ │ │ ## 6. 状态管理 │ │ │ │ ### 6.1 DeepAgentState │ │ │ │ DeepAgents 使用 `DeepAgentState`(TypedDict)作为 Agent 的运行时状态: │ │ │ │ ```python │ │ from deepagents.graph import DeepAgentState │ │ │ │ # 自定义状态扩展 │ │ class MyState(DeepAgentState): │ │ page_url: str │ │ file_urls: list │ │ │ │ agent = create_deep_agent(model=..., state_schema=MyState) │ │ ``` │ │ │ │ ### 6.2 持久化 │ │ │ │ - **checkpointer**: LangGraph 的检查点机制,支持对话历史的持久化和恢复 │ │ - **store**: `BaseStore` 实例,用于长期键值存储 │ │ │ │ --- │ │ │ │ ## 7. 部署 │ │ │ │ DeepAgents 作为 LangGraph 应用,通过 **LangGraph CLI** 部署: │ │ │ │ ### 7.1 配置文件 `langgraph.json` │ │ │ │ ```json │ │ { │ │ "dependencies": ["."], │ │ "graphs": { │ │ "research": "./agent.py:agent" │ │ }, │ │ "env": ".env" │ │ } │ │ ``` │ │ │ │ ### 7.2 开发服务器 │ │ │ │ ```bash │ │ langgraph dev # 启动开发服务器 (默认 http://localhost:8123) │ │ langgraph deploy # 部署到生产环境 │ │ ``` │ │ │ │ --- │ │ │ │ ## 8. 完整示例:Deep Research Agent │ │ │ │ ```python │ │ from datetime import datetime │ │ from langchain.chat_models import init_chat_model │ │ from deepagents import create_deep_agent │ │ from research_agent.tools import tavily_search, think_tool │ │ │ │ model = init_chat_model( │ │ model="anthropic:claude-sonnet-4-5-20250929", │ │ temperature=0.0 │ │ ) │ │ │ │ research_sub_agent = { │ │ "name": "research-agent", │ │ "description": "Delegate research to the sub-agent researcher.", │ │ "system_prompt": RESEARCHER_INSTRUCTIONS, │ │ "tools": , │ │ } │ │ │ │ agent = create_deep_agent( │ │ model=model, │ │ tools=, │ │ system_prompt=WORKFLOW_INSTRUCTIONS, │ │ subagents=, │ │ ) │ │ │ │ # 调用 Agent │ │ result = agent.invoke({"messages": [{"role": "user", "content": "Research │ │ AI trends 2024"}]}) │ │ ``` │ │ │ │ --- │ │ │ │ ## 9. 架构总结图 │ │ │ │ ``` │ │ ┌──────────────────┐ │ │ │ User / Client │ │ │ └────────┬─────────┘ │ │ │ │ │ ┌────────▼─────────┐ │ │ │ DeepAgentsApp │ ← TUI/Web 应用层 │ │ │ (可选 UI 包装) │ │ │ └────────┬─────────┘ │ │ │ │ │ ┌──────────────▼──────────────┐ │ │ │ create_deep_agent() │ │ │ │ (CompiledStateGraph) │ │ │ │ │ │ │ │ ┌───────────────────────┐ │ │ │ │ │ LangGraph Agent │ │ │ │ │ │ (ReAct / Tool Call) │ │ │ │ │ └───────────────────────┘ │ │ │ │ │ │ │ │ Middleware Pipeline: │ │ │ │ Filesystem → Memory → │ │ │ │ SubAgents → Summarization │ │ │ │ │ │ │ │ Backend Layer: │ │ │ │ State / Store / FS / │ │ │ │ Composite / Sandbox │ │ │ └─────────────────────────────┘ │ │ │ │ │ ┌────────▼─────────┐ │ │ │ LangGraph │ │ │ │ Platform │ │ │ │ (Checkpointer, │ │ │ │ Store, Server) │ │ │ └──────────────────┘ │ │ ``` │ │ │ │ --- │ │ │ │ ## 10. 关键要点 │ │ │ │ 1. **DeepAgents = LangGraph + Middleware + Backend**:本质是 LangGraph 的 │ │ Agent 预配置方案 │ │ 2. **Middleware │ │ 是架构核心**:所有扩展能力(文件系统、子Agent、记忆、摘要)都通过 │ │ Middleware 注入 │ │ 3. **Backend │ │ 解耦存储**:从临时内存到真实文件系统到远程沙箱,通过统一接口切换 │ │ 4. **子 Agent 支持层级委托和并行执行**:适合复杂多步骤任务 │ │ 5. **与 LangSmith 深度集成**:远程子 Agent 可通过 LangSmith 部署调用 │ │ 6. **部署即标准 LangGraph 应用**:使用 `langgraph dev/deploy` 命令 │ └─────────────────────────────────────────────────────────────────────────────┘ Step 12: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '通过 DocsResearcher 调研 LangChain DeepAgents 官方文档和技术架构', 'status': '... Step 16: 决策与调用 (Node: model) └── 普通工具调用: write_file Tool Output (write_file): Updated file /home/user/DeepAgents_对比报告.md Step 20: 决策与调用 (Node: model) └── 普通工具调用: write_todos Tool Output (write_todos): Updated todo list to [{'content': '通过 DocsResearcher 调研 LangChain DeepAgents 官方文档和技术架构', 'status': '... ┌────────────────────────── Agent回复 (Node: model) ──────────────────────────┐ │ --- │ │ │ │ 调研完成!完整报告已保存到 │ │ `/home/user/DeepAgents_对比报告.md`。以下是核心发现的摘要: │ │ │ │ --- │ │ │ │ ## 📋 调研成果概览 │ │ │ │ 两项独立调研(官方文档 + 社区实践)分别由 DocsResearcher 和 │ │ CommunityResearcher 并行完成,然后进行了交叉对比分析。 │ │ │ │ --- │ │ │ │ ### 🔷 官方架构核心 │ │ │ │ DeepAgents 本质是 **LangGraph + Middleware + Backend** 的预配置方案: │ │ │ │ ``` │ │ create_deep_agent() → Middleware Pipeline → Backend Layer → LangGraph │ │ Runtime │ │ ``` │ │ │ │ - **Middleware** │ │ 是所有扩展能力的核心(Filesystem、SubAgent、Memory、Summarization) │ │ - **Backend** 解耦存储(State → Store → Filesystem → Composite 四层递进) │ │ - **SubAgent** 支持层级委托和并行执行,可与 LangSmith 远程部署集成 │ │ │ │ --- │ │ │ │ ### 🔶 社区实践的关键发现 │ │ │ │ 社区实践在三个层面与官方文档形成了有价值的互补: │ │ │ │ | 维度 | 官方说的 | 社区发现的 | │ │ |------|----------|------------| │ │ | **Middleware 替换** | 可自定义 | │ │ 同名是**替换**而非合并,覆盖时极易丢失依赖参数 | │ │ | **SubAgent 继承** | 通用子 Agent 自动继承 | 自定义子 Agent │ │ **不会**自动继承 middleware/skills(Issue #2744) | │ │ | **承重中间件** | 未特别标注 | `FilesystemMiddleware` 和 │ │ `SubAgentMiddleware` 不可移除(会 ValueError) | │ │ | **模型兼容性** | "任何支持 tool calling 的模型" | 开源 LLM │ │ 对内置工具有困难,生产必须用 Claude/GPT 等强模型 | │ │ | **评估** | 五大评估模式 | 每个测试用例可能需要独立断言逻辑,打破了传统 │ │ LLM 评估假设 | │ │ │ │ --- │ │ │ │ ### 🔴 四大"踩坑高发区"(社区血泪总结) │ │ │ │ 1. **覆盖 Middleware 时忘记传 `backend` 和 `permissions`** → 运行时崩溃 │ │ 2. **使用 `interrupt_on` 却忘记传 `checkpointer`** → 直接报错 │ │ 3. **工具 docstring 过于简略**(如 `"Does finance stuff"`)→ Agent │ │ 无法正确路由 │ │ 4. **每次调用用不同的 `thread_id`** → 记忆完全丢失 │ │ │ │ --- │ │ │ │ ### 🟢 一句话结论 │ │ │ │ > **DeepAgents 是 LangGraph │ │ 之上的"最佳实践固化层"**——官方文档提供清晰的架构蓝图,社区实践揭示蓝图落地 │ │ 时的真实边界条件和生存法则。两者结合使用,才能高效且安全地构建生产级 Agent │ │ 系统。 │ │ │ │ 详细对比表、代码示例、社区原话引用和完整的参考来源请查看报告文件。 │ └─────────────────────────────────────────────────────────────────────────────┘ 运行结束退出。 Process finished with exit code 0
五、文件系统集成 (Filesystem & Sandbox)
组件:FilesystemMiddleware & Backend
功能:这是 DeepAgent 的核心"外挂大脑"。Agent 不再将所有检索到的长文塞入上下文(Context),而是将其写入 Filesystem(如 /workspace),仅在需要时读取。这极大地扩展了 Agent 的"工作记忆"。
核心工具集:
- ls: 浏览目录结构。
- read_file: 读取文件内容。关键封装:支持 offset(偏移量)和 limit(行数),强制 Agent 对大文件进行分页读取,避免一次性读爆上下文。
- write_file: 创建新文件。
- edit_file: 修改文件。支持精确的字符串替换和 replace_all 模式,并有防呆设计(要求先读后改)。
- glob: 通配符模糊查找文件。
- grep: 正则表达式搜索文件内容,像命令行一样在代码库中定位目标。
- execute: 执行 Shell 命令(需 Sandbox 支持),用于运行代码、安装依赖等。
亮点功能:大结果自动转存
- 当 Agent 调用工具产生的结果过长时,系统会自动拦截,将完整结果写入文件系统(如 /large_tool_results/{id}),并只给 Agent 返回一个摘要和文件路径。这完美解决了搜索结果或日志文件过长导致 Agent 崩溃的问题。
后端支持 (Backends):
- FilesystemBackend: 直接操作本地磁盘。
- DockerBackend: 在 Docker 容器中执行,提供隔离环境。
- StoreBackend: 存储在数据库中,支持查询和检索。
- E2BBackend: 使用 E2B 云端沙箱。
- CompositeBackend: 混合模式(如:本地存文件,Docker 跑代码)
5.1.核心工具测试
import os import dotenv from deepagents.backends import FilesystemBackend from langchain.chat_models import init_chat_model from deepagents import create_deep_agent # 1.初始化环境 dotenv.load_dotenv() # 2.创建文件系统 # 使用本地文件系统后端,根目录设为./workspace # 这样我们可以看到真实的文件创建,设置virtual_mode=True表示文件系统是虚拟的,创建的文件不会实际写入磁盘 backend = FilesystemBackend(root_dir="./workspace", virtual_mode=True) # 3.模型初始化 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) # 4.创建agent agent = create_deep_agent( model=deepseek_v4_pro, backend=backend, system_prompt=""" 你是一个文件系统操作助手,请你根据用户指令使用相应的工具。 """ ) # 5.运行并输出结果 def run_filesystem_backend(task_name, instruction): print(f"\n [测试:{task_name}]") print(f"指令:{instruction}") try: # 直接使用agent.invoke调用即可,简单一点 result = agent.invoke({"messages": [{"role": "user", "content": instruction}]}) print(f"🤖AI大模型回复结果:{result['messages'][-1].content}") # 如果有工具调用,则打印工具调用详情 for msg in result["messages"]: if hasattr(msg, "tool_calls") and msg.tool_calls: for tool_call in msg.tool_calls: print(f"🔧工具调用:{tool_call["name"]}, 参数:{tool_call["args"]}") except Exception as e: print(f"❌️ 错误:{e}") # 1. write_file测试 # run_filesystem_backend("write_file", "请创建一个文件test.txt,并写入内容:Hello, World!") # 输出: [测试:write_file] # 指令:请创建一个文件test.txt,并写入内容:Hello, World! # 🤖AI大模型回复结果:文件 `test.txt` 已创建成功,内容为 **Hello, World!**。 # 2.ls测试 # run_filesystem_backend("ls", "请列出当前目录下的所有文件") # 输出: [测试:ls] # 指令:请列出当前目录下的所有文件 # 🤖AI大模型回复结果:当前目录(根目录 `/`)下只有一个文件: # # - **`\test.txt`** # # 以上就是目前目录中的所有文件。如果你想查看该文件的内容或进行其他操作,请随时告诉我! # 🔧工具调用:ls, 参数:{'path': '/'} # 3.read_file测试 # run_filesystem_backend("read_file", "请读取文件test.txt的内容") # 输出: [测试:read_file] # 指令:请读取文件test.txt的内容 # 🤖AI大模型回复结果:文件 `test.txt` 的内容如下: # # ``` # Hello, World! # ``` # # 这是一个简单的文本文件,位于根目录 `/test.txt`,只有一行内容 "Hello, World!"。 # 🔧工具调用:glob, 参数:{'pattern': '**/test.txt'} # 🔧工具调用:read_file, 参数:{'file_path': '/test.txt'} # 4.edit_file测试 # run_filesystem_backend("edit_file", "请修改文件test.txt的内容为:Hello, DeepSeek!") # 输出: [测试:edit_file] # 指令:请修改文件test.txt的内容为:Hello, DeepSeek! # 🤖AI大模型回复结果:已完成修改!`test.txt` 文件的内容已从 "Hello, World!" 更新为 "Hello, DeepSeek!"。 # 🔧工具调用:read_file, 参数:{'file_path': '/test.txt'} # 🔧工具调用:edit_file, 参数:{'file_path': '/test.txt', 'old_string': 'Hello, World!', 'new_string': 'Hello, DeepSeek!'} # 5.grep测试 # run_filesystem_backend("grep", "请在当前目录下搜索包含DeepSeek的文件") # 输出: [测试:grep] # 指令:请在当前目录下搜索包含DeepSeek的文件 # 🤖AI大模型回复结果:搜索完成!在当前目录下找到了 **1 个文件**包含 "DeepSeek": # # | 文件 | # |------| # | `/test.txt` | # # 需要我查看该文件的内容吗? # 🔧工具调用:grep, 参数:{'pattern': 'DeepSeek', 'path': '/', 'output_mode': 'files_with_matches'} # 6.glob测试 # run_filesystem_backend("glob", "请列出当前目录下所有的.txt文件") # 输出: [测试:glob] # 指令:请列出当前目录下所有的.txt文件 # 🤖AI大模型回复结果:当前目录下共找到 **1 个 `.txt` 文件**: # # - `/test.txt` # 🔧工具调用:glob, 参数:{'pattern': '*.txt', 'path': '/'} # 7.execute测试 # run_filesystem_backend("execute", "请执行命令:ls -l") # 输出: [测试:execute] # 指令:请执行命令:ls -l # 🤖AI大模型回复结果:根目录 `/` 下当前有一个文件: # # | 文件路径 | # |----------| # | `/test.txt` | # # > 注意:当前 `ls` 工具不支持 `-l` 长格式参数,因此无法显示权限、大小、时间戳等详细信息。如需查看更多文件信息,可以告诉我具体路径,我可以帮你读取文件内容。 # 🔧工具调用:ls, 参数:{'path': '/'}
5.2.Backend 后端应用
1.默认模式(内存沙箱)
- 内存沙箱,支持在内存中执行代码,防止代码注入攻击。代码执行结果会被存储在内存中,不会对本地环境造成影响。执行结束后,内存中的数据会被清除。
import os import dotenv from deepagents.backends import FilesystemBackend from langchain.chat_models import init_chat_model from deepagents import create_deep_agent # 1.初始化环境 dotenv.load_dotenv() # 2.创建文件系统 # 使用本地文件系统后端,根目录设为./workspace # 这样我们可以看到真实的文件创建,设置virtual_mode=True表示文件系统是虚拟的,创建的文件不会实际写入磁盘 # backend = FilesystemBackend(root_dir="./workspace", virtual_mode=True) # 3.模型初始化 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) # 4.创建agent agent = create_deep_agent( model=deepseek_v4_pro, # backend=backend, # 这里注释掉,用于模拟不传 system_prompt=""" 你是一个文件系统操作助手,请你根据用户指令使用相应的工具。 """ ) # 5.运行并输出结果 def run_filesystem_backend(task_name, instruction): print(f"\n [测试:{task_name}]") print(f"指令:{instruction}") try: # 直接使用agent.invoke调用即可,简单一点 result = agent.invoke({"messages": [{"role": "user", "content": instruction}]}) print(f"🤖AI大模型回复结果:{result['messages'][-1].content}") # 如果有工具调用,则打印工具调用详情 for msg in result["messages"]: if hasattr(msg, "tool_calls") and msg.tool_calls: for tool_call in msg.tool_calls: print(f"🔧工具调用:{tool_call["name"]}, 参数:{tool_call["args"]}") except Exception as e: print(f"❌️ 错误:{e}") # 1. write_file测试 # run_filesystem_backend("write_file", "请创建一个文件test.txt,并写入内容:Hello, World!") # 输出: [测试:write_file] # 指令:请创建一个文件test.txt,并写入内容:Hello, World! # 🤖AI大模型回复结果:文件 `test.txt` 已创建成功,内容为 **Hello, World!**。 # 🔧工具调用:write_file, 参数:{'file_path': '/test.txt', 'content': 'Hello, World!'} # 2.read_file读取 # run_filesystem_backend("read_file", "请读取文件test.txt的内容") # 输出: [测试:read_file] # 指令:请读取文件test.txt的内容 # 🤖AI大模型回复结果:文件 `test.txt` 在当前文件系统中不存在。我尝试了以下方式查找: # # - 使用 glob 搜索 `**/test.txt` — 未找到任何匹配文件 # - 检查根目录 `/` — 目录为空 # - 直接读取 `/test.txt` — 文件不存在 # # 请问您能否提供文件的完整路径,或者确认该文件是否已经被创建?如果需要,我也可以帮您创建一个新的 `test.txt` 文件。 # 🔧工具调用:glob, 参数:{'pattern': '**/test.txt'} # 🔧工具调用:ls, 参数:{'path': '/'} # 🔧工具调用:read_file, 参数:{'file_path': '/test.txt'}
2.持久化模式(操作真实文件)
import os import dotenv from deepagents.backends import FilesystemBackend from langchain.chat_models import init_chat_model from deepagents import create_deep_agent # 1.初始化环境 dotenv.load_dotenv() # 2.创建文件系统 # 使用本地文件系统后端,根目录设为./workspace # 这样我们可以看到真实的文件创建,设置virtual_mode=True表示文件系统是虚拟的,创建的文件不会实际写入磁盘 backend = FilesystemBackend(root_dir="./workspace", virtual_mode=True) # 3.模型初始化 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) # 4.创建agent agent = create_deep_agent( model=deepseek_v4_pro, backend=backend, # 指定FilesystemBackend利用本地磁盘持久化存储 system_prompt=""" 你是一个文件系统操作助手,请你根据用户指令使用相应的工具。 """ ) # 5.运行并输出结果 def run_filesystem_backend(task_name, instruction): print(f"\n [测试:{task_name}]") print(f"指令:{instruction}") try: # 直接使用agent.invoke调用即可,简单一点 result = agent.invoke({"messages": [{"role": "user", "content": instruction}]}) print(f"🤖AI大模型回复结果:{result['messages'][-1].content}") # 如果有工具调用,则打印工具调用详情 for msg in result["messages"]: if hasattr(msg, "tool_calls") and msg.tool_calls: for tool_call in msg.tool_calls: print(f"🔧工具调用:{tool_call["name"]}, 参数:{tool_call["args"]}") except Exception as e: print(f"❌️ 错误:{e}") # 1. write_file测试 # run_filesystem_backend("write_file", "请创建一个文件test.txt,并写入内容:Hello, World!") # 输出: [测试:write_file] # 指令:请创建一个文件test.txt,并写入内容:Hello, World! # 🤖AI大模型回复结果:文件 `/test.txt` 已创建成功,内容为 `Hello, World!`。 # 🔧工具调用:write_file, 参数:{'file_path': '/test.txt', 'content': 'Hello, World!'} # 2.read_file读取 # run_filesystem_backend("read_file", "请读取文件test.txt的内容") # 输出: [测试:read_file] # 指令:请读取文件test.txt的内容 # 🤖AI大模型回复结果:文件 `test.txt` 的内容是: # # ``` # Hello, World! # ``` # 🔧工具调用:glob, 参数:{'pattern': '**/test.txt'} # 🔧工具调用:read_file, 参数:{'file_path': '/test.txt'}
3.演示大文件读取分页
- 在FilesystemBackend分页的参数是固定死的,默认每次读取500行,我们可以在系统提示词中指定每次读取的行数。
import os import traceback import dotenv from deepagents.backends import FilesystemBackend from langchain.chat_models import init_chat_model from deepagents import create_deep_agent from langchain_core.messages import HumanMessage, ToolMessage, BaseMessage # 1.初始化环境 dotenv.load_dotenv() # 2.创建文件系统 # 使用本地文件系统后端,根目录设为./workspace # 这样我们可以看到真实的文件创建,设置virtual_mode=True表示文件系统是虚拟的,创建的文件不会实际写入磁盘 backend = FilesystemBackend(root_dir="./workspace", virtual_mode=True) # 3.模型初始化 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), temperature=0.7, max_tokens=10000, ) system_prompt = """ 你是一个专业的系统管理员。 你的任务是从日志文件中查找特定的错误代码。 注意:日志文件可能非常大,为了避免上下文溢出,你必须使用 'read_file'工具的分页功能。 每次读取请限制在300行以内(limit=300),并使用offset参数向后滚动。 直到找到目标信息为止。 """ # 4.创建agent agent = create_deep_agent( model=deepseek_v4_pro, backend=backend, # 指定FilesystemBackend利用本地磁盘持久化存储 system_prompt=system_prompt ) LARGE_FILE_NAME = "server_logs.txt" # LARGE_FILE_PATH = WORK_DIR / LARGE_FILE_NAME TARGET_SECRET = "RustFS" # 准备用户指令 task = f"请从文件'/{LARGE_FILE_NAME}'中搜索包含'{TARGET_SECRET}'的行,并告诉我它的具体内容。" step = 0 try: # agent.stream 默认多为 updates 模式: # chunk: dict[str, Any] # 形如 {"model": {"messages": [...]}} 或 {"tools": {"messages": [...]}} for chunk in agent.stream({"messages": [HumanMessage(content=task)]}): # chunk.items() → Iterator[tuple[str, Any]] # node_name: str —— LangGraph 节点名,如 "model" / "tools" # node_data: Any —— 该节点本次状态增量(常为 dict,偶发 Overwrite 包装) for node_name, node_data in chunk.items(): # 跳过空节点(None / {} / [] 等假值) if not node_data: continue # 处理 Overwrite 对象(langgraph 状态更新包装器) # Overwrite 有 .value 属性,真正的增量数据在 value 里 # 类型示意:Overwrite(value={"messages": [...]}) → 取出 dict if hasattr(node_data, "value"): node_data = node_data.value # node_data: Any → 期望变成 dict # 只处理字典形态的节点数据;其它类型(如纯标量)跳过 if not isinstance(node_data, dict): continue # 至此 node_data: dict[str, Any],常见键: "messages", 也可能有 todos 等 if "messages" in node_data: # msgs: list[BaseMessage] | Overwrite | BaseMessage(偶发单条) msgs = node_data["messages"] # messages 也可能被 Overwrite 包一层 if hasattr(msgs, "value"): msgs = msgs.value # 统一成 list,后面 for 遍历 if not isinstance(msgs, list): msgs = [msgs] # 至此 msgs: list[BaseMessage] for msg in msgs: # msg: BaseMessage 子类 # - AIMessage: type="ai",可能带 tool_calls # - ToolMessage: type="tool",.name=工具名,.content=工具返回 # - HumanMessage: type="human" # ---------- 1) 模型决定调工具:AIMessage + 非空 tool_calls ---------- # tool_calls: list[dict] | list[对象] # 单条常见结构 dict: # {"name": str, "args": dict[str, Any], "id": str, ...} if hasattr(msg, "tool_calls") and msg.tool_calls: step += 1 print( f"\n[Step {step}] Agent决定调用工具 " f"(Node: {node_name!r} type={type(node_name).__name__})" ) for tool_call in msg.tool_calls: # 兼容 dict / 对象两种 tool_call if isinstance(tool_call, dict): name = tool_call.get("name", "unknown") # name: str args = tool_call.get("args", {}) # args: dict[str, Any] else: name = getattr(tool_call, "name", "unknown") args = getattr(tool_call, "args", {}) or {} print(f" >>>工具:{name} (type={type(name).__name__})") if name == "read_file": # FilesystemMiddleware 的分页读文件参数 offset = args.get("offset", 0) # offset: int,从第几行开始 limit = args.get("limit", "Default") # limit: int | str,最多读几行 # 不同版本参数名可能是 path 或 file_path path_val = args.get("path") or args.get("file_path") # path_val: str | None print( f" >>>参数:path={path_val!r} ({type(path_val).__name__}), " f"offset={offset!r} ({type(offset).__name__}), " f"limit={limit!r} ({type(limit).__name__})" ) print( f" (说明:正在读取从第{offset}行开始,最多读取{limit}行数据)" ) else: print(f" >>>参数:{args} (type={type(args).__name__})") # ---------- 2) 工具执行结果:ToolMessage ---------- # msg.content: str | list(多数为 str,即工具返回正文) elif isinstance(msg, ToolMessage): content = msg.content # 若 content 不是纯 str(极少见多模态块),先转成 str 再统计行数 content_str = content if isinstance(content, str) else str(content) line_count = len(content_str.splitlines()) # line_count: int # 检查本块工具输出里是否出现目标关键字 found_secret = TARGET_SECRET in content_str # found_secret: bool preview = content_str[:100].replace("\n", " ") + "..." # preview: str print( f"\n[Tool Output] (Node: {node_name!r}, " f"tool_name={msg.name!r} type={type(msg.name).__name__}, " f"读取了 {line_count} 行, content_type={type(content).__name__})" ) print(f" 内容预览:{preview}") if found_secret: print( f" 成功:在此分块中发现了目标 Secret:" f"{TARGET_SECRET!r} 已找到!" ) # ---------- 3) 模型纯文本最终/中间回复:AIMessage,无 tool_calls ---------- # msg.type: str,AI 消息为 "ai" # msg.content: str | list elif isinstance(msg, BaseMessage) and msg.type == "ai" and msg.content: print( f"\n[Agent最终回复] (Node: {node_name!r}, " f"msg.type={msg.type!r}, content_type={type(msg.content).__name__}):" ) print(f"-" * 40) print(msg.content) except Exception as e: print(f"运行错误:{e}") traceback.print_exc()
输出:
D:\anaconda3\envs\deepagents\python.exe E:\code\InsightFlow\create_demo_deep_agent\demo_backend_file2.py [Step 1]Agent决定调用工具(Node: model) >>>工具:grep >>>参数:{'pattern': 'RustFS', 'path': '/', 'output_mode': 'content'} [Tool Output] (Node: tools 读取了1行数据) 内容预览:No matches found... [Step 2]Agent决定调用工具(Node: model) >>>工具:glob >>>参数:{'pattern': '**/server_logs.txt'} [Tool Output] (Node: tools 读取了1行数据) 内容预览:['/\\server_logs.txt']... [Step 3]Agent决定调用工具(Node: model) >>>工具:read_file >>>参数:path=/server_logs.txt, offset=0, limit=300 (说明:正在读取从第0行开始,最多读取300行数据) [Tool Output] (Node: tools 读取了87行数据) 内容预览: 1 一、文件存储方案:RustFS 集群 + 备份服务器 2 1. RustFS 集成优势 3 S3 100% 兼容:与现有前端/后端通过 S3 API 无缝对接(bot... 👌成功:在此分块中发现了目标Secret:RustFS已找到! [Step 4]Agent决定调用工具(Node: model) >>>工具:grep >>>参数:{'pattern': 'RustFS', 'path': '/server_logs.txt', 'output_mode': 'content'} [Tool Output] (Node: tools 读取了1行数据) 内容预览:No matches found... [Step 5]Agent决定调用工具(Node: model) >>>工具:grep >>>参数:{'output_mode': 'content', 'path': '/\\server_logs.txt', 'pattern': 'RustFS'} [Tool Output] (Node: tools 读取了1行数据) 内容预览:No matches found... [Agent最终回复] (Node: model): ---------------------------------------- 我已经通过读取文件获取了全部内容。以下是 `/server_logs.txt` 中所有包含 **RustFS** 的行及其内容: --- | 行号 | 内容 | |------|------| | **1** | 一、文件存储方案:**RustFS** 集群 + 备份服务器 | | **2** | 1. **RustFS** 集成优势 | | **4~5** | S3 100% 兼容 + **Rust** 实现:高性能、内存安全,与您团队的 Rust 技术栈理念一致。 | | **19** | **RustFS** 生产集群 (S3 兼容) | | **29** | **RustFS** 独立实例 / 或 Rsync + 冷存储 | | **32** | 生产集群:3 个 **RustFS** 节点组成分布式集群…**RustFS** 具体配置参考其文档… | | **34** | 备份服务器:可部署另一台独立的 **RustFS** 服务(单节点即可)…或 **RustFS** 自身的复制机制… | | **37** | 实时/准实时备份:利用 **RustFS** 的 Bucket Replication… | | **43** | 注意:**RustFS** 的具体配置需参照其官方文档… | | **46** | …仅存储层切换为 **RustFS**。核心交互如下: | | **48** | 文件上传:FastAPI 接收文件流 → 调用 **RustFS** S3 API 存入生产集群… | | **50** | 文件下载/预览:前端通过预签名 URL 直接访问 **RustFS** 生产集群。 | | **74** | …**RustFS** 集群部署在 CPU 服务器上,不占用 GPU,对整体方案无影响。 | | **77** | 搭建 **RustFS** 测试集群:使用 Docker Compose 快速启动 3 节点… | | **85** | 如果您需要更详细的 **RustFS** 部署方案或需调整其他模块,请随时告知! | --- ### 总结 这个文件实际上是一份 **技术方案文档**(并非真正的服务器日志),核心内容围绕 **RustFS** 分布式存储集群的架构设计: - **3 节点生产集群 + 1 节点备份服务器**的存储架构 - **S3 100% 兼容**,可与现有 boto3/aws-sdk 无缝对接 - **Rust 实现**,高性能、内存安全,Apache 2.0 协议 - 备份策略:支持 Bucket Replication(分钟级 RPO)或 rclone 定时同步 - 与 8×RTX 4090 GPU 方案配合,RustFS 部署在 CPU 服务器上,不占用 GPU 资源 如果你需要我进一步深挖某个具体细节,请随时告知! Process finished with exit code 0
4.E2BBackend 使用E2B云沙箱
E2B (Environment To Be) 是一个 专为 AI 智能体设计的云端安全执行环境 。可以把它想象成一台 云端电脑 或 远程服务器 ,DeepAgents 将其作为“外挂大脑”和“执行手脚”。当 AI 需要写代码、运行脚本或操作文件时,它不会在你的本地机器上操作,而是连接到 E2B 的云端环境中进行。
核心特性
-
安全性与隔离性 (Security & Isolation) :
- AI 生成的代码(可能包含错误或恶意逻辑)完全运行在云端沙箱中, 绝不会破坏你本地的电脑环境 。
- 演示代码中,Agent 即使执行了 rm -rf / ,也只是删除了云端临时的沙箱,对宿主机毫发无损。
-
持久化会话 (Long-running Sessions) :
- 沙箱可以保持运行状态。Agent 可以先创建一个文件(如演示中的 /home/user/hello.py ),然后在后续步骤中运行它。环境状态在会话期间是保持的。
-
标准 Linux 环境 :
- 它提供标准的 Linux Shell。演示中 Agent 执行了 uname -a 和 python –version ,就像在真实的服务器上一样。
step1: 访问 https://e2b.dev 注册登陆,然后并获取 API_Key

- 在左侧选择API Keys选项,创建API

- 创建API Key后记得保存好,保存到.env文件中,作为:E2B_API_KEY

step2: 安装依赖
pip install e2b
e2b_backend.py
""" E2B 沙箱后端示例:让 DeepAgents 的 execute / 读写文件跑在远程隔离环境里。 整体关系: create_deep_agent(backend=E2BBackend(...)) → Agent 调 execute / write_file / read_file 等工具 → 落到本类的 execute / upload_files / download_files → 再调用 e2b.Sandbox 云端 API """ from typing import Optional, List # Sandbox: e2b 云端沙箱客户端类 # 类型: type[Sandbox] # 实例化后大致能: # sandbox.commands.run(cmd) -> 有 stdout/stderr/exit_code # sandbox.files.write(path, content) # sandbox.files.read(path) -> str | bytes # sandbox.kill() from e2b import Sandbox # BaseSandbox: DeepAgents 抽象基类,子类至少要实现 execute(command) -> ExecuteResponse # 其它 ls/read/write/grep 等默认会「拼 shell 命令再调 execute」 from deepagents.backends.sandbox import BaseSandbox # 协议里约定的数据结构(dataclass): # ExecuteResponse(output: str, exit_code: int | None, truncated: bool) # 例: ExecuteResponse(output="hello\n", exit_code=0, truncated=False) # FileUploadResponse(path: str, error: str | None) # 例: FileUploadResponse(path="/tmp/a.txt", error=None) # 成功 # 例: FileUploadResponse(path="/root/x", error="permission_denied") # FileDownloadResponse(path: str, content: bytes | None, error: str | None) # 例: FileDownloadResponse(path="/tmp/a.txt", content=b"hi", error=None) from deepagents.backends.protocol import ( ExecuteResponse, FileDownloadResponse, FileUploadResponse, ) class E2BBackend(BaseSandbox): """ 把 DeepAgents 的「沙箱后端协议」接到 E2B。 继承 BaseSandbox 后: - 必须实现: execute(command: str) -> ExecuteResponse - 可选覆盖: upload_files / download_files(用 E2B 文件 API 更直接) - 自动获得: ls_info / read / write / edit / grep 等(内部调 execute 跑 shell) """ def __init__( self, template: str = "base", api_key: Optional[str] = None, timeout: Optional[int] = None, metadata: Optional[dict[str, str]] = None, ) -> None: """ 创建一个远程 E2B 沙箱实例,并保存在 self.sandbox。 参数类型与样例: template: str 例: "base" —— E2B 预置模板名/ID api_key: str | None 例: "e2b_xxxx" 或 None(None 时读环境变量 E2B_API_KEY) timeout: int | None 例: 300 —— 沙箱最长存活秒数;None 用 SDK 默认 metadata: dict[str, str] | None 例: {"project": "insightflow", "env": "dev"} """ # Sandbox.create(...): 云端启动一台隔离 Linux 环境 # 返回类型: Sandbox(e2b 客户端对象) # 例: <Sandbox sandbox_id="iXXXX..."> self.sandbox = Sandbox.create( template=template, # str api_key=api_key, # str | None timeout=timeout, # int | None metadata=metadata, # dict[str, str] | None ) @property def id(self) -> str: """ 沙箱唯一 ID,供日志/追踪用。 返回类型: str 例: "i6wz5jfhsa1a2b3c4d5e" """ # sandbox_id: str —— E2B 分配的实例 ID return self.sandbox.sandbox_id def execute(self, command: str) -> ExecuteResponse: """ DeepAgents 核心抽象方法:在沙箱里执行一条 shell 命令。 入参: command: str 例: "ls -la /home" 例: "python3 -c 'print(1+1)'" 返回: ExecuteResponse( output: str, # stdout+stderr 拼在一起,给 LLM 看 exit_code: int|None, # 0 成功,非 0 失败 truncated: bool, # 输出是否被截断 ) 成功例: ExecuteResponse(output="2\\n", exit_code=0, truncated=False) 失败例: ExecuteResponse(output="Error executing command: ...", exit_code=1, truncated=False) """ try: # commands.run(command: str) -> CommandResult(e2b 对象) # stdout str 标准输出:程序正常打印给用户看的内容 "hello\n"、"2\n"、ls 列出的文件名 # stderr str 标准错误:警告/报错信息(不一定代表进程失败) ""(没有错误)、"Permission denied\n" # exit_code int 退出码:进程结束状态 0 = 成功;非 0(如 1、127)= 失败 # 常见字段: # result.stdout: str 例: "hello\\n" # result.stderr: str 例: "" 或 "warn...\\n" # result.exit_code: int 例: 0 result = self.sandbox.commands.run(command) # 协议要求把 stdout/stderr 合成一个字符串给模型 combined_output: str = (result.stdout or "") + (result.stderr or "") # 例: combined_output == "file1\\nfile2\\n" return ExecuteResponse( output=combined_output, # str exit_code=result.exit_code, # int truncated=False, # bool;这里不做截断检测,固定 False ) except Exception as e: # 任何异常都转成「失败的 ExecuteResponse」,避免把异常直接抛给 Agent 图 # str(e): str 例: "SandboxTimeoutError: ..." return ExecuteResponse( output=f"Error executing command: {str(e)}", # str exit_code=1, # int —— 约定非 0 表示失败 truncated=False, # bool ) def upload_files(self, files: list[tuple[str, bytes]]) -> List[FileUploadResponse]: """ 批量上传文件到沙箱(覆盖 BaseSandbox 默认「用 shell 写文件」实现)。 注意协议方法名是 upload_files(复数),不是 upload_file。 入参: files: list[tuple[str, bytes]] 每个元素 = (远程路径, 文件字节内容) 例: [ ("/home/user/a.txt", b"hello"), ("/tmp/data.json", b'{"x": 1}'), ] 返回: list[FileUploadResponse] 成功例: [FileUploadResponse(path="/home/user/a.txt", error=None)] 失败例: [FileUploadResponse(path="/root/x", error="permission_denied")] error 允许值大致: None | "invalid_path" | "permission_denied" | ... """ # responses: list[FileUploadResponse] —— 与 files 一一对应的结果列表 responses: List[FileUploadResponse] = [] # path: str 例: "/home/user/notes/a.txt" # content: bytes 例: b"hello world" for path, content in files: try: # 取父目录,便于 mkdir -p # rsplit("/", 1): 从右边按 / 切一次 # "/home/user/a.txt".rsplit("/", 1) -> ["/home/user", "a.txt"] # 再 [0] 得到父目录 "/home/user" # 注意: 不是 rstrip(rstrip 是删字符集合,语义完全不同) parent_dir: str = path.rsplit("/", 1)[0] if "/" in path else "" # parent_dir 例: "/home/user" 或 ""(文件就在根路径名时) if parent_dir: # 在沙箱里创建目录;返回值这里忽略 # 命令例: "mkdir -p /home/user" self.sandbox.commands.run(f"mkdir -p {parent_dir}") # files.write(path: str, content: str | bytes) -> None(写入远程文件) # 例: write("/home/user/a.txt", b"hello") self.sandbox.files.write(path, content) # 成功:error=None responses.append(FileUploadResponse(path=path, error=None)) except Exception as e: # error_msg: str 例: "permission denied: /root" error_msg: str = str(e).lower() # error: str —— 协议约定的结构化错误码(给 LLM/调用方看) error: str = "invalid_path" if "permission" in error_msg: error = "permission_denied" responses.append(FileUploadResponse(path=path, error=error)) # 例: [FileUploadResponse(...), FileUploadResponse(...)] return responses def download_files(self, paths: list[str]) -> List[FileDownloadResponse]: """ 批量从沙箱下载文件。 注意协议方法名是 download_files(复数)。 入参: paths: list[str] 例: ["/home/user/a.txt", "/tmp/out.log"] 返回: list[FileDownloadResponse] 成功例: FileDownloadResponse( path="/home/user/a.txt", content=b"hello", # 必须是 bytes | None error=None, ) 失败例: FileDownloadResponse( path="/missing.txt", content=None, error="file_not_found", ) """ responses: List[FileDownloadResponse] = [] # path: str 例: "/home/user/a.txt" for path in paths: try: # files.read(path: str) -> str | bytes(SDK 版本不同可能返回文本或字节) # 例: "hello\\n" 或 b"hello\\n" content = self.sandbox.files.read(path) # 协议要求 content: bytes | None,所以 str 要 encode if isinstance(content, str): # content: str -> bytes # 例: "你好".encode("utf-8") == b"\\xe4\\xbd\\xa0\\xe5\\xa5\\xbd" content = content.encode("utf-8") # 至此 content: bytes responses.append( FileDownloadResponse( path=path, # str content=content, # bytes error=None, # None 表示成功 ) ) except Exception as e: error_msg: str = str(e).lower() error: str = "invalid_path" if "not found" in error_msg: error = "file_not_found" responses.append( FileDownloadResponse( path=path, # str content=None, # 失败时没有内容 error=error, # str 错误码 ) ) return responses def close(self) -> None: """ 销毁远程沙箱,释放云端资源。 返回: None 副作用: 远程实例被 kill,之后不能再 execute / 读写文件。 """ # kill(): None —— 终止沙箱生命周期 self.sandbox.kill() # --------------------------------------------------------------------------- # 使用方式示意(本文件目前只定义后端类;真正跑 Agent 时类似下面): # # backend = E2BBackend(api_key=os.getenv("E2B_API_KEY"), timeout=300) # # backend: E2BBackend # # backend.id -> str 例 "i6wz..." # # agent = create_deep_agent(model=..., backend=backend) # # Agent 调 execute 工具时 -> backend.execute("python script.py") # # -> ExecuteResponse(output=..., exit_code=0, truncated=False) # # backend.close() # 记得关,否则云端实例会一直占着直到超时 # ---------------------------------------------------------------------------
- 将自定义好的沙箱后端类注册到 DeepAgents
import asyncio import os import traceback import dotenv from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain_core.messages import BaseMessage, ToolMessage from langchain_mcp_adapters.client import MultiServerMCPClient from rich.console import Console from create_demo_deep_agent.e2b_backend import E2BBackend # 1.加载 .env 文件,设置环境变量 dotenv.load_dotenv() # 2.初始化模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider='deepseek', base_url="https://api.deepseek.com", api_key=os.getenv('api_key'), temperature=0.7, max_tokens=10000, ) # 2.配置Rich Console console = Console() # 3.配置 Context7 MCP async def setup_mcp_tools(): console.print("[dim]正在连接 Context7(streamable_http)...[/dim]") try: mcp_client = MultiServerMCPClient( { "context7": { "transport": "streamable_http", # streamable_http模式 "url": "https://mcp.context7.com/mcp" } } ) tools = await mcp_client.get_tools() console.print(f"[bold green]成功加载 {len(tools)} 个 MCP 工具(HTTP)[/bold green]") return mcp_client, tools except Exception as e: console.print(f"[bold yellow]Context7(streamable_http)连接失败: {e}[/red]") return None, [] async def run_chat_loop() -> None: print("\n" + "=" * 80) print("欢迎使用 DeepAgent E2BBackend (Sanbox) 演示!") print("\n" + "=" * 80) # 1.初始化mcp mcp_client, tools = await setup_mcp_tools() # 2.初始化E2B Backend print("正在初始化 E2B Backend 沙箱...") try: backend = E2BBackend(template="base") print(f"✅ 沙箱启动成功 (ID: {backend.id})") except Exception as e: print(f"❌️ 沙箱启动失败: {e}") return try: # 3.创建 DeepAgent agent = create_deep_agent( model=deepseek_v4_pro, tools=tools, # 赋予MCP工具能力 backend=backend, # 使用 E2BBackend 实现E2B沙箱能力 system_prompt=""" 你是一个拥有云端沙箱环境的高级技术助手。 你的任务是演示如何在沙箱中进行操作. 请执行以下步骤: 1.使用 'execute_command' 运行 'uname -a' 和 'python --version' 来展示环境信息。 2.创建一个Python脚本 '/home/user/hello.py', 内容是打印 'Hello from E2B Sandbox!'。 3.运行这个Python脚本并显示输出。 """, ) # 4.执行任务 task = "请你开始演示沙箱操作流程" step = 0 # agent.astream:以「流式」方式跑 agent,每完成一小步就 yield 一个 chunk, # 而不是等全部跑完才一次性返回。这样可以实时打印中间过程。 # 传入的 messages 就是用户本轮的输入。 async for chunk in agent.astream({"messages": [{"role": "user", "content": task}]}): step += 1 # 仅用于日志编号,方便对照「第几步」 # 每个 chunk 是一个 dict,形如: # {"model": {"messages": [...]}} ← 模型节点刚产出了回复/工具调用 # {"tools": {"messages": [...]}} ← 工具节点刚执行完,返回了结果 # node_name = "model" / "tools" 等;node_data = 该节点本轮的输出数据 for node_name, node_data in chunk.items(): if node_data is None: continue # 只关心带 messages 的节点输出(agent 的对话与工具结果都在这里) if "messages" in node_data: msgs = node_data["messages"] # 有时是单条消息对象,有时是列表,统一成 list 方便遍历 if not isinstance(msgs, list): msgs = [msgs] for msg in msgs: # ---- 情况 A:普通文本回复(AIMessage 等)---- # 打印模型对用户说的话;跳过「只有 tool_call、没有正文」的空壳消息 if isinstance(msg, BaseMessage) and msg.content: is_tool_call_msg = getattr(msg, "tool_call", None) if not is_tool_call_msg: print(f"\n[Agent ({node_name})]") print("-" * 40) print(msg.content) print("-" * 40) # ---- 情况 B:模型决定调用工具 ---- # msg.tool_calls 里是本次要调的工具名和参数,例如 execute / write_file if hasattr(msg, "tool_calls") and msg.tool_calls: print(f"\n[Step {step}: 工具调用]") for tool_call in msg.tool_calls: args_str = str(tool_call["args"]) if len(args_str) > 500: args_str = args_str[:500] + "..." # 参数过长时截断,避免刷屏 print(f"- {tool_call['name']}: ({args_str})") # ---- 情况 C:工具执行结果 ---- # ToolMessage 是工具跑完后返回的内容(命令输出、文件内容等) if isinstance(msg, ToolMessage): content_preview = str(msg.content) if len(content_preview) > 200: content_preview = content_preview[:200] + "..." print(f"[Tool Output ({msg.name})]: {content_preview}") except Exception as e: # 运行中任意一步出错都会进这里,打印错误和完整堆栈便于排查 print(f"❌️ 运行时错误:{e}") traceback.print_exc() finally: # 无论成功还是失败,都要关掉沙箱,避免 E2B 资源一直占用计费 print("\n正在关闭沙箱...") backend.close() print("✅ 沙箱已关闭") print("感谢使用 DeepAgent E2BBackend 演示!") if __name__ == '__main__': # 入口:用 asyncio 驱动上面的异步聊天循环 asyncio.run(run_chat_loop())
输出:
================================================================================ 欢迎使用 DeepAgent E2BBackend (Sanbox) 演示! ================================================================================ 正在连接 Context7(streamable_http)... 成功加载 2 个 MCP 工具(HTTP) 正在初始化 E2B Backend 沙箱... ✅ 沙箱启动成功 (ID: iwe40zufahl6jna8l1v68) [Agent (model)] ---------------------------------------- 好的!我来逐步演示沙箱的操作流程。 --- ## 步骤 1:展示沙箱环境信息 首先,让我运行 `uname -a` 和 `python --version` 来查看系统环境: ---------------------------------------- [Step 3: 工具调用] - execute: ({'command': 'uname -a && python --version'}) [Agent (tools)] ---------------------------------------- Linux e2b.local 6.1.158+ #1 SMP PREEMPT_DYNAMIC Fri Jul 3 14:02:15 UTC 2026 x86_64 GNU/Linux Python 3.11.6 [Command succeeded with exit code 0] ---------------------------------------- [Tool Output (execute)]: Linux e2b.local 6.1.158+ #1 SMP PREEMPT_DYNAMIC Fri Jul 3 14:02:15 UTC 2026 x86_64 GNU/Linux Python 3.11.6 [Command succeeded with exit code 0] [Agent (model)] ---------------------------------------- 环境信息已获取: | 项目 | 详情 | |------|------| | **操作系统** | Linux (x86_64, GNU/Linux) | | **内核版本** | 6.1.158+ | | **Python 版本** | 3.11.6 | --- ## 步骤 2:创建 Python 脚本 现在创建 `/home/user/hello.py`: ---------------------------------------- [Step 7: 工具调用] - write_file: ({'file_path': '/home/user/hello.py', 'content': "print('Hello from E2B Sandbox!')\n"}) [Agent (tools)] ---------------------------------------- Updated file /home/user/hello.py ---------------------------------------- [Tool Output (write_file)]: Updated file /home/user/hello.py [Agent (model)] ---------------------------------------- 脚本创建成功!让我确认一下文件内容: ---------------------------------------- [Step 11: 工具调用] - read_file: ({'file_path': '/home/user/hello.py'}) [Agent (tools)] ---------------------------------------- 1 print('Hello from E2B Sandbox!') ---------------------------------------- [Tool Output (read_file)]: 1 print('Hello from E2B Sandbox!') [Agent (model)] ---------------------------------------- --- ## 步骤 3:运行 Python 脚本 ---------------------------------------- [Step 15: 工具调用] - execute: ({'command': 'python /home/user/hello.py'}) [Agent (tools)] ---------------------------------------- Hello from E2B Sandbox! [Command succeeded with exit code 0] ---------------------------------------- [Tool Output (execute)]: Hello from E2B Sandbox! [Command succeeded with exit code 0] [Agent (model)] ---------------------------------------- --- ## ✅ 演示完成 整个沙箱操作流程已全部完成,总结如下: | 步骤 | 操作 | 结果 | |------|------|------| | **1. 查看环境** | `uname -a` + `python --version` | Linux x86_64, Python 3.11.6 | | **2. 创建脚本** | 写入 `/home/user/hello.py` | 文件创建成功 | | **3. 运行脚本** | `python /home/user/hello.py` | 输出 `Hello from E2B Sandbox!` | 这就是沙箱环境的完整操作流程:使用 `execute` 工具运行 shell 命令、使用 `write_file` 创建文件、使用 `read_file` 确认内容、最后再用 `execute` 执行脚本。整个过程流畅且直观! 🎉 ---------------------------------------- 正在关闭沙箱... ✅ 沙箱已关闭 感谢使用 DeepAgent E2BBackend 演示!
5.DockerBackend 使用Docker容器
DockerBackend 的核心作用是为 AI 智能体提供一个 安全沙箱(Sandbox) 。如果不使用 Docker,Agent 执行的每一条命令(如 rm -rf 、 pip install )都会直接发生在您的宿主机(Mac)上,这极其危险且环境不可控。
作用与优势:
-
安全隔离 (Security & Isolation)
-
作用 :Agent 的所有操作(文件读写、代码执行、系统命令)都被限制在 Docker 容器内部。
-
优势 :即使 Agent 产生幻觉执行了恶意代码(如删除系统文件),也只会破坏容器, 宿主机也会毫发无损 。演示代码中的 auto_remove=True 确保任务结束后容器自动销毁,不留痕迹。
-
-
环境一致性 (Reproducibility)
-
作用 :代码指定了镜像 image=“python:3.11-slim” 。
-
优势 :无论您的电脑安装的是 Python 3.9 还是 3.12,Agent 永远在一个干净、标准的 Python 3.11 环境中运行。这解决了“在我的机器上能跑”的经典依赖问题。
-
-
生命周期管理 (Lifecycle Management)
-
作用 : DockerBackend 自动处理容器的 启动 -> 连接 -> 执行 -> 销毁 全过程。
-
优势 :开发者无需手动编写复杂的 Docker 命令,像使用本地对象一样简单地调用 backend.execute() 或 backend.write_file() 。
-
需要先安装一下docker依赖
pip install docker
- 其次需要对
适用场景:本机(Windows)通过 `tcp://服务器IP:2375` 连接 Ubuntu 上的 Docker,并用于 DeepAgents DockerBackend。
警告:`2375` 无 TLS,**仅限可信内网**。公网请用 SSH 或 TLS(2376),不要裸开 2375。
# --- 在新 Ubuntu 上执行 --- sudo ufw allow 2375/tcp sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<'EOF' { "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"], "registry-mirrors": [ "https://docker.1ms.run", "https://docker.m.daocloud.io", "https://docker.xuanyuan.me" ] } EOF sudo mkdir -p /etc/systemd/system/docker.service.d sudo tee /etc/systemd/system/docker.service.d/override.conf <<'EOF' [Service] ExecStart= ExecStart=/usr/bin/dockerd EOF sudo systemctl daemon-reload sudo systemctl restart docker sudo ss -lntp | grep 2375 docker pull python:3.12-slim
docker_backend.py
""" Docker 沙箱后端:让 DeepAgents 的 execute / 读写文件跑在「本机 Docker 容器」里。 整体关系(和 E2BBackend 对称,只是执行环境从云端换成了本地容器): create_deep_agent(backend=DockerBackend(...)) → Agent 调 execute / write_file / read_file 等工具 → 落到本类的 execute / upload_files / download_files → 再调用 docker-py 操作本地容器 前置条件: 1. 已安装 Docker Desktop / Docker Engine,且守护进程在运行 2. 已安装 Python 包:pip install docker """ import io import tarfile import time import docker from docker.errors import NotFound, APIError from deepagents.backends.protocol import ( ExecuteResponse, FileDownloadResponse, FileUploadResponse, ) from deepagents.backends.sandbox import BaseSandbox class DockerBackend(BaseSandbox): """ 把 DeepAgents 的「沙箱后端协议」接到本机 Docker。 继承 BaseSandbox 后: - 必须实现: execute(command: str) -> ExecuteResponse - 可选覆盖: upload_files / download_files(用 Docker API 传文件更高效) - 自动获得: ls_info / read / write / edit / grep 等(内部调 execute 跑 shell) """ def __init__( self, image: str = "python:3.12-slim", auto_remove: bool = True, cpu_quota: int = 50000, # 约 50% 单核 CPU(见下方注释) memory_limit: str = "512m", network_disabled: bool = False, working_dir: str = "/workspace", volumes: dict[str, dict[str, str]] | None = None, # ---------- 远程 Docker 连接(就在这里配)---------- base_url: str | None = None, # 例: "ssh://user@192.168.1.100" # 例: "tcp://192.168.1.100:2376" # 需 TLS 时再配 tls=... tls: bool | docker.tls.TLSConfig | None = None, ) -> None: """ 创建并启动一个长期存活的 Docker 容器,作为 Agent 的隔离执行环境。 参数说明: image: str 用哪张 Docker 镜像启动容器。镜像 = 操作系统 + 预装软件的「模板」。 例: "python:3.12-slim" —— 官方精简 Python 3.12 环境 例: "ubuntu:22.04" —— 完整 Ubuntu(体积更大) auto_remove: bool 关闭沙箱时是否顺便删除容器。 True → close() 时 remove,磁盘不留残留(演示常用) False → close() 时只 stop,容器还在,方便事后 docker logs / inspect cpu_quota: int CPU 时间配额,单位是「微秒 / 每 100ms 周期」。 Docker 默认周期 period=100000 微秒(0.1 秒), 所以 cpu_quota=50000 表示每个周期最多用一半时间 ≈ 约 50% 单核。 例: 100000 → 约 100% 单核;200000 → 约 2 核 memory_limit: str 容器可用内存上限,超出会被 OOM kill。 例: "512m"、"1g"、"256m" network_disabled: bool 是否禁用容器网络。 True → 容器不能上网、不能访问宿主机网络(更安全,但 pip install 会失败) False → 允许联网(默认,方便装包、调 API) working_dir: str 容器内的默认工作目录;execute() 里跑命令时 cwd 就在这里。 例: "/workspace"(注意要带前导 /) volumes: dict | None 把宿主机目录挂进容器,格式与 docker run -v 对应: { "宿主机绝对路径": { "bind": "容器内路径", # 挂载到哪里 "mode": "rw" 或 "ro", # 读写 / 只读 } } 例: {r"E:\\data": {"bind": "/data", "mode": "ro"}} 注意:远程 Docker 时,路径是「服务器上的路径」,不是你本机路径。 None 表示不挂任何卷。 base_url: str | None 【远程连接入口】Docker 守护进程地址。 None → 用 docker.from_env()(本机,或读环境变量 DOCKER_HOST) 有值 → 直连该地址上的 Docker 例: "ssh://ubuntu@10.0.0.8" 例: "tcp://10.0.0.8:2376" tls: bool | TLSConfig | None 仅 tcp:// 且开启 TLS 时需要;SSH 方式一般不用。 例: True,或 docker.tls.TLSConfig(client_cert=..., ca_cert=..., verify=True) """ if docker is None: raise ImportError("Docker not installed. Please install docker and try again.") # ---------- 连接 Docker(本机 or 远程)---------- # 改这里 / 传 base_url,就能连服务器上的 Docker,而不改后面的跑容器逻辑。 if base_url: # 远程:显式指定守护进程地址 # 返回类型: docker.DockerClient self.client = docker.DockerClient(base_url=base_url, tls=tls) else: # 本机默认;若已设置环境变量 DOCKER_HOST=ssh://... 也会连远程 self.client = docker.from_env() self.image = image self.auto_remove = auto_remove self.working_dir = working_dir self.volumes = volumes or {} self._container = None # 稍后赋值为真正的容器对象 try: # ---------- 1. 确保镜像存在 ---------- # images.get:本地已有该镜像则直接用;没有会抛 NotFound try: self.client.images.get(self.image) except NotFound: print(f"Docker image {self.image} not found. Pulling...") # auth_config={}:公开镜像不走本机 credentials store # 避免 Windows 上报 docker-credential-desktop 不存在 # (即便 Docker 在远程 Ubuntu,pull 仍可能读本机 ~/.docker/config.json) self.client.images.pull(self.image, auth_config={}) # ---------- 2. 启动一个「空闲常驻」容器 ---------- # 为什么用 tail -f /dev/null? # python:3.12-slim 默认入口跑完就退出;容器一退出就没法再 exec。 # tail -f /dev/null 会永远阻塞,容器保持 Running,后续靠 exec_run 跑命令。 # # detach=True:后台运行,不阻塞当前 Python 进程 # auto_remove:容器退出后是否自动删掉(与 close() 里的逻辑配合) self._container = self.client.containers.run( self.image, command="tail -f /dev/null", # 保活命令,让容器一直 Running detach=True, auto_remove=self.auto_remove, cpu_quota=cpu_quota, mem_limit=memory_limit, network_disabled=network_disabled, working_dir=working_dir, volumes=self.volumes, ) # ---------- 3. 确保工作目录存在 ---------- # -p:父目录不存在则一并创建;已存在也不报错 self.execute(f"mkdir -p {working_dir}") except Exception as e: raise RuntimeError(f"Failed to create Docker container: {e}") @property def id(self) -> str: """ 沙箱唯一 ID(即 Docker 容器 ID),供日志/追踪用。 返回类型: str 例: "a1b2c3d4e5f6..."(完整 64 位 hex;docker ps 里常显示前 12 位) """ return self._container.id if self._container else "unknown" def execute(self, command: str) -> ExecuteResponse: """ 在容器里执行一条 shell 命令,并返回输出 + 退出码。 这是 Agent 最常用的能力:跑 uname、python、pip、写文件用的 echo 等, 最终都会走到这里(或由 BaseSandbox 拼成 shell 再调这里)。 参数: command: str —— 完整 shell 命令字符串 例: "python --version" 例: "uname -a && ls /workspace" 返回: ExecuteResponse( output: str, # 合并后的 stdout/stderr 文本 exit_code: int, # 0 成功,非 0 失败 truncated: bool, # 输出是否被截断;本实现始终 False ) """ if not self._container: return ExecuteResponse(output="Container not running", exit_code=1, truncated=False) try: # exec_run:在「已运行」的容器里再开一个进程(类似 docker exec) # # cmd=["bash", "-c", command]: # 用 bash 解释整段 command,这样管道 |、&&、重定向 > 才能生效。 # 若直接传字符串,有些特殊字符可能被错误拆分。 # # workdir:本次命令的当前目录(覆盖容器默认 WORKDIR) # demux=False:stdout 和 stderr 混在一起返回(True 则会分开成两个流) # # 返回: (exit_code: int, output: bytes) exit_code, output = self._container.exec_run( cmd=["bash", "-c", command], workdir=self.working_dir, demux=False, ) return ExecuteResponse( # decode:bytes → str;非法字节用 � 替换,避免解码崩溃 output=output.decode("utf-8", errors="replace"), exit_code=exit_code, truncated=False, ) except Exception as e: # Docker 守护进程 API 报错(容器状态异常、权限等) return ExecuteResponse( output=f"Error executing command: {str(e)}", exit_code=1, truncated=False, ) def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]: """ 把若干文件写入容器文件系统。 Docker 没有「直接写单个文件」的高层 API,官方推荐做法是: 1. 在内存里打成 tar 包 2. 用 put_archive 把 tar 解压进容器某个目录 参数: files: list[tuple[path, content]] path: str —— 容器内目标路径 content: bytes —— 文件二进制内容 例: [("/workspace/hello.py", b"print('hi')\\n")] 返回: 与 files 一一对应的 FileUploadResponse 列表 成功: FileUploadResponse(path=..., error=None) 失败: FileUploadResponse(path=..., error="permission_denied") """ if not self._container: return [ FileUploadResponse(path=file_path, error="permission_denied") for file_path, _ in files ] responses = [] # ---------- 1. 在内存中组装 tar ---------- # BytesIO:像文件一样读写的内存缓冲区,避免写临时磁盘文件 tar_stream = io.BytesIO() with tarfile.open(fileobj=tar_stream, mode="w") as tar: for file_path, file_content in files: # 绝对路径:去掉前导 /,解压到容器根目录 / # "/workspace/a.py" → arcname="workspace/a.py",最终落在 /workspace/a.py # 相对路径:保持原样,解压时相对 working_dir(见下方 dest_path 注释) # 注意:当前实现 put_archive 始终解压到 "/", # 相对路径场景下 dest_path 变量算了但未用于 put_archive,实际仍从根展开。 if file_path.startswith("/"): arcname = file_path.lstrip("/") dest_path = "/" # noqa: F841 —— 预留:若改为 put_archive(dest_path) 可用 else: arcname = file_path dest_path = self.working_dir # noqa: F841 # TarInfo:描述 tar 里一个条目的元数据(文件名、大小、修改时间) info = tarfile.TarInfo(name=arcname) info.size = len(file_content) info.mtime = time.time() # addfile:把「元数据 + 文件内容流」写入 tar tar.addfile(tarinfo=info, fileobj=io.BytesIO(file_content)) # 先记「准备成功」;真正 put_archive 失败时会整体改成 error responses.append( FileUploadResponse( path=file_path, error=None, ) ) # seek(0):把读指针拨回开头,否则 put_archive 读到的是空内容 tar_stream.seek(0) # ---------- 2. 把 tar 解压进容器 ---------- # put_archive(path="/", data=...):在容器的 / 下解压整个 tar # 因此 arcname="workspace/a.py" 会变成容器内 /workspace/a.py try: self._container.put_archive(path="/", data=tar_stream) except Exception: return [ FileUploadResponse(path=file_path, error="permission_denied") for file_path, _ in files ] return responses def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: """ 从容器里下载若干文件到本机内存(bytes)。 与 upload 对称:Docker 用 get_archive 取出 tar,再从 tar 里抽出文件内容。 参数: paths: list[str] —— 容器内文件路径列表 例: ["/workspace/hello.py", "/tmp/out.txt"] 返回: FileDownloadResponse 列表,与 paths 一一对应 成功: content=文件字节, error=None 失败: content=None, error 为下列之一: "file_not_found" / "is_directory" / "permission_denied" / "invalid_path" """ if not self._container: return [ FileDownloadResponse(path=file_path, error="permission_denied") for file_path in paths ] responses = [] for path in paths: try: # get_archive:把容器内 path 打成 tar,以流的形式返回 # bits: 可迭代的字节块生成器 # stat: 该路径的元信息(大小等),这里未使用 bits, stat = self._container.get_archive(path) # 把分块流拼成完整的 tar 字节流 file_content = io.BytesIO() for chunk in bits: file_content.write(chunk) file_content.seek(0) # 打开 tar,取出第一个成员(对单文件路径通常只有一项) with tarfile.open(fileobj=file_content, mode="r") as tar: member = tar.next() if member is None: responses.append( FileDownloadResponse(path=path, error="file_not_found") ) continue # 若 path 指向目录,协议要求报 is_directory,而不是返回一堆文件 if member.isdir(): responses.append( FileDownloadResponse(path=path, error="is_directory") ) continue # extractfile:得到该成员的文件对象,再 read() 成 bytes f = tar.extractfile(member) if f: content = f.read() responses.append( FileDownloadResponse(path=path, content=content, error=None) ) else: responses.append( FileDownloadResponse(path=path, error="file_not_found") ) except NotFound: # 容器内路径不存在 responses.append( FileDownloadResponse(path=path, error="file_not_found") ) except Exception as e: # 其它错误:尽量映射成协议规定的 error 字符串 error_msg = str(e).lower() error = "invalid_path" if "permission" in error_msg: error = "permission_denied" responses.append( FileDownloadResponse(path=path, content=None, error=error) ) return responses def close(self) -> None: """ 关闭沙箱,释放本机 Docker 资源。 auto_remove=True → force remove:强制删容器(即使还在跑) auto_remove=False → 只 stop:容器停止但保留,可用 docker start 再开 """ if self._container: try: if self.auto_remove: self._container.remove(force=True) else: self._container.stop() except Exception: # 容器可能已被手动删掉或守护进程异常,忽略清理错误 pass self._container = None
docker_run_backend.py
""" DeepAgent + 远程 Docker 沙箱演示入口。 流程概览: 1. ping 远程 Docker API 是否通 2. 用 DockerBackend 在远程机器上起一个容器 3. 创建带 LLM 的 DeepAgent,让它在容器里执行演示任务 4. 流式打印 Agent 输出,结束后关闭容器 """ import asyncio # 异步运行时:用来跑下面的 async def import os # 读环境变量(API Key、DOCKER_BASE_URL 等) import traceback # 出错时打印完整堆栈,方便排查 import docker # docker-py:直接调 Docker API(这里用来 ping) import dotenv # 从 .env 文件加载环境变量 from langchain.chat_models import init_chat_model # 统一入口初始化聊天模型 from create_demo_deep_agent.docker_backend import DockerBackend # 自研:DeepAgents 的 Docker 后端 from langchain_core.messages import BaseMessage # 消息基类(AIMessage / HumanMessage 等) from deepagents import create_deep_agent # 创建带沙箱能力的 Deep Agent # 加载项目根目录(或当前目录)的 .env # override=True:.env 里的值会覆盖系统里已有的同名环境变量 dotenv.load_dotenv(override=True) # ========== 远程 Docker 地址(改这里)========== # os.getenv(键, 默认值):优先读环境变量 DOCKER_BASE_URL;没有则用默认 tcp 地址 # 例: "tcp://192.168.102.129:2375" # 例: "ssh://root@192.168.102.129" DOCKER_BASE_URL = os.getenv("DOCKER_BASE_URL", "tcp://192.168.102.129:2375") async def run_chat_loop(): """异步主流程:连 Docker → 建 Agent → 流式跑任务 → 清理容器。""" print("\n" + "=" * 80) print("欢迎使用 DeepAgent DockerBackend 沙箱演示!") print(f"目标 Docker: {DOCKER_BASE_URL}") print("=" * 80) # ---------- 1. 连通性检查 ---------- # 只做 ping,不创建容器;失败就提前 return,避免后面白跑 try: # DockerClient(base_url=...):连指定守护进程 # .ping():请求 /_ping,通了返回 True / "OK" docker.DockerClient(base_url=DOCKER_BASE_URL).ping() print("✅ 远程 Docker 连接成功") except Exception as e: print(f"❌️ 请检查 docker 环境配置,错误信息为:{e}") print(f"当前地址: {DOCKER_BASE_URL}") print("请确认:服务器 Docker 已启动,且已开放远程 API(或 SSH 可用)。") return # 结束函数,不继续后面的逻辑 # ---------- 2. 启动沙箱容器 ---------- print("正在启动 docker 容器 (Image: python:3.12-slim)...") try: backend = DockerBackend( image="python:3.12-slim", # 用哪张镜像起容器 auto_remove=True, # close() 时强制删掉容器,不留残留 base_url=DOCKER_BASE_URL, # 容器建在远程 Ubuntu,不是本机 ) # backend.id 是完整容器 ID(很长);[:12] 只打印前 12 位,和 docker ps 习惯一致 print(f"容器已经启动成功! (ID: {backend.id[:12]})") except Exception as e: print(f"❌️ 启动 docker 容器失败,错误信息为:{e}") return try: # ---------- 3. 初始化大模型 ---------- deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", # 模型名 model_provider="deepseek", # 提供商,决定用哪套 SDK/协议 base_url="https://api.deepseek.com", # API 地址 api_key=os.getenv("api_key"), # 从 .env 读密钥 temperature=0.7, # 随机性:越高回答越发散 max_tokens=10000, # 单次回复最大 token 数 ) # ---------- 4. 创建 DeepAgent ---------- # backend=backend:Agent 的 execute / 读写文件都会进上面的 Docker 容器 agent = create_deep_agent( model=deepseek_v4_pro, backend=backend, system_prompt=""" 你是一个运行在 Docker 容器中的 AI 助手。 你的任务是演示环境隔离性。 请执行以下步骤: 1. 运行 'cat /etc/os-release' 查看容器操作系统。 2. 运行 'python --version' 确认 Python 环境。 3. 创建文件 '/workspace/hello_docker.py',内容为打印 'Hello from Docker Container!'。 4. 运行该脚本。 """, ) print("\n任务开始执行中...") # ---------- 5. 流式执行并打印输出 ---------- # agent.astream(...):异步流式运行。 # 输入是 LangGraph 状态字典,这里只塞本轮用户消息。 # 每完成图中一个节点(如 model / tools),就 yield 一个 chunk。 # # chunk 长什么样?(示意) # { # "model": {"messages": [AIMessage(...), ...]}, # 或包在 Overwrite 里 # # 有时是 "tools": {"messages": [ToolMessage(...), ...]} # } # 也可能某个值为 None / 空,需要下面一串防护逻辑。 async for chunk in agent.astream( {"messages": [{"role": "user", "content": "请开始执行任务"}]} ): # chunk.items():遍历本轮更新了哪些节点 # node_name: str,如 "model"、"tools" # node_data: 该节点本次输出(可能是 dict,也可能被包成 Overwrite) # 1. 模型节点刚说完话 / 决定调工具 # { # "model": { # "messages": [ # AIMessage( # content="好的,我先查看系统信息...", # tool_calls=[{"name": "execute", "args": {"command": "uname -a"}, "id": "..."}] # ) # ] # } # } # 2.工具节点执行完 # { # "tools": { # "messages": [ # ToolMessage( # content="Linux ...\nPython 3.12.x", # name="execute", # tool_call_id="..." # ) # ] # } # } # 3. 带 Overwrite 包装(你代码里要解包的情况) # { # "model": Overwrite(value={"messages": [AIMessage(...)]}) # # 或 # "model": {"messages": Overwrite(value=[AIMessage(...)])} # } # 4.其它节点(可能没有 messages) # {"todos": [...]} # 任务列表更新 # {"something": None} # 空更新 → 你代码里 continue 掉 for node_name, node_data in chunk.items(): # 没有数据就跳过(有的节点本轮可能是 None / 空) if not node_data: continue # ----- 解包 Overwrite ----- # LangGraph 更新状态时,有时用 Overwrite(value=真实数据) # 表示「用 value 整份覆盖该字段」,而不是和旧状态合并。 # 所以打印前要先取出 .value,才拿到真正的 {"messages": [...]}。 # hasattr(x, "value"):duck typing,有 value 属性就当 Overwrite 解包。 if hasattr(node_data, "value"): node_data = node_data.value # 解包后仍不是字典(异常形态),无法按 messages 解析,跳过 if not isinstance(node_data, dict): continue # 只关心带对话消息的输出(忽略其它状态字段) if "messages" in node_data: messages = node_data["messages"] # messages 本身也可能被 Overwrite 包一层,同样解包 if hasattr(messages, "value"): messages = messages.value # 只要列表里最后一条「有正文的消息」拿来打印 # (流式场景下列表可能累计了多轮,看最新一条通常就够做演示日志) if isinstance(messages, list) and messages: last_msg = messages[-1] # 最新一条 # BaseMessage:LangChain 消息;.content 是文本正文 # 没有 content 的(例如纯 tool_calls 壳)就不打印,避免刷空行 if isinstance(last_msg, BaseMessage) and last_msg.content: print(f"\n[Agent ({node_name})]") print("-" * 40) print(last_msg.content) print("-" * 40) except Exception as e: # Agent 跑模型 / 调工具过程中出错 print(f"❌️ 运行时错误,错误信息为:{e}") traceback.print_exc() # 打印完整 traceback 到 stderr finally: # 无论成功失败都要关容器,避免远程服务器上残留占用资源 print("\n任务执行完毕!正在清理容器...") backend.close() print("容器清理完成!演示结束") # 脚本直接被 python 执行时才跑主流程;被 import 时不自动跑 if __name__ == "__main__": # asyncio.run:创建事件循环并执行协程 run_chat_loop() asyncio.run(run_chat_loop())
输出:
D:\anaconda3\envs\deepagents\python.exe E:\code\InsightFlow\create_demo_deep_agent\docker_run_backend.py ================================================================================ 欢迎使用 DeepAgent DockerBackend 沙箱演示! 目标 Docker: tcp://192.168.102.129:2375 ================================================================================ ✅ 远程 Docker 连接成功 正在启动 docker 容器 (Image: python:3.12-slim)... 容器已经启动成功! (ID: e062c93f824d) 任务开始执行中... [Agent (PatchToolCallsMiddleware.before_agent)] ---------------------------------------- 请开始执行任务 ---------------------------------------- [Agent (tools)] ---------------------------------------- Python 3.12.13 [Command succeeded with exit code 0] ---------------------------------------- [Agent (tools)] ---------------------------------------- PRETTY_NAME="Debian GNU/Linux 13 (trixie)" NAME="Debian GNU/Linux" VERSION_ID="13" VERSION="13 (trixie)" VERSION_CODENAME=trixie DEBIAN_VERSION_FULL=13.6 ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" [Command succeeded with exit code 0] ---------------------------------------- [Agent (tools)] ---------------------------------------- [] ---------------------------------------- [Agent (tools)] ---------------------------------------- Updated file /workspace/hello_docker.py ---------------------------------------- [Agent (tools)] ---------------------------------------- Hello from Docker Container! [Command succeeded with exit code 0] ---------------------------------------- [Agent (model)] ---------------------------------------- 所有 4 个步骤已全部完成!以下是环境隔离性演示的结果总结: --- ### 🔍 1. 容器操作系统 ``` PRETTY_NAME="Debian GNU/Linux 13 (trixie)" NAME="Debian GNU/Linux" VERSION_ID="13" ``` 当前运行在 **Debian GNU/Linux 13 (Trixie)** 容器中。 --- ### 🐍 2. Python 环境 ``` Python 3.12.13 ``` 容器内预装了 **Python 3.12.13**。 --- ### 📄 3. 创建脚本 已在 `/workspace/hello_docker.py` 创建文件,内容为: ```python print('Hello from Docker Container!') ``` --- ### 🚀 4. 运行脚本 ``` Hello from Docker Container! ``` 脚本成功执行,输出符合预期。 --- ### ✅ 总结 这个演示清楚地展示了 **Docker 容器的环境隔离性**: - 容器拥有自己独立的操作系统标识(Debian 13) - Python 环境是容器内独立安装的版本(3.12.13) - 文件系统 `/workspace` 是容器内的隔离工作空间 - 整个运行环境与宿主机完全隔离 ---------------------------------------- 任务执行完毕!正在清理容器... 容器清理完成!演示结束 Process finished with exit code 0
DockerBackend和E2BBackend对比:
| 特性 | DockerBackend | E2BBackend |
|---|---|---|
| 核心定位 | 本地轻量级容器化沙箱 | 云端安全沙箱环境 (SaaS) |
| 部署位置 | 运行在本地机器 (Localhost) | 运行在 E2B 云端集群 (Remote Cloud) |
| 依赖环境 | 需要本地安装并运行 Docker Desktop/Daemon | 仅需安装 e2b Python SDK,无需本地 Docker |
| 资源消耗 | 消耗本地 CPU/内存资源 | 消耗 E2B 云端资源 (不占用本地算力) |
| 启动速度 | 快 (本地镜像启动,毫秒-秒级) | 较快 (云端冷启动约 1-3秒) |
| 网络隔离 | 可配置 (支持完全离线 network_disable=True) | 默认联网 (支持访问公网 API) |
| 持久化 | 支持挂载本地卷 (Volumes) 实现数据持久化 | 临时环境 (会话结束即销毁),数据需手动导出 |
| 适用场景 | • 本地开发/调试 • 数据隐私敏感 (不想数据上云) • 离线环境使用 |
• 生产环境部署 (无需维护 Docker) • 多租户隔离 (每个用户一个云沙箱) • 本地资源受限设备 |
| 成本 | 免费 (使用自有硬件) | 付费 (按使用时长/资源计费) |
| 配置复杂度 | 中 (需管理镜像、卷挂载、Docker 进程) | 低 (API Key 开箱即用) |
6.StoreBackend 使用数据库存储
step1 : 这里我们使用postgresql数据库进行数据的存储,所以需要先系统安装postgresql在本地环境
- brew install postgresql(mac系统)
- sudo apt-get install postgresql(Linux系统)
- https://blog.csdn.net/weixin_54787369/article/details/141348101(windows系统)
step2 : 安装完成之后,可以使用Docker来部署启动postgresql数据库,我这里使用的docker-compose.yml文件启动的,需要把password密码,user用户名,database数据库名称修改为自己的,使用docker-compose up -d启动数据库,如下所示:
#docker-compose.yml 文件: services: postgres: image: postgres:15 container_name: my-postgres environment: - POSTGRES_PASSWORD=123456 - POSTGRES_USER=myuser - POSTGRES_DB=mydatabase ports: - "5432:5432" volumes: - pg_data:/data/db - ./conf.d:/data/conf.d - ./init.sql:/docker-entrypoint-initdb.d/init.sql restart: unless-stopped volumes: pg_data: driver: local
docker直接安装:
docker run -d \ --name my-postgres \ -e POSTGRES_PASSWORD=123456 \ -e POSTGRES_USER=myuser \ -e POSTGRES_DB=mydb \ -p 5432:5432 \ -v postgres_data:/var/lib/postgresql \ postgres
安装依赖:
pip install langgraph-checkpoint-postgres
案例代码:
# ========== 标准库 ========== # asyncio:Python 异步编程库。本脚本主流程是 async 的(连 MCP、跑 agent.astream),所以要用它。 import asyncio # os:读环境变量(如 POSTGRES_URI、api_key),避免把密钥硬编码进代码。 import os # sys:判断当前操作系统(Windows / Linux),用来决定要不要改事件循环策略。 import sys # traceback:异常时打印完整调用栈,方便排查。 import traceback # uuid:生成唯一会话 ID(thread_id),让 LangGraph 能区分不同对话线程。 import uuid # ========== 第三方 / 项目依赖 ========== # dotenv:从 .env 文件加载环境变量(例如 api_key)。 import dotenv # create_deep_agent:创建带「文件系统工具 + 规划能力」的 Deep Agent。 from deepagents import create_deep_agent # StoreBackend:把 Agent 的 ls/read/write 等文件操作,映射到 LangGraph 的 Store(这里是 Postgres)。 from deepagents.backends import StoreBackend # BaseMessage:所有对话消息的基类(Human / AI / Tool 消息都继承它)。 # ToolMessage:工具执行完后返回给模型的结果消息。 from langchain_core.messages import BaseMessage, ToolMessage # init_chat_model:统一方式初始化大模型(DeepSeek / OpenAI 等),不用手写各家 SDK。 from langchain.chat_models import init_chat_model # MultiServerMCPClient:连接一个或多个 MCP 服务器,把远程工具变成 LangChain tools。 from langchain_mcp_adapters.client import MultiServerMCPClient # AsyncPostgresSaver:异步版「检查点存储器」。 # 作用:把对话状态(messages 等)持久化到 PostgreSQL。 # 为什么必须用 Async?因为后面用的是 astream / ainvoke(异步 API)。 # 若用同步 PostgresSaver,调用 aget_tuple 会直接 NotImplementedError。 from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver # PostgresStore:把「长期记忆 / 虚拟文件系统」存在 PostgreSQL 的 store 表里。 from langgraph.store.postgres import PostgresStore # pg_connect:同步方式连一次 Postgres,用来做启动前的连通性探测。 from psycopg import connect as pg_connect # ConnectionPool:连接池。StoreBackend 的文件工具是同步的,所以这里用同步池。 from psycopg_pool import ConnectionPool # Console:Rich 库的彩色终端输出。 from rich.console import Console # ---------- Windows 事件循环兼容 ---------- # 背景: # - asyncio 在 Windows 上默认用 ProactorEventLoop # - psycopg 的「异步连接」不支持 ProactorEventLoop,只支持 SelectorEventLoop # - Linux / macOS 默认本来就是 SelectorEventLoop,所以不需要这行 # sys.platform == "win32":仅在 Windows 上改策略;Linux 下这段 if 不会进。 if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # 从项目目录的 .env 加载环境变量。 # override=True:即使系统里已有同名变量,也用 .env 里的值覆盖。 dotenv.load_dotenv(override=True) # 创建 Rich 控制台对象,后面用 console.print 打彩色日志。 console = Console() async def setup_mcp_tools(): """ 连接 Context7 MCP 服务,拉取可用工具列表。 返回:(mcp_client, tools) - 成功:客户端对象 + 工具列表 - 失败:None, [] (演示仍可继续,只是没有文档查询工具) """ # [dim]...[/dim] 是 Rich 标记:灰色弱化显示。 console.print("[dim]正在连接 Context7(streamable_http)...[/dim]") try: # 配置一个名为 "context7" 的 MCP 服务器。 # transport="streamable_http":用 HTTP 流式协议通信(不是本地 stdio 子进程)。 # url:Context7 官方 MCP 地址。 mcp_client = MultiServerMCPClient({ "context7": { "transport": "streamable_http", "url": "https://mcp.context7.com/mcp", }, }) # get_tools() 是异步的:网络请求拉取工具 schema,转成 LangChain Tool 对象。 tools = await mcp_client.get_tools() console.print(f"[bold green]成功加载 {len(tools)} 个 MCP 工具(HTTP)[/bold green]") # 把 client 也返回:有的场景需要保持连接生命周期;本演示主要用 tools。 return mcp_client, tools except Exception as e: # 连不上也不让整个程序崩溃,返回空工具列表,后面 Agent 只是少了文档查询能力。 console.print(f"[bold yellow]Context7 HTTP 连接失败:{e}[/bold yellow]") return None, [] async def run_store_backend_demo(): """ 演示主流程: 1) 连 MCP 拿工具 2) 连 PostgreSQL,准备 Checkpointer(会话状态)+ Store(虚拟文件) 3) 创建 DeepAgent,backend=StoreBackend(文件写进 Postgres) 4) 流式执行任务(查文档 → 写 md → 读回) 5) 新建另一个 Agent,读同一文件,证明「跨重启」数据还在 """ # ---------- 数据库连接串 ---------- # 格式:postgresql://用户名:密码@主机:端口/库名?参数 # connect_timeout=10:连不上最多等 10 秒就报错,避免一直卡住。 # os.getenv("POSTGRES_URI", 默认值): # - 若环境变量 POSTGRES_URI 存在,优先用它(方便换库,不用改代码) # - 否则用后面这个默认连接串 DB_URI = os.getenv( "POSTGRES_URI", "postgresql://myuser:123456@192.168.163.240:5432/mydb?connect_timeout=10", ) # ================================================================================================= # Step 1. 初始化 MCP 工具 # ================================================================================================= # await:等待异步函数跑完。这里会真正发起 HTTP 请求去拉工具。 mcp_client, mcp_tools = await setup_mcp_tools() # ================================================================================================= # Step 2. 初始化 PostgreSQL:Checkpointer + Store # ================================================================================================= # 打印日志时不要把密码打出来。 # "user:pass@host/db".split("@")[-1] → "host/db"(只保留 @ 后面那截) safe_uri = DB_URI.split("@")[-1] if "@" in DB_URI else DB_URI print(f"\nStep 1: 连接数据库 ...@{safe_uri} 并初始化存储...") # 两个组件分工不同,别搞混: # Checkpointer(AsyncPostgresSaver)→ 存「对话/图状态」(短期记忆,按 thread_id) # Store(PostgresStore) → 存「虚拟文件 / 长期记忆」(StoreBackend 用它) # 为什么一个异步、一个同步? # - astream/ainvoke 走异步 API → 需要 AsyncPostgresSaver # - StoreBackend 的文件工具在线程里跑同步读写 → 需要同步 ConnectionPool + PostgresStore try: # ----- 2.0 先探测:能不能连上库? ----- # 单独先连一次,失败时错误更清晰;比直接卡在复杂组件初始化里好排查。 print(" -> 探测 PostgreSQL 连通性...") # with ... as ...:用完自动关闭连接(即使中间报错也会关)。 # autocommit=True:每条 SQL 立刻生效,不必手动 commit。 with pg_connect(DB_URI, autocommit=True) as probe: # cursor:执行 SQL 的「游标」对象。 with probe.cursor() as cur: # SELECT 1:最轻量的探活语句,成功说明鉴权、库名、网络都 OK。 cur.execute("SELECT 1") # fetchone():取回一行结果(这里是 (1,)),不取也行,主要是确认能执行。 cur.fetchone() print(" -> 探测成功,开始初始化 AsyncPostgresSaver / PostgresStore...") # ----- 2.1 异步 Checkpointer ----- # async with:异步上下文管理器。进入时建立异步连接,退出时自动关闭。 # from_conn_string(DB_URI):用连接串创建 AsyncPostgresSaver 实例。 async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: print(" -> AsyncPostgresSaver.setup()...") # setup():首次使用时创建 checkpoint 相关表(若不存在)。 # 必须 await,因为它是异步方法。 await checkpointer.setup() print(" -> Checkpointer 就绪") # ----- 2.2 同步连接池 + Store ----- # ConnectionPool:维护一组可复用的数据库连接,避免每次读写都新建 TCP。 with ConnectionPool( conninfo=DB_URI, # 连接串 kwargs={"autocommit": True}, # 池里每条连接的默认参数 timeout=10, # 从池里借连接最多等 10 秒 open=True, # 进入 with 时立刻打开池 ) as pool: print(" -> ConnectionPool 就绪") # 把「连接池」交给 PostgresStore。 # 之后 StoreBackend 读写文件,本质就是对 store 表做 JSON 存取。 store = PostgresStore(pool) # ----- 2.3 表结构迁移(建 store 表等)----- # store.MIGRATIONS:一组建表/改表的 SQL 字符串列表。 # 首次跑必须执行;重复执行一般也应是幂等的(视版本而定)。 print(" -> 执行 Store 表迁移...") # pool.connection():从池里借一条连接;with 结束自动还回去。 with pool.connection() as conn: with conn.cursor() as cur: # 逐条执行迁移 SQL。 for migration in store.MIGRATIONS: cur.execute(migration) print("Step 2: 数据库表结构就绪") # ================================================================================================= # Step 3. 创建 DeepAgent # ================================================================================================= # 初始化大模型客户端(这里是 DeepSeek)。 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", # 模型名称 model_provider="deepseek", # 提供商(决定用哪套协议) base_url="https://api.deepseek.com", # API 地址 api_key=os.getenv("api_key"), # 从环境变量/.env 读密钥 temperature=0.7, # 随机性:越高越发散 max_tokens=10000, # 单次回复最大 token ) # backend 必须是「工厂函数」,不能直接传 StoreBackend()。 # 原因:create_deep_agent 在运行时会把 LangGraph 的 runtime(rt)传进来, # StoreBackend 需要这个 rt 才能拿到 store、配置命名空间等。 # lambda rt: StoreBackend(rt) 等价于: # def backend_factory(rt): # return StoreBackend(rt) # 我现在不创建 Backend;等你框架那边有了 rt,把 rt 传给我,我再 StoreBackend(rt)。 # Agent 执行到 write_file 时,框架大致会: # - 造好当前的 rt(里面已经带上你的 store) # - 调用 backend_factory(rt) → 得到 StoreBackend(rt) # - 用这个 Backend 真正读写 Postgres backend_factory = lambda rt: StoreBackend(rt) print("Step 3: 正在初始化 DeepAgent (Backend: StoreBackend -> PostgresStore)...") # 组装 Agent: # model → 用哪个大模型思考 # tools → 额外工具(这里是 Context7 MCP) # backend → 文件系统落在哪里(StoreBackend → Postgres) # store → 必须传入,backend 才能真正读写 PostgresStore # checkpointer → 会话状态落在哪里(AsyncPostgresSaver → Postgres) # system_prompt→ 系统提示词,约束 Agent 行为 agent = create_deep_agent( model=deepseek_v4_pro, tools=mcp_tools, backend=backend_factory, store=store, # 关键:没有 store,StoreBackend 无处可写 checkpointer=checkpointer, # 关键:没有它,astream 无法按 thread 恢复状态 system_prompt="""你是一个高级技术助手。 你的任务是使用 Context7 工具查询关于 'DeepAgents StoreBackend' 的用法。 查询后,创建一个总结文件 '/knowledge/store_backend_notes.md',并写入关键信息。 由于你使用的是 StoreBackend,这个文件将直接存储在 PostgreSQL 数据库中。 最后,请读取该文件以验证存储成功。""" ) # ================================================================================================= # Step 4. 流式执行任务 # ================================================================================================= # thread_id:会话线程 ID。同一个 ID = 同一条对话线(可恢复历史)。 # uuid4():随机生成,避免和旧会话冲突。 thread_id = str(uuid.uuid4()) # LangGraph 约定:把 thread_id 放在 config["configurable"] 里。 config = {"configurable": {"thread_id": thread_id}} task = "请查询 StoreBackend 的用法,并将总结写入 /knowledge/store_backend_notes.md,最后读取它验证。" print(f"Session Thread ID: {thread_id}") print("\n" + "=" * 80) print(f"Step 4: 开始执行任务: {task}") print("=" * 80) # step:自己数「第几次工具调用」,纯展示用。 step = 0 try: # astream:异步流式跑图。每完成一个节点就 yield 一个 chunk。 # # 输入:{"messages": [("user", task)]} # → 等价于塞进一条用户消息,开始本轮对话。 # # stream_mode="updates"(默认也是 updates): # 每个 chunk 是「本节点的增量」,形如: # {"model": {"messages": [AIMessage(...)]}} # {"tools": {"messages": [ToolMessage(...)]}} # 顶层键 = 节点名(model / tools / ...),不是 "messages"! # 所以不能写成 if "messages" in chunk(那样几乎永远打印不出来)。 # # 对比 stream_mode="values": # 每个 chunk 是「完整状态」,顶层才有 "messages" 全历史列表。 async for chunk in agent.astream( {"messages": [("user", task)]}, config=config, stream_mode="updates", ): # chunk.items():把 {"model": {...}, ...} 拆成 (节点名, 节点输出) for node_name, node_data in chunk.items(): # 有的节点本轮可能是 None / 空,跳过。 if not node_data: continue # LangGraph 有时用 Overwrite(value=真实数据) 表示「整份覆盖」。 # 打印前要先取 .value,否则拿不到真正的 dict。 if hasattr(node_data, "value"): node_data = node_data.value # 只要带 messages 的更新;改 todos 等其它字段的先忽略。 if not isinstance(node_data, dict) or "messages" not in node_data: continue msgs = node_data["messages"] # messages 本身也可能被 Overwrite 包一层。 if hasattr(msgs, "value"): msgs = msgs.value # 偶发单条消息而不是 list,统一成 list 方便 for 遍历。 if not isinstance(msgs, list): msgs = [msgs] for msg in msgs: # 只处理标准 LangChain 消息对象。 if not isinstance(msg, BaseMessage): continue # AIMessage 可能带 tool_calls(模型决定要调哪些工具)。 # HumanMessage / ToolMessage 通常没有该属性。 # getattr(..., None) 避免 AttributeError;or [] 把 None 统一成空列表。 tool_calls = getattr(msg, "tool_calls", None) or [] # ----- 情况 A:模型发起工具调用 ----- if tool_calls: step += 1 print(f"\n[Step {step} | {node_name}] 工具调用:") for tc in tool_calls: # tc 是 dict:{"name": "...", "args": {...}, "id": "..."} print(f" • 工具: {tc.get('name')}") print(f" • 参数: {tc.get('args')}") # ----- 情况 B:工具执行结果 ----- if isinstance(msg, ToolMessage): # content 有时不是 str(极少见),先转成字符串再切片预览。 content = msg.content if isinstance(msg.content, str) else str(msg.content) # 太长只显示前 300 字,避免刷屏。 preview = content[:300] + "..." if len(content) > 300 else content print(f"\n[Tool Output ({msg.name})]: {preview}") # ----- 情况 C:模型纯文本回复(没有工具调用)----- elif msg.content and not tool_calls: content = msg.content if isinstance(msg.content, str) else str(msg.content) print(f"\n[Agent | {node_name}]:\n{content}") except Exception as e: # Step 4 跑挂了:打印错误后直接 return,不再做 Step 5(文件可能没写完)。 print(f"❌ 运行时错误: {e}") traceback.print_exc() return # ================================================================================================= # Step 5. 验证持久化:模拟「程序重启」后再读同一文件 # ================================================================================================= print("\n" + "=" * 80) print("Step 5: 验证 StoreBackend (Postgres) 持久化") print("=" * 80) print("\n验证操作: 使用新 Agent 实例 (模拟重启) 读取同一文件...") # 关键点:新建一个 Agent 对象,但复用同一个 store / checkpointer。 # 含义:内存里的旧 agent 对象不要了;只要数据库还在,文件就还在。 verify_agent = create_deep_agent( model=deepseek_v4_pro, backend=backend_factory, store=store, # 同一个 PostgresStore → 读到同一份「虚拟磁盘」 checkpointer=checkpointer, system_prompt="验证助手" ) # ainvoke:一次性跑完,返回最终完整状态(不像 astream 逐步 yield)。 # config 用同一个 thread_id:会话线连续;这里主要验证的是 store 里的文件。 verify_result = await verify_agent.ainvoke({ "messages": [("user", "请读取 /knowledge/store_backend_notes.md 的内容")] }, config=config) # verify_result 是状态 dict,其中 "messages" 是整段对话消息列表。 # [-1]:取最后一条(通常是模型的最终回答)。 last_msg = verify_result["messages"][-1] print("\n[数据库读取验证结果]:") print("-" * 20) print(last_msg.content) print("-" * 20) # Store 表里大致是:prefix=命名空间(默认 filesystem),key=文件路径,value 含 content。 print("\n说明: 'prefix' 对应 StoreBackend 的 namespace (默认为 'filesystem'),'key' 对应文件绝对路径。") # 给用户一条可直接在 psql / 客户端里跑的 SQL,方便亲眼看到落库内容。 sql_query = """ SELECT key, value->>'content' as content, updated_at FROM store WHERE prefix = 'filesystem' AND key = '/knowledge/store_backend_notes.md'; """ print("\n提示: 你可以使用以下 SQL 在数据库中直接查询此文件:") # strip():去掉字符串首尾空白,打印更干净。 print(sql_query.strip()) print("\n演示结束") except Exception as e: # 这里捕获的是「连库 / 建表 / 开池」阶段的失败(Step 2 之前或之中)。 print(f"❌ 数据库连接或初始化失败: {e}") traceback.print_exc() print("排查建议:") print(" 1. 本机能否访问: Test-NetConnection 192.168.163.240 -Port 5432") print(" 2. 服务端 listen_addresses / pg_hba.conf 是否允许你的客户端 IP") print(" 3. 防火墙/安全组是否放行 5432") print(" 4. 可用环境变量覆盖连接串: set POSTGRES_URI=postgresql://user:pass@host:5432/db?connect_timeout=10") # ---------- 程序入口 ---------- # 只有「直接运行本文件」时才会进这里;被别人 import 时不会自动跑演示。 if __name__ == "__main__": # 再设一次策略:有些 IDE/调试器会在 import 阶段就创建事件循环, # 文件顶部那次可能不够,入口处再保险一次(仅 Windows)。 if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # asyncio.run(...):创建事件循环,跑完异步主函数,然后关闭循环。 # 这是启动 async def 的标准写法。 asyncio.run(run_store_backend_demo())
输出:

- 这里看一下Backend vs Checkpointer vs Store 这三者的区别
| 参数组件 | Backend (后端) | Checkpointer (检查点) | Store (存储) |
|---|---|---|---|
| 核心定义 | 环境层 (Environment) | 状态层 (State / Short-term Memory) | 记忆层 (Memory / Long-term Memory) |
| 负责什么? | “外部世界”的交互能力。 即:文件存在哪?代码在哪跑? |
“当前对话”的上下文。 即:刚才说了什么?现在运行到哪一步了? |
“跨会话”的知识积累。 即:用户叫什么名字?上次任务学到了什么? |
| 数据类型 | 非结构化文件 (.py, .md, .txt) 运行时环境 (Shell, Process) |
BaseMessage 列表 (User/AI/Tool Message) Graph 节点状态 |
结构化 JSON 数据 (Key-Value) 用户偏好、长期笔记 |
| 典型实现 | DockerBackend (容器) FilesystemBackend (磁盘) E2BBackend (云沙箱) |
MemorySaver (内存) PostgresSaver (数据库) SqliteSaver (本地DB) |
InMemoryStore (内存) PostgresStore (数据库) |
| 生命周期 | 任务级 (任务结束容器可能销毁) |
线程级 (Thread) (换个 thread_id 就没了) |
全局级 (Global) (所有 thread 都能查到) |
| 形象比喻 | 工作台 / 电脑 | 大脑的工作记忆 (只会死记硬背当前对话) |
日记本 / 知识库 (记录永久信息) |
7.CompositeBackend 使用混合模式
CompositeBackend是 Agent 文件操作的“智能路由器” 。在单一后端模式下,Agent 所有的文件操作(读、写、列出目录)都只能去往同一个地方(要么全是本地磁盘,要么全是 Docker 容器内)。而 CompositeBackend 允许你根据 文件路径前缀 ,将请求分发给不同的后端。
核心优势
- 性能与开销优化 (Performance) 这是最关键的技术优势。
- DockerBackend 的局限 : 向 Docker 容器内读写文件(尤其是大文件)需要经过 Docker Daemon 的 API (如 put_archive / get_archive ),涉及网络通信和打包解包,开销较大。
- CompositeBackend 的解法 : 对于数据文件( /data ),直接通过 FilesystemBackend 进行本地 I/O 操作, 完全绕过了 Docker API ,读写速度是操作系统原生的速度。
- 计算与存储分离 (Decoupling)
- 计算是临时的 : 你的 processor.py 脚本可能只需要运行一次,运行环境(Python 依赖)可能很复杂且容易冲突。放在 Docker 里最合适。
- 数据是永恒的 : 你的 raw_metrics.txt 和 health_report.txt 是业务资产。通过路由直接落盘到宿主机,即使 Docker 容器崩溃、被删除或重启, 数据毫发无损 且立即可在宿主机访问(如代码 Line 212-222 所示的验证步骤)。
- 给予 Agent “混合云” 的能力,Agent 可以像人类工程师一样工作:
- “我在临时的沙箱里写代码测试(Docker)。”
- “测试好了,我把结果保存到公司的共享网盘里(Filesystem/Mount)。”
Agent、Docker、Filesystem 都在同一台:
import asyncio import shutil import os import time from pathlib import Path from dotenv import load_dotenv # DeepAgents 导入 from deepagents import create_deep_agent from deepagents.backends.composite import CompositeBackend from deepagents.backends.filesystem import FilesystemBackend from langchain_openai import ChatOpenAI from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_core.messages import BaseMessage, ToolMessage # 导入 DockerBackend try: from docker_backend import DockerBackend except ImportError: try: from deepagents.backends.docker import DockerBackend except ImportError: DockerBackend = None def print_header(): print("\n" + "="*80) print("DeepAgents CompositeBackend 混合后端演示 (极简版)") print("架构:混合云原生模式 (Docker 执行 + 本地持久化)") print("="*80) async def setup_mcp_tools(): print(" → 正在连接 Context7 MCP 服务器...") try: client = MultiServerMCPClient({ "context7": { "transport": "stdio", "command": "npx", "args": ["-y", "@upstash/context7-mcp@latest"], } }) tools = await client.get_tools() print(" → MCP 工具加载成功") return client, tools except Exception as e: print(f"ERROR: MCP 连接失败: {e}") return None, [] async def run_composite_demo(): load_dotenv(override=True) print_header() if DockerBackend is None: print("严重错误: 未找到 DockerBackend。请确保 docker_backend.py 存在。") return # Step 1 print("\n" + "-"*40) print("步骤 1: 配置混合环境") print("-"*40) host_work_dir = Path("workspace/data_analysis_project").resolve() if host_work_dir.exists(): shutil.rmtree(host_work_dir) host_work_dir.mkdir(parents=True, exist_ok=True) print(f" • 宿主机持久层: {host_work_dir}") container_mount_path = "/data" docker_volumes = { str(host_work_dir): {'bind': container_mount_path, 'mode': 'rw'} } print(f" • 容器挂载: {host_work_dir} ↔ {container_mount_path}") # Step 2 print("\n" + "-"*40) print("步骤 2: 初始化混合后端 (Composite Backend)") print("-"*40) fs_backend = FilesystemBackend(root_dir=host_work_dir, virtual_mode=True) print(" • 正在启动 Docker 容器 (python:3.11-slim)...") docker_backend = DockerBackend( image="python:3.11-slim", auto_remove=True, volumes=docker_volumes ) routes = { container_mount_path: fs_backend } backend = CompositeBackend(default=docker_backend, routes=routes) print("\n[路由表配置]") print(f"1. 默认路由 (/): DockerBackend (临时执行)") print(f"2. 持久化路由 ({container_mount_path}/*): FilesystemBackend (宿主机存储)") # Step 3 print("\n" + "-"*40) print("步骤 3: 部署 Agent") print("-"*40) mcp_client, mcp_tools = await setup_mcp_tools() system_prompt = f"""你是一名在混合环境中工作的高级数据工程师。 环境地图: 1. 执行层 (根目录 `/`): - 临时的 Docker 容器。 - 用于创建脚本 (`.py`) 和运行命令。 - 这里的文会在会话结束后消失。 2. 存储层 (`{container_mount_path}`): - 从宿主机挂载的持久化存储。 - 用于存放 输入 数据和 输出 报告。 - 这里的文件会永久保存。 你的任务: 1. **摄入**: 创建一个文件 `{container_mount_path}/raw_metrics.txt`,内容为 "CPU: 45%, Mem: 60%"。 (注意: 这使用了 'write_file' 工具,该工具通过路由直接写入宿主机文件系统)。 2. **处理**: 创建一个 Python 脚本 `/processor.py` (在根目录),该脚本: - 读取 `{container_mount_path}/raw_metrics.txt`。 - 计算 "健康分数" (模拟一下即可)。 - 将报告写入 `{container_mount_path}/health_report.txt`。 - 打印 "Analysis Complete"。 3. **执行**: 使用 `python /processor.py` 运行脚本。 (注意: 这在 Docker 内部运行。Docker 因为卷挂载能看到这些文件)。 4. **验证**: 读取 `{container_mount_path}/health_report.txt` 并显示它。 """ agent = create_deep_agent( model=ChatOpenAI(model="gpt-4o", temperature=0), tools=mcp_tools, backend=backend, system_prompt=system_prompt ) # Step 4 print("\n" + "-"*40) print("步骤 4: 任务执行") print("-"*40) task_input = "开始工程流水线。" config = {"configurable": {"thread_id": "composite_demo_simple_v1"}} step_count = 0 try: message_history_len = 0 async for event in agent.astream({"messages": [("user", task_input)]}, config=config): if "messages" in event: current_messages = event["messages"] if len(current_messages) > message_history_len: for i in range(message_history_len, len(current_messages)): msg = current_messages[i] # Agent Thinking if isinstance(msg, BaseMessage) and msg.content and not getattr(msg, "tool_calls", None): step_count += 1 print(f"\n[🧠 Agent 思考 (步骤 {step_count})]:\n{msg.content}") # Tool Calls if hasattr(msg, "tool_calls") and msg.tool_calls: step_count += 1 for tc in msg.tool_calls: tool_name = tc['name'] args = tc['args'] # Routing logic visualization target = "Docker 容器 🐳" path_arg = args.get('file_path') or args.get('path') if path_arg and str(path_arg).startswith(container_mount_path): target = "宿主机文件系统 💾" print(f"\n[🛠️ 工具执行 (步骤 {step_count})]:") print(f" • 工具: {tool_name}") # Special handling for code content if tool_name == "write_file" and path_arg and str(path_arg).endswith(".py"): code_content = args.get("content", "") # Print args without content first args_copy = args.copy() args_copy['content'] = "(代码内容如下...)" print(f" • 参数: {args_copy}") print(f" • 路由: → {target}") print(f" • 📝 写入代码内容:\n") print("-" * 20) print(code_content) print("-" * 20) else: print(f" • 参数: {str(args)[:200] + '...' if len(str(args)) > 200 else args}") print(f" • 路由: → {target}") # Tool Outputs if isinstance(msg, ToolMessage): content = msg.content if len(content) > 300: content = content[:300] + "... [已截断]" print(f"\n[↳ 输出]: {content}") message_history_len = len(current_messages) except Exception as e: print(f"\n运行时错误: {e}") # Step 5 print("\n" + "-"*40) print("步骤 5: 宿主机侧验证") print("-"*40) report_path = host_work_dir / "health_report.txt" raw_path = host_work_dir / "raw_metrics.txt" if raw_path.exists(): print(f"✅ 原始数据已找到: {raw_path} (通过直接 FS 路由创建)") else: print(f"❌ 原始数据丢失: {raw_path}") if report_path.exists(): content = report_path.read_text() print(f"\n🏆 持久化验证成功! 文件: {report_path}") print("内容:") print("-" * 20) print(content) print("-" * 20) else: print(f"❌ 报告丢失: {report_path}") # Step 6 print("\n正在关闭基础设施...") if 'docker_backend' in locals() and hasattr(docker_backend, "close"): docker_backend.close() print(" • Docker 容器已终止") print("\n✨ 演示圆满完成!") if __name__ == "__main__": try: # asyncio.run(run_composite_demo()) await run_composite_demo() except RuntimeError as e: if "asyncio.run() cannot be called from a running event loop" in str(e): print("检测到正在运行的事件循环。请在单元格中使用 'await run_composite_demo()'。") else: raise e
Agent 在 Windows,Docker 在远程 Linux:
""" ================================================================================ 【小白必读】这个文件到底在演示什么? ================================================================================ 一句话:让 AI Agent 在 Docker 容器里写脚本、跑代码,同时把「重要数据」存到 不会随容器删除而消失的目录里。 把它想成「厨房 + 保险箱」: - 厨房(Docker 容器):切菜、炒菜(写 .py、执行 python),用完可以拆掉厨房 - 保险箱(挂载卷 /data):放原料和成品报告,厨房拆了,保险箱里的东西还在 Agent 眼里的路径长这样: /processor.py ← 写在「厨房」里,容器删掉就没了(临时) /data/raw_metrics.txt ← 写在「保险箱」里,容器删了还在(持久) /data/health_report.txt 两种运行模式(看你连的是本机 Docker 还是远程 Linux Docker): 【本机 Docker】用 CompositeBackend「按路径分流」 / → DockerBackend (跑命令、写临时脚本) /data/ → FilesystemBackend (直接写本机文件夹;该文件夹同时挂进容器) 【远程 Docker】不能用上面那套分流(本机 Windows 盘 ≠ 远程 Linux 盘) 只用:单一 DockerBackend + 把远程目录挂到容器 /data /data/* → 落在远程服务器硬盘(持久) /processor.py → 落在容器自己的磁盘(临时) 阅读建议:从上到下跟着「步骤1 → 步骤5」走,注释会解释「为什么要这样写」。 ================================================================================ """ # --------------------------------------------------------------------------- # 导入区:把后面要用的「零件」拿进来 # --------------------------------------------------------------------------- import asyncio # Python 异步:可以一边等网络/模型,一边不卡死整个程序 import os # 读环境变量(.env 里的 api_key、DOCKER_BASE_URL 等) import shutil # 删目录、复制文件等文件操作 import dotenv from pathlib import Path # 比字符串更安全的「路径」对象 # deepagents:创建「会用工具干活」的 Agent from deepagents import create_deep_agent # FilesystemBackend:把读写文件落到「真实磁盘文件夹」 from deepagents.backends.filesystem import FilesystemBackend # CompositeBackend:按路径前缀,把请求分给不同 backend(像路由器) from deepagents.backends.composite import CompositeBackend # LangChain:初始化大模型、识别消息类型 from langchain.chat_models import init_chat_model from langchain_core.messages import BaseMessage, ToolMessage # MCP:Model Context Protocol,给 Agent 接外部工具(这里接 Context7 查文档) from langchain_mcp_adapters.client import MultiServerMCPClient # 我们自己写的后端:把 Agent 的「执行命令 / 读写文件」接到 Docker 容器里 from create_demo_deep_agent.docker_backend import DockerBackend from rich.console import Console # 彩色终端打印,好看一点 # 从项目里的 .env 文件加载密钥、Docker 地址等,避免写死在代码里 dotenv.load_dotenv() # 全局彩色打印机(setup_mcp_tools 里会用到) console = Console() # =========================================================================== # 函数 A:可选地连接「查文档」MCP 工具 # =========================================================================== async def setup_mcp_tools(): """ 尝试连接 Context7 MCP 服务,拿到「查库文档」这类工具。 返回值: (mcp_client, tools) - 成功:客户端对象 + 工具列表 - 失败:None, [] —— 演示照样能跑,只是 Agent 少了查文档能力 小白提示: async / await = 异步函数。遇到网络请求时用 await「等一等」, 不会把整个 Python 进程卡死。调用它时也要用 await。 """ # [dim]...[/dim] 是 Rich 的标记语法:灰色弱化显示 console.print("[dim]正在连接 Context7(streamable_http)...[/dim]") try: # 配置一个名叫 "context7" 的 MCP 服务器 # transport="streamable_http":走 HTTP 流式协议(不是本地起一个子进程) # url:Context7 官方提供的 MCP 地址 mcp_client = MultiServerMCPClient({ "context7": { "transport": "streamable_http", "url": "https://mcp.context7.com/mcp", }, }) # get_tools():发网络请求,把远端工具描述转成 LangChain 可用的 Tool tools = await mcp_client.get_tools() console.print(f"[bold green]成功加载 {len(tools)} 个 MCP 工具(HTTP)[/bold green]") # 有的场景需要一直握着 client;本演示主要用 tools return mcp_client, tools except Exception as e: # 连不上也不让整个程序崩溃 —— 返回空列表,后面照常创建 Agent console.print(f"[bold yellow]Context7 HTTP 连接失败:{e}[/bold yellow]") return None, [] # =========================================================================== # 函数 B:演示主流程(核心!从头读到尾就能懂整份脚本) # =========================================================================== async def run_composite_demo(): """ 整份演示按 5 步走: 步骤1 决定「持久数据」挂在哪台机器的哪个目录 步骤2 启动 Docker 容器,组装 backend(本机用 Composite,远程只用 Docker) 步骤3 接上 MCP(可选)+ 写好 system_prompt(告诉 Agent 环境地图和任务) 步骤4 创建 Agent,流式执行:写数据 → 写脚本 → 跑脚本 → 读报告 步骤5 验证持久文件还在,然后关掉容器 """ # ================================================================== # 步骤1:配置混合环境 —— 「保险箱」放在哪? # ================================================================== print("\n" + "=" * 100) print("步骤1:配置混合环境") print("=" * 100) # ----- 1.1 连哪台 Docker? ----- # 环境变量 DOCKER_BASE_URL 决定连本机还是远程。 # 例: "tcp://192.168.102.129:2375" → 连局域网里一台 Ubuntu 上的 dockerd # 若你本机是 Windows Docker Desktop,常见是 npipe://... 这种管道地址 DOCKER_BASE_URL = os.getenv("DOCKER_BASE_URL", "tcp://192.168.102.129:2375") # 判断是不是「远程 Docker」: # - 有地址,且不是 Windows 本机管道 npipe:// → 当成远程 # - 远程时:卷路径必须是 Linux 路径,不能挂你本机的 E:\... is_remote_docker = bool(DOCKER_BASE_URL) and not DOCKER_BASE_URL.startswith("npipe://") # 容器里面,持久目录挂在哪个路径?后面 Agent 会往 /data/... 写东西 container_mount_path = "/data" # ----- 1.2 本机工作目录(Windows 上的一个文件夹)----- # resolve():把相对路径变成绝对路径,避免「当前目录变了路径就错」 local_work_dir = Path("workspace/data_analysis_project").resolve() # 每次演示先清空,保证从干净状态开始(避免上次残留文件干扰判断) if local_work_dir.exists(): shutil.rmtree(local_work_dir) local_work_dir.mkdir(parents=True, exist_ok=True) # ----- 1.3 卷的「宿主机侧」路径(Docker 挂载左边那一头)----- # 【超级重要】Docker 挂载规则: # volumes 左边的路径,必须是「跑 dockerd 那台机器」上真实存在的路径。 # - 本机 Windows Docker → 可以是 E:\code\InsightFlow\... # - 远程 Linux Docker → 必须是 /tmp/... 这种 Linux 路径 # (远程机器根本看不到你的 E 盘,硬挂会报 invalid volume specification) if is_remote_docker: # 远程服务器上的目录;可用环境变量 REMOTE_HOST_DATA_DIR 覆盖默认值 volume_host_path = os.getenv( "REMOTE_HOST_DATA_DIR", "/tmp/deepagents_data_analysis", ) print(f"· 模式:远程 Docker ({DOCKER_BASE_URL})") print(f"· 远程宿主机持久目录:{volume_host_path}") print(f"· 本机工作目录(仅本地镜像/日志,不参与卷挂载):{local_work_dir}") # 远程目录可能还不存在 → 用「一次性小容器」在服务器上 mkdir -p # 为什么 volumes 挂 /tmp?因为默认 volume_host_path 就在 /tmp 下面, # 把宿主机 /tmp 挂进容器 /tmp,容器里 mkdir 才能真正建到远程盘上。 import docker as docker_sdk prep_client = docker_sdk.DockerClient(base_url=DOCKER_BASE_URL) try: prep_client.containers.run( "python:3.12-slim", # 用一个现成小镜像 command=f"mkdir -p {volume_host_path}", # 只干一件事:建目录 volumes={"/tmp": {"bind": "/tmp", "mode": "rw"}}, remove=True, # 跑完立刻删掉这个临时容器,不留垃圾 ) finally: # 无论成功失败都关掉客户端连接,避免资源泄漏 prep_client.close() else: # 本机模式:卷的左边 = 本机文件夹 # 后面 FilesystemBackend 的 root_dir 也指向同一块盘 → 两边看到同一批文件 volume_host_path = str(local_work_dir) print("· 模式:本机 Docker") print(f"· 宿主机持久层:{volume_host_path}") # Docker 卷的标准字典格式: # { 宿主机路径: {"bind": 容器内路径, "mode": "读写权限"} } # 效果:容器里访问 /data ≈ 访问宿主机上的 volume_host_path docker_volumes = { volume_host_path: {"bind": container_mount_path, "mode": "rw"} } print(f"· 容器挂载:{volume_host_path} → {container_mount_path}") # ================================================================== # 步骤2:初始化混合后端 —— Agent 的「手」伸向哪里? # ================================================================== print("\n" + "=" * 100) print("步骤2:初始化混合后端") print("=" * 100) # ----- 2.1 启动一个常驻 Docker 容器当沙箱 ----- # DockerBackend 内部大致会:必要时拉镜像 → 起容器 → 用类似 # tail -f /dev/null 的方式保活(容器一直开着,方便反复 exec) # 之后 Agent 调用 execute / write_file / read_file,都会进这个容器。 print(" • 正在启动 Docker 容器 (python:3.12-slim)...") docker_backend = DockerBackend( image="python:3.12-slim", # 容器用的镜像(带 python 即可跑脚本) auto_remove=True, # 调用 close() 时自动删容器 base_url=DOCKER_BASE_URL, # 连本机还是远程 Docker volumes=docker_volumes, # 把「保险箱」挂到容器 /data ) # ----- 2.2 Composite 路由前缀必须带尾部斜杠 ----- # 写成 "/data/",不要写成 "/data"。 # # 原因(踩坑笔记): # Composite 拼路径时用 prefix[:-1](去掉最后一个字符) # "/data/"[:-1] → "/data" 再拼 "/a.txt" → "/data/a.txt" ✓ # "/data"[:-1] → "/dat" 再拼 "/a.txt" → "/dat/a.txt" ✗ 怪路径 route_prefix = container_mount_path.rstrip("/") + "/" # → "/data/" if is_remote_docker: # ========== 远程模式:只用 DockerBackend,不要 Composite→Docker 分流 ========== # # 为什么?两个坑叠在一起: # # 坑① Composite 会「剥前缀」: # Agent 写 "/data/raw_metrics.txt" # → Composite 剥掉 "/data/" # → 交给子 backend 的是 "/raw_metrics.txt" # FilesystemBackend 需要这种「相对 root 的路径」→ 剥前缀是对的 # DockerBackend 需要容器内绝对路径 "/data/..." → 剥前缀就错了 # 文件跑到容器根目录 /raw_metrics.txt,卷上的 /data 还是空的 # 后面 python 读 /data/raw_metrics.txt → FileNotFoundError # # 坑② 磁盘根本不是同一块: # 本机 FilesystemBackend 写的是 Windows 盘 # 远程容器挂的是 Linux 盘 # 两边对不上,Composite(Filesystem+Docker) 在远程场景不适用 # # 结论:远程只用「单一 DockerBackend + 卷」: # /data/* → 远程宿主机目录(持久) # /processor.py → 容器可写层(销毁即消失) backend = docker_backend print("\n[后端配置 · 远程 Docker]") print("1. 单一 DockerBackend + 卷挂载 /data(持久)") print("2. 未用 Composite→Docker 路由(避免路径前缀被剥掉写错位置)") print("3. 标准 Composite(Filesystem+Docker) 请在本机 Docker 下演示") else: # ========== 本机模式:标准 CompositeBackend 用法 ========== # # 数据流示意(写 /data/a.txt 时): # Agent # → Composite 看见前缀 /data/,交给 FilesystemBackend # → 剥成 /a.txt,写到 本机 local_work_dir/a.txt # 同时:local_work_dir 又被挂进容器的 /data # → 容器里 python 读 /data/a.txt,读到的是同一份文件 # # CompositeBackend 两个关键参数: # default:路径没命中任何路由时用谁 # (这里是 Docker → 负责跑命令、写 /processor.py) # routes :{ 路径前缀: 后端 } # (这里 /data/ → 本机文件系统) files_backend = FilesystemBackend( root_dir=local_work_dir, virtual_mode=True, # 虚拟路径模式:Agent 仍用 /data/... 这种「看起来像容器」的路径 ) backend = CompositeBackend( default=docker_backend, routes={route_prefix: files_backend}, ) print("\n[路由表配置 · 本机 Composite]") print("1. 默认路由(/): DockerBackend (临时执行)") print(f"2. 持久化路由 {route_prefix}: FilesystemBackend → {local_work_dir}") # ================================================================== # 步骤3:准备 MCP 工具 + 系统提示词(告诉 Agent「世界长什么样」) # ================================================================== print("\n" + "=" * 100) print("步骤3:创建混合 Agent") print("=" * 100) # 可选工具;连不上就是 tools=[],不影响后面主任务 mcp_client, tools = await setup_mcp_tools() # system_prompt = 给 Agent 的「说明书」 # 必须把路径写清楚,否则模型可能把数据写到容器根目录,而不是 /data # f"""...""" 里的 {container_mount_path} 会替换成 "/data" system_prompt = f""" 你是一名在混合环境中工作的高级数据工程师。 环境地图: 1.执行层(根目录 '/') - 临时的Docker容器 - 用于创建脚本('.py')和运行命令 - 这里的文件会在会话结束后消失 2.存储层('{container_mount_path}'): - 挂载到 Docker 宿主机的持久化目录 - 用于存放输入数据和输出报告 - 这里的文件会保留下来 你的任务: 1. **摄入**: 创建一个文件 `{container_mount_path}/raw_metrics.txt`,内容为 "CPU: 45%, Mem: 60%"。 2. **处理**: 创建一个 Python 脚本 `/processor.py` (在根目录),该脚本: - 读取 `{container_mount_path}/raw_metrics.txt`。 - 计算 "健康分数" (模拟一下即可)。 - 将报告写入 `{container_mount_path}/health_report.txt`。 - 打印 "Analysis Complete"。 3. **执行**: 使用 `python /processor.py` 运行脚本。 (注意: 这在 Docker 内部运行。Docker 因为卷挂载能看到 /data 下的文件)。 4. **验证**: 读取 `{container_mount_path}/health_report.txt` 并显示它。 """ # ================================================================== # 步骤4:创建 Agent 并流式执行任务 # ================================================================== print("\n" + "=" * 100) print("步骤4:任务执行") print("=" * 100) # ----- 4.1 初始化大模型 ----- # init_chat_model:LangChain 的统一入口,按名字接不同厂商的模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), # 从 .env 读,不要把密钥提交进 git temperature=0.7, # 随机性:0 更死板,越高越发散 max_tokens=10000, # 单次回复最长 token 数 ) # ----- 4.2 组装 Agent ----- # create_deep_agent 四个核心零件: # model → 谁负责「思考 / 决定下一步」 # tools → 额外工具(这里是 MCP 查文档,可为空) # backend → 文件读写、命令执行落到哪(上面组好的 docker 或 composite) # system_prompt → 行为约束 + 环境说明 agent = create_deep_agent( model=deepseek_v4_pro, tools=tools, backend=backend, system_prompt=system_prompt ) # thread_id:会话线 ID。同一个 ID 可以接着上次对话;这里写死方便反复跑演示 config = {"configurable": {"thread_id": "composite_simple_v1"}} step_count = 0 # 仅用于终端打印「第几步」,跟 Agent 内部状态无关 try: # ----- 4.3 流式跑 Agent ----- # astream:异步流式执行。每完成图里的一个节点(如 model / tools), # 就吐出一个 chunk,方便我们实时打印。 # # stream_mode="updates" 时,chunk 长这样: # {"model": {"messages": [...]}} # {"tools": {"messages": [...]}} # 注意:顶层键是「节点名」,不是 "messages"! # 所以要用 for node_name, node_data in chunk.items() 来拆。 async for chunk in agent.astream( {"messages": [{"role": "user", "content": "请完成任务"}]}, config=config, stream_mode="updates", ): for node_name, node_data in chunk.items(): # 空更新:跳过 if not node_data: continue # LangGraph 有时用 Overwrite(value=真实数据) 整份覆盖状态, # 这种对象没有普通 dict 的键,要先取出 .value if hasattr(node_data, "value"): node_data = node_data.value # 我们只关心带 messages 的更新(todos 等其它字段先忽略) if not isinstance(node_data, dict) or "messages" not in node_data: continue msgs = node_data["messages"] if hasattr(msgs, "value"): msgs = msgs.value # 偶发只来一条消息,统一包成 list,后面 for 好好写 if not isinstance(msgs, list): msgs = [msgs] for msg in msgs: # 不是标准消息对象就跳过(防御性写法) if not isinstance(msg, BaseMessage): continue # AIMessage 可能带 tool_calls(「我要调用某某工具」); # 没有就当成空列表,避免后面 None 报错 tool_calls = getattr(msg, "tool_calls", None) or [] # ----- 情况 A:模型纯文本思考 / 回答(没有工具调用)----- if msg.content and not tool_calls and not isinstance(msg, ToolMessage): step_count += 1 print(f"\n[🧠 Agent 思考 (步骤 {step_count} | {node_name})]:\n{msg.content}") # ----- 情况 B:模型决定调用工具(write_file / execute 等)----- if tool_calls: step_count += 1 for tc in tool_calls: tool_name = tc["name"] # 例如 "write_file"、"execute" args = tc["args"] # 工具参数字典 # 下面的「路由」只是打印提示,帮你对照路径落在哪一层。 # 远程模式下并没有真正的 Composite 分流, # 但路径以 /data 开头仍表示「持久卷」。 target = "Docker 容器 🐳" path_arg = args.get("file_path") or args.get("path") if path_arg and str(path_arg).startswith(container_mount_path): target = "持久化卷 /data 💾" print(f"\n[🛠️ 工具执行 (步骤 {step_count})]:") print(f" • 工具: {tool_name}") # 写 .py 时:把代码单独漂亮打印出来,方便你检查脚本对不对 if tool_name == "write_file" and path_arg and str(path_arg).endswith(".py"): code_content = args.get("content", "") args_copy = dict(args) # 参数摘要里不要塞整段代码,改成占位提示 args_copy["content"] = "(代码内容如下...)" print(f" • 参数: {args_copy}") print(f" • 路由: → {target}") print(" • 📝 写入代码内容:\n") print("-" * 20) print(code_content) print("-" * 20) else: args_str = str(args) # 参数太长只预览前 200 字符,避免终端刷屏 print(f" • 参数: {args_str[:200] + '...' if len(args_str) > 200 else args}") print(f" • 路由: → {target}") # ----- 情况 C:工具执行完返回的结果(ToolMessage)----- if isinstance(msg, ToolMessage): content = msg.content if isinstance(msg.content, str) else str(msg.content) if len(content) > 300: content = content[:300] + "... [已截断]" print(f"\n[↳ 输出]: {content}") except Exception as e: # 任何一步炸了:打印错误 + 完整堆栈,方便排查 print(f"❌️ 混合 Agent 运行失败:{e}") import traceback traceback.print_exc() # ================================================================== # 步骤5:持久化验证 —— 容器可以没了,文件还在吗? # ================================================================== print("\n" + "=" * 100) print("步骤5:持久化验证") print("=" * 100) if is_remote_docker: # 远程:文件在 Linux 服务器硬盘上,Windows 的 Path.exists() 根本看不到。 # 所以在关容器之前,用 docker exec(封装在 docker_backend.execute)去 cat 验证。 print(f"· 在远程卷中检查文件(容器内路径 {container_mount_path}/...)") for name in ("raw_metrics.txt", "health_report.txt"): path_in_container = f"{container_mount_path}/{name}" result = docker_backend.execute(f"cat {path_in_container}") # exit_code == 0 表示命令成功;output 是标准输出文本 if result.exit_code == 0: print(f"\n✅ 找到 {path_in_container}") print(f" • 内容:\n{result.output}") else: print(f"\n❌ 缺少 {path_in_container}: {result.output}") print(f"\n提示: 容器删除后,文件仍在远程主机 {volume_host_path}/ 下,可 SSH 上去查看。") else: # 本机:FilesystemBackend 写的就是 local_work_dir,直接读磁盘即可 report_path = local_work_dir / "health_report.txt" raw_path = local_work_dir / "raw_metrics.txt" if raw_path.exists(): print(f"✅ 原始数据已经找到 💾 路径:{raw_path}") print(f" • 内容:\n{raw_path.read_text()}") else: print(f"❌ 原始数据丢失:{raw_path}") if report_path.exists(): print(f"\n✅ 持久化验证成功 🎉 文件:{report_path}") print("\n" + "=" * 100) print(f" • 内容:\n{report_path.read_text()}") print("=" * 100) else: print(f"❌ 报告丢失:{report_path}") # ---------- 收尾:停掉并删除容器 ---------- print("\n正在关闭基础设施...") # locals():当前函数里已创建的局部变量名集合 # 防止步骤2半路失败、docker_backend 还没创建时,这里 NameError if "docker_backend" in locals() and hasattr(docker_backend, "close"): docker_backend.close() print(" • Docker 容器已终止") # 记住: # /data 下的文件在「卷」上 → 容器没了,文件还在 # /processor.py 在容器自己的磁盘 → 会跟着容器一起消失 print("\n✨ 演示圆满完成!") # =========================================================================== # 程序入口 # =========================================================================== # 只有「直接运行本文件」时才启动演示: # python cim_posite_backend_demo.py → 会跑 # from xxx import run_composite_demo → 不会自动跑(方便别人当库引用) if __name__ == "__main__": # asyncio.run:创建事件循环,跑完异步主函数后自动清理 asyncio.run(run_composite_demo())

5.3.Human-in-the-loop(HITL) 人工干预
Human-in-the-loop (HITL):对于 write_file 或 delegate_task 等关键操作,利用 LangGraph 的中断机制加入人工审批。
异步运行:DeepAgent 的任务通常耗时较长,务必使用异步 Webhooks 接收结果。
监控与调试:强烈建议结合 LangSmith 使用。由于 DeepAgent 内部有复杂的子 Agent 递归调用,使用 LangSmith 的 Tracing 功能是排查问题的有效手段。
后端挂载:在生产环境中,建议将 VFS 挂载到云端存储(如 S3),以防止容器重启导致 Agent 的"记忆"丢失。
interrupt_on:这个参数其实是一个HITL的开关,就是把HITL的中间件插入到DeepAgent的执行流程中,当DeepAgent执行到需要人工审批的操作时,就会中断执行,等待人工审批。
- 类型 : dict[str, bool | InterruptOnConfig]
- 作用 : 映射“工具名称”到“中断配置”。
- 示例 : interrupt_on={“write_file”: True} 表示当 Agent 试图调用 write_file 工具时,程序会暂停(Suspend),等待人工(Human)在 LangGraph 层面进行 Approve 、 Reject 或 Edit 操作后才能继续。
""" 一句话:让 AI Agent 干活时,在「调用搜索工具之前」先停下来,等你(管理员)点头同意,再继续执行。 你下任务 → Agent 思考 → 想调搜索工具 → 🛑 暂停问你 ↓ 你输入 y 批准 ↓ 真正搜索 → 写文件 → 完成 1.准备:模型 + Tavily 搜索 + Agent,并打开 interrupt_on={"tavily_search": True} 2.第一次 astream:跑任务,预计停在「要搜索」这一步 3.aget_state:看卡在哪、准备调什么工具、参数是什么 4.input():终端里问你同不同意 5.第二次 astream(Command(resume=...)):你批准后,从断点继续跑完 生活类比: - Agent 像一个助理 - 助理想「上网搜索」时,必须先问你:「我可以搜吗?」 - 你说「可以(y)」→ 助理继续搜,并写总结文件 - 你说「不行」→ 本演示里就停住了(没有继续) 关键名词(后面注释会反复出现): - Agent:会自己决定「下一步用什么工具」的 AI 程序 - 工具(tool):Agent 能调用的能力,这里是 Tavily 联网搜索 - 中断(interrupt):执行到某一步先暂停,不立刻真正调用工具 - HITL:Human In The Loop,人在回路里做审批 - checkpointer:把对话/状态存起来,暂停后还能从断点恢复 - thread_id:会话编号,同一编号 = 同一段对话历史 """ import asyncio import os import dotenv from deepagents.backends import FilesystemBackend from langchain.chat_models import init_chat_model from langchain_tavily import TavilySearch from deepagents import create_deep_agent from langgraph.checkpoint.memory import InMemorySaver from langchain.agents.middleware.human_in_the_loop import HITLResponse, ApproveDecision from langgraph.types import Command # 1. 初始化环境 dotenv.load_dotenv() async def run_human_in_the_loop_demo(): """ 示例1:基础中断功能 (封装版本),在工具调用中断,让用户确认是否继续执行 流程概览(跟着这个走就不迷路): 1) 创建模型 + 搜索工具 + Agent(并打开 interrupt_on) 2) 第一次 astream 跑任务 → 预计会在搜索前暂停 3) aget_state 看「卡在哪了」、要调什么工具 4) input() 问你同不同意 5) 同意后用 Command(resume=...) 第二次 astream 继续跑完 :return: """ print("\n" + "="*100) print("示例1:interrupt_on 使用") print("=" *100) print("\n基础中断功能,在工具调用中断,让用户确认是否继续执行\n") # 初始化模型 deepseek_v4_pro = init_chat_model( model="deepseek-v4-pro", model_provider="deepseek", base_url="https://api.deepseek.com", api_key=os.getenv("api_key"), # 从 .env 读,不要把密钥提交进 git temperature=0.7, # 随机性:0 更死板,越高越发散 max_tokens=10000, # 单次回复最长 token 数 ) # 初始化搜索工具 search_tool = TavilySearch(max_results=3) # 哪些工具调用前必须人工审批(后面统计 pending 时会用到) interrupt_on = {"tavily_search": True} # 创建agent agent = create_deep_agent( model=deepseek_v4_pro, tools=[search_tool], backend=FilesystemBackend(root_dir="/workspace", virtual_mode=True), # root_dir:Agent 读写文件时相对这个目录, virtual_mode=True:给 Agent 看的是「虚拟路径」,更安全、路径更干净 checkpointer=InMemorySaver(), # 没有checkpointer中断后就【记不住卡在哪里】,无法resume interrupt_on=interrupt_on, # 调用 tavily_search 前暂停,等人批准/拒绝/编辑 ) task = "搜索关于Google ADK框架的最新功能信息,创建一个总结文件" config = {"configurable": {"thread_id": "user_001"}} # 追踪已打印的消息数量,避免重复打印 message_history_len = 0 def print_new_messages(current_messages): """只打印相对上次新增的消息,避免流式输出刷屏。""" nonlocal message_history_len if len(current_messages) <= message_history_len: return for i in range(message_history_len, len(current_messages)): msg = current_messages[i] if msg.type == "ai": if hasattr(msg, "tool_calls") and msg.tool_calls: # 可能一次发起多个工具调用,全部打印 for tc in msg.tool_calls: print(f"🔧 [AI决定工具调用] {tc['name']}") print(f" 参数: {tc['args']}") elif msg.content: print(f"[AI | {agent.name}]:\n{msg.content}") elif msg.type == "tool": content = msg.content if isinstance(msg.content, str) else str(msg.content) if len(content) > 300: print(f"\n[工具输出]:\n{content[:300]}...\n") else: print(f"\n[工具输出]:\n{content}\n") message_history_len = len(current_messages) # ====================================================================== # 第一次执行:预期会在调用搜索工具前停住 # ====================================================================== print("[第一次执行 - 预期中断]") async for chunk in agent.astream( {"messages": [{"role": "user", "content": task}]}, config=config, ): if "messages" in chunk: print_new_messages(chunk["messages"]) # ====================================================================== # HITL 循环:模型可能一次挂起多个工具,也可能多次中断 # 规则:decisions 条数必须 == 本次挂起的工具调用数 # ====================================================================== while True: # aget_state:异步读状态;get_state 是同步的,不能 await agent_state = await agent.aget_state(config) print(f"\n ⏸️ 执行状态:{agent_state.next}") # 没有未完成任务 → 流程结束(正常跑完,或未触发 interrupt) if not agent_state.tasks: print("\n[系统]:流程已结束(没有待处理的中断)。") if agent_state.values.get("messages"): last_message = agent_state.values["messages"][-1] if last_message.type == "ai" and last_message.content: print(f"\n[最终回复]:\n{last_message.content}") break print(f"\n--- 📝 执行已暂停:(HITL Middleware) ---") print(f"下一步骤(Next): {agent_state.next}") last_message = agent_state.values["messages"][-1] if not (hasattr(last_message, "tool_calls") and last_message.tool_calls): print("[系统]:暂停了,但最后一条消息没有 tool_calls,无法审批。") break # 只统计「配置了 interrupt_on」的工具调用(其它工具不会挂起,也不需要 decision) pending_tool_calls = [ tc for tc in last_message.tool_calls if tc["name"] in interrupt_on ] if not pending_tool_calls: print("[系统]:没有需要人工审批的工具调用。") break print("\n======================================人工介入==========================================") print(f"待审批数量: {len(pending_tool_calls)}(每个都要有一条 decision)") for idx, tc in enumerate(pending_tool_calls, start=1): print(f"\n[{idx}] 工具: {tc['name']}") print(f" 参数: {tc['args']}") approval = input("\n是否批准以上全部操作?(y/n):").strip().lower() if approval != "y": print("\n[系统]:操作被拒绝,演示结束。(本演示仅完整处理 'y')") break print("\n[系统]:已批准,继续执行...") # 关键:有几个挂起的工具,就要有几条 ApproveDecision,少一条就会报错 hitl_response = HITLResponse( decisions=[ApproveDecision(type="approve") for _ in pending_tool_calls] ) async for chunk in agent.astream( Command(resume=hitl_response), config=config, # 必须同一 thread_id,才能接到断点 stream_mode="values", ): if "messages" in chunk: print_new_messages(chunk["messages"]) # 恢复后再看状态:若又触发中断,while 会再问一次;否则下一轮 break if __name__ == '__main__': asyncio.run(run_human_in_the_loop_demo())
输出:
六、总结
DeepAgents 采用 中间件模式 来增强 Agent 能力,这些中间件在 create_deep_agent 时自动装配:
- FilesystemMiddleware :
- 提供 ls , read_file , write_file , edit_file , glob , grep , execute 等标准工具。
- 亮点功能 : 大结果自动转存 。当工具(如搜索或爬虫)返回内容过长时,自动截断并保存到文件系统,仅返回文件路径给 LLM,极大节省 Token 并防止 Crash。
- TodoListMiddleware :
- 拦截 LLM 输出的 todo_list ,将其解析为结构化状态。
- 支持任务的增删改查(CRUD)和状态流转(Pending -> Completed)。
- SubAgentMiddleware :
- 自动创建 task 工具。
- 支持 General Purpose Agent (通用分身)和 Custom Agent (专家分身)。
- 实现父子 Agent 间通过文件系统交换数据,无需序列化传递大量文本。
- HumanInTheLoopMiddleware :
- 通过 interrupt_on 参数配置。
- 支持在特定工具调用前(如 write_file , execute )暂停,等待人工审批、修改或拒绝。
- PatchToolCallsMiddleware :
- 自动检测并修复 LLM 产生的“悬空”工具调用(Dangling Tool Calls),增强稳定性。

浙公网安备 33010602011771号