ValueError: LLM call blocked by before_llm_call hook

当你在 CrewAI 中使用原生 provider(openai / anthropic / bedrock / gemini / azure)或 LiteLLM fallback 的异步路径 acall() / kickoff_async ,并注册了 before_llm_call 拦截钩子时,钩

快速结论:当你在 CrewAI 中使用原生 provider(openai / anthropic / bedrock / gemini / azure)或 LiteLLM fallback 的异步路径 acall()/kickoff_async,并注册了 before_llm_call 拦截钩子时,钩子不会被调用,导致本应被阻断的请求仍然发出并被计费。优先排查是否走了异步调用路径,以及钩子是否在同步 call() 路径上正常生效。

适用环境:Issue 在 main 分支提交 ebe0082 上复现,涉及 CrewAI 原生 provider(openai、anthropic、bedrock、gemini、azure)以及 crewai/llm.py 中的 LiteLLM fallback(需 LLM(model=..., is_litellm=True) 才能进入)。复现脚本使用 stub client,无需真实网络。Issue 未确认具体的 Python、CUDA、PyTorch、显卡或依赖版本。

最快修复方案:暂无确认的一步修复方案。Issue 中对应的修复 PR(#6740 覆盖五个原生 provider 的 before 路径,#6737 覆盖 after 路径)在关闭时仍在评审中,LiteLLM fallback 的漏洞另需单独修复。

注意事项:该问题只影响异步 acall() 路径,同步 call() 的拦截仍正常。仅断言返回值的测试无法区分“钩子运行并放行”和“钩子从未运行”,因此验证时应观察请求是否真正发出。LiteLLM fallback 并不像早期描述的那样“两条路径都有保护”,它同样存在漏洞。

问题场景

在 CrewAI 中通过 register_before_llm_call_hook 注册拦截钩子(如 PII 门禁、预算上限、策略检查、提示注入过滤器),钩子返回 False 或通过 reducer 抛出 HookAborted,期望阻止 LLM 请求发出。在同步 call() 路径下拦截正常,但改用异步 acall() 或 kickoff_async 时,请求仍然被发送到 provider 并产生费用。该问题影响五个原生 provider(openai、anthropic、bedrock、gemini、azure),以及使用 is_litellm=True 的 LiteLLM fallback。

报错原文

sync  -> LLM call blocked by before_llm_call hook
sync  request issued: no
async -> LEAKED
async request issued: ['async']

ValueError: LLM call blocked by before_llm_call hook

原因分析

最可能的原因是:五个原生 provider 只在同步 call() 路径调用了 _invoke_before_llm_call_hooks,而在异步 acall() 路径中完全没有调用点。Issue 中给出的覆盖表显示:openai completion :465、anthropic completion :377、bedrock completion :380、gemini completion :316、azure completion :524 均在同步路径有钩子调用,异步路径缺失。

此外,LiteLLM fallback 位于 crewai/llm.py(而非仅定义 helper 的 llms/base_llm.py):其 call() 在 :1876、:1904 对两个钩子家族都有保护,但 acall() 在 :1959 对两者都没有保护。因此 fallback 并非安全路径。

由于 _invoke_before_llm_call_hooks 在 from_agent is not None 时会立即返回 True,修复时从各 provider 的 acall() 调用其自身同步路径的 guard 不会造成重复派发。

环境排查

  • 确认调用的入口:是同步 call() 还是异步 acall()/kickoff_async,异步路径是问题触发条件。
  • 确认使用的 provider:是否为 openai、anthropic、bedrock、gemini、azure 之一,或是否通过 LLM(model=..., is_litellm=True) 走 LiteLLM fallback。
  • 确认 CrewAI 代码版本是否包含对应修复(Issue 基于 main 提交 ebe0082 复现)。
  • 确认钩子类型:是 before_llm_call(本 Issue 主体)还是 after_llm_call(相关的 #6736/#6737)。
  • Issue 未确认 Python、CUDA、PyTorch、显卡及依赖版本,这些不作为必要排查项。

解决步骤

  1. 先区分同步与异步路径。用最小复现脚本(见 Issue 正文,替换 provider client 为记录请求是否发出的 stub),分别执行 llm.call(msgs) 和 asyncio.run(llm.acall(msgs)),观察 ISSUED 记录与返回内容。
  2. 确认同步路径已被拦截:call() 应抛出 ValueError: LLM call blocked by before_llm_call hook,且 request issued: no。
  3. 确认异步路径是否泄漏:acall() 返回了本应被拦截的响应,且 request issued: ['async'],说明钩子未运行、请求已发出。
  4. 若使用原生 provider,等待并合入 #6740 类修复:在各 provider 的 acall() 中补上与同步路径相同的 _invoke_before_llm_call_hooks 调用。Issue 指出 gemini 需特别处理,其格式化 payload 是 Content 对象而非 dict,需保留同步路径的 _convert_contents_to_dict 行,否则钩子收到的 payload 结构与同步路径不一致。
  5. 若使用 LiteLLM fallback(is_litellm=True),需单独修复 crewai/llm.py 中 acall() 缺失的两处钩子调用;该改动与 provider 侧修复属不同文件、不同故障,不会随 #6737/#6740 一起被覆盖。
  6. 在修复合入前,如果拦截是安全关键(如 PII 门禁、预算上限),可优先尝试在异步流程外层自行增加请求前校验,避免依赖异步路径上的钩子。

验证方法

修复后,同步与异步路径都应拒绝发出请求:

sync  -> LLM call blocked by before_llm_call hook
sync  request issued: no
async -> LLM call blocked by before_llm_call hook
async request issued: no

验证时应以“请求是否离开进程”为观测点(stub 的 create/acreate 是否被调用),而不是仅断言返回字符串;否则无法区分“钩子运行并放行”与“钩子从未运行”。如需在真实环境验证,可同时注册一个只观察的 before 钩子与一个重写响应的 after 钩子,确认异步路径的计数由 before:0 变为 before:1。

参考来源

crewAIInc/crewAI #6739

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 29183

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注