引言

Agent 的推理过程往往是漫长的等待——用户盯着加载动画,不知道 Agent 在做什么。流式响应把"等待结果"变成"实时观察思考",是 Agent 用户体验的关键升级。2026年,三种流式协议各有优劣,选型不当会导致体验降级或工程复杂度爆炸。

一、三种协议对比

核心特性矩阵

特性SSEWebSocketgRPC Stream
通信方向服务器→客户端(单向)双向双向
底层协议HTTP/1.1 或 HTTP/2HTTP 升级HTTP/2
数据格式文本(text/event-stream)文本/二进制Protobuf(二进制)
自动重连内置需手动实现需手动实现
浏览器支持原生 EventSource原生 WebSocket需 gRPC-Web
代理/CDN兼容优秀良好较差
连接数限制浏览器6个/域名无限制无限制
序列化效率低(文本)高(Protobuf)
移动端友好

Agent 场景适配分析

Agent 流式需求频谱:

单向输出流          双向交互流
(LLM→用户)        (用户↔Agent)
    │                   │
    │   ┌───────────┐   │
    │   │  SSE 最佳  │   │
    │   └───────────┘   │
    │                   │
    │   ┌───────────┐   │
    │   │ WebSocket │   │
    │   │   最佳    │   │
    │   └───────────┘   │
    │                   │
    │   ┌───────────┐   │
    │   │ gRPC 最佳 │   │
    │   └───────────┘   │
    
简单聊天 ←─────────────→ 复杂多Agent
低延迟    ←─────────────→ 高吞吐
Web前端   ←─────────────→ 微服务后端

二、SSE 实现方案

2.1 服务端

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

class AgentStreamEvent:
    """Agent 流式事件类型"""
    THINKING = "thinking"        # Agent 思考中
    TOOL_CALL = "tool_call"      # 工具调用
    TOOL_RESULT = "tool_result"  # 工具结果
    CONTENT = "content"          # 内容输出
    ERROR = "error"              # 错误
    DONE = "done"                # 完成

async def agent_stream_generator(
    query: str,
    session_id: str
):
    """Agent SSE 流式生成器"""
    
    try:
        # 1. 发送思考状态
        yield _format_sse(AgentStreamEvent.THINKING, {
            "message": "正在分析您的请求...",
            "session_id": session_id
        })
        
        # 2. Agent 推理(流式 LLM 输出)
        async for chunk in agent.think_stream(query):
            if chunk.type == "tool_call":
                yield _format_sse(AgentStreamEvent.TOOL_CALL, {
                    "tool": chunk.tool_name,
                    "args": chunk.tool_args,
                    "thinking": chunk.reasoning
                })
                
                # 3. 工具执行
                result = await agent.execute_tool(chunk.tool_call)
                yield _format_sse(AgentStreamEvent.TOOL_RESULT, {
                    "tool": chunk.tool_name,
                    "result": result.summary,
                    "duration_ms": result.duration_ms
                })
            
            elif chunk.type == "content":
                yield _format_sse(AgentStreamEvent.CONTENT, {
                    "text": chunk.text,
                    "tokens_so_far": chunk.token_count
                })
        
        # 4. 完成
        yield _format_sse(AgentStreamEvent.DONE, {
            "session_id": session_id,
            "total_tokens": agent.total_tokens,
            "duration_ms": agent.total_duration_ms
        })
        
    except Exception as e:
        yield _format_sse(AgentStreamEvent.ERROR, {
            "message": str(e),
            "session_id": session_id
        })

def _format_sse(event_type: str, data: dict) -> str:
    return f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"

@app.post("/api/agent/chat")
async def chat(request: ChatRequest):
    return StreamingResponse(
        agent_stream_generator(request.query, request.session_id),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",  # Nginx 禁用缓冲
        }
    )

2.2 客户端

class AgentSSEClient {
  private eventSource: EventSource | null = null;
  private reconnectAttempts = 0;
  private maxReconnects = 3;

  connect(query: string, sessionId: string) {
    // 使用 fetch POST + ReadableStream(EventSource 仅支持 GET)
    fetch('/api/agent/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, sessionId }),
    }).then(response => {
      const reader = response.body!.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      const read = () => {
        reader.read().then(({ done, value }) => {
          if (done) return;
          
          buffer += decoder.decode(value, { stream: true });
          const events = buffer.split('\n\n');
          buffer = events.pop() || '';
          
          events.forEach(raw => this.handleEvent(raw));
          read();
        });
      };
      read();
    });
  }

  private handleEvent(raw: string) {
    const lines = raw.split('\n');
    let event = 'message';
    let data = '';
    
    lines.forEach(line => {
      if (line.startsWith('event: ')) event = line.slice(7);
      if (line.startsWith('data: ')) data = line.slice(6);
    });

    const parsed = JSON.parse(data);
    
    switch (event) {
      case 'thinking':
        this.onThinking?.(parsed);
        break;
      case 'tool_call':
        this.onToolCall?.(parsed);
        break;
      case 'tool_result':
        this.onToolResult?.(parsed);
        break;
      case 'content':
        this.onContent?.(parsed.text);
        break;
      case 'error':
        this.onError?.(parsed);
        break;
      case 'done':
        this.onDone?.(parsed);
        break;
    }
  }
}

三、WebSocket 实现方案

3.1 服务端

from fastapi import WebSocket, WebSocketDisconnect

class AgentConnectionManager:
    """Agent WebSocket 连接管理"""
    
    def __init__(self):
        self.active: dict[str, WebSocket] = {}  # session_id → WebSocket
        self.agent_tasks: dict[str, asyncio.Task] = {}
    
    async def connect(self, ws: WebSocket, session_id: str):
        await ws.accept()
        self.active[session_id] = ws
        logger.info(f"WebSocket connected: {session_id}")
    
    async def disconnect(self, session_id: str):
        if session_id in self.active:
            del self.active[session_id]
        if session_id in self.agent_tasks:
            self.agent_tasks[session_id].cancel()
            del self.agent_tasks[session_id]
    
    async def handle_session(self, ws: WebSocket, session_id: str):
        """处理 WebSocket 会话"""
        await self.connect(ws, session_id)
        
        try:
            while True:
                # 接收客户端消息
                message = await ws.receive_json()
                
                if message["type"] == "chat":
                    # 启动 Agent 任务
                    task = asyncio.create_task(
                        self._run_agent(ws, session_id, message["content"])
                    )
                    self.agent_tasks[session_id] = task
                
                elif message["type"] == "interrupt":
                    # 用户中断当前 Agent 执行
                    if session_id in self.agent_tasks:
                        self.agent_tasks[session_id].cancel()
                        await ws.send_json({
                            "type": "interrupted",
                            "session_id": session_id
                        })
                
                elif message["type"] == "feedback":
                    # 用户实时反馈(Human-in-the-loop)
                    await self._handle_feedback(session_id, message)
                
        except WebSocketDisconnect:
            await self.disconnect(session_id)
    
    async def _run_agent(self, ws: WebSocket, session_id: str, query: str):
        """运行 Agent 并通过 WebSocket 推送更新"""
        try:
            async for event in agent.run_stream(query):
                await ws.send_json({
                    "type": event.type,
                    "data": event.data,
                    "timestamp": time.time()
                })
        except asyncio.CancelledError:
            logger.info(f"Agent task cancelled: {session_id}")
        except Exception as e:
            await ws.send_json({
                "type": "error",
                "data": {"message": str(e)}
            })

manager = AgentConnectionManager()

@app.websocket("/ws/agent/{session_id}")
async def websocket_endpoint(ws: WebSocket, session_id: str):
    await manager.handle_session(ws, session_id)

3.2 客户端

class AgentWSClient {
  private ws: WebSocket | null = null;
  private messageQueue: string[] = [];
  
  connect(sessionId: string) {
    this.ws = new WebSocket(`wss://api.example.com/ws/agent/${sessionId}`);
    
    this.ws.onopen = () => {
      // 发送排队消息
      this.messageQueue.forEach(msg => this.ws!.send(msg));
      this.messageQueue = [];
    };
    
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.handleMessage(data);
    };
    
    this.ws.onclose = () => {
      // 自动重连
      setTimeout(() => this.connect(sessionId), 3000);
    };
  }
  
  sendChat(content: string) {
    const msg = JSON.stringify({ type: 'chat', content });
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(msg);
    } else {
      this.messageQueue.push(msg);
    }
  }
  
  interrupt() {
    this.ws?.send(JSON.stringify({ type: 'interrupt' }));
  }
}

四、gRPC 流式方案

4.1 Proto 定义

// agent.proto
syntax = "proto3";

service AgentService {
  // 服务端流式:Agent → 客户端
  rpc ChatStream(ChatRequest) returns (stream ChatResponse);
  
  // 双向流式:支持实时交互
  rpc ChatBidirectional(stream ChatMessage) returns (stream ChatResponse);
}

message ChatRequest {
  string session_id = 1;
  string query = 2;
  map<string, string> metadata = 3;
}

message ChatMessage {
  string session_id = 1;
  MessageType type = 2;  // CHAT, INTERRUPT, FEEDBACK
  string content = 3;
}

message ChatResponse {
  ResponseType type = 1;  // THINKING, TOOL_CALL, CONTENT, DONE, ERROR
  string session_id = 2;
  bytes data = 3;          // JSON 编码的事件数据
  int64 timestamp = 4;
}

enum MessageType {
  CHAT = 0;
  INTERRUPT = 1;
  FEEDBACK = 2;
}

enum ResponseType {
  THINKING = 0;
  TOOL_CALL = 1;
  TOOL_RESULT = 2;
  CONTENT = 3;
  DONE = 4;
  ERROR = 5;
}

4.2 服务端实现

import grpc
from concurrent import futures

class AgentServicer(agent_pb2_grpc.AgentServiceServicer):
    
    def ChatStream(self, request, context):
        """服务端流式:逐条推送 Agent 事件"""
        try:
            for event in agent.run(request.query):
                response = agent_pb2.ChatResponse(
                    type=self._map_event_type(event.type),
                    session_id=request.session_id,
                    data=json.dumps(event.data).encode(),
                    timestamp=int(time.time())
                )
                yield response
        except Exception as e:
            yield agent_pb2.ChatResponse(
                type=agent_pb2.ERROR,
                data=json.dumps({"error": str(e)}).encode()
            )
    
    def ChatBidirectional(self, request_iterator, context):
        """双向流式:支持中断和实时反馈"""
        session = None
        
        for message in request_iterator:
            if message.type == agent_pb2.CHAT:
                # 启动 Agent 执行
                for event in agent.run(message.content):
                    if not context.is_active():
                        break
                    yield agent_pb2.ChatResponse(
                        type=self._map_event_type(event.type),
                        data=json.dumps(event.data).encode()
                    )
            
            elif message.type == agent_pb2.INTERRUPT:
                agent.interrupt()
                yield agent_pb2.ChatResponse(
                    type=agent_pb2.DONE,
                    data=json.dumps({"reason": "interrupted"}).encode()
                )

五、性能基准测试

测试环境

  • 服务端:4 vCPU / 16GB RAM / Python 3.12 / FastAPI
  • 客户端:1000 并发连接
  • 消息大小:平均 200 bytes / 消息
  • 持续时间:5 分钟

结果对比

指标SSEWebSocketgRPC Stream
最大并发连接10,00050,00030,000
消息延迟 P5012ms8ms5ms
消息延迟 P9545ms25ms12ms
消息延迟 P99120ms60ms30ms
吞吐量 (msg/s)50,000200,000150,000
内存/连接32KB48KB24KB
CPU 利用率 (1k连接)35%28%22%
带宽效率基准+15%+40%

六、选型决策树

                    ┌─────────────────┐
                    │ 是否需要双向通信?│
                    └────────┬────────┘
              ┌──────────────┴──────────────┐
              │ 否                          │ 是
              ▼                             ▼
     ┌────────────────┐          ┌──────────────────┐
     │ 是否是微服务    │          │ 是否是浏览器前端?│
     │ 内部通信?      │          └────────┬─────────┘
     └───────┬────────┘                   │
             │                    ┌───────┴───────┐
     ┌───────┴───────┐            │ 否            │ 是
     │ 是            │ 否         ▼               ▼
     ▼               ▼     ┌──────────┐  ┌────────────┐
  ┌──────┐     ┌────────┐  │  gRPC    │  │ WebSocket  │
  │gRPC  │     │  SSE   │  │  Stream  │  │            │
  └──────┘     └────────┘  └──────────┘  └────────────┘

场景推荐

场景推荐协议原因
Web 聊天界面SSE原生支持、简单、自动重连
实时协作编辑WebSocket双向低延迟
移动 AppSSE移动网络友好、自动重连
微服务间 Agent 通信gRPC高效序列化、强类型
多 Agent 系统gRPC流式 RPC 适配 Agent 通信
简单 LLM 问答SSE单向流足够、实现简单
Human-in-the-loopWebSocket需要双向交互
大规模推送SSECDN 兼容、连接效率高

七、生产环境关键配置

Nginx SSE 配置

location /api/agent/chat {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;           # 关键:禁用缓冲
    proxy_cache off;
    proxy_read_timeout 300s;       # 长连接超时
    chunked_transfer_encoding on;
}

WebSocket 配置

location /ws/agent/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400;      # 24小时
    proxy_send_timeout 86400;
}

八、流式架构 Checklist

□ 协议选型基于通信方向和客户端类型
□ SSE 禁用代理缓冲(proxy_buffering off)
□ WebSocket 实现心跳和重连机制
□ gRPC 配置 keepalive 和流控
□ 消息序列化使用高效格式(JSON/Protobuf)
□ 背压机制防止慢客户端拖垮服务端
□ 连接超时和最大连接数限制
□ 流式错误不中断连接,通过事件传递
□ 客户端实现优雅降级(流式不可用时回退轮询)
□ 监控流式连接的延迟和消息丢失率

结语

流式响应是 Agent 从"工具"到"伙伴"的关键体验升级。协议选型没有银弹:SSE 简单可靠适合 Web 场景,WebSocket 灵活双向适合交互场景,gRPC 高效强类型适合微服务场景。理解你的通信模式——是单向输出还是双向交互——然后选择最适合的工具。在 Agent 时代,好的流式架构让用户感觉 Agent 在"思考",而不是在"卡住"。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。