LangChain Tools: инструменты и вызов функций | Курс по LangChain урок 6
Цель урока
Вы научитесь создавать инструменты с помощью @tool, привязывать их к модели используя .bind_tools().
Реализовывать полный цикл tool use, когда модель сама выбирает нужный инструмент,
вызывает его и строит ответ на основе результата.
Необходимые знания:
- Уроки 1–5 (LCEL, промпты, нелинейные пайплайны)
- Декораторы в Python
- Pydantic на базовом уровне
Ключевые концепции:
- Что такое tool use и зачем он нужен
@tool- создание инструмента из функции.bind_tools()- привязка инструментов к моделиToolMessage- передача результата инструмента обратно в модель- Ручной цикл вызовов
tool_callsвAIMessage
Зачем модели инструменты
LLM хорошо рассуждает и генерирует текст, но не умет:
- Получить актуальные данные (курс валюты, погода, данные из БД)
- Выполнить вычисления (математика, даты)
- Взаимодействовать с внешними системами (отправить письмо, создать задачу)
Tool use решает это, разработчик описывает набор функций, модель решает, какую из них вызвать и с какими аргументами. Код функции выполняется, а результат возвращается модели и она строит финальный ответ.
Схема цикла:
пользователь → модель → [решает вызвать инструмент]
↓
вызов функции в коде
↓
результат → модель → финальный ответ
Создание инструмента с помощью @tool
Декоратор @tool превращает обычную функцию в инструмент. Модель получает название, описание из docstring и схему аргументов из type hints:
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Возвращает текущую погоду в указанном городе."""
# В реальном приложении, вызов weather API
weather_data = {
"москва": "Облачно, +5°C, ветер 12 км/ч",
"санкт-петербург": "Дождь, +3°C, влажность 89%",
"новосибирск": "Снег, -8°C, ветер 20 км/ч",
}
return weather_data.get(city.lower(), f"Данные для '{city}' недоступны")
@tool
def calculate(expression: str) -> str:
"""Вычисляет математическое выражение. Принимает строку, например '2 + 2 * 10'."""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Ошибка вычисления: {e}"
# Смотрим, что видит модель
print(get_weather.name) # "get_weather"
print(get_weather.description) # "Возвращает текущую погоду в указанном городе."
print(get_weather.args_schema.model_json_schema())
# {'properties': {'city': {'title': 'City', 'type': 'string'}}, ...}
Важно: пишите Docstring, именно по нему модель решает, подходит ли инструмент для текущей задачи. Чем точнее он будет написан, тем модели легче понять что инструмент делает.
Инструмент с Pydantic схемой
Для инструментов с несколькими параметрами используйте Pydantic.
from langchain_core.tools import tool
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
query: str = Field(description="поисковый запрос")
max_results: int = Field(default=5, description="максимальное количество результатов, от 1 до 20")
@tool(args_schema=SearchInput)
def search_codebase(query: str, max_results: int = 5) -> list[dict]:
"""Ищет по кодовой базе проекта. Возвращает список файлов с описаниями."""
# В реальном приложении, поиск по индексу
return [
{"file": f"src/module_{i}.py", "snippet": f"...{query}...", "score": 0.9 - i * 0.1}
for i in range(min(max_results, 3))
]
Привязка инструментов к модели
.bind_tools() сообщает модели о доступных инструментах.
Когда модель решает использовать инструмент, она возвращает AIMessage с заполненным
tool_calls и пустым content. Это говорит о трм, что нужно выполнить функцию и вернуть результат.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
@tool
def get_weather(city: str) -> str:
"""Возвращает текущую погоду в указанном городе."""
data = {"москва": "Облачно, +5°C", "лондон": "Дождь, +8°C"}
return data.get(city.lower(), "Данные недоступны")
@tool
def calculate(expression: str) -> str:
"""Вычисляет математическое выражение."""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"Ошибка: {e}"
tools = [get_weather, calculate]
model_with_tools = model.bind_tools(tools)
# Задаем вопрос, ответ на который нужен инструмент
response = model_with_tools.invoke("Какая погода в Москве?")
print(response.tool_calls)
# [{'name': 'get_weather', 'args': {'city': 'москва'}, 'id': 'call_abc123'}]
print(response.content)
# '' (content пустой, модель решила вызвать инструмент, а не отвечать текстом)
Ручной цикл tool use
ToolMessage - это специальный тип сообщения, который привязывается к конкретному вызову с помощью tool_call_id.
Без него модель не поймет, к какому вызову относится результат.
Полный цикл выглядит так:
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
@tool
def get_weather(city: str) -> str:
"""Возвращает текущую погоду в указанном городе."""
data = {
"москва": "Облачно, +5°C, ветер 12 км/ч",
"лондон": "Дождь, +8°C",
"токио": "Солнечно, +18°C",
}
return data.get(city.lower(), f"Нет данных для '{city}'")
@tool
def calculate(expression: str) -> str:
"""Вычисляет математическое выражение. Пример: '150 * 1.2'"""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"Ошибка: {e}"
tools = [get_weather, calculate]
tools_map = {t.name: t for t in tools} # словарь для быстрого поиска по имени
model_with_tools = model.bind_tools(tools)
def run_with_tools(user_message: str) -> str:
messages = [HumanMessage(content=user_message)]
# Шаг 1: отправляем запрос модели
response = model_with_tools.invoke(messages)
messages.append(response)
# Шаг 2: пока модель хочет вызвать инструменты — выполняем их
while response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_id = tool_call["id"]
# Выполняем инструмент
tool_result = tools_map[tool_name].invoke(tool_args)
# Добавляем результат в историю как ToolMessage
messages.append(ToolMessage(
content=str(tool_result),
tool_call_id=tool_id,
))
# Шаг 3: снова вызываем модель с результатами инструментов
response = model_with_tools.invoke(messages)
messages.append(response)
return response.content
# Тест
print(run_with_tools("Какая погода в Москве и Лондоне?"))
# "В Москве облачно, +5°C, ветер 12 км/ч. В Лондоне дождь, +8°C."
print(run_with_tools("Сколько будет 1234 * 5678 + 99?"))
# "1234 × 5678 + 99 = 7 006 751."
print(run_with_tools("Привет, как дела?"))
# "Привет! Всё хорошо, чем могу помочь?" (инструменты не нужны)
Параллельные вызовы инструментов
Модель может вызвать несколько инструментов за один раз.
В примере выше цикл for tool_call in response.tool_calls уже это обрабатывает.
Но вызовы в нём последовательны.
Когда инструменты делают сетевые запросы (API, БД), параллельность сокращает время ответа.
Пример параллельного выполнения:
import concurrent.futures
def run_tools_parallel(tool_calls: list, tools_map: dict) -> list[ToolMessage]:
def execute_single(tool_call):
result = tools_map[tool_call["name"]].invoke(tool_call["args"])
return ToolMessage(
content=str(result),
tool_call_id=tool_call["id"],
)
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [executor.submit(execute_single, tc) for tc in tool_calls]
return [f.result() for f in concurrent.futures.as_completed(futures)]
Управление поведением с помощью tool_choice
По умолчанию модель сама решает, использовать инструмент или нет.
Это поведение можно изменить:
tool_choice="auto"- модель решает сама какой инструмент вызыватьtool_choice="ВАШ_ИНСТРУМЕНТ"- модель ОБЯЗАНА использовать конкретный инструментtool_choice="none"- модель НЕ должна использовать инструментыtool_choice="required"- модель ОБЯЗАНА использовать хотя бы один инструмент
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
@tool
def get_weather(city: str) -> str:
"""Возвращает текущую погоду в указанном городе."""
data = {"москва": "Облачно, +5°C", "лондон": "Дождь, +8°C"}
return data.get(city.lower(), "Данные недоступны")
@tool
def calculate(expression: str) -> str:
"""Вычисляет математическое выражение."""
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"Ошибка: {e}"
tools = [get_weather, calculate]
# Модель ОБЯЗАНА использовать хотя бы один инструмент
model_forced = model.bind_tools(tools, tool_choice="required")
# Модель ОБЯЗАНА использовать конкретный инструмент
model_specific = model.bind_tools(tools, tool_choice="get_weather")
# Модель НЕ должна использовать инструменты (только текстовый ответ)
model_no_tools = model.bind_tools(tools, tool_choice="none")
# По умолчанию: модель решает сама
model_auto = model.bind_tools(tools, tool_choice="auto")
Инструменты для извлечения данных
Интересный паттерн использовать инструменты не для действий,
а для извлечения структурированных данных из текста.
Это альтернатива .with_structured_output() с большим контролем.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from pydantic import BaseModel, Field
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
class ContactInfo(BaseModel):
name: str = Field(description="имя человека")
email: str = Field(description="email адрес")
phone: str | None = Field(default=None, description="номер телефона, если указан")
company: str | None = Field(default=None, description="компания, если указана")
@tool(args_schema=ContactInfo)
def save_contact(name: str, email: str, phone: str | None = None, company: str | None = None) -> str:
"""Сохраняет контактную информацию из текста."""
return f"Контакт сохранен: {name} <{email}>"
model_extractor = model.bind_tools([save_contact], tool_choice="save_contact")
response = model_extractor.invoke(
"Привет, я Иван Петров из компании Roga & Copyta, "
"мой email ivan@roga.ru, телефон +7-999-123-45-67"
)
tool_call = response.tool_calls[0]
print(tool_call["args"])
# {'name': 'Иван Петров', 'email': 'ivan@roga.ru',
# 'phone': '+7-999-123-45-67', 'company': 'Roga & Copyta'}
Распространенные ошибки
1. Нет docstring у инструмента
@tool
def get_price(item: str) -> float:
return 99.9 # нет docstring — модель не понимает, что делает функция
Всегда пишите docstring. Это не просто документация, а инструкция для модели.
2. Забытый ToolMessage
# Неправильно: возвращаем результат, но не добавляем ToolMessage
response = model_with_tools.invoke(messages)
tool_result = some_tool.invoke(response.tool_calls[0]["args"])
# Если не добавить ToolMessage, то следующий вызов модели упадет с ошибкой
После каждого вызова инструмента обязательно добавляйте ToolMessage с tool_call_id.
3. Бесконечный цикл
while response.tool_calls: # цикл без ограничения
...
Добавьте максимальное количество итераций:
max_iterations = 10
iteration = 0
while response.tool_calls and iteration < max_iterations:
iteration += 1
...
4. eval без ограничений
@tool
def calculate(expression: str) -> str:
"""Вычисляет выражение."""
return str(eval(expression)) # опасно! eval без ограничений
Всегда ограничивай доступные имена: eval(expression, {"__builtins__": {}}, {}).
MCP: стандарт для подключения инструментов
В 2025–2026 годах активно развивается стандарт MCP (Model Context Protocol)
- открытый протокол для подключения инструментов к языковым моделям.
Идея похожа на то, что мы делали с помощью @tool,
но более стандартизировано.
Любой MCP-сервер (GitHub, база данных, файловая система) подключается к любому MCP-клиенту.
LangChain поддерживает MCP с помощью адаптера langchain-mcp-adapters.
# pip install langchain-mcp-adapters
from langchain_mcp_adapters.client import MultiServerMCPClient
async with MultiServerMCPClient({
"github": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"]},
}) as client:
tools = client.get_tools()
model_with_mcp = model.bind_tools(tools)
MCP не заменяет @tool для кастомной логики, но открывает экосистему готовых серверов.
Вместо того чтобы самому писать обёртку над GitHub API,
можно подключить готовый MCP-сервер и сразу получить десятки инструментов.
Практическое задание
Создайте ассистента для интернет-магазина.
Требования:
1) Реализуйте следующие инструменты:
search_products(query: str, category: str | None) -> list- поиск товаров (возвращайте захардкоженные данные)get_product_details(product_id: int) -> dict- детали товара (название, цена, наличие)check_delivery_date(city: str, product_id: int) -> str- срок доставкиcalculate_total(product_ids: list[int], promo_code: str | None) -> dict- итоговая сумма со скидкой
2) Каждый инструмент должен иметь подробный docstring
3) Реализуйте цикл обработки с лимитом в 10 итераций
4) Добавьте вывод всех вызовов инструментов в консоль
Примеры запросов для теста:
"Найди мне ноутбуки до 80000 рублей"
"Когда доставят товар #3 в Новосибирск?"
"Добавь товары #1 и #5 в корзину и примени промокод SALE10"
Итоги урока
Мы научились вручную обрабатывать вызовы инструментов.
Но с ростом числа инструментов и сложности задач ручной цикл становится громоздким.
В следующем уроке разберём Agents, как create_agent из langchain.agents автоматизирует этот цикл, возвращая CompiledStateGraph на основе LangGraph, и когда стоит использовать агентов вместо явных цепочек.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru