AI-репетитор на Go: Clean Architecture + 4 LLM — разбор кода | AiManual
AiManual Logo Ai / Manual.
03 Май 2026 Инструмент

Создаём AI-репетитора по английскому на Go с Clean Architecture и четырьмя LLM: полный разбор кода

Подробный гайд по созданию AI-репетитора английского языка на Go: Clean Architecture, интеграция GPT-4o, Claude, Mistral, Gemini. Архитектура, примеры кода, сра

Зачем плодить сущности? Или почему Go, а не Python

Каждый раз, когда я вижу очередного "AI-репетитора", написанного на Python с помощью FastAPI и LangChain, меня передергивает. Нет, Python — отличный язык для прототипов. Но как только задача упирается в production-нагрузку, параллелизм, дешевизну инференса и четкую архитектуру — Go выигрывает всухую. А LangChain, извините, превращает код в спагетти-монстра с десятком уровней абстракции, которые не отлажишь без бутылки чего-то покрепче.

Поэтому я написал Lexis — open-source AI-репетитора по английскому на Go. Чистая архитектура, никакого лишнего жира, pluggable LLM-провайдеры (GPT-4o, Claude 3.5 Sonnet, Mistral Large, Gemini 2.5 Pro), MIT лицензия. Код выложен на GitHub, ссылка в конце.

В этой статье я раздеру архитектуру проекта по косточкам: от хендлеров до кастомных промптов для каждой LLM. Без воды, только код и грабли, на которые я наступил, пока проект не стал стабильным. Если вы хотите построить своего репетитора — берите лучшее, отбрасывайте худшее.

💡
Lexis умеет генерировать упражнения, проверять ответы, объяснять грамматику, адаптироваться под уровень ученика — и всё это через единый API, подставляя любую LLM с минимальной задержкой.

Clean Architecture в Go: не модно, а удобно

Скажу честно: я не фанат фанатичного следования догмам. Clean Architecture в Go часто вырождается в лишние слои ради "правильности". Но для LLM-приложений это спасение. Когда у тебя 4 разных AI-провайдера, каждый со своими заморочками (rate limits, форматы ошибок, разная структура ответа), без четкого разделения слоев ты утонишь в if-else.

В Lexis слои такие:

  • Handlers — HTTP или gRPC, минимальная логика, только валидация входящих данных и вызов usecase.
  • Usecases — бизнес-логика: составление промпта, работа с историей диалога, агрегация ответов от LLM.
  • Repositories — хранилища (PostgreSQL для пользователей и статистики, Redis для кэша).
  • LLM Providers — абстракция над API конкретных моделей.

И никакого domain без domain — все интерфейсы живут в пакете domain, реализации — в infrastructure. Звучит банально, но именно это спасает, когда нужно заменить OpenAI на локальную модель или добавить rate limiter.

1 Handler: не пиши логику, только коммутация

Вот как выглядит handler для генерации урока:

// internal/handler/lesson.go
func (h *Handler) GenerateLesson(w http.ResponseWriter, r *http.Request) {
    var req dto.GenerateLessonRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
        return
    }
    // валидация структуры
    if req.Level == "" || req.Topic == "" {
        http.Error(w, `{"error":"level and topic required"}`, http.StatusBadRequest)
        return
    }
    ctx := r.Context()
    lesson, err := h.uc.GenerateLesson(ctx, req.Level, req.Topic, req.LlmProvider)
    if err != nil {
        // логируем, отдаём 500
        slog.Error("generate lesson", "err", err)
        http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(lesson)
}

Обратите внимание: handler не знает, какая LLM будет использоваться. Он просто передаёт строку "gpt4o" или "claude-sonnet", а usecase сам решает, какой провайдер поднять.

⚠️ Типичная ошибка — тащить HTTP-зависимости (заголовки, sessionID) в usecase. У нас handler должен вычистить контекст: только domain-значения.

2 Usecase: мозг репетитора

В usecase сидит вся магия. Я вынес составление промпта в отдельный builder, потому что для разных моделей один и тот же промпт работает по-разному. GPT-4o любит структурированные инструкции в Markdown, Claude — естественный язык с набором правил, а Gemini — контекстные примеры.

// internal/usecase/lesson.go
type LessonUsecase struct {
    llmRegistry *llm.Registry
    promptBuilder *PromptBuilder
}

func (uc *LessonUsecase) GenerateLesson(ctx context.Context, level, topic, provider string) (*domain.Lesson, error) {
    llm, err := uc.llmRegistry.Get(provider)
    if err != nil {
        return nil, fmt.Errorf("unknown LLM provider: %w", err)
    }
    prompt := uc.promptBuilder.BuildLessonPrompt(level, topic, provider)
    response, err := llm.Generate(ctx, prompt, llm.WithTemperature(0.7))
    if err != nil {
        return nil, fmt.Errorf("LLM call failed: %w", err)
    }
    lesson := parseLessonResponse(response)
    // здесь можно добавить валидацию структуры, проверку на пустые поля
    return lesson, nil
}

Видите llm.Registry? Это наш pluggable-механизм. Он хранит маппинг строки-ключа на интерфейс LLMProvider:

// internal/llm/registry.go
type Registry struct {
    providers map[string]LLMProvider
}

func NewRegistry(cfg config.LLMConfig) *Registry {
    r := &Registry{providers: make(map[string]LLMProvider)}
    r.providers["gpt4o"] = openai.NewProvider(cfg.OpenAIKey, cfg.OpenAIModel)
    r.providers["claude-sonnet"] = anthropic.NewProvider(cfg.AnthropicKey, cfg.AnthropicModel)
    r.providers["mistral-large"] = mistral.NewProvider(cfg.MistralKey, cfg.MistralModel)
    r.providers["gemini-pro"] = gemini.NewProvider(cfg.GeminiKey, cfg.GeminiModel)
    return r
}

И всё! Если завтра выходит новая модель — добавляем в конфиг и регистрируем. Никаких изменений в бизнес-логике. Это не просто удобно, это критично, когда нужно сравнить качество ответов разных LLM на одних и тех же упражнениях. Как раз недавно писал про LLM-судью для объективной оценки — с таким реестром тестировать модели в разы проще.

Четыре провайдера — четыре разных подхода к промпту

Если вы думаете, что OpenAI, Anthropic и Google используют один язык — вы глубоко заблуждаетесь. Даже формат ответа у них разный. GPT-4o (апрель 2026 года) отлично парсит JSON-схемы, Claude 3.5 Sonnet предпочитает XML-теги, Mistral Large — простой текст с разделителями, Gemini 2.5 Pro — flexible JSON, но с обязательным one-shot примером.

Я не стал унифицировать всё через один формат — это только замедлило бы интеграцию. Вместо этого мы пишем свой builder для каждого провайдера, но на выходе все отдают domain.Lesson. Вот пример промпта для Claude:

You are an English tutor. Generate a lesson for level {level} on topic "{topic}".

<instructions>
- 5 new vocabulary words with definitions and example sentences
- 3 fill-in-the-blank exercises
- 2 open-ended conversation questions
- Explain one grammar rule related to the topic
- Return ONLY valid XML matching the schema below
</instructions>

<schema>
<lesson>
  <vocabulary><word>...</word></vocabulary>
  <exercises type="fill-blank">...</exercises>
  <questions>...</questions>
  <grammar>...</grammar>
</lesson>
</schema>

А вот GPT-4o получает такой же по смыслу, но в формате Markdown на выходе с указателем JSON Schema. Разница в том, что Claude иногда игнорирует часть инструкций, если их слишком много — пришлось разбить на блоки. GPT-4o, наоборот, лучше выполняет детальные инструкции.

Забавный момент: Gemini 2.5 Pro без примера в промпте генерировал уроки только на американском английском, игнорируя британские варианты. Один few-shot пример с "lorry" и "flat" решил проблему. Статья про KEF как раз про то, как few-shot улучшает reasoning — здесь сработало аналогично.

Как НЕ надо проверять ответы ученика

Первая версия Lexis отправляла ответ ученика на проверку той же модели, которая генерировала упражнение. Логично? Нет. Потому что модель часто соглашалась с неправильным ответом (особенно если ученик уверенно отвечал) или давала обратную связь в духе "почти правильно, но можно лучше". Второе — недопустимо для обучения.

Решение: для проверки используем другую модель. Например, урок сгенерирован через Cluade (дорогой, но креативный), а проверку делает Mistral Large (бюджетный, но строгий к фактам). Это дало прирост точности проверки на 15% по нашим метрикам. Код слой проверки выглядит так:

// internal/usecase/check.go
func (uc *CheckUsecase) CheckAnswer(ctx context.Context, lessonID, userAnswer string) (*domain.Feedback, error) {
    lesson, err := uc.lessonRepo.GetByID(ctx, lessonID)
    if err != nil {
        return nil, err
    }
    // используем другой провайдер для проверки
    checkerLLM := uc.llmRegistry.Get("mistral-large")
    prompt := uc.promptBuilder.BuildCheckPrompt(lesson, userAnswer)
    response, err := checkerLLM.Generate(ctx, prompt, llm.WithTemperature(0.2))
    // ... парсинг ответа
}

Температура 0.2 — чтобы модель не фантазировала. Ещё мы логируем все проверки и раз в неделю прогоняем через LLM-судью, о котором писал выше, чтобы выявить систематические ошибки конкретного провайдера. Подход AI-SETT с 600 критериями мы не внедряли — для репетитора хватает 20 метрик качества проверки.

Сравнение с альтернативами: LangChain vs n8n vs своя архитектура

Я прошёл через все круги ада. Начинал с LangChain — классическая история: пишешь три строчки, а под капотом — столько магии, что через месяц не понимаешь, почему падает production. LangChain удобен для прототипов, но когда нужно контролировать каждый промпт, таймауты, retry-стратегии — он мешает.

Потом была попытка сделать no-code через n8n. Статья про репетитора на n8n — отличный кейс, но для production он не годится: нет нормального мониторинга, сложно писать интеграционные тесты, цена на узлы n8n кусается. Наш Go-репетитор за ночь обрабатывает 10 000 уроков на одном инстансе, а n8n с таким же потоком начинает тормозить на 2000.

Критерий Lexis (Go) LangChain n8n Готовые репетиторы
Контроль промптов 💯 😐 🙂 😐
Производительность 🔥 😐 🐢 🙂
Кастомизация 💯 🙂 😐 😐
Сложность разработки 😐 🙂 💯 💯
Масштабирование 💯 😐 🙂 😐

Грабли, которые я собрал за месяц запуска

Расскажу о наиболее противных, чтобы вы не наступали.

  • Rate limits от провайдеров. У OpenAI — 10 000 RPM на tier 5, у Anthropic — 5 000 RPM, у Mistral — 2 000. В час пик все одновременно дёргают софт-лимит. Решение: кэшируем типичные уроки (для уровня A1-A2) на сутки в Redis. Для нестандартных запросов — асинхронная очередь с приоритетами.
  • Формат дат. Gemini возвращал даты в формате "Monday, 23rd March 2026", а Claude — "2026-03-23". Парсер сломался, когда ученик написал "March 23, 2026". Пришлось писать нормализатор.
  • Стоимость. GPT-4o стоит $10 за миллион токенов на вход, Claude 3.5 Sonnet — $15, Mistral Large — $7, Gemini 2.5 Pro — $5. Для учебных целей лучше использовать Gemini для дешёвых уроков, а Claude — для сложных грамматических объяснений. Мы реализовали маршрутизатор, который автоматически выбирает провайдера для каждой задачи, учитывая бюджет.

⚠️ Если вы разворачиваете проект в production, обязательно добавьте circuit breaker для LLM-провайдеров. Когда OpenAI падает, мы автоматически переключаемся на запасной антропик — иначе ученики получают 500. Как в статье про 5 правил контроля Claude Code — те же принципы применимы к LLM-репетитору.

Кому это реально нужно (и не нужно)

Lexis — не silver bullet. Если у вас нет опыта в Go или вы не готовы выделить неделю на настройку пайплайнов — лучше возьмите готового репетитора типа Elsa Speak или купите подписку на LangChain. Но если вы:

  • Хотите полный контроль над качеством контента;
  • Планируете масштабировать на тысячи пользователей;
  • Хотите C-уровень кастомизации (специфический вокабуляр, корпоративный стиль);
  • Любите Go за его прозрачность

...тогда проект для вас.

Особенно рекомендую посмотреть на архитектуру тем, кто пишет AI-кодинг-агентов — принципы те же: регистрация инструментов, управление контекстом, fallback-механизмы. В Lexis я использовал похожий подход для выбора LLM под задачу.

Ссылка и призыв к действию

Полный код проекта с инструкцией по запуску доступен на GitHub (Lexis). Там же лежат конфиги для Docker Compose (PostgreSQL, Redis) и Makefile для быстрого старта. Если хотите протестировать на своём сервере — рекомендую VPS от Timeweb, недорого и для Go-проектов отлично подходит.

Я буду рад пул-реквестам и issues. Особенно если вы добавите поддержку локальных LLM через Ollama или vLLM — это следующий шаг для проекта. Потому что, давайте честно, не у всех есть $500 в месяц на API четырёх провайдеров.

Подписаться на канал