实时需求场景

用户对 AI Agent 的期望正在从"能用"变成"好用"。好用的核心指标之一是响应速度——用户发送消息后多久能看到第一个字。

延迟感知分级

场景 可接受首字延迟 可接受完整响应时间 技术要求
闲聊对话 <500ms <3s 流式输出 + 缓存
知识问答 <1s <5s RAG + 流式推理
代码助手 <1s <10s 流式 + 增量渲染
语音助手 <200ms <2s 流式 ASR/TTS + 边缘部署
实时翻译 <300ms <1s 超低延迟管道
金融监控告警 <100ms <500ms 事件触发 + 预计算

传统请求-响应模式下,用户需要等待 Agent 完成全部推理后才能看到结果。一个需要 5 秒推理的回复,用户体验就是"等 5 秒然后突然出一大段文字"。流式输出改变了这个体验——首字延迟降到几百毫秒,后续内容逐步呈现。

WebSocket/SSE 流式输出

SSE vs WebSocket 选型

特性 SSE (Server-Sent Events) WebSocket
通信方向 服务器 → 客户端(单向) 双向
协议 HTTP 独立协议
自动重连 浏览器原生支持 需手动实现
代理兼容性 好(基于 HTTP) 部分代理不支持
适用场景 流式文本输出 需要客户端实时输入(语音)

对于大多数 Agent 场景,SSE 足够且更简单。语音助手等双向实时场景用 WebSocket。

SSE 流式输出实现

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

app = FastAPI()

class ChatRequest(BaseModel):
    session_id: str
    message: str

async def stream_agent_response(session_id: str, message: str):
    """流式生成 Agent 响应"""
    # 1. 发送会话元信息
    yield f"data: {json.dumps({'type': 'meta', 'session_id': session_id})}\n\n"
    
    # 2. 发送思考状态
    yield f"data: {json.dumps({'type': 'status', 'status': 'thinking'})}\n\n"
    await asyncio.sleep(0.1)
    
    # 3. 检索知识(如果有 RAG)
    yield f"data: {json.dumps({'type': 'status', 'status': 'retrieving'})}\n\n"
    context = await retrieve_knowledge(message)
    await asyncio.sleep(0.05)
    
    # 4. 流式生成回复
    yield f"data: {json.dumps({'type': 'status', 'status': 'generating'})}\n\n"
    async for token in llm_stream_generate(message, context):
        yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"
    
    # 5. 发送完成信号
    yield f"data: {json.dumps({'type': 'done'})}\n\n"


async def llm_stream_generate(prompt: str, context: str = ""):
    """调用 LLM 流式 API"""
    # 模拟流式 token 生成
    full_response = f"基于您的问题「{prompt}」,这是流式生成的回答。"
    for char in full_response:
        await asyncio.sleep(0.03)  # 模拟推理延迟
        yield char


async def retrieve_knowledge(query: str) -> str:
    """检索知识库"""
    await asyncio.sleep(0.1)
    return f"相关知识: {query}"


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

WebSocket 双向通信实现

from fastapi import WebSocket, WebSocketDisconnect
from typing import Dict
import asyncio

class ConnectionManager:
    """WebSocket 连接管理器"""
    
    def __init__(self):
        self.active: Dict[str, WebSocket] = {}
    
    async def connect(self, session_id: str, ws: WebSocket):
        await ws.accept()
        self.active[session_id] = ws
    
    def disconnect(self, session_id: str):
        self.active.pop(session_id, None)
    
    async def send_token(self, session_id: str, token: str):
        ws = self.active.get(session_id)
        if ws:
            await ws.send_json({"type": "token", "content": token})

manager = ConnectionManager()

@app.websocket("/ws/chat/{session_id}")
async def ws_chat(ws: WebSocket, session_id: str):
    await manager.connect(session_id, ws)
    try:
        while True:
            data = await ws.receive_json()
            
            if data.get("type") == "message":
                message = data["content"]
                
                # 流式输出
                async for token in llm_stream_generate(message):
                    await manager.send_token(session_id, token)
                
                await ws.send_json({"type": "done"})
            
            elif data.get("type") == "interrupt":
                # 用户中断生成
                await ws.send_json({"type": "interrupted"})
    
    except WebSocketDisconnect:
        manager.disconnect(session_id)

流式推理

流式推理的核心是让 LLM 边生成边输出,而不是等整个序列生成完毕。

import httpx
from typing import AsyncIterator

class StreamingLLMClient:
    """流式 LLM 推理客户端"""
    
    def __init__(self, api_base: str, api_key: str):
        self.api_base = api_base
        self.headers = {"Authorization": f"Bearer {api_key}"}
    
    async def stream_chat(
        self,
        messages: list[dict],
        model: str = "gpt-4",
        temperature: float = 0.7,
        max_tokens: int = 2048
    ) -> AsyncIterator[str]:
        """流式调用 LLM Chat API"""
        async with httpx.AsyncClient() as client:
            async with client.stream(
                "POST",
                f"{self.api_base}/v1/chat/completions",
                json={
                    "model": model,
                    "messages": messages,
                    "temperature": temperature,
                    "max_tokens": max_tokens,
                    "stream": True  # 关键:开启流式
                },
                headers=self.headers,
                timeout=60.0
            ) as response:
                async for line in response.aiter_lines():
                    if line.startswith("data: "):
                        data = line[6:]
                        if data == "[DONE]":
                            break
                        
                        chunk = json.loads(data)
                        delta = chunk["choices"][0].get("delta", {})
                        if "content" in delta:
                            yield delta["content"]


# 带首字延迟统计的包装器
class LatencyAwareStream:
    """追踪首字延迟和吞吐量的流式包装器"""
    
    def __init__(self, stream: AsyncIterator[str]):
        self.stream = stream
        self.first_token_time = None
        self.token_count = 0
        self.total_time = 0
        self._start = asyncio.get_event_loop().time()
    
    async def __aiter__(self):
        async for token in self.stream:
            if self.first_token_time is None:
                self.first_token_time = asyncio.get_event_loop().time() - self._start
            self.token_count += 1
            yield token
        self.total_time = asyncio.get_event_loop().time() - self._start
    
    def metrics(self) -> dict:
        return {
            "first_token_latency_ms": round(self.first_token_time * 1000, 1) if self.first_token_time else None,
            "total_time_ms": round(self.total_time * 1000, 1),
            "token_count": self.token_count,
            "tokens_per_second": round(self.token_count / self.total_time, 1) if self.total_time > 0 else 0,
        }

管道并行:重叠各阶段处理

Agent 典型处理流程:意图理解 → 知识检索 → 推理生成 → 后处理。串行执行时各阶段延迟叠加;管道并行可以重叠它们。

import asyncio
from typing import Any, AsyncIterator

class PipelineStage:
    """管道阶段"""
    
    def __init__(self, name: str, process_fn, min_delay: float = 0):
        self.name = name
        self.process_fn = process_fn
        self.min_delay = min_delay
    
    async def process(self, input_data: Any) -> Any:
        if self.min_delay:
            await asyncio.sleep(self.min_delay)
        return await self.process_fn(input_data)

class AgentPipeline:
    """Agent 处理管道,支持阶段并行重叠"""
    
    def __init__(self, stages: list[PipelineStage]):
        self.stages = stages
    
    async def execute(self, query: str) -> AsyncIterator[dict]:
        """管道执行,每个阶段完成后立即传递给下一阶段"""
        
        # 阶段1: 意图理解(快速完成)
        intent = await self.stages[0].process(query)
        yield {"stage": "intent", "result": intent}
        
        # 阶段2&3 并行: 知识检索 + 开始推理(不等检索完成就开始推理)
        retrieval_task = asyncio.create_task(self.stages[1].process(query))
        
        # 先用无增强上下文开始流式推理
        async for token in self._stream_with_early_start(query, retrieval_task):
            yield {"stage": "token", "result": token}
        
        # 如果检索结果在推理后完成,可作为"补充信息"
        retrieval_result = await retrieval_task
        if retrieval_result:
            yield {"stage": "retrieval", "result": retrieval_result}
        
        yield {"stage": "done"}
    
    async def _stream_with_early_start(self, query: str, retrieval_task):
        """在检索进行的同时开始推理"""
        # 策略:等待检索一小段时间,超时则直接开始推理
        try:
            context = await asyncio.wait_for(
                asyncio.shield(retrieval_task), 
                timeout=0.3  # 最多等 300ms
            )
        except asyncio.TimeoutError:
            context = None  # 检索未完成,先开始推理
        
        # 流式生成
        messages = [{"role": "user", "content": query}]
        if context:
            messages.insert(0, {"role": "system", "content": f"参考信息: {context}"})
        
        async for token in mock_stream_generate(query):
            yield token


async def mock_stream_generate(query: str):
    """模拟流式生成"""
    response = f"针对「{query}」的分析回答。"
    for char in response:
        await asyncio.sleep(0.02)
        yield char


# 构建管道
pipeline = AgentPipeline([
    PipelineStage("intent", lambda q: asyncio.sleep(0.05, result={"intent": "question"})),
    PipelineStage("retrieval", lambda q: asyncio.sleep(0.2, result="relevant docs")),
    PipelineStage("generation", lambda q: asyncio.sleep(0.3, result="answer")),
])

管道并行效果对比

模式 首字延迟 总延迟 说明
串行执行 550ms (50+200+300) 550ms 意图→检索→推理 顺序执行
检索+推理并行 300ms 350ms 检索和推理同时开始
推理优先+检索兜底 50ms 350ms 先开始推理,检索结果作为补充

延迟优化技术

1. 推测解码(Speculative Decoding)

用小模型快速生成草稿,大模型验证。小模型生成速度快 5-10 倍,大模型批量验证通过率高时整体加速明显。

class SpeculativeDecoder:
    """推测解码:小模型草稿 + 大模型验证"""
    
    def __init__(self, draft_model, target_model):
        self.draft = draft_model  # 小模型(快)
        self.target = target_model  # 大模型(准)
    
    async def generate(self, prompt: str, max_tokens: int = 200) -> AsyncIterator[str]:
        tokens = []
        while len(tokens) < max_tokens:
            # 1. 小模型生成 K 个草稿 token
            draft_tokens = await self.draft.generate(prompt, n=4)
            
            # 2. 大模型批量验证
            verified = await self.target.verify(prompt, draft_tokens)
            
            # 3. 输出通过的 token
            for token in verified.accepted:
                yield token
                tokens.append(token)
            
            # 4. 如果全部通过,继续;否则从拒绝点重新开始
            if not verified.all_accepted:
                yield verified.corrected_token
                tokens.append(verified.corrected_token)

2. KV Cache 复用

class KVCacheManager:
    """KV Cache 管理:避免重复计算历史 token 的注意力"""
    
    def __init__(self, max_cache_entries: int = 100):
        self.cache: dict[str, dict] = {}
        self.max_entries = max_cache_entries
        self.lru: list[str] = []
    
    def get(self, session_id: str) -> dict | None:
        """获取会话的 KV Cache"""
        if session_id in self.cache:
            self.lru.remove(session_id)
            self.lru.insert(0, session_id)
            return self.cache[session_id]
        return None
    
    def set(self, session_id: str, kv_cache: dict):
        """保存 KV Cache"""
        if session_id in self.cache:
            self.lru.remove(session_id)
        
        self.cache[session_id] = kv_cache
        self.lru.insert(0, session_id)
        
        # LRU 淘汰
        while len(self.lru) > self.max_entries:
            evicted = self.lru.pop()
            del self.cache[evicted]
    
    def append_token(self, session_id: str, token_kv: dict):
        """增量更新 KV Cache(新 token)"""
        if session_id in self.cache:
            self.cache[session_id]["layers"].append(token_kv)

3. 响应缓存

对高频相同问题缓存响应,跳过推理直接返回。

import hashlib
from typing import Optional

class ResponseCache:
    """语义感知的响应缓存"""
    
    def __init__(self, ttl: int = 3600, max_entries: int = 10000):
        self.cache: dict[str, dict] = {}
        self.ttl = ttl
        self.max_entries = max_entries
    
    def _key(self, query: str, session_context: str = "") -> str:
        """生成缓存键"""
        normalized = query.strip().lower()
        return hashlib.sha256(f"{normalized}:{session_context}".encode()).hexdigest()
    
    def get(self, query: str, context: str = "") -> Optional[dict]:
        import time
        key = self._key(query, context)
        entry = self.cache.get(key)
        if entry and time.time() - entry["timestamp"] < self.ttl:
            return entry["response"]
        return None
    
    def set(self, query: str, response: dict, context: str = ""):
        import time
        key = self._key(query, context)
        self.cache[key] = {
            "response": response,
            "timestamp": time.time()
        }
        # 简单容量控制
        if len(self.cache) > self.max_entries:
            oldest = min(self.cache.items(), key=lambda x: x[1]["timestamp"])
            del self.cache[oldest[0]]

延迟优化效果汇总

优化技术 首字延迟降低 吞吐量提升 实现复杂度
流式输出 80%+ 0%
管道并行 40-60% 10-20%
推测解码 30-50% 2-3x
KV Cache 复用 60-80%(多轮对话) 30-50%
响应缓存 99%(命中时) 10-50%(命中率依赖场景)
模型量化 20-40% 1.5-2x

边缘部署:进一步降低延迟

将推理服务部署到离用户更近的边缘节点,减少网络延迟。

class EdgeRouter:
    """边缘路由器:选择最近的推理节点"""
    
    def __init__(self):
        self.regions = {
            "cn-east": {"endpoint": "https://shanghai.inference.ai", "latency_base": 20},
            "cn-south": {"endpoint": "https://guangzhou.inference.ai", "latency_base": 25},
            "cn-north": {"endpoint": "https://beijing.inference.ai", "latency_base": 22},
            "cn-west": {"endpoint": "https://chengdu.inference.ai", "latency_base": 30},
        }
    
    def select_region(self, client_ip: str = None, client_region: str = None) -> str:
        """选择最佳推理区域"""
        if client_region and client_region in self.regions:
            return client_region
        
        # 基于 IP 地理定位
        detected = self._geo_lookup(client_ip) if client_ip else "cn-east"
        return detected if detected in self.regions else "cn-east"
    
    def _geo_lookup(self, ip: str) -> str:
        """IP 地理定位(简化版)"""
        # 实际中使用 IP 地理位置数据库
        return "cn-east"
    
    def get_endpoint(self, region: str) -> str:
        return self.regions[region]["endpoint"]

边缘部署架构

用户 (北京) → CDN 边缘节点 (北京) → 推理服务 (北京) → 中心知识库 (上海)
     ↑                                                          |
     └────────── 流式响应回流 ←──────────────────────────────────┘
部署模式 网络延迟 推理延迟 适用场景
中心化部署 50-200ms 500-2000ms 低频高复杂度
区域部署 10-30ms 500-2000ms 区域用户集中
边缘部署 5-15ms 500-2000ms 超低延迟需求
混合部署 5-200ms 动态 复杂业务场景

实战建议

  1. 流式输出是性价比最高的优化。实现成本低,用户体验提升立竿见影。从第一天就上流式。

  2. 设首字延迟 SLA。团队需要对齐目标:P95 首字延迟 < 500ms。达不到就查瓶颈。

  3. 不要过度优化。如果当前首字延迟已经 <300ms,优化精力应该放到准确率上而非延迟上。

  4. 监控长尾。平均延迟好看但 P99 糟糕意味着部分用户体验差。重点关注 P99。

  5. 降级方案必备。LLM 服务可能超时或限流。准备规则引擎兜底:

    async def safe_respond(query: str) -> AsyncIterator[str]:
        try:
            async for token in llm_stream(query):
                yield token
        except Exception:
            yield "服务暂时繁忙,以下是快速回复:"
            yield await rule_based_fallback(query)
    
  6. 预热模型。服务启动时发送一个 dummy 请求预热模型权重到 GPU,避免第一个真实请求的冷启动延迟。


加入讨论

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

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