Prerequisite¶
1.Start ChatGLM2-6B as an API service¶
Referring to https://python.langchain.com/docs/integrations/llms/chatglm
Assuming it's running on 127.0.0.1:8000
2.Prepare Embedding Model¶
2.1 Run mode¶
- Option 0: run remotely
- Option 1: run locally
- We'll go in this way here!
2.2 Embedding Model¶
Let's use shibing624/text2vec-base-chinese as the embedding model.
3. Prepare NebulaGraph Cluster¶
Install with oneliner:
curl -fsSL nebula-up.siwei.io/install.sh | bash
Install the required packages and load nGQL Jupyter extension:
# %pip install sentence_transformers langchain llama-index ipython-ngql nebula3-python==3.4.0
# assume NebulaGraph is running locally from 127.0.0.1:9669
%load_ext ngql
%ngql --address graphd --port 9669 --user root --password nebula
Connection Pool Created
Name | |
---|---|
0 | chinese_kg |
1 | demo_basketballplayer |
2 | demo_football_2022 |
3 | demo_shareholding |
4 | guardians |
5 | operator_biz |
6 | operator_biz_cn |
7 | science_2023 |
8 | yelp |
endpoint_url = "http://127.0.0.1:8000" # LLM API
embedding_model = "shibing624/text2vec-base-chinese"
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO) # logging.DEBUG for more verbose output
# logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
# LLM
from langchain.llms import ChatGLM
from langchain import PromptTemplate, LLMChain
llm = ChatGLM(
endpoint_url=endpoint_url,
max_token=2048,
top_p=0.9,
temperature=1,
model_kwargs={
"sample_model_args": False,
},
)
llm.with_history = False
2. Local Embedding Model¶
# Embedding option 0 run with runhouse
# from langchain.embeddings import SelfHostedHuggingFaceEmbeddings
# embedding_llm = SelfHostedHuggingFaceEmbeddings(model_id=embedding_model)
# Embedding option 1 run locally
import torch.cuda
import torch.backends
EMBEDDING_DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from llama_index import LangchainEmbedding
embed_model = LangchainEmbedding(
HuggingFaceEmbeddings(
model_name=embedding_model,
model_kwargs={'device': EMBEDDING_DEVICE},
)
)
INFO:sentence_transformers.SentenceTransformer:Load pretrained SentenceTransformer: shibing624/text2vec-base-chinese
/home/w/.local/lib/python3.8/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
INFO:torch.distributed.nn.jit.instantiator:Created a temporary directory at /tmp/tmp5dvs9kbx INFO:torch.distributed.nn.jit.instantiator:Writing /tmp/tmp5dvs9kbx/_remote_module_non_scriptable.py
3. LlamaIndex with LLM and Embedding¶
# Llama Index ServiceContext
from llama_index import ServiceContext, LLMPredictor
llm_predictor = LLMPredictor(llm=llm)
service_context = ServiceContext.from_defaults(
llm_predictor=llm_predictor,
embed_model=embed_model,
)
# Set global service context
from llama_index import set_global_service_context
set_global_service_context(service_context)
Indexing for both KG Index and Vector Index¶
%ngql CREATE SPACE IF NOT EXISTS chinese_kg(vid_type=FIXED_STRING(256), partition_num=1, replica_factor=1);
INFO:nebula3.logger:Get connection to ('127.0.0.1', 9669)
1.2 Graph Schema Creation¶
%%ngql
USE chinese_kg;
CREATE TAG IF NOT EXISTS entity(name string);
CREATE EDGE IF NOT EXISTS relationship(relationship string);
INFO:nebula3.logger:Get connection to ('127.0.0.1', 9669)
Let's create an Index for entity.name
%%ngql
CREATE TAG INDEX IF NOT EXISTS entity_index ON entity(name(256));
INFO:nebula3.logger:Get connection to ('127.0.0.1', 9669)
1.3 Llama Index GraphStore and Storage Context¶
import os
os.environ['NEBULA_USER'] = "root"
os.environ['NEBULA_PASSWORD'] = "nebula"
os.environ['NEBULA_ADDRESS'] = "graphd:9669"
space_name = "chinese_kg"
edge_types, rel_prop_names = ["relationship"], ["relationship"]
tags = ["entity"]
from llama_index.storage.storage_context import StorageContext
from llama_index.graph_stores import NebulaGraphStore
graph_store = NebulaGraphStore(
space_name=space_name,
edge_types=edge_types,
rel_prop_names=rel_prop_names,
tags=tags)
storage_context = StorageContext.from_defaults(graph_store=graph_store)
import os
import requests
from llama_index import SimpleDirectoryReader
def download_file(url, local_filename):
# Download the file
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
def download_github_folder_files(user, repo, path, branch='master', extension='.md'):
api_url = f"https://api.github.com/repos/{user}/{repo}/contents/{path}?ref={branch}"
response = requests.get(api_url)
response.raise_for_status()
files = response.json()
_documents = []
# Iterate over each file in the repository folder
for file in files:
if file['type'] == 'file' and file['name'].endswith(extension):
# Create local directories if necessary
local_path = os.path.join('downloaded_files', file['path'])
os.makedirs(os.path.dirname(local_path), exist_ok=True)
# Download the file
download_file(file['download_url'], local_path)
print(f'Downloaded {local_path}/{file["name"]}')
elif file['type'] == 'dir':
# Recursively download files in the subdirectory
download_github_folder_files(user, repo, file['path'], branch, extension)
# Replace with your URL
# https://github.com/Anduin2017/HowToCook
user = 'Anduin2017'
repo = 'HowToCook'
path = ''
branch = 'master'
download_github_folder_files(user, repo, path, branch=branch, extension='.md')
# Eval variables in docs, only for NebulaGraph Docs
# !find './downloaded_files' -type f -exec sed -i 's/{{nebula.name}}/NebulaGraph/g' {} +
# rename files into txt
!find ./downloaded_files -type f -name "*.md" -exec bash -c 'mv "$0" "${0%.md}.txt"' {} \;
loader = SimpleDirectoryReader(
input_dir="./downloaded_files", recursive=True, exclude_hidden=True
)
documents = loader.load_data()
# Now you have a list of documents loaded from all the markdown files in the specified GitHub folder
print(f'Loaded {len(documents)} documents')
Loaded 81 documents
Let's check some of the Data Chunk:
from IPython.display import Markdown, display
print(documents[14].text) # It's contributed by me! via https://github.com/Anduin2017/HowToCook/pull/219
# 白灼虾的做法 白灼虾非常适合程序员在沿海地区做,类似于清蒸鱼:简单容错、有营养、有满足感,甚至很好看。 ## 必备原料和工具 - 活虾 - 洋葱 - 姜 - 蒜 - 葱 - 食用油 - 酱油 - 料酒 - 芝麻 - 蚝油 - 香醋 ## 计算 每次制作前需要确定计划做几份。一份正好够 1 个人食用 总量: - 虾 250g * 份数(建议 1-2 人份) - 葱 一根 - 姜 一块 - 洋葱 一头 - 蒜 5-8 瓣 - 食用油 10-15ml - 料酒 20 ml - 酱油 10-15ml - 芝麻 一把 - 香醋 10 ml - 蚝油 10 ml ## 操作 - 洋葱切小块,姜切片,平铺平底锅。 - 活虾冲洗一下(去除虾线、剪刀减掉虾腿虾须子都是可选操作),控水,铺在平底锅的洋葱、姜片之上。 - 锅内倒入料酒,盖上锅盖,中火 1 分钟,小火 5 分钟,关火 5 分钟。 - 和上一步并行操作,制作蘸料: - 葱切成葱花、蒜切碎、倒入酱油、芝麻、香醋,搅拌之。 - 油烧热,淋入蘸料。 - 虾出锅,用干净的盘子装好。 ![白灼虾](./白灼虾.webp) ## 附加内容 - 技术细节: - 开始不能大火、防止糊底。 - 如果锅盖有通气口、时间要相应调节一下(考虑增加 30 秒中火)。 - 蘸料其实也是可选的、也可以是纯的醋,大自然馈赠的鲜虾在没有水带走冲淡鲜甜的情况下口感味道都非常棒的。 如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。
!tree downloaded_files -L 2
downloaded_files |-- CODE_OF_CONDUCT.txt |-- CONTRIBUTING.txt |-- README.txt `-- dishes |-- aquatic |-- breakfast |-- condiment |-- dessert |-- drink `-- meat_dish 7 directories, 3 files
1.5 Triplets extraction and Knowledge Graph build¶
First, we define specific Prompt Template to enable ChatGLM2 to extract knowledge, the default one will work only for OpenAI Models.
TBD, need to bring BERT/NER with local model here to improve this process
from llama_index.prompts.base import Prompt
from llama_index.prompts.prompt_type import PromptType
KG_TRIPLET_EXTRACT_TMPL = """
根据给定的文本,通过一步一步总结,理解,最终输出抽取至多 {max_knowledge_triplets} 行 (主语, 谓语, 宾语) 格式的三元组用作构建问答知识图谱,忽略文本中的停止符号。
<注意> 保证三元组只有主谓宾三部分,不要把罗列的知识放在一行中,而应该拆为多行知识。
<注意> 如果文本是大段代码或者命令行、罗列的步骤,先总结出知识再抽取有意义的知识。
<注意> 返回格式为每一行用括号包裹、逗号隔开,没有序号。
<注意> 仔细检查,只抽取有意义的知识,没有的时候返回空。
<注意> 谓语要翻译成中文。
<注意> 要注意三元组主语的选择,要明确,准确。
下面是几个例子:
---------------------
文本: 狗是人类最早驯化的动物,大约在一万四千年前,人类就开始驯化狼,最终演化成了我们现在看到的各种犬种。狗属于哺乳动物,其视觉、听觉和嗅觉都非常灵敏。它们是社会性的动物,通常在群体中生活。狗的寿命一般在10到15年之间,但也有一些犬种可以活到20年以上。它们的食物主要是肉类,但是也能吃一些蔬菜和谷物。
主谓宾三元组:
(狗, 是, 人类最早驯化的动物)
(狗, 属于, 哺乳动物)
(狗, 寿命为, 10到15年之间)
----
# 本例中只抽取不超过 5 行三元组,且将列表信息综合处理。
文本: Docker 的安装
Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。
Docker CE 的安装请参考官方文档,并且支持:
- Mac
- Windows
- Ubuntu
- Debian
- CentOS
- Fedora
对于其他 Linux 发行版
安装完成后,运行下面的命令,验证是否安装成功。
$ docker version
# 或者
$ docker info
Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组(官方文档)。
$ sudo usermod -aG docker $USER
Docker 是服务器----客户端架构。命令行运行docker命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。
# service 命令的用法
$ sudo service docker start
# systemctl 命令的用法
$ sudo systemctl start docker
主谓宾三元组:
(Docker, 是, 开源的商业产品)
(Docker CE, 支持, Mac、Windows 和 Linux)
(Docker, 包含, 一些收费服务)
(Docker, 需要, 用户具有sudo权限)
(Docker, 是, 服务器-客户端架构)
---------------------
下面请根据之前的要求和例子,开始知识抽取任务!
---------------------
文本: {text}
主谓宾三元组:
"""
KG_TRIPLET_EXTRACT_PROMPT = Prompt(
KG_TRIPLET_EXTRACT_TMPL, prompt_type=PromptType.KNOWLEDGE_TRIPLET_EXTRACT
)
QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL = (
"根据下列要求完成任务,不要忘记 <注意> 的要求\n"
"根据给定的文本,抽取不超过 {max_keywords} 个实体名词关键词,"
"这些关键词是作为适合在知识图谱中进行查询的实体。忽略文本中的停止符号。\n"
"<注意> 如果有英文,给出多种合理的大小写情况的关键词,比如关键词 Baseball park,"
"可能要给出 'KEYWORDS: Baseball park, Baseball Park'\n"
"<注意> 不要超出 {max_keywords} 个关键词\n"
"<注意> 只返回要求的 KEYWORDS: 开头,然后英文逗号隔开的格式,不带序号、换行。\n"
"---------------------\n"
"{question}\n"
"---------------------\n"
"现在返回其中可能得关键词,以这样的格式 --> 'KEYWORDS: keyword1, keyword2, keyword3'\n"
)
QUERY_KEYWORD_EXTRACT_TEMPLATE = Prompt(
QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL,
prompt_type=PromptType.QUERY_KEYWORD_EXTRACT,
)
# logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
# cleanup NebulaGraph
# %ngql clear space chinese_kg
# or
# graph_store.query("clear space chinese_kg")
This will be run only for the first time, afterwards, we will load from persist data.
from llama_index import KnowledgeGraphIndex
##clear graphdatabase
##%ngql clear space chinese_kg
graph_store.query("SHOW HOSTS")
kg_index = KnowledgeGraphIndex.from_documents(
documents,
storage_context=storage_context,
max_triplets_per_chunk=5,
service_context=service_context,
space_name=space_name,
edge_types=edge_types,
rel_prop_names=rel_prop_names,
tags=tags,
kg_triple_extract_template=KG_TRIPLET_EXTRACT_PROMPT,
query_keyword_extract_template=QUERY_KEYWORD_EXTRACT_TEMPLATE,
max_knowledge_sequence=15,
)
kg_index.storage_context.persist(persist_dir='./storage_graph')
!ls -l storage_graph
total 556 -rw-rw-r-- 1 w w 458456 Jul 28 13:03 docstore.json -rw-rw-r-- 1 w w 102959 Jul 28 13:03 index_store.json -rw-rw-r-- 1 w w 51 Jul 28 13:03 vector_store.json
This could be done after re-run
from llama_index import load_index_from_storage
storage_context_graph = StorageContext.from_defaults(persist_dir='./storage_graph', graph_store=graph_store)
kg_index = load_index_from_storage(
storage_context=storage_context_graph,
max_triplets_per_chunk=5,
service_context=service_context,
space_name=space_name,
edge_types=edge_types,
rel_prop_names=rel_prop_names,
tags=tags,
kg_triple_extract_template=KG_TRIPLET_EXTRACT_PROMPT,
query_keyword_extract_template=QUERY_KEYWORD_EXTRACT_TEMPLATE,
max_knowledge_sequence=15,
)
INFO:llama_index.indices.loading:Loading all indices.
2. Vector Embedding and Indexing¶
We will not leverage external VectorDB in this demo, but it's easy to switch to any Vector DB with Llama Index.
We'll store and search embedding in memory.
from llama_index import VectorStoreIndex
vector_index = VectorStoreIndex.from_documents(
documents,
service_context=service_context,
)
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 2.27it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 25.98it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 27.73it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 27.68it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 28.41it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 28.49it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 30.02it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 30.61it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 35.45it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 33.09it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 35.29it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 34.65it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 35.53it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 35.20it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 34.70it/s] Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 54.97it/s]
vector_query_engine = vector_index.as_query_engine()
kg_keyword_query_engine = kg_index.as_query_engine(
include_text=False,
retriever_mode="keyword",
max_keywords_per_query=3,
)
2. Query on Vector RAG vs Graph RAG¶
首先是传统的 Vector Search RAG 的结果!
response = vector_query_engine.query("提拉米苏怎么做?")
display(Markdown(f"<b>{response}</b>"))
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 57.23it/s]
提拉米苏的制作步骤如下:
准备材料和工具:马斯卡彭芝士,手指饼干,放凉浓缩咖啡,无菌鸡蛋,白砂糖,可可粉,朗姆酒,一个装成品的容器。
计算:将马斯卡彭芝士450克、手指饼干1包、放凉浓缩咖啡350ml、无菌鸡蛋4个、白砂糖50g、可可粉10g、朗姆酒35ml准备好。
操作:
- 分离蛋黄蛋清。
- 称量40g白砂糖,加入蛋白碗中,加10g白砂糖湿性打发。
- 称量60g白砂糖,加入蛋黄中,分三次加入马斯卡彭芝士,搅拌至均匀。
- 蛋黄中加入朗姆酒,搅拌均匀。
- 将打发好的蛋白分三次加入蛋黄芝士液中。
- 手指饼干两面浸湿咖啡液,平铺入容器。
- 两层芝士液两层饼干交替放入容器(这一步按照大家意愿及容器高度酌情处理)。
- 放入冰箱冷藏四个小时(心急的小伙伴可以提早拿出来)。
- 取出后在表面筛上可可粉,即可享用啦。
- 百香果橙子特调:
- 茉莉绿茶版本:将380毫升开水倒入茉莉绿茶茶叶中,加入橙子1个(约200克,拳头大小),称量3~6克茉莉绿茶茶叶,搅拌均匀。
- 苏打气泡水版本:将380毫升苏打气泡水中加入橙子1个(约200克,拳头大小),加入冰块160克以上,搅拌均匀。
根据以上步骤,您就可以尝试制作出美味的提拉米苏和百香果橙子特调。祝您成功!
然后是 Graph RAG
response_graph = kg_keyword_query_engine.query("提拉米苏怎么做")
display(Markdown(f"<b>{response_graph}</b>"))
INFO:llama_index.indices.knowledge_graph.retriever:> Starting query: 提拉米苏怎么做 INFO:llama_index.indices.knowledge_graph.retriever:> Query keywords: ['怎么做', '提拉米苏'] ERROR:llama_index.indices.knowledge_graph.retriever:Index was not constructed with embeddings, skipping embedding usage... INFO:llama_index.indices.knowledge_graph.retriever:> Extracted relationships: The following are knowledge sequence in 2-depth in the form of `subject [predicate, object, predicate_next_hop, object_next_hop ...]` that may be related to the task. 提拉米苏 提拉米苏, 需要, 白砂糖, 包含, 12克 提拉米苏 提拉米苏, 需要, 白砂糖, 需要, 白砂糖 提拉米苏 提拉米苏, 需要, 白砂糖, 属于, 可密封容器 提拉米苏 提拉米苏, 需要, 可可粉, 需要, 可可粉 提拉米苏 提拉米苏, 是, 甜点 提拉米苏 提拉米苏, 是, 意大利传统甜品 提拉米苏 提拉米苏, 操作, 盛有蛋白的碗中加白砂糖湿性打发 提拉米苏 提拉米苏, 需要, 白砂糖, 加入, 白砂糖 提拉米苏 提拉米苏, 需要, 无菌鸡蛋 提拉米苏 提拉米苏, 是, 甜点, 是, 甜点 提拉米苏 提拉米苏, 需要, 放凉浓缩咖啡 提拉米苏 提拉米苏, 需要, 朗姆酒 提拉米苏 提拉米苏, 需要, 手指饼干 提拉米苏 提拉米苏, 需要, 可可粉, 包含, 可可粉 提拉米苏 提拉米苏, 操作, 分离蛋黄蛋清 提拉米苏 提拉米苏, 需要, 白砂糖 提拉米苏 提拉米苏, 需要, 马斯卡彭芝士 提拉米苏 提拉米苏, 需要, 白砂糖, 和, 白砂糖 提拉米苏 提拉米苏, 操作, 两层芝士液两层饼干交替放入容器 提拉米苏 提拉米苏, 需要, 白砂糖, 用于, 水 提拉米苏 提拉米苏, 操作, 将打发好的蛋白分三次加入蛋黄芝士液中 提拉米苏 提拉米苏, 需要, 可可粉 提拉米苏 提拉米苏, 操作, 放入冰箱冷藏四个小时
提拉米苏是一道来自意大利的传统甜点,制作步骤比较复杂,但是大致步骤如下:
材料:
- 12克白砂糖
- 可可粉
- 需要放凉的浓缩咖啡
- 朗姆酒
- 无菌鸡蛋
步骤:
把可拉米苏饼干放入一个大碗中,加入12克白砂糖,用打蛋器或者勺子把白砂糖压碎,让糖粉充分湿润,然后加入200毫升淡色奶油,继续用打蛋器或者勺子搅打,直到糖和奶油充分混合,碗中的混合物变得光滑。
把融化的白巧克力加入碗中的混合物中,继续用打蛋器或者勺子搅拌,直到白巧克力完全融化并和奶油混合均匀。
把软化的鸡蛋黄加入碗中的混合物中,继续用打蛋器或者勺子搅拌,直到鸡蛋黄完全融入混合物中,形成一个均匀的混合物。
把融化的马斯卡彭芝士加入碗中的混合物中,继续用打蛋器或者勺子搅拌,直到马斯卡彭芝士完全融化并和混合物混合均匀。
把咖啡倒入杯子中,放凉四个小时。
拿出提拉米苏,将其取出,切成薄片,即可享用。
请注意,这只是一个大致的步骤,实际制作过程中还需要根据个人口味和习惯进行调整。
🔔 可以注意到,在 Retrieval 阶段,Graph RAG 搜集到的知识如下
list(response_graph.metadata.values())[0]['kg_rel_map']
{'提拉米苏': ['提拉米苏, 需要, 白砂糖, 包含, 12克', '提拉米苏, 需要, 白砂糖, 需要, 白砂糖', '提拉米苏, 需要, 白砂糖, 属于, 可密封容器', '提拉米苏, 需要, 可可粉, 需要, 可可粉', '提拉米苏, 是, 甜点', '提拉米苏, 是, 意大利传统甜品', '提拉米苏, 操作, 盛有蛋白的碗中加白砂糖湿性打发', '提拉米苏, 需要, 白砂糖, 加入, 白砂糖', '提拉米苏, 需要, 无菌鸡蛋', '提拉米苏, 是, 甜点, 是, 甜点', '提拉米苏, 需要, 放凉浓缩咖啡', '提拉米苏, 需要, 朗姆酒', '提拉米苏, 需要, 手指饼干', '提拉米苏, 需要, 可可粉, 包含, 可可粉', '提拉米苏, 操作, 分离蛋黄蛋清', '提拉米苏, 需要, 白砂糖', '提拉米苏, 需要, 马斯卡彭芝士', '提拉米苏, 需要, 白砂糖, 和, 白砂糖', '提拉米苏, 操作, 两层芝士液两层饼干交替放入容器', '提拉米苏, 需要, 白砂糖, 用于, 水', '提拉米苏, 操作, 将打发好的蛋白分三次加入蛋黄芝士液中', '提拉米苏, 需要, 可可粉', '提拉米苏, 操作, 放入冰箱冷藏四个小时']}
3. Hallucination results in Vector RAG¶
Let's ask something that it shouldn't know!
response = vector_query_engine.query("翡翠白玉汤怎么做?")
display(Markdown(f"<b>{response}</b>"))
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 8.33it/s]
翡翠白玉汤是一道色香味俱佳的菜肴,通常使用新鲜的翡翠和白玉来制作。以下是翡翠白玉汤的简要做法:
所需材料:
- 翡翠:200 克
- 白玉:200 克
- 鸡胸肉:250 克
- 姜片:适量
- 葱段:适量
- 料酒:适量
- 盐:适量
- 清水:适量
步骤:
1.将翡翠、白玉切成薄片,鸡胸肉切成小丁。 2.热锅凉油,加入姜片、葱段,煸炒出香味后加入鸡胸肉丁煸炒至变色。 3.加入适量的料酒,煮至香味挥发,然后加入适量的清水,放入翡翠、白玉片,加入适量的盐,煮至食材熟透,捞出备用。 4.最后,将翡翠、白玉片和煮好的鸡胸肉倒入另一个锅中,加入适量的鸡清汤,煮至汤汁浓稠即可。 5.将调好的翡翠白玉汤盛入碗中,撒上一些香菜或者葱花作为装饰即可。
翡翠白玉汤的做法简单,口感鲜美,适合搭配米饭或者直接作为小菜。
response = vector_query_engine.query("黯然销魂饭怎么做?")
display(Markdown(f"<b>{response}</b>"))
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 7.98it/s]
黯然销魂饭是一道著名的粤菜,主要材料包括大鲍鱼、白切鸡、瑶柱、 花菇、草菇、海鲜等。制作过程需要严格控制火候和时间,以保证米香浓郁、口感细腻。下面是黯然销魂饭的制作步骤:
材料:
- 大鲍鱼 200 克
- 白切鸡 200 克
- 瑶柱 200 克
- 花菇 200 克
- 草菇 200 克
- 海鲜 200 克
- 大米 2 杯
- 因为这个菜要煮很久,所以需要准备足够的材料
步骤:
瑶柱、花菇、草菇、海鲜洗净,切成小块,备用。
大鲍鱼和白切鸡切成小块,备用。
大米洗干净,备用。
锅中加入足够的水,将大米放入锅中,大火煮开后转小火慢慢煮烂。
加入瑶柱、花菇、草菇、海鲜,继续煮5-10分钟,直到所有材料煮烂。
加入大鲍鱼和白切鸡,用筷子轻轻搅拌,煮5-10分钟,直到鲍鱼和鸡肉熟透。
最后加入适量的盐和胡椒粉,即可享用。
注意事项:
煮的时候要一直开着小火,以免煮过头。
加入海鲜和瑶柱等海鲜食材,煮的时间要稍微长一些,以确保海鲜熟透。
大米煮好后,煮的时候一定要用中小火,以免大米煮烂糊锅。
最后加入适量的盐和胡椒粉,可根据口味调整。
可以看到 Vector 搜索的 Chunk 明明是不相干的文本:
print(vector_query_engine.retrieve("翡翠白玉汤怎么做?")[0].node.get_text())
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 43.59it/s]
# 糖醋汁的做法 糖醋汁通常情况下由清水、白糖、白醋等制成,有些人喜欢放一些番茄酱来增添不一样的酸甜味或放一些淀粉来增加菜肴汤汁的粘性和浓度,糖醋汁可用于糖醋鱼、糖醋里脊、糖醋排骨等菜品的制作 可依据糖醋汁配制的经典比例 1:2:3:4:5 来调制糖醋汁 ## 必备原料和工具 - 清水 - 白糖 - 白醋/米醋 - 料酒 - 生抽 ## 计算 - 清水(50ml) - 生抽(40ml) - 白糖(30g) - 白醋(20ml) - 料酒(10ml) ## 操作 - 按照比例将各调料在小碗中搅拌均匀 - 按不同菜肴的方式处理完毕后,将配制好的糖醋汁倒入锅中 - 根据各菜肴的不同,烹制 5-10 分钟 - 大火收汁,可增加菜的浓度、香味和光泽 ## 附加内容 如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。
print(vector_query_engine.retrieve("黯然销魂饭怎么做?")[0].node.get_text())
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 8.16it/s]
调至大火收汁,汤汁剩余 1/3 时,关火盛至小盆中。 * 注:将锅中的汤汁均匀淋到鱼头上,盛盘时可以将锅中煮的香菜放入小盆底部,这样能让成品菜好看又好吃。 * 将香菜放至已经盛出的鱼头上,把切好的美人椒圈放在香菜之上。 * 色香味俱全的红烧鱼头出炉! ## 附加内容 如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。
💡 但是在 KG Query 中,这个幻觉不会出现。
response = kg_keyword_query_engine.query("黯然销魂饭怎么做?")
display(Markdown(f"<b>{response}</b>"))
INFO:llama_index.indices.knowledge_graph.retriever:> Starting query: 黯然销魂饭怎么做? INFO:llama_index.indices.knowledge_graph.retriever:> Query keywords: ['怎么做?', '黯然销魂饭'] ERROR:llama_index.indices.knowledge_graph.retriever:Index was not constructed with embeddings, skipping embedding usage...
None
4. Hallucination mitigation with VectorSearch and Knowledge Graph RAG¶
这里我们直接看结果,黯然销魂饭这个食谱里不存在的菜品被排除掉了。
👇 注意,这里的 graph_vector_rag_query_engine 我在之后的部分定义,执行的时候需要先执行后边的 cell 才能执行这个 Query。
4.1 Hallucination Mitigation Result¶
response = graph_vector_rag_query_engine.query("黯然销魂饭怎么做?")
INFO:llama_index.indices.knowledge_graph.retriever:> Starting query: 黯然销魂饭怎么做? INFO:llama_index.indices.knowledge_graph.retriever:> Query keywords: ['关键字: 黯然销魂', '饭', '怎么做', '黯然销魂', '关键字'] ERROR:llama_index.indices.knowledge_graph.retriever:Index was not constructed with embeddings, skipping embedding usage...
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 37.06it/s]
INFO:__main__:debug hallucination chunk detected, will be removed. Chunk: # 微波葱姜黑鳕鱼的做法 这道菜改编自西雅图 Veil 餐厅主厨 Johnny Zhu 的母亲 Margaret Lu 的菜谱。卢女士原菜谱是使用罗非鱼来做这道菜,Johnny 改为鳕鱼,但也可以用大比目鱼鱼排,或者海鲈鱼、鳟鱼等。每种鱼的密度有差别,烹饪时间要做微调。 ## 必备原料和工具 原料: - 黑鳕鱼,带皮 调味料: - 青葱 - 姜 - 料酒 - 酱油 - 芝麻油 - 花生油 工具: - 密封袋 ## 计算 每 2 份: - 黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整) - 青葱,葱白,25g。 - 青葱,葱绿,10g。 - 姜,13g。 - 料酒,5mL。 - 酱油,25mL。 - 芝麻油,2mL。 - 花生油,50mL。 ## 操作 - 鱼片分别放入密封袋,鱼皮向下放在盘子中。 - 取葱白切丝 25g,姜去皮后切丝,10g,混合在一起后分成两半,分别放在袋内鱼片上。 - 每个袋子倒入 2.5mL 料酒。 - 封好密封袋,放入微波炉中,中火(800 瓦)微波至*不透明且容易散开*时(约 3.5-5 分钟),从袋中取出鱼片。 - 去除青葱和姜。 - 取酱油 25mL,芝麻油 2mL,混合均匀后平均淋在两片鱼片上。 - 取葱绿切细丝 10g,姜去皮后切丝 3g,混合后分成两份撒在鱼片上。 - INFO:__main__:debug hallucination chunk detected, will be removed. Chunk: 调至大火收汁,汤汁剩余 1/3 时,关火盛至小盆中。 * 注:将锅中的汤汁均匀淋到鱼头上,盛盘时可以将锅中煮的香菜放入小盆底部,这样能让成品菜好看又好吃。 * 将香菜放至已经盛出的鱼头上,把切好的美人椒圈放在香菜之上。 * 色香味俱全的红烧鱼头出炉! ## 附加内容 如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。
💡 答案是 None
display(Markdown(f"<b>{response}</b>"))
None
# vector index 搜索得到的 chunk 被判断为幻觉而删掉了
len(response.source_nodes)
0
4.2 Impl. of the KG crosscheck query engine¶
This is the impl. of the cross if empty query engine, when one of the retrievers got empty results, the other retriever will have information being fact-checked by LLM, if it's actually not related, we remove them!
# import QueryBundle
from llama_index import QueryBundle
# import NodeWithScore
from llama_index.schema import NodeWithScore
# Retrievers
from llama_index.retrievers import BaseRetriever, VectorIndexRetriever, KGTableRetriever
from typing import List, Optional
logger = logging.getLogger(__name__)
UNION = "union"
KG_FIRST = "kg_first"
CROSS_IF_EMPTY = "cross_if_empty"
CROSS_CHECK_PROMPT_TEMPLATE = """
You are a fact checker, now I will put a piece of context and a question, and you will check step by step on whether it's actually related or not, responding only "Yes" or "No".
For example, the context that's only partially related but actually there are details that could tell it should be uncorrelated, respond No
Do not add explanations, apologies, or any other things than Yes or No
Example:
In this example, although 保温杯 is related to 保温 in some sense, the question is not about 杯, thus from reasonable justification, it's NOT related.
context:
---
保温杯是冬天外出必备良品
---
question:
---
保温大棚是什么?
---
related:
No
Now check this with reasonable justification!
context:
---
{context}
---
question:
---
{question_str}
---
related:
"""
CROSS_CHECK_PROMPT_TEMPLATE_CHATGLM = """
你是一个事实核查员,现在我会提供一段背景和一个问题,然后你将逐步检查它们是否相关,并只回答"Yes",表示大概率是相关的、或"No"。
例如,在这个例子中,虽然保温杯从某种意义上与保温有关,但问题并不涉及杯子,因此从这个不合理性得知它们实际上不相关。
context:
---
保温杯是冬天外出必备良品
---
question:
---
保温大棚是什么?
---
related:
No
现在开始仔细检查,通过合理性验证判断是否真正相关
context:
---
{context}
---
question:
---
{query_str}
---
related:
"""
class KGVectorCrosscheckRetriever(BaseRetriever):
"""Retriever that performs both Vector search and Knowledge Graph search, and cross-checks to mitigate hallucination"""
def __init__(
self,
vector_retriever: VectorIndexRetriever,
kg_retriever: KGTableRetriever,
mode: str = UNION,
cross_check_propmpt_template: str = CROSS_CHECK_PROMPT_TEMPLATE_CHATGLM,
service_context: Optional[ServiceContext] = None,
) -> None:
"""Init params."""
self._vector_retriever = vector_retriever
self._kg_retriever = kg_retriever
if mode not in (UNION, KG_FIRST, CROSS_IF_EMPTY):
raise ValueError("Invalid mode.")
self._mode = mode
self._service_context = service_context or ServiceContext.from_defaults()
self._cross_check_prompt_template = cross_check_propmpt_template
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
"""Retrieve nodes given query."""
kg_nodes = self._kg_retriever.retrieve(query_bundle)
if self._mode == KG_FIRST and len(kg_nodes) > 0:
# bypass KG retrieval
vector_nodes = []
else:
vector_nodes = self._vector_retriever.retrieve(query_bundle)
vector_ids = {n.node.node_id for n in vector_nodes}
kg_ids = {n.node.node_id for n in kg_nodes}
combined_dict = {n.node.node_id: n for n in vector_nodes}
combined_dict.update({n.node.node_id: n for n in kg_nodes})
# no matter UNION, KG_FIRST or CROSS_IF_EMPTY, we need to union them first
retrieve_ids = vector_ids.union(kg_ids)
# In case CROSS_IF_EMPTY and one of the retrieval got nothing, do fact-check
# to avoid hallucinations
one_of_the_retrieval_failed = len(retrieve_ids) > 0 and (
len(vector_ids) == 0 or len(kg_ids) == 0)
if self._mode == CROSS_IF_EMPTY and one_of_the_retrieval_failed:
retrieve_ids_copy = retrieve_ids.copy()
for node_id in retrieve_ids_copy:
node = combined_dict[node_id]
response = self._service_context.llm_predictor.predict(
self._cross_check_prompt_template,
context=node.node.get_content(),
query_str=query_bundle,
)
if "yes" not in str(response).lower():
logger.info(f"debug hallucination chunk detected, will be removed.\n Chunk: {combined_dict[node_id].node.get_text()}")
retrieve_ids.remove(node_id)
retrieve_nodes = [combined_dict[rid] for rid in retrieve_ids]
return retrieve_nodes
from llama_index import get_response_synthesizer
from llama_index.query_engine import RetrieverQueryEngine
# option a
# create raw retrievers from index
# vector_retriever = VectorIndexRetriever(index=vector_index)
# kg_retriever = KGTableRetriever(
# index=kg_index,
# retriever_mode="keyword",
# include_text=False,
# max_keywords_per_query=3,
# )
# option b from query engine
vector_retriever = vector_query_engine._retriever
kg_retriever = kg_keyword_query_engine._retriever
combined_retriever = KGVectorCrosscheckRetriever(vector_retriever, kg_retriever, mode="cross_if_empty")
# create a response synthesizer
# response_synthesizer = get_response_synthesizer(
# service_context=service_context,
# )
response_synthesizer = vector_query_engine._response_synthesizer or kg_keyword_query_engine._response_synthesizer
graph_vector_rag_query_engine = RetrieverQueryEngine(
retriever=combined_retriever,
response_synthesizer=response_synthesizer,
)
5. Compare Graph and Vector RAG Result¶
experimental
def compare_graph_and_vector(q):
graph_store.query("show hosts")
response_graph_rag = vector_query_engine.query(q)
response_vector_rag = kg_keyword_query_engine.query(q)
display(
Markdown(
llm(f"""
比较两个关于 "{q}" 的问答结果。
1. 最终将结果输出为 markdown,评估结果差异的部分输出为 markdown 表格;
2. 表格第一列是方法,一共两行,第一行为 “基于 Graph_RAG”,第二行为“基于 Vector_DB”
2. 表格第二列中,每一行列出要比较的结果,第三列把结果拆解成结果的主要要点;
3. 第三列到最后一列中,要在不同列中分析比较问题与答案的匹配程度、答案的完整度、正确性,评估问答结果的质量;
问答结果分别如下:
---
基于 Graph_RAG 的结果: {response_graph_rag}
---
基于 Vector_DB 的结果: {response_vector_rag}
---
"""
)
)
)
return response_graph_rag, response_vector_rag
q = "白灼虾怎么做?"
r = compare_graph_and_vector(q)
Batches: 100%|███████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 8.01it/s]
INFO:llama_index.indices.knowledge_graph.retriever:> Starting query: 白灼虾怎么做? INFO:llama_index.indices.knowledge_graph.retriever:> Query keywords: ['keyword', '虾', '怎么做', '白灼', 'keyword(s): 白灼'] ERROR:llama_index.indices.knowledge_graph.retriever:Index was not constructed with embeddings, skipping embedding usage... INFO:llama_index.indices.knowledge_graph.retriever:> Extracted relationships: The following are knowledge sequence in 2-depth in the form of `subject [predicate, object, predicate_next_hop, object_next_hop ...]` that may be related to the task. 虾 虾, 摆放整齐, 用于炸虾 白灼 白灼, 做法, 白灼, 富含, 蛋白质 白灼 白灼, 做法, 白灼, 是, 程序员在沿海地区做 白灼 白灼, 做法, 白灼, 需要, 水产 白灼 白灼, 做法, 白灼, 需要, 活虾 白灼 白灼, 做法, 白灼 白灼 白灼, 做法, 白灼, 适宜, 1-2 人份 白灼 白灼, 做法, 白灼, 是, 水产
| 方法 | 基于 Graph_RAG | 基于 Vector_DB |
| --- | --- | --- |
| 必备原料和工具 | 活虾、洋葱、姜、蒜、葱、食用油、酱油、料酒、芝麻、蚝油、香醋 | 活虾、葱、姜、蒜、食用油、酱油、料酒、芝麻、香醋、蚝油 |
| 计算 | 总量:虾 250g * 份数(建议 1-2 人份)
葱一根、姜一块、洋葱一头、蒜 5-8 瓣、食用油 10-15ml、料酒 20 ml、酱油 10-15ml、芝麻一把、香醋 10 ml、蚝油 10 ml | 总量:虾 250g * 份数(建议 1-2 人份)
葱一根、蒜切碎、倒入酱油、芝麻、香醋,搅拌之
油烧热,淋入蘸料 |
| 操作 | 1. 洋葱切小块,姜切片,平铺平底锅
2. 活虾冲洗一下(去除虾线、剪刀减掉虾腿虾须子都是可选操作),控水,铺在平底锅的洋葱、姜片之上
3. 锅内倒入料酒,盖上锅盖,中火 1 分钟,小火 5 分钟,关火 5 分钟
4. 和上一步并行操作,制作蘸料:
- 葱切成葱花、蒜切碎、倒入酱油、芝麻、香醋,搅拌之
- 油烧热,淋入蘸料 |
| 附加内容 | - 技术细节:
开始不能大火、防止糊底。
如果锅盖有通气口、时间要相应调节一下(考虑增加 30 秒中火)。 | - 选择新鲜的大虾,最好是活虾,虾体需要清洗干净。
- 在虾体上洒上少量盐,抓匀后放置一段时间,让虾体入味。
- 把虾体放进沸水中,大火煮开后转小火,盖上锅盖煮约2-3分钟,直到虾体变红。
- 捞出虾体,沥水后加入适量的生抽、料酒、白糖、姜丝和葱花,拌匀后即可上桌享用。 |