Миграция с одной LLM на другую: GPT → Claude → Open Source
Представьте: ваш сервис на GPT-4 начинает выдавать нерелевантные ответы после перехода на Claude. Или вы хотите перейти на Open Source модели, чтобы снизить затраты, но боитесь потерять качество. Мы сталкивались с этим десятки раз. Наш опыт показывает: правильно спланированная миграция не только сохраняет, но и улучшает качество за счёт лучшей модели под вашу задачу.
Рассмотрим конкретный случай: переход с GPT-4 на Claude 3 Sonnet для чат-бота поддержки. Мы адаптировали 150 промптов, переписали логику вызова инструментов и провели A/B тестирование. Результат — снижение стоимости на 60% при росте CSAT на 10%. И это не единичный пример: 20+ успешных миграций за 5 лет работы.
Проблемы, с которыми сталкиваются при миграции
- Разные форматы промптов. OpenAI использует массив messages с system role, Claude — отдельный параметр system с XML-тегами. Прямой перенос system prompt приводит к потере части инструкций.
- Разная структура tool calls. У OpenAI — functions с описанием параметров, у Claude — tool_use с input_schema. Конвертация требует маппинга типов и строгого соблюдения формата.
- Разные модели токенизации и окна контекста. GPT-4 имеет окно 8k/32k, Claude 3 — 200k. Open Source модели (LLaMA 3, Mistral) — 8k-32k. Это влияет на стратегию разбиения длинных текстов.
- Потеря качества. Даже при одинаковых промптах модели ведут себя по-разному. Без тестирования можно получить снижение точности на 20-30%.
Как мы решаем эти проблемы
Мы используем LLM-as-judge для автоматического сравнения ответов. Это позволяет быстро оценить, насколько новая модель соответствует старой. Для адаптации промптов мы написали класс PromptAdapter, который конвертирует system prompt между провайдерами и добавляет XML-теги для Claude. Унифицированный клиент UnifiedLLMClient скрывает различия API и позволяет переключать провайдера одной строкой кода.
from anthropic import Anthropic
from openai import OpenAI
import json
import time
from typing import Callable
anthropic_client = Anthropic()
openai_client = OpenAI()
class LLMMigrationAnalyzer:
"""Анализирует совместимость и качество при миграции"""
def compare_responses(
self,
test_cases: list[dict],
source_fn: Callable,
target_fn: Callable,
) -> dict:
"""Сравнивает ответы двух моделей на тестовых случаях"""
results = []
for case in test_cases:
source_response = source_fn(case["messages"], case.get("system"))
target_response = target_fn(case["messages"], case.get("system"))
# LLM-as-judge для оценки качества
quality_score = self.judge_quality(
case["messages"][-1]["content"],
source_response,
target_response,
)
results.append({
"input": case["messages"][-1]["content"],
"source": source_response[:200],
"target": target_response[:200],
"quality_score": quality_score,
"recommendation": "migrate" if quality_score >= 0.8 else "review",
})
return {
"total_cases": len(results),
"safe_to_migrate": len([r for r in results if r["recommendation"] == "migrate"]),
"needs_review": len([r for r in results if r["recommendation"] == "review"]),
"avg_quality": sum(r["quality_score"] for r in results) / len(results),
"cases": results,
}
def judge_quality(self, question: str, source: str, target: str) -> float:
"""Оценивает качество ответа target относительно source"""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""Compare two AI responses to the same question.
Question: {question}
Response A: {source[:500]}
Response B: {target[:500]}
Rate Response B compared to A on a scale 0-1 where:
1.0 = B is better or equal to A
0.7 = B is slightly worse but acceptable
0.5 = B has notable quality degradation
0.0 = B is significantly worse
Return only a number."""
}],
temperature=0,
)
try:
return float(response.choices[0].message.content.strip())
except ValueError:
return 0.5
Адаптация промптов при миграции GPT → Claude
class PromptAdapter:
"""Адаптирует промпты между провайдерами"""
# Различия между моделями
GPT_TO_CLAUDE_RULES = {
# OpenAI использует messages array для system
# Claude использует отдельный system параметр
"system_prompt": "separate_parameter",
# Claude предпочитает XML-теги для структурирования
# GPT не требует специального форматирования
"prefer_xml_tags": True,
# Claude лучше следует инструкциям с явными ограничениями
"explicit_constraints": True,
}
def adapt_system_prompt(self, gpt_system: str) -> str:
"""Адаптирует system prompt для Claude"""
response = anthropic_client.messages.create(
model="claude-haiku-4-5",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Адаптируй этот system prompt от OpenAI GPT для Anthropic Claude.
Правила адаптации:
- Сохрани основной смысл и инструкции
- Используй XML-теги для структурирования (<instructions>, <constraints>, <format>)
- Claude лучше следует конкретным примерам, добавь их если нужно
- Убери упоминания "GPT", "ChatGPT" если есть
Исходный prompt:
{gpt_system}
Верни только адаптированный prompt."""
}]
)
return response.content[0].text
def adapt_function_tools(self, openai_tools: list) -> list:
"""Конвертирует OpenAI tools в Claude tool_use формат"""
claude_tools = []
for tool in openai_tools:
if tool.get("type") == "function":
func = tool["function"]
claude_tools.append({
"name": func["name"],
"description": func["description"],
"input_schema": func.get("parameters", {
"type": "object",
"properties": {}
})
})
return claude_tools
Абстракционный слой для плавной миграции
from enum import Enum
class LLMProvider(str, Enum):
OPENAI = "openai"
ANTHROPIC = "anthropic"
OLLAMA = "ollama"
class UnifiedLLMClient:
"""Единый интерфейс для всех провайдеров"""
def __init__(self, provider: LLMProvider, model: str):
self.provider = provider
self.model = model
def complete(self, messages: list[dict], system: str = "", **kwargs) -> str:
"""Единый метод для всех провайдеров"""
if self.provider == LLMProvider.ANTHROPIC:
response = anthropic_client.messages.create(
model=self.model,
max_tokens=kwargs.get("max_tokens", 2048),
system=system,
messages=messages,
temperature=kwargs.get("temperature", 0.1),
)
return response.content[0].text
elif self.provider == LLMProvider.OPENAI:
all_messages = []
if system:
all_messages.append({"role": "system", "content": system})
all_messages.extend(messages)
response = openai_client.chat.completions.create(
model=self.model,
messages=all_messages,
max_tokens=kwargs.get("max_tokens", 2048),
temperature=kwargs.get("temperature", 0.1),
)
return response.choices[0].message.content
elif self.provider == LLMProvider.OLLAMA:
import requests
all_messages = []
if system:
all_messages.append({"role": "system", "content": system})
all_messages.extend(messages)
response = requests.post(
"http://localhost:11434/v1/chat/completions",
json={"model": self.model, "messages": all_messages}
)
return response.json()["choices"][0]["message"]["content"]
# Изменить провайдера — одна строка
client = UnifiedLLMClient(LLMProvider.ANTHROPIC, "claude-haiku-4-5")
# → client = UnifiedLLMClient(LLMProvider.OPENAI, "gpt-4o-mini")
Почему прямой перенос промптов не работает?
OpenAI использует system message как часть контекста, Claude — отдельный параметр с большим весом. Если просто скопировать текст, Claude может игнорировать часть инструкций из-за отсутствия XML-разметки. Кроме того, GPT и Claude по-разному интерпретируют роли: в GPT можно указать "role": "system", в Claude система задаётся вне массива сообщений. Без адаптации вы рискуете получить формальные, шаблонные ответы или потерю контекста.
Как ускорить миграцию без потери качества?
Наш подход — автоматизация через LLM-as-judge и PromptAdapter. Мы собираем 50-100 реальных запросов из продакшена, прогоняем через обе модели и оцениваем качество. Если средняя оценка ниже 0.8, адаптируем промпты. Это позволяет выявить проблемные кейсы за день вместо недели ручного тестирования. Для типовых задач мы используем шаблоны адаптации, что ускоряет процесс в 2-3 раза.
Типичные ошибки при миграции
- Копирование system prompt без XML-тегов для Claude.
- Игнорирование разницы в окне контекста (обрезка текста без адаптации).
- Пропуск тестирования нестандартных кейсов (длинные диалоги, tool calls).
- Отказ от fallback-стратегии на случай сбоев.
Что входит в услугу
| Этап | Что делаем | Результат |
|---|---|---|
| Анализ | Изучаем текущую архитектуру, собираем 50-100 тестовых запросов | Отчёт о совместимости |
| Адаптация промптов | Конвертируем system prompts и tool calls | Адаптированные промпты, протестированные на тестовых кейсах |
| Разработка клиента | Внедряем UnifiedLLMClient с поддержкой fallback | Единый интерфейс для всех провайдеров |
| A/B тест | Запускаем 5% трафика на новую модель | Сравнение метрик качества и стоимости |
| Rollout | Поэтапно увеличиваем долю новой модели до 100% | Стабильная работа на новой LLM |
| Документация и обучение | Описываем процесс переключения провайдера, обучаем команду | Документация и workshop |
| Поддержка | 2 недели пост-миграционного мониторинга | Быстрое решение возможных проблем |
Сроки
- Анализ совместимости + тестирование: 1 неделя
- Адаптация промптов + инструментов: 1 неделя
- A/B тест в production + rollout: 1–2 недели
Чеклист миграции
| Шаг | Действие | Критичность |
|---|---|---|
| 1 | Собрать 50–100 тестовых запросов из production | Обязательно |
| 2 | Провести A/B сравнение через LLM-as-judge | Обязательно |
| 3 | Адаптировать system prompts | Обязательно |
| 4 | Конвертировать format tool calls | Обязательно |
| 5 | Обновить обработку ошибок (разные error codes) | Обязательно |
| 6 | Настроить retry/fallback | Рекомендуется |
| 7 | Обновить cost monitoring | Рекомендуется |
| 8 | A/B тест в production (5% трафика) | Рекомендуется |
Свяжитесь с нами для бесплатной оценки вашего проекта. Мы проанализируем текущую архитектуру и предложим план миграции под ваш бюджет. При миграции с нами вы получаете гарантию сохранения качества и поддержку на всех этапах.







