What is LangChain? Build Your First Agent in 15 Lines
LangChain is the most-used framework for building LLM apps — agents, RAG, multi-step workflows. This tutorial shows you what it actually is, the three primitives that matter, and your first working agent with one tool in under fifteen lines of Python — runnable today on Google's free Gemini tier, no credit card. Plus the same agent rewritten without LangChain for comparison, so you can see exactly what the framework is doing for you.
LangChain is the most-installed Python package in the LLM ecosystem and the most-argued-about framework in the AI builder community. If you've spent any time reading about agents, RAG pipelines, or multi-step LLM workflows, you've seen its name. You've also probably seen people complain about it. Before you can have an opinion, you need to know what it actually does.
This post is the short version: what LangChain is, the three primitives that account for most of what you'll ever use, and a working agent that uses a tool — in fifteen lines of Python you can run on your laptop in the next ten minutes.
What is LangChain, in plain English
LangChain is an orchestration framework for applications built on top of large language models. Orchestration is the operative word. The LLM SDKs from Anthropic, OpenAI, and Google give you a client.messages.create(...) call — one shot in, one response out. LangChain wraps that primitive in a set of reusable building blocks: models, tools, agents, retrievers, memory, prompts. You assemble those blocks into pipelines and the framework handles the plumbing in between.
Two things to keep in mind from the start.
It is not magic. Everything LangChain does, you could write yourself with the raw SDK and a few hundred lines of Python. The value is that you don't have to — the patterns are pre-built and tested across thousands of production apps.
It is also not always the right choice. For a single-team agent with three tools and one LLM provider, the framework can be heavier than what it abstracts. For a multi-provider, multi-tool, RAG-heavy application, the ecosystem can save weeks. Knowing the difference is the point of learning it.
This tutorial is the first half of that learning — what LangChain does. A future post on this blog will cover the second half — when the bare SDK beats it.
Install
The modern LangChain stack uses two packages together: langchain for the core abstractions, and langgraph for agent flows. Add the provider integration for the model you want to call. For this tutorial we'll use Google's Gemini Flash Lite, because it has a genuinely free tier (no credit card required) — every example here runs without paying for tokens.
pip install langchain langchain-google-genai langgraph
Grab a free API key at aistudio.google.com and set it as an environment variable:
export GOOGLE_API_KEY="..."
That's the entire setup. No config files, no credential JSON, no separate vector database. The rest of the post is one Python file.
Want to use Claude, GPT-4, or another provider instead? Swap the install package (
langchain-anthropic,langchain-openai, etc.), set that provider's API key, and change one string in theinit_chat_model(...)call below. The rest of the code is identical. That's the whole point ofinit_chat_model, and we'll come back to it in the comparison section.
Your first agent — 15 lines
Here is the whole program. Save it as agent.py and we'll walk through it line by line.
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain.agents import create_agent
@tool
def get_weather(city: str) -> str:
"""Returns the current weather for a city."""
fake = {"Zurich": "12°C, cloudy", "Berlin": "8°C, rain"}
return fake.get(city, "no data")
model = init_chat_model("google_genai:gemini-2.5-flash-lite", temperature=0)
agent = create_agent(model, tools=[get_weather])
result = agent.invoke({
"messages": [{"role": "user", "content": "What's the weather in Zurich?"}]
})
print(result["messages"][-1].content)
Run it:
python agent.py
Output:
The current weather in Zurich is 12°C and cloudy.
Fifteen lines, three imports, one fake weather function, one model call, one agent, one question, one answer. That is a working agent. The LLM decided on its own to call the tool, the framework executed it, the result went back to the LLM, and the LLM phrased the final answer for the user.
What just happened, line by line
from langchain.chat_models import init_chat_model
A factory function that returns a unified interface to any supported LLM provider. The same calling convention works whether you're hitting Claude, GPT-4, Gemini, or Mistral. Switching providers is a one-string change.
from langchain_core.tools import tool
A decorator that turns a plain Python function into something the LLM can call. It reads the function's docstring as the tool's description (which the model uses to decide when to call it) and the function's type hints as the input schema (which the model uses to format its arguments). You don't write any JSON schemas by hand.
from langchain.agents import create_agent
A helper that builds an agent graph: the model calls tools when it needs them, the framework loops until the model produces a final answer, and you don't write while True yourself. Under the hood, create_agent is powered by langgraph (LangChain's flow-orchestration sibling), but for the common case you don't import from langgraph directly — you only drop down to it when you need custom flows like branching, parallel tool execution, or human-in-the-loop steps.
@tool def get_weather(city: str) -> str:
Define one tool. The docstring matters — it's what the model reads to decide whether this tool is relevant to the user's question. Write it like you're explaining the function to a teammate.
model = init_chat_model("google_genai:gemini-2.5-flash-lite", temperature=0)
Instantiate the LLM. The "google_genai:..." prefix routes to the Google Generative AI provider; the colon-separated string is LangChain's shorthand for "use this provider with this model." temperature=0 means deterministic outputs — useful while learning, often desirable in production. To switch providers, change one string: "anthropic:claude-sonnet-4-6", "openai:gpt-4o", "mistralai:mistral-large-latest", etc. Everything else in the file stays the same.
agent = create_agent(model, tools=[get_weather])
Build the agent. Bind the model to the list of tools. The function returns a runnable graph — a state machine that knows how to call the model, route tool calls, append results, and decide when to stop. This is the part replacing your hand-written loop.
result = agent.invoke({"messages": [...]})
Run the agent. The input is a dict containing the conversation so far (in this case, just the user's question). The agent runs the full loop and returns a final state with every message it produced — tool calls, tool results, assistant turns, all in order.
print(result["messages"][-1].content)
The last message in the list is the agent's final answer to the user. Print it.
That's the whole program. No callbacks to register, no executor to configure, no prompt template to construct. The framework's job here is to hide the loop you would otherwise have to write.
The same agent without LangChain — for comparison
The fastest way to understand what LangChain just did is to see what the code looks like without it. Here is the identical agent — same weather tool, same question, same output, same Gemini Flash Lite model — written directly against Google's google-genai SDK with no framework at all.
You'll need one extra package for this version (the raw SDK is separate from LangChain's provider wrapper):
pip install google-genai
from google import genai
client = genai.Client() # reads GOOGLE_API_KEY from env
def get_weather(city: str) -> str:
"""Returns the current weather for a city."""
fake = {"Zurich": "12°C, cloudy", "Berlin": "8°C, rain"}
return fake.get(city, "no data")
tools = [{
"function_declarations": [{
"name": "get_weather",
"description": "Returns the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
}]
contents = [{"role": "user", "parts": [{"text": "What's the weather in Zurich?"}]}]
while True:
response = client.models.generate_content(
model="gemini-2.5-flash-lite",
contents=contents,
config={
"tools": tools,
"automatic_function_calling": {"disable": True},
},
)
candidate = response.candidates[0]
contents.append(candidate.content)
function_calls = [p.function_call for p in candidate.content.parts if p.function_call]
if not function_calls:
print(candidate.content.parts[0].text)
break
function_responses = []
for fc in function_calls:
result = get_weather(**dict(fc.args))
function_responses.append({
"function_response": {"name": fc.name, "response": {"result": result}}
})
contents.append({"role": "user", "parts": function_responses})
One detail worth flagging: "automatic_function_calling": {"disable": True} is deliberate. By default, google-genai also hides the agent loop — it'll call your Python function for you and return the final answer in one shot. To see the manual version (the one LangChain is wrapping), we disable that helper so the loop is explicit.
That's about thirty lines. LangChain's version was fifteen. The doubling isn't because LangChain is doing magic — it's because the framework absorbs four small chores you would otherwise handle by hand.
- The tool schema. With
@tool, the docstring became the tool description and the type hints became the JSON input schema. Without it, you write that schema dict yourself —function_declarations,parameters,properties,required. Maybe five extra lines per tool, and you need to know the provider's exact schema dialect. - The loop.
create_agentpackaged the "call model → check for tool calls → execute tools → append results → repeat" logic into a singleagent.invoke(...)call. Without it, that's awhileloop with explicit termination logic. Easy to write the first time; tedious by the tenth agent. - The state machine. LangChain's agent quietly tracks message history, tool calls, and termination. In the raw version you manage
contentsyourself, append the model's response, then append a separate user message containingfunction_responseparts — and you have to follow the provider's exact ordering rules or the next call fails. Each SDK has its own quirks here. LangChain absorbs them. - Provider portability. This one is hypothetical when both versions use Gemini, but it's the biggest reason the framework exists. To switch the LangChain version above from Gemini to Claude, you change one string:
init_chat_model("anthropic:claude-sonnet-4-6", ...). To switch the raw version from Gemini to Claude, you rewrite roughly eighty percent of the code: Anthropic's SDK has a different tool-call format (input_schemainstead ofparameters), a different response shape (stop_reasonchecks instead of scanningpartsforfunction_call), and different termination semantics. The frameworks are selling you exactly the cost of not having to do that rewrite.
So when is each one right?
- Use LangChain when you might switch providers, you want pre-built RAG components or tracing integrations, you're shipping more than two or three agents and want a consistent shape across them, or your team would rather learn one set of abstractions than every SDK's idiosyncrasies.
- Use the raw SDK when you're committed to a single provider, you want zero extra dependencies, you're writing your first agent and want to understand what's actually happening on the wire, or you have an unusual flow that doesn't fit a
create_agentshape and you'd rather write the loop than learnlanggraph's state-machine API.
Neither version is "better" in the abstract. Both produce the same answer. The framework version is shorter and the patterns reuse across many agents; the raw version is more explicit and exposes you to the SDK details — which is useful when you're learning, and a tax when you're shipping.
A future post on this blog will go deeper into the raw-SDK pattern — when it's worth the extra lines, and how to add persistence, replay, and FastAPI wiring around it. For now, having both versions side by side is the cleanest way to see what LangChain costs and what it buys.
The three primitives that matter
LangChain has dozens of classes, but most of what you'll see in real codebases is some combination of three things:
Models — provider-agnostic wrappers around chat LLMs, instantiated via init_chat_model. They expose a unified interface so the rest of your code doesn't change when you switch providers.
Tools — Python functions, decorated with @tool, that the LLM is allowed to call during an agent run. The decorator handles the schema conversion; you write normal Python.
Agents and graphs — orchestrators that run the LLM in a loop, route tool calls, accumulate state, and decide when to stop. create_agent (from langchain.agents) is the one-line shortcut for the common case. Underneath, it's built on langgraph — LangChain's lower-level flow runtime — which you reach for directly when you need custom branching, parallel tool execution, or human-in-the-loop steps.
Almost every LangChain example you'll read online is some recipe involving those three. Everything else — prompt templates, output parsers, retrievers, memory classes, document loaders — is utility code layered on top.
Why people reach for it
Four reasons explain most of LangChain's adoption.
Provider portability. With the SDK, switching from OpenAI to Anthropic to Gemini is a rewrite — different client classes, different message formats, different tool-use schemas. With LangChain, you change one string in init_chat_model. For teams that A/B test models or need a multi-provider fallback, this is real value.
Ecosystem. langchain-community ships over two hundred document loaders (PDF, Notion, S3, Confluence, you name it), fifty-plus retriever integrations (Pinecone, Weaviate, ChromaDB, pgvector, Elasticsearch, OpenSearch), and a long list of pre-built tools. If your agent needs to read PDFs from a Notion workspace and search a vector store, someone already wrote the loader.
Tracing. Wire up LangSmith and every run becomes a debuggable artifact in a hosted dashboard — full message history, tool inputs, latency per step, token counts. The integration is zero code changes; you set an env var. One thing worth knowing up front: LangSmith itself is closed-source and paid (free tier capped at 5,000 traces per month; paid plans start around $39 per user). The underlying LangChain tracing primitives — callback handlers, the LANGCHAIN_TRACING_V2 env var — are open source in langchain-core, but the polished dashboard is the part LangSmith sells. If you need an open-source alternative, Langfuse (MIT, fully self-hostable) is the closest 1:1 replacement and plugs into LangChain via the same callback handler. Arize Phoenix (Apache 2.0) is another well-regarded option, especially when you want evaluation and tracing in one tool.
Pre-built patterns. Beyond create_agent, LangChain ships pre-built helpers for common shapes — SQL agents, structured-output agents, retrieval agents, and more. The common shapes are one function call. You write business logic; you don't write loop scaffolding.
What it isn't
A few realistic caveats worth knowing before you build:
- LangChain is not lightweight. Installing it pulls in roughly fifty megabytes of transitive dependencies.
- It is not always stable across versions. The library has historically broken APIs between minor releases; pin versions in production.
- It is not the only right answer. For a single-provider, single-team agent with a handful of custom tools, writing the loop yourself in thirty lines of raw SDK code can be cleaner and more maintainable than learning the framework's idioms. That's the subject of a future post on this blog.
None of these are reasons to skip the framework — they're reasons to use it deliberately. The wrong move is to import LangChain because you saw it in a tutorial. The right move is to import it because you've decided one of the four value propositions above applies to what you're building.
Other frameworks you'll hear about
LangChain is the most-used, but it isn't the only framework in this space. Three others come up often enough that you should know what they are and how they compare.
LlamaIndex. Sister project, similar age. Originally focused on RAG and retrieval — loading documents, building indexes, querying them — and has since expanded into agents. Its out-of-the-box experience for "load PDFs, build an index, query it" is stronger than LangChain's equivalent recipe. The two libraries are interoperable, and many production stacks use both: LlamaIndex for the retrieval layer, LangChain for the orchestration. License: MIT.
CrewAI. Higher-level multi-agent framework. The abstraction unit is a crew — a team of role-based agents (researcher, writer, editor) collaborating on a task with a defined process (sequential, hierarchical, etc). Best when "team of specialized agents collaborating" maps cleanly to your problem. For single-purpose agents, it's heavier than create_agent. Worth knowing: langgraph itself now supports multi-agent flows directly, so the old argument of "you must use CrewAI for multi-agent" has weakened over the past year. License: MIT.
AutoGen. Microsoft Research's multi-agent framework, similar mental model to CrewAI but oriented around conversational agents — one agent talks to another agent until consensus or task completion. Strong research backing, but less production-ready out of the box than LangChain or CrewAI. Best for prototypes where the value is exploring agent-to-agent dialogue patterns rather than shipping a hardened product. License: MIT.
All three are open source. All three install their own dependency trees. The choice between them is less about capability — they're all Turing-complete wrappers around the same SDK calls underneath — and more about which abstraction matches the shape of your problem. If you can describe your application as "an agent with tools," start with LangChain. If it's clearly "a team of agents with roles," CrewAI is the path of least resistance. If it's "retrieve from a large corpus and answer questions about it," LlamaIndex.
What to try next
Now that the basic agent works, three small extensions teach you most of what you'd use LangChain for day to day.
Add a second tool. Define a second @tool-decorated function — a calculator, a string reverser, anything. Pass both tools to create_agent. Ask a question that requires picking between them. Watch the agent choose. This is the heart of how multi-tool agents work.
Replace the fake function with a real API. Swap the fake dict in get_weather for a call to a public weather API. The agent code does not change. The model now reads live data through your tool.
Use langgraph directly. When create_agent becomes too restrictive — for example, when you want a custom branching flow, human-in-the-loop steps, or parallel tool execution — drop down to langgraph and define the state graph yourself. The prebuilt agent is a shortcut for one common shape; the underlying library handles arbitrary flows.
What's next on this blog
This was episode-one-of-the-blog in the Lean AI Stack Explained track. The next posts in the series will cover, in order:
- What is a tool, and how an agent picks between two of them.
- What is RAG, and the smallest pipeline that counts as one.
- When LangChain is the wrong default — the critique you can only earn after the tutorial.
If you want the more opinionated content first, the Build Your Own Model Registry post applies the same "lean stack" philosophy to a different layer of the system.
LangChain is a useful tool when you use it deliberately. The fifteen lines above are the smallest deliberate use of it. Run them, change one thing, run them again — that's the whole way you ever learn a framework.