SERP API + Claude function calling:从 tool use 到 agent 的完整实现

12 阅读4分钟

场景

做一个 AI 助手,用户问"2026 年 SERP API 价格趋势",助手需要查实时数据,不能只靠训练数据。下面是完整的工程实现,包含 tool schema 设计 + 调用 + 上下文管理。

1. 选 API

我用 SerpBase 的 /google/search,原因:

  • 返回结构化 JSON,不用解析 HTML
  • POST 单 endpoint 简单
  • 失败自动 refund(不浪费 credit)
  • 1 credit / call,价格友好

2. Tool Schema 设计

Claude function calling 的核心是工具描述。Schema 写得好,模型才知道何时调。

import anthropic

tools = [
    {
        "name": "search_google",
        "description": """Search Google for real-time information. Use when:
- The question asks about recent events (last 6 months)
- The question asks about pricing, statistics, or current state
- The question involves specific companies, products, or current facts
- The user asks "what is the latest" or "current state"

Returns structured Google search results including title, link, snippet, and related questions. Max 5 organic results per call.""",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query, in English, 2-6 words. Do not include date."
                },
                "gl": {
                    "type": "string",
                    "description": "Country code for results. Examples: us, uk, cn, jp",
                    "default": "us"
                },
                "num": {
                    "type": "integer",
                    "description": "Number of results to return. 1-10.",
                    "default": 5,
                    "minimum": 1,
                    "maximum": 10
                }
            },
            "required": ["query"]
        }
    }
]

description 写得具体:何时用、返回什么、限制。模型靠这个判断何时调工具。

3. Tool 调用循环

import requests

def execute_search(query: str, gl: str = "us", num: int = 5) -> str:
    """调 SerpBase 查 SERP,返回裁剪后的文本"""
    try:
        r = requests.post(
            "https://api.serpbase.dev/google/search",
            headers={"X-API-Key": SERPBASE_KEY},
            json={"q": query, "gl": gl, "hl": "en", "page": 1, "num": num},
            timeout=10,
        )
        r.raise_for_status()
        data = r.json()

        # 裁剪到 top 5
        results = []
        for item in data.get("organic", [])[:num]:
            results.append(
                f"[{item['rank']}] {item['title']}\n"
                f"    {item['link']}\n"
                f"    {item.get('snippet', '')}"
            )
        return "\n\n".join(results)
    except requests.exceptions.Timeout:
        return f"SERP API timeout for query: {query}"
    except requests.exceptions.HTTPError as e:
        return f"SERP API error: {e.response.status_code}"
    except Exception as e:
        return f"SERP API failed: {str(e)}"

def run_agent(user_query: str, max_iterations: int = 3) -> str:
    """Run Claude agent with search tool"""
    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": user_query}]

    for _ in range(max_iterations):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=2048,
            tools=tools,
            messages=messages,
        )

        # 1. 收集 tool_use blocks
        tool_uses = [b for b in response.content if b.type == "tool_use"]

        # 2. 没 tool_use → 结束
        if not tool_uses:
            return next(b.text for b in response.content if b.type == "text")

        # 3. 把 assistant 消息加入历史
        messages.append({"role": "assistant", "content": response.content})

        # 4. 执行所有 tool 调用
        tool_results = []
        for tool_use in tool_uses:
            if tool_use.name == "search_google":
                result = execute_search(
                    tool_use.input["query"],
                    tool_use.input.get("gl", "us"),
                    tool_use.input.get("num", 5),
                )
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": result,
                })

        # 5. 把 tool 结果加入历史
        messages.append({"role": "user", "content": tool_results})

    return "Agent reached max iterations"

4. 关键设计

max_iterations 设 3:防止 agent 死循环(Claude 一直调工具不出结论)。

tool_use_id 必填:每个 tool_result 必须关联到对应 tool_use,否则模型分不清。

tool 失败返文本而不是抛异常:模型看得到失败信息,可以调整策略(换个 query 重试)。

client 不要复用:Anthropic client 线程安全,但 messages 列表要保持顺序(append assistant → tool_result)。

5. 错误处理细节

SERP API 调用可能失败:

  • 网络超时 → 返 "SERP API timeout,query: X"
  • HTTP 4xx → 返 status code
  • HTTP 5xx → 返 status code
  • 解析失败 → 返 "SERP API failed: ..."
  • QPS 超限 → 返 status code

模型收到这些错误后,会:

  • 换 query 重试(对 5xx)
  • 跳过这个问题(对 4xx)
  • 给出降级答案(对所有失败)

不要让工具失败 throw 异常传到 model,那会中断整个 agent 循环。

6. 上下文压缩

agent 跑多轮后 messages 会很长(每次 tool 调 + result 都进 messages)。Sonnet 200K context 不怕长,但成本和延迟会上去。

def compress_history(messages: list, keep_recent: int = 5) -> list:
    """保留最近 N 条,把早期 tool 结果压缩"""
    if len(messages) <= keep_recent:
        return messages

    # 保留 system(如有)+ 前 1 条 user + 最近 N 条
    # 把中间所有 tool_result 替换成 "[previous tool results omitted]"
    compressed = messages[:2]  # system + first user

    for msg in messages[2:-keep_recent]:
        if msg["role"] == "user" and isinstance(msg["content"], list):
            for block in msg["content"]:
                if block.get("type") == "tool_result":
                    block["content"] = "[previous results omitted for brevity]"
        compressed.append(msg)

    compressed.extend(messages[-keep_recent:])
    return compressed

跑长 agent 时,在每 10 轮调一次 compress_history,省 token 30-50%。

7. 一个完整 demo

result = run_agent("2026 年 SERP API 的价格区间")
# 1. Claude 决定调 search_google("SERP API pricing 2026")
# 2. 拿到 SERP 结果
# 3. 整理答案,带 [1] [2] [3] 引用
# 4. 输出给用户
print(result)

8. 监控指标

  • tool_call_rate:每次 session 调 SERP 几次(1-3 正常,5+ 可能死循环)
  • latency_p50 / p99:tool call 延迟(SERP API 1-1.5s + 网络 + Claude 处理)
  • failure_rate:tool 失败比例(超 5% 查 vendor 状态)
  • cache_hit_rate:相同 query 5 分钟内复用次数

小结

SERP API + Claude function calling 的关键不是调 API,而是:

  1. Tool schema 写得具体,模型才知道何时调
  2. 失败返文本不抛异常,模型可以降级
  3. 上下文压缩,长 agent 不爆 token
  4. max_iterations 防死循环

SerpBase 的 auto-refund 帮了大忙:失败不扣 credit,放心调。

实际项目里,我用这套模式搭了 3 个 agent:

  • SEO 监控 agent(查 SERP 排名)
  • 竞品分析 agent(查 SERP + 提取数据)
  • 内容生成 agent(SERP PAA 当提示)

代码不到 200 行,跑了3个月没出问题。