1. 云端智能体的基础设施困局:从一个真实场景说起
去年下半年我接手了一个内部项目,要把一个基于大模型的智能体从本地Demo推到云端生产环境。本地跑的时候一切丝滑,响应快、逻辑清晰、工具调用准确。结果一上云,问题像开了闸一样涌出来:并发一上来延迟飙升到十几秒,沙箱里的代码执行偶尔把宿主机资源吃满,多个智能体同时调用外部工具时状态互相污染,日志散落在四五个地方根本串不起来。那段时间我几乎每天都在排查“为什么本地好好的,云端就崩了”这类问题。
这段经历让我意识到一个很现实的问题:智能体本身的能力在快速进步,但支撑它稳定运行的基础设施远远没跟上。大家讨论智能体,焦点往往在模型选型、提示词工程、工具编排上,但真正决定一个智能体能不能在云端扛住生产流量的,是它脚下的那层基础设施——运行时怎么设计、沙箱怎么隔离、资源怎么调度、状态怎么管理、可观测性怎么做。这些听起来不如“多智能体协同”“自主容错”那么性感,但它们才是决定项目能不能上线的硬约束。
这篇文章面向的是正在或准备把智能体推向云端生产环境的开发者和架构师。不管你是用现成平台搭建智能体,还是用Python从零构建,只要涉及云端部署和规模化运行,下面这些关于运行时、沙箱、Kubernetes调度的经验都值得参考。我会从整体设计思路讲起,然后逐层拆解核心细节、实操过程和踩坑记录,尽量把“为什么这么做”讲透,而不是只丢一堆配置。
2. 云端智能体基础设施的整体设计思路
2.1 为什么本地能跑通不代表云端能扛住
本地环境和云端生产环境之间的差距,比大多数人想象的要大得多。本地跑智能体,通常是一个进程、一个用户、串行执行,资源几乎独占,出了问题重启就行。云端生产环境面对的是完全不同的约束条件。
第一是并发模型的变化。本地你可能一次只跑一个任务,云端可能同时有几十上百个智能体实例在处理不同用户的请求。每个智能体都可能调用大模型API、执行代码、读写文件、访问外部服务,这些操作对CPU、内存、网络IO的消耗模式完全不同,混在一起就会互相干扰。
第二是隔离性的硬要求。智能体经常需要执行动态生成的代码,比如让它写一段Python做数据分析,或者生成一个脚本去调用某个接口。这些代码是不可信的,如果直接在宿主机上跑,一个死循环或者一个内存泄漏就能把整个节点拖垮。本地开发时你信任自己的代码,云端你必须假设每一段代码都可能出问题。
第三是状态管理的复杂度。智能体是有状态的,它需要记住对话历史、中间结果、工具调用的返回值。本地你可以把状态放在内存里,进程不重启就没问题。云端实例随时可能被调度、重启、扩缩容,状态必须外置到可靠的存储层,否则用户会发现智能体“失忆”了。
第四是可观测性的缺失。本地调试你可以直接看控制台输出,云端你需要把日志、指标、链路追踪全部收集起来,否则出了问题根本不知道是哪一层挂了。智能体的一次请求可能跨越模型调用、代码执行、外部API、数据库查询等多个环节,没有完整的链路追踪,排查效率极低。
我踩过的最大的坑就是低估了这四点的叠加效应。单独看每一点都有解法,但当你同时面对并发、隔离、状态、可观测性四个维度的约束时,架构设计的取舍会变得非常棘手。
2.2 基础设施分层的核心逻辑
把云端智能体的基础设施拆开看,我倾向于分成四层来理解,每一层解决不同的问题,层与层之间有清晰的边界。
最底层是资源调度层,核心是Kubernetes。它负责把智能体实例调度到合适的节点上,管理CPU和内存的配额,处理扩缩容和故障恢复。这一层的关键词是“编排”和“弹性”。为什么选Kubernetes而不是自己写调度?因为智能体的负载模式是突发性的,可能几分钟内从零扩到几十个实例,也可能长时间空闲,Kubernetes的HPA和节点自动伸缩能省掉大量自研成本。
第二层是运行时层,负责智能体进程的生命周期管理。这一层要解决的是:智能体怎么启动、怎么接收请求、怎么管理上下文、怎么优雅退出。运行时需要和调度层配合,暴露健康检查接口,支持优雅停机,管理连接池和线程池。这一层的关键词是“生命周期”和“资源管理”。
第三层是沙箱层,专门负责隔离不可信代码的执行。智能体生成的代码、调用的外部工具、执行的shell命令,都应该在沙箱里跑。沙箱要限制CPU时间、内存上限、网络访问、文件系统权限。这一层的关键词是“隔离”和“安全”。沙箱的实现方式有很多种,从轻量的进程级隔离到重量级的虚拟机级隔离,选择取决于你的安全要求和性能预算。
最上层是状态与可观测层,负责持久化智能体的状态和收集运行数据。状态包括对话历史、任务进度、中间结果,通常用Redis或数据库存储。可观测性包括结构化日志、指标采集、分布式追踪,通常用OpenTelemetry标准来统一。这一层的关键词是“持久化”和“可追踪”。
这四层不是孤立的,它们之间有大量的交互。比如沙箱的资源使用情况要反馈给调度层做决策,运行时的健康状态要暴露给Kubernetes做探针,状态层的读写延迟会影响运行时的响应时间。设计的时候必须把这些交互考虑进去,否则各层各自为政,整体就会出问题。
2.3 方案选型背后的取舍
在实际选型时,有几个关键决策点需要仔细权衡。
沙箱方案的选择是第一个大决策。常见的有几种路线:基于容器 namespace 和 cgroup 的轻量隔离、基于 gVisor 的用户态内核隔离、基于 Firecracker 的微虚拟机隔离。轻量隔离性能最好,启动毫秒级,但隔离强度最弱,内核漏洞可能被利用。gVisor 隔离强度中等,兼容性有一些坑,某些系统调用不支持。Firecracker 隔离最强,接近虚拟机级别,但启动要几百毫秒,内存开销也更大。我的建议是:如果智能体只执行你自己写的代码,轻量隔离够用;如果执行用户提供的代码,至少上 gVisor;如果涉及多租户且安全要求高,直接上 Firecracker。
状态存储的选择是第二个决策点。Redis 适合存短期状态和缓存,读写快但持久性弱。PostgreSQL 适合存长期状态和需要事务的场景,可靠但延迟高。我的做法是分层存储:热状态放 Redis,冷状态落 PostgreSQL,中间加一层消息队列做异步同步。这样既保证了响应速度,又不会丢数据。
可观测性方案的选择是第三个决策点。自建 ELK 栈灵活但维护成本高,用云厂商的托管服务省事但有绑定风险。我倾向于用 OpenTelemetry 做采集标准,后端可以换,这样不会被某一家锁死。日志用结构化 JSON,指标用 Prometheus 格式,追踪用 W3C Trace Context 标准,这三样统一了,后面换后端就是改配置的事。
3. 核心细节解析与实操要点
3.1 运行时设计:智能体进程的生命周期管理
运行时层是智能体基础设施的“骨架”,它决定了智能体进程怎么活、怎么死、怎么干活。我在设计运行时的时候,重点关注了四个环节。
启动阶段,智能体进程需要完成模型客户端初始化、工具注册、状态存储连接、配置加载这几件事。这里有个容易忽略的点:模型客户端的初始化应该懒加载,不要一启动就去连模型API。因为智能体可能启动后几分钟才收到第一个请求,提前连接会浪费连接池资源,而且模型API偶尔抖动会导致启动失败。我的做法是第一次调用时才初始化客户端,同时加一个预热接口,在流量到来前主动触发一次轻量调用。
请求处理阶段,运行时需要管理并发。智能体的请求处理通常是IO密集型的,大部分时间在等模型返回、等工具执行、等数据库查询。用异步框架(比如Python的asyncio或Go的goroutine)能显著提升并发能力。但要注意,模型调用和工具执行要设置超时,否则一个慢请求会拖住整个事件循环。我的经验值是:模型调用超时设30秒,工具执行超时设60秒,整体请求超时设120秒。超过就主动断开,返回降级结果。
状态管理阶段,运行时要在每个关键节点持久化状态。我的做法是在请求开始时从Redis加载状态,处理过程中每完成一个步骤就写一次Redis,请求结束时把最终状态落PostgreSQL。这样即使进程中途崩溃,重启后也能从Redis恢复最近的进度。这里有个细节:写Redis要用pipeline批量写,不要每步都单独写,否则网络往返会把延迟拉高。
退出阶段,运行时需要支持优雅停机。Kubernetes发送SIGTERM后,进程应该停止接收新请求,等待正在处理的请求完成,然后关闭连接池,最后退出。这个等待时间要设合理,太短会导致请求被中断,太长会导致滚动更新变慢。我一般设30秒,同时配合Kubernetes的preStop钩子,先sleep 5秒让负载均衡器把流量切走,再开始优雅停机。
# 运行时优雅停机的核心逻辑示意 import asyncio import signal class AgentRuntime: def __init__(self): self.shutting_down = False self.active_requests = 0 async def handle_request(self, request): if self.shutting_down: raise RuntimeError("runtime is shutting down") self.active_requests += 1 try: # 加载状态、处理请求、持久化状态 await self.process(request) finally: self.active_requests -= 1 async def shutdown(self): self.shutting_down = True # 等待正在处理的请求完成,最多等30秒 for _ in range(30): if self.active_requests == 0: break await asyncio.sleep(1) await self.close_connections()注意事项:优雅停机的等待时间要和Kubernetes的terminationGracePeriodSeconds对齐,后者要比前者大5到10秒,否则Kubernetes会在进程还没退完时就强杀。
3.2 沙箱隔离:不可信代码的安全执行环境
沙箱是云端智能体基础设施里最容易被低估的一层。很多人觉得“不就是跑个代码吗,subprocess开一下就行”,结果线上出了安全事故才后悔。我在这块踩过的坑最多,下面把关键点拆开讲。
隔离级别的选择。前面提过三种路线,这里补充一下实操细节。轻量容器隔离用Docker的默认配置就行,但一定要加这几个参数:--read-only让根文件系统只读,--tmpfs /tmp给临时目录,--cap-drop ALL去掉所有特权,--security-opt no-new-privileges禁止提权,--pids-limit 64限制进程数防止fork炸弹。gVisor的话,用runsc作为runtime,兼容性测试要重点跑,特别是涉及网络和文件系统的操作。Firecracker的话,启动时间要优化,可以用预热池的方式提前创建好微虚拟机,请求来了直接分配。
资源限制的设定。CPU限制用cgroup的cpu quota,我一般给每个沙箱0.5到1核。内存限制用memory limit,给256MB到512MB。这两个值要根据实际负载调,太小会导致正常代码跑不动,太大会导致资源浪费。我的做法是先给一个保守值,然后通过监控观察P99的资源使用,逐步调整。磁盘IO也要限制,用--device-read-bps和--device-write-bps控制,防止某个沙箱把磁盘打满。
网络访问的控制。智能体执行的代码经常需要访问外部服务,但不能让它随便访问。我的做法是默认禁止所有出站网络,只白名单特定的域名和端口。在Kubernetes里可以用NetworkPolicy实现,在容器级别可以用iptables规则。如果沙箱需要访问模型API,就把模型API的域名加白名单。这样即使代码里有恶意逻辑,也没法把数据传到外部。
执行超时的处理。沙箱里的代码必须有超时,否则一个死循环就能占住资源。超时时间根据任务类型设,简单的计算任务给10秒,复杂的数据处理给60秒。超时后要强制杀掉进程,不能只是发个信号等它自己退。我的做法是用timeout命令包裹执行,或者用cgroup的 freezer 功能强制冻结再清理。
# Kubernetes中沙箱Pod的安全配置示例 apiVersion: v1 kind: Pod metadata: name: agent-sandbox spec: containers: - name: sandbox image: sandbox-runtime:latest securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] runAsNonRoot: true runAsUser: 1000 resources: limits: cpu: "1" memory: "512Mi" requests: cpu: "0.5" memory: "256Mi" volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: sizeLimit: "100Mi"实操心得:沙箱镜像要尽可能小,用alpine或distroless基础镜像,减少攻击面。同时要定期更新镜像,修补已知漏洞。我见过因为沙箱镜像里带了旧版本的curl导致安全扫描不通过的案例。
3.3 Kubernetes调度:智能体负载的弹性伸缩
Kubernetes是云端智能体基础设施的调度底座,但默认配置不一定适合智能体的负载特征。智能体的负载有几个特点:突发性强、单实例资源需求波动大、对启动速度敏感。针对这些特点,我在调度层做了几项优化。
资源请求与限制的配置。智能体的CPU和内存使用波动很大,请求处理时可能吃满CPU,空闲时几乎不用。如果requests设太高,节点利用率低;设太低,突发时会被限流。我的做法是requests设低一点(比如0.25核256MB),limits设高一点(比如2核2GB),配合Burstable QoS。这样空闲时能超卖,突发时能burst。但要注意,如果节点上所有Pod都突发,会互相争抢,所以节点要留足够的预留资源。
HPA的指标选择。默认的HPA基于CPU利用率,但智能体的瓶颈往往不在CPU,而在模型API的响应时间或队列长度。我建议用自定义指标做HPA,比如请求队列深度、P99延迟、活跃请求数。这些指标更能反映智能体的真实负载。配置的时候要注意冷却时间,扩容可以快一点(30秒),缩容要慢一点(5分钟),避免频繁抖动。
PodDisruptionBudget的设置。滚动更新或节点维护时,要保证有足够的实例在线。PDB设minAvailable: 50%或maxUnavailable: 1,根据实例数量调整。实例少的时候用maxUnavailable更灵活,实例多的时候用minAvailable更稳。
节点亲和性与反亲和性。智能体实例最好分散在不同节点上,避免单节点故障导致全部不可用。用podAntiAffinity让同一智能体的实例尽量分散。如果用了沙箱,沙箱Pod和运行时Pod也要分开调度,避免互相影响。
# HPA基于自定义指标的配置示例 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: agent-runtime-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: agent-runtime minReplicas: 2 maxReplicas: 50 metrics: - type: Pods pods: metric: name: agent_request_queue_depth target: type: AverageValue averageValue: "10" behavior: scaleUp: stabilizationWindowSeconds: 30 policies: - type: Percent value: 100 periodSeconds: 30 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60注意事项:自定义指标需要配合Prometheus Adapter或KEDA使用,部署和配置有一定复杂度。如果团队没有现成的监控体系,建议先用CPU指标跑起来,再逐步迁移到自定义指标。
3.4 状态管理与可观测性:让智能体不再“失忆”和“黑盒”
状态管理和可观测性是智能体基础设施里最“脏活累活”的部分,但也是生产环境不可或缺的。
状态管理的分层设计。我把状态分成三类:会话状态(对话历史、用户偏好)、任务状态(当前进度、中间结果)、系统状态(连接池、限流计数器)。会话状态和任务状态要持久化,系统状态可以放内存。会话状态用Redis的Hash结构存,每个会话一个key,字段存不同的属性。任务状态用Redis的List或Stream存,按时间顺序记录每一步。持久化到PostgreSQL时,用异步任务批量写,不要阻塞主流程。
状态一致性。多个智能体实例可能同时操作同一个会话的状态,需要加锁。用Redis的分布式锁(Redlock或单实例SETNX)可以解决,但要注意锁的超时和续期。我的做法是:读状态不加锁,写状态加锁,锁的粒度到会话级别,超时设5秒,后台有续期线程。这样既保证了并发安全,又不会因为锁竞争导致性能下降。
可观测性的三支柱。日志、指标、追踪,一个都不能少。日志用结构化JSON,每条日志带trace_id、span_id、agent_id、session_id,方便串联。指标用Prometheus格式,重点采集请求量、延迟分布、错误率、沙箱资源使用、模型API调用次数和延迟。追踪用OpenTelemetry,把一次智能体请求的完整链路串起来,从接收请求到模型调用到工具执行到返回结果,每个环节都有span。
# 结构化日志和追踪的集成示例 import logging import json from opentelemetry import trace logger = logging.getLogger(__name__) tracer = trace.get_tracer(__name__) def process_request(request): with tracer.start_as_current_span("agent.process") as span: span.set_attribute("agent.id", request.agent_id) span.set_attribute("session.id", request.session_id) logger.info(json.dumps({ "event": "request_start", "trace_id": format(span.get_span_context().trace_id, '032x'), "agent_id": request.agent_id, "session_id": request.session_id })) # 处理逻辑... logger.info(json.dumps({ "event": "request_end", "trace_id": format(span.get_span_context().trace_id, '032x'), "duration_ms": span.end_time - span.start_time }))实操心得:日志量大的时候,采样很重要。我的做法是错误日志全采,正常日志按1%采样,但trace_id始终保留,这样出问题时能通过trace_id找到完整的链路。另外,日志的保留时间要设合理,生产环境一般保留7到30天,太短不够排查,太长成本高。
4. 实操过程与核心环节实现
4.1 从零搭建一个可运行的智能体运行时
这一节我把前面讲的东西串起来,给一个从零搭建智能体运行时的完整流程。假设你已经有一个Kubernetes集群,下面按步骤来。
第一步:准备基础镜像。运行时镜像用Python 3.11的slim版本,装好依赖,镜像大小控制在500MB以内。沙箱镜像用alpine,只装必要的运行时,控制在100MB以内。两个镜像都要做安全扫描,用trivy或grype都行。
# 运行时镜像 FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . USER 1000 EXPOSE 8080 CMD ["python", "-m", "agent_runtime"]第二步:部署Redis和PostgreSQL。生产环境建议用云厂商的托管服务,省去运维成本。如果自建,Redis用哨兵模式或集群模式保证高可用,PostgreSQL用主从复制加流复制。连接字符串通过Kubernetes Secret注入,不要硬编码在镜像里。
第三步:部署运行时。用Deployment部署,副本数先设2,配置好资源请求和限制,挂载ConfigMap做配置,挂载Secret做密钥。配置健康检查接口,liveness探针检查进程是否存活,readiness探针检查是否能接收请求。
apiVersion: apps/v1 kind: Deployment metadata: name: agent-runtime spec: replicas: 2 selector: matchLabels: app: agent-runtime template: metadata: labels: app: agent-runtime spec: containers: - name: runtime image: agent-runtime:latest ports: - containerPort: 8080 envFrom: - secretRef: name: agent-secrets - configMapRef: name: agent-config livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "2" memory: "2Gi"第四步:部署沙箱执行服务。沙箱可以做成一个独立的服务,运行时通过gRPC调用它执行代码。沙箱服务用DaemonSet部署,每个节点一个,这样沙箱和运行时在同一节点,网络延迟低。沙箱服务内部管理一个沙箱池,请求来了从池里分配,用完回收。
第五步:配置HPA和PDB。HPA基于自定义指标,PDB保证滚动更新时至少有50%的实例在线。这两个配置前面有示例,这里不重复。
第六步:接入可观测性。部署OpenTelemetry Collector,配置日志、指标、追踪的采集和导出。日志导出到Loki或Elasticsearch,指标导出到Prometheus,追踪导出到Jaeger或Tempo。Grafana做统一展示。
4.2 沙箱执行的完整链路实现
沙箱执行的链路比运行时复杂,涉及运行时、沙箱服务、沙箱容器三层。我把完整链路拆开讲。
请求发起。运行时收到需要执行代码的请求,把代码、语言、输入参数、资源限制打包成一个执行请求,通过gRPC发给沙箱服务。请求里要带trace_id,方便串联链路。
沙箱分配。沙箱服务收到请求后,从沙箱池里找一个空闲的沙箱。如果池里没有,就创建一个新的。创建沙箱的过程包括:拉取镜像(如果本地没有)、创建容器、配置资源限制、启动容器、等待就绪。这个过程可能要几百毫秒到几秒,所以池的预热很重要。我的做法是保持至少2个热沙箱,请求来了直接分配,同时后台异步补充。
代码注入与执行。沙箱就绪后,把代码和输入参数注入进去。注入方式有两种:一种是挂载volume,把代码文件写进去;另一种是通过stdin传入。我倾向于挂载volume,因为代码可能比较大,stdin有大小限制。注入后启动执行进程,设置超时,等待结果。
结果收集与沙箱回收。执行完成后,收集stdout、stderr、退出码、资源使用情况。结果返回给运行时,沙箱标记为可回收。回收时清理临时文件、重置状态,如果沙箱用了多次就销毁重建,避免状态残留。
# 沙箱服务核心逻辑示意 class SandboxPool: def __init__(self, min_size=2, max_size=20): self.min_size = min_size self.max_size = max_size self.available = [] self.in_use = set() async def acquire(self, timeout=10): # 先从池里找空闲的 while self.available: sandbox = self.available.pop() if await sandbox.is_healthy(): self.in_use.add(sandbox) return sandbox # 池里没有就创建新的 if len(self.in_use) < self.max_size: sandbox = await self.create_sandbox() self.in_use.add(sandbox) return sandbox # 达到上限就等待 raise TimeoutError("no sandbox available") async def release(self, sandbox): self.in_use.discard(sandbox) await sandbox.cleanup() if len(self.available) < self.min_size: self.available.append(sandbox) else: await sandbox.destroy()注意事项:沙箱池的大小要根据并发量调。太小会导致请求排队,太大会浪费资源。我的经验值是:池大小设为P99并发数的1.5倍,同时设置一个上限防止资源耗尽。
4.3 参数计算与容量规划
容量规划是很多人忽略的环节,但它是保证生产环境稳定的关键。我以一个有1000个日活用户、每个用户每天发起20次智能体请求的场景为例,算一下需要多少资源。
请求量估算。1000用户乘以20次等于每天20000次请求。假设请求集中在8小时内,平均每秒约0.7次。峰值按平均值的5倍算,约3.5次每秒。这是运行时的并发需求。
运行时资源估算。每个运行时实例能处理约10个并发请求(取决于模型调用延迟和异步效率)。峰值3.5次每秒,假设每次请求平均处理时间5秒,那么并发数约17.5。需要2个实例,留一倍余量就是4个实例。每个实例2核2GB,总共8核8GB。
沙箱资源估算。假设30%的请求需要执行代码,峰值约1次每秒。每次执行平均10秒,并发约10个沙箱。每个沙箱1核512MB,总共10核5GB。加上池的预热,预留15个沙箱的容量。
状态存储估算。每个会话状态约10KB,1000个活跃会话就是10MB。Redis给1GB足够。任务状态每个请求约50KB,每天20000次就是1GB,PostgreSQL给10GB够用一个月。
模型API配额。每次请求平均调用模型2次,每天40000次调用。要确认模型API的QPS限制和配额,必要时申请提升。
这个估算只是起点,实际运行后要根据监控数据调整。我的做法是每周review一次资源使用率,CPU持续超过70%就扩容,低于30%就缩容。
5. 常见问题与排查技巧实录
5.1 智能体云端运行的典型故障速查
下面这张表是我在实际运维中整理的常见问题速查表,覆盖了大部分线上故障场景。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 请求延迟突然飙升 | 模型API限流或抖动 | 查看模型调用延迟指标和错误率 | 加限流重试,降级到备用模型 |
| 沙箱执行超时 | 代码死循环或资源不足 | 查看沙箱CPU和内存使用 | 强制杀掉,调整资源限制 |
| 智能体“失忆” | 状态未持久化或Redis连接断开 | 检查Redis连接和状态写入日志 | 修复连接,加本地缓存兜底 |
| 实例频繁重启 | 内存泄漏或OOM | 查看Pod重启原因和内存曲线 | 修复泄漏,调高内存限制 |
| 并发上不去 | 事件循环阻塞或连接池耗尽 | 查看事件循环延迟和连接池使用 | 优化异步逻辑,扩大连接池 |
| 日志丢失 | 日志采集配置错误或磁盘满 | 检查采集器和磁盘使用 | 修复配置,清理磁盘 |
| 追踪链路断裂 | trace context未正确传递 | 检查跨服务调用的header | 统一用W3C标准传递 |
5.2 独家避坑技巧
坑一:模型客户端的连接池配置。默认的连接池大小往往不够,高并发时会排队。我的做法是把连接池大小设为并发数的1.5倍,同时设置连接超时和读取超时。另外,模型API偶尔会返回5xx,要加指数退避重试,但重试次数不要超过3次,否则会放大延迟。
坑二:沙箱的僵尸进程。沙箱里的代码如果fork了子进程,主进程退出后子进程可能变成僵尸。我的做法是在沙箱启动时设置--init参数,用tini作为init进程,自动回收僵尸进程。另外,沙箱销毁时要确保所有进程都被杀掉,用cgroup的kill功能强制清理。
坑三:Redis的大key和热key。会话状态如果存了很长的对话历史,会变成大key,读写慢还容易阻塞。我的做法是限制单个会话状态的大小,超过就分片存储。热key的话,用本地缓存加Redis的多级缓存,减少Redis压力。
坑四:Kubernetes的DNS解析延迟。智能体频繁调用外部服务,DNS解析可能成为瓶颈。我的做法是用NodeLocal DNSCache,把DNS缓存放到节点本地,减少网络往返。同时给关键服务配置IP直连,绕过DNS。
坑五:日志的敏感信息泄露。智能体的日志里可能包含用户输入、模型输出、API密钥等敏感信息。我的做法是在日志采集层加脱敏规则,对特定字段做掩码。同时,日志存储要加密,访问要审计。
这些坑都是我实际踩过的,有些还导致了线上事故。写出来是希望大家能跳过这些坑,把时间花在更有价值的事情上。
5.3 性能调优的实操记录
性能调优是个持续的过程,我记录了几个关键的调优节点。
第一次调优:异步化。最初运行时是同步的,每个请求占一个线程,并发上不去。改成asyncio后,单实例并发从10提升到100,延迟P99从5秒降到2秒。关键改动是把模型调用、数据库查询、HTTP请求全部改成异步。
第二次调优:连接池。异步化后发现连接池成了瓶颈,模型客户端和Redis客户端的连接池经常耗尽。把连接池大小从10调到50,同时加了连接复用和健康检查,延迟P99降到1.5秒。
第三次调优:沙箱预热。沙箱创建耗时约2秒,导致首次执行代码的请求延迟很高。加了预热池后,首次执行延迟降到200毫秒以内。池的大小设为P99并发的1.5倍,效果最好。
第四次调优:状态缓存。每次请求都读Redis,网络往返累积起来很可观。加了本地LRU缓存后,读状态的延迟从5毫秒降到0.1毫秒。缓存失效策略用写时失效,保证一致性。
第五次调优:日志采样。日志量太大导致采集器CPU打满。改成错误全采、正常1%采样后,采集器CPU降到20%以下,同时保留了排查能力。
这五次调优下来,整体P99延迟从最初的10秒降到了800毫秒,单实例并发从10提升到200。当然,这只是一个参考,具体效果取决于你的场景和负载特征。
6. 智能体基础设施的扩展方向
聊完核心内容,再说几个我觉得值得关注的扩展方向。这些不是必须做的,但如果你在规划长期的基础设施演进,可以参考。
方向一:多租户隔离。如果你的智能体平台要服务多个团队或客户,租户隔离就很重要。除了沙箱级别的隔离,还要考虑网络隔离、存储隔离、配额隔离。Kubernetes的Namespace加ResourceQuota可以做基础隔离,更细粒度的可以用服务网格做流量隔离。
方向二:智能体编排。单个智能体的基础设施搞定后,多智能体协同会成为下一个瓶颈。多个智能体之间的通信、协调、状态同步,需要一套编排层。可以考虑用消息队列做异步通信,用工作流引擎做流程编排,用分布式锁做资源协调。
方向三:成本优化。智能体运行的成本大头在模型调用和计算资源。模型调用可以用缓存、批处理、小模型替代来降本。计算资源可以用Spot实例、自动伸缩、资源超卖来降本。我的经验是,合理的成本优化能省30%到50%的费用。
方向四:安全加固。智能体的安全风险包括提示词注入、工具滥用、数据泄露。除了沙箱隔离,还要做输入过滤、输出审查、权限控制。我的做法是给每个工具定义明确的权限边界,智能体只能调用被授权的工具,且调用参数要经过校验。
方向五:标准化。智能体基础设施目前还没有统一标准,各家实现差异很大。OpenTelemetry在可观测性上做了统一,但在运行时、沙箱、编排上还没有。如果你的团队有余力,可以参与开源社区的标准制定,或者至少保持接口的开放性,方便后续迁移。
最后分享一个我在实际使用中的小技巧:给智能体的每个关键操作都加一个“逃生舱”。比如模型调用失败时降级到规则引擎,沙箱执行超时时返回部分结果,状态存储不可用时用本地缓存兜底。这些逃生舱平时用不上,但关键时刻能保证系统不整体崩溃。智能体的自主容错能力很重要,但基础设施层面的容错同样不可忽视,两者配合才能构建真正可靠的系统。