06-capacity-billing.md 12 KB

路线 6:模型资源与费用

代码依据:2026-09-30 当前工作区。学习场景:多个商家的咨询和记忆提取同时等待模型,平台需要分配并发额度,并保存每次调用的 token 与人民币费用。

图中的后端路径相对 src/zhifu/。连续箭头表示当前进程内的调用或标明条件的执行步骤;间断箭头标注框架回调、网络请求或持久记录之间的联系。框架负责调用 BudgetCallback,阅读时注意调用来源。返回路线目录。

图 6A:工作进程申请并发额度

flowchart TD
    A1["6A-1 modules/agent_runtime/worker.py<br/>main → execute_one<br/>开始一次任务处理"]
    A2["6A-2 modules/agent_runtime/business_wait.py<br/>reconcile<br/>检查业务等待事件"]
    A3["6A-3 modules/agent_runtime/worker.py<br/>claim<br/>开始领取事务"]
    A4["6A-4 modules/agent_runtime/capacity.py<br/>reservations<br/>锁定并发策略并读取商家预留"]
    A5["6A-5 modules/agent_runtime/worker.py<br/>claim 内部<br/>选择租户、重新统计并发、锁定运行"]
    A6["6A-6 modules/agent_runtime/capacity.py<br/>acquire<br/>检查全局、交互、后台与租户预留"]
    A7["Redis state<br/>CAPACITY_SCRIPT 原子更新有效额度"]
    A8["6A-8 modules/agent_runtime/worker.py<br/>claim 内部<br/>RUNNING、lease_token、lease_until"]
    A9["6A-9 modules/agent_runtime/runtime.py<br/>build_graph<br/>由 execute_one 调用 graph.ainvoke"]
    A10["6A-10 modules/agent_runtime/capacity.py<br/>release<br/>execute_one 的 finally 释放额度"]
    A11["本次没有取得任务<br/>execute_one 返回 False"]
    A1 --> A2
    A1 -->|reconcile 返回后| A3
    A3 --> A4
    A4 -->|返回预留配置,继续 claim| A5
    A5 -->|存在有效候选| A6
    A5 -->|没有候选或租户已满| A11
    A6 -.->|Redis eval| A7
    A7 -.->|取得额度| A8
    A7 -.->|额度不足| A11
    A8 -->|领取事务提交后返回运行与租约| A9
    A9 -->|成功、失败或取消后的 finally| A10
    A11 -->|finally| A10

源码导航:

租户调度优先照顾尚未用完预留额度的商家,随后参考上次领取时间;租户内部按照创建时间与运行标识选择任务。claim 取得租户锁后重新检查并发数量。Redis 将全局、交互、后台和租户预留放在一次 Lua 调用内判断。

记忆任务从 memory.worker.execute_one 进入,同样调用 reservations 和 acquire,使用 background 分类;只有成功取得额度才进入 execute_claimed。取得的额度在 finally 释放。Redis 额度还有有效期限,工作进程中断后的记录由后续申请清理。

图 6B:一次模型调用的预占与结算

flowchart TD
    B1["6B-1 modules/agent_runtime/runtime.py<br/>build_graph 内部 model<br/>保存 ModelAttempt,调用 ainvoke"]
    B2["6B-2 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_chat_model_start<br/>估算输入与最大输出"]
    B3["6B-3 modules/agent_runtime/budget.py<br/>reserve 内部依次调用 lock_tenant、account<br/>保存 token 预占"]
    B4["6B-4 modules/billing/ledger.py<br/>reserve_money<br/>按已批准价格保存金额预占"]
    B5["供应商模型服务<br/>实际请求与 usage"]
    B6["6B-6 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_llm_end<br/>读取 usage_metadata"]
    B7["6B-7 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_llm_error<br/>记录未知用量"]
    B8["6B-8 modules/agent_runtime/budget.py<br/>change_entry<br/>结算 token 或保留 UNKNOWN"]
    B9["6B-9 modules/billing/ledger.py<br/>settle_money → charge<br/>结算金额或保留 UNKNOWN"]
    B10["6B-10 modules/agent_runtime/runtime.py<br/>model 返回处理<br/>独立保存 ModelAttempt.usage,继续业务检查"]
    B11["6B-11 modules/agent_runtime/budget.py<br/>mark_pending_unknown<br/>取消或恢复时检查未结算尝试"]
    B12["预占事务终止<br/>预算错误向上传播"]
    B1 -.->|LangChain 请求前回调| B2
    B2 --> B3 --> B4
    B4 -.->|预占事务成功后,SDK 发送 HTTP| B5
    B3 -->|token 不足| B12
    B4 -->|金额不足或缺少已批准价格| B12
    B5 -.->|LangChain 成功回调| B6
    B5 -.->|LangChain 错误回调| B7
    B6 --> B8
    B7 --> B8
    B8 --> B9
    B9 -->|成功回调结束,ainvoke 返回| B10
    B1 -->|外层执行被取消或发生异常| B11
    B11 --> B8

源码导航:

reserve 使用独立事务,在租户锁保护下同时处理 token 与金额。存在 MoneyPolicy 时才启用人民币预算;已启用金额预算却没有对应 BillingRoute 会拒绝调用。价格保存在每次 MoneyEntry.price 中,结算读取该次调用的价格快照。

输入预估使用序列化字节数与协议余量,输出使用配置上限。实际结算使用供应商 usage_metadata,缓存 token 单独参与金额计算。金额使用整数微元,每次请求汇总后向上取整。

没有 usage 时保留预占并登记 UNKNOWN。change_entry 对重复的相同 usage 返回原状态;不同 usage 产生冲突。人工释放后收到有效迟到 usage,仍然记录已经发生的消耗。模型用量的事务先完成,之后的业务权限复验仍可能拒绝答复提交。

图 6C:账单差异与费用审批

flowchart TD
    C1["6C-1 modules/billing/api.py<br/>import_statement<br/>授权并检查导入幂等标识"]
    C2["6C-2 modules/billing/statements.py<br/>compare<br/>按费用记录比较金额、token 和模型"]
    C3["BillingStatement<br/>保存账单,返回比较结果"]
    C4["6C-4 modules/billing/api.py<br/>submit<br/>员工提交 RECONCILE 申请"]
    C5["6C-5 modules/billing/statements.py<br/>validate_evidence<br/>核对账单明细与申请"]
    C6["BillingChange<br/>PENDING,保存申请人与原目标版本"]
    C7["6C-7 modules/billing/api.py<br/>decide<br/>另一员工审批并复验权限、版本及期限"]
    C8["6C-8 modules/billing/api.py<br/>apply_change → validate_evidence<br/>再次核对证据"]
    C9["6C-9 modules/billing/ledger.py<br/>reconcile<br/>更新费用、账户与审计"]
    C10["BillingChange<br/>APPROVED 或 REJECTED"]
    C1 --> C2 -->|比较通过后继续 import_statement| C3
    C3 -.->|员工查看差异,另一次 HTTP 提交| C4
    C4 --> C5 -->|返回后保存申请| C6
    C6 -.->|审批员工发起独立 HTTP 请求| C7
    C7 -->|批准| C8
    C8 -->|证据校验成功后| C9
    C9 -->|返回 decide,同一事务保存审批结果| C10
    C7 -->|拒绝| C10

源码导航:

账单比较产生 MATCHED、DIFFERENT、UNKNOWN、UNLINKED 或 ROUTE_MISMATCH,比较操作只返回差异。费用变更由独立审批请求触发,申请人不能审批自己的申请。批准操作、费用调整和审计记录共用事务。

关键数据与状态

  • AgentRun、ModelAttempt、TokenAccount、TokenEntry:运行可以包含多次模型尝试。ModelAttempt.id 传给回调作为 reference_id;LangChain 回调的 run_id.bytes 用作 TokenEntry.id 和 MoneyEntry.id。
  • MoneyPolicy、BillingRoute、MoneyAccount、MoneyEntry:分别记录金额预算、已批准价格、当日账户和单次调用金额。
  • 并发额度在 Redis 记录,同时数据库保存任务租约;token 与金额账户分别记录 reserved 和 consumed。并发额度释放与费用结算具有各自的记录和条件。

阅读检查

  1. 在 claim 中找到租户选择、重新统计并发和 Redis 申请的位置,解释不同锁与额度检查分别保护什么。
  2. 从 ModelAttempt.id 跟踪到 TokenEntry.reference_id,说明一次运行、多次尝试和回调标识之间的关系。
  3. 沿 UNKNOWN、重复 usage、迟到 usage 和人工审批四种情况,指出费用记录怎样变化,以及每次变化所属的事务。

测试阅读入口

以下文件用于追踪预期行为;本文编写时只阅读用例,没有执行其中的业务请求。