Agent Frameworks
LangChain
LangChain began as a library of "chains" and has become something more useful and less opinionated: a component library plus a composition syntax.
Jr Codex Agentic AI Notes
Level: Intermediate Prerequisites: Chapter 1: Why Use a Framework Time to complete: ~25 minutes
Table of Contents
- What LangChain Is Now
- Runnables — the Core Abstraction
- LCEL and Composition
- Defining Tools
- Building an Agent
- Seeing What Is Actually Sent
- Strengths and Sharp Edges
- Summary & Next Steps
1. What LangChain Is Now
LangChain began as a library of "chains" and has become something more useful and less opinionated: a component library plus a composition syntax.
The Modern Split
─────────────────────────────────────────
langchain-core the Runnable interface,
message types, prompt
templates. Small and stable.
langchain-openai, model providers, one package
langchain-anthropic each
langchain-community the vast integration
catalogue — loaders, vector
stores, tools
langgraph control flow for agents —
Chapter 3. Separate library,
and where agent orchestration
now lives.
─────────────────────────────────────────
The Important Consequence
─────────────────────────────────────────
The old `AgentExecutor` is legacy. New agent work
goes to LangGraph.
So the right way to read LangChain today is as
Chapter 1's "borrow the components" — take the
loaders, models and tools; get your control flow
from LangGraph or from your own loop.
─────────────────────────────────────────
2. Runnables — the Core Abstraction
Everything in LangChain implements one interface. Learning it is most of learning the library.
The Runnable Interface
─────────────────────────────────────────
invoke(input) one input ──► one output
batch(inputs) many, in parallel
stream(input) yields chunks as produced
ainvoke / abatch async variants
Models, prompts, parsers, retrievers, tools and
whole chains are ALL Runnables. That uniformity is
the point — anything composes with anything.
─────────────────────────────────────────
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You explain concepts in exactly two sentences."),
("human", "{question}"),
])
parser = StrOutputParser()
print(prompt.invoke({"question": "What is a vector database?"})) # a prompt value
print(model.invoke("Hello")) # an AIMessage3. LCEL and Composition
LangChain Expression Language composes Runnables with the | operator.
chain = prompt | model | parser # each step's output feeds the next
chain.invoke({"question": "What is a vector database?"})
chain.batch([{"question": q} for q in questions]) # PARALLEL, automatically
for chunk in chain.stream({"question": "..."}): # streaming through the whole chain
print(chunk, end="", flush=True)from langchain_core.runnables import RunnableParallel, RunnablePassthrough
# Run several things at once, then combine — a fan-out/fan-in shape.
rag = (
RunnableParallel(
context=retriever | format_docs, # these two branches run CONCURRENTLY
question=RunnablePassthrough(),
)
| prompt
| model
| parser
)What LCEL Actually Buys
─────────────────────────────────────────
Not brevity — the same pipeline in plain Python is
a similar length.
What you get for free from composing Runnables:
- batch() parallelises automatically
- stream() streams end to end
- async variants exist without extra code
- every step is traced (Module 7, Chapter 3)
- .with_retry() and .with_fallbacks() attach to
any step
Those last three are the real reasons to use it.
─────────────────────────────────────────
robust = (
model.with_retry(stop_after_attempt=3) # backoff, built in
.with_fallbacks([ChatOpenAI(model="gpt-4o-mini")]) # degrade, do not fail
)4. Defining Tools
from langchain_core.tools import tool
from pydantic import BaseModel, Field
class OrderLookup(BaseModel):
customer_id: str = Field(description="Customer id, e.g. 'cust_4821'")
since: str | None = Field(default=None, description="ISO date YYYY-MM-DD")
@tool(args_schema=OrderLookup)
def list_orders(customer_id: str, since: str | None = None) -> dict:
"""List a customer's orders, most recent first.
USE FOR: order history, purchase questions, refund eligibility.
DO NOT USE FOR: customer contact details — use get_customer instead.
Returns: up to 20 orders with id, date, total, status. Read-only, ~80ms.
"""
rows = db.orders.find({"customer": customer_id, "date": {"$gte": since}})
return {"orders": [slim(r) for r in rows[:20]],
"total_matching": len(rows), "truncated": len(rows) > 20}Everything From Module 2 Applies Unchanged
─────────────────────────────────────────
The docstring becomes the tool description sent to
the model — so "use for", "do not use for",
returns and cost all belong there.
The Pydantic schema becomes the parameter schema,
so Field descriptions are prompt text.
The framework changed the syntax. It did not
change what makes a tool good.
─────────────────────────────────────────
5. Building an Agent
The current recommended path uses LangGraph's prebuilt agent, which Chapter 3 unpacks.
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_react_agent(
model=ChatOpenAI(model="gpt-4o", temperature=0),
tools=[list_orders, get_customer, issue_refund],
prompt=AGENT_SYSTEM, # your Module 2, Chapter 2 policy
checkpointer=MemorySaver(), # state persists across invocations
)
config = {"configurable": {"thread_id": "ticket-8241"}} # the conversation identity
result = agent.invoke(
{"messages": [("user", "Has cust_4821 had any failed orders this month?")]},
config=config,
)
print(result["messages"][-1].content)
# A LATER call with the same thread_id continues the same conversation —
# the checkpointer reloads prior state automatically.
agent.invoke({"messages": [("user", "Refund the most recent one.")]}, config=config)# Stream the trajectory rather than waiting for the final answer.
for chunk in agent.stream({"messages": [("user", question)]}, config=config):
for node, update in chunk.items():
print(f"[{node}] {update['messages'][-1]}") # see each step as it happensNote What thread_id Does
─────────────────────────────────────────
It is the memory key. Same thread_id = same
conversation state, reloaded from the
checkpointer.
This is Module 3's short-term memory, handled for
you — and Module 6, Chapter 2's durable state when
you swap MemorySaver for a database-backed
checkpointer.
─────────────────────────────────────────
6. Seeing What Is Actually Sent
Chapter 1 warned about hidden prompts. This is how you look.
from langchain_core.globals import set_debug
set_debug(True) # full payloads for every call, to stdout
# More surgically — a callback that captures the exact prompts:
from langchain_core.callbacks import BaseCallbackHandler
class ShowPrompts(BaseCallbackHandler):
def on_chat_model_start(self, serialized, messages, **kwargs):
for msg in messages[0]:
print(f"--- {msg.type} ---\n{msg.content}\n")
agent.invoke(payload, config={**config, "callbacks": [ShowPrompts()]})Do This Before You Debug Anything Else
─────────────────────────────────────────
Most "the model is ignoring my instructions" bugs
are "my instructions are not in the payload" bugs.
Print the payload first. It costs one minute and
resolves a large fraction of framework confusion.
─────────────────────────────────────────
7. Strengths and Sharp Edges
STRENGTHS
─────────────────────────────────────────
The integration catalogue is genuinely unmatched —
document loaders, vector stores, tool wrappers,
model providers.
LCEL composition gives batching, streaming, async,
retries and fallbacks uniformly.
Tracing is first class through LangSmith
(Module 7, Chapter 3).
SHARP EDGES
─────────────────────────────────────────
API CHURN the ecosystem has reorganised
repeatedly. Tutorials date
quickly. PIN VERSIONS.
DEEP STACKS an error inside a composed
chain produces a long,
framework-heavy traceback.
ABSTRACTION FOG it is easy to build something
working without understanding
what it does — which is fine
until it misbehaves.
LEGACY SURFACE much documentation still shows
deprecated chains and
AgentExecutor. Check dates.
─────────────────────────────────────────
8. Summary & Next Steps
Key Takeaways
- Modern LangChain is a component library plus a composition syntax; agent control flow has moved to LangGraph, and legacy
AgentExecutormaterial should be skipped. - Everything implements the Runnable interface, which is why anything composes with anything and why batch, stream, async, retry and fallback come free.
- Tool quality is unchanged by the framework — the docstring is still the description, and every Module 2 rule applies verbatim.
- Learn to print the actual payload before debugging behaviour; most "it ignores my prompt" problems are "my prompt is not in the payload."
Concept Check
- What does composing with LCEL give you that writing the same pipeline in plain Python does not?
- What does
thread_idcontrol, and which module's concept does it implement? - An agent ignores a rule that is clearly in your system prompt. What is the first diagnostic step and why?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index