Головна / Статті

Місцевий агент підтримки з SmolLM3: режим мислення, інструменти та захисні механізми

Робота з моделлю 3B на ноутбуці як агента підтримки: коли використовувати think чи no_think, як працює виклик інструментів та механізми, що запобігають викликам інструментів-замінників.

Видавець: REACTAPP.TOP9 хв читання1979 слів

Джерела: **A Local Support Agent with SmolLM3: Think Mode, Tools, and When Not to Reason**

Чи може модель з 3 мільярдами параметрів обробляти запити підтримки без допомоги моделі з 70 мільярдами параметрів? Для 90% звичайних запитів — так, але лише за умови, що ви перестанете довіряти власним викликам інструментів моделі. Тут SmolLM3 працює локально як невеликий агент підтримки та показує, де вона поводиться належним чином, а де код навколо неї мусить бути суворішим, ніж сама модель.

Служба підтримки клієнтів — це не найкраще місце для використання загального чат-бота. Звичайні запити вимагають пошуку інформації, присвоєння мітки та короткої відповіді згідно з правилами, тоді як меншість запитів пов’язана з подвійними оплатами, загубленими замовленнями чи незадоволеними клієнтами. Щоб ефективно обслуговувати обидва типи запитів за допомогою однієї моделі, її потрібно запускати двома різними способами.

SmolLM3-3B підходить для цього з трьох причин, які приховує кількість параметрів:

  • У нього вбудовано два режими міркування. Додавання /no_think змушує його відповідати негайно, а /think змушує його спочатку створити прихований запис міркувань.
  • Вбудоване викликання інструментів: інструменти, описані у форматах JSON, потрапляють у шаблон чату, а модель відповідає блоком <tool_call>, коли потребує його використання.
  • Це достатньо компактно для ноутбука. Усе тут працювало на MacBook із 24 ГБ об’єднаної пам’яті: модель повної точності через Transformers на MPS та 8-бітна версія MLX для циклу агента.
  • Навчання не відбувається, і ваги моделі ніколи не змінюються. Робота полягає у розділенні процесу мислення та дії та у зробленні навколишнього коду суворішим за саму модель.

    Налаштування

    Два скрипти використовують два середовища виконання, але одну родину моделей. Для порівняння у режимі мислення відбір даних здійснюється згідно з інструкціями моделі з параметрами temperature=0.6 та top_p=0.95. Агент працює ефективніше при temperature=0.3, тож JSON-дані інструментів залишаються простими.

    На цій машині подорож даних 8-бітного агента зайняла близько чотирьох секунд. Повна модель об’ємом 3 мільярди елементів у рамках Transformers генерувала приблизно 10 токенів на секунду, що підходить для подальшого читання журналів подій, але занадто повільно для клієнта, який спостерігає за вікном чату. Transformers зручний для вивчення шаблонів. Коли на іншому кінці знаходиться реальна людина, слід перейти на MLX або llama.cpp.

    Рішення про використання — це рішення продукту

    SmolLM3 за замовчуванням виконує обробку даних. Існує два еквівалентних способи вимкнути цю функцію. Один з них — системне повідомлення:

    messages = [
        {"role": "system", "content": "/no_think"},
        {"role": "user", "content": prompt},
    ]
    

    Інший — це прапорець під час відтворення шаблону:

    tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=False,  # the /no_think flag in the system prompt wins if both are set
    )
    

    Шаблон робить більше, ніж просто змінює значення булевої величини. Він записує режим міркувань у заголовок системи, а коли обирається /no_think, додатково вставляє порожній блок міркувань заздалегідь, щоб генерація продовжувалася безпосередньо з відповіддю:

    <|im_start|>assistant
    <think>
    
    </think>
    

    Ось чому звичайний виклик generate() у режимі без обробки все одно може повернути в тексті <think></think>. Для відповіді клієнту його потрібно видалити:

    final = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
    

    У обох режимах використовувався той самий запит на підтримку: клієнту стягують гроші двічі за одне замовлення, тож які три конкретні кроки має здійснити служба підтримки?

    Два годинники

    Пропускна здатність у обох режимах була приблизно однаковою, але обсяг роботи — ні. Режим «think» витратив більшу частину своїх 939 токенів усередині блоку «think». Клієнт ніколи не читає цей запис, але все одно чекає під час його обробки.

    Дві схеми дій

    /no_think безпосередньо спрямовувався до стандартного переліку дій: підтвердити замовлення, зв’язатися з продавцем, повернути гроші. Це працює як первинний проект, але він трохи не підходить для власної команди підтримки продавця, оскільки «зв’язатися з продавцем» — це порада, яку дає банк, а не те, що внутрішній агент каже своєму клієнту.

    /think присвятив приблизно одну сторінку суперечкам із самим собою щодо видачі грошей назад чи надання кредиту, ескалації ситуації та необхідності перевірки доставки, а потім запропонував більш чіткий план:

    1. Вибачитися та зібрати номер замовлення, дату та спосіб оплати.
    2. Знайти дубльовану транзакцію та скасувати її.
    3. Пояснити клієнту, що сталося, та підтвердити, що гроші будуть повернені у вигляді видачі грошей назад чи магазинного кредиту.

    Розподіл завдань щодо доставки випливає з цього:

    • /no_think для роботи з великим обсягом: класифікація намірів, запити типу „Де моє замовлення“, готові відповіді за правилами та виклики інструментів.
    • /think у випадках, коли неправильний крок може мати серйозні наслідки: видача грошей назад, стягнення коштів, проблеми з безпекою та все, що потребує перевірки за шаблоном, схожим на людську роботу, перед тим, як надати відповідь.

    Дані, надані самою Hugging Face, також вказують на те саме щодо використання інструментів. У тесті BFCL SmolLM3 показує результат 92,3 без розширеного мислення та 88,8 з ним. Розширене мислення не є безкоштовним, і для такого типу завдань воно навіть не покращило результатів. Наведений нижче агент вимушено встановлює enable_thinking=False, оскільки пошук замовлення має бути викликом функції, а не есею.

    Один інструмент та цикл з двох кроків

    SmolLM3 очікує інструменти у вигляді об’єктів JSON-schema, які передаються через xml_tools. Шаблон копіює їх у блок інструментів та вказує моделі відповідати у цій структурі:

    <tool_call>
    {"name": <function-name>, "arguments": <args-json-object>}
    </tool_call>
    

    Інструмент підтримки навмисно має обмежений функціонал:

    TOOLS = [
        {
            "name": "lookup_order_status",
            "description": (
                "Look up the current status, estimated delivery date, and carrier "
                "for a specific customer order. Call this when the customer mentions "
                "an order number or asks where their order is."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "The order ID, usually in the format ORD-XXXXXX.",
                    }
                },
                "required": ["order_id"],
            },
        }
    ]
    

    За нею знаходиться фальшива база даних. У реальних умовах це міг би бути SQL-запит або виклик сервісу замовлень.

    ORDERS = {
        "ORD-4821": {"status": "shipped", "eta": "June 18, 2026", "carrier": "DHL"},
        "ORD-3307": {"status": "processing", "eta": "June 20, 2026", "carrier": None},
        "ORD-1190": {"status": "delivered", "eta": None, "carrier": "FedEx"},
    }
    

    Цей цикл є всім продуктом. Модель отримує повідомлення від клієнта, створює першу відповідь, і якщо це вимога до інструменту, код виконує відповідну функцію, передає результат назад та просить другу відповідь:

    def respond_with_tools(user_message: str) -> str:
        messages = [{"role": "user", "content": user_message}]
        turn1 = generate_reply(messages)
        name, args = parse_tool_call(turn1)
        if name == "lookup_order_status":
            result = lookup_order_status(**args)
            messages += [
                {"role": "assistant", "content": turn1},
                {"role": "tool", "content": json.dumps(result), "name": name},
            ]
            return generate_reply(messages)
        return turn1
    

    Щасливий сценарій

    Через тиждень клієнт запитує, де знаходиться замовлення ORD-4821. Модель не відповідає. Вона викликає інструмент:

    <tool_call>
    {"name": "lookup_order_status", "arguments": {"order_id": "ORD-4821"}}
    </tool_call>
    

    Функція повертає інформацію про відправку 18 червня 2026 року через DHL. Під час другої спроби, загалом за 4,2 секунди, отримується відповідь про те, що замовлення вже відправлено та має прибути 18 червня через DHL.

    Для ORD-3307 статус — обробка без вказаного перевізника, а приблизна дата доставки — 20 червня. Модель зазначила, що перевізник недоступний, замість того щоб вигадати його, що саме і потрібно від простої моделі: читати JSON-дані інструменту та не додавати зайвих елементів.

    Для ORD-9999 виклик інструменту був правильним, і функція повернула значення not_found. У відповіді було вибачення та прохання до клієнта перевірити номер замовлення. Структурована відповідь про відсутність замовлення від функції була достатньою сама по собі, без додаткових попереджень.

    Де помирає наївний цикл

    Третє запитання було призначене як негативний контроль: клієнт просто хоче змінити адресу електронної пошти. Жодних інструментів для цього не існує, тож хороший агент це каже та припиняє роботу. Наївний алгоритм зробив щось інше:

    1. Він почав правильно, сказавши, що не може змінити особисту інформацію, і що клієнт повинен звернутися до служби підтримки.
    2. Потім він продовжив роботу та надіслав запит до інструменту з місцем для підстановки:
    <tool_call>
    {"name": "lookup_order_status", "arguments": {"order_id": "your_order_id"}}
    </tool_call>
    
    1. Парсер виконав усе, що було обгорнуте в <tool_call>. Він знайшов у фальшивій базі даних your_order_id, отримав значення not_found та передав його назад моделі.
    2. Під час другої спроби модель показала клієнту схему інструменту та попросила його замінити your_order_id на справжній номер замовлення.

    Чотири речі зламалися одночасно, чергуючись одна за одною. Частина, пов’язана з моделлю, є найменш цікавою. Невеликі моделі уникають ризиків, і це нормально. Якщо ваш цикл полягає у використанні регулярних виразів для пошуку <tool_call>, а потім у виклику функції з будь-якими наявними аргументами, зрештою ви запустите lookup_order_status("your_order_id") у продакшені, але цього не станеться під час демонстрації.

    Захисні механізми, які не потребують доробки

    Два прості правила вирішують більшість проблем без зміни параметрів ваг.

    По-перше, виконуйте виклик інструменту лише тоді, коли весь етап обробки складається саме з цього виклику. Абзац, що слідує за викликом, вважається звичайним текстом, і його потрібно видалити перед тим, як клієнт його побачить.

    По-друге, перевіряйте аргументи перед тим, як вони потраплять у базу даних. ID замовлень тут мають формат ORD- разом із цифрами, тому your_order_id ніколи не пройде перевірку.

    ORDER_RE = re.compile(r"^ORD-\d{4,}quot;)
    PURE_TOOL_RE = re.compile(r"^\s*<tool_call>(.*?)</tool_call>\s*quot;, re.DOTALL)
    
    def parse_executable_tool_call(output: str):
        cleaned = re.sub(r"<think>.*?</think>", "", output, flags=re.DOTALL).strip()
        match = PURE_TOOL_RE.match(cleaned)
        if not match:
            return None
        payload = json.loads(match.group(1).strip())
        order_id = str(payload.get("arguments", {}).get("order_id", "")).strip()
        if not ORDER_RE.match(order_id):
            return None
        return payload
    

    Також потрібно уточнити опис інструменту, зазначивши, що його слід викликати лише тоді, коли клієнт вже надав конкретний ідентифікатор замовлення. Після цієї зміни електронний запит під час захищеного виконання зовсім не викликав інструмент, що зайняло 3,5 секунди проти 15,4 у пошкодженій версії з двома кроками. У відповіді просили вибачення, зазначили, що не можуть обробляти зміни на рахунку, та направили на службу підтримки клієнтів. Це все ще загальні формулювання, але без інструменту update_email все одно не мало б існувати жодного автоматичного шляху самообслуговування. Принаймні припинилося вдачею імітування пошуку замовлення.

    Пошуки за „щасливим сценарієм“ продовжували працювати, і замовлення ORD-9999 все ще вважалося загубленим. Модель не стала розумнішою. Вважайте це скоріше ременем безпеки, ніж трансплантацією.

    Що це не доводить

    Це лише демонстрація, а справжній копілот підтримки потребує набагато більшого, ніж одна функція та словник з трьома замовленнями. Кілька тверджень варто відхилити:

    • Це не є плануванням з використанням багатофункціональних інструментів. Один інструмент використовується для пошуку, а приблизно два інструменти вже змушені створювати ідентифікатори замовлень, щоб відповідати схемам, яких вони не розуміють повністю.
    • База даних є фальшивою, тому автентифікація, персональні дані та можливість „щоб цей користувач бачив це замовлення“ ніколи не були проблемою моделі.
    • Протягом усього процесу було активовано вибірку даних, тому повторна обробка запиту електронної пошти не поверне тих самих слів. Те, що повторювалося з кожної спроби, — це сама невдача, а сам інструмент повертав значення з місцем для підстановки. Точні формулювання змінюються, тож не можна на них покладатися.
    • Нічого не порівнювалося з хмарною моделлю чи не оброблялося за допомогою справжнього експорту замовлень, тому ці цифри стосуються лише однієї машини, а не є показником ефективності.

    Це дає вужче твердження, ніж «малі моделі можуть надавати підтримку»: модель об’ємом 3 мільярди може викликати добре описаний інструмент, підсумувати результат та припинити свою роботу, за умови, що сам виклик інструменту розглядається як щось, що потребує перевірки дозволів, а не як щось, у що можна довіряти лише через те, що його створила модель.

    Запуск

    У каталозі проекту з активним віртуальним середовищем запустіть три скрипти по черзі: порівняння з використанням механізму think та без нього, звичайного агента та захищеного агента.

    python llm-learning/smollm3-local-support-agent/code/think_vs_nothink.py
    python llm-learning/smollm3-local-support-agent/code/support_agent.py
    python llm-learning/smollm3-local-support-agent/code/support_agent_guarded.py
    

    Ці скрипти створюють транскрипції, які можна прочитати пізніше для порівняння виконань.