Расширенные возможности инструмента LangChain
В предыдущем разделе мы изучили базовое использование @tool. В этом разделе представлены расширенные возможности инструментов: return_direct, InjectedToolCallId, ToolException и обработка ошибок.
return_direct — прямое возвращение конечного результата
По умолчанию после выполнения инструмента результат возвращается модели, которая затем формирует окончательный ответ на основе результата работы инструмента. Но иногда результат инструмента сам по себе является нужным вам окончательным ответом.
Если установитьreturn_direct=True, то после этого цикл Agent завершается сразу после выполнения инструмента, а возвращённое инструментом содержимое напрямую используется как окончательный вывод.
Пример
load_dotenv()
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
# Обычный инструмент: результат возвращается модели, затем модель обобщает
@tool
def search_normal(keyword: str) -> str:
"""Поиск курсов EXAMPLE Tutorial (обычный режим)"""
return f"Результаты поиска: Учебник по основам Python3, Анализ данных на Python, Введение в Python Crawler"
# инструмент return_direct: результат напрямую используется как окончательный вывод
@tool(return_direct=True)
def search_direct(keyword: str) -> str:
"""Поиск курсов EXAMPLE Tutorial (режим прямого возврата).
Используйте этот инструмент, когда пользователю нужны только результаты поиска и не требуется ничего дополнительного.
"""
return f"Результаты поиска: Учебник по основам Python3, Анализ данных на Python, Введение в Python Crawler"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
# Сравнение: обычный режим против режима прямого возврата
agent_normal = create_agent(
model=model,
tools=[search_normal],
system_prompt="Вы консультант по обучению в EXAMPLE Tutorial.",
)
agent_direct = create_agent(
model=model,
tools=[search_direct],
system_prompt="Вы консультант по обучению в EXAMPLE Tutorial.",
)
# Обычный режим: модель формирует сводку на основе результатов поиска
result = agent_normal.invoke({
"messages": [HumanMessage(content="Поиск курсов Python")]
})
print("=== Обычный режим (модель обрабатывает дальше) ===")
print(result["messages"][-1].content[:150])
# Режим прямого возврата: результат инструмента — окончательный ответ
result = agent_direct.invoke({
"messages": [HumanMessage(content="Поиск курсов Python")]
})
print("\n=== Режим прямого возврата (результат инструмента — окончательный ответ) ===")
print(result["messages"][-1].content[:150])
Результат выполнения:
=== 普通模式(模型会再加工)=== 在Example 中,我为您找到了以下 Python 相关课程: 1. Python3 基础教程 - 适合零基础入门 2. Python 数据分析 - 进阶学习 3. Python 爬虫入门 - 实战项目 === 直接返回模式(工具结果即最终答案)=== 搜索结果:Python3 基础教程、Python 数据分析、Python 爬虫入门
| Mode | После выполнения инструмента | Применимые сценарии |
|---|---|---|
| return_direct=False (по умолчанию) | Модель получает результат инструмента → модель продолжает размышлять → генерирует окончательный ответ | Требуется анализ/обобщение/дальнейшее принятие решений |
| return_direct=True | Завершается сразу после выполнения инструмента → результат инструмента является окончательным выводом | Тип запроса, тип извлечения данных, предварительно отформатированные результаты |
Когда вы устанавливаете return_direct=True, Agent пропускает последующие этапы размышления модели и напрямую возвращает результат инструмента. Это очень ценно для экономии токенов и снижения задержки, но также означает, что модель не будет выполнять никакой вторичной обработки результата инструмента.
Примечание:Если у Agent одновременно подключено несколько инструментов, включая как инструменты с return_direct=True, так и обычные инструменты, то, как только модель вызовет любой инструмент с return_direct в этом раунде вызовов, цикл Agent немедленно завершится — даже если другие обычные инструменты вызываются параллельно в том же раунде, их результаты больше не будут обработаны или обобщены моделью. Учитывайте это при проектировании Agent с несколькими инструментами, чтобы избежать "пропуска контента, который должен быть обобщён".
InjectedToolCallId — Получение ID вызова инструмента
Иногда инструменту нужно знать, "кто его вызвал"—InjectedToolCallIdВы можете внедрить текущий tool_call_id в функцию инструмента:
Пример
from langchain.messages import ToolCall
from langchain.tools import tool, InjectedToolCallId
@tool
def log_user_action(
action: str,
tool_call_id: Annotated[str, InjectedToolCallId],
) -> str:
"""Записывает действия пользователя в систему логирования.
Аргументы:
action: описание действия пользователя
tool_call_id: автоматически внедряемый ID вызова инструмента
"""
# В реальном проекте это было бы записано в базу данных или отправлено в сервис логирования
return f"Операция записана (ID вызова: {tool_call_id}): {action}"
# Вручную смоделировать ToolCall, сгенерированный моделью
tool_call: ToolCall = {
"name": "log_user_action",
"args": {
"action": "Пользователь запросил курсы Python"
},
"id": "call_log_001",
"type": "tool_call",
}
# InjectedToolCallId будет автоматически внедрён из tool_call.id
result = log_user_action.invoke(tool_call)
print(result)
Результат выполнения:
content='操作已记录 (调用ID: call_log_001): 用户查询了 Python 课程' name='log_user_action' tool_call_id='call_log_001'
Параметры, помеченныеInjectedToolArg, не должны предоставляться Агентом (моделью); эти параметры автоматически внедряются средой выполнения LangChain при выполнении инструмента. Поскольку эти параметры не отображаются в схеме инструмента, модель не может их видеть, поэтому их не следует описывать как параметры инструмента, которые должен заполнять пользователь.
Например,InjectedToolCallId— это особый вид внедряемого параметра. LangChain автоматически передаёт уникальный ID текущего ToolCall во время выполнения инструмента. Он не появляется в параметрах, сгенерированных моделью, но автоматически привязывается к текущему вызову инструмента.
Прямой вызов.invoke()и ручное созданиеToolCall, нужны только для демонстрации того, как LangChain реализует механизм внедрения параметров. В реальных проектах вы обычно не передаёте этот ID вручную.
В рабочих процессах Агента,InjectedToolCallIdчаще используется в сценариях, где нужно связать контекст текущего вызова, например, вместе с объектомCommandдля обновления состояния Агента внутри инструмента,messagesдобавлять в список элементы, связанные с текущим вызовом,ToolMessage, или реализовывать такие функции, как трассировка вызовов инструментов, аудит и корреляция логов.
Вот почему он спроектирован как «внедряемый параметр», а не как обычный параметр: он представляет контекст ToolCall, выполняемого в данный момент средой выполнения LangChain, а не часть пользовательского ввода.
ToolException — Обработка исключений инструментов
Во время выполнения инструмента могут возникать ошибки. ИспользуйтеToolException, чтобы выбросить понятное исключение инструмента, сообщив Агенту, что что-то пошло не так.
Пример
@tool
def get_user_info(user_id: int) -> str:
"""Запрос информации о пользователе по ID пользователя.
Аргументы:
user_id: ID пользователя, должен быть положительным целым числом
"""
# Проверка данных
if user_id <= 0:
# Выбросить ToolException, а не обычное Exception
# ToolException будет перехвачен Агентом и передан модели
raise ToolException(f"ID пользователя должен быть положительным целым числом, получено: {user_id}")
# Имитация запроса к базе данных
users = {
1: "Чжан Сань (VIP-участник, зарегистрирован 2024-01-15)",
2: "Ли Сы (обычный пользователь, зарегистрирован 2024-03-20)",
}
if user_id not in users:
raise ToolException(f"Пользователь с ID {user_id} не найден")
return users[user_id]
# Обычный вызов
print(get_user_info.invoke({"user_id": 1}))
# Вызов с исключением 1: неверный ID
try:
get_user_info.invoke({"user_id": -1})
except ToolException as e:
print(f"Исключение инструмента: {e}")
# Вызов с исключением 2: пользователь не существует
try:
get_user_info.invoke({"user_id": 999})
except ToolException as e:
print(f"Исключение инструмента: {e}")
Результат выполнения:
张三(VIP 会员,注册于 2024-01-15) 工具异常: 用户 ID 必须为正整数,收到了: -1 工具异常: 未找到 ID 为 999 的用户
Причина, по которой ToolException можно перехватить с помощью try/except вне .invoke(), здесь в том, что значение по умолчанию атрибута инструментаhandle_tool_error— False: исключение не проглатывается самим инструментом, а как обычно выбрасывается наружу. Если вы хотите, чтобы инструмент «сам обработал ошибку» и преобразовал сообщение об ошибке в строку для возврата модели вместо выбрасывания исключения, то это handle_tool_error, о котором пойдёт речь в следующем разделе.
handle_tool_error — позволить инструменту обрабатывать ошибки самостоятельно
Когда вы хотите, чтобы инструмент не прерывал программу при ошибке, а вместо этого преобразовывал сообщение об ошибке в текст и передавал его модели как обычное возвращаемое значение, чтобы модель могла понять и исправить, это можно задать при определении инструмента.handle_tool_error(обратите внимание: в единственном числе, без s). Это атрибут BaseTool, и проще всего задать его, написав напрямую в декораторе @tool:
Пример
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
# handle_tool_error=True: больше не выбрасывает исключение при ошибке,
# вместо этого преобразует содержимое ToolException в строку и обычным образом возвращает её модели
@tool(handle_tool_error=True)
def get_weather(city: str) -> str:
"""Запросить погоду для указанного города.
Аргументы:
city: название города, должно быть полным китайским названием, например, "Hangzhou", "Beijing"
"""
weather_data = {
"Ханчжоу": "Солнечно, 25°C",
"Пекин": "Облачно, 18°C",
"Shanghai": Небольшой дождь, 22°C,
}
if city not in weather_data:
# Возбудить ToolException, когда города нет в данных
raise ToolException(
f"Город '{city}' отсутствует в базе данных."
f"Доступные города: {', '.join(weather_data.keys())}."
f"Пожалуйста, используйте полное китайское название города."
)
return f"Погода в {city}: {weather_data[city]}"
# Тест: вызов с неправильным названием города
# Когда handle_tool_error=True, сообщение об ошибке возвращается как обычный результат, а не вызывает исключение
result = get_weather.invoke({"city": "Северная территория"})
print(f"handle_tool_error=True: {result}")
# Если вы хотите, чтобы все ошибки инструментов единообразно обрабатывались агентом (вместо настройки для каждого инструмента),
# это можно настроить на уровне ToolNode, используемого внутри create_agent.
# В более новых версиях langchain можно включить резервную обработку ошибок для всего агента следующим образом:
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather],
system_prompt="Вы — ассистент по запросам погоды.",
)
result = agent.invoke({
"messages": [HumanMessage⟦⟧content(content=查询 "北京" 的天气)]
})
print("\n=== Agent 接收到由 handle_tool_error 转换的错误消息,并相应地向用户解释 ===)
print(result["messages"][-1].content[:200])
Результат выполнения:
handle_tool_error=True: 未收录城市 '北境'。可使用城市:杭州, 北京, 上海。请使用中文城市全称。 === Agent 收到 handle_tool_error 转换后的错误信息,并据此向用户解释 === 抱歉,我没有查询到"北境"的天气数据。目前支持查询的城市有:杭州、北京、上海。 请问您是想查询其中哪一个城市呢?
| handle_tool_error | Поведение | Применимые сценарии |
|---|---|---|
| False ( | ToolException 照常向上抛出;调用者必须使用 try/except 处理它 | 需要中断流程的不可恢复错误 |
| True | 捕获 ToolException 并将其作为工具的常规返回值交给模型 | 当你希望 Agent 看到错误消息本身并进行纠正/重试时 |
| str | 捕获异常后,将错误消息替换为指定的固定字符串 | 不希望暴露具体的错误细节;仅提供统一的提示 |
| Callable[[ToolException], str] | 捕获异常后,使用自定义函数处理它并生成返回内容 | Нужно настраивать разные промпты в зависимости от типа/содержимого исключения |
Примечание:Не используйтеtool.with_config(handle_tool_errors=True)этот синтаксис —with_config()Это универсальный метод конфигурации времени выполнения для Runnable, используемый только для настройки callbacks, tags, metadata и т. п., и он фактически не изменяет поведение обработки ошибок инструмента. Чтобы управлять обработкой ошибок отдельного инструмента, используйте@tool(handle_tool_error=...)(в единственном числе); если вы хотите единообразно настроить стратегии обработки ошибок для всех инструментов на уровне Agent/Graph, вы задаёте handle_tool_errors (во множественном числе) у нижележащего ToolNode, а не у отдельных объектов инструментов. Эти два имени поля похожи, но работают на разных уровнях; обращайте внимание на это различие при их использовании.
Полный пример — агент с обработкой ошибок
Пример
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
@tool(handle_tool_error=True)
def book_course(user_name: str, course_name: str) -> str:
"""Забронировать пользователю курс на EXAMPLE.
Аргументы:
user_name: имя пользователя
course_name: название курса
"""
# Проверить, существует ли пользователь
valid_users = {"Zhang San", "Li Si", "Wang Wu"}
if user_name not in valid_users:
raise ToolException(
f"Пользователь '{user_name}' не существует."
f"Допустимые пользователи: {', '.join(sorted(valid_users))}"
)
# Проверить, существует ли курс
valid_courses = {"Python3 Basic Tutorial", "HTML Basic Tutorial", "Java Object-Oriented"}
if course_name not in valid_courses:
raise ToolException(
f"Курс '{course_name}' не существует."
f"Допустимые курсы: {', '.join(sorted(valid_courses))}"
)
return f"Успешно забронирован {course_name} для {user_name}"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[book_course],
system_prompt="Вы консультант по курсам в EXAMPLE.",
)
# Обычный вызов
result = agent.invoke({
"messages": [HumanMessage(content="Забронировать Python3 Basic Tutorial для Zhang San")]
})
print(f"Успех: {result['messages'][-1].content[:100]}")
# Вызов с ошибкой: пользователь не существует
result = agent.invoke({
"messages": [HumanMessage(content="Книга: Базовое руководство по Python3 для Чжао Лю")]
})
print(f"\n"Ошибка - пользователь не существует:")
for msg in result["messages"]:
if msg.type == "tool":
print(f" [{msg.type}] {msg.content[:80]}")
elif msg.type == "ai" and msg.content:
print(f" [{msg.type}] {msg.content[:100]}")
Результат выполнения:
Success: 已经为您成功预订《Python3 基础教程》,张三同学,祝您学习愉快! 错误-用户不存在: [tool] 用户 '赵六' 不存在。有效用户:张三, 李四, 王五 [ai] 抱歉,系统中没有找到名为"赵六"的用户,暂时无法为您完成预订。 目前可预订的用户有:张三、李四、王五。请确认姓名后重新尝试。
Другие расширенияМожно заметить, что после того, как инструмент настроен с помощьюhandle_tool_error=Trueсодержимое ToolException будет нормально отображаться в истории диалога как ToolMessage (строка [tool] в выводе выше), не приводя к сбою программы; увидев это сообщение об ошибке, модель передаст его пользователю на естественном языке и предложит доступные альтернативы. Это типичный эффект комбинации return_direct, InjectedToolCallId, ToolException и handle_tool_error: он обеспечивает надёжность вызовов инструментов, не жертвуя удобством пользователя.