通过Embedding向量模型解析QA问题,结合prompt实现垂直领域GPT助手

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

在我们对接ChatGPT时,不论是国内的阿里云的qwen-turbo或者是OpenAI的gpt-4o,我们都会发现在某些垂直领域的场景下内容回答的不是很好的情况,接下来我将通过text-embedding向量模型结合prompt提示词来实现对垂直领域问题的完善。

例如:question:阿尔茨海默病是什么?answer:阿尔茨海默病是一种渐进性脑部疾病,会导致痴呆。它会影响记忆、思维和行为,并随着时间推移而恶化。


一、Embedding向量模型是什么?

Embedding 是一种将高维数据(如文本、图像等)映射到低维空间的技术。具体来说,embedding 是将文本嵌入到向量空间中,用向量来表示文本的含义。通过这种方式,我们可以将复杂的文本数据转换为固定长度的向量,使其更易于处理和分析。在我们的实际案例当中需要将question转换成高阶向量模型。

二、什么是prompt?

在人工智能(AI)领域中,“prompt” 是指向模型提供输入以引导其生成特定输出的文本或指令。 它是与模型进行交互时用户提供的文本段落,用于描述用户想要从模型获取的信息、回答、文本等内容。 Prompt 的目的是引导模型产生所需的回应,以便更好地控制生成的输出。在我们的实际案例当中,需要取出数据库当中和我们question相似度最高的文本答案,将其当做prompt提示词投喂给LLM模型,让其能够有更好的回答效果。

三、实际操作

1.将准备好的语料整理成csv格式或者execl格式,将数据整理好后使用python脚本将question的Embedding向量计算出来存储到数据库

  1. 创建数据库的表结构,
    此处我使用的是postgres数据库,创建表之前需要安装pgvector插件,pgvector 是一款PostgreSQL扩展,专门用于存储矢量并在这些矢量中执行相似搜索。 后续我们需要使用pgvector插件来查询相似度最高的答案。
-- 第一次需要执行该sql,不然会报错vector" does not exist
-- https://stackoverflow.com/questions/76220715/type-vector-does-not-exist-on-postgresql-langchain
-- CREATE EXTENSION vector;

-- 创建我们需要创建存储的文本向量的表,指定的数据维度一定要和我们使用的Embedding模型
-- 计算出来的维度一致,不然入库会报错。大家根据实际使用的去修改。
CREATE TABLE jl_knowledge(
    id SERIAL PRIMARY KEY,
    question TEXT,
    answer TEXT,
    embedding VECTOR(3072)   -- 指定数据维度
);
  1. 准备数据,将问题解析成向量维度数组数据存贮到数据库,题主使用的是python,大家根据实际需求转换成自己的语言
import json
import uuid

import numpy as np
import pandas as pd
import psycopg2
import requests
import tiktoken
from openai import AzureOpenAI

# postgres数据库连接
def get_db_connection():
    conn = psycopg2.connect(
        dbname="postgres",
        user="postgres",
        password="root",
        host="localhost",
        port="5432"
    )
    return conn


conn = get_db_connection()
cursor = conn.cursor()

tokenizer = tiktoken.get_encoding("cl100k_base")
# Note: The openai-python library support for Azure OpenAI is in preview.
# Note: This code sample requires OpenAI Python library version 1.0.0 or higher.

# 这里我使用的是微软的OpenAI服务,大家参照微软提供的参数赋值
client = AzureOpenAI(
    azure_endpoint="https://xxxx.com/",
    api_key="xxxxx",
    api_version="2024-02-15-preview"
)

# 获取文本原生的embeddings,这里边的azure_endpoint就是上面AzureOpenAI的azure_endpoint参数
def get_native_embeddings(text):
    url = f"${azure_endpoint}openai/deployments/Embedding/embeddings?api-version=2024-02-01"
    headers = {"api-key": 'xxxxxxxxxxxxxx'}
    data = {
        'input': text,
        'model': "text-embedding-3-large",
        'user': uuid.uuid4().urn
    }
    response = requests.post(url, headers=headers, json=data)
    response_json = response.json()
    # 数据解析,获取embedding
    return np.array(response_json['data'][0]['embedding'], dtype=np.float32)


# def get_native_embeddings(text):
#     return client.embeddings.create(input = [text], model="text-embedding-ada-002").data[0].embedding

# 解析csv文件,将所有的QA及问题的向量维度数据解析后入库
def get_embeddings():
    df = pd.read_csv("./knowledge_base.csv", encoding='gbk')
    df["text"] = df["text"].apply(lambda x: x.replace("\n", " ")).tolist()
    # 长度校验
    df['token'] = df["text"].apply(lambda x: len(tokenizer.encode(x)))
    df_delete = df[df.token > 8191]
    df = df[df.token < 8192]
    df['search_embeddings'] = df["text"].apply(lambda x: get_native_embeddings(x)).tolist()
    for index, row in df.iterrows():
        answer = row['text']
        embedding = row['search_embeddings'].tolist()
        print(len(embedding))
        sql = f"INSERT INTO jl_knowledge (question, answer, embedding) VALUES ('{answer}', '{answer}', '{embedding}') RETURNING id;"
        cursor.execute(sql)
        conn.commit()
    print(df)
    return df


dataframe = get_embeddings()

2.查询数据并且将相似度最高的回答返回,把数据当做prompt提示词填充给GPT模型进行回答。

import uuid

import numpy as np
import psycopg2
import requests
import tiktoken
from openai import AzureOpenAI
from redis.commands.search.query import Query

def get_db_connection():
    conn = psycopg2.connect(
        dbname="postgres",
        user="postgres",
        password="root",
        host="localhost",
        port="5432"
    )
    return conn


conn = get_db_connection()
cursor = conn.cursor()

tokenizer = tiktoken.get_encoding("cl100k_base")
# Note: The openai-python library support for Azure OpenAI is in preview.
# Note: This code sample requires OpenAI Python library version 1.0.0 or higher.

client = AzureOpenAI(
    azure_endpoint="https://xxxxxxxxxxx.com/",
    api_key="xxxxxxxxxxxxx",
    api_version="2024-02-15-preview"
)

def dochat(text):
    completion = client.chat.completions.create(
        model="chatGPT4",  # model = "deployment_name"
        messages=text,
        temperature=0.7,
        max_tokens=800,
        top_p=0.95,
        frequency_penalty=0,
        presence_penalty=0,
        stop=None
    )
    rs = completion.choices[0].message.content
    print('==================================start==============================')
    print(rs)
    print('===================================end===============================')
    return rs


def get_native_embeddings(text):
    url = f"${azure_endpoint}/openai/deployments/Embedding/embeddings?api-version=2024-02-01"
    headers = {"api-key": 'xxxxxxxxxxxxxxxxxxxxxxxxx'}
    data = {
        'input': text,
        'model': "text-embedding-3-large",
        'user': uuid.uuid4().urn
    }
    response = requests.post(url, headers=headers, json=data)
    response_json = response.json()
    # 数据解析,获取embedding
    return np.array(response_json['data'][0]['embedding'], dtype=np.float32)

# 查询相似度最高的几条数据
def find_similar_items(embedding, limit):
    start = '['
    query_embedding_str = ",".join(map(str, embedding))
    end = ']'
    query = start + query_embedding_str + end
    print(query)
    sql = """
           SELECT id, question, answer, 1 - ( embedding <=> '{query}') AS similarity
           FROM jl_knowledge ORDER BY similarity DESC LIMIT '{limit}';
       """.format(query=query, limit=limit)
    cursor.execute(sql)
    results = cursor.fetchall()
    return results


# 根据用户的问题,查询相似度最高的问题的答案,组装prompt提示词
def search_docs(user_query, top_n=3, to_print=True):
	# 查询问题的Embedding向量
    embed_query = get_native_embeddings(user_query)
    embedding = embed_query.tolist()
    # 查询相似度最高的topn条数据
    results = find_similar_items(embedding, top_n)


	# 拼接所有满足相似度的问题的答案
    prompt = ''
    for row in results:
        print(row)
        text = row[2]
        score = row[3]
        score = float(row[3])
        # 防御,防止不相干的问题,可动态调节分数,相似度低于多少舍弃,我这边是0.8
        if score < 0.8:
            continue
        prompt += text
        print(f"\t{text} (Score: {round(score, 3)})")

        # 特殊处理过滤
        if len(prompt) == 0:
            return "No results found"
    print(prompt)
    message_text = [{"role": "system",
                     "content":  prompt},
                    {"role": "user", "content": user_query}]
    return dochat(message_text)

res = search_docs("阿尔茨海默病是什么? ", top_n=3)

  1. 下面是题主根据python代码转换成的java代码,大家可以用于参考,题主线上就是根据这种形式对外提供服务的
@PostMapping(value = "/chat/completions")
    public void chatCompletions(HttpServletRequest request, HttpServletResponse response, @RequestBody @Validated List<YgjChatMessage> chatMessages) {
        DashScopeClient chatClient = getDashScopeClient(response);
        final AsyncContext asyncContext = request.startAsync();
        // 设置超时为永不超时
        asyncContext.setTimeout(0L);
        // 创建请求
        List<Message> messages = new ArrayList<>(chatMessages.size());
        for (YgjChatMessage chatMessage : chatMessages) {
            String role = chatMessage.getRole().toUpperCase();
            if (Objects.equals(role, OpenAiRoleEnum.USER.name())) {
                List<Content<?>> userMessage = new ArrayList<>();
                userMessage.add(Content.ofText(chatMessage.getContent()));
                messages.add(new MessageImpl(Message.Role.USER, userMessage));
            } else if (Objects.equals(role, OpenAiRoleEnum.ASSISTANT.name())) {
                messages.add(new MessageImpl(Message.Role.AI, List.of(Content.ofText(chatMessage.getContent()))));
            } else if (Objects.equals(role, OpenAiRoleEnum.SYSTEM.name())) {
                messages.add(new MessageImpl(Message.Role.SYSTEM, List.of(Content.ofText(chatMessage.getContent()))));
            } else {
                throw new IllegalArgumentException("role is not support");
            }
        }
        messages.stream().filter(message -> Objects.equals(message.role(), Message.Role.USER)).findFirst()
                .ifPresent(message -> {
                    // append embedding
                    appendEmbedding(chatClient, message);
                });
        ChatRequest chatRequest = ChatRequest.newBuilder()
                // qwen-turbo:0.008元/1,000tokens
                .model(ChatModel.QWEN_PLUS).option(ChatOptions.ENABLE_INCREMENTAL_OUTPUT, true)
                .messages(messages).build();
        DashScopeClient.OpAsyncOpFlow<ChatResponse> chat = chatClient.chat(chatRequest);
        Flow.Publisher<ChatResponse> publisher = chat.flow().join();
        ChatReceiver chatReceiver = new ChatReceiver("", asyncContext);
        publisher.subscribe(chatReceiver);
    }

/**
     * 生成DashScopeClient对象
     *
     * @param response 响应 ,设置响应的参数
     * @return DashScopeClient对象,该对象为请求阿里云通用千问的客户端
     */
    private DashScopeClient getDashScopeClient(HttpServletResponse response) {
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Connection", "keep-alive");
        response.setHeader("Content-Type", "text/event-stream");
        return DashScopeClient.newBuilder().ak(aliOpenAIProperties.getApiKey()).executor(executor).build();
    }

/**
     * 追加embedding
     * 关于pgvector向量操作符的说明{@link <a href="https://zhuanlan.zhihu.com/p/641516393"/>}
     * <=>:该运算符计算两个向量之间的余弦相似度。余弦相似度比较两个向量的方向而不是它们的大小。余弦相似度的范围在 -1 到 1 之间,1 表示向量相同,0 表示无关,-1 表示向量指向相反方向。
     * 使用公式cosine_similarity = 1 - cosine_distance进行计算,距离越近,相似度越高
     * 余弦相似度的使用逻辑参照阿里云文档:{@link <a href="https://help.aliyun.com/zh/rds/apsaradb-rds-for-postgresql/pgvector-for-high-dimensional-vector-similarity-searches"/>}
     * @param message 消息
     */
    private void appendEmbedding(DashScopeClient chatClient, Message message) {

        String text = message.text();
        //查询阿里云该条数据的embedding向量数组集合
        EmbeddingRequest request = EmbeddingRequest.newBuilder()
                .model(EmbeddingModel.TEXT_EMBEDDING_V2)
                .documents(text)
                .build();
        EmbeddingResponse response = chatClient.embedding(request).async().join();
        float[] vector = response.output().embeddings().get(0).vector();
        Object[] neighborParams = new Object[]{new PGvector(vector)};
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT id, question, answer, 1 - ( embedding <=> ?) AS similarity" +
                " FROM jl_knowledge ORDER BY similarity DESC LIMIT 3", neighborParams);
        for (Map<String, Object> row : rows) {
            Double similarity = (Double) row.get("similarity");
            //阿里云的embedding相似度结果比较差,相似度取值不能太高
            if (similarity > 0.6) {
                message.contents().add(Content.ofText((String) row.get("answer")));
            }
        }
    }

总结

对于垂直领取的问题回答,我们如果想要做的更好的话,不同的场景使用的方式可能不太一样,前期调研,我们使用过gpt的fine-tuning去基于gpt-3.5模型的微调,但是因为语料库的数据有限,微调出来的效果并不理想,且微调的成本很高,最终我们使用embedding的形式来实现上述的功能。
如有问题,欢迎大家留言讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值