Analytics Vidhya · DataHack Summit
WORKSHOP
From 0 to Agentic AI:
Design, Build & Deploy with LangGraph
Build a production AI Knowledge Assistant — from a single LLM call to a deployed multi-agent system
INSTRUCTOR
Alessandro Romano
About me
Alessandro Romano speaking on stage

PyCon DE 2025

HELLO 👋

Alessandro Romano

Your instructor for the day.

Data Scientist / AI Engineer AI Advocate Educator Lifelong Learner Musician Wizard Skater

CO-HOST

My Data Guest — on Substack

Interviews with top AI & data experts, deep dives, and courses.

Follow along mydataguest.substack.com
How to be successful today

Six ground rules. They all point the same way: spend your attention on thinking, not typing.

🤝

Team up when it gets heavy

If the workload piles up or your brain is fried, pair with your neighbour. Two people, one screen, twice the ideas.

🧍

…or fly solo

Working alone is completely fine. Pick the mode that keeps you learning — and switch whenever you like.

💡

Share ideas out loud

Say the half-formed thought. Questions and tangents are the best part — someone else is stuck on the same thing.

📦

Nothing to catch up on

All materials are shared and yours to keep. Everything runs offline, at your own pace, long after today.

🧠

Design, don't type

The skill we're building is problem solving and agent design. Generate code only when it's genuinely the bottleneck.

🎉

Have fun — it's not a test

No grades, no leaderboard. Broken output is data; the interesting stuff happens when things misbehave.

🌟 If you only remember one thing

You are not here to finish the notebooks — you're here to leave able to design an agent from scratch. Fall behind on purpose if a question is more interesting than the next cell.

How to work through the assignments

🔓 Yes — the solutions are already in the repo

Every assignment ships with its _SOLUTION notebook. Nobody is stopping you from opening them — and I'm not going to pretend otherwise. But the learning is in the attempt, not the answer.

01
✍️

Try it yourself

Write something — even if you're sure it's wrong

02
🤔

Sit in the stuck

Two minutes of confusion is where it actually clicks

03
👨‍🏫

Then watch me

I walk through the solution live and explain the why

04
🔍

Read the solution

Compare it to yours — now it means something

💚 This material is yours forever

The repo doesn't expire and it doesn't get taken away. So don't spend today racing to copy — you can read every solution line-by-line tonight, next month, or next year. Spend today on the part you can't do alone: asking me questions.

The map for today

Nine modules, one build, three phases. Each block is concept → hands-on notebook, and every hands-on plugs into the same growing application.

PHASE 1 · UNDERSTAND~1 h
  • 1Why Agentic AI
  • 2From LLMs to Agents
  • 3The ReAct pattern
PHASE 2 · BUILD~4.5 h
  • 4LangGraph
  • 5Tools
  • 6Knowledge & Context
  • 7Multi-Agent Systems
PHASE 3 · SHIP~1.5 h
  • 8Streamlit interface
  • 9Deployment & monitoring

🎯 One thing to take home

Agentic AI isn't a bigger prompt — it's a control loop. Once you can reason about state, tools, and routing, the rest (RAG, MCP, multi-agent, deployment) is just wiring you already understand.

What we're building toward

An AI Knowledge Assistant for company operations.

Ask it a question about the company; it decides where to look — internal docs, Slack, GitHub — retrieves what's relevant, and synthesises a grounded answer.

When it makes sense, it also acts: draft a Slack message, open a GitHub issue, suggest a next step. By the end of the day it runs live on the internet.

🧩 It comes together piece by piece

No throwaway toy examples. Every module adds a real component to this app — you leave with the whole thing, deployed and yours.

LangGraph RAG MCP Multi-agent Streamlit Render
How the day runs

Every module follows the same rhythm. I explain the idea on a few slides, then we open a notebook and run it together. The slides are the map; the notebooks are the territory.

A
🧠

Concept

Short slides — the mental model & the "why"

B
📓

Hands-on

Open the notebook, run it, read the output

C
🧩

Plug in

Fold what we built into the assistant

D

Q & A

Breathe, ask, break — then next module

📓 Look for the teal slide

Each section closes on a Hands-on slide that names the exact notebook(s) to open and what to watch for. That's your cue to open the notebook. Everything is in the shared repo — run it locally in Jupyter (uv run jupyter lab); Colab works as a fallback.

Before we start — get set up
📦

Clone this first

github.com/pigna90/langgraph-workshop

WHAT YOU NEED

  • Basic Python — functions, classes, virtual environments
  • An OpenAI API key — for LLM access
  • A Tavily key and a Seltz AI key — web search; Tavily in Modules 2&4, Seltz from Module 5 on
  • A Render account (optional) — to deploy; you can also just run it locally

WHERE WE'LL WORK

  • Locally in Jupyter with uv — how we'll work all day
  • Google Colab — fallback only: upload a notebook & run
  • A laptop + internet — able to install packages
  • The shared repo — cloned and ready to go

⚠️ Two minutes now saves twenty later

Put your keys in a .env file today and load them with python-dotenv. Never hard-code a key in a notebook cell — it will end up in git.

01
SECTION · UNDERSTAND
Why Agentic AI
Deterministic code, a single LLM call, or a full agent — the trade-offs, and when each is the right tool.
Three ways to build a system

The same task can be solved three ways. They differ in who decides what happens next — your code, or the model.

DETERMINISTIC

Hard-coded logic

You write every branch. if/else, pipelines, rules.

  • Predictable, cheap, testable
  • Brittle; can't handle the unforeseen
LLM-BASED

A single model call

prompt → response. The model reasons once, you use the output.

  • Flexible language, fast to ship
  • No memory, no actions, no loop
AGENTIC

A decision loop

The model chooses actions, observes results, and repeats until done.

  • Dynamic, tool-using, multi-step
  • Costly, harder to control

The dial isn't "how smart" — it's how much control you hand to the model. More autonomy = more capability and more risk.

It's a spectrum, not three boxes

In reality these blur into a continuum of autonomy. Each rung hands a little more control to the model — and trades predictability for flexibility.

📏 Rules

Hard-coded if/else & pipelines

💬 Single LLM call

One prompt → one response

🔧 LLM + tools

One call that can act, once

🔁 ReAct agent

Loop: reason · act · observe

🕸️ Multi-agent

Several agents coordinating

← YOUR CODE decides the next step predictable & cheap  ↔  flexible & capable THE MODEL decides the next step →

🧭 Where we'll live today

Most of the workshop sits in the middle-right: an LLM with tools, wrapped in a ReAct loop, orchestrated by LangGraph. We touch multi-agent at the end — only once a single agent stops being enough.

One task, three ways

💬 A colleague asks: "Who owns the billing service, and can you ping them about the failing deploy?"

DETERMINISTIC

Works only if someone hard-coded this exact question → lookup. Any rewording breaks it.

✕ Too rigid for open questions

SINGLE LLM CALL

Understands the request & writes a lovely reply — but doesn't know your org and can't ping anyone. May invent an owner.

⚠ Fluent, but no facts & no actions

AGENT

Searches internal docs → finds the owner → drafts a Slack ping → confirms. Grounded, and it acts.

✓ Decides the path, uses tools

✅ Why this one needs an agent

The steps aren't known in advance — which tool, in what order, depends on what the docs say. That's the exact signature of a job for an agent, and it's our assistant's bread and butter.

When is an agent the right call?

✅ REACH FOR AN AGENT WHEN

  • The steps aren't known in advance — they depend on what's found
  • The task needs tools — search, APIs, a database
  • It requires iterative reasoning — try, observe, adjust
  • You must choose among many possible actions

🛑 AN AGENT IS OVERKILL WHEN

  • A fixed pipeline already solves it
  • One prompt gives a good enough answer
  • Latency & cost matter more than flexibility
  • You need guaranteed, auditable behaviour

💡 The rule of thumb

Use the least autonomy that solves the problem. Start deterministic, add an LLM call where language matters, and only reach for a full agent when the path itself must be decided at runtime.

Where agents already earn their keep

This isn't hypothetical — agentic systems are in production today. The pattern is always the same: dynamic path + real tools + a goal to reach.

🏥 Medical triage

Read the intake notes, check symptoms against protocol, route or escalate to a clinician.

🛡️ Compliance & investigations

Pull the records, cross-check policy, assemble an evidence trail a human signs off.

✅ QA automation

Read the spec, generate cases, run them, triage the failures — loop until green.

🛒 E-commerce & campaigns

Segment the audience, draft the message, pick the channel, act on what converts.

🔎 Research & discovery

Search literature, cross-check sources, synthesise a cited report — from ops to drug discovery.

🧭 Knowledge assistants

Our build — answer over docs, Slack & GitHub, then act.

🎯 Notice the shared shape

Every one of these retrieves information, reasons over it, and takes actions across multiple steps. Same skeleton, different tools and data — which is why the assistant we build today is a template for the one you need at work.

02
SECTION · UNDERSTAND
From LLMs to Agents
A plain model call can't remember, act, or loop. We add those three things — and get an agent.
A plain LLM call, and where it stops

In an application, an LLM is a pure function: text in, text out. Powerful — but on its own it hits three hard walls.

💬

prompt

🧠

LLM

📝

response

🧠 No memory

Each call is stateless. It forgets everything the moment it replies.

🖐️ No actions

It can describe an API call, but it cannot make one. Just text.

🔁 No control flow

One shot only. It can't decide to try again, or take a different step.

Watch it hit the walls

Point our future assistant at three real company-ops questions with nothing but a prompt. Each one fails in a different, revealing way.

ASK

"How many PTO days do I get?"

🧠 Hallucinates

Invents "25 days" confidently — it has never seen your HR policy.

ASK

"Ping the billing owner on Slack."

🖐️ Can't act

Writes a lovely message — but has no way to actually send it.

ASK

"And who did I just mention?"

🔁 Forgets

Blank stare — the previous turn is already gone.

🎯 The gaps map to the fixes

Hallucinates → give it knowledge (RAG). Can't act → give it tools. Forgets → give it state. The rest of the workshop is literally closing these three walls, one at a time.

First, get the single call right

An agent is many LLM calls in a loop — so a sloppy prompt fails many times over. A few patterns do most of the work:

🧾 Structured outputs

Ask for JSON against a Pydantic schema, not prose. Now the output is parseable — the backbone of tools & routing.

🎯 Few-shot examples

Show 2–3 worked examples. The model imitates format and tone far more reliably than from instructions alone.

🧩 Templates & roles

Separate system (rules, persona) from user (the task). Reusable, testable, easier to reason about.

🧠 Reasoning prompts

Chain-of-thought & step-by-step for hard tasks — the seed of the ReAct "Thought" we'll formalise next.

💡 DID YOU KNOW · one agent, several models

Nothing says every call must hit the same model. Production agents route by job: a small fast model for classifying, extracting and routing; a strong one for the final synthesis; a multimodal one when the input is a scan, chart or screenshot. Same graph — you just bind a different model per node. Often the cheapest latency and cost win available.

Structured output is the one to internalise — it's what lets code trust the model's answer.

Tool calling: the model asks, your code acts

This is how we break the "can't act" wall — and it's just structured output with a job. You hand the model some functions; it replies with a request to call one. It never runs code itself.

① You describe tools name · args schema · description ② Model returns a tool-call request {name, args} ③ Your runtime runs the function the actual API / DB call ④ Result back to model as an observation

⚠️ The model never executes anything

It only chooses a tool and fills in the arguments — safe by design. You stay in control of what actually runs. That single request → run → feed-back is one turn of the agent loop.

Add three things → you have an agent
  • Tools — give the model functions it can call to act on the world
  • Memory / state — carry context forward across steps
  • A loop — let it decide the next step, again and again, until the goal is met

That's the whole idea. An agent is an LLM placed inside a decision-making loop, with tools to act and state to remember.

🔁 The agent loop, informally

thinkpick a toolactobserve the result → repeat …
stop when the answer is ready.

This loop is the heart of everything we build today. Next: the most common way to structure it — ReAct.

The easy button: an agent in ~5 lines
from langchain.agents import create_agent

@tool
def web_search(query: str) -> str:
    """Search the web for current info."""
    return search_client.run(query)

agent = create_agent(llm, tools=[web_search])

agent.invoke({"messages": [("user", "...")]})

LangGraph ships the whole loop prebuilt. We use it today — then rebuild it by hand in Module 4 to see inside.

WHAT THOSE 5 LINES DO

  • Wire the LLM to the tool
  • Run the reason → act → observe loop for you
  • Stop when the answer is ready
  • Return the full message trace to inspect

🎓 Why start with the easy button?

See a working agent first, so the concepts land against something real. Understanding (Module 3) and rebuilding (Module 4) come next.

📓 Hands-on · First calls → first agent
OPEN THE NOTEBOOK
💬

Talking to the Model

Module_2_Demo_Talking_to_the_Model.ipynb

Plain call & streaming → hit the 3 walls → structured outputs (Pydantic) → a @tool the model requests.

DEMO
🤖

Your First Agent

Module_2_Demo_Your_First_Agent.ipynb

create_agent + a web-search tool answers a question end-to-end — a working agent in ~5 lines.

DEMO

WATCH FOR

  • The three walls, live
  • Structured output = trustable data
  • The message trace: request → run → answer
  • The agent acts — search breaks a wall

Both are teaching demos. The real app build starts in Module 4.

03
THE CORE LOOP · UNDERSTAND
The ReAct pattern
Reason + Act: the Thought → Action → Observation loop that lets a model use tools deliberately.
Thought → Action → Observation

ReAct interleaves reasoning and acting. The model narrates a thought, takes one action, reads the result — then decides whether to loop again or answer.

💭 Thought reason about what to do next ⚙️ Action call a tool with chosen arguments 👁️ Observation read the tool's result loop until the answer is ready

🧭 Why interleave, not plan-then-do?

Because each observation changes what the next step should be. Reasoning between actions lets the agent adapt to what it just learned — instead of committing to a plan that reality breaks.

A minimal ReAct loop — and how it breaks
# the loop, stripped to its essence
while not done:
    thought = llm(prompt(history))
    if thought.is_final:
        return thought.answer
    # the model chose a tool + args
    result = tools[thought.tool](**thought.args)
    history.append(thought, result)   # observe

LangGraph will give us this loop with state, retries and routing handled properly — but this is all it is underneath.

⚠️ FAILURE MODES TO WATCH

  • Looping — repeats the same action forever. Cap the steps.
  • Hallucinated actions — invents a tool that doesn't exist. Validate the choice.
  • Tool misuse — right tool, wrong arguments. Enforce a schema.
  • Never stopping — no clear finish condition. Define "done".
📓 Hands-on · The loop, by hand
OPEN THE NOTEBOOK
🔁

ReAct from Scratch

Module_3_Demo_ReAct_From_Scratch.ipynb

Build the agent loop in a raw Python whiletwo tools so the model must choose — then watch it break and add the guards.

DEMO

No framework here — we hand-build the loop Module 2 hid, so it's never a black box again.

WATCH FOR

  • The model chooses between two tools by their docstrings
  • A multi-step question → the loop takes several turns
  • The step cap firing when steps run out
  • Thought → Action → Observation, in plain Python
04
SECTION · BUILD
Introduction to LangGraph
Model the agent loop as a state machine — nodes, edges and shared state you can control and inspect.
Why a graph instead of a linear chain?

A chain runs A → B → C, once, forwards. Agents need to branch, loop and retry — that's a graph, not a line.

NeedLinear chainGraph (LangGraph)
BranchingFixed path only✓ Conditional edges route on state
LoopsNo way back✓ Edges can point backwards
RetriesFails the whole run✓ Route to a retry / fallback node
Inspecting stateHidden between steps✓ One explicit, shared state object

✅ The mental model

Think of your agent as a state machine: nodes do work, edges decide where to go next, and a single shared state travels through it. The ReAct loop becomes a graph you can see.

Three primitives: State · Nodes · Edges
STATE

The shared memory

A typed dict passed to every node. Nodes read it and return updates — this is how steps remember each other.

NODES

The units of work

Plain Python functions. state in → state update out. Call the LLM, run a tool, transform data.

EDGES

The wiring

Connect nodes. Normal edges always fire; conditional edges pick the next node based on the state.

🔀 Conditional edges = the agent's decisions

"Did the model ask for a tool? → go to the tools node. Otherwise → finish." That single branch, evaluated each loop, is what makes the graph agentic rather than a fixed pipeline.

Your first LangGraph agent
class State(TypedDict):
    messages: Annotated[list, add_messages]

def call_model(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

graph = StateGraph(State)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode(tools))

graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")   # loop back

app = graph.compile()

READ IT AS A LOOP

  • START → agent — the model reasons over the messages
  • should_continue — tool call? → tools. Else → END
  • tools → agent — feed the result back and reason again
  • add_messages — the reducer that appends to state

That's the ReAct loop from Section 3 — now explicit, inspectable, and safe to extend.

Loops, retries & termination

A back-edge makes the graph loop — but a loop must be able to stop, and steps can fail. All three are the same tool you already have: a conditional edge.

🔁 Loop

An edge that points backwards (tools → agent). The agent iterates until its work is done.

🔂 Retry

On failure, route back to the node — or to a fallback. A retry is just another branch.

🛑 Terminate

A stop-condition edge to END — plus recursion_limit as a backstop.

⚠️ Always have a way to stop

Your stop condition is how the agent should finish; recursion_limit catches it when it doesn't — hit the limit and LangGraph raises GraphRecursionError instead of looping (and billing) forever. This is Module 3's max_steps, now built in.

Beyond the loop: shapes a graph unlocks

Once the loop is a graph, you can express richer control flow than a single agent-tools cycle:

🗺️ Plan → execute

A planner node breaks a goal into steps; downstream nodes carry them out. Structure, not one big prompt.

⚡ Fan-out with Send()

Map work across N parallel branches, then reduce — e.g. answer 5 sub-questions at once, compile one report.

👁️ Visualise & stream

Draw the graph to see the flow; stream node-by-node so users watch the agent think.

🧩 Same primitives, bigger structures

Planning, parallelism and streaming are all just nodes and edges. You don't learn new concepts — you compose the three you already have.

📓 Hands-on · The build begins
OPEN THE NOTEBOOKS
🧩

LangGraph Basics

Module_4_Demo_LangGraph_Basics.ipynb

Tiny examples: state + nodes + edges, a conditional branch, and add_messages.

DEMO
🛠️

Knowledge Assistant v1 — you build it

Module_4_Project_Knowledge_Assistant_v1.ipynb

Assemble the real agent as a StateGraph. Key functions are TODO — you implement them.

PROJECT

…v1 — Solution

Module_4_Project_Knowledge_Assistant_v1_SOLUTION.ipynb

The full implementation — for reference if you get stuck.

SOLUTION

WATCH FOR

  • The compiled graph — draw it, read the loop
  • The tools → agent edge that makes it iterate
  • State accumulating via add_messages
  • The full message trace end-to-end

This is v1 of the app we grow & deploy — the same loop from Module 3, now a real graph.

The assistant so far  after Module 4 · v1
👤 User a question 🧠 Agent LLM + reasoning StateGraph loop 🔧 Tools 2 starter tools 🗂️ State · messages add_messages reducer loop until done

🧱 The skeleton is in place

This agent ⇄ tools loop over shared state is the whole app's backbone. Every later module just adds tools and knowledge to this — the graph doesn't change.

Module 4 · the graph (v1) — NEW
05
SECTION · BUILD
Tools
The hands of the agent — how to design reliable tools and connect real external systems.
A tool is a function the model can call

You describe a function — name, arguments, what it does — and the model decides when to call it and with what arguments. The model never runs code; your runtime does, then hands back the result.

✅ RELIABLE TOOLS ARE

  • Narrow — one clear job, obvious when to use
  • Well-described — the docstring is the interface the model reads
  • Typed & validated — a schema on inputs and outputs
  • Defensive — handle errors, return a useful message

🛑 TOOLS GO WRONG WHEN

  • The name/description is vague — the model guesses
  • They do five things — the model picks the wrong path
  • Errors crash instead of returning text the model can read
  • Outputs are unstructured — nothing downstream can parse them

⚠️ The description is a prompt

The model chooses tools from their names and docstrings alone. Write them like instructions to a new teammate — ambiguity there becomes tool misuse at runtime.

From a Python function to a tool
from langchain_core.tools import tool

# @tool turns a typed, documented
# function into a callable the LLM sees
@tool
def search_github_issues(repo: str, query: str) -> str:
    """Search open issues in a GitHub repo (repo is 'owner/name')."""
    return gh.search(repo, query)   # your API call

WHAT THE MODEL RECEIVES

  • namesearch_github_issues
  • args schema — from the type hints
  • description — from the docstring

FOR OUR ASSISTANT

  • Web search (Seltz)
  • Slack: draft a message
  • GitHub: draft an issue
Errors, fallbacks & tool schemas
# 1 · a schema pins down the arguments
class RefundArgs(BaseModel):
    order_id: str
    amount: float = Field(gt=0)              # must be > 0
    reason: Literal["damaged", "late"]

@tool(args_schema=RefundArgs)
def issue_refund(order_id, amount, reason) -> str:
    """Issue a refund for an order."""
    try:
        return billing.refund(order_id, amount, reason)
    except Exception as e:
        return f"error: {e}"   # readable, not a crash
  • Tool schema — a Pydantic args_schema encodes real rules (ranges, enums). The model is told the constraints.
  • Handle errors — wrap the risky call; return a readable string, never let it raise into the loop.
  • Fallback — on failure, degrade gracefully: a cached value, a simpler tool, or "I couldn't do X, try Y".
  • Structured output — a tool can return a Pydantic object, so downstream nodes get fields, not prose.

✅ The rule

A tool that can't crash and can't be mis-called is what keeps the agent loop alive. The model reads your error text and recovers.

Use tools built for LLMs, not humans

When you connect a tool, prefer one designed for an AI to consume. Web search is the clearest case: a human search engine and an LLM search API return very different things.

🧑 Built for humans

  • Ten blue links & short snippets — you click through
  • Raw HTML: nav, ads, cookie banners, clutter
  • Wastes tokens; the model must scrape & guess

🤖 Built for LLMs SELTZ

  • Full, clean content — not snippets — ready to reason over
  • Filtered & ranked for relevance, no clutter
  • Token-efficient, with sources for grounding

✅ Why it matters for an agent

The model can only reason over what a tool hands back. Seltz treats the AI as the primary consumer — clean, ranked web context instead of human-facing snippets — so the agent spends tokens thinking, not de-cluttering HTML. That's why our assistant's web_search wraps Seltz.

Connecting external APIs

A tool is just a Python function — so anything with an API becomes a tool. You wrap the call, write a clear docstring, and handle errors. That's the whole move.

🔎 Read the world

Wrap a web-search API (Seltz) in a @tool — the assistant can fetch current, external facts.

✍️ Act on the world

Draft a Slack message, open a GitHub issue. The assistant stops answering and starts doing.

🧪 Draft, don't send

Action tools return the payload for review first. Safe by default — a human approves before anything real happens.

⚠️ Actions need a safety valve

A tool that sends or deletes is irreversible. Start with dry-run (return the draft), add a confirmation step, and only then wire the real send. Guard the risky steps.

📓 Hands-on · Real tools → v2
OPEN THE NOTEBOOKS
🛠️

Designing Reliable Tools

Module_5_Demo_Designing_Reliable_Tools.ipynb

Args schemas, validation, defensive try/except, structured output.

DEMO
🧩

Knowledge Assistant v2 — you build it

Module_5_Project_Knowledge_Assistant_v2.ipynb

Wrap Seltz search + draft Slack / GitHub tools. Key functions are TODO.

PROJECT

…v2 — Solution

Module_5_Project_Knowledge_Assistant_v2_SOLUTION.ipynb

The full implementation, for reference.

SOLUTION

WATCH FOR

  • Wrapping an external API as a @tool
  • Docstring + try/except = a reliable tool
  • The agent choosing search vs. an action
  • Draft tools returning a payload, not sending

Adding tools didn't touch the graph — same v1 loop, richer hands.

The assistant so far  after Module 5 · v2
👤 User a question 🧠 Agent LLM + reasoning StateGraph loop 🔧 Tools routes to a tool 🔎 web_search · Seltz 💬 draft_slack · dry-run 🐙 draft_github · dry-run loop
Module 4 · the graph (v1) Module 5 · real tools (v2) — NEW
06
SECTION · BUILD
Knowledge & Context Layer
RAG + context engineering + MCP — ground the model in real data and control exactly what it sees.
RAG: give the model the right facts

The model can't know your internal docs. So we retrieve the relevant pieces at query time and inject them into the prompt — the model answers from real, cited context.

01
🗂️

Index

Chunk docs, Slack, code → embeddings in a vector store

02
🔎

Retrieve

Embed the query, fetch the most similar chunks

03
📎

Inject

Add retrieved text to the prompt as grounding

04

Answer

Model responds from context, with sources

✅ The one-liner

RAG changes what the model knows — without retraining it. Retrieval is just a tool: the agent can decide when to reach for internal knowledge vs. web search vs. answering directly.

Building the index: three decisions that matter
1 · LOAD

Document loaders

PDFs, Office docs, HTML, code. Each source becomes clean Documents with metadata you can filter on.

2 · CHUNK

Splitters & chunkers

Too big = noise; too small = lost meaning. Chunk size & overlap are the dials that make or break retrieval.

3 · EMBED

Vector database

Embed chunks and store in ChromaDB. Add, update, delete, persist — retrieval by semantic similarity.

⚠️ Retrieval quality is decided before the LLM

If chunking is bad, the model never sees the right context — and no prompt can fix that. Most RAG failures are retrieval failures, upstream of generation.

💡 DID YOU KNOW · beyond plain vector search

Vectors are the 80% case, not the ceiling. Two cheap upgrades when retrieval plateaus: hybrid search (keyword + vector — for IDs, codes and exact names) and reranking (retrieve 50, let a cross-encoder keep the best 5). And when a question spans related facts rather than similar text, a graph beats both — that's the next slide.

RAG vs KAG: similar text vs connected facts

KAG — Knowledge-Augmented Generation — retrieves from a knowledge graph instead of a pile of chunks. Same goal (ground the model in your data), different question it can answer:

RAG · VECTORS

Stores text chunks as embeddings

Retrieves by similarity: "give me the 5 passages that look most like this question."

Wins when the answer sits in a passage — policies, docs, tickets, papers.

KAG · GRAPH

Stores entities & relationships

Retrieves by traversal: walk the edges from one entity to the next.

Wins when the answer is a path — no single passage contains it.

🔗 The question that breaks plain RAG — a multi-hop join

"Which suppliers are connected to this flagged invoice?"  No chunk holds that answer — it's three facts in three documents:

Invoice 412  —issued_by→  Supplier X  —shares_director_with→  Supplier Y  —also_billed→  Case 99

Similarity returns invoices that read like yours. A graph follows the link — and shows the exact path it took, which is what makes the answer auditable.

⚖️ THE HONEST TRADE-OFF · and why we use vectors today

A graph costs you an extraction step (an LLM pulling entities & relations out of text — imperfectly), a schema decision, and a store like Neo4j. Start with vectors; add a graph when questions are truly relational. Mature systems run both. Note: KAG isn't a standardised term — also sold as GraphRAG.

📓 Hands-on · Build a RAG pipeline
OPEN THE NOTEBOOKS · RAG BLOCK
🔬

RAG Pipeline

Module_6_Demo_RAG_Pipeline.ipynb

Chunk → embed (Chroma) → retrieve → grounded answer with sources.

DEMO
🧩

Knowledge Assistant v3 — you build it

Module_6_Project_Knowledge_Assistant_v3.ipynb

Wrap retrieval as a @tool → the agent routes internal docs vs. web. TODOs to fill.

PROJECT

…v3 — Solution

Module_6_Project_Knowledge_Assistant_v3_SOLUTION.ipynb

Full implementation, for reference.

SOLUTION

WATCH FOR

  • Chunk size / overlap shaping retrieval
  • Unknown question → "I don't know" (not a guess)
  • Sources cited in the answer
  • Retrieval as a tool → agent routes docs vs. web
Context engineering: what goes in the window

The prompt the model actually sees is assembled from many parts. Designing that assembly — what to include, in what order, and what to leave out — is context engineering.

  • System instructions — role, rules, output format
  • User input — the current question
  • History — relevant prior turns, trimmed
  • Retrieved data — RAG chunks, tool outputs
  • State — passed cleanly through LangGraph

⚠️ More context ≠ better

A stuffed window adds cost, latency and noise — the model loses the signal. Curate ruthlessly: include what's relevant, drop the rest.

🧭 The goal

Give the model exactly what it needs to answer well — no more. Context is a budget, not a bucket.

Memory: short-term vs long-term

Our assistant still forgets between turns (the third wall from Module 2). Memory is just context you persist — and LangGraph makes the short-term kind almost free.

SHORT-TERM · WE BUILD THIS

Within a conversation

Compile with a checkpointer and pass a thread_id — LangGraph saves & reloads the history per thread.

app = builder.compile(checkpointer=InMemorySaver())
app.invoke(msg, {"configurable": {"thread_id": "user-1"}})
LONG-TERM · CONCEPT

Across sessions & users

Durable facts written to a store — preferences, past chats — recalled in future sessions. Makes the assistant adaptive. (Beyond today's build.)

🧵 Same thread remembers, new thread is fresh

A thread_id is one conversation. Same id → the assistant recalls earlier turns; a new id → a clean session. That's exactly how the Streamlit chat (Module 8) will keep each user's conversation going.

MCP: a standard plug for tools & data

The Model Context Protocol is a common interface between agents and external systems. Instead of hand-wiring every integration, an agent speaks one protocol to many MCP servers.

🤖

Your agent

MCP client

📁  Docs / filesystem server
💬  Slack server
🐙  GitHub server

✅ Use existing servers

Plug into ready-made MCP servers — no bespoke glue for every API.

🔧 Or expose your own

Wrap a simple internal API as an MCP server → modular, reusable, scalable architectures.

📓 Hands-on · MCP servers
OPEN THE NOTEBOOK
🔌

MCP Servers

Module_6_Demo_MCP_Servers.ipynb

Expose your own MCP server with FastMCP, connect to it with langchain-mcp-adapters, then hand its tools to an agent.

DEMO

A tiny local stdio server — no hosting, no network. To the agent, MCP tools are just tools.

WATCH FOR

  • A server defined in ~6 lines (FastMCP)
  • Its functions loaded as LangChain tools
  • One client, many servers possible
  • The agent using them like any other tool
The assistant so far  after Module 6 · v3.1
👤 User a question 🧠 Agent LLM + reasoning StateGraph loop 🔧 Tools routes to a tool 🔎 web_search · Seltz 💬 draft_slack 🐙 draft_github 📚 search_company_docs RAG over internal docs 🗄️ Vector store Chroma embeddings 🧵 Memory checkpointer · thread_id loop
Module 4 · the graph (v1) Module 5 · real tools (v2) Module 6 · knowledge + memory (v3.1) — NEW
07
SECTION · BUILD
Multi-Agent Systems
When one agent isn't enough — split responsibilities, and weigh clarity against complexity.
Split the work across specialised agents

When one agent juggles too many tools and goals, it gets confused. Splitting responsibilities gives each agent a narrow prompt, few tools, one job — easier to steer and debug. Common patterns:

🧭 Planner / Executor

One agent breaks the goal into steps; another carries each out.

🔎 Retriever / Analyzer

One gathers the evidence; another reasons over it to answer.

✍️ Generator / Reviewer

One drafts; another critiques and approves before it ships.

In LangGraph each agent is a subgraph — a node that is itself a graph. Coordination is just more edges.

Two topologies: Supervisor vs Swarm

Once you have multiple agents, how do they hand off control? Two common shapes:

Aspect🧭 Supervisor🐝 Swarm
ControlCentralised — a router agent delegatesDecentralised — agents hand off peer-to-peer
RoutingSupervisor decides who acts nextEach agent decides when to pass control
Best forClear task decomposition, auditabilityFluid, exploratory collaboration
Watch outSupervisor is a bottleneckHarder to trace & contain

🧭 How to choose

Start with a supervisor — it's easier to reason about and debug. Reach for a swarm only when hand-offs are genuinely dynamic and a central router gets in the way.

Coordination — and the honest trade-off

HOW THEY TALK

  • Shared state — agents read/write the same graph state
  • A supervisor — a router agent decides who acts next
  • Handoffs — one agent passes control (and context) to another
  • Structured messages — clear contracts between them

⚖️ Complexity vs clarity

More agents = more moving parts, more latency, more ways to fail. Only split when a single agent genuinely can't hold the job.

✅ Start simple

One good agent beats a tangle of mediocre ones. Grow into multi-agent when the task demands it — not before.

📓 Hands-on · Single vs multi-agent
OPEN THE NOTEBOOK
🧭

Multi-Agent · Supervisor

Module_7_Demo_Multi_Agent_Supervisor.ipynb

A supervisor routes to two specialists — a researcher (web) and a docs expert (internal) — who report back until the task is done.

DEMO

Standalone demo — our assistant stays a single agent. Swarm (peer-to-peer hand-off) is covered on the previous slide.

WATCH FOR

  • The supervisor routing to the right specialist
  • Each specialist has one job, one tool
  • Specialists report back to the supervisor
  • When the extra complexity is (and isn't) worth it
08
SECTION · SHIP
Streamlit Interface
Turn the backend into something people can actually use — a chat UI with session state.
A chat UI in a handful of lines
import streamlit as st
from assistant import build_assistant   # our v3.1 graph

# build once; one thread_id per session = memory
if "agent" not in st.session_state:
    st.session_state.agent = build_assistant()
    st.session_state.tid = str(uuid.uuid4())

for role, text in st.session_state.history:
    st.chat_message(role).write(text)

if prompt := st.chat_input("Ask…"):
    st.chat_message("user").write(prompt)
    cfg = {"configurable": {"thread_id": st.session_state.tid}}
    ans = st.session_state.agent.invoke({"messages":[("user",prompt)]}, cfg)
    st.chat_message("assistant").write(ans["messages"][-1].content)

THE ESSENTIALS

  • chat_input / chat_message — the whole UI
  • session_state — survives Streamlit's rerun-on-every-keystroke
  • thread_id — per session → the memory from Module 6
  • the UI just invokes the graph — all logic lives in assistant.py

The graph is imported, not rebuilt — same code the deploy step (Module 9) ships.

Good AI UX: show the work

An agent can take seconds and use several tools. A blank spinner feels broken. A few small touches make it feel trustworthy — and they're the outline's "display structured outputs & tool results."

⏳ Stream, don't stall

.stream() the graph and render as it goes, so the user sees progress instead of a frozen screen.

🔧 Show what it did

Surface the tools used and sources cited — "used: search_company_docs". Turns a black box into something you trust.

📋 Render structure

Structured tool output? Show it as a table / card, not a JSON blob. Match the display to the data.

✅ The principle

An agent is non-deterministic and slow-ish — so make its work visible. Progress, tools, and sources on screen are what separate a demo from something people actually rely on.

📓 Hands-on · Wrap the assistant in a UI
RUN LOCALLY
🧠

assistant.py — the graph

src/app/assistant.py

You write this. Fill the TODOs — it's the v3 graph (tools + RAG + memory) moved out of the notebook into reusable code.

YOU BUILD
💬

app.py — the chat UI

uv run streamlit run app/app.py

Already written. Don't touch it — it just imports build_assistant() from your file and renders the chat.

PROVIDED

We leave Jupyter — this runs on localhost, the same files Module 9 deploys.

WATCH FOR

  • Graph logic in assistant.py, UI in app.py
  • Conversation in session_state
  • thread_id → it remembers within the chat
  • The tools it used, surfaced in the UI

✅ Your goal

When the app answers a question, you assembled the whole day correctly. Reference in app/solution/.

The assistant so far  after Module 8 · now with a UI
💬 Streamlit chat UI · app.py sidebar toggles MCP departments · session_state · thread_id 👤 User chat box 🧠 Agent assistant.py graph 🔧 Tools 🔎 web_search · Seltz 💬 draft_slack / 🐙 github 📚 search_company_docs 🔌 MCP: HR · Finance … 🗄️ Vector store department servers 🧵 Memory checkpointer · thread_id
M4 graph M5 tools M6 knowledge + memory M8 UI + toggleable MCP — NEW
09
SECTION · SHIP
Monitoring & deployment
See what your agent is really doing with tracing & evaluation — then ship it live.
Why an agent needs monitoring

A normal app is deterministic — same input, same path, and a stack trace when it breaks. An agent is neither: it decides its own steps, calls tools, and can fail silently — a wrong tool, a bad answer, a runaway loop. You can't fix what you can't see.

🕶️ Without monitoring

  • "It gave a weird answer" — but why? No idea.
  • Which tool did it call? With what args?
  • How slow, how many tokens, how much cost?
  • Did it error, or just quietly do the wrong thing?

🔦 With monitoring (tracing)

  • A trace = the full step-by-step record of one run
  • Every prompt, tool call, and result, in order
  • Latency, tokens & cost per step
  • Failures surfaced, not hidden

🧭 The mental model

Monitoring an agent = recording every run so you can replay it. The terminal trace we added is the mini version; LangSmith is the same idea, in a dashboard, for production. It's how you go from "it feels off" to "here's the exact step that broke."

Simple monitoring with LangSmith

An agent is a black box until you can see inside it. LangSmith records every run — prompts, tool calls, tokens, latency, errors — so you can debug and watch it in production.

The best part: zero code changes. Set four env vars and every run is traced automatically:

LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_pt_...
LANGSMITH_PROJECT=knowledge-assistant
LANGSMITH_ENDPOINT=https://api.smith.langchain.com

WHAT YOU GET IN THE DASHBOARD

  • Every trace — the full agent → tool → agent path, step by step
  • Which tool ran, with its inputs & outputs
  • Tokens, latency & cost per run
  • Errors surfaced, so failures aren't invisible

✅ Start here

Tracing is the cheapest, highest-value observability. Evaluation (scoring on a dataset, LLM-as-a-judge) is the natural next step once you're tracing.

🖥️ Demo · Set up LangSmith tracing
LIVE IN THE BROWSER

Let's do it together: create a project on smith.langchain.com, generate an API key, and paste four lines into .env. Then chat with the assistant and watch the trace appear.

  • 1New project — name it knowledge-assistant
  • 2Pick the framework — LangGraph, then switch the language to Python (it opens on TypeScript)
  • 3Generate API Key — copy it once, it won't show again
  • 4Paste into .env — the four LANGSMITH_* vars
  • 5Ask a question — refresh the dashboard, the trace is there

✅ Zero code changes

We don't touch assistant.py. The env vars alone auto-instrument every LangGraph run.

smith.langchain.com
LangSmith setup screen — waiting for traces, framework picker, language selector, and the LANGSMITH_* environment variables

Note the language tab: it lands on TypeScript. Click Python, then use the .env tab instead of Shell.

From laptop to live in four steps

Deployment is mostly discipline, not magic. Pin what you depend on, keep secrets out of code, push, and test the running app.

01
📦

Package

A committed uv.lock + a render.yaml blueprint

02
🔗

Connect repo

Render reads render.yaml & provisions the service

03
🔑

Set secrets

API keys as env vars in the dashboard

04
🚀

Push = deploy

Every git push auto-redeploys · live URL

✅ Same uv workflow as your laptop

The server runs uv sync --frozen against the committed uv.lock — the exact versions you tested. Nothing new to learn, no second deps file to drift.

⚠️ What bites on the free tier

Cold starts (~30-60s to wake), ephemeral disk (re-embeds once per deploy), and the classic missing env var.

The blueprint  ·  infrastructure as code
ONE FILE DESCRIBES THE SERVER

We never click "create a service." We commit a render.yaml that declares what should exist — and let the platform make reality match the file.

  • Clicking through a dashboard — nobody remembers what you picked, nothing is reviewable, and it dies with your browser tab
  • A file in git — diffable, reviewable, and it recreates the same service on a fresh account
  • The repo is the source of truth — change the file, push, the infrastructure follows

✅ Same idea as your code

Your agent's behaviour lives in .py files. Its server now lives in a .yaml file. Both versioned, both reviewed.

RENDER, READING OUR BLUEPRINT

dashboard.render.com
Render's Blueprint screen: it read render.yaml and pre-filled the knowledge-assistant web service and its three secret env vars

We typed none of the left column. Render read it from the file.

🔎 Everything except the secrets

The service name, runtime, plan and every key came from render.yaml at the repo root. The empty boxes are exactly our sync: false values — the one thing a file in git must never hold.

🖥️ Demo · Deploy the blueprint to Render
LIVE IN THE BROWSER

Nothing to write — the blueprint is already in the repo. We point Render at it and paste our keys.

  • 1New + → Blueprint — connect the GitHub repo
  • 2Render reads render.yaml — build & start commands declared
  • 3Paste the secrets — every sync: false key, in the dashboard
  • 4Watch the build loguv sync --frozen → Streamlit boots
  • 5Open the live URL — ask a question, find that run in LangSmith

✅ It's the same app

Identical files to Module 8 — no rewrite for "production." Every git push redeploys.

THE WHOLE DEPLOY CONFIG

# render.yaml (repo root)
services:
  - type: web
    name: knowledge-assistant
    runtime: python
    plan: free
    rootDir: src
    buildCommand: pip install uv && uv sync --frozen
    startCommand: uv run streamlit run app/solution/app.py ...
    healthCheckPath: /_stcore/health
    envVars:
      - key: OPENAI_API_KEY
        sync: false   # ← set in the dashboard
      - key: LANGSMITH_API_KEY
        sync: false

⚠️ First hit is slow

Free tier cold-starts (~30–60 s) and re-embeds Chroma once per deploy. Don't panic on stage.

📓 Hands-on · Ship & observe
SHIP IT · LOCAL FILES → LIVE
🔍

1 · Turn on LangSmith tracing

4 env vars in .env → chat → open the dashboard

See a full trace of one request: agent → tools → answer, with tokens & latency.

MONITOR
🚀

2 · Deploy via Render blueprint

render.yaml · uv.lock → connect repo

Connect the repo, set secrets in the dashboard, get a live shareable URL.

LIVE

Same files from Module 8 — now traced and on the internet.

WATCH FOR

  • A full trace appear in LangSmith
  • render.yaml driving the deploy
  • The live URL serving our agent
  • A cold start on the first hit
What you built today

🔁 An agent, not a prompt

A ReAct loop with state, tools and routing — built on LangGraph.

🧩 Grounded & connected

RAG + MCP wire it to real docs, Slack and GitHub.

🚀 Deployed & observable

A Streamlit app, live on Render, traced with LangSmith.

🧠 Judgement

When to use an agent — and when a plain call is smarter.

🛠️ Reliable tools

Narrow, typed, well-described — the model's dependable hands.

⚖️ Scale sensibly

Multi-agent when it earns its complexity — not by default.

🎯 The meta-lesson

Agentic AI is engineering, not alchemy. A loop over state, tools, and routing — grounded in real data and shipped like any other app. You now have the whole stack.

Now make it yours

You built one assistant — but the skeleton is the same for all of them. To retarget it you swap four things, and keep the loop:

Your use case Knowledge source Tools it needs Shape
🏥 Medical triage Protocols, patient history Symptom lookup, scheduling Single agent + human approval
🛡️ Compliance / investigations Case files, policy, audit logs Record search, evidence writer Retriever → analyser → reviewer
✅ QA automation Specs, past defects Test runner, log parser ReAct loop, retry on fail
🛒 E-commerce / campaigns Catalogue, segments, past sends Query audience, draft, send Planner → executor, gated send
🔎 Research & discovery Papers, internal datasets Search, fetch, cite Fan-out sub-questions → synthesise

✅ Start narrower than feels satisfying

Pick one question your agent must answer well, wire the two tools it truly needs, and put a human in the loop wherever an action is expensive or irreversible. Widen only once it's reliable — that order is what separates the demos from the systems.

What we didn't cover — and when you'll need it

One day buys you the build. These four are what stand between a working agent and one you'd put in front of patients, auditors or customers — worth knowing they exist:

📏 Evaluation

"It looked fine" isn't a test. Build a dataset of real questions with expected answers, then score changes against it — LLM-as-judge for quality, exact checks for facts. LangSmith does this; today we only traced.

🛡️ Guardrails & access control

Who is asking, what may they see, what must never leave? PII redaction, per-user permissions on retrieval, and human approval before irreversible actions — non-negotiable in health, finance and legal.

💸 Cost & latency

Every loop is tokens. Cap the iterations, cache what repeats, route cheap work to a small model, and stream so it feels fast. Measure before optimising — the trace tells you where it goes.

🎛️ Fine-tuning

Almost always the last resort, not the first. Prompting, tools and retrieval fix most problems; fine-tune only for a consistent behaviour, format or tone a prompt can't buy — it can't teach facts. RAG changes what it knows, fine-tuning changes how it acts.

💡 DID YOU KNOW · the order matters more than the techniques

Reach for them in this order: get it working → measure it → make it safe → make it cheap → and only then consider fine-tuning. Teams that invert this spend months tuning a model to fix what a better tool description would have solved in an afternoon.

THANK YOU

You went from 0 to a deployed
agentic system — today. Now go build something that reaches beyond the prompt.

Code & notebooks shared LangGraph + RAG + MCP Live on Render

Alessandro Romano  ·  Questions?

SCAN BEFORE YOU GO

QR code linking to the DataHack Summit 2026 workshop feedback form

Two minutes of feedback — it shapes
the next edition of this workshop.