← Back to Agentic AI map
Lesson 2.1 · Building Agents with LangChain

Why a Framework

What LangChain gives you that Module 1’s hand-rolled loop did not - and what it still leaves to you.

framework

What you will be able to do

  • Explain what a framework adds on top of the agent you built in Module 1
  • Map each piece of Module 1’s loop to the part of LangChain that replaces it
  • Read the tool description LangChain generates from a Python function
  • Name what LangChain still leaves to you - tool errors, step limits, and a model that supports tools
  • Explain why building the agent by hand first makes the framework easier to use and debug

The idea, in plain English

In Module 1 you built every part of an agent yourself: the prompt that listed the tools, the regex that found the Action, the dictionary that ran the tool, the loop, the step limit, the error handling. That is a lot of machinery, and it grows with every tool, agent, and feature you add.

A framework such as LangChain provides that machinery as reusable parts. You hand it a model and some Python functions; it describes the functions to the model, runs the loop, calls the tools the model asks for, and feeds the results back. The calculator agent that took a page of code in Module 1 fits in about ten lines.

It does not make the model any smarter. The model still decides; your functions still do the work. LangChain is the plumbing in between - and because you built that plumbing yourself, you know what it is doing and where to look when it misbehaves.

One honest note about this lesson. LangChain agents rely on the model’s built-in tool calling, and the llama3 model installed where this lesson was written does not support it. So every behaviour shown below was checked against LangChain 1.4.3 itself, with a scripted stand-in model playing the LLM’s part. Run the ten-line agent with llama3.1, which does support tools.

Worked example: The same calculator, rebuilt in 10 lines.

workflowInside create_agent: the loop you already wrotestep 1 / 4

1 - The question goes to the model

Module 1 built the messages list and called ollama.chat(). Here LangChain does both - and adds a description of every tool, generated from your Python functions.

Module 1
ollama.chat(...)
LangChain
model node
tool list
from your docstring
steps used
1

These are the actual nodes LangChain builds for the calculator agent. Step through one question - it is Module 1’s loop with different names.

What a framework is for

Picture the agent from Module 1 grown up: ten tools, several agents, memory, structured output, retries, streaming, logging, retrieval over your documents. Every one of those is code you would write and maintain - prompt construction, parsing, tool dispatch, error handling, the loop - before you got to the part that is actually your application.

A framework packages those common pieces. You configure a model, tools, and an agent instead of writing the machinery that connects them. It is the same trade an ORM makes for SQL: the queries still happen, you just stop writing every one by hand.

What LangChain takes over

Every row below was checked against LangChain 1.4.3. The left column is what you wrote in Module 1; the right column is what replaces it.

Module 1 and LangChain, piece by piece
Tool list in SYSTEM_PROMPTGenerated from the function name, type hints, and docstring.
Regex on "Action:"Structured tool calls from the model - no parsing.
TOOLS[name](arg)The tools node looks the tool up and runs it.
"Observation: ..."Added to the conversation as a tool message.
for step in range(max_steps)The graph loops model -> tools -> model until no tool is called.
Unknown tool handling (1.10)Built in - the model is told which tools exist.

What it still leaves to you

Tool errors. A tool that raises an exception does not become an Observation by default - in our test, calculate("47 *") raised SyntaxError and ended the whole run, exactly like Lesson 1.7’s read_file(".") did. Lesson 1.10’s fix still works: catch the exception inside the tool and return the error as text, and the model sees "error: invalid syntax" and carries on.

Step limits. Module 1 stopped after max_steps=5. LangChain’s limit is recursion_limit, and its default is 10,007 - in our test 200 tool calls in a row did not reach it. Each model call and each tool run is one step, so N tool rounds need 2N + 2; Module 1’s max_steps=5 is roughly recursion_limit=10. Set it yourself.

The model. Built-in tool calling needs a model that supports it. With llama3, create_agent fails before the first step: "llama3:latest does not support tools". The framework cannot work around a model that cannot ask for a tool.

Watch out: A framework removes code, not responsibility. Limits, error handling, and the safety rules from Lesson 1.7 are still yours to set - and the defaults are not the safe choice.

Why Module 1 came first

Start a course with agent = create_agent(...) and you can have something working in minutes. Then it calls the wrong tool, or loops, or ignores a result, and every question - where is the prompt? why did it retry? what did the model actually see? - has its answer hidden inside the framework.

Having built the prompt, the parser, the loop, the observations, the memory, and the error handling yourself, you can read the framework’s behaviour in those terms. The nodes in the simulation above are not new ideas; they are the parts of run_agent() with new names.

Step-by-step code

The calculator agent in LangChain - about 10 lines
from langchain.agents import create_agent from langchain_ollama import ChatOllama def calculate(expression: str) -> str: """Evaluate an arithmetic expression such as 47 * 89.""" return str(eval(expression)) agent = create_agent(ChatOllama(model="llama3.1", temperature=0), tools=[calculate]) result = agent.invoke({"messages": [{"role": "user", "content": "What's 47 * 89?"}]}) print(result["messages"][-1].content)
What the model is told about calculate - generated, not written
{ "type": "function", "function": { "name": "calculate", "description": "Evaluate an arithmetic expression such as 47 * 89.", "parameters": { "properties": { "expression": { "type": "string" } }, "required": [ "expression" ], "type": "object" } } }
The same loop, with a scripted model standing in for the LLM
from langchain.agents import create_agent from langchain_core.language_models.fake_chat_models import GenericFakeChatModel from langchain_core.messages import AIMessage class ScriptedModel(GenericFakeChatModel): """Stands in for the LLM: replays the replies we give it, in order.""" def bind_tools(self, tools, **kwargs): return self def calculate(expression: str) -> str: """Evaluate an arithmetic expression such as 47 * 89.""" return str(eval(expression)) # What a tool-calling model would send: first a tool call, then an answer. replies = iter([ AIMessage(content="", tool_calls=[{"name": "calculate", "args": {"expression": "47 * 89"}, "id": "call_1"}]), AIMessage(content="47 * 89 = 4183"), ]) agent = create_agent(ScriptedModel(messages=replies), tools=[calculate]) result = agent.invoke({"messages": [{"role": "user", "content": "What's 47 * 89?"}]}) for message in result["messages"]: print(message.type, message.content or message.tool_calls)
Output
human What's 47 * 89? ai [{'name': 'calculate', 'args': {'expression': '47 * 89'}, 'id': 'call_1', 'type': 'tool_call'}] tool 4183 ai 47 * 89 = 4183
What LangChain handles - and what it does not - checked in LangChain 1.4.3
The model asks for a tool that does not exist: tool Error: google_search is not a valid tool, try one of [calculate]. handled A tool raises an exception - calculate("47 *"): SyntaxError: invalid syntax run ends The same tool with try/except inside it: tool error: invalid syntax (<string>, line 1) handled A model that never stops calling tools: default recursion_limit = 10007 - 200 tool calls did not reach it recursion_limit=6 -> GraphRecursionError: Recursion limit of 6 reached Running the 10-line agent with llama3: ResponseError: llama3:latest does not support tools (status code: 400)
Setting the limits yourself
def calculate(expression: str) -> str: """Evaluate an arithmetic expression such as 47 * 89.""" try: return str(eval(expression)) except Exception as e: return f"error: {e}" # Lesson 1.10: the error becomes something the model can read result = agent.invoke( {"messages": [{"role": "user", "content": "What's 47 * 89?"}]}, {"recursion_limit": 10}, # about Module 1's max_steps=5 )

Tip: Read a tool’s docstring as a prompt. It is no longer a comment for other developers - it is the only description the model gets, so write it for the model.

Watch out: calculate still uses eval here to keep the comparison short. Lesson 1.6’s safe_calculate works unchanged as a LangChain tool - use it anywhere a stranger can reach.

The pieces you will meet

ChatOllama

LangChain’s wrapper around a local Ollama model.

ChatOllama(model="llama3.1", temperature=0)
Tool

A Python function with type hints and a docstring.

def calculate(expression: str) -> str:
create_agent

Builds the model-tools loop.

create_agent(model, tools=[calculate])
invoke

Runs the agent on a conversation.

agent.invoke({"messages": [...]})
recursion_limit

The step limit - set it, the default is 10,007.

{"recursion_limit": 10}

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Run it for real

“Pull a tool-calling model with ollama pull llama3.1 and run the 10-line agent.”

Read the schema

“Change calculate’s docstring and print convert_to_openai_tool(calculate) to see what the model is told.”

Break a tool

“Ask for "47 *" with and without the try/except inside calculate.”

Find the limit

“Set recursion_limit to 2, 3, and 4 and see which ones allow one tool call and an answer.”

What usually goes wrong

Thinking LangChain is the agent

The model still decides and your functions still act. LangChain connects them.

Using a model without tool support

Built-in tool calling needs a model that offers it. llama3 fails before the first step.

✗ ChatOllama(model="llama3")
✓ ChatOllama(model="llama3.1")
Leaving tool errors uncaught

An exception in a tool ends the run by default. Catch it inside the tool and return the error as text.

✗ return str(eval(expression))
✓ try:
    return str(eval(expression))
except Exception as e:
    return f"error: {e}"
Trusting the default step limit

The default is 10,007 steps. A looping agent will run for a very long time before it stops.

✗ agent.invoke(inputs)
✓ agent.invoke(inputs, {"recursion_limit": 10})
Writing a vague docstring

The docstring is the tool description the model reads. "Does maths" helps far less than "Evaluate an arithmetic expression such as 47 * 89."

Key points

  • LangChain provides the machinery Module 1 hand-built: tool descriptions, parsing, dispatch, the loop.
  • The model still decides; your Python functions still do the work.
  • Tool descriptions come from the function name, type hints, and docstring.
  • create_agent builds the same loop: model -> tools -> model, until no tool is called.
  • Unknown tools are handled; tool exceptions are not - catch them inside the tool.
  • The default recursion_limit is 10,007 - set your own (2 per tool round, plus 2).
  • Built-in tool calling needs a model that supports tools, such as llama3.1.
  • Building it by hand first is what makes the framework readable when it misbehaves.

Quick check before you move on

Does LangChain replace the LLM?
No. LangChain orchestrates; the LLM still generates text and makes the decisions.
Who runs the tool when the model asks for one?
The framework calls your Python function - the model only requests it.
Why build the agent by hand in Module 1?
To understand the mechanics, so the framework’s abstractions are readable and debuggable.
What does a framework mainly provide?
Reusable pieces and orchestration around models, tools, prompts, agents, parsing, memory, and retrieval.
Name two things LangChain still leaves to you.
Catching tool exceptions, and setting a sensible step limit - plus choosing a model that supports tool calling.

Quiz

  1. 1.

    What replaces Module 1’s hand-written tool list in the system prompt?

  2. 2.

    What replaces the regex that found "Action:" in the model’s text?

  3. 3.

    A LangChain tool raises an exception. What happens by default, and how do you fix it?

  4. 4.

    How many recursion_limit steps does one tool call followed by an answer need?

  5. 5.

    Why does the 10-line agent fail with llama3?

Interview questions

Why use LangChain if you can build an agent yourself?

It provides reusable abstractions for prompts, models, tools, agents, output parsing, memory, and retrieval, so you maintain far less orchestration code. Understanding the underlying loop first makes those abstractions easier to reason about and debug.

What does a framework not do for you?

It does not make the model more capable, and its defaults are not your policy: you still choose the step limit, handle tool errors, restrict what tools can touch, and pick a model that supports the features you rely on.

How does LangChain know what a Python tool does?

It builds a JSON schema from the function signature and uses the docstring as the description. That schema is what the model sees, so the docstring is effectively part of the prompt.

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...