lct-hack/backend/app/dialog/factory.py
Ivan Gerasimov 2ceb26f2cd lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.

Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.

Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.

Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.

Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00

44 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Кто играет звонящего: LLM, если есть ключ, иначе заготовки.
Провайдер и модель меняются значением в конфиге, а не кодом. Заготовки —
не запасной костыль, а рабочий режим: занятие идёт и без сети.
"""
import logging
from app.config import get_settings
from app.dialog.caller import Caller, LlmCaller, TemplateCaller
from app.dialog.llm import LlmClient
from app.dialog.tree import TreeCaller, has_table
from app.db.base import get_sessionmaker
log = logging.getLogger(__name__)
def build_caller(scenario_id: str | None = None, sessionmaker=None) -> Caller:
settings = get_settings()
# Офлайн и «нет ключа» — это один и тот же путь: предгенерированная таблица,
# а не локальная модель. Формулировки в ней от облачной модели, а задержка
# нулевая (docs/arch/STACK.md).
if (settings.offline or not settings.llm_api_key) and scenario_id and has_table(scenario_id):
log.info("звонящий по предгенерированной таблице сценария %s", scenario_id)
return TreeCaller(scenario_id)
if not settings.llm_api_key or settings.offline:
reason = "офлайн-режим" if settings.offline else "нет ключа LLM"
log.info("звонящий отвечает заготовками: %s (таблицы нет — make pregen)", reason)
return TemplateCaller()
client = LlmClient(sessionmaker=sessionmaker or _safe_sessionmaker())
log.info("звонящий на модели %s", settings.llm_model_caller)
return LlmCaller(client, model=settings.llm_model_caller)
def _safe_sessionmaker():
"""Кэш в Postgres — приятный бонус, а не условие работы звонящего."""
try:
return get_sessionmaker()
except Exception: # noqa: BLE001
log.warning("кэш LLM выключен: база недоступна")
return None