图形模型显示了曲目及其与艺人的PERFORMED_BY关系。照片由作者拍摄。
我的工作很大一部分是提高用户在使用 Neo4j 时的体验。通常,将数据导入 Neo4j 并高效建模是用户面临的关键挑战,尤其是在刚开始使用的时候。虽然初始的数据模型很重要,也需要仔细考虑,但它可以很容易地进行重构,以提高性能,随着数据量或用户数量的增加。
所以,作为一次挑战自己,我想看看大型语言模型能否帮助构建初始数据模型。至少,它能展示事物之间的关联,并让用户能快速展示一些结果给他人。
直觉上,我知道数据建模是一个迭代过程,而且某些LLM面对大量数据时容易分心,所以这正好可以利用LangGraph来循环处理数据。
让我们来看看哪些提示让它得以实现。
《GraphAcademy 的图数据建模基础课程》(https://graphacademy.neo4j.com/courses/modeling-fundamentals/?ref=adam) 带领您了解在图中建模数据的基础知识,简单来说,我使用了一些基本准则:
动词也可以是节点;你可能很高兴知道某个人订购了某个产品,但这种基本模型无法告诉你产品是在哪里和什么时候订购的。在这种情况下,订购就成了模型中的一个新节点。
我相信这可以提炼成一个提示,用来创建无需训练的图形数据建模方法。
几个月之前,我短暂地尝试了这个,发现我用的那个模型在处理更复杂的架构时很容易分神,结果提示信息很快达到了最大令牌数限制。
这次我打算一次试一次地来,每次处理一个键值对。这样可以避免分心,因为每次LLM只需专注于处理一个键值对。
最终的步骤采用了如下步骤:
我在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(""" 你是一位专业的图数据库专家。 基于现有数据源提供的信息,你的工作是设计数据模型。
你需要决定以下列在现有数据模型中的位置。考虑:
考虑提供的示例。是否需要进行数据准备以确保数据格式正确?
你必须在描述中包括任何关于数据准备的信息。
这是一个好的输出示例:
{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()函数将描述转换为小写,以确保在判断节点和关系类型时的一致性。
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
# 数据模型检查 为了保持理智,检查模型是否已经优化过是值得的。`模型提示` 在识别名词和动词方面表现得很出色,但在更复杂的模型中。 在一次迭代中,`*_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
进行设置。
{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) // 表示歌曲由该艺术家表演
这个提示的制作需要一些技术处理以实现一致的效果:
MERGE
语句会定义多个字段,这在最佳情况下也显得不够理想。如果任何列为空,整个导入将失败。[apoc.period.iterate](https://neo4j.com/docs/apoc/current/overview/apoc.periodic/apoc.periodic.iterate/)
,这个功能现在不再需要了,我想要一个我可以使用 Python 驱动程序执行的代码。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) # 打印结果计数器
这篇帖子若没有用 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 看起来是正确的,并且正确的结果包含在提示中,但 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分析和建模是最费时的,生成可能会花上超过五分钟。
成本本身相当低。在八小时的实验里,我差不多发送了数百个请求,只花了一美元左右。
为了达到这一点,我们遇到了不少难题。
[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。