Maqola · 2026-08-16 · ~12 daqiqa o‘qiladi · murakkab
AI tushunadi, formula hisoblaydi: LLM'ni narx hisobiga aralashtirmaslik
Mundarija
Humo Logistics botida mijoz yuk narxini ikki yo'l bilan so'raydi: qadam-baqadam forma orqali yoki erkin matn bilan:
«Xitoydan 20 quti telefon aksessuari keladi, har quti 3 kg, jami yarim kub»
Bot bunga aniq narx qaytarishi kerak. Birinchi ko'ringan yechim — matnni GPT'ga berib, «narxini hisoblab ber» deyish. Bu yechim production'da portlaydi, va nega portlashini tushunish LLM bilan jiddiy tizim quradigan har bir backendchi uchun majburiy saboq.
Nega LLM'ga hisoblatish mumkin emas#
| Muammo | Oqibat |
|---|---|
| LLM arifmetikada adashadi | 3 × 20 = 60 o'rniga "taxminan 58" chiqishi mumkin — mijozga noto'g'ri narx |
| Natija qayta tiklanmaydi | Bir xil savolga bugun boshqa, ertaga boshqa javob — audit qilib bo'lmaydi |
| Tariflar modelda emas | Model kompaniyaning joriy narxlarini bilmaydi; prompt'ga tiqilgan tarif esa har o'zgarishda prompt-versiya muammosi |
| Javobgarlik | "Nega bu narx?" degan savolga "AI shunday dedi" — biznes uchun javob emas |
Yechim — mas'uliyatni kesib ajratish:
Erkin matn ──► LLM (STRUKTURA AJRATADI) ──► {"name": "...", "qty": 20, ...}
│
▼
Deterministik tarif xizmati (NARXNI HISOBLAYDI)
│
▼
Bir xil kirish = bir xil narx
AI'ning vazifasi — tabiiy tildan strukturali ma'lumot chiqarish (bunga u zo'r). Narx esa oddiy, testlangan, versiyalangan Python funksiyasidan chiqadi (bunga LLM umuman kerak emas).
1-qadam: chiqish sxemasi — Pydantic#
bot/
├── ai/
│ ├── __init__.py
│ ├── schemas.py ← LLM chiqishi sxemasi (hozir shu yerdamiz)
│ └── extractor.py ← LLM chaqiruvi
├── tariff/
│ ├── __init__.py
│ └── calculator.py ← deterministik hisob
└── handlers/
└── ai_calc.py ← bot handler: ikkalasini bog'laydi
# bot/ai/schemas.py
from pydantic import BaseModel, Field
class CargoItem(BaseModel):
"""LLM'dan qaytishi SHART bo'lgan struktura — boshqa hech narsa."""
name: str = Field(description="Tovar nomi, qisqa")
quantity: int = Field(gt=0, description="Nechta birlik (quti, dona...)")
unit_weight_kg: float | None = Field(
default=None, ge=0, description="Bir birlik og'irligi, kg. Aytilmagan bo'lsa null"
)
total_volume_m3: float | None = Field(
default=None, ge=0, description="Umumiy hajm, m³. Aytilmagan bo'lsa null"
)
class ExtractionResult(BaseModel):
items: list[CargoItem]
# LLM topa olmagan narsani TAXMIN QILMASIN — ochiq aytsin:
missing: list[str] = Field(
default_factory=list,
description="Hisob uchun yetishmayotgan ma'lumotlar ro'yxati (o'zbekcha)",
)
missing maydoni — dizayndagi eng muhim qaror. LLM'ga "bilmasang null qoldir
va missing'ga yoz" degan yo'lni ochamiz; aks holda u taxmin qiladi — va
taxminiy og'irlikdan hisoblangan narx to'g'ridek ko'rinadigan yolg'on bo'ladi.
2-qadam: ajratuvchi — structured output bilan#
# bot/ai/extractor.py
from openai import OpenAI
from .schemas import ExtractionResult
client = OpenAI() # OPENAI_API_KEY muhitdan olinadi
SYSTEM = """Sen logistika botining ajratuvchisisan. Foydalanuvchi yuk tavsifini
o'zbek, rus yoki aralash tilda yozadi. Vazifang — FAQAT strukturani ajratish.
Qoidalar:
- hech narsani hisoblama, narx aytma;
- aytilmagan qiymatni taxmin qilma — null qoldir va missing'ga yoz;
- birliklarni normalizatsiya qil: tonna→kg, litr→m³ emas (hajm faqat m³/kub)."""
def extract_cargo(text: str) -> ExtractionResult:
# parse() javobni to'g'ridan-to'g'ri Pydantic sxemaga majburlaydi:
# model sxemadan tashqari JSON qaytara olmaydi
response = client.chat.completions.parse(
model="gpt-4o-mini", # ajratish uchun kichik model yetarli va arzon
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text},
],
response_format=ExtractionResult,
temperature=0, # ajratish ijod emas — barqarorlik kerak
)
return response.choices[0].message.parsed
Nega temperature=0 va kichik model
Ajratish — "ijodiy" vazifa emas: bitta to'g'ri javob bor. temperature=0
barqarorlikni oshiradi, gpt-4o-mini esa bu turdagi vazifada katta modeldan
deyarli farq qilmaydi, lekin ~20 baravar arzon. Katta modelni katta ishga
saqlang (seriyaning 4-qismida ko'ramiz).
3-qadam: deterministik kalkulyator#
Bu qism LLM'dan mutlaqo bexabar — oddiy, sof funksiya. Humo'dagi kabi zichlikka asoslangan tarif: yengil-hajmli yuk kubda, og'ir-zich yuk kilo'da hisoblanadi:
# bot/tariff/calculator.py
from dataclasses import dataclass
from bot.ai.schemas import CargoItem
@dataclass(frozen=True)
class Tariff:
"""Zichlik oralig'iga bitta narx: [min_density, max_density) kg/m³."""
min_density: float
max_density: float
price_per_kg: float | None # zich yuk uchun
price_per_m3: float | None # hajmli yuk uchun
# Real tizimda bu ro'yxat bazadan/Excel'dan keladi (Humo'da menejerlar
# o'zi yangilaydi) — bu yerda soddalik uchun konstanta
TARIFFS = [
Tariff(0, 100, None, 180.0), # juda hajmli: kub bilan
Tariff(100, 250, 1.10, None), # o'rtacha: kilo bilan
Tariff(250, float("inf"), 0.85, None), # zich: kilo bilan, arzonroq
]
class CalculationError(Exception):
pass
def calculate(items: list[CargoItem]) -> dict:
total_kg = sum((i.unit_weight_kg or 0) * i.quantity for i in items)
total_m3 = sum(i.total_volume_m3 or 0 for i in items)
if total_kg <= 0 or total_m3 <= 0:
# bu holat handler'gacha yetmasligi kerak (missing tekshiruvi bor),
# lekin sof funksiya o'z shartlarini O'ZI himoya qiladi
raise CalculationError("og'irlik va hajm musbat bo'lishi shart")
density = total_kg / total_m3
tariff = next(t for t in TARIFFS if t.min_density <= density < t.max_density)
if tariff.price_per_kg is not None:
price = round(total_kg * tariff.price_per_kg, 2)
basis = f"{total_kg:.0f} kg × ${tariff.price_per_kg}"
else:
price = round(total_m3 * tariff.price_per_m3, 2)
basis = f"{total_m3:.2f} m³ × ${tariff.price_per_m3}"
return {"price_usd": price, "density": round(density, 1), "basis": basis}
E'tibor bering: natijada basis bor — "narx qayerdan chiqdi" izohi. Mijozga
ham, auditga ham javob tayyor: "58 kg × $1.10". LLM'li yechimda bu savolga
javob yo'q edi.
4-qadam: handler — ikkalasini bog'lash#
# bot/handlers/ai_calc.py
from aiogram import types
from bot.ai.extractor import extract_cargo
from bot.tariff.calculator import CalculationError, calculate
async def handle_ai_calc(message: types.Message):
try:
extraction = extract_cargo(message.text)
except Exception:
# AI ishlamasa — mijozni klassik formaga yo'naltiramiz.
# Bot AI'siz ham TO'LIQ ishlashi shart (graceful degradation)
await message.answer(
"Hozir avtomatik tahlil ishlamayapti. /calc bilan forma orqali hisoblang."
)
return
if extraction.missing:
# taxmin qilmaymiz — so'raymiz. AI'ning "bilmadim"i — xato emas, xususiyat
missing = "\n".join(f"— {m}" for m in extraction.missing)
await message.answer(f"Hisob uchun quyidagilar yetishmayapti:\n{missing}")
return
try:
result = calculate(extraction.items)
except CalculationError as exc:
await message.answer(f"Hisoblab bo'lmadi: {exc}")
return
names = ", ".join(i.name for i in extraction.items)
await message.answer(
f"📦 {names}\n"
f"Zichlik: {result['density']} kg/m³\n"
f"Hisob: {result['basis']}\n"
f"<b>Narx: ${result['price_usd']}</b>",
parse_mode="HTML",
)
AI yiqilganda bot yiqilmasin
OpenAI kvotasi tugashi, timeout, 500 — bularning hammasi bo'ladi. Humo'da bunday xatoda adminlarga avtomatik ogohlantirish ketadi (3-qismda ko'ramiz), mijoz esa klassik formaga yo'naltiriladi. AI — qulaylik qatlami; asosiy yo'l usiz ham ishlashi shart.
5-qadam: test — har qatlam alohida#
Ajratishni mock qilamiz, kalkulyatorni esa to'g'ridan-to'g'ri testlaymiz — u sof funksiya, mock umuman kerak emas:
# tests/test_calculator.py
import pytest
from bot.ai.schemas import CargoItem
from bot.tariff.calculator import CalculationError, calculate
def item(qty=20, kg=3.0, m3=0.5):
return CargoItem(name="aksessuar", quantity=qty, unit_weight_kg=kg, total_volume_m3=m3)
def test_dense_cargo_priced_per_kg():
# 60 kg / 0.5 m³ = 120 kg/m³ → o'rta tarif, kilo bilan
result = calculate([item()])
assert result == {"price_usd": 66.0, "density": 120.0, "basis": "60 kg × $1.10"}
def test_bulky_cargo_priced_per_m3():
# 10 kg / 0.5 m³ = 20 kg/m³ → hajmli tarif, kub bilan
result = calculate([item(qty=10, kg=1.0)])
assert result["basis"] == "0.50 m³ × $180.0"
def test_zero_weight_rejected():
with pytest.raises(CalculationError):
calculate([item(kg=0.0, m3=0.0)])
Bir xil kirish har doim bir xil chiqish berishi — shu testlarning o'zi LLM'li yechimda yozib bo'lmaydigan narsa edi.
Xulosa#
- LLM — parser, kalkulyator — formula: chegara shu yerdan o'tadi;
- Structured output (
response_format+ Pydantic) modelning "erkin ijodini" sxema ichiga qamaydi; missingmaydoni taxminni taqiqlaydi — bilmagan narxdan noto'g'ri narx yaxshi emas, so'ralgan savol yaxshi;- AI — qulaylik qatlami: u yiqilganda bot klassik yo'lda ishlashda davom etadi;
- Narxning izohi (
basis) bo'lishi shart — auditga "AI shunday dedi" javob emas.
Keyingi qismda o'sha "klassik yo'l"ning o'zini to'g'ri quramiz: aiogram FSM — ko'p bosqichli dialog-formalar, holatlar mashinasi va "Ortga" tugmasi bilan ishlash.