Lokalny agent wsparcia z SmolLM3: tryb myślenia, narzędzia i zasady bezpieczeństwa
Uruchamianie modelu 3B na laptopie jako agent obsługi klienta: kiedy używać think lub no_think, jak działa wywoływanie narzędzi oraz mechanizmy zapobiegające niechcianym wywołaniom narzędzi zastępczych.
Źródła: **A Local Support Agent with SmolLM3: Think Mode, Tools, and When Not to Reason**
Czy model z 3 miliardami parametrów może obsłużyć zgłoszenie klienta bez wsparcia modelu o mocy 70 miliardów parametrów? W przypadku 90% prostych zgłoszeń – tak, ale tylko wtedy, gdy przestaniemy ufać samym wywołaniom narzędzi przez model. Tutaj SmolLM3 działa lokalnie jako mały agent obsługi klienta i pokazuje, w których sytuacjach zachowuje się poprawnie, a gdzie kod otaczający go musi być bardziej rygorystyczny niż sam model.
Obsługa klienta to nieodpowiednie miejsce na zastosowanie zwykłego chatbota. Typowe zgłoszenia wymagają wyszukiwania informacji, nadania etykiety oraz krótkiej odpowiedzi opartej na zasadach, podczas gdy mniejszość dotyczy problemów takich jak podwójne opłaty, zagubione zamówienia lub niezadowoleni klienci. Dobry serwis obu grup z jednego modelu oznacza konieczność jego działania na dwa różne sposoby.
SmolLM3-3B nadaje się do tego z trzech powodów, które ukrywa liczba parametrów:
- Dołączone są do niego dwa tryby rozumowania. Dodanie
/no_thinksprawia, że odpowiada natychmiast, natomiast/thinkzmusza go do napisania ukrytego śladu rozumowania wcześniej.
<tool_call>, gdy chce je uruchomić.Nie odbywa się żadne szkolenie, a wagi modelu nigdy nie są modyfikowane. Zadanie polega na oddzieleniu procesu myślenia od działania oraz na uczynieniu otaczającego kodu bardziej rygorystycznym niż sam model.
Ustawienia
Dwa skrypty wykorzystują dwa środowiska wykonawcze, ale jedną rodzinę modeli. W porównaniu trybu myślenia próbkowanie odbywa się zgodnie z specyfikacją modelu, przy użyciu wartości temperature=0.6 i top_p=0.95. Agent działa wolniej przy temperature=0.3, dzięki czemu dane JSON dotyczące narzędzi pozostają niewielkie.
Na tym urządzeniu podróż tam i z powrotem agenta 8-bitowego trwała około czterech sekund. Pełny model 3B w ramach Transformers generował około 10 tokenów na sekundę, co jest wystarczające do późniejszego czytania logów, ale zbyt wolne dla klienta obserwującego okno czatu. Transformers jest przydatny do badania szablonów. Gdy po drugiej stronie znajduje się rzeczywała osoba, należy przejść na MLX lub llama.cpp.
Rozmyślanie to decyzja biznesowa
SmolLM3 działa w trybie automatycznym. Istnieją dwa równoważne sposoby na wyłączenie tej funkcji. Jeden z nich to komunikat systemowy:
messages = [
{"role": "system", "content": "/no_think"},
{"role": "user", "content": prompt},
]
Pozostała to flaga przy renderowaniu szablonu:
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
)
Śablon robi więcej niż tylko zmienia wartość logiczną. Zapisuje tryb rozumowania w nagłówku systemu, a gdy wybrano /no_think, dodatkowo wstępnie wstawia pusty blok rozumowania, dzięki czemu generowanie kontynuuje się bezpośrednio z odpowiedzią:
<|im_start|>assistant
<think>
</think>
Dlatego zwykłe wywołanie generate() w trybie no-think może nadal zwracać w tekście <think></think>. W przypadku odpowiedzi skierowanej do klienta należy to usunąć:
final = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
To samo zapytanie obsługi trafiło do obu trybów: klient jest opłacany dwukrotnie za jeden zamówienie, więc jakie trzy konkretne kroki powinna podjąć obsługa?
Dwa zegary
Przepustowość była mniej więcej taka sama w obu trybach, ale ilość pracy nie. Tryb think spędził większość swoich 939 tokenów w bloku think. Klient nigdy nie czyta tego zapisu, ale mimo to czeka na jego zakończenie.
Dwa podręczniki postępowania
/no_think skierowało się bezpośrednio do standardowej listy kontrolnej: potwierdzić zamówienie, skontaktować się z sprzedawcą, zwrócić pieniądze. Działa to jako wstępny projekt, ale jest nieco niewłaściwe dla własnego zespołu obsługi klienta sprzedawcy, ponieważ „skontaktuj się z sprzedawcą” to rada udzielana przez bank, a nie coś, co agent wewnętrzny mówi swojemu klientowi.
/think poświęcił około jednej strony na kłótnię sam ze sobą dotyczącą zwrotu pieniędzy lub kredytu sklepowego, eskalacji sytuacji oraz sprawdzenia procesu dostawy, a następnie doszedł do bardziej precyzyjnego planu:
- Przeprosić i uzyskać numer zamówienia, datę oraz metodę płatności.
- Zlokalizować dublowaną transakcję i ją anulować.
- Wyjaśnić klientowi, co poszło nie tak, i potwierdzić, że pieniądze zostaną zwrócone w formie zwrotu lub kredytu sklepowego.
Podział zadań związanych z dostawą wynika z tego:
/no_thinkprzy dużym obciążeniu pracy: klasyfikacja intencji, zapytania typu „gdzie jest moje zamówienie”, gotowe odpowiedzi z polityki oraz wywoływanie narzędzi./thinkw sytuacjach, gdy błąd może mieć poważne konsekwencje: zwroty pieniędzy, odwołania transakcji, kwestie bezpieczeństwa oraz wszystko, co wymaga sprawdzenia według listy kontrolnej w stylu ludzkim przed udzieleniem odpowiedzi.
Własne dane przedstawione przez Hugging Face wskazują na to samo dotyczące wykorzystania narzędzi. W teście BFCL SmolLM3 osiąga wynik 92,3 bez rozszerzonego myślenia oraz 88,8 z nim. Rozszerzone myślenie nie jest bezkosztowe, a w tym typie zadań nie przyniosło nawet lepszych rezultatów. Poniższy agent wymusza ustawienie enable_thinking=False, ponieważ wyszukiwanie zamówień powinno polegać na wywołaniu funkcji, a nie na pisaniu eseju.
Jedno narzędzie i pętla dwóch kroków
SmolLM3 oczekuje narzędzi w postaci obiektów JSON-schema przekazywanych pod kluczem xml_tools. Szablon kopiuje je do bloku narzędzi i instruuje model, aby odpowiadał w takiej formie:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call>
Narzędzie wsparcia jest celowo ograniczone:
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"],
},
}
]
Za nim znajduje się fałszywa baza danych. W środowisku produkcyjnym byłaby to zapytanie SQL lub wywołanie usługi obsługującej zamówienia.
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"},
}
Pętla stanowi całość procesu. Model otrzymuje wiadomość od klienta, tworzy pierwszą odpowiedź, a jeśli chodzi o wywołanie narzędzia, kod uruchamia odpowiednią funkcję, przekazuje wynik z powrotem i prosi o drugą odpowiedź:
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
Szczęśliwa ścieżka
Klient po tygodniu pyta, gdzie jest zamówienie ORD-4821. Model nie odpowiada. Wywołuje narzędzie:
<tool_call>
{"name": "lookup_order_status", "arguments": {"order_id": "ORD-4821"}}
</tool_call>
Funkcja zwraca informację o wysyłce przesyłki przez DHL datowaną na 18 czerwca 2026 roku. Po drugiej próbie, trwającej łącznie 4,2 sekundy, otrzymuje się odpowiedź mówiącą, że zamówienie zostało wysłane i powinno dotrzeć 18 czerwca za pośrednictwem DHL.
Dla ORD-3307 status to „przetwarzanie”, bez wskazania przewoźnika, z szacowanym terminem dostawy na 20 czerwca. Model stwierdził, że przewoźnik nie jest dostępny, zamiast wymyślić go samodzielnie – to dokładnie to, czego oczekuje się od małego modelu: powinien czytać dane z pliku JSON i ich nie modyfikować.
Dla ORD-9999 wywołanie narzędzia było poprawne, a funkcja zwróciła wartość not_found. W odpowiedzi przeproszono i poproszono klienta o sprawdzenie numeru. Samodzielna, ustrukturyzowana informacja o braku znalezienia wystarczyła, bez konieczności dodatkowych podpowiedzi.
Gdzie umiera naiwny pętla
Trzecie zapytanie miało służyć jako kontrola negatywna: klient chce jedynie zmienić adres e-mail. Nie istnieje żaden narzędzie do obsługi e-maili, więc dobry agent o tym mówi i kończy rozmowę. Naiwny algorytm zrobił coś innego:
- Zaczął poprawnie, mówiąc, że nie można zmienić informacji osobistych i że klient powinien skontaktować się z obsługą.
- Następnie kontynuował działanie i wysłał żądanie do narzędzia zawierające miejsce zastępcze:
<tool_call>
{"name": "lookup_order_status", "arguments": {"order_id": "your_order_id"}}
</tool_call>
- Parser wykonał wszystko, co było zamknięte w
<tool_call>. Poszukał w fałszywej bazie danychyour_order_id, otrzymał wartośćnot_foundi przekazał ją z powrotem modelowi. - Podczas drugiej próby model ujawnił klientowi schemat narzędzia i poinstruował go, aby zastąpić
your_order_idprawdziwym numerem zamówienia.
Cztery elementy zawiodły jednocześnie, nakładając się na siebie. Część związana z modelem jest najmniej istotna. Małe modele unikają ryzyka, co jest normalne. Jeśli twój pętla polega na użyciu wyrażenia regularnego do wyszukiwania <tool_call>, a następnie na wywołaniu funkcji z dostępnymi argumentami, ostatecznie uruchomisz w środowisku produkcyjnym funkcję lookup_order_status("your_order_id"), a to się nie wydarzy podczas demonstracji.
Zabezpieczenia, które nie wymagają dopracowania
Dwie proste zasady rozwiązują większość problemów bez zmiany wag.
Najpierw wywołuj narzędzie tylko wtedy, gdy cała kolejna akcja składa się wyłącznie z tego wywołania. Akapit następujący po wywołaniu jest uważany za zwykły tekst, więc XML musi zostać usunięte, zanim klient go zobaczy.
Z drugiej strony waliduj argumenty przed tym, jak trafią do bazy danych. ID zamówień mają tu postać ORD- w połączeniu z cyframi, więc your_order_id nigdy nie zostanie przyjęty.
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
Należy również udoskonalić opis narzędzia, wskazując, że należy je używać tylko wtedy, gdy klient już dostarczył konkretny identyfikator zamówienia. Po tej zmianie pytanie e-mailowe w trybie zabezpieczonym w ogóle nie wywoływało żadnego wezwania narzędzia, zajmując 3,5 sekundy w porównaniu z 15,4 sekund u uszkodzonej wersji wymagającej dwóch kroków. Odpowiedź przeprosiła, poinformowała, że nie może obsłużyć zmian konta, i odesłała do obsługi klienta. To nadal jest rozwiązanie ogólne, ale bez narzędzia update_email i tak nie powinno istnieć żadnej magicznej ścieżki samodzielnej obsługi. Przynajmniej przestało udawać, że wyszukuje zamówienie.
Wyszukiwania w trybie „happy path” nadal działały, a zamówienie ORD-9999 wciąż było zgłaszane jako zaginione. Model nie stał się mądrzejszy. Traktuj to raczej jak pas bezpieczeństwa niż transplantację.
Czego to nie dowodzi
To jest tylko demonstracja, a prawdziwy kopilot wsparcia wymaga znacznie więcej niż jednej funkcji i słownika z trzema zamówieniami. Należy odrzucić kilka twierdzeń:
- To nie jest planowanie przy użyciu narzędzia wielofunkcyjnego. Jedno narzędzie służy do wyszukiwania, a około dwóch narzędzi to moment, w którym modele 3B zaczynają tworzyć identyfikatory zamówień, aby spełnić schematy, których w pełni nie rozumieją.
- Baza danych jest fałszywa, więc autoryzacja, dane osobowe oraz opcja „niech ten użytkownik zobaczy to zamówienie” nigdy nie były problemem modelu.
- Przez cały czas aktywne było pobieranie próbek, więc ponowne uruchomienie zapytania e-mailowego nie przywróci tych samych słów. To, co powtarzało się za każdym razem, to sam błąd – wywołanie narzędzia zawierające miejsce zastępcze. Dokładne sformułowanie się różni, więc nie można na to liczyć.
- Nic nie zostało porównane z modelem chmurowym ani przetestowane na rzeczywistym eksporcie ticketów, więc podane liczby odnoszą się do jednej maszyny, a nie do standardu referencyjnego.
To daje węższe stwierdzenie niż „małe modele mogą zapewniać wsparcie”: model o pojemności 3 miliardów parametrów może wywołać dobrze opisany narzędzie, podsumować wynik i odejść, pod warunkiem że samo wywołanie narzędzia jest traktowane jako coś, co wymaga sprawdzenia uprawnień, a nie jako coś, czemu można ufać tylko dlatego, że model je wygenerował.
Rozpoczęcie działania
Z katalogu projektu z aktywnym środowiskiem wirtualnym uruchom kolejno trzy skrypty: porównanie z użyciem i bez użycia procesu myślenia, zwykłego agenta oraz agenta zabezpieczonego.
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
Skrypty tworzą transkrypcje, które można później przeczytać w celu porównania różnych wykonań.
Literatura pokrewna
- Budowanie routera multi-agentowego z LangGraph i agentem nadzorczym — Jak podzielić jeden przeciążony agent na specjalistów przy użyciu agenta nadzorczego w LangGraph, wykorzystując gotowy pomocnik oraz ręcznie stworzony StateGraph, a także omawiając projektowanie stanów i typowe pułapki.
- Triaging zapytań supportowych za pomocą LangGraph i Ollamy — Budowanie grafu triagingu zapytań składającego się z trzech agentów przy użyciu LangGraph: klasyfikatora, odpowiedziwcy oraz ludzkiego moderatora, działającego na lokalnym Ollamie lub przez API przy użyciu jednej zmienionej linii kodu.