AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


将 Microsoft GraphRAG 集成到 Neo4j
发布日期:2024-08-06 12:38:12 浏览次数: 1706


微软的 GraphRAG 实现最近引起了广泛关注。在我上一篇博客文章中,我讨论了如何构建图表,并探讨了研究论文中强调的一些创新方面。从高层次来看,GraphRAG 库的输入是包含各种信息的源文档。使用大型语言模型 (LLM) 处理文档,以提取有关文档中出现的实体及其关系的结构化信息。然后使用提取的结构化信息来构建知识图谱。

知识图谱构建完成后,GraphRAG 库结合图算法,特别是莱顿社区检测算法和 LLM 提示,生成知识图谱中发现的实体和关系社区的自然语言摘要。

在这篇文章中,我们将从GraphRAG 库中获取输出,将其存储在 Neo4j 中,然后使用 LangChain 和 LlamaIndex 编排框架直接从 Neo4j 设置检索器。

代码和 GraphRAG 输出可在GitHub上访问,从而让您跳过 GraphRAG 提取过程。

数据集

这篇博文中介绍的数据集是查尔斯·狄更斯的《圣诞颂歌》,可通过古腾堡计划免费访问。


查尔斯·狄更斯的《圣诞颂歌》

免费的 kindle 书籍和 epub 由志愿者数字化并校对。

www.gutenberg.org


我们选择这本书作为源文档,因为它在介绍文档中突出显示,使我们能够毫不费力地进行提取。

图形构建

尽管您可以跳过图提取部分,但我们将讨论我认为最重要的几个配置选项。例如,图提取可能非常耗费 token 且成本高昂。因此,使用相对便宜但性能良好的 LLM(如 gpt-4o-mini)测试提取是有意义的。正如这篇博文所述,与 gpt-4-turbo 相比,成本降低幅度可能很大,同时保持良好的准确性。

GRAPHRAG_LLM_MODEL =gpt -4 o-mini

最重要的配置是我们要提取的实体类型。默认情况下,会提取组织、人员、事件和地理位置。

GRAPHRAG_ENTITY_EXTRACTION_ENTITY_TYPES =组织、人员、事件、地理

这些默认实体类型可能适用于书籍,但请确保根据给定用例要处理的文档的领域对其进行相应的更改。

另一个重要配置是最大收集值。作者发现,而且我们也单独验证了,LLM 不会在一次提取过程中提取所有可用信息。

收集配置允许 LLM 执行多次提取过程。在上图中,我们可以清楚地看到,在执行多次过程(收集)时,我们可以提取更多信息。多次过程需要大量令牌,因此像 gpt-4o-mini 这样更便宜的模型有助于降低成本。

GRAPHRAG_ENTITY_EXTRACTION_MAX_GLEANINGS = 1

此外,默认情况下不会提取声明或协变量信息。您可以通过设置配置来启用此功能GRAPHRAG_CLAIM_EXTRACTION_ENABLED

GRAPHRAG_CLAIM_EXTRACTION_ENABLED =
GRAPHRAG_CLAIM_EXTRACTION_MAX_GLEANINGS = 1

似乎并非所有结构化信息都能一次性提取出来,这是一个反复出现的主题。因此,我们在这里也有收集配置选项。

另一个有趣的部分是提示调整部分,但我还没有时间深入研究。提示调整是可选的,但强烈建议这样做,因为它可以提高准确性。


及时调整⚙️

GraphRAG 提供了创建领域自适应模板以生成知识图谱的功能。此步骤…

微软.github.io


配置完成后,我们可以按照说明运行图形提取管道,它包含以下步骤。

提取管道执行上图中的所有蓝色步骤。查看我之前的博客文章,了解有关图形构建和社区摘要的更多信息。MSFT GraphRAG 库的图形提取管道的输出是一组 parquet 文件,如Operation Dulce 示例所示。

这些 parquet 文件可以轻松导入 Neo4j 图形数据库,以便进行下游分析、可视化和检索。我们可以使用免费的云 Aura 实例或设置本地 Neo4j 环境。我的朋友Michael Hunger完成了将 parquet 文件导入 Neo4j 的大部分工作。我们将在本篇博文中跳过导入说明,但它包括从五六个 CSV 文件导入和构建知识图谱。如果您想了解有关 CSV 导入的更多信息,可以查看Neo4j Graph Academy 课程

导入代码以Jupyter 笔记本的形式在 GitHub 上提供,并附有示例 GraphRAG 输出。


blogs/msft_graphrag/ms_graphrag_import.ipynb at master · tomasonjo/blogs

Jupyter 笔记本支持我的图形数据科学博客文章,网址为 https://bratanic-tomaz.medium.com/...

github.com


导入完成后,我们可以打开 Neo4j 浏览器来验证和可视化导入的图表的各个部分。

图表分析

在开始实现检索器之前,我们将进行一个简单的图形分析,以熟悉提取的数据。我们首先定义数据库连接和一个执行 Cypher 语句(图形数据库查询语言)并输出 Pandas DataFrame 的函数。

NEO4J_URI="bolt://localhost"“bolt://localhost”
NEO4J_USERNAME = “neo4j”
NEO4J_PASSWORD = “密码”

driver = GraphDatabase.driver(NEO4J_URI,auth =(NEO4J_USERNAME,NEO4J_PASSWORD))

def db_querycypher:str,params:Dict [ strAny ] = {})-> pd.DataFrame:
“”执行Cypher语句并返回DataFrame“”
返回driver.execute_query(
cypher,parameters_ = params,result_transformer_ = Result.to_df

在执行图形提取时,我们使用了 300 的块大小。从那时起,作者将默认块大小更改为 1200。我们可以使用以下 Cypher 语句验证块大小。

db_query( 
“MATCH (n:__Chunk__) RETURN n.n_tokens as token_count,count(*) AS count”“匹配(n:__Chunk__)将 n.n_tokens 返回为 token_count,count(*)AS count”

# token_count count
# 300 230
# 155 1

230 个块有 300 个 token,而最后一个只有 155 个 token。现在让我们检查一个示例实体及其描述。

db_query( 
"MATCH (n:__Entity__) RETURN n.name AS name, n.description AS description LIMIT 1"“MATCH(n:__Entity__)RETURN n.name AS name,n.description AS description LIMIT 1”

结果

书中似乎在某处描述了古腾堡计划,可能是在开头。我们可以观察到描述如何能够捕捉比实体名称更详细和复杂的信息,MSFT GraphRAG 论文介绍了实体名称,以从文本中保留更复杂和细微的数据。

我们也来检查一下示例关系。

db_query( 
"MATCH ()-[n:RELATED]->() RETURN n.description AS 描述 LIMIT 5"“MATCH ()-[n:RELATED]->() RETURN n.description AS description LIMIT 5”

结果

MSFT GraphRAG 不仅仅能够提取实体之间的简单关系类型,还能捕获详细的关系描述。此功能使其能够捕获比直接关系类型更细致入微的信息。

我们还可以检查单个社区及其生成的描述。

db_query(""""""
匹配 (n:__Community__)
返回 n.title 作为标题、n.summary 作为摘要、n.full_content 作为完整内容 LIMIT 1
"""
)

结果

社区有标题、摘要和使用 LLM 生成的完整内容。我还没有看到作者在检索过程中是使用完整上下文还是仅使用摘要,但我们可以在两者之间进行选择。我们可以在 full_content 中观察到引文,这些引文指向信息来源的实体和关系。有趣的是,如果引文太长,LLM 有时会将其删减,如下例所示。

[数据:实体 (11, 177);关系 (25, 159, 20, 29, +更多)]11 , 177 ); 关系 ( 25 , 159 , 20 , 29 , +更多)]

没有办法扩展+more符号,所以这是处理 LLM 长引用的一种有趣方式。

现在让我们评估一些分布。我们首先检查从文本块中提取的实体数量的分布。

entity_df = db_query( 
""""""
MATCH (d:__Chunk__)
RETURN count {(d)-[:HAS_ENTITY]->()} AS entity_count
"""

)
# 绘制分布
plt.figure(figsize=( 10 , 6 ))
sns.histplot(entity_df[ 'entity_count' ], kde= True , bins= 15 , color= 'skyblue' )
plt.axvline(entity_df[ 'entity_count' ].mean(), color= 'red' , linestyle= 'dashed' , linewidth= 1 )
plt.axvline(entity_df[ 'entity_count' ].median(), color= 'green' , linestyle= 'dashed' , linewidth= 1 )
plt.xlabel( 'Entity Count' , fontsize= 12 )
plt.ylabel( '频率',fontsize= 12
plt.title('实体数量分布',fontsize= 15
plt.legend({ '平均值':entity_df [ 'entity_count' ] .mean(),'中位数':entity_df [ 'entity_count' ] .median()})
plt.show()

结果

请记住,文本块有 300 个标记。因此,提取的实体数量相对较小,平均每个文本块约有 3 个实体。提取是在没有任何收集的情况下完成的(一次提取过程)。如果我们增加收集数量,看看分布情况会很有趣。

接下来,我们将评估节点度分布。节点度是节点具有的关系数量。

degree_dist_df = db_query( 
""""""
MATCH (e:__Entity__)
RETURN count {(e)-[:RELATED]-()} AS node_degree
"""

)
# 计算平均值和中位数
mean_degree = np.mean(degree_dist_df[ 'node_degree' ])
percentiles = np.percentile(degree_dist_df[ 'node_degree' ], [ 25 , 50 , 75 , 90 ])
# 使用对数刻度创建直方图
plt.figure(figsize=( 12 , 6 ))
sns.histplot(degree_dist_df[ 'node_degree' ], bins= 50 , kde= False , color= 'blue' )
# 对 x 轴使用对数刻度
plt.yscale( 'log' )
# 添加标签和标题
plt.xlabel( 'Node Degree' )
plt.ylabel( '计数(对数尺度)' )
plt.title( '节点度分布' )
# 添加平均值、中位数和百分位数线
plt.axvline(mean_degree, color= 'red' , linestyle= 'dashed' , linewidth= 1 , label= f'平均值:{mean_degree: .2 f} ' )
plt.axvline(percentiles[ 0 ], color= 'purple' , linestyle= 'dashed' , linewidth= 1 , label= f'25th 百分位数:{percentiles[ 0 ]: .2 f} ' )
plt.axvline(percentiles[ 1 ], color= 'orange' , linestyle= 'dashed' , linewidth= 1 , label= f'50th 百分位数:{percentiles[ 1 ]: .2 f} ' )
plt.axvline(百分位数[ 2 ], color= '黄色' , linestyle= '虚线' , linewidth= 1 , label= f'75 百分位数:{百分位数[ 2 ]: .2 f} ' )
plt.axvline(百分位数[ 3 ], color= '棕色' , linestyle= '虚线' , linewidth= 1 , label= f'90 百分位数:{百分位数[ 3 ]: .2 f} ' )
# 添加图例
plt.legend()
# 显示图表
plt.show()

结果

大多数现实世界网络遵循幂律节点度分布,大多数节点的度相对较小,而一些重要节点的度则很大。虽然我们的图很小,但节点度遵循幂律。确定哪个实体有 120 个关系(与 43% 的实体相关)会很有趣。

db_query("""""" 
MATCH (n:__Entity__)
RETURN n.name AS name, count{(n)-[:RELATED]-()} AS degree
ORDER BY degree DESC LIMIT 5"""
)

结果

我们可以毫不犹豫地假设 Scrooge 是这本书的主角。我还大胆猜测Ebenezer ScroogeScrooge实际上是同一个实体,但由于 MSFT GraphRAG 缺少实体解析步骤,因此它们没有合并。

它还表明,分析和清理数据是减少噪音信息的重要步骤,因为古腾堡计划有 13 种关系,即使它们不是书中故事的一部分。

最后,我们将检查每个层级的社区规模分布。

community_data = db_query("""""" 
MATCH (n:__Community__)
RETURN n.level AS level, count{(n)-[:IN_COMMUNITY]-()} AS members
"""
)

stats = community_data.groupby( 'level' ).agg(
min_members=( 'members' , 'min' ),
max_members=( 'members' , 'max' ),
median_members=( 'members' , 'median' ),
avg_members=( 'members' , 'mean' ),
num_communities=( 'members' , 'count' ),
total_members=( 'members' , 'sum' )
).reset_index()

# 创建箱线图
plt.figure(figsize=( 10 , 6 ))
sns.boxplot(x= 'level' , y= 'members' , data=community_data, palette= 'viridis' )
plt.xlabel( 'Level' )
plt.ylabel( 'Members' )

# 添加统计注释
for i in range (stats.shape[ 0 ]):
level = stats[ 'level' ][i]
max_val = stats[ 'max_members' ][i]
text = ( f"num: {stats[ 'num_communities' ][i]} \n"
f"all_members: {stats[ 'total_members' ][i]} \n"
f"min: {stats[ 'min_members' ][i]} \n"
f"max: {stats[ 'max_members' ][i]} \n"
f"med: {stats[ 'median_members' ][i]} \n"
f"avg: {stats[ 'avg_members' ][i]: .2 f} " )
plt.text(level, 85 , text, HorizontalAlignment= 'center' , Fontsize= 9 )

plt.show()

结果

Leiden 算法确定了三个级别的社区,其中级别越高的社区平均规模越大。但是,有一些技术细节我不清楚,因为如果你检查 all_members 计数,你会看到每个级别都有不同的所有节点数量,即使它们在理论上应该是相同的。此外,如果社区在更高级别合并,为什么我们在 0 级有 19 个社区,在 1 级有 22 个社区?作者在这里做了一些优化和技巧,我还没有时间详细探讨。

实现检索器

在这篇博文的最后一部分,我们将讨论 MSFT GraphRAG 中指定的本地和全局检索器。检索器将与 LangChain 和 LlamaIndex 一起实现和集成。

本地猎犬

本地检索器首先使用向量搜索来识别相关节点,然后收集链接信息并将其注入 LLM 提示中。

虽然此图看起来很复杂,但可以轻松实现。我们首先使用基于实体描述文本嵌入的向量相似性搜索来识别相关实体。识别出相关实体后,我们可以遍历相关文本块、关系、社区摘要等。使用retrieval_queryLangChain 和 LlamaIndex 中的功能可以轻松实现使用向量相似性搜索然后遍历整个图的模式。

首先,我们需要配置向量索引。

index_name = “entity”“实体”

db_query(
“” “
创建向量索引” “” “

+ index_name
+ ” “” “如果不存在,则为(e:__Entity__)在e.description_embedding
选项{indexConfig:{
`vector.dimensions`:1536,
`vector.similarity_function`:'cosine'
}}
“”

我们还将计算并存储社区权重,它定义为社区中的实体出现的不同文本块的数量。

db_query( 
""""""
MATCH (n:`__Community__`)<-[:IN_COMMUNITY]-()<-[:HAS_ENTITY]-(c)
WITH n, count(distinct c) AS chunkCount
SET n.weight = chunkCount"""

)

每个部分的候选词(文本单元、社区报告等)数量是可配置的。虽然原始实现基于标记计数的过滤稍微复杂一些,但我们将在此简化它。我根据默认配置值开发了以下简化的顶级候选过滤器值。

topChunks = 33
topCommunities = 3
topOutsideRels = 10
topInsideRels = 10
topEntities = 10

我们将从 LangChain 的实现开始。我们唯一需要定义的是retrieval_query,这比较复杂。

lc_retrieval_query = """""" 
WITH collect(node) as nodes
// 实体 - 文本单元映射
WITH
collect {
UNWIND nodes as n
MATCH (n)<-[:HAS_ENTITY]->(c:__Chunk__)
WITH c, count(distinct n) as freq
RETURN c.text AS chunkText
ORDER BY freq DESC
LIMIT $topChunks
} AS text_mapping,
// 实体 - 报告映射
collect {
UNWIND nodes as n
MATCH (n)-[:IN_COMMUNITY]->(c:__Community__)
WITH c, c.rank as rank, c.weight AS weight
RETURN c.summary
ORDER BY rank, weight DESC
LIMIT $topCommunities
} AS report_mapping,
// 外部关系
collect {
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE NOT m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT $topOutsideRels
} as outsideRels,
// 内部关系
收集 {
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT $topInsideRels
} as insideRels,
// 实体描述
收集 {
UNWIND nodes as n
RETURN n.description AS descriptionText
} as entities
// 我们这里没有协变量或声明
RETURN {Chunks: text_mapping, Reports: report_mapping,
Relationships: outsideRels + insideRels,
Entities: entities} AS text, 1.0 AS score, {} AS metadata
"""


lc_vector = Neo4jVector.from_existing_index(
OpenAIEmbeddings(model= "text-embedding-3-small" ),
url=NEO4J_URI,
用户名=NEO4J_USERNAME,
密码=NEO4J_PASSWORD,
索引名称=index_name,
检索查询=lc_retrieval_query

此 Cypher 查询对一组节点执行多个分析操作,以提取和组织相关文本数据:

1.实体-文本单元映射:对于每个节点,查询会识别链接的文本块(“__Chunk__”),根据与每个块关联的不同节点的数量对其进行聚合,并按频率对其进行排序。排名靠前的块将作为“text_mapping”返回。

2.实体报告映射:对于每个节点,查询找到相关的社区(“__Community__”),并根据排名和权重返回排名靠前的社区的摘要。

3.外部关系:此部分提取关系(`RELATED`)的描述,其中相关实体(`m`)不是初始节点集的一部分。关系按等级排列,并限制为顶级外部关系。

4.内部关系:与外部关系类似,但这次它只考虑两个实体都在初始节点集内的关系。

5.实体描述:简单收集初始集合中每个节点的描述。

最后,查询将收集的数据组合成一个结构化结果,该结果由区块、报告、内部和外部关系以及实体描述以及默认分数和空元数据对象组成。您可以选择删除部分检索内容,以测试它们对结果的影响。

现在您可以使用以下代码运行检索器:

docs = lc_vector.similarity_search( 
"您对 Cratchitt 家族了解多少?",“您对克拉奇特家族了解多少?” ,
k=topEntities,
params={
"topChunks" : topChunks,
"topCommunities" : topCommunities,
"topOutsideRels" : topOutsideRels,
"topInsideRels" : topInsideRels,
},
)
# print(docs[0].page_content)

同样的检索模式可以用 LlamaIndex 来实现。对于 LlamaIndex,我们首先需要将元数据添加到节点,这样向量索引才能起作用。如果没有将默认元数据添加到相关节点,向量索引将返回错误

#https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/vector_stores/utils.py#L32
llama_index.core.schema导入TextNode
llama_index.core.vector_stores.utils导入node_to_metadata_dict

content = node_to_metadata_dict(TextNode(), remove_text= True, flat_metadata= False )

db_query(
"""
MATCH (e:__Entity__)
SET e += $content"""

{ "content" : content},
)

同样,我们可以使用retrieval_queryLlamaIndex 中的功能来定义检索器。与 LangChain 不同,我们将使用 f 字符串而不是查询参数来传递最佳候选过滤器参数。

retrieval_query = f"""f""" 
WITH collect(node) as nodes
// 实体 - 文本单元映射
WITH
nodes,
collect {{
UNWIND nodes as n
MATCH (n)<-[:HAS_ENTITY]->(c:__Chunk__)
WITH c, count(distinct n) as freq
RETURN c.text AS chunkText
ORDER BY freq DESC
LIMIT {topChunks}
}} AS text_mapping,
// 实体 - 报告映射
collect {{
UNWIND nodes as n
MATCH (n)-[:IN_COMMUNITY]->(c:__Community__)
WITH c, c.rank as rank, c.weight AS weight
RETURN c.summary
ORDER BY rank, weight DESC
LIMIT {topCommunities}
}} AS report_mapping,
// 外部关系
collect {{
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE NOT m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT {topOutsideRels}
}} as outsideRels,
// 内部关系
收集 {{
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT {topInsideRels}
}} as insideRels,
// 实体描述
收集 {{
UNWIND nodes as n
RETURN n.description AS descriptionText
}} as entities
// 我们这里没有协变量或声明
RETURN "Chunks:" + apoc.text.join(text_mapping, '|') + "\nReports: " + apoc.text.join(report_mapping,'|') +
"\nRelationships: " + apoc.text.join(outsideRels + insideRels, '|') +
"\nEntities: " + apoc.text.join(entities, "|") AS 文本, 1.0 AS 分数, nodes[0].id AS id, {{_node_type:nodes[0]._node_type, _node_content:nodes[0]._node_content}} AS 元数据
"""

此外,返回结果略有不同。我们需要将节点类型和内容作为元数据返回;否则,检索器将崩溃。现在我们只需实例化 Neo4j 向量存储并将其用作查询引擎。

neo4j_vector = Neo4jVectorStore(
NEO4J_USERNAME,
NEO4J_PASSWORD,
NEO4J_URI,
embed_dim,
index_name=index_name,
retrieval_query=retrieval_query,

loaded_index = VectorStoreIndex.from_vector_store(neo4j_vector)。as_query_engine(
similarity_top_k=topEntities,embed_model=OpenAIEmbedding(model="text-embedding-3-large")“文本嵌入-3-大”

我们现在可以测试GraphRAG本地检索器。

response = loaded_index.query("您对 Scrooge 了解多少?")“您对史高治了解多少?” ) 
print (response.response)
#print(response.source_nodes[0].text) # 史高治是一名深受Fezziwig 家族
慷慨和节日气氛影响的员工,尤其是 Fezziwig 先生和夫人。他参加了 Fezziwig 家族举办的令人难忘的家庭舞会,这场舞会极大地影响了他的生活,并为更广泛的善良和社区精神的叙述做出了贡献。



我立即想到的一件事是,我们可以通过使用混合方法(向量+关键字)来查找相关实体而不是仅仅进行向量搜索来改进本地检索。

全球检索器

全局检索器架构稍微简单一些。它似乎在指定的层次结构上迭代所有社区摘要,生成中间摘要,然后根据中间摘要生成最终响应。

我们必须提前确定要迭代的层次结构,这不是一个简单的决定,因为我们不知道哪一个会更好。层次结构越高,社区就越大,但社区数量就越少。这是我们在不手动检查摘要的情况下获得的唯一信息。

其他参数允许我们忽略低于排名或权重阈值的社区,我们不会在这里使用这些参数。我们将使用 LangChain 实现全局检索器,并使用与 GraphRAG 论文中相同的mapReduce 提示。由于系统提示很长,我们不会在这里包含它们或链构造。但是,所有代码都可以在笔记本中找到

def  global_retriever查询:str,级别:int,响应类型:str = response_type)-> str
community_data = graph.query(
“”“
MATCH(c:__Community__)
WHERE c.level = $level
RETURN c.full_content AS output
“”

params = { “level”:level},

intermediate_results = []
for community in tqdm(community_data,desc = “处理社区”):
intermediate_response = map_chain.invoke(
{ “question”:query,“context_data”:community [ “output” ]}

intermediate_results.append(intermediate_response)
final_response = reduce_chain.invoke(
{
“report_data”:intermediate_results,
“question”:query,
“response_type”:response_type,
}

返回最终回应

现在让我们测试一下。

print (global_retriever( "这个故事讲的是什么?" , 2 ))

结果

故事主要围绕埃比尼泽·斯克鲁奇展开,他是一个吝啬鬼,最初对生活持愤世嫉俗的态度,鄙视圣诞节。他的转变始于他已故商业伙伴雅各布·马利的鬼魂来访,随后三个幽灵出现——代表过去、现在和未来的圣诞节。这些遭遇促使斯克鲁奇反思自己的生活和行为的后果,最终使他拥抱圣诞精神并经历了重大的个人成长 [数据:报告(32、17、99、86 等)]。

### 雅各布·马利和幽灵的作用

雅各布·马利的鬼魂充当了超自然的催化剂,警告斯克鲁奇三个幽灵即将来访。每个幽灵都引导斯克鲁奇经历自我发现之旅,说明他的选择的影响以及同情心的重要性。幽灵向史高治揭示了他的行为不仅影响了他自己的生活,也影响了他人的生活,尤其突出了救赎和互联互通的主题 [数据:报告(86、17、99、+更多)]。

### 史高治的关系和转变史

高治与克拉奇特一家的关系,尤其是与鲍勃·克拉奇特和他的儿子小蒂姆的关系,对他的转变至关重要。通过幽灵呈现的景象,史高治产生了同理心,这激励他采取切实行动来改善克拉奇特一家的境况。叙述强调个人行为可以对社会产生深远的影响,因为史高治新发现的慷慨在他的社区中培养了同情心和社会责任感 [数据:报告(25、158、159、+更多)]。

### 救赎与希望的主题

总体而言,这个故事是希望的永恒象征,强调了同情、内省和个人改变的潜力等主题。史高治从一个孤独的守财奴变成一个仁慈人物的历程表明,改变永远不会太晚;小小的善举可以对个人和更广泛的社区产生重大的积极影响 [数据:报告(32、102、126、148、158、159 等)]。

总之,这个故事概括了圣诞节的变革力量和人际关系的重要性,使其成为一个关于救赎和一个人在节日期间对他人的影响的感人故事。

响应非常长且详尽,因为它适合全局检索器,该检索器会遍历指定级别的所有社区。您可以测试如果更改社区层次结构级别,响应会如何变化。



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询