«Галлюцинация» — это когда LLM уверенно выдаёт то, чего нет или не может быть. В твоей системе это стоит денег напрямую: модель придумает длину марша — ты закажешь лишнюю доску; придумает цену материала — смета «поедет» на десятки тысяч; придумает тип соединения — мастер начнёт пилить не то.
Поэтому на твоих задачах галлюцинации — не «неприятность», а эксплуатационный риск. Они лечатся промптом, архитектурой и разделением ответственности между LLM и обычным кодом.
Если данных для поля нет — ставь null. Не пытайся угадать.
Если заявка бессмысленна — верни {"parse_error": "причина"}.
Что здесь происходит: одна строчка снимает с модели внутреннее давление «я должна что-то ответить». В парсере заявок это убирает 80% выдуманных длин и материалов.
Никогда не давай модели считать цены или нормы «из головы». Передавай прайс и правила явно внутрь промпта:
<prices>
oak_board_40mm: 3500 руб/м²
pine_board_40mm: 1200 руб/м²
stainless_screw_6x80: 15 руб/шт
lacquer_matte_10l: 4200 руб/шт
</prices>
Считай смету только по этим ценам. Если нужной позиции нет в прайсе —
поставь unit_cost_rub: null.
Что здесь происходит: модель теперь «видит» реальный прайс и знает, что всё остальное — запретная зона. Прайс в твоей системе лежит в Postgres — достал SELECT-ом, подставил в промпт.
Современные модели умеют считать, но иногда ошибаются на умножениях, округлениях, процентах. Для смет это неприемлемо. Правильный подход: LLM возвращает список позиций с количествами, арифметику делает Kotlin-код:
val lineItems = parseModelOutput(response) // массив {material, qty, unit}
val total = lineItems.sumOf { item ->
val price = priceRepo.getUnitPrice(item.material)
?: return@sumOf BigDecimal.ZERO
price * item.qty.toBigDecimal()
}
Что здесь происходит: модель указывает «нужно 12 м² дуба», твой код умножает на цену из БД. Никакой BigDecimal не выходит из Kotlin — LLM его даже не видит.
<thinking> перед
ответом. Увидишь в логах, где она «перешла к выдумкам».