RH
RouterHub AI API 中转站导航与平台推荐
博客

CrewAI Flow 重跑后工具限额失效怎么修

1.15.29 在每次 kickoff 前重置 Flow 工具计数,修复复用同一实例时后续执行被误判超限的问题。

muchacha 2026-10-24 02:22:47

正文

核心结论

CrewAI 1.15.29 修复了一个只在复用同一 Flow 实例时暴露的工具限额问题:通过 Flow 声明构建的工具对象会跨多次 kickoff 复用,但旧版本没有在新一轮执行开始前清零 current_usage_count。一旦某次执行耗尽 max_usage_count,后续执行即使只调用一次工具,也可能立刻收到 ToolFailure(USAGE_LIMIT),并最终抛出 ToolExecutionFailedError。

修复后的语义不是“取消工具限额”,而是把限额恢复为每次 kickoff 独立计算。单次执行内部仍然严格执行 max_usage_count;如果一个 each 循环在同一轮执行中调用两次只允许调用一次的工具,第二次调用仍会失败。对常驻服务、队列消费者、定时任务和循环复用 Flow 对象的程序来说,这个边界非常重要。

如果你的程序每次请求都重新创建 Flow,这个缺陷可能一直不会出现;如果程序把 Flow 放进单例、依赖注入容器或长生命周期 Worker 中,升级到 1.15.29 或更高版本后,应增加“同一实例连续执行多次”的回归测试,而不能只验证第一次执行。

故障为什么只在重跑后出现

CrewAI 官方文档允许为工具设置 max_usage_count,用于限制工具在执行期间被调用的次数。例如,一个涉及写文件、发送消息或调用外部系统的工具,可以通过调用上限阻止 Agent 无限重复动作。工具默认没有调用次数上限,只有显式配置后才会启用这条预算。

问题不在 max_usage_count 本身,而在工具对象的生命周期。Flow 的工具动作在 Flow 初始化时构建工具实例,之后同一个 Flow 对象的多次执行会继续复用该工具。旧实现会累加 current_usage_count,却没有在下一次 kickoff 的边界重置它。

可以把旧行为简化成下面的状态变化:

创建 Flow:current_usage_count = 0
第一次 kickoff 调用工具:current_usage_count = 1
第二次 kickoff 调用工具:current_usage_count = 2
第三次 kickoff 开始:计数仍为 2,直接命中 usage limit

这与开发者通常理解的“每轮运行最多调用两次”不同,实际变成了“这个 Flow 对象从创建到销毁一共只能调用两次”。官方 issue 明确指出,长生命周期 Flow 会在预算首次耗尽后持续失败,除非重建对象或手动重置状态。

报错之所以看起来像工具本身执行失败,是因为调用链还有第二层语义。CrewAI 的工具失败模型会把达到 max_usage_count 记录为结构化 ToolFailure;Flow 工具动作检测到该失败后,可以把它转换成 ToolExecutionFailedError。因此日志最后显示的是执行错误,但根因不是外部 API、参数或网络,而是上一轮执行遗留的计数。

如何确认自己遇到的是同一个问题

这个故障有三个同时成立的特征。

第一,第一次或前几次执行成功,失败只在复用同一 Flow 实例后出现。重启进程、重新创建 Flow 或重新部署后,调用又会暂时恢复。

第二,错误信息指向工具达到使用次数上限,常见关键字包括 usage limit、USAGE_LIMIT 或 ToolExecutionFailedError。如果错误是鉴权失败、请求超时、模型拒绝工具调用或工具参数校验失败,就不属于这次修复的范围。

第三,同样的输入在全新 Flow 实例上可以成功,而在已经执行过的实例上失败。排查时可以临时对照运行:一组复用对象,另一组每轮创建对象。如果只有复用组失败,就应重点检查 CrewAI 版本和 max_usage_count。

下面的最小测试能验证升级后的关键语义。工具每轮最多调用两次,但连续三次执行都只调用一次,因此三次都应成功:

from crewai.flow import Flow
from crewai.flow.flow_definition import FlowDefinition
from crewai.tools import BaseTool


class BudgetedEchoTool(BaseTool):
    name: str = "BudgetedEchoTool"
    description: str = "Echo tool with a per-run usage budget."
    max_usage_count: int = 2

    def _run(self, text: str) -> str:
        return f"ok:{text}"


definition = FlowDefinition.from_declaration(
    contents={
        "schema": "crewai.flow/v1",
        "name": "ToolUsageBudgetFlow",
        "methods": {
            "run_tool": {
                "start": True,
                "do": {
                    "call": "tool",
                    "ref": "__main__:BudgetedEchoTool",
                    "with": {"text": "${state.text}"},
                },
            }
        },
    }
)

flow = Flow.from_declaration(contents=definition)
assert flow.kickoff(inputs={"text": "a"}) == "ok:a"
assert flow.kickoff(inputs={"text": "b"}) == "ok:b"
assert flow.kickoff(inputs={"text": "c"}) == "ok:c"

旧版本可能在第三次执行时报错,1.15.29 的回归测试则明确要求第三次仍然成功。这里必须复用同一个 flow 变量;如果每次测试都重新构建 Flow,就无法覆盖缺陷。

1.15.29 具体改了什么

修复分为两步。首先,Flow 工具动作在构建工具时,把实例登记到 Flow 的 _tool_action_tools 集合中,让运行时能够找到所有需要重置预算的工具。其次,kickoff_async 的执行路径在每轮开始时调用 _reset_tool_action_usage_budgets(),对登记过的工具执行 reset_usage_count()。

选择 kickoff_async 作为重置边界并非只覆盖异步调用。官方 PR 说明,同步 kickoff、异步执行、流式执行以及检查点恢复都会经过这条运行路径,因此不需要在每个入口分别清零。对应用开发者而言,重要结论是:只要使用修复版本,就不应再为不同调用入口维护手工重置逻辑。

修复同时保留了单轮执行内的限制。官方测试还覆盖了另一种情况:一个工具的 max_usage_count=1,同一次 kickoff 中的 each 循环尝试调用两次,第二次仍应抛出使用上限错误。也就是说,新的边界是“每次执行重置”,不是“每次工具调用前重置”。

这一区分也影响故障策略。CrewAI 文档说明,工具失败可根据 tool_failure_policy 选择忽略、警告或抛错,但策略只决定失败之后如何处理,并不会改变调用预算本身。把策略改成 ignore 可能隐藏错误,却不能让旧版本的计数重新变得正确;把 raise 改成 warn 也可能让 Flow 继续运行,却会把真实工具结果替换成失败信息。因此不应把修改失败策略当作根治方案。

升级前后的处理方法

最直接的修法是升级到 CrewAI 1.15.29 或更高版本,并锁定经过测试的版本范围:

python -m pip install "crewai>=1.15.29"
python -m pip show crewai

升级后至少执行两类测试。第一类是同一 Flow 实例连续运行超过 max_usage_count 次,但每轮调用量都没有超限;它验证跨执行计数已经清零。第二类是在单次执行内故意超过限制;它验证安全预算仍然生效。只跑第一类容易误把“计数不再工作”当作修复成功,只跑第二类则发现不了跨执行残留。

如果短期无法升级,可以在每轮请求中重新创建 Flow,避免共享已经累积计数的工具对象。这是基于对象生命周期的止血办法,会失去复用带来的初始化收益,也可能影响你依赖的其他内存状态,因此应明确标记为临时措施。不要直接修改私有字段 current_usage_count,也不要在业务代码里调用以实现细节命名的私有方法;这些做法会把应用绑定到内部结构,后续版本更难迁移。

常驻服务还应检查实例的保存位置。以下模式都可能复用 Flow:模块级单例、Web 框架的应用级依赖、后台 Worker 的成员变量、定时任务调度器持有的对象,以及从容器中反复取得同一实例。如果业务原本预期每个租户、任务或会话拥有独立状态,还需要进一步确认 Flow 状态本身是否也被错误共享。工具计数修复只解决调用预算,不会自动替你隔离其他业务状态。

对工具较多的 Agent 工作流,可以在 RouterHub 平台目录 核对模型与接入方式,但模型渠道不会改变 CrewAI 本地的工具计数语义。若你的故障还伴随检查点恢复后状态不一致,可对照 LangGraph 检查点丢状态的修法 中的排查方法,把持久化状态、运行时对象状态和流式输出分开验证。

不要把这个修复误读成什么

它不是工具重试机制。达到调用上限后的请求不会因为升级而自动重试;在单次 kickoff 内超过预算仍然是失败。

它不是并发隔离承诺。官方修复说明的是每次执行开始时重置工具计数,没有声明同一 Flow 实例可以安全地被多个请求并发调用。若服务可能并发复用对象,仍应根据 CrewAI 的状态模型设计实例范围,并用并发测试验证自己的用法。

它也不是外部 API 限流修复。max_usage_count 是 CrewAI 工具层的本地调用预算,与模型提供商的 429、云服务配额或第三方接口速率限制不是同一层。升级后如果仍收到外部限流,应继续检查重试、退避和上游配额,而不是提高工具调用上限。

最终验收标准可以压缩成两句话:同一 Flow 实例的多次 kickoff 之间,工具计数彼此独立;同一次 kickoff 内,max_usage_count 仍然严格生效。只要这两条同时通过,就覆盖了 1.15.29 修复的核心行为。

参考来源

以上来源均于 2026 年 10 月 24 日打开并核对原文。