LangChain Tool Advanced Features
In the previous section, we learned the basic usage of @tool. This section introduces advanced features of tools: return_direct, InjectedToolCallId, ToolException, and error handling.
return_direct — Directly Return the Final Result
By default, after a tool executes, the result is returned to the model, which then generates the final reply based on the tool result. But sometimes the tool result itself is the final answer you want.
Settingreturn_direct=Trueafter this, the Agent loop ends immediately once the tool finishes executing, and the tool's returned content is directly used as the final output.
Example
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
# Ordinary tool: result returned to the model, model then summarizes
@tool
def search_normal(keyword: str) -> str:
"""Search EXAMPLE Tutorial courses (normal mode)"""
return f"Search results: Python3 Basics Tutorial, Python Data Analysis, Python Crawler Introduction"
# return_direct tool: result directly used as final output
@tool(return_direct=True)
def search_direct(keyword: str) -> str:
"""Search EXAMPLE Tutorial courses (direct return mode).
Use this tool when the user only needs search results and no additional analysis is needed.
"""
return f"Search results: Python3 Basics Tutorial, Python Data Analysis, Python Crawler Introduction"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
# Comparison: normal mode vs direct return mode
agent_normal = create_agent(
model=model,
tools=[search_normal],
system_prompt="You are the study advisor for EXAMPLE Tutorial.",
)
agent_direct = create_agent(
model=model,
tools=[search_direct],
system_prompt="You are the study advisor for EXAMPLE Tutorial.",
)
# Normal mode: the model generates a summary based on the search results
result = agent_normal.invoke({
"messages": [HumanMessage(content="Search Python courses")]
})
print("=== Normal mode (model processes further) ===")
print(result["messages"][-1].content[:150])
# Direct return mode: the tool result is the final answer
result = agent_direct.invoke({
"messages": [HumanMessage(content="Search Python courses")]
})
print("\n=== Direct return mode (tool result is the final answer) ===")
print(result["messages"][-1].content[:150])
Running result:
=== 普通模式(模型会再加工)=== 在Example 中,我为您找到了以下 Python 相关课程: 1. Python3 基础教程 - 适合零基础入门 2. Python 数据分析 - 进阶学习 3. Python 爬虫入门 - 实战项目 === 直接返回模式(工具结果即最终答案)=== 搜索结果:Python3 基础教程、Python 数据分析、Python 爬虫入门
| Mode | After tool execution | Applicable scenarios |
|---|---|---|
| return_direct=False (default) | Model receives tool result → model continues thinking → generates final reply | Needs analysis/summary/further decision-making |
| return_direct=True | Ends immediately after tool execution → tool result is the final output | Query type, data retrieval type, pre-formatted results |
When you set return_direct=True, the Agent skips subsequent model thinking steps and directly returns the tool result. This is very valuable for saving tokens and reducing latency, but it also means the model will not do any secondary processing on the tool result.
Note:If an Agent has multiple tools mounted at the same time, including both return_direct=True tools and ordinary tools, then as long as the model triggers any return_direct tool in this round of calls, the Agent loop will end immediately—even if other ordinary tools are called in parallel in the same round, their results will no longer be processed or summarized by the model. Keep this in mind when designing an Agent with multiple tools to avoid "content that should be summarized being skipped."
InjectedToolCallId — Getting the Tool Call ID
Sometimes a tool needs to know "who called it"—InjectedToolCallIdYou can inject the current tool_call_id into the tool function:
Example
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:
"""Record user actions to the logging system.
Args:
action: description of the user action
tool_call_id: automatically injected tool call ID
"""
# In a real project, this would be written to a database or sent to a logging service
return f"Operation recorded (call ID: {tool_call_id}): {action}"
# Manually simulate a ToolCall generated by the model
tool_call: ToolCall = {
"name": "log_user_action",
"args": {
"action": "The user queried Python courses"
},
"id": "call_log_001",
"type": "tool_call",
}
# InjectedToolCallId will be automatically injected from tool_call.id
result = log_user_action.invoke(tool_call)
print(result)
Running result:
content='操作已记录 (调用ID: call_log_001): 用户查询了 Python 课程' name='log_user_action' tool_call_id='call_log_001'
WithInjectedToolArgmarked parameters do not need to be provided by the Agent (model); these parameters are automatically injected by the LangChain runtime when executing the tool. Since these parameters do not appear in the tool's schema, the model cannot see them, so they should not be described as tool parameters that the user needs to fill in.
For example,InjectedToolCallIdis a special kind of injected parameter. LangChain automatically passes in the unique ID of the current ToolCall during tool execution. It does not appear in the parameters generated by the model, but it is automatically bound to the current tool call.
Directly calling.invoke()and manually constructingToolCall, is only to demonstrate how LangChain implements the parameter injection mechanism. In real projects, you usually do not pass this ID manually.
In Agent workflows,InjectedToolCallIdit is more commonly used in scenarios where the current call context needs to be associated, for example, together withCommandobject to update Agent state inside the tool,messagesappend to the list the items associated with the current call,ToolMessage, or implement features such as tool call tracing, auditing, and log correlation.
This is also why it is designed as an "injected parameter" rather than an ordinary parameter: it represents the context of the ToolCall currently being executed by the LangChain Runtime, not part of the user input.
ToolException — Tool Exception Handling
Errors may occur during tool execution. UseToolExceptionto throw a clear tool exception, letting the Agent know something went wrong.
Example
@tool
def get_user_info(user_id: int) -> str:
"""Query user information by user ID.
Args:
user_id: user ID, must be a positive integer
"""
# Data validation
if user_id <= 0:
# Raise ToolException, not a regular Exception
# ToolException will be caught by the Agent and reported to the model
raise ToolException(f"User ID must be a positive integer, received: {user_id}")
# Simulate database query
users = {
1: "Zhang San (VIP member, registered on 2024-01-15)",
2: "Li Si (regular user, registered on 2024-03-20)",
}
if user_id not in users:
raise ToolException(f"User with ID {user_id} not found")
return users[user_id]
# Normal call
print(get_user_info.invoke({"user_id": 1}))
# Exception call 1: invalid ID
try:
get_user_info.invoke({"user_id": -1})
except ToolException as e:
print(f"Tool exception: {e}")
# Exception call 2: user does not exist
try:
get_user_info.invoke({"user_id": 999})
except ToolException as e:
print(f"Tool exception: {e}")
Running result:
张三(VIP 会员,注册于 2024-01-15) 工具异常: 用户 ID 必须为正整数,收到了: -1 工具异常: 未找到 ID 为 999 的用户
The reason ToolException can be caught with try/except outside .invoke() here is that the tool's defaulthandle_tool_erroris False—the exception is not swallowed by the tool itself, but is thrown upward as usual. If you want the tool to "handle the error itself" and convert the error message into a string to return to the model instead of throwing an exception, that is handle_tool_error, which will be covered in the next section.
handle_tool_error — Let the Tool Handle Errors Itself
When you want a tool to not interrupt the program on error, but instead convert the error message into a piece of text and hand it to the model as a normal return value for the model to understand and correct, you can set it when defining the tool.handle_tool_error(note it is singular, without an s). This is an attribute on BaseTool, and the simplest way to set it is to write it directly in the @tool decorator:
Example
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
# handle_tool_error=True: no longer throws an exception on error,
# instead converts the content of ToolException into a string and returns it normally to the model
@tool(handle_tool_error=True)
def get_weather(city: str) -> str:
"""Query the weather for a specified city.
Args:
city: city name, must be the full Chinese name, e.g., "Hangzhou", "Beijing"
"""
weather_data = {
"Hangzhou": "Sunny, 25°C",
"Beijing": "Cloudy, 18°C",
"Shanghai": Light rain, 22°C,
}
if city not in weather_data:
# Raise ToolException when the city is not in the data
raise ToolException(
f"City '{city}' is not in the database."
f"Available cities: {', '.join(weather_data.keys())}."
f"Please use the full Chinese city name."
)
return f"{city} weather: {weather_data[city]}"
# Test: call with an incorrect city name
# When handle_tool_error=True, the error message is returned as a normal result instead of raising an exception
result = get_weather.invoke({"city": "Northern Territory"})
print(f"handle_tool_error=True: {result}")
# If you want all tools' errors to be handled uniformly by the Agent (instead of setting them per tool),
# you can configure this at the ToolNode level used inside create_agent.
# In newer versions of langchain, you can enable error fallback for the entire Agent like this:
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather],
system_prompt="You are a weather query assistant.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Query the weather in Northern Territory")]
})
print("\n=== Agent receives the error message converted by handle_tool_error and explains it to the user accordingly ===)
print(result["messages"][-1].content[:200])
Run result:
handle_tool_error=True: 未收录城市 '北境'。可使用城市:杭州, 北京, 上海。请使用中文城市全称。 === Agent 收到 handle_tool_error 转换后的错误信息,并据此向用户解释 === 抱歉,我没有查询到"北境"的天气数据。目前支持查询的城市有:杭州、北京、上海。 请问您是想查询其中哪一个城市呢?
| handle_tool_error | Behavior | Applicable scenarios |
|---|---|---|
| False (default) | ToolException is thrown upward as usual; the caller must handle it with try/except | Unrecoverable errors that require interrupting the flow |
| True | Catch ToolException and hand its content to the model as the tool's normal return value | When you want the Agent to read the error message itself and correct/retry |
| str | After catching the exception, replace the error message with a specified fixed string | Do not want to expose specific error details; only give a unified prompt |
| Callable[[ToolException], str] | After catching the exception, use a custom function to process it and generate the return content | Need to customize different prompts based on exception type/content |
Note:Do not usetool.with_config(handle_tool_errors=True)this syntax —with_config()This is a generic runtime configuration method for Runnable, used only for setting callbacks, tags, metadata, etc., and does not actually modify the tool's error handling behavior. To control a single tool's error handling, use@tool(handle_tool_error=...)(singular); if you want to uniformly configure error handling strategies for all tools at the Agent/Graph level, you set handle_tool_errors (plural) on the underlying ToolNode, rather than on individual tool objects. The two field names are similar but operate at different levels; pay attention to the distinction when using them.
Complete Example — Agent with Error Handling
Example
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:
"""Book a course on EXAMPLE for the user.
Args:
user_name: user's name
course_name: course name
"""
# Check whether the user exists
valid_users = {"Zhang San", "Li Si", "Wang Wu"}
if user_name not in valid_users:
raise ToolException(
f"User '{user_name}' does not exist."
f"Valid users: {', '.join(sorted(valid_users))}"
)
# Check whether the course exists
valid_courses = {"Python3 Basic Tutorial", "HTML Basic Tutorial", "Java Object-Oriented"}
if course_name not in valid_courses:
raise ToolException(
f"Course '{course_name}' does not exist."
f"Valid courses: {', '.join(sorted(valid_courses))}"
)
return f"Successfully booked {course_name} for {user_name}"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[book_course],
system_prompt="You are a course consultant at EXAMPLE.",
)
# Normal invocation
result = agent.invoke({
"messages": [HumanMessage(content="Book Python3 Basic Tutorial for Zhang San")]
})
print(f"Success: {result['messages'][-1].content[:100]}")
# Error call: user does not exist
result = agent.invoke({
"messages": [HumanMessage(content="Book Python3 Basic Tutorial for Zhao Liu")]
})
print(f"\n"Error - user does not exist:")
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]}")
Run result:
Success: 已经为您成功预订《Python3 基础教程》,张三同学,祝您学习愉快! 错误-用户不存在: [tool] 用户 '赵六' 不存在。有效用户:张三, 李四, 王五 [ai] 抱歉,系统中没有找到名为"赵六"的用户,暂时无法为您完成预订。 目前可预订的用户有:张三、李四、王五。请确认姓名后重新尝试。
Other extensionsIt can be seen that after the tool is configured withhandle_tool_error=Truethe ToolException content will appear normally in the conversation history as a ToolMessage (the [tool] line in the output above) without crashing the program; after seeing this error message, the model will relay it to the user in natural language and offer available alternatives. This is the typical effect of combining return_direct, InjectedToolCallId, ToolException, and handle_tool_error: it ensures the robustness of tool calls without sacrificing user experience.