Суть Читай реальные сниппеты композиции — раскладку кэша, state machine стрим/тулзы, agent loop с бюджет-гейтом и RAG retrieve+inject — и выбирай фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Швы видны в коде, не на слайдах. Читай каждый сниппет так, как читал бы PR для production LLM-фичи: найди границу, где один слой тихо нарушает допущение следующего, и выбери фикс, который senior-инженер делает первым.
Цель
Отработай цикл, который гоняешь при сборке слоёв: прочитай раскладку запроса или цикл, найди границу, ломающуюся под реальным трафиком, и тянись к структурному фиксу — а не к ручке тюнинга.
Брейкпойнт стоит после полученных чанков. Что эта раскладка реально кэширует и как это починить?
Heads-up Кэш переиспользует самый длинный идентичный префикс до ПОСЛЕДНЕГО брейкпойнта. С брейкпойнтом после чанков кэшируемый отрезок включает чанки, меняющиеся на каждом запросе — поэтому он никогда не хитит. Под брейкпойнтом должно быть только то, что до чанков.
Heads-up Попроцессные чанки — это ровно то, что не надо кэшировать: они меняются на каждом запросе, так что кэширование их — впустую. Переиспользуемая дорогая часть — длинные статические правила + схемы; они должны стоять до брейкпойнта.
Heads-up Больше брейкпойнтов внутри попроцессного контента не помогает — префикс всё равно отличается с первого байта, который меняют чанки. Фикс — вообще убрать динамику из кэшируемого префикса, а не добавлять в неё брейкпойнты.
Сниппет 2 — state machine стрима / тулзы
async for event in stream: if event.type == "content_block_delta": ui.append(event.delta.text) elif event.type == "message_stop": ui.done() # спиннер гаснет здесь# (других веток нет)
Викторина
Completed
Ход с тулзой стримит текст, затем заканчивается с stop_reason: tool_use. Протрассируй поведение этого цикла и выбери фикс.
Heads-up message_stop срабатывает для любого stop_reason, включая tool_use. Трактовка его как «ход завершён» гасит UI, пока модель на деле ждёт tool_result, который ты не отправил.
Heads-up Аппенд дельт — корректное поведение стриминга. Не хватает ветки на tool_use-стоп — буферизация ничего не меняет в необработанном переходе тулзы.
Heads-up Таймаут скрывает застой вместо завершения хода; пользователь всё равно получает полу-ответ. Надо детектить tool_use-стоп, запускать тулзу и возобновлять ход.
Сниппет 3 — agent loop
def run_agent(task): msgs = [task] while True: resp = client.messages.create(model=MODEL, messages=msgs, tools=TOOLS) if resp.stop_reason != "tool_use": return resp for call in tool_uses(resp): msgs.append(tool_result(call, execute(call))) # транскрипт растёт каждый цикл
Викторина
Completed
Malformed-результат тулзы заставляет модель переформулировать и вечно повторять тот же вызов. Сколько это стоит и какой минимальный фикс?
Heads-up Стоимость не плоская — msgs растёт каждый цикл, и весь транскрипт пересылается на каждом вызове, поэтому input-стоимость за шаг растёт с числом итераций. Незавершённый цикл и медленный, И суперлинейно дорогой.
Heads-up Retry детерминированно malformed-результата лишь добавляет итераций — это кормит цикл. Циклу нужен завершающий гейт (step/budget-капы + dedupe), а не больше попыток.
Heads-up Исключения нет — тулза возвращает значение, которое не нравится модели, поэтому цикл идёт на валидных ответах. Останавливают только явный потолок step/budget и dedupe вызовов.
Сниппет 4 — RAG retrieve + расчёт стоимости
hits = vectordb.query(embed(user_q), top_k=20) # без rerank, без порогаcontext = "\n".join(h.text for h in hits) # ~20 чанков × ~800 ток ≈ 16k ток# input ≈ 16k контекст + 2k промпт = 18k ток; цена $3 / 1M input токcost_per_call = 18_000 / 1_000_000 * 3 # ≈ $0.054, на каждый запрос, без кэша
Викторина
Completed
Этот retrieve-and-inject путь корректен в изоляции, но дорог в собранном приложении. Прочитай числа и выбери изменение с наибольшим рычагом.
Heads-up Удвоение top_k удваивает динамическую токен-стоимость и отвлечение, при этом recall за пределами первых нескольких релевантных чанков добавляет мало. Рычаг — меньше, но лучше чанков (rerank + порог), а не больше чанков.
Heads-up Этот контекст попроцессный, поэтому его нельзя закэшировать — кэшу нужен стабильный префикс. Выигрыш — сжать динамический контекст (rerank) и держать его вне кэшируемого префикса, а не кэшировать то, что меняется на каждом вызове.
Heads-up Дешёвая модель снижает цену за токен, но ты всё равно шлёшь 16k в основном нерелевантного контекста на каждом вызове, вредя качеству и стоимости. Сначала чини retrieval: rerank до тех немногих чанков, что важны.
Итог
Баги композиции видны в коде: брейкпойнт кэша, поставленный после попроцессных RAG-чанков, не кэширует ничего переиспользуемого; стрим-цикл без ветки tool_use гасит UI, пока модель ждёт tool_result; agent loop без гейта step/budget вечно пересылает растущий транскрипт; а неранжированный top-20 retrieval впрыскивает ~16k некэшируемых токенов по ~$0.054 за вызов. Фикс в каждом случае структурный — перенести динамику, ветвиться по stop_reason, загейтить цикл, переранжировать контекст — а не ручка тюнинга.