#31 Agent 的实时性挑战:流式输出、低延迟推理与异步架构

从一次线上事故说起

凌晨两点,告警电话把我从床上拽起来。用户反馈我们的Agent在对话中“卡住了”——输入问题后,等了整整8秒才看到第一个token输出。更诡异的是,有些请求直接超时,日志里全是asyncio.TimeoutError

我盯着监控面板,CPU使用率只有40%,内存也没爆。问题出在哪?翻看调用链,发现Agent内部在等一个RAG检索结果,而检索服务因为并发请求堆积,平均响应时间从50ms飙升到了2.3秒。更致命的是,Agent的推理流程是串行阻塞的——检索没完成,LLM推理就干等着,用户界面一片空白。

这不是个例。几乎所有生产环境下的Agent系统,都会在某个时刻撞上“实时性”这堵墙。今天这篇笔记,就聊聊我踩过的坑和填坑的姿势。

流式输出:别让用户盯着转圈圈

第一个坑:全量输出才返回

早期版本,我天真地让Agent等LLM生成完整回复再一次性返回。用户输入“帮我写一篇5000字的技术方案”,然后看着转圈圈转了40秒。这种体验,用户不骂娘才怪。

正确的姿势:用SSE(Server-Sent Events)或者WebSocket做流式传输。LLM每生成一个token,立刻推送到前端。

# 别这样写:等全部生成完再返回
async def bad_agent(query):
    full_response = await llm.generate(query)  # 阻塞等待全部token
    return full_response  # 用户要等几十秒

# 这样写:流式推送
async def stream_agent(query, websocket):
    async for token in llm.stream_generate(query):  # 逐token流式
        await websocket.send_text(token)  # 实时推送
        # 这里踩过坑:记得加await,否则协程不执行

但流式输出不是简单地把LLM的stream接口透传就完事了。Agent内部可能有多个步骤:先检索、再推理、再调用工具。每个步骤的中间结果,都应该以流的形式推送给前端。

我的做法:定义一套流式事件协议。比如{"type": "thinking", "content": "正在检索知识库..."}{"type": "tool_call", "name": "search_web", "args": {...}}{"type": "token", "content": "根据"}。前端根据事件类型渲染不同的UI组件。

第二个坑:流式中断与重连

网络抖动是常态。用户在地铁上,信号时断时续,流式连接断了怎么办?

方案:前端实现断线重连,后端支持断点续传。LLM生成过程中,后端把已生成的token缓存起来(比如用Redis的List结构),客户端重连时带上最后一个token的序号,后端从断点处继续推送。

# 断点续传的伪代码
async def stream_with_resume(query, client_id, last_token_id):
    cache_key = f"stream_cache:{client_id}"
    cached_tokens = await redis.lrange(cache_key, 0, -1)
    # 跳过已发送的token
    start_idx = len(cached_tokens) if last_token_id is None else last_token_id + 1
    
    async for idx, token in enumerate(llm.stream_generate(query)):
        if idx < start_idx:
            continue
        await redis.rpush(cache_key, token)  # 缓存新token
        yield token

这个方案有个代价:缓存会占用内存。我通常设置TTL为5分钟,超时自动清理。

低延迟推理:从模型到硬件的全链路优化

模型层面的取舍

别迷信“大模型就是好”。在Agent场景下,延迟和效果需要trade-off。

我的经验法则

  • 简单意图识别、实体抽取:用6B-7B的模型,量化到INT4,推理延迟控制在100ms以内
  • 复杂推理、代码生成:用13B-14B的模型,配合vLLM或TensorRT-LLM做推理加速
  • 只有核心决策环节才用70B+的大模型,而且要做好降级预案

量化踩坑记录:有一次我把一个13B模型用GPTQ量化到4bit,推理速度是快了,但输出质量明显下降——Agent开始频繁出现幻觉,把“张三”说成“李四”。后来换成AWQ量化,质量损失小很多。别为了速度牺牲太多精度,用户不是傻子

推理引擎的选择

vLLM是目前最成熟的方案,支持PagedAttention、连续批处理、前缀缓存。但有个坑:vLLM的异步接口在Python中需要小心使用。

# 别这样写:在异步循环中同步调用vLLM
async def bad_inference(prompts):
    for prompt in prompts:
        result = vllm.generate(prompt)  # 阻塞!会卡住事件循环
        process(result)

# 这样写:使用vLLM的异步API
from vllm import AsyncLLMEngine, SamplingParams

engine = AsyncLLMEngine.from_engine_args(engine_args)

async def good_inference(prompts):
    tasks = []
    for prompt in prompts:
        sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
        task = engine.add_request(prompt, sampling_params)
        tasks.append(task)
    
    results = await asyncio.gather(*tasks)  # 并发执行
    return results

硬件层面的骚操作

如果你用的是NVIDIA GPU,有几个参数值得调:

  1. CUDA graphs:减少kernel launch开销。vLLM默认开启,但如果你自己写推理代码,记得手动启用。
  2. MPS(Multi-Process Service):多进程共享GPU上下文,减少显存占用。适合同时部署多个小模型。
  3. GPU Direct RDMA:如果Agent需要频繁从向量数据库读取数据,用RDMA绕过CPU直接访问GPU显存,延迟能降30%以上。

一个真实案例:我们把Agent的embedding模型从CPU迁移到GPU上,用TensorRT优化后,向量化延迟从15ms降到了0.8ms。代价是显存多了2GB,但值得。

异步架构:别让一个慢操作拖垮整个系统

事件循环的陷阱

Python的asyncio是协作式多任务,一个协程如果阻塞了,整个事件循环都会卡住。

常见阻塞操作

  • 同步的HTTP请求(requests.get
  • 同步的文件读写
  • CPU密集型的计算(比如正则匹配大量文本)
# 别这样写:同步请求阻塞事件循环
async def agent_workflow(query):
    # 这里踩过坑:requests.get是同步的,会阻塞事件循环
    docs = requests.get("http://rag-service/search", params={"q": query})
    result = await llm.generate(docs.text)
    return result

# 这样写:使用异步HTTP客户端
import aiohttp

async def agent_workflow(query):
    async with aiohttp.ClientSession() as session:
        async with session.get("http://rag-service/search", params={"q": query}) as resp:
            docs = await resp.text()
    result = await llm.generate(docs)
    return result

异步工作流引擎

Agent的流程往往包含多个步骤,而且步骤之间有依赖关系。用简单的await串行执行,效率太低。

我的方案:基于DAG(有向无环图)的异步工作流引擎。

class AsyncWorkflow:
    def __init__(self):
        self.graph = {}  # node_id -> (dependencies, coroutine)
        self.results = {}
    
    def add_node(self, node_id, deps, coro):
        self.graph[node_id] = (deps, coro)
    
    async def execute(self):
        # 拓扑排序,并行执行无依赖的节点
        ready = [n for n, (deps, _) in self.graph.items() if not deps]
        while ready:
            tasks = [self._run_node(n) for n in ready]
            await asyncio.gather(*tasks)
            # 更新ready队列
            ready = self._get_ready_nodes()
        return self.results
    
    async def _run_node(self, node_id):
        deps, coro = self.graph[node_id]
        # 收集依赖节点的结果
        dep_results = {d: self.results[d] for d in deps}
        self.results[node_id] = await coro(dep_results)

这个引擎的好处是:检索、工具调用、LLM推理可以并行执行。比如用户问“今天北京天气怎么样,顺便帮我查一下上海到北京的机票”,检索天气和检索机票可以同时进行,互不阻塞。

超时与降级

异步架构最怕“死等”。一个外部服务挂了,整个Agent卡住。

我的三板斧

  1. 每个异步操作都设置超时asyncio.wait_for(coro, timeout=5.0)
  2. 熔断机制:连续失败N次后,直接返回缓存结果或降级回复
  3. 优雅降级:如果RAG检索超时,直接让LLM基于自身知识回答(虽然可能不准确,但比卡死强)
async def safe_retrieve(query):
    try:
        result = await asyncio.wait_for(
            rag_service.search(query), 
            timeout=3.0
        )
        return result
    except asyncio.TimeoutError:
        # 降级:返回空结果,让LLM自己发挥
        logger.warning(f"RAG检索超时,query={query}")
        return []

实战:一个实时Agent的架构设计

最后分享一个我在生产环境中验证过的架构。

用户请求
    ↓
API Gateway (Nginx + 限流)
    ↓
WebSocket Manager (维护长连接)
    ↓
异步工作流引擎 (DAG调度)
    ├── 意图识别 (小模型,INT4量化,<100ms)
    ├── 上下文管理 (Redis缓存,异步读写)
    ├── 工具调用 (并行执行,每个工具独立超时)
    │   ├── RAG检索 (异步HTTP,超时3s)
    │   ├── 数据库查询 (异步DB驱动,超时2s)
    │   └── 外部API (异步HTTP,超时5s)
    └── LLM推理 (vLLM异步API,流式输出)
        ↓
流式响应 (SSE/WebSocket)
    ↓
前端渲染 (逐token展示,支持中断)

关键指标

  • P50延迟:从用户输入到第一个token输出,控制在500ms以内
  • P99延迟:不超过3s
  • 流式输出速率:每秒20-30个token(对于7B模型,INT4量化,单卡A10)
  • 系统吞吐:单节点支持50并发连接,CPU和GPU利用率均衡

个人经验性建议

  1. 先做可观测性,再做优化。没有全链路追踪,你根本不知道瓶颈在哪。我推荐OpenTelemetry + Jaeger,每个异步操作都打上span。

  2. 别迷信“全异步”。Python的asyncio在CPU密集型任务上表现很差。如果Agent需要做大量文本处理(比如正则匹配、JSON解析),考虑用concurrent.futures.ProcessPoolExecutor丢到子进程执行。

  3. 流式输出不是银弹。如果Agent的推理步骤很短(比如简单的意图识别),全量返回反而更简单。流式输出的价值在于长文本生成和中间状态展示。

  4. 硬件预算要留余量。别把GPU显存用满,留20%给峰值流量。我见过太多因为显存OOM导致推理服务崩溃的案例。

  5. 最后一条,也是最重要的:实时性不是技术问题,而是产品问题。和产品经理对齐预期:哪些场景可以接受延迟,哪些场景必须实时。别为了追求极致的延迟,把系统搞得太复杂,最后维护成本爆炸。


以上是我在Agent实时性优化上的一些实战经验。代码片段都经过生产环境验证,但具体参数需要根据你的业务场景调整。如果你有更好的方案,欢迎在评论区交流——毕竟,这行没有银弹,只有不断踩坑和填坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的陶老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值