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

[ blog · tutorial ]8 min read

20 行 Python 给 LangChain 加网页搜索工具

Sarah Choy发布于 2026年9月23日约 8 分钟阅读
20 行 Python 给 LangChain 加网页搜索工具

LangChain 自带的搜索封装,把两个真正要紧的决定替你做了:结果以什么形状回来,以及 Agent 调了又扔掉的那些请求由谁买单。自己写这个工具只要二十行,而这两件事就重新回到你手里。

一句话总结

  • langchain_core 的 @tool 能把普通函数变成工具——docstring 就是模型读到的工具说明。
  • langgraph.prebuilt 的 create_react_agent 是当前的正路;initialize_agent 已是遗留写法。
  • 返回一个精简的 dict 列表,不要把原始 API 负载直接丢回去——每个用不上的字段,每一步都在替你烧 token。
  • POST https://www.apipick.com/api/search/web,带 x-api-key 头:每次 15 额度,仅 HTTP 200 扣费。
  • 再加一个读页面的工具(Extract,每个 URL 2 额度),让 Agent 能「搜得宽、读得窄」。

为什么要自己写,而不是 import 一个?

LangChain 为好几家搜索厂商提供了封装,做 demo 完全够用。但它们也替你做了两个决定,而这两个决定在 Agent 开始进循环的那一刻你就会想要回来:返回的形状,以及一次被丢弃的调用要花你多少钱

ReAct Agent 每一步都会重读整个消息历史。一个返回臃肿负载的工具不是一次性成本,而是这一轮之后每一步的税。自己写这个工具只要二十行,而裁剪也就回到了它该在的位置。

最小可用的工具长什么样?

import os, httpx
from langchain_core.tools import tool

API_KEY = os.environ["APIPICK_KEY"]

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

    只要答案依赖训练截止之后的信息、依赖当前价格、依赖新闻,
    或者依赖任何一个正常人会打开浏览器去查的东西,就调用它。

    查询词按普通人往搜索框里敲的方式写:大白话,不要操作符,
    不要引号,不要 site: 过滤。

    返回 {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 [
        {"title": x["title"], "url": x["url"], "snippet": x["snippet"]}
        for x in r.json()["results"]
    ]

怎么把它挂到 Agent 上?

langgraph.prebuiltcreate_react_agent 是当前的正路。它返回一个图;你用一个消息列表调用它,工具循环由它来跑。

from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_react_agent

llm = init_chat_model("anthropic:claude-sonnet-5")

agent = create_react_agent(
    llm,
    tools=[web_search],
    prompt=(
        "你是一名研究助理。每一个事实性断言都必须落在搜索结果上,"
        "并给出 URL。如果搜索没返回有用的东西,就直说,不要猜。"
    ),
)

out = agent.invoke({"messages": [
    {"role": "user", "content": "今年欧盟 AI 法案的时间表有什么变化?"}
]})
print(out["messages"][-1].content)

多数老教程用的 initialize_agent 是遗留路径。如果你在抄一篇 2024 年的文章,这一行就是要换掉的那行。

怎么让 Agent 不只是搜,还会读?

摘要回答的是「哪个页面」,很少能回答「它到底说了什么」。管用的模式是搜得宽、读得窄:给 Agent 第二个工具,能对指定 URL 取回干净正文,再由它自己决定哪两三个值得打开。

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

    在 web_search 之后、摘要不足以回答问题时调用。
    只传真正相关的 URL——最多三个。

    返回 {url, content} 的列表,导航和广告已去除。
    """
    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 [
        {"url": x["url"], "content": x["content"][:6000]}
        for x in r.json()["results"] if x["status"] == "ok"
    ]

agent = create_react_agent(llm, tools=[web_search, read_pages], prompt=...)

注意那个 [:3][:6000]。两者都是护栏,防着模型一次要十二个页面然后把自己淹死。搜索每次 15 额度、抽取每个 URL 2 额度,这份克制两边成本都不高——稀缺的是上下文窗口。

跑一轮到底花多少钱?

  • 一次搜索:15 额度 ≈ $0.015。
  • 读三个页面:6 额度 ≈ $0.006。
  • 一个典型的「搜三次、读两次」问题:工具调用约 $0.06,而且额度只在 HTTP 200 时扣。

最后那半句,才是真正改变你写 Agent 代码方式的东西。失败不要钱的时候,你可以放手让 Agent 去探——试探性的搜索、换个说法再来一次——而不必给每一条死路都开着计价器。完整论证见仅成功计费如何改变 Agent 设计

上线前该检查什么?

  • 每个工具都设超时。ReAct 循环里一个卡住的 HTTP 调用,看起来就是一个卡死的 Agent。
  • 明确写上「不知道就说不知道」。没有这句,一组糟糕的搜索结果是模型自信编造的最常见触发点。
  • 在源头限制条数,而不是在提示词里。模型会跟提示词讨价还价,但跟 max_num_results 讨不了。
  • 输出里要有引用。如果 Agent 说不出某个断言来自哪个 URL,那接地就没起作用。

把同一个工具接到别的框架,基本是翻译工作——见 CrewAI 版Vercel AI SDK 版,以及裸 OpenAI 与 Claude 版。先拿一把免费 key:100 额度,不用卡。

常见问题

该用 @tool 还是继承 BaseTool?

用 @tool。它会注册函数名,把 docstring 当作模型读到的工具说明,并从类型标注推导参数 schema。只有在确实需要显式的 Pydantic args_schema、需要实例上的共享状态、或者需要分开的同步/异步实现时,继承 BaseTool 才划算——搜索工具很少属于这一类。

还该用 initialize_agent 来搭 Agent 吗?

不该。initialize_agent 是遗留路径。当前写法是 langgraph.prebuilt 的 create_react_agent,它返回一个图,你用一个 messages 列表去调用它。工具循环、消息状态和流式输出它都替你处理了,不用自己写 executor。

为什么 docstring 这么关键?

因为它就是模型看到的全部规格。@tool 会把 docstring 解析成工具说明,把类型标注解析成参数 schema。一个只写着「搜索网页」的 docstring,换来的是把用户原话整句拿去搜的 Agent;而一个写清了「什么时候用、查询词怎么写、会返回什么」的 docstring,换来的是像人一样搜索的 Agent。

怎么防止 Agent 把上下文烧在搜索结果上?

在工具里裁,不要在提示词里求。只返回 title、url、snippet 三个字段,并在源头限制条数——max_num_results 取值 1–5,默认 5。ReAct Agent 每一步都会重读完整消息历史,所以一个臃肿的工具返回不是只付一次费,而是这一轮后面每一步都在付。

Agent 重试失败的调用,账单会怎样?

如果服务方只在成功时计费,就什么都不会发生。额度只在 HTTP 200 时扣除,所以超时、key 写错的 401、参数畸形的 400,成本都是零。这一点在 Agent 代码里比在脚本里重要得多:ReAct 循环拿到工具报错,通常会换个说法再试一次,而不是直接停下。

本文涉及的 API

Sarah Choy
作者
Sarah Choy
CEO, API Pick

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