Learnixo

LangChain Mastery · Lesson 20 of 33

Agents: How LangChain Agents Work

What is an Agent?

A chain is deterministic — the same path every time. An agent is non-deterministic — it decides at runtime which actions to take based on the query.

Chain:  Query → Step1 → Step2 → Step3 → Answer  (fixed)

Agent:  Query → Think → Tool? → Think → Tool? → Think → Answer  (adaptive)
         ↑________________________↓
                 loop until done

An agent consists of:

  1. LLM (the "brain") — decides what to do next
  2. Tools — actions the LLM can take
  3. Reasoning loop — runs until the LLM produces a final answer

The ReAct Pattern

ReAct (Reasoning + Acting) is the foundational agent pattern. The LLM alternates between:

  • Thought: Reasoning about what to do
  • Action: Calling a tool
  • Observation: Receiving the tool's result
User: What is the warfarin dose for a patient with CYP2C9 *1/*3 variant?

Thought: I need to find warfarin dosing and how CYP2C9 *1/*3 affects it.
Action: search_drug_database({"drug": "warfarin", "topic": "pharmacogenomics"})
Observation: CYP2C9 *1/*3 is an intermediate metabolizer — reduce standard dose by 30-40%.

Thought: I have the genetic info. Now I need the standard dose to calculate the adjustment.
Action: get_standard_dose({"drug": "warfarin", "indication": "anticoagulation"})
Observation: Standard dose: 2-10mg daily, INR-guided.

Thought: I can now answer: standard 2-10mg, but for *1/*3 intermediate metabolizer, reduce by 30-40%.
Final Answer: For CYP2C9 *1/*3 (intermediate metabolizer), start at 30-40% below the standard 2-10mg dose (approximately 1.5-6mg). Titrate based on INR monitoring.

Tool Calling Agent vs ReAct Agent

LangChain provides two primary agent types:

ReAct Agent: Uses prompt engineering to get the LLM to output "Thought/Action/Observation" text, then parses it.

Python
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def search_clinical_database(query: str) -> str:
    """Search the clinical pharmacology database for drug information."""
    return f"Clinical data for: {query}"

@tool
def check_interactions(drug_a: str, drug_b: str) -> str:
    """Check for interactions between two drugs."""
    return f"Interaction data for {drug_a} + {drug_b}"

tools = [search_clinical_database, check_interactions]
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# ReAct uses a special prompt format
react_prompt = hub.pull("hwchase17/react")
react_agent = create_react_agent(llm, tools, react_prompt)
executor = AgentExecutor(agent=react_agent, tools=tools, verbose=True)

result = executor.invoke({"input": "What are the interactions between warfarin and aspirin?"})
print(result["output"])

Tool Calling Agent: Uses the model's native function/tool calling API. More reliable, less prompt-sensitive.

Python
from langchain.agents import create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

# Standard prompt for tool calling
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist. Use tools to answer drug questions accurately."),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

tool_calling_agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=tool_calling_agent, tools=tools, verbose=True)

result = executor.invoke({
    "input": "What is the warfarin-aspirin interaction and how severe is it?",
    "chat_history": [],
})

The Reasoning Loop: Step by Step

Python
# What AgentExecutor does internally:

def agent_loop(user_input: str, agent, tools_dict: dict, max_iterations: int = 10) -> str:
    """Simplified view of the agent reasoning loop."""
    
    messages = [{"role": "user", "content": user_input}]
    scratchpad = []   # Accumulates: tool calls + observations
    
    for iteration in range(max_iterations):
        # 1. LLM decides what to do next
        response = agent.invoke({
            "input": user_input,
            "agent_scratchpad": scratchpad,
            "chat_history": [],
        })
        
        # 2. Is the LLM done? Return final answer
        if not response.tool_calls:
            return response.content  # Final answer
        
        # 3. Execute each tool call
        for tool_call in response.tool_calls:
            tool_name = tool_call["name"]
            tool_args = tool_call["args"]
            
            if tool_name not in tools_dict:
                observation = f"Error: tool '{tool_name}' not found"
            else:
                observation = tools_dict[tool_name].invoke(tool_args)
            
            # Add tool call + result to scratchpad
            scratchpad.append(("tool_call", tool_name, tool_args))
            scratchpad.append(("observation", tool_name, observation))
        
        # Loop continues  LLM sees scratchpad and decides next step
    
    return "Max iterations reached — unable to complete task"

Agent Scratchpad

The agent_scratchpad placeholder in the prompt holds the accumulated tool calls and observations. This is how the agent "remembers" what it's done so far within a single run:

Python
# What {agent_scratchpad} looks like at runtime:
"""
Tool calls made so far:

Tool: search_clinical_database
Input: {"query": "warfarin pharmacogenomics CYP2C9"}
Output: CYP2C9 *1/*3 reduces metabolism by 50%...

Tool: get_standard_dose
Input: {"drug": "warfarin"}
Output: Standard dose 2-10mg daily...
"""

# The LLM sees this and decides: "I have enough information. Final answer is..."

Agent Stopping Conditions

Python
from langchain.agents import AgentExecutor

executor = AgentExecutor(
    agent=tool_calling_agent,
    tools=tools,
    max_iterations=5,           # Stop after 5 tool calls
    max_execution_time=30.0,    # Stop after 30 seconds
    early_stopping_method="generate",  # Ask LLM to produce best answer when stopped
    handle_parsing_errors=True,  # Don't crash on malformed tool calls
    verbose=True,               # Log each step
)

# Output includes metadata about why execution stopped
result = executor.invoke({"input": "...", "chat_history": []})
print(result["output"])

Agent vs Chain: When to Use Each

Use a Chain when:

  • The steps are known in advance and fixed
  • You always need steps A → B → C
  • Determinism and predictability matter
  • Cost and latency are tightly constrained

Use an Agent when:

  • The model needs to decide which tools to use
  • Different queries need different steps
  • The number of steps is unknown in advance
  • Tasks require adaptive multi-step reasoning
Python
# Heuristic for choosing
def should_use_agent(query: str) -> bool:
    agent_indicators = [
        "find", "search", "look up", "compare",
        "calculate", "check", "what is the current",
        "analyze", "investigate",
    ]
    return any(ind in query.lower() for ind in agent_indicators)

# Rule of thumb:
# "Summarize this document"  Chain (fixed steps)
# "Answer this drug question using any information you need"  Agent

Agent Output Structure

Python
result = executor.invoke({
    "input": "What is the interaction between warfarin and aspirin?",
    "chat_history": [],
})

# result is a dict
print(result.keys())
# dict_keys(["input", "output", "chat_history", "intermediate_steps"])

# Final answer
print(result["output"])

# What tools were called (for logging/audit)
for step in result.get("intermediate_steps", []):
    action, observation = step
    print(f"Tool: {action.tool}")
    print(f"Input: {action.tool_input}")
    print(f"Output: {observation[:100]}")