Два вызова одного инструмента завершились вразнобой: связываем ответы по call_id
Помощник службы поддержки сравнивает сроки доставки по двум заказам. Модель дважды вызывает get_delivery. Ответ по второму заказу приходит раньше первого. Если приложение собирает результаты по порядку прихода и затем приклеивает к исходному списку через zip, в следующий запрос уйдут сроки от чужих заказов. Значения выглядят правдоподобно, поэтому такой баг легко пропустить на тесте с одним инструментом.
Как отличить вызовы с одинаковым именем
В Responses API каждый элемент типа function_call несёт имя функции, аргументы и call_id. Следующий запрос содержит элементы function_call_output с теми же call_id. Имя инструмента нужно для выбора обработчика, а его аргументы — для конкретного запроса. Идентификатор элемента id и идентификатор вызова call_id — разные поля. В результат передаём именно call_id.
Передавайте идентификатор внутрь каждой корутины вместе с аргументами. Тогда обработчик вернёт готовую пару даже при завершении в произвольном порядке. Отправляйте результаты всех вызовов текущего хода после того, как собрали их исходы. Если предусмотрен частичный отказ, верните по соответствующему call_id структурированную ошибку, которую модель сможет объяснить пользователю. Нельзя брать результат соседнего вызова, чтобы заполнить пустое место.
Локальная проверка с обратным порядком
В примере два элемента JSON уже содержат полные аргументы, как после завершённого ответа модели. Встроенный поиск по двум строкам заменяет сервис доставки. Первый вызов ждёт сигнала, который подаёт второй. Затем собираем результаты в порядке готовности и строим тело следующего HTTP-запроса. Так можно проверить связь вызовов даже без ключа и модели.
import asyncio
import json
calls = [
{"type": "function_call", "id": "fc_A", "call_id": "call_A",
"name": "get_delivery", "arguments": '{"order_id":"A"}'},
{"type": "function_call", "id": "fc_B", "call_id": "call_B",
"name": "get_delivery", "arguments": '{"order_id":"B"}'},
]
delivery_days = {"A": 2, "B": 5}
tools = [{
"type": "function", "name": "get_delivery", "strict": True,
"description": "Возвращает срок доставки заказа в днях.",
"parameters": {
"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"], "additionalProperties": False,
},
}]
async def build_next_request(response_id):
second_ready = asyncio.Event()
async def execute(call):
assert call["name"] == "get_delivery"
order_id = json.loads(call["arguments"])["order_id"]
if order_id == "A":
await second_ready.wait()
result = {"order_id": order_id, "days": delivery_days[order_id]}
if order_id == "B":
second_ready.set()
return {
"type": "function_call_output",
"call_id": call["call_id"],
"output": json.dumps(result),
}
pending = [execute(call) for call in calls]
outputs = [await done for done in asyncio.as_completed(pending)]
return {"model": "gpt-4.1", "previous_response_id": response_id,
"tools": tools, "input": outputs}
next_request = asyncio.run(build_next_request("resp_demo"))
outputs = next_request["input"]
print("Порядок:", [item["call_id"] for item in outputs])
assert [item["call_id"] for item in outputs] == ["call_B", "call_A"]
by_call = {item["call_id"]: json.loads(item["output"]) for item in outputs}
assert by_call == {
"call_A": {"order_id": "A", "days": 2},
"call_B": {"order_id": "B", "days": 5},
}
print(by_call)
Сначала готов результат для call_B, затем для call_A. Проверка раскладывает результаты по идентификатору и сверяет срок каждого заказа. Если вместо идентификатора из собственной корутины взять его из исходного списка по позиции, оба срока поменяются местами и тест упадёт. Для реального теста инструмента дополнительно проверяйте, что возвращаемый ID заказа совпадает с тем, который был в аргументах. Это ловит ошибку внутри самого обработчика.
Как отправить результаты модели
В примере используется продолжение по previous_response_id. Передайте сформированные поля в client.responses.create(**next_request) в рабочем приложении. Если инструментов на следующем ходе может быть несколько, приложите их определения снова. Если первый запрос задавал правила через instructions, передайте нужные правила повторно: это поле предыдущего ответа не переносится через previous_response_id. При самостоятельном ведении истории добавляйте в неё элементы ответа модели и результаты инструментов вместе, сохраняя связь по call_id.
Один вызов модели может закончиться очередной порцией инструментов. Примените ту же схему к новому ходу и храните номер хода и response.id в трассе. Два полезных сценария для теста — одинаковое имя функции с разными аргументами и ошибка только второго вызова. Оба теста проверяют, что следующий шаг агента знает, какой заказ он действительно проверил.
Практика на Latorn
Если ещё не подключали модель из Python, начните с бесплатного курса «LLM API с нуля». Там есть первый запрос из Python и подключение модели к HTTP-сервису.
На платном AI Agents PRO вы свяжете модель с инструментами и внешними API, а затем научитесь разбирать по трассировкам, где исполнился неверный шаг. Эта практика пригодится, когда вызовы возвращают разные статусы и агент должен объяснить результат через собственный API. Посмотреть программу AI Agents PRO.
Продолжить на практике
- AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшне ↗
Курс разработки AI-агентов на Python: соберёте сервис с поиском по документам, инструментами и внешними API. LangGraph, RAG, оценка качества, контроль расходов и запуск с Docker. Самостоятельная практика для разработчиков; пробные уроки доступны до покупки.
