Qwen 3 агент не пишет в файлы через llama-cpp: диагностика и решение | AiManual
AiManual Logo Ai / Manual.
26 Янв 2026 Гайд

Qwen 3 в агентском режиме застрял: почему модель не пишет в файлы через llama-cpp и как это исправить

Пошаговое руководство по диагностике и решению проблемы, когда Qwen 3 в режиме агента не записывает данные в файлы через llama-cpp. Проверка промптов, конфигура

Молчание агента: когда 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))
💡
Llama-cpp версии 0.8.0 и ниже имеют кривую реализацию tool calling. Обновитесь до 0.9.0+. Если не можете обновить — читайте статью про проверку версий.

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
💡
На MacBook с 24GB RAM Qwen 3 14B работает с tool calling в 2-3 раза медленнее, чем специализированные код-модели типа Devstral. Если нужна скорость — сравните производительность в нашем тесте моделей на MacBook.

Чеклист на будущее: как не попасть снова

  1. Всегда проверяйте версию llama-cpp перед началом работы
  2. Используйте verbose=True при инициализации модели
  3. Тестируйте tool calling на простейшем примере перед сложной логикой
  4. Пишите явные промпты с директивой "используй инструмент X"
  5. Проверяйте права доступа к файловой системе
  6. Используйте абсолютные пути в инструментах записи файлов
  7. Логируйте вызовы функций-обработчиков
  8. Имейте fallback — ручной парсинг вывода модели

Самая частая ошибка, которую я вижу в проектах: разработчики тратят дни на отладку сложной системы агентов, когда проблема в одной строке — отсутствии chat_handler в конструкторе Llama. Или в промпте, который говорит модели "опиши файл", а не "создай файл".

Tool calling в локальных LLM — всё ещё молодая технология. На 26.01.2026 она работает стабильно в 80% случаев. Остальные 20% — это танцы с бубном вокруг конкретных версий, конкретного железа и конкретных моделей. Но когда работает — это магия. Магия, которая пишет в файлы.