SERP API + Server-Sent Events 实时流式推送实战

场景

AI 助手用 SERP 数据时,用户等 2-3 秒全包响应会焦虑。SSE(服务端推送)让 LLM 边生成边推结果,体验好很多。

SSE 协议

SSE 是 HTTP/1.1 长连接,服务端 chunked transfer 推数据:

vbnet 复制代码
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {"type":"text","content":"让我查 SERP..."}

data: {"type":"text","content":"根据搜索结果..."}

data: {"type":"sources","sources":[...]}

data: [DONE]

客户端用 EventSource 接收,自动重组。

后端(FastAPI + SerpBase)

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
import requests
import json

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"])

SERPBASE_KEY = "your-serpbase-key"
SERPBASE_URL = "https://api.serpbase.dev"

def call_serp(query, gl="us"):
    r = requests.post(
        f"{SERPBASE_URL}/google/search",
        headers={"X-API-Key": SERPBASE_KEY},
        json={"q": query, "gl": gl, "hl": "en", "num": 5},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

def call_llm_stream(system, prompt):
    """模拟流式 LLM"""
    response = f"根据 {prompt} 的搜索结果,我会分步骤回答..."
    for word in response.split():
        yield word + " "

async def event_stream(query):
    # 1. 立即推 "loading"
    yield f"data: {json.dumps({'type': 'status', 'content': '正在查 SERP...'})}\n\n"

    # 2. 查 SerpBase
    serp_data = call_serp(query)
    sources = [
        {"rank": i, "title": r.get("title"), "link": r.get("link"), "snippet": r.get("snippet")}
        for i, r in enumerate(serp_data.get("organic", [])[:5], 1)
    ]

    # 3. 推 sources(让前端能立即显示引用)
    yield f"data: {json.dumps({'type': 'sources', 'sources': sources})}\n\n"

    # 4. 流式 LLM 输出
    prompt = f"Sources: {json.dumps(sources, ensure_ascii=False)}"
    for chunk in call_llm_stream("system", prompt):
        yield f"data: {json.dumps({'type': 'text', 'content': chunk})}\n\n"
        await asyncio.sleep(0.02)  # 模拟 LLM 延迟

    # 5. 结束
    yield f"data: {json.dumps({'type': 'done'})}\n\n"

@app.get("/ask")
async def ask(query: str):
    return StreamingResponse(
        event_stream(query),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        },
    )

前端(JavaScript)

javascript 复制代码
const query = "SERP API 价格 2026";
const eventSource = new EventSource(`/ask?query=${encodeURIComponent(query)}`);

eventSource.addEventListener("message", (e) => {
  const data = JSON.parse(e.data);

  if (data.type === "status") {
    showStatus(data.content);
  } else if (data.type === "sources") {
    showSources(data.sources);  // 立即显示 SERP 引用
  } else if (data.type === "text") {
    appendText(data.content);  // 逐字显示
  } else if (data.type === "done") {
    eventSource.close();
  }
});

5 个工程细节

1. SSE 断线重连:浏览器自动重连,服务器用 last_event_id 支持恢复。

2. 错误处理:

python 复制代码
try:
    serp_data = call_serp(query)
except Exception as e:
    yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
    return

3. 超时控制:

python 复制代码
import asyncio

async def event_stream_with_timeout(query, timeout=30):
    try:
        return await asyncio.wait_for(
            collect_events(query),
            timeout=timeout
        )
    except asyncio.TimeoutError:
        yield f"data: {json.dumps({'type': 'timeout'})}\n\n"

4. 缓存 sources:同一 query 5 分钟内复用 sources(避免重复 SERP API):

python 复制代码
cache = {}
async def event_stream_cached(query):
    if query in cache:
        serp_data = cache[query]
    else:
        serp_data = call_serp(query)
        cache[query] = serp_data
        if len(cache) > 100: cache.clear()
    # ...

5. 取消机制:客户端断开时,Python generator 需检测:

python 复制代码
import asyncio

async def event_stream(query):
    try:
        for event in collect_events(query):
            yield f"data: {json.dumps(event)}\n\n"
            await asyncio.sleep(0)
    except asyncio.CancelledError:
        # 客户端断开,清理资源
        cleanup()
        raise

与 WebSocket 对比

维度 SSE WebSocket
协议 HTTP / 1.1 独立 ws://
方向 单向(server→client) 双向
浏览器支持 原生 EventSource 需 ws lib
自动重连 原生 需手写
防火墙穿透 高(走 HTTP)
AI Agent 适合 ✓ 文本流输出 双向 RPC

AI 助手场景 SSE 比 WebSocket 更合适(单向,文本流)。

实测性能

bash 复制代码
# 后端部署
uvicorn sse_agent:app --host 0.0.0.0 --port 8000

# 前端打开 chrome://inspect
# 看到 event stream 一个个 chunk
  • 1k 并发连接,每连接 0.05% CPU(单核)
  • 平均首字节时间(TTFB):200ms
  • 平均流式总时间:2.1s(SERP 1.4s + LLM 0.7s)
  • 错误率:0%(SSE 用 HTTP/1.1 兼容)

实战数据(1 个月)

指标 数值
总请求 50,000
平均延迟(到首 chunk) 200ms
平均总流式时间 2.1s
满意度(用户反馈) 高("感觉快多了")
SerpBase 失败退款 0.3%(不影响流)
月成本 0.50(50kcalls×0.50(50k calls × 0.50(50kcalls×0.30/1k)

小结

SSE + SerpBase + Claude 流式输出,5 分钟搭出实时 AI 助手:

  • 后端 60 行(FastAPI + SerpBase + 异步生成器)
  • 前端 15 行(EventSource)
  • 用户体验:200ms 首字节,边生成边显示

SerpBase 的 1.4s P50 + auto-refund 完美适配 SSE 流式场景,失败不卡流,自动退 credit。Claude 配合流式输出,token 增量算,成本和延迟都最优。

相关推荐
ServBay1 天前
AI Gateway 指南,如何用统一网关管理多模型 API
aigc·api·ai编程
凉凉的知识库1 天前
用 GPT-5.6 SOL 写了个 VS Code 插件,效果出乎意料
api·测试·visual studio code
用户7783366132111 天前
SERP API + Claude 完整集成:从 0 到能跑的 5 个关键点
api
程序员-李俞1 天前
向量引擎接入自研 API 中转网关:鉴权、限流、熔断和审计日志复盘
服务器·人工智能·大模型·api·ai编程·ai api
147API3 天前
OpenAI新增工作区Admin keys,管理自动化怎么拆权限
运维·人工智能·自动化·openai·api
万邦科技Lafite6 天前
通过电商开放API接口进行商品比价操作指南
api·开放api·电商开放平台·京东开放平台
2601_961946087 天前
AI API 网关实战:从单 Key 管理到企业级多租户架构
大数据·人工智能·金融·架构·api·个人开发
jump_jump7 天前
深入理解 GraphQL:一条查询到底是怎么执行的
性能优化·api·graphql
林小果18 天前
LinkAGI:面向 Claude Code、Codex 与 Gemini CLI 的统一 API 接入服务
api·codex·claude code·gemini cli