Материал основан на разборе freecodecamp.org. Ниже — главное и практические шаги, которые можно быстро применить в работе.
Многие туториалы по AI-агентам допускают одну и ту же ошибку: они направляют каждый вызов к самой дорогой доступной модели.
Для подсчёта символов не нужен GPT-4. Для проверки наличия элемента не нужен Sonnet. Для регулярного выражения не нужно ничего, кроме Python.
Ошибка не в использовании AI — ошибка в том, что не знаешь, когда его не использовать.
В данном руководстве представлена система многоуровневой маршрутизации, которая направляет задачи к самой доступной модели, способной их решить. Этот подход, известный как cost curve («кривая стоимости»), был предложен в комментариях к статье на DEV.to и успешно реализован тремя разработчиками за выходные. Он позволяет сократить стоимость обработки одного URL в реальном SEO-агенте почти до нуля для большинства страниц.
По завершении у вас будет рабочий модуль cost_curve.py, который можно подключить к любому агентному проекту.
- Что вы создадите
- Предварительные требования
- Проблема вызова Claude для всего подряд
- Объяснение cost curve
- Настройка проекта
- Уровень 1: детерминированный Python
- Уровень 2: Claude Haiku для неоднозначных случаев
- Уровень 3: Claude Sonnet для семантического суждения
- Маршрутизатор: audit_url()
- Корректная обработка сбоев
- Тестирование cost curve
- Применение этого паттерна к вашему агенту
- Подведение итогов
- Ответы на эти вопросы могут быть для вас полезными
Что вы создадите
Трёхуровневую функцию маршрутизации, которая:
- Сначала выполняет детерминированные проверки на Python — нулевая стоимость API
- Передаёт задачу Claude Haiku только для действительно неоднозначных случаев — ~\$0.0001 за вызов
- Передаёт задачу Claude Sonnet только тогда, когда требуется семантическая оценка — ~\$0.006 за вызов
- Корректно обрабатывает сбои на любом уровне
- Возвращает единообразную схему результата вне зависимости от того, какой уровень обработал запрос
Вся реализация интегрирована в проект dannwaneri/seo-agent — открытый SEO-агент. Модуль cost curve представляет собой важный слой маршрутизации и применим к любому агенту с разнообразием задач.
По этой теме полезно отдельно посмотреть Составьте еженедельное расписание занятий, чтобы расширить контекст и сравнить подходы.
По этой теме полезно отдельно посмотреть Аннотации типов для декораторов в Python, чтобы расширить контекст и сравнить подходы.
Предварительные требования
- Python 3.11 или выше
- Ключ API Anthropic
- Базовое знакомство с Python и Claude API
Проблема вызова Claude для всего подряд
Вот как выглядит большинство агентного кода:
def audit_url(snapshot: dict) -> dict:
prompt = build_prompt(snapshot)
response = client.messages.create(
model="claude-sonnet-4",
messages=[{"role": "user", "content": prompt}],
)
return parse_response(response)
Тем не менее, Sonnet вызывается для каждого URL в списке, включая те, для которых заголовок состоит из 142 символов и ответ очевидно будет отрицательным без участия модели.
Claude Sonnet 4 стоит \$3 за миллион входных токенов и \$15 за миллион выходных токенов. Типичный снимок страницы занимает около 500 входных токенов. Это \$0.0015 за URL только за входные токены — до учёта выходных. При еженедельном аудите 20 URL итоговая сумма составит около \$0.12. Не дорого. Но большинство этих страниц имеют механические SEO-проблемы: отсутствующие описания, заголовки длиннее 60 символов, отсутствие canonical-тега. Подсчёт символов выявит всё это. Модель не нужна.
Cost curve решает эту проблему, направляя задачи в зависимости от их реальных потребностей, а не от возможностей модели.
Объяснение cost curve
В cost curve есть три уровня, три инструмента и три ценовых точки.
Уровень 1 — детерминированный Python. Стоимость: \$0. Проверка длины заголовка, длины описания, количества H1, наличия canonical. Это не оценочные суждения. Это строковые операции. Если длина заголовка > 60, FAIL. Модель не нужна.
Уровень 2 — Claude Haiku. Стоимость: ~\$0.0001 за вызов. Заголовок присутствует, но содержит всего 4 символа. Описание присутствует, но занимает лишь 30 символов. Код статуса — редирект. Они проходят механический аудит, но что-то не так. Haiku достаточно быстр и дёшев, чтобы передача неоднозначных случаев обходилась дешевле, чем время отладки, которое вы потратите на ложные срабатывания.
Уровень 3 — Claude Sonnet. Стоимость: ~\$0.006 за вызов. Страницы, которые Haiku помечает как требующие семантической оценки. «Этот заголовок проходит по длине, но читается как навигационная метка». «Это описание дословно дублирует заголовок». Sonnet оправдывает свою стоимость на действительно сложных случаях — а не на каждом URL в списке.
Решение о маршрутизации принимается до любого вызова API. Схема результата идентична вне зависимости от того, какой уровень обработал запрос.
Настройка проекта
mkdir cost-curve-demo && cd cost-curve-demo
pip install anthropic
Установите ключ API:
# macOS/Linux
export ANTHROPIC_API_KEY="sk-ant-..."
# Windows PowerShell
$env:ANTHROPIC_API_KEY = "sk-ant-..."
Создайте cost_curve.py — этот модуль вы будете строить шаг за шагом.
Уровень 1: детерминированный Python
Уровень 1 запускается первым для каждого URL. Он проверяет четыре поля, используя только строковые операции Python. Нет вызовов API, нет задержки и нет стоимости.
REDIRECT_CODES = {301, 302, 307, 308}
AMBIGUOUS_TITLE_MAX = 10
AMBIGUOUS_DESC_MAX = 50
def tier1_check(snapshot: dict) -> dict:
result = build_empty_result(snapshot, method="deterministic")
title = snapshot.get("title") or ""
description = snapshot.get("meta_description") or ""
h1s = snapshot.get("h1s") or []
canonical = snapshot.get("canonical") or ""
if not title or len(title) > 60:
result["flags"].append("Заголовок отсутствует или слишком длинный")
if not description or len(description) > 160:
result["flags"].append("Описание отсутствует или слишком длинное")
if len(h1s) != 1:
result["flags"].append("Неверное количество H1")
if not canonical:
result["flags"].append("Отсутствует canonical")
return result
Ключевое архитектурное решение: tier1_check() никогда не решает, нужна ли эскалация. Она просто выполняет проверки и возвращает результат. Решение об эскалации принимает маршрутизатор на основе этого результата.
Уровень 2: Claude Haiku для неоднозначных случаев
Уровень 2 запускается, когда Уровень 1 обнаруживает что-то механически подозрительное, но результат может потребовать повторного взгляда. Заголовок из 4 символов присутствует, но явно неверен. Описание из 30 символов технически есть, но бесполезно. Статус редиректа, которому нужно человекочитаемое объяснение.
Haiku — правильная модель здесь. Она быстрая, дешёвая ($1 за входящие / $5 за исходящие на миллион токенов) и достаточна для суждений на уровне триажа (triage). Промпт задаёт узкий вопрос: достаточно ли этот случай неоднозначен, чтобы передать его Sonnet?
def tier2_check(snapshot: dict) -> dict:
prompt = build_haiku_triage_prompt(snapshot)
try:
raw = call_haiku(prompt)
parsed = json.loads(raw)
result = tier1_check(snapshot)
result["method"] = "haiku"
result["needs_tier3"] = parsed.get("needs_tier3", False)
return result
except Exception:
fallback = tier1_check(snapshot)
fallback["method"] = "haiku-fallback"
return fallback
Откат — критически важная часть. Если Haiku даёт сбой — ограничение частоты запросов, сетевая ошибка, некорректный ответ — функция возвращает результат Уровня 1, а не падает с ошибкой. Аудит продолжается. URL помечается с method="haiku-fallback", чтобы вы могли идентифицировать его позже.
Уровень 3: Claude Sonnet для семантического суждения
Уровень 3 — это место, где выполняется полный промпт извлечения данных. Это тот же вызов, который вы делали бы в наивной реализации — разница в том, что только небольшая доля URL достигает этого уровня.
def tier3_check(snapshot: dict) -> dict:
prompt = build_sonnet_prompt(snapshot)
try:
raw = call_sonnet(prompt)
result = json.loads(raw)
result["method"] = "sonnet"
result["needs_tier3"] = False
return result
except Exception:
fallback = tier1_check(snapshot)
fallback["method"] = "sonnet-fallback"
return fallback
Обратите внимание на дополнение в промпте Уровня 3, которого нет в Уровне 1: "or if present but clearly not a real title" и "or if present but meaningless". Это то самое семантическое суждение, которое Haiku определил как необходимое. Уровень 3 действует на его основе.
Маршрутизатор: audit_url()
Маршрутизатор — это публичный интерфейс. Всё остальное — детали реализации.
def audit_url(snapshot: dict, tiered: bool = False) -> dict:
if not tiered:
return tier3_check(snapshot)
t1_result = tier1_check(snapshot)
title = snapshot.get("title") or ""
description = snapshot.get("meta_description") or ""
status_code = snapshot.get("status_code")
needs_tier2 = (
(title and len(title) < AMBIGUOUS_TITLE_MAX)
or (description and len(description) < AMBIGUOUS_DESC_MAX)
or (status_code in REDIRECT_CODES)
)
if not needs_tier2:
return t1_result
t2_result = tier2_check(snapshot)
if not t2_result.get("needs_tier3", False):
return t2_result
return tier3_check(snapshot)
Логика маршрутизатора явная и читаемая. Каждая точка принятия решения — это именованное условие. Когда tiered=False, поведение идентично наивной реализации v1 — это гарантия обратной совместимости, которая позволяет добавлять кривую стоимости постепенно, не ломая существующие аудиты.
Корректная обработка сбоев
Паттерн отката присутствует как в Уровне 2, так и в Уровне 3. Стоит сделать его явным.
Каждая функция уровня оборачивает вызов API в try/except. При любом исключении — ограничение частоты запросов, таймаут сети, некорректный JSON в ответе — функция возвращает результат Уровня 1 с изменённым полем method: "haiku-fallback" или "sonnet-fallback". Аудит никогда не прерывается из-за сбоя модели.
Это важно по двум причинам. Во-первых, агент продолжает работу даже при деградации API. Во-вторых, поле method в результате позволяет точно определить, какой уровень фактически обработал каждый URL — это упрощает отладку и мониторинг стоимости в продакшене.
Тестирование cost curve
Создайте файл test_cost_curve.py для проверки поведения маршрутизации без реальных вызовов API:
from unittest import mock
from cost_curve import audit_url
def test_clean_page_returns_tier1_no_api_calls():
...
def test_suspiciously_short_title_escalates_to_tier2():
...
def test_haiku_api_failure_falls_back_to_tier1():
...
Запустите:
python test_cost_curve.py
Тесты покрывают все ключевые пути маршрутизации: чистая страница без вызова API, FAIL от Уровня 1 без вызова API, эскалация на Haiku, прямой вызов Sonnet при tiered=False и откат при сбое Haiku. Я рекомендую запускать их после каждого изменения условий эскалации — это самый быстрый способ убедиться, что маршрутизатор не начал отправлять лишние запросы.
Применение этого паттерна к вашему агенту
Кривая стоимости не является специфичной для SEO. Любой агент со смешанными по сложности задачами может её использовать.
Принцип: классифицируйте задачи по тому, что они реально требуют, прежде чем решать, какую модель вызывать.
Агент поддержки клиентов:
- Уровень 1: сопоставление по ключевым словам для известных тем FAQ — без модели
- Уровень 2: Haiku для классификации намерений в неоднозначных запросах
- Уровень 3: Sonnet для сложных жалоб, требующих суждения
Агент проверки кода:
- Уровень 1: правила линтера, синтаксические проверки — без модели
- Уровень 2: Haiku для обнаружения распространённых паттернов
- Уровень 3: Sonnet для архитектурного ревью
Агент модерации контента:
- Уровень 1: сопоставление с блок-листом — без модели
- Уровень 2: Haiku для пограничных случаев
- Уровень 3: Sonnet для суждений, зависящих от контекста
Паттерн реализации одинаков во всех трёх случаях. Маршрутизатор audit_url() превращается в route_task(). Функции уровней меняют свои промпты и условия эскалации. Логика отката остаётся идентичной.
Ключевой вопрос, который нужно задать перед написанием любого кода агента: какая доля входных данных решается механически? Эта доля уходит на Уровень 1. Остальное эскалируется. На практике я обнаружил, что для большинства агентов с однородными входными данными эта доля составляет 60–80% — и именно здесь кривая стоимости даёт наибольший эффект.
Подведение итогов
Полная реализация — включая SEO-агент аудита, который использует этот модуль в продакшене — находится по адресу dannwaneri/seo-agent. Директория core/ лицензирована по MIT. Многоуровневая маршрутизация находится в premium/cost_curve.py.
Этот туториал является сопроводительным материалом к статье «I Was Paying $0.006 Per URL for SEO Audits Until I Realized Most Needed $0» на DEV.to, которая охватывает архитектурные решения, лежащие в основе кривой стоимости.
Ответы на эти вопросы могут быть для вас полезными
Когда стоит использовать многоуровневую маршрутизацию, а когда достаточно одной модели?
Многоуровневая маршрутизация оправдана, когда значительная доля входных данных решается механически — проверками длины, наличия поля, сопоставлением с паттерном. Если все задачи требуют семантического суждения, дополнительная сложность не окупается.
Можно ли добавить больше трёх уровней?
Технически да, но на практике три уровня покрывают большинство случаев. Каждый дополнительный уровень увеличивает сложность маршрутизатора и усложняет отладку. Лучше начать с трёх и расширять только при наличии конкретного измеримого обоснования.
Как понять, что условия эскалации настроены правильно?
Смотрите на поле method в результатах аудита. Если большинство URL получают method="sonnet", условия эскалации слишком мягкие. Если вы видите ложные срабатывания на Уровне 1 — слишком жёсткие. Тесты из раздела выше позволяют проверить граничные значения без реальных вызовов API.
Что происходит, если Sonnet тоже даёт сбой?
Уровень 3 использует тот же паттерн отката: при любом исключении возвращается результат Уровня 1 с method="sonnet-fallback". Аудит не прерывается. URL помечается для последующей проверки.
Нужно ли менять схему результата при переходе между уровнями?
Нет. Функция _build_result() гарантирует идентичную схему вне зависимости от уровня. Это принципиальное архитектурное решение: код, который потребляет результат аудита, не должен знать, какой уровень его сгенерировал.



