欢迎光临
我们一直在努力

Agent 终于能上网了:WebSearch + WebFetch

Agent 终于能上网了:WebSearch + WebFetch

系列回顾:主循环 · 代码库工具 · REPL · 项目上下文 · Skills · 权限 + Write · MCP 概念 · MCP 实现 · Context Budget · Bash · compact 2.0 · autocompact · Hooks · Memory

到目前为止,react-agent-mini 能读本地仓库、改文件、跑 Bash、挂 MCP、开子代理,也能半路 Ctrl+C。 但还有一个很常见的缺口:问时效问题只能靠模型瞎猜,或者让 Bash 去 curl——既难测,又不安全。 这篇讲 v7-web-search + v7-web-fetch:一对只读联网工具,把「搜一下 → 打开看」补进工具表。


先说结论:上网是两步,不是一个大锤子

人查资料通常是:

先搜索 → 扫标题/摘要 → 点开几页细读

Agent 也一样。所以仓库没有做成「一个超级 Web 工具」,而是两把专用钥匙:

工具干什么像什么
WebSearch 给关键词,拿回 title / url / snippet 列表 搜索引擎结果页
WebFetch 给具体 URL,拿回可读正文(HTML 去标签) 打开链接读内容

合在一起才闭环:

WebSearch("某库最新 breaking change")
→ 挑一个靠谱 URL
→ WebFetch(url)
→ 用正文回答 / 再决定要不要改代码

只 Search 没有 Fetch:模型只能靠 snippet 猜。 只 Fetch 没有 Search:模型得自己知道 URL——很多时效问题根本起不了步。


缺口到底是什么?

没有这对工具时,常见「假上网」有三种:

  • 模型凭训练记忆答——过期就错
  • 让 Bash 调 curl / wget——权限更重、输出难控、还容易碰到内网
  • 指望 MCP 临时挂一个搜索 server——能用,但不是默认能力
  • Claude Code 把联网检索做成内置 WebSearch,并配有 WebFetch。mini 用的是 DeepSeek 等兼容 Chat Completions,没有 Anthropic 那种服务端 search,所以走的是:

    客户端工具 + 外部 Search API + 本地 HTTP 拉页

    目标很克制:可中断、可测试、缺 Key 时失败得清楚,而不是静默装死。


    WebSearch:先把「找得到」做稳

    入参很短

    必填只有 query。可选:

    字段作用
    allowed_domains 只保留这些域名的结果
    blocked_domains 排除这些域名
    num_results 返回条数(默认约 8)

    工具本身是只读、可并发安全的——不改磁盘,也不需要像 Write/Bash 那样弹 y/N。

    Adapter:换搜索后端,不换工具形状

    真正发请求的不是 WebSearchTool 硬编码某一家,而是 adapter:

    WebSearchTool.call
    → resolveWebSearchAdapter()
    → adapter.search(query, { signal, … })
    → 格式化成给模型看的文本

    当前支持:

    Adapter主要环境变量
    Brave(默认) BRAVE_API_KEY(或 BRAVE_SEARCH_API_KEY)
    Tavily TAVILY_API_KEY

    选择顺序在 resolveWebSearchAdapterKey 里写得很直白:

    export function resolveWebSearchAdapterKey(
    env: Record<string, string | undefined> = process.env,
    ): WebSearchAdapterKey {
    const explicit = env.WEB_SEARCH_ADAPTER?.trim().toLowerCase()
    if (explicit === 'brave' || explicit === 'tavily') {
    return explicit
    }
    if (readTavilyApiKey(env) && !readBraveApiKey(env)) {
    return 'tavily'
    }
    return 'brave'
    }

    可以记成:

    显式 WEB_SEARCH_ADAPTER
    → 否则:只有 Tavily Key 就用 Tavily
    → 否则默认 Brave

    测试可以 setWebSearchAdapterForTests(mock),不必每次打真网——这和主循环可注入 callModel 是同一味道。

    缺 Key / 失败:进 tool_result,不炸进程

    async call(args, context: ToolUseContext) {
    const adapter = resolveWebSearchAdapter()
    try {
    const hits = await adapter.search(args.query, {
    signal: context.abortController?.signal,
    numResults: args.num_results,
    allowedDomains: args.allowed_domains,
    blockedDomains: args.blocked_domains,
    })
    return { data: formatHits(args.query, hits) }
    } catch (err) {
    if (err instanceof WebSearchConfigError) {
    return { data: err.message, isError: true }
    }
    if (isAbortError(err) || context.abortController?.signal.aborted) {
    return { data: 'WebSearch aborted', isError: true }
    }
    const msg = err instanceof Error ? err.message : String(err)
    return { data: `WebSearch failed: ${msg}`, isError: true }
    }
    },

    要点:

    • 配置错误(没 Key)→ WebSearchConfigError → isError 文案
    • 用户 Ctrl+C → WebSearch aborted
    • 其它网络/API 失败 → 短错误,主循环继续

    模型看到失败可以改策略(换问法、提醒配置 Key),而不是整条会话崩掉。

    结果长什么样?

    给模型的是可读列表,不是原始 JSON 甩脸上:

    Web search results for "…":

    1. 标题
    https://…
    摘要……

    title / url / 可选 snippet——够用来挑链接,也够在 snippet 已经足够时少打一次 Fetch。


    WebFetch:再把「打得开」做安全

    Search 给的是候选 URL;真正核对文档、changelog、issue 正文,要靠 Fetch。

    入参更短:只要 url

    const webFetchInputSchema = z.object({
    url: z.string().min(1).describe('The URL to fetch'),
    })

    流程:

    assertSafeFetchUrl
    → GET(带超时 + 可与 turn abort 合并)
    → 按 content-type 取文本
    → HTML 去标签压空白
    → 超大则截断并标注 [truncated]

    默认大约 30s 超时 / 512KB 上限——防止一页把上下文撑爆。

    为什么必须有 SSRF 护栏?

    模型(或被污染的提示)可能让你去拉:

    • file:///etc/passwd
    • http://127.0.0.1:6379/…
    • 云厂商 metadata 地址
    • 家里的 192.168.x.x

    Agent 若「有网就能 GET」,等于把内网扫端口能力交给了不可信输入。所以 Fetch 在发请求前先拦:

    export function assertSafeFetchUrl(raw: string): URL {
    // …
    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new WebFetchUrlError(
    `Only http(s) URLs are allowed (got ${url.protocol})`,
    )
    }
    // localhost / 私网 / 链路本地 / 部分 metadata 主机名 → Blocked host
    return url
    }

    这是基础护栏,不是完美沙箱:DNS rebinding、跳板代理之类都刻意不在本刀范围内。但对「默认别把内网暴露给模型」已经够用。

    失败同样 fail-soft

    非法 URL、被拦主机、超时、abort,都变成带 isError 的 tool_result,不把异常抛穿主循环。


    两者怎么配合?一张图就够

    用户:这个依赖上周有 breaking change 吗?


    WebSearch(query)
    │ title / url / snippet

    模型挑选 URL(或发现 snippet 已够)


    WebFetch(url) ← 可选第二步
    │ 可读正文(可能 truncated)

    回答 / 再调本地 Read·Edit·Bash

    和本地工具的分工也很清楚:

    能力工具
    仓库里已有的文件 Read / Grep / Glob
    外网「有没有、在哪」 WebSearch
    外网「这一页写了什么」 WebFetch
    验证改动 Bash

    MCP Resource 仍然是「挂已声明的材料」;Web* 是「临时去公网查」。两者不互替。


    和 interrupt / 权限怎么接?

    上一篇 interrupt cascade 之后,联网工具必须接 AbortSignal,否则用户按了 Ctrl+C,搜索请求还在飞。

    两边都做了:

    • Search:adapter.search(…, { signal })
    • Fetch:AbortSignal.any([用户 signal, 超时 signal])

    权限侧:二者 isReadOnly() === true,REPL 不会为「读网页」弹写确认;仍可走 Hooks(若你 matcher 配了 WebSearch / WebFetch / *)。

    所以:

    急停拉索(interrupt)→ 能掐断搜索/拉页
    门卫(canUseTool)→ 默认放行只读联网
    项目规约(Hooks)→ 仍可额外拦或记日志


    30 秒感受一下

    配置任一搜索 Key(PowerShell 例):

    $env:TAVILY_API_KEY = "tvly-…"
    # 或
    $env:BRAVE_API_KEY = "…"

    然后:

    bun run dev

    自然语言即可,例如:

    用 WebSearch 查 react 19 的 release notes 链接,再 WebFetch 打开其中一篇,摘要三点。

    缺 Key 时不应崩进程,而应在工具结果里看到可读的配置错误提示。


    刻意没做什么?

    没做意味着什么
    Anthropic 服务端 search 走客户端 adapter,不绑一家模型厂商
    多 provider 配置 UI / /web-tools 面板 env 选 adapter 即可
    无头浏览器 / JS 渲染页 只拿静态 HTML/文本;SPA 可能残缺
    PDF / 二进制正文解析 非文本会失败或短错误
    完整代理绕过式 SSRF 攻防 基础拒绝私网与危险 scheme
    把 Search 默认绑死 Tavily Extract Fetch 独立 HTTP 直取

    这一刀验证的是最小闭环:

    内置 WebSearch(Brave/Tavily)
    + 内置 WebFetch(护栏 + 截断 + abort)
    → 搜得到、打得开、停得住、失败不炸


    系列拼图

    篇补的是哪一段
    代码库工具 本地读改
    Bash 本地跑
    MCP 外挂能力协议
    Interrupt 半路拉闸
    本篇 公网查与读

    Harness 又多一根柱子:循环、工具、会话、上下文、技能、权限、MCP、预算、hooks、memory、subagent、interrupt、web。


    你可以从这里带走什么?

  • 上网拆成 Search + Fetch——找链接和读正文是两件事。
  • Search 用 adapter——换 Brave/Tavily 不改工具契约;缺 Key 要清晰失败。
  • Fetch 先护栏再请求——只允许 http(s),默认拒绝 localhost / 私网。
  • 超时、截断、abort 都要有——否则上下文和会话都会被拖死。
  • 失败进 tool_result——联网是效应器,不是让主循环崩溃的理由。

  • 仓库与相关文档

    • GitHub:https://github.com/jimchou-h/react-agent-mini
    • 相关前作:Bash · Interrupt · MCP 概念
    • 源码:WebSearchTool.ts · resolveAdapter.ts · WebFetchTool.ts · fetchUrl.ts
    • 术语:src/tools/CONTEXT.md
    • OpenSpec:v7-web-search · v7-web-fetch

    欢迎 Star、Issue 和 PR。


    本文基于 react-agent-mini 变更 v7-web-search 与 v7-web-fetch 撰写:用一对只读联网工具补上「搜索 → 读页」闭环。

    赞(0)
    未经允许不得转载:171主机测评 » Agent 终于能上网了:WebSearch + WebFetch
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址