Модель знаний¶
Статья описывает, как memory-service хранит знания: граф на Apache AGE, чанки в pgvector, наблюдения, temporal-факты, provenance и аудит. Она нужна интеграторам, которые проектируют, что и как класть в память, и администраторам, которым важно понимать, что лежит в базе.
Коротко о модели:
Observations — свидетельства. Facts — интерпретации. Documents — источники. Граф выражает связи. Retrieval находит кандидатов. Context Compiler решает, что полезно сейчас.
Хранилище¶
Вся память живёт в одной базе PostgreSQL 16 с расширениями age (граф, openCypher),
vector (pgvector) и pg_trgm (триграммы). Расширения и граф создаёт init-скрипт
образа memory-db; остальную схему сервис создаёт сам при старте — идемпотентно
(если БД на старте недоступна, схема досоздаётся при первом обращении или командой
cb init-db). Сам сервис stateless.
| Объект | Где | Имя по умолчанию | Переменная |
|---|---|---|---|
| Граф знаний (узлы и рёбра) | схема AGE | company_brain |
CB_GRAPH_NAME |
| Чанки с эмбеддингами | public.<table> |
chunks |
CB_CHUNKS_TABLE |
| Наблюдения (observations) | таблица | observations |
CB_OBSERVATIONS_TABLE |
| Трейсы компиляции контекста | таблица | context_traces |
CB_CONTEXT_TRACES_TABLE |
| Реестр доменных пакетов | таблица | domain_packs |
CB_DOMAIN_PACKS_TABLE |
| Настройки видов namespace | таблица | namespace_settings |
CB_NAMESPACE_SETTINGS_TABLE |
| Журнал снимков источников | таблицы | source_snapshots, source_snapshots_items |
CB_SNAPSHOTS_TABLE |
Все namespaces одного инстанса лежат в одном графе и одних таблицах; изоляция
обеспечивается свойством/колонкой namespace и предикатом в каждом запросе (см.
Namespaces и доступ).
flowchart TB
SRC[Внешний источник] --> OBS[Observation<br/>таблица observations]
OBS -- provenance --> EP[Episode<br/>узел type=episode]
OBS --> ENT[Entity<br/>узел графа]
OBS --> FACT[Fact<br/>ребро с fact_id и интервалом]
OBS --> TXT[Text-фрагмент<br/>узел + чанк]
DOC[Документ / статья] --> NODE[Узел документа] --> CH[Чанки<br/>pgvector + FTS + trgm]
ENT --- FACT
subgraph G[Граф AGE]
EP
ENT
FACT
NODE
end
G --> CC[Context Compiler]
CH --> CC
OBS --> CC
CC --> PACK[ContextPack]
Узлы графа¶
Узел — типизированная сущность, уникальная в паре (namespace, natural_key).
Повторная запись того же ключа обновляет узел (upsert через MERGE), а не создаёт
новый.
| Свойство | Назначение |
|---|---|
natural_key |
Стабильный ключ узла: URL статьи, doc:<id>, person:alice, идентификатор задачи трекера и т. п. |
namespace |
База знаний, которой принадлежит узел |
type |
Вид сущности: article, document, note, episode, entity, виды доменных пакетов |
title |
Заголовок (по умолчанию — сам ключ) |
source_path |
Цель цитаты: путь файла, URI, agent:run/<run_id> |
source_id |
Идентификатор источника или прогона |
confidence |
Уверенность (сигнал ранжирования и аудита) |
last_seen |
Когда узел последний раз видели при записи |
origin |
vault — проекция документного vault, agent — запись через API |
props |
Карта свободных свойств: content (оригинал статьи), provenance, pii, pii_categories, scopes, meta и др. |
Метка AGE узла — это его type, приведённый к допустимому идентификатору
([A-Za-z_][A-Za-z0-9_]*; прочие символы заменяются на _, пустой тип становится
entity). Для каждой метки движок заводит GIN-индекс по свойствам и hash-индекс по
natural_key, чтобы сверка снимков и поиск по ключу не сканировали таблицу.
Оригинал статьи хранится в узле
POST /api/brain/retain кладёт полный текст в props.content. Поэтому
GET /api/brain/sources/{natural_key} возвращает статью ровно в том виде, в каком
её загрузили (content_source: "original"). Если оригинала в узле нет
(например, документ загружен готовыми чанками), текст восстанавливается склейкой
чанков по порядку (content_source: "chunks").
Происхождение: vault и agent¶
Движок различает два происхождения данных:
vault— узлы и рёбра, спроецированные командойcb ingestиз каталога Markdown-документов. Повторный полный ingest работает по схеме mark-and-sweep: всё vault-происхождение namespace, не встреченное в текущем прогоне, удаляется.agent— всё, что записано через HTTP API (retain,facts,documents, наблюдения, снимки). Такие данные ре-ingest vault не подметает.
HTTP-сервис никогда не пишет в сами документы-источники: запись через API попадает только в граф и индекс.
Рёбра и связи¶
Ребро соединяет два узла одного namespace (рёбра не пересекают namespaces). Служебные типы рёбер:
| Тип | Кто создаёт | Смысл |
|---|---|---|
LINKS_TO |
retain, facts, documents (поле links), vault-ссылки |
Узел ссылается на существующий узел-цель |
IN_TRACE |
запись с trace_id |
Факт относится к трейсу задачи или прогона |
| типы из frontmatter vault | cb ingest |
Связи документов (SUPERSEDED_BY, PART_OF_PROJECT и т. п.) |
| предикаты фактов | наблюдения, снимки | Temporal-факты (см. ниже) |
Ссылка links на несуществующий узел молча пропускается: ребро создаётся только
между существующими узлами.
Факты: время и свидетельства¶
Факт — ребро графа с уникальным fact_id и интервалом валидности. Несколько рёбер
одного типа между одной парой узлов могут сосуществовать, различаясь интервалами.
person:alice WORKS_ON project:alpha
fact_id = fact-<sha256(namespace|subject|predicate|object|valid_from)[:24]>
valid_from = 2026-08-01T00:00:00Z
valid_to = (нет — факт действует)
evidence = asserted confidence = 0.9
observation_ids = [obs-…] (чем доказан)
Свойства ребра факта: namespace, observed_at, valid_from, valid_to,
evidence, confidence, observation_ids, supersedes, superseded_by,
source_path, scopes, origin; у фактов из снимков — ещё attributes,
snapshot_source, snapshot_scope, snapshot_id.
Правила:
- Три времени различаются явно:
occurred_atнаблюдения (когда произошло), время приёма (когда узнала память) иvalid_from/valid_to(когда факт истинен). Все метки нормализуются к ISO-8601 UTC — на лексикографическом сравнении строк стоит вся temporal-фильтрация. - Идемпотентность.
fact_idдетерминирован: повтор того же утверждения из нового наблюдения не создаёт ребро, а усиливает факт — дописываетobservation_idsи поднимаетconfidenceдо максимума. - Supersession. Новый факт с
supersedesзакрываетvalid_toстарого и связывает егоsuperseded_by. История не удаляется: «больше не работает над X» — это закрытие интервала. - Конфликты не разрешаются автоматически. Перекрывающиеся версии одного
(subject, predicate)остаются обе; при сборке контекста они помечаютсяconflict: true, решение за потребителем. - Класс свидетельства (
evidence) — сигнал ранжирования, не истина:
| Класс | Ранг | Откуда |
|---|---|---|
asserted |
3 | Структурированное утверждение источника (assertions наблюдения, снимок) |
extracted |
2 | Детерминированное правило |
inferred |
1 | Вывод LLM из неструктурированного текста |
derived |
1 | Производное consolidation (сводка, слияние) |
- Потеря свидетельства. Если наблюдение, на котором держался факт, удалено
(
redact/purge), свидетельство вычёркивается изobservation_ids; факт без оставшихся свидетельств получаетevidence_lostи выпадает из выдачи, оставаясь в графе для аудита.
Наблюдения (observations)¶
Наблюдение — неизменяемая запись того, что сообщил внешний источник: событие трекера, письмо, доменное событие ядра. Хранится в реляционной таблице, а не в графе. Движок никогда не переписывает наблюдение; меняется только статус его обработки (и содержимое при redaction).
| Поле | Смысл |
|---|---|
observation_id |
obs-<sha256[:24]> от namespace и source identity — клиент может вычислить заранее |
source.system, source.stream, source.external_id |
Source identity: повтор того же события не создаёт дубликат |
kind |
Вид события (task.completed, work.completed…), [a-z0-9][a-z0-9._-]{0,127} |
occurred_at |
Время события |
actor, subject |
Участники, {type, id} |
scopes |
Метки видимости type:id (до 20) |
content |
Человекочитаемый текст (до 1 МиБ) |
data |
Структурированный payload (до 1 МиБ) |
assertions |
Структурированные утверждения для проекции (до 200) |
provenance |
{uri, …} — ссылка на исходное событие |
Без external_id дубликаты отсекаются по хешу канонической формы содержимого.
Статусы обработки: received, processed, partially_processed, failed,
redacted. Проекция в граф описана в Загрузке знаний.
Эпизоды и сущности¶
- Entity — узел графа произвольного вида. Assertion
entityсоздаёт или обновляет его; концы факта, которых ещё нет, создаются placeholder-узлами, так что порядок доставки не важен. Ключ сущности — строка или{type, id}(ключtype:id). - Episode — осмысленный фрагмент опыта («деплой упал», «решение принято»).
Физически — узел
type=episodeс provenance до наблюдений; отдельной таблицы нет.
Чанки и документы¶
Чанк — фрагмент текста в таблице chunks, по которому идёт поиск.
| Колонка | Назначение |
|---|---|
node_key |
Узел-владелец |
namespace |
База знаний |
chunk_order |
Порядковый номер фрагмента в документе |
source_path |
Цель цитаты |
title, heading |
Заголовок узла и раздел внутри документа |
text |
Текст фрагмента |
embedding |
Вектор vector(CB_EMBEDDING_DIM) |
meta |
jsonb: теги потребителя (collection и т. п.), observation_id, scopes |
seen_run |
Метка прогона vault-ingest; пустая у записей через API |
Уникальность — (namespace, node_key, chunk_order): повторная запись того же
порядкового номера заменяет фрагмент. Индексы: HNSW по косинусному расстоянию
эмбеддинга, GIN по полнотекстовому документу на русской конфигурации
(title + heading + text), GIN по meta, B-tree по namespace и, если есть права
на CREATE EXTENSION, триграммный GIN по text для поиска точных идентификаторов.
В эмбеддинг уходит не голый текст, а текст с заголовком: "<title> — <heading>\n<text>".
Так короткий фрагмент сохраняет контекст документа.
Размерность эмбеддинга фиксируется при создании таблицы
Колонка embedding создаётся с размерностью CB_EMBEDDING_DIM. Если позже
поменять модель или размерность, сервис откажется работать с понятной ошибкой —
нужен переиндекс (см. Конфигурацию).
Provenance: путь до источника¶
Каждый значимый элемент выдачи восстановим до исходного события или документа:
ContextItem
→ Fact (observation_ids) / Chunk (node_key, meta.observation_id) / Observation
→ Observation (source.system / stream / external_id, provenance.uri)
→ исходное событие внешней системы
Для статей, записанных через retain, поле provenance запроса сохраняется целиком
в props.provenance и возвращается source-view как provenance.metadata. Для
индексации Git-репозиториев в нём принято передавать repository, branch,
commit_sha, path, content_hash, author, committed_at, indexed_at.
Аудит-контур¶
События аудита — это узлы графа audit_event, привязанные ребром к якорю трейса
pc_trace. Их пишут:
- явный вызов
POST /api/brain/audit; - каждое удаление (
action="delete"со снимком удалённого: тип, заголовок, число чанков); - выдача немаскированных ПДн токену с допуском (
action="pii_access", если защита ПДн включена).
Аудит ведётся в namespace той базы знаний, где произошло событие. Узлы типов
audit_event и pc_trace защищены: механизм удаления отвечает на них 400, чтобы
нельзя было стереть след удаления тем же механизмом. След читается через
GET /api/brain/trace/{trace_id}?namespace=….
Видимость внутри namespace¶
Помимо namespace, у узла, чанка, наблюдения или факта может быть список scopes —
меток type:id. Особое значение имеют префиксы workspace: и principal:: элемент
с такими scopes виден только вызывающему, чьи разрешённые scopes его пересекают.
Элемент без scopes (или только со scopes релевантности вроде task:/project:)
видят все, кто может читать namespace. Правила — в
Namespaces и доступ.
Персональные данные¶
При CB_PII_PROTECTION=true запись маркирует узел pii: true и pii_categories по
результатам автодетекции (phone, email, passport_rf, snils, card, inn) и
явной метке запроса. ФИО и адреса детектор не распознаёт — их нужно помечать явно
("pii": true, "pii_categories": ["fio"]). Маскирование на выдаче работает и без
маркировки — по тем же шаблонам. Подробнее — в API.
Доменные пакеты и строгий режим¶
Движок не содержит доменных видов в коде. Виды сущностей, связи и шаблоны
идентификаторов предметной области описываются доменным пакетом — версионируемым
JSON, который регистрируется через POST /api/memory/packages:
| Элемент пакета | Смысл |
|---|---|
kinds[].kind |
Имя вида (endpoint, component, issue…) |
kinds[].naturalKey |
JSON Schema ключа или шаблон ключа с плейсхолдерами <name> / {name} |
kinds[].aliases |
Альтернативные формы ключа (по ним разрешаются якоря обхода) |
kinds[].kindAliases |
Синонимы имени вида |
kinds[].idPatterns |
Регулярные выражения для извлечения идентификаторов вида из текста |
kinds[].attributes |
JSON Schema атрибутов |
relations[] |
relation, fromKinds, toKinds, temporal (по умолчанию true), cardinality (many/one) |
Базовые виды document, entity, fact есть всегда; встроенный пакет default
зарезервирован. Версия пакета неизменяема: регистрация той же версии с другим
содержимым даёт 409.
Пакет действует только в namespaces, где явно включён
(PUT /api/memory/namespaces/{ns}/kinds). В строгом режиме (strict: true)
запись сущности неизвестного вида или с ключом/атрибутами вне схемы отвергается
ответом 422; при сверке снимка проверяются и связи. Без строгого режима неизвестные
виды сохраняются как свободные узлы.
Пакеты используются вместе со сверкой снимков источников (POST /api/memory/reconcile)
и типизированным обходом (POST /api/memory/context/typed) — см.
Загрузку знаний и Поиск и контекст.