Классический векторный RAG оперирует концепцией «плоского пространства документов». Он эффективно решает задачу локального семантического поиска: находит фрагменты, текстово или тематически похожие на запрос. Однако при усложнении задач в Enterprise-системах векторный подход демонстрирует фундаментальную слепоту в двух ключевых классах запросов:
Для преодоления этих ограничений исследовательское подразделение Microsoft Research совместно с сообществом графовых баз данных разработало подход GraphRAG (Graph-Augmented Retrieval). В основе GraphRAG лежит автоматическое преобразование неструктурированного текста в Граф знаний (Knowledge Graph) с последующей иерархической кластеризацией сообществ (Community Detection) и поддержкой запросов на графовом языке Cypher.
GraphRAG объединяет топологию графа и векторное пространство, разделяя поиск на два взаимно дополняющих режима: Local Search (локальный поиск по окрестностям сущностей) и Global Search (поиск по иерархическим резюме сообществ).
[ Unstructured Documents ]
|
v
[ Triplet Extraction (LLM + Zod) ] ---> (Subject)-[:PREDICATE]->(Object)
|
v
[ Knowledge Graph (Neo4j / Memgraph) ]
|
v
[ Leiden Community Detection Algorithm ]
|
+---> Level 0: Микро-кластеры (тесно связанные компоненты)
+---> Level 1: Мезо-кластеры (функциональные подсистемы)
+---> Level 2: Макро-кластеры (доменные архитектурные области)
|
v
[ Pre-computed Community Summaries ]
|
+-----------------------------------------------------------------------+
| QUERY PROCESSING DUAL ENGINE |
| |
| [ User Query: Multi-hop question ] [ User Query: Global summary ] |
| | | |
| v v |
| [ Local Search Engine ] [ Global Search Engine ] |
| 1. Entity Vector Matching 1. Filter Community Summaries |
| 2. K-Hop Graph Traversal (Cypher) 2. Map-Reduce over Clusters |
| 3. Extract Subgraph Context 3. Synthesize Global Answer |
+-----------------------------------------------------------------------+(Субъект) -> [Предикат] -> (Объект). Например: (:Service {name: "OrderService"}) -[:PUBLISHES]-> (:Event {name: "OrderCreated"}).
Иерархический алгоритм обнаружения сообществ в графах, оптимизирующий плотность модулярности. Разбивает сложный граф на вложенные кластеры связности без образования изолированных артефактных узлов.
Режим ретривала, при котором запрос проецируется на ключевые узлы графа, после чего выполняется обход окрестностей $k$-го порядка ($k$-hop traversal) для сбора контекста связанных сущностей.
Режим ретривала для широких обобщающих вопросов. Выполняет агрегацию по предварительно сгенерированным саммари кластеров алгоритма Лейдена методом Map-Reduce в обход плоских чанков.
| Параметр | Традиционный Vector RAG | GraphRAG (Knowledge Graph) | | :--- | :--- | :--- | | Базовая единица индекса | Изолированный чанк текста | Сущности, связи, свойства и кластеры | | Транзитивность (Multi-Hop) | Отсутствует (только прямое сходство) | Полная поддержка обхода путей любой длины ($k$-hop) | | Глобальные вопросы | Провал (выборка случайных $k$ чанков) | Отлично (синтез через Community Summaries) | | Стоимость индексации | Низкая (только инференс эмбеддингов) | Высокая (множественные вызовы LLM для извлечения сущностей) | | Контроль фактологии | Скрыт внутри скрытого слоя нейросети | Эксплицитен: граф можно просмотреть, отредактировать и валидировать |
На этапе индексации текст документа подается в LLM с инструкцией извлечения структурированных триплетов.
import OpenAI from "openai";
import { z } from "zod";
// Zod-схема графовых сущностей и связей
export const ExtractedEntitySchema = z.object({
id: z.string().describe("Каноническое нормализованное имя сущности в snake_case"),
name: z.string().describe("Отображаемое имя сущности"),
type: z.enum(["SERVICE", "DATABASE", "API_ENDPOINT", "EVENT", "DEVELOPER"]),
description: z.string().describe("Краткое резюме роли сущности в контексте"),
});
export const ExtractedRelationSchema = z.object({
sourceEntityId: z.string(),
targetEntityId: z.string(),
predicate: z.enum(["CALLS", "READS_FROM", "WRITES_TO", "PUBLISHES", "SUBSCRIBES_TO", "MAINTAINS"]),
description: z.string().describe("Контекст и детали взаимосвязи"),
weight: z.number().min(0).max(1).default(1.0),
});
export const GraphExtractionResultSchema = z.object({
entities: z.array(ExtractedEntitySchema),
relations: z.array(ExtractedRelationSchema),
});
export type GraphExtractionResult = z.infer<typeof GraphExtractionResultSchema>;
export class TripletExtractor {
private openai: OpenAI;
constructor(apiKey: string) {
this.openai = new OpenAI({ apiKey });
}
public async extractFromText(text: string): Promise<GraphExtractionResult> {
const prompt = `
Извлеки все архитектурные сущности и направленные взаимосвязи между ними из технического описания системы.
Нормализуй идентификаторы сущностей к snake_case (например: "auth_service", "billing_db").
ТЕКСТ:
${text}
`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.0,
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
});
const rawJson = JSON.parse(response.choices[0]?.message.content ?? "{}");
return GraphExtractionResultSchema.parse(rawJson);
}
}Для исключения уязвимостей Cypher Injection и дублирования сущностей используется инструкция MERGE со строгой параметризацией.
import neo4j, { Driver, Session } from "neo4j-driver";
import { GraphExtractionResult } from "./triplet-extractor";
export class Neo4jGraphService {
private driver: Driver;
constructor(uri: string, user: string, pass: string) {
this.driver = neo4j.driver(uri, neo4j.auth.basic(user, pass));
}
public async close(): Promise<void> {
await this.driver.close();
}
/**
* Атомарная запись извлеченных сущностей и связей в граф
*/
public async ingestGraphData(data: GraphExtractionResult): Promise<void> {
const session: Session = this.driver.session();
try {
await session.executeWrite(async (tx) => {
// 1. Создание или обновление сущностей
for (const entity of data.entities) {
await tx.run(
`
MERGE (e:Entity {id: $id})
ON CREATE SET
e.name = $name,
e.type = $type,
e.description = $description,
e.createdAt = datetime()
ON MATCH SET
e.description = e.description + " | " + $description,
e.updatedAt = datetime()
`,
{
id: entity.id,
name: entity.name,
type: entity.type,
description: entity.description,
}
);
}
// 2. Создание связей между сущностями
for (const rel of data.relations) {
// Динамический тип связи через аппроксимацию в Cypher
await tx.run(
`
MATCH (source:Entity {id: $sourceId})
MATCH (target:Entity {id: $targetId})
MERGE (source)-[r:RELATION {predicate: $predicate}]->(target)
ON CREATE SET
r.description = $description,
r.weight = $weight,
r.createdAt = datetime()
ON MATCH SET
r.weight = (r.weight + $weight) / 2.0
`,
{
sourceId: rel.sourceEntityId,
targetId: rel.targetEntityId,
predicate: rel.predicate,
description: rel.description,
weight: rel.weight,
}
);
}
});
} finally {
await session.close();
}
}
/**
* Выполнение K-Hop поиска окрестностей сущности для Local Search
*/
public async getEntityNeighborhood(entityId: string, maxHops: number = 2): Promise<string> {
const session = this.driver.session();
try {
const result = await session.run(
`
MATCH path = (start:Entity {id: $entityId})-[*1..2]-(neighbor:Entity)
WITH start, relationships(path) AS rels, nodes(path) AS ns
UNWIND rels AS r
UNWIND ns AS n
RETURN DISTINCT
start.name AS root,
start.type AS rootType,
type(r) AS relationType,
r.predicate AS predicate,
r.description AS relDescription,
n.name AS neighborName,
n.type AS neighborType
LIMIT 50
`,
{ entityId }
);
const formattedContext = result.records.map((record) => {
return `[${record.get("root")}] -(${record.get("predicate")}: ${record.get("relDescription")})-> [${record.get("neighborName")}]`;
});
return formattedContext.join("\n");
} finally {
await session.close();
}
}
}Для генерации ответа на сложный вопрос агент сначала находит релевантные сущности через векторный поиск, затем запрашивает $k$-hop окрестности в графе Neo4j и передает полученный структурированный контекст в LLM.
import OpenAI from "openai";
import { Neo4jGraphService } from "./neo4j-service";
export interface GraphRAGResponse {
answer: string;
graphContextUsed: string[];
}
export class HybridGraphRAGPipeline {
private openai: OpenAI;
private graphService: Neo4jGraphService;
constructor(apiKey: string, graphService: Neo4jGraphService) {
this.openai = new OpenAI({ apiKey });
this.graphService = graphService;
}
/**
* Поиск ответа с использованием локального графового контекста
*/
public async executeLocalSearch(
query: string,
targetEntityId: string
): Promise<GraphRAGResponse> {
// 1. Извлекаем 2-hop подграф вокруг целевой сущности
const graphContext = await this.graphService.getEntityNeighborhood(targetEntityId, 2);
// 2. Формируем промпт с топологическим контекстом графа
const prompt = `
Ты — ведущий системный архитектор. Ответь на вопрос пользователя, опираясь ИСКЛЮЧИТЕЛЬНО на предоставленную топологию графа знаний.
ТОПОЛОГИЯ ГРАФА СВЯЗЕЙ:
${graphContext || "Связи не обнаружены."}
ВОПРОС:
${query}
`;
const completion = await this.openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.1,
messages: [{ role: "user", content: prompt }],
});
return {
answer: completion.choices[0]?.message.content ?? "",
graphContextUsed: graphContext.split("\n").filter(Boolean),
};
}
}Если разные документы упоминают одну сущность под разными именами: AuthService, auth_service, Сервис Аутентификации, OAuth Provider, наивное извлечение создаст 4 изолированных узла. Граф распадается на несвязанные островки, и транзитивный поиск путей перестает работать. На этапе вставки обязателен слой Entity Resolution: сопоставление синонимов через векторный поиск по узлам графа перед вызовом MERGE.
Предоставление агенту права выполнять сырой Cypher: MATCH (n)-[*]->(m) RETURN *. Запрос с неограниченной длиной пути [*] на графе с миллионом ребер приведет к комбинаторному взрыву обхода, исчерпанию всей оперативной памяти экземпляра Neo4j/Memgraph (OOM Crash) и падению базы данных. Разрешайте только параметризованные запросы со строгим ограничением глубины ([*1..3]) и директивой LIMIT.
Создание статических ребер (User)-[:OWNS]->(Company) без указания дат валидности validFrom / validTo. Если компания была продана в 2024 году, агент в 2026 году сделает ложный вывод о текущей структуре владения. Связи в графах знаний Enterprise-уровня всегда должны быть темпоральными.
Использование GraphRAG для поиска простых фактов («Какая версия PostgreSQL используется в проекте?») создает оверхед в 5-10 раз по стоимости токенов и задержке по сравнению с обычным векторным или гибридным поиском. GraphRAG должен активироваться только маршрутизатором запросов (Query Router) при детекции многошаговых или глобальных аналитических намерений.
LLM извлекает сущности, типы и предикаты из документов с валидацией контракта через Zod Schema.
Слияние синонимов и связывание новых упоминаний с существующими узлами графа через векторный индекс сущностей.
Выполнение транзакций MERGE в Neo4j или Memgraph с фиксацией темпоральных свойств ребер.
Фоновое разбиение топологии графа на многоуровневые сообщества и генерация Community Summaries для Global Search.
Маршрутизация пользовательского запроса в Local Search (k-hop Cypher) или Global Search (Map-Reduce по кластерам).