В предыдущих уроках вы узнали, как в Responses API выполняются вызовы инструментов, включая инструмент файлового поиска (RAG). Для этого требовалось задать точное описание параметров всех функций в формате JSON Schema, а затем вручную обработать функциональный вызов — посмотреть на ответ модели и вызвать нужную функцию в коде Python.
ЛогикаА что, если сделать этот процесс единообразнее? Мы можем применить объектно-ориентированный подход и библиотеку Pydantic для работы с данными. Сейчас всё разберём!📚 В тексте урока содержатся фрагменты кода агента. Полный код проекта доступен в репозитории на GitHub.
Использование Pydantic-объектов
💡 Pydantic — одна из самых популярных Python-библиотек для валидации данных. С её помощью можно описывать классы с полями строгой типизации и проверять, чтобы данные соответствовали заданной структуре.
ЛогикаКроме того, Pydantic-классы могут автоматически генерировать описание своей структуры в формате JSON Schema — это пригодится в нашей реализации!Для сложных приложений удобнее использовать объектно-ориентированный подход, где инструменты — это классы с методами для обработки вызовов.Например, при создании фитнес-ассистента можно описать класс, который будет представлять упражнение:
from pydantic import BaseModel, Field
from typing import Optional, List
class Exercise(BaseModel):
тип: Optional[str] = Field(description="Тип упражнения (кардио или силовое)", default=None)
название: Optional[str] = Field(description="Название упражнения", default=None)
болевые_ощущения: Optional[str] = Field(description="Болевые ощущения при выполнении упражнения", default=None)
пульс: Optional[int] = Field(description="Пульс в момент выполнения упражнения", default=None)
подходы: Optional[int] = Field(description="Количество подходов", default=None)
повторения: Optional[int] = Field(description="Количество повторений", default=None)
На что обратить внимание в этом описании:
- Класс унаследован от класса
BaseModelиз библиотеки Pydantic; - Классы всех полей чётко описаны с помощью стандартной типизации Python;
- Поля инициализированы с помощью
Field, где задаются дополнительные параметры: значение по умолчанию и описание. Это важно, потому что эти данные переносятся в автоматически сгенерированную JSON-схему. В дальнейшем эта схема служит подсказкой, которую языковая модель использует для извлечения аргументов функции из входного запроса пользователя.
💡 Для полей можно использовать англоязычные названия.Объект Exercise, описанный выше, используется для валидации данных и не содержит функциональности. Но Pydantic-классы — это полноценные Python-классы, поэтому в этот же объект можно добавить функцию обработки данных. Она будет работать при функциональном вызове языковой модели. Название для этой функции — process. С учётом этого класс Exercise будет выглядеть так:
# База данных для хранения упражнений
exercise_db = {}
# Модель для упражнения
class Exercise(BaseModel):
"""Эта функция позволяет добавлять информацию о сделанном в зале упражнении."""
тип: Optional[str] = Field(description="Тип упражнения (кардио или силовое)", default=None)
название: Optional[str] = Field(description="Название упражнения", default=None)
болевые_ощущения: Optional[str] = Field(description="Болевые ощущения при выполнении упражнения", default=None)
пульс: Optional[int] = Field(description="Пульс в момент выполнения упражнения", default=None)
подходы: Optional[int] = Field(description="Количество подходов", default=None)
повторения: Optional[int] = Field(description="Количество повторений", default=None)
def process(self, session_id):
"""Обрабатывает добавление упражнения"""
if session_id not in exercise_db:
exercise_db[session_id] = []
exercise_db[session_id].append(self)
return "Упражнение добавлено"
💡 К классу выше также добавили docstring — описание функции добавления упражнения. Вследствии оно будет использовано при формировании описания инструмента для языковой модели.Функция process принимает на вход специальный параметр — session_id. Это уникальный ключ диалоговой сессии пользователя. Он нужен, чтобы сохранять списки сделанных упражнений для каждого собеседника. Для хранения упражнений используется словарь exercise_db, но в настоящей реализации ассистента для этого используют базу данных.Аналогично можно описать функцию, которая выводит все сохранённые упражнения для каждого пользователя. В этом случае класс не будет содержать полей данных, но сохранит описание функции в виде docstring и реализует метод process.
# Функция для получения списка упражнений
class ListExercises(BaseModel):
"""Эта функция позволяет получить список сделанных упражнений"""
def process(self, session_id):
"""Возвращает список упражнений для сессии"""
if session_id not in exercise_db:
return "Упражнений нет"
else:
return '\n'.join([
f"{i+1}. {x.название} ({x.тип}, {x.подходы} подходов, {x.повторения} повторений)"
for i, x in enumerate(exercise_db[session_id])
])
ЛогикаОтлично, классы для представления отдельных функций описаны! Теперь опишем класс Agent, который реализует агента с возможностями Function Calling.
Класс для описания агентов
Задача — описать класс Agent, который инкапсулирует процесс вызова инструментов и сохранение диалога с пользователем. В качестве отправной точки, послужит класс Agent — его уже описывали ранее в курсе. В нём было показано, как организовать поддержку диалога с пользователем через запоминание previous_response_id в процессе разговора.Чтобы организовать поддержку инструментов, нужно передать в функцию client.responses.create список описаний инструментов в виде словарей. Чтобы преобразовать описание функции в виде Pydantic-класса в такой словарь, нужно описать специальную функцию:
def _create_tool_annot(self, x):
"""Создаёт описание инструмента для API"""
if isinstance(x,dict):
return x
if issubclass(x, BaseModel):
return {
"type": "function",
"name": x.__name__,
"description": x.__doc__,
"parameters": x.model_json_schema(),
}
else:
return x
Этой функции можно подать на вход Pydantic-класс, получить для неё описание и передать его языковой модели. 💡 Описание функции берётся из doc-строки класса, а JSON-схема генерируется с помощью встроенной в Pydantic-функции — model_json_schema.Если же функции передать исходное описание инструмента в виде словаря, оно будет возвращено без изменений. Это позволяет передавать в класс Agent Pydantic-инструменты и инструмент файлового поиска.Теперь в конструктор класса нужно передать системный промт instruction, список инструментов tools, модель model и стратегию вызова инструментов tool_choice:
# Расширенный класс агента с поддержкой объектов-инструментов
class Agent():
def __init__(self, instruction, tools=None, model=None, tool_choice='auto'):
self.instruction = instruction
self.model = model
self.tool_choice = tool_choice
self.tool_map = {x.__name__: x for x in tools if not isinstance(x,dict) and issubclass(x, BaseModel)}
self.tools = [ _create_tool_annot(x) for x in tools ]
self.user_sessions = {}
В конструкторе сохраняются все переданные параметры в переменных класса. Затем метод _create_tool_annot преобразует список инструментов в формат, который можно передать языковой модели. Параллельно формируется словарь tool_map, позволяющий по имени инструмента получить соответствующий Pydantic-класс.Теперь нужно реализовать внутри этого класса функцию __call__, которая будет обрабатывать реплику пользователя и формировать ответ:
class Agent():
...
def __call__(self, message, session_id='default'):
"""Обрабатывает сообщение пользователя"""
s = self.user_sessions.get(
session_id,
{'previous_response_id': None, 'history': []})
s['history'].append({'role': 'user', 'content': message})
# Вызов модели с инструментами
res = client.responses.create(
model=self.model,
store=True,
tools=self.tools,
tool_choice=self.tool_choice,
instructions=self.instruction,
previous_response_id=s['previous_response_id'],
input=message
)
# Обработка вызова инструментов
tool_calls = [item for item in res.output if item.type == "function_call"]
if tool_calls:
s['history'].append({'role': 'func_call', 'content': res.output_text})
out = []
for call in tool_calls:
print(f" + Обработка: {call.name} ({call.arguments})")
try:
fn = self.tool_map[call.name]
args = call.arguments or "{}"
obj = fn.model_validate(json.loads(args))
result = obj.process(session_id)
except Exception as e:
result = f"Ошибка: {e}"
out.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": result
})
# Отправка результатов обратно модели
res = client.responses.create(
model=self.model,
input=out,
tools=self.tools,
previous_response_id=res.id,
store=True
)
# Сохранение состояния
if res.status=='incomplete':
print(f"WARNING: Incomplete response status. Reason={res.incomplete_details.reason}")
else:
s['previous_response_id'] = res.id
s['history'].append({'role': 'assistant', 'content': res.output_text})
self.user_sessions[session_id] = s
return res
Логика работы функции:
- Входящее сообщение пользователя сохраняется в истории. Для каждой сессии (
session_id) ведётся отдельная история переписки. Еслиsession_idне указан, используется сессияdefault. - Из словаря сессий
self.user_sessionsизвлекаетсяprevious_response_id. Затем вызывается языковая модель с передачей этого ID, списка инструментов, системного промпта и других параметров. - Если в ответе модели есть вызовы функций (их может быть несколько), каждый обрабатывается по очереди. Для каждого вызова:
- по полю
nameопределяется имя функции; через словарьtool_mapнаходится соответствующий Pydantic-класс; - по аргументам
argumentsсоздаётся Pydantic-объектobjс валидацией данных с помощьюmodel_validate; - вызывается метод
processу полученного объекта.
- по полю
- После обработки сохраняется последний
response_idв словаре сессий и возвращается ответ пользователю. Это выполняется только при успешном завершении запроса и статусе, отличном отincomplete. Такой статус возможен, например, при срабатывании контентного фильтра.
При использовании такого агента можно добавить инструмент файлового поиска (RAG). Для этого нужно передать в класс словарь с его описанием. В этом случае описание инструмента отправляется в Responses API, и вызов полностью выполняется на стороне облака — класс получает уже готовый ответ модели без дополнительной обработки инструментов поиска.
Использование класса Agent
Выше уже описан набор из двух инструментов в виде Pydantic-классов. Создать агента, который использует эти инструменты, можно с помощью кода:
instruction = """
Ты — опытный фитнес-тренер, задача которого — помочь мне тренироваться в зале. Ты можешь
советовать упражнения, давать рекомендации по питанию и т. д. Ты также можешь вести
дневник выполненных пользователем упражнений - для этого используй функцию `Exercise`. Чтобы
показать список выполненных упражнений, используй `ListExercises`.
"""
fit_agent = Agent(instruction, tools=[Exercise, ListExercises], model=model)
# Общение с агентом
response = fit_agent('Я сделал 10 приседаний, запиши!')
print(response.output_text)
response = fit_agent('Напомни, какие я сделал упражнения?')
print(response.output_text)
Можно добавить к этому агенту инструмент для файлового поиска по векторному хранилищу (которое вы создали в прошлом уроке):
search_tool = {
"type" : "file_search",
"vector_store_ids" : [vector_store.id],
"max_num_results" : 5
}
fit_agent = Agent(
instruction,
tools=[Exercise, ListExercises, search_tool],
model=model,
tool_choice='required')
Теперь в диалоге с агентом мы можем спрашивать его про пищевые добавки и конкретный фитнес-клуб, а ещё запоминать сделанные упражнения! Кстати, следующее упражнение для вас — небольшой тест.
Как при работе с классомAgentизменить параметры инструмента файлового поиска — например, количество найденных чанков?
Тоже правильный ответ
Задать, как один из параметров в словарном описании инструмента
Правильно, все встроенные в облако инструменты поиска описываются в виде словарей, в которых можно также задавать параметры инструментов.Внести изменения в класс Agent
Неправильный ответ
Внести изменения в Pydantic-класс, описывающий инструмент поискаНет, все встроенные в облако инструменты поиска описываются в виде словарей, в которых можно также задавать параметры инструментов.Указать параметры при инициализации AgentКак вам задача?
При использовании описанного классаAgentоткуда модель узнаёт, какие инструменты ей следует вызвать и какие аргументы им передать? Выберите три ответа.
Тоже правильный ответ
Из doc-строки Pydantic-класса, описывающего инструмент
Правильно, doc-строка передаётся модели как часть описания инструмента.Неправильный ответИз таблицы отображения инструментов tool_mapНет, таблица отображения инструментов служит для нахождения правильного Pydantic-класса по имени инструмента и не передаётся модели.
Правильный ответ
Из системного промта
Правильно, системный промт может помочь модели в правильном вызове инструментов.Правильный ответИз описания полей в PydanПравильно, описания полей класса передаются модели как часть JSON-схемы инструмента.Из истории перепискиКак вам задача?
Итоги урока
В этом уроке вы узнали, как описать подход к определению локальных инструментов в виде отдельных классов с помощью Pydantic. Это модульный и расширяемый подход для сложных приложений с множеством инструментов. При этом вся типовая логика отработки инструментов и поддержания диалога спрятана внутрь класса Agent, который вы можете взять за основу при разработке диалоговых систем на основе LLM.
Источник : https://practicum.yandex.ru/trainer/yc-ml-aiagents/lesson/f46d951c-9698-4469-bf2d-13eabccbdc3b/
0 комментариев