Skip to content

LangSmith 提供了与 Vitest 和 Jest 的集成,允许 JavaScript 和 TypeScript 开发者使用熟悉的语法定义数据集并进行评估。

Jest/Vitest 报告器输出

evaluate() 评估流程相比,这在以下情况下非常有用:

  • 每个示例需要不同的评估逻辑:标准评估流程假设所有数据集示例都采用一致的应用和评估器执行。对于更复杂的系统或全面的评估,特定的系统子集可能需要使用特定的输入类型和指标进行评估。将这些异构评估编写为独立的测试用例套件并一起跟踪会更简单。
  • 您想要断言二元期望:在 LangSmith 中跟踪断言,并在本地(例如在 CI 流水线中)引发断言错误。测试工具在评估系统输出并断言其基本属性时很有帮助。
  • 您希望利用 Vitest/Jest 生态系统的模拟(mocks)、监视模式(watch mode)、本地结果或其他功能

需要 JS/TS SDK 版本 langsmith>=0.3.1

Vitest/Jest 集成目前处于测试阶段,在未来的版本中可能会发生变化。

Python SDK 有一个类似的 pytest 集成

设置

按如下方式设置集成。请注意,虽然您可以使用现有的测试配置文件将 LangSmith 评估与其他单元测试(作为标准的 *.test.ts 文件)一起添加,但以下示例还将设置一个单独的测试配置文件和命令来运行您的评估。它将假设您的测试文件以 .eval.ts 结尾。

这确保了自定义测试报告器和其他 LangSmith 接触点不会修改您现有的测试输出。

Vitest

如果尚未安装,请安装所需的开发依赖项:

bash
yarn add -D vitest dotenv
bash
npm install -D vitest dotenv
bash
pnpm add -D vitest dotenv

以下示例还需要 openai(当然还有 langsmith!)作为依赖项:

bash
yarn add langsmith openai
bash
npm install langsmith openai
bash
pnpm add langsmith openai

然后创建一个单独的 ls.vitest.config.ts 文件,包含以下基本配置:

typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["**/*.eval.?(c|m)[jt]s"],
    reporters: ["langsmith/vitest/reporter"],
    setupFiles: ["dotenv/config"],
    testTimeout: 30000,
  },
});
  • include 确保只运行项目中以 eval.ts 的某种变体结尾的文件
  • reporters 负责将输出格式化为如上所示的样式
  • setupFiles 在运行评估之前运行 dotenv 来加载环境变量
  • testTimeout 为每个测试设置全局默认超时时间。由于 LLM 调用可能较慢,我们将其从 Vitest 默认值提高

目前不支持 JSDom 环境。您应该从配置中省略 "environment" 字段,或将其设置为 "node"

最后,将以下内容添加到 package.jsonscripts 字段中,以使用您刚刚创建的配置运行 Vitest:

json
{
  "name": "YOUR_PROJECT_NAME",
  "scripts": {
    "eval": "vitest run --config ls.vitest.config.ts"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  }
}

请注意,上述脚本禁用了 Vitest 的默认监视模式以运行评估,因为许多评估器可能包含运行时间较长的 LLM 调用。

Jest

如果尚未安装,请安装所需的开发依赖项:

bash
yarn add -D jest dotenv
bash
npm install -D jest dotenv
bash
pnpm add -D jest dotenv

以下示例还需要 openai(当然还有 langsmith!)作为依赖项:

bash
yarn add langsmith openai
bash
npm install langsmith openai
bash
pnpm add langsmith openai

以下设置说明适用于基本的 JS 文件和 CJS。要添加对 TypeScript 和 ESM 的支持,请参阅 Jest 的官方文档或使用 Vitest

然后创建一个单独的配置文件,命名为 ls.jest.config.cjs

javascript
module.exports = {
  testMatch: ["**/*.eval.?(c|m)[jt]s"],
  reporters: ["langsmith/jest/reporter"],
  setupFiles: ["dotenv/config"],
  testTimeout: 30000,
};
  • testMatch 确保只运行项目中以 eval.js 的某种变体结尾的文件
  • reporters 负责将输出格式化为如上所示的样式
  • setupFiles 在运行评估之前运行 dotenv 来加载环境变量
  • testTimeout 为每个测试设置全局默认超时时间。由于 LLM 调用可能较慢,我们将其从 Jest 默认值提高

目前不支持 JSDom 环境。您应该从配置中省略 "testEnvironment" 字段,或将其设置为 "node"

最后,将以下内容添加到 package.jsonscripts 字段中,以使用您刚刚创建的配置运行 Jest:

json
{
  "name": "YOUR_PROJECT_NAME",
  "scripts": {
    "eval": "jest --config ls.jest.config.cjs"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  }
}

定义和运行评估

您现在可以使用熟悉的 Vitest/Jest 语法将评估定义为测试,但有一些注意事项:

  • 您应该从 langsmith/jestlangsmith/vitest 入口点导入 describetest
  • 您必须将测试用例包装在 describe 块中
  • 声明测试时,签名略有不同——多了一个包含示例输入和预期输出的参数

尝试创建一个名为 sql.eval.ts 的文件(如果您使用 Jest 而不使用 TypeScript,则为 sql.eval.js),并将以下内容粘贴到其中:

typescript
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";

// 添加 "openai" 作为依赖项,并将 OPENAI_API_KEY 设置为环境变量
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 }); // <-- 记录运行输出,可选
      expect(sql).toEqual(referenceOutputs?.sql); // <-- 断言结果记录在 'pass' 反馈键下
    }
  );
});

您可以将每个 ls.test() 用例视为对应一个数据集示例,而 ls.describe() 则定义了一个 LangSmith 数据集。如果您在运行测试套件时设置了 LangSmith 追踪环境变量,SDK 将执行以下操作:

  • 如果不存在,则在 LangSmith 中创建一个与传递给 ls.describe() 的名称相同的数据集
  • 如果尚不存在匹配项,则为传递给测试用例的每个输入和预期输出在数据集中创建一个示例
  • 为每个测试用例创建一个包含一个结果的新的实验
  • 在每个测试用例的 pass 反馈键下收集通过/失败率

当您运行此测试时,它将根据测试用例的通过/失败情况,拥有一个默认的 pass 布尔反馈键。它还将跟踪您使用 ls.logOutputs() 记录的任何输出或从测试函数返回的值,作为实验中应用程序的“实际”结果值。

如果您还没有,请创建一个包含您的 OPENAI_API_KEY 和 LangSmith 凭据的 .env 文件:

bash
OPENAI_API_KEY="YOUR_KEY_HERE"
LANGSMITH_API_KEY="YOUR_LANGSMITH_KEY"
LANGSMITH_TRACING="true"

现在使用我们在上一步中设置的 eval 脚本来运行测试:

bash
yarn run eval
bash
npm run eval
bash
pnpm run eval

您声明的测试应该会运行!

完成后,如果您设置了 LangSmith 环境变量,您应该会看到一个链接,指向在 LangSmith 中创建的实验以及测试结果。

以下是针对该测试套件的实验示例:

实验

追踪反馈

默认情况下,LangSmith 会在每个测试用例的 pass 反馈键下收集通过/失败率。您可以使用 ls.logFeedback()wrapEvaluator() 添加额外的反馈。为此,请尝试将以下内容作为您的 sql.eval.ts 文件(如果您使用 Jest 而不使用 TypeScript,则为 sql.eval.js):

typescript
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";

// 添加 "openai" 作为依赖项,并将 OPENAI_API_KEY 设置为环境变量
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 = [
    "如果 ACTUAL 和 EXPECTED 答案在语义上等价,则返回 1,",
    "否则返回 0。只返回 0 或 1,不返回其他内容。",
  ].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);
      // 将自动记录 "correctness" 作为反馈
      await wrappedEvaluator({
        outputs: { sql },
        referenceOutputs,
      });
      // 您也可以使用 `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);
      // 将自动记录 "correctness" 作为反馈
      await wrappedEvaluator({
        outputs: { sql },
        referenceOutputs,
      });
      // 您也可以使用 `ls.logFeedback()` 手动记录反馈
      ls.logFeedback({
        key: "harmfulness",
        score: 0.2,
      });
    }
  );
});

请注意在 myEvaluator 函数周围使用了 ls.wrapEvaluator()。这使得 LLM 作为评判者的调用与测试用例的其余部分分开追踪,以避免混乱,并且如果包装函数的返回值匹配 { key: string; score: number | boolean },则会方便地创建反馈。在这种情况下,评估器追踪将不会出现在主要的测试用例运行中,而是出现在与 correctness 反馈键关联的追踪中。

您可以通过在 UI 中点击相应的反馈芯片,在 LangSmith 中查看评估器的运行情况。

针对一个测试用例运行多个示例

您可以使用 ls.test.each() 在多个示例上运行相同的测试用例,并对测试进行参数化。当您希望以相同的方式针对不同输入评估您的应用程序时,这非常有用:

typescript
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()

typescript
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 中。

您也可以直接从测试函数返回一个值:

typescript
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 方法:

typescript
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 字段来为单个测试配置测试套件:

typescript
ls.describe("test suite name", () => {
  ls.test(
"test name",
{
inputs: { ... },
referenceOutputs: { ... },
// 测试运行的额外配置
config: {

LangChain 中文文档