AI Agent开发:如何评估与选择Web Search API?
2026/8/28 8:34:41 网站建设 项目流程

做 AI Agent 开发这段时间,我最大的感受是:模型能力决定 Agent 的上限,但工具调用质量决定 Agent 的下限。所有工具里,web search 又是最特殊的一个——几乎每个 Agent 都要用,但市面上大多数搜索 API 并不是为 Agent 设计的。

最近 Show HN 上出现了一个新项目 Keenable,定位是 “A different web search API for AI agents”。这个定位很有意思,它背后站着一个判断:从大模型应用里调搜索,和从浏览器里用搜索,根本不是一回事。

这篇文章我想聊透一件事:当你在给自己的 Agent 接入搜索能力时,真正要评估的到底是什么。我会从传统搜索 API 的痛点出发,分析 Agent 专用搜索 API 的设计思路,再给出可直接复制的 Python 封装示例、重试策略、评估方法和生产环境注意点。无论你最终选 Keenable、Tavily 还是自建搜索服务,这套判断框架都适用。

1. 为什么 AI agent 需要一套“不同”的 web search API

先说一个常见的误解:很多人以为 Agent 搜索就是把“搜索接口”接到代码里,剩下的事情都由大模型自己理解。但实际开发中你会发现,搜索结果的返回方式,往往决定了 Agent 的稳定程度

人类使用搜索 API 时,拿到一段 HTML 也好、一段 JSON 也好,可以通过眼睛快速找到有用信息。但 Agent 不是人,它拿到的是一段上下文窗口里的文本。如果返回结构不统一、内容冗长、错误码不可预测,大模型就会在解析上消耗大量 token,甚至直接把错误信息当成正确答案。

我在接入多个搜索服务时遇到的三个典型问题:

第一个是返回格式不适合程序解析。有些搜索 API 返回的是完整网页片段,里面夹杂大量导航、广告、脚本残留。Agent 拿到后需要再思考一遍“哪些是有用内容”,这个过程既容易出错,又浪费 token。

第二个是错误响应不可预测。限流返回 429、服务过载返回 529、余额不足返回 402,更麻烦的是有些服务会在连接中途断开,Agent 只能拿到半截内容。如果代码里没有统一处理这些异常,Agent 就会表现得很“笨”——重试、报错、甚至直接回答“无法获取信息”。

第三个是结果与上下文不匹配。人类搜一个话题,希望看到 10 个链接自己点。Agent 搜一个话题,只希望拿到足够回答问题的一段摘要。很多通用搜索 API 没有考虑“压缩结果适合放进 Prompt”这个需求,导致开发者还要再做一层摘要。

Keenable 这一类 Agent 专用搜索 API 想解决的问题,就是把这些“人用”的搜索能力,改造成“模型用”的标准函数。它不是为了取代 Google/Bing 这类通用搜索,而是为了填上 Agent 工具链里“搜索工具”这个位置的缺口。

2. Agent 专用搜索 API 与通用搜索 API 的核心差异

先看一张对比表。

维度通用搜索 APIAgent 专用搜索 API
面向对象人类阅读,开发者二次解析大模型直接消费,结构化输出
返回内容网页标题、链接、HTML 片段摘要、来源、结构化字段
Token 控制默认返回长内容提供 max_results / snippet 长度控制
错误处理按常规接口返回错误更强调可重试、降级和限流语义
上下文适配需要二次加工才能进 Prompt开箱即用,适合 function calling
使用场景搜索引擎、数据分析、爬虫Agent 工具、RAG、实时问答、代码助手

这张表里最核心的差异是“结果适配度”。

像 Keenable 这类 API 在设计上通常会把搜索结果处理成“给模型看的结构”:每个结果包含标题、URL、摘要,摘要长度可以配置。这样一来,Agent 可以少写很多清理逻辑,直接把结果交给大模型总结。

另一个容易被忽略的差异是调用方式的重试友好性。Agent 的 tool call 是自动触发的,不会像人类一样手动刷新。如果 API 返回 429,Agent 需要的是明确的 Retry-After 提示;如果返回 529 这类服务过载错误,Agent 需要判断“这是临时的,可以等 1 秒再试一次”。通用搜索 API 不太会为“机器自动重试”做专门设计,Agent 专用 API 则会把这类语义做得更明确。

还有一点:输出可靠性。Agent 的工具调用经常是链式的——搜索完还要继续推理、继续调其他工具。如果搜索返回 JSON 里字段名不稳定,或者某些字段偶尔缺失,整条链路就会中断。所以 Agent 专用 API 会更注重字段的确定性,比如固定返回queryresultstotal这种稳定结构。

3. Keenable 这类 API 的设计思路:把搜索做成“工具”

要理解 Keenable 这类项目,得先理解 Agent 工具调用的三个环节:参数定义、调用执行、结果解析

参数定义是第一步。Agent 框架通过 function schema 告诉模型“你有这个工具可以用”,比如搜索工具接收querymax_resultsfreshness这几个参数。这一步决定了模型会不会正确调用它。

调用执行是第二步。API 收到请求后,去真实搜索引擎或索引库里查询,然后做摘要、去重、排序,返回结果。这一步的关键是:延迟不能太高,否则 Agent 会超时。

结果解析是第三步。返回的 JSON 要被模型消费。如果字段设计得好,模型不需要额外处理,直接基于摘要就能回答用户问题。

大多数通用搜索 API 把功夫花在“检索结果好不好”,而 Agent 专用搜索 API 把功夫花在“从第一步到第三步整条链路顺不顺”。这是两种产品取舍,没有绝对对错,但用在 Agent 场景里,后者的体验往往更好。

Keenable 声称自己是 “different”,我理解其“不同”很可能体现在几个思路上:

第一个是更面向工具调用。它可能不满足于只返回网页链接,而是把“搜索结果”包装成适合模型直接消费的信息单元。

第二个是更重视控制力。开发者可以控制返回多少条、每段摘要多长、是否包含原始链接、是否带时间过滤。这些看似细节的选项,在几千 token 的上下文窗口里就是真金白银。

第三个是更关注失败场景。Agent 调用搜索 API 的频率高,失败率即使只有 1%,在长链路任务里也会被放大。所以专用 API 的 SDK 和错误码设计,会更偏重“让程序自己恢复”。

当然,这些都是基于产品定位做出的合理推测。具体能力以官方文档为准。但可以确定的是,这个方向看重的是“Agent 整条 tool call 链路的稳定性”,而不仅仅是“搜索质量”。

4. 最小接入示例:用 Python 封装搜索 API 并接入 Agent

下面用一个最小示例展示“把搜索 API 封装成 Agent 工具”的完整套路。这里的 URL 和参数是示意写法,实际请以你选择的 API 官方文档为准,但结构是通用的。

4.1 环境准备

建议使用 Python 3.9 以上版本,安装requests

pip install requests

如果你后面要跑 Function Call 示例,再安装openaianthropicSDK,按自己用的模型选一个就好。

4.2 封装一个搜索客户端

新建文件web_search_client.py

import os import requests from typing import Optional class WebSearchClient: """一个通用的搜索 API 客户端骨架。 真实接入时,只需要替换 base_url、请求参数和返回字段映射。 """ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None): self.api_key = api_key or os.getenv("SEARCH_API_KEY", "") self.base_url = base_url or "https://api.example.com/v1/search" self.timeout = 10 # 根据 API 实际情况调整 def search(self, query: str, max_results: int = 5, **kwargs) -> dict: """执行一次搜索,返回结构化 JSON。""" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "query": query, "max_results": max_results, # 不同 API 支持的参数不同,例如 freshness、rerank、answer 等 **kwargs, } resp = requests.post( self.base_url, headers=headers, json=payload, timeout=self.timeout, ) resp.raise_for_status() data = resp.json() # 这里要做字段映射,对齐官方返回结构 return { "query": data.get("query", query), "results": data.get("results", []), "total": data.get("total", len(data.get("results", []))), } def format_for_llm(self, result: dict, max_chars_per_result: int = 500) -> str: """把搜索结果压缩成适合放入 Prompt 的文本。 减少 token 消耗,同时保留模型回答需要的关键内容。 """ fragments = [] for idx, item in enumerate(result.get("results", []), start=1): title = item.get("title", "无标题") url = item.get("url", "") snippet = item.get("snippet", item.get("content", "")) if len(snippet) > max_chars_per_result: snippet = snippet[:max_chars_per_result] + "..." fragments.append(f"{idx}. {title}\n URL: {url}\n {snippet}") if not fragments: return "未搜索到相关结果。" return "\n\n".join(fragments)

这段代码的关键逻辑:

  • search方法只负责发起请求、解析 JSON、做字段映射。字段映射非常重要,因为不同 API 的返回结构不一样,统一成query / results / total后,上层工具就能稳定消费。
  • format_for_llm方法是给大模型用的格式化器。它不是把原始 JSON 直接丢进上下文,而是压缩成“标题 + URL + 摘要”的可读片段,省 token 且不容易被模型误解。

4.3 注册为 Function Calling 工具

以 OpenAI 的函数调用格式为例,我们需要为模型声明一个web_search工具:

{ "type": "function", "function": { "name": "web_search", "description": "搜索互联网上的最新信息,适合回答时效性问题或模型知识之外的查询。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "用户要搜索的关键词或问题" }, "max_results": { "type": "integer", "description": "返回结果数量,默认 5", "minimum": 1, "maximum": 10 } }, "required": ["query"] } } }

当模型判断需要搜索时,会返回一个 tool call,类似:

tool_call = { "function": { "name": "web_search", "arguments": '{"query": "Keenable web search API 支持哪些参数", "max_results": 5}' } }

你的代码解析出arguments,调用WebSearchClient,然后把format_for_llm的结果作为 tool message 返回给模型,模型再基于搜索结果完成最终回答。

4.4 对接不同 Agent 框架的通用思路

热词里常看到 “Claude Code 能配置 Tavily 作为 web search 工具吗”“DeepSeek API 如何调用”之类的问题。其实不管前端是什么框架,接搜索工具的路子都差不多:

  • 如果你用的是支持 MCP(Model Context Protocol)的客户端,搜索 API 可以包成一个 MCP server,再让客户端去连接。
  • 如果你用的是纯 Function Calling 流程,就按 4.3 节的 schema 注册工具,自己维护调用循环。
  • 如果你用的是成熟 Agent 框架(比如 LangChain、LlamaIndex),一般都有现成的工具基类,把搜索客户端封装成基础工具即可。

真正要关注的不是“某个框架怎么接”,而是“你的工具函数返回的内容是否稳定、是否省 token”。这一层做好,换框架只是改一行注册代码的事。

5. API 调用常见错误与重试策略

做 AI Agent 开发,最烦的不是模型回答不好,而是 API 调着调着就报错。以下错误我都实际遇到过,处理思路也是通用的。

问题现象可能原因排查方式解决方案
429 Too Many Requests触发了限流检查响应头 Retry-After指数退避重试,降低并发
529 Overloaded服务端过载,通常临时查看官方状态页短暂等待后重试,最多 2-3 次
402 Insufficient Balance账户余额不足查看账户控制台充值或切换 API Key
400 Bad Request参数不合法,例如 thinking_budget 非正整数解析错误响应体,检查参数类型修正参数后再调用,不要盲目重试
Connection lost mid-response网络波动或服务端断流检查超时设置和服务端日志设置合理超时,使用流式响应的要处理半截数据
403 Forbidden / Transport failure权限不足或网关拒绝代理检查 API Key、网关配置、白名单申请对应权限,或检查网关路由

注意一个关键原则:不是所有错误都该重试

  • 429529、连接中断这类错误,通常是临时性的,可以重试。
  • 400403这类错误,说明是参数或权限问题,你重试一百次结果还是一样,反而会进一步触发限流。
  • 402余额不足,重试没有意义,应该先解决账户问题。

下面是一个带指数退避和抖动的重试装饰器,适合用在搜索调用上:

import time import random from functools import wraps RETRYABLE_STATUS = {429, 500, 502, 503, 504, 529} def retry_with_backoff(max_retries=3, base_delay=0.5, max_delay=8.0): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): delay = base_delay for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as exc: status = getattr(exc, "response", None) status_code = status.status_code if status is not None else None # 参数类错误不重试,直接抛出 if status_code in (400, 401, 403): raise if attempt == max_retries - 1: raise # 指数退避 + 随机抖动,避免多个请求同时重试打爆服务端 sleep_time = min(max_delay, delay * (2 ** attempt)) sleep_time += random.uniform(0, 0.1 * sleep_time) time.sleep(sleep_time) return None return wrapper return decorator # 用法示例 @retry_with_backoff(max_retries=3) def search_with_retry(query: str): client = WebSearchClient() return client.search(query)

这里真正容易踩坑的地方是:如果上游服务已经 529,你所有进程同时重试,会把上游打得更惨。所以重试一定要加抖动,并且限制最大重试次数。更好的做法是在服务端或网关层做统一限流,这属于生产环境优化,后面会讲到。

6. 如何评估一个搜索 API 是否适合你的 Agent

很多分享只讲“接入”,不讲“评估”。但选型才是真正决定项目质量的一步。我建议你在正式接入前,做一轮简单的评测。

评估搜索 API 至少要看五个指标:

  • 成功率:100 次调用里成功多少次。失败太多,Agent 体验会非常差。
  • 时延:P50 和 P95。P95 比均值更能反映真实体验,时延过高会导致 Agent 超时。
  • 结果相关性:前 3 条结果是否和搜索意图匹配。不相关的结果会直接带偏模型回答。
  • Token 消耗:格式化后的内容有多大。同样结果,越省 token 越划算。
  • 错误类型分布:错误集中在 429 还是 529。如果是稳定的 429,可以通过控制并发解决;如果是 529,说明服务端容量不够,换一家更稳妥。

下面是一个简单的评测脚本骨架,你可以根据自己的 query 集合跑出对比数据:

import json import time from statistics import median, quantiles QUERIES = [ "Keenable web search API 最新动态", "2025 年大模型 Agent 发展趋势", "Python 异步编程最佳实践", "搜索 API 的限流策略", ] ERROR_CODES = {} def evaluate(client, queries): latencies = [] success_count = 0 total = len(queries) for q in queries: start = time.time() try: result = client.search(q, max_results=5) latency = time.time() - start latencies.append(latency) success_count += 1 print(f"[OK] {q} -> {len(result.get('results', []))} 条结果, 耗时 {latency:.2f}s") except Exception as exc: latency = time.time() - start latencies.append(latency) code = getattr(getattr(exc, "response", None), "status_code", "unknown") ERROR_CODES[code] = ERROR_CODES.get(code, 0) + 1 print(f"[FAIL] {q} -> status {code}, 耗时 {latency:.2f}s") latencies.sort() print("\n===== 评估结果 =====") print(f"成功率: {success_count}/{total}") print(f"P50 时延: {median(latencies):.2f}s") if len(latencies) >= 5: p95 = quantiles(latencies, n=20)[18] print(f"P95 时延: {p95:.2f}s") print(f"错误分布: {json.dumps(ERROR_CODES, ensure_ascii=False)}") if __name__ == "__main__": client = WebSearchClient() evaluate(client, QUERIES)

注意一点:评测 query 集合要尽量贴近你真实业务。如果你做的是代码助手,就多放“某个 library 更新了什么”;如果你做的是舆情监控,就多放“某个事件的最新进展”。不要用泛泛的搜索词,否则评估结果参考价值有限。

如果两个 API 的成功率和时延差不多,我会优先选“错误语义更清晰”的那家。因为 Agent 链路里,可诊断性比单次快慢更重要。

7. 生产环境接入最佳实践与坑

从 Demo 到生产,中间还有很多工程问题。这里总结几条我自己踩过后的经验。

第一,API Key 必须走环境变量或密钥管理服务,不要硬编码。

# .env SEARCH_API_KEY=your_key_here

硬编码 Key 最大的风险是泄露。一旦代码被提交到公共仓库,别人就能拿你的 Key 刷接口。最好配置密钥管理服务,并给 Key 设置预算上限,防止被恶意刷量。

第二,一定要做结果缓存。

Agent 对同样的 query 可能会在短时间内调用多次。比如多个用户问同一个热点事件,搜索内容基本一样。合理的做法是以“query + 时间窗口”为 key,把搜索结果缓存 5-10 分钟,可以显著降低成本和时延。可以用 Redis,本地开发也可以先用字典缓存。

第三,设置合理的超时和降级策略。

搜索 API 不是每次都可靠的。你要给 Agent 留退路:搜索失败时,是直接告诉用户“暂时无法获取实时信息”,还是使用模型自身的知识回答?建议默认让模型基于已有知识回答,并在回答里说明“该信息未经过最新检索验证”。

第四,注意 token 成本。

搜索结果进入上下文后会被算进 token。返回 10 条长摘要和返回 3 条短摘要,成本差别可以到好几倍。建议在 Function Calling 的max_results参数上做控制:普通问答 3 条就够,复杂调研再放宽到 5-8 条。

第五,记录完整的调用日志。

日志里至少要包含:query、返回结果条数、耗时、状态码、缓存是否命中、最终使用哪个模型回答。这样一旦 Agent 回答质量下滑,你能快速定位是模型问题、搜索问题还是格式化问题。建议用结构化日志,字段统一,方便后面做统计分析。

第六,并发控制要和上游限流对齐。

接口在低并发下很稳定,但如果你用多线程或异步批量调用,很容易触发 429。一个稳妥做法是信号量限流,把并发数控制在 API 服务商建议的值以下。另外,如果你用的是某些在线编码工具或网关,注意 403 通常是网关路由或鉴权问题,不要盲改业务代码。

第七,保持版本兼容意识。

搜索 API 的返回结构可能会迭代。建议在客户端代码里做一层“适配器”,不要直接在业务代码里依赖某个字段。这样上游字段改了,你只需要改适配器。

8. 总结与建议:搜索 API 正在变成 Agent 的“标准工具”

回到 Keenable 这个项目。它让我印象最深的不是某个具体功能,而是它对问题的定义:搜索 API 不应该只关心“搜得好不好”,还要关心“Agent 用起来顺不顺”。

这其实是整个 Agent 工具链正在经历的变化。早期大家把 API 一接了之,后来发现,工具返回结构、错误语义、重试策略这些“工程小事”才是决定 Agent 体验的关键。Keenable 这类 Agent-first 搜索 API 的出现,说明这个赛道开始有人认真对待这些工程细节了。

如果你正在给自己的 Agent 接入搜索能力,我的建议是:

  1. 先用一个最小示例跑通搜索工具,确认返回结构和模型消费都没问题。
  2. 设计一套贴近业务场景的评测 query,对比候选 API 的成功率、时延、token 消耗。
  3. 把重试、缓存、降级、日志这四件事在第一天就做好,不要等上线了再补。
  4. 定期检查调用量和成本,根据实际使用情况调整max_results和缓存策略。

搜索 API 选型没有绝对的最好,只有适不适合你的 Agent。关键是建立一套“用数据说话”的评估方式,而不是只看宣传文案或 GitHub Star 数。希望这篇文章能帮你少踩一些坑,尤其是在工具调用稳定性和成本控制这两个环节。如果你正在用某个具体搜索 API 做 Agent 集成,欢迎在评论区交流你的踩坑经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询