lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация

Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.

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

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

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

Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
This commit is contained in:
Ivan Gerasimov 2026-09-17 23:50:19 +03:00
commit 2ceb26f2cd
19 changed files with 888 additions and 26 deletions

View file

@ -9,6 +9,7 @@
Команды: /подсказка, /факты, /итог, /выход
"""
import asyncio
import sys
from pathlib import Path
@ -16,7 +17,7 @@ ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.config import get_settings # noqa: E402
from app.dialog.caller import TemplateCaller # noqa: E402
from app.dialog.factory import build_caller # noqa: E402
from app.dialog.embeddings import E5Embedder # noqa: E402
from app.dialog.persona import PersonaState # noqa: E402
from app.dialog.slots import SlotMachine # noqa: E402
@ -35,7 +36,7 @@ def summary(slots: SlotMachine) -> None:
print(f" E1 не добыт {fact_id}{hint}")
def main() -> None:
async def main() -> None:
scenario_id = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else None
try:
library = {scenario.id: scenario for scenario in load_library(LIBRARY)}
@ -53,7 +54,7 @@ def main() -> None:
slots = SlotMachine(scenario, embedder)
persona = PersonaState(scenario.persona)
caller = TemplateCaller()
caller = build_caller(scenario.id)
print(f"── {scenario.title} ({scenario.level.value}) ──")
print("Вы — оператор 112. Команды: /подсказка /факты /итог /выход\n")
@ -82,7 +83,7 @@ def main() -> None:
continue
turn = slots.hear(line)
reply = caller.reply(turn, persona, slots)
reply = await caller.reply(turn, persona, slots)
if turn.matched:
print(f" [понято: {', '.join(turn.matched)}]")
print(f"ЗВОНЯЩИЙ [{reply.mood.value}]: {reply.text}")
@ -91,4 +92,4 @@ def main() -> None:
if __name__ == "__main__":
main()
asyncio.run(main())