Миграция с одной LLM на другую: GPT → Claude → Open Source
Миграция LLM — не просто замена API endpoint. Каждая модель имеет разные сильные стороны, форматы промптов, ограничения и поведение. Неправильная миграция ухудшает качество; правильная позволяет снизить стоимость в 5–10× или улучшить качество при той же стоимости.
Анализ совместимости
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")
Чеклист миграции
| Шаг | Действие | Критичность |
|---|---|---|
| 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% трафика) | Рекомендуется |
Сроки
- Анализ совместимости + тестирование: 1 неделя
- Адаптация промптов + инструментов: 1 неделя
- A/B тест в production + rollout: 1–2 недели







