图形模型显示了曲目及其与艺人的PERFORMED_BY关系。照片由作者拍摄。
我的工作很大一部分是提高用户在使用 Neo4j 时的体验。通常,将数据导入 Neo4j 并高效建模是用户面临的关键挑战,尤其是在刚开始使用的时候。虽然初始的数据模型很重要,也需要仔细考虑,但它可以很容易地进行重构,以提高性能,随着数据量或用户数量的增加。
所以,作为一次挑战自己,我想看看大型语言模型能否帮助构建初始数据模型。至少,它能展示事物之间的关联,并让用户能快速展示一些结果给他人。
直觉上,我知道数据建模是一个迭代过程,而且某些LLM面对大量数据时容易分心,所以这正好可以利用LangGraph来循环处理数据。
让我们来看看哪些提示让它得以实现。
图模型入门《GraphAcademy 的图数据建模基础课程》(https://graphacademy.neo4j.com/courses/modeling-fundamentals/?ref=adam) 带领您了解在图中建模数据的基础知识,简单来说,我使用了一些基本准则:
- 名词变成标签——它们描述节点所代表的 东西。
- 动词变成关系类型——它们描述 东西 之间的连接方式。
- 其他内容变成属性(特别是副词)——你有一个名字,而且可能开着灰色的车。
动词也可以是节点;你可能很高兴知道某个人订购了某个产品,但这种基本模型无法告诉你产品是在哪里和什么时候订购的。在这种情况下,订购就成了模型中的一个新节点。
我相信这可以提炼成一个提示,用来创建无需训练的图形数据建模方法。
迭代几个月之前,我短暂地尝试了这个,发现我用的那个模型在处理更复杂的架构时很容易分神,结果提示信息很快达到了最大令牌数限制。
这次我打算一次试一次地来,每次处理一个键值对。这样可以避免分心,因为每次LLM只需专注于处理一个键值对。
最终的步骤采用了如下步骤:
- 将 CSV 文件加载到 Pandas 数据框中。
- 分析 CSV 中的每一列,并将其信息追加到一个基于 JSON Schema 的数据模型中。
- 识别并为每个实体添加缺失的唯一标识符(ID)。
- 检查数据模型的准确性。
- 生成用于导入节点和关系的 Cypher 语句。
- 生成支撑导入语句的唯一性约束。
- 创建约束并执行导入。
我在Kaggle上快速浏览了一下,发现了一个有趣的数据集。其中最吸引我的是《Spotify最热门歌曲》Spotify Most Streamed Songs。
import pandas as pd
csv_file = '/Users/adam/projects/datamodeller/数据/spotify/spotify-most-streamed-songs.csv'
df = pd.read_csv(csv_file)
df.head()
# 代码用于读取并展示最热播放的歌曲数据
歌曲名称 艺术家名称 艺术家数量 发行年份 发行月份 发行日期 Spotify播放列表中 Spotify排行榜中 流媒体播放次数 Apple播放列表中 … 调式 模式 动感度% 情感度% 能量度% 清脆度% 乐器演奏度% 现场演出度% 人声度% 封面图片链接
0 Seven (feat. Latto) (Explicit版本) Latto, Jung Kook 2 2023 7 14 553 147 141381703 43 … B大调 80 89 83 31 0 8 4 未找到
1 LALA Myke Towers 1 2023 3 23 1474 48 133716286 48 … 升C大调 71 61 74 7 0 10 4 https://i.scdn.co/image/ab67616d0000b2730656d5…
2 吸血鬼 Olivia Rodrigo 1 2023 6 30 1397 113 140003974 94 … F大调 51 32 53 17 0 31 6 https://i.scdn.co/image/ab67616d0000b273e85259…
3 Cruel Summer Taylor Swift 1 2019 8 23 7858 100 800840817 116 … A大调 55 58 72 11 0 11 15 https://i.scdn.co/image/ab67616d0000b273e787cf…
4 WHERE SHE GOES Bad Bunny 1 2023 5 18 3133 50 303236322 84 … A小调 65 23 80 14 63 11 6 https://i.scdn.co/image/ab67616d0000b273ab5c9c…
5行25列
这比较简单,但一眼就能看出来,曲目和艺术家之间应该有关联。
还有一些数据清洗的难题需要解决,特别是在artist(s)_name
列中的艺术家名称以逗号分隔的情况下。
我真的很想用本地的LLM来搞定这个任务,但很快就发现Llama 3不太够用。如果有疑问,直接用OpenAI就好:
从 langchain_core.prompts 导入 PromptTemplate
从 langchain_core.pydantic_v1 导入 BaseModel, Field
从 typing 导入 List
从 langchain_core.output_parsers 导入 JsonOutputParser
from langchain_openai import ChatOpenAI
# 初始化一个ChatOpenAI模型,使用"gpt-4o"模型
llm = ChatOpenAI(model="gpt-4o")
让我们来创建一个数据模型吧
我用简化的建模指令来创建数据建模提示。我反复调整提示,以确保稳定结果。
零样本的例子表现得相对不错,但我发现输出有些不一致。定义一个结构化的输出来存放JSON数据真的很有用。
class JSONSchemaSpecification(BaseModel):
notes: str = Field(description="关于模式的任何备注或说明")
jsonschema: str = Field(description="描述数据模型中实体的JSON模式规范的JSON数组")
以下是少样本示例输出
JSON本身也不一致,所以我最终根据电影推荐数据集定义了一个模式。
例如的输出:
example_output = [
dict(
title="人物",
type="对象",
description="节点",
properties=[
dict(name="name", column_name="person_name", type="字符串", description="人物的名字", examples=["汤姆·汉克斯(Tom Hanks)"]),
dict(name="date_of_birth", column_name="person_dob", type="日期", description="出生日期", examples=["1987-06-05"]),
dict(name="id", column_name="person_name, date_of_birth", type="字符串", description="ID由名字和出生日期组成,确保唯一性", examples=["tom-hanks-1987-06-05"]),
],
),
dict(
title="导演",
type="对象",
description="节点",
properties=[
dict(name="name", column_name="director_names", type="字符串", description="导演的名字,列中的值由逗号分隔", examples=["弗朗西斯·福特·科波拉"]),
],
),
dict(
title="电影",
type="对象",
description="节点",
properties=[
dict(name="title", column_name="title", type="字符串", description="标题", examples=["玩具总动员"]),
dict(name="released", column_name="released", type="整型", description="发行年份", examples=["1990"]),
],
),
dict(
title="参与",
type="对象",
description="关系",
properties=[
dict(name="_from", column_name="od", type="字符串", description="通过ID找到的人物,ID由名字和出生日期组成,确保唯一性", examples=["人物"]),
dict(name="_to", column_name="title", type="字符串", description="标题", examples=["电影"]),
dict(name="roles", type="字符串", column_name="person_roles", description="角色", examples=["伍迪"]),
],
),
dict(
title="导演",
type="对象",
description="关系",
properties=[
dict(name="_from", type="字符串", column_name="director_names", description="导演的名字,列中的值由逗号分隔", examples=["导演"]),
dict(name="_to", type="字符串", column_name="title", description="关系的终点", examples=["电影"]),
],
),
]
我不得不偏离严格的JSON模式,并在输出中添加了 column_name
字段来辅助LLM生成导入脚本文件。提供描述的例子也有帮助,否则在 MATCH
子句中使用的属性会不一致。
下面就是最后的提示:
model_prompt = PromptTemplate.from_template("""
你是一位专业的图数据库专家。
基于现有数据源提供的信息,你的工作是设计数据模型。
你需要决定以下列在现有数据模型中的位置。考虑:
- 列是否表示一个实体,例如人、地方或电影?如果是,则应作为节点。
- 列是否表示两个实体之间的关系?如果是,则应作为两个节点之间的关系。
- 列是否表示实体或关系的属性?如果是,则应作为节点或关系的属性。
- 列是否表示一个可以用来查询相似节点的共享属性,例如类型?如果是,则应作为一个节点。 节点说明
- 节点标题应使用UpperCamelCase格式,例如Person、Place或Movie 关系说明
- 关系通常是动词,例如ACTED_IN、DIRECTED或PURCHASED
- 关系应使用UPPER_SNAKE_CASE格式(例如:ACTED_IN)
- 关系的良好示例是(:Person)-[:ACTED_IN]->(:Movie)或(:Person)-[:PURCHASED]->(:Product)
- 在描述中提供字段的任何特定说明。例如,字段是否包含逗号分隔的值列表或单个值? 属性说明
- 属性应使用lowerPascalCase格式
- 尽可能使用较短的名称(例如:将'person_id'和'personId'简化为'id')
- 如果你更改了属性名称,应在描述中提及原始字段名称
- 不要为整数或日期字段提供示例,请直接描述数据准备步骤。
- 总是需要说明数据准备步骤,例如是否需要将字段转换为字符串,或根据分隔符拆分为多个字段。
- 属性键应只包含字母,不应包含数字或特殊字符。
重要!
考虑提供的示例。是否需要进行数据准备以确保数据格式正确?
示例输出
你必须在描述中包括任何关于数据准备的信息。这是一个好的输出示例:
新数据:键:{key}{example_output}
数据类型:{type}
现有数据模型
示例值:{examples}这是现有数据模型:
保留现有数据模型{existing_model}
将你的更改应用于现有数据模型,但不要删除任何现有定义。
""
为了迭代地更新模型,我遍历了数据框中的键,并将每个键及其数据类型,以及前五个唯一的值传递给提示:
from json_repair 导入 dumps, loads
existing_model = {}
for i, key in enumerate(df):
print("\n", i, key)
print("----------------")
尝试:
res = try_chain(model_chain, dict(
existing_model=dumps(existing_model),
key=key,
type=df[key].dtype,
examples=dumps(df[key].unique()[:5].tolist())
))
print(res.notes)
existing_model = loads(res.jsonschema)
print([n['title'] for n in existing_model])
除了 Exception as e:
print(e)
pass
existing_model
控制台信息
0 track_name
----------------
添加 'track_name' 到现有数据模型中。这代表一个音乐曲目实体。
['Track']
1 artist(s)_name
----------------
在现有数据模型中添加一个新字段 'artist(s)_name'。此字段表示与曲目相关的多个艺术家,并应作为新节点 'Artist' 和从 'Track' 到 'Artist' 的关系 'PERFORMED_BY' 来建模。
['Track', 'Artist', 'PERFORMED_BY']
2 artist_count
----------------
将 artist_count 添加为 Track 节点的属性值。此属性值表示在曲目中表演的艺术家数量。
['Track', 'Artist', 'PERFORMED_BY']
3 released_year
----------------
在现有数据模型中作为 Track 节点的属性值添加 released_year 字段。
['Track', 'Artist', 'PERFORMED_BY']
4 released_month
----------------
在现有数据模型中添加 'released_month' 字段,将其视为 Track 节点的属性值。
['Track', 'Artist', 'PERFORMED_BY']
5 released_day
----------------
将新的属性值 'released_day' 添加到 Track 节点,以记录曲目发布的具体日期。
['Track', 'Artist', 'PERFORMED_BY']
6 in_spotify_playlists
----------------
在现有数据模型中作为 Track 节点的属性值添加新的字段 'in_spotify_playlists'。
['Track', 'Artist', 'PERFORMED_BY']
7 in_spotify_charts
----------------
在现有数据模型中作为 Track 节点的属性值添加 'in_spotify_charts' 字段。
['Track', 'Artist', 'PERFORMED_BY']
8 streams
----------------
在现有数据模型中添加新的字段 'streams',表示曲目的播放次数。
['Track', 'Artist', 'PERFORMED_BY']
9 in_apple_playlists
----------------
在现有数据模型中添加新的字段 'in_apple_playlists'。
['Track', 'Artist', 'PERFORMED_BY']
10 in_apple_charts
----------------
将 'in_apple_charts' 作为 Track 节点的属性值添加到现有数据模型中,表示曲目出现在 Apple 排行榜中的次数。
['Track', 'Artist', 'PERFORMED_BY']
11 in_deezer_playlists
----------------
将 'in_deezer_playlists' 添加到现有的音乐曲目数据模型中。
['Track', 'Artist', 'PERFORMED_BY']
12 in_deezer_charts
----------------
在现有 'Track' 节点中添加新的属性值 'inDeezerCharts',表示曲目出现在 Deezer 排行榜中的次数。
['Track', 'Artist', 'PERFORMED_BY']
13 in_shazam_charts
----------------
将新的属性值 'in_shazam_charts' 添加到现有数据模型中。这似乎是 Track 节点的一个属性值,表示曲目出现在 Shazam 排行榜中的次数。
['Track', 'Artist', 'PERFORMED_BY']
14 bpm
----------------
将 bpm 字段作为 Track 节点的属性值添加,因为它代表了曲目的特点。
['Track', 'Artist', 'PERFORMED_BY']
15 key
----------------
在现有数据模型中添加 'key' 字段。'key' 表示曲目的音乐调式,这是一个可以通过查询共享属性来查找类似曲目的有趣特性。
['Track', 'Artist', 'PERFORMED_BY']
16 mode
----------------
在现有数据模型中添加 'mode' 属性值。它代表曲目的音乐特性,最好作为 Track 节点的属性值捕获。
['Track', 'Artist', 'PERFORMED_BY']
17 danceability_%
----------------
将 'danceability_%' 添加为现有数据模型中 Track 节点的属性值。该字段表示曲目的舞蹈性百分比值。
['Track', 'Artist', 'PERFORMED_BY']
18 valence_%
----------------
将 valence 百分比值字段添加到现有数据模型中作为 Track 节点的属性值。
['Track', 'Artist', 'PERFORMED_BY']
19 energy_%
----------------
将新的字段 'energy_%' 集成到现有数据模型中。此字段代表 Track 实体的属性值,并应作为 Track 节点的属性值添加。
['Track', 'Artist', 'PERFORMED_BY']
20 acousticness_%
----------------
将 acousticness_% 添加为现有数据模型中 Track 节点的字段。
['Track', 'Artist', 'PERFORMED_BY']
21 instrumentalness_%
----------------
将新的字段 'instrumentalness_%' 添加到现有的 Track 节点中,因为它代表了 Track 实体的属性值。
['Track', 'Artist', 'PERFORMED_BY']
22 liveness_%
----------------
将新的字段 'liveness_%' 添加到现有数据模型中,作为 Track 节点的属性值。
['Track', 'Artist', 'PERFORMED_BY']
23 speechiness_%
----------------
将新的字段 'speechiness_%' 添加到现有数据模型中,作为 'Track' 节点的属性值。
['Track', 'Artist', 'PERFORMED_BY']
24 cover_url
----------------
将新的属性值 'cover_url' 添加到现有的 'Track' 节点中。此属性值表示曲目的封面图 URL。
['Track', 'Artist', 'PERFORMED_BY']
经过对提示做了一些调整来适应不同情况后,我最终得到了一个让我很满意的结果。大模型成功地确定了数据集包括Track
、Artist
,以及连接它们的PERFORMED_BY
关系。
[
{
"title": "Track",
"type": "object",
"description": "节点(Node)",
"properties": [
{
"name": "name",
"column_name": "track_name",
"type": "string",
"description": "曲名",
"examples": [
"Seven (feat. Latto) (Explicit Ver.)",
"LALA",
"vampire",
"Cruel Summer",
"WHERE SHE GOES",
],
},
{
"name": "artist_count",
"column_name": "artist_count",
"type": "integer",
"description": "参与曲目的艺术家人数",
"examples": [2, 1, 3, 8, 4],
},
{
"name": "released_year",
"column_name": "released_year",
"type": "integer",
"description": "曲目发布的年份",
"examples": [2023, 2019, 2022, 2013, 2014],
},
{
"name": "released_month",
"column_name": "released_month",
"type": "integer",
"description": "曲目发布的月份",
"examples": [7, 3, 6, 8, 5],
},
{
"name": "released_day",
"column_name": "released_day",
"type": "integer",
"description": "曲目发布的具体日期",
"examples": [14, 23, 30, 18, 1],
},
{
"name": "inSpotifyPlaylists",
"column_name": "in_spotify_playlists",
"type": "integer",
"description": "曲目在Spotify播放列表中的数量。将值转换为整数。",
"examples": [553, 1474, 1397, 7858, 3133],
},
{
"name": "inSpotifyCharts",
"column_name": "in_spotify_charts",
"type": "integer",
"description": "曲目在Spotify排行榜中的排名次数。将值转换为整数。",
"examples": [147, 48, 113, 100, 50],
},
{
"name": "streams",
"column_name": "streams",
"type": "array",
"description": "曲目的流媒体ID列表。保持数组格式。",
"examples": [
"141381703",
"133716286",
"140003974",
"800840817",
"303236322",
],
},
{
"name": "inApplePlaylists",
"column_name": "in_apple_playlists",
"type": "integer",
"description": "曲目在Apple播放列表中的数量。将值转换为整数。",
"examples": [43, 48, 94, 116, 84],
},
{
"name": "inAppleCharts",
"column_name": "in_apple_charts",
"type": "integer",
"description": "曲目在Apple排行榜中的排名次数。将值转换为整数。",
"examples": [263, 126, 207, 133, 213],
},
{
"name": "inDeezerPlaylists",
"column_name": "in_deezer_playlists",
"type": "array",
"description": "曲目在Deezer播放列表中的ID列表。保持数组格式。",
"examples": ["45", "58", "91", "125", "87"],
},
{
"name": "inDeezerCharts",
"column_name": "in_deezer_charts",
"type": "integer",
"description": "曲目在Deezer排行榜中的排名次数。将值转换为整数。",
"examples": [10, 14, 12, 15, 17],
},
{
"name": "inShazamCharts",
"column_name": "in_shazam_charts",
"type": "array",
"description": "曲目在Shazam排行榜中的ID列表。保持数组格式。",
"examples": ["826", "382", "949", "548", "425"],
},
{
"name": "bpm",
"column_name": "bpm",
"type": "integer",
"description": "曲目的每分钟节拍数。将值转换为整数。",
"examples": [125, 92, 138, 170, 144],
},
{
"name": "key",
"column_name": "key",
"type": "string",
"description": "曲目的音乐调性。将值转换为字符串。",
"examples": ["B", "C#", "F", "A", "D"],
},
{
"name": "mode",
"column_name": "mode",
"type": "string",
"description": "曲目的调式(如Major,Minor)。将值转换为字符串。",
"examples": ["Major", "Minor"],
},
{
"name": "danceability",
"column_name": "danceability_%",
"type": "integer",
"description": "曲目的可舞性百分比。将值转换为整数。",
"examples": [80, 71, 51, 55, 65],
},
{
"name": "valence",
"column_name": "valence_%",
"type": "integer",
"description": "曲目的情感强度百分比。将值转换为整数。",
"examples": [89, 61, 32, 58, 23],
},
{
"name": "energy",
"column_name": "energy_%",
"type": "integer",
"description": "曲目的能量百分比。将值转换为整数。",
"examples": [83, 74, 53, 72, 80],
},
{
"name": "acousticness",
"column_name": "acousticness_%",
"type": "integer",
"description": "曲目的原声度百分比。将值转换为整数。",
"examples": [31, 7, 17, 11, 14],
},
{
"name": "instrumentalness",
"column_name": "instrumentalness_%",
"type": "integer",
"description": "曲目的伴奏性百分比。将值转换为整数。",
"examples": [0, 63, 17, 2, 19],
},
{
"name": "liveness",
"column_name": "liveness_%",
"type": "integer",
"description": "曲目的现场感百分比。将值转换为整数。",
"examples": [8, 10, 31, 11, 28],
},
{
"name": "speechiness",
"column_name": "speechiness_%",
"type": "integer",
"description": "曲目的说唱性百分比。将值转换为整数。",
"examples": [4, 6, 15, 24, 3],
},
{
"name": "coverUrl",
"column_name": "cover_url",
"type": "string",
"description": "曲目的封面图片链接。如果值为'Not Found',应将其转换为空字符串。",
"examples": [
"https://i.scdn.co/image/ab67616d0000b2730656d5ce813ca3cc4b677e05",
"https://i.scdn.co/image/ab67616d0000b273e85259a1cae29a8d91f2093d",
],
},
],
},
{
"title": "Artist",
"type": "object",
"description": "节点(Node)",
"properties": [
{
"name": "name",
"column_name": "artist(s)_name",
"type": "string",
"description": "艺术家的名称。将列中的值按逗号分割。",
"examples": [
"Latto",
"Jung Kook",
"Myke Towers",
"Olivia Rodrigo",
"Taylor Swift",
"Bad Bunny",
],
}
],
},
{
"title": "PERFORMED_BY",
"type": "object",
"description": "关系(Relationship)",
"properties": [
{
"name": "_from",
"type": "string",
"description": "此关系开始于的节点标签",
"examples": ["Track"],
},
{
"name": "_to",
"type": "string",
"description": "此关系结束于的节点标签",
"examples": ["Artist"],
},
],
},
]
[
{
"title": "曲目",
"type": "object",
"description": "音轨节点",
"properties": [
{
"name": "name",
"column_name": "track_name",
"type": "string",
"description": "曲目名称",
"examples": [
"Seven (feat. Latto) (Explicit Ver.)",
"LALA",
"vampire",
"Cruel Summer",
"WHERE SHE GOES",
],
},
{
"name": "artist_count",
"column_name": "artist_count",
"type": "integer",
"description": "参与艺术家人数",
"examples": [2, 1, 3, 8, 4],
},
{
"name": "released_year",
"column_name": "released_year",
"type": "integer",
"description": "发行年",
"examples": [2023, 2019, 2022, 2013, 2014],
},
{
"name": "released_month",
"column_name": "released_month",
"type": "integer",
"description": "发行月",
"examples": [7, 3, 6, 8, 5],
},
{
"name": "released_day",
"column_name": "released_day",
"type": "integer",
"description": "发行日",
"examples": [14, 23, 30, 18, 1],
},
{
"name": "inSpotifyPlaylists",
"column_name": "in_spotify_playlists",
"type": "integer",
"description": "Spotify播放列表中的曲目数",
"examples": [553, 1474, 1397, 7858, 3133],
},
{
"name": "inSpotifyCharts",
"column_name": "in_spotify_charts",
"type": "integer",
"description": "Spotify排行榜中的排名次数",
"examples": [147, 48, 113, 100, 50],
},
{
"name": "streams",
"column_name": "streams",
"type": "array",
"description": "流媒体ID列表",
"examples": [
"141381703",
"133716286",
"140003974",
"800840817",
"303236322",
],
},
{
"name": "inApplePlaylists",
"column_name": "in_apple_playlists",
"type": "integer",
"description": "Apple播放列表中的曲目数",
"examples": [43, 48, 94, 116, 84],
},
{
"name": "inAppleCharts",
"column_name": "in_apple_charts",
"type": "integer",
"description": "Apple排行榜中的排名次数",
"examples": [263, 126, 207, 133, 213],
},
{
"name": "inDeezerPlaylists",
"column_name": "in_deezer_playlists",
"type": "array",
"description": "Deezer播放列表中的ID列表",
"examples": ["45", "58", "91", "125", "87"],
},
{
"name": "inDeezerCharts",
"column_name": "in_deezer_charts",
"type": "integer",
"description": "Deezer排行榜中的排名次数",
"examples": [10, 14, 12, 15, 17],
},
{
"name": "inShazamCharts",
"column_name": "in_shazam_charts",
"type": "array",
"description": "Shazam排行榜中的ID列表",
"examples": ["826", "382", "949", "548", "425"],
},
{
"name": "bpm",
"column_name": "bpm",
"type": "integer",
"description": "节拍数",
"examples": [125, 92, 138, 170, 144],
},
{
"name": "key",
"column_name": "key",
"type": "string",
"description": "调式",
"examples": ["B", "C#", "F", "A", "D"],
},
{
"name": "mode",
"column_name": "mode",
"type": "string",
"description": "调性(例如大调、小调)",
"examples": ["Major", "Minor"],
},
{
"name": "danceability",
"column_name": "danceability_%",
"type": "integer",
"description": "舞曲性",
"examples": [80, 71, 51, 55, 65],
},
{
"name": "valence",
"column_name": "valence_%",
"type": "integer",
"description": "情感强度",
"examples": [89, 61, 32, 58, 23],
},
{
"name": "energy",
"column_name": "energy_%",
"type": "integer",
"description": "能量值",
"examples": [83, 74, 53, 72, 80],
},
{
"name": "acousticness",
"column_name": "acousticness_%",
"type": "integer",
"description": "原声性百分比",
"examples": [31, 7, 17, 11, 14],
},
{
"name": "instrumentalness",
"column_name": "instrumentalness_%",
"type": "integer",
"description": "器乐性百分比",
"examples": [0, 63, 17, 2, 19],
},
{
"name": "liveness",
"column_name": "liveness_%",
"type": "integer",
"description": "现场感百分比",
"examples": [8, 10, 31, 11, 28],
},
{
"name": "speechiness",
"column_name": "speechiness_%",
"type": "integer",
"description": "语言性",
"examples": [4, 6, 15, 24, 3],
},
{
"name": "coverUrl",
"column_name": "cover_url",
"type": "string",
"description": "封面图链接",
"examples": [
"https://i.scdn.co/image/ab67616d0000b2730656d5ce813ca3cc4b677e05",
"https://i.scdn.co/image/ab67616d0000b273e85259a1cae29a8d91f2093d",
],
},
],
},
{
"title": "Artist",
"type": "object",
"description": "艺术家节点",
"properties": [
{
"name": "name",
"column_name": "artist(s)_name",
"type": "string",
"description": "艺术家名称。将列中的值按逗号分割",
"examples": [
"Latto",
"Jung Kook",
"Myke Towers",
"Olivia Rodrigo",
"Taylor Swift",
"Bad Bunny",
],
}
],
},
{
"title": "PERFORMED_BY",
"type": "object",
"description": "关系",
"properties": [
{
"name": "_from",
"type": "string",
"description": "关系开始的节点标签",
"examples": ["Track"],
},
{
"name": "_to",
"type": "string",
"description": "关系结束的节点标签",
"examples": ["Artist"],
},
],
},
]
添加独特的标识符
我注意到这个架构中没有包含任何唯一标识符,这在导入关系时可能引起问题。理所当然,不同的艺术家可能会发行同名歌曲,例如这个例子所示,而且两名艺术家可能同名。
这样一来,为了在数据集中区分Tracks,创建一个标识符变得非常重要。
# 添加主键/唯一标识符
uid_prompt = PromptTemplate.from_template("""
你是一位图数据库专家,正在检查一位同事生成的数据模型中的一个单一实体。
你需要确保所有导入数据库的节点都是独一无二的,确保这些节点没有重复。
## 示例A模式包含具有多个属性的演员,包括姓名和出生日期。如果有两个演员名字相同,则添加一个新的组合属性,将姓名和出生日期结合。
如果组合值,请包括将值转换成符合slug格式的指令。将新属性命名为'id'(通常指唯一标识符)。如已确定新属性,请将其添加到属性列表中,保持其他属性不变。
描述中应包括需要连接的字段。
## 示例输出如下
```
{example_output}
```## 当前实体模式如下
```
{entity}
```
uid_chain = uid_prompt | llm.with_structured_output(JSONSchemaSpecification)
这一步主要针对节点,因此我从模式中提取了节点,为每个节点运行了链路,然后将这些关系与更新定义进行了整合。
# 提取节点和关系类型
nodes = [n for n in existing_model if "node" in n["description"].lower()]
rels = [n for n in existing_model if "node" not in n["description"].lower()]
# 注意:这里使用了lower()函数将描述转换为小写,以确保在判断节点和关系类型时的一致性。
生成节点的唯一ID:
with_uids = []
for entity in nodes:
res = uid_chain.invoke(dict(entity=dumps(entity)))
json = loads(res.jsonschema)
with_uids = with_uids + json if type(json) == list else with_uids + [json]
with_uids = with_uids + rels
rels: 关系列表 调用生成唯一ID的链式方法
# 数据模型检查
为了保持理智,检查模型是否已经优化过是值得的。`模型提示` 在识别名词和动词方面表现得很出色,但在更复杂的模型中。
在一次迭代中,`*_playlists` 和 `_charts` 列被视为ID,并尝试创建 `Stream` 节点及 `IN_PLAYLIST` 关系。这可能是因为超过1,000的计数使用了带有逗号的格式(如 `1,001`)。
不错的想法,但也许有点太复杂了。这说明理解数据结构的人类角色很重要。
# 添加主键/唯一标识符
review_prompt = PromptTemplate.from_template("""
你是一名图数据库专家,正在审查同事生成的数据模型。
你的任务是审查数据模型,确保它适合其目的。
检查以下内容:
## 检查嵌套的实体
请记住,Neo4j无法存储对象数组或嵌套对象。
这些必须转换成单独的节点,并通过关系进行连接。
你必须在输出模式中包括新节点和关系的引用。
## 检查实体中的属性
如果有一个表示ID数组的属性,则应为该实体创建一个新节点。
你必须在输出模式中包括新节点和关系的引用。
# 保留说明
确保节点、关系和属性的说明清晰简洁。
你可以在不影响细节的情况下改进它们。
## 当前实体模式
{entity}
""")
review_chain = review_prompt | llm.with_structured_output(JSONSchemaSpecification)
review_nodes = [n for n in with_uids if "node" in n["description"].lower() ]
review_rels = [n for n in with_uids if "node" not in n["description"].lower() ]
reviewed = []
for entity in review_nodes:
res = review_chain.invoke(dict(entity=dumps(entity)))
json = loads(res.jsonschema)
reviewed = reviewed + json
# 将关系重新加入
reviewed = reviewed + review_rels
len(reviewed) # 输出reviewed的长度
reviewed = with_uids # 将uid信息重新赋值给reviewed
在实际情况中,我会多跑几次以逐步优化数据模型。我会设定一个上限,然后迭代到数据模型不再变化为止。
导入需要的语句
到这时,架构应该足够健壮和完整,并包含尽可能多的信息,让大型语言模型能够生成一整套导入脚本。
根据[Neo4j 数据导入建议](https://graphacademy.neo4j.com/courses/importing-fundamentals/?ref=adam),文件需要分多次处理,每次只导入一个数据库节点或关系,以避免贪婪操作和锁定。
import_prompt = PromptTemplate.from_template("""
根据数据模型,编写一个Cypher语句,将以下数据从CSV文件导入Neo4j中。
请勿使用LOAD CSV,因为这些数据将使用Neo4j Python Driver导入,而应使用$rows参数的UNWIND。
你正在编写一个多步骤的导入过程,因此请专注于提到的实体。
在导入数据时,您必须遵循以下指南:
-
根据描述中的说明识别主键。
-
在识别属性时,根据描述中的说明确定属性格式。
-
在将字段合并为ID时,使用apoc.text.slug函数将任何文本转换为缩略格式,并使用toLower将字符串转换为小写 - apoc.text.slug(toLower(row.
name
)) -
如果拆分属性,则将其转换为字符串,并使用trim函数删除任何空格 - trim(toString(row.
name
)) -
在将属性组合在一起时,将每个属性包装在coalesce函数中,以确保当其中一个值未设置时属性不为null - coalesce(row.
id
, '') + '--'+ coalesce(row.title
) -
使用
column_name
字段将CSV列映射到数据模型中的属性。 -
将CSV中的所有列名用反引号括起来 - 例如 row.
column_name
。 -
在合并节点时,仅根据唯一标识符进行合并。将所有其他属性使用
SET
进行设置。 - 请不要使用apoc.periodic.iterate,文件将在应用程序中批量处理。
数据模型:{data_model}
当前实体信息:
{entity}
""")
这条链需要与之前的步骤有不同的输出对象。在这个情况下,cypher
成员显得尤为重要,但我还想加入一个 chain_of_thought
关键字以鼓励链式思维。
class CypherOutputSpecification(BaseModel):
chain_of_thought: str = Field(description="用于生成Cypher语句的任何推理过程")
cypher: str = Field(description="用于导入数据的Cypher查询语句")
notes: Optional[str] = Field(description="关于Cypher语句的任何注释或其他说明")
import_chain = import_prompt | llm.with_structured_output(CypherOutputSpecification) # 导入链是从导入提示开始,然后通过带有Cypher结构化输出规范的llm
同样的步骤也适用于遍历每个已审核定义,以此生成Cypher。
import_cypher = []
for n in reviewed:
print('\n\n------', n['title']) # 打印当前项的标题
res = import_chain.invoke(dict(
data_model=dumps(reviewed),
entity=n
))
import_cypher.append(res.cypher) # 将查询语句添加到列表中
print(res.cypher) # 输出查询语句,便于调试
控制台输出结果:
------ 轨道
UNWIND $rows AS row
MERGE (t:Track {id: apoc.text.slug(toLower(coalesce(row.`track_name`, '') + '-' + coalesce(row.`released_year`, '')))}) // COALESCE(row.`track_name`, '') // 如果 `track_name` 为空则使用空字符串替代
SET t.name = trim(toString(row.`track_name`)),
t.artist_count = toInteger(row.`artist_count`),
t.released_year = toInteger(row.`released_year`),
t.released_month = toInteger(row.`released_month`),
t.released_day = toInteger(row.`released_day`),
t.inSpotifyPlaylists = toInteger(row.`in_spotify_playlists`),
t.inSpotifyCharts = toInteger(row.`in_spotify_charts`),
t.streams = row.`streams`,
t.inApplePlaylists = toInteger(row.`in_apple_playlists`),
t.inAppleCharts = toInteger(row.`in_apple_charts`),
t.inDeezerPlaylists = row.`in_deezer_playlists`,
t.inDeezerCharts = toInteger(row.`in_deezer_charts`),
t.inShazamCharts = row.`in_shazam_charts`,
t.bpm = toInteger(row.`bpm`),
t.key = trim(toString(row.`key`)),
t.mode = trim(toString(row.`mode`)),
t.danceability = toInteger(row.`danceability_%`),
t.valence = toInteger(row.`valence_%`),
t.energy = toInteger(row.`energy_%`),
t.acousticness = toInteger(row.`acousticness_%`),
t.instrumentalness = toInteger(row.`instrumentalness_%`),
t.liveness = toInteger(row.`liveness_%`),
t.speechiness = toInteger(row.`speechiness_%`),
t.coverUrl = CASE row.`cover_url` WHEN 'Not Found' THEN '' ELSE trim(toString(row.`cover_url`)) END // 封面URL 如果 `cover_url` 为 'Not Found' 则使用空字符串替代
------ 艺术家
UNWIND $rows AS row
WITH row, split(row.`artist(s)_name`, ',') AS artistNames
UNWIND artistNames AS artistName
MERGE (a:Artist {id: apoc.text.slug(toLower(trim(artistName)))})
SET a.name = trim(artistName)
------ 演唱
UNWIND $rows AS row
UNWIND split(row.`artist(s)_name`, ',') AS artist_name
MERGE (t:Track {id: apoc.text.slug(toLower(row.`track_name`)) + '-' + trim(toString(row.`released_year`))})
MERGE (a:Artist {id: apoc.text.slug(toLower(trim(artist_name)))})
MERGE (t)-[:PERFORMED_BY]->(a) // 表示歌曲由该艺术家表演
这个提示的制作需要一些技术处理以实现一致的效果:
- 有时 Cypher 中的
MERGE
语句会定义多个字段,这在最佳情况下也显得不够理想。如果任何列为空,整个导入将失败。 - 有时结果会包括
[apoc.period.iterate](https://neo4j.com/docs/apoc/current/overview/apoc.periodic/apoc.periodic.iterate/)
,这个功能现在不再需要了,我想要一个我可以使用 Python 驱动程序执行的代码。 - 我反复强调在创建关系时应该使用指定的列名。
- 在用关系两端节点的唯一标识符时,LLM 总是不听指令,我得反复尝试才能让它按照描述中的指示去做。在这个提示和
model_prompt
之间有不少来回。 - 如果列名包含特殊字符(例如
energy_%
),就需要用反引号括起来。
将这个任务分成两个提示也会很有帮助——一个是关于节点,另一个是关于关系的。但这留待以后再说吧。
创建唯一性约束接下来,可以以导入的脚本为基础,在数据库中创建唯一性约束。
constraint_prompt = PromptTemplate.from_template("""
你是一位专业的图数据库管理员。
根据以下Cypher语句,编写一个Cypher语句来为MERGE语句中使用的任何属性创建唯一约束。
""")
唯一约束的正确语法如下:
CREATE CONSTRAINT movie_title_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.title IS UNIQUE;Cypher:
{cypher}
(示例Cypher代码如下)
""")constraint_chain = constraint_prompt | llm.with_structured_output(CypherOutputSpecification) # Cypher输出规范
constraint_queries = []
for statement in import_cypher:
res = constraint_chain.invoke(dict(cypher=statement))
statements = res.cypher.split(";")
for cypher in statements:
constraint_queries.append(cypher)
控制台输出信息:
如果不存在,则创建约束 track_id_unique,应用于 (t:Track),要求 t.id 唯一
如果不存在,则创建约束 stream_id,应用于 (s:Stream),要求 s.id 唯一
如果不存在,则创建约束 playlist_id,应用于 (p:Playlist),要求 p.id 唯一
如果不存在,则创建约束 chart_id,应用于 (c:Chart),要求 c.id 唯一
如果不存在,则创建约束 track_id_unique,应用于 (t:Track),要求 t.id 唯一
如果不存在,则创建约束 stream_id_unique,应用于 (s:Stream),要求 s.id 唯一
如果不存在,则创建约束 track_id_unique,应用于 (t:Track),要求 t.id 唯一
如果不存在,则创建约束 playlist_id_unique,应用于 (p:Playlist),要求 p.id 唯一
如果不存在,则创建约束 track_id_unique,应用于 (track:Track),要求 track.id 唯一
如果不存在,则创建约束 chart_id_unique,应用于 (chart:Chart),要求 chart.id 唯一
有时候这个提示会返回关于索引和约束的声明,因此会在分号处进行分隔处理。
运行导入操作一切都准备好了,该执行这些Cypher语句了:
从 os 模块导入 getenv
从 neo4j 模块导入 GraphDatabase
driver = GraphDatabase.driver(
getenv("NEO4J_URI"),
auth=(
getenv("NEO4J_USERNAME"),
getenv("NEO4J_PASSWORD")
)
)with driver.session() as session: # 使用driver创建一个session
# 清空数据库:
session.run("MATCH (n) DETACH DELETE n") # 创建约束条件
for q in constraint_queries:
if q.strip() != "":
session.run(q) # 导入数据记录
for q in import_cypher:
if q.strip() != "":
res = session.run(q, rows=rows).consume()
print(q) # 打印查询语句
print(res.counters) # 打印结果计数器
数据集QA
这篇帖子若没有用 GraphCypherQAChain
做一些 QA 就不完整了。
从langchain.chains导入GraphCypherQAChain模块
从langchain_community.graphs导入Neo4jGraph模块
graph = Neo4jGraph(
url=getenv("NEO4J_URI"),
username=getenv("NEO4J_USERNAME"),
password=getenv("NEO4J_PASSWORD"),
enhanced_schema=True
)qa = GraphCypherQAChain.from_llm(
llm,
graph=graph,
allow_dangerous_requests=True,
verbose=True
)
最受欢迎的艺人
数据库里哪些艺术家最受欢迎?
qa.invoke({"query": "哪些艺术家最受欢迎?"})
> 进入新的GraphCypherQAChain链...
生成的Cypher语句:
MATCH (:Track)-[:PERFORMED_BY]->(a:Artist)
返回 a.name, COUNT(*) AS popularity
按 popularity 降序排列
限制为前10条
完整信息:
[{'a.name': 'Bad Bunny', 'popularity': 40}, {'a.name': 'Taylor Swift', 'popularity': 38}, {'a.name': 'The Weeknd', 'popularity': 36}, {'a.name': 'SZA', 'popularity': 23}, {'a.name': 'Kendrick Lamar', 'popularity': 23}, {'a.name': 'Feid', 'popularity': 21}, {'a.name': 'Drake', 'popularity': 19}, {'a.name': 'Harry Styles', 'popularity': 17}, {'a.name': 'Peso Pluma', 'popularity': 16}, {'a.name': '21 Savage', 'popularity': 14}]> 完成的链。
{
"query": "哪些问题是关于谁是最受欢迎的艺术家?",
"result": "Bad Bunny、Taylor Swift 和韦史密斯是目前最火的歌手。"
}
似乎,LLM是以一个艺术家参与的歌曲数量来判断其受欢迎程度,而不是他们的总播放量。
每分钟拍数哪首歌BPM最高?
qa.invoke({"请求": "哪首曲目的节奏最快?"})
> 正在进入新的GraphCypherQAChain链...
生成的Cypher代码:
MATCH (t:Track)
RETURN t
按 t.bpm DESC 排序
限制为 1
完整上下文:,
[{'t': {'id': 'seven-feat-latto-explicit-ver--2023'}}]> 链已完成。
{
"query": "哪首曲子的BPM最高?",
"result": "这我不知道哦。"
}
优化Cypher生成提示
在这种情况下,Cypher 看起来是正确的,并且正确的结果包含在提示中,但 gpt-4o
(注:gpt-4o
)无法解释答案。看来传递到 GraphCypherQAChain
的 CYPHER_GENERATION_PROMPT
可能需要添加更多指令来使列名更加清晰。
在Cypher语句中始终使用详细的列名,使用标签和属性的名称。例如,用‘person_name’代替‘name’。
GraphCypherQAChain,使用了自定义提示哦。
YPHER_GENERATION_TEMPLATE = """任务:生成Cypher查询语句以查询图数据库。
指令:
仅使用提供的关系类型和模式中的属性。
不使用除提供的模式之外的任何关系类型或属性。
模式:
{schema}
注意:不要在您的响应中包含任何解释或道歉。
不要回答任何除构造Cypher语句外的其他问题。
仅包含生成的Cypher语句。
在Cypher语句中始终使用完整的列名,例如使用'person_name'而不是'name'。
结果中包含节点周围直接网络的数据以提供额外的上下文。例如,包括电影的发行年份、演员列表及其角色,或电影的导演。
当按属性排序时,请添加`IS NOT NULL`检查以确保仅返回具有该属性的节点。
示例:以下是根据特定问题生成的Cypher语句示例:
# 有多少人在《Top Gun》中出演?
MATCH (m:Movie {name:"Top Gun"})
RETURN COUNT { (m)<-[:ACTED_IN]-() } AS numberOfActors
问题:
{question}"""
CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"], template=CYPHER_GENERATION_TEMPLATE
)
qa = GraphCypherQAChain.from_llm(
llm,
graph=graph,
allow_dangerous_requests=True,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)
最多艺术家唱过的歌曲
图表擅长按类型和方向统计关系的数量。
qa.invoke({"query": "哪些曲目由最多艺术家演唱/演奏?"})
> 进入新的GraphCypherQAChain链...
生成的Cypher代码:
cypher
执行匹配:MATCH (t:Track)
WITH t, COUNT { (t)-[:PERFORMED_BY]->(:Artist) } as artist_count
其中artist_count不为空
RETURN t.id AS track_id, t.name AS track_name, artist_count
按artist_count降序排列
完整上下文如下:
[{'track_id': 'los-del-espacio-2023', 'track_name': 'Los del Espacio', 'artist_count': 8}, {'track_id': 'se-le-ve-2021', 'track_name': 'Se Le Ve', 'artist_count': 8}, {'track_id': 'we-dont-talk-about-bruno-2021', 'track_name': "We Don't Talk About Bruno", 'artist_count': 7}, {'track_id': 'cayï-ï-la-noche-feat-cruz-cafunï-ï-abhir-hathi-bejo-el-ima--2022', 'track_name': 无, 'artist_count': 6}, {'track_id': 'jhoome-jo-pathaan-2022', 'track_name': 'Jhoome Jo Pathaan', 'artist_count': 6}, {'track_id': 'besharam-rang-from-pathaan--2022', 'track_name': 无, 'artist_count': 6}, {'track_id': 'nobody-like-u-from-turning-red--2022', 'track_name': 无, 'artist_count': 6}, {'track_id': 'ultra-solo-remix-2022', 'track_name': 'ULTRA SOLO REMIX', 'artist_count': 5}, {'track_id': 'angel-pt-1-feat-jimin-of-bts-jvke-muni-long--2023', 'track_name': 无, 'artist_count': 5}, {'track_id': 'link-up-metro-boomin-dont-toliver-wizkid-feat-beam-toian-spider-verse-remix-spider-man-across-the-spider-verse--2023', 'track_name': 无, 'artist_count': 5}]> 完成。
{
"query": "哪首歌曲由最多的艺术家演唱?",
"result": "这两首歌曲\"Los del Espacio\"和\"Se Le Ve\"由最多的艺术家演唱,每首歌曲都有8位艺术家。"
}
总结.
CSV分析和建模是最费时的,生成可能会花上超过五分钟。
成本本身相当低。在八小时的实验里,我差不多发送了数百个请求,只花了一美元左右。
为了达到这一点,我们遇到了不少难题。
- 提示需要经过多次调整才能正确。这个问题可以通过微调模型或提供几个示例来克服。
- GPT-4o 的 JSON 响应有时可能不一致。我被推荐了
[json-repair](https://github.com/mangiucugna/json_repair)
,这比让 LLM 自己验证 JSON 输出更好。
这种方法在LangGraph实现中可能会运行得很好,其中操作是按顺序执行的,这使得LLM能够逐步构建和优化模型。随着新模型的不断发布,它们也可能从进一步的微调中获益。
一个了解更多信息的地方了解更多信息,可以查看Harnessing Large Language Models With Neo4j以了解更多关于使用LLM简化知识图谱的创建过程的信息。阅读Create a Neo4j GraphRAG Workflow Using LangChain and LangGraph以了解更多关于LangGraph和Neo4j的信息。想了解更多关于微调的内容,可以看看Knowledge Graphs and LLMs: Fine-Tuning vs. Retrieval-Augmented Generation。