Местный агент поддержки с SmolLM3: режим размышлений, инструменты и ограничения
Запуск модели объемом 3 миллиарда параметров на ноутбуке в качестве агента поддержки: когда использовать функции think или no_think, как работает вызов инструментов, и механизмы защиты, препятствующие вызову инструментов-заменителей.
Источники: **A Local Support Agent with SmolLM3: Think Mode, Tools, and When Not to Reason**
Может ли модель с 3 миллиардами параметров обрабатывать заявки на поддержку без модели с 70 миллиардами параметров, работающей в качестве резерва? Для 90% простых заявок — да, но только при условии отказа от доверия к собственным вызовам инструментов модели. Здесь SmolLM3 работает локально в качестве небольшого агента поддержки и показывает, в каких случаях она правильно себя ведет, а где код вокруг нее должен быть более строгим, чем сама модель.
Служба поддержки клиентов — не лучшее место для использования обычного чат-бота. Обычные заявки требуют поиска информации, присвоения метки и краткого ответа в соответствии с правилами, тогда как небольшая их часть связана с двойными списаниями, потерянными заказами или недовольными клиентами. Чтобы хорошо обслуживать оба типа заявок с помощью одной модели, необходимо использовать ее двумя разными способами.
SmolLM3-3B подходит для этой задачи по трём причинам, которые скрываются за количеством параметров:
- В её состав входят два режима обработки информации. Добавление параметра
/no_thinkзаставляет её отвечать сразу, а параметр/thinkзаставляет её сначала создать скрытый отчет о процессе рассуждений.
<tool_call>, когда требуется его использование.Обучение не проводится, и веса модели никогда не изменяются. Основная задача заключается в разделении процессов мышления и действия, а также в сделании окружающего кода более строгим, чем сама модель.
Настройка
Два скрипта используют два фреймворка выполнения, но одну семью моделей. При сравнении в режиме мышления выбор вариантов происходит согласно параметрам модели: 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 токенов внутри блока для обработки мыслей. Клиент никогда не читает этот отчет, но всё равно ждёт его завершения.
Два набора инструкций
/no_think напрямую приводил к стандартному списку действий: подтвердить заказ, связаться с продавцом, вернуть сумму. Этот подход подходит в качестве первой версии решения, но он не совсем подходит для собственной команды поддержки продавца, поскольку фраза «связаться с продавцом» — это совет, который даёт банк, а не то, что говорит сотрудник компании своему собственному клиенту.
/think потратил примерно одну страницу на споры с самим собой относительно возврата денег или предоставления кредита, необходимости усиления мер реагирования и проверки статуса доставки, после чего пришел к более строгому плану:
- Попросить извинения и узнать номер заказа, дату и способ оплаты.
- Найти дублирующуюся транзакцию и аннулировать ее.
- Объяснить клиенту, в чем произошла ошибка, и подтвердить, что деньги будут возвращены в виде возврата или кредита в магазине.
Разделение задач на те, которые можно обработать без дополнительных размышлений, и те, которые требуют их, выглядит следующим образом:
/no_think— для работы с большим объемом задач: классификация намерений, запросы типа «где мой заказ», готовые ответы по правилам и вызовы инструментов./think— там, где неправильный шаг может привести к серьезным последствиям: возвраты денег, статус-возвраты платежей, проблемы с безопасностью, а также любые ситуации, требующие проверки по списку шагов в стиле работы человека перед ответом.
Согласно данным самой Hugging Face, ситуация с использованием инструментов аналогична. В тесте BFCL SmolLM3 показывает результат 92,3 без расширенного мышления и 88,8 с ним. Расширенное мышление имеет свою стоимость, и для такого типа задач оно даже не улучшало качество результата. Приведённый ниже агент задаёт параметр enable_thinking=False, поскольку поиск заказа должен представлять собой вызов функции, а не развернутый отчёт.
Один инструмент и цикл из двух шагов
SmolLM3 ожидает, что инструменты будут предоставлены в виде объектов JSON-schema через параметр xml_tools. Шаблон копирует их в блок 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>
Функция возвращает информацию о том, что заказ отправлен DHL 18 июня 2026 года. При второй попытке, всего за 4,2 секунды, поступает ответ о том, что заказ отправлен и должен прибыть 18 июня через DHL.
Для ORD-3307 статус — обработка, перевозчик не указан, а срок доставки оценивается на 20 июня. Модель указала, что перевозчик недоступен, вместо того чтобы придумать его сама, что именно нужно от небольшой модели: читать JSON-данные инструмента и не дополнять их лишней информацией.
Для ORD-9999 вызов инструмента был правильным, и функция вернула значение not_found. В ответе просили прощения и попросили клиента проверить номер заказа. Само по себе структурированное сообщение об отсутствии данных было достаточным, без дополнительных указаний.
Где погибает наивный цикл
Третий запрос предназначался в качестве отрицательного контроля: клиент просто хотел изменить адрес электронной почты. Поскольку такого инструмента для работы с электронной почтой не существует, хороший оператор сообщает об этом и прекращает действия. Наивный алгоритм же поступил иначе:
- Он начал правильно, сказав, что нельзя изменять личную информацию и что клиенту следует обратиться в службу поддержки.
- Затем он продолжил работу и отправил запрос к инструменту с местоимением-заменителем:
<tool_call>
{"name": "lookup_order_status", "arguments": {"order_id": "your_order_id"}}
</tool_call>
- Парсер выполнил любой код, заключённый в
<tool_call>. Он искал в фиктивной базе данных значениеyour_order_id, получил ответnot_foundи передал его обратно модели. - На втором этапе модель показала клиенту схему инструмента и попросила заменить
your_order_idна реальный номер заказа.
Четыре проблемы возникли одновременно и накопились друг на друге. Часть, связанная с моделью, — самая менее значимая. Небольшие модели избегают рисков, и это нормально. Если ваш цикл представляет собой регулярное выражение для поиска <tool_call>, за которым следует вызов функции с любыми передаваемыми аргументами, в реальных условиях работа функции lookup_order_status("your_order_id") всё равно будет выполняться, в отличие от демо-режима.
Механизмы защиты, не требующие доработки
Два простых правила решают большинство проблем без изменения параметров весов.
Во-первых, выполняйте вызов инструмента только тогда, когда весь этап состоит исключительно из этого вызова. Абзац, следующий за вызовом, считается обычным текстом, и XML следует удалить до того, как его увидит клиент.
Во-вторых, проверяйте аргументы перед тем, как они попадут в базу данных. 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 по-прежнему отмечался как пропавший. Модель не стала ни на йоту умнее. Считайте это скорее ремнем безопасности, чем полноценной заменой.
Чего это не доказывает
Это всего лишь демонстрация, а настоящий помощник поддержки требует гораздо большего, чем одна функция и словарь из трех заказов. Нескольким утверждениям следует противостоять:
- Это не планирование с использованием многofункциональных инструментов. Один инструмент предназначен для поиска информации, а примерно два инструмента используются для генерации идентификаторов заказов с целью соответствия схемам, которые они не полностью понимают.
- База данных является фиктивной, поэтому вопросы аутентификации, персональных данных и возможности отображения заказа конкретным пользователем никогда не были проблемой модели.
- Всюду использовался метод выборки, поэтому повторная обработка запроса через электронную почту не приведёт к получению одинаковых результатов. То, что повторялось с каждой попытки, — это сама ошибка, заключающаяся в вызове инструмента с местоимением-заменителем. Точные формулировки меняются, поэтому на них нельзя рассчитывать.
- Ничто не сравнивалось с облачной моделью и не тестировалось на реальных экспортированных данных о заказах, поэтому приведённые цифры относятся к одной конкретной машине, а не к эталонным показателям.
Это даёт более узкое определение, чем «маленькие модели могут обеспечивать поддержку»: 3-миллиардная модель может вызвать хорошо описанный инструмент, подвести итог его работы и прекратить свою деятельность, при условии что сам вызов инструмента рассматривается как процесс, требующий проверки разрешений, а не как что-то, во что можно верить только потому, что его сгенерировала модель.
Запуск
Из каталога проекта с активной виртуальной средой запустите три скрипта по порядку: сравнение с использованием размышлений и без них, обычного агента и защищённого агента.
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
Скрипты генерируют транскрипции, которые можно прочитать позже для сравнения результатов работы.