Episode 4 — Generative AI Engineering / 4.17 — LangChain Practical
4.17.d — Working with Agents
In one sentence: A LangChain agent uses an LLM as a reasoning engine to decide which tools to call, in what order, and with what arguments — executing a dynamic loop of think-act-observe until it has enough information to answer the user's question.
Navigation: <- 4.17.c Tools and Memory | 4.17.e — LCEL Overview ->
1. What Is an Agent?
A chain follows a fixed path: prompt -> model -> parser. You define the steps at development time.
An agent follows a dynamic path: the LLM decides at runtime which tools to call, what to do with the results, and when to stop. The steps are determined by the model's reasoning, not by your code.
Chain (fixed):
User question -> Prompt -> Model -> Parser -> Answer
Always the same steps, every time.
Agent (dynamic):
User question -> Model thinks: "I need to search for this"
-> Calls search tool -> Gets results
-> Model thinks: "I need to calculate something"
-> Calls calculator -> Gets result
-> Model thinks: "Now I have enough info"
-> Generates final answer
Different steps depending on the question.
The agent loop
Every agent follows this fundamental loop:
1. THINK — The LLM receives the user's question + tool descriptions
and decides what to do next
2. ACT — If the LLM decides to use a tool, LangChain executes that tool
with the arguments the LLM specified
3. OBSERVE — The tool's output is fed back to the LLM as a new observation
4. REPEAT — Go back to step 1 with the new observation included
5. FINISH — When the LLM decides it has enough information, it generates
the final answer
2. AgentExecutor
The AgentExecutor is the runtime that manages the think-act-observe loop. It:
- Passes the prompt and tool descriptions to the LLM
- Parses the LLM's output to detect tool calls
- Executes the requested tools
- Feeds results back to the LLM
- Detects when the agent is done (final answer)
- Handles errors, timeouts, and maximum iterations
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { Calculator } from '@langchain/community/tools/calculator';
// 1. Define tools
const tools = [
new TavilySearchResults({ maxResults: 3 }),
new Calculator()
];
// 2. Create the prompt
const prompt = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful research assistant. Use tools when needed. Always cite your sources.'],
['user', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
// 3. Create the model
const model = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0 });
// 4. Create the agent (binds tools to the model)
const agent = await createOpenAIToolsAgent({ llm: model, tools, prompt });
// 5. Create the executor (manages the loop)
const executor = new AgentExecutor({
agent,
tools,
verbose: true, // Log reasoning steps to console
maxIterations: 5, // Safety: stop after 5 tool calls
returnIntermediateSteps: true // Include tool calls in the response
});
// 6. Run the agent
const result = await executor.invoke({
input: 'What is the current population of Tokyo, and what is that divided by the number of Tokyo subway lines?'
});
console.log(result.output);
// "Tokyo's population is approximately 14 million. Tokyo has 13 subway lines.
// 14,000,000 / 13 = approximately 1,076,923 people per subway line."
// See the intermediate steps
console.log(result.intermediateSteps);
// [
// { action: { tool: "tavily_search", input: "current population of Tokyo 2025" }, observation: "..." },
// { action: { tool: "tavily_search", input: "number of Tokyo subway lines" }, observation: "..." },
// { action: { tool: "calculator", input: "14000000 / 13" }, observation: "1076923.07..." }
// ]
The agent_scratchpad
The agent_scratchpad is a special placeholder that holds the agent's working memory — the sequence of tool calls and their results from previous iterations. The executor fills this automatically.
Iteration 1:
agent_scratchpad: [] (empty — first turn)
Model output: "I need to search for Tokyo's population"
Tool call: search("current population of Tokyo")
Tool result: "Tokyo has approximately 14 million residents..."
Iteration 2:
agent_scratchpad: [
AIMessage with tool_call("search", "current population of Tokyo"),
ToolMessage("Tokyo has approximately 14 million residents...")
]
Model output: "Now I need to find the number of subway lines"
Tool call: search("number of Tokyo subway lines")
Tool result: "Tokyo has 13 subway lines..."
Iteration 3:
agent_scratchpad: [
previous calls + results,
AIMessage with tool_call("search", "number of Tokyo subway lines"),
ToolMessage("Tokyo has 13 subway lines...")
]
Model output: "Now I'll calculate 14000000 / 13"
Tool call: calculator("14000000 / 13")
Tool result: "1076923.076923..."
Iteration 4:
agent_scratchpad: [all previous calls + results]
Model output: FINAL ANSWER "Tokyo's population is approximately 14 million..."
3. Agent Types
OpenAI Tools Agent (recommended for OpenAI models)
Uses OpenAI's native tool calling feature. The model outputs structured tool call objects that are reliably parsed. This is the most robust agent type for OpenAI models.
import { createOpenAIToolsAgent, AgentExecutor } from 'langchain/agents';
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
const model = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0 });
const prompt = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful assistant with access to tools.'],
['user', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
const agent = await createOpenAIToolsAgent({
llm: model,
tools: tools,
prompt: prompt
});
const executor = new AgentExecutor({ agent, tools });
How it works: The model returns a structured tool_calls array in its response. LangChain parses this directly — no text parsing needed. This is reliable because the model's API guarantees the format.
ReAct Agent (provider-agnostic)
Uses the ReAct (Reasoning + Acting) prompting pattern. The model outputs its reasoning as text, including explicit "Thought", "Action", and "Observation" steps. Works with any model that can follow instructions.
import { createReActAgent, AgentExecutor } from 'langchain/agents';
import { ChatOpenAI } from '@langchain/openai';
import { pull } from 'langchain/hub';
// Pull the standard ReAct prompt from LangChain Hub
const prompt = await pull('hwchase17/react');
const model = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0 });
const agent = await createReActAgent({
llm: model,
tools: tools,
prompt: prompt
});
const executor = new AgentExecutor({ agent, tools, verbose: true });
const result = await executor.invoke({
input: 'What is the square root of the year LangChain was first released?'
});
How it works: The model generates text like:
Thought: I need to find when LangChain was first released.
Action: tavily_search_results_json
Action Input: "LangChain first release date"
Observation: LangChain was first released in October 2022...
Thought: Now I need to calculate the square root of 2022.
Action: calculator
Action Input: sqrt(2022)
Observation: 44.966...
Thought: I now know the final answer.
Final Answer: The square root of 2022 (the year LangChain was released) is approximately 44.97.
LangChain parses this text to extract the Action and Action Input.
Structured Chat Agent
A variant that handles multi-input tools better by using structured JSON for action inputs.
import { createStructuredChatAgent, AgentExecutor } from 'langchain/agents';
const agent = await createStructuredChatAgent({
llm: model,
tools: tools,
prompt: prompt
});
Agent type comparison
| Agent Type | Provider | Tool Calling Method | Reliability | Best For |
|---|---|---|---|---|
| OpenAI Tools | OpenAI only | Native API tool_calls | Very high | Production with OpenAI |
| ReAct | Any model | Text-based parsing | Medium-high | Multi-provider, debugging |
| Structured Chat | Any model | JSON in text | Medium-high | Multi-input tools |
| XML Agent | Anthropic | XML-based parsing | High | Claude models |
4. Agent Execution Flow and Logging
Understanding the execution flow is critical for debugging agent behavior.
Verbose logging
const executor = new AgentExecutor({
agent,
tools,
verbose: true // Prints every step to console
});
// Console output for verbose mode:
//
// [chain/start] Entering agent executor chain...
// [agent/action]
// Tool: tavily_search_results_json
// Tool Input: {"query": "current weather in London"}
// [tool/start] Starting tool: tavily_search_results_json
// [tool/end] Tool returned: [{"title":"Weather in London","content":"Currently 15C..."}]
// [agent/action]
// Tool: calculator
// Tool Input: "15 * 9/5 + 32"
// [tool/start] Starting tool: calculator
// [tool/end] Tool returned: 59
// [agent/finish] Agent decided to finish.
// Output: "The current temperature in London is 15C (59F)."
Callbacks for custom logging
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
class AgentLogger extends BaseCallbackHandler {
name = 'AgentLogger';
handleAgentAction(action, runId) {
console.log(`[AGENT] Calling tool: ${action.tool}`);
console.log(`[AGENT] Input: ${JSON.stringify(action.toolInput)}`);
console.log(`[AGENT] Run ID: ${runId}`);
}
handleAgentEnd(output, runId) {
console.log(`[AGENT] Finished with output: ${output.output}`);
}
handleToolStart(tool, input, runId) {
console.log(`[TOOL] Starting: ${tool.name || tool.id}`);
}
handleToolEnd(output, runId) {
console.log(`[TOOL] Result: ${output.substring(0, 200)}...`);
}
handleToolError(error, runId) {
console.error(`[TOOL ERROR] ${error.message}`);
}
handleLLMStart(llm, prompts, runId) {
console.log(`[LLM] Starting inference...`);
}
handleLLMEnd(output, runId) {
const tokenUsage = output.llmOutput?.tokenUsage;
if (tokenUsage) {
console.log(`[LLM] Tokens used: ${tokenUsage.totalTokens}`);
}
}
}
const executor = new AgentExecutor({
agent,
tools,
callbacks: [new AgentLogger()]
});
Intermediate steps
const executor = new AgentExecutor({
agent,
tools,
returnIntermediateSteps: true
});
const result = await executor.invoke({ input: 'What is 2+2 and what is the capital of France?' });
// result.intermediateSteps is an array of {action, observation} pairs
for (const step of result.intermediateSteps) {
console.log(`Tool: ${step.action.tool}`);
console.log(`Input: ${JSON.stringify(step.action.toolInput)}`);
console.log(`Output: ${step.observation}`);
console.log('---');
}
5. Handling Agent Errors and Loops
Agents can fail in several ways. Robust error handling is essential for production.
Maximum iterations
const executor = new AgentExecutor({
agent,
tools,
maxIterations: 5, // Stop after 5 tool calls (prevent infinite loops)
earlyStoppingMethod: 'generate' // Ask the model to give a best-effort answer
// Alternative: 'force' — return "Agent stopped due to iteration limit"
});
Tool execution errors
import { DynamicTool } from '@langchain/community/tools/dynamic';
const riskyTool = new DynamicTool({
name: 'api_call',
description: 'Call an external API',
func: async (input) => {
try {
const response = await fetch(`https://api.example.com/data?q=${input}`);
if (!response.ok) {
// Return error as string — agent can read it and adjust
return `Error: API returned status ${response.status}. Try a different query.`;
}
const data = await response.json();
return JSON.stringify(data);
} catch (error) {
// Don't throw — return error message for the agent to handle
return `Error: Could not reach the API. Error: ${error.message}`;
}
}
});
Handle execution errors at the executor level
const executor = new AgentExecutor({
agent,
tools,
handleParsingErrors: true, // If the model output can't be parsed, send error back to model
maxIterations: 5
});
// Or provide a custom error handler
const executor2 = new AgentExecutor({
agent,
tools,
handleParsingErrors: (error) => {
return `There was an error parsing your output. Please format your response correctly. Error: ${error.message}`;
},
maxIterations: 5
});
Common agent failure patterns
| Pattern | Cause | Solution |
|---|---|---|
| Infinite loop | Agent keeps calling the same tool with the same input | Set maxIterations, improve tool descriptions |
| Wrong tool selection | Vague tool descriptions | Write precise descriptions with examples of when to use |
| Hallucinated tool | Agent tries to call a tool that doesn't exist | Ensure tool names are clear; handleParsingErrors: true |
| Bad tool input | Agent passes wrong argument types | Use Zod schemas for structured input validation |
| Token overflow | Too many iterations fill the context window | Set maxIterations, use concise tool output |
| Slow execution | Many sequential tool calls | Set timeouts; consider if a chain (not agent) is more appropriate |
Timeout handling
const executor = new AgentExecutor({
agent,
tools,
maxIterations: 5
});
// Add a timeout wrapper
async function invokeWithTimeout(executor, input, timeoutMs = 30000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await executor.invoke(input, {
signal: controller.signal
});
return result;
} catch (error) {
if (error.name === 'AbortError') {
return { output: 'The request timed out. Please try a simpler question.' };
}
throw error;
} finally {
clearTimeout(timeout);
}
}
6. Full Working Example: Research Agent
Here is a complete, production-style research agent with search, calculation, and a custom knowledge base tool:
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { DynamicStructuredTool } from '@langchain/community/tools/dynamic';
import { z } from 'zod';
// --- Tool 1: Web search ---
const searchTool = new TavilySearchResults({
maxResults: 3,
name: 'web_search',
description: 'Search the web for current information. Use this for recent events, statistics, or facts you are not sure about.'
});
// --- Tool 2: Calculator ---
const calculatorTool = new DynamicStructuredTool({
name: 'calculator',
description: 'Perform mathematical calculations. Input should be a valid mathematical expression like "2 + 2" or "sqrt(144)" or "15 * 0.2".',
schema: z.object({
expression: z.string().describe('The mathematical expression to evaluate')
}),
func: async ({ expression }) => {
try {
// Using Function constructor for safe math evaluation
// In production, use a proper math library like mathjs
const sanitized = expression.replace(/[^0-9+\-*/().%\s^sqrt]/g, '');
const withSqrt = sanitized.replace(/sqrt\(([^)]+)\)/g, 'Math.sqrt($1)');
const withPow = withSqrt.replace(/(\d+)\s*\^\s*(\d+)/g, 'Math.pow($1,$2)');
const result = new Function(`return ${withPow}`)();
return `${expression} = ${result}`;
} catch (error) {
return `Error calculating "${expression}": ${error.message}`;
}
}
});
// --- Tool 3: Internal knowledge base ---
const knowledgeBase = {
'company_founding': 'TechCorp was founded in 2019 by Jane Smith and Bob Lee.',
'product_pricing': 'TechCorp Pro costs $49/month, Enterprise costs $199/month.',
'team_size': 'TechCorp has 150 employees as of Q1 2025.',
'headquarters': 'TechCorp is headquartered in Austin, Texas.',
'revenue': 'TechCorp reported $25 million ARR in 2024.'
};
const knowledgeBaseTool = new DynamicStructuredTool({
name: 'internal_knowledge_base',
description: `Search the internal company knowledge base for information about TechCorp. Use this BEFORE web search for any company-specific questions. Available topics: company founding, product pricing, team size, headquarters, revenue.`,
schema: z.object({
query: z.string().describe('A search query about the company')
}),
func: async ({ query }) => {
const queryLower = query.toLowerCase();
const results = [];
for (const [key, value] of Object.entries(knowledgeBase)) {
const keyWords = key.replace(/_/g, ' ');
if (queryLower.includes(keyWords) || keyWords.split(' ').some(w => queryLower.includes(w))) {
results.push(value);
}
}
if (results.length === 0) {
return 'No results found in the internal knowledge base for this query. Try web_search instead.';
}
return results.join('\n');
}
});
// --- Agent setup ---
const model = new ChatOpenAI({
modelName: 'gpt-4o',
temperature: 0
});
const tools = [searchTool, calculatorTool, knowledgeBaseTool];
const prompt = ChatPromptTemplate.fromMessages([
['system', `You are a thorough research assistant for TechCorp employees.
Rules:
1. For company-specific questions, ALWAYS check the internal_knowledge_base FIRST
2. For general knowledge or current events, use web_search
3. For any calculations, use the calculator (do NOT calculate in your head)
4. Always cite which tool provided the information
5. If you cannot find reliable information, say so honestly
6. Provide clear, structured answers with bullet points when appropriate`],
['user', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
const agent = await createOpenAIToolsAgent({ llm: model, tools, prompt });
const executor = new AgentExecutor({
agent,
tools,
verbose: true,
maxIterations: 8,
returnIntermediateSteps: true,
handleParsingErrors: true
});
// --- Run queries ---
// Query 1: Company question (should use internal KB)
const r1 = await executor.invoke({
input: 'How much does TechCorp Pro cost per year?'
});
console.log(r1.output);
// Agent flow:
// 1. Checks internal_knowledge_base for "pricing" -> finds "$49/month"
// 2. Calls calculator: "49 * 12" -> "588"
// 3. Answer: "TechCorp Pro costs $49/month, which is $588/year."
// Query 2: Mixed question (internal KB + web search + calculator)
const r2 = await executor.invoke({
input: 'How does TechCorp revenue compare to the average Series B startup revenue?'
});
console.log(r2.output);
// Agent flow:
// 1. Checks internal_knowledge_base for "revenue" -> "$25M ARR in 2024"
// 2. Web search for "average Series B startup revenue 2024"
// 3. Calculator for comparison
// 4. Synthesizes answer with citations
// Query 3: Pure research (web search only)
const r3 = await executor.invoke({
input: 'What are the top 3 AI frameworks for JavaScript in 2025?'
});
console.log(r3.output);
// --- Inspect execution trace ---
console.log('\n=== Execution Trace for Query 1 ===');
for (const step of r1.intermediateSteps) {
console.log(`Tool: ${step.action.tool}`);
console.log(`Input: ${JSON.stringify(step.action.toolInput)}`);
console.log(`Output: ${step.observation.substring(0, 200)}`);
console.log('---');
}
7. Agent Design Best Practices
Prompt design for agents
// BAD: Vague system prompt
const badPrompt = ChatPromptTemplate.fromMessages([
['system', 'You are helpful. Use tools when needed.'],
['user', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
// GOOD: Specific system prompt with rules
const goodPrompt = ChatPromptTemplate.fromMessages([
['system', `You are a customer support agent for ShopCo.
Available tools and when to use them:
- order_lookup: Use when the customer asks about an order status, shipping, or delivery
- product_search: Use when the customer asks about product availability or details
- refund_processor: Use ONLY when the customer explicitly requests a refund
Rules:
1. Always greet the customer by name if available
2. Look up the order before answering shipping questions
3. Never process a refund without confirming with the customer first
4. If unsure, say "Let me check on that" and use the appropriate tool
5. Keep responses concise and friendly`],
['user', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
When to use agents vs chains
| Use an Agent When... | Use a Chain When... |
|---|---|
| The required tools depend on the user's question | The steps are always the same |
| Multiple tools might be needed in unpredictable order | You know exactly which tools/steps are needed |
| The model needs to reason about intermediate results | No reasoning is needed between steps |
| User questions are open-ended | User input follows a known format |
| You are building a research/exploration interface | You are building a data pipeline |
8. Key Takeaways
- Agents are dynamic — unlike chains that follow fixed steps, agents use the LLM to decide which tools to call at runtime based on the user's question.
- AgentExecutor manages the loop — it handles the think-act-observe cycle, tool execution, error handling, and iteration limits.
- OpenAI Tools Agent is the most reliable for OpenAI models — it uses native tool calling instead of text parsing. Use ReAct for multi-provider support.
- agent_scratchpad is the working memory — it accumulates tool calls and results across iterations so the model has context for its next decision.
- Error handling is essential — set
maxIterations, usehandleParsingErrors, return error messages from tools as strings (not exceptions), and implement timeouts. - Tool descriptions drive agent behavior — a precise description is the difference between an agent that works and one that calls the wrong tools.
- Not everything needs an agent — if the steps are predictable, a chain is simpler, faster, and more reliable.
Explain-It Challenge
- Draw the think-act-observe loop for an agent that needs to answer: "What is the population of Japan divided by the number of prefectures?" Show each iteration.
- Your agent keeps calling the same search tool in an infinite loop. Diagnose three possible causes and propose a fix for each.
- A teammate asks "should our customer support bot be an agent or a chain?" Walk through the decision criteria and give a recommendation.
Navigation: <- 4.17.c Tools and Memory | 4.17.e — LCEL Overview ->