Agentic AI

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.

JrCodex·7 min read

Jr Codex Agentic AI Notes

Level: Intermediate Prerequisites: Chapter 1: Why Use a Framework Time to complete: ~25 minutes


Table of Contents

  1. What LangChain Is Now
  2. Runnables — the Core Abstraction
  3. LCEL and Composition
  4. Defining Tools
  5. Building an Agent
  6. Seeing What Is Actually Sent
  7. Strengths and Sharp Edges
  8. 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 AIMessage

3. 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 happens
Note 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 AgentExecutor material 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

  1. What does composing with LCEL give you that writing the same pipeline in plain Python does not?
  2. What does thread_id control, and which module's concept does it implement?
  3. An agent ignores a rule that is clearly in your system prompt. What is the first diagnostic step and why?

Next Chapter

Chapter 3: LangGraph


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index