使用 TypeScript 构建基于 pgvector 的 LLM 自动化测试用例
使用 TypeScript 实现基于 pgvector 的 LLM 自动化测试用例,核心是通过 TypeScript 连接 PostgreSQL(含 pgvector 扩展),结合向量生成库和 LLM 客户端,完成“预期向量存储→实际输出生成→相似度验证”的全流程。以下是具体实现方案,包含完整代码示例和关键步骤说明。
一、环境准备
依赖安装:
# 核心依赖 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数据库配置:
- 确保 PostgreSQL 已安装 pgvector 扩展(
CREATE EXTENSION vector;) - 新建数据库(如
llm_test_db),并创建测试用例表和结果表(SQL 见下文)
- 确保 PostgreSQL 已安装 pgvector 扩展(
二、核心实现代码
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);
});
});
});
三、执行流程
初始化数据库:
// 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运行测试:
配置 Jest 后(npx jest --init),执行:npx jest src/tests/llmE2ETest.test.ts
四、关键技术点说明
向量格式处理:
- pgvector 要求向量以
[x1,x2,...,xn]字符串格式存储,因此需将 TypeScript 数组转换为该格式。 - 余弦相似度计算:pgvector 中
vector <-> vector返回欧氏距离,余弦相似度 =1 - 欧氏距离(需确保向量已归一化)。
- pgvector 要求向量以
类型安全:
- 通过 TypeScript 接口(如
TestCase)定义测试用例结构,避免类型错误。 - 数据库查询结果建议用类型断言进一步约束(如
result.rows as TestCaseRow[])。
- 通过 TypeScript 接口(如
性能优化:
- 向量索引:对
expected_vector建立ivfflat索引,加速相似度查询(尤其测试用例较多时)。 - 模型复用:
textToVector中复用embedder实例,避免重复加载模型。
- 向量索引:对
扩展场景:
- 多预期向量:若一个测试用例有多个合理输出,可扩展表结构存储多个
expected_vector,验证时取最大相似度。 - 阈值动态调整:根据
scenario字段自动适配阈值(如scenario === '合规性'时阈值设为 0.9)。
- 多预期向量:若一个测试用例有多个合理输出,可扩展表结构存储多个
五、总结
该方案通过 TypeScript 整合了 pgvector 向量存储、Sentence-BERT 向量生成和 LLM 调用,实现了对 LLM 输出的“语义级自动化测试”。相比传统关键词匹配,其优势在于:
- 容忍 LLM 输出的表述多样性(如同义词、句式变化);
- 通过向量相似度量化输出质量,而非刚性文本比对;
- 测试结果可持久化存储,便于后续分析 LLM 性能波动。
实际使用时,可根据测试场景调整向量模型(如用更大的 BERT 模型提升精度)和相似度阈值,平衡测试的严格性与灵活性。
浙公网安备 33010602011771号