Multi-agent чатбот на GX10 с рутер
Един модел трудно е едновременно най-добрият програмист, математик и събеседник. Тук правим чатбот от няколко агента: малък и бърз модел (рутер) прочита въпроса и го подава на правилния специалист. Всичко върви локално през Ollama, а данните не напускат машината.
qwen3:7b, а математикът и изследователят — qwen3:72b. И двата не са в каталога на Ollama (размерите на qwen3 са 0,6b, 1,7b, 4b, 8b, 14b, 30b, 32b и 235b). Сега: рутер и разговор — llama3.2:3b, код — qwen2.5-coder:7b, математика и изследване — qwen3:32b. Поправихме кода за интерфейса. В Gradio 6 форматът с двойки (кортежи) в чатбота е премахнат; историята е списък от съобщения с роля и текст — затова интерфейсът е написан наново. Поправихме историята: „последните 6 съобщения“ означава 6 съобщения (3 размени), а не 6 двойки. Добавихме: JSON режим за рутера и безопасно връщане към разговорния агент, разделяне на ядрото от интерфейса, виртуална среда, затворен порт (интерфейсът слуша само на 127.0.0.1) с достъп през SSH тунел и сметка за паметта. Махнахме двуезичния интерфейсен текст и сложния брандиран Gradio-вид.
01Какво ще научиш
- Защо няколко специализирани агента често вършат по-добра работа от един универсален.
- Как работи шаблонът „рутер → специалист“ и как рутерът връща точен отговор във формат JSON.
- Как да подадеш на специалиста предишната част от разговора.
- Как да дадеш на всеки агент собствен модел и собствено указание (системен промпт).
- Как да направиш прост уеб интерфейс с Gradio и да го отвориш безопасно от своя компютър.
- Как да провериш дали рутерът насочва правилно и какво да правиш, когато греши.
02Преди да започнеш
- Машина от класа NVIDIA GB10 (например ASUS Ascent GX10 или DGX Spark) с DGX OS и достъп до терминала — директно или по SSH.
- Ollama, инсталиран и работещ на машината. Ако още нямаш, започни от урока Open WebUI и Ollama. Провери с
ollama --version. - Python 3 с модула за виртуални среди (
venv). Ако на Ubuntu липсва, инсталира се пакетътpython3-venv. - Интернет за изтеглянето на моделите — общо около 27 GB (виж таблицата в стъпка 3) — и свободно място на диска (провери с
df -h).
qwen3:32b с qwen3:8b (5,2 GB).03Стъпки
-
Как тече един въпрос
Системата има един рутер и четирима специалисти. Защо така? Рутерът прави само една евтина работа — решава „чий е този въпрос“. Скъпият голям модел се събужда само когато наистина трябва.
Агент За какво е Модел Рутер Класифицира въпроса и връща JSON llama3.2:3bcode Програмиране, скриптове, дебъгване qwen2.5-coder:7bmath Уравнения, изчисления, стъпка по стъпка qwen3:32bresearch Обяснения, сравнения, структурирани отговори qwen3:32bchat Разговор, поздрави, дребни задачи llama3.2:3bПътят е: твоето съобщение → рутер → избран специалист → отговор. Ако рутерът върне нещо неразбираемо, въпросът отива при разговорния агент — така системата не се чупи.
-
Подготви папка и виртуална среда
Виртуалната среда държи пакетите на проекта настрана от системния Python — не развалят едно друго.
bash · на машинатаmkdir -p ~/multi-agent && cd ~/multi-agent python3 -m venv .venv source .venv/bin/activate pip install "gradio>=6,<7" openaiПакетът
openaiползваме само като клиент: Ollama има съвместим с него програмен интерфейс наhttp://localhost:11434/v1. Gradio е закачен на версия 6 (към 03.10.2026 е 6.29.1), защото кодът по-долу е за нея. ⚠️ Командите не са пускани от нас. -
Изтегли моделите
bashollama pull llama3.2:3b ollama pull qwen2.5-coder:7b ollama pull qwen3:32b ollama listМодел Размер (по ollama.com) Роля llama3.2:3b2,0 GB рутер и разговор qwen2.5-coder:7b4,7 GB код qwen3:32b20 GB математика и изследване ✅Защо рутерът не е qwen3Qwen3 е „мислещ“ модел: преди отговора може да напише дълъг блок с разсъждения. За рутера това е излишно забавяне — той трябва да каже една дума. Затова ползваме малкияllama3.2:3b, който отговаря веднага. Ако искаш друг рутер, смени го в една променлива. -
Ядрото: файлът multi_agent.py
Тук е цялата логика — без интерфейс. Така можеш да я пробваш в терминала, а после да сложиш върху нея какъвто искаш екран.
python · ~/multi-agent/multi_agent.pyimport json from openai import OpenAI # Ollama игнорира ключа, но клиентът изисква някаква стойност client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") ROUTER_MODEL = "llama3.2:3b" AGENTS = { "code": { "model": "qwen2.5-coder:7b", "system": "Ти си опитен програмист. Пишеш чист код с кратки коментари. " "Обясняваш какво прави кодът, показваш пример за употреба " "и споменаваш граничните случаи. Предпочиташ Python и Bash.", }, "math": { "model": "qwen3:32b", "system": "Ти си математик. Решаваш задачите стъпка по стъпка, " "обясняваш какво правиш и защо, и проверяваш резултата накрая.", }, "research": { "model": "qwen3:32b", "system": "Ти си изследователски асистент. Даваш точни и пълни отговори, " "подредени със заглавия и точки. Разграничаваш факти от мнения " "и казваш, когато не си сигурен.", }, "chat": { "model": "llama3.2:3b", "system": "Ти си приятелски асистент. Отговаряш на български, " "кратко, ясно и полезно.", }, } ROUTER_SYSTEM = """Класифицирай въпроса в ТОЧНО ЕДИН от следните типове: - "code": програмиране, скриптове, дебъгване - "math": математика, уравнения, изчисления, статистика - "research": обяснения, понятия, история, сравнения, анализ - "chat": разговор, поздрав, лично мнение, малки задачи Отговори САМО с JSON: {"agent": "code|math|research|chat"}""" def route(message: str) -> str: """Връща името на агента; при всяко съмнение - 'chat'.""" r = client.chat.completions.create( model=ROUTER_MODEL, temperature=0.1, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": ROUTER_SYSTEM}, {"role": "user", "content": message}, ], ) try: agent = json.loads(r.choices[0].message.content).get("agent", "chat") except (json.JSONDecodeError, TypeError, AttributeError): agent = "chat" return agent if agent in AGENTS else "chat" def ask_agent(name: str, message: str, history: list) -> str: """history е списък от {"role": ..., "content": ...}; взимаме последните 6.""" agent = AGENTS[name] messages = [{"role": "system", "content": agent["system"]}] messages += history[-6:] messages.append({"role": "user", "content": message}) r = client.chat.completions.create( model=agent["model"], temperature=0.7, messages=messages, ) return r.choices[0].message.content def respond(message: str, history: list): """Връща (име на агента, отговор).""" name = route(message) return name, ask_agent(name, message, history) if __name__ == "__main__": history = [] while True: msg = input("Ти: ").strip() if msg.lower() in ("exit", "quit"): break if not msg: continue name, answer = respond(msg, history) history += [ {"role": "user", "content": msg}, {"role": "assistant", "content": answer}, ] print(f"\n[{name.upper()}] {answer}\n")💡Какво се случва в кодаJSON режимът (response_format) е поддържан от съвместимия интерфейс на Ollama и кара модела да връща валиден JSON. Проверкатаagent in AGENTSхваща случая, когато рутерът измисли непозната категория. Историята е обикновен списък; „последните 6“ са 6 съобщения, тоест 3 размени. Ниската температура на рутера (0,1) прави класифицирането стабилно, а 0,7 при специалистите оставя място за изразност. -
Пробвай в терминала
bashpython multi_agent.pyНапиши „Здравей, как си?“ и после „Напиши Python скрипт за четене на CSV файл“. Пред всеки отговор трябва да видиш името на агента. Първият отговор на всеки модел е по-бавен — моделът се зарежда в паметта. С
exitизлизаш. -
Интерфейс: файлът app.py
Gradio прави уеб страница с няколко реда. Тук важното е историята: в Gradio 6 тя е списък от речници с
roleиcontent. Съдържанието може да е текст или списък от части, затова го привеждаме до обикновен текст, преди да го върнем на модела. Етикетът с името на агента показваме на екрана, но го махаме от историята, за да не обърква специалистите.python · ~/multi-agent/app.pyimport re import gradio as gr from multi_agent import respond TAG = re.compile(r"^\*\*\[[A-Z]+\]\*\*\n\n") def as_text(content) -> str: """Gradio може да даде текст или списък от части; връщаме обикновен текст.""" if isinstance(content, str): return content if isinstance(content, list): return "".join( p.get("text", "") if isinstance(p, dict) else str(p) for p in content ) return str(content) def chat(message, history): if not message.strip(): return "", history plain = [ {"role": h["role"], "content": TAG.sub("", as_text(h["content"]))} for h in history ] name, answer = respond(message, plain) history = history + [ {"role": "user", "content": message}, {"role": "assistant", "content": f"**[{name.upper()}]**\n\n{answer}"}, ] return "", history with gr.Blocks(title="Multi-agent чатбот") as demo: gr.Markdown("# Multi-agent чатбот\nРутерът насочва въпроса към: **code** · **math** · **research** · **chat**") chatbot = gr.Chatbot(height=500, label="Разговор") msg = gr.Textbox(placeholder="Задай въпрос...", label="Съобщение") with gr.Row(): send_btn = gr.Button("Изпрати", variant="primary") clear_btn = gr.Button("Изчисти") send_btn.click(chat, [msg, chatbot], [msg, chatbot]) msg.submit(chat, [msg, chatbot], [msg, chatbot]) clear_btn.click(lambda: [], None, chatbot) demo.launch(server_name="127.0.0.1", server_port=7860)bashpython app.pyИнтерфейсът слуша само на
127.0.0.1:7860— другите в мрежата не го виждат. От своя компютър стигаш до него през SSH тунел:bash · на твоя компютърssh -L 7860:localhost:7860 <потребител>@<адрес-на-машината>Остави връзката отворена и отвори
http://localhost:7860в браузъра. Ако работиш директно на машината, просто отвори същия адрес.⚠️Не слагай 0.0.0.0Старият вариант на урока пускаше интерфейса на0.0.0.0, тоест към цялата мрежа — без парола. Не го правим: всеки в мрежата би могъл да разговаря с твоите модели. -
Провери насочването
Задай тези четири въпроса и виж кой агент отговаря:
Въпрос Очакван агент Модел „Напиши Python скрипт за четене на CSV файл“ CODE qwen2.5-coder:7b„Реши: 3x² + 5x − 2 = 0“ MATH qwen3:32b„Обясни какво е архитектурата transformer“ RESEARCH qwen3:32b„Здравей, как си?“ CHAT llama3.2:3bЗа уравнението очакваш корени
x = 1/3иx = −2— така можеш да провериш дали математикът е прав. Малкият рутер може да сгреши, особено при смесени въпроси („напиши код, който решава уравнение“). Тогава поправката е вROUTER_SYSTEM: добави по един кратък пример за всяка категория. -
Капани и подобрения
- Първо бавно. Зареждането на 20-гигабайтов модел отнема време. Следващите отговори са бързи, докато моделът е в паметта (5 минути след последната заявка). Ако искаш да освободиш паметта веднага, ползвай
ollama stop <модел>. - Историята струва. Всяко старо съобщение се изпраща пак към модела — повече история значи по-бавен отговор. Затова пазим само последните 6.
- Рутерът е предложение. Той не знае всичко. Ако е важно, добави бутон „друг агент“ или показвай името на агента (както правим), за да се вижда кой е отговорил.
- Човек одобрява действията. Този чатбот само отговаря. Щом му дадеш права да праща имейли или да пише в системи, всяко действие с реален ефект трябва да минава през човек.
- Първо бавно. Зареждането на 20-гигабайтов модел отнема време. Следващите отговори са бързи, докато моделът е в паметта (5 минути след последната заявка). Ако искаш да освободиш паметта веднага, ползвай
04Проверка
ollama listпоказваllama3.2:3b,qwen2.5-coder:7bиqwen3:32b.python multi_agent.pyотговаря на поздрав през разговорния агент.- Четирите тестови въпроса отиват при CODE, MATH, RESEARCH и CHAT.
- Математикът решава уравнението стъпка по стъпка и стига до
x = 1/3иx = −2. - Интерфейсът се отваря през тунела на
http://localhost:7860. - Допълнителен въпрос („а можеш ли да го обясниш по-просто?“) използва предишните съобщения.
- Нищо не слуша на
0.0.0.0.
Тест
1. Защо рутерът е малък модел, а не най-големият?
2. Какво прави кодът, ако рутерът върне JSON с непозната категория?
3. Защо Gradio е пуснат с server_name="127.0.0.1"?
4. В какъв вид е историята на разговора в чатбота на Gradio 6?
05Какво следва
06Източници
- Ollama: съвместимост с OpenAI 🔒 локално — адрес
/v1,response_format, ключът се игнорира. - Ollama: често задавани въпроси — 5 минути в паметта,
ollama stop, няколко модела наведнъж. - llama3.2 · qwen2.5-coder · qwen3 (тагове) 🔒 локално — наличните размери: 2,0 GB, 4,7 GB, 20 GB.
- Gradio в PyPI · Gradio: списък с промените — версия 6.29.1; форматът с двойки в чатбота е премахнат.
- openai в PyPI — клиентът, който ползваме за връзка с Ollama.
- NVIDIA DGX Spark: хардуер — 128 GB обща памет.