LangChain
A Scout Loop around Langchain, Agents, Agentic Workflows, Python: build evidence, then choose the next action.
Did this fit you?
What It Is
LangChain is a Python (and TypeScript) framework for building LLM agents: programs where a model receives context, decides which tool to call, the tool runs, and the loop repeats until the model is done. As of its first stable v1.0 release in late October 2025, the library is deliberately narrow. The headline abstraction is a single function, create_agent, that wraps that model-decides-then-acts loop and returns a ready-to-run agent. Everything heavier — durable state, streaming, human checkpoints — comes from the LangGraph runtime underneath it.
The concrete thing you will touch is create_agent from the langchain.agents namespace. You will hand it a model string, a tool you wrote as a plain Python function, and a system prompt, then invoke it and read back the messages. v1 also reorganized imports (langchain.agents, langchain.messages, langchain.tools, langchain.chat_models), so older tutorials that import from langchain.chains or langgraph.prebuilt.create_react_agent will not match what you build here — those legacy pieces moved to a separate langchain-classic package.
Why It Matters
You evaluate tools before you adopt them, so the question that matters is not "can LangChain call an LLM" but "what does it buy me over a raw provider SDK, and where does it get in the way." v1 is a clean place to answer that. The agent loop is one function call, model selection is a provider-prefixed string so swapping anthropic: for openai: is a one-line change, and the production concerns you would otherwise hand-roll — structured/typed output, retries, summarization of long histories, approval gates before risky tool calls, PII handling — are exposed as composable middleware hooks rather than framework forks.
That design is what makes a 40-minute read-through worthwhile: you can see the abstraction boundary directly. create_agent covers the common agent shape; when you need fine-grained orchestration, durable persistence across long workflows, or deterministic-plus-agentic hybrids, you drop to LangGraph — the same runtime, no rewrite. By the end you will have enough evidence to decide whether that boundary sits where you want it for your own systems.
Guided First Play
You will build a working LangChain v1 agent from nothing, then push on it three ways an evaluating engineer cares about: typed output, a middleware approval gate, and one-line tracing. Each move ends with something you can verify so you are reading evidence, not vibes.
Get a clean workspace and the API key in place
Work in a fresh virtual environment so the v1 namespaces are not shadowed by an old install. Install only the agent harness plus one provider package — this loop uses Anthropic, but any provider works; the only line that changes later is the model string. Then export the matching API key for the provider you chose.
Fresh environment + install
python -m venv .venv && source .venv/bin/activate
pip install -U langchain langchain-anthropic
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY if you swap providersRun pip show langchain and confirm the Version line reads 1.x. If it shows 0.x, you are in an environment with the legacy package and the imports below will not resolve.
Make one real move: an agent with a tool
Define a tool as an ordinary function decorated with @tool — its docstring is what the model reads to decide when to call it. Then pass create_agent a provider-prefixed model string, your tool, and a system prompt. The model string ("anthropic:claude-sonnet-4-6" here) is resolved under the hood by init_chat_model; substitute any model you have access to, keeping the provider: prefix.
agent.py · Path: agent.py
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def word_count(text: str) -> int:
"""Return the number of whitespace-separated words in text."""
return len(text.split())
agent = create_agent(
model="anthropic:claude-sonnet-4-6",
tools=[word_count],
system_prompt="You are a precise assistant. Use tools when they make the answer exact.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "How many words are in: the quick brown fox jumps"}]}
)
for message in result["messages"]:
message.pretty_print()Run python agent.py. In the printed transcript you should see an assistant turn that issues a word_count tool call, a tool message returning 5, and a final assistant message stating five words. If the model answers "5" with no tool call in between, the loop still worked but the model shortcut the tool — tighten the system prompt to require the tool and rerun.
What just changed: you did not write a prompt-parsing loop, a tool dispatcher, or a JSON schema for the tool call. create_agent built the agent loop, inferred the tool schema from your function signature and docstring, and ran model → tool → model until there were no more tool calls. That is the whole core abstraction. Everything after this is shaping it.
Variation one: make the output typed, not prose
Production code rarely wants a chat string. Pass response_format=<a Pydantic model> and the validated instance comes back at result["structured_response"]. In v1 this is folded into the main agent loop rather than tacked on as an extra LLM call, which is the kind of detail that matters when you are pricing out latency and tokens.
Typed result
from pydantic import BaseModel
class Count(BaseModel):
word_count: int
sentence: str
agent = create_agent(
model="anthropic:claude-sonnet-4-6",
tools=[word_count],
response_format=Count,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Count the words in: ship it on Friday"}]}
)
print(type(result["structured_response"]), result["structured_response"])You should see a Count instance, e.g. word_count=4 sentence='ship it on Friday'. Confirm it is a typed object, not a dict or a string — that means validation ran and you can hand it straight to downstream code.
Variation two: add a middleware hook
Middleware is how v1 wants you to customize behavior without forking the loop. Hooks (before_model, wrap_tool_call, after_model, and siblings) run inside the same compiled LangGraph create_agent returns. Add a built-in one — HumanInTheLoopMiddleware — to gate a named tool behind an approval interrupt. This is the production-relevant move: the same mechanism is how you attach summarization, retries, model fallback, or PII redaction.
Approval gate via middleware
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="anthropic:claude-sonnet-4-6",
tools=[word_count],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"word_count": True})],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "demo-1"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Count words in: pause before you act"}]},
config=config,
)
print(result.get("__interrupt__", "no interrupt — tool ran straight through"))Instead of a finished answer, the run should surface an interrupt describing the pending word_count call awaiting approval. That paused state is the evidence: an approval gate dropped in as one list entry, no graph rewrite. (Interrupts require a checkpointer and a thread_id, which is why both are present.)
If you see a final answer instead of an interrupt, the middleware import path or the interrupt_on tool name did not match. Confirm the imported tool's name is exactly 'word_count', that interrupt_on uses that same key, and that both checkpointer=InMemorySaver() and config={'configurable': {'thread_id': '...'}} are set — interrupts need durable state to pause against. Middleware lives under langchain.agents.middleware; if that import fails, your install predates v1.
Verify what the agent actually did: turn on tracing
For a tool you are evaluating, observability is part of the verdict. LangSmith tracing is a single environment toggle plus a key — set them before running any of the scripts above and every model call, tool call, and middleware hook shows up as a structured trace you can inspect.
One-env-var observability
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_... # from smith.langchain.com
python agent.pyRe-run agent.py, then open your LangSmith project. You should see a trace tree for the run with the model call and the word_count tool call as nested spans. Seeing the tool span confirms the agent loop executed end to end and gives you the latency and token breakdown to judge overhead.
You now have enough to decide. You built the core loop, made it return typed data, gated a tool behind an approval interrupt, and traced the whole thing — the exact surface you would lean on in production. The open question for your own work is the abstraction boundary: create_agent covers the standard agent shape, and when you need fine-grained orchestration, durable long-running state, or deterministic-plus-agentic hybrids, you drop to the LangGraph runtime underneath without a rewrite. Whether that line sits where your systems need it is the call to make next.
Your Next Move
You have a running v1 agent and a feel for where its abstraction stops. Does this fit how you want to build and evaluate agents?
Go Deeper would spend two hours past hello-world: composing several middleware (summarization for long histories, retry and model-fallback, PII), wiring real conversation memory across thread_ids, and dropping a create_agent agent into a larger LangGraph as a node to see hooks still fire. Project would turn this into a scoped build — a specific agent in your stack with provider swap, typed outputs, approval gates, and LangSmith evals as an adoption decision you could defend.
Did this fit you?
Sources
Grounded in LangChain's official v1 documentation (the v1 release notes, agent and middleware guides, quickstart, and ecosystem/LangGraph overviews), the v1.0 announcement, and the 1.1 changelog. Code shapes — create_agent, the @tool decorator, provider-prefixed model strings, response_format, HumanInTheLoopMiddleware, and the LANGSMITH_TRACING toggle — follow those primary sources. Model identifiers are illustrative; substitute one your account can call. Full links are in this loop's source list.