Module 2: Tools and Tool Calling
Tool Execution Loop
Capsule overview
In the previous capsule you learned that the model doesn't execute tools — it just tells you what it wants to call. Now comes the other half of the flow: you run the tool and hand the result back to the model.
The full cycle is: the user asks a question → the model looks at its available tools → it answers with tool_calls saying what to call → you run each tool → you build a ToolMessage with the result → you send everything back to the model → the model produces the final answer using the tool results.
That cycle is called the tool execution loop, and it's the fundamental pattern that lets an LLM interact with the outside world. Every time ChatGPT searches the web, runs code, or reads a file — it's running exactly this loop. In this capsule you're going to implement it by hand, step by step, so you understand every piece before a framework automates it for you.
The full flow, step by step
The tool execution loop has 5 steps:
1. HumanMessage → The user asks a question
2. model.invoke() → The model analyzes and decides which tools to call
3. AIMessage → The model returns tool_calls (it runs nothing)
4. You run the tools → You call the functions and get results
5. ToolMessage → You package the results and send them to the model
6. model.invoke() → The model produces the final answer with the results
Let's see it in full code:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])
# Step 1: The user's message
messages = [HumanMessage(content="What's the weather in Madrid?")]
# Steps 2-3: The model decides to call get_weather
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Tool calls: {ai_response.tool_calls}")
# [{'name': 'get_weather', 'args': {'city': 'Madrid'}, 'id': 'call_abc123'}]
# Step 4: Run the tool
tool_call = ai_response.tool_calls[0]
result = get_weather.invoke(tool_call["args"])
print(f"Tool result: {result}")
# The weather in Madrid is sunny, 22°C
# Step 5: Build a ToolMessage with the result
tool_message = ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
)
messages.append(tool_message)
# Step 6: The model produces the final answer
final_response = model_with_tools.invoke(messages)
print(f"Final answer: {final_response.content}")
# The weather in Madrid is sunny, with a temperature of 22°C.
The model now has everything: the original question, its own decision to call get_weather, and the result of running it. With all of that, it produces a natural answer for the user.
ToolMessage: the key piece
ToolMessage is the message type you use to send a tool's result back to the model. It comes from langchain_core.messages and has two required fields:
| Field | Type | Description |
|---|---|---|
content | str | The result of running the tool (always a string) |
tool_call_id | str | The ID of the tool call that triggered this execution |
from langchain_core.messages import ToolMessage
tool_message = ToolMessage(
content="The weather in Madrid is sunny, 22°C",
tool_call_id="call_abc123"
)
Why does it need tool_call_id?
The tool_call_id ties the result back to the original request. When the model asked to call get_weather, it generated a unique ID (call_abc123). When you return the result, you use that same ID so the model knows "this is the result of the tool I asked for".
Without the right tool_call_id, the model can't match the result to its request and the invocation will fail:
from langchain_core.messages import ToolMessage
# Correct — the ID matches the tool call
tool_message = ToolMessage(
content="22°C and sunny",
tool_call_id="call_abc123" # Same ID as in ai_response.tool_calls[0]["id"]
)
# Wrong — a made-up ID that matches nothing
tool_message = ToolMessage(
content="22°C and sunny",
tool_call_id="made_up_id" # Error: doesn't match any tool call
)
content is always a string
The tool's result has to be a string. If your tool returns a number, a dictionary, or any other type, convert it to a string:
from langchain_core.messages import ToolMessage
import json
result_dict = {"temp": 22, "condition": "sunny", "humidity": 45}
tool_message = ToolMessage(
content=json.dumps(result_dict, ensure_ascii=False),
tool_call_id="call_abc123"
)
print(tool_message.content)
# {"temp": 22, "condition": "sunny", "humidity": 45}
The model can read JSON, lists, tables — any textual format.
The message list: the loop's memory
The tool execution loop works by accumulating messages in a list. Each step appends a new message, and the model receives the whole list on every invocation:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage(content="What's the weather in Madrid?")]
print(f"Step 1 — messages: {len(messages)}")
# Step 1 — messages: 1
# [HumanMessage]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Step 2 — messages: {len(messages)}")
# Step 2 — messages: 2
# [HumanMessage, AIMessage(tool_calls=[...])]
result = get_weather.invoke(ai_response.tool_calls[0]["args"])
tool_msg = ToolMessage(content=str(result), tool_call_id=ai_response.tool_calls[0]["id"])
messages.append(tool_msg)
print(f"Step 3 — messages: {len(messages)}")
# Step 3 — messages: 3
# [HumanMessage, AIMessage(tool_calls=[...]), ToolMessage]
final = model_with_tools.invoke(messages)
messages.append(final)
print(f"Step 4 — messages: {len(messages)}")
# Step 4 — messages: 4
# [HumanMessage, AIMessage(tool_calls), ToolMessage, AIMessage(content)]
for i, msg in enumerate(messages):
print(f" [{i}] {type(msg).__name__}: {msg.content[:50] if msg.content else f'tool_calls={len(msg.tool_calls)}'}")
# Output:
# [0] HumanMessage: What's the weather in Madrid?
# [1] AIMessage: tool_calls=1
# [2] ToolMessage: The weather in Madrid is sunny, 22°C
# [3] AIMessage: The weather in Madrid is sunny, with a temperature
The message list is the conversation's "memory". The model needs to see the whole sequence to understand the context: what the user asked, which tool it decided to call, what result came back — and with that, it produces the answer.
A loop with multiple tools
When the model has several tools available, it can call a different one depending on the question. The loop is the same — you just need a map of tools so you know which one to run:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
@tool
def search_web(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': LangChain is a framework for LLMs created in 2022."
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def run_tool_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
if not ai_response.tool_calls:
return ai_response.content
for tool_call in ai_response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_fn = tool_map[tool_name]
result = tool_fn.invoke(tool_args)
messages.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
))
final_response = model_with_tools.invoke(messages)
return final_response.content
print(run_tool_loop("What's the weather in Tokyo?"))
# The weather in Tokyo is sunny, with a temperature of 22°C.
print(run_tool_loop("What's 256 * 48?"))
# 256 × 48 = 12,288.
print(run_tool_loop("What is LangChain?"))
# LangChain is a framework for building applications with LLMs, created in 2022.
print(run_tool_loop("Hi, how are you?"))
# Hi! I'm doing well, thanks. How can I help you?
The tool_map pattern
The tool_map dictionary is essential:
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
# {'get_weather': <tool>, 'calculator': <tool>, 'search_web': <tool>}
When the model says "call calculator", you look up tool_map["calculator"] to get the function and run it. This pattern scales to any number of tools.
Parallel tool calls
Sometimes the model needs to call multiple tools in a single turn. For example, if you ask "What's the weather in Madrid and what's 15 * 37?", the model can generate two tool calls at once:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
tools = [get_weather, calculator]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content="What's the weather in Madrid and what's 15 * 37?")]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Tool calls: {len(ai_response.tool_calls)}")
for tc in ai_response.tool_calls:
print(f" - {tc['name']}({tc['args']})")
# Tool calls: 2
# - get_weather({'city': 'Madrid'})
# - calculator({'expression': '15 * 37'})
for tool_call in ai_response.tool_calls:
tool_fn = tool_map[tool_call["name"]]
result = tool_fn.invoke(tool_call["args"])
messages.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
))
final = model_with_tools.invoke(messages)
print(f"\nFinal answer: {final.content}")
# Final answer: The weather in Madrid is sunny, 22°C. And 15 × 37 = 555.
The loop is identical — you just iterate over every entry in tool_calls instead of assuming there's only one. Each tool call produces its own ToolMessage with its matching tool_call_id.
Manual loop vs agent
The manual loop gives you full control, but it's repetitive. In Module 3 you'll learn to use create_react_agent, which automates all of this for you:
| Feature | Manual loop | Agent |
|---|---|---|
| Control over execution | ✅ Total — you decide whether to run | ⚠️ Runs automatically |
| Code simplicity | ❌ More boilerplate | ✅ Minimal code |
| Automatic multi-round | ❌ You have to write the while loop | ✅ Built in |
| Debugging | ✅ You see every step | ⚠️ You need logging |
Rule of thumb: Use the manual loop to learn, to prototype, and for scenarios where you need granular control. Use agents for production and standard flows.
Building a reusable loop function
Let's consolidate everything into a reusable function that handles multiple rounds (in case the model needs to call more tools after seeing the first results), errors, and parallel tool calls:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
def tool_execution_loop(
question: str,
tools: list,
model_name: str = "openai:gpt-4.1-mini",
max_rounds: int = 5,
) -> str:
"""Run the full tool execution loop with multi-round support."""
tool_map = {t.name: t for t in tools}
model = init_chat_model(model_name)
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content=question)]
for round_num in range(max_rounds):
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
if not ai_response.tool_calls:
return ai_response.content
for tc in ai_response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: tool '{tc['name']}' not found"
else:
try:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
except Exception as e:
result = f"Error running {tc['name']}: {e}"
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
return "Round limit reached."
print(tool_execution_loop("What's the weather in Madrid and what's 100 / 7?", [get_weather, calculator]))
# The weather in Madrid is sunny, 22°C. And 100 ÷ 7 ≈ 14.29.
max_rounds is a safety net against infinite loops. In practice, most questions resolve in 1-2 rounds.
Handling errors in tools
Tools can fail — APIs that don't respond, invalid arguments, timeouts. The key is to catch the error and return it as a ToolMessage instead of letting the loop crash. When the model receives an error as a ToolMessage, it reads it and produces an appropriate answer for the user:
for tc in ai_response.tool_calls:
try:
result = tool_map[tc["name"]].invoke(tc["args"])
except Exception as e:
result = f"Error: {e}"
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
This pattern is already baked into the tool_execution_loop function above. Always wrap execution in try/except — never let a failing tool break the whole cycle.
Connection to the project
In the Assistant with External Tools (Capsule 08), you'll implement the full tool execution loop with three real tools. The assistant will be conversational — it'll keep the message history between turns so the user can ask follow-up questions.
The project's base pattern will be:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
@tool
def search_web(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': ..."
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
assistant = model.bind_tools(tools)
conversation = []
def chat(user_input: str) -> str:
conversation.append(HumanMessage(content=user_input))
response = assistant.invoke(conversation)
conversation.append(response)
while response.tool_calls:
for tc in response.tool_calls:
try:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
except Exception as e:
result = f"Error: {e}"
conversation.append(ToolMessage(content=result, tool_call_id=tc["id"]))
response = assistant.invoke(conversation)
conversation.append(response)
return response.content
In Module 3, you'll learn to use create_react_agent, which automates this whole loop — but understanding how it works internally gives you the foundation for debugging and customization.
Troubleshooting
Problem 1: "ToolMessage must have a tool_call_id"
Symptom: An error when invoking the model after appending a ToolMessage.
Cause: You built the ToolMessage without a tool_call_id, or the ID doesn't match any tool call from the previous AIMessage.
Fix: Always use the id from the matching tool call:
for tc in ai_response.tool_calls:
result = tool_fn.invoke(tc["args"])
messages.append(ToolMessage(
content=str(result),
tool_call_id=tc["id"] # Always from the original tool call
))
Problem 2: the model ignores the tool's result
Symptom: The model's final answer doesn't use the tool's information.
Cause: The ToolMessage wasn't appended to the message list, or it was appended in the wrong order.
Fix: Check that the message sequence is: HumanMessage → AIMessage(tool_calls) → ToolMessage(s) → invoke():
print([type(m).__name__ for m in messages])
# ['HumanMessage', 'AIMessage', 'ToolMessage'] ← correct
# ['HumanMessage', 'ToolMessage', 'AIMessage'] ← wrong
Problem 3: "Tool X not found" in the tool_map
Symptom: A KeyError because tool_call["name"] isn't in tool_map.
Cause: The model "hallucinated" a tool name that doesn't exist, or there's a typo in the name.
Fix: Always check that the name exists before running it:
for tc in ai_response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: tool '{tc['name']}' doesn't exist"
else:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
Exercises
Exercise 1: the basic loop, step by step (Easy)
Implement the full tool execution loop for a single tool, get_population(country: str), that returns a country's population. Print every step of the loop (the user's message, the tool call, the result, and the final answer).
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_population(country: str) -> str:
"""Get a country's current population."""
populations = {
"Mexico": "129 million",
"Spain": "47 million",
"Argentina": "46 million",
"Colombia": "52 million",
}
return populations.get(country, f"I have no data for {country}")
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_population])
messages = [HumanMessage(content="How many people live in Mexico?")]
print(f"Step 1 — User: {messages[0].content}")
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Step 2 — The model decides: {ai_response.tool_calls}")
tc = ai_response.tool_calls[0]
result = get_population.invoke(tc["args"])
print(f"Step 3 — Tool executed: {result}")
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
print(f"Step 4 — ToolMessage created")
final = model_with_tools.invoke(messages)
print(f"Step 5 — Final answer: {final.content}")
# Output:
# Step 1 — User: How many people live in Mexico?
# Step 2 — The model decides: [{'name': 'get_population', 'args': {'country': 'Mexico'}, 'id': '...'}]
# Step 3 — Tool executed: 129 million
# Step 4 — ToolMessage created
# Step 5 — Final answer: Mexico's population is roughly 129 million people.
Explanation: Every step of the loop is cleanly separated. The model first asks for the data (the tool call), you run it and return it as a ToolMessage, and finally the model produces a natural answer that folds the information in.
Exercise 2: a loop with two tools (Easy)
Create two tools: get_weather(city) and get_time(timezone). Implement the loop for both. Try it with "What's the weather in Lima?" and "What time is it in UTC-5?".
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is cloudy, 18°C"
@tool
def get_time(timezone: str) -> str:
"""Get the current time in a timezone."""
return f"In {timezone} it's 14:30"
tools = [get_weather, get_time]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def run_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
result = tool_map[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
return final.content
print(run_loop("What's the weather in Lima?"))
# The weather in Lima is cloudy, with a temperature of 18°C.
print(run_loop("What time is it in UTC-5?"))
# In the UTC-5 timezone it's 14:30.
print(run_loop("What's the capital of Peru?"))
# The capital of Peru is Lima.
Explanation: The tool_map lets you dispatch dynamically to whichever tool the model asks for. The third question shows the model answering directly when it doesn't need tools.
Exercise 3: parallel tool calls (Medium)
Ask a question that needs two tools at once (for example "What's the weather in Madrid and what's 99 * 13?"). Print how many tool calls the model generated and show each one's result before the final answer.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 25°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
tools = [get_weather, calculator]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content="What's the weather in Madrid and what's 99 * 13?")]
response = model_with_tools.invoke(messages)
messages.append(response)
print(f"Tool calls generated: {len(response.tool_calls)}")
for i, tc in enumerate(response.tool_calls):
result = tool_map[tc["name"]].invoke(tc["args"])
print(f" [{i+1}] {tc['name']}({tc['args']}) → {result}")
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
print(f"\nFinal answer: {final.content}")
# Output:
# Tool calls generated: 2
# [1] get_weather({'city': 'Madrid'}) → The weather in Madrid is sunny, 25°C
# [2] calculator({'expression': '99 * 13'}) → 1287
#
# Final answer: The weather in Madrid is sunny, 25°C. And 99 × 13 = 1,287.
Explanation: The model generated two tool calls in a single turn — one for the weather and one for the calculation. The loop iterates over both, runs each tool, and creates a ToolMessage for each result. The model gets both results and produces one integrated answer.
Exercise 4: a loop with error handling (Medium)
Modify the loop to handle three kinds of errors: tool not found, an exception during execution, and an empty result. Test each case with tools that fail on purpose.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
if city.lower() == "atlantis":
raise ValueError("City not found in the database")
if city.lower() == "empty":
return ""
return f"The weather in {city} is sunny, 22°C"
tools = [get_weather]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def safe_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: the tool '{tc['name']}' doesn't exist"
print(f" ⚠️ Tool not found: {tc['name']}")
else:
try:
raw_result = tool_map[tc["name"]].invoke(tc["args"])
result = str(raw_result) if raw_result else "No result available"
if not result.strip():
result = "The tool returned an empty result"
print(f" ⚠️ Empty result from {tc['name']}")
except Exception as e:
result = f"Error running {tc['name']}: {e}"
print(f" ⚠️ Error: {e}")
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
return final.content
print("Test 1 — Valid city:")
print(safe_loop("What's the weather in Madrid?"))
# Output: The weather in Madrid is sunny, with a temperature of 22°C.
print("\nTest 2 — Invalid city:")
print(safe_loop("What's the weather in Atlantis?"))
# Output:
# ⚠️ Error: City not found in the database
# Sorry, I couldn't get the weather for Atlantis...
print("\nTest 3 — Empty result:")
print(safe_loop("What's the weather in Empty?"))
# Output:
# ⚠️ Empty result from get_weather
# I couldn't get any weather information...
Explanation: Error handling has three layers: check that the tool exists, catch execution exceptions, and handle empty results. In all three cases, an error message is sent as a ToolMessage so the model can produce an appropriate answer.
Summary
In this capsule you learned:
- The tool execution loop is the cycle:
HumanMessage → AIMessage(tool_calls) → run tools → ToolMessage → final answer ToolMessagepackages a tool's result and requirescontent(a string) andtool_call_id(from the original tool call)- The message list accumulates every step — the model needs to see the whole sequence to produce the final answer
- The tool_map (
{name: tool_fn}) lets you dispatch dynamically to whichever tool the model asks for - Parallel tool calls work the same way — you just iterate over every entry in
tool_callsand create oneToolMessageper call - Multiple rounds are handled with a
whileloop that keeps running as long as the model asks for more tools - Error handling is critical — failing tools should return an error as a
ToolMessage, not crash the loop - Manual loop vs agent: the manual one gives you full control; the agent (Module 3) automates it
Next capsule: Parallel Tool Calls and Streaming — you'll learn to handle multiple simultaneous tool calls with streaming, accumulate tool call chunks, and build progressive answers when tools are in the mix.
Further reading
- Tool Calling — LangChain Docs — Full conceptual guide to tool calling
- How to call tools using ToolCall — Step-by-step tutorial of the tool execution loop
- ToolMessage API Reference — Full ToolMessage reference
- How to handle tool errors — Error handling in tool calling
- ReAct Agent — LangGraph — The agent that automates the tool execution loop
- OpenAI Function Calling — How tool calling works at the API level
- Messages — LangChain Docs — Message types: HumanMessage, AIMessage, ToolMessage
- How to pass tool results back to model — Guide to ToolMessage and results
Module 2 — LangChain & LangGraph: From Chains to Agents