Responses API: почему аргументы потокового tool call нельзя разбирать по первому фрагменту
Помощник должен проверить статус заказа и вызвать get_order с номером, который назвал пользователь. Без инструмента модель не знает текущий статус: он живёт во внешней системе. В обычном ответе вы получаете готовый объект вызова. При stream: true интерфейс может показать имя функции раньше её аргументов, а следующие события принесут куски строки: {"order_, затем id":"A-17"}. Попытка вызвать JSON.parse на первом куске даёт ошибку, хотя итоговый JSON корректен.
Один буфер на один вызов
В Responses API события response.function_call_arguments.delta несут output_index, item_id и очередной фрагмент delta. Если модель вызвала два инструмента, их куски могут перемежаться. Склеивать их в одну строку нельзя. Ниже обработчик хранит данные по output_index, сверяет item_id и запускает инструмент только после response.function_call_arguments.done. Это минимальная логика приложения вокруг реального формата потока; она не зависит от того, как интерфейс показывает промежуточный текст.
const pending = new Map();
function onEvent(event) {
const i = event.output_index;
if (event.type === "response.output_item.added" &&
event.item.type === "function_call") {
pending.set(i, { id: event.item.id, callId: event.item.call_id,
name: event.item.name, argumentsText: event.item.arguments ?? "" });
return null;
}
if (event.type === "response.function_call_arguments.delta") {
const call = pending.get(i);
if (!call || call.id !== event.item_id) throw new Error("Unknown call");
call.argumentsText += event.delta;
return null;
}
if (event.type === "response.function_call_arguments.done") {
const call = pending.get(i);
if (!call || call.id !== event.item_id) throw new Error("Unknown call");
pending.delete(i);
const args = JSON.parse(event.arguments);
if (call.name !== "get_order" || typeof args.order_id !== "string")
throw new Error("Invalid tool arguments");
return { callId: call.callId, name: call.name, args };
}
return null;
}
Событие done содержит полную строку аргументов, поэтому пример разбирает именно её. Накопленный argumentsText пригодится для отображения хода генерации или диагностики; при получении done можно дополнительно сверить его с полной строкой. Из результата onEvent приложение вызывает настоящий get_order(args.order_id), а ответ инструмента отправляет модели с тем же callId. До этого шага интерфейс может писать «ищу заказ», но не должен показывать выдуманный статус.
Пример намеренно не вызывает внешний API из обработчика события. Это помогает держать одну точку исполнения: цикл чтения потока получает завершённый объект, затем диспетчер проверяет разрешённое имя инструмента, вызывает ваш сервис заказов и записывает результат в историю диалога. Если инструментов несколько, диспетчер обрабатывает каждый завершённый вызов отдельно. callId нужен для ответа модели: без него она не сможет связать статус заказа с запросом, который породил вызов. Индекс выхода годится для сборки чанков только внутри одного ответа модели.
Ошибка в готовом JSON — отдельная ситуация. Завершение потока говорит лишь о том, что аргументы больше не дополнятся. Оно не гарантирует, что поле order_id имеет допустимый формат и что пользователю разрешено видеть заказ. После разбора проверьте схему, права и таймаут внешнего API. Если поток оборвался без done, не выполняйте частичный вызов; сохраните технический статус прерванного запроса и предложите повторить операцию с тем же контекстом.
Различайте три исхода в логах: поток прервался до окончания аргументов, готовый JSON не прошёл проверку, внешний сервис вернул ошибку. Первый исход можно связать с сетевым соединением и повтором генерации. Во втором полезно сохранить имя функции и безопасно усечённую строку аргументов для разбора схемы. Третий требует проверки API заказов и его прав доступа. Один общий статус «tool call failed» скрывает место сбоя и толкает к повторному вызову функции, которая могла уже выполнить побочный эффект.
Верните результат модели
В коде, который читает for await (const event of stream), передавайте каждое событие в onEvent(event) и складывайте его ненулевые результаты в массив завершённых вызовов. Дождитесь завершения ответа модели, сохраните его response.id из события response.completed, затем выполните проверенные вызовы. Для каждого результата отправьте новый запрос Responses API с previous_response_id: response.id и элементом {type: "function_call_output", call_id: call.callId, output: JSON.stringify(order)}. Так модель получит настоящий статус заказа и сможет сформулировать ответ пользователю. Если вызовов несколько, передайте несколько таких элементов с соответствующими call_id.
Проверка на простых фрагментах показывает, зачем нужен отдельный буфер: первый вызов может прислать {"order_, второй — {"order_id":"B-21"}, а затем первый завершится фрагментом id":"A-17"}. При сборке по индексу получаются два независимых заказа. При сборке в одну глобальную переменную строка повреждается, даже если каждый отдельный вызов был корректен. Отображение имени функции во время потока допустимо, но содержимое аргументов ещё не является подтверждённым действием.
Если ошибка возникает уже после получения полного объекта, пригодится разбор политики повтора при ошибке парсера LangChain. А если система одновременно принимает несколько инструментов, смотрите разбор частичного сбоя параллельных веток агента: завершение одного вызова не означает завершение других.
В платном курсе LangChain 1.0.x: c нуля до продакшн LLM-приложений вы подключаете инструменты и внешние API к агенту, строите интерфейс и API, затем добавляете таймауты, повторы и наблюдаемость. Такой маршрут помогает превратить корректно собранный вызов в работающий сценарий проверки заказа. Откройте программу курса и посмотрите, как инструментальная логика входит в приложение целиком.
Продолжить на практике
- LangChain 1.0.x: c нуля до продакшн LLM-приложений ↗
Практический курс по LangChain. Соберёте продакшн LLM-бота: RAG по PDF с цитатами, веб-поиск и трекинг заказов, многошаговый агент для анализа CSV и финальное веб-приложение (UI + API). Плюс профессиональные блоки: PromptOps и версионирование промптов, метрики качества RAG, гибридный поиск с реранком, observability/стоимость, надёжность (ретраи, таймауты), guardrails и отдельный модуль по LangGraph для устойчивых workflow. P.S. Полностью обновили материалы на актуальную версию LangChain
