数据截至 (上游 commit 601093ddfe23)
AutoGPT — 计费、安全与嵌套(深水区)
本章讲什么: 前三章讲了块、图、引擎。这一章是把平台跑成「能托管、能收费、敢让陌生人用」的三块深水区:怎么算钱、怎么不真花钱地试跑、怎么在危险动作前拦一道人审、怎么让 agent 调 agent。
1. 计费:预扣 + 事后对账
1.1 要解决的小问题
有的块成本跑之前就知道(固定 1 积分/次);有的只有跑完才知道(LLM 按 token、视频按时长)。要既能在跑前挡住「余额不足」,又能按真实用量收费,AutoGPT 用了两段式。
1.2 成本类型
BlockCostType(blocks/_base.py:100)把成本分两类:
| 类型 | 含义 | 何时定价 |
|---|---|---|
RUN / BYTE | 每次运行 / 每字节,固定 | 跑前就定 |
SECOND / ITEMS / COST_USD / TOKENS | 按时长/条数/美元/token | 跑后按 stats 结算 |
后四种叫动态成本(blocks/_base.py:124 _DYNAMIC_COST_TYPES)。它的 is_dynamic 属性(blocks/_base.py:115)有句关键注释:动态类型跑前返回 0、跑后用 charge_reconciled_usage 按 stats 结算。
1.3 两段式流程
调度循环取到节点
│
① 预扣(pre-flight) billing.charge_usage() manager.py:1057
│ RUN 类:扣真实价 ; 动态类:扣 0(占位)
│ 记下 pre_flight_charge 作为对账基线 manager.py:1066
▼
节点执行,产出 stats(真实 token 数 / 时长 / provider_cost)
│
② 对账(reconcile) billing.charge_reconciled_usage() manager.py:637
│ 按 stats 算真实价,与预扣的差额补扣或退款
│ delta 累加进 graph_stats.cost manager.py:642
▼
整图 cost 反映真实花费
几个值得记住的细节:
- 失败/中止也要对账。 注释明说:即使节点
FAILED/TERMINATED,也要 reconcile(manager.py:621)——因为它可能已经烧了真实的 provider token,预扣的钱应退到「实际用掉」的量,不能整笔吞掉用户的钱。 - 对账基线被钉死。
pre_flight_charge在预扣那一刻就记下(manager.py:1066),防止「预扣和对账之间有人热更新了估价 JSON」导致对账对错基线。 - dry-run 完全跳过对账(
manager.py:636):仿真不碰用户钱包。 - 余额闸门在循环里:取节点前若
get_credits <= 0直接InsufficientBalanceError优雅停图(manager.py:976),并发通知(manager.py:1090)。
2. dry-run:用 LLM 仿真整张图,不真花钱
2.1 直觉
用户搭了张复杂图,想先看看「大概会怎么流、输出长啥样」,但不想真去调 OpenAI、真发推特、真扣钱。dry-run 让整张图照常调度,但每个块的产出由 LLM 假装生成。
2.2 怎么实现
开关是 ExecutionContext.dry_run(data/execution.py:108),这个 context 贯穿整个执行流。在 execute_node 里(manager.py:342):
# manager.py:342(精简)—— dry-run 分流
if execution_context.dry_run and _dry_run_input is None:
block_iter = simulate_block(node_block, input_data, user_id=user_id) # LLM 假装产出
else:
block_iter = node_block.execute(input_data, **extra_exec_kwargs) # 真跑
配套的容错铺在多处:
validate_exec在 dry-run 下给缺失的凭据字段塞None占位,让节点能入队(executor/utils.py:344)——反正仿真不调真 API。Block._execute在 dry-run 下跳过凭据字段的 schema 必填校验(blocks/_base.py:844),只校非凭据输入。- 少数块(如编排块)在 dry-run 下仍真跑(用平台自己的模拟模型 + OpenRouter key,不需要用户凭据),由
prepare_dry_run决定(manager.py:250)。
巧妙之处: dry-run 不是「另一套执行路径」,而是同一个引擎 + 同一个 context 字段 + 一个
simulate_block分流点。整图的调度、路由、状态机全复用,只把「真正调块」这一步换成 LLM 仿真。
3. 人在回路(HITL):危险动作前拦一道
3.1 要解决的小问题
agent 自动跑,但某些动作(发邮件、删数据、花钱)用户想先看一眼再批。
3.2 怎么实现
块在 __init__ 里声明 is_sensitive_action=True(例:PrintToConsoleBlock,blocks/basic.py:132)。执行管线 _execute 一进来就检查(blocks/_base.py:833):
# blocks/_base.py:775(精简)—— 敏感动作 + 安全模式 → 走审核
if not (self.is_sensitive_action and execution_context.sensitive_action_safe_mode):
return False, input_data # 不是敏感动作,或没开安全模式 → 放行
decision = await HITLReviewHelper.handle_review_decision(...)
if decision is None: return True, input_data # 还在等审 → 暂停
if not decision.should_proceed: raise BlockExecutionError(...) # 被拒 → 停
return False, decision.review_result.data # 批准(可能改过输入)→ 用新数据跑
几个点:
- 三态返回:暂停(等审)/ 拒绝(停)/ 批准(可带审核人修改过的输入继续)。
- 暂停时整图状态变成
REVIEW(manager.py:1204has_pending_reviews_for_graph_exec),引擎把图挂起;用户批了之后,图从REVIEW状态续跑(manager.py:849)。这复用了第 3 章的「历史状态续跑」机制。 - 是否启用由图设置
sensitive_action_safe_mode(data/graph.py:61)和human_in_the_loop_safe_mode(data/graph.py:58)控制,默认后者开、前者关。
4. 嵌套:agent 调 agent
4.1 直觉
你做了个「翻译 agent」,现在想在「新闻摘要 agent」里把它当一个步骤用。AutoGPT 让一张图作为一个块嵌进另一张图——这就是 AgentExecutorBlock(blocks/agent.py:24)。
4.2 怎么实现:复用同一个引擎,递归调用
AgentExecutorBlock.run(blocks/agent.py:83)做的事极简洁——它就是再调一次 add_graph_execution,启动子图:
# blocks/agent.py:93(精简)—— 子 agent = 再跑一张图
graph_exec = await execution_utils.add_graph_execution(
graph_id=input_data.graph_id,
user_id=input_data.user_id,
inputs=input_data.inputs,
execution_context=execution_context.model_copy(
update={"parent_execution_id": graph_exec_id}, # 记住父执行
),
dry_run=execution_context.dry_run, # dry-run 沿袭
)
然后它监听子图的执行事件总线(blocks/agent.py:156 event_bus.listen),把子图各节点的产出当成自己的输出 yield 出去,让父图的下游能接。
4.3 两个关键卷回
- 开销卷回父图。 子图自己已经逐节点扣过费了;子图
COMPLETED时,AgentExecutorBlock把子图总 cost 作为reconciled_cost_delta合并进自己的 stats(blocks/agent.py:179),这样父图的graph_stats.cost反映完整的子图花费。extra_steps同理把子图节点数卷进父图的步数统计(manager.py:647)。 - 超时豁免。 前面说过,
AgentExecutorBlock把execution_timeout_seconds = None(blocks/agent.py:28),因为子图各节点自己有时限,外层不该再卡一道。
巧妙之处: 嵌套没有引入任何新机制。子 agent 就是「再调一次入口函数 + 监听事件总线 + 把子图成本卷回」。整个第 3 章的引擎被原样复用,
ExecutionContext的parent_execution_id/dry_run字段沿调用链传下去——这是一个把「递归」做得几乎免费的设计。
5. 把四件事串起来:ExecutionContext
上面四个能力(对账、dry-run、人审、嵌套)的开关全挂在一个对象上:ExecutionContext(data/execution.py:89)。它从入口一路传到每个块,携带:身份(user/graph/node id)、安全设置(dry_run、sensitive_action_safe_mode)、层级(parent_execution_id、root_execution_id)。
add_graph_execution(execution_context) ──┐
_on_graph_execution │ 同一个 context
execute_node │ 逐层 model_copy 透传
block.execute(execution_context=...)
└─ dry-run? 敏感动作? 子 agent? 全看 context
一个对象统一承载「这次执行的所有横切关注点」,是这套设计能保持简洁的关 键。
6. 边界与局限
- 动态计费依赖块如实上报
stats(token/时长);块少报就少收。对账只在节点终态触发一次。 - dry-run 的仿真质量取决于
simulate_block背后的 LLM,不保证和真跑的输出形状一致——只是「像样的假数据」。 - 嵌套的成本卷回发生在子图
COMPLETED事件;子图若以非完成态结束,卷回逻辑走的是不同分支(blocks/agent.py:161的状态过滤)。
7. 横向对比
- 预扣+对账和云厂商的计量计费一脉相承(预留额度 → 按实结算),在 agent 平台里少见——多数开源 agent 框架不计费,这是 AutoGPT 作为托管商业平台的必备件。
- dry-run 仿真比「mock 每个工具」更省事:不用为每个块写 mock,直接让 LLM 顶上。代价是不精确。
- 嵌套复用入口函数和 Temporal 的 child workflow 思路一致,但实现轻得多(一个块 + 事件总线监听)。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 成本类型(含动态判 定) | autogpt_platform/backend/backend/blocks/_base.py | BlockCostType、BlockCost |
| 预扣 | executor/manager.py → executor/billing.py | billing.charge_usage |
| 事后对账 | executor/manager.py → executor/billing.py | charge_reconciled_usage |
| 取块成本 | data/credit.py | get_block_cost、BLOCK_COSTS |
| dry-run 分流 | executor/manager.py | execute_node(simulate_block 分支) |
| dry-run 校验放行 | executor/utils.py、blocks/_base.py | validate_exec、Block._execute |
| 人在回路 | blocks/_base.py | Block.is_block_exec_need_review |
| 子 agent 块 | blocks/agent.py | AgentExecutorBlock |
| 贯穿全程的上下文 | data/execution.py | ExecutionContext |