The Daily Newsstand · Free, Always
Monday, October 26, 2026

[Перевод] Как Codemode меняет работу ИИ‑агентов с инструментами — и что не так с MCP

Translate

Больше года назад я писал материалы, в которых советовал не загружать пользовательские инструменты (в том числе MCP‑серверы) в контекст модели, а по возможности обходиться скриптами.

Теперь, с выходом Pi 1.0, мы добавили поддержку MCP через Codemode.

С одной стороны, к этому всё давно шло, а с другой — для кого‑то такое решение могло оказаться неожиданным. Поэтому я хочу поделиться свежими мыслями о том, что всё это значит.

Что такое инструменты

Когда обвязка агента, например Pi, предоставляет LLM инструменты для вызова, она передаёт модели их описания, которые на стороне сервера преобразуются в определённую структуру токенов. Будет ли модель склонна вызывать тот или иной инструмент, зависит от того, как она обучалась с подкреплением.

Одна из главных причин, по которым мы отдаём предпочтение CLI и bash, — возможность легко комбинировать вызовы. К тому же во время обучения модель усваивает принципы работы файловой системы.

Поэтому, когда она выполняет команду echo foo > /tmp/test.txt, то понимает, что после этого в каталоге /tmp появится файл test.txt.

Но у bash есть одно фундаментальное ограничение: с его помощью можно комбинировать только исполняемые программы. А некоторые инструменты программами не являются — они встроены непосредственно в интерфейс взаимодействия с LLM, и иначе здесь не обойтись.

Самый очевидный пример — read или view_image. Если мультимодальной модели нужно прочитать изображение, просто вызвать cat не получится: обвязка агента должна передать сами данные изображения непосредственно через протокол взаимодействия с LLM.

Другой наглядный пример — субагенты. Запускать их и управлять ими без инструментов, предоставляемых обвязкой агента, довольно сложно.

Теоретически можно создать CLI‑инструмент, который будет взаимодействовать с внешней обвязкой через переменные окружения и Unix‑сокеты, но решение получится довольно грубым.

Впрочем, есть и другая проблема — где именно выполняется код.

Мозг и руки

Чтобы лучше разобраться в этом, нужно понять, где именно выполняются все компоненты системы. Обычно здесь задействованы две разные части.

  • «Мозг» — обвязка агента. Она работает на отдельной машине и считается доверенной.

  • «Руки» — часть, которая часто находится на той же машине, но отвечает непосредственно за выполнение инструментов. В Pi мы теперь называем эту часть средой выполнения (execution environment), но по сути это целевая среда, в которой производятся все операции.

Для нас принципиально важно, что между «мозгом» (обвязкой агента) и целевой средой, где запускается bash и работают инструменты, существует чёткая граница.

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

Например, если использовать Gondolin или похожее решение для изоляции, все команды bash будут выполняться в песочнице, но сама обвязка агента останется за её пределами.

Управление обвязкой агента

И здесь мы подходим к тому, для чего на самом деле нужен Codemode.

Он позволяет LLM описывать сложные операции и управлять их выполнением на стороне обвязки агента, а не в целевой среде выполнения.

Codemode работает внутри обвязки, в собственной песочнице. В Pi для этого используется QuickJS в среде выполнения WASM с намеренно введёнными ограничениями:

  • нет доступа к сети и файловой системе;

  • нет таймеров;

  • объём оперативной памяти ограничен.

Единственный способ взаимодействовать с внешним миром — вызывать другие инструменты. В принципе, вместо JavaScript в Codemode можно было бы использовать Scheme или другой язык.

Если вы ещё не знакомы с Codemode, то по сути это способ вызывать инструменты прямо из кода на определённом языке, в нашем случае JavaScript.

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

Само название придумали коллеги из Cloudflare, которые впервые предложили этот термин.

Например, если LLM вызывает bash как обычный инструмент, мы передаём в контекст только последние 2000 строк вывода. Если агенту нужно больше, ему придётся самостоятельно обратиться к файлу с оставшейся частью вывода.

Но если тот же вызов выполняется через Codemode, он получает более объёмные результаты в структурированном виде.

Что ещё важнее, поскольку Codemode использует JavaScript, агент может описывать параллельные операции и несложные рабочие процессы.

Сейчас агенты часто используют такую схему:

  1. Сначала проверяют 5–10 элементов из ответа инструмента, чтобы разобраться в структуре данных.

  2. Затем пишут скрипт для Codemode, который обрабатывает следующие n элементов.

Codemode также позволяет сохранять состояние в истории сессии!

Это значит, что один вызов Codemode может сохранить данные, а следующий вызов в той же сессии — снова их загрузить.

И не забывайте: всё это происходит на хосте обвязки агента, а не в песочнице целевой среды.

В Pi через Codemode также доступны вызовы, которые просто не вписываются в традиционный интерфейс инструментов.

Например, если нужно сгенерировать изображения с помощью соответствующей модели или выполнить классификацию текста за один вызов модели‑классификатора, эти API Pi доступны через Codemode. В виде обычных инструментов они бы только зря занимали место в контексте.

Как это выглядит на практике

Теперь, когда мы уже достаточно об этом поговорили, стоит показать всё чуть конкретнее. Давайте разберём несколько вызовов Codemode из моих недавних сессий в Pi.

Обратите внимание: весь этот код написан не человеком. Это реальные сессии Pi, я лишь поправил отступы, чтобы его было удобнее читать.

Агент начинает использовать Codemode автоматически либо потому, что задача сама подталкивает модель выбрать этот инструмент, либо потому, что пользователь прямо об этом попросил.

Ещё один момент: по умолчанию Codemode в Pi включается только вместе с MCP, но его можно активировать и отдельно через "defaultTools": ["+codemode"] в настройках.

Достаточно просто попросить Pi это сделать.

Генерация изображений

Начнём с простого — с генерации изображений.

Генерация изображений поддерживается в AI SDK core, но не оформлена как инструмент, доступный агенту.

Раньше использовать модели изображений можно было либо через специально написанное расширение, либо заставив агента запускать node и обращаться к внутренним API для работы с изображениями.

Но поскольку в Codemode мы открываем доступ к части внутренних API моделей, агент теперь может делать это напрямую:

const [painter] = await models.getAvailableOfType("image");
const result = await models.generateImages(painter, {
  input: [{ type: "text", text: "A cute little puppy sitting on a grassy " +
    "lawn, soft natural light, photorealistic" }],
});
if (result.stopReason !== "stop") return result.errorMessage;

for (const block of result.output) {
  if (block.type === "image") image(block);
  else text(block.text);
}

Обратите внимание: вызов image() возвращает изображение в LLM как содержимое типа image.

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

Классификация

Примерно так же дело обстоит и с моделями‑классификаторами, например Jev.

Они тоже плохо вписываются в обычные рабочие процессы агента через типовые инструменты. Но вместо того, чтобы делать для них отдельный специализированный инструмент, Codemode просто позволяет агенту обращаться к AI SDK напрямую и вызывать их оттуда.

Ниже показано, как Jev используется для массовой обработки GitHub Issues и быстрого анализа тональности:

const jev = await models.getModelOfType("classifier", "typesafe", "jev-latest");
const r = await tools.bash({
  command: "gh issue list --state open --limit 100 " +
    "--json number,title,body,comments",
});
const issues = JSON.parse(r.output);

const results = await Promise.all(issues.map(async (issue) => {
  const res = await models.classify(jev, {
    state: {
      title: issue.title,
      body: (issue.body || "").slice(0, 4000),
      comments: issue.comments.slice(-5).map(c => c.body.slice(0, 800)),
    },
    questions: {
      sentiment: {
        type: "choice",
        instructions: "What is the overall sentiment of the author towards pi?",
        criteria: {
          positive: "Appreciative, happy, constructive praise",
          neutral: "Matter-of-fact report or request without emotion",
          negative: "Frustrated, annoyed, upset, or angry",
        },
      },
      frustration: {
        type: "score",
        instructions: "How frustrated is the reporter?",
        criteria: ["not at all", "mildly", "clearly frustrated", "very angry"],
      },
      kind: {
        type: "choice",
        instructions: "What kind of issue is this?",
        criteria: {
          bug: "Bug report or regression",
          feature: "Feature request or enhancement",
          question: "Question or support request",
          other: "Docs, discussion, meta, spam",
        },
      },
    },
  });
  if (res.stopReason !== "stop") {
    return { n: issue.number, title: issue.title, error: res.errorMessage };
  }
  return { n: issue.number, title: issue.title, ...res.answers };
}));

store("sentiment_results", results);
return results
  .filter(r => !r.error)
  .sort((a, b) => b.frustration.score - a.frustration.score)
  .slice(0, 12)
  .map(r => `#${r.n} ${r.frustration.score.toFixed(2)} [${r.kind.choice}] ${r.title}`);

Обратите внимание, что в примере выше мы также вызываем store(), который сохраняет результат выполнения в истории сессии. Благодаря этому при следующем вызове Codemode агент сможет снова получить эти данные, если они ему понадобятся.

Использовать здесь Promise.all вполне нормально, поскольку Pi сам ограничивает число одновременно выполняемых вызовов инструментов четырьмя, а остальные ставит в очередь.

Более интересный пример — использование Jev для управления игровым движком при отладке:

Codemode с Jev для отладки игры

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

Обратите внимание, как он организовал цикл из 30 шагов. На каждом шаге агент обращается к игровому движку, чтобы получить текстовое представление текущего состояния игры, а затем к Jev, чтобы определить следующее действие:

const jev = await models.getModelOfType("classifier", "typesafe", "jev-latest");
const tank = async (cmd) =>
  (await tools.bash({ command: `tools/tankctl "${cmd}"` })).output;
await tank("start --map assets/maps/night_arena.map");

const questions = {
  action: {
    type: "choice",
    instructions: "You control the tank '@' in a top-down tank game. " +
      "Choose the best next action.",
    criteria: {
      attack: "an enemy has line of sight to you and you can fire at it",
      approach: "no enemy has line of sight; drive toward the nearest enemy",
      dodge: "an enemy shot is heading at you and will hit soon",
      powerup: "a powerup is close and no enemy threatens you",
    },
  },
};

function commandFor(choice, st) {
  const p = st.player;
  const enemy = st.enemies.filter(e => !e.dead)
    .sort((a, b) => (b.los - a.los) || (a.dist - b.dist))[0];
  if (choice === "attack" && enemy) {
    return `fire_at tank ${enemy.id}; frames 30 until clear,damage,kill`;
  }
  if (choice === "dodge") {
    // move perpendicular to the closest incoming shot
    const s = st.projectiles.filter(s => !s.yours)
      .sort((a, b) => a.eta - b.eta)[0];
    const dir = s && Math.abs(s.vel[0]) > Math.abs(s.vel[1])
      ? (p.pos[1] > s.pos[1] ? "+down" : "+up")
      : (p.pos[0] > (s ? s.pos[0] : 0) ? "+right" : "+left");
    return `input ${dir}; frames 20 until damage; input stop`;
  }
  const powerup = st.powerups.filter(u => u.available)
    .sort((a, b) => a.dist - b.dist)[0];
  if (choice === "powerup" && powerup) {
    return `goto ${powerup.pos[0]} ${powerup.pos[1]} 180`;
  }
  return enemy ? `goto ${enemy.pos[0]} ${enemy.pos[1]} 90` : null;
}

const log = [];
for (let step = 0; step < 30; step++) {
  const st = JSON.parse(await tank("state"));
  if (st.state !== "playing") break;
  const threats = st.projectiles
    .filter(s => !s.yours && s.miss_dist < 1.5 && s.eta < 1.5)
    .map(s => `incoming shot dist ${s.dist} eta ${s.eta}s`)
    .join("\n") || "no incoming shots";
  const r = await models.classify(jev, {
    state: { map: await tank("view 8"), threats, hp: st.player.hp },
    questions,
  });
  if (r.stopReason !== "stop") {
    log.push(`#${step} classifier error: ${r.errorMessage}`);
    break;
  }
  const choice = r.answers.action.choice;
  const cmd = commandFor(choice, st);
  if (!cmd) break;
  log.push(`#${step} hp=${st.player.hp} ${choice} -> ${await tank(cmd)}`);
}
return log.join("\n");

Вызов MCP‑серверов

Наконец, Codemode отлично подходит для вызова MCP‑серверов.

Поскольку мы не предоставляем LLM прямой доступ к MCP‑инструментам, агент сначала использует доступные API для поиска инструментов внутри Codemode, чтобы выяснить, какие возможности есть у подключённых серверов.

Благодаря такому постепенному обнаружению инструментов работа с MCP уже сегодня оказывается достаточно удобной для многих задач.

Например, здесь агент сразу обращается к Sentry MCP, даже не выполняя предварительный поиск инструментов.

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

const orgs = await tools.mcp__sentry__find_organizations({});
const { organizations } = orgs.structuredContent;
const results = await Promise.allSettled(organizations.map(org =>
  tools.mcp__sentry__find_projects({
    organizationSlug: org.slug,
    regionUrl: org.regionUrl,
  })
));
return organizations.map((org, i) => {
  const r = results[i];
  if (r.status !== "fulfilled") return { org: org.slug, error: String(r.reason) };
  if (r.value.isError) return { org: org.slug, error: r.value.content };
  return {
    org: org.slug,
    projects: r.value.structuredContent.projects.map(p => p.slug),
  };
});

С современным MCP приходится бороться

Не хочу слишком углубляться в тему MCP, но на самом деле этот протокол очень выигрывает от использования Codemode.

Проблема отчасти в том, что на практике MCP часто ориентирован на обвязки агентов, которые пока (или вообще?) не используют Codemode. Впрочем, ситуация постепенно меняется.

Тем временем в качестве временного решения использовали подход Cloudflare: запускали Codemode внутри самого MCP‑сервера.

Но теперь у нас получается Codemode внутри Codemode, а это уже довольно плохо.

Возникает двойное экранирование JSON, в котором небольшие модели легко путаются, а код внутреннего Codemode не может вызывать инструменты внешнего.

Например, если использовать MCP‑серверы Cloudflare в Pi, агенту приходится писать JavaScript и передавать его через ещё один слой JavaScript.

Решение далеко не оптимальное, хотя вполне понятно, почему всё так сложилось:

const accRes = await tools.mcp__cloudflare__execute({
  code: `async () => {
    const r = await cloudflare.request({ method: "GET", path: "/accounts" });
    return r.result.map(a => ({ id: a.id, name: a.name }));
  }`,
});
const accounts = JSON.parse(accRes.content.map(c => c.text).join(""));

const out = [];
for (const account of accounts) {
  const r = await tools.mcp__cloudflare__execute({
    account_id: account.id,
    code: `async () => {
      const r = await cloudflare.request({
        method: "GET",
        path: \`/accounts/\${accountId}/workers/scripts\`,
      });
      return r.result.map(s => ({ id: s.id, modified: s.modified_on }));
    }`,
  });
  out.push({ account: account.name, workers: r.content.map(c => c.text).join("") });
}
return out;

Чего не хватает MCP

Итак, подведём итог: насколько хорошо Codemode сегодня работает с MCP?

Ну… пока не сказать, что отлично.

Дело в том, что MCP‑серверы ещё толком не рассчитаны на обвязки агентов с Codemode, хотя, насколько я понимаю, большинство обвязок его уже поддерживают.

Чтобы всё заработало как следует, стоит учесть несколько моментов:

1. Структурированные данные

Codemode удобнее работать с вызовами, которые возвращают аккуратно сформированный JSON. Поэтому MCP‑серверы должны отдавать результаты именно в таком виде, а многие пока этого не делают.

Механизм outputSchema в MCP отлично для этого подходит.

2. Единообразие результатов

Интересный случай сбоя возникает, когда MCP‑сервер возвращает данные в разном формате. Например, пытается оптимизировать расход токенов в зависимости от количества элементов в ответе.

В результате пробный запрос на пять элементов успешно выполняется, а когда сервер возвращает пакет максимального размера, обработка ломается.

3. Большие бинарные данные

Сегодня MCP ещё не поддерживает передачу больших объёмов бинарных данных, поэтому многие интересные сценарии пока работают из рук вон плохо.

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

4. Комбинируемый поиск инструментов

MCP‑сервер может лучше MCP‑клиента понимать, какой инструмент подходит для конкретной задачи.

Но пока нет удобного механизма, который позволял бы обвязке агента распределять поиск инструментов параллельно между несколькими MCP‑серверами.

Сейчас всё работает за счёт поведения, которое складывается само собой, а такой подход плохо масштабируется при одновременной работе с несколькими серверами.

Будущее Codemode

И к чему мы в итоге пришли? Получается, я отказываюсь от того, что писал год назад, когда рекомендовал использовать CLI?

Не думаю.

Более того, с моей точки зрения, экосистема MCP как раз взяла на вооружение то, о чём мы говорили ещё год назад: код действительно работает.

Но Codemode выходит за рамки MCP. Он даёт агенту больше свободы, позволяя описывать и выполнять сложные операции непосредственно через обвязку.

Впрочем, некоторые вопросы ещё предстоит решить.

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

А может быть, для комбинирования операций лучше подойдёт не JavaScript, а что‑то вроде Starlark с его детерминированностью.

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

Так что до идеального решения Codemode пока далеко. Но уже сейчас это весьма полезный подход, и я уверен, что мы будем применять его всё активнее.

Подключить MCP‑сервер к ИИ‑агенту — только половина задачи. Дальше нужно научить агента выбирать нужные инструменты, комбинировать их вызовы и обрабатывать результаты, не перегружая контекст LLM. Для этого важно понимать, как устроена агентная архитектура, какую роль играют reasoning и RAG и когда стоит распределять задачи между несколькими агентами.

Подробнее об этих механизмах и развитии агентных систем поговорим на бесплатных открытых уроках:

  • 28 октября в 18:00. «ИИ агенты на основе LLM и мультиагентные системы». Записаться

  • 16 ноября в 18:00. «Тенденции LLM и генеративного ИИ (GenAI)». Записаться

Больше онлайн-уроков об искусственном интеллекте, разработке и не только можно найти в календаре мероприятий OTUS

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.