json[ "有一只小狗,体重为10公斤", "有一只老狗,体重为18公斤", "有一个石头,重量为2吨", "有一个人,他位高权重,却以百姓为刍狗" ]
yml# docker-compose.yml volumes: notes-qdrant-data: services: # Qdrant 向量数据库服务 qdrant: container_name: notes-qdrant image: qdrant/qdrant:latest # 端口映射:6333为REST API端口,6334为gRPC端口 ports: - '6333:6333' - '6334:6334' # 数据持久化 volumes: - notes-qdrant-data:/qdrant/storage # 环境变量配置 environment: # 注意:这里可以设置API密钥来保护向量数据库 QDRANT__SERVICE__API_KEY: '123456' # 配置集群模式(单节点) QDRANT__CLUSTER__ENABLED: 'false' # 配置日志级别 QDRANT__LOG_LEVEL: 'INFO' # 重启策略 restart: always
bashdocker compose -f docker-compose.yml up -d --build
vector.controller.ts
、vector.service.ts
、vector.module.ts
bashnpm i @qdrant/js-client-rest openai uuid
vector.controller.ts
tsimport { Controller, Get, Res, Query, Post, Body, Sse } from '@nestjs/common'; import { Observable } from 'rxjs'; import { VectorService } from './service'; @Controller('vector') export class VectorController { constructor(private readonly vectorService: VectorService) {} // 添加向量 @Post('addVector') async addVector(@Body('texts') texts: string[]) { const data = await this.vectorService.addVector(texts); return data; } // 搜索向量 @Post('searchVectors') async searchVector(@Body('text') text: string) { const data = await this.vectorService.searchVectors({ text }); return data; } // 删除向量集合 @Get('deleteVectorCollection') async deleteVectorCollection() { await this.vectorService.deleteCollection(); return { success: true, message: '向量集合删除成功', }; } // 获取所有向量点 @Get('getAllPoints') async getAllPoints() { const data = await this.vectorService.getAllPoints({}); return data; } // 获取所有集合列表 @Get('getAllCollections') async getAllCollections() { const data = await this.vectorService.getAllCollections(); return data; } // 流式返回向量点 @Sse('sseChatTrue') stream(@Res() res: MyRes, @Query() query: { prompt: string }) { const { prompt } = query; return new Observable((observer) => { const run = async () => { try { const { stream, stop } = await this.vectorService.littleNoteStream({ prompt, }); // 检测客户端断开连接 res.on('close', async () => { console.log('客户端断开连接'); await stop(); observer.complete(); // 正确关闭 SSE 连接 }); for await (const chunk of stream) { if (chunk) { const content = chunk.choices[0].delta.content; if (content) { observer.next({ done: false, value: content }); } } } observer.next({ done: true, data: null }); observer.complete(); // 流结束时正确关闭连接 } catch (error: any) { observer.next({ error: error.message || '传输错误', done: true }); observer.error(error); // 发生错误时关闭连接 } }; run(); // 返回清理函数,当客户端断开连接时会被调用 return () => { // 这里可以添加额外的清理逻辑 }; }); } }
vector.service.ts
tsimport { Inject, Injectable, Logger } from '@nestjs/common'; import { QdrantClient } from '@qdrant/js-client-rest'; import { v4 as uuidv4 } from 'uuid'; import { OpenAI } from 'openai'; interface PointsType { id: string; vector: number[]; payload?: Record<string, any>; } @Injectable() export class VectorService { private readonly logger = new Logger(VectorService.name); private readonly DIMENSIONS = 1024; // 维度 private readonly COLLECTION_NAME = 'home'; // 集合名称 private openai: OpenAI; private isCollectionInitialized: Promise<boolean>; // 集合是否初始化 aiModel = { LLM: 'Doubao-1.5-pro-32k', Embedding: 'text-embedding-3-large', url: 'https://api.302.ai/v1', key: '你自己在302AI申请的key', }; constructor(@Inject('QDRANT_CLIENT') private readonly client: QdrantClient) { this.openai = new OpenAI({ apiKey: this.aiModel.key, baseURL: this.aiModel.url, }); this.isCollectionInitialized = this.initVectorCollection(); } async initVectorCollection(): Promise<boolean> { try { const result = await this.createCollection({ collection_name: this.COLLECTION_NAME, vector_size: this.DIMENSIONS, distance: 'Cosine', }); if (result) { this.logger.log(`向量集合 ${this.COLLECTION_NAME} 初始化成功`); return true; } return false; } catch (error: any) { this.logger.error(`初始化向量集合失败: ${error.message}`, error); return false; } } async createCollection(params: { collection_name: string; vector_size: number; distance?: 'Cosine' | 'Euclid' | 'Dot'; }) { const { collection_name, vector_size, distance = 'Cosine' } = params; try { // 检查集合是否已存在 const collections = await this.client.getCollections(); this.logger.log('collections', JSON.stringify(collections)); const exists = collections.collections.some( (col) => col.name === collection_name, ); if (exists) { this.logger.log(`集合 ${collection_name} 已存在`); return true; // 返回成功状态 } const result = await this.client.createCollection(collection_name, { vectors: { size: vector_size, distance, }, }); this.logger.log(`集合 '${collection_name}' 创建成功`); return result; } catch (error: any) { // 如果是集合已存在的错误,也视为成功 if (error.status === 409 || error.message?.includes('already exists')) { this.logger.log(`集合 ${collection_name} 已存在`); return true; } this.logger.error(`创建集合 '${collection_name}' 失败: ${error.message}`); throw error; } } /** * 通过模型获取文本的向量 * @param content 文本 * @returns 返回向量嵌入 */ async getEmbedding(content: string) { const embedding = await this.openai.embeddings.create({ model: this.aiModel.Embedding, input: content, dimensions: this.DIMENSIONS, }); console.log('embedding', embedding); const arr = embedding.data[0].embedding; this.logger.log( `获取向量成功, text: ${content}, vector: ${JSON.stringify(arr)}, length: ${arr.length}`, ); return arr; } async addVector(texts: string[]) { await this.isCollectionInitialized; const vectors = await Promise.all( texts.map(async (text) => { const vector = await this.getEmbedding(text); return { id: uuidv4(), vector, payload: { text, }, }; }), ); await this.upsertPoints(vectors); } /** * 插入或更新向量点 * @param points 向量点数据 * @returns 返回操作结果 */ async upsertPoints(points: PointsType[]) { const pointsData = points.map((point) => ({ id: uuidv4(), vector: point.vector, payload: point.payload || {}, })); const handleChunk = <T = any>(arr: T[], size = 10) => { const result: T[][] = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; }; const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; // 每次40个有概率会报错,减少并发 const chunks = handleChunk(pointsData, 20); const upload = async (chunk: PointsType[], tryMaxCount = 3) => { try { const result = await this.client.upsert(this.COLLECTION_NAME, { wait: true, points: chunk, }); return result; } catch (error: any) { this.logger.error( `向集合 '${this.COLLECTION_NAME}' 插入/更新点失败, 重试剩余 ${tryMaxCount} 次, chunk: ${JSON.stringify(chunk.map((item) => item.payload))}, error: ${JSON.stringify(error)}`, ); if (tryMaxCount > 0) { await sleep(1000); return upload(chunk, tryMaxCount - 1); } } }; try { let result: any; for (const chunk of chunks) { result = await upload(chunk); } this.logger.log( `向集合 '${this.COLLECTION_NAME}' 插入/更新了 ${points.length} 个点`, ); return result; } catch (error: any) { this.logger.error( `向集合 '${this.COLLECTION_NAME}' 插入/更新点失败: ${error.message}`, ); } } /** * 搜索相似向量 * @param params 参数对象 * @param params.text 查询文本 * @param params.limit 返回结果数量限制,默认为 10 * @param params.score_threshold 相似度阈值,默认为 0.5 * @param params.filter 过滤条件 * @returns 返回搜索结果 */ async searchVectors(params: { text: string; limit?: number; score_threshold?: number; filter?: Record<string, any>; }) { const { text, limit = 10, score_threshold = 0.1, filter } = params; try { const vector = await this.getEmbedding(text); const result = await this.client.search(this.COLLECTION_NAME, { vector, limit, score_threshold, filter, with_payload: true, with_vector: false, }); return result; } catch (error: any) { this.logger.error( `在集合 '${this.COLLECTION_NAME}' 中搜索向量失败: ${error.message}`, ); throw error; } } /** * 删除集合 * @returns 返回操作结果 */ async deleteCollection() { try { const result = await this.client.deleteCollection(this.COLLECTION_NAME); this.logger.log(`集合 '${this.COLLECTION_NAME}' 删除成功`); return result; } catch (error: any) { this.logger.error( `删除集合 '${this.COLLECTION_NAME}' 失败: ${error.message}`, ); throw error; } } /** * 获取集合中的所有向量点 * @param params 参数对象 * @param params.limit 返回结果数量限制,默认为 1000 * @param params.offset 偏移量,用于分页 * @param params.with_vector 是否包含向量数据,默认为 false * @returns 返回所有向量点数据 */ async getAllPoints(params: { limit?: number; offset?: number; with_vector?: boolean; }) { const { limit = 1000, offset = 0, with_vector = false } = params; try { const result = await this.client.scroll(this.COLLECTION_NAME, { limit, offset, with_payload: true, with_vector, }); this.logger.log( `从集合 '${this.COLLECTION_NAME}' 获取了 ${result.points.length} 个点`, ); return { points: result.points, next_page_offset: result.next_page_offset, }; } catch (error: any) { this.logger.error( `从集合 '${this.COLLECTION_NAME}' 获取所有点失败: ${error.message}`, ); throw error; } } /** * 获取所有集合列表 * @returns 返回集合列表 */ async getAllCollections() { try { const result = await this.client.getCollections(); return result.collections; } catch (error: any) { this.logger.error(`获取集合列表失败: ${error.message}`); throw error; } } /** * 创建笔记助手的流式聊天 * @param params 参数对象 * @param params.prompt 用户输入的提示文本 * @returns 返回的流对象 */ async littleNoteStream(params: { prompt: string }) { const { prompt } = params; // 搜索相似向量 const searchResult = await this.searchVectors({ text: prompt, limit: 5, score_threshold: 0.4, }); // 格式化搜索结果 const data = searchResult.map((item) => { const chunk = (item.payload?.text as string) || ''; return { score: item.score, chunk, }; }); // 组装prompt const promptTemplate = ` <data>${JSON.stringify(data)}</data> <question>${prompt}</question> 根据以上数据回答用户问题。 **回答要求:** 1. 回答要简洁明了,不要过于复杂。 2. 回答要符合用户问题,不要偏离主题。 3. 禁止使用“数据”“chunk”“score”“数组”“字段”等与原始数据结构相关的词汇; 4. 如果数据中没有相关信息,回答“根据已知信息无法回答”。 `; console.log('promptTemplate', promptTemplate); // 调用OpenAI API // @ts-ignore const stream = await this.openai.chat.completions.create({ model: this.aiModel.LLM, // 模型名称 messages: [ { role: 'user', content: promptTemplate, }, ], hide_thoughts: true, // 隐藏思考过程 stream: true, // 流式输出 }); const stop = async () => { await stream.controller.abort(); }; return { stream, stop, }; } }
vector.module.ts
tsimport { Global, Module } from '@nestjs/common'; import { QdrantClient } from '@qdrant/js-client-rest'; import { VectorService } from './service'; import { VectorController } from './controller'; @Global() @Module({ controllers: [VectorController], providers: [ VectorService, { provide: 'QDRANT_CLIENT', async useFactory() { try { const client = new QdrantClient({ host: '127.0.0.1', port: 6333, https: false, apiKey: '123456', timeout: 30000, // 30秒超时 checkCompatibility: false, // 跳过版本兼容性检查 }); // 测试连接 - 添加更详细的错误信息 await client.getCollections(); return client; } catch (error: any) { throw new Error(`无法连接到 Qdrant 服务器,请确保服务器正在运行`); } }, }, ], exports: [VectorService], }) export class VectorModule {}
json{ "info": { "_postman_id": "cd54cfbf-9f5d-4fd7-860d-1e69e6de2eb8", "name": "vector", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "17779284", "_collection_link": "https://solar-star-708793-2808.postman.co/workspace/%25E5%2590%2591%25E9%2587%258F~8c394ee0-0b44-471f-905c-b8277939d299/collection/17779284-cd54cfbf-9f5d-4fd7-860d-1e69e6de2eb8?action=share&source=collection_link&creator=17779284" }, "item": [ { "name": "addVector", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\n \"texts\": [\n \"有一只小狗,体重为10公斤\",\n \"有一只老狗,体重为18公斤\",\n \"有一个石头,重量为2吨\",\n \"有一个人,他位高权重,却以百姓为刍狗\"\n ]\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8020/vector/addVector", "protocol": "http", "host": ["localhost"], "port": "8020", "path": ["vector", "addVector"] } }, "response": [] }, { "name": "searchVectors", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\n \"text\": \"找出所有的狗\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8020/vector/searchVectors", "protocol": "http", "host": ["localhost"], "port": "8020", "path": ["vector", "searchVectors"] } }, "response": [] }, { "name": "deleteVectorCollection", "protocolProfileBehavior": { "disableBodyPruning": true }, "request": { "method": "GET", "header": [], "body": { "mode": "raw", "raw": "", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8020/vector/deleteVectorCollection", "protocol": "http", "host": ["localhost"], "port": "8020", "path": [ "vector", "deleteVectorCollection" ] } }, "response": [] }, { "name": "getAllPoints", "protocolProfileBehavior": { "disableBodyPruning": true }, "request": { "method": "GET", "header": [], "body": { "mode": "raw", "raw": "", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8020/vector/getAllPoints", "protocol": "http", "host": ["localhost"], "port": "8020", "path": ["vector", "getAllPoints"] } }, "response": [] }, { "name": "sseChatTrue", "protocolProfileBehavior": { "disableBodyPruning": true }, "request": { "method": "GET", "header": [], "body": { "mode": "raw", "raw": "", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8020/vector/sseChatTrue?prompt=找出所有的狗", "protocol": "http", "host": ["localhost"], "port": "8020", "path": ["vector", "sseChatTrue"], "query": [ { "key": "prompt", "value": "找出所有的狗" } ] } }, "response": [] }, { "name": "getAllCollections", "request": { "method": "GET", "header": [], "url": { "raw": "http://localhost:8020/vector/getAllCollections", "protocol": "http", "host": ["localhost"], "port": "8020", "path": ["vector", "getAllCollections"] } }, "response": [] } ] }
addVector
的接口,向向量数据库添加4条文本向量数据。searchVectors
接口验证查询结果searchVectors
方法中修改 score_threshold 阈值。tsconst { text, limit = 10, score_threshold = 0.1, filter } = params;
tsconst { text, limit = 10, score_threshold = 0.4, filter } = params;
public
:tsimport { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; import { NestExpressApplication, ExpressAdapter, } from '@nestjs/platform-express'; import * as cookieParser from 'cookie-parser'; import { join } from 'path'; import { AppModule } from 'src/modules/app.module'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>( AppModule, new ExpressAdapter(), ); app.useStaticAssets(join(__dirname, '..', 'public')); app.use(cookieParser()); app.useGlobalPipes(new ValidationPipe({ transform: true })); await app.listen(8020); } bootstrap().catch((error) => { console.error('应用启动失败:', error); process.exit(1); });
public
目录,在目录中创建 ai-chat.html
文件,文件内容为:html<!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AI问答助手</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .chat-container { background: white; border-radius: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); width: 100%; max-width: 800px; height: 600px; display: flex; flex-direction: column; overflow: hidden; } .chat-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; font-size: 24px; font-weight: 600; } .chat-messages { flex: 1; padding: 20px; overflow-y: auto; background: #f8f9fa; } .message { margin-bottom: 15px; display: flex; align-items: flex-start; } .message.user { justify-content: flex-end; } .message-content { max-width: 70%; padding: 12px 16px; border-radius: 18px; word-wrap: break-word; line-height: 1.4; } .message.user .message-content { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .message.ai .message-content { background: white; border: 1px solid #e1e5e9; color: #333; } .chat-input { padding: 20px; background: white; border-top: 1px solid #e1e5e9; } .input-container { display: flex; gap: 10px; align-items: center; } .input-field { flex: 1; padding: 12px 16px; border: 2px solid #e1e5e9; border-radius: 25px; font-size: 16px; outline: none; transition: border-color 0.3s ease; } .input-field:focus { border-color: #667eea; } .send-button { padding: 12px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 25px; font-size: 16px; font-weight: 600; cursor: pointer; transition: transform 0.2s ease; } .send-button:hover { transform: translateY(-2px); } .send-button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .typing-indicator { display: none; padding: 12px 16px; background: white; border: 1px solid #e1e5e9; border-radius: 18px; max-width: 70%; } .typing-dots { display: flex; gap: 4px; } .typing-dot { width: 8px; height: 8px; background: #667eea; border-radius: 50%; animation: typing 1.4s infinite; } .typing-dot:nth-child(2) { animation-delay: 0.2s; } .typing-dot:nth-child(3) { animation-delay: 0.4s; } @keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-10px); } } .error-message { background: #fee; color: #c33; border: 1px solid #fcc; } @media (max-width: 768px) { .chat-container { height: 100vh; border-radius: 0; } .message-content { max-width: 85%; } } </style> </head> <body> <div class="chat-container"> <div class="chat-header">🤖 AI问答助手</div> <div class="chat-messages" id="chatMessages"> <div class="message ai"> <div class="message-content"> 你好!我是AI助手,有什么问题可以问我哦~ </div> </div> </div> <div class="chat-input"> <div class="input-container"> <input type="text" class="input-field" id="messageInput" placeholder="输入你的问题..." maxlength="500" /> <button class="send-button" id="sendButton">发送</button> </div> </div> </div> <script> class AIChat { constructor() { this.messagesContainer = document.getElementById('chatMessages'); this.messageInput = document.getElementById('messageInput'); this.sendButton = document.getElementById('sendButton'); this.currentEventSource = null; this.isStreaming = false; this.init(); } init() { this.sendButton.addEventListener('click', () => this.sendMessage()); this.messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); } async sendMessage() { const message = this.messageInput.value.trim(); if (!message || this.isStreaming) return; // 添加用户消息 this.addMessage(message, 'user'); this.messageInput.value = ''; // 显示输入状态 this.setInputState(true); // 添加AI消息容器 const aiMessageElement = this.addMessage('', 'ai', true); try { await this.streamAIResponse(message, aiMessageElement); } catch (error) { console.error('发送消息失败:', error); this.addMessage( '抱歉,发生了错误,请稍后重试。', 'ai', false, true, ); } finally { this.setInputState(false); } } streamAIResponse(prompt, messageElement) { return new Promise((resolve, reject) => { const messageContent = messageElement.querySelector('.message-content'); let fullResponse = ''; // 创建EventSource连接 const eventSource = new EventSource( `/vector/sseChatTrue?prompt=${encodeURIComponent(prompt)}`, ); this.currentEventSource = eventSource; eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.error) { messageContent.textContent = `错误: ${data.error}`; messageContent.classList.add('error-message'); eventSource.close(); reject(new Error(data.error)); return; } if (data.done) { eventSource.close(); this.currentEventSource = null; resolve(); return; } if (data.value) { fullResponse += data.value; messageContent.textContent = fullResponse; this.scrollToBottom(); } } catch (error) { console.error('解析响应数据失败:', error); messageContent.textContent = '解析响应失败'; messageContent.classList.add('error-message'); eventSource.close(); reject(error); } }; eventSource.onerror = (error) => { console.error('EventSource错误:', error); messageContent.textContent = '连接失败,请检查网络或稍后重试'; messageContent.classList.add('error-message'); eventSource.close(); this.currentEventSource = null; reject(error); }; eventSource.onopen = () => { console.log('EventSource连接已建立'); }; }); } addMessage(content, type, isStreaming = false, isError = false) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${type}`; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; if (isError) { contentDiv.classList.add('error-message'); } if (isStreaming) { contentDiv.textContent = ''; } else { contentDiv.textContent = content; } messageDiv.appendChild(contentDiv); this.messagesContainer.appendChild(messageDiv); this.scrollToBottom(); return messageDiv; } setInputState(isDisabled) { this.isStreaming = isDisabled; this.sendButton.disabled = isDisabled; this.messageInput.disabled = isDisabled; if (isDisabled) { this.sendButton.textContent = '发送中...'; } else { this.sendButton.textContent = '发送'; } } scrollToBottom() { this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; } // 清理方法,在页面卸载时调用 cleanup() { if (this.currentEventSource) { this.currentEventSource.close(); this.currentEventSource = null; } } } // 初始化聊天应用 const aiChat = new AIChat(); // 页面卸载时清理资源 window.addEventListener('beforeunload', () => { aiChat.cleanup(); }); </script> </body> </html>
使用的是 openAI 的 text-embedding-3-large 模型测试。
使用的是 openAI 的 text-embedding-3-large 模型测试。
prompt | 与“有一只小狗, 体重为10公斤”相似度 | 与“有一只老狗, 体重为18公斤”相似度 |
---|---|---|
找出所有的狗 | 0.3818805 (100维) 0.43531436 (1024维) | 0.27178484 (100维) 0.40916687 (1024维) |
关于狗的内容 | 0.45164186 (100维) 0.43591332 (1024维) | 0.43328193 (100维) 0.41741773 (1024维) |
你好,帮我找出 关于狗的内容 | 0.37944922 (100维) 0.3759594 (1024维) | 0.2893664 (100维) 0.3401547 (1024维) |
所有的狗 | 0.5181277 (100维) 0.47305614 (1024维) | 0.50474775 (100维) 0.44513088 (1024维) |
狗 | 0.4424646 (100维) 0.42782572 (1024维) | 0.42330262 (100维) 0.4014681 (1024维) |
体重 | 0.41686216 (100维) 0.42644602 (1024维) | 0.49011505 (100维) 0.38479653 (1024维) |
公斤 | 0.43894738 (100维) 0.413822 (1024维) | 0.48421618 (100维) 0.35434955 (1024维) |
狗的体重 | 0.6909904 (100维) 0.698545 (1024维) | 0.67722666 (100维) 0.68216 (1024维) |
有一只小狗, 体重为10公斤 | 0.99999994 (100维) 0.99999905 (1024维) | 0.80675054 (100维) 0.8170248 (1024维) |
有一只老狗, 体重为18公斤 | 0.80675054 (100维) 0.8170315 (1024维) | 1 (100维) 1.0000001 (1024维) |
text-embedding-3-large
模型基础下下得出的相似度,其他的模型得出的结论是否和他一致?prompt | 有一只小狗, 体重为10公斤 (1024维度) | 有一只老狗, 体重为18公斤 (1024维度) |
---|---|---|
找出所有的狗 | 0.4283638 | 0.4423697 |
关于狗的内容 | 0.44277573 | 0.4427782 |
你好,帮我找出 关于狗的内容 | 0.57391346 | 0.5698793 |
所有的狗 | 0.3930607 | 0.413149 |
狗 | 0.49583945 | 0.50600433 |
体重 | 0.48813787 | 0.50087357 |
公斤 | 0.47048008 | 0.46403456 |
狗的体重 | 0.5652957 | 0.5581999 |
有一只小狗, 体重为10公斤 | 1 | 0.74792135 |
有一只老狗, 体重为18公斤 | 0.74792135 | 0.9999999 |
prompt | 有一只小狗, 体重为10公斤 (1024维度) | 有一只老狗, 体重为18公斤 (1024维度) |
---|---|---|
我出所有的狗 | 0.46850893 | 0.45931795 |
关于狗的内容 | 0.48942578 | 0.4830734 |
你好,帮我找出 关于狗的内容 | 0.4833926 | 0.44802487 |
所有的狗 | 0.3930607 | 0.413149 |
狗 | 0.49583945 | 0.50600433 |
体重 | 0.48813787 | 0.50087357 |
公斤 | 0.47048008 | 0.46403456 |
狗的体重 | 0.5652957 | 0.5581999 |
有一只小狗, 体重为10公斤 | 1 | 0.74792135 |
有一只老狗, 体重为18公斤 | 0.74792135 | 0.9999999 |
openAI | 厂商A | 厂商B |
---|---|---|
0.3759594 | 0.57391346 | 0.4833926 |
核心结论
在这个具体的例子中,OpenAI的相似度分数(0.376)更准,更符合人类的语义直觉。为什么?
- 第一句
“你好,帮我找出关于狗的内容”
是一个查询指令,意图是寻找信息。- 第二句
“有一只小狗,体重为10公斤”
是一个事实陈述,描述了一个具体的狗。- 从语义上看,这两句话的意图和主题相关,但并非高度相似。它们不是在说同一件事。一句是“找资料”,另一句是“一个具体的资料”。所以,一个中等偏低(但在0.3-0.5之间)的相似度是合理的。
- 厂商A(0.574)和厂商B(0.483)的分数明显偏高,这使得它们看起来比实际更相似,这可能会在后续应用(如搜索)中引入噪声。
评测标准是什么?
评测文本嵌入模型的好坏,绝对不是看一两个句子的结果,而是需要通过一个大规模、有标注的基准测试集来进行综合评估。常用的评测标准和方法如下:...(巴拉巴拉一大堆,有兴趣的自己去问问AI)
召回率 = TP / (TP + FN)TP = 真实正例(True Positive, TP):模型预测为正例,且实际确实是正例的数量。
FN = 假负例(False Negative, FN):模型预测为负例,但实际是正例的数量(即被遗漏的正例)。