在开始看这篇文章之前,推荐你阅读前一篇概念介绍文章。这篇文章之内不会再额外介绍概念上的内容,仅仅从实践角度演示如何是用 HyDE 和 RAG 结合。正如同前文提到的那样,我们会是用 LangChain 构建这个演示 Demo。当然,如果你熟悉 LlamaIndex 或者其他框架,其实主要流程大致差不多,只不过是用的 API 和库有一些区别。这些就不再赘述。
问题背景
让我们假设你在为麦当劳制作一个问答系统。这个问答系统的目的是帮助用户解决一些常见问题,比如:麦当劳的营业时间,麦当劳的菜单,麦当劳的优惠活动等等。这些问题的答案通常是可以在麦当劳的官方网站上找到的,但是用户可能并不知道这些信息,或者他们可能会用自己的方式提出问题。这时候,我们就需要一个 RAG 模型来帮助我们回答这些问题。但是麦当劳实际上有非常多的商品,那么它的知识库可能会非常大,而且用户提出的问题可能并不在知识库中。这时候 HyDE 就可以帮助我们处理这些问题。
准备工作
我们会是用一些基本的 Python 相关工具(pdm 等等)、Ollama和chroma。关于 Ollama 的介绍,可以查看我的之前的文章。出于成本考虑,这里使用了开源的本地模型实现,请确保你的 Ollama 是在运行的状态。当然,如果你是用 GPT-4 Turbo(今天凌晨刚刚发布了新版本),效果要远比开源模型好很多。
mkdir hydemo && cd hydemo
pdm init
pdm add langchain chromadb beautifulsoup4
source .venv/bin/activate
请注意,在本文编写时,使用的 LangChain 的版本为 0.1.14,如果你的版本较低或者过高,可能存在部分 API 出入,请根据具体情况调整引用的模块和对应的 API。
功能实现
实现生成模拟文档
from langchain.chains.hyde.base import HypotheticalDocumentEmbedder
from langchain.chains.llm import LLMChain
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.globals import set_debug
from langchain.llms.ollama import Ollama
from langchain.vectorstores.chroma import Chroma
def main():
set_debug(True) # 设置 langchain 的调试模式,后面我们会看到具体的效果
llm = Ollama(model="qwen:7b") # 使用通义千问 7b 模型
olemb = OllamaEmbeddings(
model="nomic-embed-text"
) # 嵌入模型我们是使用的 nomic-embed-text
embeddings = HypotheticalDocumentEmbedder.from_llm(
llm,
base_embeddings=olemb,
prompt_key="web_search", # 加载预置的 Web Search Prompt
)
print(embeddings.llm_chain.prompt)
if __name__ == "__main__":
main()
让我们先执行一下,看一下 Prompt 会是什么样子的:
python -m src.hydemo.main
input_variables=['QUESTION'] template='Please write a passage to answer the question \nQuestion: {QUESTION}\nPassage:'
让我们开始对它进行提问吧,这个问题我们可能没有提供一些具体的指向,比如:“麦当劳生产哪些商品?”:
# 上面的一些代码重复代码我们就省略了
embeddings = HypotheticalDocumentEmbedder.from_llm(
llm,
base_embeddings=olemb,
prompt_key="web_search", # 加载预置的 Web Search Prompt
)
result = embeddings.embed_query("麦当劳生产哪些商品?")
print(result)
if __name__ == "__main__":
main()
现在我们重复执行一下获取一下结果:
> python -m src.hydemo.main
[llm/start] [1:llm:Ollama] Entering LLM run with input:
{
"prompts": [
"Please write a passage to answer the question \nQuestion: 麦当劳生产哪些商品?\nPassage:"
]
}
[llm/end] [1:llm:Ollama] [10.39s] Exiting LLM run with output:
{
"generations": [
[
{
"text": "麦当劳是一家全球知名的快餐连锁企业,主要生产和销售一系列快餐商品。这些商品包括:\n\n1. 快餐汉堡:如麦当劳的经典汉堡(Big Mac)、薯条汉堡(Filet-O-Fish)等。\n\n2. 炸鸡和鸡肉产品:例如麦辣鸡腿堡(McChicken Leg Sandwich)。\n\n3. 薯条和其他小食:比如炸薯条、麦乐鸡翅等。\n\n4. 饮料和咖啡:包括碳酸饮料、果汁、茶以及麦当劳的特调饮品,如冰爽可乐(Coca-Cola Ice Blast)。\n\n总之,麦当劳生产和销售的商品种类丰富多样,满足了不同消费者的需求。\n",
"generation_info": {
"model": "qwen:7b",
"created_at": "2024-04-09T11:15:31.251426Z",
"response": "",
"done": true,
"context": [
151644,
...省略内容...
8997
],
"total_duration": 10384822542,
"load_duration": 5002751375,
"prompt_eval_count": 33,
"prompt_eval_duration": 279537000,
"eval_count": 153,
"eval_duration": 5101916000
},
"type": "Generation"
}
]
],
"llm_output": null,
"run": null
}
[0.1035556048154831,...省略内容..., -0.0629570260643959]
这看上去似乎是有点太长了,我们能否把内容缩减的更短一点呢?这可以通过自定义模板实现:
from langchain.chains.hyde.base import HypotheticalDocumentEmbedder
from langchain.chains.llm import LLMChain
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.globals import set_debug
from langchain.llms.ollama import Ollama
from langchain.prompts import PromptTemplate
from langchain.vectorstores.chroma import Chroma
def main():
set_debug(True) # 设置 langchain 的调试模式,后面我们会看到具体的效果
prompt_template = """请使用单个食物回答用户的问题
问题: {question}
答案:"""
prompt = PromptTemplate(input_variables=["question"], template=prompt_template)
llm = Ollama(model="qwen:7b") # 使用通义千问 7b 模型
llm_chain = LLMChain(llm=llm, prompt=prompt)
olemb = OllamaEmbeddings(
model="nomic-embed-text"
) # 嵌入模型我们是使用的 nomic-embed-text
embeddings = HypotheticalDocumentEmbedder(
llm_chain=llm_chain,
base_embeddings=olemb,
)
result = embeddings.embed_query("麦当劳生产哪些商品?")
print(result)
if __name__ == "__main__":
main()
我们执行一下获取结果:
> python -m src.hydemo.main
[llm/start] [1:llm:Ollama] Entering LLM run with input:
{
"prompts": [
"请使用单个食物回答用户的问题\n问题: 麦当劳生产哪些商品?\n答案:"
]
}
[llm/end] [1:llm:Ollama] [4.92s] Exiting LLM run with output:
{
"generations": [
[
{
"text": "汉堡、薯条和饮料等快餐产品。\n",
"generation_info": {
"model": "qwen:7b",
"created_at": "2024-04-09T11:18:14.114445Z",
"response": "",
"done": true,
"context": [
151644,
..省略内容...
8997
],
"total_duration": 4913308166,
"load_duration": 4352575416,
"prompt_eval_count": 31,
"prompt_eval_duration": 164615000,
"eval_count": 13,
"eval_duration": 395684000
},
"type": "Generation"
}
]
],
"llm_output": null,
"run": null
}
[-0.00834527239203453, ...省略内容..., -0.5279630422592163]
当然,这一般适用于你知道目标的范围并且希望添加一些限制条件让结果更加聚焦。
你拿到这些向量数据之后就可以去具体的向量数据库中去搜索了。这里我使用一些来自于网络的麦当劳数据进行统计。
构建 RAG 文档
构建 RAG 文档有多种方式,可以读取网页上的内容,也可以通过 TXT,DOCX,PDF 等文件方式导入到向量数据库中。这里我是用 HTML 读取方式演示:
from langchain.chains.hyde.base import HypotheticalDocumentEmbedder
from langchain.chains.llm import LLMChain
from langchain.document_loaders.web_base import WebBaseLoader
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.globals import set_debug
from langchain.llms.ollama import Ollama
from langchain.prompts import PromptTemplate
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores.chroma import Chroma
def main():
set_debug(True) # 设置 langchain 的调试模式,后面我们会看到具体的效果
prompt_template = """请使用单个食物回答用户的问题
问题: {question}
答案:"""
prompt = PromptTemplate(input_variables=["question"], template=prompt_template)
llm = Ollama(model="qwen:7b") # 使用通义千问 7b 模型
llm_chain = LLMChain(llm=llm, prompt=prompt)
olemb = OllamaEmbeddings(
model="nomic-embed-text"
) # 嵌入模型我们是使用的 nomic-embed-text
embeddings = HypotheticalDocumentEmbedder(
llm_chain=llm_chain,
base_embeddings=olemb,
)
# result = embeddings.embed_query("麦当劳生产哪些商品?")
# print(result)
# 从指定的 URL 获取内容生成 RAG 内容。当然也可以从 txt,docx,pdf 之类的文件中读取。
docs = WebBaseLoader(
web_paths=("https://www.sohu.com/a/396117591_100107516",)
).load()
text_splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=0,
)
texts = text_splitter.split_documents(docs)
searcher = Chroma.from_documents(texts, embedding=embeddings)
query = "麦当劳哪个最畅销?"
found = searcher.similarity_search(query)
print(found[0].page_content)
if __name__ == "__main__":
main()
让我们执行一下这个命令看一下返回结果
> python -m src.hydemo.main
[llm/start] [1:llm:Ollama] Entering LLM run with input:
{
"prompts": [
"请使用单个食物回答用户的问题\n问题: 麦当劳哪个最畅销?\n答案:"
]
}
[llm/end] [1:llm:Ollama] [3.57s] Exiting LLM run with output:
...省略的内容...
"llm_output": null,
"run": null
}
10
Filet-O-Fish 麦香鱼堡
很多人心中的麦当劳单品top1,鱼柳外皮炸得金黄酥脆,内里又保留了鱼肉的鲜嫩多汁。美味鱼肉与滋味沙拉酱配搭,风味独特。返回搜狐,查看更多
...省略的内容...
组合在一起
我们已经有希望获得的结果了,我们需要把这个结果嵌入到我们的 LLM Chain 中去。
from langchain.chains.hyde.base import HypotheticalDocumentEmbedder
from langchain.chains.llm import LLMChain
from langchain.document_loaders.web_base import WebBaseLoader
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.globals import set_debug
from langchain.llms.ollama import Ollama
from langchain.prompts import PromptTemplate
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores.chroma import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def main():
set_debug(True) # 设置 langchain 的调试模式,后面我们会看到具体的效果
prompt_template = """请使用单个食物商品回答用户的问题
问题: {question}
答案:"""
prompt = PromptTemplate(input_variables=["question"], template=prompt_template)
llm = Ollama(model="qwen:7b") # 使用通义千问 7b 模型
llm_chain = LLMChain(llm=llm, prompt=prompt)
olemb = OllamaEmbeddings(
model="nomic-embed-text"
) # 嵌入模型我们是使用的 nomic-embed-text
embeddings = HypotheticalDocumentEmbedder(
llm_chain=llm_chain,
base_embeddings=olemb,
)
# 从指定的 URL 获取内容生成 RAG 内容。当然也可以从 txt,docx,pdf 之类的文件中读取。
docs = WebBaseLoader(
web_paths=("https://www.sohu.com/a/396117591_100107516",)
).load()
# 切割成 chunk
text_splitter = CharacterTextSplitter(
chunk_size=150,
chunk_overlap=0,
)
texts = text_splitter.split_documents(docs)
# 将 chunks 向量化保存
searcher = Chroma.from_documents(texts, embedding=embeddings)
retriever = searcher.as_retriever()
# 模拟用户问题
query = "麦当劳哪个商品在德布勒森最受喜爱?"
# 拼合数据
def format_docs(docs):
return "\n\n".join([d.page_content for d in docs])
# 构建链,并使用它
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
chain.invoke(query)
if __name__ == "__main__":
main()
让我们直接获取一下答案:
> python -m src.hydemo.main
...省略内容...
[chain/start] [1:chain:RunnableSequence > 9:parser:StrOutputParser] Entering Parser run with input:
{
"input": "在德布勒森最受喜爱的麦当劳商品可能是“薯条”(Potato Strips)。薯条是麦当劳经典菜单之一,深受各地消费者的喜爱。不过,具体最受欢迎的商品可能会因地区和时间而有所不同。\n"
}
[chain/end] [1:chain:RunnableSequence > 9:parser:StrOutputParser] [0ms] Exiting Parser run with output:
{
"output": "在德布勒森最受喜爱的麦当劳商品可能是“薯条”(Potato Strips)。薯条是麦当劳经典菜单之一,深受各地消费者的喜爱。不过,具体最受欢迎的商品可能会因地区和时间而有所不同。\n"
}
[chain/end] [1:chain:RunnableSequence] [6.67s] Exiting Chain run with output:
{
"output": "在德布勒森最受喜爱的麦当劳商品可能是“薯条”(Potato Strips)。薯条是麦当劳经典菜单之一,深受各地消费者的喜爱。不过,具体最受欢迎的商品可能会因地区和时间而有所不同。\n"
}
原始内容如下:
下一步(也许?)
本篇文章演示了如何是用 HyDE 和 RAG 结合的实际用例,其实这个例子基本上还没有达到生产环境的使用标准。当然实际是用过程中,可能会更加复杂,比如替换成一个更好的向量数据库。至于有没有第三篇…可能有可能没有,不过希望有吧!
STAY TUNED!