Asosiy kontentga o‘tish

Maqolalar

Maqola · 2026-08-16 · ~12 daqiqa o‘qiladi · murakkab

AI agent botning ichida: bazaga erkin savol, tool-use va zaxira model

#python #ai #llm #agent #telegram-bot #sql

Mundarija

Seriya davomida AI'ga qat'iy chegara qo'yib keldik: 1-qismda u faqat ajratdi, 3-qismda signalni formula chiqardi. Yakuniy qismda AI'ga eng katta vakolat beriladi — lekin baribir qattiq nazorat ostida: Humo botining admin-agenti kompaniya bazasi bo'yicha istalgan erkin savolga javob beradi.

Farqni his qiling: 3-qismdagi hisobot — oldindan yozilgan so'rovlar. Agent esa oldindan yozib bo'lmaydigan savollarga javob beradi: "iyulda eng katta 5 mijoz kim?", "zichligi 100 dan past yuklar ulushi o'sdimi?" Bularning har biriga alohida handler yozib bo'lmaydi — lekin SQL biladigan model ularga o'zi so'rov tuzadi.

Arxitektura: agent = model + asboblar + halqa#

Admin savoli
    │
    ▼
LLM ──"menga SQL kerak"──► run_sql(READ-ONLY!) ──natija──► LLM ──► javob
 ▲                                                          │
 └────────── kerak bo'lsa yana so'rov (halqa) ◄─────────────┘

Model bazani ko'rmaydi — u faqat asbob chaqirishni so'raydi (tool use). Asbobni bajarish, natijani qaytarish, xavfsizlik — hammasi bizning kodda.

bot/agent/
├── __init__.py
├── tools.py         ← read-only SQL asbobi + sxema tavsifi
├── history.py       ← suhbat tarixini boshqarish
├── agent.py         ← asosiy halqa (+ zaxira model)
└── handlers.py      ← faqat adminlar uchun kirish

1-qadam: eng muhim qaror — read-only SQL#

Agent'ning SQL yozishiga ruxsat beramiz — lekin faqat o'qishga. Bu ishonch masalasi emas, arxitektura masalasi: model xato qilsa ham, prompt-injection bo'lsa ham, ulanishning o'zi yozishga qodir emas.

-- bazada bir marta: faqat SELECT huquqli rol
CREATE USER agent_ro WITH PASSWORD '...';
GRANT CONNECT ON DATABASE humo TO agent_ro;
GRANT USAGE ON SCHEMA public TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;
-- kelajakdagi jadvallar uchun ham:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent_ro;
# bot/agent/tools.py
import psycopg

RO_DSN = "postgresql://agent_ro:...@127.0.0.1/humo"  # .env'dan olinadi

MAX_ROWS = 50  # model 10 ming qatorni baribir o'qiy olmaydi
QUERY_TIMEOUT_MS = 5000

# Modelga beriladigan sxema tavsifi — qo'lda yozilgan, muhim jadvallar bilan.
# information_schema'ni to'liq berish shart emas: ortiqcha jadval — ortiqcha adashuv
SCHEMA_DOC = """
Jadvallar:
- orders(id, client_id, direction, weight_kg, volume_m3, price_usd, created_at)
- clients(id, name, phone, created_at)
- calc_requests(id, chat_id, product, result_price, created_at)
"""


def run_sql(query: str) -> str:
    """Faqat SELECT; natija matn jadval ko'rinishida (model o'qishi uchun)."""
    lowered = query.strip().lower()
    # read-only rol asosiy himoya; bu tekshiruv — tezkor va tushunarli xato uchun
    if not lowered.startswith("select"):
        return "XATO: faqat SELECT so'rovlarga ruxsat berilgan."
    with psycopg.connect(RO_DSN) as conn:
        conn.execute(f"SET statement_timeout = {QUERY_TIMEOUT_MS}")
        rows = conn.execute(query).fetchmany(MAX_ROWS + 1)
        if not rows:
            return "(bo'sh natija)"
        clipped = rows[:MAX_ROWS]
        suffix = f"\n... (faqat birinchi {MAX_ROWS} qator)" if len(rows) > MAX_ROWS else ""
        return "\n".join(" | ".join(str(v) for v in row) for row in clipped) + suffix

«Faqat SELECT» satr tekshiruvi — himoya EMAS

startswith("select") ni chetlab o'tish mumkin (CTE, ko'p so'rov, izoh bilan boshlash...). Haqiqiy himoya — bazadagi read-only rol: yozish huquqining o'zi yo'q. Satr tekshiruvi faqat modelga tez va tushunarli xato qaytarish uchun. Bu mavzuning ildizi: SQL injection'dan himoya.

2-qadam: suhbat tarixi — 400 xatoning manbai#

Agent suhbati oddiy chat emas: unda tool_use (model so'rovi) va tool_result (bizning javob) bloklari juft bo'lib yashaydi. Tarixni shunchaki "oxirgi 20 xabar" deb kessangiz, juftning yarmi kesilib qoladi — va API 400 qaytaradi. Humo'da bu real production xato edi; yechim — kesishdan keyin tarix boshini "tozalash":

# bot/agent/history.py
HISTORY_MAX = 20


def trim(messages: list[dict]) -> list[dict]:
    """Oxirgi HISTORY_MAX xabar, lekin tarix TOZA user xabaridan boshlanishi shart.

    Yetim tool_result (juftining tool_use'i kesilib ketgan) yoki yolg'iz
    assistant xabari bosh bo'lib qolsa — API 400 beradi. Shuning uchun
    boshlanishni birinchi "toza" user turn'gacha suramiz.
    """
    tail = messages[-HISTORY_MAX:]
    for i, msg in enumerate(tail):
        if msg["role"] != "user":
            continue
        content = msg.get("content")
        # user turn ichida tool_result bo'lsa — bu "javob turn", boshlanish emas
        if isinstance(content, list) and any(
            block.get("type") == "tool_result" for block in content
        ):
            continue
        return tail[i:]
    return []  # toza boshlanish topilmadi — yangi suhbat boshlaganimiz ma'qul

3-qadam: agent halqasi va zaxira model#

# bot/agent/agent.py
import anthropic

from .tools import SCHEMA_DOC, run_sql

client = anthropic.Anthropic()  # ANTHROPIC_API_KEY muhitdan

SYSTEM = f"""Sen Humo Logistics'ning tahlilchi-agentisan. Savollarga faqat
bazadagi ma'lumot asosida javob ber; bilmasang — bilmayman de, o'ylab topma.
Javob tili — o'zbekcha, qisqa va raqamli.
{SCHEMA_DOC}"""

TOOLS = [
    {
        "name": "run_sql",
        "description": "PostgreSQL bazasida read-only SELECT bajaradi",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    }
]

MAX_TURNS = 6  # cheksiz halqadan saqlovchi: 6 tool-chaqiruvdan keyin to'xtatamiz


def ask(history: list[dict]) -> tuple[str, list[dict]]:
    """(javob_matni, yangilangan_tarix). Xatoni YUTMAYDI — yuqoriga otadi."""
    messages = list(history)
    for _ in range(MAX_TURNS):
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=1500,
            system=SYSTEM,
            tools=TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            text = "".join(b.text for b in response.content if b.type == "text")
            return text, messages

        # model asbob so'radi — bajaramiz va natijani qaytaramiz
        results = [
            {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": run_sql(block.input["query"]),
            }
            for block in response.content
            if block.type == "tool_use"
        ]
        messages.append({"role": "user", "content": results})
    return "So'rov juda murakkab bo'ldi — savolni bo'lib bering.", messages

Endi Humo'ning o'ziga xos qarori — xatoni yashirmaydigan zaxira:

# bot/agent/handlers.py (asosiy qismi)
@router.message(AdminFilter())
async def agent_message(message: types.Message):
    history = trim(load_history(message.chat.id))
    history.append({"role": "user", "content": message.text})
    try:
        answer, new_history = ask(history)
    except anthropic.APIError as exc:
        # JIMGINA boshqa modelga TUSHMAYMIZ. Admin xatoni ko'radi va
        # o'zi qaror qiladi — chunki zaxira model kuchsizroq bo'lishi mumkin
        await message.answer(
            f"Asosiy model xatosi: {type(exc).__name__}.\n"
            "Zaxira (gpt-4o) bilan davom etaymi? /fallback",
        )
        return
    save_history(message.chat.id, new_history)
    await message.answer(answer)

Jim fallback — yashirin sifat pasayishi

Ko'p tizimlar xatoda avtomatik zaxira modelga o'tadi. Muammo: zaif javob "asosiy model javobi"dek ko'rinadi, hech kim sezmaydi. Humo'dagi qoida: xato ko'rinsin, tanlov foydalanuvchida qolsin. Bu — 1-qismdagi "taxmin qilma, so'ra" printsipining agent darajasidagi ko'rinishi.

4-qadam: kirish nazorati — agent hamma uchun emas#

Agent bazani o'qiy oladi — demak u admin-asbob, mijoz-funksiya emas:

# bot/agent/handlers.py
from aiogram.filters import BaseFilter

ADMIN_IDS = {123456789, 987654321}  # .env'dan


class AdminFilter(BaseFilter):
    async def __call__(self, message: types.Message) -> bool:
        return message.from_user.id in ADMIN_IDS

Mijozlarga AI kerak bo'lsa — 1-qismdagi tor, sxemali kalkulyator bor. Keng vakolatli agent va tor vakolatli ajratuvchi — ikkalasi bitta botda, har biri o'z auditoriyasi va o'z chegarasi bilan yashaydi.

5-qadam: test — tarix kesuvchini sinash#

Agentning eng sinuvchan joyi tarmoq emas — tarix boshqaruvi. U esa sof funksiya:

# tests/test_history.py
from bot.agent.history import trim


def user(text="salom"):
    return {"role": "user", "content": text}


def tool_result_turn():
    return {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1"}]}


def assistant():
    return {"role": "assistant", "content": [{"type": "text", "text": "javob"}]}


def test_orphan_tool_result_dropped_from_front():
    # kesishdan keyin bosh: [tool_result, assistant, user...] — yetimlar tashlanadi
    messages = [tool_result_turn(), assistant(), user("keyingi savol"), assistant()]
    trimmed = trim(messages)
    assert trimmed[0] == user("keyingi savol")


def test_clean_history_untouched():
    messages = [user(), assistant()]
    assert trim(messages) == messages


def test_no_clean_start_resets():
    assert trim([assistant(), tool_result_turn()]) == []

Xulosa — va butun seriyaning qoidasi#

  • Agent = model + bizning qo'limizdagi asboblar + aylanish chegarasi;
  • Bazaga yo'l faqat read-only rol orqali — himoya prompt'da emas, huquqlarda;
  • Tarix kesish — tool_use/tool_result juftlarini hurmat qilib; yetim blok = 400;
  • Zaxira model jim ishga tushmaydi — xato ko'rinadi, qaror odamda;
  • Agent — admin-asbob; mijozga tor, sxemali AI yetadi.

Humo bot amaliyoti seriyasi shu bilan yakun: AI chegaralari (1), dialog mashinasi (2), o'z-o'zidan ishlaydigan hisobotlar (3) va nazoratdagi agent (4) — birgalikda 2 600+ foydalanuvchili jonli botning skeleti.