2026-09-26 18:12:27 +03:00
|
|
|
|
"""Живые проверки локальной LLM: клиент, кэш и ответы звонящего.
|
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
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
Читают `backend/.env.test` с `LLM_PROVIDER=local` и loopback URL. Запускайте
|
|
|
|
|
|
после `make local-llm` отдельной командой `make test-llm-local`. Сеть не нужна;
|
|
|
|
|
|
основной pytest намеренно исключает медленный инференс.
|
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
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
ENV_TEST = Path(__file__).resolve().parents[1] / ".env.test"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
def _read_test_env() -> dict[str, str]:
|
|
|
|
|
|
values: dict[str, str] = {}
|
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
|
|
|
|
for line in ENV_TEST.read_text(encoding="utf-8").splitlines():
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line and not line.startswith("#") and "=" in line:
|
|
|
|
|
|
key, value = line.split("=", 1)
|
2026-09-26 18:12:27 +03:00
|
|
|
|
values[key.strip()] = value.strip()
|
|
|
|
|
|
return values
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
HAS_TEST_ENV = ENV_TEST.is_file()
|
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
|
|
|
|
pytestmark = [
|
|
|
|
|
|
pytest.mark.llm,
|
2026-09-26 18:12:27 +03:00
|
|
|
|
pytest.mark.skipif(not HAS_TEST_ENV, reason="нет backend/.env.test — живые проверки LLM пропущены"),
|
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
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
|
def local_llm_test_environment():
|
|
|
|
|
|
"""Изолировать live-конфиг: collection обычных тестов не меняет env."""
|
|
|
|
|
|
if not HAS_TEST_ENV:
|
|
|
|
|
|
yield
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
values = _read_test_env()
|
|
|
|
|
|
original = {key: os.environ.get(key) for key in values}
|
|
|
|
|
|
for key, value in values.items():
|
|
|
|
|
|
os.environ[key] = value
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
from app.dialog.llm import is_loopback_url
|
|
|
|
|
|
|
|
|
|
|
|
get_settings.cache_clear()
|
|
|
|
|
|
try:
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
if (
|
|
|
|
|
|
settings.llm_provider != "local"
|
|
|
|
|
|
or not is_loopback_url(settings.llm_base_url)
|
|
|
|
|
|
or settings.llm_api_key
|
|
|
|
|
|
):
|
|
|
|
|
|
raise pytest.UsageError(
|
|
|
|
|
|
"test-llm-local требует LLM_PROVIDER=local, loopback URL и пустой LLM_API_KEY"
|
|
|
|
|
|
)
|
|
|
|
|
|
yield
|
|
|
|
|
|
finally:
|
|
|
|
|
|
for key, value in original.items():
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
os.environ.pop(key, None)
|
|
|
|
|
|
else:
|
|
|
|
|
|
os.environ[key] = value
|
|
|
|
|
|
get_settings.cache_clear()
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
async def client():
|
|
|
|
|
|
from app.dialog.llm import LlmClient
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# Локальная модель может быть медленной на слабом CPU; в занятии таймаут
|
|
|
|
|
|
# короче, и при отказе используются проверенные заготовки.
|
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
|
|
|
|
llm = LlmClient(timeout=180)
|
|
|
|
|
|
yield llm
|
|
|
|
|
|
await llm.aclose()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
async def database_client(postgres_access):
|
|
|
|
|
|
"""Подключить проверку кэша к PostgreSQL, когда тестовый стенд её дал.
|
|
|
|
|
|
|
|
|
|
|
|
Живые inference smoke должны работать и без БД, но проверка устойчивого
|
|
|
|
|
|
кэша имеет смысл только в отдельной DB-интеграционной цели.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
|
|
from app.db.base import get_sessionmaker
|
|
|
|
|
|
from app.db.models import LlmCache
|
|
|
|
|
|
from app.dialog.llm import LlmClient
|
|
|
|
|
|
|
|
|
|
|
|
sessionmaker = get_sessionmaker()
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with sessionmaker() as db:
|
|
|
|
|
|
await db.scalar(select(LlmCache.context_hash).limit(1))
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001 — кэш необязателен для обычного inference smoke
|
|
|
|
|
|
if os.environ.get("DATABASE_URL"):
|
|
|
|
|
|
raise
|
|
|
|
|
|
pytest.skip(f"таблица кэша LLM недоступна: {type(exc).__name__}")
|
|
|
|
|
|
|
|
|
|
|
|
db_client = LlmClient(sessionmaker=sessionmaker, timeout=180)
|
|
|
|
|
|
yield db_client
|
|
|
|
|
|
await db_client.aclose()
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
async def test_provider_answers(client):
|
|
|
|
|
|
from app.dialog.llm import LlmRequest, LlmUnavailable
|
|
|
|
|
|
|
|
|
|
|
|
request = LlmRequest(
|
|
|
|
|
|
messages=[{"role": "user", "content": "Ответь одним словом: работает"}],
|
|
|
|
|
|
model=os.environ["LLM_MODEL_CALLER"],
|
|
|
|
|
|
temperature=0,
|
|
|
|
|
|
max_tokens=400,
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
text = await client.complete(request, use_cache=False)
|
|
|
|
|
|
except LlmUnavailable as exc:
|
2026-09-26 18:12:27 +03:00
|
|
|
|
pytest.fail(f"локальная модель недоступна: {exc}")
|
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
|
|
|
|
assert text, "пустой ответ модели"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_caller_speaks_only_revealed_facts(client):
|
|
|
|
|
|
"""Главное свойство продукта не должно зависеть от послушности модели:
|
|
|
|
|
|
в промпт попадают только раскрытые факты."""
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
from app.dialog.caller import LlmCaller
|
|
|
|
|
|
from app.dialog.llm import LlmUnavailable
|
|
|
|
|
|
from app.dialog.persona import PersonaState
|
|
|
|
|
|
from app.dialog.slots import SlotMachine
|
|
|
|
|
|
from tests.test_slots import SCENARIO, StemEmbedder
|
|
|
|
|
|
|
|
|
|
|
|
slots = SlotMachine(SCENARIO, StemEmbedder(), floor=0.5)
|
|
|
|
|
|
persona = PersonaState(SCENARIO.persona)
|
|
|
|
|
|
caller = LlmCaller(client, model=os.environ["LLM_MODEL_CALLER"])
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Оператор спрашивает не об адресе — адрес прозвучать не должен.
|
|
|
|
|
|
reply = await caller.reply(slots.hear("Что у вас случилось?"), persona, slots)
|
|
|
|
|
|
except LlmUnavailable as exc:
|
2026-09-26 18:12:27 +03:00
|
|
|
|
pytest.fail(f"локальная модель недоступна: {exc}")
|
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
|
|
|
|
|
|
|
|
|
|
assert caller.fallbacks == 0, "ответила не модель, а заготовка"
|
|
|
|
|
|
assert "Ленина" not in reply.text, f"звонящий выдал адрес без вопроса: «{reply.text}»"
|
|
|
|
|
|
assert len(reply.text) < 300, "звонящий пишет объяснительную вместо крика"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
async def test_same_context_comes_from_cache(database_client):
|
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
|
|
|
|
"""Кэш по хешу контекста: та же реплика на том же месте занятия звучит
|
|
|
|
|
|
одинаково у каждой группы и не стоит второго запроса."""
|
|
|
|
|
|
from app.dialog.llm import LlmRequest, LlmUnavailable
|
|
|
|
|
|
|
|
|
|
|
|
request = LlmRequest(
|
|
|
|
|
|
messages=[{"role": "user", "content": "Назови одно слово: пожар"}],
|
|
|
|
|
|
model=os.environ["LLM_MODEL_CALLER"],
|
|
|
|
|
|
temperature=0,
|
|
|
|
|
|
max_tokens=400,
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
2026-09-26 18:12:27 +03:00
|
|
|
|
first = await database_client.complete(request)
|
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
|
|
|
|
except LlmUnavailable as exc:
|
2026-09-26 18:12:27 +03:00
|
|
|
|
pytest.fail(f"локальная модель недоступна: {exc}")
|
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
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
started = time.monotonic()
|
2026-09-26 18:12:27 +03:00
|
|
|
|
second = await database_client.complete(request)
|
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
|
|
|
|
elapsed = time.monotonic() - started
|
|
|
|
|
|
|
|
|
|
|
|
assert second == first, "кэш вернул другой ответ"
|
|
|
|
|
|
assert elapsed < 1.0, f"второй запрос занял {elapsed:.2f} с — кэш не сработал"
|