В предыдущих уроках вы узнали, как в 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/

Рубрики: Uncategorized

0 комментариев

Добавить комментарий

Заполнитель аватара

Ваш адрес email не будет опубликован.