Make your AI answer questions and use tools.#
You already talked to a model. Now you give it tools. You will meet LangChain, build your first chain, and then build an agent that decides when to use a tool. The examples below show each step.
get_price for a price question. It answers directly when
the user only asks what it can do.Scaler Academy โ type along. Every code block has ๐ Copy. Every example shows its full input and output.
Mental Models & Adaptation#
Review core LLM limits. Compare prompt engineering, fine-tuning, and tool workflows.
You already know the basics#
10-Second Recap#
- โ Called an LLM with the OpenAI library
- โ
system/user/assistantmessages - โ
Key safely in
.env - โ Shipped a Summarizer + an LLM Arena
๐ ๏ธ The deal โ minimal slides, maximum doing. Everything here is the same OpenAI call you already know, organised more cleverly. Keep your LLMs & Prompting project folder open; we build directly on top of it.
3 ways to steer a model#
Prompting, RAG & Fine-Tuning#
Before you build, remember the three main ways to steer a pre-trained model. You will use the first one most of the time. You will use the second one soon. You will rarely need the third.
- โ๏ธ 1. PromptingJust tell it clearly. Free, instant, no training. 90% of real work lives here โ including everything on this page.
- ๐ 2. RAGHand it your documents at question-time so it answers from real data. Coming up next.
- ๐ 3. Fine-tuningRe-train the model on examples. It is powerful and costly but rarely needed. Use this last.
๐งญ Why this matters here. Chains, tools, agents โ it's all still option 1, organised cleverly. Nothing new to fear. Let's build.
LangChain Core & LCEL#
Build prompt templates, models, and parsers using LangChain Expression Language (LCEL).
LangChain connects the pieces#
Why LangChain (and Install It)#
With the raw OpenAI API you called the model by hand โ perfect for one call. The moment you want reusable prompts, multi-step pipelines, and memory, you'd be rebuilding the same plumbing forever. LangChain is that plumbing, pre-built.
The raw OpenAI call is a Lego brick. LangChain connects these bricks into machines.
Install it
bash
$ pip install langchain langchain-openai
The 3 new words in plain English#
LangChain code uses three names that look scary. They aren't. Read these before we touch code โ each one is a thing you already understand:
ChatPromptTemplate= a prompt with blanksA normal prompt where some parts are left as{blanks}to fill in later. Like a wedding-invite template: "Dear{name}, join us on{date}". Write once, reuse for every guest.
say it as: "my reusable prompt"ChatOpenAI= the model, in a LangChain wrapperThe exact same GPT you called with the raw OpenAI API โ just wrapped so it can snap onto other LangChain pieces. Samemodel=, sametemperature=.
say it as: "the model"StrOutputParser= unwraps the answerThe model doesn't return plain text โ it returns a package (text + metadata like token counts). This piece opens the package and hands you just the string. ("Str" = string, i.e. plain text.)
say it as: "give me just the text"
Because it builds prompts in the chat format you already know from the raw OpenAI API โ system / user / assistant messages. Same grammar, now reusable.
Feel a Template (Before Coding One)#
A template is text with blanks. You fill the blanks with a dict. The dict you pass to LangChain, such
as {"tone": "witty", ...}, tells LangChain what value goes into each blank.
Template: Write a {tone} {length} post about {topic}.
tonewitty,professional,inspiringlengthshort,medium,detailedtopicAI agents,LangChain,your first job
tone="witty",length="short",topic="AI agents"Write a witty short post about AI agents.tone="professional",length="medium",topic="LangChain"Write a professional medium post about LangChain.tone="inspiring",length="detailed",topic="your first job"Write an inspiring detailed post about your first job.
tone,
length, and topic.
What the model returns and why the parser helps#
When the model replies, you do not get plain text. You get a package called an
AIMessage. It holds the text and bookkeeping data. The parser opens the package
and returns only the text.
response.content by hand each
time. With the raw OpenAI API, you wrote response.choices[0].message.content. The parser
does that cleanup for you.Build the chain in order#
A chain is three pieces joined by |, the pipe. Read the pipe as "then":
prompt then model then parser.
- ๐ Prompt The prompt comes first. It shapes the request.
- ๐ค Model The model goes in the middle. It does the thinking.
- ๐งน Parser The parser comes last. It cleans the output into plain text.
The tray showed the pieces in this order: ๐ค Model, ๐งน Parser, ๐ Prompt. If you picked a piece too early, feedback explained what was expected next.
prompt | model | parser reads like a small assembly line.
The same chain in real code, decoded line by line#
Let's rebuild the website summarizer using LangChain. The numbered comments match the decoder below. There is no mystery code here:
summarizer_langchain.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
from scraper import fetch_website_contents # reuse the earlier scraper
load_dotenv()
prompt = ChatPromptTemplate.from_template( # โ
"Give a short, friendly summary of this website:\n\n{website}")
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) # โก
parser = StrOutputParser() # โข
chain = prompt | model | parser # โฃ
def summarize(url):
return chain.invoke({"website": fetch_website_contents(url)}) # โค
print(summarize("https://anthropic.com"))
๐ Decoder โ what each numbered line does
from_template(...)turns my text into a reusable prompt. The{website}part is the blank โ it will be filled in later, just like the playground above.- The model. The same model you called with the raw OpenAI API,
wrapped for LangChain.
temperature=0.3= mostly focused (summaries shouldn't be wildly creative). - The parser. The package-opener you just saw in the animation:
takes the model's
AIMessagepackage, hands back plain text. - The pipe
|means "then". Read aloud: "the prompt, then the model, then the parser." Data flows left โ right, exactly like the builder. invokemeans "run it". The dict{"website": ...}says which blank gets what โ the key"website"matches the{website}blank by name.
โจ The unlock. chain is now a reusable building
block. New task? Swap the template. Different model? Swap line โก. Hindi summaries? Add
one word to the prompt. That composability is LangChain's entire point.
Bonus piece: memory, decoded#
Remember the basic truth: models forget everything between calls. The fix is simple โ re-send the old messages every time. LangChain gives that a tidy home. Two tiny new words first:
HumanMessage/AIMessage= labelled chat bubblesThese store 'the human said X' and 'the AI replied Y'. They are the user and assistant roles from the OpenAI API, as Python objects.MessagesPlaceholder= a parking spot for historyA blank in your prompt that holds a list of past messages instead of one word. "Insert the whole conversation so far, right here."
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
prompt = ChatPromptTemplate.from_messages([
("system", "You are a friendly tutor."), # โ personality, like the raw API
MessagesPlaceholder("history"), # โก past turns park here
("human", "{question}"), # โข the new question
])
chain = prompt | model
history = [HumanMessage("My name is Aarav."), AIMessage("Hi Aarav!")]
print(chain.invoke({"history": history, "question": "What's my name?"}).content)
# โ "Your name is Aarav." It remembered because we re-sent the history
๐ก De-mystify this for them. The model didn't magically remember. We
re-sent the old messages, and the placeholder slotted them in. All "chatbot memory"
everywhere is exactly this trick. (One thing: this invoke returns the package โ that's why
we wrote .content. Add | parser to the chain and you wouldn't need it. See
how the pieces connect?)
๐๏ธ Speaker note. Run the summarizer live and change the template in front of them ("now make it snarky"). The target feeling: "oh โ it's just my raw OpenAI code, tidied up."
Tool-Using Agents & Capstone#
Define custom Python tools. Run a reasoning loop. Build a Smart Shop Assistant, and explore memory.
Your first small agent#
Agent = LLM + tool + loop#
Everything so far only talks. An agent is a model with a tool and the freedom to decide when to use it. You will build a small shop assistant with one skill: looking up real prices.
agent = LLM + tool + loop. The model checks, "Do I need a tool here?" If yes, it asks for the tool, reads the result, and answers. You do not hard-code when. That decision is the difference between a chatbot and an agent.
LLMs are great with language, terrible with facts they don't have โ today's price, live stock, exact math. A tool lets the model fetch truth instead of guessing. Tools cure "confident but wrong."
Our shop database is small on purpose#
Step 1 โ the tool is just a Python function
agent.py ยท part 1
import json
from openai import OpenAI
from dotenv import load_dotenv
# โ load the API key and create the OpenAI client
load_dotenv()
client = OpenAI()
PRICES = {"shoes": 799, "hat": 399, "bag": 1420, "shorts": 1299, "pants": 1699}
def get_price(item):
# โ log the tool call so learners can see when it fired
print(f"๐ง tool called: get_price({item})") # show when the tool runs
# โก look up the item and return a readable price string
return f"โน{PRICES.get(item.lower(), 'unknown')}"
Two tiny notes for the Python: PRICES is an ordinary dict standing in for a database,
and .get(item, 'unknown') means "look it up, and if it's not there, say
unknown instead of crashing." That's the entire tool โ any function you can write
can become an agent's tool.
Step 2 โ describe the tool so the model knows it exists
The model can't see your Python. You hand it a menu card describing the tool โ written as a dict (the nested braces look busy, but it's only four facts). Decoder below:
agent.py ยท part 2
tools = [{
"type": "function", # โ
"function": {
"name": "get_price", # โก
"description": "Get the price of a shop item the user asks about.", # โข
"parameters": { # โฃ
"type": "object",
"properties": {"item": {"type": "string", "description": "the item name"}},
"required": ["item"],
},
},
}]
๐ Decoder โ the menu card, four facts
- What kind of tool? A function. This is the only kind you will use for now. Just write this line as-is.
- Its name โ This must exactly match the Python function's name. This helps find it when the model asks for it.
- When to use it โ written for the model to read. This sentence is literally how the model decides whether to call your tool. Write it clearly!
- What inputs it needs โ one input called
item, which is text ("string"), and it'srequired. That's all the nesting says.
โ๏ธ The non-obvious insight. Line โข is prompt engineering in disguise. A vague description ("does stuff with items") โ the model misuses the tool. A clear one โ it behaves. Your words steer the machine, even inside JSON.
Step 3 โ the loop: think โ maybe call tool โ answer
agent.py ยท part 3
def agent(user_message):
messages = [{"role": "user", "content": user_message}]
response = client.chat.completions.create( # โ send message + tools menu
model="gpt-4o-mini", messages=messages, tools=tools)
msg = response.choices[0].message
if msg.tool_calls: # โก did it ask for a tool?
messages.append(msg)
for call in msg.tool_calls:
args = json.loads(call.function.arguments) # โข read its request, run it
result = get_price(args["item"])
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
response = client.chat.completions.create( # โฃ send it all back โ nice answer
model="gpt-4o-mini", messages=messages)
msg = response.choices[0].message
return msg.content
print(agent("How much are the shoes?")) # โ tool fires โ "โน799"
print(agent("Hi! What can you help with?")) # โ no tool โ just chats
๐ Decoder โ the loop, step by step
- Send message + menu. We send the user's message plus our tools menu. The model now knows a tool exists and may ask to use it.
- Check for a tool request.
msg.tool_calls= "did the model ask to run a tool?" If it did, this holds which tool and with what input โ e.g.get_price,item="shoes". If not, it's empty and we skip straight to the answer. - Run the tool. The model's request arrives as text, so
json.loads(...)converts it into a Python dict we can read โ then we run the real function. (Important: the model never runs code itself. It asks; your Python does.) - Send it all back. We append the tool's result to the conversation with
role: "tool"(a third role, joining system/user/assistant!) and send everything back, so the model can write a friendly final answer using the real data.
Watch the message list grow#
The whole agent is a list of messages that gets longer. One question adds four
records. These are the same records created by the messages.append(...) lines in the code.
- Step 1. The list starts with the user's question and the tools menu.
- Step 2. The model does not answer yet. It asks to run
get_priceforshoes. This is whatmsg.tool_callsholds. - Step 3. Your Python runs
get_price("shoes")and appends the result with the new role:tool. The model never runs code. It asks, and your Python does the work. - Step 4. You send the longer list back. The model writes an answer using the real price.
๐คฏ if msg.tool_calls is the core check. The model asked to run
get_price("shoes") by itself. If you give it ten tools, it can pick among them. Cursor,
support bots, and many "AI agent" systems use this same pattern with a bigger toolbox.
Quick check: think like the agent#
Before you trust the agent, check the rule. For each question, decide whether the model should call the tool or answer directly. The answers and reasons are below.
- "How much is the bag?"
Correct answer: Calls the tool.
Reason: A price question needs real data, so the model callsget_price("bag"). - "Hi! How are you today?"
Correct answer: Just answers.
Reason: This is small talk. It does not need shop facts. - "Is the hat cheaper than the shorts?"
Correct answer: Calls the tool.
Reason: Comparing prices requires real numbers. The model calls the tool twice. - "What's the capital of France?"
Correct answer: Just answers.
Reason: This is a trick question. The model knows this. The price tool cannot help. - "I have โน1500 โ can I afford the pants?"
Correct answer: Calls the tool.
Reason: It must check the real price before answering. It calls the tool and explains the budget is too low.
๐๏ธ Speaker note. Use these as class questions. The fourth question shows that a tool should not run if it cannot help.
Project: Smart Shop Assistant#
Give the agent a chat loop and run it - The Build#
Same agent() from earlier โ we give it a simple chat loop in the terminal, so this
takes a few lines:
from agent import agent # use the function you wrote
while True: # โ keep chatting until the user types quit
message = input("You: ")
if message.strip().lower() in ("quit", "exit"):
break
print("Shop:", agent(message)) # โก
๐ Decoder
- The chat loop.
input()reads each new message you type, and the loop keeps going until you typequit. We only pass the message today; your homework hint: keep ahistorylist of past turns and pass it into the agent and it gains memory โ exactly the trick from the memory section. - One agent turn.
agent(message)runs the full think โ maybe-tool โ answer loop, and we print its reply.
Run it
bash โ your project folder
$ pip install openai python-dotenv
$ python main.py
You: How much are the pants?
๐ง tool called: get_price(pants)
Shop: The pants are โน1699.
- Ask a price question. "How much are the pants?" โ watch your terminal print
๐ง tool called: get_price(pants). Your agent used its tool! - Then ask small talk. "What's your return policy?" โ no tool fires. It's deciding, not following a script.
- Compare the two. One tool question, one chat question, terminal visible โ that contrast is the whole agent idea in 15 seconds.
Example run of the working agent#
This trace shows a sample chat. Watch when the tool line appears and when the assistant answers directly.
Starting message: Hi! I'm your shop assistant. Ask me the price of anything ๐
get_price("shoes") โ โน799get_price("hat") โ โน399get_price("bag") โ โน1420
So the hat is the cheaper one!
get_price(...) โ unknownShop data: shoes โน799, hat โน399, bag โน1420, shorts โน1299, pants โน1699.
How this sample trace was made: keyword rules stand in for the model. They look
for item names, including simple singular forms. They treat price, cost,
how much, cheap, expensive, afford, and
โน as price-related words. Each item found adds one tool call. A price question about an
unknown item adds one "unknown" tool call. Anything else is a direct answer.
agent.py, GPT makes the decision more flexibly, but the loop and trace stay the same.
Industry spotlight ยท real agents use this same loop. Today, the assistant has one tool. Tomorrow, you can add tools for a database, email, and calendar. Then, it can handle tasks like 'find my order, refund it, email the customer.' Production agents use this loop with a bigger toolbox.
What you did and what comes next#
What You Did#
- ๐งญ 3 ways to steerPrompting, RAG, and fine-tuning. You will mostly use prompting.
- ๐ LangChainprompt | model | parser. These are chains you can connect.
- ๐ค AgentsLLM + tool + loop. The model decides.
- ๐๏ธ You shippedA working tool-using agent.
The road ahead, still hands-on#
- More tools (your homework). Add
check_stock(item)orapply_discount(item)and watch the agent pick the right one. - RAG โ chat with your own PDFs. The next big build: a model that answers from your documents.
- Multi-agent teams. Several agents can hand work to each other.
๐ A note on theory. We're deliberately deferring the deep "how models are built" topics โ attention, training, scaling โ until you're comfortable building. They'll land as "oh, that's why it works" instead of abstract lecture.