Подготовка помощника к сопровождению
В серии мы собрали помощника, у которого каждое основание имеет происхождение: собственный Markdown-корпус, смысловые части, BM25, SQLite, бюджет, цитаты и отказ. Теперь зафиксируем, как это состояние сопровождать. Последний урок не публикует сервис и не превращает лабораторию в готовую инфраструктуру. Его результат — ясный порядок изменений и границы будущего выпуска, чтобы новый документ или модель не нарушили уже понятный контракт источников и доступа.
Состояние lesson-16 содержит полную версию всех модулей. Никакие зависимости не установлены, модель не загружена, сервер и сценарий оценки не запускались. Python 3.13.7 выбран как конкретный стабильный учебный профиль; при реальной работе нужны актуальные исправления используемой ветки. Необязательный Ollama зависит от модели, которую выбирает читатель. Архив предоставляет исходники и авторский корпус, а не бинарное доказательство выполненного запуска. Такое разделение сохраняет воспроизводимость и честность описанного результата.
Три вида изменения
Первый вид — содержимое документа. Исправление фразы меняет hash, синхронизация заменяет части, ревизия разрешённого набора меняется. Второй — метаданные: URL, title или visibility. Они сравниваются отдельно, поэтому закрытие источника действует без изменения текста. Третий — алгоритм обработки. Новый parser, размер фрагмента или нормализация термов могут потребовать полной переиндексации, даже если Markdown прежний. Если считать все три случая одним «обновили базу», легко пропустить тот, который не обнаруживается нынешним инкрементальным сравнением.
В частности, изменение chunks.py не входит автоматически в hash документа. Нужно явно выполнить полную sync или добавить processing-version к сохранённым признакам. Версия политики кэша также должна измениться при новом пороге или составе prompt. Эти операции связаны, но не тождественны: кэш хранит производный ответ, индекс — подготовленный текст. Очистка кэша не перестраивает старые фрагменты. А перестроенный индекс не гарантирует, что долгоживущий ответ из внешнего хранилища перестал использовать старую политику.
Порядок локальной подготовки
Сначала проверяется полный манифест и читаются документы. Ошибка загрузчика должна остановить изменение индекса. Затем выполняется согласованная синхронизация. Только после этого сервер начинает принимать вопросы. В текущем app.py этот порядок заметен: команда serve идёт после sync. Если изменить Markdown во время работы страницы, база сама не перечитает его на каждый запрос. Для обновления нужен отдельный контролируемый вызов синхронизации либо рестарт выбранного локального процесса. Не следует обещать мгновенное обновление только потому, что исходник хранится рядом с сервером.
Для большого корпуса полезнее строить новое поколение индекса отдельно и после подготовки менять указатель. Тогда посетители читают прежнюю согласованную версию, пока новая ещё собирается. Наша маленькая SQLite-транзакция уже показывает принцип атомарности данных, но не реализует полноценное переключение поколений или миграции схемы. Если в будущем добавятся embeddings, потребуется связать generation также с моделью эмбеддингов и её размерностью. Простой текстовый hash не гарантирует совместимость двух разных векторных представлений.
Резервная копия и восстановление
Исходный авторитетный набор — Markdown и manifest. Локальную базу можно восстановить из них, поэтому knowledge.sqlite3 не попадает в source control. Если в базе появятся независимые данные оператора, это утверждение перестанет быть верным: их нужно будет сохранять отдельно. Для настоящего резервирования SQLite при активной записи полезен штатный backup API, а не случайное копирование одного файла. Журнал и дополнительные файлы режима хранения могут влиять на согласованность; текущий курс не выполнял такую процедуру и не показывает выдуманный результат восстановления.
Откат должен возвращать согласованную пару корпуса и политики. Если вернуть только старый app.py, а сохранить новый индекс с другим разбиением, программа может работать технически и давать неожиданные цитаты. Если вернуть старый корпус, но оставить внешний кэш, он может повторить новый ответ. Поэтому редакторский выпуск полезно описывать ревизией исходников, версией processing и поколением данных. Такой контракт помогает объяснить проблему конкретно: какой набор источников использовался и какой механизм выбрал ответ.
Публичный выпуск требует другой границы
ThreadingHTTPServer и памятьный кэш предназначены для локального разбора. Перед настоящей публикацией нужно выбрать серверный стек, ограничения параллельности, отмену долгого запроса, мониторинг и безопасную конфигурацию модели. Корпус должен пройти редакционный контроль адресов и прав. Если сервис начинает принимать документы посетителей, существующий загрузчик доверенного каталога уже недостаточен. Входной файл становится недоверенным объектом, а размер, структура, происхождение и хранение потребуют нового контракта и самостоятельной обработки ошибок.
Роль editor остаётся аргументом доверенного локального оператора. Веб-форма всегда reader. Чтобы расширить её до нескольких пользователей, понадобится настоящее подтверждение личности и серверное назначение прав. Курс единого входа объясняет такие механизмы отдельно; это не означает, что два учебных архива автоматически соединяются. Нужен адаптер контекста пользователя, согласованный срок полномочий и стратегия отзыва. Не стоит добавлять параметр role в публичный URL ради быстрого показа редакторского ответа: это разрушит главную границу этой серии.
Наблюдение без лишних данных
Для сопровождения полезны количество документов, число изменённых источников, ревизия, длительность retrieval, размер контекста и тип результата. Не обязательно сохранять полный вопрос и prompt в обычный журнал. Они могут раскрывать пользовательские сведения или закрытые документы. Ошибку модели можно связать с request id и режимом, сохранив детали в ограниченном диагностическом канале. Выбор данных журнала должен помогать объяснить сбой, а не превращать наблюдение в ещё одну незащищённую копию корпуса.
Оценка качества также остаётся многослойной. Контрольный вопрос может потеряться из-за нормализации, количества кандидатов, упаковки, порога или генерации. Увеличение размера модели не является универсальным исправлением. Сначала смотрите, какой источник реально дошёл до контекста. Если он отсутствует, работайте над corpus и retrieval. Если основание есть, но текст исказил условие, разбирайте модельный слой и формат ответа. Такое разделение сохраняет причинность, которую мы строили с первого урока, вместо общего ярлыка «AI плохо ответил».
Полный app.py ниже объединяет итоговую цепочку. При самостоятельной работе она должна сначала построить индекс, затем вернуть извлекающий либо явно отмеченный модельный ответ, а в serve-режиме показать его на локальной странице. Все эффекты остаются ожидаемыми, сценарии не выполнялись. Для дальнейшего развития выберите один измеримый шаг: полноценную русскую нормализацию, новое поколение индекса или подтверждённый пользовательский контекст. Сохраните тот же контрольный набор, чтобы видеть, что именно улучшилось и какое ограничение осталось.
Полный файл app.py из состояния lesson-16:
import json
import sys
from pathlib import Path
from loader import load_documents
from chunks import flatten
from ranking import rank
from storage import sync, read_rows
from context import pack
from answer import extract
from model import synthesize
from web import serve
from policy import cached
def ask(question, role="reader"):
if len(question) > 500:
raise ValueError("Question is too long")
rows = read_rows(role)
candidates = rank(question, rows)
candidates = pack(candidates)
def build():
baseline = extract(question, candidates, strict=True)
return synthesize(question, candidates, baseline)
return cached(question, role, build)
def main():
changes = sync(load_documents(), incremental=True)
if len(sys.argv) > 1 and sys.argv[1] == "serve":
serve(ask)
return
question = sys.argv[1] if len(sys.argv) > 1 else "сохранить старых URL"
role = sys.argv[2] if len(sys.argv) > 2 else "reader"
print(json.dumps(ask(question, role), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Оглавление серии · Исходники всех шагов · Учебные маршруты
Пример остаётся локальным черновиком. При подготовке он не выполнялся; все описанные эффекты являются ожидаемыми, а не результатами запущенных проверок.