textmachine/docs/research/10-vojo-ai-bot-review.md
Claude (backend session) 9bb00d49f2 Initial commit: documentation, eval polygon, backend step 0 verdict
TextMachine project repository (AI translation of literary books).
Includes: v2 architecture decisions, MVP plan, research 01-12,
polygon experiments 01-03, backend-session revalidation verdict
(03-implementation-notes.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:50:59 +03:00

34 KiB
Raw Blame History

Ревью кодовой базы vojo/apps/ai-bot (Go) и оценка переиспользования для TextMachine

Дата: 2026-07-04. Источник: локальный код /home/ubuntu/projects/vojo/apps/ai-bot (интернет не использовался; все утверждения о внешних API — из комментариев кода и помечены при необходимости «(не проверено)»).

TL;DR

  1. Кодовая база зрелая и на удивление хорошо спроектирована для «бота»: ~10 600 строк Go (из них ~3 850 — тесты, 36%), 125+ тест-функций, минимум зависимостей (pgx, goldmark, bluemonday, yaml — всё), Go 1.25, статическая CGO-free сборка в distroless.
  2. Ядро LLM-инфраструктуры провайдеро-нейтрально и переносимо почти как есть: интерфейс LLMClient + нейтральные типы (llm.go, 83 строки), общий OpenAI-совместимый HTTP-транспорт с retry/backoff и самолечением 400-ошибок (httpllm.go), тонкие адаптеры xAI/Gemini/локального сервера (6090 строк каждый).
  3. Failover «локальная GPU → облако» уже написан и оттестирован (failover.go): circuit breaker + активные health-пробы GET /models, анти-flapping, «терминальный 4xx не маскируется облаком». Это ровно сценарий TextMachine (8GB VRAM дома + облачные API).
  4. Учёт денег — самая ценная часть: таблица цен per-model (pricing.go), CostBreakdown по компонентам, биллинг по фактическому usage API (включая reasoning-токены, которые xAI считает ПОВЕРХ completion_tokens — их пропуск занижал расход на 3044%), и ledger в Postgres с резервированием бюджета до вызова и settle после (TOCTOU-защита потолка расходов через advisory lock).
  5. Телеметрия production-уровня: request_log (43 колонки, 8 идемпотентных миграций) — маршрут, $/компонент, латентность по стадиям, degrade-причины, фидбек пользователя; trace_id (W3C/OTel-форма) через context + обёртка slog-хендлера. Пишется асинхронно, никогда не роняет ответ.
  6. Паттерн «чистое ядро решений + оффлайн-реплей» (internal/routedecide + cmd/routereval): роутинг вынесен в пакет без I/O, и golden-set гоняется через ТОТ ЖЕ код, что и прод, со свипом порогов. Прямо переносится на QA-оценку перевода в TextMachine.
  7. Чего нет: стриминга (везде stream:false), нативного Anthropic-адаптера, tool-calling (кроме web_search), мультимодальности, batch API, очередей/durable jobs, пакетной структуры (почти всё в package main). Это и есть основной объём дописывания.
  8. Рекомендация по языку — Go. Реально переиспользуемых ~2 0002 500 строк проверенного кода + идеальное соответствие задаче (тысячи параллельных HTTP-вызовов, один статический бинарь, дешёвая конкурентность). C++/userver не даёт ни переиспользования, ни скорости разработки Claude-ом; Python/TS проигрывают на долгоживущем конкурентном оркестраторе как продукте.

1. Что это за система

ai-bot — Matrix-бот (Synapse appservice), отвечающий через xAI Grok, с опциональным каскадом: двухслойный роутер (regex + дешёвый Gemini-классификатор) выбирает маршрут (trivial_direct / grok_direct / web_then_grok / reason_then_grok / project_then_grok), любой сбой любого слоя деградирует к прямому вызову Grok («никогда молчание»). Есть web-grounding (два провайдера: xAI web_search и Gemini native google_search с верификацией цитат), локальный LLM-бэкенд (ollama/llama.cpp за SSH-туннелем) как бесплатная «первая нога» с прозрачным фейловером в облако, жёсткий дневной потолок расходов в USD и полная телеметрия.

Объём: ~6 760 строк не-тестового кода. Крупнейшие файлы: bot.go (1 273 — Matrix-логика), config.go (695), store.go (620), cascade.go (562).

2. (a) Архитектура: адаптеры, интерфейс, роутинг/каскад/фейловер

2.1 Провайдеро-нейтральный шов (llm.go)

Весь бизнес-код зависит от одного интерфейса и нейтральных типов:

type LLMClient interface {
    Complete(ctx context.Context, req LLMRequest) (*LLMResponse, error)
}
  • LLMRequest: Model, Messages (role/content — только текст), MaxTokens, Temperature, Tools, ReasoningEffort, ConvID (хинт prompt-кэша, уходит заголовком), JSONOnly (response_format: json_object).
  • LLMResponse: Text, Usage, Model — модель, которая ФАКТИЧЕСКИ ответила (может отличаться от запрошенной при фейловере; биллинг и телеметрия следуют за ответившей, иначе бесплатный локальный ответ книжится по облачной цене), ProviderRequestID.
  • Usage: PromptTokens, CachedTokens (подмножество prompt), CompletionTokens, ReasoningTokensс явно задокументированной семантикой: у xAI reasoning-токены аддитивны к completion_tokens и биллятся по output-ставке; у провайдеров с subset-семантикой (ollama, OpenAI-спека) адаптер обязан оставлять 0, чтобы не задвоить биллинг. Это тонкое место, которое в vojo уже «оплачено» реальным недоучётом 3044% расходов.

2.2 Общий транспорт (httpllm.go, 299 строк)

Один HTTP-клиент для всех OpenAI-совместимых провайдеров: сборка /chat/completions, классификация ошибок (429/5xx/сетевые = retryable с экспоненциальным backoff 0.5s→8s + jitter; прочие 4xx = терминальные), per-attempt дедлайн 60s внутри общего бюджета запроса, типизированная httpStatusError (фейловер классифицирует ошибки структурно, не по тексту). Умные детали: 2xx с пустым контентом — успех (вызов оплачен, деньги книжатся); самолечение reasoning_effort-несовместимости — модель, вернувшая 400 на параметр, запоминается, параметр срезается и запрос повторяется один раз, WARN один раз.

2.3 Адаптеры

  • provider_xai.go (66 строк) — маппинг нейтральных типов на wire, заголовок x-grok-conv-id.
  • provider_gemini.go (240 строк) — два лица: OpenAI-compat Complete + нативный v1beta generateContent c google_search и verify-gate по цитатам (нет groundingChunks ⇒ ответ не заземлён ⇒ ошибка ⇒ деградация). Ключ — в заголовке x-goog-api-key, а не в query (иначе *url.Error утёк бы секретом в лог), запрет редиректов как анти-exfil.
  • provider_local.go (91 строка) — адаптер любого OpenAI-совместимого локального сервера (ollama/llama.cpp/vLLM/LM Studio): своя модель (не имена каскада), своя температура (request-температура перебивает Modelfile), свой max_tokens (у ollama лимит покрывает thinking+ответ вместе, в отличие от xAI), пустой ключ = без Authorization.

2.4 Роутинг и каскад

  • Layer-0 — бесплатные regex-эвристики RU+EN (internal/routedecide, чистый пакет без I/O): freshness-слова → web, приветствия/арифметика → trivial, «lookup-intent» — мягкий хинт.
  • Layer-1 — Gemini-классификатор с эпистемологическим промптом (не «тема», а «навредит ли ответ по памяти»): JSON-вердикт needs_web/verifiable/entity_obscure/time_sensitive/trivial/about_project/search_query/confidence, суб-дедлайн 4s, любой сбой → вердикт Layer-0.
  • Combine (чистая функция с параметризуемыми порогами) сводит оба слоя; порядок веток даёт атрибуцию web_decided_by для аналитики. Классификатору не доверяют слепо: trivial требует согласия обоих слоёв, needs_web — порога уверенности и «verifiable».
  • Каскад (cascade.go): диспетчеризация по маршруту, каждая ветка при ошибке деградирует к genGrokDirect с «честными» хеджами (staleness-каведж для recency-промаха, abstain-инструкция для промаха проверяемого факта — чтобы модель не выдала уверенную галлюцинацию). Частично исполненный каскад книжит фактически потраченное.
  • Фейловер (failover.go): декоратор над LLMClient — локальная нога под своим таймаутом, на transport/5xx/timeout/429 → облако в том же запросе + trip брейкера; восстановление только по пробам (2 подряд после request-trip, анти-flapping); терминальный 4xx локальной ноги (неверный тег модели) падает громко, облаком не маскируется; одноразовый WARN «локальный бэкенд ни разу не поднялся с boot».

Оценка: это учебниково-правильная композиция декораторов над одним интерфейсом. Каскад, правда, «зашит» под конкретные 5 маршрутов чат-бота — для TextMachine логика маршрутов (переводчик→редактор→судья) будет иной, но каркас «маршрут → исполнение → деградация → учёт» переносится.

3. (b) Телеметрия и учёт токенов/стоимости — готовность к переиспользованию: высокая

  • pricing.go: ModelPrice{InputPerM, CachedPerM, OutputPerM} в Config.Prices (паттерн LiteLLM), priceFor(model) с fallback на дефолтную модель (никогда $0 — «$0 ослепил бы потолок»); CostBreakdown{Token, Grounding, WebTool, Router, GroundingFee} с вычисляемым Total().
  • computeUSD (bot.go): цена по фактическому usage API; (promptcached)·in + cached·cachedIn + (completion+reasoning)·out.
  • Резервирование (store.go Reserve/Settle/ReleaseReservation/RefundRequest): до вызова книжится оценка максимальной стоимости включённого маршрута (reserveEstimate), потолок проверяется по committed + reserved под advisory-lock на день (иначе burst конкурентных запросов проскочил бы потолок — деньги ложатся только после ответа); после ответа Settle атомарно снимает резерв и книжит факт по компонентам. Отдельно: возврат слота запроса без возврата денег (2xx оплачен, даже если ответ не доставлен), refund квоты grounding при пустом результате.
  • telemetry.go + миграции v3v8: одна строка request_log на запрос — маршрут, источник решения, confidence, JSONB models/stage_ms, токены (включая reasoning), $ по 5 компонентам, latency, degrade-причина (стабильные токены для GROUP BY), сигналы классификатора, цитаты, prompt_version (хэш ВСЕЙ поведенческой поверхности промптов — системного, классификатора, хеджей), reply_event_id + эмодзи-фидбек пользователя как сигнал качества. Запись асинхронна (safego с recover), отказ пишет WARN и никогда не роняет ответ; text-поля — только под отдельным флагом (privacy by default); ретеншн-трим раз в 200 записей.
  • trace.go + logging.go: trace_id — 16 случайных байт в hex (форма W3C/OTel), кладётся в context один раз на запрос; contextHandler — обёртка slog, автоматически штампующая trace_id на каждую строку *Context-логов (с корректным re-wrap в WithAttrs/WithGroup — типовая ошибка обёрток тут учтена). Тела LLM-обменов логируются только по allowlist пользователей И на DEBUG, без URL/заголовков (ключ не утечёт).

Для TextMachine это переиспользуется процентов на 80: понадобится (1) добавить в Usage/ModelPrice стоимость записи в кэш (у Anthropic cache write дороже input — 1.25x/2x в зависимости от TTL (не проверено, сверить с актуальным прайсом)), (2) заменить ось учёта «пользователь/день» на «проект/книга/глава/роль агента», (3) добавить в request_log аналоги: chapter_id, agent_role, счёт итераций редактуры.

4. (c) Хранилище (store.go)

Postgres через pgx/v5 (pool MaxConns=4), таймаут 10s на любую операцию. Схема — 8 версионированных идемпотентных миграций под advisory-lock (schema_version + CREATE TABLE/ADD COLUMN IF NOT EXISTS), самонакатываются при старте. Таблицы:

  • processed_txn / processed_event — дедуп с LRU-обрезкой (5k/20k записей): идемпотентность «at most once» через INSERT … ON CONFLICT DO NOTHING + RowsAffected.
  • spend(date, mxid, requests, usd, router_usd, grounding_usd, webtool_usd, reserved_usd) — дневной ledger с резервированием.
  • request_log — аналитика (43 колонки, см. выше), индексы по ts и reply_event_id.
  • grounding_count — дневная квота grounding: check-and-increment одним стейтментом (INSERT … ON CONFLICT … WHERE n < cap RETURNING), гонки невозможны.
  • warned_encrypted — Matrix-специфика.

Контента сообщений в БД нет (by design). Качество SQL высокое: атомарные однооператорные апдейты с GREATEST(0, …) против отрицательных значений, комментарии объясняют каждое решение о блокировках. Для TextMachine схема почти вся не подходит по смыслу (нужны проекты/главы/глоссарий/память), но паттерны (миграции, advisory-lock, атомарные квоты, reserve/settle) — прямо в перенос.

5. (d) Качество кода и тесты

Качество — заметно выше среднего. Плотнейшие пояснительные комментарии с обоснованием каждого решения и отсылками к инцидентам («вот из-за этого была многоминутная тишина»), fail-fast валидация конфига (несовместимые флаги отказывают в старте), секреты через *_FILE, никакой lock не держится через сетевые вызовы, панк-recovery вокруг фоновых горутин, privacy-by-default в логах и телеметрии, prompt-injection-споттинг (<DATA>-маркеры вокруг веб-дайджеста, OWASP LLM01). Зависимости — 5 прямых. Минусы: почти всё в package main (кроме internal/routedecide) — как библиотека не импортируется, только копируется; конфиг — один «толстый» структ на 60+ полей; нет CI-конфига в каталоге; rand.Intn для jitter (без seed — в Go 1.20+ ок).

Тесты: 17 файлов, ~3 850 строк, 125+ тест-функций. cascade_test.go — 29 тестов (деградации, частичный биллинг, хеджи), store_test.go — 19 (включая конкурентную гарантию per-user cap и durability через рестарт; требуют Postgres через AI_BOT_TEST_DATABASE_URL, иначе skip), failover_test.go — 12 (брейкер, терминальные 4xx, пустой ответ локальной ноги), config_test.go — 15, транспорт — через httptest с реальными HTTP-раундтрипами, LLM — через fakeLLM-дублёры интерфейса. README заявляет go vet/gofmt clean; в текущем окружении Go toolchain не установлен, прогнать go test не удалось (не проверено), но структура тестов и golden-set-харнесс говорят о рабочей дисциплине.

Отдельно ценен cmd/routereval — оффлайн-реплей golden-набора через прод-функции решения с confusion matrix и метриками (misroute, false-web, trivial-leak, «lie-metric»), со свипом порогов флагами. Это готовый шаблон для eval-гейта качества перевода.

6. (e) Что конкретно переиспользовать, что переписать

Брать почти как есть (адаптация < 20%)

Артефакт Строк Что менять
llm.go — типы + LLMClient 83 + стриминг (интерфейс CompleteStream или каналы), + multi-part content, + TopP/StopSequences, + системные блоки
httpllm.go — транспорт+retry 299 + поддержка SSE-стриминга; вынести max_tokensmax_completion_tokens маппинг per-provider (новые OpenAI-модели требуют второй вариант (не проверено))
failover.go — локально-облачный декоратор 234 почти ничего; probe URL и пороги — уже конфиг
provider_local.go 91 ничего существенного
pricing.go + computeUSD ~70 + CacheWritePerM, + тиры по длине контекста (Gemini >200k (не проверено))
trace.go + logging.go 180 ничего; это идиома userver, знакомая владельцу
store.go: механизм миграций, reserve/settle, атомарные квоты ~200 схему таблиц — под домен TextMachine
telemetry.go — паттерн async request_log 154 колонки под пайплайн перевода
Паттерн routedecide + routereval ~470 сам код не нужен, нужен паттерн «чистое ядро + golden-реплей»
config.go — env-парсинг, fail-fast, *_FILE-секреты, Summary с redaction ~400 поля под TextMachine

Итого прямо переносимого проверенного кода: ~2 0002 500 строк — это и есть весь «скучный» инфраструктурный риск LLM-бэкенда (retry, деньги, трейсинг, фейловер), уже отлаженный на проде.

Писать заново

  1. Пакетная структура: разнести package main на pkg/llm, pkg/pricing, pkg/ledger, pkg/telemetry и т.д.
  2. Anthropic-адаптер (нативный Messages API): система блоков, cache_control (эксплицитный кэш — критично для экономики TextMachine: system-промпт + глоссарий + резюме предыдущих глав кэшируются), tool_use, thinking-блоки. OpenAI-compat слоя vojo недостаточно.
  3. Стриминг — в vojo отсутствует полностью; для IDE-фронтенда обязателен.
  4. Batch API (Anthropic/OpenAI 50% стоимости (не проверено, сверить)) — для оффлайн-перевода глав это главный рычаг себестоимости; в vojo концепции нет.
  5. Оркестратор пайплайна: у vojo модель конкурентности — «одна горутина на комнату + in-memory буферы + single-flight»; рестарт теряет in-flight работу. TextMachine нужны durable jobs (глава = job, стадии = переводчик→редактор→судья, resume после падения, ретраи, приоритеты) — очередь поверх Postgres (напр. river) или своя таблица jobs; это новый код.
  6. Домен: память/глоссарий/консистентность имён, сегментация текста, диффы редактур, чекпоинты книги — ничего этого в vojo нет и быть не могло.
  7. Хранилище контента: vojo принципиально не хранит текст; TextMachine — наоборот, текст и есть данные (главы, варианты, память). SQLite (modernc.org/sqlite, CGO-free) для локального режима + тот же SQL-слой на Postgres для продакшена.
  8. 18+/цензура: маршрутизация «этот фрагмент не пройдёт у провайдера X → локальная abliterated-модель / другой провайдер» — в vojo есть только зачаток (abliterated Qwen3 как локальная модель и фейловер), политику придётся строить самим.

7. (f) Выбор языка бэкенда: Go vs C++/userver vs Rust vs Python/TS

Вводные: код пишет в основном Claude; владелец лучше всего знает C++/userver; профиль нагрузки — I/O-bound оркестратор (сотни-тысячи параллельных HTTP-вызовов к LLM, ожидание по 10120 с), SQLite/Postgres, очереди, стриминг; продукт коммерческий, деплой должен быть дешёвым.

Go — рекомендация.

  • ~22.5k строк готовой, проверенной прод-нагрузкой инфраструктуры из этого репозитория (транспорт, retry, фейловер на локальную GPU, прайсинг, ledger, телеметрия, trace) — 36 недель работы, которые не надо повторять и повторно отлаживать (недоучёт reasoning-токенов, TOCTOU потолка, flapping брейкера — всё уже поймано).
  • Goroutines + context — идеальная модель для «тысяча параллельных вызовов с дедлайнами и отменой»; в кодовой базе видно, что она уже освоена правильно (никаких lock-через-I/O, single-flight, safego).
  • Claude генерирует Go стабильно и дёшево в ревью: язык маленький, ошибки видны компилятором, нет UB, рефакторинги безопасны. Один статический бинарь → деплой и себестоимость инфраструктуры минимальны.
  • SQLite без CGO (modernc), pgx, SSE — всё есть; официальные SDK Anthropic/OpenAI для Go существуют (anthropic-sdk-go (не проверено, актуальность)), а собственный тонкий транспорт уже написан.

C++/userver — не рекомендуется, несмотря на экспертизу владельца.

  • Ноль переиспользования этой базы; экосистема LLM-клиентов/JSON/SDK несравнимо тоньше.
  • Главный аргумент «владелец знает язык» ослаблен собственной вводной: код пишет Claude. Для Claude C++ — самый дорогой язык поддержки: UB, время сборки, управление зависимостями (userver и его окружение — тяжёлая сборка), а корпус userver-кода в обучающих данных мал — качество генерации будет заметно ниже, чем на Go/Python/TS.
  • Производительность C++ здесь не монетизируется: узкое место — ожидание LLM API, а не CPU. Знание userver останется полезным на уровне ревью идиом (trace-id через контекст в vojo прямо назван «the userver idiom» — концепции совпадают).

Rust — второй по адекватности, но минусы перевешивают: безопасность и скорость не нужны в этом профиле, компиляция и borrow-checker замедляют итерации (а итераций в исследовательском продукте будет много), переиспользования нет. Оправдан был бы, если бы планировался embedded-движок или WASM-ядро.

Python — оставить для оффлайн-евалов и экспериментов с промптами (богатейшая экосистема, ноутбуки), но не для ядра продукта: долгоживущий конкурентный сервис с очередями на asyncio + деплой с зависимостями + слабее типизация = дороже сопровождение и хуже маржа на инфраструктуре.

TypeScript — единственная реальная альтернатива Go: первоклассные SDK всех провайдеров, один язык с будущим IDE-фронтендом, event-loop нормально держит I/O-bound нагрузку. Против: нет переиспользования этой Go-базы, слабее модель отмены/дедлайнов (AbortController против context), рантайм тяжелее, у монолитных бэкендов на Node дисциплина типов и конкурентности требует больше ревью. Если бы Go-базы не существовало, выбор между TS и Go был бы спорным; с ней — нет.

Итог: Go для бэкенда-оркестратора; Python допустим как вспомогательный слой евалов; TypeScript — для фронтенд-IDE позже.

Выводы для TextMachine

  1. Стартовать бэкенд на Go, выпилив из vojo/ai-bot ядро в пакеты: llm.go+httpllm.go+адаптеры+failover.gopkg/llm; pricing.go+computeUSD+reserve/settle → pkg/ledger; trace.go+logging.gopkg/obs; механизм миграций → pkg/store. Это снимает главный инфраструктурный риск в первые же дни.
  2. Первым новым кодом писать Anthropic-нативный адаптер с cache_control и thinking-блоками — экономика художественного перевода живёт на кэшировании длинного контекста (глоссарий, память книги, стиль-гайд) и на дешёвых моделях с редкой эскалацией; каркас каскада/деградации из cascade.go — образец.
  3. Скопировать дисциплину денег целиком: биллинг по usage из ответа API, per-model таблица цен, резервирование до вызова, потолок на проект/день, request_log per-агент/per-глава. Для продукта с маржой это не «телеметрия», а ядро юнит-экономики; у vojo это уже добито до цента (сверка с cost_in_usd_ticks).
  4. Повторить паттерн routedecide/routereval для качества перевода: чистое ядро решений (эскалация к дорогой модели, вердикты судьи) + golden-set реплей со свипом порогов — тот же «offline-eval gate перед включением слоя», который vojo применяет к роутеру, TextMachine применит к качеству перевода.
  5. Фейловер локальной GPU уже готов: provider_local.go + failover.go покрывают сценарий «8GB VRAM дома обслуживает дешёвые роли (черновой перевод, классификация), облако — фейловер и дорогие роли»; семантика max_tokens (thinking внутри лимита у ollama) и температурных override уже учтена. Для 18+ контента локальная нога дополнительно снимает риск цензуры провайдера.
  6. Не тащить: Matrix-слой, каскад чат-маршрутов, схему spend/request_log буквально, in-memory модель конкурентности. Оркестрацию глав строить как durable jobs в Postgres/SQLite с resume — этого в vojo нет, и это основная новая разработка вместе с банком памяти/глоссарием.
  7. Учесть две ловушки биллинга из опыта vojo: (а) reasoning-токены — аддитивные у xAI, subset у ollama/OpenAI-спеки, у Anthropic thinking внутри output (не проверено) — семантику фиксировать per-adapter; (б) неизвестная модель никогда не должна стоить $0 (fallback на дефолтную цену).

Источники

Все — локальные файлы, прочитаны 2026-07-04:

  • /home/ubuntu/projects/vojo/apps/ai-bot/README.md — обзор, статус верификации, флаги.
  • /home/ubuntu/projects/vojo/apps/ai-bot/llm.go, httpllm.go, provider_xai.go, provider_gemini.go, provider_local.go — интерфейс и адаптеры.
  • /home/ubuntu/projects/vojo/apps/ai-bot/router.go, cascade.go, failover.go, web.go, internal/routedecide/routedecide.go, cmd/routereval/main.go — роутинг/каскад/фейловер/eval.
  • /home/ubuntu/projects/vojo/apps/ai-bot/pricing.go, telemetry.go, trace.go, logging.go, store.go, config.go, main.go, bot.go (фрагменты) — деньги, телеметрия, хранилище, конфиг.
  • Тесты: cascade_test.go, store_test.go, failover_test.go, httpllm_test.go, config_test.go и др. (17 файлов, ~3 849 строк).
  • go.mod — Go 1.25.0; зависимости: pgx/v5 v5.9.2, bluemonday, goldmark, x/net, yaml.v3.

Утверждения о внешних API (цены Anthropic cache write, batch 50%, max_completion_tokens, actual-модели) в этом документе взяты из комментариев кода vojo или общих знаний и помечены «(не проверено)» — перед использованием сверить онлайн.