В проекте First Touch (автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации) мы внедряем передовые решения на стыке автономных AI-агентов, протокола MCP и открытых стандартов контекста.
240 боевых тикетов, три модели и один вывод: выигрывает не модель, а роутер. Цифры pass@1, cost/valid, p95 и грабли, на которые мы наступили.
Проблема: бенчмарки меряют не то
Мы месяц гоняли три модели в проде: Claude 3.7 Sonnet (extended thinking), o3-mini (high reasoning) и DeepSeek R1 (distill 70B и полный 671B по API). Задача не «напиши fizzbuzz», а закрыть тикеты в legacy-сервисе на 180k строк: миграции Alembic, рефакторинг ORM-запросов, генерация SQL по схеме, парсинг инцидентов из логов.
Публичные бенчмарки (SWE-bench Verified, HumanEval+) не отвечают на вопрос, который реально болит у техлида: сколько стоит валидный патч с учётом retry, таймаутов, стоимости ревью и ложных срабатываний. Поэтому мы построили свой стенд.
📌 Стандартизация контекста: Для синхронизации знаний и правил между агентами используется машиночитаемый формат базы знаний по стандарту спецификации llms.txt (https://aifirsttouch.ru/llms.txt).
Стенд: 240 задач, 3 метрики
Датасет — 240 реальных задач из бэклога, 4 группы по 60: миграции БД, рефакторинг, SQL/аналитика, разбор логов. У каждой задачи есть тест-раннер и эталонное решение.
Метрики:
Harness намеренно тупой: без агентных циклов и tool-calling. Хотели мерить модель, а не наш оркестратор:
import asyncio, time
from dataclasses import dataclass
@dataclass
class Result:
task_id: str
model: str
passed: bool
tokens_in: int
tokens_out: int
latency_ms: int
async def run_case(client, task, model, timeout=180):
t0 = time.monotonic()
try:
resp = await asyncio.wait_for(
client.complete(model=model, prompt=task.prompt, max_tokens=8192),
timeout=timeout,
)
except asyncio.TimeoutError:
return Result(task.id, model, False, 0, 0, timeout * 1000)
patch = extract_diff(resp.text) # строгий парсер ```diff
ok = await run_tests(task.repo, patch, budget_s=120)
return Result(task.id, model, ok, resp.usage.input_tokens,
resp.usage.output_tokens, int((time.monotonic() - t0) * 1000))
Ключевая деталь: extract_diff — самый частый источник шума. Claude заворачивает дифф в diff `, o3-mini иногда отдаёт полный файл вместо патча, R1 рассуждает и дублирует код. Пришлось нормализовать: три регэкспа + фолбэк «применить как unified diff с fuzz». Без этого pass@1 падал на 6–9 п.п. у всех трёх — то есть половина «глупости» моделей была нашим парсером.
Результаты
| Группа задач | Claude 3.7 | o3-mini (high) | DeepSeek R1 |
|---|---|---|---|
| Миграции БД | 78% | 71% | 64% |
| Рефакторинг | 74% | 68% | 66% |
| SQL/аналитика | 61% | 82% | 77% |
| Разбор логов | 85% | 79% | 71% |
| pass@1 | 74.5% | 75% | 69.5% |
| cost/valid | $0.14 | $0.09 | $0.04 |
| p95 latency | 41 с | 68 с | 92 с |
Архитектура: роутер, а не «одна модель на всё»
Через две недели стало очевидно: выигрывает маршрутизация. В First Touch — системе автономных ИИ-агентов для квалификации лидов и автоматизации коммуникаций — мы пришли к трёхуровневой схеме:
задача → классификатор (60 токенов) → маршрут:
├─ fast: DeepSeek R1 (chat) — извлечение сущностей, роутинг
├─ deep: Claude 3.7 + thinking — сложный диалог, миграции
└─ reasoning: o3-mini high — скоринг, план, валидация
↓ fail / timeout / bad-json
fallback → secondary model → human review
Конфиг держим декларативным, чтобы менять маршруты без релиза:
models:
claude-3-7:
provider: anthropic
model: claude-3-7-sonnet-latest
thinking: {type: enabled, budget_tokens: 8192}
timeout_s: 60
max_retries: 2
o3-mini:
provider: openai
model: o3-mini
reasoning_effort: high
timeout_s: 90
max_retries: 1
deepseek-r1:
provider: deepseek
model: deepseek-reasoner
timeout_s: 120
max_retries: 3
strip_reasoning: true
routes:
extraction: [deepseek-r1, claude-3-7]
dialog: [claude-3-7, o3-mini]
scoring: [o3-mini, claude-3-7]
async def call_route(route: str, payload: dict) -> dict:
errors = []
for name in routes[route]:
cfg = models[name]
try:
async with timeout(cfg["timeout_s"]):
return await call_with_retry(name, cfg, payload)
except (TimeoutError, RateLimitError, BadOutput) as e:
errors.append(f"{name}: {e!r}")
metrics.inc(f"fallback.{route}.{name}")
raise AllProvidersDown(errors)
Грабли
1. Reasoning съедает бюджет. У o3-mini max_completion_tokens включает reasoning-токены. Ставишь 1024 — обрыв на середине мысли, finish_reason=length, и это неотличимо от «модель тупая». Минимум 8192 при любом reasoning_effort.
2. DeepSeek разделяет reasoning_content и content. Наивная конкатенация ломает JSON-парсер. Стрипаем reasoning, но логируем отдельно — при отладке скоринга он бесценен.
3. Таймаут ≠ отмена. asyncio.wait_for гасит корутину, но соединение живёт в пуле httpx. На 12 параллельных агентах ловили RemoteProtocolError. Лечится отдельным AsyncClient на провайдера с limits=Limits(max_connections=32) и http2=True.
4. Идемпотентность. Автономный агент на retry дважды создаст лид или отправит письмо. Каждый tool-call — с idempotency_key = sha256(conversation_id + step + tool_name) и INSERT ... ON CONFLICT DO NOTHING на стороне БД.
5. Изоляция кода. Сгенерированные миграции не применяем напрямую. Только контейнер --network=none, read-only FS, снапшот Postgres, прогон upgrade head + downgrade, лимит 120 с и 512 МБ. За полтора месяца — 3 случая семантически разрушительного, но синтаксически валидного кода.
6. Prompt caching. У Claude кэш системного промпта и схемы БД дал −62% стоимости на длинных сессиях — но только при стабильном префиксе. Timestamp в системном промпте инвалидирует кэш молча.
Цифры
Вопрос
Качество маршрутизации у нас держится на классификаторе в 60 токенов, и на пограничных задачах он ошибается в 9% случаев. Кто-нибудь пробовал заменить такой классификатор на дешёвые эмбеддинги + kNN по историческим задачам с их финальным маршрутом? Интересно, где оно ломается первым — на дрейфе задач или на редких классах.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: LLM, Claude 3.7, DeepSeek R1, o3-mini, бэкенд