teachyou.ai academy
← All posts
LangChainLLM orchestrationAI agentsLangGraphRAG

LangChain Agents vs Chains: When to Use Each

Pramod Dutta · Jul 4, 2026 · 12 min read

Picking between LangChain agents vs chains comes down to one question: does the LLM need to decide what happens next, or do you already know the steps? A chain is a fixed pipeline, prompt goes to model, model output goes to a parser, parser output goes to the next step, always in that order. An agent is a loop where the model looks at the current state, picks a tool, reads the result, and decides whether to keep going or stop. Chains are cheaper, faster, and predictable. Agents are flexible but slower, harder to test, and can burn tokens looping when the task is ambiguous. Most production LangChain apps use both: chains for the parts that are deterministic, agents for the parts that genuinely require judgment.

This guide walks through the actual mechanics of each, when to reach for one over the other, and how to build both with the current LangChain and LangGraph APIs.

What a Chain Actually Does

A chain is a directed sequence of calls, LCEL (LangChain Expression Language) composes them with the pipe operator. Each step takes the previous step's output as input. There's no branching based on what the model "thinks" it should do next, the branching (if any) is code you wrote, not a decision the LLM makes at runtime.

A typical chain: format a prompt, call the model, parse the output into structured data, maybe call a second model with that structured data. That's it. Run it ten times with the same input and you get the same sequence of calls every time (the only variance is the model's actual output, not which steps execute).

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template(
    "Summarize this support ticket in one sentence:\n\n{ticket}"
)
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
parser = StrOutputParser()

summarize_chain = prompt | model | parser

result = summarize_chain.invoke({"ticket": "Customer can't reset password, gets 500 error on submit"})
print(result)

That pipe syntax (prompt | model | parser) is the whole chain. No tool calls, no loop, no decision-making. Swap in a PydanticOutputParser and you get a chain that reliably turns free text into a typed object every single time, which is exactly what you want for something like ticket triage or data extraction.

What an Agent Actually Does

An agent adds a loop and a set of tools the model can call. Instead of you deciding the sequence, the model decides: it sees the user's request plus a list of available tools (each with a name, description, and input schema), picks one, the tool runs, the result goes back into the model's context, and the model decides again, call another tool, or return a final answer.

This is the ReAct pattern (Reason plus Act) under the hood, whether you're using an old-style AgentExecutor or the current recommended approach, LangGraph's prebuilt create_react_agent. The loop keeps running until the model decides it has enough information to answer, or you hit a step limit.

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

@tool
def get_order_status(order_id: str) -> str:
    """Look up the current status of an order by its ID."""
    # pretend this hits a real database
    return f"Order {order_id} is out for delivery, arriving in 2 days."

@tool
def issue_refund(order_id: str, reason: str) -> str:
    """Issue a refund for an order given a reason."""
    return f"Refund issued for order {order_id}. Reason logged: {reason}"

model = ChatOpenAI(model="gpt-4.1", temperature=0)
agent = create_react_agent(model, tools=[get_order_status, issue_refund])

response = agent.invoke({
    "messages": [("user", "Where's my order 8842, and if it's delayed more than 5 days just refund it")]
})
print(response["messages"][-1].content)

Notice what you didn't write: no explicit "call get_order_status first, then check the delay, then maybe call issue_refund." The model figures that sequencing out on its own by reading the tool descriptions and the user's request. That's the entire value proposition of an agent, and it's also the entire risk. If the model picks the wrong tool, calls it with bad arguments, or loops indefinitely trying to satisfy an ambiguous instruction, you don't get a clean stack trace, you get a plausible-looking wrong answer.

The Real Decision Criteria

Ignore the marketing distinction for a second and ask three concrete questions about your workflow.

  • Do you know the steps ahead of time? If you can write down "step 1, step 2, step 3" and that sequence never changes based on what the model finds along the way, use a chain. Extraction, summarization, classification, translation, and most RAG retrieval-then-answer patterns are chains.
  • Does the number of steps vary per request? If one user's question needs zero tool calls and another needs five, in an order you can't predict, that's an agent. A "research this company" request might need one web search or ten, depending on how much public information exists.
  • Is a wrong intermediate step recoverable? Chains fail loud (an exception, a schema validation error). Agents can fail quiet (the model confidently calls the wrong tool and produces a coherent but incorrect answer). If wrong answers reaching a user is expensive, either don't use an agent, or wrap it in tight guardrails, human approval steps, and tool-level validation.

A good rule of thumb: default to a chain. Only reach for an agent when you've tried to write down the fixed sequence and genuinely can't, because the right sequence depends on information you don't have until the model starts working.

Building a RAG Chain (Fixed Pipeline)

Retrieval-augmented generation is the canonical chain use case when the retrieval step doesn't need to be reconsidered mid-task. Embed the query, fetch documents, stuff them into a prompt, generate an answer.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(persist_directory="./docs_db", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag_prompt = ChatPromptTemplate.from_template(
    "Answer the question using only the context below. "
    "If the context doesn't contain the answer, say you don't know.\n\n"
    "Context:\n{context}\n\nQuestion: {question}"
)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    | StrOutputParser()
)

answer = rag_chain.invoke("What's our refund policy for digital products?")
print(answer)

This runs the same four steps every time: retrieve, format, prompt, generate. No looping, no tool selection. It's fast, cheap, and you can unit test each stage independently because the inputs and outputs at every step are fixed shapes.

Building an Agentic RAG System (Variable Pipeline)

Now compare that to agentic RAG, where the model decides whether it even needs to retrieve, how many times, and whether to search a different source if the first pass comes up empty.

from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def search_docs(query: str) -> str:
    """Search the internal knowledge base for relevant passages."""
    docs = retriever.invoke(query)
    return "\n\n".join(d.page_content for d in docs) or "No results found."

@tool
def search_web(query: str) -> str:
    """Search the public web when internal docs don't have an answer."""
    # wire this up to a real search tool in production
    return f"Web results for: {query}"

agent = create_react_agent(
    ChatOpenAI(model="gpt-4.1", temperature=0),
    tools=[search_docs, search_web],
)

response = agent.invoke({
    "messages": [("user", "What's our refund policy, and how does it compare to Shopify's default policy?")]
})
print(response["messages"][-1].content)

For this question the agent will likely call search_docs once for the internal policy, then search_web for Shopify's, then synthesize both, three model calls and two tool calls in a sequence you didn't hardcode. That flexibility is the entire reason to pay for an agent instead of a chain: the question spans two knowledge sources and a fixed chain would need a branch for every possible combination.

Cost, Latency, and Reliability Tradeoffs

These aren't abstract concerns, they show up in your bill and your p99 latency immediately.

  • Token cost. A chain makes a fixed number of model calls. An agent makes a variable number, and each loop iteration re-sends the growing conversation history (including every prior tool call and result) back to the model. A five-step agent loop can easily use 5-10x the tokens of an equivalent chain.
  • Latency. Every agent loop iteration is a full round trip to the model. If your chain does one model call in 800ms, a three-iteration agent loop is 2.5+ seconds minimum, more if any tool call itself is slow (a database query, an external API).
  • Determinism. Chains are much easier to cache, snapshot-test, and reason about because the same input structure produces the same call sequence. Agents can take a different path on retries even with temperature=0, because tool results (like "today's stock price") change between calls, or because the model's tool selection has enough entropy at the margin to vary.
  • Failure mode. A chain that hits a schema validation error throws an exception you can catch. An agent that misuses a tool doesn't throw, it just produces output. Add tool-level input validation, and consider a max iteration limit so a confused agent can't loop forever burning tokens.

Hybrid Patterns: Chains Inside Agent Tools

The strongest production pattern isn't picking one over the other, it's using chains as the tools an agent calls. The agent handles the parts that need judgment (which tool, in what order); each tool itself is a deterministic chain underneath.

from langchain_core.tools import tool

@tool
def classify_and_summarize_ticket(ticket_text: str) -> str:
    """Classify a support ticket's urgency and produce a one-line summary."""
    result = summarize_chain.invoke({"ticket": ticket_text})
    return result

@tool
def draft_customer_reply(summary: str, tone: str = "friendly") -> str:
    """Draft a reply to a customer given a ticket summary and desired tone."""
    reply_chain = (
        ChatPromptTemplate.from_template(
            "Write a {tone} reply addressing this issue: {summary}"
        )
        | ChatOpenAI(model="gpt-4.1-mini", temperature=0.3)
        | StrOutputParser()
    )
    return reply_chain.invoke({"summary": summary, "tone": tone})

support_agent = create_react_agent(
    ChatOpenAI(model="gpt-4.1", temperature=0),
    tools=[classify_and_summarize_ticket, draft_customer_reply, get_order_status, issue_refund],
)

Now the agent decides the sequence (summarize first, check order status if relevant, maybe refund, then draft a reply), but every individual step is a tested, deterministic chain. This gets you the flexibility of an agent without turning every single sub-task into an unpredictable free-for-all. It's also much easier to debug: if the reply tone is wrong, you know the bug is in draft_customer_reply's chain, not somewhere in an opaque multi-step agent trace.

Debugging and Observability

Chains are easy to debug because you can call .invoke() on any intermediate step and inspect the output directly, the data flow is linear. For agents, use LangSmith (or an equivalent tracing tool) from day one, not as an afterthought. You need to see the full sequence of tool calls, their arguments, and their outputs to understand why an agent did what it did, print statements won't cut it once you have more than two tools in play.

A few habits that pay off fast:

  • Log every tool call's input and output, even in development. Silent wrong answers are the hardest agent bugs to catch.
  • Set a hard recursion_limit (LangGraph's term for max loop iterations) on every agent. Fifteen is a reasonable default for most workflows, an agent that needs more than that is usually stuck, not making progress.
  • Write tool descriptions like you're writing documentation for a new engineer, not a comment for yourself. The model only knows what the tool does from that description plus its name and schema, vague descriptions cause vague tool selection.
  • Test agents with a fixed set of adversarial prompts (ambiguous requests, requests with no valid tool, requests that could match two tools) the same way you'd write unit tests for a chain.

Migration Path: When to Convert a Chain Into an Agent

Start with a chain. If you find yourself writing if/else branches around a chain to handle different request types, that's usually the signal it's time to convert to an agent, because you're manually doing the routing that an agent's tool selection would do for you. The reverse move (agent to chain) is common too, once you've watched an agent in production for a while and noticed it always calls the same three tools in the same order for 95% of real traffic, hardcode that path as a chain and keep the agent only for the remaining edge cases. That hybrid keeps your median-case latency and cost low while still handling the long tail correctly.

FAQ

Are LangChain agents just chains with extra steps? No. A chain executes a fixed sequence you define in code. An agent has the model choose the sequence at runtime by selecting from a set of tools, the number and order of steps can differ on every call.

Should I use AgentExecutor or LangGraph for building agents? Use LangGraph's create_react_agent (or a custom graph) for new work. AgentExecutor still runs but LangGraph is the actively developed path and gives you explicit control over state, checkpointing, and human-in-the-loop interrupts that AgentExecutor doesn't expose cleanly.

Can a chain call tools too? Yes, but the tool calls in a chain are hardcoded by you, not chosen by the model. If you write retriever | prompt | model, the retriever step always runs, that's a fixed pipeline step, not a decision.

Why does my agent keep looping without giving a final answer? Usually one of three causes: a tool description that doesn't clearly signal when the task is complete, a tool that returns an error the model doesn't know how to recover from, or a missing recursion_limit letting a confused loop run indefinitely. Add explicit "you have enough information, stop here" guidance in your system prompt and set a hard iteration cap.

Is an agent always slower and more expensive than a chain? For a task with a fixed number of steps, yes, an agent adds overhead for no benefit. For a task where the number of steps genuinely varies (some requests need one lookup, others need five), an agent can actually be cheaper than a chain built to handle the worst case every time, because it only does the work each specific request needs.

Do I need LangGraph specifically, or can I build agents without it? You can write your own loop around tool calls and a model with plain Python, that's all an agent is under the hood. LangGraph just gives you a tested implementation of that loop plus state persistence, checkpointing, and streaming, which saves you from re-solving those problems yourself.