Building AI agents with LangChain
LangChain agents are built on LangGraph: the model calls tools in a loop until it returns a final answer. The high-level entry point is createAgent - pass a model, tools defined with tool(), and an optional systemPrompt.
This post builds the same support triage agent as the Vercel AI SDK agents and OpenAI Agents SDK posts so you can compare SDKs on one scenario. It follows the LangChain overview for Node.js and fits as post #4 in the LangChain series (after loaders/chunking and the RAG with pgvector pipeline).
Prerequisites
- OpenAI account
- Generated API key
- Enabled billing
- Node.js version 26
langchain,@langchain/openai,@langchain/core, andzodinstalled:
npm i langchain @langchain/openai @langchain/core zod
OPENAI_API_KEYset in the environment
Mental model - turns and the agent loop
A turn is one model generation. In that turn the model either:
- returns final text (the run ends), or
- returns tool calls (LangChain executes them and starts another turn with the results)
Typical flow for the support triage agent: user question → model calls lookup tools (get_customer, get_invoice, search_knowledge_base) → model creates a ticket or escalates → final answer.
A single turn can include multiple parallel tool calls. Set recursionLimit on invoke or stream to cap how many graph steps run (each model generation and tool batch counts toward the limit).
Defining tools
Use tool() from langchain with a Zod schema, plus name and description so the model knows when to call each tool:
import { tool } from 'langchain';import { z } from 'zod';const getInvoice = tool(async ({ invoiceId }) => {const invoice = invoices.find((item) => item.id === invoiceId);if (!invoice) {return { found: false, invoiceId, error: 'Invoice not found' };}return { found: true, invoice };},{name: 'get_invoice',description: 'Look up an invoice by ID, including payment IDs and status',schema: z.object({invoiceId: z.string().describe('Invoice ID, e.g. inv_8891'),}),},);
LangChain uses schema (not Vercel's inputSchema or OpenAI Agents' parameters). The handler receives validated input as the first argument.
createAgent
Wire the model, tools, and triage instructions:
import { createAgent } from 'langchain';const agent = createAgent({model: 'gpt-5.5',tools: [getInvoice],systemPrompt: `You are a billing support triage agent.Look up records before recommending refunds or creating tickets.`,});
model can be a provider string ('gpt-5.5', 'openai:gpt-5.5') or a chat model instance from @langchain/openai.
Invoke
Pass a messages array and read the final answer from result.messages:
const result = await agent.invoke({messages: [{role: 'user',content: 'What is the status of invoice inv_8891? Reply in one sentence.',},],});const lastAi = [...result.messages].reverse().find((message) => message.type === 'ai');console.log(lastAi?.content);
The last AI message is the agent's final reply after any tool calls complete.
Support triage scenario
Example prompt:
Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?
A realistic chain:
get_customer- plan tier, open ticket countget_invoice- amount, status, payment IDssearch_knowledge_base- duplicate-charge and refund policycreate_support_ticketorescalate_to_human- write action or escalation
The demo uses in-memory fixtures (customers, invoices, knowledge-base articles) so scripts run without a database.
Multi-tool agent
Register all triage tools on one agent:
import { createAgent } from 'langchain';import {getCustomer,getInvoice,searchKnowledgeBase,createSupportTicket,escalateToHuman,TRIAGE_INSTRUCTIONS,} from './tools/index.js';const agent = createAgent({model: 'gpt-5.5',tools: [getCustomer,getInvoice,searchKnowledgeBase,createSupportTicket,escalateToHuman,],systemPrompt: TRIAGE_INSTRUCTIONS,});const result = await agent.invoke({messages: [{role: 'user',content:'Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?',},],recursionLimit: 15,});const answer = [...result.messages].reverse().find((message) => message.type === 'ai');console.log(answer?.content);
Inspect result.messages for the full trace: human input, AI tool-call messages, tool results, and the final AI reply.
Streaming
agent.stream() yields state updates as the graph runs. Use streamMode: 'values' to receive the full message list after each step:
const stream = await agent.stream({messages: [{role: 'user',content:'Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?',},],},{ streamMode: 'values', recursionLimit: 15 },);let finalMessages = [];for await (const state of stream) {if (state.messages) {finalMessages = state.messages;}}const answer = [...finalMessages].reverse().find((message) => message.type === 'ai');console.log(answer?.content);
For token-level streaming, use streamMode: 'messages' or streamEvents (see LangGraph streaming).
When to pick LangChain
LangChain createAgent | Vercel AI SDK | OpenAI Agents SDK | |
|---|---|---|---|
| Best for | RAG + LCEL + agents in one stack | TypeScript apps already on AI SDK | OpenAI-first agent primitives |
| Tool definition | tool() + Zod schema | tool() + inputSchema | tool() + Zod parameters |
| Run API | agent.invoke / agent.stream | generateText + stopWhen | run() + maxTurns |
| Handoffs / guardrails | Middleware (advanced) | Limited | Built-in |
| Memory | LangGraph checkpointers | Bring your own | Session helpers |
Pick LangChain when document loaders, retrievers, and agents should share one ecosystem. Pick Vercel AI SDK or OpenAI Agents SDK when you want a focused agent layer without the broader LangChain surface.
Demo
See the langchain-agents-nodejs-demo folder for runnable scripts: single-tool lookup, full triage, and streaming.