# Agent 可靠执行实战（九）：钱还没扣，预算怎么已经不够了？

> 用 Python 标准库、SQLite 与真实并发进程，建立金额预留与最终账单对账协议；验证超时不释放、重复结算不退款、崩溃恢复和超额账单冻结，并说明应用预算不能替代供应商硬限额。

- 作者：芝士AI吃鱼
- 发布日期：2026-10-17
- 主题：Agent、AI工程、预算控制、SQLite
- HTML 正文：[https://ai-knowledgepoints.cn/blog/agent-reliability-09-monetary-budget-reservations](https://ai-knowledgepoints.cn/blog/agent-reliability-09-monetary-budget-reservations)
- Markdown 永久链接：[https://ai-knowledgepoints.cn/blog/agent-reliability-09-monetary-budget-reservations/index.html.md](https://ai-knowledgepoints.cn/blog/agent-reliability-09-monetary-budget-reservations/index.html.md)

引用本文时，请注明作者与 HTML 正文链接。Markdown 版本用于机器阅读，与 HTML 正文共享同一内容来源。

## 正文

预算只剩一美元，两个 Agent 同时站到收银台前，各要买六十美分的服务。它们都看了一眼余额，都觉得够，都下了单。没有谁算错减法，账却多出了二十美分。

这是假设场景。真正让人头疼的还在后面：第一笔请求超时了。执行器非常体贴地把六十美分退回可用额度，第二笔顺利出发。几分钟后，第一笔账单也来了。

[第五篇](https://ai-knowledgepoints.cn/blog/agent-reliability-05-durable-dispatch-reservations)讲“调用券”不能靠重启补满，[第八篇](https://ai-knowledgepoints.cn/blog/agent-reliability-08-intent-bound-approvals)讲“同意”必须绑定具体动作。这一篇把收银台上的问题补齐：**金额不等于次数；预留不等于扣款；没收到扣款消息，也不等于没有欠款。**

2026 年 10 月 17 日核对的 [Composio Instant Tools 文档](https://docs.composio.dev/docs/instant-tools)明确说明：Session 没有内置金额上限，异步余额检查也不是硬限额。它要求应用预留可信费用上界，并保留未查清的调用占用。本文以此为问题入口，没有调用模型，不接入 Composio，不调用付费服务，也不测量其扣费行为。以下数据库、并发进程与退出故障是真实执行；价格、账单和供应商行为全部是合成测试输入。

## 1. 账本先记“可能要付多少”，再记“最终付了多少”

假设一项任务被允许使用 100 美分。我们只处理 USD 整数美分，省去浮点四舍五入，也拒绝布尔值冒充整数。真实 API 如果按百万分之一美元计价，应选足够细的固定整数单位，或者采用精确定点规则；不能先把单次价格向下取整成零，再声称预算受控。本文没有实现币种兑换、税费、价格阶梯或多币种账户。

每个调用只有三种账本状态。reserved 表示已占住额度，还没有领取派发机会；unknown 表示机会已领取，费用尚未核清；settled 表示已接收可信的最终账单。unknown 刻意覆盖“领取后尚未发出去”和“已经做完但没有回音”两种情况，因为本地数据库不能替远端作证。

定义当前占用 exposure：已结算调用使用最终费用，所有未结算调用使用事先确定的最大费用，再把它们相加。可用额度就是 cap 减去 exposure。这不是现金余额，也不是银行冻结，而是执行器自己的保守风险账。

```python
# 未结算保留 maximum；结算后改记 charged。
def exposure(c):
    return sum(row[0] for row in c.execute(
        'SELECT CASE WHEN state="settled" THEN charged ELSE maximum END FROM call'))
```

上式使用 Python 整数累加，避免账本记录异常高费用后，SQLite 有界整数 SUM 溢出遮住真实异常。单项金额与 cap 都限制在 10 的 12 次方美分以内；这只是样例输入范围，不是建议给 Agent 配置如此大的预算。

假设预留 60，最终账单为 20。对账后只释放差额 40，历史已花的 20 继续占预算。再接一笔上界 80 的调用，整体占用恰好达到 100。如果把整张预留全部删掉，就把“已经花出去的 20”也从任务预算里擦掉了。

![合成预算100美分；预留60后可用40；最终费用20后可用80；没有最终账单仍占60](https://ai-knowledgepoints.cn/images/articles/agent-reliability-09/exposure.svg)

合成金额示例，单位为 USD 美分。预留记录潜在支出，最终账单只释放未使用的差额。

[查看竖版大图](https://ai-knowledgepoints.cn/images/articles/agent-reliability-09/exposure-mobile.svg)

## 2. 上界不是模型猜出来的价格

整个协议最容易被偷换的词，是 maximum。模型说“大概三毛钱”，不是可信上界。历史平均值、厂商介绍文字、一次成功请求的账单，也不自动成为下一次调用的最大费用。

要有严格金额约束，上界必须覆盖本次被允许的全部收费因素：工具、参数、使用量限制、币种、税费或附加费用，以及价格版本。若供应商只承诺按实际用量计费，而执行器无法限制用量，就没有可以拿来证明限额的上界。这个调用应被拒绝，或者明确转入另一个由人承担风险的流程，不能悄悄降级成“尽量别超”。

样例只从“可信执行器已经验证上界与批准”之后开始。reserve 接受 tool、arguments、maximum、quote，并把它们与固定币种一起规范化存储，绑定操作 ID。同一 ID 同一内容重复预留返回 existing；任意内容变化返回 conflict。quote 是合成报价版本标识，不是数字签名，不会自行验证价格是否真实。

这份代码没有真人审批、身份认证、供应商报价认证，也没有价格有效期实现。生产中不能把 reserve 暴露给模型，让它自己填写 maximum。若报价只在有限时间内有效，必须在实际派发路径上验证有效期；过期后不能继续沿用旧上界。第八篇的内容绑定是可组合的前置条件，但本文没有把两篇代码拼成一个完整授权系统。

arguments 限定为字符串键和值的字典，不假装实现任意 JSON 的跨语言规范化。执行器保存完整规范化内容与 SHA-256 摘要；摘要用来检查一致性，不证明批准者身份。一个数据库对应一个预算范围，操作 ID、账单证据 ID 也在该范围内解释。把同一任务写进两个独立数据库，会获得两份预算，代码不会替你跨库合账。

## 3. “够不够”和“占住它”必须同时发生

错误方案的结构很朴素：先读 exposure，判断足够，稍后写一条预留。并发的两个工作进程都能在写入前看到旧数字。即使每条 INSERT 都是事务，组合起来仍然会穿透预算。

安全入口在同一个 BEGIN IMMEDIATE 事务里读取现有操作、检查冻结标记、计算占用，并插入预留。按照 [SQLite 事务文档](https://www.sqlite.org/lang_transaction.html)，单库只有一个同时进行的写事务；IMMEDIATE 在事务起点尝试取得写事务，而不是等到最后插入时才升级。锁等待超时会报错，不能把数据库错误解释成“额度充足”。

```python
with transaction(path) as c:
    # 先处理同一操作的重放与冲突，再检查新的准入。
    cap, frozen = c.execute('SELECT cap,frozen FROM wallet').fetchone()
    if frozen:
        return 'frozen'
    if exposure(c) + maximum > cap:
        return 'budget_exceeded'
    # 在这个事务里插入 reserved；提交以后才能领取派发机会。
```

实际并发实验启动 8 个独立进程，在 Barrier 同步后各申请 30 美分。cap 为 100。运行结果是 3 次 reserved、5 次 budget_exceeded，占用 90。赢家的操作 ID 取决于调度，不应该写成固定黄金结果；results.json 只固定结果计数与总占用。这不是吞吐测试，没有延迟结论。

另一个 unsafe_check_then_spend 对照只记录确定性的错误交错：两次读到 100，各花 60，总计 120。它是合成调度算例，没有把两笔真实外部扣款伪装成实验。真正执行的进程竞争发生在安全账本上。

## 4. 先把派发机会用掉，才能伸手叫供应商

仅有预留还不够。若每次恢复都能拿着同一张预留再次调用供应商，它可能承担多个收费动作，原来的单次上界就失效了。

claim 在事务里检查完整意图仍然相同且状态是 reserved，然后先把状态改成 unknown，提交后才返回参数快照。返回快照的执行器最多派发一次；再次 claim 返回 None。样例没有 SDK 重试，也没有任何真实网络调用。生产适配器如果在一个 claim 后偷偷重试三次，每次都可能收费，这个上界证明就不成立。

这里的保守代价很明显：进程在 claim 提交后、网络发送前崩溃，额度仍被占住。恢复进程不能因为“本地没有发出记录”就退款。一次领取不能拿到两次发送机会，但一次领取也可能一次都没发出去。

本实验用真实子进程的 os._exit(73) 在六个位置突然退出。reserve 提交前退出，数据库里没有预留，占用 0；提交后退出，占用 60。claim 提交前退出，恢复后仍能领取；提交后退出，恢复后不能再领，占用仍为 60。对账提交前退出，仍保留 60；对账提交后退出，已变成最终费用 20，重复对账返回 replayed。

这些是单机进程退出和数据库事务恢复证据，不是主机断电、文件系统损坏、网络分区或跨区域容灾实验。账本采用 WAL 与 synchronous=FULL，配置语义参考 [SQLite synchronous 文档](https://www.sqlite.org/pragma.html#pragma_synchronous)，不能把配置名当成完成了所有持久性测试。

![预留提交后单次领取派发机会，unknown保留全额，可信最终账单到达后对账；超上界账单记录真实金额并冻结](https://ai-knowledgepoints.cn/images/articles/agent-reliability-09/settlement.svg)

本地提交不能和供应商扣款原子完成。缺回音保留占用，异常账单保留真相。

[查看竖版大图](https://ai-knowledgepoints.cn/images/articles/agent-reliability-09/settlement-mobile.svg)

## 5. 对账入口比退款按钮更需要谨慎

reconcile 接收 charged 与 evidence。这里的 evidence 只是可信账单校验器提供的测试标识，代码没有验证签名、供应商账号、币种或账单完整性。把外部 HTTP 响应里的一个数字不加校验地塞进来，会直接破坏协议。

必须核实的是这次调用的**最终、完整、不可变费用**。第一条部分账单 20 并不能证明之后没有另一条 40；日志里没有费用字段，也不能证明免费。本文不处理后续调账、退款或分批收费。一旦业务存在这些情况，就需要账单版本、累计收费与重新预留规则，不能复用“结算一次就结束”的状态机。

在这个受限前提下，对账只允许 unknown 转 settled。同一操作、同一最终金额、同一 evidence 再来一次，返回 replayed，不再次释放差额；修改金额或证据返回 conflict。不同操作不能共用同一个 evidence。尚未 claim 的 reserved 不能用一张零元账单直接退款，因为它仍有未消费的派发机会；若以后需要取消，必须把取消和派发资格的撤销做成同一个原子决定，本例故意不提供取消退款入口。

最终确认零费用是允许的，但“确认零”与“什么也没查到”完全不同。测试中 verified_zero_bill 将占用降为 0；unknown_keeps_maximum 保留 60，并拒绝另一笔 60 的重试申请。后者可能显得浪费，可如果真实费用尚未查清，这份浪费换来的正是保守边界。

没有自动按 TTL 释放占用，是有意为之。时间过去十分钟，只能说明十分钟过去了，不能说明供应商不再补账。保留过多 unknown 会让任务停止接单，这是需要运维对账的可用性问题，不能靠把账删掉来伪装修复。

## 6. 一条超额账单，能戳破什么保证

若已结算费用不超过各自预留上界，每次新增预留只在新 exposure 不超过 cap 时提交，那么协议可以归纳地维持 exposure 不超过 cap：领取不改占用，正常结算只减小或保持占用，新预留则先做原子检查。

进一步想把它解释成“外部最终费用也不超预算”，还需要所有收费都经过唯一入口、每个领取最多产生一个受界定的收费动作、供应商遵守上界、账单校验正确且最终费用不再变化。**本地不变量和供应商收费保证不是同一件事。**

因此实验专门保留一个破坏前提的反例：预留 60，供应商夹具却报最终费用 120。reconcile 返回 bound_breached，把真实的 120 写进账本，并冻结新的预留和尚未领取的调用。remaining 变成 -20。不能把 charged 截断到 60，否则面板会绿得很漂亮，欠款却被藏起来。

冻结也无法撤回已经返回给执行器的派发机会。已有调用可能仍在进行，后续账单仍要继续进入对账；测试验证冻结后仍接受另一项已领取调用的正常账单。它不会自动解冻，也没有自动退款动作。需要人工调查上界来源、适配器重试和供应商计费契约，再决定如何恢复。

如果你没有可信上界，或者无法把所有收费路径收进一个执行器，就不要把这个应用账本宣传成硬消费上限。可以做告警、软预算、人工批准，或者选择真正提供强制限额的服务，但这些都是另一种明确承诺。

## 7. 下载后，先看那个不太好看的结果

五个文件放入同一空目录即可运行：[budget.py](https://ai-knowledgepoints.cn/examples/agent-reliability-09/budget.py)、[test_budget.py](https://ai-knowledgepoints.cn/examples/agent-reliability-09/test_budget.py)、[run_experiment.py](https://ai-knowledgepoints.cn/examples/agent-reliability-09/run_experiment.py)、[results.json](https://ai-knowledgepoints.cn/examples/agent-reliability-09/results.json)、[README.md](https://ai-knowledgepoints.cn/examples/agent-reliability-09/README.md)。需要 Python 3.10 或更新版本，无第三方依赖、模型密钥或真实付款。

```bash
python3 test_budget.py
python3 run_experiment.py --check
python3 run_experiment.py --output reproduced.json
```

本次实跑环境为 Linux、Python 3.12.14、SQLite 3.53.1。34 项 unittest 通过，12 组具名场景与完整 results.json 重放一致。测试覆盖整数输入、意图冲突、重复领取、重复结算、证据重用、正常释放、零费用、冻结、真实并发预留和六个进程退出点。Barrier 与进程等待超时只用于防挂死，不是服务性能指标；未做 Windows 验证。

建议先打开 provider_bound_violation：它有一笔超过预算的真实账本记录，当然，账单本身仍是合成输入。一个值得信赖的控制系统，应该在前提被打破时明确承认失败，而不是偷偷修整数字，好让所有断言继续好看。

收银台最重要的能力，不是每次都答应“余额够”。是能说清这笔钱为什么占着、谁有证据把它释放，以及哪一张账单已经超出了原来的承诺。

系列讨论可查看[知识星球](https://ai-knowledgepoints.cn/planet)，或通过[公众号入口](https://ai-knowledgepoints.cn/about#wechat)关注后续。本文、原创图解与样例代码由 AI 辅助制作，并经过执行与内容核验；不构成生产支付、认证、供应商限额或跨服务原子性实现。
