Міграція з однієї LLM на іншу: GPT → Claude → Open Source
Уявіть: ваш сервіс на GPT-4 починає видавати нерелевантні відповіді після переходу на Claude. Або ви хочете перейти на Open Source моделі, щоб зменшити витрати, але боїтеся втратити якість. Ми стикалися з цим десятки разів. Наш досвід показує: правильно спланована міграція не лише зберігає, але й покращує якість завдяки кращій моделі під ваше завдання.
Розглянемо конкретний випадок: перехід з GPT-4 на Claude 3 Sonnet для чат-бота підтримки. Ми адаптували 150 промптів, переписали логіку виклику інструментів і провели A/B тестування. Результат — зниження вартості на 60% (з $5000/міс до $2000/міс) при зростанні 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% трафіку) | Рекомендується |
Чому ми? 5+ років на ринку, 20+ виконаних проєктів, гарантія збереження якості
Оцініть ваш проєкт безкоштовно — пишіть нам. Ми проаналізуємо поточну архітектуру та запропонуємо план міграції під ваш бюджет. При міграції з нами ви отримуєте гарантію збереження якості та підтримку на всіх етапах.







