В первом уроке модуля вы видели набросок агента с загадочной функцией llm_decide: модель сама решает, вызвать инструмент или дать ответ. Пора сделать эту функцию настоящей. Для этого у Responses API есть механизм function calling: вы описываете модели свои функции, и она отвечает не текстом, а просьбой «вызови get_weather с аргументом city="Казань"». Выполняет функцию ваша программа, а результат возвращается модели.
После урока у вас будет рабочий агент на чистом Python и пакете openai: с инструментами, циклом, лимитом шагов и обработкой ошибок. Каждый фреймворк, который вы изучите дальше, делает внутри то же самое, поэтому этот урок — ключ к пониманию всего курса.
Что уже умеем
Вы умеете отправлять запросы через Responses API, вести историю сообщений в списке и получать ответ по схеме. В прошлом уроке схема описывала ответ модели. Сегодня схемы опишут аргументы функций, а история превратится в журнал шагов агента.
Что вы узнаете в рамках статьи
- как описать функцию для модели через параметр
toolsи JSON Schema; - как распознать в ответе запрос на вызов функции и прочитать аргументы;
- как вернуть результат модели через
function_call_outputиcall_id; - как собрать из этого цикл агента с ограничением
max_steps; - как обрабатывать ошибки инструментов, чтобы агент не падал.
Ключевые термины
- Function calling — механизм, при котором модель возвращает запрос на вызов функции с аргументами вместо текста.
- Описание инструмента — словарь с именем функции, описанием и JSON Schema её параметров.
- function_call — элемент ответа модели: имя функции, аргументы в виде JSON-строки и идентификатор вызова.
- call_id — идентификатор конкретного вызова, по которому модель сопоставляет запрос и результат.
- function_call_output — элемент, которым программа возвращает модели результат функции.
- max_steps — предел числа обращений к модели в одном запуске агента.
Описание инструментов для модели
Модель не видит ваш код на Python. Она знает о функции только то, что вы написали в описании: имя, назначение и параметры. Параметры описываются JSON Schema — тем же форматом, в который Pydantic превращал классы в прошлом уроке.
def get_weather(city: str) -> str:
# учебная заглушка; в реальном агенте здесь запрос к сервису погоды
data = {"Казань": "+12 °C, облачно", "Сочи": "+21 °C, солнечно"}
return data.get(city, "нет данных")
def calculate(expression: str) -> str:
return str(eval(expression, {"__builtins__": {}})) # только для учебного примера
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Возвращает текущую погоду в городе. Используй, когда спрашивают о погоде.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Название города, например Казань"},
},
"required": ["city"],
},
},
{
"type": "function",
"name": "calculate",
"description": "Вычисляет арифметическое выражение. Используй для любых расчётов.",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Выражение на Python, например 12 * 9 / 5 + 32"},
},
"required": ["expression"],
},
},
]
TOOLS = {"get_weather": get_weather, "calculate": calculate}
Здесь две разные вещи. Список tools — описания для модели, он уходит в API. Словарь TOOLS — связь имени с настоящей функцией, его использует ваша программа. Описание — это по сути инструкция: чем точнее вы напишете, когда инструмент нужен и что означает каждый параметр, тем реже модель будет ошибаться в выборе.
Первый вызов функции
Передадим описания в запрос и посмотрим, что вернёт модель:
import json
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-luna",
input="Какая погода в Казани?",
tools=tools,
)
for item in response.output:
if item.type == "function_call":
print(item.name, item.arguments, item.call_id)
Что увидим в консоли:
get_weather {"city": "Казань"} call_a1b2c3
Текста в ответе нет — response.output_text будет пустым. Вместо этого в списке response.output лежит элемент типа function_call. Обратите внимание: item.arguments — это строка в формате JSON, а не словарь. Перед вызовом функции её нужно разобрать через json.loads. Модель может попросить и несколько вызовов в одном ответе, поэтому response.output всегда перебирают циклом.
Возвращаем результат модели
Модель попросила вызвать функцию, но сама её не выполняет. Выполняем мы и отправляем результат обратно. Как и в диалоге из урока 1.4, ведём историю в списке: добавляем в неё все элементы ответа модели, а затем результат с тем же call_id.
history = [{"role": "user", "content": "Какая погода в Казани?"}]
response = client.responses.create(model="gpt-6-luna", input=history, tools=tools)
history += response.output # запрос модели на вызов функции
for item in response.output:
if item.type == "function_call":
args = json.loads(item.arguments)
result = TOOLS[item.name](**args)
history.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": result,
})
response = client.responses.create(model="gpt-6-luna", input=history, tools=tools)
print(response.output_text)
Что увидим: «Сейчас в Казани +12 °C, облачно». Второй запрос получил всю цепочку: вопрос, запрос на вызов и результат. По call_id модель понимает, на какой из её вызовов пришёл ответ. Если забыть добавить response.output в историю, API вернёт ошибку: результат будет ссылаться на вызов, которого модель «не делала».
Цикл агента с max_steps
Осталось повторять эти шаги, пока модель не перестанет вызывать функции. Это и есть настоящая реализация run_agent из урока 1.1:
def run_agent(task: str, max_steps: int = 5) -> str:
history = [{"role": "user", "content": task}]
for step in range(max_steps):
response = client.responses.create(
model="gpt-6-luna",
instructions="Ты помощник. Для фактов и расчётов используй инструменты.",
input=history,
tools=tools,
)
history += response.output
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
return response.output_text # финальный ответ
for call in calls:
args = call.arguments
try:
args = json.loads(call.arguments)
result = TOOLS[call.name](**args)
except Exception as error:
result = f"Ошибка инструмента: {error}"
print(f"Шаг {step}: {call.name}({args}) -> {result}")
history.append({"type": "function_call_output",
"call_id": call.call_id, "output": str(result)})
return "Не удалось решить задачу за отведённое число шагов"
print(run_agent("Какая погода в Казани и сколько это в Фаренгейтах?"))
Что увидим в консоли:
Шаг 0: get_weather({'city': 'Казань'}) -> +12 °C, облачно
Шаг 1: calculate({'expression': '12 * 9 / 5 + 32'}) -> 53.6
В Казани +12 °C, облачно — это 53.6 °F.
Разберём ключевые решения. Признак окончания — отсутствие вызовов функций в ответе: модель решила, что данных достаточно. Ошибка в инструменте не роняет программу, а возвращается модели как результат: увидев «Ошибка инструмента», она может исправить аргументы и попробовать снова. А max_steps гарантирует, что даже запутавшийся агент остановится — каждый шаг стоит денег, и, как вы помните из урока 1.2, каждый следующий шаг дороже предыдущего.
Частые ошибки
- Вызывать функцию с
item.argumentsнапрямую. Это JSON-строка, а не словарь. Сначалаjson.loads, потом*args. - Не добавлять
response.outputв историю. Результат функции без исходного запроса на вызов API не примет. Добавляйте весь вывод модели передfunction_call_output. - Путать
call_id. Каждый результат должен идти сcall_idсвоего вызова, иначе модель перепутает ответы при нескольких вызовах. - Размытые описания инструментов. Описание «погода» модели мало что говорит. Пишите, что делает функция, когда её вызывать и в каком формате параметры.
Наиболее частые вопросы
- Модель сама выполняет мои функции? — Нет. Модель только просит вызвать функцию, выполняет её всегда ваша программа. Поэтому вы полностью контролируете, что происходит.
- Можно ли вернуть результат не строкой? — Поле
outputожидает строку. Словари и списки превращайте в строку черезjson.dumps. - Что, если модель вызвала несуществующую функцию? — В нашем цикле
TOOLS[call.name]вызоветKeyError, он перехватится, и модель получит сообщение об ошибке. - Можно ли вместо истории использовать previous_response_id? — Да: тогда в
inputотправляются толькоfunction_call_output. Ручная история нагляднее, поэтому в уроке используем её.
Итоги
Поздравляем, вы написали настоящего AI-агента с нуля! Функция llm_decide, которая в первом уроке была условной, теперь работает: модель получает описания инструментов, сама выбирает нужный, ваша программа выполняет функцию и возвращает результат, а цикл с max_steps доводит задачу до ответа и не даёт агенту зациклиться. Вы прошли весь первый модуль: от токенов и стоимости до структурированных ответов и цикла агента.
Теперь вы видите, сколько служебного кода нужно даже простому агенту: описания на JSON Schema, разбор аргументов, история, обработка ошибок. Во втором модуле вы перейдёте на LangChain, где create_agent берёт эту рутину на себя, — и благодаря этому уроку будете точно понимать, что происходит у него внутри.