一、任务目标
为了清晰直观地展示RAG(检索增强生成)方法的有效性,我们手搓一套RAG的流程进行演示,作为后续LangChain等技术的预热。本文编程实践的目的是展示RAG的工作原理及流程(科普为主),不过多关注技术实现的效果和效率,因此如果是奔着代码拿来即用的目的阅读本文的话,那可能不会有什么大的收获。
二、常规的大模型问答
这里直接给出本文的示例Query:
“请告诉我,花花是一个怎么样的人”
下面,调用gpt-3.5-turbo大模型回答这个问题。我们使用openai的大模型接口,在这个过程中需要用到你个人的api_key等信息:
import openai
# 可以去openai官网或者国内某些镜像网站注册一个
openai.api_key = "你的api key"
openai.api_base = "你的api base"
query = '请告诉我,花花是一个怎么样的人'
chat_completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": query}]
)
response = chat_completion.choices[0].message.content
print(response)
下面是大模型的回答,很明显,大模型也没办法在没有任何背景知识的情况下回答这个问题:
三、基于RAG的大模型问答
1、知识库构建
首先,我们需要有一个存储着各类信息的知识库,里面需要有花花相关的内容。有了基本的知识储备,才能够正确回答问题。这里给出一个简单的知识库示例,其中包含了磊磊、花花、明明三个人各自性格、特长等信息:
dataBase = [
"磊磊是一个充满活力的北京小伙子,性格外向,总是人群中的焦点。他对生活充满了热情,喜欢与人交流,总是能够轻松地与陌生人打成一片。磊磊的运动爱好广泛,尤其擅长篮球和游泳,他经常参加各种体育活动,不仅锻炼了身体,也结交了许多志同道合的朋友。",
"磊磊的特长之一是公共演讲,无论是在学校的辩论赛还是公司的年会上,磊磊总能用他那富有感染力的言辞吸引听众的注意。此外,磊磊还对摄影有着浓厚的兴趣,喜欢捕捉生活中的美好瞬间,他的社交媒体上充满了他拍摄的风景和人物照片。",
"在人际交往方面,磊磊有着广泛的社交圈,他善于倾听,乐于助人,这使得他在朋友中非常受欢迎。磊磊总是能够用他的幽默感和积极态度为周围的人带来正能量。尽管磊磊的朋友众多,但磊磊也非常重视与家人的关系,经常抽时间陪伴家人,享受家庭的温暖。",
"花花是一个温柔而细腻的女孩,她的性格内向,喜欢安静和独处。花花来自江南的一个小镇,那里的宁静和美丽深深地影响了她的性格和审美。花花的爱好是绘画和写作,她喜欢用画笔捕捉自然的美丽,用文字记录内心的感受。",
"花花在社交场合可能显得有些害羞和保守,但她的真诚和善良总能赢得他人的尊重和喜爱。她更倾向于深度交流而非表面的寒暄,这使得她的友谊往往更加长久和稳固。在业余时间,花花喜欢阅读和研究心理学,这不仅帮助她更好地理解自己,也让她在与人交往时更加敏感和体贴。尽管她不常成为聚会的中心,但花花的存在总能为周围的人带来一份宁静和安慰。",
"明明,一个性格外向的年轻人,总是带着阳光般的笑容,他的热情和活力很容易感染周围的人。他出生在繁华的上海,这个城市的快节奏和多元文化塑造了他开朗和包容的性格。",
"明明的爱好多种多样,他特别喜欢户外运动,如徒步旅行和自行车骑行,这些活动不仅让他保持了健康的体魄,也让他有机会结识来自不同背景的朋友。明明还是各种社交活动的常客,无论是公司的团队建设还是朋友间的聚会,他总能成为调动气氛的关键人物。",
"在工作之余,明明也热衷于公益事业,他经常参与志愿者活动,帮助那些需要帮助的人。明明的这种乐于助人的精神,让他在社区中也赢得了良好的声誉。"
]
2、知识检索
我们的示例知识库规模很小,但是实际业务过程中,数据的规模可能非常大,我们不可能把整个知识库同时作为prompt输入给大模型。那一个基本的方案就是去检索跟Query相关的知识来辅助大模型理解,例如我们可以通过计算余弦相似度的方式来选择跟Query最相似的Top N条知识,然后合并这些知识到Prompt中。
由于我们这是一个简易版RAG,秉着简易的原则,我们在这里使用Word2Vec进行文本的向量化。这里,Word2Vec模型的训练语料就是dataBase,完成训练之后直接对dataBase中的每一句话进行向量化(Word2Vec句向量由所有词向量按位取平均所得)。
from gensim.models import Word2Vec
import numpy as np
import jieba
texts_cut = [list(jieba.cut(text)) for text in dataBase]
model = Word2Vec(sentences=texts_cut, vector_size=100, window=10, min_count=1, sg=1, epochs=100)
sentence_vectors = []
for sentence in texts_cut:
vector = np.mean([model.wv[word] for word in sentence if word in model.wv], axis=0)
sentence_vectors.append(vector)
现在,我们已经实现了知识库的向量化,接下来就是检索Query与知识库中的每一条知识的相似度了。这里我们直接使用Word2Vec对Query进行向量化,并比对Query向量和知识向量表示的相似度,由于数据量不大,这里直接使用for循环。
from scipy.spatial import distance
query_emb = np.mean([model.wv[word] for word in list(jieba.cut('请告诉我,花花是一个怎么样的人')) if word in model.wv], axis=0)
most_relative = -1
max_sims = -1
for idx, vec in enumerate(sentence_vectors):
cosine_similarity = 1 - distance.cosine(query_emb, vec)
print("向量ID:{}, 余弦相似度:{}".format(idx, cosine_similarity))
if cosine_similarity>max_sims:
max_sims = cosine_similarity
most_relative = idx
结果如下:
由于知识库数据量太小,这里我们只选择Top 1最相关的知识。可以看到,最相似的为ID为3的知识表示,对应文本为:
“花花是一个温柔而细腻的女孩,她的性格内向,喜欢安静和独处。花花来自江南的一个小镇,那里的宁静和美丽深深地影响了她的性格和审美。花花的爱好是绘画和写作,她喜欢用画笔捕捉自然的美丽,用文字记录内心的感受。”
3、大模型问答
我们把上述文本加入到prompt中并让大模型回答我们的问题:
query = '请告诉我,花花是一个怎么样的人。'
prompt = f"以下是一些相关的可用信息:{dataBase[most_relative]}"
chat_completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": query+prompt}]
)
response = chat_completion.choices[0].message.content
print(response)
结果如下,是否优雅了许多?
四、完整代码
import openai
from gensim.models import Word2Vec
import numpy as np
import jieba
from scipy.spatial import distance
# api配置
openai.api_key = "你的api key"
openai.api_base = "你的api base"
dataBase = [
"磊磊是一个充满活力的北京小伙子,性格外向,总是人群中的焦点。他对生活充满了热情,喜欢与人交流,总是能够轻松地与陌生人打成一片。磊磊的运动爱好广泛,尤其擅长篮球和游泳,他经常参加各种体育活动,不仅锻炼了身体,也结交了许多志同道合的朋友。",
"磊磊的特长之一是公共演讲,无论是在学校的辩论赛还是公司的年会上,磊磊总能用他那富有感染力的言辞吸引听众的注意。此外,磊磊还对摄影有着浓厚的兴趣,喜欢捕捉生活中的美好瞬间,他的社交媒体上充满了他拍摄的风景和人物照片。",
"在人际交往方面,磊磊有着广泛的社交圈,他善于倾听,乐于助人,这使得他在朋友中非常受欢迎。磊磊总是能够用他的幽默感和积极态度为周围的人带来正能量。尽管磊磊的朋友众多,但磊磊也非常重视与家人的关系,经常抽时间陪伴家人,享受家庭的温暖。",
"花花是一个温柔而细腻的女孩,她的性格内向,喜欢安静和独处。花花来自江南的一个小镇,那里的宁静和美丽深深地影响了她的性格和审美。花花的爱好是绘画和写作,她喜欢用画笔捕捉自然的美丽,用文字记录内心的感受。",
"花花在社交场合可能显得有些害羞和保守,但她的真诚和善良总能赢得他人的尊重和喜爱。她更倾向于深度交流而非表面的寒暄,这使得她的友谊往往更加长久和稳固。在业余时间,花花喜欢阅读和研究心理学,这不仅帮助她更好地理解自己,也让她在与人交往时更加敏感和体贴。尽管她不常成为聚会的中心,但花花的存在总能为周围的人带来一份宁静和安慰。",
"明明,一个性格外向的年轻人,总是带着阳光般的笑容,他的热情和活力很容易感染周围的人。他出生在繁华的上海,这个城市的快节奏和多元文化塑造了他开朗和包容的性格。",
"明明的爱好多种多样,他特别喜欢户外运动,如徒步旅行和自行车骑行,这些活动不仅让他保持了健康的体魄,也让他有机会结识来自不同背景的朋友。明明还是各种社交活动的常客,无论是公司的团队建设还是朋友间的聚会,他总能成为调动气氛的关键人物。",
"在工作之余,明明也热衷于公益事业,他经常参与志愿者活动,帮助那些需要帮助的人。明明的这种乐于助人的精神,让他在社区中也赢得了良好的声誉。"
]
# 知识库文本向量化
texts_cut = [list(jieba.cut(text)) for text in dataBase]
model = Word2Vec(sentences=texts_cut, vector_size=100, window=10, min_count=1, sg=1, epochs=100)
sentence_vectors = []
for sentence in texts_cut:
vector = np.mean([model.wv[word] for word in sentence if word in model.wv], axis=0)
sentence_vectors.append(vector)
print(sentence_vectors)
# query
query = '请告诉我,花花是一个怎么样的人。'
# 余弦相似度匹配/知识检索
query_emb = np.mean([model.wv[word] for word in list(jieba.cut(query)) if word in model.wv], axis=0)
most_relative = -1
max_sims = -1
for idx, vec in enumerate(sentence_vectors):
cosine_similarity = 1 - distance.cosine(query_emb, vec)
print("向量ID:{}, 余弦相似度:{}".format(idx, cosine_similarity))
if cosine_similarity>max_sims:
max_sims = cosine_similarity
most_relative = idx
# 结合知识库知识的大模型问答
prompt = f"以下是一些相关的可用信息:{dataBase[most_relative]}"
chat_completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": query+prompt}]
)
response = chat_completion.choices[0].message.content
print(response)
五、总结
本文使用极其简单的流程实现了一个简易版RAG。可见,结合RAG技术,大模型能够轻松回答原先无法回答的一些需要知识背景的问题。RAG的概念非常简单,但是它对于提升大模型的表现却非常有效。本文只是进行了一个科普性质的讲解,后续将会详细介绍RAG技术的实现。毕竟,实际业务知识库的规模可能达到了大数据的量级,因此必然无法直接逐条知识和Query比较相似度,这使得我们需要进行一些必要的检索优化;另外,将知识文本添加到Prompt中的方式也较为原始,实际上有更加高效的知识应用策略;最后,RAG的链路也早就有诸如LangChain等开源解决方案帮助我们进行开发了,无需人工重复造轮子。