Создание инструментов через декоратор @tool, конфигурация агента с create_agent, структурированный вывод, контекст выполнения.
Инструменты позволяют языковой модели взаимодействовать с внешним миром. Без них модель может только генерировать текст.
Инструмент — это не «функция, которую вызывает модель». Модель не выполняет код: она получает описание инструмента (имя, текст описания, JSON-схему аргументов) и возвращает намерение — структуру tool_call с именем и аргументами. Выполняет функцию фреймворк, а результат возвращается модели как ToolMessage.
Из этого следует главное практическое правило: имя, описание и имена параметров — часть промпта. Модель выбирает инструмент, читая их, и больше ей опереться не на что. Плохой docstring это не «некрасиво», а прямая причина неправильных вызовов.
Самый простой способ создать инструмент — декоратор @tool из langchain.tools. Декоратор берёт имя функции как имя инструмента, docstring — как описание, а аннотации типов — как схему аргументов:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Получить прогноз погоды для указанного города."""
return f"В городе {city} сегодня солнечно, +25°C"Декоратор не обязателен: любую Python-функцию с аннотациями и docstring можно передать в create_agent напрямую — LangChain обернёт её сам. Декоратор нужен, когда требуется переопределить имя, описание или схему аргументов:
def search_database(query: str, limit: int = 10) -> str:
"""Поиск в базе данных по запросу.
Args:
query: поисковые термины
limit: максимальное количество результатов
"""
return f"Найдено {limit} результатов для: {query}"
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[search_database],
)Секция Args в docstring попадает в описания отдельных параметров схемы. Для параметров с неочевидным форматом (даты, идентификаторы, единицы измерения) это решает большинство проблем: вместо дата: str модель видит «дата в формате ГГГГ-ММ-ДД».
Один инструмент, одно действие. Универсальный manage_data(action, payload) заставляет модель угадывать допустимые комбинации; три отдельных инструмента с говорящими именами она вызывает точнее.
Всегда указывайте типы. Без аннотации параметр попадает в схему как строка произвольного вида, и модель начинает передавать "десять" вместо 10.
Используйте Literal для перечислений: status: Literal["new", "paid", "shipped"] превращается в enum схемы, и модель физически не сможет придумать четвёртый статус.
Возвращайте то, что полезно модели, а не то, что удобно программе. Дамп на 50 КБ съест контекст и ухудшит следующие решения; лучше вернуть краткую сводку и идентификаторы для уточняющих запросов.
Инструмент может вернуть строку, объект или мультимодальное содержимое. Строка отправляется модели как есть. Словарь сериализуется, поэтому структурированный ответ читается моделью лучше плоского текста, если полей много:
@tool
def get_weather_data(city: str) -> dict:
"""Получить структурированные данные о погоде в городе."""
return {"city": city, "temperature_c": 22, "conditions": "sunny"}Мультимодальный результат возвращается списком блоков — так инструмент может отдать модели изображение вместе с подписью:
@tool
def capture_screenshot() -> list[dict]:
"""Сделать снимок текущей страницы."""
return [
{"type": "text", "text": "Снимок текущей страницы:"},
{"type": "image", "url": "https://example.com/page.png"},
]Инструменту часто нужны данные, которых нет в аргументах: идентификатор пользователя, состояние диалога, долговременная память. Для этого объявляется параметр типа ToolRuntime — он внедряется фреймворком автоматически и не показывается модели, поэтому она не может его подделать:
from langchain.tools import tool, ToolRuntime
@tool
def get_user_preference(pref_name: str, runtime: ToolRuntime) -> str:
"""Получить значение пользовательской настройки."""
preferences = runtime.state.get("user_preferences", {})
return preferences.get(pref_name, "не задано")Через runtime доступны: state для текущего состояния диалога, context для неизменяемых данных запуска (идентификатор пользователя, роль, ключи), store для долговременного хранилища между потоками, stream_writer для отправки промежуточных событий, tool_call_id с идентификатором текущего вызова, execution_info с идентификаторами потока и запуска.
Это ключевой приём безопасности: идентификатор пользователя нельзя принимать аргументом инструмента, иначе модель сможет подставить чужой. Он должен приходить из runtime.context, куда его положил ваш код.
Обычно инструмент возвращает данные. Но если он должен изменить состояние агента, возвращают Command. Вместе с обновлением состояния нужно вернуть ToolMessage с tool_call_id — иначе история сообщений останется незакрытой, и провайдер отклонит следующий вызов:
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
@tool
def set_user_name(new_name: str, runtime: ToolRuntime) -> Command:
"""Записать имя пользователя в состояние диалога."""
return Command(
update={
"user_name": new_name,
"messages": [
ToolMessage(
content=f"Имя пользователя установлено: {new_name}.",
tool_call_id=runtime.tool_call_id,
)
],
}
)create_agentАгент создаётся фабрикой create_agent. Минимум — модель и инструменты; системный промпт задаёт роль и правила:
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def calculate(expression: str) -> str:
"""Вычислить арифметическое выражение, например '15 * 37'."""
return str(eval(expression))
@tool
def get_current_time() -> str:
"""Получить текущее время сервера."""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[calculate, get_current_time],
system_prompt="Ты помощник. Используй инструменты для выполнения задач.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Сколько будет 15 * 37?"}]}
)
print(result["messages"][-1].text)Агент разбирает запрос, выбирает инструмент, вызывает его с аргументами и формирует ответ по результату. В result["messages"] останется весь след: запрос, вызов инструмента, ToolMessage и финальный ответ — по нему удобно отлаживать поведение.
eval в примере годится только для учебного кода. В реальном инструменте вычисление выражения делают через безопасный парсер: модель может передать в аргумент произвольную строку, а значит, это недоверенный ввод.
Когда ответ нужен программе, а не человеку, задают response_format с Pydantic-схемой. Агент вернёт валидированный объект в result["structured_response"]:
from pydantic import BaseModel, Field
class Answer(BaseModel):
city: str = Field(description="Город, о котором спрашивали")
temperature_c: int = Field(description="Температура в градусах Цельсия")
summary: str = Field(description="Краткое описание погоды одной фразой")
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather],
response_format=Answer,
)
result = agent.invoke({"messages": [{"role": "user", "content": "Погода в Казани?"}]})
answer: Answer = result["structured_response"]
print(answer.temperature_c)Описания в Field работают так же, как docstring инструмента: это подсказка модели, чем заполнять поле. Без них модель угадывает по имени поля.
Постоянные для запуска данные (кто пользователь, какая у него роль, на каком языке отвечать) передают не в сообщениях, а через контекст. Схема объявляется отдельно, значения передаются при вызове:
from dataclasses import dataclass
@dataclass
class Context:
user_id: str
locale: str
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_user_preference],
context_schema=Context,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Какая у меня тема оформления?"}]},
context=Context(user_id="u-42", locale="ru"),
)Разница между контекстом и состоянием: контекст неизменен в пределах запуска и задаётся снаружи, состояние меняется узлами по ходу выполнения.
Сам по себе агент не помнит прошлые запросы: каждый invoke начинается с переданных сообщений. Память включается чекпоинтером — тогда история привязывается к thread_id:
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "user-42-chat-1"}}
agent.invoke({"messages": [{"role": "user", "content": "Меня зовут Алексей"}]}, config=config)
result = agent.invoke({"messages": [{"role": "user", "content": "Как меня зовут?"}]}, config=config)Во втором вызове передаётся только новое сообщение — предыдущие подгружаются из контрольной точки. thread_id — это идентификатор диалога: один пользователь может иметь несколько потоков, и они не пересекаются.
Параметр name задаёт идентификатор агента. В одиночном агенте он почти не важен, но в мультиагентной системе именно по нему субагента отличают в трассировке и адресуют при передаче управления — задавайте его сразу, чтобы потом не переписывать.
Инструмент без docstring: модель видит только имя и вызывает его наугад либо игнорирует.
Идентификатор пользователя как аргумент инструмента: модель может подставить чужой. Берите его из runtime.context.
Возврат исключения наружу: непойманное исключение в инструменте роняет весь запуск. Ошибку лучше вернуть текстом, чтобы модель могла исправиться, или подключить мидлварь обработки ошибок.
Слишком много инструментов: при нескольких десятках модель начинает путаться. Тогда нужен отбор инструментов (LLMToolSelectorMiddleware) или разделение на субагентов.
Далее: Мидлварь: как менять поведение агента