切换主题

Agent 成本控制:模型路由、工具调用预算、缓存和失败重试怎么设计

Easton editorial illustration: agent rollout and rollback rail
7
预算层级
user、tenant、workflow、task、tool、retry、cache。
4
控制动作
route、degrade、pause、abort。
3
缓存类型
prompt prefix cache、business result cache、tool response cache。
数据来源: 本文工程清单基于阶段一官方文档调研整理;价格、折扣、模型可用性发布前需按官方页面复核。

"OpenAI API Pricing"

凌晨 3 点,后台 report Agent 返回空响应。HTTP 状态码是 200,body 却是空的。重试逻辑只检查状态码,于是继续尝试。每次请求发送 500 token 输入,重试 1500 次,烧掉 75 万 token。第二天早上看到账单,血压拉满。

根本原因不是“模型选贵了”,而是三个缺失:熔断器、预算检查、错误分类。Agent 成本失控有三种典型模式:

重试无上限。 失败模式分类缺失,把空响应当成可重试错误。熔断器缺失,连续失败 1500 次也不停。每次重试都重新发送完整上下文,成本放大 2-5 倍。

上下文膨胀。 长任务跑 6 小时后对话历史膨胀到 80K tokens。没有 checkpoint,失败后从头重跑,每一步都重新付费。

模型滥用。 所有任务都用 frontier 模型,没有路由策略。简单分类任务也用最贵模型,浪费 70% 的 token。

Agent 成本控制不是单点优化,而是多层设计:预算对象、路由策略、缓存命中、重试熔断、成本日志和告警阈值。把这六个工程对象拆成判断表或可执行清单。

预算对象设计:记什么、记在哪、怎么熔断

成本控制先要有预算对象,不能只记总数。没有分层,账单异常时你无法定位是哪个用户、哪个任务、哪个工具烧钱。

七层预算对象

预算对象从粗到细分成七层:

预算层级预算对象预算上限建议告警触发条件
Layer 1user每用户每日/每月上限剩余 < 20% 触发告警
Layer 2tenant每租户独立预算池剩余 < 30% 触发告警
Layer 3workflow每类 workflow 独立预算剩余 < 40% 触发告警
Layer 4task每任务类型独立预算剩余 < 50% 触发告警
Layer 5tool每工具调用独立预算超上限跳过工具
Layer 6retry重试次数上限 + 熔断连续失败 N 次停用工具
Layer 7cache缓存命中率监控命中率 < 预期值告警

每层预算上限依赖业务模型,属于易变配置;分层结构本身更稳定。预算剩余字段要写入成本日志,用于告警和熔断判断。

每层要记的字段

每层预算都要记录以下字段:

字段名用途类型必须记录原因
model定位模型string判断模型路由是否合理
inputTokens输入 token 数integer计算输入成本
outputTokens输出 token 数integer输出成本常常高于输入,需独立记录
cachedTokens缓存命中 token 数integer计算缓存节省
costEstimate本次成本估算float实时成本累加
budgetRemaining预算剩余float熔断判断依据

预算对象设计核心是“按维度记账”,不是“只记 total_cost”。多租户场景要按 tenantId 分摊成本,多工具场景要按 toolName 定位黑盒。

熔断逻辑

预算剩余 < 阈值时触发熔断:

def check_budget_before_retry(budget_remaining, retry_cost_estimate):
    if budget_remaining < retry_cost_estimate:
        return "skip_retry"  # 超预算跳过重试
    if budget_remaining < threshold:  # threshold 如 20%
        return "wait_approval"  # 剩余不足,等待审批
    return "continue"

熔断逻辑要检查预算剩余,不是重试后才发现没预算。每次重试前先估算成本,检查是否超预算,避免“空响应重试 1500 次”烧掉整日预算。

关于上下文工程中哪些内容进入稳定 prefix、哪些只放运行时变量,会在同系列的上下文工程文章中展开。

模型路由策略:不是所有任务都需要最贵模型

一个 ticket triage Agent 的实际分布:70% 任务是简单分类,20% 需要起草回复,只有 10% 要发客户邮件前才升级 frontier 模型。路由策略可节省 40-85% 成本。

模型层路由

模型层路由按任务复杂度分级:

任务层级典型任务推荐模型层级占比成本特征
70% - S 级分类、提取、过滤、简单问答nano/flash(最便宜)70%输出短、轮次少、工具调用少
20% - M 级起草、总结、代码生成、中等推理mid-tier(中等价格)20%输出中等长度、可能调用工具
10% - L 级审核、架构设计、复杂推理、多工具协调frontier(最贵)10%输出长、轮次多、工具调用频繁

路由策略三步骤:

第一步:任务分级。 给每个 workflow 定义 S/M/L 三档复杂度标准,包括输出长度、工具调用次数、推理深度和风险级别。

第二步:默认路由到 S 级。 只有命中复杂特征时才升级到 M 或 L 级。

第三步:级联路由。 S 级模型失败时升级到 M 级,M 级失败升级到 L 级,L 级失败进入人工介入。升级前检查预算剩余,超预算时跳过升级。

服务层路由

同一模型也可按 latency priority 分流。折扣比例和完成窗口属于易变事实,发布前复核官方定价。

服务层级成本折扣完成时间适用场景
实时 API无折扣即时响应交互式 Agent 对话、高优先级任务
Batch API50% cost discount(发布前复核)24-hour turnaround(发布前复核)批量 eval、分类、embedding、内容仓库处理
Flex Processing低成本(发布前复核)较慢响应,偶发不可用低优先级异步任务、model evaluations、data enrichment

离线任务路由决策清单:

  • 是否需要即时响应?是 -> 实时 API,走模型路由。
  • 是否可以接受 24 小时延迟?是 -> Batch API。
  • 是否低优先级且可容忍偶发失败?是 -> Flex Processing。
  • 是否批量处理 eval、分类、embedding?是 -> Batch API。

风险分级

任务按风险级别分级,对应不同路由路径:

风险级别典型操作路由策略预算分支
低风险分类、提取、内部总结S 级模型 + 自动路径无审批,预算上限宽松
中风险起草客户回复、代码变更建议M 级模型 + 可选审批超预算触发审批,可继续
高风险发客户邮件、扣款、架构变更L 级模型 + 必审批审批等待、拒绝、超时进入预算分支

关于审批等待、拒绝、超时如何影响预算分支,会在同系列的 Human-in-the-Loop 文章中展开。

关键约束

路由策略的关键约束:

  • 不硬编码价格数字:价格是易变事实,记录 model + pricingVersion,不写死成本公式。
  • 预算剩余检查:升级前检查 budgetRemaining,超预算时跳过升级或进入审批。
  • 错误分类:路由失败要区分可恢复的模型能力不足,以及不可恢复的参数错误、权限拒绝。

工具调用预算:per-tool budget、timeout 和重试上限

工具调用本身有 schema + API 双重成本。每次调用要发送 schema、参数、响应解析,加上外部 API 可能限流或超时,这些开销独立于模型调用。

工具预算控制

每个工具设置独立预算控制:

控制项建议配置监控字段触发动作
Per-tool budget单次调用上限tool_cost_estimate超上限跳过工具或降级处理
Tool timeout外部 API 被动超时tool_duration超时标记为可重试错误
Retry limit per tool单工具重试上限tool_retry_count超上限放弃工具,不进入重试循环

外部 API 用工具,包括搜索、数据库和第三方服务,都要单独记账,避免工具调用成为成本黑盒。

工具失败分类

工具失败分两类:可恢复和不可恢复。

失败类型典型错误处理策略成本影响
可恢复失败网络超时、503 Service Unavailable、429 Rate Limit自动重试,配合指数退避和 retry-after每次重试消耗完整上下文
不可恢复失败403 Permission Denied、400 Bad Request、工具不存在不重试,注入错误让模型判断不重试,避免无效消耗

失败分类的核心是“外部原因导致的临时失败才重试,内部配置错误不重试”。

熔断器

连续失败 N 次后停用工具,避免“空响应重试 1500 次”:

def circuit_breaker_tool(tool_name, consecutive_failures, threshold=5):
    if consecutive_failures >= threshold:
        return "disable_tool"  # 停用工具
    return "continue"

熔断器状态要写入日志,用于排查“为什么工具停用”。熔断后等待人工介入或自动恢复检查,不要反复调用不稳定工具。

关于工具调用基础,已在工具调用文章中展开,本文扩展 per-tool budget、timeout 和重试上限。

Prompt Caching 设计:稳定 prefix、变量位置、1024 token 门槛

Prompt Caching 是稳定 prompt prefix 的输入 token 优化,不是业务结果缓存。它用相同 prompt prefix 路由到近期处理过同前缀的服务器,能降低延迟和输入 token 成本。

Prompt Caching ≠ 结果缓存

Prompt Caching 缓存的是稳定 prompt prefix,不是业务结果。关键区别:

  • Prompt Caching:缓存稳定 prompt prefix,如 system prompt、tool schema;命中时节省输入 token,但仍会重新执行推理。
  • 业务结果缓存:缓存完整输出,如工具调用结果、数据库查询;命中时直接返回,不调用模型。

两者目的不同:Prompt Caching 优化输入 token 成本,业务结果缓存优化完整调用成本。可以同时使用:稳定前缀走 Prompt Caching,高频工具调用结果走业务缓存。

结构要求

Prompt Caching 的关键是区分稳定前缀和运行时变量:

内容类型位置命中缓存可能性典型内容
稳定前缀(进 cache)Prompt 前部System prompt、Tool schema、Policy 文档、Few-shot 示例
运行时变量(不进 cache)Prompt 后部User input、File fragments、Runtime state(当前对话轮次、临时变量)

设计步骤:

  1. 把 System prompt、Tool schema、Policy 放在前部:这些内容在多次调用中稳定不变,容易命中缓存。
  2. 把 User input、File fragments、Runtime state 放在后部:这些内容每次调用都变化,不应该进稳定前缀。
  3. 监控 Cache hit rate:记录 cachedTokens 和总输入 token,计算命中率。Cache hit rate > 40% 为健康,< 20% 需调整前缀结构。

门槛与效果

Prompt Caching 自动启用门槛和效果都属于易变事实,发布前复核官方文档:

  • 门槛:1024 tokens 及以上自动启用(发布前复核)。
  • 效果:命中可降低成本和延迟(发布前复核具体比例)。
  • 检查命中usage.prompt_tokens_details.cached_tokens 字段。

Prompt Caching 的支持模型、门槛、折扣比例都是易变事实,发布前按官方 pricing 页复核。关键设计原则是“静态内容在前,变量在后”,这个结构本身是稳定的。

失败重试熔断:幂等性、状态保存、预算剩余检查

重试是成本失控的最大来源。一个后台 report Agent 卡在不稳定工具上,每次失败都重新发送完整上下文,重试 1500 次后成本会迅速偏离正常路径。

重试前检查预算

重试前检查预算剩余,不是重试后才发现没预算:

def should_retry(error_type, budget_remaining, retry_cost_estimate):
    # 错误分类
    if error_type in ["403", "400", "tool_not_exist"]:
        return False  # 不可恢复错误,不重试

    # 预算检查
    if budget_remaining < retry_cost_estimate:
        return False  # 超预算,不重试

    return True  # 可重试

预算检查要写在重试逻辑之前,避免“空响应重试 1500 次”烧掉整日预算。

幂等性

重试不会重复执行副作用,比如发邮件、扣款:

  • 工具调用用幂等 ID,例如 requestId;外部 API 收到相同 ID 时返回缓存结果,不重复处理。
  • 幂等 ID 要写入成本日志,用于判断是否重复调用。

幂等设计的核心是“同一操作不重复消耗”,避免重试放大成本。

状态保存(Checkpoint)

长任务失败恢复不重跑完整流程:

  • Agent 执行到关键节点时保存 checkpoint,包括已完成步骤、当前状态和上下文摘要。
  • 失败恢复时从 checkpoint 继续,不从头重跑。
  • Checkpoint 要写入持久化存储,不只留在内存。

关于 checkpoint 和 thread state 的设计,已在LangGraph Agent 架构文章中展开。

重试策略

不同错误类型要不同重试策略:

错误类型典型错误重试策略成本影响
网络超时10 秒未响应指数退避 + retry-after,最多 3 次每次重试消耗完整上下文
503/429Service Unavailable、Rate Limit等待限流窗口 + retry-after,最多 3 次等待时间不消耗 token,但重试会消耗
403/400Permission Denied、Bad Request不重试,注入错误让模型判断不重试,避免无效消耗

重试策略核心是“可恢复错误才重试,不可恢复错误不重试”,避免把无效请求反复发给模型。

熔断器

连续失败 N 次后停止重试,等待人工介入:

def circuit_breaker(consecutive_failures, threshold=5):
    if consecutive_failures >= threshold:
        return "stop_retry"  # 停止重试
    return "continue"

熔断器要写入日志,用于排查“为什么停止重试”。熔断后等待人工介入或预算恢复,不要反复调用不稳定工具或模型。

成本日志与告警:记录哪些字段、设置什么阈值

成本可观测性是控制的前提。日志字段不完整就无法定位问题。

OpenTelemetry trace span attributes

成本日志按三层 span 设计:

Agent run span(顶层)

字段名用途类型必须记录原因
runId定位具体执行string区分同一 workflow 的多次执行
tenantId定位租户string多租户场景分摊成本
userId定位用户string按用户统计成本趋势
workflowName定位 workflowstring按 workflow 类型统计成本
totalCost总成本估算float实时成本累加
budgetRemaining预算剩余float熔断判断依据
totalRetries总重试次数integer重试放大成本

Model call span(子 span)

字段名用途类型必须记录原因
model定位模型string判断模型路由是否合理
pricingVersion定价版本string成本公式不写死,记录版本
inputTokens输入 token 数integer计算输入成本
outputTokens输出 token 数integer输出成本需独立记录
cachedTokens缓存命中 token 数integer计算缓存节省
costEstimate本次成本估算float实时成本累加
latencyMs调用延迟integer判断是否走 Batch/Flex

Tool call span(子 span)

字段名用途类型必须记录原因
toolName定位工具string定位工具调用 overhead
toolBudget工具预算上限float熔断判断依据
toolTimeout工具超时时间integer判断是否超时失败
retryCount重试次数integer判断重试放大
errorType错误类型string区分可恢复和不可恢复

不要只记 total_cost,要按维度拆分。没有这些字段,账单异常时你只能看到“超标”,却不知道是哪个用户、哪个工具、哪次重试烧钱。

关于日志、告警、失败恢复的完整设计,已在Agent 监控恢复文章中展开,本文补充成本字段和预算对象。

告警阈值

告警阈值按维度设置:

告警维度告警阈值告警方式触发动作
全局预算消耗70%、90%、100%Slack/邮件告警70% 提示,90% 降级,100% 熔断
单用户/tenant 消耗超过均值 3 倍Slack/邮件告警检查是否有异常调用
单模型调用失败率> 5%Dashboard 告警检查模型路由或服务状态
单工具重试次数> 阈值Dashboard 告警检查工具稳定性
缓存命中率< 预期值Dashboard 告警检查 prompt 结构是否错误

告警阈值要写在成本日志里,用于自动触发告警和熔断。

降级策略

告警触发后的降级路径:

降级路径降级方式适用场景成本影响
模型降级大模型 -> 小模型单模型失败率过高降成本,可能降质量
路径降级实时 API -> Batch API -> Flex Processing全局预算消耗过快延迟增加,成本下降
功能降级关闭非核心工具调用单工具重试次数过高减少工具调用 overhead
用户降级限流、排队、提示“稍后重试”单用户消耗异常避免单用户烧预算

降级策略要写在预算逻辑里,告警触发时自动降级,而不是人工介入后才降级。

下一步推荐

Agent 成本控制不是孤立的,需要监控、工具调用、上下文工程配合:

  • 已发布:Agent 监控恢复:日志字段、告警设置、失败恢复,本文补充成本字段和预算对象。
  • 已发布:LangGraph Agent 架构:checkpoint、thread state、失败恢复,用于说明长任务状态保存。
  • 已发布:工具调用:工具调用基础,本文扩展 per-tool budget、timeout、重试上限。
  • 同系列:上下文工程:稳定 prefix 与缓存命中,哪些上下文进入稳定 prefix,哪些只放运行时变量。
  • 同系列:Human-in-the-Loop:审批等待、拒绝、超时的预算分支,审批如何影响成本和重试路径。

从 Session 级防线开始:设置 per-session cost limit,单次会话超预算自动终止。这是最快见效的第一步,避免一个跑飞的任务烧掉整日预算。后续逐步扩展到预算对象分层、模型路由、工具调用预算、Prompt Caching、重试熔断和成本日志。

设计 Agent 成本预算和熔断机制

用预算对象、模型路由、服务层路由、缓存、工具预算和重试熔断,把 Agent 成本控制前移到每次 run 执行前。

⏱️ 预计耗时: 45 分钟

  1. 1

    步骤 1: 列出所有成本路径

    列出 Agent 的模型调用、工具调用、文件读取、外部 API、批处理、缓存和重试路径。
  2. 2

    步骤 2: 定义预算对象

    按 tenant、user、run、workflow、model、tool、retry、cache 和 time window 记录预算对象。
  3. 3

    步骤 3: 设置路由策略

    为不同任务类型设置模型层路由和服务层路由,区分 online、batch、flex 和 queue。
  4. 4

    步骤 4: 设计缓存命中

    把稳定上下文放入 prompt prefix,变量内容放后面,同时区分 prompt caching、业务结果缓存和工具响应缓存。
  5. 5

    步骤 5: 限制工具和重试

    为每个工具设置 timeout、max retries、idempotency key、per-tool budget 和 fallback。
  6. 6

    步骤 6: 记录成本 span

    在 run/span 上记录 token、cached token、tool、retry、latency、estimated cost、budget remaining 和 traceId。
  7. 7

    步骤 7: 配置降级和熔断

    设置降级、暂停、熔断和告警阈值,并用真实失败案例回归测试。

常见问题

Agent 成本应该按用户、会话、任务还是工具统计?
按多层记账:用户、tenant、workflow、task、tool、retry、cache 七层,每层独立预算和熔断。不能只记 total_cost,要按维度拆分,否则账单异常时无法定位问题。
模型路由是不是只要把简单任务换成小模型?
不只是模型层路由,还要考虑服务层路由(Batch/Flex/实时)和风险分级。同一模型也可按 latency priority 分流,离线任务走 Batch API,低优先级任务走 Flex Processing。
Prompt Caching 和普通业务缓存有什么区别?
Prompt Caching 缓存的是稳定 prompt prefix,如 system prompt、tool schema、policy,不是业务结果。业务结果缓存保存完整输出或工具响应;两者目的不同,可以同时使用。
工具调用失败时重试几次才算合理?
不是固定次数,而是看失败分类、预算剩余和熔断器。可恢复错误最多少量重试;403、400、工具不存在等不可恢复错误不重试,避免无效消耗。
Agent 长任务跑到一半没预算了,应该降级、暂停还是直接失败?
优先暂停并保存 checkpoint,等待预算恢复或人工介入。直接失败会丢失进度,盲目降级可能影响质量;暂停通常是更稳的工程选择。
成本日志应该记录哪些字段?
至少记录 runId、tenantId、workflow、model、input/output/cached tokens、toolName、retryCount、latency、costEstimate、budgetRemaining、decision 和 traceId。

16 分钟阅读 · 发布于: 2026年9月17日

评论

使用 GitHub 账号登录后即可评论

Easton BlogEaston Blog