Почему vLLM не понимает вашу модель
Вы скачали Apriel-1.6-15B-Thinker, запустили через vLLM, а tool calling не работает. Модель выводит какой-то текст, но vLLM ждет строгий JSON. Знакомая ситуация? vLLM из коробки поддерживает несколько форматов, но кастомные модели часто имеют свои причуды.
vLLM не волнует, как ваша модель обучена. Он ждет, что вывод будет в определенном формате. Если это не так — вам нужен кастомный parser.
В этой статье разберем, как заставить vLLM понимать вывод Apriel-1.6-15B-Thinker для tool calling. Мы создадим плагин с нуля, и я покажу все подводные камни.
Зачем это нужно?
Tool calling — это когда модель вызывает внешние инструменты, например, поиск в интернете или вычисления. vLLM использует парсеры для преобразования вывода модели в структурированный формат. Если ваш модель не соответствует ожиданиям vLLM, вы получаете ошибки или молчание.
Ссылаясь на статью "Держите свой JSON", помните: заставить модель выдавать JSON — полдела. Нужно еще правильно его распарсить.
Как vLLM обрабатывает tool calling
vLLM имеет систему плагинов для парсеров. BaseOutputParser — абстрактный класс, который вы должны наследовать. Основные методы: parse и format. Parse берет сырой вывод модели и возвращает структурированные данные. Format, наоборот, форматирует промпт для модели.
Для Apriel-1.6-15B-Thinker, модель обучена на определенном шаблоне. Нужно понять этот шаблон и написать под него парсер.
1 Анализ вывода модели
Сначала получите сырой вывод модели. Запустите vLLM с простым запросом, который должен вызвать инструмент. Посмотрите, что выводит модель. Например, Apriel может использовать формат как Hermes.
# Пример запроса
from vllm import LLM, SamplingParams
llm = LLM(model="path/to/apriel-1.6-15b-thinker")
sampling_params = SamplingParams(temperature=0)
output = llm.generate("Call tool X with params Y")
print(output[0].outputs[0].text)
Вы увидите что-то вроде:
Я нужно вызвать инструмент.
{"name": "search", "arguments": {"query": "что-то"}}
Или может быть без тегов. Важно зафиксировать точный формат.
2 Создание класса парсера
Создайте новый файл, например, apriel_tool_parser.py. Наследуйтесь от BaseOutputParser из vLLM.
from vllm.engine.output_parser import BaseOutputParser
from typing import Dict, Any, Optional
import json
import re
class AprielToolParser(BaseOutputParser):
def __init__(self):
super().__init__()
# Паттерны для извлечения JSON
self.pattern = re.compile(r'\s*(.*?)\s* ', re.DOTALL)
def parse(self, output: str) -> Optional[Dict[str, Any]]:
# Ищем JSON между тегами
match = self.pattern.search(output)
if not match:
return None
json_str = match.group(1)
try:
return json.loads(json_str)
except json.JSONDecodeError:
# Если JSON сломан, попробуем починить
# ... логика исправления
return None
def format(self, tool_call: Dict[str, Any]) -> str:
# Форматируем tool call в промпт для модели
# Apriel ожидает теги
json_str = json.dumps(tool_call, ensure_ascii=False)
return f"\n{json_str}\n "
Это базовый пример. В реальности, возможно, нужно обрабатывать несколько tool calls или другие форматы.
hermes_tool_parser.py в исходниках vLLM для вдохновения.3 Интеграция с vLLM
Теперь нужно зарегистрировать парсер в vLLM. Есть несколько способов. Самый простой — указать при создании LLMEngine.
from vllm.engine.arg_utils import EngineArgs
from vllm.engine.llm_engine import LLMEngine
from apriel_tool_parser import AprielToolParser
engine_args = EngineArgs(model="path/to/model", ...)
engine = LLMEngine.from_engine_args(engine_args)
engine.output_parser = AprielToolParser()
Или через конфигурацию, если вы используете высокоуровневый API.
Но vLLM также поддерживает плагины через entry points. Создайте setup.py или pyproject.toml для вашего плагина.
# setup.py
from setuptools import setup
setup(
name="apriel-vllm-parser",
entry_points={
'vllm.output_parsers': [
'apriel = apriel_tool_parser:AprielToolParser',
],
},
)
Тогда вы можете указать парсер через аргумент командной строки или конфиг.
4 Тестирование и отладка
Запустите тестовые запросы. Убедитесь, что парсер корректно извлекает JSON и что модель понимает отформатированные промпты.
Используйте логирование для отладки.
import logging
logging.basicConfig(level=logging.DEBUG)
Если парсер не работает, проверьте:
- Регулярные выражения: они должны захватывать весь JSON, но не лишнее.
- Кодировку: модель может выводить Unicode-символы.
- Экранирование: JSON может содержать escaped-символы, которые нужно обработать.
Нюансы, которые вас убьют
1. Модель может выдавать неполный JSON. Например, обрываться на середине. В таком случае, нужно либо достраивать JSON, либо игнорировать вывод.
2. Несколько tool calls в одном выводе. Парсер должен возвращать список вызовов, а не один.
3. Промпт engineering. Чтобы модель стабильно выдавала нужный формат, возможно, придется настроить промпт. Смотрите статью "Когда LLM врёт о документах" для идей по контролю вывода.
Вот улучшенная версия парсера с обработкой ошибок:
class RobustAprielToolParser(BaseOutputParser):
def parse(self, output: str) -> Optional[List[Dict[str, Any]]]:
# Ищем все tool calls
pattern = re.compile(r'\s*(.*?)\s* ', re.DOTALL)
matches = pattern.findall(output)
if not matches:
return None
tool_calls = []
for json_str in matches:
try:
tool_calls.append(json.loads(json_str))
except json.JSONDecodeError:
# Попробуем исправить распространенные ошибки
json_str = self.fix_json(json_str)
try:
tool_calls.append(json.loads(json_str))
except:
continue # Пропускаем сломанный вызов
return tool_calls if tool_calls else None
def fix_json(self, json_str: str) -> str:
# Удаляем лишние запятые в конце объектов или массивов
json_str = re.sub(r',\s*}', '}', json_str)
json_str = re.sub(r',\s*]', ']', json_str)
# Другие исправления...
return json_str
Частые ошибки
| Ошибка | Причина | Решение |
|---|---|---|
| Парсер не находит tool call | Модель выводит в другом формате | Изучите вывод модели и адаптируйте регулярные выражения |
| JSONDecodeError | Некорректный JSON | Добавьте логику исправления JSON или валидацию |
| Модель игнорирует tool call | Промпт не отформатирован правильно | Настройте метод format для соответствия ожиданиям модели |
А что если модель не Apriel?
Принцип тот же: анализ вывода, создание парсера, интеграция. Для других моделей, например, GLM 4.5 Air, смотрите статью "GLM 4.5 Air в режиме тупняка". Там могут быть свои особенности.
И помните: tool calling — это не магия. Это просто парсинг текста. Главное — понять, как ваша модель думает.
Если вы работаете с большими контекстами, обратите внимание на статью "Lost in the Middle", чтобы избежать потери информации.
Итог
Создание кастомного парсера для vLLM — это не ракетостроение. Это анализ вывода модели и написание кода, который этот вывод понимает. Для Apriel-1.6-15B-Thinker мы использовали подход на основе тегов
Не бойтесь копать в исходниках vLLM и смотреть, как реализованы другие парсеры. И всегда тестируйте с реальными запросами.
Удачи в интеграции! И если что-то не работает, помните: чаще всего проблема в регулярных выражениях.