LangSmith 提供与 Vitest 和 Jest 的集成,允许 JavaScript 和 TypeScript 开发人员使用熟悉的语法定义其数据集并进行评估。
与 evaluate() 评估流程相比,当出现以下情况时,此方法非常有用:
- 每个示例需要不同的评估逻辑
- 您希望断言二元预期,并且在 LangSmith 中跟踪这些断言并在本地(例如在 CI 管道中)引发断言错误
- 您希望利用 Vitest/Jest 生态系统中的模拟、监视模式、本地结果或其他功能
需要 JS/TS SDK 版本 langsmith>=0.3.1。
Vitest/Jest 集成处于测试阶段,可能会在未来的版本中进行更改。
按如下方式设置集成。请注意,虽然您可以使用现有的测试配置文件将 LangSmith 评估与其他单元测试(作为标准 *.test.ts 文件)一起添加,但以下示例还将设置一个单独的测试配置文件和命令来运行您的评估。它将假定您的测试文件以 .eval.ts 结尾。 这确保了自定义测试报告器和其他 LangSmith 接触点不会修改您现有的测试输出。Vitest
如果尚未安装,请安装所需的开发依赖项
yarn add -D vitest dotenv
以下示例还需要 openai(当然还有 langsmith!)作为依赖项
yarn add langsmith openai
然后创建一个单独的 ls.vitest.config.ts 文件,其中包含以下基本配置
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.eval.?(c|m)[jt]s"],
reporters: ["langsmith/vitest/reporter"],
setupFiles: ["dotenv/config"],
},
});
include 确保只运行项目中以 eval.ts 某种变体结尾的文件
reporters 负责如上所示地美化您的输出
setupFiles 在运行您的评估之前运行 dotenv 以加载环境变量
目前不支持 JSDom 环境。您应该从配置中省略 "environment" 字段或将其设置为 "node"。
最后,将以下内容添加到您的 package.json 的 scripts 字段中,以使用您刚刚创建的配置运行 Vitest
{
"name": "YOUR_PROJECT_NAME",
"scripts": {
"eval": "vitest run --config ls.vitest.config.ts"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
请注意,上述脚本禁用了 Vitest 默认的监视模式以运行评估,因为许多评估器可能包含耗时较长的 LLM 调用。
Jest
如果尚未安装,请安装所需的开发依赖项
以下示例还需要 openai(当然还有 langsmith!)作为依赖项
yarn add langsmith openai
以下设置说明适用于基本的 JS 文件和 CJS。要添加对 TypeScript 和 ESM 的支持,请参阅 Jest 的官方文档或使用 Vitest。
然后创建一个名为 ls.jest.config.cjs 的单独配置文件
module.exports = {
testMatch: ["**/*.eval.?(c|m)[jt]s"],
reporters: ["langsmith/jest/reporter"],
setupFiles: ["dotenv/config"],
};
testMatch 确保只运行项目中以 eval.js 某种变体结尾的文件
reporters 负责如上所示地美化您的输出
setupFiles 在运行您的评估之前运行 dotenv 以加载环境变量
目前不支持 JSDom 环境。您应该从配置中省略 "testEnvironment" 字段或将其设置为 "node"。
最后,将以下内容添加到您的 package.json 的 scripts 字段中,以使用您刚刚创建的配置运行 Jest
{
"name": "YOUR_PROJECT_NAME",
"scripts": {
"eval": "jest --config ls.jest.config.cjs"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
定义并运行评估
现在,您可以使用熟悉的 Vitest/Jest 语法将评估定义为测试,但有一些注意事项
- 您应该从
langsmith/jest 或 langsmith/vitest 入口点导入 describe 和 test
- 您必须将测试用例包装在
describe 块中
- 在声明测试时,签名略有不同——有一个额外的参数包含示例输入和预期输出
通过创建一个名为 sql.eval.ts(如果使用不带 TypeScript 的 Jest,则为 sql.eval.js)的文件并粘贴以下内容来尝试一下
import * as ls from "langsmith/vitest";
import { expect } from "vitest";
// import * as ls from "langsmith/jest";
// import { expect } from "@jest/globals";
import OpenAI from "openai";
import { traceable } from "langsmith/traceable";
import { wrapOpenAI } from "langsmith/wrappers/openai";
// Add "openai" as a dependency and set OPENAI_API_KEY as an environment variable
const tracedClient = wrapOpenAI(new OpenAI());
const generateSql = traceable(
async (userQuery: string) => {
const result = await tracedClient.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content:
"Convert the user query to a SQL query. Do not wrap in any markdown tags.",
},
{
role: "user",
content: userQuery,
},
],
});
return result.choices[0].message.content;
},
{ name: "generate_sql" }
);
ls.describe("generate sql demo", () => {
ls.test(
"generates select all",
{
inputs: { userQuery: "Get all users from the customers table" },
referenceOutputs: { sql: "SELECT * FROM customers;" },
},
async ({ inputs, referenceOutputs }) => {
const sql = await generateSql(inputs.userQuery);
ls.logOutputs({ sql }); // <-- Log run outputs, optional
expect(sql).toEqual(referenceOutputs?.sql); // <-- Assertion result logged under 'pass' feedback key
}
);
});
您可以将每个 ls.test() 用例视为对应一个数据集示例,并将 ls.describe() 视为定义一个 LangSmith 数据集。如果您在运行测试套件时设置了 LangSmith 跟踪环境变量,SDK 会执行以下操作
- 如果在 LangSmith 中不存在,则创建与传递给
ls.describe() 的名称相同名称的数据集
- 如果不存在匹配的示例,则为传递到测试用例的每个输入和预期输出在数据集中创建示例
- 创建新的实验,每个测试用例一个结果
- 收集每个测试用例在
pass 反馈键下的通过/失败率
当您运行此测试时,它将根据测试用例通过/失败具有默认的 pass 布尔反馈键。它还将跟踪您使用 ls.logOutputs() 记录的任何输出或从测试函数返回的任何输出,作为实验中应用程序的“实际”结果值。 如果您还没有 .env 文件,请创建一个,其中包含您的 OPENAI_API_KEY 和 LangSmith 凭据:OPENAI_API_KEY="YOUR_KEY_HERE"
LANGSMITH_API_KEY="YOUR_LANGSMITH_KEY"
LANGSMITH_TRACING="true"
现在使用我们在上一步中设置的 eval 脚本来运行测试
您声明的测试应该会运行! 完成后,如果您已设置 LangSmith 环境变量,您应该会看到一个链接,指向在 LangSmith 中创建的实验以及测试结果。 以下是针对该测试套件的实验示例: 
追踪反馈
默认情况下,LangSmith 会收集每个测试用例在 pass 反馈键下的通过/失败率。您可以使用 ls.logFeedback() 或 wrapEvaluator() 添加额外的反馈。为此,请尝试将以下内容作为您的 sql.eval.ts 文件(如果您使用不带 TypeScript 的 Jest,则为 sql.eval.js)
import * as ls from "langsmith/vitest";
// import * as ls from "langsmith/jest";
import OpenAI from "openai";
import { traceable } from "langsmith/traceable";
import { wrapOpenAI } from "langsmith/wrappers/openai";
// Add "openai" as a dependency and set OPENAI_API_KEY as an environment variable
const tracedClient = wrapOpenAI(new OpenAI());
const generateSql = traceable(
async (userQuery: string) => {
const result = await tracedClient.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content:
"Convert the user query to a SQL query. Do not wrap in any markdown tags.",
},
{
role: "user",
content: userQuery,
},
],
});
return result.choices[0].message.content ?? "";
},
{ name: "generate_sql" }
);
const myEvaluator = async (params: {
outputs: { sql: string };
referenceOutputs: { sql: string };
}) => {
const { outputs, referenceOutputs } = params;
const instructions = [
"Return 1 if the ACTUAL and EXPECTED answers are semantically equivalent, ",
"otherwise return 0. Return only 0 or 1 and nothing else.",
].join("\n");
const grade = await tracedClient.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: instructions,
},
{
role: "user",
content: `ACTUAL: ${outputs.sql}\nEXPECTED: ${referenceOutputs?.sql}`,
},
],
});
const score = parseInt(grade.choices[0].message.content ?? "");
return { key: "correctness", score };
};
ls.describe("generate sql demo", () => {
ls.test(
"generates select all",
{
inputs: { userQuery: "Get all users from the customers table" },
referenceOutputs: { sql: "SELECT * FROM customers;" },
},
async ({ inputs, referenceOutputs }) => {
const sql = await generateSql(inputs.userQuery);
ls.logOutputs({ sql });
const wrappedEvaluator = ls.wrapEvaluator(myEvaluator);
// Will automatically log "correctness" as feedback
await wrappedEvaluator({
outputs: { sql },
referenceOutputs,
});
// You can also manually log feedback with `ls.logFeedback()`
ls.logFeedback({
key: "harmfulness",
score: 0.2,
});
}
);
ls.test(
"offtopic input",
{
inputs: { userQuery: "whats up" },
referenceOutputs: { sql: "sorry that is not a valid query" },
},
async ({ inputs, referenceOutputs }) => {
const sql = await generateSql(inputs.userQuery);
ls.logOutputs({ sql });
const wrappedEvaluator = ls.wrapEvaluator(myEvaluator);
// Will automatically log "correctness" as feedback
await wrappedEvaluator({
outputs: { sql },
referenceOutputs,
});
// You can also manually log feedback with `ls.logFeedback()`
ls.logFeedback({
key: "harmfulness",
score: 0.2,
});
}
);
});
请注意在 myEvaluator 函数周围使用了 ls.wrapEvaluator()。这使得 LLM-as-judge 调用与测试用例的其余部分分开跟踪,以避免混乱,并且如果包装函数的返回值与 { key: string; score: number | boolean } 匹配,则方便地创建反馈。在这种情况下,评估器跟踪将不会出现在主测试用例运行中,而是出现在与 correctness 反馈键关联的跟踪中。 您可以通过单击 UI 中相应的反馈芯片在 LangSmith 中查看评估器运行。针对测试用例运行多个示例
您可以使用 ls.test.each() 对多个示例运行相同的测试用例并参数化您的测试。当您想以相同的方式针对不同的输入评估应用程序时,这非常有用
import * as ls from "langsmith/vitest";
// import * as ls from "langsmith/jest";
const DATASET = [
{
inputs: { userQuery: "whats up" },
referenceOutputs: { sql: "sorry that is not a valid query" }
},
{
inputs: { userQuery: "what color is the sky?" },
referenceOutputs: { sql: "sorry that is not a valid query" }
},
{
inputs: { userQuery: "how are you today?" },
referenceOutputs: { sql: "sorry that is not a valid query" }
}
];
ls.describe("generate sql demo", () => {
ls.test.each(DATASET)(
"offtopic inputs",
async ({ inputs, referenceOutputs }) => {
...
},
);
});
如果您启用了跟踪,本地数据集中的每个示例都将同步到 LangSmith 中创建的示例。
日志输出
每次我们运行测试时,都会将其同步到一个数据集示例并将其作为运行进行跟踪。要跟踪运行的最终输出,您可以使用 ls.logOutputs(),如下所示
import * as ls from "langsmith/vitest";
// import * as ls from "langsmith/jest";
ls.describe("generate sql demo", () => {
ls.test(
"offtopic input",
{
inputs: { userQuery: "..." },
referenceOutputs: { sql: "..." }
},
async ({ inputs, referenceOutputs }) => {
ls.logOutputs({ sql: "SELECT * FROM users;" })
},
);
});
记录的输出将显示在您的报告摘要和 LangSmith 中。 您也可以直接从测试函数返回一个值:import * as ls from "langsmith/vitest";
// import * as ls from "langsmith/jest";
ls.describe("generate sql demo", () => {
ls.test(
"offtopic input",
{
inputs: { userQuery: "..." },
referenceOutputs: { sql: "..." }
},
async ({ inputs, referenceOutputs }) => {
return { sql: "SELECT * FROM users;" }
},
);
});
但是请记住,如果您这样做,如果您的测试由于断言失败或其他错误而未能完成,您的输出将不会出现。
LangSmith 将自动跟踪在测试用例执行过程中发生的任何可跟踪的中间调用。
聚焦或跳过测试
您可以在 ls.test() 和 ls.describe() 上链式调用 Vitest/Jest 的 .skip 和 .only 方法
import * as ls from "langsmith/vitest";
// import * as ls from "langsmith/jest";
ls.describe("generate sql demo", () => {
ls.test.skip(
"offtopic input",
{
inputs: { userQuery: "..." },
referenceOutputs: { sql: "..." }
},
async ({ inputs, referenceOutputs }) => {
return { sql: "SELECT * FROM users;" }
},
);
ls.test.only(
"other",
{
inputs: { userQuery: "..." },
referenceOutputs: { sql: "..." }
},
async ({ inputs, referenceOutputs }) => {
return { sql: "SELECT * FROM users;" }
},
);
});
配置测试套件
您可以通过向 ls.describe() 传递一个额外的参数来配置整个测试套件的元数据或自定义客户端,或者通过向 ls.test() 传递一个 config 字段来配置单个测试
ls.describe("test suite name", () => {
ls.test(
"test name",
{
inputs: { ... },
referenceOutputs: { ... },
// Extra config for the test run
config: { tags: [...], metadata: { ... } }
},
{
name: "test name",
tags: ["tag1", "tag2"],
skip: true,
only: true,
}
);
}, {
testSuiteName: "overridden value",
metadata: { ... },
// Custom client
client: new Client(),
});
测试套件还将自动从 process.env.ENVIRONMENT、process.env.NODE_ENV 和 process.env.LANGSMITH_ENVIRONMENT 中提取环境变量,并将它们设置为创建的实验的元数据。然后,您可以在 LangSmith 的 UI 中按元数据筛选实验。 有关配置选项的完整列表,请参阅 API 参考。试运行模式
如果您想在不将结果同步到 LangSmith 的情况下运行测试,您可以省略您的 LangSmith 跟踪环境变量,或者在您的环境中设置 LANGSMITH_TEST_TRACKING=false。 测试将正常运行,但实验日志将不会发送到 LangSmith。