🤔 还在为大模型的“胡说八道”头疼? 🤔
你是否也遇到过这样的情况:满怀期待地向大模型(LLM)提问,得到的却是些驴唇不对马嘴的答案? 🤯 这可不是你提问的姿势不对,而是大模型“一本正经地胡说八道”了,也就是所谓的“幻觉”(Hallucination)。
别担心,你不是一个人!大模型虽强,但也有“软肋”:
* “脑子”里的知识会过时:就像我们不常更新自己的知识库一样,大模型也可能对新信息“一脸懵”。
* “偏科”严重:面对特定领域的专业问题,大模型可能就“抓瞎”了。
* “逻辑”有时会掉线:大模型在推理方面还有待加强,有时会给出些“匪夷所思”的结论。
😵 难道我们就只能对大模型的“幻觉”束手无策了吗? 😵
当然不是!今天,我们就来一起揭秘 AI 时代的“新宠”——检索增强生成技术(Retrieval-Augmented Generation,RAG),让你的大模型从此告别“幻觉”,变身“靠谱小助手”!
🚀 RAG:给大模型装上“外挂”,让它更懂你! 🚀
如果把大模型比作一个“学霸”,那么 RAG 就像是给这位“学霸”配备了一位“超级图书管理员”和一本“百科全书”。
这位“图书管理员”神通广大,能在海量文档中迅速找到“学霸”需要的知识(检索过程)。而这本“百科全书”则包罗万象,涵盖了各种领域的专业知识(文档数据库)。
有了这两样“神器”的加持,“学霸”在回答问题时,不仅能引经据典,还能结合最新信息,给出更准确、更全面的答案。
简单来说,RAG 的工作原理就是:
1. “提问”:你向大模型提出一个问题(Query)。
2. “检索”:“图书管理员”根据你的问题,从“百科全书”中找到相关的知识片段。
3. “生成”:“学霸”结合“图书管理员”提供的知识片段,生成更靠谱的答案。
有了RAG,大模型就能:
* 告别“幻觉”:有了“参考答案”,再也不怕胡说八道了。
* 及时更新知识:随时获取最新信息,告别“落伍”。
* 变身“专家”:轻松应对各领域专业问题。
* 推理更“靠谱”:有理有据,结论更可信。
🎉 是不是很神奇?想不想亲手打造一个属于自己的 RAG? 🎉
别急,跟着我一步步来,你也能轻松掌握这项“黑科技”!
🛠️ RAG 的“五脏六腑”:核心模块大揭秘! 🛠️
要构建一个 RAG,我们需要准备以下几个核心模块:
1. 向量化模块:将文档片段转换成计算机能理解的“数字指纹”(向量)。
2. 文档加载和切分模块:将各种格式的文档(如 PDF、Markdown、TXT 等)整理成“知识碎片”。
3. 数据库:存储“知识碎片”及其对应的“数字指纹”。
4. 检索模块:根据你的问题,找出最相关的“知识碎片”。
5. 大模型模块:结合检索到的“知识碎片”,生成最终答案。
不用担心这些模块听起来很复杂,其实它们就像乐高积木一样,只要按照一定的规则组合起来,就能搭建出一个强大的 RAG 系统。
💡 RAG 的“工作流程”:三步走战略! 💡
RAG 的工作流程可以概括为三个步骤:
1. 索引:将文档库切分成小块(Chunk),并为每个小块生成“数字指纹”(向量索引)。
2. 检索:根据你的问题,找到“数字指纹”最相似的文档片段。
3. 生成:大模型根据检索到的文档片段,生成最终答案。
🤓 手把手教你撸代码:从零开始构建 RAG! 🤓
接下来,我们将一步步实现 RAG 的各个核心模块,让你对 RAG 的内部运作机制有更深入的了解。
1️⃣ 向量化:给文档片段打上“数字指纹”
向量化模块的核心任务是将文本转换成向量。为了方便扩展和使用不同的模型,我们先定义一个向量化基类(BaseEmbeddings):
class BaseEmbeddings:
"""
Base class for embeddings
"""
def __init__(self, path: str, is_api: bool) -> None:
self.path = path
self.is_api = is_api
def get_embedding(self, text: str, model: str) -> List[float]:
raise NotImplementedError
@classmethod
def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float:
"""
calculate cosine similarity between two vectors
"""
dot_product = np.dot(vector1, vector2)
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)
if not magnitude:
return 0
return dot_product / magnitude
这个基类定义了两个方法:
* `get_embedding`:获取文本的向量表示(具体实现由子类完成)。
* `cosine_similarity`:计算两个向量的余弦相似度(用于衡量文档片段与问题的相关性)。
有了基类,我们就可以轻松实现各种 Embedding 模型,比如 OpenAI 的 Embedding API:
class OpenAIEmbedding(BaseEmbeddings):
"""
class for OpenAI embeddings
"""
def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api)
if self.is_api:
from openai import OpenAI
self.client = OpenAI()
self.client.api_key = os.getenv("OPENAI_API_KEY")
self.client.base_url = os.getenv("OPENAI_BASE_URL")
def get_embedding(self, text: str, model: str = "text-embedding-3-large") -> List[float]:
if self.is_api:
text = text.replace("\n", " ")
return self.client.embeddings.create(input=[text], model=model).data[0].embedding
else:
raise NotImplementedError
2️⃣ 文档加载和切分:把“大部头”变成“小纸条”
我们需要一个能处理各种文档格式(如 PDF、Markdown、TXT)的工具,并将它们切分成大小合适的“知识碎片”。
def read_file_content(cls, file_path: str):
# 根据文件扩展名选择读取方法
if file_path.endswith('.pdf'):
return cls.read_pdf(file_path)
elif file_path.endswith('.md'):
return cls.read_markdown(file_path)
elif file_path.endswith('.txt'):
return cls.read_text(file_path)
else:
raise ValueError("Unsupported file type")
切分文档时,要注意以下几点:
* 长度适中:太长了会影响检索效率,太短了又可能丢失关键信息。
* 内容重叠:相邻的“知识碎片”之间最好有一些重叠,以保证检索的连贯性。
* 句子完整:尽量以句子为单位进行切分,避免“断章取义”。
def get_chunk(cls, text: str, max_token_len: int = 600, cover_content: int = 150):
chunk_text = []
curr_len = 0
curr_chunk = ''
lines = text.split('\n') # 假设以换行符分割文本为行
for line in lines:
line = line.replace(' ', '')
line_len = len(enc.encode(line))
if line_len > max_token_len:
print('warning line_len = ', line_len)
if curr_len + line_len <= max_token_len:
curr_chunk += line
curr_chunk += '\n'
curr_len += line_len
curr_len += 1
else:
chunk_text.append(curr_chunk)
curr_chunk = curr_chunk[-cover_content:]+line
curr_len = line_len + cover_content
if curr_chunk:
chunk_text.append(curr_chunk)
return chunk_text
3️⃣ 数据库与向量检索:找到最相关的“知识碎片”
我们需要一个“仓库”来存储“知识碎片”及其“数字指纹”,并能根据问题快速找到最相关的“碎片”。
class VectorStore:
def __init__(self, document: List[str] = ['']) -> None:
self.document = document
def get_vector(self, EmbeddingModel: BaseEmbeddings) -> List[List[float]]:
# 获得文档的向量表示
pass
def persist(self, path: str = 'storage'):
# 数据库持久化,本地保存
pass
def load_vector(self, path: str = 'storage'):
# 从本地加载数据库
pass
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
# 根据问题检索相关的文档片段
pass
`query` 方法是向量检索的核心:
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
query_vector = EmbeddingModel.get_embedding(query)
result = np.array([self.get_similarity(query_vector, vector)
for vector in self.vectors])
return np.array(self.document)[result.argsort()[-k:][::-1]].tolist()
4️⃣ 大模型模块:生成最终答案
最后,我们需要一个大模型来根据检索到的“知识碎片”生成答案。
class BaseModel:
def __init__(self, path: str = '') -> None:
self.path = path
def chat(self, prompt: str, history: List[dict], content: str) -> str:
pass
def load_model(self):
pass
以 InternLM2-chat-7B 模型为例:
class InternLMChat(BaseModel):
def __init__(self, path: str = '') -> None:
super().__init__(path)
self.load_model()
def chat(self, prompt: str, history: List = [], content: str='') -> str:
prompt = PROMPT_TEMPLATE['InternLM_PROMPT_TEMPALTE'].format(question=prompt, context=content)
response, history = self.model.chat(self.tokenizer, prompt, history)
return response
def load_model(self):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
self.tokenizer = AutoTokenizer.from_pretrained(self.path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(self.path, torch_dtype=torch.float16, trust_remote_code=True).cuda()
为了方便管理,我们可以用一个字典来保存所有的 prompt:
PROMPT_TEMPLATE = dict(
InternLM_PROMPT_TEMPALTE="""先对上下文进行内容总结,再使用上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。
问题: {question}
可参考的上下文:
···
{context}
···
如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。
有用的回答:"""
)
✅ 大功告成:见证奇迹的时刻! ✅
现在,我们可以将所有模块组合起来,运行一个完整的 RAG Demo:
from RAG.VectorBase import VectorStore
from RAG.utils import ReadFiles
from RAG.LLM import OpenAIChat, InternLMChat
from RAG.Embeddings import JinaEmbedding, ZhipuEmbedding
# 没有保存数据库
docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150) # 获得data目录下的所有文件内容并分割
vector = VectorStore(docs)
embedding = ZhipuEmbedding() # 创建EmbeddingModel
vector.get_vector(EmbeddingModel=embedding)
vector.persist(path='storage') # 将向量和文档内容保存到storage目录下,下次再用就可以直接加载本地的数据库
question = 'git的原理是什么?'
content = vector.query(question, model='zhipu', k=1)[0]
chat = InternLMChat(path='model_path')
print(chat.chat(question, [], content))
我们也可以从本地加载已有的数据库:
from RAG.VectorBase import VectorStore
from RAG.utils import ReadFiles
from RAG.LLM import OpenAIChat, InternLMChat
from RAG.Embeddings import JinaEmbedding, ZhipuEmbedding
# 保存数据库之后
vector = VectorStore()
vector.load_vector('./storage') # 加载本地的数据库
question = 'git的原理是什么?'
embedding = ZhipuEmbedding() # 创建EmbeddingModel
content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = InternLMChat(path='model_path')
print(chat.chat(question, [], content))
👏 恭喜你,已经成功构建了一个属于自己的 RAG 系统! 👏
现在,你可以尽情地向它提问,感受它强大的知识整合能力和精准的回答能力吧!
📚 总结:RAG 的核心要点回顾 📚
让我们再来回顾一下 RAG 的核心要点:
* 五大模块:向量化、文档加载和切分、数据库、向量检索、大模型。
* 三步流程:索引、检索、生成。
* 核心优势:告别“幻觉”、知识及时更新、变身“专家”、推理更“靠谱”。
有了 RAG,大模型不再是“空中楼阁”,而是真正成为了我们工作和生活中的“得力助手”。
🚀 未来已来,让我们一起拥抱 RAG,探索大模型时代的无限可能! 🚀