Галоўная / Артыкулы

Створыце простую паіплайн GraphRAG з Neo4j, Gemini і LangChain.

Невелика паіплайн GraphRAG з Neo4j, Gemini і LangChain: выкарыстоўванне дадзеных па схеме, запыты да суседзей і кроз кальцы, толькі чытанне тексту ў формат Cypher.

Выдавец: REACTAPP.TOP7 хв чытання1684 слоў

Крыніцы: 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, якія можна было б застаўляць для звітав пра інцидэты чы аблікаванняя графаў: зберагваць адносы раз, запытваць іх рознымі спосабамі та не даваць модэлю стварваць запыты для збіркі дадзеных у режыме запису.