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