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

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

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

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

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

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

我一般这样写:

python 复制代码
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,再随机抖动一下避免雪崩
python 复制代码
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_codemessage。客户端要分开处理这两类错:

python 复制代码
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 限制,多用户同时触发容易打爆。生产里加一个信号量:

python 复制代码
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 里可能搜两三次同样的东西。给搜索加一层缓存能省不少钱:

python 复制代码
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 怎么办?

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

  1. 重试 2 次后还是失败 → 返回结构化错误,模型收到错误信息
  2. 模型收到错误 → 改为基于已有知识回答,并明确告诉用户"以下信息未实时核实"
  3. 如果是降级回答 → 在 UI 上加个标识,让用户知道这是"猜测"
python 复制代码
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 也会跟着出问题。生产里我加了一层简单的熔断器:

python 复制代码
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 校验能第一时间发现。
python 复制代码
# 用 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 校验
  • 录制回放的测试数据

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

相关推荐
武子康1 小时前
VLA 已经能输出动作,为什么机器人仍需要多时间尺度闭环
人工智能·机器人·agent
阿里云云原生1 小时前
AI Agent 上线容易稳定难?阿里云 AgentLoop 推出“经验自进化”闭环治理方案
人工智能·阿里云·mybatis·agentscope
魔力女仆1 小时前
【RUST AI】把 TTS 搬进浏览器:kokoroi-rs 的 WASM 实践
人工智能·rust·wasm
小宋加油啊1 小时前
opencv工作中的基础知识点
人工智能·opencv·计算机视觉
sugar__salt1 小时前
向量数据库从零到实战:用 Milvus + Zilliz 构建 AI 日记语义检索系统
数据库·人工智能·embedding·milvus·rag·zilliz
m沐沐1 小时前
【深度学习】循环神经网络RNN——结构、原理与长期依赖问题解析
人工智能·pytorch·python·rnn·深度学习·算法·机器学习
Wang's Blog2 小时前
Go-Zero项目开发38:深入限流器实现与应用
开发语言·golang·go-zero
硬核子牙2 小时前
不要小瞧y=wx+b
人工智能·chatgpt·程序员
workflower2 小时前
情境感知系统
人工智能·机器学习·设计模式·自然语言处理·机器人