mcpField
T
test-crewai-project

test-crewai-project

github.com/crewaiinc测试与质量48735 次安装3 周前更新
代码仓库

关于 test-crewai-project

这是一份 CrewAI crew 和 Flow 测试指南,面向需要在无 API 密钥时进行确定性测试并检查真实运行的开发者。内容涵盖 stub LLM 测试框架、提示词与路由断言、guardrail 测试,以及 `crewai test` 的行为。

主要功能

  1. 01使用 stub LLM 测试 crew
  2. 02断言提示词内容和工具执行
  3. 03涵盖 guardrail 与 Flow 路由测试
  4. 04说明 `crewai test` 的评估行为
  5. 05介绍事件和追踪方面的注意事项

适用场景

  1. 01在 CI 中运行离线单元测试
  2. 02检查提示词输入和任务上下文
  3. 03测试 guardrail 的接受与重试
  4. 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 和 contentcrewai 会向每条消息添加 cache_breakpoint: True;Anthropic API 会因此拒绝请求,返回 400 ... messages.0.cache_breakpoint: Extra inputs are not permitted
supports_function_calling() 返回 Falsecrewai 会对工具使用 ReAct 文本循环(Thought / Action / Action Input)
传入 response_model 时返回经过验证的模型实例原生提供方会这样做;返回文本也可以,但请参见第 6 节,了解此时护栏会如何读取 output.pydantic

references/stub_llm.py 实现了以上所有内容。直接复制使用;除非有需要,否则不要自行编写。

3. 设置测试框架

  1. 将 references/stub_llm.py 复制到项目中的 tests/stub_llm.py。
  2. 执行 uv add --dev pytest,然后使用 uv run pytest 运行测试。
  3. 不要将 uv run test 用作 pytest。在脚手架生成的 crew 项目中,test 是一个项目脚本,会运行 crewai test(第 8 节),并调用真实模型。
  4. 将 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_usageCrew 的用量会按每个 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_TELEMETRYtrue、1、yes、on(不区分大小写,去除首尾空格)会禁用遥测。false、0、no、off、空值会保持启用。其他值会被忽略并发出警告关闭 CrewAI 遥测
CREWAI_DISABLE_TRACKING与上述相同关闭 CrewAI 遥测
OTEL_SDK_DISABLED上述同一组值会禁用 CrewAI 遥测。只有 true(不区分大小写)还会阻止执行跟踪并禁用 OpenTelemetry SDK 本身1 不是完整的关闭开关
CREWAI_TRACING_ENABLEDtrue / 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),然后保存回答作为同意状态。
  • 从 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 调用了 OpenAItest 是脚手架中的 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 或部署环境变量中

参考资料

相关技能:

  • check-crewai-api - 当前的 Agent/Task/Crew API、结构化输出字段、防护栏契约
  • build-flow - 流状态、路由器、or_/and_、持久化
  • connect-tools-and-mcp - 构建由你的 stub 驱动的工具
  • design-task - 编写值得测试的防护栏和 expected_output
  • ask-docs - 当某个版本相关条目可能已变更时,查询实时文档

评论

支持 Markdown