当一个 Agent 从简单的问答脚本变成能够调用工具、保留历史、并行执行任务的服务时,“需要多少内存”就不再等于模型上下文窗口有多大。真正占用内存的,通常还包括会话历史、工具返回值、中间状态、缓存,以及同时运行的任务数量。
这类问题适合用预算来回答,而不是凭经验猜一个机器规格。可以先把 Agent 的运行时内存拆开,再根据单个请求和并发数计算总需求。
先拆分 Agent 的内存账单
一个实用的近似模型可以写成:
总内存 ≈ 基础进程内存
+ 模型或 SDK 运行时内存
+ 单个会话状态
+ 单个请求的工具与中间结果
+ 并发带来的额外内存
+ 安全余量
其中,单个会话状态可能包含:
- 消息历史和序列化后的请求体
- Agent 当前计划、工具调用参数和结果
- 压缩前后的摘要
- 用户身份、权限和业务上下文
- 临时缓存,例如检索结果或网页内容
工具输出尤其容易被低估。一个“读取目录”工具可能只返回几 KB,但网页抓取、日志查询、数据库导出或代码搜索很快就会产生 MB 级字符串。如果这些结果同时保存在消息历史、日志对象和缓存中,实际占用可能是原始数据的数倍。
用峰值而不是平均值做估算
平均内存只能说明系统平稳时的表现。生产环境更应该估算峰值:一次请求会产生多少中间数据,多少请求可能同时卡在工具调用阶段,以及失败重试是否会保留旧状态。
可以使用下面的简化公式:
峰值内存 ≈ 基础内存
+ 并发数 × (会话状态 + 工具结果 + 中间状态)
+ 缓存上限
+ 安全余量
例如,假设一个服务的基础进程和 SDK 占用 350 MB,每个活跃会话平均保留 2 MB,工具和中间结果最多占 8 MB,缓存限制为 512 MB,目标并发为 40,并预留 30% 安全余量:
(350 + 40 × (2 + 8) + 512) × 1.3 ≈ 1,001 MB
这个数字不是容量承诺,而是一个可审查的起点。真实部署前仍应使用压测记录峰值 RSS、容器限制和请求分布。
一个可以直接改造的预算脚本
下面的 Python 脚本只使用标准库。它把每项内存都转换为 MB,并输出目标并发下的估算值。示例参数是人为假设,运行前应替换成压测或线上观测数据。
#!/usr/bin/env python3
from dataclasses import dataclass
@dataclass
class MemoryBudget:
base_mb: float
session_mb: float
tool_result_mb: float
intermediate_mb: float
cache_mb: float
concurrency: int
headroom: float = 0.30
def estimate_mb(self) -> float:
per_request = (
self.session_mb
+ self.tool_result_mb
+ self.intermediate_mb
)
raw = self.base_mb + self.concurrency * per_request + self.cache_mb
return raw * (1 + self.headroom)
budget = MemoryBudget(
base_mb=350,
session_mb=2,
tool_result_mb=6,
intermediate_mb=2,
cache_mb=512,
concurrency=40,
headroom=0.30,
)
estimated = budget.estimate_mb()
print(f"Estimated peak memory: {estimated:.0f} MB")
print(f"Recommended container limit: {estimated / 1024:.2f} GiB")
可以把 tool_result_mb 分别替换为网页抓取、数据库查询、代码搜索等工具的实测峰值。若不同工具差异很大,最好按工具类型分别建模,而不是使用一个过于乐观的平均值。
控制内存的几个关键边界
限制工具输出,而不是只扩大上下文窗口
工具应该有明确的结果上限。例如日志查询可以限制返回行数,网页工具可以只保留正文,代码搜索可以只返回匹配片段。超过限制时,返回分页标记或摘要,让 Agent 决定是否继续获取。
def cap_text(text: str, max_chars: int = 20_000) -> str:
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n...[truncated; request a narrower query]"
字符数不是精确的内存大小,但它是工具接口中容易执行和监控的第一道边界。生产实现还应限制嵌套 JSON、数组元素数量和单次下载大小。
让会话历史有生命周期
不要无限追加消息。可以按消息数量、字符数或估算 token 数触发摘要,并删除已经不再需要的原始工具输出。摘要本身也应该设置上限,否则只是把无限历史换成另一种无限增长。
区分并发限制和队列长度
把并发限制设为 40,不代表可以让队列无限增长。排队请求可能已经包含完整请求体、身份上下文和重试状态。应同时限制:
- 正在执行的 Agent 数量
- 等待队列长度
- 单个请求的最大执行时间
- 工具调用的最大次数
- 重试次数和保留状态的时间
当预算不足时,优先拒绝或延迟新任务,而不是让进程持续增长直到被操作系统终止。
监控分项,而不只看容器总内存
至少记录基础进程内存、活跃会话数、每个会话的历史大小、工具结果大小、缓存大小和并发请求数。只有看到分项数据,才能判断问题来自上下文增长、某个工具失控,还是并发配置过高。
部署前的检查清单
- 用真实工具输出测量一次请求的平均值和 P95/P99 峰值
- 分别压测低、中、高上下文长度
- 验证超大工具结果会被截断、分页或拒绝
- 为会话历史、缓存、队列和重试状态设置上限
- 用峰值而不是平均值计算容器内存限制
- 为运行时抖动和发布期间的额外进程预留空间
- 观察 OOM 前的指标,确认服务能够尽早返回可诊断的错误
Agent 的内存需求不是一个固定常数,而是请求形态和并发策略共同产生的结果。先定义每个状态的上限,再用压测数据填入预算模型,通常比直接选择更大的机器更容易解释,也更容易长期控制成本。