Skip to content

Community 工具与搜索集成

本章目标:

  1. 理解为什么 DeerFlow 把网页搜索 / 抓取 / 截屏后端做成可替换的 community/ 包,而不是硬编码进 agent。
  2. 掌握 community provider 如何经 config.yamltools[] 注入到 agent 工具集,以及每个 provider 暴露的工具名、参数与限额。
  3. 能按最小模板新增一个 community 搜索 provider 并通过 tools[] 接入,知道约束在哪里。

TL;DR

DeerFlow 的网页能力(web_search / web_fetch / image_search)不绑死任何厂商:community/ 下每个 provider 各自实现同名 @tool 函数,通过 tools[].use 的变量路径被 resolve_variable() 动态导入。换 provider = 改 config.yaml 一行,agent 代码零改动。内置 8 个 provider(tavily / jina_ai / firecrawl / exa / serper / ddg_search / infoquest / image_search),覆盖搜索、抓取、截屏三类;限额(如 tavily 默认 5 结果、fetch 正文截断 4096 字符)和 API key 均来自 tools[] 同级字段或环境变量。aio_sandbox 也住在 community/,但它是 SandboxProvider 而非工具,经 sandbox.use 注入(沙箱细节见第 17 章)。

Overview

为什么搜索 / 抓取后端要做成可替换的 community 包?

agent 在线检索是高频需求,但「用哪家搜索」是部署方决策,而非框架决策:有人有 Tavily key、有人只想用免费的 DuckDuckGo、有人在字节内网必须走 InfoQuest。如果把某个 SDK 硬编码进 lead agent,换厂商就要改 agent 代码并重新发版,违背 harness 「框架可发布、应用可配置」的分层原则。

DeerFlow 的解法是「契约 + 反射」:框架只约定工具名契约——agent 系统提示词里固定提到 web_search / web_fetch / image_search 三个能力名;每个 community provider 各自实现一份同名 @tool 函数,例如 tavily 的 web_search_tool@tool("web_search", ...) 标注 backend/packages/harness/deerflow/community/tavily/tools.py:17,ddg_search 的也是 @tool("web_search", ...) backend/packages/harness/deerflow/community/ddg_search/tools.py:55。它们对 agent 完全等价,只是结果质量与限额不同。

注入靠 tools[].use 字段写一个「模块:变量」路径,resolve_variable() 在启动时 import_modulegetattr 取出该工具对象 backend/packages/harness/deerflow/reflection/resolvers.py:44-70config.example.yaml 默认启用 DuckDuckGo 搜索 + Jina AI 抓取 config.example.yaml:371-428,其余 provider 全部以注释形式列出,换厂商只需注释/取消注释。这样 harness 包本身不依赖任何特定搜索厂商的 key,真正做到「框架与厂商解耦」。

Architecture

community provider 经 tools[] 注入的链路如下。AppConfig.tools 是一个 ToolConfig 列表,ToolConfigextra="allow" 允许 api_key / max_results / timeout 等任意附加字段直接写在工具同级 backend/packages/harness/deerflow/config/tool_config.py:11-20get_available_tools()group 过滤后,对每个 cfg.useresolve_variable(cfg.use, BaseTool) 取出工具对象 backend/packages/harness/deerflow/tools/tools.py:67-88。运行期 provider 内部再用 get_app_config().get_tool_config("web_search") 反查自己那条配置读限额 backend/packages/harness/deerflow/config/app_config.py:304-313

注意一个关键事实:provider 用 get_tool_config(name)工具名(web_search)而非 provider 名查配置 backend/packages/harness/deerflow/community/tavily/tools.py:10。因此 tools[] 里同一能力名只能有一条生效条目——config.example.yaml 明确警告 web_fetch 同时只能启一个 provider config.example.yaml:416-422

Source 列表:

  • backend/packages/harness/deerflow/config/tool_config.pyToolConfig 模型,extra="allow" 承载限额字段
  • backend/packages/harness/deerflow/tools/tools.pyget_available_tools(),按 group 过滤 + resolve_variable
  • backend/packages/harness/deerflow/reflection/resolvers.pyresolve_variable() 动态导入
  • backend/packages/harness/deerflow/config/app_config.pyget_tool_config() 按工具名反查配置
  • config.example.yamltools[] 各 provider 模板与默认启用项

Components / Subsystems

下面逐 provider 说明职责、暴露的工具、关键代码位置与限额。所有 provider 共用一个隐性约定:web_search 返回归一化 JSON(含 title/url/snippetcontent),web_fetch 返回 # 标题\n\n正文,正文统一截断到 4096 字符。

tavily — 搜索 + 抓取

web_search_toolTavilyClient.search,默认 max_results=5,可被 tools[] 同级的 max_results 覆盖,结果归一化为 title/url/snippet 列表 backend/packages/harness/deerflow/community/tavily/tools.py:24-40web_fetch_toolclient.extract([url]),失败时返回首个 failed_results 错误,成功时取 raw_content 并截断到 4096 字符 backend/packages/harness/deerflow/community/tavily/tools.py:54-62。API key 从 web_search 配置的 api_key 字段读 backend/packages/harness/deerflow/community/tavily/tools.py:9-14

jina_ai — 抓取 + readability(默认 fetch provider)

只暴露 web_fetch_tool(异步)。它用 JinaClient.crawl() 拿 HTML,再用 ReadabilityExtractor 在线程池里抽正文转 markdown,截断 4096 字符 backend/packages/harness/deerflow/community/jina_ai/tools.py:13-32JinaClient POST https://r.jina.ai/,默认 timeout=10(可经 web_fetch 配置覆盖),JINA_API_KEY 环境变量可选——没设只发警告并走免费限速 backend/packages/harness/deerflow/community/jina_ai/jina_client.py:12-27。readability 抽取基于 readabilipy + markdownify,Readability.js 失败时回退纯 Python 抽取 backend/packages/harness/deerflow/utils/readability.py:59-83

firecrawl — 搜索 + scrape

web_search_toolFirecrawlApp.search(query, limit=max_results),取 result.web 归一化 backend/packages/harness/deerflow/community/firecrawl/tools.py:24-44web_fetch_toolclient.scrape(url, formats=["markdown"]) 直接拿 markdown,截断 4096 backend/packages/harness/deerflow/community/firecrawl/tools.py:60-73。注意 _get_firecrawl_clienttool_name 参数,search 读 web_search 配置、fetch 读 web_fetch 配置 backend/packages/harness/deerflow/community/firecrawl/tools.py:9-14

exa — 搜索(带 highlights) + 抓取

web_search_toolExa.search,支持三个 tools[] 字段:max_results(默认 5)、search_type(默认 auto,可选 neural/keyword)、contents_max_characters(默认 1000,控制 highlights 长度),snippet 由 highlights 拼接 backend/packages/harness/deerflow/community/exa/tools.py:24-51web_fetch_toolclient.get_contents([url], text={"max_characters": 4096}) 并再截 4096 backend/packages/harness/deerflow/community/exa/tools.py:67-79

serper — Google Search(纯 HTTP)

只暴露 web_search_tool,无 SDK 依赖,直接 httpx.Client POST https://google.serper.dev/search(30s 超时),取 organic 归一化为 title/url/content,输出含 query/total_results/results 包裹 backend/packages/harness/deerflow/community/serper/tools.py:60-95。key 解析双通道:先看 web_search 配置的 api_key,再回退 SERPER_API_KEY 环境变量;两者都没则返回错误 JSON 而非抛异常 backend/packages/harness/deerflow/community/serper/tools.py:23-54

ddg_search — DuckDuckGo(零 key,默认 search provider)

只暴露 web_search_tool,通过 ddgs 库的 DDGS().text()(30s 超时,默认 region wt-wt、safesearch moderate)检索 backend/packages/harness/deerflow/community/ddg_search/tools.py:33-52max_results 默认 5,可被 tools[] 覆盖;ddgs 未安装时记错误并返回空列表(不崩) backend/packages/harness/deerflow/community/ddg_search/tools.py:66-95。无需任何 API key,故 config.example.yaml 默认启它。

infoquest — BytePlus InfoQuest(搜索 + 抓取 + 图搜,一站式)

唯一同时暴露三个工具的 provider:web_search_tool / web_fetch_tool / image_search_tool backend/packages/harness/deerflow/community/infoquest/tools.py:46-93_get_infoquest_client 跨三条配置(web_searchsearch_time_rangeweb_fetchfetch_time/timeout/navigation_timeoutimage_searchimage_search_time_range/image_size)聚合参数 backend/packages/harness/deerflow/community/infoquest/tools.py:11-43。客户端 POST search.infoquest.bytepluses.comreader.infoquest.bytepluses.com,INFOQUEST_API_KEY 环境变量做 Bearer 鉴权;image_size 仅接受 l/m/i,image_search_time_range 仅 1-365 有效,越界忽略并告警 backend/packages/harness/deerflow/community/infoquest/infoquest_client.py:109-123 backend/packages/harness/deerflow/community/infoquest/infoquest_client.py:325-340。fetch 结果同样过 readability 转 markdown 截 4096 backend/packages/harness/deerflow/community/infoquest/tools.py:69-74

image_search — DuckDuckGo 图片搜索(默认 image provider)

只暴露 image_search_tool,定位是「图像生成前找参考图」(docstring 明确这一用途) backend/packages/harness/deerflow/community/image_search/tools.py:85-101。底层 DDGS().images(),支持 size/type_image/layout 等过滤,max_results 默认 5 可被 tools[] 覆盖;返回结果统一用 thumbnail 字段作为 image_url/thumbnail_url 并附 usage_hint backend/packages/harness/deerflow/community/image_search/tools.py:102-135。无需 API key。

aio_sandbox — 容器隔离 SandboxProvider(本章只讲定位)

aio_sandbox 也住在 community/,但它不是工具:它实现 SandboxProvider 抽象类 backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox_provider.py:69-71,经 sandbox.use: deerflow.community.aio_sandbox:AioSandboxProvider 注入而非 tools[] config.example.yaml:585-586。它和搜索 provider 共享同一个「community = 可替换外部集成」的设计哲学:__init__.py 导出 AioSandboxProvider 等类 backend/packages/harness/deerflow/community/aio_sandbox/init.py:1-15。其内部 backend 选择(local container vs provisioner / K8s)、warm pool、idle 回收等沙箱实现细节属于第 17 章范畴,本章不展开。

Data Flow

下面是 agent 调一次 web_search(以 tavily 为例)从工具调用到归一化结果的完整链路。

速查表

全部 community provider 矩阵(限额列中「4KB」指正文截断到 4096 字符):

Provider类型暴露工具关键限额 / 默认值需要的 keySource
tavilysearch + fetchweb_search, web_fetchsearch 默认 5 结果(max_results 可覆盖);fetch 正文截 4KBweb_search.api_key(Tavily)tools.py:17-62
jina_aifetchweb_fetchtimeout 默认 10s;readability 抽取后截 4KBJINA_API_KEY 环境变量(可选,免费限速)jina_client.py:12-27
firecrawlsearch + scrapeweb_search, web_fetchsearch 默认 5 结果;scrape markdown 截 4KBweb_search/web_fetchapi_key(Firecrawl)tools.py:17-73
exasearch + fetchweb_search, web_fetchsearch 默认 5 结果 / search_type=auto / highlights 1000 字符;fetch 截 4KBweb_search/web_fetchapi_key(Exa)tools.py:24-79
serpersearchweb_search默认 5 结果;HTTP 30s 超时;取 organicweb_search.api_keySERPER_API_KEY 环境变量tools.py:23-95
ddg_searchsearchweb_search默认 5 结果;30s 超时;region wt-wt无(零 key,默认启用)tools.py:33-95
infoquestsearch + fetch + imageweb_search, web_fetch, image_searchsearch_time_range/fetch_time/timeout/navigation_timeout 默认 -1(关);image_size ∈ {l,m,i};image_search_time_range 1-365;fetch 截 4KBINFOQUEST_API_KEY 环境变量tools.py:46-93
image_searchimage searchimage_search默认 5 结果;size/type_image/layout 可选过滤无(零 key,默认启用)tools.py:77-135
aio_sandboxsandbox provider(非工具)sandbox.use 注入见第 17 章见第 17 章aio_sandbox_provider.py:69-91

扩展指南

新增一个 community 搜索 provider 并经 tools[] 接入,最小模板如下(以新增「mysearch」搜索为例):

步骤 1:建 backend/packages/harness/deerflow/community/mysearch/tools.py,实现一个 @tool("web_search", ...):

python
import json
from langchain.tools import tool
from deerflow.config import get_app_config

@tool("web_search", parse_docstring=True)
def web_search_tool(query: str) -> str:
    """Search the web.

    Args:
        query: The query to search for.
    """
    config = get_app_config().get_tool_config("web_search")
    max_results = 5
    api_key = None
    if config is not None:
        max_results = config.model_extra.get("max_results", max_results)
        api_key = config.model_extra.get("api_key")
    # ... 调你的搜索 API ...
    normalized = [{"title": ..., "url": ..., "snippet": ...}]
    return json.dumps(normalized, indent=2, ensure_ascii=False)

步骤 2:在 config.yamltools[] 用变量路径接入(注释掉原 web_search 那条,因为同一能力名只能有一条生效):

yaml
tools:
  - name: web_search
    group: web
    use: deerflow.community.mysearch.tools:web_search_tool
    max_results: 5
    api_key: $MYSEARCH_API_KEY

约束(均有源码依据):

  1. @tool 的第一个参数(工具名)必须是契约里的 web_search / web_fetch / image_search 之一——agent 系统提示词按这些名字调用;get_available_tools 还会校验 cfg.name 与工具 .name 是否一致,不一致会告警并以工具自身 .name 绑定 backend/packages/harness/deerflow/tools/tools.py:79-88
  2. use 必须是 模块路径:变量名 格式,否则 resolve_variable 抛 ImportError;模块导入失败会附带依赖安装提示 backend/packages/harness/deerflow/reflection/resolvers.py:44-57
  3. 解析出的对象必须是 BaseTool 实例(resolve_variable(cfg.use, BaseTool) 做 isinstance 校验)backend/packages/harness/deerflow/tools/tools.py:73
  4. 限额 / key 不要写死,统一用 get_tool_config(<工具名>).model_extra.get(...)tools[] 同级字段,保持与现有 provider 一致的可配置契约 backend/packages/harness/deerflow/config/tool_config.py:11-20
  5. harness 包不能 import app.*(分层防火墙);provider 只许依赖 deerflow.* 与第三方 SDK。

Configuration

各 provider 的 key / env 配置($VAR 在启动时解析为环境变量,见第 04 章配置系统):

Provider配置位置安全标注
ddg_search / image_search无需 key,config.example.yaml 默认启用 config.example.yaml:371-374无敏感信息
tavilytools[].api_key: $TAVILY_API_KEY config.example.yaml:386-391$TAVILY_API_KEY 引用环境变量,勿把明文 key 提交进 config.yaml
serpertools[].api_keySERPER_API_KEY 环境变量(优先 config,回退 env)config.example.yaml:376-384优先用环境变量;config 未填时自动回退 SERPER_API_KEY
exatools[].api_key: $EXA_API_KEY(search 与 fetch 各一条)config.example.yaml:400-422同上,$EXA_API_KEY 引用
firecrawltools[].api_key: $FIRECRAWL_API_KEY config.example.yaml:409-445同上
jina_aiJINA_API_KEY 环境变量(可选,缺省走免费限速)backend/packages/harness/deerflow/community/jina_ai/jina_client.py:19-23只走环境变量,不入 config
infoquestINFOQUEST_API_KEY 环境变量(Bearer 鉴权)backend/packages/harness/deerflow/community/infoquest/infoquest_client.py:117-121只走环境变量,不入 config
aio_sandboxsandbox.use + sandbox 段(见第 17 章)config.example.yaml:585-586sandbox environment 同样支持 $VAR 引用 backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox_provider.py:180-190

Common Pitfalls/Tips

References

页面关系
./20-工具系统与内置工具.md上游:get_available_tools 如何把 community 工具与内置/MCP/subagent 工具合并装配
./07-沙箱与工具配置.md平行:tools[] / tool_groups[] 配置语义与本章 provider 注入互为补充
./21-MCP集成.md对比:MCP 是另一条外部工具接入路径,与 community 静态 tools[] 注入并列
./17-沙箱系统架构.md延伸:aio_sandbox provider 的容器/backend/warm-pool 实现细节

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