LLM - 配置 GraphRAG + Ollama 服务 构建 中文知识图谱

news2024/12/24 2:30:26

欢迎关注我的CSDN:https://spike.blog.csdn.net/
本文地址:https://spike.blog.csdn.net/article/details/142795151

免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。


GraphRAG

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 个部分,即 .envsettings.yamlprompts

使用的 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_keymodelapi_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.pyquery.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_tokenstemperature,提升回答质量:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2200713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot的公司财务管理系统(含源码+sql+视频导入教程+文档+PPT)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于springboot的公司财务管理系统拥有两种角色 管理员&#xff1a;员工管理、部门管理、工资管理、资产管理、经营管理、利润管理等 员工&#xff1a;查看工资、查看公告、登录注册 1.…

OurTV 3.3.0 |流畅电视直播,收藏无忧

OurTV 是一款流畅的电视直播应用&#xff0c;支持电视版和手机版。增加收藏功能&#xff0c;修正了网络问题和潜在内存泄漏&#xff0c;调整最小版本到22&#xff0c;观看体验更佳。 大小&#xff1a;34M 百度网盘&#xff1a;https://pan.baidu.com/s/1UqEBfQx_1ztIUNx4fWopu…

《神经网络》—— 长短期记忆网络(Long Short-Term Memory,LSTM)

文章目录 一、LSTM的简单介绍二、 LSTM的核心组件三、 LSTM的优势四、 应用场景 一、LSTM的简单介绍 传统RNN循环神经网络的局限&#xff1a; 示例&#xff1a;当出现“我的职业是程序员。。。。。。我最擅长的是电脑”。当需要预测最后的词“电脑”。当前的信息建议下一个词可…

iOS Object-C 将数组倒置(倒叙)

使用NSArray自带的对象方法:reverseObjectEnumerator 代码如下: NSArray * tempArray [[NSArray alloc]initWithObjects:"a","b","c","d", nil]; //将tempArray转换成["d","c","b","a"]; …

PasteForm最佳CRUD实践,实际案例PasteTemplate详解之3000问(四)

无论100个表还是30个表&#xff0c;在使用PasteForm模式的时候&#xff0c;管理端的页面是一样的&#xff0c;大概4个页面&#xff0c; 利用不同操作模式下的不同dto数据模型&#xff0c;通过后端修改对应的dto可以做到控制前端的UI&#xff0c;在没有特别特殊的需求下可以做到…

【光追模组】雷神之锤4光追mod,调色并修改光影,并且支持光追效果,游戏画质大提升

大家好&#xff0c;今天小编我给大家继续引入一款游戏mod&#xff0c;这次这个模组主要是针对雷神之锤4进行修改&#xff0c;如果你觉得游戏本身光影有缺陷&#xff0c;觉得游戏色彩有点失真的话&#xff0c;或者说你想让雷神之锤4这款游戏增加对光线追踪的支持的话&#xff0c…

在docker中安装并运行mysql8.0.31

第一步&#xff1a;命令行拉取mysql镜像 docker pull mysql:8.0.31查看是否拉取成功 docker images mysql:latest第二步&#xff1a;运行mysql镜像&#xff0c;启动mysql实例 docker run -p 3307:3307 -e MYSQL_ROOT_PASSWORD"123456" -d mysql:8.0.313307:3307前…

FMCW 雷达芯片关键技术学习

CLOCK GENERATION 借助外部晶体产生的 50 MHz 时钟&#xff0c;时钟生成模块为 RF 子系统生成 76 至 81 GHz 时钟信号。时钟生成模块包含内置振荡器电路、参考 PLL、FMCW PLL 和 X4 乘法器。内置振荡器电路与外部晶体一起为参考 PLL 生成 50 MHz 时钟。参考 PLL 为 FMCW PLL 和…

腾讯云SDK项目管理

音视频终端 SDK&#xff08;腾讯云视立方&#xff09;控制台提供项目管理功能&#xff0c;您可参照以下步骤为您的应用快速添加音视频通话能力和多人音视频互动能力。 若需正式开发并上线音视频应用&#xff0c;请在完成创建后&#xff0c;参照 集成指南 进行开发包下载、集成…

fastadmin 列表页表格实现动态列

记录&#xff1a;fastadmin 列表页表格实现动态列 后端代码 /*** 商品库存余额表*/public function kucunbalance(){$houseList (new House)->where([shop_id>SHOP_ID])->order(id desc)->field(name,id)->select();//设置过滤方法$this->request->filte…

Java速成之反射,轻松搞定反射

Hello&#xff0c;大家好&#xff0c;我是Feri&#xff0c;一枚十多年的程序员&#xff0c;同时也是一名在读研究生&#xff0c;关注我&#xff0c;且看一个平凡的程序员如何在自我成长&#xff0c;只为各位小伙伴提供编程相关干货知识&#xff0c;希望在自我蜕变的路上&#x…

记录一次搭建Nacos集群的问题

Java环境&#xff1a;jdk1.8.0_231 Nacos版本&#xff1a;nacos-server-2.2.0.zip 虽然官方推荐的是3个节点&#xff0c;我们还是使用的是2个节点&#xff0c;首先解压创建nacos_config库&#xff0c;导入nacos/conf目录下的mysql-schema.sql SQL文件&#xff0c;如下表&…

ubuntu双系统分区划分

EFI系统分区&#xff08;Windows&#xff09;&#xff1a;自Windows 8起&#xff0c;UEFI模式下的BIOS使用该分区。简单来说&#xff0c;它用于存储已安装系统的EFI引导程序。此分区在资源管理器中无法查看&#xff0c;因为它没有驱动器号&#xff0c;但它必须存在&#xff0c;…

【ISAC】通感算一体化

北京邮电大学冯志勇&#xff1a;面向智能交通的通感算一体化网络技术 香港中文大学&#xff08;深圳&#xff09;许杰&#xff1a;面向通感算融合的无线资源优化 三者逻辑 感知增强: 多个视角的通信&#xff0c;感知其他视角看不到的通信增强&#xff1a;以前做信道估计都是盲的…

ctfshow-web 萌新题

给她 spring漏洞 pyload: 1.dirsearch扫描&#xff0c;发现git 2. GitHack工具得到.git文件 <?php $passsprintf("and pass%s",addslashes($_GET[pass])); $sqlsprintf("select * from user where name%s $pass",addslashes($_GET[name])); ?>…

01 Solidity--

第一个 Solidity 程序 Solidity 是一种用于编写以太坊虚拟机&#xff08;EVM&#xff09;智能合约的编程语言。 掌握 Solidity 是参与链上项目的必备技能 在 Remix 中&#xff0c;左侧菜单有三个按钮&#xff0c;分别对应文件&#xff08;编写代码&#xff09;、编译&#x…

CUDA编程基础概念

1. CPU和GPU 其中绿色的是计算单元&#xff0c;橙红色的是存储单元&#xff0c;橙黄色的是控制单元。 2. 什么样的任务适合GPU&#xff1f; 计算密集型的程序。易于并行的程序。GPU其实是一种SIMD(Single Instruction Multiple Data)架构。 3. 内存模型以及硬件 Device对应…

QD1-P9 HTML常用标签:超链接(1)

本节学习&#xff1a;HTML 超链接标签&#xff0c;也就是 a 标签。 在前端开发中&#xff0c;<a>​ 标签是超链接&#xff08;anchor&#xff09;标签&#xff0c;用于创建指向其他网页、文件、位置等的链接。 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p9 简单示…

【模板进阶】std::function

一、 s t d : : f u n c t i o n std::function std::function的介绍 s t d : : f u n c t i o n std::function std::function是 C 11 C11 C11引入的一个可调用对象包装器&#xff0c;它可以通过指定模板参数&#xff0c;统一来处理各自可调用对象。 二、实现类似 s t d : …

食家巷小程序:品味平凉美食,领略甘肃风情

甘肃平凉犹如一颗璀璨的明珠&#xff0c;散发着独特的魅力。这里不仅有壮丽的自然风光&#xff0c;更有令人垂涎欲滴的美食。而食家巷小程序&#xff0c;就如同一个神奇的美食宝库&#xff0c;将平凉的美味呈现在你的指尖。 &#x1f4a5;平凉&#xff0c;美食的天堂。 平凉红…