Перейти к содержанию

Урок 5: профиль пользователя — память о человеке

Прошлые уроки учили бота помнить диалог. Но есть память и другого рода: кто этот человек. Его имя, роль, стек, любовь к спискам. Она должна жить не внутри одного чата, а между сессиями. Разберёмся, как это устроено в protoprompt.

Концепция: почему профиль — это не пересказ

Когда пользователь пишет «Меня зовут Илья, я пишу на python», это два разных вида информации:

  • Текущая задача — «вот что я делаю прямо сейчас». Завтра забудется.
  • Долговечный факт — «меня зовут Илья». Должен пережить сессию.

Профиль копит второе. Он не пересказывает задачу, а отвечает на вопрос: что про этого человека стоит помнить всегда?

Концепция: инкрементальность

Профиль не переписывается с нуля при каждой встрече. Он растёт: новая встреча добавляет дельту изменений к тому, что уже есть. Отсюда три понятия:

  • Сигнал (Signal) — одно входящее сообщение пользователя.
  • Дельта (ProfileDelta) — что нового мы узнали из порции сигналов.
  • Слияние (merge) — как дельта вливается в существующий профиль.
сигналы → источник → дельта → merge → профиль (version += 1)

Практика: читаем проект

Откройте examples/tutorials/05_user_profile/main.py. Программа идёт по шагам: правила → LLM-источник → менеджер → промпт.

Шаг 1. Правила без LLM

rules = RuleProfileSource()
delta = await rules.extract(
    "u1",
    signals("Здравствуйте, пожалуйста, помогите разобраться с задачей"),
)

RuleProfileSource — детерминированные правила: средняя длина сообщений → verbosity, алфавит → language, маркеры вежливости → formality. Без единого вызова модели. Вывод:

verbosity: balanced
language : ru
formality: formal

Шаг 2. Модель, которая «не умеет» в чистый JSON

messy = "не JSON, извините"
fenced = "```json\n{...профиль с русскими метками...}\n```"
llm2 = FakeLLM(messy, fenced)                 # первый ответ — мусор
source = LLMProfileSource(llm2, language="ru", retries=1)
delta = await source.extract("u1", signals(...))

Источник получил мусор, сделал ретрай и на втором ответе получил markdown-обёрнутый JSON с русскими метками. Дальше работает кодек:

вызовов chat: 2 (первый был мусор -> ретрай)
fact_ops   : [('add', 'imya', 'Илья'), ('add', 'stek', 'python')]
traits     : {'expertise': 'expert'}
preferences: {'format': 'bullets', 'language': 'ru'}

Обратите внимание: «эксперт» → expert, «списки» → bullets, «Имя» → imya. Кодек нормализовал всё в канонический вид.

Зачем кодек

Модели — не JSON-движки. Они оборачивают JSON в , пишут русские метки, приписывают пояснения. Кодек это вычищает: вытаскивает первый JSON-объект, переводит метки в энумы из schema.json, а мусор отбрасывает.

Шаг 3. Менеджер: профиль растёт между сессиями

manager = ProfileManager(store, LLMProfileSource(...))

profile = await manager.update("u1", signals("Я пишу на python и люблю списки"))
# version=1, facts={'imya': 'Илья', 'stek': 'python'}

profile = await manager.update("u1", signals("Теперь я тимлид"))
# version=2, facts={'imya': 'Илья', 'stek': 'python', 'rol': 'тимлид'}

ProfileManager — это весь цикл: загрузил профиль, извлёк дельту, слил, сохранил. Версия растёт, факты копятся, ничего не теряется между сессиями.

Шаг 4. Профиль в промпте

print(render(profile))

render превращает профиль в готовый текст секции. А ContextBuilder вставляет его в system_prompt — модель «вспоминает» пользователя на каждой встрече:

Профиль пользователя:
- imya: Илья
- stek: python
- rol: тимлид
- expertise: expert
...

Шаг 5. Сброс и удаление

fresh = await manager.reset("u1")   # всё с нуля, version=0
await manager.delete("u1")          # профиль удалён

Задание

  1. Замените InMemoryProfileStore() на SqliteProfileStore("users.db") и запустите дважды. Профиль переживает перезапуск — вторая встреча добавит факты, а не создаст профиль заново.
  2. Во втором ответе FakeLLM уберите факт role — убедитесь, что после второго update версия всё равно выросла, а факты остались прежними.
  3. Поменяйте очередь ответов так, чтобы оба ответа были мусором. Что произойдёт? (Подсказка: fallback на правила.)
  4. Создайте CompositeProfileSource([RuleProfileSource(), llm_source]) и подставьте в менеджер — правила и модель будут работать вместе.

Итог курса

Теперь у вас есть полная картина памяти LLM-приложения:

Урок Вы научились
1 Хранить факты и искать по смыслу (store)
2 Сжимать длинный диалог (session)
3 Не платить за повторные эмбеддинги (cache)
4 Различать сигнал и шум в памяти агента (agent)
5 Помнить человека между сессиями (profile)

Следующие шаги: общий обзор профиля и API-справочник.