LangSmith pytest 插件让 Python 开发者可以将数据集和评估定义为 pytest 测试用例。
与标准评估流程相比,这在以下情况下很有用:
- * **每个示例需要不同的评估逻辑**:标准评估流程假设所有数据集示例的应用和评估器执行是一致的。对于更复杂的系统或全面的评估,特定系统子集可能需要使用特定输入类型和指标进行评估。这些异构评估更容易作为独立测试用例套件编写并一起跟踪。
- * **您想要断言二元期望**:在 LangSmith 中跟踪断言并在本地引发断言错误(例如在 CI 管道中)。测试工具在评估系统输出并断言其基本属性时很有帮助。
- * **您想要类似 pytest 的终端输出**:获取熟悉的 pytest 输出格式
- * **您已经使用 pytest 测试您的应用**:将 LangSmith 跟踪添加到现有的 pytest 工作流程
安装
此功能需要 Python SDK 版本 langsmith>=0.3.4.
如需额外功能如 富文本终端输出 和 测试缓存 install:
pip install -U "langsmith[pytest]"
uv add "langsmith[pytest]"
定义和运行测试
pytest 集成让您可以将数据集和评估器定义为测试用例。
要跟踪 LangSmith 中的测试,请添加 @pytest.mark.langsmith 装饰器。每个装饰后的测试用例都会同步到数据集示例。运行测试套件时,数据集将更新,并将创建一个新实验,每个测试用例对应一个结果。
###################### my_app/main.py ######################
from langsmith import traceable, wrappers
oai_client = wrappers.wrap_openai(openai.OpenAI())
@traceable
def generate_sql(user_query: str) -> str:
result = oai_client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": "Convert the user query to a SQL query."},
{"role": "user", "content": user_query},
],
)
return result.choices[0].message.content
###################### tests/test_my_app.py ######################
from langsmith import testing as t
def is_valid_sql(query: str) -> bool:
"""Return True if the query is valid SQL."""
return True # Dummy implementation
@pytest.mark.langsmith # <-- Mark as a LangSmith test case
def test_sql_generation_select_all() -> None:
user_query = "Get all users from the customers table"
t.log_inputs({"user_query": user_query}) # <-- Log example inputs, optional
expected = "SELECT * FROM customers;"
t.log_reference_outputs({"sql": expected}) # <-- Log example reference outputs, optional
sql = generate_sql(user_query)
t.log_outputs({"sql": sql}) # <-- Log run outputs, optional
t.log_feedback(key="valid_sql", score=is_valid_sql(sql)) # <-- Log feedback, optional
assert sql == expected # <-- Test pass/fail status automatically logged to LangSmith under 'pass' feedback key
运行此测试时,它将有一个默认的 pass boolean feedback key based on the test case passing / failing. It will also track any inputs, outputs, and reference (expected) outputs that you log.
使用 pytest 像往常一样运行测试:
pytest tests/
在大多数情况下,我们建议设置测试套件名称:
LANGSMITH_TEST_SUITE='SQL app tests' pytest tests/
每次运行此测试套件时,LangSmith 会:
- * 为每个测试文件创建一个 数据集 。如果该测试文件的数据集已存在,则会更新它
- * 创建一个 实验 in each created/updated dataset
- * 为每个测试用例创建一个实验行,包含您记录的输入、输出、参考输出和反馈
- * collects the pass/fail rate under the
pass每个测试用例的反馈键
测试套件数据集的外观如下:
!数据集
以及针对该测试套件的实验外观:
!实验
记录输入、输出和参考输出
每次运行测试时,我们都会将其同步到数据集示例并作为运行进行追踪。我们可以通过几种不同的方式来追踪示例输入和参考输出以及运行输出。最简单的方法是使用 log_inputs, log_outputs和 log_reference_outputs 方法。您可以在测试中随时运行这些方法来更新该测试的示例和运行:
from langsmith import testing as t
@pytest.mark.langsmith
def test_foo() -> None:
t.log_inputs({"a": 1, "b": 2})
t.log_reference_outputs({"foo": "bar"})
t.log_outputs({"foo": "baz"})
assert True
Running this test will create/update an example with name "test_foo",输入 {"a": 1, "b": 2},参考输出 {"foo": "bar"} 并追踪带有输出的运行 {"foo": "baz"}.
NOTE:如果您运行 log_inputs, log_outputs, or log_reference_outputs 两次,以前的值将被覆盖。
Another way to define example inputs and reference outputs is via pytest fixtures/parametrizations. By default any arguments to your test function will be logged as inputs on the corresponding example. If certain arguments are meant to represent reference outputs, you can specify that they should be logged as such using @pytest.mark.langsmith(output_keys=["name_of_ref_output_arg"]):
@pytest.fixture
def c() -> int:
return 5
@pytest.fixture
def d() -> int:
return 6
@pytest.mark.langsmith(output_keys=["d"])
def test_cd(c: int, d: int) -> None:
result = 2 * c
t.log_outputs({"d": result}) # Log run outputs
assert result == d
This will create/sync an example with name "test_cd", inputs {"c": 5} 并引用输出 {"d": 6},并运行输出 {"d": 10}.
记录反馈
By default LangSmith collects the pass/fail rate under the pass 每个测试用例的反馈键。您可以通过以下方式添加额外反馈 log_feedback.
from langsmith import wrappers
from langsmith import testing as t
oai_client = wrappers.wrap_openai(openai.OpenAI())
@pytest.mark.langsmith
def test_offtopic_input() -> None:
user_query = "what's up"
t.log_inputs({"user_query": user_query})
sql = generate_sql(user_query)
t.log_outputs({"sql": sql})
expected = "Sorry that is not a valid query."
t.log_reference_outputs({"sql": expected})
# Use this context manager to trace any steps used for generating evaluation
# feedback separately from the main application logic
with t.trace_feedback():
instructions = (
"Return 1 if the ACTUAL and EXPECTED answers are semantically equivalent, "
"otherwise return 0. Return only 0 or 1 and nothing else."
)
grade = oai_client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": instructions},
{"role": "user", "content": f"ACTUAL: {sql}\nEXPECTED: {expected}"},
],
)
score = float(grade.choices[0].message.content)
t.log_feedback(key="correct", score=score)
assert score
请注意 use of the trace_feedback() 上下文管理器的使用。这样,LLM-as-judge 调用就会与测试用例的其余部分分开追踪。它不会出现在主测试用例运行中,而是会出现在 correct 反馈键的追踪中。
NOTE:确保 log_feedback 与反馈追踪相关联的调用发生在 trace_feedback 上下文中。这样我们就能够将反馈与追踪关联起来,当在UI中看到反馈时,您可以点击它来查看生成它的追踪。
追踪中间调用
LangSmith会自动追踪测试用例执行过程中发生的任何可追踪的中间调用。
将测试分组到测试套件中
默认情况下,给定文件中的所有测试将被分组为一个带有相应数据集的单一"测试套件"。您可以通过传递 test_suite_name 参数到 @pytest.mark.langsmith 来进行逐个案例分组,或者您可以设置 LANGSMITH_TEST_SUITE 环境变量来将执行中的所有测试分组到一个测试套件中:
LANGSMITH_TEST_SUITE="SQL app tests" pytest tests/
我们通常建议设置 LANGSMITH_TEST_SUITE 以获取所有结果的综合视图。
命名实验
您可以使用以下方式为实验命名 LANGSMITH_EXPERIMENT 环境变量:
LANGSMITH_TEST_SUITE="SQL app tests" LANGSMITH_EXPERIMENT="baseline" pytest tests/
实验元数据
您可以为每次测试运行创建的实验(项目)附加自定义元数据。这对于跟踪给定实验使用的模型、提示版本或环境非常有用。
选项 1:Fixture(推荐)
在您的文件中定义一个会话级 fixture conftest.py:
# conftest.py
@pytest.fixture(scope="session")
def langsmith_experiment_metadata():
return {
"model": "gpt-4o",
"prompt_version": "v2.3",
"environment": os.environ.get("ENV", "local"),
}
该 fixture 是动态的(可以读取环境变量、调用函数等),并遵循 pytest 的 conftest.py 层级结构,因此可以按目录设置作用域。
选项 2:环境变量
设置 LANGSMITH_EXPERIMENT_METADATA to a JSON string. This is useful in CI/CD pipelines where you don't want to modify code:
LANGSMITH_EXPERIMENT_METADATA='{"model":"gpt-4o","env":"staging"}' pytest tests/
如果同时设置了 fixture 和环境变量,则 fixture 优先。系统管理的元数据键(如 revision_id 和 git 信息)始终优先于用户提供的键。
缓存
在 CI 中每次提交都调用 LLM 可能成本很高。为了节省时间和资源,LangSmith 允许您将 HTTP 请求缓存到磁盘。要启用缓存,请使用 langsmith[pytest] 安装并设置一个环境变量: LANGSMITH_TEST_CACHE=/my/cache/path:
pip install -U "langsmith[pytest]"
LANGSMITH_TEST_CACHE=tests/cassettes pytest tests/my_llm_tests
uv add "langsmith[pytest]"
LANGSMITH_TEST_CACHE=tests/cassettes pytest tests/my_llm_tests
所有请求都将被缓存到 tests/cassettes 并在后续运行中从该位置加载。如果您将此文件提交到仓库,您的 CI 也将能够使用缓存。
In langsmith>=0.4.10,您可以像这样有选择地为单个 URL 或主机名启用缓存:
@pytest.mark.langsmith(cached_hosts=["api.openai.com", "https://api.anthropic.com"])
def my_test():
...
pytest 功能
@pytest.mark.langsmith 旨在不打扰您,并与熟悉的工具配合良好 pytest features.
使用参数化 pytest.mark.parametrize
您可以使用 parametrize 装饰器与之前相同。这将为每个参数化的测试实例创建一个新的测试用例。
@pytest.mark.langsmith(output_keys=["expected_sql"])
@pytest.mark.parametrize(
"user_query, expected_sql",
[
("Get all users from the customers table", "SELECT * FROM customers"),
("Get all users from the orders table", "SELECT * FROM orders"),
],
)
def test_sql_generation_parametrized(user_query, expected_sql):
sql = generate_sql(user_query)
assert sql == expected_sql
Note: 随着参数化列表的增长,您可以考虑使用 evaluate() 来替代。这会对评估进行并行化处理,使其更容易控制单个实验和相应的数据集。
并行化 pytest-xdist
您可以使用 pytest-xdist 按照通常的方式并行化测试执行:
pip install -U pytest-xdist
pytest -n auto tests
uv add pytest-xdist
pytest -n auto tests
异步测试与 pytest-asyncio
@pytest.mark.langsmith 可以与同步或异步测试配合使用,因此您可以像以前一样运行异步测试。
监视模式与 pytest-watch
使用监视模式可以快速迭代测试。我们 *强烈* 建议仅在启用测试缓存(见下文)时使用此功能,以避免不必要的 LLM 调用:
pip install pytest-watch
LANGSMITH_TEST_CACHE=tests/cassettes ptw tests/my_llm_tests
uv add pytest-watch
LANGSMITH_TEST_CACHE=tests/cassettes ptw tests/my_llm_tests
丰富的输出
如果您想查看测试运行的 LangSmith 结果的丰富显示,您可以指定 --langsmith-output:
pytest --langsmith-output tests
Note: 此标志以前是 --output=langsmith in langsmith<=0.3.3 但已更新以避免与其他 pytest 插件冲突。
您将获得每个测试套件的精美表格,这些表格会随着结果上传到 LangSmith 而实时更新:
使用此功能的一些重要说明:
- * 确保您已安装
pip install -U "langsmith[pytest]" - * 丰富的输出目前不适用于
pytest-xdist
试运行模式
如果您想运行测试而不将结果同步到 LangSmith,您可以设置 LANGSMITH_TEST_TRACKING=false 在您的环境中。
LANGSMITH_TEST_TRACKING=false pytest tests/
测试将正常运行,但实验日志不会发送到 LangSmith。
期望值
LangSmith 提供了一个 expect 工具来帮助定义关于 LLM 输出的期望。例如:
from langsmith import expect
@pytest.mark.langsmith
def test_sql_generation_select_all():
user_query = "Get all users from the customers table"
sql = generate_sql(user_query)
expect(sql).to_contain("customers")
这会将二进制的"期望"分数记录到实验结果中,另外 assert表示期望已满足,可能触发测试失败。
expect 还提供"模糊匹配"方法。例如:
@pytest.mark.langsmith(output_keys=["expectation"])
@pytest.mark.parametrize(
"query, expectation",
[
("what's the capital of France?", "Paris"),
],
)
def test_embedding_similarity(query, expectation):
prediction = my_chatbot(query)
expect.embedding_distance(
# This step logs the distance as feedback for this run
prediction=prediction, expectation=expectation
# Adding a matcher (in this case, 'to_be_*"), logs 'expectation' feedback
).to_be_less_than(0.5) # Optional predicate to assert against
expect.edit_distance(
# This computes the normalized Damerau-Levenshtein distance between the two strings
prediction=prediction, expectation=expectation
# If no predicate is provided below, 'assert' isn't called, but the score is still logged
)
此测试用例将被分配 4 个分数:
- 预测值与期望值之间的
embedding_distance余弦相似度 - 二进制
expectation分数(如果余弦距离小于 0.5 则为 1,否则为 0) - 预测值与期望值之间的
edit_distance语义相似度 - The overall test pass/fail score (binary)
该 expect 工具效仿了 Jest的 expect API,并添加了一些现成的功能,使您更容易对 LLM 进行评分。
旧版
@test / @unit 装饰器
标记测试用例的旧方法是使用 @test or @unit decorators:
from langsmith import test
@test
def test_foo() -> None:
pass