<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Zero-Shot to Hero]]></title><description><![CDATA[Zero-Shot to Hero]]></description><link>https://sarahbconnolly.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a909313201313c999943873/c303d0cf-cca2-445b-be26-8bc862c76fd3.jpg</url><title>Zero-Shot to Hero</title><link>https://sarahbconnolly.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 07 Sep 2026 07:19:53 GMT</lastBuildDate><atom:link href="https://sarahbconnolly.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LangChain vs. LlamaIndex: Which Should You Use for RAG?]]></title><description><![CDATA[My PDF Q&A app was built entirely on LangChain, mostly because that's what the certificate I'm working through teaches. But LangChain isn't the only framework for this — LlamaIndex is the other major ]]></description><link>https://sarahbconnolly.hashnode.dev/langchain-vs-llamaindex-which-should-you-use-for-rag</link><guid isPermaLink="true">https://sarahbconnolly.hashnode.dev/langchain-vs-llamaindex-which-should-you-use-for-rag</guid><category><![CDATA[langchain]]></category><category><![CDATA[AI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[LlamaIndex]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Sarah Connolly]]></dc:creator><pubDate>Tue, 01 Sep 2026 17:09:11 GMT</pubDate><content:encoded><![CDATA[<p>My PDF Q&amp;A app was built entirely on LangChain, mostly because that's what the certificate I'm working through teaches. But LangChain isn't the only framework for this — <strong>LlamaIndex</strong> is the other major option, and it's worth understanding the differences before you commit to one for your next project.</p>
<p>Both frameworks solve the same problem. Where they differ is in how much they do <em>for</em> you versus how much they let you configure yourself.</p>
<h2>Same pipeline, different defaults</h2>
<p>Every RAG system — regardless of framework — goes through the same basic stages: load the source documents, break them into chunks, convert those chunks into vectors, store the vectors, then at query time convert the user's question into a vector too, retrieve the closest-matching chunks, insert them into a prompt, and send that prompt to the LLM.</p>
<p>LangChain and LlamaIndex both implement all of this. The difference is how many decisions get made <em>for</em> you by default.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/b116f28e-cbce-4934-8565-9a0973a2a2c5.svg" alt="" style="display:block;margin:0 auto" />

<h2>Loading and chunking documents</h2>
<p>LangChain gives you a long list of format-specific loaders — one for plain text, one for CSVs, one for JSON, one for web pages — plus a <a href="https://reference.langchain.com/python/langchain-community/document_loaders/directory/DirectoryLoader"><code>DirectoryLoader</code></a> that can point at any of them to bulk-load a folder. It's flexible, but you're often picking the right tool from a menu.</p>
<p>LlamaIndex's answer is <a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/simpledirectoryreader/"><code>SimpleDirectoryReader</code></a>: one loader that natively handles most common formats (PDF, Word, PowerPoint, markdown, and more) and can recursively walk an entire directory tree without much configuration. It's less flexible in the sense that you're not choosing between a dozen options, but for typical use cases it's noticeably less setup.</p>
<p>Chunking looks similar on the surface — both frameworks offer a "smart" recursive splitter (LangChain's <a href="https://reference.langchain.com/python/langchain-text-splitters/character/RecursiveCharacterTextSplitter"><code>RecursiveCharacterTextSplitter</code></a>, LlamaIndex's <a href="https://developers.llamaindex.ai/python/framework-api-reference/node_parsers/sentence_splitter/"><code>SentenceSplitter</code></a>) that tries to break text at natural boundaries before falling back to harder cuts. Both also offer semantic chunking, which splits based on meaning rather than character count. LlamaIndex even provides a wrapper that lets you use any LangChain splitter inside a LlamaIndex pipeline, if you want LlamaIndex's other defaults but LangChain's specific splitting logic.</p>
<h2>Embedding and storing vectors</h2>
<p>This is where the philosophy difference becomes concrete. In LangChain, embedding and storing are two separate steps — you generate the vectors, then you store them, and you're expected to wire that together yourself if you want anything beyond the basic in-memory store.</p>
<p>In LlamaIndex, both happen in a single call: build an index from your chunks, and the embedding and storage happen together. It's a smaller amount of code to get something working, though it also means less visibility into each individual step.</p>
<p>LlamaIndex also automatically tracks metadata about each chunk as part of that index. LangChain can do this too, but it's typically something you set up manually, and the exact approach shifts depending on which vector database you're using underneath — a direct consequence of LangChain not having one single, unified vector store class the way LlamaIndex does.</p>
<h2>Retrieval and prompt handling</h2>
<p>For standard "retrieve the top few most relevant chunks" use cases, the two frameworks are functionally similar. Where they diverge is in the more advanced patterns — LangChain, for instance, has a retriever that can pull back the entire parent document a matching chunk came from, rather than just the isolated chunk, which is useful when a chunk alone lacks enough context to be useful.</p>
<p>The more interesting difference is in how each framework treats prompt customization. In LangChain, augmenting the prompt with retrieved context is its own distinct step, decoupled from everything else — which makes it easy to inspect or rewrite exactly what gets sent to the model. In LlamaIndex, that step is typically bundled together with response generation (or, if you're using a "query engine," bundled together with retrieval <em>and</em> generation). The defaults work well without much tuning, but if you want to heavily customize the actual prompt template, LangChain's more decoupled structure gives you an easier point of entry.</p>
<h2>So which one should you use?</h2>
<p>Neither framework is objectively better — they're optimized for different priorities:</p>
<p><strong>Reach for LangChain if</strong> you want fine-grained control over each step, you're combining RAG with other things (agents, tools, complex multi-step chains), or you're pulling from a wide variety of data source types where LangChain's larger integration ecosystem helps.</p>
<p><strong>Reach for LlamaIndex if</strong> you want to get a solid RAG pipeline running with less boilerplate, your use case is squarely "answer questions about documents," and you'd rather lean on sensible defaults than configure every piece by hand.</p>
<p>For my own PDF app, LangChain's separation between retrieval and prompt augmentation actually made it easier to swap in TF-IDF as a stand-in for real embeddings while debugging — I could isolate exactly which step was going wrong without untangling it from response generation. That's a LangChain-shaped advantage more than a universal one, though; a LlamaIndex version of the same app might have taken less code to get to a first working version.</p>
<p>If you're just starting out, I'd say don't overthink the choice — both frameworks are actively maintained, both cover the same fundamental RAG workflow, and skills from one transfer conceptually to the other. Pick whichever one's documentation makes more sense to you on a first read, and switch later if you hit a wall.</p>
]]></content:encoded></item><item><title><![CDATA[I Built a RAG App That Answers Questions About Any PDF]]></title><description><![CDATA[TL;DR: I built a small Flask app that lets you upload a PDF and ask it questions, grounded in that specific document — not the model's general training. Under the hood it's a classic RAG pipeline: ext]]></description><link>https://sarahbconnolly.hashnode.dev/i-built-a-rag-app-that-answers-questions-about-any-pdf</link><guid isPermaLink="true">https://sarahbconnolly.hashnode.dev/i-built-a-rag-app-that-answers-questions-about-any-pdf</guid><category><![CDATA[langchain]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Sarah Connolly]]></dc:creator><pubDate>Tue, 01 Sep 2026 13:46:30 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR:</strong> I built a small Flask app that lets you upload a PDF and ask it questions, grounded in that specific document — not the model's general training. Under the hood it's a classic RAG pipeline: extract text → split into overlapping chunks → embed each chunk as a vector → retrieve the closest-matching chunks for a question → hand those to the LLM. I tested it on a real government PDF (not a toy example) and it correctly answered a question about Medicare's risk-scoring model — but also retrieved the <em>wrong</em> chunk on a trickier question, a real example of why keyword-based retrieval isn't the same as true semantic search. Code, diagrams, and screenshots below if you want to build your own.</p>
</blockquote>
<p>I built a small app this week that does one thing: you upload a PDF, and then you can ask it questions — real questions, answered using the actual contents of that specific document, not a generic response pulled from the model's general training.</p>
<p>To prove it actually works (and not just on a fake example), I pointed it at a real government document — a 4-page PDF from the Centers for Medicare &amp; Medicaid Services on how Medicare risk-adjusts its cost and quality measures. Here it is in action:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/179b1665-336e-4c8f-a37d-58ef9a533c05.png" alt="" style="display:block;margin:0 auto" />

<p>Then I asked it something only someone who'd actually read the document would know:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/81d6a17d-a452-4a0e-8872-5437714ec5bc.png" alt="" style="display:block;margin:0 auto" />

<p>That's a real, running Flask app, answering a real question, grounded in a document it had never seen before I uploaded it. No fine-tuning, no manual pre-loading of facts — just a pipeline that reads the PDF, breaks it into searchable pieces, and hands the relevant pieces to the model when a question comes in.</p>
<p>Here's how it works, and how you can build your own version.</p>
<h3>Why you can't just paste the PDF into a prompt</h3>
<p>The tempting shortcut is to extract the text and drop the whole thing into your prompt. For a short PDF, that technically works — but it breaks down fast once documents get longer:</p>
<p>Context window limits. Most models cap how much text they can process per call. A 40-page contract won't fit, and even when it does, you're paying for every token on every single question. Signal-to-noise. If someone asks "what's the termination clause," you don't want the model wading through 40 pages to find three relevant sentences — slower, and more likely to miss the point. Reusability. If a user asks five different questions about the same document, you don't want to reprocess the whole thing from scratch every time.</p>
<p>The fix is to process the document once, break it into searchable pieces, and only pull in the pieces relevant to whatever's being asked. This pattern has a name — retrieval-augmented generation (RAG) — and a "PDF Q&amp;A" app is really just the ingestion half of a RAG pipeline with a friendly UI on top.</p>
<h3>The pieces involved</h3>
<p>File Upload — an endpoint that accepts a PDF and saves it somewhere the app can read it.</p>
<p>Text Extraction — pulling raw text out of the PDF. PDFs aren't structured like plain text files, so this step handles the parsing for you.</p>
<p>Chunking — splitting extracted text into smaller, overlapping pieces. The overlap matters: cut a chunk mid-idea and you risk losing context that spans the boundary between two chunks.</p>
<p>Embeddings — converting each chunk into a vector that captures its meaning, so you can search by meaning instead of exact keyword matches.</p>
<p>Vector Store — a database built to store and search those vectors efficiently. A question gets converted to a vector too, and the store returns whichever chunks are closest to it.</p>
<p>Retrieval + LLM — the closest-matching chunks get handed to the LLM along with the question, so it answers using the actual document instead of guessing from general knowledge.</p>
<h3>How it fits together</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/8d5b8005-20bf-48e3-98bf-f30e6520b81e.svg" alt="" style="display:block;margin:0 auto" />

<p>The key thing to notice: ingestion happens once, when the file is uploaded. Query happens every time someone asks something. You're never re-reading the whole PDF on every question — just searching against chunks you already prepared.</p>
<h3>Building it: a Flask PDF Q&amp;A app</h3>
<p>Here's the actual pipeline behind the screenshots above, using LangChain and Flask.</p>
<p><strong>Step 1: Accept the upload and extract text</strong></p>
<pre><code class="language-python">from flask import Flask, request, jsonify 
from langchain_community.document_loaders import PyPDFLoader import os

app = Flask(name) UPLOAD_FOLDER = "uploads" os.makedirs(UPLOAD_FOLDER, exist_ok=True)

@app.route("/upload", methods=["POST"]) def upload_pdf(): file = request.files["file"] path = os.path.join(UPLOAD_FOLDER, file.filename) file.save(path)

loader = PyPDFLoader(path)
pages = loader.load()  # one Document object per page

return process_document(pages)
</code></pre>
<p><a href="https://reference.langchain.com/python/langchain-community/document_loaders/pdf/PyPDFLoader">PyPDFLoader</a> handles the messy work of extracting text from the PDF's internal structure — you get back page-level text objects instead of raw bytes.</p>
<p><strong>Step 2: Split the extracted text into chunks</strong></p>
<pre><code class="language-python">from langchain.text_splitter import RecursiveCharacterTextSplitter

def process_document(pages):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,      # characters per chunk
        chunk_overlap=150,    # overlap so context isn't lost at chunk boundaries
    )
    chunks = splitter.split_documents(pages)
    return embed_and_store(chunks)
</code></pre>
<p>A chunk_size of 1000 characters is a reasonable starting point — small enough to keep retrieval precise, large enough to preserve context within a chunk.</p>
<p><strong>Step 3: Embed the chunks and store them</strong></p>
<pre><code class="language-python">from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

vector_store = None  # in a real app, key this by user/session/document id

def embed_and_store(chunks):
    global vector_store
    embeddings = OpenAIEmbeddings()
    vector_store = FAISS.from_documents(chunks, embeddings)
    return jsonify({"status": "PDF processed", "chunks_created": len(chunks)})
</code></pre>
<p><a href="https://faiss.ai/index.html">FAISS</a> is a simple, local vector store — good for learning and small apps. For production, you'd typically swap in a managed vector database, but the rest of the pipeline stays the same.</p>
<p><strong>Step 4: Answer questions using retrieval</strong></p>
<pre><code class="language-python">from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA

@app.route("/ask", methods=["POST"])
def ask_question():
    question = request.json["question"]

    retriever = vector_store.as_retriever(search_kwargs={"k": 3})  # top 3 matching chunks
    qa_chain = RetrievalQA.from_chain_type(
        llm=ChatOpenAI(model="gpt-4o-mini"),
        retriever=retriever,
    )

    answer = qa_chain.run(question)
    return jsonify({"answer": answer})
</code></pre>
<p>as_retriever(search_kwargs={"k": 3}) tells the vector store to return the 3 most relevant chunks for whatever question comes in. RetrievalQA handles the rest — combining those chunks and the question into a prompt, then sending it to the LLM.</p>
<p>Upload a PDF once, then ask questions as many times as you want — each call retrieves fresh, relevant chunks from the same stored document.</p>
<h3>Putting it through a real test</h3>
<p>I didn't just want to show clean, cherry-picked output, so here's the full run — including where it stumbled.</p>
<p>For retrieval in this demo, I swapped in TfidfVectorizer + cosine similarity from scikit-learn instead of OpenAIEmbeddings, so the whole thing runs with no API key. Same concept — convert text to vectors, compare a question's vector to every chunk's vector — just a simpler, keyword-based version of it.</p>
<p>The empty app, before anything's uploaded:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/1430b2c8-06e4-4f0b-9ca4-655e4826e01e.png" alt="" style="display:block;margin:0 auto" />

<p>After uploading the real CMS PDF, it extracted 11,149 characters and split them into 14 chunks — real numbers from the actual PdfReader + RecursiveCharacterTextSplitter run, not placeholders.</p>
<p>First question: <em>"What does the CMS-HCC model use to calculate a beneficiary's risk score?"</em></p>
<p>Retrieval pulled the exact right chunk — the one explaining that the model generates a risk score from each beneficiary's expected cost of care, with separate logic for new versus continuing enrollees. Correct, grounded answer.</p>
<p>Second question: <em>"How is the 30-day hospital readmission measure risk adjusted?"</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/560742bf-47ec-4744-8533-04de910a1cae.png" alt="" style="display:block;margin:0 auto" />

<p>This is where it got interesting. The retrieved chunk scored higher (0.526) than the first question's match — but it's the wrong chunk. It's just the list of which measures get risk adjusted, not the paragraph explaining how the readmission measure specifically works. That explanation exists elsewhere in the document but scored lower, because it doesn't repeat the exact phrase "30-day All-Cause Hospital Readmission measure" as densely.</p>
<blockquote>
<div>
<div>💡</div>
<div><strong>How retrieved chunk score is calculated</strong></div>
</div>

<p><a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html"><code>TfidfVectorizer</code></a> turns each chunk — and the question — into a vector of word weights. Each word's weight combines how often it appears <em>in that chunk</em> with how rare it is <em>across all chunks</em>, so common words like "measure" get down-weighted and distinctive words get up-weighted. <code>cosine_similarity</code> then measures the angle between the question's vector and each chunk's vector, producing a score from 0 (no shared distinctive words) to 1 (near-identical word patterns).</p>
</blockquote>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6a909313201313c999943873/40c61cdb-3695-4c3b-9f4b-fecc88ce247f.svg" alt="" style="display:block;margin:0 auto" />

<p>The important part: this process has no idea what any of the words <em>mean</em>. It's pure vocabulary overlap. Chunk 4 won because it literally repeats "30-day," "hospital," "readmission," and "measure" — the same words in the question. The methodology chunk explains the identical concept using different words ("beneficiary age," "clinical risk factors," "specialty cohorts") and scored lower purely because the phrasing doesn't match, even though the content does.</p>
<p>A real embedding model (like <a href="https://developers.openai.com/api/docs/models/text-embedding-3-small"><code>text-embedding-3-small</code></a>) doesn't have this blind spot — it maps text into a space learned from patterns across huge amounts of text, where "how is X risk-adjusted" and "risk adjustment accounts for age and clinical factors" land close together even with zero literal word overlap, because the model has learned they're related concepts, not just related words.</p>
</blockquote>
<p>That's a real, visible example of a known limitation of keyword-based retrieval: TF-IDF matches on repeated exact terms, not meaning. A true semantic embedding model (like text-embedding-3-small) recognizes that "how is X risk adjusted" and a paragraph describing risk-adjustment methodology are related, even without exact keyword overlap — which is exactly why production RAG systems use embeddings instead of keyword matching.</p>
<p>I could have left this out and just shown the clean success. But the miss is arguably the more useful result — it's a concrete, visible reason to use real embeddings once you move past a demo, instead of just taking that advice on faith.</p>
<h3>A few things to watch for if you build this yourself</h3>
<ul>
<li><p>k is a tradeoff in retriever = vector_<a href="http://store.as">store.as</a>_retriever(search_kwargs={"k": 3}). Too few retrieved chunks and the model might miss the relevant section; too many and you're back to burning tokens on irrelevant text. 3–5 is a common starting range.</p>
</li>
<li><p>Scanned PDFs won't extract cleanly. PyPDFLoader works on text-based PDFs. For scanned documents or images of text, you'll need OCR (like <a href="https://pypi.org/project/pytesseract/">pytesseract</a>) as an extra step before chunking.</p>
</li>
<li><p>This demo stores one document globally, which is fine for a single-user proof of concept but won't hold up for multiple users or documents at once. In practice, key each vector store by user session or document ID. Where this fits in the bigger picture</p>
</li>
</ul>
<p>This is the same underlying pattern behind every "chat with your data" product you've probably already used. It's also just the ingestion half of a full RAG system — there's a lot more to explore in retrieval strategies, evaluating answer quality, and knowing when RAG is (and isn't) the right tool for the job.</p>
<p>I'll be digging into those next as I continue learning. If you build your own version of this, I'd like to hear what document you pointed it at, and what it got wrong first.</p>
]]></content:encoded></item><item><title><![CDATA[5 Building Blocks I Learned Building My First LangChain App]]></title><description><![CDATA[I just wrapped up Course 1: Develop Generative AI Applications — Get Started, the first course in IBM's RAG and Agentic AI Professional Certificate. It covers the fundamentals you need before touching]]></description><link>https://sarahbconnolly.hashnode.dev/5-building-blocks-i-learned-building-my-first-langchain-app</link><guid isPermaLink="true">https://sarahbconnolly.hashnode.dev/5-building-blocks-i-learned-building-my-first-langchain-app</guid><category><![CDATA[AI]]></category><category><![CDATA[langchain]]></category><category><![CDATA[pthon]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Sarah Connolly]]></dc:creator><pubDate>Thu, 27 Aug 2026 19:55:31 GMT</pubDate><content:encoded><![CDATA[<p>I just wrapped up Course 1: Develop Generative AI Applications — Get Started, the first course in IBM's RAG and Agentic AI Professional Certificate. It covers the fundamentals you need before touching anything "agentic": prompt engineering, LangChain's core building blocks, and shipping a small GenAI-powered Flask app with structured output.</p>
<p>I'm writing this partly to reinforce what I learned, and partly because I think these five concepts are becoming baseline knowledge for anyone working near AI products — not just ML engineers. If you're evaluating AI tools, managing a team that builds them, or just trying to have an informed opinion in a planning meeting, this is a good place to start.</p>
<p>Here's what stuck with me, with small code examples for each.</p>
<h2>1. In-Context Learning</h2>
<p>The core insight: an LLM doesn't need to be retrained to handle a new task. You can teach it the pattern you want just by showing examples inside the prompt itself.</p>
<p>This is called few-shot prompting, and it's the simplest form of in-context learning:</p>
<pre><code class="language-python">python prompt = """ Classify the sentiment of each review as Positive, Negative, or Neutral.

Review: "This laptop exceeded my expectations." Sentiment: Positive

Review: "Arrived broken and support was unhelpful." Sentiment: Negative

Review: "It's fine, does what it says." Sentiment: Neutral

Review: "Best purchase I've made all year!" Sentiment: """
</code></pre>
<p>No fine-tuning, no training data pipeline — just a well-structured prompt. The model infers the pattern from the examples and applies it to the new input. This is also why prompt quality matters so much more than people expect: the examples you choose directly shape the model's output.</p>
<h2>2. Prompt Templates</h2>
<p>Once you're writing more than a couple of prompts, hardcoding strings gets messy fast. Prompt templates solve this the same way parameterized queries solve SQL injection risk and repetition — you separate the fixed structure from the variable inputs.</p>
<p>In LangChain, this looks like:</p>
<pre><code class="language-python">python from langchain.prompts import PromptTemplate

template = PromptTemplate( input_variables=["product", "audience"], template=( "Write a two-sentence product description for {product}, " "targeted at {audience}. Keep the tone confident and concise." ), )

prompt = template.format(product="a noise-cancelling headset", audience="remote workers")
</code></pre>
<p>The template is reusable across any product/audience pair. This becomes essential once your app needs to generate prompts dynamically from user input or database records, rather than from text you typed once and never touched again.</p>
<h2>3. Chains</h2>
<p>A chain links multiple steps together, where the output of one step becomes the input to the next. Instead of asking a model to do everything in a single giant prompt, you break the task into smaller, testable pieces.</p>
<p>A simple two-step chain: summarize a document, then translate the summary.</p>
<pre><code class="language-python">python from langchain.chains import LLMChain

summarize_chain = LLMChain(llm=llm, prompt=summarize_prompt) translate_chain = LLMChain(llm=llm, prompt=translate_prompt)

summary = summarize_chain.run(document=long_text) translated = translate_chain.run(text=summary, language="Spanish")
</code></pre>
<p>The benefit isn't just cleaner code — it's debuggability. If the final output is wrong, you can inspect each intermediate step to see exactly where it broke, instead of guessing what went wrong inside one massive prompt.</p>
<h2>4. Agents</h2>
<p>This is where things start to feel less like "calling an API" and more like "building a system." A chain follows a fixed sequence you define in advance. An agent decides, at runtime, which tool or action to use next based on the input it's given.</p>
<p>Conceptually:</p>
<pre><code class="language-python">python from langchain.agents import initialize_agent, Tool

tools = [ Tool(name="Calculator", func=calculator_tool, description="Use for math"), Tool(name="WebSearch", func=search_tool, description="Use for current events"), ]

agent = initialize_agent(tools, llm, agent="zero-shot-react-description") agent.run("What's the population of Portugal divided by the population of Iceland?")
</code></pre>
<p>The agent has to figure out: this requires a search (to find both populations) and a calculation (to divide them) — and in what order. That reasoning-and-tool-selection loop is the shift from "AI feature" to "AI system," and it's the foundation everything in the rest of this certificate builds on.</p>
<h2>5. Structured Output</h2>
<p>An LLM's natural output is free text. That's fine for a chat window, but useless if another piece of software needs to consume that output. Structured output means constraining the model to return data in a fixed schema — usually JSON — so your application can parse it reliably.</p>
<pre><code class="language-python">from langchain.output_parsers import StructuredOutputParser, ResponseSchema

response_schemas = [
    ResponseSchema(name="sentiment", description="Positive, Negative, or Neutral"),
    ResponseSchema(name="confidence", description="A score from 0 to 1"),
]

parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = parser.get_format_instructions()

# format_instructions gets injected into the prompt, telling the model
# exactly what JSON shape to return

parsed = parser.parse(model_output)
# parsed = {"sentiment": "Positive", "confidence": 0.92}
</code></pre>
<p>This is the piece that makes it possible to connect an LLM into a real application — a form, a dashboard, a database — instead of just a text box.</p>
<h2><strong>Why this matters beyond the course</strong></h2>
<p>None of this requires being a machine learning engineer to understand. But as agentic AI moves from research demos into production systems, I think knowing how these pieces fit together is becoming table stakes — for evaluating vendor claims, scoping what's realistic for a project, or just not glazing over when "agent" comes up in a meeting.</p>
<p>I'll be posting similar breakdowns as I move through the rest of the specialization. Next up: retrieval-augmented generation (RAG) and, eventually, multi-agent systems.</p>
<p>If you're working through similar material or have opinions on where LangChain's abstractions help vs. get in the way, I'd like to hear them.</p>
<p><em>This post covers Course 1 (Develop Generative AI Applications: Get Started) of IBM's RAG and Agentic AI Professional Certificate on Coursera.</em></p>
]]></content:encoded></item></channel></rss>