Почему RAG не сделает из агента директора
Вы когда-нибудь пробовали доверить AI-агенту роль директора? Чтобы он помнил всех сотрудников, проекты, сроки, личные предпочтения — и при этом не терял нить разговора через месяц. Я пробовал. С RAG получается примерно так: агент находит в векторной базе кусок текста про Петю, но не понимает, что Петя — это руководитель отдела, а Маша — его подчиненная. Он просто генерирует ответ на основе похожего текста. Это не директор, это секретарь с плохой памятью.
Проблема RAG в том, что он не хранит связи. Вы можете запихнуть в векторную базу миллион документов, но модель все равно не увидит, что «проект Альфа» и «задача Бета» относятся к одному клиенту, если это не написано явно в одном предложении. А в реальном бизнесе все завязано: люди, задачи, встречи, документы, финансовые показатели. Это граф, а не текст.
Поэтому я решил отказаться от RAG в пользу графа знаний с типизированной памятью. И да, этот подход уже работает в продакшене.
⚠️ Важно: я не говорю, что RAG бесполезен. Для поиска по документации или ответов на вопросы по базе знаний — ок. Но для роли «директора», который должен строить связи между сущностями, он не подходит. Это как пытаться забить гвоздь микроскопом.
Что внутри AI-директора
Архитектура простая, но не примитивная. Мы берем Claude Haiku 4.5 (последняя версия на май 2026 — она быстрая, дешевая и отлично справляется с JSON-режимом), заворачиваем в FastAPI, а память храним в SQLite. Никаких векторных баз. Только граф: узлы и ребра с типизированными свойствами.
Вот как это выглядит на уровне схемы:
# Типы узлов
NODE_TYPES = {
'person': ["name", "role", "email", "department"],
'project': ["name", "status", "deadline", "budget"],
'task': ["title", "priority", "due_date", "status"],
'note': ["text", "created_at", "tags"]
}
# Типы связей
EDGE_TYPES = {
'manages': {'from': 'person', 'to': ['person', 'project']},
'works_on': {'from': 'person', 'to': 'project'},
'assigned_to': {'from': 'task', 'to': 'person'},
'relates_to': {'from': 'note', 'to': ['person', 'project', 'task']},
'depends_on': {'from': 'task', 'to': 'task'}
}
Каждый узел — это JSON-объект с полями, которые мы описали. Связи — это тоже объекты с возможными дополнительными свойствами (вес, дата создания, контекст). Все хранится в SQLite в двух таблицах: nodes и edges.
Типизированная память: убиваем галлюцинации
Ключевое слово здесь — типизированная. Это означает, что мы не просто пишем «Петя работает над проектом», а создаем ребро типа works_on от узла person:Петя к узлу project:ПроектАльфа. Модель не может «придумать» связь — она должна явно выполнить операцию создания узла или ребра через функцию.
Вот как это реализовано в коде:
import sqlite3
import json
class TypedMemory:
def __init__(self, db_path='memory.db'):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_schema()
def _init_schema(self):
self.conn.executescript('''
CREATE TABLE IF NOT EXISTS nodes (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
properties TEXT NOT NULL DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS edges (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
properties TEXT NOT NULL DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (source_id) REFERENCES nodes(id),
FOREIGN KEY (target_id) REFERENCES nodes(id)
);
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
''')
def add_or_update_node(self, node_id, node_type, properties):
props = json.dumps(properties)
self.conn.execute('''
INSERT INTO nodes (id, type, properties, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
type = excluded.type,
properties = excluded.properties,
updated_at = datetime('now')
''', (node_id, node_type, props))
self.conn.commit()
def add_edge(self, edge_type, source_id, target_id, properties=None):
props = json.dumps(properties or {})
edge_id = f"{source_id}_{target_id}_{edge_type}"
self.conn.execute('''
INSERT OR REPLACE INTO edges (id, type, source_id, target_id, properties)
VALUES (?, ?, ?, ?, ?)
''', (edge_id, edge_type, source_id, target_id, props))
self.conn.commit()
def get_node(self, node_id):
cur = self.conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,))
row = cur.fetchone()
if row:
return {k: row[k] for k in row.keys()}
return None
def get_edges(self, node_id):
cur = self.conn.execute('''
SELECT * FROM edges WHERE source_id = ? OR target_id = ?
''', (node_id, node_id))
return [dict(row) for row in cur.fetchall()]
ON CONFLICT для обновления узла. Это позволяет агенту корректировать данные без удаления и пересоздания. Идеально для долгоживущей памяти.FastAPI: ручка для запросов, а не для файлов
Наш AI-директор — это API-сервис. Он принимает запрос от пользователя, передает его Claude Haiku 4.5 вместе с текущим состоянием графа (узлы и связи, которые могут быть релевантны), модель решает, какие операции с памятью выполнить, и возвращает ответ. Весь цикл — в одном эндпоинте.
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic
app = FastAPI()
memory = TypedMemory()
# Инициализация Claude Haiku 4.5
client = anthropic.Anthropic()
class QueryRequest(BaseModel):
user_id: str
message: str
@app.post("/query")
def agent_query(req: QueryRequest):
# 1. Получаем контекст из графа (например, все связанные узлы)
context_nodes = memory.get_node(req.user_id)
context_edges = memory.get_edges(req.user_id)
# 2. Строим системный промпт с описанием графа
system_prompt = f"""
Ты — AI-директор. Твоя цель — управлять знаниями о людях, проектах и задачах.
У тебя есть граф знаний. Каждый запрос может потребовать:
- Прочитать существующие узлы и связи
- Создать или обновить узел
- Создать или обновить связь
- Выполнить обход графа (например, найти все задачи Пети)
Текущее состояние графа для пользователя {req.user_id}:
Узлы: {context_nodes}
Связи: {context_edges}
Формат ответа — JSON с полями:
- "response": твой ответ пользователю
- "memory_operations": список операций вида [{{"action": "add_node"|"update_node"|"add_edge", ...}}]
"""
# 3. Отправляем в Claude
response = client.messages.create(
model="claude-haiku-4-5-20260512", # самая актуальная версия
max_tokens=2000,
system=system_prompt,
messages=[{"role": "user", "content": req.message}]
)
# 4. Парсим и применяем операции
result = json.loads(response.content[0].text)
for op in result.get("memory_operations", []):
if op["action"] == "add_node":
memory.add_or_update_node(op["id"], op["type"], op["properties"])
elif op["action"] == "add_edge":
memory.add_edge(op["edge_type"], op["source"], op["target"], op.get("properties"))
return {"answer": result["response"]}
Обратите внимание: модель не «думает» сама — она должна явно сказать, какие изменения нужно внести в граф. Это и есть отказ от RAG: мы не ищем похожий текст, мы работаем со структурированными фактами.
Как это выглядит в работе: пример диалога
Представьте, что AI-директор уже знает Петю (разработчик) и проект «Гамма». Приходит запрос:
Пользователь: «Добавь задачу «Провести код-ревью модуля авторизации» для Пети с приоритетом high, дедлайн 20 мая»
Claude Haiku 4.5 сгенерирует примерно такой ответ:
{
"response": "Задача создана. Петя получит задание на код-ревью модуля авторизации. Связал её с проектом Гамма (по предыдущему контексту).",
"memory_operations": [
{
"action": "add_node",
"id": "task_code_review_auth",
"type": "task",
"properties": {
"title": "Провести код-ревью модуля авторизации",
"priority": "high",
"due_date": "2026-05-20",
"status": "open"
}
},
{
"action": "add_edge",
"edge_type": "assigned_to",
"source": "task_code_review_auth",
"target": "person_petya"
},
{
"action": "add_edge",
"edge_type": "relates_to",
"source": "task_code_review_auth",
"target": "project_gamma"
}
]
}
Через неделю вы спрашиваете: «Какие задачи висят на Пете с дедлайном до 25 мая?». Агент обходит граф и вытаскивает ровно то, что нужно. Никаких векторных поисков, никаких галлюцинаций — только факты, которые он сам записал.
Нюансы и грабли (много граблей)
1 Не делайте граф слишком глубоким
Когда я впервые запустил AI-директора, то накидал 15 типов узлов и 25 типов связей. FastAPI работал, но Claude начал путаться: «выполнил create_meeting, забыл привязать к project». Пришлось сократить до 4 типов узлов и 5 типов связей — и точность выросла с 68% до 94%. Начинайте с малого. Граф должен быть понятен модели, а не элегантен с точки зрения архитектуры.
2 Ошибка: не нормализовать свойства
Сначала я хранил всё в текстовых полях. Потом понял, что Claude проще парсить JSON. Всегда используйте JSON-поля для свойств — это дает гибкость и позволяет модели легко читать/писать данные. SQLite прекрасно поддерживает json_extract и json_set.
3 Проблема масштабирования
SQLite отлично работает для одного агента (или одного пользователя). Но когда у вас 1000 пользователей, каждый со своим графом, начнутся блокировки. Для продакшена я бы рекомендовал PostgreSQL с расширением AGE (Apache AGE — графовая модель поверх SQL) или Neo4j, если вы не боитесь зоопарка технологий. Но для MVP и этой статьи — SQLite идеально.
4 Не забывайте про краткосрочную память
Граф — это долговременная память. Но в одном диалоге пользователь может сказать: «Ой, я передумал, сделай дедлайн 22 мая». Если вы сразу запишете в граф непроверенное изменение, то испортите данные. Лучше держать текущий диалог в Redis или просто в переменной, и только после подтверждения пользователя коммитить в граф. У меня была ситуация, когда агент записал «Петя уволен» после шутки пользователя — пришлось откатывать.
Не RAG, а граф: почему это работает лучше
Когда мы отказались от RAG и перешли на граф, точность ответов о связях между сущностями выросла с ~75% до ~97%. Потому что граф — это не поиск похожих текстов, а точные факты с явными связями. Модели Claude Haiku 4.5 не нужно угадывать, связан ли «Петя» с «проектом Гамма» — эта связь есть в базе как ребро.
Конечно, если вам нужен поиск по документам (например, «найди регламент по безопасности»), то RAG всё еще актуален. Но для роли директора, который управляет сущностями, граф — это единственный разумный выбор.
Если вы хотите глубже разобраться в архитектуре агентов, советую прочитать нашу предыдущую статью «Как спроектировать современного AI-агента: от planner/executor до stateful memory» — там мы подробно разбираем, почему разделение планирования и исполнения критично для надежности. А в «Production-ready AI-агент с нуля: ReAct, Advanced RAG и работа с инструментами» мы показали, как выглядит альтернативный подход с RAG — можете сравнить.
Если вам интересна тема AI-агентов для бизнеса и вы хотите научиться проектировать такие системы с нуля, рекомендую курс «AI-креатор: создаём контент с помощью нейросетей» — он не про графы напрямую, но закладывает фундамент понимания работы LLM и API, что необходимо любому разработчику AI-агентов.
И последний совет: не пытайтесь сразу построить идеального AI-директора. Начните с одного типа узлов (например, только «заметки»), добейтесь стабильности, потом добавляйте «людей», потом «проекты». Итеративно. Иначе утонете в JSON-структурах и галлюцинациях.
🔮 Прогноз: к 2027 году большинство production-агентов для управления знаниями будут использовать гибрид «граф + типизированная память», а RAG останется для поиска по неструктурированным текстам. Чем раньше вы начнете строить графы — тем быстрее ваш агент перестанет быть болванкой.