Урок 5: профиль пользователя — память о человеке¶
Прошлые уроки учили бота помнить диалог. Но есть память и другого
рода: кто этот человек. Его имя, роль, стек, любовь к спискам. Она должна
жить не внутри одного чата, а между сессиями. Разберёмся, как это
устроено в protoprompt.
Концепция: почему профиль — это не пересказ¶
Когда пользователь пишет «Меня зовут Илья, я пишу на python», это два разных вида информации:
- Текущая задача — «вот что я делаю прямо сейчас». Завтра забудется.
- Долговечный факт — «меня зовут Илья». Должен пережить сессию.
Профиль копит второе. Он не пересказывает задачу, а отвечает на вопрос: что про этого человека стоит помнить всегда?
Концепция: инкрементальность¶
Профиль не переписывается с нуля при каждой встрече. Он растёт: новая встреча добавляет дельту изменений к тому, что уже есть. Отсюда три понятия:
- Сигнал (
Signal) — одно входящее сообщение пользователя. - Дельта (
ProfileDelta) — что нового мы узнали из порции сигналов. - Слияние (
merge) — как дельта вливается в существующий профиль.
Практика: читаем проект¶
Откройте examples/tutorials/05_user_profile/main.py. Программа идёт по
шагам: правила → LLM-источник → менеджер → промпт.
Шаг 1. Правила без LLM¶
rules = RuleProfileSource()
delta = await rules.extract(
"u1",
signals("Здравствуйте, пожалуйста, помогите разобраться с задачей"),
)
RuleProfileSource — детерминированные правила: средняя длина сообщений →
verbosity, алфавит → language, маркеры вежливости → formality. Без
единого вызова модели. Вывод:
Шаг 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. Профиль в промпте¶
render превращает профиль в готовый текст секции. А ContextBuilder
вставляет его в system_prompt — модель «вспоминает» пользователя на
каждой встрече:
Шаг 5. Сброс и удаление¶
fresh = await manager.reset("u1") # всё с нуля, version=0
await manager.delete("u1") # профиль удалён
Задание¶
- Замените
InMemoryProfileStore()наSqliteProfileStore("users.db")и запустите дважды. Профиль переживает перезапуск — вторая встреча добавит факты, а не создаст профиль заново. - Во втором ответе
FakeLLMуберите фактrole— убедитесь, что после второгоupdateверсия всё равно выросла, а факты остались прежними. - Поменяйте очередь ответов так, чтобы оба ответа были мусором. Что
произойдёт? (Подсказка:
fallbackна правила.) - Создайте
CompositeProfileSource([RuleProfileSource(), llm_source])и подставьте в менеджер — правила и модель будут работать вместе.
Итог курса¶
Теперь у вас есть полная картина памяти LLM-приложения:
| Урок | Вы научились |
|---|---|
| 1 | Хранить факты и искать по смыслу (store) |
| 2 | Сжимать длинный диалог (session) |
| 3 | Не платить за повторные эмбеддинги (cache) |
| 4 | Различать сигнал и шум в памяти агента (agent) |
| 5 | Помнить человека между сессиями (profile) |
Следующие шаги: общий обзор профиля и API-справочник.