13. Подключение инструментов к LLM
В главе 12 мы создали инструменты(tools), которые может использовать агент. Теперь пришло время подключить эти инструменты к LLM, чтобы агент действительно мог ими пользоваться.
Каким бы хорошим ни был инструмент, LLM не сможет его использовать, не зная о его существовании. Ей нужно знать, что доступно, и выбрать подходящий инструмент для каждого запроса. Однако, как мы узнали в главе 12, LLM не выполняет инструменты напрямую. Вместо этого она запрашивает «вызови этот инструмент с такими аргументами», а наш код агента вызывает инструмент. Весь этот механизм называется вызовом инструментов(tool calling).
В этой главе мы привяжем инструменты к LLM, выполним запросы на вызов инструментов и передадим результаты обратно в LLM — реализуя полный цикл. Этот цикл является основой цикла агента, который мы построим в главе 14.
13.1) Привязка инструментов и анализ запросов на вызов инструментов
Чтобы LLM могла использовать инструменты, ей сначала нужно знать, что доступно. Когда мы передаём в LLM name, description и схему входных данных каждого инструмента, она узнаёт, когда и как использовать каждый инструмент. Этот процесс называется привязкой инструментов(tool binding).
После привязки инструментов LLM при получении вопроса сделает одно из двух. Если инструмент не нужен, она отвечает текстом, как обычно. Если инструмент нужен, она возвращает структурированный запрос: «вызови этот инструмент с такими аргументами». Затем наш агент анализирует этот запрос и вызывает указанный инструмент.
Начнём с того, как привязывать инструменты.
13.1.1) Привязка инструментов с помощью bind_tools()
Привязка инструментов выполняется одним методом: bind_tools(). Каждая чат-модель, поддерживающая вызов инструментов, предоставляет этот метод. Вызов bind_tools() возвращает новый объект модели с привязанными к ней инструментами.
Давайте привяжем инструмент get_weather, который мы создали в главе 12.
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Получает текущую погоду для заданного города."""
return f"It's always sunny in {city}!"
# Привязываем инструмент к модели — возвращается новый объект модели с привязанным инструментом
llm_with_tools = llm.bind_tools([get_weather])Вот и всё. Возвращённый llm_with_tools — это модель, которая знает об инструменте get_weather. Обратите внимание, что исходная llm совсем не изменилась — она по-прежнему ничего не знает ни о каких инструментах.
bind_tools() принимает список инструментов в качестве аргумента и возвращает новый объект модели с привязанными к нему инструментами. Внутри он преобразует метаданные каждого инструмента (name, description и схему входных данных) в формат, понятный провайдеру LLM, поэтому схемы инструментов отправляются вместе с каждым вызовом модели.
13.1.2) Анализ запросов на вызов инструментов
Что происходит, когда мы отправляем вопрос о погоде модели с привязанным инструментом погоды?
response = llm_with_tools.invoke("What's the weather in Paris?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")Вывод:
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''Поскольку llm_with_tools по-прежнему является чат-моделью, она возвращает AIMessage. Но в этом ответе есть два отличия.
Во-первых, response.tool_calls содержит список запросов на вызов инструментов. Каждый запрос — это словарь с четырьмя ключами:
name— имя вызываемого инструмента. Соответствует значениюnameиз метаданных инструмента.args— аргументы, которые нужно передать при вызове инструмента. Модель проанализировала схему входных данных и вопрос пользователя, чтобы составить{'city': 'Paris'}.id— уникальный идентификатор этого вызова. LLM использует его позже, чтобы сопоставить результаты выполнения инструментов с запросами на их вызов.type— всегда'tool_call'.
Во-вторых, response.content — пустая строка. Этот ответ не является финальным текстовым ответом — это запрос на вызов инструмента.
Чтобы определить, является ли ответ запросом на вызов инструмента, проверяйте tool_calls, а не content. Если tool_calls непуст, это запрос на вызов инструмента. Если он пуст, значит, LLM дала текстовый ответ напрямую.
13.2) Выполнение инструментов и передача результатов обратно
LLM запросила вызов инструмента get_weather. Теперь нашему коду агента нужно выполнить инструмент и передать результат обратно в LLM.
Обработка запроса на вызов инструмента состоит из трёх шагов:
- Извлечь имя инструмента и аргументы из
tool_callsи выполнить соответствующий инструмент. - Преобразовать результат выполнения в
ToolMessage. - Отправить всю беседу (вопрос пользователя + запрос LLM на вызов инструмента + результат выполнения инструмента) обратно в LLM, чтобы получить ответ.
Вот схема этого процесса:
13.2.1) Выполнение инструмента и создание ToolMessage
Сначала давайте извлечём имя инструмента и аргументы из tool_calls и запустим соответствующий инструмент. Чтобы искать инструмент по имени, создадим словарь с ключами по именам инструментов.
# Словарь для поиска инструментов по имени
tool_map = {get_weather.name: get_weather}Далее мы извлекаем запрос на вызов инструмента из tool_calls, находим соответствующий инструмент в tool_map и выполняем его.
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # Вывод: It's always sunny in Paris!Чтобы передать результат выполнения инструмента обратно в LLM, нам нужно обернуть его в ToolMessage. У ToolMessage есть два обязательных поля:
content— результат выполнения инструмента в виде строки.tool_call_id— значениеidизtool_calls. LLM использует его, чтобы определить, к какому запросу относится результат.
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # должен совпадать с id запроса
)
print(tool_message)Вывод:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'В коде выше мы передали args для выполнения инструмента, а затем объединили результат с tool_call_id, чтобы построить ToolMessage.
Существует более простой способ сделать это за один шаг. Вместо передачи только args в .invoke() передайте весь словарь tool_call. LangChain выполнит инструмент и автоматически вернёт ToolMessage.
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)Вывод:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'Результат тот же, но код намного проще, и нет риска несоответствия tool_call_id. В дальнейшем мы будем использовать этот подход.
13.2.2) Отправка результатов работы инструмента в LLM
Теперь давайте передадим результат выполнения инструмента в LLM. Мы отправляем всю историю беседы — вопрос пользователя, запрос LLM на вызов инструмента и результат выполнения инструмента — обратно в LLM, и она генерирует ответ на основе результата работы инструмента.
Давайте соберём весь процесс от начала до конца в едином фрагменте кода.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# Определяем инструменты и привязываем их
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Получает текущую погоду для заданного города."""
return f"It's always sunny in {city}!"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# Шаг 1: Отправляем вопрос пользователя и получаем ответ LLM
messages = [HumanMessage("What's the weather in Paris?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# Шаг 2: Если был запрошен вызов инструмента, выполняем инструмент и добавляем результат
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# Шаг 3: Отправляем всю историю беседы обратно в LLM
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Вывод:
Сейчас в Париже солнечно. Хорошего дня!LLM сгенерировала финальный текстовый ответ на основе результата выполнения инструмента. Ранее в разделе 13.1 content был пустой строкой — но после передачи результата работы инструмента обратно мы получили настоящий ответ.
Давайте проверим, какие сообщения находятся в списке messages на Шаге 3.
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")Вывод:
HumanMessage: "What's the weather in Paris?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"Три сообщения хранятся по порядку: вопрос пользователя, запрос LLM на вызов инструмента (пустое содержимое) и результат выполнения инструмента. LLM увидела вопрос пользователя, решила, что ей нужен инструмент, и запросила вызов get_weather. Код агента выполнил get_weather и добавил результат в список сообщений. Получив всё необходимое, LLM сгенерировала свой финальный ответ.
13.3) Предоставление LLM нескольких инструментов
У реальных агентов обычно есть несколько инструментов. Всё, что мы рассмотрели до сих пор, работает точно так же и с несколькими инструментами.
Давайте добавим calculate и search_web вместе с get_weather, чтобы привязать три инструмента.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Получает текущую погоду для заданного города."""
return f"It's always sunny in {city}!"
@tool
def calculate(expression: str) -> str:
"""Вычисляет простое арифметическое выражение, например '123 * 456'."""
return str(eval(expression)) # Внимание: eval() представляет угрозу безопасности. Не используйте в продакшене.
@tool
def search_web(query: str) -> str:
"""Ищет в интернете актуальную информацию по теме."""
return f"Top result for '{query}': ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}Давайте зададим вопрос на вычисление.
response = llm_with_tools.invoke("What is 123 multiplied by 456?")
print(response.tool_calls)Вывод:
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]LLM запросила вызов calculate. Она сравнила description каждого инструмента с вопросом пользователя и определила, что calculate — подходящий инструмент.
Мы можем выполнить инструмент и получить финальный ответ, используя тот же шаблон из раздела 13.2.
messages = [HumanMessage("What is 123 multiplied by 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # Вывод: 123 умножить на 456 равно 56 088.В разделе 13.2 мы обрабатывали только первый запрос с помощью tool_calls[0], но здесь мы перебираем все запросы с помощью цикла for. Причина в том, что LLM может запросить несколько вызовов инструментов одновременно.
Несколько одновременных запросов на вызов инструментов
Давайте посмотрим, что происходит, когда несколько инструментов запрашиваются одновременно. Код тот же — меняется только вопрос.
messages = [HumanMessage("What's the weather in Tokyo, and what is 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Вывод:
Tool: get_weather
Tool: calculate
В Токио сейчас солнечно, а 123 умножить на 456 равно 56 088.LLM запросила и get_weather, и calculate, а цикл for выполнил все запросы в tool_calls и добавил их результаты в список сообщений. Затем LLM использовала эти результаты для генерации финального ответа.