使用 TypeScript 实现基于 pgvector 的 LLM 自动化测试用例

使用 TypeScript 实现基于 pgvector 的 LLM 自动化测试用例,核心是通过 TypeScript 连接 PostgreSQL(含 pgvector 扩展),结合向量生成库和 LLM 客户端,完成“预期向量存储→实际输出生成→相似度验证”的全流程。以下是具体实现方案,包含完整代码示例和关键步骤说明。

一、环境准备

  1. 依赖安装

    # 核心依赖
    npm install pg @types/pg  # PostgreSQL 客户端及类型
    npm install @xenova/transformers  # 向量生成(基于 Sentence-BERT)
    npm install openai  # 若使用 OpenAI 模型(可选)
    npm install jest @types/jest ts-jest  # 测试框架(可选,也可用 Mocha)
    
    # 类型定义及工具
    npm install -D typescript ts-node @types/node
    
  2. 数据库配置

    • 确保 PostgreSQL 已安装 pgvector 扩展(CREATE EXTENSION vector;
    • 新建数据库(如 llm_test_db),并创建测试用例表和结果表(SQL 见下文)

二、核心实现代码

1. 数据库初始化(创建表结构)

// src/db/schema.ts
import { Pool } from 'pg';

// 数据库连接配置
export const pool = new Pool({
  user: 'postgres',
  host: 'localhost',
  database: 'llm_test_db',
  password: 'your_password',
  port: 5432,
});

// 初始化表结构(首次运行时执行)
export async function initDatabase() {
  const client = await pool.connect();
  try {
    // 创建测试用例表(存储 prompt、预期向量等)
    await client.query(`
      CREATE TABLE IF NOT EXISTS llm_test_cases (
        id SERIAL PRIMARY KEY,
        test_case_id VARCHAR(50) UNIQUE NOT NULL,
        prompt TEXT NOT NULL,
        expected_vector vector(384) NOT NULL,  -- 若用 all-MiniLM-L6-v2 模型,向量维度为 384
        similarity_threshold FLOAT NOT NULL,
        scenario VARCHAR(50) NOT NULL
      );
    `);

    // 创建测试结果表
    await client.query(`
      CREATE TABLE IF NOT EXISTS llm_test_results (
        id SERIAL PRIMARY KEY,
        test_case_id VARCHAR(50) REFERENCES llm_test_cases(test_case_id),
        actual_output TEXT NOT NULL,
        actual_vector vector(384) NOT NULL,
        similarity FLOAT NOT NULL,
        passed BOOLEAN NOT NULL,
        test_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
    `);

    // 为向量创建索引(加速相似度查询)
    await client.query(`
      CREATE INDEX IF NOT EXISTS idx_test_cases_vector 
      ON llm_test_cases USING ivfflat (expected_vector vector_cosine_ops) WITH (lists = 100);
    `);

    console.log('数据库表结构初始化完成');
  } catch (err) {
    console.error('数据库初始化失败:', err);
  } finally {
    client.release();
  }
}

2. 向量生成工具(基于 Sentence-BERT)

使用 @xenova/transformers 库生成文本向量(无需依赖外部 API,适合本地测试):

// src/embeddings/vectorGenerator.ts
import { pipeline } from '@xenova/transformers';

// 初始化 Sentence-BERT 模型(生成文本向量)
let embedder: any;
async function initEmbedder() {
  if (!embedder) {
    embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
  }
  return embedder;
}

/**
 * 将文本转换为向量
 * @param text 输入文本
 * @returns 384 维向量数组
 */
export async function textToVector(text: string): Promise<number[]> {
  const model = await initEmbedder();
  const result = await model(text, { pooling: 'mean', normalize: true });
  // 提取向量数据(flatten 为一维数组)
  return Array.from(result.data) as number[];
}

3. LLM 调用客户端(示例:调用 OpenAI API)

// src/llm/llmClient.ts
import { OpenAI } from 'openai';

const openai = new OpenAI({
  apiKey: 'your-openai-api-key', // 从环境变量读取更安全
});

/**
 * 调用 LLM 生成输出
 * @param prompt 输入提示词
 * @returns LLM 生成的文本
 */
export async function callLLM(prompt: string): Promise<string> {
  const response = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.7, // 控制输出随机性(测试时可适当降低)
  });

  return response.choices[0].message.content || '';
}

4. 测试用例管理(初始化测试数据)

// src/testCases/testCaseManager.ts
import { pool } from '../db/schema';
import { textToVector } from '../embeddings/vectorGenerator';

// 测试用例类型定义
export interface TestCase {
  test_case_id: string;
  prompt: string;
  expected_output: string; // 预期输出的核心语义
  similarity_threshold: number;
  scenario: string;
}

/**
 * 初始化测试用例(将预期输出转为向量并存入数据库)
 */
export async function initTestCases() {
  // 定义测试用例(可根据实际场景扩展)
  const testCases: TestCase[] = [
    {
      test_case_id: 'FACT-001',
      prompt: '地球的平均半径约为多少公里?',
      expected_output: '地球的平均半径约为6371公里',
      similarity_threshold: 0.85, // 事实类场景:高阈值
      scenario: '事实准确性',
    },
    {
      test_case_id: 'RELEV-001',
      prompt: '电脑频繁蓝屏的可能原因是什么?',
      expected_output: '蓝屏可能由驱动错误、硬件故障或系统损坏导致',
      similarity_threshold: 0.75, // 相关性场景:中阈值
      scenario: '核心相关性',
    },
  ];

  // 将预期输出转为向量并入库
  for (const caseItem of testCases) {
    const expectedVector = await textToVector(caseItem.expected_output);
    
    const client = await pool.connect();
    try {
      await client.query(`
        INSERT INTO llm_test_cases 
          (test_case_id, prompt, expected_vector, similarity_threshold, scenario)
        VALUES ($1, $2, $3, $4, $5)
        ON CONFLICT (test_case_id) DO NOTHING
      `, [
        caseItem.test_case_id,
        caseItem.prompt,
        `[${expectedVector.join(',')}]`, // pgvector 向量格式:[x1,x2,...,xn]
        caseItem.similarity_threshold,
        caseItem.scenario,
      ]);
      console.log(`测试用例 ${caseItem.test_case_id} 已初始化`);
    } catch (err) {
      console.error(`初始化测试用例 ${caseItem.test_case_id} 失败:`, err);
    } finally {
      client.release();
    }
  }
}

5. 自动化测试逻辑(基于 Jest)

// src/tests/llmE2ETest.test.ts
import { pool } from '../db/schema';
import { textToVector } from '../embeddings/vectorGenerator';
import { callLLM } from '../llm/llmClient';

// 从数据库获取所有测试用例
async function getTestCases() {
  const result = await pool.query(`
    SELECT test_case_id, prompt, expected_vector, similarity_threshold 
    FROM llm_test_cases
  `);
  return result.rows;
}

// 执行单个测试用例
async function runTestCase(testCase: any) {
  const { test_case_id, prompt, expected_vector, similarity_threshold } = testCase;

  // 1. 调用 LLM 生成实际输出
  const actualOutput = await callLLM(prompt);
  console.log(`\n测试用例 ${test_case_id} 实际输出:`, actualOutput);

  // 2. 将实际输出转为向量
  const actualVector = await textToVector(actualOutput);

  // 3. 计算与预期向量的余弦相似度(pgvector 中 <-> 为欧氏距离,余弦相似度 = 1 - 距离)
  const similarityResult = await pool.query(`
    SELECT 1 - (expected_vector <-> $1) AS cosine_similarity
    FROM llm_test_cases
    WHERE test_case_id = $2
  `, [
    `[${actualVector.join(',')}]`, // 实际向量
    test_case_id,
  ]);
  const similarity = similarityResult.rows[0].cosine_similarity;
  console.log(`相似度:${similarity.toFixed(4)}(阈值:${similarity_threshold}`);

  // 4. 判断是否通过
  const passed = similarity >= similarity_threshold;

  // 5. 记录测试结果到数据库
  await pool.query(`
    INSERT INTO llm_test_results 
      (test_case_id, actual_output, actual_vector, similarity, passed)
    VALUES ($1, $2, $3, $4, $5)
  `, [
    test_case_id,
    actualOutput,
    `[${actualVector.join(',')}]`,
    similarity,
    passed,
  ]);

  // 6. 断言结果(Jest 会捕获失败)
  expect(passed).toBe(true);
}

// 批量执行所有测试用例
describe('LLM E2E 测试', () => {
  let testCases: any[];

  // 测试前获取所有用例
  beforeAll(async () => {
    testCases = await getTestCases();
  });

  // 逐个执行测试用例
  testCases.forEach((testCase) => {
    it(`测试用例 ${testCase.test_case_id}`, async () => {
      await runTestCase(testCase);
    });
  });
});

三、执行流程

  1. 初始化数据库

    // src/index.ts
    import { initDatabase } from './db/schema';
    import { initTestCases } from './testCases/testCaseManager';
    
    async function main() {
      await initDatabase(); // 创建表结构
      await initTestCases(); // 初始化测试用例
    }
    
    main().catch(console.error);
    

    执行:npx ts-node src/index.ts

  2. 运行测试
    配置 Jest 后(npx jest --init),执行:

    npx jest src/tests/llmE2ETest.test.ts
    

四、关键技术点说明

  1. 向量格式处理

    • pgvector 要求向量以 [x1,x2,...,xn] 字符串格式存储,因此需将 TypeScript 数组转换为该格式。
    • 余弦相似度计算:pgvector 中 vector <-> vector 返回欧氏距离,余弦相似度 = 1 - 欧氏距离(需确保向量已归一化)。
  2. 类型安全

    • 通过 TypeScript 接口(如 TestCase)定义测试用例结构,避免类型错误。
    • 数据库查询结果建议用类型断言进一步约束(如 result.rows as TestCaseRow[])。
  3. 性能优化

    • 向量索引:对 expected_vector 建立 ivfflat 索引,加速相似度查询(尤其测试用例较多时)。
    • 模型复用:textToVector 中复用 embedder 实例,避免重复加载模型。
  4. 扩展场景

    • 多预期向量:若一个测试用例有多个合理输出,可扩展表结构存储多个 expected_vector,验证时取最大相似度。
    • 阈值动态调整:根据 scenario 字段自动适配阈值(如 scenario === '合规性' 时阈值设为 0.9)。

五、总结

该方案通过 TypeScript 整合了 pgvector 向量存储、Sentence-BERT 向量生成和 LLM 调用,实现了对 LLM 输出的“语义级自动化测试”。相比传统关键词匹配,其优势在于:

  • 容忍 LLM 输出的表述多样性(如同义词、句式变化);
  • 通过向量相似度量化输出质量,而非刚性文本比对;
  • 测试结果可持久化存储,便于后续分析 LLM 性能波动。

实际使用时,可根据测试场景调整向量模型(如用更大的 BERT 模型提升精度)和相似度阈值,平衡测试的严格性与灵活性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值