Skip to content

工具系统与内置工具

本章目标:

  1. 讲清 get_available_tools() 如何把 config 定义工具 / MCP 工具 / 内置工具 / 子代理 task 四类来源装配成一份去重、可绑定的工具清单。
  2. 逐个剖析各内置工具(present_files / ask_clarification / view_image / setup_agent / update_agent / tool_search / invoke_acp_agent / skill_manage)的职责与绑定条件
  3. 说清 deferred tool(延迟工具) 机制:为什么需要、注册表如何工作、与 DeferredToolFilterMiddleware 如何配合,以及如何经 config.yamlresolve_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.yamltools[] 段 + 反射 resolve_variable 动态加载,无需改代码即可增删工具 tools.py:67-73
  • 条件装配:内置工具按运行时上下文与配置开关选择性追加,而不是全量暴露 tools.py:91-111
  • 名字即契约:以工具对象自身的 .name 为绑定锚点,并对 config name.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-73ToolConfig 三个必填字段 name / group / use,use 形如 deerflow.community.ddg_search.tools:web_search_tool tool_config.py:11-20resolve_variablemodule: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 presentedToolMessage 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_toolreturn_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

DeferredToolRegistryDeferredToolEntry(name, description, tool),只有搜索命中才返回完整 tool 对象;promote(names) 直接删除条目(不保留历史)tool_search.py:39-126。注册表用 ContextVar 而非模块全局,保证并发请求互不干扰;子代理重入 get_available_tools 时会复用已存在的注册表,避免把父 agent 已 promote 的工具又重新 defer 掉(issue #2884)tools.py:136-180。配套的 DeferredToolFilterMiddlewarewrap_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_imagesimage_path模型 supports_vision=Trueview_image_tool.py:49-162
setup_agent写新自定义 agent 的 SOUL.md/config.yamlsoul, description, skillsis_bootstrap=True(工厂外层追加)agent.py:413-415
update_agent自定义 agent 持久化自更新(原子两阶段)soul, description, skills, tool_groups, modelagent_name 已设置且非 bootstrapagent.py:429-436
tool_search取 deferred 工具完整 schema 并 promotequery(select: / + / 正则)MCP 启用且 tool_search.enabled=Truetools.py:132-180
invoke_acp_agent调用外部 ACP 兼容 agentagent, promptconfig.acp_agents 非空tools.py:186-201
skill_manage管理 skills/custom/ 自定义技能action, name, content, path, find, replaceskill_evolution.enabled=Truetools.py:92-96
task委派子代理(子代理内再装配工具)description, prompt, subagent_typesubagent_enabled=Truetools.py:20-23

扩展指南

config.yamltools[] 段 + 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.yamltools[] 追加一条(参考 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_variableImportError;module 必须在运行环境可 import,缺依赖会提示 uv add resolvers.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_search promote 直接调用会被中间件拦截返回错误 deferred_tool_filter_middleware.py:49-69
  • MCP 配置走磁盘热读:get_available_toolsExtensionsConfig.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

章节关系
./21-MCP集成.md本章只讲 MCP 工具如何并入装配与 deferred,MCP 缓存 / 传输 / OAuth 细节在 21 章
./19-子代理委派系统.mdtask 工具的执行引擎、并发与事件流在 19 章;本章只讲其绑定条件与重入装配
./07-沙箱与工具配置.mdconfig.yamltools[] / tool_groups[] / 沙箱工具配置细节在 07 章
./10-LeadAgent与Agent工厂.mdlead agent 工厂如何调用 get_available_tools 并拼 setup_agent/update_agent 在 10 章
./34-反射与动态加载机制.mdresolve_variable / resolve_class 反射机制的完整说明在 34 章
./22-技能系统.mdskill_manage 工具背后的技能存储、安全扫描与加载在 22 章
./25-Community工具与搜索集成.md下游:get_available_tools 装配的 web search / fetch 等工具,其 community provider 实现与限额在 25 章

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析