AI EngineeringZero to ProductionHome·About·Contact
LangChain & LangGraph · Chapter L2

LangChain Core — Chains, Memory & RAG

LangChain's core idea is small: everything is a Runnable, and you pipe Runnables together with |. Learn that one abstraction and prompts, models, parsers, retrievers, and memory all compose the same way. This chapter builds a chain, adds memory, then rebuilds Chapter 3's RAG in a few lines.

⏱️ ~65 min🧪 4 labs🎯 Intermediate
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • an Anthropic API key (ANTHROPIC_API_KEY) + pip install anthropic
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain the Runnable interface and LCEL (the | pipe).
  • Compose prompt → model → output-parser into a chain.
  • Get typed output with structured-output parsers (Pydantic).
  • Add conversation memory / history to a chain.
  • Build a RAG chain with a retriever and see it as Chapter 3, composed.
Versions move fast — learn the shapes, verify the importsLangChain's package layout and class names change across versions (e.g. langchain-anthropic, langchain-core, LCEL vs legacy Chain classes). The concepts here are stable; treat exact import paths as illustrative and check them against the version you install.

The one idea: Runnables + LCEL essential

A Runnable is anything with an .invoke(input) method (also .stream(), .batch(), and async variants). Prompts, models, parsers, retrievers — all Runnables. LCEL (LangChain Expression Language) lets you connect them with the | operator: the output of the left becomes the input of the right, exactly like a Unix pipe.

Prompt | Model | Output parser chain = prompt | model | parser  →  chain.invoke({...}) each box is a Runnable; the pipe wires output → input Compose, don't wire by hand. Where C2 had you build the request, call the API, and parse the reply as separate steps, LCEL makes that one composable object you can invoke, stream, or batch — and swap any piece without touching the rest.
🗺️ How to read this diagram

This is the single idea the whole chapter rests on. LangChain calls every building block a Runnable — a thing you can hand an input and get an output back. The | (pipe) symbol wires them in a line: whatever the left box produces is fed straight into the box on its right.

  • Read it left to right. The Prompt box turns your inputs (like a topic) into a proper message for the model. The Model box is the LLM that writes an answer. The Output parser box tidies that answer into a plain string.
  • Each | between the boxes is the pipe. It means "take the output of the box on the left and use it as the input to the box on the right" — exactly like a Unix pipe joining commands.
  • The line chain = prompt | model | parser builds the whole assembly once into a single object. chain.invoke({...}) then runs the input through all three boxes in order and hands you the final result.
  • Because each box is an independent Runnable, you can swap one (a different model, a different parser) without rewiring the others.

In short: A chain is just boxes joined by pipes: input goes in the left, flows through each box, and the finished answer comes out the right. Everything else in this chapter is variations on this one picture.

Lab L2.1 · Your first chain essential

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab L2.1
shellpip install langchain langchain-core langchain-anthropic
first_chain.pyfrom langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatAnthropic(model="claude-opus-4-8", max_tokens=512)
prompt = ChatPromptTemplate.from_template(
    "Explain {topic} to a {audience} in two sentences."
)

chain = prompt | model | StrOutputParser()   # LCEL: three Runnables piped

print(chain.invoke({"topic": "vector databases", "audience": "new engineer"}))
▶ How this works

This is your first complete chain. In plain Anthropic code (Chapter 2) you built the request, called the API, then dug the text out of the reply as three separate steps. Here those steps become three Runnables joined by pipes — one object you can run in a single line.

  1. The three import lines bring in the three pieces: ChatAnthropic (the model), ChatPromptTemplate (turns your inputs into a message), and StrOutputParser (pulls the plain text out of the model's reply).
  2. ChatPromptTemplate.from_template("Explain {topic} to a {audience}...") makes a reusable prompt with blanks. The {topic} and {audience} in curly braces are placeholders you fill in when you run the chain.
  3. chain = prompt | model | StrOutputParser() is the key line. The | pipes wire the three together: your inputs → filled-in prompt → model reply → plain string.
  4. chain.invoke({"topic": ..., "audience": ...}) runs it. The dictionary fills the two blanks, and the chain returns a finished sentence as a string you can print.

What the output means: You get a two-sentence explanation of vector databases aimed at a new engineer — returned as a plain string, because StrOutputParser stripped away the message wrapper the model normally returns.

Try this: Change "vector databases" to "recursion" and "new engineer" to "5-year-old", then re-run. Same chain, brand new answer — you only changed the inputs, not the wiring.

What each piece doesChatPromptTemplate turns your dict into messages. ChatAnthropic is the model Runnable (wrapping the same claude-opus-4-8 from C2). StrOutputParser pulls the plain text out of the response object so the chain returns a string, not a message. Swap any one without touching the others.

Lab L2.2 · Structured output essential

Free text is a liability once code consumes it (Chapter 2). LangChain gives you a typed path: bind a schema to the model and it returns a validated object.

Lab L2.2
structured.pyfrom pydantic import BaseModel, Field
from typing import Literal
from langchain_anthropic import ChatAnthropic

class Ticket(BaseModel):
    category: Literal["bug","feature","billing","other"]
    priority: Literal["low","medium","high"]
    summary: str = Field(description="one-line summary")

model = ChatAnthropic(model="claude-opus-4-8", max_tokens=512)
structured = model.with_structured_output(Ticket)   # returns a Ticket instance

t = structured.invoke("I was charged twice and support hasn't replied in 3 days!")
print(t.category, t.priority)   # billing high
▶ How this works

Sometimes you don't want a paragraph — you want clean, labelled data your code can act on. This lab makes the model fill in a fixed form (a support ticket) and hands you back a real Python object with typed fields, not free text you'd have to parse by hand.

  1. class Ticket(BaseModel) uses Pydantic to describe the exact shape you want back. Literal["bug","feature",...] means that field must be one of those listed words — the model isn't allowed to invent a different value.
  2. model.with_structured_output(Ticket) wraps the model so that, instead of prose, it returns something matching your Ticket form. This one call is doing the heavy work.
  3. structured.invoke("I was charged twice...") sends the complaint. The model reads it, decides the category, priority, and summary, and returns a filled-in Ticket.
  4. t.category and t.priority read the fields off that object like any normal Python attribute — no string-slicing, no guessing.

What the output means: Prints billing high — the model classified the double-charge complaint as a billing issue of high priority, delivered as typed data you could store or branch on directly.

Try this: Feed it a different message like "The dark-mode button is missing" and print t.summary too. Watch the category flip to feature or bug while the shape of the result stays identical.

Same guarantee as C2's messages.parse()with_structured_output is LangChain's wrapper over the provider's structured-output feature. The Literal types become an enum the model must satisfy — no regex, no surprise values. It's Chapter 2's lesson with a framework interface on top.

Lab L2.3 · Adding memory intermediate

The API is stateless (C2) — every call resends the history. LangChain wraps that pattern so a chain "remembers" a conversation by session id, without you managing the list by hand.

Lab L2.3
memory_chain.pyfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_anthropic import ChatAnthropic

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a concise assistant."),
    MessagesPlaceholder("history"),        # prior turns slot in here
    ("human", "{input}"),
])
chain = prompt | ChatAnthropic(model="claude-opus-4-8", max_tokens=512)

store = {}
def get_history(session_id):
    return store.setdefault(session_id, InMemoryChatMessageHistory())

chat = RunnableWithMessageHistory(chain, get_history,
    input_messages_key="input", history_messages_key="history")

cfg = {"configurable": {"session_id": "user-1"}}
chat.invoke({"input": "My name is Alice."}, config=cfg)
print(chat.invoke({"input": "What's my name?"}, config=cfg))  # "Alice"
▶ How this works

The model has no memory — each call is independent, so on its own it can't recall your name from an earlier message. This lab wraps a chain so it automatically remembers a conversation, tracked by a session_id, without you juggling the message list yourself.

  1. The prompt is built from three parts: a fixed system instruction, a MessagesPlaceholder("history") — an empty slot where past turns get inserted — and the new {input} from the user.
  2. store = {} plus get_history keep one history object per session id. setdefault creates a fresh empty history the first time it sees a new session, then reuses it after that.
  3. RunnableWithMessageHistory(chain, get_history, ...) is the wrapper that does the magic: before each call it drops the saved history into the placeholder, and after each call it appends the new exchange back into the store.
  4. The two chat.invoke(...) calls share the same cfg (session user-1). The first tells it "My name is Alice"; the second asks "What's my name?" — and because the first turn was remembered, it can answer.

What the output means: Prints Alice. The second question had no name in it, but the remembered first turn was resent along with it, so the model had the context to answer.

Try this: Add a third call asking "And what did I say first?" with the same cfg. Then change the session id to "user-2" and ask the name again — the fresh session won't know it, proving memory is per-session.

Memory is just managed history — mind the window & costInMemoryChatMessageHistory is fine for a demo but lost on restart and unbounded — every turn resends the full history, growing tokens and cost (C2). For production, back it with a real store and trim/summarize old turns. Memory isn't magic; it's the resend-the-list pattern with a nicer interface. For durable, checkpointed state you'll want LangGraph (L5).

Lab L2.4 · RAG, composed intermediate

Chapter 3 built RAG from scratch: chunk, embed, store, retrieve, generate. LangChain gives you each stage as a component, and LCEL wires them into one chain.

Retriever question Prompt Model retrieved context + question → grounded answer Chapter 3, as a chain. The retriever fetches relevant chunks; a prompt template stitches context + question; the model answers grounded in the context. Same pipeline, same failure modes (bad chunking, bad top-k) — assembled from components.
🗺️ How to read this diagram

This shows RAG (Retrieval-Augmented Generation) drawn as a chain. RAG means: before the model answers, fetch relevant reference text and hand it to the model so the answer is grounded in your documents instead of the model's memory. Chapter 3 built this by hand; here it's the same idea as connected boxes.

  • There are two inputs on the left that run at the same time: the Retriever (which searches your documents for chunks related to the question) and the question itself (passed through unchanged).
  • Both arrows feed into the Prompt box, which stitches them together: "here is the context I found, and here is the question — answer using only this."
  • The Model box then writes the answer, but constrained to the fetched context. The final arrow off the right is that grounded answer coming out.
  • The caption's point: this is the same pipeline as building RAG from scratch, so it has the same weak spots — if the retriever fetches the wrong chunks, the answer suffers no matter how good the model is.

In short: RAG in one line: look things up first, then answer using what you found. The retriever supplies the facts; the model just phrases them.

Lab L2.4

Requires: pip install langchain-anthropic langchain-core

rag_chain.pyfrom langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_anthropic import ChatAnthropic

# retriever = vectorstore.as_retriever()  # built from your embedded docs (Ch 3 / A7)

prompt = ChatPromptTemplate.from_template(
    "Answer using ONLY this context. If it's not there, say you don't know.\n\n"
    "Context:\n{context}\n\nQuestion: {question}"
)
model = ChatAnthropic(model="claude-opus-4-8", max_tokens=512)

def format_docs(docs): return "\n\n".join(d.page_content for d in docs)

rag = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt | model | StrOutputParser()
)
print(rag.invoke("What is our refund window?"))
▶ How this works

This builds the RAG diagram in code. It's the fullest chain in the chapter because it does two things at once — look up context and keep the question — before prompting the model. Read it bottom-up: the rag = (...) block is where the whole assembly comes together.

  1. The commented retriever = vectorstore.as_retriever() line is where your document search would come from (built in Chapter 3 / A7). It's commented out so the snippet stays focused on the wiring.
  2. The prompt template has two blanks, {context} and {question}, and firmly instructs the model to answer only from the supplied context — the guardrail that stops it from making things up.
  3. format_docs(docs) is a small helper: the retriever returns a list of document objects, and this glues their text together into one string the prompt can drop into {context}.
  4. The rag = ({"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | model | StrOutputParser()) block is the chain. The opening dictionary runs two branches in parallel — one fetches and formats context, the other (RunnablePassthrough) just forwards the question — then pipes the pair through prompt → model → parser.

What the output means: rag.invoke("What is our refund window?") returns a plain-text answer built only from the retrieved context — or an honest "I don't know" if the context doesn't cover it.

Try this: Notice the dictionary at the start: that's how LCEL runs steps side by side, not just in a line. Try imagining a third key, e.g. "user", added to that dict — it would flow through to the prompt too.

The dict is a parallel RunnableThat opening {"context": ..., "question": ...} runs both branches and passes a dict downstream — the retriever fetches+formats context while RunnablePassthrough forwards the raw question. LCEL composes parallel steps as easily as sequential ones. Everything you learned in Chapter 3 about chunking and retrieval quality still governs the result.

The LangChain ecosystem, briefly intermediate

PieceWhat it is
langchain-coreThe Runnable/LCEL primitives and base interfaces
langchain-anthropic etc.Provider integrations — swap models by swapping the package
Retrievers / vector storesThe RAG components (Chapter 3, A7)
LangGraphStateful, cyclic agent graphs — L4/L5 of this module
LangSmithTracing & eval across chains/agents (later module)
Chains are for known paths; agents/graphs are for decisionsA chain is a fixed pipeline — great when you know the steps (the "workflow" from L1). When the path must be decided at runtime (which tool? loop again?), you need an agent (L3) or a LangGraph graph (L4). Pick the simplest that fits.

Common pitfalls advanced

PitfallFix
Copying import paths from old tutorialsVerify against your installed version; packages get reorganized
Unbounded in-memory historyTrim/summarize; back with a real store for production
Parsing free text downstreamUse with_structured_output for typed results
Blaming LangChain for bad RAG answersIt's usually chunking/top-k — same tuning as Chapter 3
Using a chain where you need runtime decisionsMove to an agent (L3) or LangGraph (L4)
Over-abstracting a one-off scriptFor a single call, the raw SDK (C2) is simpler

Exercises advanced

Exercise L2.1 — Swap the parser

Context: The composability payoff is that swapping a chain's tail is a one-line change.

Your task: Take a string-output chain and replace the StrOutputParser with a structured parser so the chain returns a typed object — confirm only one line changed.

Requirements:

  • Start from a working chain ending in a string parser
  • Swap in a structured/typed parser
  • The chain now returns a typed object instead of a string
  • Verify the rest of the chain was untouched

💡 Hint: Only the final Runnable in the pipe changes — that's the whole point of composition.

Exercise L2.2 — Cited RAG

Context: Grounding gets stronger when the chain also returns which documents it used — and comparing that to a from-scratch version shows the framework earning its keep.

Your task: Extend the RAG chain to also return which documents it relied on, by tagging each chunk with its source and asking the model to cite the tag.

Requirements:

  • Prefix each chunk with a source tag in the document-formatting step
  • Instruct the prompt to cite the source tag it used
  • Return (or surface) the sources alongside the answer
  • Compare grounding quality to a from-scratch RAG

💡 Hint: Add [source: filename] in format_docs and ask the model to cite it; for true structured citations, return a model with an answer plus a sources list.

Show hint

Prefix each chunk with [source: filename] in format_docs, and add "cite the source tag" to the prompt. For true structured citations, return a Pydantic model with an answer and a sources list.

Exercise L2.3 — Chain vs raw

Context: Knowing when the framework wins and when the raw SDK is clearer is the judgment the chapter is really teaching.

Your task: Rebuild a simple LCEL chain using only the raw Anthropic SDK, then write two sentences on which is shorter and which makes swapping the model easier.

Requirements:

  • Reproduce the chain's behavior with the raw SDK only
  • Compare line count against the LCEL version
  • Compare how easy it is to swap the model in each
  • State when each approach wins

💡 Hint: The raw SDK is often shorter for one call; the framework wins as soon as you want to swap parts or reuse the structure.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Model the LCEL pipe with __or__Beginner

Context: LangChain wires Runnables with the | operator: the left's output feeds the right. Under the hood, LCEL's pipe is just function composition.

Your task: Model a tiny Runnable class whose | composes callables, exactly like LCEL.

Requirements:

  • Wrap a callable and expose an invoke(x) method
  • Implement __or__ so a | b feeds a's output into b
  • Composing prompt | model | parser produces a single runnable
  • Show chain.invoke(...) running the whole pipe

💡 Hint: __or__ returns a new Runnable whose function calls self then the other — that composition is the pipe.

Show solution

LCEL's pipe is function composition. Model it in stdlib:

class Runnable:
    def __init__(self, fn):
        self.fn = fn
    def invoke(self, x):
        return self.fn(x)
    def __or__(self, other):        # self | other
        return Runnable(lambda x: other.invoke(self.invoke(x)))

prompt = Runnable(lambda topic: f"Write about {topic}.")
model  = Runnable(lambda p: f"[answer to] {p}")
parser = Runnable(lambda a: a.upper())

chain = prompt | model | parser
print(chain.invoke("cats"))   # [ANSWER TO] WRITE ABOUT CATS.

Everything in LangChain Core is a Runnable, and | is just "feed left's output into right" — this class is the whole idea.

Exercise 2 · Structured output parsing (Pydantic-style)Intermediate

Context: Chains often end in a parser that turns free model text into a typed, validated object downstream code can trust — the role of PydanticOutputParser in real LCEL.

Your task: Model a parser that extracts fields from a text reply into a validated object, raising on a bad shape.

Requirements:

  • Parse specific fields out of the reply text
  • Coerce into a typed object (e.g. a dataclass with the right field types)
  • Raise a clear error when the text doesn't match the expected shape
  • Show it succeeding on good text

💡 Hint: A regex to pull the fields plus a dataclass to hold them is enough; the job is coercing untrusted text into a trusted, typed value.

Show solution

The parser is the last Runnable in the chain. Model it with stdlib:

import re
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

def parse_person(text):
    m = re.search(r"name=(\w+)\s+age=(\d+)", text)
    if not m:
        raise ValueError(f"cannot parse: {text!r}")
    return Person(name=m.group(1), age=int(m.group(2)))

print(parse_person("name=Ada age=36"))   # Person(name='Ada', age=36)

Real LCEL uses PydanticOutputParser; the job is identical — coerce free text into a typed, validated object so downstream code can trust it (needs langchain installed).

Exercise 3 · Conversation memory as a Runnable stepAdvanced

Context: Memory is state threaded into the prompt: load prior turns, prepend them, save the new turn — the mechanic behind LangChain's message-history Runnables.

Your task: Model a memory object that stores history and a chat step that injects it, so the model appears to remember across invocations.

Requirements:

  • A memory object stores turns and can render them as history
  • The chat step prepends the loaded history to the prompt
  • After replying, the new user/AI turn is saved
  • Demonstrate a follow-up that depends on an earlier turn

💡 Hint: Three moves per turn: load history, prepend it to the prompt, save the new exchange — the model stays stateless while the memory carries the state.

Show solution

Memory is state threaded into the prompt. Runnable stdlib:

class Memory:
    def __init__(self):
        self.turns = []
    def load(self):
        return "\n".join(f"{r}: {t}" for r, t in self.turns)
    def save(self, user, ai):
        self.turns += [("user", user), ("ai", ai)]

mem = Memory()
def chat(user_msg):
    prompt = f"History:\n{mem.load()}\nUser: {user_msg}"
    reply = f"(reply to '{user_msg}')"      # stand-in for the model
    mem.save(user_msg, reply)
    return reply

chat("my name is Ada")
print(chat("what's my name?"))
print("---history---"); print(mem.load())

LangChain wraps this as message-history Runnables; the mechanic is: load history, prepend it, save the new turn.

Exercise 4 · A RAG chain: retrieve -> stuff -> answerExpert

Context: RAG is just another chain — retriever | prompt | model — which is why Chapter 3's from-scratch pipeline collapses into a few composed Runnables.

Your task: Rebuild RAG as a composed chain: a retriever returns top-k chunks, they're stuffed into the prompt, then the model answers. Model it offline end-to-end.

Requirements:

  • A retriever returns the top-k most relevant chunks for a query
  • The retrieved chunks are stuffed into the prompt as context
  • A (stand-in) model answers from that context
  • Wire it so the retriever could be swapped for a real vector store without touching the rest
  • Show a query returning a grounded answer

💡 Hint: Keep retrieve / prompt-build / answer as separate composable steps — the retriever is the only piece that changes when you move to a real store.

Show solution

RAG is just another chain: retriever | prompt | model. Runnable stdlib:

DOCS = [
    "Refunds are processed within 30 days.",
    "Support hours are 9am to 5pm.",
    "Passwords reset under Settings > Security.",
]
def retrieve(query, k=1):
    # toy relevance: overlap of query words with the doc
    q = set(query.lower().split())
    scored = sorted(DOCS, key=lambda d: len(q & set(d.lower().split())), reverse=True)
    return scored[:k]

def rag(query):
    ctx = "\n".join(retrieve(query))
    prompt = f"Context:\n{ctx}\nQ: {query}\nAnswer from context only."
    return f"[grounded answer] {retrieve(query)[0]}"   # stand-in model

print(rag("how long for a refund?"))   # cites the 30-day doc

Chapter 3's from-scratch RAG becomes a few composed Runnables — retriever swaps in for a real vector store without touching the rest (needs langchain installed).

Exercise 5 · invoke vs batch vs streamProfessional

Context: Because every piece is a Runnable, you get three call shapes for free: invoke (one), batch (many), stream (chunks) — and callers pick the shape they need.

Your task: Model all three call shapes over the same underlying function.

Requirements:

  • invoke(x) returns one result
  • batch(xs) maps the function over many inputs
  • stream(x) yields the output chunk by chunk (e.g. word by word)
  • All three share one underlying function — no duplicated logic
  • Demonstrate each

💡 Hint: One stored callable, three methods over it; the point is that swapping one part of a chain never forces callers to change how they call it.

Show solution

One abstraction, three call shapes. Runnable stdlib:

class Runnable:
    def __init__(self, fn):
        self.fn = fn
    def invoke(self, x):
        return self.fn(x)
    def batch(self, xs):
        return [self.fn(x) for x in xs]
    def stream(self, x):
        for word in self.fn(x).split():
            yield word            # emit chunk by chunk

r = Runnable(lambda t: f"answer for {t} here")
print(r.invoke("a"))
print(r.batch(["a", "b"]))
print(list(r.stream("a")))       # ['answer', 'for', 'a', 'here']

Because every piece is a Runnable, you get invoke/batch/stream for free at every layer — swap one part without rewriting the callers.

Exercise 6 · Build the real chain (needs langchain installed)Industry scenario

Context: The production version of prompt | model | parser uses the real langchain-core / langchain-anthropic classes — and the same LCEL pipe gives batch and stream for free.

Your task: Write the real LCEL chain with a chat prompt template, a Claude chat model, and an output parser, structured to be memory-ready. (Needs the libraries installed.)

Requirements:

  • Use documented classes: a ChatPromptTemplate, ChatAnthropic, and an output parser
  • Compose them with the | pipe
  • Invoke the chain on a templated input
  • Note that batch and stream come for free from the same chain
  • Label it as needing langchain-core/langchain-anthropic and an API key

💡 Hint: It's the same shape as the offline model — the framework just supplies real templating, the Claude client, and streaming; verify import paths against your installed version.

Show solution

Correct LCEL using documented classes. Needs pip install langchain-core langchain-anthropic + ANTHROPIC_API_KEY:

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_anthropic import ChatAnthropic

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a concise assistant."),
    ("human", "Explain {topic} in one sentence."),
])
model  = ChatAnthropic(model="claude-opus-4-8", max_tokens=256)
parser = StrOutputParser()

chain = prompt | model | parser          # the LCEL pipe, for real
print(chain.invoke({"topic": "prompt caching"}))
# batch and stream come for free:
# chain.batch([{"topic": "RAG"}, {"topic": "agents"}])
# for chunk in chain.stream({"topic": "LCEL"}): print(chunk, end="")

Same shape as the offline model above — the framework supplies real prompt templating, the Claude client, and streaming. Import paths shift across versions; verify against what you install.

✓ Checkpoint — you can move on when you can…

  • Explain Runnables and the LCEL pipe in one sentence.
  • Compose prompt → model → parser and invoke it.
  • Get a typed object with with_structured_output.
  • Add session memory to a chain and explain what it really does.
  • Build a RAG chain and map each piece to Chapter 3.
🏗️ Toward the capstoneThe capstone's RAG-onboarding step — index the runbooks, retrieve the relevant procedure for an incident — is exactly this RAG chain. Whether you express it as a LangChain chain, a Flowise flow (N4), or hand-rolled (Chapter 3) is an engineering choice; the retrieval quality that decides whether the agent grounds its fix is the same either way. See the RAG + safety-gate build →

Knowledge check check yourself

✓ Knowledge check

What is a Runnable, and what does the LCEL | pipe do when you write chain = prompt | model | parser?

Show answer
A Runnable is anything with an .invoke() method (plus .stream()/.batch()) — prompts, models, parsers, retrievers all qualify. The | pipe wires them so the output of the left becomes the input of the right (like a Unix pipe), composing them into one object you can invoke, stream, or batch, and swap any piece without touching the rest.
✓ Knowledge check

In the RAG chain, what does the opening dictionary {"context": retriever | format_docs, "question": RunnablePassthrough()} accomplish?

Show answer
It's a parallel Runnable: it runs two branches at once — one fetches and formats context via the retriever, the other forwards the raw question unchanged with RunnablePassthrough — then passes the resulting dict downstream to the prompt. LCEL composes parallel steps as easily as sequential ones.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in