欢迎光临
我们一直在努力

2026 年 7 月,SERP API 调用的稳定性实战:超时、重试、降级

接 SERP API 跑 Agent,第一个版本能 demo,第二个版本能上线,但从 demo 到生产之间,最难的是稳定性。上游抖动、网络异常、限流、模型发起多次相同调用,每一个点都可能让你的服务出问题。

这篇文章把我踩过的坑整理一下:怎么设计超时和重试,怎么处理上游错误体,怎么在搜索失败时让 Agent 不至于崩。下面的例子都以 serpbase 的接口为例。

客户端要扛住的第一件事:超时

Google 这类上游偶发超时很正常,一次调用设个 8s 大概够用。但要分开两层来设:

  • 客户端到 serpbase 的 HTTP 超时(requests 的 timeout 参数)
  • 整个 Agent tool call 的整体超时(防止一个搜索拖垮整个对话)

我一般这样写:

import requests

HTTP_TIMEOUT = 8 # 单次 HTTP 调用
TOTAL_TIMEOUT = 15 # 含重试的整体预算

def serp_search(query, hl="en", gl="us"):
return requests.post(
"https://api.serpbase.dev/google/search",
json={"q": query, "hl": hl, "gl": gl, "page": 1, "device": "default"},
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
timeout=HTTP_TIMEOUT,
).json()

TOTAL_TIMEOUT 这层用 signal.alarm 或者 asyncio.wait_for 都可以,看你跑在什么 runtime 里。

重试不是越多越好

重试要回答三个问题:

  • 哪些状态码要重试?
  • 重试几次?
  • 退避怎么算?

我的经验是:

  • 重试对象:网络超时、429、5xx
  • 不重试:4xx 业务错(status=401 这种,瞎重试只是浪费额度)
  • 次数:2 次够了
  • 退避:指数 0.5s, 1.5s,再随机抖动一下避免雪崩

import random
import time
import requests

RETRY_STATUS = {429, 500, 502, 503, 504}

def with_retry(fn, max_retry=2):
for i in range(max_retry + 1):
try:
r = fn()
except (requests.Timeout, requests.ConnectionError):
if i == max_retry:
raise
time.sleep(0.5 * (2 ** i) + random.uniform(0, 0.2))
continue
if r.status_code in RETRY_STATUS and i < max_retry:
time.sleep(0.5 * (2 ** i) + random.uniform(0, 0.2))
continue
return r
return r

业务失败 vs 网络失败

serpbase 的失败响应也是 JSON,成功外壳和失败外壳字段一样,只是 status 非 0,并且多了 error_code、message。客户端要分开处理这两类错:

def call_serp(query):
r = with_retry(lambda: requests.post(
"https://api.serpbase.dev/google/search",
json={"q": query, "hl": "en", "gl": "us", "page": 1, "device": "default"},
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
timeout=HTTP_TIMEOUT,
))
body = r.json()
if body.get("status") != 0:
raise SerpBaseBizError(body.get("error_code"), body.get("message"), body.get("request_id"))
return body

业务错要落日志,request_id 必须打进去——找客服或者自己排查都靠它。监控告警也要分开:

  • 网络失败率:看 HTTP 状态码
  • 业务失败率:看 status != 0 的占比

这两类错的原因不同,处理方式也不同,别混在一起。

并发控制

SERP API 一般有 QPS 限制,多用户同时触发容易打爆。生产里加一个信号量:

import asyncio

sem = asyncio.Semaphore(5) # 最多 5 个并发

async def guarded_search(query):
async with sem:
return await call_serp_async(query)

Semaphore(5) 不是越大越好,要看你的套餐 QPS 限制。如果不确定,先从 3 开始,观察一周的 P95 延迟和成功率,再调大。

缓存

模型爱"确认性检索",同一个 session 里可能搜两三次同样的东西。给搜索加一层缓存能省不少钱:

import hashlib
import json
from cachetools import TTLCache

cache = TTLCache(maxsize=2000, ttl=600) # 10 分钟

def cached_serp_search(query, hl, gl, page):
key = hashlib.sha256(
json.dumps({"q": query, "hl": hl, "gl": gl, "p": page}, sort_keys=True).encode()
).hexdigest()[:16]
if key in cache:
return cache[key]
result = call_serp(query, hl, gl, page)
cache[key] = result
return result

新闻类查询 TTL 给 5–10 分钟,事实类查询可以给 30 分钟。top_stories 变化快,缓存要短。

失败降级

最关键的一点:搜索失败的时候,Agent 怎么办?

我现在的做法是三层降级:

  • 重试 2 次后还是失败 → 返回结构化错误,模型收到错误信息
  • 模型收到错误 → 改为基于已有知识回答,并明确告诉用户"以下信息未实时核实"
  • 如果是降级回答 → 在 UI 上加个标识,让用户知道这是"猜测"
  • def agent_with_fallback(user_query):
    try:
    results = cached_serp_search(user_query, "en", "us", 1)
    except (SerpBaseBizError, requests.HTTPError) as e:
    return {
    "answer": None,
    "degraded": True,
    "reason": str(e),
    "request_id": getattr(e, "request_id", None),
    }
    return {"answer": synthesize_with_results(user_query, results), "degraded": False}

    监控和告警

    跑稳了之后要加监控。我一般盯这几个指标:

    • 成功率:HTTP 2xx + 业务 status=0 的占比。低于 95% 就要查。
    • P95 延迟:超过 SLA 阈值要告警。serpbase 这类服务一般 2s 内能回来。
    • 5xx 占比:超过 1% 就要排查,可能是 IP 池或上游问题。
    • 业务错码分布:某个 error_code 突然飙升一般是参数问题。

    把这些指标用 Prometheus 或者你喜欢的监控系统打点,告警阈值按历史数据调,别一上来就设得很严。

    一些实际跑出来的经验

    • request_id 一定要落日志。我每次问客服第一个问题就是"能不能给我 request_id"。
    • 客户端和上游之间最好加一层代理(哪怕只是一个简单的函数),这样换上游服务的时候改动小。
    • 监控别只看平均值。平均值会把问题平均掉,要看 P95、P99。
    • 失败降级的提示要"诚实"。让模型说"这个我没查到"比"我编一个"对用户友好得多。

    熔断和隔离

    SERP API 是外部依赖,一旦上游出问题,你的 Agent 也会跟着出问题。生产里我加了一层简单的熔断器:

    import time

    class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_time=30):
    self.failures = 0
    self.threshold = failure_threshold
    self.recovery_time = recovery_time
    self.open_since = None

    def allow(self):
    if self.open_since is None:
    return True
    if time.time() self.open_since > self.recovery_time:
    self.open_since = None
    self.failures = 0
    return True
    return False

    def record_failure(self):
    self.failures += 1
    if self.failures >= self.threshold:
    self.open_since = time.time()

    def record_success(self):
    self.failures = 0

    breaker = CircuitBreaker(failure_threshold=5, recovery_time=30)

    def serp_with_breaker(query):
    if not breaker.allow():
    raise SerpBaseBizError("circuit_open", "上游连续失败,临时熔断", None)
    try:
    r = with_retry(lambda: requests.post(
    "https://api.serpbase.dev/google/search",
    json={"q": query, "hl": "en", "gl": "us", "page": 1, "device": "default"},
    headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
    timeout=HTTP_TIMEOUT,
    ))
    body = r.json()
    if body.get("status") != 0:
    breaker.record_failure()
    raise SerpBaseBizError(body.get("error_code"), body.get("message"), body.get("request_id"))
    breaker.record_success()
    return body
    except (requests.Timeout, requests.ConnectionError):
    breaker.record_failure()
    raise

    熔断器打开时直接返回失败,不打上游。30 秒后尝试半开,恢复后继续。

    熔断的核心不是"省几次调用",而是防止故障扩散——上游可能因为被打爆而恢复更慢,熔断可以让它喘口气。

    测试

    SERP API 的测试比较特殊,因为它依赖外部服务。几个常用的做法:

    • 录制回放:用真实 API 录一份响应,存到文件,测试时回放。注意 date 这类时间字段要脱敏。
    • Mock 客户端:写一个假的 serp_search 函数,返回固定 JSON。在单元测试里用。
    • 契约测试:用 schemathesis 或者 pydantic 校验响应 JSON 符合预期 schema。这点对 SERP API 特别重要——上游偶尔会改字段,schema 校验能第一时间发现。

    # 用 pydantic 校验响应
    from pydantic import BaseModel

    class OrganicResult(BaseModel):
    rank: int
    title: str
    link: str
    snippet: str | None = None

    def test_response_shape():
    body = call_serp("python asyncio")
    organic = body["search"]["organic"]
    for item in organic:
    OrganicResult.model_validate(item) # 不符合 schema 会抛错

    把这些都补上之后,SERP API 的稳定性基本能撑住生产。下面以 serpbase 的接口为例(文档),它的错误体结构和成功外壳一致,写客户端不用维护两套。

    上线前 checklist

    最后留个 checklist,上线前对着过一遍:

    • HTTP 超时设置(建议 5–10s)
    • 重试逻辑(2 次,指数退避,只重试 429/5xx)
    • 业务错和 HTTP 错分开处理
    • request_id 进日志
    • QPS 信号量
    • 缓存层(5–30 分钟 TTL)
    • 熔断器
    • 失败降级路径
    • 监控指标(成功率、P95、5xx 占比)
    • 告警阈值
    • Pydantic schema 校验
    • 录制回放的测试数据

    按这个走完一遍,稳定性这块基本就稳了。

    赞(0)
    未经允许不得转载:171主机测评 » 2026 年 7 月,SERP API 调用的稳定性实战:超时、重试、降级
    分享到: 更多 (0)

    评论 抢沙发

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