Миграция с одной 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% трафика) | Рекомендуется |
Свяжитесь с нами для бесплатной оценки вашего проекта. Мы проанализируем текущую архитектуру и предложим план миграции под ваш бюджет. При миграции с нами вы получаете гарантию сохранения качества и поддержку на всех этапах.







