LangChain

LangChain Tools: инструменты и вызов функций | Курс по LangChain урок 6

LangChain Tools: инструменты и вызов функций | Курс по LangChain урок 6
Mikhail
Автор
Mikhail
Опубликовано 23.02.2026
0,0
Views 3

Цель урока

Вы научитесь создавать инструменты с помощью @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, и когда стоит использовать агентов вместо явных цепочек.

<< Урок 5

Урок 7 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы