从一次线上事故说起
凌晨两点,告警电话把我从床上拽起来。用户反馈我们的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,有几个参数值得调:
- CUDA graphs:减少kernel launch开销。vLLM默认开启,但如果你自己写推理代码,记得手动启用。
- MPS(Multi-Process Service):多进程共享GPU上下文,减少显存占用。适合同时部署多个小模型。
- 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卡住。
我的三板斧:
- 每个异步操作都设置超时:
asyncio.wait_for(coro, timeout=5.0) - 熔断机制:连续失败N次后,直接返回缓存结果或降级回复
- 优雅降级:如果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利用率均衡
个人经验性建议
-
先做可观测性,再做优化。没有全链路追踪,你根本不知道瓶颈在哪。我推荐OpenTelemetry + Jaeger,每个异步操作都打上span。
-
别迷信“全异步”。Python的asyncio在CPU密集型任务上表现很差。如果Agent需要做大量文本处理(比如正则匹配、JSON解析),考虑用
concurrent.futures.ProcessPoolExecutor丢到子进程执行。 -
流式输出不是银弹。如果Agent的推理步骤很短(比如简单的意图识别),全量返回反而更简单。流式输出的价值在于长文本生成和中间状态展示。
-
硬件预算要留余量。别把GPU显存用满,留20%给峰值流量。我见过太多因为显存OOM导致推理服务崩溃的案例。
-
最后一条,也是最重要的:实时性不是技术问题,而是产品问题。和产品经理对齐预期:哪些场景可以接受延迟,哪些场景必须实时。别为了追求极致的延迟,把系统搞得太复杂,最后维护成本爆炸。
以上是我在Agent实时性优化上的一些实战经验。代码片段都经过生产环境验证,但具体参数需要根据你的业务场景调整。如果你有更好的方案,欢迎在评论区交流——毕竟,这行没有银弹,只有不断踩坑和填坑。


被折叠的 条评论
为什么被折叠?



