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 TypeProviderTool Calling MethodReliabilityBest For
OpenAI ToolsOpenAI onlyNative API tool_callsVery highProduction with OpenAI
ReActAny modelText-based parsingMedium-highMulti-provider, debugging
Structured ChatAny modelJSON in textMedium-highMulti-input tools
XML AgentAnthropicXML-based parsingHighClaude 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

PatternCauseSolution
Infinite loopAgent keeps calling the same tool with the same inputSet maxIterations, improve tool descriptions
Wrong tool selectionVague tool descriptionsWrite precise descriptions with examples of when to use
Hallucinated toolAgent tries to call a tool that doesn't existEnsure tool names are clear; handleParsingErrors: true
Bad tool inputAgent passes wrong argument typesUse Zod schemas for structured input validation
Token overflowToo many iterations fill the context windowSet maxIterations, use concise tool output
Slow executionMany sequential tool callsSet 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 questionThe steps are always the same
Multiple tools might be needed in unpredictable orderYou know exactly which tools/steps are needed
The model needs to reason about intermediate resultsNo reasoning is needed between steps
User questions are open-endedUser input follows a known format
You are building a research/exploration interfaceYou are building a data pipeline

8. Key Takeaways

  1. 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.
  2. AgentExecutor manages the loop — it handles the think-act-observe cycle, tool execution, error handling, and iteration limits.
  3. 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.
  4. agent_scratchpad is the working memory — it accumulates tool calls and results across iterations so the model has context for its next decision.
  5. Error handling is essential — set maxIterations, use handleParsingErrors, return error messages from tools as strings (not exceptions), and implement timeouts.
  6. Tool descriptions drive agent behavior — a precise description is the difference between an agent that works and one that calls the wrong tools.
  7. Not everything needs an agent — if the steps are predictable, a chain is simpler, faster, and more reliable.

Explain-It Challenge

  1. 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.
  2. Your agent keeps calling the same search tool in an infinite loop. Diagnose three possible causes and propose a fix for each.
  3. 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 ->