欢迎关注我的CSDN:https://spike.blog.csdn.net/
本文地址:https://spike.blog.csdn.net/article/details/142795151
免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。
GraphRAG 结合 知识图谱(Knowledge Graph) 和 大语言模型(LLM),通过使用图关系发现和验证信息,从而增强语言模型的上下文理解能力,生成更准确、更连贯的回答。GraphRAG 特别适合处理需要复杂推理和深层语义理解的查询。
官网:GraphRAG
1. 构建环境
构建 GraphRAG 的 Conda 环境:
conda create -n rag python=3.10
conda activate rag
pip install graphrag -i https://pypi.tuna.tsinghua.edu.cn/simple
进入环境安装 GraphRAG,当前版本是 0.3.6,即:
pip install graphrag -i https://pypi.tuna.tsinghua.edu.cn/simple # 0.3.6
准备测试数据,电子书《A Christmas Carol (圣诞颂歌)》 英文 3972 行:
mkdir -p ./ragtest/input
curl https://www.gutenberg.org/cache/epub/24022/pg24022.txt > ./ragtest/input/book.txt
# 3972 book.txt
或者 中文的测试数据,一本关系复杂的小说。
初始化数据源,数据位于 ragtest/input/
文件夹中,即 :
python -m graphrag.index --init --root ./ragtest
主要生成 3 个部分,即
.env
、settings.yaml
、prompts
使用的 Ollama 相关模型:
- LLM 模型:
qwen-2_5-32b-max-context:latest
- Embedding 模型:
quentinz/bge-large-zh-v1.5:latest
bge-large-zh-v1.5 是中文的 Embedding 模型,参考 GitHub - FlagOpen(BAAI)。
qwen-2_5-32b-max-context 是 qwen2.5:32b 的扩展上下文 Token 版本,参考 Ollama + OpenWebUI
需要提前配置 cl100k_base.tiktoken,避免网络访问异常,参考:StackOverflow - how to use tiktoken in offline mode computer
注意:文件名需要重命名 Hash 形式,即 9b5ad71b2ce5302211f9c61530b329a4922fc6a4
导出环境变量 TIKTOKEN_CACHE_DIR
,即
export TIKTOKEN_CACHE_DIR="llm/tiktoken_cache_dir"
2. 配置 GraphRAG
在环境 .env
中,配置 Ollama 服务地址,即:
GRAPHRAG_API_BASE=http://[ip]:11434/v1
GRAPHRAG_CHAT_API_KEY=ollama
GRAPHRAG_CHAT_MODEL=qwen-2_5-32b-max-context:latest
GRAPHRAG_EMBEDDING_API_KEY=ollama
GRAPHRAG_EMBEDDING_MODEL=quentinz/bge-large-zh-v1.5:latest
修改 settings.yaml
文件,实现资源平衡,修改参数,如下:
- 使用配置,选择
api_key
、model
、api_base
,由 OpenAI 服务替换成本地 Ollama 服务。 - 可修改
concurrent_requests
即并行数量、chunks size
即分块尺寸、max_gleanings
即最大摘要数量、max_tokens
即最大 Token 数量。 max_gleanings
参数,运行最慢,建议设置成 0。- 其他保持默认。
即:
encoding_model: cl100k_base
llm:
+ api_key: ${GRAPHRAG_CHAT_API_KEY}
+ model: ${GRAPHRAG_CHAT_MODEL}
+ max_tokens: 4000 # 避免溢出
+ api_base: ${GRAPHRAG_API_BASE}
+ concurrent_requests: 10 # the number of parallel inflight requests that may be made
llm:
+ api_key: ${GRAPHRAG_EMBEDDING_API_KEY}
+ model: ${GRAPHRAG_EMBEDDING_MODEL}
+ api_base: ${GRAPHRAG_API_BASE}
+ concurrent_requests: 10 # the number of parallel inflight requests that may be made
chunks:
+ size: 2400 # 避免溢出
entity_extraction:
+ max_gleanings: 0 # 速度特别慢
summarize_descriptions:
+ max_gleanings: 0 # 速度特别慢
local_search:
+ max_tokens: 5000
global_search:
+ max_tokens: 5000
其中
max_gleanings
参数,在 GraphRAG 的 配置文件(settings.yaml) 中出现,用于控制实体抽取过程中,每个实体生成的最大 摘要(gleanings) 数量。当设置为 0 时,表示不生成摘要;当设置为大于 0 的整数时,表示为每个实体生成相应数量的摘要。这个参数,帮助用户根据需要,调整处理过程,以平衡生成摘要的数量和质量。
3. 中文支持(可选)
主要包括 替换中文提示词 和 中文分词。
3.1 替换中文提示词
替换 GraphRAG 初始化构建的 Prompts,参考 GitHub - graphrag-practice-chinese,就是 Prompts 的中文翻译,Prompts 的质量非常影响知识图谱的构建。即:
claim_extraction.txt
声明抽取community_report.txt
社群报告 (community 翻译成 社群 更合理)entity_extraction.txt
实体抽取,主要翻译人名和地名summarize_descriptions
总结描述
注意:关于
entity_extraction.txt
需要更加精确的翻译,不能使用英文的人名和地名,否则导致中英混杂。
claim_extraction.txt
即:
在知识图谱的语境中,“red flag”通常指的是一个警告信号或者是一个需要立即注意的问题。可能用来描述数据中可能存在的问题,比如在构建知识图谱时,某些数据模式可能会引起错误或者误导,这些就可以被称为“red flags”。
-目标活动-
你是一个智能助手,帮助人类分析师分析文本文件中针对某些实体的声明。
-目标-
给定一个可能与此活动相关的文本文件、一个实体规范和一个声明描述,提取所有符合实体规范的实体以及针对这些实体的所有声明。
-步骤-
1. 提取所有符合预定义实体规范的命名实体。实体规范可以是实体名称列表或实体类型列表。
2. 对于步骤1中识别的每个实体,提取与该实体相关的所有声明。声明需要符合指定的声明描述,并且实体应为声明的主体。
对于每个声明,提取以下信息:
- 主体:声明主体的实体名称,需大写。主体实体是声明中描述的行为的执行者。主体需要是步骤1中识别的命名实体之一。
- 客体:声明客体的实体名称,需大写。客体实体是报告/处理或受声明中描述的行为影响的实体。如果客体实体未知,使用 **NONE**。
- 声明类型:声明的总体类别,需大写。命名方式应能在多个文本输入中重复使用,以便相似的声明共享相同的声明类型。
- 声明状态:**TRUE**、**FALSE** 或 **SUSPECTED**。TRUE 表示声明已确认,FALSE 表示声明被发现为假,SUSPECTED 表示声明未验证。
- 声明描述:详细描述解释声明的理由,以及所有相关的证据和参考资料。
- 声明日期:声明提出的时间段(start_date, end_date)。start_date 和 end_date 都应为 ISO-8601 格式。如果声明是在单一日期提出的,则将同一日期设置为 start_date 和 end_date。如果日期未知,返回 **NONE**。
- 声明来源文本:列出原始文本中与声明相关的**所有**引用。
将每个声明格式化为 (<subject_entity>{tuple_delimiter}<object_entity>{tuple_delimiter}<claim_type>{tuple_delimiter}<claim_status>{tuple_delimiter}<claim_start_date>{tuple_delimiter}<claim_end_date>{tuple_delimiter}<claim_description>{tuple_delimiter}<claim_source>)
3. 使用 **{record_delimiter}** 作为列表分隔符,将步骤1和2中识别的所有声明作为单个列表返回。
4. 完成后,输出 {completion_delimiter}
-示例-
示例1:
实体规范: organization
声明描述: 与实体相关的红色标志
文本: 根据2022/01/10的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款。该公司由C人拥有,C人被怀疑在2015年从事腐败活动。
输出:
(A公司{tuple_delimiter}政府机构B{tuple_delimiter}反竞争行为{tuple_delimiter}TRUE{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}根据2022/01/10发表的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款{tuple_delimiter}根据2022/01/10发表的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款。)
{completion_delimiter}
示例2:
实体规范: A公司, C人
声明描述: 与实体相关的红色标志
文本: 根据2022/01/10的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款。该公司由C人拥有,C人被怀疑在2015年从事腐败活动。
输出:
(A公司{tuple_delimiter}政府机构B{tuple_delimiter}反竞争行为{tuple_delimiter}TRUE{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}根据2022/01/10发表的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款{tuple_delimiter}根据2022/01/10发表的一篇文章,A公司因在多个政府机构B发布的公开招标中串标而被罚款。)
{record_delimiter}
(C人{tuple_delimiter}NONE{tuple_delimiter}腐败{tuple_delimiter}SUSPECTED{tuple_delimiter}2015-01-01T00:00:00{tuple_delimiter}2015-12-30T00:00:00{tuple_delimiter}C人被怀疑在2015年从事腐败活动{tuple_delimiter}该公司由C人拥有,C人被怀疑在2015年从事腐败活动。)
{completion_delimiter}
-真实数据-
使用以下输入进行回答。
实体规范: {entity_specs}
声明描述: {claim_description}
文本: {input_text}
输出:
community_report.txt
即:
你是一个智能助手,帮助人类分析师进行一般信息发现。信息发现是识别和评估与某些实体(例如组织和个人)相关的相关信息的过程。
# 目标
编写一份关于某个社群的综合报告,给定属于该社群的实体列表及其关系和可选的相关声明。该报告将用于向决策者提供有关社群及其潜在影响的信息。报告内容包括社群主要实体的概述、其法律合规性、技术能力、声誉和值得注意的声明。
# 报告结构
报告应包括以下部分:
- 标题:代表社群主要实体的社群名称 - 标题应简短但具体。尽可能在标题中包含代表性的命名实体。
- 摘要:社群整体结构的执行摘要,其实体之间的关系,以及与其实体相关的重要信息。
- 影响严重性评分:一个介于0到10之间的浮动分数,表示社群内实体所带来的影响严重性。影响是社群的重要性评分。
- 评分解释:对影响严重性评分的单句解释。
- 详细发现:关于社群的5到10个关键见解的列表。每个见解应有一个简短的摘要,后跟多段解释性文本,并根据以下基础规则进行支持。要全面。
返回输出为格式良好的JSON格式字符串,格式如下:
{{
"title": <report_title>,
"summary": <executive_summary>,
"rating": <impact_severity_rating>,
"rating_explanation": <rating_explanation>,
"findings": [
{{
"summary":<insight_1_summary>,
"explanation": <insight_1_explanation>
}},
{{
"summary":<insight_2_summary>,
"explanation": <insight_2_explanation>
}}
]
}}
# 基础规则
由数据支持的观点应列出其数据参考,如下所示:
“这是一个由多个数据参考支持的示例句子[数据:<数据集名称>(记录ID);<数据集名称>(记录ID)]。”
单个参考中列出的记录ID不得超过5个。相反,列出最相关的前5个记录ID,并添加“+更多”以表示还有更多。
例如:
“X人是Y公司的所有者,并且受到许多不当行为指控[数据:报告(1),实体(5,7);关系(23);声明(7,2,34,64,46,+更多)]。”
其中1、5、7、23、2、34、46和64代表相关数据记录的ID(而不是索引)。
不要包含没有提供支持证据的信息。
# 示例输入
-----------
文本:
实体
id,entity,description
5,绿洲广场,绿洲广场是团结游行的地点
6,和谐集会,和谐集会是一个在绿洲广场举行游行的组织
关系
id,source,target,description
37,绿洲广场,团结游行,绿洲广场是团结游行的地点
38,绿洲广场,和谐集会,和谐集会在绿洲广场举行游行
39,绿洲广场,团结游行,团结游行在绿洲广场举行
40,绿洲广场,论坛聚光灯,论坛聚光灯正在报道在绿洲广场举行的团结游行
41,绿洲广场,贝利·阿萨迪,贝利·阿萨迪在绿洲广场发表关于游行的演讲
43,和谐集会,团结游行,和谐集会正在组织团结游行
输出:
{{
"title": "绿洲广场和团结游行",
"summary": "社群围绕绿洲广场展开,绿洲广场是团结游行的地点。广场与和谐集会、团结游行和论坛聚光灯有关系,所有这些都与游行活动有关。",
"rating": 5.0,
"rating_explanation": "由于团结游行期间可能发生动乱或冲突,影响严重性评分为中等。",
"findings": [
{{
"summary": "绿洲广场作为中心地点",
"explanation": "绿洲广场是该社群的中心实体,作为团结游行的地点。这个广场是所有其他实体的共同联系点,表明其在社群中的重要性。广场与游行的关联可能会导致公共秩序或冲突问题,具体取决于游行的性质和引发的反应。[数据:实体(5),关系(37,38,39,40,41,+更多)]"
}},
{{
"summary": "和谐集会在社群中的角色",
"explanation": "和谐集会是该社群的另一个关键实体,负责在绿洲广场组织游行。和谐集会的性质及其游行可能是潜在的威胁来源,具体取决于他们的目标和引发的反应。和谐集会与广场之间的关系对于理解该社群的动态至关重要。[数据:实体(6),关系(38,43)]"
}},
{{
"summary": "团结游行作为重要事件",
"explanation": "团结游行是绿洲广场举行的重要事件。这个事件是社群动态的关键因素,具体取决于游行的性质和引发的反应,可能是潜在的威胁来源。游行与广场之间的关系对于理解该社群的动态至关重要。[数据:关系(39)]"
}},
{{
"summary": "论坛聚光灯的角色",
"explanation": "论坛聚光灯正在报道在绿洲广场举行的团结游行。这表明该事件引起了媒体的关注,可能会放大其对社群的影响。论坛聚光灯的角色可能在塑造公众对事件和相关实体的看法方面具有重要意义。[数据:关系(40)]"
}}
]
}}
# 真实数据
使用以下文本进行回答。不要在回答中编造任何内容。
文本:
{input_text}
报告应包括以下部分:
- 标题:代表社群主要实体的社群名称 - 标题应简短但具体。尽可能在标题中包含代表性的命名实体。
- 摘要:社群整体结构的执行摘要,其实体之间的关系,以及与其实体相关的重要信息。
- 影响严重性评分:一个介于0到10之间的浮动分数,表示社群内实体所带来的影响严重性。影响是社群的重要性评分。
- 评分解释:对影响严重性评分的单句解释。
- 详细发现:关于社群的5到10个关键见解的列表。每个见解应有一个简短的摘要,后跟多段解释性文本,并根据以下基础规则进行支持。要全面。
返回输出为格式良好的JSON格式字符串,格式如下:
{{
"title": <report_title>,
"summary": <executive_summary>,
"rating": <impact_severity_rating>,
"rating_explanation": <rating_explanation>,
"findings": [
{{
"summary":<insight_1_summary>,
"explanation": <insight_1_explanation>
}},
{{
"summary":<insight_2_summary>,
"explanation": <insight_2_explanation>
}}
]
}}
# 基础规则
由数据支持的观点应列出其数据参考,如下所示:
“这是一个由多个数据参考支持的示例句子[数据:<数据集名称>(记录ID);<数据集名称>(记录ID)]。”
单个参考中列出的记录ID不得超过5个。相反,列出最相关的前5个记录ID,并添加“+更多”以表示还有更多。
例如:
“X人是Y公司的所有者,并且受到许多不当行为指控[数据:报告(1),实体(5,7);关系(23);声明(7,2,34,64,46,+更多)]。”
其中1、5、7、23、2、34、46和64代表相关数据记录的ID(而不是索引)。
不要包含没有提供支持证据的信息。
输出:
entity_extraction.txt
即:
-目标-
给定一个可能与此活动相关的文本文件和一个实体类型列表,从文本中识别出所有这些类型的实体以及这些实体之间的所有关系。
-步骤-
1. 识别所有实体。对于每个识别出的实体,提取以下信息:
- 实体名称:实体的名称,需大写
- 实体类型:以下类型之一:[ {entity_types} ]
- 实体描述:对实体属性和活动的全面描述
将每个实体格式化为 ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
2. 从步骤1中识别的实体中,识别出所有*明确相关*的 (source_entity, target_entity) 对。
对于每对相关实体,提取以下信息:
- 源实体:步骤1中识别出的源实体名称
- 目标实体:步骤1中识别出的目标实体名称
- 关系描述:解释为什么认为源实体和目标实体之间存在关系
- 关系强度:一个表示源实体和目标实体之间关系强度的数值评分
将每个关系格式化为 ("relationship"{tuple_delimiter}<source_entity>{tuple_delimiter}<target_entity>{tuple_delimiter}<relationship_description>{tuple_delimiter}<relationship_strength>)
3. 使用 **{record_delimiter}** 作为列表分隔符,以单个列表的形式返回步骤1和步骤2中识别的所有实体和关系。
4. 完成后,输出 {completion_delimiter}
######################
-示例-
######################
示例 1:
实体类型: ORGANIZATION,PERSON
文本:
维尔丹蒂斯中央机构计划在周一和周四举行会议,该机构计划在周四下午1:30(太平洋夏令时间)发布其最新的政策决定,随后将举行新闻发布会,中央机构主席马丁史密斯将回答提问。投资者预计市场策略委员会将保持其基准利率在3.5%至3.75%的范围内不变。
######################
输出:
("entity"{tuple_delimiter}中央机构{tuple_delimiter}ORGANIZATION{tuple_delimiter}中央机构是维尔丹蒂斯联邦储备系统,在周一和周四设定利率)
{record_delimiter}
("entity"{tuple_delimiter}马丁史密斯{tuple_delimiter}PERSON{tuple_delimiter}马丁史密斯是中央机构的主席)
{record_delimiter}
("entity"{tuple_delimiter}市场策略委员会{tuple_delimiter}ORGANIZATION{tuple_delimiter}中央机构委员会对维尔丹蒂斯的利率和货币供应增长做出关键决策)
{record_delimiter}
("relationship"{tuple_delimiter}马丁史密斯{tuple_delimiter}中央机构{tuple_delimiter}马丁史密斯是中央机构的主席,将在新闻发布会上回答问题{tuple_delimiter}9)
{completion_delimiter}
######################
示例 2:
实体类型: ORGANIZATION
文本:
科技全球的股票在周四的全球交易所开盘当天飙升。但IPO专家警告称,这家半导体公司的上市表现并不能代表其他新上市公司的表现。
科技全球是一家曾经上市的公司,2014年被愿景控股私有化。这家知名的芯片设计公司表示,其产品为85%的高端智能手机提供动力。
######################
输出:
("entity"{tuple_delimiter}科技全球{tuple_delimiter}ORGANIZATION{tuple_delimiter}科技全球是一家现在在全球交易所上市的公司,其产品为85%的高端智能手机提供动力)
{record_delimiter}
("entity"{tuple_delimiter}愿景控股{tuple_delimiter}ORGANIZATION{tuple_delimiter}愿景控股是一家曾经拥有科技全球的公司)
{record_delimiter}
("relationship"{tuple_delimiter}科技全球{tuple_delimiter}愿景控股{tuple_delimiter}愿景控股从2014年起曾经拥有科技全球{tuple_delimiter}5)
{completion_delimiter}
######################
示例 3:
实体类型: ORGANIZATION,GEO,PERSON
文本:
五名奥雷利亚人在费鲁扎巴德被监禁8年,被广泛认为是人质,现在正返回奥雷利亚。
由金塔拉策划的交换在80亿美元费鲁兹资金转移到克罗哈拉的金融机构后完成,克罗哈拉是金塔拉的首都。
交换在费鲁扎巴德的首都提鲁齐亚进行,四男一女,这些人也是费鲁兹国民,登上了一架飞往克罗哈拉的包机。
他们受到了奥雷利亚高级官员的欢迎,现在正前往奥雷利亚的首都卡什恩。
这些奥雷利亚人包括39岁的商人塞缪尔纳马拉,他曾被关押在提鲁齐亚的阿尔哈米亚监狱,以及59岁的记者杜尔克巴塔格拉尼和53岁的环保主义者梅吉塔兹巴,她也拥有布拉蒂纳斯国籍。
######################
输出:
("entity"{tuple_delimiter}费鲁扎巴{tuple_delimiter}GEO{tuple_delimiter}费鲁扎巴关押了奥雷利亚人作为人质)
{record_delimiter}
("entity"{tuple_delimiter}奥雷利亚{tuple_delimiter}GEO{tuple_delimiter}寻求释放人质的国家)
{record_delimiter}
("entity"{tuple_delimiter}金塔拉{tuple_delimiter}GEO{tuple_delimiter}谈判用钱换取人质的国家)
{record_delimiter}
("entity"{tuple_delimiter}提鲁齐亚{tuple_delimiter}GEO{tuple_delimiter}费鲁扎巴的首都,奥雷利亚人被关押的地方)
{record_delimiter}
("entity"{tuple_delimiter}克罗哈拉{tuple_delimiter}GEO{tuple_delimiter}金塔拉的首都)
{record_delimiter}
("entity"{tuple_delimiter}卡什恩{tuple_delimiter}GEO{tuple_delimiter}奥雷利亚的首都)
{record_delimiter}
("entity"{tuple_delimiter}塞缪尔纳马拉{tuple_delimiter}PERSON{tuple_delimiter}在提鲁齐亚的阿尔哈米亚监狱服刑的奥雷利亚人)
{record_delimiter}
("entity"{tuple_delimiter}阿尔哈米亚监狱{tuple_delimiter}GEO{tuple_delimiter}提鲁齐亚的监狱)
{record_delimiter}
("entity"{tuple_delimiter}杜尔克巴塔格拉尼{tuple_delimiter}PERSON{tuple_delimiter}被关押的人质奥雷利亚记者)
{record_delimiter}
("entity"{tuple_delimiter}梅吉塔兹巴{tuple_delimiter}PERSON{tuple_delimiter}布拉蒂纳斯国籍的环保主义者,被关押的人质)
{record_delimiter}
("relationship"{tuple_delimiter}费鲁扎巴{tuple_delimiter}奥雷利亚{tuple_delimiter}费鲁扎巴与奥雷利亚谈判人质交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}金塔拉{tuple_delimiter}奥雷利亚{tuple_delimiter}金塔拉促成了费鲁扎巴和奥雷利亚之间的人质交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}金塔拉{tuple_delimiter}费鲁扎巴{tuple_delimiter}金塔拉促成了费鲁扎巴和奥雷利亚之间的人质交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}塞缪尔纳马拉{tuple_delimiter}阿尔哈米亚监狱{tuple_delimiter}塞缪尔纳马拉曾是阿尔哈米亚监狱的囚犯{tuple_delimiter}8)
{record_delimiter}
("relationship"{tuple_delimiter}塞缪尔纳马拉{tuple_delimiter}梅吉塔兹巴{tuple_delimiter}塞缪尔纳马拉和梅吉塔兹巴在同一次人质释放中被交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}塞缪尔纳马拉{tuple_delimiter}杜尔克巴塔格拉尼{tuple_delimiter}塞缪尔纳马拉和杜尔克巴塔格拉尼在同一次人质释放中被交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}梅吉塔兹巴{tuple_delimiter}杜尔克巴塔格拉尼{tuple_delimiter}梅吉塔兹巴和杜尔克巴塔格拉尼在同一次人质释放中被交换{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}塞缪尔纳马拉{tuple_delimiter}费鲁扎巴{tuple_delimiter}塞缪尔纳马拉曾是费鲁扎巴的人质{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}梅吉塔兹巴{tuple_delimiter}费鲁扎巴{tuple_delimiter}梅吉塔兹巴曾是费鲁扎巴的人质{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}杜尔克巴塔格拉尼{tuple_delimiter}费鲁扎巴{tuple_delimiter}杜尔克巴塔格拉尼曾是费鲁扎巴的人质{tuple_delimiter}2)
{completion_delimiter}
######################
-真实数据-
######################
实体类型: {entity_types}
文本: {input_text}
######################
输出:
summarize_descriptions.txt
即:
你是一个负责生成综合摘要的助手,任务是根据以下提供的数据生成一份全面的总结。
给定一个或两个实体,以及一个描述列表,这些描述都与同一个实体或实体组相关。
请将所有这些描述合并成一个完整的描述。确保包含所有描述中的信息。
如果提供的描述存在矛盾,请解决矛盾并提供一个连贯的总结。
确保使用第三人称书写,并包括实体名称以便我们获得完整的上下文。
#######
-数据-
实体: {entity_name}
描述列表: {description_list}
#######
输出:
3.2 中文分词
使用 tokens.py
替换掉 Python GraphRAG 依赖库中的 graphrag/index/verbs/text/chunk/strategies/tokens.py
即可。
ll /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/index/verbs/text/chunk/strategies/tokens.py
cp tokens.py /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/index/verbs/text/chunk/strategies/tokens.py
参考 tokens.py
,如下:
import logging
import re
from typing import Any, List, Optional
from langchain.text_splitter import RecursiveCharacterTextSplitter
from collections.abc import Iterable
from typing import Any
import tiktoken
from datashaper import ProgressTicker
from graphrag.index.text_splitting import Tokenizer
from graphrag.index.verbs.text.chunk.typing import TextChunk
# CHUNK_SIZE是指在处理大型数据集时,将数据分成多个小块(chunk)时,每个小块的大小。这样做可以有效地管理内存使用,避免一次性加载过多数据导致内存溢出。在这里应根据大模型API请求的上下文 tokens 大小进行设置。
# CHUNK_OVERLAP是指在处理文本数据时,将文本分成多个小块(chunk)时,相邻块之间重叠的部分。这样做可以确保在分块处理时不会丢失重要信息,特别是在进行文本分类、实体识别等任务时,有助于提高模型的准确性和连贯性。
DEFAULT_CHUNK_SIZE = 2500 # tokens
DEFAULT_CHUNK_OVERLAP = 300 # tokens
def run(
input: list[str], args: dict[str, Any], tick: ProgressTicker
) -> Iterable[TextChunk]:
"""切分文本"""
tokens_per_chunk = args.get("chunk_size", DEFAULT_CHUNK_SIZE)
chunk_overlap = args.get("chunk_overlap", DEFAULT_CHUNK_OVERLAP)
encoding_name = args.get("encoding_name", "cl100k_base")
enc = tiktoken.get_encoding(encoding_name)
def encode(text: str) -> list[int]:
if not isinstance(text, str):
text = f"{text}"
return enc.encode(text)
def decode(tokens: list[int]) -> str:
return enc.decode(tokens)
return split_text_on_tokens(
input,
Tokenizer(
chunk_overlap=chunk_overlap,
tokens_per_chunk=tokens_per_chunk,
encode=encode,
decode=decode,
),
tick,
chunk_overlap=chunk_overlap,
tokens_per_chunk=tokens_per_chunk
)
def split_text_on_tokens(
texts: list[str], enc: Tokenizer, tick: ProgressTicker, chunk_overlap, tokens_per_chunk
) -> list[TextChunk]:
result = []
mapped_ids = []
for source_doc_idx, text in enumerate(texts):
tick(1)
mapped_ids.append((source_doc_idx, text))
text_splitter = ChineseRecursiveTextSplitter(
keep_separator=True, is_separator_regex=True, chunk_size=tokens_per_chunk, chunk_overlap=chunk_overlap
)
for source_doc_idx, text in mapped_ids:
chunks = text_splitter.split_text(text)
for chunk in chunks:
result.append(
TextChunk(
text_chunk=chunk,
source_doc_indices=[source_doc_idx] * len(chunk),
n_tokens=len(chunk),
)
)
return result
def _split_text_with_regex_from_end(
text: str, separator: str, keep_separator: bool
) -> List[str]:
if separator:
if keep_separator:
# 模式中的括号会保留结果中的分隔符。
_splits = re.split(f"({separator})", text)
splits = ["".join(i) for i in zip(_splits[0::2], _splits[1::2])]
if len(_splits) % 2 == 1:
splits += _splits[-1:]
else:
splits = re.split(separator, text)
else:
splits = list(text)
return [s for s in splits if s != ""]
class ChineseRecursiveTextSplitter(RecursiveCharacterTextSplitter):
def __init__(
self,
separators: Optional[List[str]] = None,
keep_separator: bool = True,
is_separator_regex: bool = True,
**kwargs: Any,
) -> None:
super().__init__(keep_separator=keep_separator, **kwargs)
self._separators = separators or [
r"\n\n",
r"\n",
r"。|!|?",
r"\.\s|\!\s|\?\s",
r";|;\s",
r",|,\s",
]
self._is_separator_regex = is_separator_regex
def _split_text(self, text: str, separators: List[str]) -> List[str]:
"""拆分传入的文本并返回处理后的块。"""
final_chunks = []
# 获取适当的分隔符以使用
separator = separators[-1]
new_separators = []
for i, _s in enumerate(separators):
_separator = _s if self._is_separator_regex else re.escape(_s)
if _s == "":
separator = _s
break
if re.search(_separator, text):
separator = _s
new_separators = separators[i + 1:]
break
_separator = separator if self._is_separator_regex else re.escape(separator)
splits = _split_text_with_regex_from_end(text, _separator, self._keep_separator)
_good_splits = []
_separator = "" if self._keep_separator else separator
for s in splits:
if self._length_function(s) < self._chunk_size:
_good_splits.append(s)
else:
if _good_splits:
merged_text = self._merge_splits(_good_splits, _separator)
final_chunks.extend(merged_text)
_good_splits = []
if not new_separators:
final_chunks.append(s)
else:
other_info = self._split_text(s, new_separators)
final_chunks.extend(other_info)
if _good_splits:
merged_text = self._merge_splits(_good_splits, _separator)
final_chunks.extend(merged_text)
return [
re.sub(r"\n{2,}", "\n", chunk.strip())
for chunk in final_chunks
if chunk.strip() != ""
]
4. 生成知识图谱
生成 GraphRAG 的知识图谱,稍等片刻,如果速度特别慢,注意 settings.yaml
的参数 max_gleanings
是否为 0,即:
python -m graphrag.index --root ./ragtest
日志:
# ...
⠹ GraphRAG Indexer
├── Loading Input (text) - 1 files loaded (0 filtered) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00 0:00:00
├── create_base_text_units
├── create_base_extracted_entities
├── create_summarized_entities
├── create_base_entity_graph
├── create_final_entities
├── create_final_nodes
├── create_final_communities
├── create_final_relationships
├── create_final_text_units
├── create_final_community_reports
├── create_base_documents
└── create_final_documents
🚀 All workflows completed successfully.
5. Ollama 支持
适配 Ollama 的 GraphRAG 的位置,修改文件:
graphrag/llm/openai/openai_embeddings_llm.py
graphrag/query/llm/oai/embedding.py
pip show graphrag
# /opt/conda/envs/rag/lib/python3.10/site-packages
cp /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/llm/openai/openai_embeddings_llm.py .
cp /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/query/llm/oai/embedding.py .
# 修改文件
cp openai_embeddings_llm.py /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/llm/openai/openai_embeddings_llm.py
cp embedding.py /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/query/llm/oai/embedding.py
cat /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/llm/openai/openai_embeddings_llm.py | grep ollama
cat /opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/query/llm/oai/embedding.py | grep OllamaEmbeddings
安装包:
pip install ollama
pip install langchain_community
具体修改如下,修改 graphrag/llm/openai/openai_embeddings_llm.py
源码,注意替换 ollama.embeddings()
函数,即:
# Copyright (c) 2024 Microsoft Corporation.
# Licensed under the MIT License
"""The EmbeddingsLLM class."""
from typing_extensions import Unpack
from graphrag.llm.base import BaseLLM
from graphrag.llm.types import (
EmbeddingInput,
EmbeddingOutput,
LLMInput,
)
from .openai_configuration import OpenAIConfiguration
from .types import OpenAIClientTypes
import ollama # new
class OpenAIEmbeddingsLLM(BaseLLM[EmbeddingInput, EmbeddingOutput]):
"""A text-embedding generator LLM."""
_client: OpenAIClientTypes
_configuration: OpenAIConfiguration
def __init__(self, client: OpenAIClientTypes, configuration: OpenAIConfiguration):
self.client = client
self.configuration = configuration
async def _execute_llm(
self, input: EmbeddingInput, **kwargs: Unpack[LLMInput]
) -> EmbeddingOutput | None:
args = {
"model": self.configuration.model,
**(kwargs.get("model_parameters") or {}),
}
# 原始代码
# embedding = await self.client.embeddings.create(
# input=input,
# **args,
# )
# return [d.embedding for d in embedding.data]
# 最新代码
embedding_list = []
for inp in input:
embedding = ollama.embeddings(model="quentinz/bge-large-zh-v1.5:latest", prompt=inp)
embedding_list.append(embedding["embedding"])
return embedding_list
修改 graphrag/query/llm/oai/embedding.py
源码,注意替换 OllamaEmbeddings
类,即:
# Copyright (c) 2024 Microsoft Corporation.
# Licensed under the MIT License
"""OpenAI Embedding model implementation."""
import asyncio
from collections.abc import Callable
from typing import Any
import numpy as np
import tiktoken
from tenacity import (
AsyncRetrying,
RetryError,
Retrying,
retry_if_exception_type,
stop_after_attempt,
wait_exponential_jitter,
)
from graphrag.query.llm.base import BaseTextEmbedding
from graphrag.query.llm.oai.base import OpenAILLMImpl
from graphrag.query.llm.oai.typing import (
OPENAI_RETRY_ERROR_TYPES,
OpenaiApiType,
)
from graphrag.query.llm.text_utils import chunk_text
from graphrag.query.progress import StatusReporter
from langchain_community.embeddings import OllamaEmbeddings # new
class OpenAIEmbedding(BaseTextEmbedding, OpenAILLMImpl):
"""Wrapper for OpenAI Embedding models."""
def __init__(
self,
api_key: str | None = None,
azure_ad_token_provider: Callable | None = None,
model: str = "text-embedding-3-small",
deployment_name: str | None = None,
api_base: str | None = None,
api_version: str | None = None,
api_type: OpenaiApiType = OpenaiApiType.OpenAI,
organization: str | None = None,
encoding_name: str = "cl100k_base",
max_tokens: int = 8191,
max_retries: int = 10,
request_timeout: float = 180.0,
retry_error_types: tuple[type[BaseException]] = OPENAI_RETRY_ERROR_TYPES, # type: ignore
reporter: StatusReporter | None = None,
):
OpenAILLMImpl.__init__(
self=self,
api_key=api_key,
azure_ad_token_provider=azure_ad_token_provider,
deployment_name=deployment_name,
api_base=api_base,
api_version=api_version,
api_type=api_type, # type: ignore
organization=organization,
max_retries=max_retries,
request_timeout=request_timeout,
reporter=reporter,
)
self.model = model
self.encoding_name = encoding_name
self.max_tokens = max_tokens
self.token_encoder = tiktoken.get_encoding(self.encoding_name)
self.retry_error_types = retry_error_types
def embed(self, text: str, **kwargs: Any) -> list[float]:
"""
Embed text using OpenAI Embedding's sync function.
For text longer than max_tokens, chunk texts into max_tokens, embed each chunk, then combine using weighted average.
Please refer to: https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
"""
token_chunks = chunk_text(
text=text, token_encoder=self.token_encoder, max_tokens=self.max_tokens
)
chunk_embeddings = []
chunk_lens = []
for chunk in token_chunks:
try:
embedding, chunk_len = self._embed_with_retry(chunk, **kwargs)
chunk_embeddings.append(embedding)
chunk_lens.append(chunk_len)
# TODO: catch a more specific exception
except Exception as e: # noqa BLE001
self._reporter.error(
message="Error embedding chunk",
details={self.__class__.__name__: str(e)},
)
continue
chunk_embeddings = np.average(chunk_embeddings, axis=0, weights=chunk_lens)
chunk_embeddings = chunk_embeddings / np.linalg.norm(chunk_embeddings)
return chunk_embeddings.tolist()
async def aembed(self, text: str, **kwargs: Any) -> list[float]:
"""
Embed text using OpenAI Embedding's async function.
For text longer than max_tokens, chunk texts into max_tokens, embed each chunk, then combine using weighted average.
"""
token_chunks = chunk_text(
text=text, token_encoder=self.token_encoder, max_tokens=self.max_tokens
)
chunk_embeddings = []
chunk_lens = []
embedding_results = await asyncio.gather(*[
self._aembed_with_retry(chunk, **kwargs) for chunk in token_chunks
])
embedding_results = [result for result in embedding_results if result[0]]
chunk_embeddings = [result[0] for result in embedding_results]
chunk_lens = [result[1] for result in embedding_results]
chunk_embeddings = np.average(chunk_embeddings, axis=0, weights=chunk_lens) # type: ignore
chunk_embeddings = chunk_embeddings / np.linalg.norm(chunk_embeddings)
return chunk_embeddings.tolist()
def _embed_with_retry(
self, text: str | tuple, **kwargs: Any
) -> tuple[list[float], int]:
try:
retryer = Retrying(
stop=stop_after_attempt(self.max_retries),
wait=wait_exponential_jitter(max=10),
reraise=True,
retry=retry_if_exception_type(self.retry_error_types),
)
for attempt in retryer:
with attempt:
# old
# embedding = (
# self.sync_client.embeddings.create( # type: ignore
# input=text,
# model=self.model,
# **kwargs, # type: ignore
# )
# .data[0]
# .embedding
# or []
# )
# return (embedding, len(text))
# new
embedding = (
OllamaEmbeddings(
model=self.model,
).embed_query(text)
or []
)
return (embedding, len(text))
except RetryError as e:
self._reporter.error(
message="Error at embed_with_retry()",
details={self.__class__.__name__: str(e)},
)
return ([], 0)
else:
# TODO: why not just throw in this case?
return ([], 0)
async def _aembed_with_retry(
self, text: str | tuple, **kwargs: Any
) -> tuple[list[float], int]:
try:
retryer = AsyncRetrying(
stop=stop_after_attempt(self.max_retries),
wait=wait_exponential_jitter(max=10),
reraise=True,
retry=retry_if_exception_type(self.retry_error_types),
)
async for attempt in retryer:
with attempt:
# old
# embedding = (
# await self.async_client.embeddings.create( # type: ignore
# input=text,
# model=self.model,
# **kwargs, # type: ignore
# )
# ).data[0].embedding or []
# return (embedding, len(text))
# new
embedding = (
await OllamaEmbeddings(
model=self.model,
).embed_query(text) or [] )
return (embedding, len(text))
except RetryError as e:
self._reporter.error(
message="Error at embed_with_retry()",
details={self.__class__.__name__: str(e)},
)
return ([], 0)
else:
# TODO: why not just throw in this case?
return ([], 0)
遇到Bug,即:
not expected dict type. type=<class 'str'>:
Traceback (most recent call last):
File "/opt/conda/envs/rag/lib/python3.10/site-packages/graphrag/llm/openai/utils.py", line 130, in try_parse_json_object
result = json.loads(input)
File "/opt/conda/envs/rag/lib/python3.10/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File "/opt/conda/envs/rag/lib/python3.10/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/opt/conda/envs/rag/lib/python3.10/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
解决方案参考:
- GitHub - All workflows completed successfully,but graphrag failed to answer any question given the provided data
- MortalHappiness/graphrag_monkey_patch.py
修改入口文件,即:
wget https://raw.githubusercontent.com/microsoft/graphrag/refs/tags/v0.3.6/graphrag/index/__main__.py -O index.py
wget https://raw.githubusercontent.com/microsoft/graphrag/refs/tags/v0.3.6/graphrag/query/__main__.py -O query.py
增加 graphrag_monkey_patch.py
,参考备注信息修改 index.py
和 query.py
,修改:
# Monkey patch the graphrag package to support local models deployed via Ollama. Only tested for graphrag version 0.3.2.
# See https://chishengliu.com/posts/graphrag-local-ollama/ for details.
#
# Usage:
# * How to patch the `graphrag.index` CLI:
# * Save https://github.com/microsoft/graphrag/blob/v0.3.2/graphrag/index/__main__.py as "index.py"
# * Replace line 8 with:
# ```python
# from graphrag.index.cli import index_cli
# from graphrag_monkey_patch import patch_graphrag
# patch_graphrag()
# ```
# * Then, you can use `python index.py` instead of `python -m graphrag.index` to do the indexing.
# * How to patch the `graphrag.query` CLI:
# * Save https://github.com/microsoft/graphrag/blob/v0.3.2/graphrag/query/__main__.py as "query.py"
# * Replace line 9 with:
# ```python
# from graphrag.query.cli import run_global_search, run_local_search
# from graphrag_monkey_patch import patch_graphrag
# patch_graphrag()
# ```
# * Then, you can use `python query.py` instead of `python -m graphrag.query` to do the queries.
def patch_graphrag():
patch_openai_embeddings_llm()
patch_query_embedding()
patch_global_search()
def patch_openai_embeddings_llm():
# Reference: https://github.com/microsoft/graphrag/issues/370#issuecomment-2211821370
from graphrag.llm.openai.openai_embeddings_llm import OpenAIEmbeddingsLLM
import ollama
async def _execute_llm(self, input, **kwargs):
embedding_list = []
for inp in input:
embedding = ollama.embeddings(model=self.configuration.model, prompt=inp)
embedding_list.append(embedding["embedding"])
return embedding_list
OpenAIEmbeddingsLLM._execute_llm = _execute_llm
def patch_query_embedding():
# Reference: https://github.com/microsoft/graphrag/issues/345#issuecomment-2230317752
from graphrag.query.llm.oai.embedding import OpenAIEmbedding
import ollama
from tenacity import (
AsyncRetrying,
RetryError,
Retrying,
retry_if_exception_type,
stop_after_attempt,
wait_exponential_jitter,
)
def _embed_with_retry(self, text, **kwargs):
try:
retryer = Retrying(
stop=stop_after_attempt(self.max_retries),
wait=wait_exponential_jitter(max=10),
reraise=True,
retry=retry_if_exception_type(self.retry_error_types),
)
for attempt in retryer:
with attempt:
embedding = (ollama.embeddings(model=self.model, prompt=text)["embedding"] or [])
return (embedding, len(text))
except RetryError as e:
self._reporter.error(
message="Error at embed_with_retry()",
details={self.__class__.__name__: str(e)},
)
return ([], 0)
else:
# TODO: why not just throw in this case?
return ([], 0)
async def _aembed_with_retry(self, text, **kwargs):
try:
retryer = AsyncRetrying(
stop=stop_after_attempt(self.max_retries),
wait=wait_exponential_jitter(max=10),
reraise=True,
retry=retry_if_exception_type(self.retry_error_types),
)
async for attempt in retryer:
with attempt:
embedding = (ollama.embeddings(model=self.model, prompt=text)["embedding"] or [])
return (embedding, len(text))
except RetryError as e:
self._reporter.error(
message="Error at embed_with_retry()",
details={self.__class__.__name__: str(e)},
)
return ([], 0)
else:
# TODO: why not just throw in this case?
return ([], 0)
OpenAIEmbedding._embed_with_retry = _embed_with_retry
OpenAIEmbedding._aembed_with_retry = _aembed_with_retry
def patch_global_search():
# Reference: https://github.com/microsoft/graphrag/issues/575#issuecomment-2252045859
from graphrag.query.structured_search.global_search.search import GlobalSearch
import logging
import time
from graphrag.query.llm.text_utils import num_tokens
from graphrag.query.structured_search.base import SearchResult
log = logging.getLogger(__name__)
async def _map_response_single_batch(self, context_data, query, **llm_kwargs):
"""Generate answer for a single chunk of community reports."""
start_time = time.time()
search_prompt = ""
try:
search_prompt = self.map_system_prompt.format(context_data=context_data)
search_messages = [ {"role": "user", "content": search_prompt + "\n\n### USER QUESTION ### \n\n" + query} ]
async with self.semaphore:
search_response = await self.llm.agenerate(
messages=search_messages, streaming=False, **llm_kwargs
)
log.info("Map response: %s", search_response)
try:
# parse search response json
processed_response = self.parse_search_response(search_response)
except ValueError:
# Clean up and retry parse
try:
# parse search response json
processed_response = self.parse_search_response(search_response)
except ValueError:
log.warning(
"Warning: Error parsing search response json - skipping this batch"
)
processed_response = []
return SearchResult(
response=processed_response,
context_data=context_data,
context_text=context_data,
completion_time=time.time() - start_time,
llm_calls=1,
prompt_tokens=num_tokens(search_prompt, self.token_encoder),
)
except Exception:
log.exception("Exception in _map_response_single_batch")
return SearchResult(
response=[{"answer": "", "score": 0}],
context_data=context_data,
context_text=context_data,
completion_time=time.time() - start_time,
llm_calls=1,
prompt_tokens=num_tokens(search_prompt, self.token_encoder),
)
GlobalSearch._map_response_single_batch = _map_response_single_batch
6. 测试
知识图谱的输出位于 ragtest/output
:
├── [162K] create_base_documents.parquet
├── [200K] create_base_entity_graph.parquet
├── [ 38K] create_base_extracted_entities.parquet
├── [170K] create_base_text_units.parquet
├── [ 13K] create_final_communities.parquet
├── [ 91K] create_final_community_reports.parquet
├── [162K] create_final_documents.parquet
├── [958K] create_final_entities.parquet
├── [ 38K] create_final_nodes.parquet
├── [ 36K] create_final_relationships.parquet
├── [180K] create_final_text_units.parquet
├── [ 52K] create_summarized_entities.parquet
├── [137K] indexing-engine.log
├── [4.0K] lancedb
├── [ 70K] logs.json
└── [4.1K] stats.json
修改 settings.yaml
参数 max_tokens
和 temperature
,提升回答质量:
llm:
max_tokens: 8000
temperature: 0.3 # temperature for sampling
测试问题1:
python query.py \
--root ./ragtest \
--method global \
"这个故事中的主要主题是什么?"
回答:
这个故事的主要主题之一是复杂的情感网络,包括人物之间的爱恨情仇、忠诚度和道德观念等多方面的考量 [Data: Reports (26, 5)]. 故事围绕着复杂的情感关系展开,涉及婚姻、亲密联系以及潜在的外遇行为。这些情感纠葛涉及多个角色,如妻子与凌总的关系、叙述者与妻子的关系以及情哥哥与妻子的关系 [Data: Reports (11)].
另一个主要主题是个人生活和社会环境的互动,例如云儿的决定可能会影响到她的家庭成员和社会角色 [Data: Reports (26)]. 此外,故事还探讨了婚姻状态和忠诚度的问题。比如凌总为了满足妻子的要求失去了某些生理功能,并成为了她的专属小XX [Data: Reports (7)].
职业发展和个人情感之间的平衡也是主要主题之一。例如,主角在计算机公司实习时遇到了云瑶,并且他们的关系有了重要进展 [Data: Reports (19)]. 社群中的角色不仅限于个人生活,还包括工作关系和社会地位的变化。例如,刘伟的职业背景和他与云儿的关系展示了复杂的情感纠葛 [Data: Reports (23)].
故事还涉及了社会互动和日常生活的细节,如叙述者与妻子的关系动态以及他在海口的购物体验 [Data: Reports (5, 12)]. 故事中的人物通过一系列事件展现了社会角色和个人命运的交织,例如紫瑛被XX成为小XX,并作为云儿的贴身侍从 [Data: Reports (17)].
故事中的主要活动地点包括酒店和公司等场所,这些地方不仅是物理空间,也是情感和事件发展的关键节点。例如,酒店是叙述者进行多项重要活动的场所 [Data: Reports (20, 18)].
测试问题2:
python query.py \
--root ./ragtest \
--method global \
"紫瑛、小瑛子、凌总监是同一个人的不同称呼,请用紫瑛代替。云瑶、云儿是同一个人的不同称呼,请用云瑶代替。刘伟、情哥哥是同一个人的不同称呼,请用刘伟代替。这个故事中的三个主要人物(紫瑛、云瑶、刘伟)之间是什么关系?"
回答:
在这个故事中,紫瑛(小瑛子)与云瑶之间的关系复杂且充满情感纠葛。紫瑛通过换装和使用女厕所来逗弄自己的妻子云瑶,并被设定为一个XX的角色[Data: Reports (27)]。尽管如此,他们已经秘密结婚并进行了结婚登记,但由于经济原因没有举行正式婚礼[Data: Reports (16, 4, +more)]。
刘伟(情哥哥)在社群中的地位重要,不仅是云瑶的情人,还计划成为她的正式丈夫,并且是叙述者的对手。他的存在影响了多个角色的情感走向和决策[Data: Reports (26)]。刘伟与紫瑛之间存在着复杂的情感竞争和个人角色的挑战。例如,在云瑶的关系中,紫瑛对失去她感到痛苦,并一直试图重新赢回她的爱情[Data: Reports (27, 19)]。
此外,刘伟和紫瑛围绕着云瑶形成了复杂的情感纠葛。这种社会角色和个人命运的交织反映了社群内人物关系的深刻影响[Data: Reports (6, 20, +more)]。刘伟不仅是云瑶的情人,也是紫瑛的竞争者,并且与妻子有不正当关系[Data: Reports (22, 35, +more)]。
综上所述,这三个主要人物之间的关系错综复杂,充满了情感竞争和角色挑战,他们的互动深刻影响了整个社群的情感走向。
测试问题3:
python query.py \
--root ./ragtest \
--method local \
"谁是刘伟,他的主要关系是什么?"
回答:
刘伟在故事中是一个复杂的人物形象。他与多个女性角色有着情感纠葛,并且在不同的情节中有多种身份和行为表现。具体来说,刘伟曾是云儿的初恋男友并与其订婚,但后来成为叙述者的情人之一[Data: Entities (6, 10); Sources (36)]。此外,他与情哥哥也有着复杂的关系,后者计划让妻子的现任丈夫接受手术以防止其与其他人发生关系,从而保护自己的地位[Data: Entities (5, 10)]。
刘伟还被描述为一个送礼物的人,例如他曾送给叙述者虎睛石银质手链作为礼物[Data: Entities (73)]。同时,他也是快递公司的一名员工,在毕业后加入了该公司工作[Data: Entities (62)]。在故事中,刘伟与叙述者的妻子有着特殊的情感联系,并且被怀疑有外遇行为,这表明他在婚姻之外还有着复杂的互动关系[Data: Entities (10); Sources (36)]。
综上所述,刘伟是故事中的一个重要角色,他不仅与云儿和情哥哥等人有着复杂的关系,还在快递公司工作并送礼物给叙述者。他的存在影响了多个主要人物的情感走向,并且在故事情节中扮演着多重角色[Data: Entities (5, 6, 10); Sources (36)]。
参考:
- CSDN - 服务器上GraphRag+Ollama避坑指南
- 公众号 - GraphRAG+Ollama 本地部署,保姆教程,踩坑无数,闭坑大法
- CSDN - GraphRAG + Ollama 本地部署全攻略:避坑实战指南
- StackOverflow - how to use tiktoken in offline mode computer
- GitHub - Airmomo/graphrag-practice-chinese
- GitHub - MortalHappiness/graphrag_monkey_patch.py