注册即送 100 个免费额度,无需绑卡查看定价

[ blog · tutorial ]8 min read

给 CrewAI 的 Crew 接上实时网页搜索

Sarah Choy发布于 2026年9月23日约 8 分钟阅读
给 CrewAI 的 Crew 接上实时网页搜索

一个 crew 就是若干个共享同一批工具的 Agent——这意味着一个稍微贵一点、稍微啰嗦一点的工具,缺点会按 Agent 数量被放大。本文两种写法都给,外加两个设置,让一个 crew 不会把同一件事搜四遍。

一句话总结

  • crewai.tools 的 @tool 足够写一个搜索工具;需要 Pydantic args_schema 和类型化契约时再继承 BaseTool。
  • 工具描述是转派任务的那个 Agent 读到的全部规格——要按「使用说明」写,不要当成标签。
  • crew 会放大工具调用:同一个问题被四个 Agent 各查一遍,就是四次搜索,除非你做缓存或集中化。
  • POST https://www.apipick.com/api/search/web,带 x-api-key 头:每次 15 额度,仅 HTTP 200 扣费。
  • 把搜索工具只给一个 researcher Agent,让其他 Agent 读它的产出,而不是人手一把。

调用方变成一个 crew 之后有什么不同?

单个 Agent 需要时才调工具。而一个 crew 是每个持有它的 Agent 都调一次——转派之后还会再调,因为接到转派任务的 Agent 往往决定自己也得看一眼证据。

这就把搜索工具的两个属性从「锦上添花」变成了「承重结构」:结果集有多宽,以及一次冗余调用要花多少钱。两者都是你在二十行工具代码里做的决定。

用 @tool 写出来是什么样?

import os, httpx
from crewai.tools import tool

API_KEY = os.environ["APIPICK_KEY"]

@tool("Web Search")
def web_search(query: str) -> str:
    """搜索实时网络并返回排好序的结果。

    当答案依赖当前信息时调用它——新闻、价格、产品细节,
    以及任何在你训练数据之后才发布的东西。查询词用大白话,
    按普通人往搜索框里敲的方式写。

    返回最多五条,格式为 'title — url — snippet' 的行。
    """
    r = httpx.post(
        "https://www.apipick.com/api/search/web",
        headers={"x-api-key": API_KEY},
        json={"query": query, "max_num_results": 5},
        timeout=20,
    )
    r.raise_for_status()
    return "\n\n".join(
        f"{x['title']} — {x['url']}\n{x['snippet']}"
        for x in r.json()["results"]
    )

什么时候值得多写 BaseTool 那些代码?

参数超过一个的那一刻。Pydantic 的 args_schema 正是那个能挡住 Agent 把 country_code 编成 country 的东西。

from typing import Type, Optional
from pydantic import BaseModel, Field
from crewai.tools import BaseTool

class WebSearchInput(BaseModel):
    query: str = Field(..., description="大白话的搜索词")
    country_code: Optional[str] = Field(
        None, description="用于本地化结果的 ISO 国家码,例如 US 或 GB"
    )
    start_date: Optional[str] = Field(
        None, description="只要这个 ISO 日期(YYYY-MM-DD)及之后发布的结果"
    )

class WebSearchTool(BaseTool):
    name: str = "Web Search"
    description: str = (
        "搜索实时网络获取当前信息。用于新闻、价格,以及任何在训练截止"
        "之后发布的内容。可选按国家或发布日期限定。"
    )
    args_schema: Type[BaseModel] = WebSearchInput

    def _run(self, query: str, country_code=None, start_date=None) -> str:
        body = {"query": query, "max_num_results": 5}
        if country_code: body["country_code"] = country_code
        if start_date:   body["start_date"] = start_date
        r = httpx.post(
            "https://www.apipick.com/api/search/web",
            headers={"x-api-key": API_KEY}, json=body, timeout=20,
        )
        r.raise_for_status()
        return "\n\n".join(
            f"{x['title']} — {x['url']}\n{x['snippet']}"
            for x in r.json()["results"]
        )

crew 该怎么接线?

直觉是给每个 Agent 都配上搜索工具。忍住。更省也更自洽的模式是只让一个 researcher 持有工具,下游 Agent 消费它的产出。

from crewai import Agent, Task, Crew

search = WebSearchTool()

researcher = Agent(
    role="Researcher",
    goal="为当前问题找到并引用当前的证据",
    backstory="没有 URL 支撑的事实,你从不陈述。",
    tools=[search],          # 只有这个 Agent 会搜索
)

analyst = Agent(
    role="Analyst",
    goal="把 researcher 的证据变成一个站得住的结论",
    backstory="你严格只基于交到你手上的证据工作。",
    tools=[],                # 不给工具——读 researcher 的产出
)

crew = Crew(
    agents=[researcher, analyst],
    tasks=[
        Task(description="研究:{topic}", agent=researcher,
             expected_output="5–8 条要点,每条附 URL"),
        Task(description="仅基于上述发现给出结论", agent=analyst,
             expected_output="一份带行内引用的简短备忘"),
    ],
)

crew.kickoff(inputs={"topic": "今年欧盟 AI 法案时间表的变化"})

两个 Agent、一个工具、一轮搜索。两个都配上,同一份证据你就付两次钱——而且往往会拿到两个略有出入的版本,这比多付一次更糟。

怎么给 crew 加上「读」的能力?

摘要能认出页面,但很少包含答案。给 researcher 第二个工具,让它去打开真正要紧的那两三个 URL。

@tool("Read Pages")
def read_pages(urls: list[str]) -> str:
    """取回指定 URL 的干净正文。

    在 Web Search 之后、摘要不足以回答时调用。只传真正相关的
    URL——最多三个。
    """
    r = httpx.post(
        "https://www.apipick.com/api/extract",
        headers={"x-api-key": API_KEY},
        json={"urls": urls[:3]},
        timeout=60,
    )
    r.raise_for_status()
    return "\n\n---\n\n".join(
        f"# {x['title']}\n{x['url']}\n\n{x['content'][:6000]}"
        for x in r.json()["results"] if x["status"] == "ok"
    )

抽取每个 URL 2 额度,读三个页面还不到一次搜索的一半。那个 [:6000] 不是为了省钱——是为了别把四万个字符丢给一个 crew,然后让下游每个 Agent 都重读一遍。

crew 上线前该检查什么?

  • 只有必须搜索的 Agent 才搜索。工具分配是 crew 里最主要的成本杠杆。
  • 每个工具都设超时。一个 Agent 里卡住的 HTTP 调用会把整个 crew 卡住。
  • expected_output 里明确要求 URL。任务描述不要求引用,转派过程中它们就会悄悄丢掉。
  • 仅成功计费。crew 产出的畸形工具调用比单 Agent 多;在 HTTP 200 计费下这些都是零成本。

同一个工具在其他框架里的写法:LangChainVercel AI SDKn8n,以及裸 OpenAI / Claude。先拿一把免费 key:100 额度,不用卡。

常见问题

CrewAI 有内置的网页搜索工具吗?

crewai-tools 为几家搜索厂商提供了封装,能用。自己写值得的情形是:你想控制返回的形状、想在源头限制条数,或者想换一家计费方式更适合多智能体负载的服务方。两种做法都是二十行左右。

用 @tool 装饰器还是继承 BaseTool?

单参数的搜索工具用 crewai.tools 的 @tool:函数就是工具,docstring 就是描述。当工具除了查询词还要接国家码和日期范围时,再上 BaseTool 配一个显式的 Pydantic args_schema——schema 能挡住 Agent 自己编参数名。

为什么同一个问题,crew 比单个 Agent 贵?

因为每个持有该工具的 Agent 都会用它。四个 Agent 研究同一个话题,发出的搜索大约是单个 Agent 的四倍,而任务转派还会再加几次。解法是架构层面的:把搜索工具只给一个 researcher Agent,让分析和写作的 Agent 基于它的产出工作,而不是再搜一遍。

怎么避免搜索结果撑爆 crew 的上下文?

只返回 title、url、snippet 三个字段,并用 max_num_results(取值 1–5,默认 5)在源头限制条数。这在 crew 里比在单 Agent 里更要紧:工具产出常常随着任务转派往下传,一组过宽的结果会被下游每一个 Agent 重读一遍。

跑一次 crew 的搜索成本是多少?

Web Search 每次 15 额度,1 美元 = 1,000 额度,约合每次 0.015 美元。一个需要五次搜索的研究任务约 0.075 美元。额度只在 HTTP 200 时扣除,所以某个 Agent 把参数写坏引发的那串重试,不会进账单。

本文涉及的 API

Sarah Choy
作者
Sarah Choy
CEO, API Pick

Sarah Choy 是 API Pick 的 CEO,专注于为 AI Agent 与 LLM 工作流构建可用于生产的 API。