Сколько стоит контекст для кодового агента: grep vs граф vs LSP на большом проекте (936 прогонов)

от автора

Продолжение статьи про graphlens. Там я описал, что инструмент делает и как устроен, и по дороге уверенно заявил, что «агент жжёт токены, бегая grep’ом по репозиторию». Заявил — но ни одной цифры не привёл. Эта статья закрывает дыру: вот замеры, вот данные, вот воспроизводимый стенд. Спойлер: вывод оказался не таким, каким я его себе рисовал, и это самое интересное.

Коротко

Я взял одного и того же агента (Claude Code), менял у него ровно одну вещь — какой MCP-сервер отдаёт контекст по коду, — и гонял по 26 задачам на apache/superset. Четыре «руки»: filesystem (grep + read), graphlens (структурный граф), serena (LSP) и codegraph. Три модели (haiku / sonnet / opus), три сида — 936 прогонов.

Главный результат: вывод переворачивается в зависимости от типа задачи.

  • На простых «где определён X / от чего наследуется» — все четыре инструмента равны по точности, разница только в цене (~3×). graphlens тут ничем не выделяется.

  • На задачах «оцени радиус поражения / найди все переопределения / разреши неоднозначное имя» инструменты резко расходятся: grep разваливается (точность 0.71, до финиша доходит 83% прогонов, а те, что доходят, стоят в 10–23 раза дороже), а структурные инструменты остаются дешёвыми и точными.

Если бы я мерил только простые задачи, я бы написал «граф не нужен, grep справляется». Если бы только сложные — «grep не нужен, берите граф». Правда — посередине, и она про то, какую работу вы поручаете агенту.


Бизнес-кейс, который мы на самом деле измеряем

Представьте типичную ситуацию. Есть большой проект: сотни тысяч строк, бэкенд на Python, фронт на TypeScript, легаси, в которое страшно лезть. Вы подключаете к нему кодового агента — для ревью, для рефакторинга, для ответов на вопросы вроде «что сломается, если я поменяю сигнатуру вот этого метода».

Агент не видит весь репозиторий разом. Кто-то должен подавать ему контекст: какие функции где определены, кто кого вызывает, что от чего наследуется. И вот тут возникает архитектурное решение, у которого есть цена: чем именно кормить агента?

Вариантов, по сути, четыре класса:

  • Дать ему grep и read — пусть ищет текстом и читает файлы. Ноль инфраструктуры, работает везде.

  • Построить структурный граф кода (graphlens) — узлы-сущности, типизированные рёбра, точные ответы на «кто вызывает».

  • Поднять LSP (serena поверх language server) — то, чем питается ваша IDE.

  • Взять готовый code-graph продукт (codegraph).

Каждый вариант — это деньги (токены), время (латентность) и риск (агент не справится и упрётся в лимит ходов). apache/superset — почти идеальный стенд под этот кейс: ~400k строк, Python + TypeScript, граница /api/v1/... между фронтом и бэком. Большой полиглотный проект — ровно то, ради чего этот вопрос вообще стоит задавать.

Так сколько стоит каждое из решений? Давайте мерить.

Дизайн эксперимента: меняем одну переменную

Вся методология держится на одном принципе: зафиксировать всё, кроме одного. Модель, системный промпт, настройки, набор задач — константы. Меняется только MCP-сервер, отдающий контекст. Тогда любая разница в цифрах — это вклад именно инструмента, а не случайности конфигурации.

Никакой инструмент не назначен «бейзлайном, который надо побить». Все четыре меряются на равных, и пусть числа их ранжируют.

Четыре «руки»

Рука

Провайдер контекста (MCP-сервер)

Шаг индексации

filesystem

@modelcontextprotocol/server-filesystem (read_file + grep)

нет

graphlens

граф graphlens поверх MCP

graphlens analyze

serena

Serena (LSP)

прогрев LSP-воркспейса

codegraph

конкурент на графах

codegraph init

Важная деталь честности стенда: встроенные инструменты Claude Code (Read / Grep / Bash и прочие) выключены. Если их не отнять, агент проигнорирует MCP и пойдёт своим привычным путём — и мы измерим не то. Поэтому стенд запускает claude -p в «чистой комнате»: свежий CLAUDE_CONFIG_DIR только с кредами подписки (без хуков, плагинов, скиллов, памяти), --strict-mcp-config (виден только сервер этой руки), --disallowedTools на все встроенные инструменты (именно запрет, а не отсутствие в allow-list — в headless-режиме allow-list сам по себе ничего не запрещает) и --allowedTools mcp__<server>, чтобы автоматически разрешить единственный сервер руки.

Вторая ось: модели

Параллельно я варьировал модель, которая отвечает на вопрос:

Ключ

model id

haiku

claude-haiku-4-5

sonnet

claude-sonnet-4-6

opus

claude-opus-4-8

Зачем вторая ось — станет ясно ближе к концу: оптимальный инструмент зависит от того, какую модель вы взяли. Это, пожалуй, самый неочевидный вывод всего замера.

Итого: 4 руки × 3 модели × 26 задач × 3 сида = 936 прогонов (на стеке Claude Code 2.1.187).

Что я считаю честным замером

Бенчмарки легко подкрутить под нужный вывод. Поэтому правила игры зафиксированы заранее, и вот они — без них цифрам верить нельзя.

  1. Эталонные ответы выверены руками по исходникам на теге 6.0.0 (каждая задача несёт ссылку file:line). Принципиально: эталон не генерируется ни одним из тестируемых инструментов (ни ty, ни pyright, ни самим graphlens). Иначе сравнение смещено в пользу того, чьим выводом мы размечали. Эталонные множества для set-задач выверены независимым оракулом — питоновским ast.

  2. У «наивной» руки есть руки. filesystem — это grep + read, а не «агент без инструментов». «Наивно» ≠ «без рук».

  3. Стоимость индексации меряется отдельно, один раз. grep не платит за индекс ничего, граф — амортизирует. Смешивать эти валюты нельзя.

  4. Детерминизма нет. temperature=0 у этих моделей не детерминирует вывод. Поэтому 3 сида, и в отчёте — медиана, а не среднее.

  5. Записаны версии моделей и каждого MCP-сервера, снимок цен и дата.

  6. cost_usd — это API-эквивалент, а не ваш счёт. Подписка — flat-rate, так что cost_usd (его отдаёт CLI) — это сколько те же токены стоили бы по API. Это не ваш реальный чек, но это корректная относительная метрика $/задача для сравнения рук между собой.

  7. Прогоны в чистой комнате — токены отражают только системный промпт + инструменты MCP-руки, без вашего личного конфига.

  8. Использовать инструмент обязательно. Системный промпт запрещает отвечать по памяти; прогон, не сделавший ни одного вызова инструмента, перезапускается (а упорный отказ помечается __NO_TOOLS__). Ответ «из головы» про известный репозиторий не измерял бы провайдера контекста.

И отдельно — провал засчитывается как точность 0. Если grep упёрся в потолок 50 ходов и не выдал ответ — это не «нет данных», это «инструмент не справился в рамках бюджета». Так и считаем.

Задачи: два режима, и почему их нельзя смешивать

26 задач делятся на два класса.

SIMPLE — 20 точечных запросов («где определён X / от чего наследуется X»). Ответ — одна точка, проверяется вхождением подстроки:

Тип

#

Что проверяет

where_defined

7

Python-класс → файл определения

inherits_from

5

Python-класс → базовый класс

abstract_methods

1

ABC → его абстрактные методы

ts_where_defined

1

TS-хук → файл определения

ts_route_call

4

роут /api/v1/... → TS-хук, который его дёргает

xlang_link

2

TS-потребитель → Python-обработчик через границу API

HARD — 6 задач на радиус поражения и неоднозначность. Это режим, где структура и семантика должны бить текстовый поиск — и который точечные запросы в принципе не измеряют:

Тип

#

Что проверяет

Оценка

disambiguate

2

неоднозначное голое имя метода (напр. cache_key, определён во многих классах) → тот самый класс

подстрока

overrides_count

2

полный набор подклассов, переопределяющих метод базы

F1 по множеству

impact_set

2

все файлы, вызывающие данный метод (радиус поражения)

F1 по множеству

Set-задачи оцениваются по F1: награда за полноту (найти всех) и штраф за мусор в точности (текстовый поиск любит вывалить каждое вхождение .get_indexes(). Эталонные множества держим маленькими (3–5 элементов, одно ≈17), чтобы их можно было исчерпывающе проверить руками.

Почему я стратифицирую, а не усредняю

Набор намеренно несбалансирован — 20 простых против 6 сложных. Если посчитать одно общее среднее, оно будет полностью продиктовано лёгкими задачами и спрячет ровно ту разницу, которую вскрывают сложные. Поэтому я докладываю каждый режим отдельно и никогда не смешиваю.

И да — я сознательно не «балансирую до 50/50» выкидыванием простых задач. Это потеряло бы данные и статистическую мощность и открыло бы дверь для cherry-pick. Стратификация нейтрализует перекос без выброса данных. Это, кстати, общий принцип: если режимы дают разные ответы, честнее показать оба, чем спрятать конфликт под усреднением.

Результаты

SIMPLE — 20 точечных запросов

Инструмент

точность

заверш.

токены

вызовы

$/задача

сек

filesystem

0.97

100%

1780

10

$0.063

43

graphlens

0.98

100%

690

3

$0.038

13

serena

0.99

100%

402

3

$0.031

20

codegraph

0.99

100%

372

1

$0.022

10

Точность — ничья (формально: критерий Фридмана χ²=0.40, незначимо). Инструменты различаются только ценой: разброс ~3×, выигрывают самые «немногословные». graphlens здесь ничем не примечателен — крепкий середняк.

Вот ровно ту историю рассказал бы стенд, измеряющий только точечные запросы: «структурные инструменты — приятно, но grep почти справляется, а самый дешёвый ответ даёт codegraph». И это была бы неполная правда.

HARD — 6 задач на радиус поражения и неоднозначность

Инструмент

точность

заверш.

токены

вызовы

$/задача

сек

filesystem

0.71

83%

12596

27

$0.424

165

graphlens

0.84

100%

748

1

$0.018

9

serena

0.85

98%

1368

5

$0.065

29

codegraph

0.93

100%

1114

2

$0.036

16

А вот тут инструменты расходятся.

grep схлопывается. Самая низкая точность (0.71), до финиша доходит лишь 83% прогонов (остальные упираются в потолок 50 ходов), а те, что доходят, стоят в 10–23 раза дороже (~$0.42 против $0.018–0.065) и работают в 10–18 раз дольше (~165 секунд против 9–29). Текстовый поиск тонет в шуме, когда вопрос — «все вызовы вот этого» или «какой именно из десятка одноимённых методов».

Структурные инструменты держатся дёшево и точно. И вот ключевое: graphlens — середняк на простых задачах — здесь самый дешёвый ($0.018) и самый быстрый (9 секунд). Его семантический граф наконец окупается: один вызов вместо двадцати семи. Самым точным оказывается codegraph (0.93). serena конкурентна (0.85).

То есть тот самый graphlens, который на точечных запросах выглядел невзрачно, в режиме реальной работы — про анализ влияния, про рефакторинг — становится самым экономичным. Ранжирование инвертируется между режимами.

Заметка о честности стенда. MCP-ресурсы отключены для всех рук. graphlens — единственный сервер, выставлявший ресурсы, и в одном из ранних прогонов агент уходил в их перечисление и раздувал стоимость ~на 24%, пока я это не запретил. Все цифры выше — с чистого перепрогона.

Где уходят деньги: механизм — это round-trips

Разница в цене — это в основном число обращений к инструменту, а оно вытекает из того, как сервер нарезает свои примитивы.

На простом «символ → файл» (where_defined) всем хватает одного вызова. Разрыв открывается на запросах об отношениях — наследование, роут → обработчик, межъязыковые связи. Здесь graphlens цепочкой дёргает мелкозернистые примитивы (findneighborsreferences), а codegraph упаковывает «исходник + пути вызовов за один заход» (explore / node).

Это не разница в том, что знает граф — графы знают примерно одно. Это разница в гранулярности API: меньше round-trips → дешевле и быстрее. Вот откуда у codegraph преимущество в эффективности на простых задачах, и вот почему grep на сложных задачах разоряется — он делает 27 обращений там, где графу хватает одного-двух.

Взаимодействие модель × инструмент: ранжирование плывёт от цены модели

Это самая неочевидная часть. Возьмём медианную $/задача (по обоим режимам) в разрезе модели:

Инструмент

haiku

sonnet

opus

filesystem

$0.053

$0.080

$0.087

graphlens

$0.020

$0.041

$0.046

serena

$0.026

$0.033

$0.042

codegraph

$0.023

$0.041

$0.031

Ранжирование по дешевизне внутри каждой модели:

  • haiku: graphlens $0.020 < codegraph $0.023 < serena $0.026 < filesystem $0.053

  • sonnet: serena $0.033 < graphlens $0.041 < codegraph $0.041 < filesystem $0.080

  • opus: codegraph $0.031 < serena $0.042 < graphlens $0.046 < filesystem $0.087

Смотрите, что происходит с graphlens. На haiku он самый дешёвый из всех. На opus он становится самым дорогим из структурных инструментов (хотя всё ещё дешевле grep).

Механизм: результаты graphlens токеноёмкие — окрестности графа, списки ссылок. На дешёвой модели этот многословный контекст почти бесплатен, на дорогой — opus тарифицирует те же токены кратно выше, и многословность бьёт по карману. serena и codegraph дёшевы на любой модели, потому что возвращают точечные результаты — они устойчивы к выбору модели, а graphlens нет.

Отсюда практический вывод, который дороже всех остальных: дешёвая модель на структурном инструменте бьёт дорогую модель на grep. codegraph + haiku (~$0.023, точность ~0.99) делает filesystem + opus (~$0.087, точность 0.93) по всем осям сразу.

Гипотеза, которая не подтвердилась

Я закладывал пару xlang_link как стресс-тест: TS-вызов резолвится в Python-обработчик через границу /api/v1/..., и я был уверен, что одноязычные инструменты на этом споткнутся.

Не споткнулись. Все руки, включая grep, решили обе межъязыковые задачи. Агент сам перешагивает границу — независимо от провайдера контекста. На этом наборе гипотеза не подтвердилась, и я докладываю это ровно так же громко, как и подтвердившиеся выводы. Бенчмарк, который рапортует только то, что хотелось увидеть, — это не бенчмарк.

Статистика честно

Критерий Фридмана по четырём инструментам, по блокам-задачам, внутри каждого режима (df=3; критические значения: 0.05 → 7.82, 0.01 → 11.34):

SIMPLE:  точность  n=20  χ²= 0.40  (н.з.)    — ничья  стоимость n=20  χ²=18.42  (p<.01)   — serena < codegraph < graphlens < filesystemHARD:  точность  n= 6  χ²= 3.50  (н.з.)    — недостаточно мощности  стоимость n= 6  χ²=11.80  (p<.01)   — graphlens < codegraph < serena < filesystem

Что отсюда честно сказать:

  • Разница в стоимости значима в обоих режимах (p<.01). На HARD graphlens достоверно самый дешёвый, grep достоверно самый дорогой. Это твёрдый результат.

  • Разница в точности на HARD большая, но статистически незначима при n=6 (χ²=3.50). Это сильный описательный сигнал, но ещё не доказанный. Шесть задач — мало.

  • Чтобы укрепить вывод про точность, нужно добавить сложных задач, а не убирать простые. Урезание простого режима не даёт сложному ни капли мощности — оно лишь выбрасывает хорошие данные.

Я специально оставляю это в статье. Соблазн написать «graphlens/codegraph точнее grep, доказано» велик, но n=6 этого не вытягивает, и притворяться было бы нечестно.

Амортизация индекса: разные валюты

Структурные инструменты строят индекс один раз — это чистая статика, ноль LLM-токенов, только wall-clock:

Инструмент

разовая индексация

filesystem

0 с

codegraph

48 с

graphlens

84 с

serena

94 с

grep не платит вперёд ничего, но платит больше за каждый запрос. Это разные валюты (секунды против $/токенов), поэтому никакой единой «точки безубыточности» я не рисую — это была бы натяжка. Картина простая: индекс — разовая трата времени без единого токена, а экономия $/задача капает на каждой задаче. На длинной сессии структурные инструменты амортизируются; на паре разовых запросов нулевой сетап grep может выиграть по «времени до первого ответа».

Выводы для бизнес-кейса

Вернёмся к исходному вопросу: чем кормить агента на большом проекте?

Ответа «вот этот инструмент всегда лучший» — нет. Есть ответ «зависит от того, какую работу вы поручаете»:

  • Разовые точечные справки («где определён класс», «от чего наследуется»): берите что угодно. grep справляется, точность та же, нулевой сетап. Платите вы тут разве что небольшим overhead’ом в токенах.

  • Постоянная работа с анализом влияния — рефакторинг, оценка радиуса поражения, разрешение неоднозначностей на большой кодовой базе: структурные инструменты режут стоимость в 10–23 раза и латентность в 10–18 раз против grep — и, что не менее важно, не упираются в потолок ходов. grep на этих задачах не просто дорог, он в 17% случаев вообще не доходит до ответа.

  • Выбор модели взаимодействует с выбором инструмента. Многословный граф дёшев на маленькой модели и дорог на большой. Если гоняете opus — берите инструмент с точечной отдачей (codegraph, serena). Если haiku — graphlens внезапно самый дешёвый.

  • Самая дешёвая комбинация — не «дорогая модель + простой инструмент», а «дешёвая модель + структурный инструмент».

И честные оговорки, без которых выводы нельзя переносить на ваш проект:

Один репозиторий (apache/superset @ 6.0.0), один стенд, 26 задач (20 простых / 6 сложных). Режимы докладываются раздельно и никогда не смешиваются. cost_usd — API-эквивалент, а не счёт по подписке. Провал = точность 0. Это не универсальный рейтинг — это воспроизводимый замер на конкретном кейсе.

Где здесь graphlens

Раз уж это продолжение статьи про него — скажу прямо. Этот бенчмарк не доказывает, что graphlens «лучший». Он показывает конкретный режим, в котором его структурный граф окупается (анализ влияния, дёшево и быстро на дешёвых моделях), и так же прямо показывает, где он проседает (на opus его многословная отдача дороже, чем у codegraph и serena; codegraph точнее на сложных задачах).

Для меня это полезнее любой победной реляции. graphlens задумывался как движок и точная мультиязычная модель графа, а не как готовое приложение. Бенчмарк ровно это и подтверждает: на структурных вопросах граф бьёт текстовый поиск с большим запасом, и одновременно есть куда расти — гранулярность MCP-инструментов (меньше round-trips, как у codegraph) и компактность отдачи (чтобы не разоряться на дорогих моделях). Это мой следующий пункт работ, и он теперь подкреплён числами, а не интуицией.

Воспроизвести

Весь стенд и сырые данные — открыты. Прогон полностью детерминированно собирается из data/.

  • Репозиторий бенчмарка: https://github.com/Neko1313/agent-context-bench

  • Смотреть metrics.ipynb (все графики и постатейная статистика) и README.md (методология).

  • uv run main.py гоняет весь пайплайн (клонирование superset → сборка индексов → 936 прогонов, resumable в рамках лимитов подписки), дальше открываете metrics.ipynb.

Если у вас есть свой большой проект и желание прогнать стенд на нём — буду рад issue и результатам. Чем больше независимых прогонов на разных кодовых базах, тем ближе мы к ответу, который можно переносить, а не «работает на superset».

ссылка на оригинал статьи https://habr.com/ru/articles/1051504/