第2010篇:Agent的Token预算管理——防止无限循环的成本控制策略
2026/4/30大约 3 分钟
第2010篇:Agent的Token预算管理——防止无限循环的成本控制策略
适读人群:需要控制AI Agent运营成本的工程师 | 阅读时长:约17分钟 | 核心价值:设计Token预算机制,让Agent在成本可控的范围内工作
上线后第一个月,我们的API账单让老板找到了我。
比预期高了3倍多。
排查下来,主要原因有两个:一是有几个复杂任务的Agent陷入了推理循环,每次循环都要消耗大量Token;二是有些Prompt写得很冗余,每次调用都带着大量无用的上下文。
Token成本看起来小,但乘以用户量和调用频率,就是一笔可观的费用。Agent系统不能让Token使用失控。
Token预算的分层设计
任务级预算 > 步骤级预算 > 单次LLM调用预算
每个层级都有上限,超出自动触发降级或终止@Data
@Builder
public class TokenBudget {
// 整个任务的Token预算上限
private int taskMaxTokens;
// 单个步骤(一次LLM调用)的Token上限
private int stepMaxInputTokens;
private int stepMaxOutputTokens;
// 已使用量
private int usedInputTokens;
private int usedOutputTokens;
// 警告阈值(使用到预算的这个比例时发出警告)
private double warningThreshold; // 默认 0.8
/**
* 检查是否还有足够预算
*/
public boolean hasRemainingBudget(int estimatedTokens) {
int total = usedInputTokens + usedOutputTokens;
return (total + estimatedTokens) <= taskMaxTokens;
}
public boolean isNearExhaustion() {
int total = usedInputTokens + usedOutputTokens;
return (double) total / taskMaxTokens >= warningThreshold;
}
public int getRemainingTokens() {
return taskMaxTokens - usedInputTokens - usedOutputTokens;
}
}在Agent主循环中集成预算管理
@Service
@Slf4j
@RequiredArgsConstructor
public class BudgetAwareAgent {
private final ChatClient chatClient;
private final TokenCounter tokenCounter;
// 不同任务类型的默认预算配置
private static final Map<TaskType, TokenBudget> DEFAULT_BUDGETS = Map.of(
TaskType.SIMPLE_QUERY, TokenBudget.builder().taskMaxTokens(5_000).build(),
TaskType.ANALYSIS, TokenBudget.builder().taskMaxTokens(20_000).build(),
TaskType.REPORT_GENERATION, TokenBudget.builder().taskMaxTokens(50_000).build()
);
public AgentResult run(String query, List<AgentTool> tools,
TokenBudget budget, TaskType taskType) {
if (budget == null) {
budget = DEFAULT_BUDGETS.getOrDefault(taskType,
TokenBudget.builder().taskMaxTokens(10_000).build());
}
List<ChatMessage> messages = initMessages(query, tools);
List<AgentStep> steps = new ArrayList<>();
for (int i = 0; i < 10; i++) {
// ★ 预算检查 ★
int estimatedNextCallTokens = estimateNextCallTokens(messages);
if (!budget.hasRemainingBudget(estimatedNextCallTokens)) {
log.warn("Token预算即将耗尽,强制终止任务");
return AgentResult.builder()
.succeeded(false)
.failureReason("Token预算不足,任务被终止。已完成 " + steps.size() + " 步。")
.steps(steps)
.build();
}
if (budget.isNearExhaustion()) {
log.warn("Token预算使用已达{}%,切换到节省模式",
(int)(budget.getWarningThreshold() * 100));
// 切换到节省模式:压缩消息历史,减少上下文长度
messages = compressMessages(messages);
}
// 实际调用LLM
String response = callLlm(messages);
// 更新Token使用量
TokenUsage usage = tokenCounter.count(messages, response);
budget.setUsedInputTokens(budget.getUsedInputTokens() + usage.getInputTokens());
budget.setUsedOutputTokens(budget.getUsedOutputTokens() + usage.getOutputTokens());
// 解析步骤
AgentStep step = parseStep(response);
if (step.getFinalAnswer() != null) {
steps.add(step);
return AgentResult.builder()
.finalAnswer(step.getFinalAnswer())
.steps(steps)
.tokensUsed(budget.getUsedInputTokens() + budget.getUsedOutputTokens())
.succeeded(true)
.build();
}
String observation = executeTool(step, tools);
step.setObservation(observation);
steps.add(step);
messages.add(new AssistantMessage(response));
messages.add(new UserMessage("Observation: " + observation));
}
return AgentResult.builder()
.succeeded(false)
.failureReason("超过最大迭代次数")
.steps(steps)
.build();
}
/**
* 压缩消息历史,减少Token消耗
* 保留System Prompt + 最近4条消息 + 摘要
*/
private List<ChatMessage> compressMessages(List<ChatMessage> messages) {
if (messages.size() <= 6) return messages;
// 保留系统提示
ChatMessage systemMsg = messages.get(0);
// 保留最近4条消息
List<ChatMessage> recentMessages = messages.subList(messages.size() - 4, messages.size());
// 中间消息生成摘要
List<ChatMessage> middleMessages = messages.subList(1, messages.size() - 4);
String summary = "【之前步骤摘要:已执行了 " + (middleMessages.size() / 2) +
" 步工具调用,但因预算压缩省略了详情】";
List<ChatMessage> compressed = new ArrayList<>();
compressed.add(systemMsg);
compressed.add(new UserMessage(summary));
compressed.addAll(recentMessages);
log.debug("消息压缩: {} -> {} 条", messages.size(), compressed.size());
return compressed;
}
private int estimateNextCallTokens(List<ChatMessage> messages) {
// 简单估算:当前消息总长度 × 1.3(预留输出空间)
int totalChars = messages.stream()
.mapToInt(m -> m.getContent().length())
.sum();
return (int)(totalChars / 3.5 * 1.3); // 约3.5字符=1 Token(中文)
}
}按用户等级配置不同预算
不同用户等级给不同的Token预算:
@Service
@RequiredArgsConstructor
public class TokenBudgetService {
public TokenBudget getBudgetForUser(String userId, TaskType taskType) {
UserTier tier = userService.getUserTier(userId);
return switch (tier) {
case FREE -> TokenBudget.builder()
.taskMaxTokens(3_000)
.stepMaxInputTokens(1_000)
.stepMaxOutputTokens(500)
.warningThreshold(0.7)
.build();
case BASIC -> TokenBudget.builder()
.taskMaxTokens(10_000)
.stepMaxInputTokens(3_000)
.stepMaxOutputTokens(1_000)
.warningThreshold(0.8)
.build();
case PRO -> TokenBudget.builder()
.taskMaxTokens(50_000)
.stepMaxInputTokens(10_000)
.stepMaxOutputTokens(4_000)
.warningThreshold(0.85)
.build();
case ENTERPRISE -> TokenBudget.builder()
.taskMaxTokens(200_000)
.stepMaxInputTokens(30_000)
.stepMaxOutputTokens(10_000)
.warningThreshold(0.9)
.build();
};
}
}引入Token预算管理后,当月的API账单降低了62%,主要是消灭了那些"无限循环"的任务。而且对用户来说,免费用户有预算限制,付费用户有更大空间,这本身也是产品分层的依据。
