How to create an AI agent with LangChain and LangGraph
Build a tool-using AI agent using LangChain and LangGraph in Python and TypeScript
AI agents are programs that use a large language model (LLM) to decide which actions to take, invoke tools, and iterate until a task is complete. LangChain provides the tool-calling primitives and model integrations, while LangGraph gives you a graph-based framework for orchestrating multi-step agent loops with state management.
This guide walks through creating a simple agent that can search the web and do math, first in Python and then in TypeScript.
What is an Agent?
At its core, an agent is a loop:
- The LLM receives a prompt (including conversation history and available tools).
- It decides whether to respond directly or call a tool.
- If it calls a tool, the tool executes and the result is fed back to the LLM.
- This repeats until the LLM produces a final answer.
LangGraph models this as a state graph where each node is a step (call the model, execute a tool) and edges control the flow.
Python Example
Install dependencies
pip install langchain langgraph langchain-openaiDefine tools
We’ll create two simple tools: one for adding numbers and one that simulates a web search.
from langchain_core.tools import tool
@tool
def add_numbers(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
@tool
def search_web(query: str) -> str:
"""Search the web for information. Returns a summary of top results."""
# In production, integrate a real search API (e.g., Tavily, SerpAPI)
return f"Search results for '{query}': Example result content..."Build the agent graph
from typing import Annotated
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
# 1. Define the state
class State(TypedDict):
messages: Annotated[list, add_messages]
# 2. Set up the model and bind tools
tools = [add_numbers, search_web]
model = ChatOpenAI(model="gpt-4o", temperature=0)
model_with_tools = model.bind_tools(tools)
SYSTEM_PROMPT = SystemMessage(
content=(
"You are a helpful assistant. "
"Use the tools available to you when you need to look something up "
"or perform calculations. Always cite your sources."
)
)
# 3. Define the model-calling node
def call_model(state: State) -> dict:
messages = [SYSTEM_PROMPT] + state["messages"]
response = model_with_tools.invoke(messages)
return {"messages": [response]}
# 4. Build the graph
graph = StateGraph(State)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", tools_condition, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
app = graph.compile()Run the agent
result = app.invoke({
"messages": [HumanMessage(content="What is 2,345 + 6,789? Also search for the population of Tokyo.")]
})
for msg in result["messages"]:
print(f"{msg.__class__.__name__}: {msg.content}")Expected output
HumanMessage: What is 2,345 + 6,789? Also search for the population of Tokyo.
AIMessage: (tool calls: add_numbers, search_web)
ToolMessage: 9134
ToolMessage: Search results for 'population of Tokyo': ...
AIMessage: The sum of 2,345 and 6,789 is **9,134**. According to my search, the population of Tokyo is approximately 13.96 million.
TypeScript Example
Install dependencies
npm install @langchain/core @langchain/openai @langchain/langgraphDefine tools
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const addNumbers = tool(
async ({ a, b }: { a: number; b: number }): Promise<number> => {
return a + b;
},
{
name: "add_numbers",
description: "Add two numbers together.",
schema: z.object({
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
}
);
const searchWeb = tool(
async ({ query }: { query: string }): Promise<string> => {
// In production, integrate a real search API
return `Search results for '${query}': Example result content...`;
},
{
name: "search_web",
description: "Search the web for information. Returns a summary of top results.",
schema: z.object({
query: z.string().describe("The search query"),
}),
}
);
const tools = [addNumbers, searchWeb];Build the agent graph
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import {
StateGraph,
START,
END,
Annotation,
messagesStateReducer,
} from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
// 1. Define the state
const State = Annotation.Root({
messages: Annotation({
reducer: messagesStateReducer,
}),
});
// 2. Set up the model and bind tools
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const modelWithTools = model.bindTools(tools);
const SYSTEM_PROMPT = new SystemMessage(
"You are a helpful assistant. " +
"Use the tools available to you when you need to look something up " +
"or perform calculations. Always cite your sources."
);
// 3. Define the model-calling node
async function callModel(state: typeof State.State) {
const messages = [SYSTEM_PROMPT, ...state.messages];
const response = await modelWithTools.invoke(messages);
return { messages: [response] };
}
// 4. Build the graph
const toolNode = new ToolNode(tools);
const graph = new StateGraph(State)
.addNode("agent", callModel)
.addNode("tools", toolNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", async (state) => {
const lastMessage = state.messages[state.messages.length - 1];
if (
"tool_calls" in lastMessage &&
Array.isArray(lastMessage.tool_calls) &&
lastMessage.tool_calls.length > 0
) {
return "tools";
}
return END;
})
.addEdge("tools", "agent");
const app = graph.compile();Run the agent
const result = await app.invoke({
messages: [
new HumanMessage("What is 2,345 + 6,789? Also search for the population of Tokyo."),
],
});
for (const msg of result.messages) {
console.log(`${msg._getType()}: ${msg.content}`);
}Key Concepts
| Concept | Description |
|---|---|
| State | A typed dictionary that flows through the graph. Typically holds the message list. |
| Nodes | Functions that read and update state. call_model invokes the LLM; ToolNode executes tool calls. |
| Conditional edges | Routes based on the LLM’s output. If the model calls a tool, we route to the tool node; otherwise, we finish. |
| Tool binding | model.bind_tools(tools) tells the LLM which tools are available and their schemas. |
tools_condition |
A built-in conditional that checks if the last AIMessage contains tool calls. |
Tips
- Streaming: Use
app.stream()(Python) orapp.stream()(TypeScript) to stream tokens and intermediate steps instead of waiting for the full result. - Persistence: Pass a
MemorySavercheckpointer tograph.compile()to persist conversation state across runs. - Human-in-the-loop: Insert a node that pauses execution before a tool runs, letting a human approve or edit the action before continuing.
- Multiple tools: Bind as many tools as you like — the LLM will decide which ones to call. Just keep the tool descriptions clear and specific so the model picks the right one.
- Error handling: Wrap tool execution in try/catch and return error messages as tool results so the agent can self-correct.