提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
在我们对接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向量计算出来存储到数据库
- 创建数据库的表结构,
此处我使用的是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) -- 指定数据维度
);
- 准备数据,将问题解析成向量维度数组数据存贮到数据库,题主使用的是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)
- 下面是题主根据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的形式来实现上述的功能。
如有问题,欢迎大家留言讨论