AI Agent开发:如何评估与选择Web Search API?

发布时间:2026/8/28 8:34:51
AI Agent开发:如何评估与选择Web Search API? 做 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 返回 429Agent 需要的是明确的 Retry-After 提示如果返回 529 这类服务过载错误Agent 需要判断“这是临时的可以等 1 秒再试一次”。通用搜索 API 不太会为“机器自动重试”做专门设计Agent 专用 API 则会把这类语义做得更明确。还有一点输出可靠性。Agent 的工具调用经常是链式的——搜索完还要继续推理、继续调其他工具。如果搜索返回 JSON 里字段名不稳定或者某些字段偶尔缺失整条链路就会中断。所以 Agent 专用 API 会更注重字段的确定性比如固定返回query、results、total这种稳定结构。3. Keenable 这类 API 的设计思路把搜索做成“工具”要理解 Keenable 这类项目得先理解 Agent 工具调用的三个环节参数定义、调用执行、结果解析。参数定义是第一步。Agent 框架通过 function schema 告诉模型“你有这个工具可以用”比如搜索工具接收query、max_results、freshness这几个参数。这一步决定了模型会不会正确调用它。调用执行是第二步。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 以上版本安装requestspip install requests如果你后面要跑 Function Call 示例再安装openai或anthropicSDK按自己用的模型选一个就好。4.2 封装一个搜索客户端新建文件web_search_client.pyimport 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: fBearer {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, headersheaders, jsonpayload, timeoutself.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, []), start1): 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 如何调用”之类的问题。其实不管前端是什么框架接搜索工具的路子都差不多如果你用的是支持 MCPModel 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 Key400 Bad Request参数不合法例如 thinking_budget 非正整数解析错误响应体检查参数类型修正参数后再调用不要盲目重试Connection lost mid-response网络波动或服务端断流检查超时设置和服务端日志设置合理超时使用流式响应的要处理半截数据403 Forbidden / Transport failure权限不足或网关拒绝代理检查 API Key、网关配置、白名单申请对应权限或检查网关路由注意一个关键原则不是所有错误都该重试。429、529、连接中断这类错误通常是临时性的可以重试。400、403这类错误说明是参数或权限问题你重试一百次结果还是一样反而会进一步触发限流。402余额不足重试没有意义应该先解决账户问题。下面是一个带指数退避和抖动的重试装饰器适合用在搜索调用上import time import random from functools import wraps RETRYABLE_STATUS {429, 500, 502, 503, 504, 529} def retry_with_backoff(max_retries3, base_delay0.5, max_delay8.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_retries3) 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_results5) 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(fP50 时延: {median(latencies):.2f}s) if len(latencies) 5: p95 quantiles(latencies, n20)[18] print(fP95 时延: {p95:.2f}s) print(f错误分布: {json.dumps(ERROR_CODES, ensure_asciiFalse)}) if __name__ __main__: client WebSearchClient() evaluate(client, QUERIES)注意一点评测 query 集合要尽量贴近你真实业务。如果你做的是代码助手就多放“某个 library 更新了什么”如果你做的是舆情监控就多放“某个事件的最新进展”。不要用泛泛的搜索词否则评估结果参考价值有限。如果两个 API 的成功率和时延差不多我会优先选“错误语义更清晰”的那家。因为 Agent 链路里可诊断性比单次快慢更重要。7. 生产环境接入最佳实践与坑从 Demo 到生产中间还有很多工程问题。这里总结几条我自己踩过后的经验。第一API Key 必须走环境变量或密钥管理服务不要硬编码。# .env SEARCH_API_KEYyour_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 接入搜索能力我的建议是先用一个最小示例跑通搜索工具确认返回结构和模型消费都没问题。设计一套贴近业务场景的评测 query对比候选 API 的成功率、时延、token 消耗。把重试、缓存、降级、日志这四件事在第一天就做好不要等上线了再补。定期检查调用量和成本根据实际使用情况调整max_results和缓存策略。搜索 API 选型没有绝对的最好只有适不适合你的 Agent。关键是建立一套“用数据说话”的评估方式而不是只看宣传文案或 GitHub Star 数。希望这篇文章能帮你少踩一些坑尤其是在工具调用稳定性和成本控制这两个环节。如果你正在用某个具体搜索 API 做 Agent 集成欢迎在评论区交流你的踩坑经验。