1. crewai 里 Litellm 报 BadRequestError,先别急着改源码
如果你在用 crewai 搭多智能体流程,模型层走的是 Litellm,然后某天突然抛出一个litellm.exceptions.BadRequestError,大概率不是 crewai 本身坏了,也不是 Litellm 不支持国内模型服务,而是模型名和 base_url 的写法没对上 Litellm 的路由规则。
我自己第一次遇到这个报错时,第一反应是「Litellm 是不是不认国内这些 OpenAI 兼容端点」,甚至去翻 Litellm 的 provider 源码想加分支。结果折腾半天发现,问题根本不在支持不支持,而在于模型名前缀少写了一个openai/。Litellm 看到没有前缀的模型名,会按它内置的 provider 映射去猜,猜不到就走到默认分支,参数拼出来不对,服务端直接返回 400,于是包装成BadRequestError抛给你。
这篇就围绕这个场景展开:crewai 调用 Litellm 时出现BadRequestError的排查路径,覆盖 openai 兼容接口、模型名与 base_url 配置,给出可复制的config.toml/settings.json骨架,以及 TaoToken 统一 Key 通道的接入步骤。适合正在用 crewai + Litellm 接国内模型、被 400 卡住的人。核心检索词就三个:crewai、Litellm、BadRequestError。
先说结论,最容易被忽略的一行改动是:
# 报错写法 llm = LLM(model="qwen3-235b-a22b-instruct-2507", base_url="...", api_key="sk-xxx") # 正确写法:模型名前加 openai/ llm = LLM(model="openai/qwen3-235b-a22b-instruct-2507", base_url="...", api_key="sk-xxx")openai/这个前缀不是让你去调 OpenAI 官方,而是告诉 Litellm:这是一个 OpenAI 兼容端点,请走/chat/completions那条路由。下面把原理、配置、验证、排障一步步拆开。
2. 为什么加openai/前缀就能解决 BadRequestError
2.1 Litellm 的 provider 路由逻辑
Litellm 处理一个模型名时,会先做一次「provider 解析」。它的规则大致是:如果模型名里带/,斜杠前面那段就被当作 provider 标识;如果没有斜杠,就拿整个字符串去匹配内置的模型清单。
你写qwen3-235b-a22b-instruct-2507,没有斜杠,Litellm 在内置清单里找不到完全匹配项,就会 fallback 到默认 provider 推断。推断出来的调用方式和你的 base_url 不匹配,请求体里可能缺字段、或者 endpoint 拼错,服务端返回 400。
你写openai/qwen3-235b-a22b-instruct-2507,斜杠前是openai,Litellm 立刻知道:走 OpenAI 兼容协议,用 openai-client 发请求,自动补/chat/completions。模型名斜杠后的部分原样传给服务端。这样 base_url 指向哪个兼容服务都行。
2.2 base_url 不要画蛇添足
官方文档里有一句很关键的话:不要在 base_url 上添加任何额外内容,比如/v1/embedding。LiteLLM 用 openai-client 发请求,会自动补上相关端点路径。
也就是说,你的 base_url 应该停在版本号那一层:
https://your-endpoint.example.com/v1而不是:
https://your-endpoint.example.com/v1/chat/completions # 错多写一段路径,openai-client 再拼一次,就变成/v1/chat/completions/chat/completions,服务端当然 400。
2.3 两种前缀对应两种端点
| 你要调的端点 | 模型名前缀 | 说明 |
|---|---|---|
/chat/completions | openai/ | 对话补全,最常用 |
/completions | text-completion-openai/ | 传统文本补全 |
通过/v1/completions路由调 openai 端点 | 不需要额外前缀 | 走路由时自动识别 |
crewai 里的 LLM 调用基本都是对话补全,所以记住openai/就够了。
3. TaoToken 统一 Key 通道的前置准备
3.1 为什么用统一 Key 通道
crewai 项目里往往不止一个模型:规划用一个大模型,执行用另一个,评审再用一个。如果每个模型都单独配 key、单独记 base_url,配置会散得到处都是,排查BadRequestError时你甚至分不清是哪个通道出的问题。
统一 Key 通道的思路是:所有 OpenAI 兼容请求都走同一个入口,模型名区分具体模型,key 只有一份。这样配置集中、报错集中、切换模型只改一个字符串。
3.2 拿到 Key 和入口地址
进入控制台创建 API Key,入口在这里:
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
创建后你会得到一串sk-开头的 key。base_url 用 API 地址:
https://taotoken.net/api注意这里不要在后面加/v1/chat/completions,原因见 2.2。如果你用的客户端要求带版本号,就写到/api这一层,让 openai-client 自己补。
3.3 环境变量先落地
在动手改 crewai 代码前,先把 key 放进环境变量,避免硬编码:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell:
$env:TAOTOKEN_API_KEY="sk-你的key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"这样后面 config.toml 和代码里都引用变量名,换 key 不用改文件。
4. 可复制的 config.toml 与 settings.json 骨架
4.1 crewai 的 config.toml 骨架
crewai 新版本支持用config.toml管理 LLM 配置。下面这份可以直接抄,重点是model字段带openai/前缀:
# config.toml [llm] # 统一走 OpenAI 兼容通道,前缀 openai/ 不能省 model = "openai/qwen3-235b-a22b-instruct-2507" base_url = "https://taotoken.net/api" api_key = "env:TAOTOKEN_API_KEY" temperature = 0.3 max_tokens = 2048 [llm.fallback] model = "openai/qwen3-235b-a22b-instruct-2507" base_url = "https://taotoken.net/api" api_key = "env:TAOTOKEN_API_KEY"api_key = "env:TAOTOKEN_API_KEY"这种写法让 crewai 从环境变量读,不把 key 写进仓库。
4.2 settings.json 骨架
如果你的项目用 JSON 配置,等价写法:
{ "llm": { "model": "openai/qwen3-235b-a22b-instruct-2507", "base_url": "https://taotoken.net/api", "api_key": "env:TAOTOKEN_API_KEY", "temperature": 0.3, "max_tokens": 2048 } }4.3 代码里直接构造 LLM
不走配置文件、直接在 Python 里构造也可以:
import os from crewai import LLM llm = LLM( model="openai/qwen3-235b-a22b-instruct-2507", base_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) if __name__ == "__main__": response = llm.call( "Analyze the following messages and return the name, age, and breed. " "Meet Kona! She is 3 years old and is a black german shepherd." ) print(response)对比一下报错版本和修复版本,唯一区别就是model字段:
# 报 BadRequestError model="qwen3-235b-a22b-instruct-2507" # 正常 model="openai/qwen3-235b-a22b-instruct-2507"4.4 多模型场景的配置
crewai 里不同 agent 用不同模型时,每个都带前缀:
[llm.planner] model = "openai/qwen3-235b-a22b-instruct-2507" base_url = "https://taotoken.net/api" api_key = "env:TAOTOKEN_API_KEY" [llm.executor] model = "openai/qwen3-235b-a22b-instruct-2507" base_url = "https://taotoken.net/api" api_key = "env:TAOTOKEN_API_KEY"模型名不同就换斜杠后面那段,前缀和 base_url 保持不变。
5. 最小复现请求与验证动作
5.1 先用 curl 验证通道本身
在改 crewai 之前,先用 curl 确认 key 和 base_url 是通的,把变量隔离出来:
curl -sS https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-235b-a22b-instruct-2507", "messages": [{"role": "user", "content": "reply with ok"}], "max_tokens": 16 }'注意这里 curl 直接打/chat/completions,因为 curl 不会自动补路径。如果这一步返回正常 JSON,说明 key 和通道没问题,问题在 Litellm 的模型名写法上。
5.2 再用 Litellm 单独验证
绕开 crewai,直接调 Litellm,确认前缀规则:
import os from litellm import completion resp = completion( model="openai/qwen3-235b-a22b-instruct-2507", base_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], messages=[{"role": "user", "content": "reply with ok"}], max_tokens=16, ) print(resp.choices[0].message.content)如果这个能通,而 crewai 里不通,那就是 crewai 的配置没把model字段传对。
5.3 最后跑 crewai 最小示例
import os from crewai import LLM llm = LLM( model="openai/qwen3-235b-a22b-instruct-2507", base_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) response = llm.call("Say hello in one word.") print(response)三步验证的顺序很重要:curl 验通道 → Litellm 验前缀 → crewai 验集成。哪一步断掉,问题就锁定在哪一层。
5.4 成功结果长什么样
正常返回是一段文本,比如Hello。如果返回里带choices、usage这些字段,说明走的是标准 OpenAI 兼容响应结构。crewai 拿到这个结构后自己解析,不会再抛BadRequestError。
6. 本篇常见错排查清单
6.1 模型名没加openai/前缀
这是最高频的原因。症状是BadRequestError,堆栈里能看到get_llm_provider相关调用。修复就是加前缀。判断方法:把模型名打印出来,看斜杠前是不是openai。
6.2 base_url 多写了路径
症状同样是 400,但错误信息里可能带Not Found或路径重复。检查 base_url 是不是写成了.../api/v1/chat/completions。正确写法停在https://taotoken.net/api。
6.3 前缀和端点不匹配
调/chat/completions用了text-completion-openai/,或者反过来。对照 2.3 的表格改。crewai 场景基本都是openai/。
6.4 key 没读到环境变量
症状是 401 而不是 400,但有时会被包装成BadRequestError。检查os.environ.get("TAOTOKEN_API_KEY")是否有值。config.toml 里写env:TAOTOKEN_API_KEY时,确认变量名拼写一致。
6.5 模型名斜杠后拼错
前缀对了,但斜杠后的模型名写错,服务端找不到模型,也可能返回 400。把模型名复制到 curl 里单独测一次,确认服务端认这个名字。
6.6 crewai 版本差异
老版本 crewai 的 LLM 构造参数和新版本略有不同。如果base_url传了不生效,检查是不是要用api_base。看 crewai 的 LLM 类签名,或者打印llm.__dict__确认参数进去了。
6.7 排查顺序建议
遇到BadRequestError别一上来就改源码。按这个顺序走:先看模型名有没有openai/前缀 → 再看 base_url 有没有多写路径 → 再用 curl 验通道 → 再用 Litellm 验前缀 → 最后看 crewai 配置。九成问题在前两步就解决了。
7. 接入文档与后续动作
配置和排查都跑通后,建议把 key 管理、模型切换这些动作固定下来。需要看更细的接入说明,可以走接入文档:
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
如果你只是想快速验证某个模型名能不能通,用模型对话页面直接试:
- 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
如果你在搭长期的编码 Agent 或者多智能体流水线,模型调用量大、需要稳定通道,可以看 Coding Plan:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
回到这篇的核心:crewai 下 Litellm 的BadRequestError,绝大多数情况就是模型名少了个openai/前缀,加上就好。base_url 停在版本层,别多写路径。先用 curl 验通道,再用 Litellm 验前缀,最后跑 crewai,问题定位会快很多。