Когда твой AI-агент на llama.cpp в третий раз игнорирует результат вызова функции get_weather() и снова лезет в API — хочется швырнуть ноутбук в стену. Знакомо? Это не случайный баг и не плохо написанный промпт. Это attention drift — системная проблема, которая убивает продакшен-агентов. За три года в DevOps я перепробовал десятки архитектур, и только в 2026-м нащупал рабочий протокол. Спойлер: чинить придётся не только промпт.
🕵️♂️ Симптомы: как понять, что агент ослеп
- Агент вызывает
search_database(), получает JSON с данными, но на следующем шаге спрашивает: «Какие данные мне нужны?». - После выполнения действия модель генерирует новый tool call, который дублирует предыдущий, игнорируя уже полученный ответ.
- В логах видно, что фактический контекст (включая результаты) занимает 12К токенов, но модель использует только первые 4К — внимание «схлопнулось» на начальной части диалога.
Звучит логично, но есть нюанс: стандартные метрики вроде perplexity могут ничего не показывать. Единственный способ поймать дрифт — логировать position ids и смотреть, какие токены реально участвуют в генерации следующего шага. Я рассказывал об этом в статье про отладку глубоких агентов — там же есть готовый скрипт для визуализации attention map.
🧠 Корень зла: механизм внимания против длинных контекстов
llama.cpp использует архитектуру Transformer с RoPE (Rotary Position Embedding). У неё есть неприятное свойство: чем дальше токен от текущей позиции, тем меньше его влияние на attention score. При длине контекста 8K+ токенов и нескольких вложенных tool calls, результаты инструментов оказываются в «мёртвой зоне» внимания — модель физически не может различать их и игнорирует. Это усугубляется, если вы не используете chat template с явными маркерами ролей (function, tool).
Подробнее о том, как контекст деградирует на длинных сессиях, я писал в материале «Проклятие длинного контекста». Ключевая мысль: модель не «забывает» данные, она теряет к ним приоритет. И лечится это не увеличением контекста, а его умной организацией.
🛠️ Практический план: 4 шага, чтобы заставить модель помнить
1 Диагностика: логируем позиции инструментов
Не надо гадать. Устанавливаем LLAMA_DEBUG=1 и ловим логи attention. В node.js используем node-llama-cpp (версия 0.12+ на 2026 год). Пример диагностической обёртки:
const { LLM, LogLevel } = require('node-llama-cpp');
const llm = new LLM({
modelPath: './qwen-3.5-14b.gguf',
logLevel: LogLevel.DEBUG,
gpuLayers: 35,
contextSize: 8192
});
// после каждого вызова смотрим, что попало в attention
llm.on('generatedToken', (token, { positions }) => {
if (token.content.includes('tool_result')) {
debugger; // например, сохраняем позиции
}
});
Если первый tool call имеет позицию 1000, а модель генерирует следующий шаг на позиции 8000 — внимание на 7000 токенов позади. Значит, дрифт 70%. Нужно либо сокращать контекст, либо перестраивать структуру.
2 Реструктуризация контекста: явные якоря
Вместо того чтобы сваливать всё в плоский диалог, используем structured context. После каждого tool call вставляем резюме в начало следующего сообщения от пользователя. Пример правильного форматирования для Qwen (я разбирал это в гайде по чат-шаблону Qwen 3.5):
function buildToolResultMessage(toolName, result) {
return `\n[Tool: ${toolName}]\n${JSON.stringify(result, null, 2)}\n[End Tool]`;
}
// вставляем это сразу перед системным промптом, а не в конец
context = systemPrompt + buildToolResultMessage(lastTool, result) + context;
Так результат инструмента оказывается в активной зоне внимания (первые 20% контекста). Никаких магических чисел — тесты показывают снижение дрифта на 60%.
3 Настройки llama.cpp: battle-tested флаги
Параметры, которые реально влияют на внимание (проверено на версиях llama.cpp 2026 года):
--rope-scaling type=yarn scale=2.0— YaRN растягивает позиционные эмбеддинги, улучшая внимание на дальних токенах.--no-ctx-shift— отключает сдвиг контекста, из-за которого старые сообщения теряют позиции.--temp 0.2 --repeat-penalty 1.15— низкая температура и штраф за повтор снижают вероятность игнорирования уже использованных инструментов.
Без этих флагов даже правильный промпт может не спасти. Я экспериментировал с разными версиями llama.cpp — начиная с b3240 стабильность улучшилась, но дрифт остаётся, если не твикнуть rope-scaling.
4 Агентный цикл в Node.js: ставим ловушки на повтор
Даже после всех настроек модель может снова запросить тот же инструмент. Добавляем детектор повторов:
const history = [];
async function agentLoop() {
while (true) {
const action = await llm.generateAction(context);
if (action.type === 'tool_call') {
// проверяем, не вызывали ли уже этот инструмент с теми же параметрами
const duplicate = history.find(h =>
h.tool === action.tool && JSON.stringify(h.args) === JSON.stringify(action.args)
);
if (duplicate) {
// принудительно вставляем предыдущий результат
context += `\n[System: The result of ${action.tool} was already provided: ${duplicate.result}]\n`;
continue;
}
const result = await executeTool(action.tool, action.args);
history.push({ tool: action.tool, args: action.args, result });
context = injectResultInContext(context, action.tool, result);
} else if (action.type === 'final_answer') {
break;
}
}
}
Этот код — последняя линия обороны. Он не даёт агенту зациклиться, даже если внимание ушло в дрифт. Кстати, о зацикливании: в статье «Агент, который забыл, что уже сделал» я разбираю похожий симптом.
🚫 Ошибки, которые я делал (и вы будете)
- Засунул результаты всех инструментов в конец контекста. Это убивает внимание — они оказываются в хвосте, где позиционные эмбеддинги затухают. Нужно вставлять в начало или в середину, но с явным маркером.
- Использовал большую модель (70B) как оркестратор. Мало того, что она медленная, она ещё и жрёт контекст. Я подробно описал, почему не стоит ставить 70B в оркестратор. 7B-14B с правильным промптом часто эффективнее и меньше дрифтуют, потому что у них меньше голов внимания и проще архитектура.
- Игнорировал chat template. Если модель ожидает
<|tool_result|>, а вы передаёте просто JSON — attention drift неизбежен. Всегда используйте официальный шаблон из репозитория модели. - Не обрезал историю. Агент на 10 шагах имеет 50К токенов контекста. Внимание размазывается. Нужно summarization или sliding window. Пример реализации — в статье «Когда память кончается».
| Практика | Результат |
|---|---|
| Вставка результатов в начало контекста | -60% attention drift |
| YaRN масштабирование + отключение ctx-shift | +35% точности tool calls |
| Модель 7B вместо 70B для оркестратора | -50% дрифта, +3x скорость |
❓ FAQ: ответы на проклятые вопросы
Поможет ли увеличение контекста с 8K до 32K?
Только если вы используете YaRN или NTK-aware scaling. Иначе attention drift становится только хуже — модель не может эффективно сканировать большой контекст. Я тестировал: на 32K без скалинга процент игнорированных tool calls вырос с 15% до 45%.
А как же agentic loop с summarization?
Summarization помогает, но если вы сжимаете результаты инструментов в текст, теряется структура. Лучше оставлять результаты в сыром JSON, но вставлять их в активную зону контекста. Пример — в моём проекте на GitHub (ссылка в профиле).
Может ли GLM-4.5 Air справиться с дрифтом лучше?
Я тестировал GLM-4.5 Air — она действительно меньше зацикливается в tool calls, но это не панацея. Подробно описал в отдельной статье. Главное — её чат-шаблон из коробки поддерживает явные маркеры результатов, что снижает дрифт.
🔥 Неочевидный совет: когда attention drift — это не баг, а фича
Иногда модель «забывает» результат, потому что он на самом деле не нужен для текущего шага. Это механизм экономии внимания. Если ваш агент работает в домене, где инструменты возвращают избыточные данные, — не боритесь с дрифтом, а проектируйте контекст иерархично. Пусть каждый tool call возвращает только релевантный саммари. Тогда дрифт исчезнет сам собой, потому что внимание останется на ключевых токенах. Я называю это «принципом минимального результата» — и он работает лучше любых архитектурных хаков.
* Все тесты проводились на модели Qwen 3.5 14B (дата релиза 2026) с llama.cpp версии b3420. Результаты могут отличаться для других моделей и версий.