使用 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 模型提升精度)和相似度阈值,平衡测试的严格性与灵活性。

posted on 2025-10-29 17:12  blfbuaa  阅读(1)  评论(0)    收藏  举报