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 增量算,成本和延迟都最优。

相关推荐
星核0penstarry2 小时前
企业 API 向后兼容实战:版本控制、废弃字段通知与灰度发布策略
人工智能·后端·api·接口隔离原则·版本控制·架构设计·后端架构
treasure1406 小时前
Django复习笔记(2026年10月)
后端·python·django·api·web·drf
用户298698530141 天前
Python 将 Word 文档转换为 XML 的实践要点
后端·python·api
用户298698530142 天前
Python 实战:Markdown 转 PDF 的代码实现与页面设置
python·api·markdown
Tanshu_API君2 天前
白银期货行情数据接入:从上海期货交易所合约到实时报价
api·金属·白银·白银价格·白银历史价格
EnCi Zheng3 天前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
千木SilvaMux3 天前
10款国产模型写离职证明实测:为什么两极分化
llm·api
用户298698530144 天前
Python 实现 Markdown 与 Word 文档互转
python·api·markdown
むはちひつなゃ初见4 天前
LLM API 上线 FAQ 与排障清单:从 401/429 到 fallback 验收
llm·api·故障排查
一级新生6 天前
API 到底是什么?用点餐窗口讲明白
人工智能·大模型·api·编程入门