关于 test-crewai-project
这是一份 CrewAI crew 和 Flow 测试指南,面向需要在无 API 密钥时进行确定性测试并检查真实运行的开发者。内容涵盖 stub LLM 测试框架、提示词与路由断言、guardrail 测试,以及 `crewai test` 的行为。
主要功能
- 01使用 stub LLM 测试 crew
- 02断言提示词内容和工具执行
- 03涵盖 guardrail 与 Flow 路由测试
- 04说明 `crewai test` 的评估行为
- 05介绍事件和追踪方面的注意事项
适用场景
- 01在 CI 中运行离线单元测试
- 02检查提示词输入和任务上下文
- 03测试 guardrail 的接受与重试
- 04调查 CrewAI 评估运行结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ crewaiinc/ skills --skill test-crewai-project安装后在 Claude Code 中输入/test-crewai-project确认已加载。
附带文件references/example-tests.mdreferences/stub_llm.py
详细介绍
测试 CrewAI 项目
了解如何确定性地测试 crew 和 flow,无需 API 密钥;以及如何通过事件和追踪调试实际运行。
已于 2026-10-01 针对 crewai 1.15.22 和 1.15.23 验证。
已于 2026-10-01 使用真实大模型进行实测。
先运行 crewai version;如果主版本号或次版本号不同,请使用 ask-docs 技能重新验证与版本相关的表格行。
1. 为什么测试需要桩 LLM
真实 LLM 每次运行都会生成不同的文本,需要花钱,还需要密钥。每个未显式指定 llm 的 Agent 都会解析为 gpt-4.1-mini,如果没有设置密钥,就会在启动时因 ValueError: OPENAI_API_KEY is required 而失败,因此没有密钥的 CI 运行甚至无法启动。你无法对无法预测的输出进行断言。
桩 LLM 会固定响应,因此断言可以精确匹配。它保留了所有真实的 crewai 代码路径:提示词构建、{placeholder} 插值、任务上下文、工具执行、护栏重试、pydantic 转换和 flow 路由。它还会记录每次调用,因此你可以断言 crewai 构建的提示词。这正是大多数错误出现的地方:缺少输入、上下文丢失、工具从未列出。
分层测试:
| 层级 | 需要 | 验证内容 |
|---|---|---|
| 单元测试:桩 LLM、纯 pytest | 无 | wiring、提示词、路由、解析、护栏逻辑 |
| 护栏真值表 | 无 | 每个护栏都能接受和拒绝正确的输出 |
真实模型运行(crewai test、冒烟启动) | 提供方密钥、费用 | 提示词质量;为这些测试标记 @pytest.mark.credentialed,并使用 pytest -m "not credentialed" 运行离线测试套件 |
2. 1.15 中自定义 BaseLLM 的约定
from crewai import BaseLLM。唯一的抽象方法是 call。crewai 会将 from_task、from_agent 和(对于结构化任务)response_model 作为关键字参数传入,因此 call 必须接受这些参数。
call 签名 | 结果 | |
|---|---|---|
| 错误(公开的自定义 LLM 文档示例) | def call(self, messages, tools=None, callbacks=None, available_functions=None) | 启动时出现 TypeError: MyLLM.call() got an unexpected keyword argument 'from_task' |
| 正确 | def call(self, messages, tools=None, callbacks=None, available_functions=None, from_task=None, from_agent=None, response_model=None, **kwargs) | 运行 |
| 要求 | 如果跳过会发生什么 |
|---|---|
接受 from_task、from_agent、response_model(或 **kwargs) | 首次调用 LLM 时出现 TypeError |
传入非空的 model=(例如 MyLLM(model="my-model")) | 构造时出现 ValidationError |
在 with llm_call_context(): 内调用 self._emit_call_started_event(...) / self._emit_call_completed_event(...)(来自 crewai.llms.base_llm) | 事件监听器和追踪永远无法看到 LLM 调用(见第 9 节) |
如果将 messages 传递给提供方 SDK,只转发 role 和 content | crewai 会向每条消息添加 cache_breakpoint: True;Anthropic API 会因此拒绝请求,返回 400 ... messages.0.cache_breakpoint: Extra inputs are not permitted |
supports_function_calling() 返回 False | crewai 会对工具使用 ReAct 文本循环(Thought / Action / Action Input) |
传入 response_model 时返回经过验证的模型实例 | 原生提供方会这样做;返回文本也可以,但请参见第 6 节,了解此时护栏会如何读取 output.pydantic |
references/stub_llm.py 实现了以上所有内容。直接复制使用;除非有需要,否则不要自行编写。
3. 设置测试框架
- 将
references/stub_llm.py复制到项目中的tests/stub_llm.py。 - 执行
uv add --dev pytest,然后使用uv run pytest运行测试。 - 不要将
uv run test用作 pytest。在脚手架生成的 crew 项目中,test是一个项目脚本,会运行crewai test(第 8 节),并调用真实模型。 - 将
tests/.crewai-test-storage/添加到.gitignore。
# tests/conftest.py
import os
import socket
from pathlib import Path
# Optional, test-only switches. The AGENTS.md that `crewai create` writes leaves
# turning observability off to the user, so ask before uncommenting them.
# Without them the socket guard below does not cover everything:
# - crewai telemetry is still sent to telemetry.crewai.com when the test process
# exits, after the guard has been undone;
# - if tracing is on (CREWAI_TRACING_ENABLED=true or tracing=True) and a CrewAI
# credential exists (CREWAI_USER_PAT, or just a saved `crewai login` on the
# machine), kickoff requests a trace grant, the guard refuses it, and the test
# fails with TraceGrantError.
# If uncommented, they must run before crewai is imported; never put them in the project's .env.
# os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
# os.environ["CREWAI_TRACING_ENABLED"] = "false"
# os.environ["OTEL_SDK_DISABLED"] = "true"
# Absolute path: crewai's SQLite files and memory store go here instead of your user data dir.
os.environ["CREWAI_STORAGE_DIR"] = str(Path(__file__).parent / ".crewai-test-storage")
import pytest
@pytest.fixture(autouse=True)
def offline(monkeypatch, tmp_path):
def refuse(*args, **kwargs):
raise RuntimeError("test tried to use the network")
monkeypatch.setattr(socket.socket, "connect", refuse)
monkeypatch.chdir(tmp_path) # output_file= and flow file writes land in a temp dir
每行代码的作用:
- 单独执行
import crewai就会创建<platform user data dir>/<cwd name>/。在 macOS 上,该目录为~/Library/Application Support/<cwd name>/。Kickoff 会在其中写入 SQLite 文件。在导入前设置绝对路径的CREWAI_STORAGE_DIR,即可重定向所有文件。 - Socket 防护会让任何意外的真实 LLM 调用立即失败,而不是一直挂起或产生费用。
- crewai 自身的内部代码会在每次 kickoff 时发出
DeprecationWarning。若要屏蔽这些警告:
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = ["ignore::DeprecationWarning:crewai.*"]
4. 驱动 Stub
StubLLM 按以下顺序从三个来源中选择每次响应:
| 来源 | 用途 |
|---|---|
responses=[...] | 每次 LLM 调用对应一个条目,按调用顺序使用。条目可以是 str、dict/list(以 JSON 发送)、pydantic 模型,或 f(messages, response_model) |
responder=f(messages, response_model) | 队列耗尽后使用。根据提示词内容或架构决定响应,而不是按调用顺序决定 |
default_response="..." | 前两者之后使用。如果未设置,并且 crewai 要求提供 response_model,Stub 会返回该模型的骨架 JSON |
工具调用会经过 ReAct 循环。tool_call(name, args) 会构建 Thought/Action/Action Input 文本。实际工具的 _run 会执行,其结果会作为 Observation: ... 返回给下一次 LLM 调用。
# tests/test_structured_and_tools.py
from pydantic import BaseModel
from crewai import Agent, Crew, Task
from crewai.tools import tool
from stub_llm import StubLLM, prompt_text, tool_call
class Brief(BaseModel):
title: str
bullets: list[str]
tool_inputs = []
@tool("Word Counter")
def word_counter(text: str) -> int:
"""Count the words in a piece of text."""
tool_inputs.append(text)
return len(text.split())
def test_tool_call_then_structured_answer():
llm = StubLLM(responses=[
tool_call("Word Counter", {"text": "bees make honey"}), # researcher: use the tool
"Final Answer: 3 words", # researcher: answer
{"title": "Bees", "bullets": ["honey"]}, # writer: JSON for Brief
])
researcher = Agent(role="Researcher", goal="Count words", backstory="Careful.",
tools=[word_counter], llm=llm)
writer = Agent(role="Writer", goal="Write briefs", backstory="Concise.", llm=llm)
count = Task(description="Count the words", expected_output="A count", agent=researcher)
brief = Task(description="Write a brief", expected_output="Title and bullets",
agent=writer, output_pydantic=Brief)
out = Crew(agents=[researcher, writer], tasks=[count, brief]).kickoff()
assert tool_inputs == ["bees make honey"] # the real tool ran
assert "Observation: 3" in prompt_text(llm.calls[1]) # its result went back to the LLM
assert llm.calls[0]["response_model"] is None # agents WITH tools get no response_model
assert llm.calls[2]["response_model"] is Brief
assert out.pydantic == Brief(title="Bees", bullets=["honey"])
让 Stub 测试保持稳定的规则:
| 规则 | 原因 |
|---|---|
为每个 output_pydantic / output_json 任务将有效 JSON(dict)加入队列 | 无法解析的答案会让 crewai 额外调用 LLM 转换器,这会消耗后续队列条目,随后还会引发 ConverterError |
output_pydantic 才会填充 out.pydantic | 单独使用 Task(response_model=X) 只会将架构发送给 LLM,但会让 out.pydantic 保持为 None |
使用 len(llm.calls) 统计调用次数,而不是 out.token_usage | Crew 的用量会按每个 Agent 在每个 LLM 对象生命周期内累计:两个 Agent 共享一个 Stub 时会被计数两次;在第二个 crew 中复用 Stub 时,也会带上第一个 crew 的 token 数 |
如果调用顺序难以预测,请使用 responder,并根据 response_model 或提示词文本决定响应 | 只有确定了准确的调用顺序,队列才适用 |
为 kickoff_for_each 创建全新的 crew | 对已 kickoff 过的 crew,每个条目都会静默复用上一次 kickoff 的输入 |
5. 测试 Crew 并断言提示词
让 LLM 可注入。对于工厂函数,接受 llm=None 并将其传给每个 Agent(生产环境中,None 表示使用 crewai 的默认模型)。对于脚手架生成的 @CrewBase 类,在 .crew() 之后替换 LLM:
# tests/test_crew.py
from pathlib import Path
from acme_research.crew import AcmeResearch
from stub_llm import StubLLM, prompt_text
def crew_with(llm):
crew = AcmeResearch().crew()
for agent in crew.agents:
agent.llm = llm
return crew
def test_research_crew_output_and_prompts():
llm = StubLLM(responses=["- bees pollinate crops", "# Bees report"])
out = crew_with(llm).kickoff(inputs={"topic": "bees", "current_year": "2026"})
assert out.raw == "# Bees report"
assert [t.raw for t in out.tasks_output] == ["- bees pollinate crops", "# Bees report"]
assert len(llm.calls) == 2
first = prompt_text(llm.calls[0])
assert "bees Senior Data Researcher" in first # {topic} filled in agents.yaml
assert "the current year is 2026" in first # {current_year} in tasks.yaml
assert "- bees pollinate crops" in prompt_text(llm.calls[1]) # task 1 output is task 2 context
assert Path("report.md").read_text() == "# Bees report" # output_file, in tmp cwd
llm.calls[i] 包含 messages(crewai 发送的系统消息和用户消息快照)、response_model、tools 和 response。prompt_text(call) 会拼接消息内容。以下内容值得断言:
| 断言 | 可发现的问题 |
|---|---|
| 输入值出现在提示词中 | kickoff 未提供 inputs(字面量 {topic} 会留在提示词中;缺少某个键的 dict 则会引发 ValueError) |
| 上一个任务的输出出现在下一个提示词中 | context= 配置错误 |
出现 Tool Name: <snake_case name> | 未附加工具。@tool("Word Counter") 会显示为 word_counter;Action: Word Counter 仍可解析 |
| 重试提示词中出现 guardrail 错误 | guardrail 的反馈没有传达到模型 |
某个分支中 llm.calls == [] | 本应不调用模型的 flow 分支却调用了模型 |
6. 测试护栏
函数护栏返回 (True, value) 表示接受,返回 (False, "error for the LLM") 表示拒绝。通过两种方式测试:将其作为普通函数,根据输出真值表进行测试;以及在 crew 中测试,以检查重试行为。如果护栏持续失败,经过 guardrail_max_retries 次重试(默认值为 3)后,会抛出 Task failed guardrail validation after N retries。真值表测试和放弃测试见 example-tests.md 第 1 节。
# tests/test_guardrails.py
from pydantic import BaseModel
from crewai import Agent, Crew, Task
from crewai.tasks.task_output import TaskOutput
from stub_llm import StubLLM, prompt_text
class Brief(BaseModel):
title: str
bullets: list[str]
def max_three_bullets(output: TaskOutput):
try: # output.pydantic can be None here, so fall back to parsing raw
brief = output.pydantic or Brief.model_validate_json(output.raw)
except ValueError:
return (False, "Return JSON matching the Brief schema.")
if len(brief.bullets) > 3:
return (False, f"Use at most 3 bullets, you used {len(brief.bullets)}.")
return (True, output.raw) # a string: crewai re-runs the pydantic conversion
def brief_crew(llm):
writer = Agent(role="Writer", goal="Write briefs", backstory="Concise.", llm=llm)
task = Task(description="Write a brief", expected_output="Title and up to 3 bullets",
agent=writer, output_pydantic=Brief,
guardrail=max_three_bullets, guardrail_max_retries=2)
return Crew(agents=[writer], tasks=[task])
def test_retry_feeds_the_error_back():
llm = StubLLM(responses=[{"title": "T", "bullets": ["a", "b", "c", "d"]},
{"title": "T", "bullets": ["a"]}])
out = brief_crew(llm).kickoff()
assert out.pydantic == Brief(title="T", bullets=["a"])
assert "Use at most 3 bullets, you used 4." in prompt_text(llm.calls[1])
有两个陷阱:
- 护栏内部的
output.pydantic可能为None。 当任务设置了护栏且大模型返回文本时,crewai 会跳过 pydantic 转换,直接调用护栏。这种情况会出现在 Agent 使用工具,或使用返回字符串的自定义大模型时。请自行解析output.raw。 return (True, output)可能会悄悄丢失out.pydantic。 如果output.pydantic为None,crewai 会原样保留该 TaskOutput。请返回(True, output.raw),让 crewai 转换字符串。然后在边界处断言out.pydantic is not None。
7. 测试 Flow
Flow 方法是普通方法,因此可以直接调用来测试某一步。使用 kickoff(inputs=...) 端到端测试路由。kickoff 会返回最后一个运行方法的返回值,并且在 @start 运行前,inputs 中的键会覆盖 state 字段。
# tests/test_flow_routing.py
from pydantic import BaseModel
from crewai.flow import Flow, listen, or_, router, start
class State(BaseModel):
topic: str = ""
log: list[str] = []
class TopicFlow(Flow[State]):
@start()
def normalize(self):
self.state.topic = self.state.topic.strip().lower()
@router(normalize)
def choose(self):
return "has_topic" if self.state.topic else "no_topic" # labels differ from method names
@listen("has_topic")
def research(self):
self.state.log.append("research")
@listen("no_topic")
def skip(self):
self.state.log.append("skip")
@listen(or_(research, skip))
def finish(self):
return f"done:{self.state.topic or '-'}"
def test_step_directly():
flow = TopicFlow()
flow.state.topic = " Bees "
flow.normalize()
assert flow.state.topic == "bees"
assert flow.choose() == "has_topic"
def test_both_branches_via_kickoff():
flow = TopicFlow()
assert flow.kickoff(inputs={"topic": " Bees "}) == "done:bees"
assert flow.state.log == ["research"]
flow = TopicFlow()
assert flow.kickoff(inputs={"topic": " "}) == "done:-"
assert flow.state.log == ["skip"]
如果某个 Flow 步骤自行构建 crew,就像 crewai create flow 脚手架通过 ContentCrew().crew() 所做的那样,那么就没有参数可以传入替身。使用 monkeypatch.setattr 将 crew 类的 crew 方法替换为一个包装器:包装器先调用原方法,然后对每个 Agent 设置 agent.llm = stub。完整测试见 example-tests.md 第 2 节。
Flow 是一个 pydantic 模型。如果 Flow 代码由你维护,在 Flow 类中声明 llm: Any = None 并传入 MyFlow(llm=stub),比打补丁更清晰。对名为 x 的方法使用 @listen("x") 会在实例化 Flow 时引发异常,因此只构建 Flow 的测试也能捕获该问题。
8. crewai test 实际做什么
crewai test 是由模型评分的评估工具,而不是单元测试运行器。
| 事实 | 详情 |
|---|---|
| 运行内容 | uv run test <n> <model>,即项目的 main.test(),它会调用 Crew.test(n_iterations, eval_llm=model, inputs=...) |
| 默认值 | -n 3、-m gpt-5.4-mini(旧版文档写的是 2 和 gpt-4o-mini) |
| 评判模型 | -m 可接受任何提供商字符串,尽管 --help 只提到 OpenAI 模型:当项目安装了相应提供商扩展(uv add "crewai[anthropic]")时,crewai test -m anthropic/claude-haiku-4-5 可以运行 |
| 每次迭代 | 对 crew 执行一次完整的真实 kickoff,使用各自的大模型,并为每项任务调用一次评判模型 |
| 评分 | 由 -m 模型驱动的评估 Agent 会为每项任务打 1 到 10 分,并打印 Tasks/Crew/Agents 表格:每项任务一行,还有一行 Crew、Execution Time (s)、每次运行一列,以及 Avg. Total |
| 需要 | crew 模型和评判模型的密钥。评判模型会在第一个任务运行后启动,因此缺少评判模型密钥时,该任务已经运行并产生费用:An error occurred while testing the crew: OPENAI_API_KEY is required,或在密钥无效时显示 Error code: 401 |
| 退出代码 | 即使运行失败也会返回 0,因此 CI 无法依赖此代码 |
| 副作用 | 它会替换所运行 crew 副本中每个任务的 callback;每项任务评分会记录一次 Sync handler error in on_crew_test_result,但不会导致运行失败 |
| Flow 项目 | Flow 脚手架没有 test 脚本,因此 uv run test 会执行 shell 的 test 内置命令:test: 1: unexpected operator |
可以用它定期针对真实大模型检查提示词质量。不要将其用于 CI,因为它会产生费用,且评分不确定。Crew.test 接受 BaseLLM 作为 eval_llm,因此为 crew 和评判模型都设置替身后,就能离线运行。此方式只检查连接是否正确(见 example-tests.md 第 4 节)。
9. 用于调试的事件监听器
每次启动都会在 crewai_event_bus 上发出类型化事件,包括 crew、任务、Agent、大模型调用、工具、工作流和记忆事件。请从 crewai.events 导入这些事件。crewai.utilities.events 已不存在。
继承 BaseEventListener,实现 setup_listeners(self, bus),并在其中使用 @bus.on(EventClass) 注册处理程序。每个处理程序的签名为 handler(source, event)。实例化监听器即可注册其处理程序。example-tests.md 的第 3 节包含一个调试监听器(ToolUsageFinishedEvent、LLMCallCompletedEvent、TaskFailedEvent)和一个记录器测试。
在测试中,请在 with crewai_event_bus.scoped_handlers(): 内注册监听器,以便之后移除它们。断言前请调用 crewai_event_bus.flush(),因为处理程序在工作线程上运行。
- 要调试实际运行,请将监听器放在单独的模块中,并从
main.py导入该模块,以便只实例化一次。其他有用的事件包括:LLMCallFailedEvent、MemorySaveFailedEvent、CrewKickoffStartedEvent、FlowFinishedEvent。 LLMCallCompletedEvent.response是文本;对于原生工具调用,它则是提供商的工具使用块列表。event.model和event.usage分别包含模型和令牌数。- 自定义
BaseLLM必须自行发出大模型调用事件。 内置提供商会发出LLMCallStartedEvent/LLMCallCompletedEvent。只返回文本的子类不会发出任何事件,而跟踪监听器订阅的也是相同事件,因此监听器和跟踪记录都会遗漏所有大模型调用。
10. 跟踪与遥测开关
这是两个独立的系统。遥测是发送给 CrewAI 的匿名使用情况统计数据。跟踪是可在 CrewAI AMP 中查看的执行跟踪(提示词、输入、输出、工具调用)。
| 变量 | 可接受的值(1.15.22-1.15.23) | 效果 |
|---|---|---|
CREWAI_DISABLE_TELEMETRY | true、1、yes、on(不区分大小写,去除首尾空格)会禁用遥测。false、0、no、off、空值会保持启用。其他值会被忽略并发出警告 | 关闭 CrewAI 遥测 |
CREWAI_DISABLE_TRACKING | 与上述相同 | 关闭 CrewAI 遥测 |
OTEL_SDK_DISABLED | 上述同一组值会禁用 CrewAI 遥测。只有 true(不区分大小写)还会阻止执行跟踪并禁用 OpenTelemetry SDK 本身 | 1 不是完整的关闭开关 |
CREWAI_TRACING_ENABLED | true / 1 强制开启跟踪。false / 0 强制关闭。yes、on、no、off 会被忽略,并回退到已保存的同意状态 | 单进程跟踪开关 |
Crew(...) 或 Flow 上的 tracing=True/False | 布尔值 | 优先于环境变量 |
跟踪优先级:tracing= 参数,然后是 CREWAI_TRACING_ENABLED,最后是通过 crewai traces enable|disable 保存的同意状态。已保存的同意状态按项目目录名称(或按 CREWAI_STORAGE_DIR)保存,因此重命名检出目录会重置它。crewai traces status 会显示环境变量(未设置时为 false)和已保存的同意状态,而不会显示你是否已登录。
规则:
- 仅在测试中禁用可观测性,并且只有在用户选择时才禁用。 将开关放在
tests/conftest.py或 CI 测试任务的环境中。切勿将其放在项目的.env、main.py或部署环境变量中,因为这会让生产环境调试失去可见性。 - 值请写作
true/false。true是所有禁用开关都会完整识别的唯一写法,而且CREWAI_TRACING_ENABLED会忽略yes/no/on/off。 - 如需调试单次运行,请使用
CREWAI_TRACING_ENABLED=true crewai run或tracing=True。跟踪发送到哪里取决于是否存在 CrewAI 凭据:CREWAI_USER_PAT或已保存的crewai login(无需 PAT)。- 有凭据: kickoff 会先请求跟踪授权,然后在有无终端的情况下,均会将跟踪上传到该登录账号下的 CrewAI AMP。1.15.23 随后会打印
View traces:链接;1.15.22 不会打印任何内容。如果授权请求失败,kickoff 会在 crew 运行前抛出TraceGrantError。 - 无凭据,1.15.23,有交互式终端: 开启跟踪即表示同意。跟踪会作为临时跟踪上传,不会提示,并会打印
Ephemeral Execution Traces链接。不会保存任何同意状态。 - 无凭据,1.15.22,有交互式终端: crewai 会在运行后询问
Share this execution trace with CrewAI? [y/N]。 - 无凭据,无终端(CI、pytest、
< /dev/null): 两个版本都会丢弃跟踪。 - 完全没有开关,首次运行: crewai 会在本地自动收集,并在终端中询问
Share this execution trace with CrewAI? [y/N] (20s timeout),然后保存回答作为同意状态。
- 有凭据: kickoff 会先请求跟踪授权,然后在有无终端的情况下,均会将跟踪上传到该登录账号下的 CrewAI AMP。1.15.23 随后会打印
- 从 1.15.23 开始,crewai 会对每个
BaseLLM子类的call添加速率限制重试:如果桩程序抛出的错误包含 "rate limit" 或 429,它会在约 3 秒内调用 3 次,然后才会抛出错误。若要测试自己的速率限制处理逻辑,请抛出其他类型的错误,或对重试后的调用次数进行断言。 - 跟踪包含任务文本、输入和输出。除非数据可以离开本机,否则请在跟踪运行中使用合成数据。
常见错误
| 症状 | 原因 | 修复方法 |
|---|---|---|
TypeError: ... call() got an unexpected keyword argument 'from_task' | 从文档示例中复制的自定义 LLM 函数签名 | 接受 from_task、from_agent、response_model、**kwargs |
CI 中出现 ValueError: OPENAI_API_KEY is required | 某个 Agent 保留了默认的 gpt-4.1-mini | 为每个 Agent 注入存根(第 5 节) |
| 测试挂起或产生费用 | 意外发起了真实的 LLM 调用 | conftest.py 中的套接字防护 |
uv run test 调用了 OpenAI | test 是脚手架中的 crewai test 脚本 | 使用 uv run pytest |
| 队列中的回答被分配给了错误的任务 | 额外的转换器调用或防护栏重试消耗了回答 | 将有效的 JSON 加入队列;使用基于 response_model 的 responder |
out.pydantic is None,运行却“成功”了 | 使用了 response_model=,却没有使用 output_pydantic=;或者防护栏返回了 (True, output) | 使用 output_pydantic;防护栏返回 (True, output.raw);断言其不为 None |
| 防护栏首次尝试就拒绝了有效的 JSON | 它读取了值为 None 的 output.pydantic | 解析 output.raw |
| 监听器或跟踪记录中没有 LLM 调用 | 自定义 LLM 不会发出事件 | 在 llm_call_context() 中发出事件(存根会这样做) |
| 监听器计数在各测试间不断增加 | 处理程序已全局注册 | 使用 crewai_event_bus.scoped_handlers() + flush() |
token_usage 是调用次数的两倍,或在测试间不断增加 | 多个 Agent 共用一个 LLM 实例,或多个 crew 共用同一个实例 | 每个 crew 使用一个全新的存根;断言 len(llm.calls) |
| 测试后用户数据目录下出现了新目录 | 导入时未设置 CREWAI_STORAGE_DIR | 在 conftest.py 顶部将其设置为绝对路径 |
pytest 中出现 TraceGrantError: AMP trace grant request failed (RuntimeError) | 跟踪已启用 + 已保存的 crewai login 凭据或 CREWAI_USER_PAT + 套接字防护 | 在 conftest.py 中设置 CREWAI_TRACING_ENABLED=false(仅限测试) |
CI 中 crewai test 显示“passed”,但没有评分结果 | 即使运行失败,它也会以 0 退出 | 检查其输出中的评分表 |
设置了 OTEL_SDK_DISABLED=1,跟踪仍在运行 | 1 只会禁用遥测;只有 true 也会阻止跟踪 | 设置 CREWAI_TRACING_ENABLED=false(仅限测试) |
检查清单
- 已从
references/stub_llm.py复制tests/stub_llm.py;已将 pytest 添加为开发依赖 -
conftest.py在导入 crewai 前设置了绝对路径的CREWAI_STORAGE_DIR并阻止网络访问;仅当用户选择启用可观测性开关时才取消注释 - 每个 Agent 的 LLM 都可注入(工厂参数、在
.crew()后替换,或修补 crew 类) - 每个结构化任务都会从存根获取有效的 JSON,且测试会断言
out.pydantic is not None - 至少有一项测试会断言提示词文本(输入、上下文、工具列表)
- 每个防护栏都有真值表测试和重试测试
- 每个 flow 路由分支都有 kickoff 测试;关键步骤也会被直接调用
- 真实模型测试已标记为
credentialed,且默认情况下不会在 CI 中运行 - 可观测性开关仅出现在测试配置中,不会出现在
.env或部署环境变量中
参考资料
- stub_llm.py - 离线确定性
StubLLM,以及tool_call()和prompt_text()辅助函数。复制到tests/中。 - 示例测试 - 防护栏真值表与重试耗尽、在某个步骤中构建其 crew 的脚手架流程、事件监听器测试,以及离线
Crew.test连线检查 - 公开文档:https://docs.crewai.com/en/concepts/event-listener、https://docs.crewai.com/en/observability/tracing、https://docs.crewai.com/en/telemetry
相关技能:
- check-crewai-api - 当前的 Agent/Task/Crew API、结构化输出字段、防护栏契约
- build-flow - 流状态、路由器、
or_/and_、持久化 - connect-tools-and-mcp - 构建由你的 stub 驱动的工具
- design-task - 编写值得测试的防护栏和 expected_output
- ask-docs - 当某个版本相关条目可能已变更时,查询实时文档