Agent 的成本预估——执行之前先告诉用户要花多少钱
Agent 的成本预估——执行之前先告诉用户要花多少钱
有一次,一个测试同学在我们的 Agent 系统上随手输了一句话:
"帮我把公司今年所有的销售数据按月份、产品线、地区做一个全面分析,生成详细报告。"
然后他去倒了杯水,回来一看——账单上多了 47 美元。
这个 Agent 很"认真",把"全面分析"理解为遍历了 200 万条记录,调用了 34 次 LLM,生成了一份 15000 字的报告。全程没有任何提示,没有任何确认,就这样把钱花出去了。
这件事之后,我们团队做了一个决定:所有 Agent 任务,执行之前必须先出一个成本预估,让用户确认后才开始执行。
这篇文章讲的就是这套机制怎么设计和实现。
一、为什么成本预估在 Agent 时代特别重要
传统接口调用,成本是可预测的:一次 HTTP 请求多少钱,一次数据库查询多少开销,都是固定的。
Agent 不一样。它的成本取决于:
- 任务复杂度(LLM 要思考几轮)
- 数据量(要处理多少 Token)
- 工具调用次数(每次调用都有成本)
- 模型选择(GPT-4o 是 GPT-3.5 的 20 倍价格)
一个 Agent 任务的成本可以从几分钱到几十美元,跨度极大。而且这个成本通常对用户不透明,用户根本不知道自己提了一个请求会花多少钱。
这带来了几个真实问题:
- 用户随手触发昂贵任务(就像我那个测试同学)
- 月底账单超预算,没人能解释钱花在哪里
- Agent 误判任务范围,把一个本应简单的任务搞得很重
- 无法提前做成本控制,只能事后追查
二、成本的来源分解
在设计预估机制之前,先搞清楚钱花在哪里。
Token 成本
这是最大头。LLM 按 Token 计费,每次调用有 input token 和 output token。
以 GPT-4o 为例(2024 年价格):
- Input: $5 / 1M tokens
- Output: $15 / 1M tokens
一个典型的 Agent 轮次:
- System Prompt: ~500 tokens(输入)
- 用户消息 + 上下文: ~2000 tokens(输入)
- LLM 回复: ~500 tokens(输出)
- 合计:~3000 tokens,约 $0.025
如果 Agent 需要 10 轮对话才能完成任务,光 LLM 调用就是 $0.25。
工具调用成本
有些工具是有成本的:
- 外部 API 调用(如地图 API、数据服务 API)
- 向量检索(按查询次数或数据量计费)
- 文件处理(如 PDF 解析服务)
计算资源成本
长时间运行的 Agent 任务会占用服务器资源,如果是按需付费的云服务,这也是成本的一部分。
三、预估的核心思路
成本预估不需要精确,需要的是量级正确。用户关心的不是"这个任务花 0.23 美元还是 0.27 美元",而是"这个任务是几分钱的量级还是几十美元的量级"。
预估的基本思路:让 LLM 先生成执行计划,然后根据计划估算成本,而不是直接执行。
用户输入任务描述
↓
[规划模式] LLM 生成执行计划(不执行,只规划)
↓
解析执行计划:
- 预计 LLM 调用次数
- 预计 Token 用量
- 预计工具调用次数和类型
↓
成本计算引擎:
- LLM 成本 = 调用次数 × 平均 Token × 单价
- 工具成本 = 各工具调用次数 × 各工具单价
↓
输出预估结果,等待用户确认
↓
[执行模式] 用户确认后,实际执行任务四、完整的代码实现
执行计划数据模型
/**
* Agent 执行计划
* 由 LLM 生成,用于成本预估
*/
@Data
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class AgentExecutionPlan {
/** 任务摘要 */
private String taskSummary;
/** 任务复杂度评估 */
private ComplexityLevel complexity;
/** 计划的执行步骤 */
private List<PlannedStep> steps;
/** LLM 调用预估 */
private LlmCallEstimate llmEstimate;
/** 工具调用预估 */
private List<ToolCallEstimate> toolEstimates;
/** 数据处理量预估 */
private DataProcessingEstimate dataEstimate;
/** 预估执行时间(秒) */
private int estimatedDurationSeconds;
public enum ComplexityLevel {
TRIVIAL, // 极简单,单次 LLM 调用
SIMPLE, // 简单,2-3 次调用
MODERATE, // 中等,5-10 次调用
COMPLEX, // 复杂,10-20 次调用
VERY_COMPLEX // 非常复杂,20+ 次调用
}
}
@Data
@Builder
public class PlannedStep {
private int stepIndex;
private String stepName;
private String description;
private String toolName; // 如果涉及工具调用
private int estimatedTokens; // 本步骤预计消耗 Token
private boolean requiresHumanApproval; // 是否需要人工确认(如删除操作)
}
@Data
@Builder
public class LlmCallEstimate {
private int estimatedCallCount;
private int estimatedInputTokensPerCall;
private int estimatedOutputTokensPerCall;
private int totalEstimatedInputTokens;
private int totalEstimatedOutputTokens;
}
@Data
@Builder
public class ToolCallEstimate {
private String toolName;
private int estimatedCallCount;
private double costPerCall; // 美元
private double totalCost;
}成本预估引擎
/**
* Agent 成本预估引擎
*/
@Service
@Slf4j
public class AgentCostEstimator {
@Autowired
private ChatLanguageModel planningModel;
@Autowired
private ModelPricingConfig pricingConfig;
/**
* 对用户任务进行成本预估
*
* @param userRequest 用户的任务描述
* @param modelId 将要使用的模型
* @param availableTools 可用工具列表
* @return 成本预估结果
*/
public CostEstimationResult estimate(
String userRequest,
String modelId,
List<String> availableTools) {
log.info("开始成本预估,任务:{}", userRequest.substring(0, Math.min(50, userRequest.length())));
// 1. 调用 LLM 生成执行计划(使用更便宜的模型来做规划)
AgentExecutionPlan plan = generateExecutionPlan(userRequest, availableTools);
// 2. 计算 LLM 成本
ModelPricing pricing = pricingConfig.getPricing(modelId);
double llmCost = calculateLlmCost(plan.getLlmEstimate(), pricing);
// 3. 计算工具调用成本
double toolCost = plan.getToolEstimates().stream()
.mapToDouble(ToolCallEstimate::getTotalCost)
.sum();
// 4. 汇总
double totalCost = llmCost + toolCost;
// 5. 构建结果
CostEstimationResult result = CostEstimationResult.builder()
.taskSummary(plan.getTaskSummary())
.complexity(plan.getComplexity())
.estimatedSteps(plan.getSteps().size())
.estimatedDurationSeconds(plan.getEstimatedDurationSeconds())
.llmCallCount(plan.getLlmEstimate().getEstimatedCallCount())
.totalInputTokens(plan.getLlmEstimate().getTotalEstimatedInputTokens())
.totalOutputTokens(plan.getLlmEstimate().getTotalEstimatedOutputTokens())
.llmCostUsd(llmCost)
.toolCostUsd(toolCost)
.totalCostUsd(totalCost)
.costLevel(classifyCostLevel(totalCost))
.warningMessages(generateWarnings(plan, totalCost))
.requiresConfirmation(totalCost > 0.5 || plan.getSteps().stream()
.anyMatch(PlannedStep::isRequiresHumanApproval))
.executionPlan(plan)
.build();
log.info("成本预估完成:总计 ${:.4f},复杂度:{}", totalCost, plan.getComplexity());
return result;
}
/**
* 使用 LLM 生成执行计划
* 这里故意用更便宜的模型(如 gpt-4o-mini)来降低预估成本
*/
private AgentExecutionPlan generateExecutionPlan(String userRequest, List<String> availableTools) {
String planningPrompt = buildPlanningPrompt(userRequest, availableTools);
// 调用规划模型,获取 JSON 格式的执行计划
String planJson = planningModel.generate(planningPrompt).content().text();
try {
return parseExecutionPlan(planJson);
} catch (Exception e) {
log.warn("执行计划解析失败,使用默认估算", e);
return buildDefaultPlan(userRequest);
}
}
private String buildPlanningPrompt(String userRequest, List<String> availableTools) {
return String.format("""
你是一个 AI Agent 规划助手。请分析以下用户请求,生成一个执行计划预估。
用户请求:
%s
可用工具:
%s
请以 JSON 格式返回执行计划,包含以下字段:
- taskSummary: 任务摘要(一句话)
- complexity: 复杂度(TRIVIAL/SIMPLE/MODERATE/COMPLEX/VERY_COMPLEX)
- estimatedDurationSeconds: 预计执行时间(秒)
- steps: 执行步骤列表,每步包含 stepIndex, stepName, description, toolName, estimatedTokens
- llmEstimate: LLM 调用预估,包含 estimatedCallCount, estimatedInputTokensPerCall, estimatedOutputTokensPerCall
- toolEstimates: 工具调用预估列表,每项包含 toolName, estimatedCallCount, costPerCall
注意:这是预估,不需要精确,量级正确即可。
只返回 JSON,不要有其他内容。
""",
userRequest,
String.join("\n", availableTools)
);
}
private double calculateLlmCost(LlmCallEstimate estimate, ModelPricing pricing) {
double inputCost = estimate.getTotalEstimatedInputTokens()
* pricing.getInputPricePerToken();
double outputCost = estimate.getTotalEstimatedOutputTokens()
* pricing.getOutputPricePerToken();
return inputCost + outputCost;
}
private CostLevel classifyCostLevel(double totalCostUsd) {
if (totalCostUsd < 0.01) return CostLevel.NEGLIGIBLE;
if (totalCostUsd < 0.1) return CostLevel.LOW;
if (totalCostUsd < 1.0) return CostLevel.MEDIUM;
if (totalCostUsd < 10.0) return CostLevel.HIGH;
return CostLevel.VERY_HIGH;
}
private List<String> generateWarnings(AgentExecutionPlan plan, double totalCost) {
List<String> warnings = new ArrayList<>();
if (totalCost > 5.0) {
warnings.add(String.format("警告:此任务预计花费 $%.2f,属于高成本操作", totalCost));
}
if (plan.getComplexity() == AgentExecutionPlan.ComplexityLevel.VERY_COMPLEX) {
warnings.add("此任务非常复杂,实际耗时和成本可能超出预估");
}
boolean hasDestructiveOps = plan.getSteps().stream()
.anyMatch(s -> s.getToolName() != null &&
(s.getToolName().contains("delete") || s.getToolName().contains("update")));
if (hasDestructiveOps) {
warnings.add("此任务包含数据修改操作(更新/删除),请确认后执行");
}
return warnings;
}
private AgentExecutionPlan buildDefaultPlan(String userRequest) {
// 简单的默认预估逻辑(当 LLM 规划失败时的 fallback)
return AgentExecutionPlan.builder()
.taskSummary(userRequest.substring(0, Math.min(50, userRequest.length())))
.complexity(AgentExecutionPlan.ComplexityLevel.MODERATE)
.steps(List.of())
.llmEstimate(LlmCallEstimate.builder()
.estimatedCallCount(5)
.estimatedInputTokensPerCall(2000)
.estimatedOutputTokensPerCall(500)
.totalEstimatedInputTokens(10000)
.totalEstimatedOutputTokens(2500)
.build())
.toolEstimates(List.of())
.estimatedDurationSeconds(60)
.build();
}
private AgentExecutionPlan parseExecutionPlan(String json) throws Exception {
return new ObjectMapper().readValue(json, AgentExecutionPlan.class);
}
}用户确认流程
/**
* Agent 任务入口,集成成本预估和用户确认
*/
@RestController
@RequestMapping("/api/agent")
@Slf4j
public class AgentController {
@Autowired
private AgentCostEstimator costEstimator;
@Autowired
private AgentExecutor agentExecutor;
@Autowired
private PendingTaskStore pendingTaskStore;
/**
* 提交任务(先预估,暂不执行)
*/
@PostMapping("/tasks/submit")
public ResponseEntity<TaskEstimationResponse> submitTask(
@RequestBody TaskSubmitRequest request,
@AuthenticationPrincipal UserDetails user) {
// 1. 成本预估
CostEstimationResult estimation = costEstimator.estimate(
request.getTaskDescription(),
request.getModelId(),
request.getAvailableTools()
);
// 2. 生成待确认任务ID
String pendingTaskId = UUID.randomUUID().toString();
// 3. 存储待确认任务(5分钟内有效)
pendingTaskStore.store(pendingTaskId, PendingTask.builder()
.taskId(pendingTaskId)
.userId(user.getUsername())
.originalRequest(request)
.estimation(estimation)
.createdAt(System.currentTimeMillis())
.build(), Duration.ofMinutes(5));
// 4. 返回预估结果,等待用户确认
return ResponseEntity.ok(TaskEstimationResponse.builder()
.pendingTaskId(pendingTaskId)
.taskSummary(estimation.getTaskSummary())
.complexity(estimation.getComplexity().name())
.estimatedSteps(estimation.getEstimatedSteps())
.estimatedDurationSeconds(estimation.getEstimatedDurationSeconds())
.estimatedCost(formatCostBreakdown(estimation))
.warnings(estimation.getWarningMessages())
.requiresConfirmation(estimation.isRequiresConfirmation())
.confirmationDeadline(System.currentTimeMillis() + 300_000) // 5分钟
.build());
}
/**
* 确认执行(用户确认后才真正跑)
*/
@PostMapping("/tasks/{pendingTaskId}/confirm")
public ResponseEntity<TaskExecutionResponse> confirmTask(
@PathVariable String pendingTaskId,
@AuthenticationPrincipal UserDetails user) {
PendingTask pendingTask = pendingTaskStore.get(pendingTaskId);
if (pendingTask == null) {
return ResponseEntity.badRequest().body(
TaskExecutionResponse.builder()
.error("预估已过期或不存在,请重新提交任务")
.build()
);
}
if (!pendingTask.getUserId().equals(user.getUsername())) {
return ResponseEntity.status(403).build();
}
// 清理待确认状态
pendingTaskStore.remove(pendingTaskId);
// 真正执行任务
String executionTaskId = agentExecutor.submitAsync(
pendingTask.getOriginalRequest(),
pendingTask.getEstimation()
);
log.info("任务已确认并开始执行,userId={}, executionTaskId={}",
user.getUsername(), executionTaskId);
return ResponseEntity.accepted().body(
TaskExecutionResponse.builder()
.executionTaskId(executionTaskId)
.message("任务已开始执行,预计 " +
pendingTask.getEstimation().getEstimatedDurationSeconds() + " 秒后完成")
.build()
);
}
/**
* 取消任务(用户看到预估后不想执行)
*/
@DeleteMapping("/tasks/{pendingTaskId}")
public ResponseEntity<Void> cancelTask(@PathVariable String pendingTaskId) {
pendingTaskStore.remove(pendingTaskId);
return ResponseEntity.noContent().build();
}
private Map<String, Object> formatCostBreakdown(CostEstimationResult estimation) {
Map<String, Object> breakdown = new LinkedHashMap<>();
breakdown.put("total", String.format("$%.4f", estimation.getTotalCostUsd()));
breakdown.put("llm", String.format("$%.4f (约%d次LLM调用)",
estimation.getLlmCostUsd(), estimation.getLlmCallCount()));
breakdown.put("tools", String.format("$%.4f", estimation.getToolCostUsd()));
breakdown.put("level", estimation.getCostLevel().name());
return breakdown;
}
}五、Mermaid:执行之前先预估的完整流程
六、真实收益:我们避免了什么
自从上线这套机制,有几个真实案例:
案例一:用户无意中触发了数据导出任务
用户的原始输入:"把我们今年所有数据导出来看看"
预估结果显示:预计调用 28 次 LLM,处理 150 万条记录,预计费用 $18.30,执行时间约 8 分钟。
用户看到之后说:"我就是随便问问,不用真跑。" 取消了任务。
这一次就节省了 $18。
案例二:发现了一个无限循环的设计问题
用户输入:"持续监控我们的订单数据,有问题立刻报警"
预估器在规划阶段就发现这个任务没有终止条件,会无限循环执行。预估的费用是"$∞"。
这暴露了一个产品设计问题——监控类任务需要明确的终止条件或时间限制,而不是让 Agent 无限跑。
案例三:帮用户选择合适的任务描述
用户想分析销售数据,但不确定要"全量分析"还是"采样分析"。
系统对两种方案都做了预估:
- 全量分析:$12.40,40分钟
- 采样分析(10%数据):$1.80,6分钟
用户选择了采样分析,先快速看一下大方向,再决定要不要做全量。
七、预估准确性的维护
预估不可能 100% 准确,但可以持续改进。
关键是记录预估 vs 实际的偏差,定期分析,调整预估模型的参数:
@Service
public class EstimationAccuracyTracker {
/**
* 任务执行完成后,记录实际消耗
*/
public void recordActualCost(
String taskId,
CostEstimationResult estimation,
ActualExecutionMetrics actual) {
double tokenAccuracy = 1.0 - Math.abs(
estimation.getTotalInputTokens() - actual.getActualInputTokens()
) / (double) Math.max(estimation.getTotalInputTokens(), 1);
double costAccuracy = 1.0 - Math.abs(
estimation.getTotalCostUsd() - actual.getActualCostUsd()
) / Math.max(estimation.getTotalCostUsd(), 0.001);
// 记录到数据库,用于后续分析
estimationRecordRepo.save(EstimationRecord.builder()
.taskId(taskId)
.estimatedCost(estimation.getTotalCostUsd())
.actualCost(actual.getActualCostUsd())
.estimatedTokens(estimation.getTotalInputTokens())
.actualTokens(actual.getActualInputTokens())
.tokenAccuracy(tokenAccuracy)
.costAccuracy(costAccuracy)
.taskType(estimation.getComplexity().name())
.recordedAt(System.currentTimeMillis())
.build());
}
}八、总结
那个 47 美元的事故,本质上是一个设计问题:Agent 系统没有给用户提供任何成本感知的机会。
实现成本预估的核心逻辑其实不复杂:
- 先规划,后执行——用 LLM 生成执行计划而不是直接执行
- 量级正确即可——不需要精确到小数点后四位,用户需要的是"大概花多少"
- 按成本等级决定是否需要确认——低成本任务无需打扰用户,高成本任务必须确认
这套机制最大的价值不是省钱,而是让用户对 AI 系统有掌控感。当用户知道"这个任务大概花多少钱、需要多少时间"时,他们对 AI 系统的信任度会显著提升。
