Building AI Agents with LangChain and Node.js: A Practical Guide
Introduction
A chatbot answers questions. An AI agent takes actions. The difference is fundamental: agents can call tools (search the web, query a database, send emails, run code), remember context across sessions, and autonomously chain multiple steps to complete complex goals.
This guide walks through building a production-ready AI agent with LangChain.js, OpenAI, and Node.js — covering tool definition, memory, and multi-agent orchestration.
Core Concepts
The ReAct Pattern
The most common agent architecture is ReAct (Reasoning + Acting):
Thought → Action → Observation → Thought → Action → ...
The LLM reasons about what to do, calls a tool, observes the result, then reasons again until the task is complete.
Setting Up Your First Agent
npm install langchain @langchain/openai @langchain/core
// agents/researchAgent.ts
import { ChatOpenAI } from '@langchain/openai';
import { createReactAgent } from 'langchain/agents';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { Calculator } from '@langchain/community/tools/calculator';
const model = new ChatOpenAI({
model: 'gpt-4o',
temperature: 0,
});
const tools = [
new TavilySearchResults({ maxResults: 3 }),
new Calculator(),
];
const agent = await createReactAgent({ llm: model, tools });
const result = await agent.invoke({
input: 'What is the market cap of Apple divided by the number of countries in the EU?',
});
console.log(result.output);
// Agent will: search Apple market cap → search EU country count → calculate ratio
Defining Custom Tools
The real power is custom tools that connect your agent to your systems:
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { db } from '../lib/db';
const getUserOrders = tool(
async ({ userId, limit }) => {
const orders = await db.order.findMany({
where: { userId },
take: limit,
orderBy: { createdAt: 'desc' },
select: { id: true, total: true, status: true, createdAt: true },
});
return JSON.stringify(orders);
},
{
name: 'get_user_orders',
description: 'Retrieve recent orders for a specific user ID. Returns order ID, total, status, and date.',
schema: z.object({
userId: z.string().describe('The UUID of the user'),
limit: z.number().min(1).max(20).default(5).describe('Number of orders to retrieve'),
}),
}
);
const cancelOrder = tool(
async ({ orderId, reason }) => {
await db.order.update({
where: { id: orderId },
data: { status: 'cancelled', cancelReason: reason },
});
return `Order ${orderId} successfully cancelled.`;
},
{
name: 'cancel_order',
description: 'Cancel an order by its ID. Only use after confirming with the user.',
schema: z.object({
orderId: z.string().describe('The order ID to cancel'),
reason: z.string().describe('Reason for cancellation'),
}),
}
);
Adding Persistent Memory
Without memory, every conversation starts fresh. Use LangChain's message history:
import { createReactAgent } from 'langchain/agents';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { ChatMessageHistory } from 'langchain/memory';
const agent = await createReactAgent({ llm: model, tools });
// In-memory store (replace with Redis in production)
const sessionStore: Record<string, ChatMessageHistory> = {};
const agentWithHistory = new RunnableWithMessageHistory({
runnable: agent,
getMessageHistory: (sessionId) => {
if (!sessionStore[sessionId]) {
sessionStore[sessionId] = new ChatMessageHistory();
}
return sessionStore[sessionId];
},
inputMessagesKey: 'input',
historyMessagesKey: 'chat_history',
});
// First turn
await agentWithHistory.invoke(
{ input: 'My user ID is usr_123. What are my recent orders?' },
{ configurable: { sessionId: 'session_abc' } }
);
// Second turn — agent remembers user ID and previous context
await agentWithHistory.invoke(
{ input: 'Cancel the most recent one.' },
{ configurable: { sessionId: 'session_abc' } }
);
Production Pattern: Express API with Streaming
// routes/agent.ts
import express from 'express';
const router = express.Router();
router.post('/chat', async (req, res) => {
const { message, sessionId } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = await agentWithHistory.stream(
{ input: message },
{ configurable: { sessionId } }
);
for await (const chunk of stream) {
if (chunk.output) {
res.write(`data: ${JSON.stringify({ type: 'output', text: chunk.output })}
`);
}
if (chunk.intermediateSteps) {
for (const step of chunk.intermediateSteps) {
res.write(`data: ${JSON.stringify({ type: 'tool_call', tool: step.action.tool })}
`);
}
}
}
res.write('data: [DONE]
');
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ type: 'error', message: 'Agent failed' })}
`);
res.end();
}
});
Multi-Agent Pattern: Supervisor + Workers
For complex tasks, use a supervisor agent that delegates to specialised workers:
const supervisorPrompt = `You are a supervisor managing these specialised agents:
- research_agent: searches the web and summarises information
- data_agent: queries the database and analyses data
- email_agent: drafts and sends emails
Given the user's request, decide which agent to call and in what order.`;
Conclusion
AI agents unlock a new category of software — applications that reason, plan, and act autonomously. Start with simple tool-use, add memory, then graduate to multi-agent patterns as complexity grows.
Key takeaways:
- Tools are the bridge between LLMs and your system
- Memory turns a chatbot into an assistant
- Stream responses for a responsive UX
- Always validate and sandbox tool inputs in production
Related: See the post on building RAG systems for how to ground your agents in your own knowledge base.