Skip to content

Хранилище

Где живёт чат как источник записи — надёжный разговор, который рендерит UI и который агент вычитывает обратно.

Postgres, за agent/chat

Чат — это транзакционные, реляционные данные: Chat → Message, привязанные к Agent / Team. Им нужна строгая консистентность и чистая пагинация для UI → Postgres — правильный дом.

prisma
model Chat {
  id        String    @id            // chat-{uuid}
  agentId   String
  channel   String                   // widget | telegram | api …
  messages  Message[]
  createdAt DateTime  @default(now())
  @@index([agentId, createdAt])
}

model Message {
  id        String   @id             // msg-{uuid}
  chatId    String
  role      String                   // user | assistant | tool | system
  content   Json                     // text + tool refs + attachments
  tokens    Int?
  createdAt DateTime @default(now())
  chat      Chat     @relation(fields: [chatId], references: [id])
  @@index([chatId, createdAt])
}

Slice agent/chat владеет этим за gateway, поэтому физическое хранилище заменяемо без правок вызывающего кода.

Чтение истории: страница, а не весь тред

Все три чтения истории пагинированы курсором, и попросить всё нельзя никак:

GET /agents/:id/chat/messages?limit=&cursor=
GET /agents/:id/chat/threads?limit=&cursor=
GET /agents/:id/chat/threads/:threadId/messages?limit=&cursor=
  • limit по умолчанию 50 и ограничен сверху 200.
  • Ответ — { items, nextCursor, hasMore }, items идут от старых к новым. hasMore вынесен отдельным полем намеренно: вывод из items.length === limit ошибается ровно один раз на тред — на последней странице, которая оказалась полной.
  • cursor непрозрачен и идёт назад, в более старые строки. Курсор, который этот api не выдавал, отвергается, а не игнорируется молча.

Это не украшение. Агент с автозапуском пишет в свой тред круглосуточно, поэтому тред растёт без всякого участия человека.

Начать заново — черта в треде, а не второй тред

Разговору некуда было закончиться: что-то обсудили, через неделю пришли с другим — а модель тащила за собой весь первый предмет. «Начать заново» проводит в том же треде строку-границу. От неё модель не получает ничего из того, что было раньше, а человек сохраняет все прежние сообщения и может к ним прокрутить.

Тред по-прежнему ровно один на (агента, канал, человека) — так говорит ключ, — и новой сущности не появилось: граница это тот же маркер, который ставит компакция, заменяя старые ходы пересказом, только поставленный сознательно, а не по мере роста треда. Нажатие кнопки дважды не рисует вторую черту.

Чего сброс не касается — это память агента. Заметки, которые он записал о человеке, лежат не в треде, поэтому после сброса он их по-прежнему знает. Выглядит как поломка, но ею не является — поэтому экран говорит об этом прямо, а не оставляет человека выяснять это самому.

Что не сохраняется

  • Ход, завершившийся ошибкой, не сохраняет ничего. Ход, который остановил пользователь, — другое дело: уже доставленный текст сохраняется с пометкой «остановлено», поэтому тред после перезагрузки совпадает с тем, что было на экране. См. Что завершает ход.

Объём — Postgres справляется

«Значительный объём» — это нормальный случай для таблицы Message; рычаги:

  • Партиционируй Message (по времени, или по agentId/teamId), чтобы горячие диапазоны оставались небольшими.
  • Индексируй под реальные запросы (chatId, createdAt); избегай широких сканов.
  • Архивируй холодные разговоры в объектное хранилище (строка хранит указатель); живая таблица остаётся компактной.
  • Сырые блобы трейсов/промптов сюда НЕ кладутся — они уходят в Debug и трейсинг. Именно вынос телеметрии держит эту таблицу быстрой.

Старт с общего → разделяй, когда заслужит

  1. Сейчас: чаты живут в основном Postgres (одна БД, проще всего).
  2. На масштабе: перенеси данные agent/chat на выделенный инстанс Postgres, когда write-IO чатов начнёт конкурировать с транзакционной нагрузкой. Поскольку это за gateway, это изменение конфига, а не переписывание — ровно тот путь «сначала в api, отдельная база потом».

См. также

  • Debug и трейсинг — почему промпты/трейсы держатся вне этой таблицы.
  • Память — поисковый индекс, производный от этих сообщений.
  • Схема БДChat / Message в более широкой модели.