网络搜索通过统一的 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 库,支持配置 region、safesearch、backend(可指定 brave、wikipedia 等子引擎): 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.yaml 的 tools: 下切换,同一时间只能有一个 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