扩展性挑战:为什么单机 Agent 走不远
单个 Agent 进程能处理的并发请求受限于 CPU、内存和网络连接数。一个典型的 LLM Agent 单次推理消耗 2-8GB 显存,单卡 GPU 同时只能服务少数几个请求。当 QPS 从个位数涨到百级、千级时,单机方案必然崩溃。
扩展 Agent 系统的核心挑战不是简单的"加机器",而是:
| 挑战 | 描述 | 影响 |
|---|---|---|
| 状态管理 | Agent 会话上下文在哪台机器? | 直接影响会话连续性 |
| GPU 资源 | LLM 推理是计算密集型 | 扩展瓶颈在 GPU 而非 CPU |
| 长连接 | 流式输出需要持久连接 | 连接亲和性限制负载均衡 |
| 一致性 | 多 Agent 实例的内存状态同步 | 分布式共识开销 |
| 尾延迟 | 少数慢请求拖垮整体 P99 | 需要超时和降级机制 |
水平扩展策略
1. 无状态 Agent:将状态外部化
扩展的第一原则:Agent 进程本身不持有任何会话状态。所有状态存储在外部系统。
from dataclasses import dataclass, field
from typing import Optional, Any
import json
# ❌ 错误做法:状态在内存中
class StatefulAgent:
def __init__(self):
self.conversations: dict[str, list] = {} # 内存中的会话状态
def chat(self, session_id: str, message: str) -> str:
history = self.conversations[session_id] # 只在本机有效
history.append({"role": "user", "content": message})
# ...处理...
return "response"
# ✅ 正确做法:状态外部化
class StatelessAgent:
"""无状态 Agent,所有会话状态存入 Redis"""
def __init__(self, state_store: 'SessionStore'):
self.store = state_store
def chat(self, session_id: str, message: str) -> str:
# 每次从外部存储加载会话状态
history = self.store.get_history(session_id)
history.append({"role": "user", "content": message})
# 处理逻辑
response = self._generate(history)
history.append({"role": "assistant", "content": response})
# 持久化回外部存储
self.store.save_history(session_id, history)
return response
class SessionStore:
"""基于 Redis 的会话存储"""
def __init__(self, redis_client):
self.redis = redis_client
self.ttl = 3600 # 会话过期时间 1 小时
def get_history(self, session_id: str) -> list[dict]:
data = self.redis.get(f"session:{session_id}")
return json.loads(data) if data else []
def save_history(self, session_id: str, history: list[dict]):
self.redis.setex(
f"session:{session_id}",
self.ttl,
json.dumps(history, ensure_ascii=False)
)
def extend_ttl(self, session_id: str):
self.redis.expire(f"session:{session_id}", self.ttl)
2. 分层架构:接入层 + 推理层 + 知识层
"""
三层架构设计:
- 接入层:处理 HTTP/WebSocket 连接,会话路由
- 推理层:LLM 推理,可水平扩展
- 知识层:向量数据库、知识图谱
"""
from abc import ABC, abstractmethod
class GatewayLayer:
"""接入层:路由请求到推理节点"""
def __init__(self, inference_pool: 'InferencePool'):
self.pool = inference_pool
async def handle_request(self, session_id: str, message: str):
# 根据会话亲和性选择节点
node = self.pool.select_node(session_id)
return await node.process(session_id, message)
class InferenceNode:
"""推理层:单个推理节点"""
def __init__(self, node_id: str, model_path: str):
self.node_id = node_id
self.model = self._load_model(model_path)
self.active_sessions = 0
self.max_sessions = 10
async def process(self, session_id: str, message: str) -> str:
self.active_sessions += 1
try:
# 推理逻辑
result = await self._infer(message)
return result
finally:
self.active_sessions -= 1
@property
def available(self) -> bool:
return self.active_sessions < self.max_sessions
class InferencePool:
"""推理节点池"""
def __init__(self):
self.nodes: list[InferenceNode] = []
def add_node(self, node: InferenceNode):
self.nodes.append(node)
def select_node(self, session_id: str = None) -> InferenceNode:
# 优先选择可用且负载最低的节点
available = [n for n in self.nodes if n.available]
if not available:
raise RuntimeError("No available inference nodes")
return min(available, key=lambda n: n.active_sessions)
3. 推理与工具调用分离
LLM 推理和工具执行有不同的资源特征:推理需要 GPU,工具调用主要消耗 CPU 和网络 I/O。将它们分离到不同服务。
import asyncio
from typing import Any
class AgentOrchestrator:
"""编排器:协调推理服务和工具服务"""
def __init__(self, llm_client: 'LLMClient', tool_service: 'ToolService'):
self.llm = llm_client
self.tools = tool_service
async def run(self, query: str, session_id: str) -> str:
messages = [{"role": "user", "content": query}]
for _ in range(10): # 最多 10 轮
# 1. 调用 LLM(GPU 节点)
response = await self.llm.chat(messages)
if not response.tool_calls:
return response.content
# 2. 执行工具调用(CPU 节点,可并行)
tool_results = await asyncio.gather(*[
self.tools.execute(tc) for tc in response.tool_calls
])
# 3. 合并结果,继续推理
messages.append({"role": "assistant", "content": response.content})
for tr in tool_results:
messages.append({"role": "tool", "content": tr})
return "达到最大轮次限制"
class LLMClient:
"""LLM 推理客户端"""
def __init__(self, endpoint: str):
self.endpoint = endpoint
async def chat(self, messages: list[dict]) -> 'LLMResponse':
# 调用推理服务
pass
class ToolService:
"""工具执行服务"""
def __init__(self, workers: int = 20):
self.semaphore = asyncio.Semaphore(workers)
async def execute(self, tool_call: dict) -> str:
async with self.semaphore:
# 执行工具调用
pass
负载均衡策略
轮询 vs 最少连接 vs 会话亲和
import hashlib
from collections import defaultdict
class LoadBalancer:
"""多种负载均衡策略"""
def __init__(self, nodes: list[str]):
self.nodes = nodes
self._index = 0
self._connections: dict[str, int] = defaultdict(int)
self._session_map: dict[str, str] = {} # session_id -> node
def round_robin(self) -> str:
"""轮询:简单但不考虑负载"""
node = self.nodes[self._index % len(self.nodes)]
self._index += 1
return node
def least_connections(self) -> str:
"""最少连接:选择当前连接数最少的节点"""
return min(self.nodes, key=lambda n: self._connections[n])
def session_affinity(self, session_id: str) -> str:
"""会话亲和:同一会话路由到同一节点
利用一致性哈希,节点增减时最小化会话迁移
"""
if session_id in self._session_map:
return self._session_map[session_id]
# 一致性哈希
ring = {}
for node in self.nodes:
for i in range(150): # 虚拟节点
key = f"{node}:{i}"
ring[self._hash(key)] = node
hash_val = self._hash(session_id)
sorted_keys = sorted(ring.keys())
# 找到第一个 >= hash_val 的节点
for k in sorted_keys:
if k >= hash_val:
node = ring[k]
self._session_map[session_id] = node
return node
node = ring[sorted_keys[0]]
self._session_map[session_id] = node
return node
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def on_connection_start(self, node: str):
self._connections[node] += 1
def on_connection_end(self, node: str):
self._connections[node] -= 1
负载均衡策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 轮询 | 实现简单 | 不考虑节点差异 | 节点配置相同 |
| 最少连接 | 自适应负载 | 需要维护连接计数 | 请求耗时差异大 |
| 会话亲和 | 会话连续性好 | 节点故障影响大 | 有状态会话 |
| 一致性哈希 | 节点增减影响小 | 需要虚拟节点 | 大规模集群 |
| 加权轮询 | 考虑节点性能差异 | 权重需手动维护 | 异构集群 |
会话亲和性:有状态请求的处理
Agent 对话有上下文连续性——同一会话的请求最好路由到同一节点,避免上下文反复传输。
class StickySessionManager:
"""粘性会话管理器"""
def __init__(self, ttl: int = 1800):
self.ttl = ttl
self._bindings: dict[str, tuple[str, float]] = {} # session -> (node, timestamp)
def get_node(self, session_id: str, available_nodes: list[str]) -> str:
"""获取会话绑定的节点,如过期或不可用则重新分配"""
import time
if session_id in self._bindings:
node, bound_time = self._bindings[session_id]
if time.time() - bound_time < self.ttl and node in available_nodes:
return node
# 重新分配
node = min(available_nodes, key=lambda n: sum(
1 for _, (nd, _) in self._bindings.values() if nd == n
))
self._bindings[session_id] = (node, time.time())
return node
def remove_binding(self, session_id: str):
self._bindings.pop(session_id, None)
def cleanup_expired(self):
import time
now = time.time()
expired = [sid for sid, (_, t) in self._bindings.items() if now - t > self.ttl]
for sid in expired:
del self._bindings[sid]
分布式追踪
多服务环境下,一次用户请求可能经过网关、推理服务、工具服务、知识库等多个节点。没有追踪,排障就是猜谜。
import uuid
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Optional
# 上下文变量传播 trace
trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
span_id_var: ContextVar[str] = ContextVar("span_id", default="")
@dataclass
class Span:
"""追踪片段"""
trace_id: str
span_id: str
parent_span_id: Optional[str]
operation: str
start_time: float
end_time: Optional[float] = None
tags: dict = field(default_factory=dict)
status: str = "ok"
class Tracer:
"""分布式追踪器"""
def __init__(self):
self.spans: list[Span] = []
def start_span(self, operation: str, **tags) -> str:
import time
trace_id = trace_id_var.get() or str(uuid.uuid4())
parent_id = span_id_var.get() or None
span_id = str(uuid.uuid4())
span = Span(
trace_id=trace_id,
span_id=span_id,
parent_span_id=parent_id,
operation=operation,
start_time=time.time(),
tags=tags
)
self.spans.append(span)
# 传播上下文
trace_id_var.set(trace_id)
span_id_var.set(span_id)
return span_id
def finish_span(self, span_id: str, status: str = "ok"):
import time
for span in self.spans:
if span.span_id == span_id:
span.end_time = time.time()
span.status = status
break
# 使用示例
tracer = Tracer()
async def handle_user_request(session_id: str, message: str):
span_id = tracer.start_span("handle_request", session_id=session_id)
try:
# 子操作:LLM 推理
llm_span = tracer.start_span("llm_inference")
response = await call_llm(message)
tracer.finish_span(llm_span)
# 子操作:工具调用
if needs_tool(response):
tool_span = tracer.start_span("tool_execution")
tool_result = await execute_tool(response)
tracer.finish_span(tool_span)
tracer.finish_span(span_id, "ok")
return response
except Exception as e:
tracer.finish_span(span_id, "error")
raise
async def call_llm(message: str) -> str:
await asyncio.sleep(0.5) # 模拟推理
return f"Response to: {message}"
async def execute_tool(response: str) -> str:
await asyncio.sleep(0.2)
return "tool result"
def needs_tool(response: str) -> bool:
return "tool" in response.lower()
关键追踪指标
请求链路示例:
trace_id: a1b2c3d4
├── handle_request (1250ms)
│ ├── llm_inference (800ms) ← GPU 推理是瓶颈
│ ├── tool_execution (350ms)
│ │ ├── search_api (200ms)
│ │ └── parse_result (150ms)
│ └── format_response (100ms)
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| P50 延迟 | 中位数请求延迟 | >2s |
| P99 延迟 | 99 分位延迟 | >10s |
| 错误率 | 失败请求占比 | >1% |
| 饱和度 | GPU 利用率 | >85% |
| 消息队列积压 | 未消费消息数 | >1000 |
自动扩缩容
class AutoScaler:
"""基于负载的自动扩缩容"""
def __init__(self, min_nodes: int = 2, max_nodes: int = 20,
scale_up_threshold: float = 0.7,
scale_down_threshold: float = 0.3):
self.min_nodes = min_nodes
self.max_nodes = max_nodes
self.scale_up_threshold = scale_up_threshold
self.scale_down_threshold = scale_down_threshold
self.current_nodes = min_nodes
def evaluate(self, metrics: dict) -> str:
"""根据指标决定扩缩容
metrics: {
"avg_gpu_utilization": 0.85,
"avg_memory_usage": 0.72,
"request_queue_depth": 15,
"avg_response_time_ms": 3500
}
"""
load_score = self._calculate_load_score(metrics)
if load_score > self.scale_up_threshold and self.current_nodes < self.max_nodes:
return self._scale_up()
elif load_score < self.scale_down_threshold and self.current_nodes > self.min_nodes:
return self._scale_down()
return "no_action"
def _calculate_load_score(self, metrics: dict) -> float:
"""综合负载评分 [0, 1]"""
weights = {
"avg_gpu_utilization": 0.4,
"avg_memory_usage": 0.2,
"request_queue_depth_norm": 0.2, # 归一化后的队列深度
"avg_response_time_norm": 0.2
}
# 归一化
queue_norm = min(metrics.get("request_queue_depth", 0) / 50, 1.0)
latency_norm = min(metrics.get("avg_response_time_ms", 0) / 5000, 1.0)
score = (
metrics.get("avg_gpu_utilization", 0) * weights["avg_gpu_utilization"] +
metrics.get("avg_memory_usage", 0) * weights["avg_memory_usage"] +
queue_norm * weights["request_queue_depth_norm"] +
latency_norm * weights["avg_response_time_norm"]
)
return score
def _scale_up(self) -> str:
# 扩容幅度基于负载严重程度
self.current_nodes = min(self.current_nodes + 2, self.max_nodes)
return f"scaled_up to {self.current_nodes}"
def _scale_down(self) -> str:
self.current_nodes = max(self.current_nodes - 1, self.min_nodes)
return f"scaled_down to {self.current_nodes}"
实战建议
先无状态化,再水平扩展。如果 Agent 进程持有会话状态,扩展就是灾难。先把状态移到 Redis/数据库,再谈加机器。
GPU 是真正的瓶颈。CPU 可以轻松扩展到几十核,但一张 A100 卡 40k+。推理服务要单独部署、单独扩展。
预热连接池。新节点启动时预建到 Redis、向量数据库、LLM API 的连接,避免冷启动延迟:
async def warmup(node: InferenceNode): await node.llm_client.ping() await node.vector_store.ping() await node.session_store.ping()优雅降级。过载时返回简化响应而非报错:
if load_score > 0.9: return await simple_fallback_response(query) # 跳过 RAG,直接用基础模型会话亲和但不要强绑定。节点故障时必须能自动迁移到其他节点,亲和只是优化而非约束。
追踪从第一天开始。等系统出问题再补追踪,成本是设计时就加的 10 倍。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
