deerflow2是如何调用网络搜索工具的?

网络搜索通过统一的 web_search 工具接口实现,但后端支持多种可插拔的搜索引擎,在 config.yaml 中通过 use: 字段切换。


统一接口,多种后端

所有搜索后端都注册为同名工具 web_search,agent 调用时无感知差异。默认使用 DuckDuckGo(无需 API key): 1


支持的搜索后端

后端 模块路径 API Key 说明
DuckDuckGo(默认) deerflow.community.ddg_search.tools 不需要 免费,支持 backend: brave/wikipedia
SearXNG deerflow.community.searxng.tools 不需要 自托管元搜索引擎
Tavily deerflow.community.tavily.tools TAVILY_API_KEY 结构化结果,质量高
Brave Search deerflow.community.brave.tools BRAVE_SEARCH_API_KEY 独立索引,官方 API
Exa deerflow.community.exa.tools EXA_API_KEY 神经检索 + 关键词
InfoQuest deerflow.community.infoquest.tools INFOQUEST_API_KEY 垂直搜索
Firecrawl deerflow.community.firecrawl.tools FIRECRAWL_API_KEY 搜索 + 爬取
fastCRW deerflow.community.fastcrw.tools CRW_API_KEY(可选) Firecrawl 兼容,可自托管
Serper deerflow.community.serper.tools SERPER_API_KEY Google Search API
GroundRoute deerflow.community.groundroute.tools GROUNDROUTE_API_KEY 元搜索,自动路由到最优引擎

DuckDuckGo 实现细节(默认)

web_search_tool 调用 ddgs 库,支持配置 regionsafesearchbackend(可指定 bravewikipedia 等子引擎): 3

返回结果统一归一化为 {title, url, content} 格式: 4


web_fetch:读取具体网页内容

除了 web_search,还有 web_fetch 工具用于抓取具体 URL 的页面内容。Tavily 后端同时提供了这两个工具: 5

默认的 web_fetch 实现是 Jina AI(deerflow.community.jina_ai.tools:web_fetch_tool),也可以换成 Browserless(headless Chrome,适合 JS 渲染页面)。


配置方式

config.yamltools: 下切换,同一时间只能有一个 web_search 实现生效:

yaml 复制代码
tools:
  # 默认(无需 key)
  - name: web_search
    use: deerflow.community.ddg_search.tools:web_search_tool
    max_results: 5

  # 切换为 Tavily(注释掉上面,取消注释下面)
  # - name: web_search
  #   use: deerflow.community.tavily.tools:web_search_tool
  #   api_key: $TAVILY_API_KEY
```[6](#6-5)

### Citations

**File:** config.example.yaml (L559-567)
```yaml
tools:
  # Web search tool (uses DuckDuckGo, no API key required)
  - name: web_search
    group: web
    use: deerflow.community.ddg_search.tools:web_search_tool
    max_results: 5
    # backend: auto          # DDGS backend(s): auto, duckduckgo, brave, wikipedia, etc.
    # region: wt-wt          # wt-wt is normalized for Wikipedia when backend includes auto/all/wikipedia.
    # safesearch: moderate   # on, moderate, off

File: config.example.yaml (L599-627)

yaml 复制代码
  # Web search tool (requires Tavily API key)
  # - name: web_search
  #   group: web
  #   use: deerflow.community.tavily.tools:web_search_tool
  #   max_results: 5
  #   # api_key: $TAVILY_API_KEY  # Set if needed

  # Web search tool (uses InfoQuest, requires InfoQuest API key)
  # - name: web_search
  #   group: web
  #   use: deerflow.community.infoquest.tools:web_search_tool
  #   # Used to limit the scope of search results, only returns content within the specified time range. Set to -1 to disable time filtering
  #   search_time_range: 10

  # Web search tool (uses Exa, requires EXA_API_KEY)
  # - name: web_search
  #   group: web
  #   use: deerflow.community.exa.tools:web_search_tool
  #   max_results: 5
  #   search_type: auto  # Options: auto, neural, keyword
  #   contents_max_characters: 1000
  #   # api_key: $EXA_API_KEY

  # Web search tool (uses Firecrawl, requires FIRECRAWL_API_KEY)
  # - name: web_search
  #   group: web
  #   use: deerflow.community.firecrawl.tools:web_search_tool
  #   max_results: 5
  #   # api_key: $FIRECRAWL_API_KEY

File: scripts/wizard/providers.py (L468-537)

python 复制代码
SEARCH_PROVIDERS: list[SearchProvider] = [
    SearchProvider(
        name="ddg",
        display_name="DuckDuckGo (free, no key needed)",
        description="No API key required",
        use="deerflow.community.ddg_search.tools:web_search_tool",
        env_var=None,
        extra_config={"max_results": 5},
    ),
    SearchProvider(
        name="tavily",
        display_name="Tavily",
        description="Recommended, free tier available",
        use="deerflow.community.tavily.tools:web_search_tool",
        env_var="TAVILY_API_KEY",
        extra_config={"max_results": 5},
    ),
    SearchProvider(
        name="infoquest",
        display_name="InfoQuest",
        description="Higher quality vertical search, API key required",
        use="deerflow.community.infoquest.tools:web_search_tool",
        env_var="INFOQUEST_API_KEY",
        extra_config={"search_time_range": 10},
    ),
    SearchProvider(
        name="exa",
        display_name="Exa",
        description="Neural + keyword web search, API key required",
        use="deerflow.community.exa.tools:web_search_tool",
        env_var="EXA_API_KEY",
        extra_config={
            "max_results": 5,
            "search_type": "auto",
            "contents_max_characters": 1000,
        },
    ),
    SearchProvider(
        name="firecrawl",
        display_name="Firecrawl",
        description="Search + crawl via Firecrawl API",
        use="deerflow.community.firecrawl.tools:web_search_tool",
        env_var="FIRECRAWL_API_KEY",
        extra_config={"max_results": 5},
    ),
    SearchProvider(
        name="fastcrw",
        display_name="fastCRW",
        description="Firecrawl-compatible web scraper, single binary, self-host or cloud",
        use="deerflow.community.fastcrw.tools:web_search_tool",
        env_var="CRW_API_KEY",
        extra_config={"max_results": 5},
    ),
    SearchProvider(
        name="brave",
        display_name="Brave Search",
        description="Independent index, official API, API key required",
        use="deerflow.community.brave.tools:web_search_tool",
        env_var="BRAVE_SEARCH_API_KEY",
        extra_config={"max_results": 5},
    ),
    SearchProvider(
        name="groundroute",
        display_name="GroundRoute",
        description="One key across six engines, price-routed with failover, API key required",
        use="deerflow.community.groundroute.tools:web_search_tool",
        env_var="GROUNDROUTE_API_KEY",
        extra_config={"max_results": 5},
    ),
]

File: backend/packages/harness/deerflow/community/ddg_search/tools.py (L133-165)

python 复制代码
@tool("web_search", parse_docstring=True)
def web_search_tool(
    query: str,
    max_results: int = 5,
) -> str:
    """Search the web for information. Use this tool to find current information, news, articles, and facts from the internet.

    Args:
        query: Search keywords describing what you want to find. Be specific for better results.
        max_results: Maximum number of results to return. Default is 5.
    """
    config = get_app_config().get_tool_config("web_search")
    region = DEFAULT_REGION
    safesearch = DEFAULT_SAFESEARCH
    backend = DEFAULT_BACKEND

    if config is not None:
        # Override tool call defaults from config if set.
        max_results = config.model_extra.get("max_results", max_results)
        region = config.model_extra.get("region", region)
        safesearch = config.model_extra.get("safesearch", safesearch)
        backend = config.model_extra.get("backend", backend)

    results = _search_text(
        query=query,
        max_results=max_results,
        region=region,
        safesearch=safesearch,
        backend=backend,
    )

    if not results:
        return json.dumps({"error": "No results found", "query": query}, ensure_ascii=False)

File: backend/packages/harness/deerflow/community/ddg_search/tools.py (L167-182)

python 复制代码
    normalized_results = [
        {
            "title": r.get("title", ""),
            "url": r.get("href", r.get("link", "")),
            "content": r.get("body", r.get("snippet", "")),
        }
        for r in results
    ]

    output = {
        "query": query,
        "total_results": len(normalized_results),
        "results": normalized_results,
    }

    return json.dumps(output, indent=2, ensure_ascii=False)

File: backend/packages/harness/deerflow/community/tavily/tools.py (L43-61)

python 复制代码
@tool("web_fetch", parse_docstring=True)
def web_fetch_tool(url: str) -> str:
    """Fetch the contents of a web page at a given URL.
    Only fetch EXACT URLs that have been provided directly by the user or have been returned in results from the web_search and web_fetch tools.
    This tool can NOT access content that requires authentication, such as private Google Docs or pages behind login walls.
    Do NOT add www. to URLs that do NOT have them.
    URLs must include the schema: https://example.com is a valid URL while example.com is an invalid URL.

    Args:
        url: The URL to fetch the contents of.
    """
    client = _get_tavily_client()
    res = client.extract([url])
    if "failed_results" in res and len(res["failed_results"]) > 0:
        return f"Error: {res['failed_results'][0]['error']}"
    elif "results" in res and len(res["results"]) > 0:
        result = res["results"][0]
        return f"# {result['title']}\n\n{result['raw_content'][:4096]}"
    else:

backend/packages/harness/deerflow/tools/ 目录结构如下:


顶层文件

文件 作用
tools.py 核心入口,get_available_tools() 汇总所有工具
types.py 工具类型定义
mcp_metadata.py 给 MCP 工具打标签(用于 tool_search 按需加载)
skill_manage_tool.py skill 管理工具(skill_evolution.enabled=true 时启用)
sync.py 将 async-only 工具包装为同步可调用

builtins/ 子目录(内置工具)

文件 工具名 说明
clarification_tool.py ask_clarification 向用户提问以澄清需求
present_file_tool.py present_file 将文件展示给用户
view_image_tool.py view_image 查看图片(仅视觉模型启用)
task_tool.py task 启动子 agent(ultra 模式专用)
setup_agent_tool.py setup_agent 创建自定义智能体(bootstrap 阶段)
update_agent_tool.py update_agent 更新自定义智能体的 SOUL.md/config
tool_search.py tool_search 按需搜索并加载 MCP 工具
invoke_acp_agent_tool.py invoke_acp_agent 调用外部 ACP agent

工具加载优先级

get_available_tools() 按以下顺序合并,同名工具后者被丢弃:

复制代码
config.yaml 配置的工具(web_search、bash 等)
  > 内置工具(present_file、ask_clarification 等)
  > MCP 工具(动态加载)
  > ACP 工具(invoke_acp_agent)
```[1](#7-0) 

---

## 条件加载规则

- `view_image_tool`:仅当所选模型 `supports_vision=True` 时加入
- `task_tool`:仅当 `subagent_enabled=True`(ultra 模式)时加入
- `skill_manage_tool`:仅当 `skill_evolution.enabled=true` 时加入
- `update_agent`:仅自定义智能体(非默认 agent)才能看到 [2](#7-1)

### Citations

**File:** backend/packages/harness/deerflow/tools/tools.py (L91-111)
```python
    builtin_tools = BUILTIN_TOOLS.copy()
    skill_evolution_config = getattr(config, "skill_evolution", None)
    if getattr(skill_evolution_config, "enabled", False):
        from deerflow.tools.skill_manage_tool import skill_manage_tool

        builtin_tools.append(skill_manage_tool)

    # Add subagent tools only if enabled via runtime parameter
    if subagent_enabled:
        builtin_tools.extend(SUBAGENT_TOOLS)
        logger.info("Including subagent tools (task)")

    # If no model_name specified, use the first model (default)
    if model_name is None and config.models:
        model_name = config.models[0].name

    # Add view_image_tool only if the model supports vision
    model_config = config.get_model_config(model_name) if model_name else None
    if model_config is not None and model_config.supports_vision:
        builtin_tools.append(view_image_tool)
        logger.info(f"Including view_image_tool for model '{model_name}' (supports_vision=True)")

File: backend/packages/harness/deerflow/tools/tools.py (L161-176)

python 复制代码
    # Deduplicate by tool name --- config-loaded tools take priority, followed by
    # built-ins, MCP tools, and ACP tools.  Duplicate names cause the LLM to
    # receive ambiguous or concatenated function schemas (issue #1803).
    all_tools = [_ensure_sync_invocable_tool(t) for t in loaded_tools + builtin_tools + mcp_tools + acp_tools]
    seen_names: set[str] = set()
    unique_tools: list[BaseTool] = []
    for t in all_tools:
        if t.name not in seen_names:
            unique_tools.append(t)
            seen_names.add(t.name)
        else:
            logger.warning(
                "Duplicate tool name %r detected and skipped --- check your config.yaml and MCP server registrations (issue #1803).",
                t.name,
            )
    return unique_tools