Створыце простую паіплайн GraphRAG з Neo4j, Gemini і LangChain.
Невелика паіплайн GraphRAG з Neo4j, Gemini і LangChain: выкарыстоўванне дадзеных па схеме, запыты да суседзей і кроз кальцы, толькі чытанне тексту ў формат Cypher.
Крыніцы: Building a Simple GraphRAG Pipeline with Neo4j, Gemini, and LangChain
У гэтым карыце ўтвораецца мініатюрны стак GraphRAG: Neo4j храніць граф, Gemini супляўляе модель мовы, а LangChain спаюе ўсі элементы. Прыкладны домэн — зьвісткі пра нешчасныя випадкі на кухні кухараў; яны выбраны навмесна простымі, ўпрымкі ўбачыце без неабходнасьці розбіраць спецыялістскі жаргон.
Настройка
З’явіце драйвер Neo4j асобна пакеты LangChain для Neo4j і Google GenAI:
pip install neo4j langchain-neo4j langchain-google-genai
З’яўленне ў Neo4j
Зацірайце URI, імя падпрыемніка, пароль і, за выборам, назву базы дадзеных з локальнага файла з крантэнаціямі, ўжо каб нічога секрэтнага не было зафіксавана:
neo4j_creds = {}
with open("Neo4j-cred.txt") as f:
for line in f:
line = line.strip()
if line and "=" in line and not line.startswith("#"):
key, value = line.split("=", 1)
neo4j_creds[key.strip()] = value.strip()
NEO4J_URI = neo4j_creds["NEO4J_URI"]
NEO4J_USERNAME = neo4j_creds["NEO4J_USERNAME"]
NEO4J_PASSWORD = neo4j_creds["NEO4J_PASSWORD"]
NEO4J_DATABASE = neo4j_creds.get("NEO4J_DATABASE")
Адрабатайце тэсты з стандартным драйверам Neo4j перш чым прыўязваць LangChain; так будуць адокремлены проблемы з західам і сетчуком:
from neo4j import GraphDatabase
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
driver.verify_connectivity()with driver.session(database=NEO4J_DATABASE) as session:
assert session.run("RETURN 1 AS ok").single()["ok"] == 1driver.close()
print("Neo4j connectivity OK.")
Калі драйвер запрацюе, захавайце тыя ж крантэнаціі ў функцыі Neo4jGraph з LangChain, якая будзе викорыстоўвана ў наступных кроках:
from langchain_neo4j import Neo4jGraph
graph = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database=NEO4J_DATABASE,
)
Запуск Gemini
Створыце кліента ChatGoogleGenerativeAI на адміністрацыйны ключ среды:
import os
from langchain_google_genai import ChatGoogleGenerativeAI
GEMINI_MODEL = "gemini-2.5-flash"
api_key = os.environ.get("GOOGLE_API_KEY")llm = ChatGoogleGenerativeAI(model=GEMINI_MODEL, google_api_key=api_key)
Адзін экземпляр модэлі выконвае два заведамення: ператварае неапранутыя адчыты на дакументы графа, а таксама ператварае запиты на англійскай мове ў формат Cypher.
Стварыце граф на адной колькасці дакументаў
Чатыры короткія файлы з інформацыяй пра нешчасныя случаі выступаюць у ролі журналу змен, калі ўсё пайшло не так. Заванесіце іх як об’екты LangChain Document:
from langchain_core.documents import Document
reports = {
"meera_souffle.txt": "Chef Meera's cheese soufflé collapsed minutes after leaving "
"the oven at her home kitchen. The stand mixer was used to whip the egg "
"whites; they were overwhipped and had already begun to separate before "
"folding, so the soufflé lost its structure and fell flat.",
"arjun_fire.txt": "A grease fire broke out in the oven at the community center "
"kitchen while Chef Arjun was roasting trays of vegetables. Built-up grease "
"ignited after the oven was left on high heat for an extended period. The "
"smoke set off alarms across the entire community center building.",
"lina_curry.txt": "Chef Lina's curry burned on the induction stove at the "
"catering truck kitchen. No timer was set, and the dish was left unattended "
"well past its cooking time.",
"devi_cake.txt": "Chef Devi's layer cake collapsed at the bakery kitchen. "
"All-purpose flour was substituted for cake flour in the stand mixer "
"without adjusting the liquid ratio, and the batter never set properly.",
}documents = [
Document(page_content=text, metadata={"source": name})
for name, text in reports.items()
]
Контролюйце процес выкарыстоўвання дакументаў. Укажыце ALLOWED_NODES і ALLOWED_RELATIONSHIPS як трыплы з назвамі from-label, edge і to-label, каб адчыты викорыстоўвалі адной тавароўніцу, а не стваралі новыя тэрміны для ідэнтычных фактов.
from langchain_neo4j import LLMGraphTransformer
ALLOWED_NODES = [
"Chef", "Kitchen", "Mishap", "Appliance", "FailureCause", "CauseCategory", "AffectedArea",
]ALLOWED_RELATIONSHIPS = [
("Chef", "WORKS_AT", "Kitchen"),
("Mishap", "OCCURRED_AT", "Kitchen"),
("Mishap", "INVOLVED_APPLIANCE", "Appliance"),
("Appliance", "HAD_ISSUE", "FailureCause"),
("Mishap", "CATEGORIZED_AS", "CauseCategory"),
("Chef", "RESPONSIBLE_FOR", "Mishap"),
("Mishap", "AFFECTED_AREA", "AffectedArea"),
]CAUSE_CATEGORY_INSTRUCTIONS = (
"A Mishap is the specific event described in the report (e.g. 'Meera's Soufflé "
"Collapse'), distinct from the Kitchen where it happened. A FailureCause should "
"use the report's own specific wording for what went wrong (e.g. 'egg whites "
"overwhipped', 'grease buildup ignited'). A CauseCategory is a normalized, shared "
"classification: always choose exactly one of 'Temperature Control Failure', "
"'Timing or Attention Lapse', or 'Technique or Substitution Error', even if the "
"report never uses this wording. Only create an AffectedArea node, connected via "
"AFFECTED_AREA, when the report says the mishap's effects spread beyond the "
"kitchen itself — not for the kitchen's own name or address."
)transformer = LLMGraphTransformer(
llm=llm,
allowed_nodes=ALLOWED_NODES,
allowed_relationships=ALLOWED_RELATIONSHIPS,
strict_mode=True,
additional_instructions=CAUSE_CATEGORY_INSTRUCTIONS,
)graph_documents = await transformer.aconvert_to_graph_documents(documents)for gd in graph_documents:
print(f"{gd.source.metadata['source']}: {len(gd.nodes)} nodes, {len(gd.relationships)} relationships")
Тыповы запуск можа выдаць такія паказнікі:
meera_souffle.txt: 6 nodes, 5 relationships
arjun_fire.txt: 7 nodes, 6 relationships
lina_curry.txt: 6 nodes, 5 relationships
devi_cake.txt: 6 nodes, 5 relationships
Запішыце дакументы графа ў Neo4j і апавярце метаданыі схемы:
graph.add_graph_documents(graph_documents, baseEntityLabel=True, include_source=True)
graph.refresh_schema()
Прямыя запиты, якія паказваюць структурную цэннась:
Для гэтых пераканальняў не трэба другога вызову модэлю — рэглі ўжо існуюць.
Пераканайцеся, што даў выкарыстоўванне дадзеных:
node_counts = graph.query(
"MATCH (n:__Entity__) RETURN labels(n) AS labels, count(*) AS count ORDER BY count DESC"
)
rel_counts = graph.query(
"MATCH ()-[r]->() WHERE type(r) <> 'MENTIONS' "
"RETURN type(r) AS relationship_type, count(*) AS count ORDER BY count DESC"
)
Суседзі ў радзе двух крокаў ад адной ентыцыты. Пошук на адпоўненась не можа замяніць запит «все, што з’яднаўся з Meera»:
SEARCH_TERM = "meera"
neighborhood = graph.query(
"""
MATCH (a:__Entity__)-[r*1..2]-(b:__Entity__)
WHERE toLower(a.id) CONTAINS $term
RETURN DISTINCT a.id AS start, [rel IN r | type(rel)] AS via, b.id AS neighbor
LIMIT 25
""",
params={"term": SEARCH_TERM},
)
Сяродзейшы шлях між людзьмі, якія ніколі не з’являюцца разам у аднам звятыку. Meera і Devi ніколі не называюць аднае другую, пры тым оба інцидэты маюць аднойчынную катэгорію прычыны:
TERM_A, TERM_B = "meera", "devi"
paths = graph.query(
"""
MATCH (a:__Entity__), (b:__Entity__)
WHERE toLower(a.id) CONTAINS $term_a AND toLower(b.id) CONTAINS $term_b
MATCH p = shortestPath((a)-[*..6]-(b))
RETURN [n IN nodes(p) | n.id] AS path_nodes, [r IN relationships(p) | type(r)] AS path_rels
LIMIT 5
""",
params={"term_a": TERM_A, "term_b": TERM_B},
)
Meera --[RESPONSIBLE_FOR]--> Meera's Soufflé Collapse --[CATEGORIZED_AS]--> Technique or Substitution Error <--[CATEGORIZED_AS]-- Devi's Cake Collapse <--[RESPONSIBLE_FOR]-- Devi
Гэты шлях ёсць пераканальны адказ на пытанне пра зв’язанась — спачатку структура, пасля тэкст.
Задавайце запытанні простаю англійскай мовай
GraphCypherQAChain — это класычны цыкл «тэкст-у-Cypher»: модель стварае Cypher на адпаведнай схеме, Neo4j яго выканае, а на другім кроку прадстаўляюцься рэкі. Паколькі створаны Cypher працуе з жывай базой дадзеных, трэба ў запите адначасна прымясці чыста для чытання інструкцыю — і памятаць, што для абэрангі ў прыменні трэба ролі базы дадзеных толькі для чытання, а не толькі правільны запит.
from langchain_core.prompts import PromptTemplate
from langchain_neo4j import GraphCypherQAChain
from langchain_neo4j.chains.graph_qa.cypher import CYPHER_GENERATION_PROMPT
READ_ONLY_INSTRUCTION = (
"\nThis is a read-only exploration tool: only ever generate MATCH / OPTIONAL MATCH / "
"WITH / RETURN / ORDER BY / LIMIT statements. Never generate CREATE, MERGE, DELETE, "
"SET, REMOVE, or DROP, even if the question seems to ask for a change."
)cypher_prompt = PromptTemplate(
template=CYPHER_GENERATION_PROMPT.template.replace(
"Instructions:", f"Instructions:{READ_ONLY_INSTRUCTION}"
),
input_variables=CYPHER_GENERATION_PROMPT.input_variables,
)graphrag_chain = GraphCypherQAChain.from_llm(
llm,
graph=graph,
cypher_prompt=cypher_prompt,
validate_cypher=True,
return_intermediate_steps=True,
allow_dangerous_requests=True,
verbose=True,
)
Маленькі дапаможнік выводзіць разам пытанне, створаны Cypher, прыклады рэкаў і заканчоўны адказ:
def ask_graph(question: str):
result = graphrag_chain.invoke({"query": question})
cypher = result["intermediate_steps"][0]["query"]
context = result["intermediate_steps"][1]["context"]
print(f"Q: {question}\n")
print(f"Generated Cypher:\n{cypher}\n")
print(f"Graph context ({len(context)} rows): {context[:5]}\n")
print(f"Answer:\n{result['result']}")
return result
Пытанне пра распіску кухні:
_ = ask_graph("Which kitchens are in the graph, and which chef works at each one?")
Generated Cypher:
MATCH (c:Chef)-[:WORKS_AT]->(k:Kitchen)
RETURN k.id AS kitchen, c.id AS chef
Answer:
The kitchens and the chefs who work there are: the Home Kitchen (Meera), the
Community Center Kitchen (Arjun), the Catering Truck Kitchen (Lina), and the
Bakery Kitchen (Devi).
Больш дакладны аналіз нешчасця:
_ = ask_graph(
"For each mishap, which chef was responsible, which appliance was involved, "
"what category was it classified under, and did it affect any area beyond the kitchen?"
)
Generated Cypher:
MATCH (m:Mishap)
OPTIONAL MATCH (c:Chef)-[:RESPONSIBLE_FOR]->(m)
OPTIONAL MATCH (m)-[:INVOLVED_APPLIANCE]->(a:Appliance)
OPTIONAL MATCH (m)-[:CATEGORIZED_AS]->(cat:CauseCategory)
OPTIONAL MATCH (m)-[:AFFECTED_AREA]->(area:AffectedArea)
RETURN m.id AS mishap, c.id AS chef, a.id AS appliance, cat.id AS category, area.id AS affected_area
Answer:
- Meera's Soufflé Collapse: Meera, Stand Mixer, Technique or Substitution Error,
no area affected beyond the kitchen.
- Community Center Kitchen Fire: Arjun, Oven, Temperature Control Failure,
affected the entire Community Center building.
- Lina's Burnt Curry: Lina, Induction Stove, Timing or Attention Lapse,
no area affected beyond the kitchen.
- Devi's Cake Collapse: Devi, Stand Mixer, Technique or Substitution Error,
no area affected beyond the kitchen.
Што пробаваць далей
Адзьёўваце методы выявлення спільнатэкаючых элементаў (на прыклад, Leiden), каб агрупаваць супакойленыя інцидэты, і заздалегідь вырахаваць статыстыку для запитоў, якія стосуюцца всього корпусу. Супараднавайце граф з вектарным індэксам сыроўга тексту звітав, каб неясныя запиты моглі знайсці входны вузол пры продалейшанні пераходжэння. Аккуратна расшироўваюце спісы дазволенага, калі з’яўляюцца новыя типы элементаў, і пераканайцеся, што створаныя запіты Cypher працуюць на незалежным наборы запитоў, прычым расширваючы правыя доступу да БД.
Чаму дэманстрацыя застаецца маленькай
Чатыры дакументы достатнія, каб практыкуваць обмежаную экстракцыю, запиты суседства, багатоэтапныя шляхі, а таксама пераклад з NL на Cypher, пры чым є видныя меры захоплення толькі для чытання. Гісторыя з кухнёй застаецца чытабельной, адночасна навучаючы тых сэрвісоў GraphRAG, якія можна было б застаўляць для звітав пра інцидэты чы аблікаванняя графаў: зберагваць адносы раз, запытваць іх рознымі спосабамі та не даваць модэлю стварваць запыты для збіркі дадзеных у режыме запису.