ValueError: LLM call blocked by before_llm_call hook

当你在 CrewAI 中使用原生 Provider(OpenAI / Anthropic / Bedrock / Gemini / Azure)的异步路径 acall() 或 kickoff_async 时, before_llm_call 守卫钩子不会被调用,返回 False 也无法阻止请求发出,

快速结论:当你在 CrewAI 中使用原生 Provider(OpenAI / Anthropic / Bedrock / Gemini / Azure)的异步路径 acall()kickoff_async 时,before_llm_call 守卫钩子不会被调用,返回 False 也无法阻止请求发出,于是出现 ValueError: LLM call blocked by before_llm_call hook 只在该拦截时不触发、请求照常计费的错位现象。优先排查你走的是同步 call() 还是异步 acall() 路径。

适用环境:Issue 已确认涉及 CrewAI 的五个原生 Provider(llms/providers/openai/completion.pyanthropic/completion.pybedrock/completion.pygemini/completion.pyazure/completion.py)以及 LiteLLM 回退路径 crewai/llm.py;审计基于 main 分支提交 ebe0082。Issue 未提供操作系统、Python、CUDA、显卡或具体依赖版本信息。

最快修复方案:暂无确认的一步修复方案。按 Issue 讨论,修复方向是在各 Provider 的 acall() 中像其同步 call() 一样调用 _invoke_before_llm_call_hooks(对应 PR #6740),以及补上 crewai/llm.py 回退路径 acall() 中缺失的守卫(该改动被拆为独立提交)。这些在撰写时仍处于审查阶段。

注意事项:在修复合并并升级到包含该修复的版本前,请勿依赖异步路径上的 before_llm_call 守卫实现 PII 闸门、花费上限或策略检查;Issue 明确指出被拦截的调用仍会发出并被计费。Issue 中“LiteLLM 回退在两条路径都有守卫”的原始说法已被作者更正,实际 crewai/llm.pyacall() 同样存在缺口。

问题场景

用户通过 register_before_llm_call_hook 注册了一个会返回 False 的阻断型钩子(例如 PII 过滤、花费上限、策略检查、提示注入过滤),期望钩子返回 False 时 LLM 请求根本不会发往 Provider。在同步 call() 路径上该行为正常,但在异步 acall()kickoff_async 路径上,原生 Provider 从不调用该钩子,请求照常发出,返回内容泄露或产生计费。

报错原文

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

预期行为:

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

原因分析

最可能的原因是同步与异步路径的实现漂移:五个原生 Provider 都在同步 call() 中调用了 _invoke_before_llm_call_hooks,但各自异步 acall() 中完全没有这个调用点。Issue 审计给出的对照:OpenAI(:465)、Anthropic(:377)、Bedrock(:380)、Gemini(:316)、Azure(:524)同步路径均有守卫,异步路径均缺失。因此 acall() 不经过任何拦截点,直接向 Provider 发出请求。

另一个可能原因是 LiteLLM 回退路径的缺口。Issue 作者最初称 llms/base_llm.py 在两条路径都有守卫,随后更正:base_llm.py 只定义了辅助函数、没有调用方,其 acall() 直接 raise NotImplementedError;真正的回退实现是 crewai/llm.py,其中 call() 守卫了两类钩子(:1876:1904),而 acall():1959)两类都不守卫。要触发该路径需使用 LLM(model=..., is_litellm=True),否则构造函数会返回原生 Provider。

讨论中另有一处相邻但独立的问题:after_llm_call 在异步路径上也存在同样漂移,且形态是“按 handler 遗漏”而非“按 Provider 遗漏”,例如 Bedrock 的 _ahandle_streaming_converse 调用了钩子而 _ahandle_converse 没有,与其他所有行极性相反;Azure 与 Gemini 因走共享 helper,异步 after 路径已覆盖,异步 before 路径仍未覆盖。这解释了为何单纯按 Provider grep 会漏判。

环境排查

  • 确认 CrewAI 版本与所用提交;Issue 审计基于 main 提交 ebe0082
  • 确认调用路径:是同步 call()、还是异步 acall()kickoff_async
  • 确认 Provider 类型:OpenAI、Anthropic、Bedrock、Gemini、Azure 原生 Provider,还是 LLM(..., is_litellm=True) 的 LiteLLM 回退。
  • 确认钩子是通过 register_before_llm_call_hook 注册,且返回值为 False 或抛出 HookAborted
  • Issue 未提供 Python、CUDA、PyTorch、显卡或依赖版本信息,无需据此排查。

解决步骤

  1. 先复现并确认路径。参考 Issue 提供的无网络复现脚本思路:把 Provider 客户端替换为记录“是否发出请求”的 stub,用 ISSUED 列表区分同步与异步是否真的发出。重点观察输出中 async request issued 是否为 ['async']
  2. 确认你依赖的安全闸门是否跑在异步路径上。若使用 acall()kickoff_async 且 Provider 为上述五者之一,则在修复合并前该 before_llm_call 阻断不会生效。
  3. 可优先尝试(推测):在非关键场景下临时改用同步 call()kickoff() 路径,或把阻断逻辑下沉到 Provider 调用之外的显式校验,避免依赖异步路径上的 before_llm_call。这一做法基于“同步路径存在守卫”的 Issue 证据推断,Issue 未直接验证。
  4. 跟进上游修复:before 类修复对应 PR #6740(覆盖五个 Provider,含逐 Provider 无网络探针);after 类修复对应 #6737;LiteLLM 回退路径的缺口被作者拆为第三个独立改动单独提交。待合并后升级至包含修复的版本。
  5. 若你自行打补丁,注意 Gemini 的特殊性:其格式化 payload 是 Content 对象而非 dict,守卫需像其同步孪生实现一样加入 _convert_contents_to_dict,否则钩子接收到的 payload 结构与同步路径不一致(讨论作者点名此处是最需要重点核对的地方)。

验证方法

用 Issue 中的 stub 方案验证:同步 call() 应抛出 ValueError: LLM call blocked by before_llm_call hookrequest issued: no;修复后异步 acall() 也应抛出同一 ValueError,且 ISSUED 列表中不出现 async。关键点是断言“请求是否离开进程”,而不是只看返回字符串——只看返回值无法区分“钩子运行并放行”与“钩子从未运行”。LiteLLM 回退路径可在 LLM(model=..., is_litellm=True) 下用 litellm.completion / acompletion 打桩做同样探针。

参考来源

crewAIInc/crewAI #6739

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 23255

发表回复

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