主题
Community 工具与搜索集成
本章目标:
- 理解为什么 DeerFlow 把网页搜索 / 抓取 / 截屏后端做成可替换的
community/包,而不是硬编码进 agent。- 掌握 community provider 如何经
config.yaml的tools[]注入到 agent 工具集,以及每个 provider 暴露的工具名、参数与限额。- 能按最小模板新增一个 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_module 并 getattr 取出该工具对象 backend/packages/harness/deerflow/reflection/resolvers.py:44-70。config.example.yaml 默认启用 DuckDuckGo 搜索 + Jina AI 抓取 config.example.yaml:371-428,其余 provider 全部以注释形式列出,换厂商只需注释/取消注释。这样 harness 包本身不依赖任何特定搜索厂商的 key,真正做到「框架与厂商解耦」。
Architecture
community provider 经 tools[] 注入的链路如下。AppConfig.tools 是一个 ToolConfig 列表,ToolConfig 用 extra="allow" 允许 api_key / max_results / timeout 等任意附加字段直接写在工具同级 backend/packages/harness/deerflow/config/tool_config.py:11-20。get_available_tools() 按 group 过滤后,对每个 cfg.use 调 resolve_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.py—ToolConfig模型,extra="allow"承载限额字段backend/packages/harness/deerflow/tools/tools.py—get_available_tools(),按 group 过滤 +resolve_variablebackend/packages/harness/deerflow/reflection/resolvers.py—resolve_variable()动态导入backend/packages/harness/deerflow/config/app_config.py—get_tool_config()按工具名反查配置config.example.yaml—tools[]各 provider 模板与默认启用项
Components / Subsystems
下面逐 provider 说明职责、暴露的工具、关键代码位置与限额。所有 provider 共用一个隐性约定:web_search 返回归一化 JSON(含 title/url/snippet 或 content),web_fetch 返回 # 标题\n\n正文,正文统一截断到 4096 字符。
tavily — 搜索 + 抓取
web_search_tool 调 TavilyClient.search,默认 max_results=5,可被 tools[] 同级的 max_results 覆盖,结果归一化为 title/url/snippet 列表 backend/packages/harness/deerflow/community/tavily/tools.py:24-40。web_fetch_tool 调 client.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-32。JinaClient 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_tool 调 FirecrawlApp.search(query, limit=max_results),取 result.web 归一化 backend/packages/harness/deerflow/community/firecrawl/tools.py:24-44。web_fetch_tool 调 client.scrape(url, formats=["markdown"]) 直接拿 markdown,截断 4096 backend/packages/harness/deerflow/community/firecrawl/tools.py:60-73。注意 _get_firecrawl_client 接 tool_name 参数,search 读 web_search 配置、fetch 读 web_fetch 配置 backend/packages/harness/deerflow/community/firecrawl/tools.py:9-14。
exa — 搜索(带 highlights) + 抓取
web_search_tool 调 Exa.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-51。web_fetch_tool 调 client.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-52。max_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_search 的 search_time_range、web_fetch 的 fetch_time/timeout/navigation_timeout、image_search 的 image_search_time_range/image_size)聚合参数 backend/packages/harness/deerflow/community/infoquest/tools.py:11-43。客户端 POST search.infoquest.bytepluses.com 与 reader.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 | 类型 | 暴露工具 | 关键限额 / 默认值 | 需要的 key | Source |
|---|---|---|---|---|---|
| tavily | search + fetch | web_search, web_fetch | search 默认 5 结果(max_results 可覆盖);fetch 正文截 4KB | web_search.api_key(Tavily) | tools.py:17-62 |
| jina_ai | fetch | web_fetch | timeout 默认 10s;readability 抽取后截 4KB | JINA_API_KEY 环境变量(可选,免费限速) | jina_client.py:12-27 |
| firecrawl | search + scrape | web_search, web_fetch | search 默认 5 结果;scrape markdown 截 4KB | web_search/web_fetch 的 api_key(Firecrawl) | tools.py:17-73 |
| exa | search + fetch | web_search, web_fetch | search 默认 5 结果 / search_type=auto / highlights 1000 字符;fetch 截 4KB | web_search/web_fetch 的 api_key(Exa) | tools.py:24-79 |
| serper | search | web_search | 默认 5 结果;HTTP 30s 超时;取 organic | web_search.api_key 或 SERPER_API_KEY 环境变量 | tools.py:23-95 |
| ddg_search | search | web_search | 默认 5 结果;30s 超时;region wt-wt | 无(零 key,默认启用) | tools.py:33-95 |
| infoquest | search + fetch + image | web_search, web_fetch, image_search | search_time_range/fetch_time/timeout/navigation_timeout 默认 -1(关);image_size ∈ {l,m,i};image_search_time_range 1-365;fetch 截 4KB | INFOQUEST_API_KEY 环境变量 | tools.py:46-93 |
| image_search | image search | image_search | 默认 5 结果;size/type_image/layout 可选过滤 | 无(零 key,默认启用) | tools.py:77-135 |
| aio_sandbox | sandbox 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.yaml 的 tools[] 用变量路径接入(注释掉原 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约束(均有源码依据):
@tool的第一个参数(工具名)必须是契约里的web_search/web_fetch/image_search之一——agent 系统提示词按这些名字调用;get_available_tools还会校验cfg.name与工具.name是否一致,不一致会告警并以工具自身.name绑定 backend/packages/harness/deerflow/tools/tools.py:79-88。use必须是模块路径:变量名格式,否则resolve_variable抛 ImportError;模块导入失败会附带依赖安装提示 backend/packages/harness/deerflow/reflection/resolvers.py:44-57。- 解析出的对象必须是
BaseTool实例(resolve_variable(cfg.use, BaseTool)做 isinstance 校验)backend/packages/harness/deerflow/tools/tools.py:73。 - 限额 / key 不要写死,统一用
get_tool_config(<工具名>).model_extra.get(...)读tools[]同级字段,保持与现有 provider 一致的可配置契约 backend/packages/harness/deerflow/config/tool_config.py:11-20。 - 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 | 无敏感信息 |
| tavily | tools[].api_key: $TAVILY_API_KEY config.example.yaml:386-391 | 用 $TAVILY_API_KEY 引用环境变量,勿把明文 key 提交进 config.yaml |
| serper | tools[].api_key 或 SERPER_API_KEY 环境变量(优先 config,回退 env)config.example.yaml:376-384 | 优先用环境变量;config 未填时自动回退 SERPER_API_KEY |
| exa | tools[].api_key: $EXA_API_KEY(search 与 fetch 各一条)config.example.yaml:400-422 | 同上,$EXA_API_KEY 引用 |
| firecrawl | tools[].api_key: $FIRECRAWL_API_KEY config.example.yaml:409-445 | 同上 |
| jina_ai | JINA_API_KEY 环境变量(可选,缺省走免费限速)backend/packages/harness/deerflow/community/jina_ai/jina_client.py:19-23 | 只走环境变量,不入 config |
| infoquest | INFOQUEST_API_KEY 环境变量(Bearer 鉴权)backend/packages/harness/deerflow/community/infoquest/infoquest_client.py:117-121 | 只走环境变量,不入 config |
| aio_sandbox | sandbox.use + sandbox 段(见第 17 章)config.example.yaml:585-586 | sandbox environment 同样支持 $VAR 引用 backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox_provider.py:180-190 |
Common Pitfalls/Tips
- 同一能力名只能配一条:provider 用
get_tool_config("web_search")按工具名反查配置,tools[]里出现多条同名web_search时next(...)只命中第一条 backend/packages/harness/deerflow/config/app_config.py:304-313。换 provider 务必注释掉旧条目,config.example.yaml对 web_fetch 也有同样警告 config.example.yaml:416-422。 - 4KB 正文截断是硬限制:所有
web_fetch实现都把正文截到 4096 字符,长文档会被腰斩;需要全文要让 agent 分段抓或换思路,而非指望 provider 返回完整页面。 - 缺 SDK 不崩但静默退化:ddg_search 在
ddgs未安装时只记日志返回空列表 backend/packages/harness/deerflow/community/ddg_search/tools.py:34-37;serper 缺 key 时返回错误 JSON 而非异常 backend/packages/harness/deerflow/community/serper/tools.py:47-54。表现为「搜不到东西」而非报错,排查时先查依赖与 key。 - jina_ai 是异步工具:它的
web_fetch_tool是async def,readability 抽取放在asyncio.to_thread避免阻塞事件循环 backend/packages/harness/deerflow/community/jina_ai/tools.py:31;自定义 provider 若同步实现,框架会用_ensure_sync_invocable_tool包装 backend/packages/harness/deerflow/tools/tools.py:88。 - infoquest 参数 -1 = 关闭:
search_time_range/fetch_time/timeout等默认 -1 表示不施加该过滤,只有正值才生效 backend/packages/harness/deerflow/community/infoquest/infoquest_client.py:136-147。
References
- backend/packages/harness/deerflow/community/tavily/tools.py — tavily 搜索 + 抓取
- backend/packages/harness/deerflow/community/jina_ai/jina_client.py — Jina reader 客户端与免费限速逻辑
- backend/packages/harness/deerflow/community/infoquest/infoquest_client.py — InfoQuest 一站式搜索/抓取/图搜
- backend/packages/harness/deerflow/tools/tools.py —
get_available_tools装配与名字校验 - backend/packages/harness/deerflow/reflection/resolvers.py —
resolve_variable动态导入 - backend/packages/harness/deerflow/config/tool_config.py —
ToolConfig模型与extra="allow" - backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox_provider.py — aio_sandbox 作为 community SandboxProvider
- config.example.yaml —
tools[]各 provider 模板
Related Pages
| 页面 | 关系 |
|---|---|
| ./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 实现细节 |