Молчание агента: когда Qwen 3 обещает, но не делает
Вы настроили Qwen 3 в режиме агента через llama-cpp. Промпт идеальный, модель работает, даже tool calling активируется. Но файлы остаются пустыми. Агент думает, что записал данные. Система считает, что всё окей. А в файле — тишина.
Это не баг в классическом понимании. Это системный сбой в коммуникации между четырьмя компонентами: моделью, llama-cpp, системой tool calling и вашей файловой системой. И каждый компонент считает, что проблема в другом.
На 26.01.2026 актуальная версия Qwen 3 — Qwen3.5 32B, но проблема встречается и в более ранних релизах. Llama-cpp должен быть версии 0.9.0 или новее для корректной работы с tool calling.
Почему это происходит? Диагностика по слоям
Представьте цепочку: промпт → модель → llama-cpp парсинг → tool calling исполнение → файловая система. Сбой может быть на любом этапе. Но чаще всего — между этапами 3 и 4.
1 Проверьте, что агент вообще получает инструменты
Самый частый промах: вы думаете, что инструменты загружены, а они — нет. Llama-cpp не кричит об ошибке, если tool calling не инициализирован. Он просто молча игнорирует запросы.
# НЕПРАВИЛЬНО — инструменты могут не загрузиться
from llama_cpp import Llama
llm = Llama(
model_path="qwen3.5-32b-q4_K_M.gguf",
n_ctx=8192,
verbose=False
)
# Где инструменты? Их нет!
response = llm.create_chat_completion(
messages=[{"role": "user", "content": "Напиши в файл test.txt 'Hello'"}]
)
# ПРАВИЛЬНО — явная загрузка инструментов
from llama_cpp import Llama
from llama_cpp.llama_chat_format import LlamaChatCompletionHandler
import json
# Определяем инструменты
tools = [
{
"type": "function",
"function": {
"name": "write_to_file",
"description": "Записать текст в файл",
"parameters": {
"type": "object",
"properties": {
"filename": {"type": "string"},
"content": {"type": "string"}
},
"required": ["filename", "content"]
}
}
}
]
llm = Llama(
model_path="qwen3.5-32b-q4_K_M.gguf",
n_ctx=8192,
chat_handler=LlamaChatCompletionHandler(tools=tools), # Вот ключ!
verbose=True # Включаем логирование
)
print("Инструменты загружены:", json.dumps(tools, indent=2))
2 Промпт — не просто текст, а инструкция для tool calling
Qwen 3 — не телепат. Если в промпте нет явного указания использовать инструменты, модель будет генерировать текст о файле, а не вызывать функцию записи.
# ПЛОХОЙ промпт — модель будет описывать, а не делать
messages = [
{"role": "user", "content": "Создай файл config.json с настройками"}
]
# ХОРОШИЙ промпт — явная инструкция
messages = [
{
"role": "system",
"content": "Ты — агент с доступом к инструментам. Когда нужно записать в файл — используй инструмент write_to_file. Не описывай действия, выполняй их."
},
{
"role": "user",
"content": "Используя доступные инструменты, создай файл config.json и запиши в него {'port': 8080, 'debug': true}. Не пиши объяснений, просто выполни действие."
}
]
Разница тонкая, но критическая. В первом случае Qwen 3 ответит: "Я создам файл config.json со следующими настройками...". Во втором — вызовет инструмент.
3 Права доступа: самый обидный баг
Даже если всё работает, инструмент вызывается, функция выполняется — файл может не создаться. Потому что процесс llama-cpp работает под другим пользователем. Или в другой директории. Или у него нет прав на запись.
# Проверяем текущую директорию и права
pwd
ls -la .
# Смотрим, под каким пользователем работает процесс
ps aux | grep llama
# Проверяем права на запись в текущую директорию
touch test_write.txt
rm test_write.txt
Особенно актуально для Docker-контейнеров и систем с ограниченными правами. Llama-cpp не падает с ошибкой "Permission denied" — он просто возвращает успешный статус, а файл не появляется.
Полный рабочий пример: от промпта до файла
Вот как должен выглядеть end-to-end рабочий процесс. Каждая строчка здесь важна.
import json
import os
from pathlib import Path
from llama_cpp import Llama
from llama_cpp.llama_chat_format import LlamaChatCompletionHandler
# 1. Определяем функцию, которая БУДЕТ записывать файл
def write_to_file(filename: str, content: str) -> str:
"""Реальная функция записи в файл"""
try:
# Абсолютный путь для избежания путаницы
path = Path(filename).resolve()
path.parent.mkdir(parents=True, exist_ok=True)
# Пишем в файл
path.write_text(content, encoding='utf-8')
# Возвращаем подтверждение для модели
return f"Файл {filename} успешно создан. Размер: {len(content)} байт."
except Exception as e:
return f"Ошибка при записи в файл {filename}: {str(e)}"
# 2. Описываем инструмент для модели
tools = [
{
"type": "function",
"function": {
"name": "write_to_file",
"description": "Записать текст или JSON в указанный файл. Если файл существует — перезаписать его.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "Полное имя файла с расширением, например config.json или script.py"
},
"content": {
"type": "string",
"description": "Содержимое файла. Для JSON используйте валидный JSON синтаксис."
}
},
"required": ["filename", "content"]
}
}
}
]
# 3. Создаём обработчик с инструментами
chat_handler = LlamaChatCompletionHandler(
tools=tools,
tool_call_handler=lambda tool_call: {
"write_to_file": lambda args: write_to_file(
args["filename"],
args["content"]
)
}
)
# 4. Инициализируем модель с обработчиком
llm = Llama(
model_path="/путь/к/qwen3.5-32b-q4_K_M.gguf",
n_ctx=8192,
chat_handler=chat_handler,
verbose=True, # Включаем логи!
n_gpu_layers=35, # Для GPU акселерации
seed=42
)
# 5. Готовим промпт с ЯВНЫМ указанием использовать инструменты
messages = [
{
"role": "system",
"content": "Ты — автономный агент с доступом к инструменту write_to_file. Когда пользователь просит создать или изменить файл — ВСЕГДА используй этот инструмент. Не описывай файл, не показывай его содержимое в ответе — просто вызови инструмент. После вывода инструмента подтверди выполнение кратко."
},
{
"role": "user",
"content": "Создай файл docker-compose.yml для веб-приложения на Python с PostgreSQL. Используй инструмент write_to_file."
}
]
# 6. Запускаем генерацию
response = llm.create_chat_completion(
messages=messages,
temperature=0.1, # Низкая температура для точности
max_tokens=512
)
# 7. Проверяем результат
print("Ответ модели:")
print(json.dumps(response, indent=2, ensure_ascii=False))
# 8. Проверяем, создался ли файл
if os.path.exists("docker-compose.yml"):
print("\nФайл создан! Содержимое:")
print(open("docker-compose.yml").read())
else:
print("\nФайл НЕ создан. Проверьте логи выше.")
Тихие убийцы: что ещё может сломать запись
| Проблема | Симптомы | Решение |
|---|---|---|
| Контекст переполнен | Модель начинает генерировать мусор, tool calling ломается | Увеличить n_ctx или очищать историю. Читайте про оптимизацию контекста |
| Температура слишком высокая | Модель "творит" вместо точного следования инструкциям | temperature=0.1 для tool calling, top_p=0.9 |
| Квантование модели | Q4_K_M может терять способность к tool calling | Используйте Q5_K_M или Q6_K. Подробнее о квантовании |
| Устаревшая версия llama-cpp | Tool calling работает через раз | Обновитесь до версии 0.9.0+. Проверьте через llama-cpp --version |
| Неверный формат GGUF | Модель загружается, но tool calling не поддерживается | Качайте модели с официального Hugging Face репозитория Qwen |
Отладка по шагам: если ничего не помогло
Когда стандартные методы не работают, включаем тяжёлую артиллерию.
Шаг 1: Включите максимальное логирование
llm = Llama(
model_path="qwen3.5-32b-q4_K_M.gguf",
n_ctx=8192,
chat_handler=chat_handler,
verbose=True, # Базовые логи
logits_all=True, # Все логиты (для отладки)
vocab_only=False, # Полный словарь
embedding=False, # Если не нужны эмбеддинги
n_threads=8, # Для производительности
n_batch=512, # Размер батча
last_n_tokens_size=64, # Размер контекста
seed=-1 # Случайный сид
)
Запустите скрипт и смотрите на вывод. Ищите строки типа "tool_call" или "function_call". Если их нет — инструменты не загружаются.
Шаг 2: Проверьте raw output модели
# Получаем сырой ответ без обработки
response = llm.create_chat_completion(
messages=messages,
temperature=0.1,
max_tokens=512,
stop=[],
echo=True # Показывает промпт в ответе
)
print("Сырой ответ:")
print(response["choices"][0]["text"])
# Или через низкоуровневый API
output = llm(
prompt=formatted_prompt,
max_tokens=512,
temperature=0.1,
stop=["<|im_end|>", "\n\n"]
)
print("Низкоуровневый вывод:", output["choices"][0]["text"])
Если в сыром выводе есть JSON-подобные структуры с "function_call", но llama-cpp их не парсит — проблема в обработчике чата.
Шаг 3: Тест с простейшим инструментом
# Упрощаем до предела
test_tools = [
{
"type": "function",
"function": {
"name": "test",
"description": "Простой тестовый инструмент",
"parameters": {
"type": "object",
"properties": {
"message": {"type": "string"}
},
"required": ["message"]
}
}
}
]
# И упрощённый промпт
test_messages = [
{"role": "user", "content": "Вызови инструмент test с message='hello'"}
]
Если и это не работает — проблема либо в модели, либо в llama-cpp. Скачайте свежую версию модели и обновите llama-cpp.
На 26.01.2026 актуальная сборка llama-cpp с полной поддержкой Qwen 3 — это версия из мастера GitHub или релиз 0.9.1. Бинарные сборки могут отставать на несколько недель.
А если всё равно не работает? Альтернативные пути
Бывает, что tool calling в llama-cpp ломается на конкретном железе или под конкретной ОС. Не тратьте недели на отладку — используйте обходные пути.
Вариант 1: Ollama вместо чистого llama-cpp
Ollama — обёртка над llama-cpp с более стабильным tool calling. Но и тут есть подводные камни, особенно с галлюцинациями инструментов.
# Установка и запуск Qwen 3 в Ollama
ollama pull qwen2.5:32b
ollama run qwen2.5:32b
Вариант 2: Ручной парсинг вывода модели
Если llama-cpp не умеет парсить tool calling вашей модели — парсите сами. Грязно, но работает.
def extract_tool_call_from_response(response_text):
"""Ручной парсинг tool call из ответа Qwen 3"""
import re
import json
# Ищем JSON-подобные структуры в ответе
pattern = r'\{.*"function".*\}'
matches = re.findall(pattern, response_text, re.DOTALL)
for match in matches:
try:
data = json.loads(match)
if "function" in data or "tool_calls" in data:
return data
except json.JSONDecodeError:
continue
return None
# Использование
response_text = response["choices"][0]["message"]["content"]
tool_call = extract_tool_call_from_response(response_text)
if tool_call:
print("Найден tool call:", tool_call)
# Выполняем функцию вручную
if tool_call.get("name") == "write_to_file":
write_to_file(**tool_call["arguments"])
else:
print("Tool call не найден в ответе")
Вариант 3: LM Studio или другое GUI
LM Studio (на 26.01.2026 уже версия 0.3.5) имеет встроенную поддержку tool calling для многих моделей. Не программируете — используйте GUI.
Производительность: почему Qwen 3 может тормозить с tool calling
Даже если запись в файлы заработала, вы можете столкнуться с другой проблемой — модель думает по 30 секунд перед каждым tool call. Это не баг, это особенность архитектуры.
Qwen 3, особенно большие версии (32B+), требует значительных ресурсов для обработки tool calling. Каждый вызов инструмента — это дополнительный проход по контексту, валидация JSON, проверка параметров.
- Проблема: 32B модель на RTX 4090 думает 15 секунд перед tool call
- Причина: Ограниченная пропускная способность памяти GPU
- Решение: Используйте меньшую модель (7B) для tool calling или оптимизируйте llama-cpp
Чеклист на будущее: как не попасть снова
- Всегда проверяйте версию llama-cpp перед началом работы
- Используйте verbose=True при инициализации модели
- Тестируйте tool calling на простейшем примере перед сложной логикой
- Пишите явные промпты с директивой "используй инструмент X"
- Проверяйте права доступа к файловой системе
- Используйте абсолютные пути в инструментах записи файлов
- Логируйте вызовы функций-обработчиков
- Имейте fallback — ручной парсинг вывода модели
Самая частая ошибка, которую я вижу в проектах: разработчики тратят дни на отладку сложной системы агентов, когда проблема в одной строке — отсутствии chat_handler в конструкторе Llama. Или в промпте, который говорит модели "опиши файл", а не "создай файл".
Tool calling в локальных LLM — всё ещё молодая технология. На 26.01.2026 она работает стабильно в 80% случаев. Остальные 20% — это танцы с бубном вокруг конкретных версий, конкретного железа и конкретных моделей. Но когда работает — это магия. Магия, которая пишет в файлы.