主题
如何用 pytest 运行评估
LangSmith pytest 插件让 Python 开发者把他们的数据集和评估定义为 pytest 测试用例。
与标准评估流程相比,这在以下情形很有用:
- 每个示例需要不同的评估逻辑:标准评估流程假定所有数据集示例上应用和评估器的执行是一致的。对于更复杂的系统或更全面的评估,特定的系统子集可能需要用特定的输入类型和指标来评估。这些异构评估作为彼此跟踪的独立测试用例套件来写更简单。
- 你想断言二元期望:在 LangSmith 中跟踪断言,并在本地(例如在 CI 流水线中)抛出断言错误。当既要评估系统输出、又要对其断言基本属性时,测试工具有所帮助。
- 你想要 pytest 风格的终端输出:获得熟悉的 pytest 输出格式
- 你已经用 pytest 测试你的应用:为现有 pytest 工作流添加 LangSmith 跟踪
INFO
JS/TS SDK 有一个类似的 Vitest/Jest 集成。
安装
此功能需要 Python SDK 版本 langsmith>=0.3.4。
bash
pip install -U "langsmith[pytest]"bash
uv add "langsmith[pytest]"定义并运行测试
pytest 集成让你把数据集和评估器定义为测试用例。
要在 LangSmith 中跟踪一个测试,加上 @pytest.mark.langsmith 装饰器。每个被装饰的测试用例都会被同步到一个数据集示例。当你运行测试套件时,数据集会被更新,并创建一个新实验,为每个测试用例一条结果。
python
###################### my_app/main.py ######################
import openai
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 ######################
import pytest
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 布尔反馈键。它还会跟踪你记录的任何输入、输出和参考(预期)输出。
像平常一样用 pytest 运行测试:
bash
pytest tests/多数情况下我们建议设置一个测试套件名:
bash
LANGSMITH_TEST_SUITE='SQL app tests' pytest tests/每次你运行这个测试套件,LangSmith:
- 为每个测试文件创建一个数据集。若该测试文件的数据集已存在,会被更新
- 在每个创建/更新的数据集中创建一个实验
- 为每个测试用例创建一个实验行,带你所记录的输入、输出、参考输出和反馈
- 在每个测试用例的
pass反馈键下收集通过/失败率
这是一个测试套件数据集的样子:

以及针对那个测试套件的实验的样子:

记录输入、输出和参考输出
我们每次运行一个测试,都会把它同步到一个数据集示例并作为一次运行追踪。有几种不同的方式可以追踪示例输入和参考输出以及运行输出。最简单的是用 log_inputs、log_outputs 和 log_reference_outputs 方法。你可以在测试中的任何时候运行它们来更新该测试的示例和运行:
python
import pytest
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运行这个测试会创建/更新一个名为 "test_foo" 的示例,输入 {"a": 1, "b": 2},参考输出 {"foo": "bar"},并追踪一条带输出 {"foo": "baz"}。
注意:如果你运行 log_inputs、log_outputs 或 log_reference_outputs 两次,前面的值会被覆盖。
定义示例输入和参考输出的另一种方式是通过 pytest fixtures/参数化。默认情况下,你测试函数的任何参数都会被作为输入记录在对应的示例上。若某些参数意在表示参考输出,你可以用以下内容指定它们应被如此记录:@pytest.mark.langsmith(output_keys=["name_of_ref_output_arg"]):
python
import pytest
@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这会创建/同步一个名为 "test_cd" 的示例,输入 {"c": 5},参考输出 {"d": 6},运行输出 {"d": 10}。
记录反馈
默认情况下,LangSmith 在每个测试用例的 pass 反馈键下收集通过/失败率。你可以用 log_feedback 添加额外反馈。
python
import openai
import pytest
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注意对 trace_feedback() 上下文管理器的使用。这使 LLM-as-judge 调用与测试用例其余部分分开追踪。它不会出现在主测试用例运行中,而是出现在 correct 反馈键的追踪中。
注意:确保与反馈追踪关联的 log_feedback 调用发生在 trace_feedback 上下文内部。这样我们能把反馈与追踪关联,当你在 UI 中看到反馈时,你能点击它查看产生它的那条追踪。
追踪中间调用
LangSmith 会自动追踪测试用例执行过程中发生的任何可追踪的中间调用。
把测试分组到一个测试套件
默认情况下,一个给定文件内的所有测试都被分组为单个"测试套件",带一个对应的数据集。你可以通过向 @pytest.mark.langsmith 传入 test_suite_name 参数来逐例分组地配置一个测试属于哪个测试套件,或者设置 LANGSMITH_TEST_SUITE 环境变量,把一次执行的所有测试分组到单个测试套件:
bash
LANGSMITH_TEST_SUITE="SQL app tests" pytest tests/我们通常建议设置 LANGSMITH_TEST_SUITE 以获得对你所有结果的一个合并视图。
命名实验
你可以用 LANGSMITH_EXPERIMENT 环境变量给一个实验命名:
bash
LANGSMITH_TEST_SUITE="SQL app tests" LANGSMITH_EXPERIMENT="baseline" pytest tests/实验元数据
你可以把自定义元数据附加到每次测试运行所创建的实验(项目)上。这对跟踪某个实验用了哪个模型、提示版本或环境很有用。
选项 1:Fixture(推荐)
在你的 conftest.py 中定义一个 session 作用域的 fixture:
python
# conftest.py
import os
import pytest
@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 设为一个 JSON 字符串。这在你不想修改代码的 CI/CD 流水线中很有用:
bash
LANGSMITH_EXPERIMENT_METADATA='{"model":"gpt-4o","env":"staging"}' pytest tests/如果 fixture 和环境变量都设置了,fixture 优先。系统管理的元数据键(如 revision_id 和 git 信息)总是优先于用户提供的键。
NOTE
此功能需要 langsmith>=0.7.13。
缓存
在 CI 中为每个 commit 跑 LLM 可能很昂贵。为节省时间和资源,LangSmith 让你把 HTTP 请求缓存到磁盘。要启用缓存,用 langsmith[pytest] 安装并设一个环境变量:LANGSMITH_TEST_CACHE=/my/cache/path:
bash
pip install -U "langsmith[pytest]"
LANGSMITH_TEST_CACHE=tests/cassettes pytest tests/my_llm_testsbash
uv add "langsmith[pytest]"
LANGSMITH_TEST_CACHE=tests/cassettes pytest tests/my_llm_tests所有请求都会被缓存到 tests/cassettes 并在后续运行从那里加载。如果你把它检入你的仓库,你的 CI 也能使用该缓存。
在 langsmith>=0.4.10 中,你可以像这样选择性地为到单个 URL 或主机名的请求启用缓存:
python
@pytest.mark.langsmith(cached_hosts=["api.openai.com", "https://api.anthropic.com"])
def my_test():
...pytest 特性
@pytest.mark.langsmith 被设计为不打扰你,并与熟悉的 pytest 特性配合良好。
用 pytest.mark.parametrize 参数化
你可以像以前一样使用 parametrize 装饰器。这会为每个参数化的测试实例创建一个新测试用例。
python
@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注意: 随着参数化列表增长,你可能考虑改用 evaluate()。它并行化评估,并让控制单个实验和对应数据集更容易。
用 pytest-xdist 并行化
你可以像平常一样用 pytest-xdist 并行化测试执行:
bash
pip install -U pytest-xdist
pytest -n auto testsbash
uv add pytest-xdist
pytest -n auto tests用 pytest-asyncio 的异步测试
@pytest.mark.langsmith 支持同步或异步测试,因此你可以像以前一样运行异步测试。
用 pytest-watch 的 watch 模式
用 watch 模式快速迭代你的测试。我们强烈建议只在启用测试缓存(见下文)的情况下使用它,以避免不必要的 LLM 调用:
bash
pip install pytest-watch
LANGSMITH_TEST_CACHE=tests/cassettes ptw tests/my_llm_testsbash
uv add pytest-watch
LANGSMITH_TEST_CACHE=tests/cassettes ptw tests/my_llm_tests富输出
如果你想看到你测试运行的 LangSmith 结果的富展示,可以指定 --langsmith-output:
bash
pytest --langsmith-output tests注意: 在 langsmith<=0.3.3 中,这个标志曾经是 --output=langsmith,但已更新以避免与其他 pytest 插件冲突。
你会得到每个测试套件一个漂亮的表格,随结果上传到 LangSmith 而实时更新:

使用此功能的一些重要说明:
- 确保你已安装
pip install -U "langsmith[pytest]" - 富输出目前不能与
pytest-xdist配合工作
NOTE
自定义输出会移除所有标准 pytest 输出。如果你在调试某些意外行为,通常展示常规 pytest 输出以获取完整的错误回溯更好。
试运行模式
如果你想在不把结果同步到 LangSmith 的情况下运行测试,你可以在环境中设置 LANGSMITH_TEST_TRACKING=false。
bash
LANGSMITH_TEST_TRACKING=false pytest tests/测试会照常运行,但实验日志不会发送到 LangSmith。
期望
LangSmith 提供一个 expect 工具,帮你定义关于你 LLM 输出的期望。例如:
python
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")这会把二元的"expectation"分数记录到实验结果,并额外 assert 期望被满足,可能触发一个测试失败。
expect 还提供"模糊匹配"方法。例如:
python
@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 - 整体测试通过/失败分数(二元)
expect 工具仿照 Jest 的 expect API 建模,带一些现成功能以使对你的 LLM 打分更容易。
遗留
@test / @unit 装饰器
标记测试用例的遗留方法是使用 @test 或 @unit 装饰器:
python
from langsmith import test
@test
def test_foo() -> None:
pass