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.
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-tuningActually re-train on examples. Powerful, costly, rarely needed. Reach for it last.
๐งญ Why this matters here. Chains, tools, agents โ it's all still option 1, organised cleverly. Nothing new to fear. Let's build.
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 is the box of connectors that snaps 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}.
| Blank | Values from the playground |
|---|---|
tone | witty, professional, inspiring |
length | short, medium, detailed |
topic | AI agents, LangChain, your first job |
| Example input | Final prompt sent to the model |
|---|---|
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.
| Piece | Why it goes there |
|---|---|
| ๐ 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 challenge tray showed the pieces in this order: ๐ค Model, ๐งน Parser, ๐ Prompt. If you picked a later piece too early, the feedback explained which piece was expected next.
prompt | model | parser reads like a small assembly line.The same chain in real code, decoded line by line#
Rebuilding the earlier website summarizer the LangChain way. The numbered comments match the decoder below โ nothing here is mystery code:
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 bubblesJust a way to store "the human said X" and "the AI replied Y" โ the same user/assistant roles from the raw 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."
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. (That's the only kind you'll use for a long time โ just write this line as-is.)
- Its name โ must exactly match your Python function's name, so we can 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, plus the tools menu on the side.
- 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. Now the model writes a friendly 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. No shop facts are needed. - "Is the hat cheaper than the shorts?"
Correct answer: Calls the tool.
Reason: Comparing prices needs the real numbers, so it calls the tool twice. - "What's the capital of France?"
Correct answer: Just answers.
Reason: This is the trick question. The model already knows this, and 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, โน1699, before answering. It calls the tool and then says the budget is not quite enough.
๐๏ธ Speaker note. Use these as class questions. The fourth question is useful because it shows that a tool should not run when it cannot help.
Project: Smart Shop Assistant#
Give the agent a chat loop and run it#
Same agent() from Block 11 โ 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 with the agent. 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 ๐
| User message | Trace | Assistant answer |
|---|---|---|
| How much are the shoes? | get_price("shoes") โ โน799 | The shoes are โน799. Anything else? ๐ |
| Is the hat cheaper than the bag? | get_price("hat") โ โน399get_price("bag") โ โน1420 | hat: โน399 ยท bag: โน1420 So the hat is the cheaper one! |
| Do you sell laptops? | โ no tool needed, answering directly โ | I'm your shop helper! Ask me any item's price and I'll look it up in the database. ๐๏ธ |
| Hi! What can you do? | โ no tool needed, answering directly โ | I'm your shop helper! Ask me any item's price and I'll look it up in the database. ๐๏ธ |
| What's your return policy? | โ no tool needed, answering directly โ | I'm your shop helper! Ask me any item's price and I'll look it up in the database. ๐๏ธ |
| How much is the laptop? | get_price(...) โ unknown | Hmm, that item isn't in my shop. I've got shoes, hat, bag, shorts and pants! |
Shop 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 your database, email, and calendar. Then it can handle tasks such as "find my order, refund it, email the customer." Production agents, including coding agents, use this loop with a bigger toolbox.
What you did and what comes next#
What You Did#
- ๐งญ 3 ways to steerPrompting, RAG, fine-tuning โ you'll mostly prompt.
- ๐ LangChainprompt | model | parser โ chains you can snap together.
- ๐ค AgentsLLM + tool + loop. The model decides. You saw it.
- ๐๏ธ 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 handing work to each other โ once one agent feels easy.
๐ 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.