资讯动态

AgentLedger:AI Agent 花费控制层,如何在调用前拦截超支

发布时间:2026/10/27 16:07:45 来源:尧图企业网站定制
1. 从一次“跑飞的账单”说起AgentLedger 到底想解决什么问题做 AI Agent 应用的人大概都经历过这种心跳时刻凌晨两点你写的一个自动化 Agent 正在后台勤勤恳恳地调用各种工具你以为它只是查几条数据、生成一份报告结果第二天早上打开账单发现它在一个循环里把某个付费接口调用了四万七千次。钱已经花出去了调用已经发生了你唯一能做的就是对着账单发呆。AgentLedger 这个项目标题里最扎眼的一句话是“a spend cap that refuses the call before it happens”——一个在调用发生之前就拒绝它的花费上限。注意这个措辞它不是“事后告警”不是“超额通知”而是在调用真正发出去之前就把它拦下来。这个区别非常关键也是我想在这篇博文里重点拆解的东西。先把这个项目是什么讲清楚。AgentLedger 是一个面向 AI Agent 场景的花费控制层它工作在 Agent 和外部工具/模型服务之间充当一个“预算守门人”的角色。每当 Agent 要发起一次可能产生费用的调用比如调用大模型 API、调用某个按次计费的第三方工具、触发一次 MCP 工具调用AgentLedger 会先做一次预算检查这次调用预计要花多少钱当前会话/任务/时间窗口内已经花了多少如果这次调用会让总额突破预设的上限它就直接拒绝调用根本不会发出去。它解决的核心问题是Agent 的自主性和成本可控性之间的矛盾。传统做法是给 API Key 设一个总额度或者用云厂商的预算告警但这些手段要么粒度太粗整个账号一个上限要么反应太慢告警来了钱已经花了。AgentLedger 把控制点前移到了“调用发生之前”这个瞬间并且把预算粒度细化到了单个 Agent、单个任务、单个会话的级别。这个内容适合谁来参考三类人最应该看一是正在做 Agent 产品、被成本问题折磨的开发者二是负责 AI 基础设施、需要给多个团队做成本治理的平台工程师三是对 MCP 生态感兴趣、想知道工具调用链路里还能插入哪些控制点的人。哪怕你现在还没被账单吓到过提前把这套思路装进脑子里早晚用得上。2. 为什么“事前拒绝”比“事后告警”难得多2.1 事后告警的天然缺陷大部分团队的成本控制是从告警开始的。云厂商给你一个预算功能你设一个月度上限超过 80% 发邮件超过 100% 再发一封。这套机制在传统业务里够用因为传统业务的调用量是可预测的流量曲线是平滑的。但 Agent 场景完全不是这样。Agent 的调用模式有三个特点突发性、递归性、不可预测性。突发性是指它可能在一个推理步骤里连续发起几十次工具调用递归性是指它可能因为一个子任务失败而不断重试重试又触发新的调用不可预测性是指同样一个用户请求Agent 走的路径可能完全不同花费可能差几十倍。在这种模式下事后告警基本等于“事后验尸”——你知道了死因但人已经没了。我见过一个真实的案例某个团队做了一个自动化的竞品分析 Agent用户输入一个公司名Agent 会去搜索、抓取、总结。测试阶段一切正常每次任务花费大概几毛钱。上线第一天有个用户输入了一个特别冷门的公司名Agent 搜不到结果于是它开始换关键词重试换了十几次之后开始尝试不同的搜索工具每个工具都调一遍最后陷入了一个“搜索-失败-换工具-再搜索”的循环。等团队发现的时候这个用户的任务已经烧掉了几百块。事后告警在凌晨三点发出来没人看。2.2 事前拒绝需要解决的技术难题“在调用发生之前拒绝”听起来简单做起来要解决好几个硬问题。第一个问题是成本预估。你要在调用发出之前知道它大概要花多少钱。对于大模型 API这个预估相对可行因为你可以根据输入 token 数估算输入成本再根据 max_tokens 参数估算输出成本上限。但对于第三方工具尤其是那些按次计费、按结果计费、或者计费规则不透明的工具预估就难得多。AgentLedger 的应对策略是维护一个成本模型注册表每个工具在接入时都要声明自己的计费方式固定费用、按 token、按调用次数、按返回数据量等等。对于无法精确预估的就按最坏情况估算宁可保守也不放过。第二个问题是拦截点的位置。你必须在调用真正发出去的那个环节做拦截而不是在更上层。如果拦截点在 Agent 的规划层Agent 可能绕过它直接调用工具如果拦截点在网络层你可能拿不到足够的上下文来判断这次调用属于哪个任务、哪个预算。AgentLedger 选择把自己做成一个代理层所有工具调用都必须经过它它才有机会在最后一刻说“不”。第三个问题是状态管理。预算是有状态的你要知道当前已经花了多少。这个状态必须是强一致的不能出现两个并发调用都以为自己还有预算、结果双双放行的情况。AgentLedger 用了一个中心化的账本这也是它名字里 Ledger 的由来每次调用前先做一次原子性的预算预留调用完成后再根据实际花费做结算多退少补。2.3 和 MCP 生态的关系最近 MCP 这个词热度很高很多人问 MCP 是什么。简单说MCP 是一套让 AI 应用和外部工具/数据源对接的协议标准你可以把它理解成“AI 世界的 USB 接口”。Agent 通过 MCP 连接到各种工具服务器然后调用工具。这个生态正在快速膨胀从数据库查询到地图服务从设计工具到调试工具都在往 MCP 上靠。这对 AgentLedger 来说既是机会也是挑战。机会在于MCP 提供了一个统一的调用入口AgentLedger 只需要在 MCP 客户端和 MCP 服务器之间插一层就能覆盖所有走 MCP 的工具调用。挑战在于MCP 工具的种类太多计费方式千差万别成本模型注册表要维护的内容会非常庞杂。而且 MCP 本身还在演进协议层面的变化可能会影响拦截的实现方式。标题里还出现了 402 和 x402 这两个词。402 是 HTTP 状态码里预留的“Payment Required”一直没被正式使用。x402 则是围绕这个状态码衍生出来的一套思路核心想法是让付费成为 HTTP 请求的一等公民——请求方在发起请求时就携带支付凭证服务方验证后返回内容。这个思路和 AgentLedger 的“事前拒绝”在哲学上是一致的把付费判断前移到请求发生之前。如果未来 x402 这类机制普及AgentLedger 的拦截逻辑可能会和支付协议深度结合预算检查直接变成支付能力检查。3. AgentLedger 的核心架构与关键组件拆解3.1 整体架构三层结构AgentLedger 的架构可以拆成三层我用一个生活化的类比来解释把它想象成公司里的报销制度。最上面是策略层相当于公司的报销政策。它定义了各种预算规则每个 Agent 每天最多花多少、每个任务最多花多少、哪些工具需要审批、超额时是拒绝还是降级。这一层是配置驱动的管理员通过配置文件或管理界面来设定。中间是决策层相当于财务审核员。每次调用请求过来它负责查政策、算成本、看余额、做决定。这一层是 AgentLedger 的核心逻辑所在也是性能瓶颈最可能出现的地方因为每次调用都要过一遍。最下面是账本层相当于公司的账本。它记录每一笔花费维护各个维度的余额提供原子性的预留和结算操作。这一层对一致性的要求最高通常需要一个支持事务的存储后端。三层之间通过明确定义的接口通信策略层的变化不需要改动账本层决策层的逻辑可以独立测试。这种分层的好处是当计费规则变化时你只需要更新策略配置当存储性能不够时你只需要替换账本层的实现。3.2 成本模型注册表让每个工具“自报身价”成本模型注册表是 AgentLedger 能做事前预估的基础。每个接入的工具都要在这里注册声明自己的计费方式。我把它支持的计费类型整理成一张表计费类型说明预估方式典型场景固定费用每次调用固定价格直接取固定值按次计费的 API按输入 token根据输入长度计费统计输入 token 数乘以单价大模型输入按输出 token根据输出长度计费用 max_tokens 上限估算大模型输出按调用次数阶梯计价查当前阶梯乘以单价批量接口按数据量根据返回数据大小计费用预估上限估算数据下载服务复合计费多种方式叠加各项相加大多数大模型 API对于大模型 API预估逻辑是这样的输入成本 输入 token 数 × 输入单价输出成本 max_tokens × 输出单价总预估 输入成本 输出成本。这里的关键是用 max_tokens 而不是期望输出长度因为你要防的是最坏情况。如果 Agent 没有设置 max_tokensAgentLedger 会强制要求设置一个默认上限否则拒绝调用。这个设计有点霸道但非常必要——没有上限的调用就是没有上限的风险。对于无法精确预估的工具注册表允许声明一个“预估上限”实际结算时如果花费低于预估差额会退回账本。这种“先按最坏情况预留再按实际情况结算”的模式和信用卡的预授权机制是一样的。3.3 账本层原子性预留与结算账本层要解决的核心问题是并发安全。假设一个任务有 10 块钱预算当前已经花了 9 块同时来了两个调用每个预估 1 块。如果没有原子性保证两个调用可能都看到“还剩 1 块”然后双双放行结果花了 11 块超了。AgentLedger 的做法是预留-结算两阶段。调用发起前先做一次原子性的预留操作检查余额是否足够如果足够就扣减预留金额返回一个预留凭证。调用完成后用实际花费做结算如果实际花费小于预留退回差额如果大于预留比如输出 token 超了预估从账本里补扣如果补扣时余额不足这笔超支会被记录为“欠款”影响后续调用的放行。这个机制的关键在于预留操作的原子性。在关系型数据库里可以用SELECT ... FOR UPDATE加事务来实现在 Redis 里可以用 Lua 脚本保证原子性。选哪种取决于你的并发量和一致性要求。如果并发量不高关系型数据库更简单可靠如果并发量很大Redis 的性能更好但要处理好持久化问题。注意预留金额的粒度要合理。如果预留太粗比如每次都预留一个很大的固定值会导致预算利用率低很多调用明明花不了那么多却被拦下。如果预留太细又会导致预留操作过于频繁增加账本层压力。我的经验是按工具类型设置不同的预留策略大模型调用按 max_tokens 预估固定费用工具直接按固定值预留。3.4 拦截器最后一刻的守门人拦截器是 AgentLedger 和外部世界交互的边界。它需要做到两件事识别出哪些调用需要拦截以及在不破坏原有调用语义的前提下拦截。识别哪些调用需要拦截靠的是调用元数据。每次调用都会携带一些上下文信息是哪个 Agent 发起的、属于哪个任务、调用的是哪个工具、传了什么参数。拦截器根据这些信息去策略层查询适用的规则。这里有个设计难点上下文信息怎么传递如果 Agent 框架本身不支持传递这些信息拦截器就拿不到足够的上下文。AgentLedger 的解决方案是提供一个轻量级的 SDKAgent 在发起调用时通过 SDK 注入上下文SDK 负责把上下文编码到调用请求的元数据里。不破坏原有调用语义意味着拦截器对上层 Agent 和下层工具都应该是透明的。Agent 不需要知道 AgentLedger 的存在它只是正常发起调用工具也不需要知道它只是正常接收请求。拦截器在中间做检查通过了就转发没通过就返回一个标准的拒绝响应。这个拒绝响应要设计得合理让 Agent 能够理解“这次调用被拒绝了原因是预算不足”而不是把它当成一个普通的调用失败去重试。如果 Agent 把预算拒绝当成网络错误去重试那就白拦了。4. 实操落地从零搭建一个 AgentLedger 式的预算控制层4.1 环境准备与技术选型要复现一套类似 AgentLedger 的机制你需要准备这些东西。存储层我推荐 PostgreSQL因为它的事务支持完善做原子性预留很自然而且最近社区里关于 PostgreSQL 的 skill 和 MCP 集成讨论很多生态在变好。如果你追求极致性能可以用 Redis 做预留层、PostgreSQL 做持久化账本两层配合。服务层用 Python 或 Node.js 都可以取决于你的 Agent 技术栈。Python 的话用 FastAPI 写拦截服务Node.js 的话用 Express 或 Fastify。我个人的偏好是 Python因为 Agent 生态里 Python 的库更全调试起来也方便。Agent 框架这边不管你用的是哪种框架关键是要能在工具调用链路上插入一个中间层。如果你的框架支持自定义工具包装器那就把 AgentLedger 的检查逻辑包在工具外面如果不支持那就得在更底层做比如包装 HTTP 客户端。4.2 数据库表结构设计账本层的表结构是整个系统的地基设计不好后面会很痛苦。我建议至少设计这几张表-- 预算账户表每个 Agent、每个任务、每个时间窗口一个账户 CREATE TABLE budget_accounts ( id BIGSERIAL PRIMARY KEY, scope_type VARCHAR(32) NOT NULL, -- agent / task / session / global scope_id VARCHAR(128) NOT NULL, period VARCHAR(32) NOT NULL, -- daily / monthly / total limit_amount NUMERIC(18, 6) NOT NULL, reserved_amount NUMERIC(18, 6) NOT NULL DEFAULT 0, spent_amount NUMERIC(18, 6) NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (scope_type, scope_id, period) ); -- 预留记录表每次预留一条用于结算和审计 CREATE TABLE reservations ( id BIGSERIAL PRIMARY KEY, account_id BIGINT NOT NULL REFERENCES budget_accounts(id), reservation_token VARCHAR(64) NOT NULL UNIQUE, estimated_amount NUMERIC(18, 6) NOT NULL, actual_amount NUMERIC(18, 6), status VARCHAR(16) NOT NULL DEFAULT reserved, -- reserved / settled / expired tool_name VARCHAR(128) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), settled_at TIMESTAMPTZ ); -- 成本模型表每个工具一条 CREATE TABLE cost_models ( id BIGSERIAL PRIMARY KEY, tool_name VARCHAR(128) NOT NULL UNIQUE, billing_type VARCHAR(32) NOT NULL, unit_price NUMERIC(18, 8) NOT NULL, config JSONB NOT NULL DEFAULT {}, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );budget_accounts表里的reserved_amount和spent_amount是分开的这样你能清楚地看到“已经花掉的”和“预留中还没结算的”。可用余额 limit_amount - reserved_amount - spent_amount。预留操作就是检查可用余额是否大于等于预估金额如果是就增加 reserved_amount。4.3 预留操作的原子性实现预留操作是整个系统里最需要保证原子性的地方。用 PostgreSQL 实现的话核心 SQL 是这样的BEGIN; -- 锁定账户行防止并发修改 SELECT id, limit_amount, reserved_amount, spent_amount FROM budget_accounts WHERE scope_type $1 AND scope_id $2 AND period $3 FOR UPDATE; -- 在应用层计算可用余额判断是否足够 -- 如果足够执行更新 UPDATE budget_accounts SET reserved_amount reserved_amount $4, updated_at now() WHERE id $5; -- 插入预留记录 INSERT INTO reservations (account_id, reservation_token, estimated_amount, tool_name) VALUES ($5, $6, $4, $7); COMMIT;FOR UPDATE是关键它会给这一行加排他锁其他并发事务必须等待。这样就能保证同一时刻只有一个事务在检查和修改余额。这个方案的性能瓶颈在于锁竞争如果同一个账户的并发调用很多会排队。缓解办法是给账户分片比如按任务 ID 哈希到不同的账户行但这样会让预算的全局视图变复杂。我的建议是先用简单方案等真的遇到性能问题再优化。4.4 拦截器的实现与集成拦截器的核心逻辑是一个函数接收调用请求返回放行或拒绝。伪代码大概长这样def intercept_call(agent_id, task_id, tool_name, params): # 1. 查成本模型 cost_model get_cost_model(tool_name) if cost_model is None: # 未注册的工具按策略决定是拒绝还是用默认模型 if policy.reject_unregistered: return Reject(tool not registered) cost_model policy.default_cost_model # 2. 预估成本 estimated estimate_cost(cost_model, params) # 3. 查适用的预算账户 accounts find_applicable_accounts(agent_id, task_id) if not accounts: return Reject(no budget account) # 4. 逐个账户做预留 reservations [] for account in accounts: result try_reserve(account, estimated, tool_name) if not result.success: # 回滚已成功的预留 for r in reservations: rollback_reservation(r) return Reject(fbudget exceeded on {account.scope_type}) reservations.append(result) # 5. 放行返回预留凭证 return Allow(reservations)这个逻辑里有个细节值得说多个账户的预留要么全成功要么全失败。因为一个调用可能同时受 Agent 级预算和任务级预算约束两个都要检查。如果 Agent 级通过了但任务级没通过Agent 级的预留必须回滚否则会白白占用预算。这就是为什么代码里有个回滚循环。集成到 Agent 框架时最干净的方式是包装工具调用函数。以 Python 为例如果你用的是函数调用模式可以写一个装饰器def with_budget_check(func): functools.wraps(func) def wrapper(*args, **kwargs): context get_current_context() decision intercept_call( agent_idcontext.agent_id, task_idcontext.task_id, tool_namefunc.__name__, paramskwargs ) if decision.is_rejected: raise BudgetExceededError(decision.reason) try: result func(*args, **kwargs) settle_reservation(decision.reservations, actual_costestimate_actual(result)) return result except Exception as e: # 调用失败释放预留 release_reservation(decision.reservations) raise return wrapper这个装饰器把预算检查、调用执行、结算/释放三个环节串起来了。注意异常处理分支如果工具调用本身失败了预留要释放因为这次调用没有产生费用或者只产生了部分费用按实际情况处理。4.5 结算与对账结算环节容易被忽视但它决定了账本的准确性。结算时要处理三种情况实际花费等于预估、小于预估、大于预估。等于预估最简单把 reserved_amount 减掉spent_amount 加上预留记录标记为 settled。小于预估时除了上面的操作还要把差额退回——实际上就是 reserved_amount 减去预估、spent_amount 加上实际差额自然就释放了。大于预估时spent_amount 加上实际值如果此时可用余额不够就产生欠款需要记录并在后续调用中优先扣减。对账是定期跑的任务用来发现账本和实际账单的差异。理想情况下AgentLedger 记录的 spent_amount 应该和云厂商账单一致。但现实中会有差异原因可能是计费延迟、汇率波动、或者某些费用没被 AgentLedger 捕获。对账任务的作用就是发现这些差异人工介入排查。我建议对账频率不要太高每天一次就够了因为账单本身也有延迟。5. 常见问题与排查技巧实录5.1 预算被“幽灵调用”吃掉这是最常见的问题之一。你发现某个任务的预算莫名其妙少了很多但查调用记录又找不到对应的调用。原因通常是预留了但没结算。比如 Agent 发起调用后进程崩溃了预留没有释放或者结算逻辑有 bug预留一直挂着。排查思路查 reservations 表里 status 为 reserved 且创建时间超过一定阈值的记录。这些就是“悬挂的预留”。解决办法是加一个过期清理任务定期把超过阈值比如 10 分钟还没结算的预留释放掉。阈值怎么定要大于你系统里最长的一次调用耗时。如果你的工具调用可能跑几分钟阈值就设 15 分钟。实操心得我在预留记录里加了一个expires_at字段创建时就设好过期时间。清理任务只扫这个字段不用做时间计算简单可靠。过期时间按工具类型设置大模型调用设 5 分钟批处理工具设 30 分钟。5.2 预估偏差导致频繁拒绝如果预估总是偏高会导致预算利用率低明明还有钱却调不了。如果预估偏低又会导致频繁超支补扣。这个平衡很难一次调好。我的经验是分工具类型调。大模型调用的预估偏差主要来自 max_tokens 设置如果 Agent 总是把 max_tokens 设得很大但实际输出很短预估就会偏高。解决办法是统计历史调用的实际输出 token 分布用 P95 分位数作为预估依据而不是直接用 max_tokens。对于固定费用工具预估就是精确值不存在偏差。对于按数据量计费的工具预估偏差最大只能靠保守估计加频繁结算来缓解。5.3 并发下的账本不一致前面讲过用FOR UPDATE保证原子性但实际部署时还是可能出问题。最常见的原因是用了连接池但事务没正确提交或回滚。比如代码里抛异常了事务没回滚锁一直占着其他调用全部阻塞。排查这类问题先看数据库的锁等待情况。PostgreSQL 里可以查pg_locks和pg_stat_activity看有没有长时间运行的事务。如果有基本就是代码里事务边界没处理好。解决办法是严格用上下文管理器管理事务确保异常时自动回滚。另一个坑是读写分离。如果你把账本读操作路由到从库可能会读到旧数据导致余额判断错误。账本层的读写必须走主库不能分离。这个原则要写进架构文档里不然新人很容易踩坑。5.4 拒绝响应被 Agent 当成错误重试这个问题的表现是预算明明已经超了但调用量还在涨。原因是 Agent 收到拒绝响应后把它当成了临时错误触发重试逻辑重试又被拒绝又重试形成循环。解决办法是让拒绝响应携带明确的语义。不要返回一个普通的 500 错误而是返回一个特定的错误码和错误信息比如BUDGET_EXCEEDED并且在信息里说明“这是预算限制重试无效”。同时在 Agent 侧的重试逻辑里要把这个错误码加入“不可重试”列表。如果你的 Agent 框架支持自定义错误处理就在这里做文章。问题现象可能原因排查方法解决措施预算莫名减少预留未结算查 reserved 状态记录加过期清理任务频繁拒绝但余额充足预估偏高对比预估与实际用历史 P95 调整预估并发调用阻塞事务未提交查 pg_locks修复事务边界拒绝后调用量仍涨Agent 重试看调用日志明确错误码 禁用重试账本与账单不符计费延迟/遗漏定期对账人工排查差异项5.5 工具未注册时的策略选择新工具接入时如果忘了在成本模型注册表里注册拦截器会面临一个选择拒绝还是放行拒绝的话业务会中断但安全放行的话业务不中断但可能产生未受控的费用。我的建议是默认拒绝但提供白名单机制。对于开发环境可以配置成放行并记录警告对于生产环境必须拒绝。同时注册流程要尽量简单最好能自动化——比如从工具的 OpenAPI 描述里自动提取计费信息减少人工遗漏。6. 从 AgentLedger 延伸出去还能怎么玩AgentLedger 的核心思路是“在花钱之前做判断”这个思路可以延伸到很多地方。第一个延伸方向是性能预算。不只是钱有预算时间也有预算。你可以给每个任务设一个超时上限Agent 在规划下一步时先检查剩余时间如果不够就换更快的方案或者直接返回部分结果。这和花费预算是同一个逻辑只是计量单位从钱变成了秒。第二个延伸方向是调用配额。有些工具不是按钱计费而是按次数限制比如免费额度每天 1000 次。你可以把配额也纳入 AgentLedger 的管理和预算共用一套预留-结算机制只是单位不同。第三个延伸方向是降级策略。当预算不足时不一定非要拒绝也可以降级。比如从 GPT-4 降级到更便宜的模型从精确搜索降级到模糊搜索。这需要在策略层定义降级规则在决策层实现降级逻辑。降级比拒绝复杂但用户体验更好。第四个延伸方向是和 x402 结合。如果未来 x402 这类支付协议普及AgentLedger 可以直接和支付层对接预算检查变成支付能力检查拒绝变成“支付失败”。这样整个链路会更简洁因为支付协议本身就包含了验证和结算的逻辑。我在实际搭建这套机制的过程中最大的体会是预算控制不是一个技术问题而是一个产品问题。你需要和业务方一起决定什么情况下宁可拒绝也不能超支什么情况下可以超支但要告警什么情况下可以降级。这些决策没有标准答案取决于你的业务特性和风险偏好。技术只是把这些决策落地的手段。最后分享一个小技巧在 AgentLedger 的拒绝响应里除了错误码还可以带上“当前余额”和“本次预估花费”。这样 Agent 在收到拒绝后可以据此调整策略——比如换一个更便宜的工具或者把任务拆小。让 Agent 知道“为什么被拒绝”比单纯告诉它“不行”要有用得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑