主题
工具系统与内置工具
本章目标:
- 讲清
get_available_tools()如何把 config 定义工具 / MCP 工具 / 内置工具 / 子代理 task 四类来源装配成一份去重、可绑定的工具清单。- 逐个剖析各内置工具(present_files / ask_clarification / view_image / setup_agent / update_agent / tool_search / invoke_acp_agent / skill_manage)的职责与绑定条件。
- 说清 deferred tool(延迟工具) 机制:为什么需要、注册表如何工作、与
DeferredToolFilterMiddleware如何配合,以及如何经config.yaml用resolve_variable扩展自定义工具。
TL;DR
DeerFlow 的工具层只有一个入口函数 get_available_tools(),它统一汇聚四类来源并按"config 优先"做名称去重,产出一份 list[BaseTool] 交给 LangGraph agent 绑定。配置工具经 resolve_variable(cfg.use, BaseTool) 反射加载;内置工具按运行时条件(vision / bootstrap / skill_evolution / subagent_enabled)有选择地追加;MCP 工具来自带 mtime 失效的缓存;tool_search + DeferredToolRegistry 把海量 MCP 工具"延迟"成只暴露名字、用时再取 schema,以省 context token。异步独占工具会被自动包一层同步 wrapper。
Overview
为什么工具装配要"统一 + 可插拔"?DeerFlow 是一个 super agent harness,同一个 lead agent 在不同运行场景下需要的工具集合差别极大:有视觉模型时要带 view_image、bootstrap 创建自定义 agent 时要带 setup_agent、自定义 agent 自更新时要带 update_agent、开启子代理时要带 task、配了 MCP 服务器时要并入几十个外部工具、配了 ACP agent 时要并入 invoke_acp_agent。如果每个调用方各自拼装工具列表,极易出现「LLM 看到一个名字、运行时路由认另一个名字」的「not a valid tool」错误(issue #1803),也很难做统一去重与权限收敛。
因此 DeerFlow 把所有装配逻辑收敛到唯一函数 get_available_tools()(backend/packages/harness/deerflow/tools/tools.py:44-220)tools.py:44-220。它的核心设计取舍:
- 可插拔:配置工具通过
config.yaml的tools[]段 + 反射resolve_variable动态加载,无需改代码即可增删工具 tools.py:67-73。 - 条件装配:内置工具按运行时上下文与配置开关选择性追加,而不是全量暴露 tools.py:91-111。
- 名字即契约:以工具对象自身的
.name为绑定锚点,并对 configname与.name不一致发出告警,从源头消除路由歧义 tools.py:79-86。 - 省 context:当 MCP 工具过多时,用 deferred 机制只在 system prompt 暴露名字,真正 schema 走
tool_search按需拉取 tool_search.py:1-10。
Architecture
get_available_tools() 的签名包含四个调节参数:groups(按工具组过滤)、include_mcp(是否并入 MCP)、model_name(决定是否带视觉工具)、subagent_enabled(是否带 task),以及关键字参数 app_config(显式传入避免重复读盘)tools.py:44-66。
四类来源及其装配规则:
来源 1 — config 定义工具:从 config.tools 取出(可按 groups 过滤),若当前 LocalSandboxProvider 不允许 host bash 则剔除 host-bash 工具,再对每个 cfg.use 调用 resolve_variable(cfg.use, BaseTool) 反射出工具对象 tools.py:67-73。ToolConfig 三个必填字段 name / group / use,use 形如 deerflow.community.ddg_search.tools:web_search_tool tool_config.py:11-20。resolve_variable 按 module:variable 拆分、import、getattr,并对缺失依赖给出 uv add 安装提示 resolvers.py:25-70。
来源 2 — 内置工具(条件追加):基础集 BUILTIN_TOOLS = [present_file_tool, ask_clarification_tool] tools.py:15-18。条件追加:skill_evolution.enabled 时加 skill_manage_tool tools.py:92-96;subagent_enabled 时加 task tools.py:99-101;模型 supports_vision 时加 view_image_tool tools.py:104-111。注意 setup_agent / update_agent 不在 get_available_tools 内追加,而是由 lead agent 工厂按 is_bootstrap / agent_name 在外层拼接(见 Components 节)。
来源 3 — MCP 工具:include_mcp=True 时,先用 ExtensionsConfig.from_file() 读最新磁盘配置(因为 Gateway 在独立进程改了配置),有启用的 MCP 服务器才取 get_cached_mcp_tools();若 config.tool_search.enabled 则把 MCP 工具注册进 DeferredToolRegistry 并追加 tool_search 工具 tools.py:113-184。MCP 细节见 21 章。
来源 4 — ACP 工具:若 config.yaml 配了 acp_agents,则 build_invoke_acp_agent_tool() 生成单个 invoke_acp_agent 工具(描述里内联可用 agent 列表)tools.py:186-201。
最后拼接顺序为 loaded_tools + builtin_tools + mcp_tools + acp_tools,按 .name 去重——同名时靠前者(config 定义工具)优先,重复名打 warning 并跳过 tools.py:205-220。装配后还有一道处理:_ensure_sync_invocable_tool 对只有 coroutine、没有 func 的异步工具补一个同步 wrapper,使同步 agent 路径也能调用 tools.py:37-41,wrapper 用一个共享线程池 asyncio.run 执行协程 sync.py:18-36。
Components / Subsystems
模块布局:tools/__init__.py 仅导出 get_available_tools 与惰性 skill_manage_tool init.py:1-11;tools/types.py 定义所有 DeerFlow 工具共用的运行时类型 Runtime = ToolRuntime[dict[str, Any], ThreadState],用具体 dict 而非 ContextT 以规避 Pydantic 序列化告警 types.py:1-11;tools/builtins/__init__.py 汇总导出 6 个内置工具对象 builtins/init.py:1-15。
present_files —— 把产物文件呈现给用户
present_file_tool 以 @tool("present_files", parse_docstring=True) 声明,接收 filepaths: list[str],逐个调 _normalize_presented_filepath 归一化路径,只允许 /mnt/user-data/outputs 下的文件,越界即返回 Error: Only files in ... can be presented 的 ToolMessage present_file_tool.py:83-122。它通过 Command(update={"artifacts": ...}) 写状态,由 merge_artifacts reducer 去重合并,所以可与其他工具并行调用 present_file_tool.py:115-121。绑定条件:基础内置工具,始终绑定 tools.py:15-18。
ask_clarification —— 向用户追问并中断
ask_clarification_tool 以 return_direct=True 声明,参数含 question 与枚举 clarification_type(missing_info / ambiguous_requirement / approach_choice / risk_confirmation / suggestion),函数体仅返回占位字符串——真正逻辑由 ClarificationMiddleware 拦截并 Command(goto=END) 中断执行 clarification_tool.py:6-55。绑定条件:基础内置工具,始终绑定。
view_image —— 读图为 base64
view_image_tool 接收 image_path,只接受 /mnt/user-data/{workspace,uploads,outputs} 下路径 view_image_tool.py:14-30。它做多重校验:路径合法性、文件存在、扩展名属于 jpg/jpeg/png/webp、大小不超过 20MB,并通过文件魔数 _detect_image_mime 校验真实类型与扩展名一致,最后 base64 编码写入 viewed_images 状态(由 merge_viewed_images reducer 处理)view_image_tool.py:49-162。绑定条件:仅当解析出的模型 model_config.supports_vision 为真时才追加 tools.py:107-111。
setup_agent —— bootstrap 专用,创建新自定义 agent
setup_agent 接收 soul / description / skills,从 runtime.context["agent_name"] 取目标 agent 名,把 SOUL.md(以及自定义 agent 的 config.yaml)写入按用户隔离的目录 paths.user_agent_dir(user_id, agent_name),失败时若目录是本次新建则回滚删除 setup_agent_tool.py:16-79。绑定条件:仅在 is_bootstrap=True 时由 lead agent 工厂以 get_available_tools(...) + [setup_agent] 追加 agent.py:413-415。
update_agent —— 自定义 agent 自更新
update_agent 让已存在的自定义 agent 在普通对话中持久化自身改动:仅传入的字段被更新(soul / description / skills / tool_groups / model),写入前会校验 model 是否在 config 中存在,采用「先全部写临时文件、再逐个原子 rename」的两阶段提交,任一失败不会留下 config 已更新而 SOUL 未更新的半成品 update_agent_tool.py:70-245。绑定条件:agent_name 已设置且 is_bootstrap=False 时,lead agent 工厂以 extra_tools = [update_agent] if agent_name else [] 追加;默认 agent 看不到此工具 agent.py:429-436。
tool_search —— deferred tool 的运行时发现入口
tool_search 接收 query,从 get_deferred_registry() 取 ContextVar 级注册表,按三种查询语法搜索:select:name1,name2 精确取名、+keyword rest 名字必含关键词、其余按正则匹配 name+description tool_search.py:69-109。命中后用 convert_to_openai_function 序列化 schema 返回,并立刻 registry.promote() 把这些工具从 deferred 注册表移除(后续模型调用即可正常 bind)tool_search.py:164-202。绑定条件:include_mcp 且有启用的 MCP 服务器且 config.tool_search.enabled=True tools.py:132-180。
invoke_acp_agent —— 调用外部 ACP agent
build_invoke_acp_agent_tool(agents) 用 StructuredTool.from_function 动态生成单个工具,描述里内联所有可用 agent 名与说明;运行时 spawn ACP 子进程、在每线程独立的 acp-workspace 工作目录里 prompt,并收集流式文本返回 invoke_acp_agent_tool.py:139-257。绑定条件:config.yaml 配了非空 acp_agents tools.py:186-201。
skill_manage —— 管理自定义技能
skill_manage_tool 以 async 实现,支持 create / patch / edit / delete / write_file / remove_file 六种 action 管理 skills/custom/ 下技能,写入前过 scan_skill_content 安全扫描(block 抛错),按 skill 名加 asyncio.Lock 串行化,并在尾部用 make_sync_tool_wrapper 挂同步 func skill_manage_tool.py:53-238。绑定条件:config.skill_evolution.enabled=True(config.example.yaml 默认 false)tools.py:92-96。技能系统细节见 22 章。
task —— 子代理委派
task_tool 以 @tool("task") 声明,接收 description / prompt / subagent_type,内部也会再调 get_available_tools(subagent_enabled=False) 为子代理构建工具集(禁止递归嵌套),其余执行细节见 19 章 task_tool.py:169-281。绑定条件:subagent_enabled=True(SUBAGENT_TOOLS = [task_tool])tools.py:20-23。
deferred tool 机制与 DeferredToolFilterMiddleware
DeferredToolRegistry 存 DeferredToolEntry(name, description, tool),只有搜索命中才返回完整 tool 对象;promote(names) 直接删除条目(不保留历史)tool_search.py:39-126。注册表用 ContextVar 而非模块全局,保证并发请求互不干扰;子代理重入 get_available_tools 时会复用已存在的注册表,避免把父 agent 已 promote 的工具又重新 defer 掉(issue #2884)tools.py:136-180。配套的 DeferredToolFilterMiddleware 在 wrap_model_call 时把 deferred 名字从 request.tools 剔除(ToolNode 仍持有全部工具用于路由),并在 wrap_tool_call 时拦截尚未 promote 的 deferred 工具调用,返回「先调 tool_search」的错误提示 deferred_tool_filter_middleware.py:34-108。
单个工具在 deferred 机制下的状态迁移:
Data Flow
下图为一次「MCP 工具很多 + 开启 tool_search」场景下,从装配到 LLM 实际调用某个 deferred 工具的完整链路。
速查表
全部内置 / 子代理工具矩阵(配置工具与 MCP 工具不在此表,分别见 07 / 21 章):
| 工具 | 作用 | 关键参数 | 绑定条件 | Source |
|---|---|---|---|---|
present_files | 把 outputs 文件呈现给用户(写 artifacts 状态) | filepaths: list[str] | 始终(BUILTIN_TOOLS) | present_file_tool.py:83-122 |
ask_clarification | 向用户追问,由中间件中断执行 | question, clarification_type, context, options | 始终(BUILTIN_TOOLS) | clarification_tool.py:6-55 |
view_image | 读图为 base64 写入 viewed_images | image_path | 模型 supports_vision=True | view_image_tool.py:49-162 |
setup_agent | 写新自定义 agent 的 SOUL.md/config.yaml | soul, description, skills | is_bootstrap=True(工厂外层追加) | agent.py:413-415 |
update_agent | 自定义 agent 持久化自更新(原子两阶段) | soul, description, skills, tool_groups, model | agent_name 已设置且非 bootstrap | agent.py:429-436 |
tool_search | 取 deferred 工具完整 schema 并 promote | query(select: / + / 正则) | MCP 启用且 tool_search.enabled=True | tools.py:132-180 |
invoke_acp_agent | 调用外部 ACP 兼容 agent | agent, prompt | config.acp_agents 非空 | tools.py:186-201 |
skill_manage | 管理 skills/custom/ 自定义技能 | action, name, content, path, find, replace | skill_evolution.enabled=True | tools.py:92-96 |
task | 委派子代理(子代理内再装配工具) | description, prompt, subagent_type | subagent_enabled=True | tools.py:20-23 |
扩展指南
经 config.yaml 的 tools[] 段 + resolve_variable 加自定义工具,无需改 harness 代码。最小步骤与模板:
1. 写一个 LangChain 工具对象(模块级变量),例如 mypkg/mytools.py:
python
from langchain.tools import tool
@tool("weather_query", parse_docstring=True)
def weather_query_tool(city: str) -> str:
"""查询指定城市天气。
Args:
city: 城市名,例如 "Beijing"。
"""
return f"{city}: sunny 25C"2. 在 config.yaml 的 tools[] 追加一条(参考 config.example.yaml 现有条目格式 config.example.yaml:369-374):
yaml
tools:
- name: weather_query # 仅做标识,绑定以工具 .name 为准
group: web # 必须是 tool_groups[] 中已声明的组名
use: mypkg.mytools:weather_query_tool # module:variable,被 resolve_variable 反射约束(必须满足,否则装配失败或被忽略):
use必须形如module.path:variable_name,否则resolve_variable抛ImportError;module必须在运行环境可 import,缺依赖会提示uv addresolvers.py:25-70。- 被
use解析出的对象必须是BaseTool实例(resolve_variable(cfg.use, BaseTool)做 isinstance 校验)tools.py:73。 - 让配置
name与工具的.name保持一致:不一致只告警、且最终以.name绑定,易引发认知错位(issue #1803)tools.py:79-86。 group应是tool_groups[]已声明的组,否则该工具不在任何组、groups过滤时拿不到 tool_config.py:4-8。- 工具名不要与内置 / MCP / ACP 工具重名;配置工具虽优先,但同名仍会让其余被静默跳过 tools.py:205-220。
- 若工具是 async-only(只有
coroutine),DeerFlow 会自动补同步 wrapper,但请确保协程可被独立asyncio.run安全执行 tools.py:37-41。
Common Pitfalls / Tips
- config name 与 .name 不一致:运行时以工具自身
.name绑定,但 LLM 提示里可能出现 config name,导致 "not a valid tool"。让二者一致是最稳做法 tools.py:79-86。 - host bash 默认不暴露:
LocalSandboxProvider下若is_host_bash_allowed为假,group=="bash"或use=="deerflow.sandbox.tools:bash_tool"的工具会被剔除——自定义 bash 类工具需注意 tools.py:26-34。 - deferred 工具调用前必须先 tool_search:LLM 即便在 prompt 看到 deferred 工具名,未经
tool_searchpromote 直接调用会被中间件拦截返回错误 deferred_tool_filter_middleware.py:49-69。 - MCP 配置走磁盘热读:
get_available_tools用ExtensionsConfig.from_file()读最新文件,而非进程内缓存的config.extensions,以反映 Gateway 在另一进程做的改动 tools.py:113-124。 - setup_agent / update_agent 不在 get_available_tools 内:它们由 lead agent 工厂按
is_bootstrap/agent_name在外层拼接;在工厂外直接调get_available_tools不会拿到这两个工具 agent.py:413-436。 - 子代理重入复用注册表:
task内部会再调get_available_tools,deferred 注册表借助 ContextVar 复用,不会清掉父 agent 的 promotion(issue #2884)tools.py:136-180。
References
- backend/packages/harness/deerflow/tools/tools.py ——
get_available_tools装配与去重 - backend/packages/harness/deerflow/tools/types.py —— 共用
Runtime类型 - backend/packages/harness/deerflow/tools/sync.py —— 异步工具同步 wrapper
- backend/packages/harness/deerflow/tools/builtins/tool_search.py —— deferred 注册表与 tool_search
- backend/packages/harness/deerflow/tools/builtins/present_file_tool.py —— present_files 实现
- backend/packages/harness/deerflow/tools/builtins/update_agent_tool.py —— update_agent 两阶段写
- backend/packages/harness/deerflow/tools/skill_manage_tool.py —— skill_manage 工具
- backend/packages/harness/deerflow/agents/middlewares/deferred_tool_filter_middleware.py —— deferred 过滤中间件
- backend/packages/harness/deerflow/reflection/resolvers.py —— resolve_variable 反射
- backend/packages/harness/deerflow/config/tool_config.py —— ToolConfig schema
Related Pages
| 章节 | 关系 |
|---|---|
| ./21-MCP集成.md | 本章只讲 MCP 工具如何并入装配与 deferred,MCP 缓存 / 传输 / OAuth 细节在 21 章 |
| ./19-子代理委派系统.md | task 工具的执行引擎、并发与事件流在 19 章;本章只讲其绑定条件与重入装配 |
| ./07-沙箱与工具配置.md | config.yaml 的 tools[] / tool_groups[] / 沙箱工具配置细节在 07 章 |
| ./10-LeadAgent与Agent工厂.md | lead agent 工厂如何调用 get_available_tools 并拼 setup_agent/update_agent 在 10 章 |
| ./34-反射与动态加载机制.md | resolve_variable / resolve_class 反射机制的完整说明在 34 章 |
| ./22-技能系统.md | skill_manage 工具背后的技能存储、安全扫描与加载在 22 章 |
| ./25-Community工具与搜索集成.md | 下游:get_available_tools 装配的 web search / fetch 等工具,其 community provider 实现与限额在 25 章 |