<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>PyImageSearch</title>
	<atom:link href="https://pyimagesearch.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://pyimagesearch.com/</link>
	<description>You can master Computer Vision, Deep Learning, and OpenCV - PyImageSearch</description>
	<lastBuildDate>Mon, 01 Jun 2026 11:52:03 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.5</generator>
	<item>
		<title>Apache Airflow Document Ingestion Pipeline for RAG Systems</title>
		<link>https://pyimagesearch.com/2026/06/01/apache-airflow-document-ingestion-pipeline-for-rag-systems/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Apache Airflow]]></category>
		<category><![CDATA[FastAPI]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Retrieval-Augmented Generation (RAG)]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[airflow dag]]></category>
		<category><![CDATA[airflow pipeline]]></category>
		<category><![CDATA[airflow tutorial]]></category>
		<category><![CDATA[apache airflow]]></category>
		<category><![CDATA[chunking]]></category>
		<category><![CDATA[data engineering]]></category>
		<category><![CDATA[data pipeline]]></category>
		<category><![CDATA[document ingestion]]></category>
		<category><![CDATA[document processing]]></category>
		<category><![CDATA[fastapi]]></category>
		<category><![CDATA[fastapi tutorial]]></category>
		<category><![CDATA[idempotency]]></category>
		<category><![CDATA[machine learning operations]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[orchestration]]></category>
		<category><![CDATA[pdf processing]]></category>
		<category><![CDATA[postgresql]]></category>
		<category><![CDATA[rag]]></category>
		<category><![CDATA[retrieval augmented generation]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[workflow orchestration]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54017</guid>

					<description><![CDATA[<p>Table of Contents Apache Airflow Document Ingestion Pipeline for RAG Systems Introduction to Production-Grade Document Ingestion Pipelines Why Airflow Instead of Cron Jobs or Celery? Apache Airflow Document Ingestion Pipeline Architecture Component 1: FastAPI Ingestion Service Component 2: Apache Airflow&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/06/01/apache-airflow-document-ingestion-pipeline-for-rag-systems/">Apache Airflow Document Ingestion Pipeline for RAG Systems</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Apache-Airflow-Document-Ingestion-Pipeline-RAG-Systems"><a rel="noopener" target="_blank" href="#h1-Apache-Airflow-Document-Ingestion-Pipeline-RAG-Systems">Apache Airflow Document Ingestion Pipeline for RAG Systems</a></li>

    <li id="TOC-h2-Introduction-Production-Grade-Document-Ingestion-Pipelines"><a rel="noopener" target="_blank" href="#h2-Introduction-Production-Grade-Document-Ingestion-Pipelines">Introduction to Production-Grade Document Ingestion Pipelines</a></li>
    <ul>
        <li id="TOC-h3-Why-Airflow-Instead-Cron-Jobs-Celery"><a rel="noopener" target="_blank" href="#h3-Why-Airflow-Instead-Cron-Jobs-Celery">Why Airflow Instead of Cron Jobs or Celery?</a></li>
    </ul>

    <li id="TOC-h2-Apache-Airflow-Document-Ingestion-Pipeline-Architecture"><a rel="noopener" target="_blank" href="#h2-Apache-Airflow-Document-Ingestion-Pipeline-Architecture">Apache Airflow Document Ingestion Pipeline Architecture</a></li>
    <ul>
        <li id="TOC-h3-Component-1-FastAPI-Ingestion-Service"><a rel="noopener" target="_blank" href="#h3-Component-1-FastAPI-Ingestion-Service">Component 1: FastAPI Ingestion Service</a></li>
        <li id="TOC-h3-Component-2-Apache-Airflow"><a rel="noopener" target="_blank" href="#h3-Component-2-Apache-Airflow">Component 2: Apache Airflow</a></li>
        <li id="TOC-h3-Component-3-PostgreSQL-Database"><a rel="noopener" target="_blank" href="#h3-Component-3-PostgreSQL-Database">Component 3: PostgreSQL Database</a></li>
        <li id="TOC-h3-Component-4-Shared-Volume"><a rel="noopener" target="_blank" href="#h3-Component-4-Shared-Volume">Component 4: Shared Volume</a></li>
    </ul>

    <li id="TOC-h2-Project-Structure"><a rel="noopener" target="_blank" href="#h2-Project-Structure">Project Structure</a></li>
    <ul>
        <li id="TOC-h3-Understanding-Structure"><a rel="noopener" target="_blank" href="#h3-Understanding-Structure">Understanding the Structure</a></li>
    </ul>

    <li id="TOC-h2-Database-Schema-Design-Document-Ingestion-Pipelines"><a rel="noopener" target="_blank" href="#h2-Database-Schema-Design-Document-Ingestion-Pipelines">Database Schema Design for Document Ingestion Pipelines</a></li>
    <ul>
        <li id="TOC-h3-documents-Table"><a rel="noopener" target="_blank" href="#h3-documents-Table">The documents Table</a></li>
        <li id="TOC-h3-chunks-Table"><a rel="noopener" target="_blank" href="#h3-chunks-Table">The chunks Table</a></li>
        <li id="TOC-h3-pipeline-runs-Table"><a rel="noopener" target="_blank" href="#h3-pipeline-runs-Table">The pipeline_runs Table</a></li>
        <li id="TOC-h3-Why-Hashes-Matter"><a rel="noopener" target="_blank" href="#h3-Why-Hashes-Matter">Why Hashes Matter</a></li>
        <li id="TOC-h3-Why-Idempotency-Matters"><a rel="noopener" target="_blank" href="#h3-Why-Idempotency-Matters">Why Idempotency Matters</a></li>
        <li id="TOC-h3-Database-Session-Management"><a rel="noopener" target="_blank" href="#h3-Database-Session-Management">Database Session Management</a></li>
    </ul>

    <li id="TOC-h2-Building-FastAPI-Document-Ingestion-Service"><a rel="noopener" target="_blank" href="#h2-Building-FastAPI-Document-Ingestion-Service">Building a FastAPI Document Ingestion Service</a></li>

    <li id="TOC-h2-Designing-Apache-Airflow-DAG"><a rel="noopener" target="_blank" href="#h2-Designing-Apache-Airflow-DAG">Designing an Apache Airflow DAG</a></li>
    <ul>
        <li id="TOC-h3-Task-1-Fetch-Pending-Documents"><a rel="noopener" target="_blank" href="#h3-Task-1-Fetch-Pending-Documents">Task 1: Fetch Pending Documents</a></li>
        <li id="TOC-h3-Task-2-Parse-Documents"><a rel="noopener" target="_blank" href="#h3-Task-2-Parse-Documents">Task 2: Parse Documents</a></li>
        <li id="TOC-h3-Task-3-Chunk-Documents"><a rel="noopener" target="_blank" href="#h3-Task-3-Chunk-Documents">Task 3: Chunk Documents</a></li>
        <li id="TOC-h3-Task-4-Validate-Chunks"><a rel="noopener" target="_blank" href="#h3-Task-4-Validate-Chunks">Task 4: Validate Chunks</a></li>
        <li id="TOC-h3-Task-5-Mark-Complete"><a rel="noopener" target="_blank" href="#h3-Task-5-Mark-Complete">Task 5: Mark Complete</a></li>
        <li id="TOC-h3-Why-This-DAG-Structure-Works"><a rel="noopener" target="_blank" href="#h3-Why-This-DAG-Structure-Works">Why This DAG Structure Works</a></li>
    </ul>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Apache-Airflow-Document-Ingestion-Pipeline-RAG-Systems"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Apache-Airflow-Document-Ingestion-Pipeline-RAG-Systems">Apache Airflow Document Ingestion Pipeline for RAG Systems</a></h2>



<p>In this lesson, you will learn how to design a production-grade document ingestion pipeline using Apache Airflow. We will build a system that accepts PDF uploads via FastAPI and orchestrates their processing using an Airflow DAG (Directed Acyclic Graph). You will see how to structure ingestion pipelines with idempotency, status tracking, and PostgreSQL-backed metadata. By the end of this lesson, you will understand how Airflow fits into modern RAG-style document ingestion workflows.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png" target="_blank" rel=" noreferrer noopener"><img fetchpriority="high" decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?lossy=2&strip=1&webp=1" alt="apache-airflow-document-ingestion-pipeline-rag-systems-featured.png" class="wp-image-54031" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/apache-airflow-document-ingestion-pipeline-rag-systems-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>This lesson is the 1st in a 2-part series on <strong>Document Ingestion with Airflow</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/8b2ey" target="_blank" rel="noreferrer noopener">Apache Airflow Document Ingestion Pipeline for RAG Systems</a></strong></em> <strong>(this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to design and orchestrate a production-ready ingestion pipeline with Apache Airflow, FastAPI, and PostgreSQL, </strong><em><strong>just keep reading</strong></em><strong>.</strong></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction-Production-Grade-Document-Ingestion-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction-Production-Grade-Document-Ingestion-Pipelines">Introduction to Production-Grade Document Ingestion Pipelines</a></h2>



<p>If you have ever built a Retrieval-Augmented Generation (RAG) system, you know that ingestion is the hardest part. Not the embeddings. Not the vector search. Not even the prompt engineering. The hardest part is reliably getting documents into your system, parsing them correctly, chunking them intelligently, and tracking every step along the way.</p>



<p>Why? Because ingestion is where the real world meets your clean ML architecture. PDFs are corrupted. Files are massive. Network requests fail halfway through. And when something breaks, you need to know exactly which document failed, why it failed, and how to restart processing without duplicating work or losing data.</p>



<p>This is where orchestration becomes critical. You need a system that can schedule work, retry failures, track progress, and give you observability into every stage of your pipeline. For ML ingestion pipelines, Apache Airflow is one of the best tools for this job.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Airflow-Instead-Cron-Jobs-Celery"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Airflow-Instead-Cron-Jobs-Celery">Why Airflow Instead of Cron Jobs or Celery?</a></h3>



<p>You might ask: why not just use cron jobs to trigger a Python script every minute? Or why not use Celery for task queueing? The answer is observability and resilience.</p>



<p>Cron jobs give you scheduling, but no visibility into what failed or why. When a cron job fails at 3am, you find out when users complain. You have no task history, no retry logic, and no dependency management. Celery gives you distributed task execution, but it does not provide workflow orchestration. You have to manually chain tasks, handle retries, and build your own monitoring.</p>



<p>Airflow gives you all of this out of the box. Think of it as a conveyor belt with inspection stations. Every document moves through the same sequence of steps (parse, chunk, validate), and at each station, Airflow records what happened. If a step fails, Airflow retries it automatically. If the entire system crashes, Airflow resumes from where it left off. The web UI shows you exactly which documents are stuck and why.</p>



<p>For production ML systems, this observability is not optional. It is the difference between debugging for hours and knowing immediately which PDF caused the parser to crash.</p>



<p>In this lesson, you will learn how to build a production-grade document ingestion pipeline using Apache Airflow. We will design a complete system that accepts PDF uploads via a REST (Representational State Transfer) API and orchestrates their processing using an Airflow DAG, with full deduplication and idempotency guarantees backed by PostgreSQL.</p>



<p>More importantly, you will understand why Airflow fits ingestion better than training or inference, and where its limitations begin. This foundation prepares you for the next lesson, where we implement the shared parsing and chunking logic and later transition to Argo Workflows for GPU-based ML compute.</p>



<p>By the end of this part, you will have a working control plane for your ingestion pipeline that you can extend for your own RAG systems and document processing workflows.</p>



<p>Let’s get started.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Apache-Airflow-Document-Ingestion-Pipeline-Architecture"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Apache-Airflow-Document-Ingestion-Pipeline-Architecture">Apache Airflow Document Ingestion Pipeline Architecture</a></h2>



<p>Before we dive into code, let&#8217;s understand what we are building. <strong>Figure 1</strong> shows the high-level architecture of our ingestion pipeline.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/06/image-4.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="203" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54033" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4.png?size=126x41&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4-300x98.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4.png?size=378x123&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4.png?size=504x164&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-4.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> High-level architecture of the Airflow-based ML ingestion pipeline. Documents flow from the FastAPI service through Airflow tasks and into PostgreSQL.</figcaption></figure></div>


<p>Our system consists of the following 4 main components.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Component-1-FastAPI-Ingestion-Service"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Component-1-FastAPI-Ingestion-Service">Component 1: FastAPI Ingestion Service</a></h3>



<p>This is the entry point for documents. It exposes a REST API on port 8000 that accepts PDF uploads. When a document arrives, the service performs three critical operations. First, it computes a SHA-256 hash of the file content to detect duplicates. Second, it saves the file to a shared volume that Airflow can access. Third, it inserts a record into the documents table in PostgreSQL with status set to <code data-enlighter-language="python" class="EnlighterJSRAW">PENDING</code>.</p>



<p>The service does not process the document. It only accepts it and marks it for processing. This separation of concerns is intentional. Ingestion and processing are different responsibilities with different scaling characteristics.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Component-2-Apache-Airflow"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Component-2-Apache-Airflow">Component 2: Apache Airflow</a></h3>



<p>Airflow is the orchestration layer. It runs two main processes: the scheduler and the webserver. The scheduler monitors our DAG (Directed Acyclic Graph) and triggers it on a schedule. In our case, the DAG runs every minute and looks for documents with status <code data-enlighter-language="python" class="EnlighterJSRAW">PENDING</code>.</p>



<p>When the DAG runs, it executes a series of tasks in order: fetch pending documents, parse PDFs into pages, chunk the text, validate chunk quality, and mark documents as complete. Each task is idempotent, meaning you can run it multiple times safely. Each task also has retry logic, so transient failures do not require manual intervention.</p>



<p>The webserver provides a UI on port 8080 where you can monitor DAG runs, inspect task logs, and manually trigger runs when needed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Component-3-PostgreSQL-Database"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Component-3-PostgreSQL-Database">Component 3: PostgreSQL Database</a></h3>



<p>PostgreSQL serves 2 purposes in our system. First, it stores Airflow&#8217;s own metadata (DAG runs, task instances, logs). Second, it stores our application data in a separate database called <code data-enlighter-language="python" class="EnlighterJSRAW">ml_orchestration</code>.</p>



<p>Our application database has 3 main tables. The documents table tracks every uploaded file with its hash, path, and processing status. The chunks table stores the parsed and chunked text with deduplication via content hashes. The <code data-enlighter-language="python" class="EnlighterJSRAW">pipeline_runs</code> table records every DAG execution with metrics like how many documents were processed and how many chunks were created.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Component-4-Shared-Volume"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Component-4-Shared-Volume">Component 4: Shared Volume</a></h3>



<p>The fourth component is not visible in the diagram, but it is critical. All containers share a Docker volume mounted at <code data-enlighter-language="python" class="EnlighterJSRAW">/tmp/ml_orchestration/uploads</code>. When the FastAPI service saves a file, Airflow tasks can read it directly without network transfers or complex file synchronization.</p>



<p>This architecture gives us several important properties. First, we have a clear separation between ingestion (FastAPI) and processing (Airflow). Second, we have observability through Airflow&#8217;s UI and PostgreSQL queries. Third, we have idempotency through content hashing and status tracking. Fourth, we have reliability through Airflow&#8217;s retry mechanisms.</p>



<p>Now let us see how this maps to the actual codebase.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure">Project Structure</a></h2>



<p>We first need to review our project directory structure.</p>



<p>Start by accessing this tutorial’s <em><strong>“Downloads”</strong></em> section to retrieve the source code and example images.</p>



<p>From there, take a look at the directory structure:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="1">├── airflow_project/ # Airflow orchestration system
│ ├── dags/
│ │ └── ingest_documents_dag.py # Main DAG: orchestrates PDF→chunks pipeline
│ │
│ ├── ingestion_service/ # FastAPI REST API for file uploads
│ │ ├── __init__.py
│ │ ├── main.py # Upload endpoint with deduplication
│ │ └── requirements.txt # FastAPI, Uvicorn dependencies
│ │
│ ├── docker-compose.yml # Orchestrates 5 services (Postgres, Airflow, API)
│ ├── Dockerfile # Airflow container image
│ ├── Dockerfile.service # FastAPI service container image
│ └── init-db.sh # PostgreSQL database initialization script
│
└── shared/ # Shared utilities (used by Airflow)
├── data_models/
│ ├── __init__.py
│ └── models.py # Pydantic schemas (Document, Chunk, PipelineRun)
│
├── parsing/
│ ├── __init__.py
│ ├── pdf_parser.py # PyPDF extraction logic
│ ├── chunker.py # Sliding window text chunking
│ └── deduplication.py # Content hashing utilities
│
├── storage/
│ ├── __init__.py
│ ├── database.py # SQLAlchemy session management (session_scope, get_session)
│ └── models.py # ORM models (DocumentModel, ChunkModel, PipelineRunModel)
│
├── utils/
│ ├── __init__.py
│ ├── hashing.py # SHA-256 file/content hashing
│ └── logging.py # Structured logging (get_logger)
│
├── __init__.py
└── requirements.txt # Shared dependencies (SQLAlchemy, Pydantic, PyPDF)
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-Structure"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-Structure">Understanding the Structure</a></h3>



<p>This project consists of 2 main directories that work together to create a production-grade document ingestion pipeline.</p>



<h4 class="wp-block-heading">The airflow_project/ Directory</h4>



<p>This folder contains everything for document ingestion using Apache Airflow. Think of it as your document processing factory &#8211; where raw PDFs enter the system and emerge as structured, searchable chunks.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">dags/ingest_documents_dag.py</code> file defines our workflow with five sequential tasks: fetch pending documents from the database, parse PDFs with PyPDF, split text into overlapping chunks, validate chunk quality, and mark documents complete. Each task is idempotent (safe to retry) and includes granular error handling so one corrupted PDF does not block an entire batch.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">ingestion_service/</code> subdirectory runs a FastAPI REST API on port 8000. Users upload PDFs via HTTP POST. The service computes a SHA-256 hash, checks for duplicates, saves the file to a shared volume, and inserts a database record with <code data-enlighter-language="python" class="EnlighterJSRAW">status=PENDING</code>. It deliberately does not process the file — that separation keeps uploads fast (users get immediate feedback) while heavy processing happens asynchronously in Airflow.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code> file orchestrates five containers: PostgreSQL (dual purpose: stores Airflow&#8217;s metadata and our application data in separate databases), Airflow webserver (UI on port 8080), Airflow scheduler (triggers the DAG every minute), init container (one-time database setup), and the ingestion service (API on port 8000). The critical piece is the shared </p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">/tmp/ml_orchestration/uploads</code> volume mounted into both Airflow containers and the API service &#8211; this lets Airflow read files the API writes without network transfers.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">Dockerfile</code> builds the Airflow container with necessary Python dependencies. The <code data-enlighter-language="python" class="EnlighterJSRAW">Dockerfile.service</code> builds the FastAPI container. The <code data-enlighter-language="python" class="EnlighterJSRAW">init-db.sh</code> script runs automatically when PostgreSQL starts, creating the <code data-enlighter-language="python" class="EnlighterJSRAW">ml_orchestration</code> database and <code data-enlighter-language="python" class="EnlighterJSRAW">mlops</code> user with proper permissions.</p>



<h4 class="wp-block-heading">The shared/ Directory</h4>



<p>This is your reusable logic layer. Everything here is pure Python business logic with zero Airflow dependencies.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">data_models/models.py</code> file contains Pydantic schemas that enforce data structure. Every document has a filename, file path, content hash, and status. Every chunk has text, a content hash, and a document reference. These schemas validate data at the API boundary and prevent type mismatches.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">parsing/</code> subdirectory implements document processing. The <code data-enlighter-language="python" class="EnlighterJSRAW">pdf_parser.py</code> module uses PyPDF to extract text page by page, preserving metadata like title and author. The <code data-enlighter-language="python" class="EnlighterJSRAW">chunker.py</code> module implements sliding window chunking (512 words with 50-word overlap) to split long documents while maintaining context across boundaries. The <code data-enlighter-language="python" class="EnlighterJSRAW">deduplication.py</code> module computes SHA-256 hashes to detect identical content both at the document and chunk level.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">storage/</code> subdirectory manages all database interaction. The <code data-enlighter-language="python" class="EnlighterJSRAW">database.py</code> file provides 2 session management utilities: <code data-enlighter-language="python" class="EnlighterJSRAW">session_scope()</code> (context manager for Airflow tasks with automatic commit/rollback) and <code data-enlighter-language="python" class="EnlighterJSRAW">get_session()</code> (generator for FastAPI dependency injection). The <code data-enlighter-language="python" class="EnlighterJSRAW">models.py</code> file defines SQLAlchemy ORM classes that map Python objects to PostgreSQL tables &#8211; <code data-enlighter-language="python" class="EnlighterJSRAW">DocumentModel</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">ChunkModel</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">PipelineRunModel</code>.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">utils/</code> subdirectory contains 2 essential helpers. The <code data-enlighter-language="python" class="EnlighterJSRAW">hashing.py</code> module computes SHA-256 hashes for both files (read in chunks to handle large PDFs) and strings (for chunk deduplication). The <code data-enlighter-language="python" class="EnlighterJSRAW">logging.py</code> module provides the <code data-enlighter-language="python" class="EnlighterJSRAW">get_logger()</code> function that returns a configured logger with consistent formatting across the entire system.</p>



<p>Now that you understand where everything lives and why, let&#8217;s dive into building the system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Database-Schema-Design-Document-Ingestion-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Database-Schema-Design-Document-Ingestion-Pipelines">Database Schema Design for Document Ingestion Pipelines</a></h2>



<p>The database schema is the backbone of our ingestion pipeline. <strong>Figure </strong><strong>2</strong> shows the 3 main tables and their relationships.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/06/image-5.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="335" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54038" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5.png?size=126x68&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5-300x161.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5.png?size=378x203&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5.png?size=504x271&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-5.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Database schema showing the <code>documents</code>, <code>chunks</code>, and <code>pipeline_runs</code> tables with their relationships and key columns.</figcaption></figure></div>


<p>Let&#8217;s examine each table and understand the design decisions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-documents-Table"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-documents-Table">The documents Table</a></h3>



<p>This table tracks every uploaded file. The <code data-enlighter-language="python" class="EnlighterJSRAW">id</code> column is an auto-incrementing primary key. The <code data-enlighter-language="python" class="EnlighterJSRAW">filename</code> stores the original name (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">research_paper.pdf</code>). The <code data-enlighter-language="python" class="EnlighterJSRAW">file_path</code> stores the absolute path where the file is saved on disk.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">content_hash</code> column is critical. It stores the SHA-256 hash of the entire file content. This hash serves 2 purposes. First, it detects duplicate uploads. If 2 users upload the same PDF with different filenames, we catch it immediately. Second, it enables idempotency. If we need to reprocess a document, we can verify the file content has not changed.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">status</code> column uses a PostgreSQL ENUM with 4 values: <code data-enlighter-language="python" class="EnlighterJSRAW">PENDING</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">PROCESSING</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">COMPLETED</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">FAILED</code>. This drives the entire workflow. The FastAPI service sets status to <code data-enlighter-language="python" class="EnlighterJSRAW">PENDING</code>. When the DAG completes successfully, Airflow updates it to <code data-enlighter-language="python" class="EnlighterJSRAW">COMPLETED</code>. If any task fails, it becomes <code data-enlighter-language="python" class="EnlighterJSRAW">FAILED</code>. (The <code data-enlighter-language="python" class="EnlighterJSRAW">PROCESSING</code> state is available for systems that want to mark documents as in-progress, though our implementation goes directly from <code data-enlighter-language="python" class="EnlighterJSRAW">PENDING</code> to <code data-enlighter-language="python" class="EnlighterJSRAW">COMPLETED</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">FAILED</code>.)</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">created_at</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">updated_at</code> columns provide audit trails. We know exactly when each document entered the system and when it was last modified.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-chunks-Table"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-chunks-Table">The chunks Table</a></h3>



<p>This table stores the processed text chunks. The <code data-enlighter-language="python" class="EnlighterJSRAW">document_id</code> foreign key creates a one-to-many relationship with documents. One document produces many chunks.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">chunk_index</code> tracks the order of chunks within a document. Chunk 0 is the first chunk, chunk 1 is the second, and so on. This ordering is important for maintaining context.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">text</code> column holds the actual chunk content. The <code data-enlighter-language="python" class="EnlighterJSRAW">content_hash</code> is the SHA-256 of this text. Just like with documents, this prevents duplicate chunks. If the same text appears in multiple places (common in academic papers with repeated abstracts), we store it once.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">page_number</code> tracks which PDF page the chunk came from. This is useful for providing citations back to users. The <code data-enlighter-language="python" class="EnlighterJSRAW">token_count</code> provides a rough estimate of length (we use word count as a proxy for tokens), which helps with embedding model limits later.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-pipeline-runs-Table"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-pipeline-runs-Table">The pipeline_runs Table</a></h3>



<p>This table tracks every DAG execution. The <code data-enlighter-language="python" class="EnlighterJSRAW">pipeline_type</code> column will eventually distinguish between <code data-enlighter-language="python" class="EnlighterJSRAW">airflow</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">argo</code> runs. For now, it is always <code data-enlighter-language="python" class="EnlighterJSRAW">airflow</code>.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">run_id</code> is Airflow&#8217;s unique execution identifier. It looks like <code data-enlighter-language="python" class="EnlighterJSRAW">manual__2026-01-26T09:56:12.565856+00:00</code>. This connects our table to Airflow&#8217;s internal metadata.</p>



<p>The status column tracks whether the entire pipeline run succeeded or failed. The <code data-enlighter-language="python" class="EnlighterJSRAW">started_at</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">completed_at</code> timestamps measure execution time.</p>



<p>The metrics columns (<code data-enlighter-language="python" class="EnlighterJSRAW">documents_processed</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">chunks_created</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">embeddings_created</code>) provide observability. You can query this table to see how many documents you have processed over time or track your processing rate.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">run_metadata</code> column is a JSON field for flexible additional data. We store the DAG ID and execution date here.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Hashes-Matter"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Hashes-Matter">Why Hashes Matter</a></h3>



<p>Content hashing is not optional in production ML systems. Without hashes, you cannot detect duplicates. Users will upload the same research paper five times, creating 5 sets of chunks and wasting embedding compute and storage.</p>



<p>Without hashes, you cannot implement idempotency. If Airflow retries a task, you might create duplicate chunks or corrupt existing data. With hashes, every operation checks &#8220;does this hash already exist?&#8221; before creating new records.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Idempotency-Matters"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Idempotency-Matters">Why Idempotency Matters</a></h3>



<p>Idempotency means you can run an operation multiple times and get the same result. This is essential in distributed systems where failures are normal. If your DAG fails halfway through, you should be able to restart it safely.</p>



<p>Our design achieves idempotency through 3 mechanisms. First, content hashes prevent duplicate records. Second, status tracking prevents reprocessing completed documents. Third, task-level checks (e.g., &#8220;does this chunk hash already exist?&#8221;) ensure partial failures are recoverable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Database-Session-Management"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Database-Session-Management">Database Session Management</a></h3>



<p>Before we dive into the ingestion service and DAG code, we need to understand how we connect to the database. All our code uses 2 key utilities from <code data-enlighter-language="python" class="EnlighterJSRAW">shared/storage/database.py</code>: <code data-enlighter-language="python" class="EnlighterJSRAW">session_scope()</code> for Airflow tasks and <code data-enlighter-language="python" class="EnlighterJSRAW">get_session()</code> for FastAPI.</p>



<p>Here is the complete database connection code:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="3"># shared/storage/database.py
import logging
import os
from contextlib import contextmanager
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

logger = logging.getLogger(__name__)

# Base class for all ORM models
Base = declarative_base()

# Database connection string from environment
DATABASE_URL = os.getenv(
    "ML_ORCHESTRATION_DB_URI",
    "postgresql://mlops:mlops_password@localhost:5432/ml_orchestration"
)

def get_engine():
    """
    Create and return a SQLAlchemy engine with connection pooling.
    """
    engine = create_engine(
        DATABASE_URL,
        pool_pre_ping=True,  # Verify connections before using
        pool_size=5,
        max_overflow=10,
        echo=False  # Set to True for SQL query logging
    )
    return engine

@contextmanager
def session_scope():
    """
    Provide a transactional scope for database operations.
    
    Usage in Airflow tasks:
        with session_scope() as session:
            documents = session.query(DocumentModel).all()
    
    This ensures:
    - Automatic commit on success
    - Automatic rollback on exception  
    - Proper connection cleanup
    """
    engine = get_engine()
    SessionLocal = sessionmaker(bind=engine)
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

def get_session() -> Generator[Session, None, None]:
    """
    FastAPI dependency for database sessions.
    
    Usage:
        @app.post("/documents")
        async def upload(session: Session = Depends(get_session)):
            # Use session here
    
    FastAPI calls this function for each request and handles cleanup.
    """
    engine = get_engine()
    SessionLocal = sessionmaker(bind=engine)
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()</pre>



<p>Let us break down these utilities.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">get_engine()</code> function creates a SQLAlchemy engine, which manages the connection pool to PostgreSQL. The <code data-enlighter-language="python" class="EnlighterJSRAW">pool_pre_ping=True</code> parameter tells SQLAlchemy to test each connection before using it. This handles cases where the database was restarted or connections went stale.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">pool_size=5</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">max_overflow=10</code> settings control connection pooling. We maintain 5 persistent connections and can create up to 10 additional temporary connections under load. This prevents overwhelming the database with thousands of connections.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">session_scope()</code> context manager is used throughout our Airflow DAG tasks. It provides a transactional scope with automatic cleanup. When you use with <code data-enlighter-language="python" class="EnlighterJSRAW">session_scope() as session:</code>, the context manager creates a session, executes your code, commits the transaction if successful, or rolls back if an exception occurs. The <code data-enlighter-language="python" class="EnlighterJSRAW">finally</code> block ensures the connection is always closed.</p>



<p>This pattern prevents common bugs like forgetting to commit, leaking connections, or leaving transactions open after errors.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">get_session()</code> generator is designed for FastAPI&#8217;s dependency injection system. FastAPI calls this function for each HTTP request and automatically handles cleanup when the request completes. You never need to manually close the session in your endpoint code.</p>



<p>These 2 utilities ensure database operations are safe, consistent, and clean across both Airflow and FastAPI. Now let us see how the ingestion service uses <code data-enlighter-language="python" class="EnlighterJSRAW">get_session()</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Building-FastAPI-Document-Ingestion-Service"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Building-FastAPI-Document-Ingestion-Service">Building a FastAPI Document Ingestion Service</a></h2>



<p>The FastAPI service is the entry point for documents. Let us walk through the code line by line to understand how it works.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/06/image-6-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="994" height="1024" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6-994x1024.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54042" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6.png?size=126x130&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6-291x300.png?lossy=2&amp;strip=1&amp;webp=1 291w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6.png?size=378x389&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6.png?size=504x519&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6.png?size=630x649&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6-768x791.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6-994x1024.png?lossy=2&amp;strip=1&amp;webp=1 994w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-6-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1049w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Request flow through the FastAPI ingestion service showing validation, storage, hashing, duplication check, and database insertion.</figcaption></figure></div>


<p>Here is the complete upload endpoint from <code data-enlighter-language="python" class="EnlighterJSRAW">airflow_project/ingestion_service/main.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="5">@app.post("/documents", response_model=DocumentResponse, status_code=201)
async def upload_document(
    file: UploadFile = File(...),
    session: Session = Depends(lambda: next(get_session()))
):
    """
    Upload a new document.
    
    The document will be stored and marked as PENDING for processing.
    """
    logger.info(f"Uploading document: {file.filename}")
    
    # Validate file type
    if not file.filename.lower().endswith('.pdf'):
        raise HTTPException(
            status_code=400,
            detail="Only PDF files are supported"
        )
    
    try:
        # Save file to disk
        file_path = UPLOAD_DIR / f"{datetime.utcnow().timestamp()}_{file.filename}"
        
        with open(file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        
        # Compute file hash and size
        content_hash = hash_file(str(file_path))
        file_size = file_path.stat().st_size
        
        # Check for duplicates
        existing_doc = session.query(DocumentModel).filter(
            DocumentModel.content_hash == content_hash
        ).first()
        
        if existing_doc:
            logger.warning(f"Duplicate document detected: {content_hash}")
            file_path.unlink()
            raise HTTPException(
                status_code=409,
                detail=f"Document already exists with ID {existing_doc.id}"
            )
        
        # Create document record
        document = DocumentModel(
            filename=file.filename,
            file_path=str(file_path),
            content_hash=content_hash,
            file_size=file_size,
            mime_type="application/pdf",
            status=DocumentStatus.PENDING
        )
        
        session.add(document)
        session.commit()
        session.refresh(document)
        
        logger.info(f"Document uploaded successfully: ID {document.id}")
        
        return DocumentResponse.from_orm(document)
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Failed to upload document: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))</pre>



<p>Let us break this down step by step.</p>



<p>The function signature uses FastAPI’s dependency injection. The <code data-enlighter-language="python" class="EnlighterJSRAW">file</code> parameter comes from the HTTP request as multipart form data. The <code data-enlighter-language="python" class="EnlighterJSRAW">session</code> parameter is injected by FastAPI using <code data-enlighter-language="python" class="EnlighterJSRAW">Depends()</code>. This gives us a database session without manual connection management.</p>



<p>The first operation is file type validation. We only accept PDFs for this lesson, so we check the filename extension. If it is not a PDF, we raise an HTTP 400 error immediately. Production systems might also validate file size, scan for malware, or check MIME types, but we keep it simple here.</p>



<p>Next, we save the file to disk. The <code data-enlighter-language="python" class="EnlighterJSRAW">UPLOAD_DIR</code> is <code data-enlighter-language="python" class="EnlighterJSRAW">/tmp/ml_orchestration/uploads</code>. This directory is mounted as a Docker volume, which means all containers can access it. We prefix the filename with a UTC timestamp to avoid collisions. If 2 users upload files named <code data-enlighter-language="python" class="EnlighterJSRAW">paper.pdf</code>, they become <code data-enlighter-language="python" class="EnlighterJSRAW">1769421678.801241_paper.pdf</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">1769421690.123456_paper.pdf</code>.</p>



<p>We use <code data-enlighter-language="python" class="EnlighterJSRAW">shutil.copyfileobj()</code> to stream the file content from the upload to disk. This is memory-efficient because it processes the file in chunks rather than loading the entire file into RAM.</p>



<p>After saving, we compute 2 important values. The <code data-enlighter-language="python" class="EnlighterJSRAW">hash_file()</code> function reads the entire file and computes its SHA-256 hash. This is a cryptographic hash function that produces a unique 64-character hexadecimal string for the file content. Even a single byte change produces a completely different hash. We also get the file size in bytes using <code data-enlighter-language="python" class="EnlighterJSRAW">file_path.stat().st_size</code>.</p>



<p>The next step is critical: duplicate detection. We query the database for any existing document with the same content hash. If we find one, we know this exact file has been uploaded before, even if it has a different filename. We delete the newly uploaded file with <code data-enlighter-language="python" class="EnlighterJSRAW">file_path.unlink()</code> and return an HTTP 409 Conflict error with the ID of the existing document. This prevents duplicate processing.</p>



<p>If the document is unique, we create a new <code data-enlighter-language="python" class="EnlighterJSRAW">DocumentModel</code> instance. Notice the status field is set to <code data-enlighter-language="python" class="EnlighterJSRAW">DocumentStatus.PENDING</code>. This tells Airflow that the document needs processing. We do not set it to <code data-enlighter-language="python" class="EnlighterJSRAW">PROCESSING</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">COMPLETED</code> because the upload service does not process documents. It only accepts them.</p>



<p>We add the model to the session, commit the transaction, and refresh the model to get the auto-generated ID. Finally, we return a <code data-enlighter-language="python" class="EnlighterJSRAW">DocumentResponse</code> with all the document details. The HTTP status code is 201 Created, which is the correct status for successful resource creation.</p>



<p>The error handling is worth noting. We re-raise <code data-enlighter-language="python" class="EnlighterJSRAW">HTTPException</code> instances without modification because FastAPI knows how to convert them to HTTP responses. For all other exceptions, we log the error and return an HTTP 500 with the error message. In production, you would want more sophisticated error handling (do not expose internal errors to clients), but this is sufficient for a lesson.</p>



<p><strong>What This Service Does Not Do</strong></p>



<p>Notice what is missing from this code. There is no PDF parsing. No text chunking. No embedding generation. The service has one responsibility: accept files and mark them for processing. This separation is intentional.</p>



<p>Ingestion and processing are different concerns. Ingestion must be fast and available. Users should be able to upload files without waiting for heavyweight processing. Processing can happen asynchronously, can retry on failure, and can take as long as needed.</p>



<p>This is where Airflow enters the picture. Let us see how the DAG processes these pending documents.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Designing-Apache-Airflow-DAG"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Designing-Apache-Airflow-DAG">Designing an Apache Airflow DAG</a></h2>



<p>The DAG is the heart of our orchestration logic. <strong>Figure </strong><strong>4</strong> shows the task graph and execution order.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/06/image-7-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="786" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7-1024x786.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54046" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7.png?size=126x97&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7-300x230.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7.png?size=378x290&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7.png?size=504x387&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7.png?size=630x484&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7-768x589.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7-1024x786.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/06/image-7-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> The Airflow DAG execution graph showing task dependencies and data flow between tasks.</figcaption></figure></div>


<p>Here is how the DAG is defined in <code data-enlighter-language="python" class="EnlighterJSRAW">airflow_project/dags/ingest_documents_dag.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="7">with DAG(
    dag_id='ingest_documents_dag',
    default_args=default_args,
    description='Ingest and process documents for ML pipeline',
    schedule_interval=timedelta(minutes=1),
    start_date=days_ago(1),
    catchup=False,
    tags=['ingestion', 'documents', 'ml-pipeline'],
) as dag:
    
    fetch_documents_task = PythonOperator(
        task_id='fetch_documents',
        python_callable=fetch_pending_documents,
        provide_context=True,
    )
    
    parse_documents_task = PythonOperator(
        task_id='parse_documents',
        python_callable=parse_documents,
        provide_context=True,
    )
    
    chunk_documents_task = PythonOperator(
        task_id='chunk_documents',
        python_callable=chunk_documents,
        provide_context=True,
    )
    
    validate_chunks_task = PythonOperator(
        task_id='validate_chunks',
        python_callable=validate_chunks,
        provide_context=True,
    )
    
    mark_complete_task = PythonOperator(
        task_id='mark_complete',
        python_callable=mark_documents_complete,
        provide_context=True,
    )
    
    # Define task dependencies
    fetch_documents_task >> parse_documents_task >> chunk_documents_task
    chunk_documents_task >> validate_chunks_task >> mark_complete_task</pre>



<p>Let us understand each configuration parameter.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">dag_id</code> is the unique identifier for this workflow. It appears in the Airflow UI and logs. The <code data-enlighter-language="python" class="EnlighterJSRAW">default_args</code> dictionary contains settings that apply to all tasks. This includes retry behavior, execution timeout, and owner information.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">schedule_interval</code> is set to <code data-enlighter-language="python" class="EnlighterJSRAW">timedelta(minutes=1)</code>. This means Airflow runs this DAG every minute. In production, you might use hourly or daily schedules, but for demos and development, 1 minute lets you see results quickly.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">start_date</code> is set to <code data-enlighter-language="python" class="EnlighterJSRAW">days_ago(1)</code>, which means the DAG is eligible to run starting yesterday. The <code data-enlighter-language="python" class="EnlighterJSRAW">catchup=False</code> parameter is important. Without this, Airflow would try to run the DAG for every missed interval since the start date. We do not want that. We only care about processing current pending documents, not creating historical backfill runs.</p>



<p>The tags list helps organize DAGs in the UI. You can filter by tag to find related workflows.</p>



<p>Each task uses a <code data-enlighter-language="python" class="EnlighterJSRAW">PythonOperator</code>, which executes a Python function. The <code data-enlighter-language="python" class="EnlighterJSRAW">task_id</code> must be unique within the DAG. The <code data-enlighter-language="python" class="EnlighterJSRAW">python_callable</code> is the function to execute. The <code data-enlighter-language="python" class="EnlighterJSRAW">provide_context=True</code> parameter gives the function access to Airflow’s execution context.</p>



<p>Why does context matter? Because it provides critical runtime information: the unique <code data-enlighter-language="python" class="EnlighterJSRAW">run_id</code> (for creating file names that do not collide across runs), the execution timestamp (for audit trails), and XCom access (for passing data between tasks). Without context, your task functions would be isolated and unable to coordinate or share state.</p>



<p>The task dependencies are defined using the <code data-enlighter-language="python" class="EnlighterJSRAW">&gt;&gt;</code> operator. This creates a directed graph. <code data-enlighter-language="python" class="EnlighterJSRAW">fetch_documents_task &gt;&gt; parse_documents_task</code> means parse documents cannot start until fetch documents completes. The final line creates a longer chain: fetch, then parse, then chunk, then validate, then mark complete. This ensures strict ordering.</p>



<p>Now let us examine what each task function does.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Task-1-Fetch-Pending-Documents"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Task-1-Fetch-Pending-Documents">Task 1: Fetch Pending Documents</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="9">def fetch_pending_documents(**context) -> List[int]:
    """
    Task 1: Fetch documents that need processing.
    
    Returns list of document IDs to process.
    """
    logger.info("Fetching pending documents...")
    
    with session_scope() as session:
        pending_docs = session.query(DocumentModel).filter(
            DocumentModel.status == DocumentStatus.PENDING
        ).all()
        
        doc_ids = [doc.id for doc in pending_docs]
        logger.info(f"Found {len(doc_ids)} pending documents: {doc_ids}")
        
        run_id = context['dag_run'].run_id
        filepath = write_data_to_file(doc_ids, f'{run_id}_document_ids.json')
        
        context['task_instance'].xcom_push(key='document_ids_file', value=filepath)
        
        return doc_ids</pre>



<p>This function queries the database for all documents where <code data-enlighter-language="python" class="EnlighterJSRAW">status = PENDING</code>. It extracts just the IDs into a list. If there are no pending documents, the list is empty and subsequent tasks have no work to do.</p>



<p>The interesting part is how we pass data to the next task. We do not use Airflow’s XCom directly for the document IDs. Instead, we write them to a JSON file and pass only the file path through XCom. Why? Because XCom stores data in the Airflow metadata database. Large payloads slow down the database and can hit size limits. By using files, we keep XCom small and handle arbitrary data sizes.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">write_data_to_file()</code> helper function writes JSON to <code data-enlighter-language="python" class="EnlighterJSRAW">/tmp/***_dag_data/</code> and returns the full path. The next task reads from this path.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Task-2-Parse-Documents"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Task-2-Parse-Documents">Task 2: Parse Documents</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="11">def parse_documents(**context) -> Dict[str, int]:
    """
    Task 2: Parse PDF documents into pages.
    
    Reads document IDs from previous task and parses each PDF.
    """
    logger.info("Parsing documents...")
    
    doc_ids_file = context['task_instance'].xcom_pull(
        key='document_ids_file',
        task_ids='fetch_documents'
    )
    doc_ids = read_data_from_file(doc_ids_file)
    
    parsed_count = 0
    
    with session_scope() as session:
        for doc_id in doc_ids:
            pages_file = TEMP_DIR / f'{run_id}_doc_{doc_id}_pages.json'
            if pages_file.exists():
                logger.info(f"Document {doc_id} already parsed, skipping")
                parsed_count += 1
                continue
            
            doc = session.query(DocumentModel).filter(
                DocumentModel.id == doc_id
            ).first()
            
            if not doc:
                logger.warning(f"Document {doc_id} not found")
                continue
            
            try:
                pages = parse_pdf(doc.file_path)
                logger.info(f"Parsed {len(pages)} pages from {doc.filename}")
                
                pages_file = write_data_to_file(pages, f'{run_id}_doc_{doc_id}_pages.json')
                parsed_count += 1
                
            except Exception as e:
                logger.error(f"Failed to parse document {doc_id}: {str(e)}")
                doc.status = DocumentStatus.FAILED
                session.commit()
    
    logger.info(f"Successfully parsed {parsed_count} documents")
    return {'parsed': parsed_count}</pre>



<p>This task pulls the document IDs from the previous task, loads the document record from the database, and calls <code data-enlighter-language="python" class="EnlighterJSRAW">parse_pdf()</code> on the file path. The <code data-enlighter-language="python" class="EnlighterJSRAW">parse_pdf()</code> function (from <code data-enlighter-language="python" class="EnlighterJSRAW">shared/parsing/pdf_parser.py</code>) uses PyPDF to extract text page by page.</p>



<p>Notice the idempotency check at the top of the loop. If a file named <code data-enlighter-language="python" class="EnlighterJSRAW">{run_id}_doc_{doc_id}_pages.json</code> already exists, we skip parsing. This means if the task retries or reruns, it does not waste time reparsing documents that succeeded before.</p>



<p>The error handling is important. If parsing fails for any reason (corrupted PDF, missing file, permission error), we catch the exception, mark that document as <code data-enlighter-language="python" class="EnlighterJSRAW">FAILED</code>, and continue with the next one. This prevents one bad document from blocking the entire batch.</p>



<p>The parsed pages are written to a file, one file per document. The next task will read these files.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Task-3-Chunk-Documents"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Task-3-Chunk-Documents">Task 3: Chunk Documents</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="13">def chunk_documents(**context) -> Dict[str, int]:
    """
    Task 3: Chunk parsed pages into text segments.
    
    Reads pages from previous task and creates chunks.
    """
    logger.info("Chunking documents...")
    
    doc_ids_file = context['task_instance'].xcom_pull(
        key='document_ids_file',
        task_ids='fetch_documents'
    )
    doc_ids = read_data_from_file(doc_ids_file)
    
    total_chunks = 0
    
    with session_scope() as session:
        for doc_id in doc_ids:
            run_id = context['dag_run'].run_id
            pages_file = TEMP_DIR / f'{run_id}_doc_{doc_id}_pages.json'
            
            if not pages_file.exists():
                logger.warning(f"No pages file found for document {doc_id}")
                continue
            
            pages = read_data_from_file(str(pages_file))
            
            existing_chunks = session.query(ChunkModel).filter(
                ChunkModel.document_id == doc_id
            ).count()
            
            if existing_chunks > 0:
                logger.info(f"Document {doc_id} already has {existing_chunks} chunks, skipping")
                total_chunks += existing_chunks
                continue
            
            try:
                full_text = "\n\n".join(page['text'] for page in pages)
                chunks = chunk_text(full_text, chunk_size=512, overlap=50)
                
                chunk_index = 0
                for chunk in chunks:
                    chunk_hash = hash_content(chunk)
                    
                    existing_chunk = session.query(ChunkModel).filter(
                        ChunkModel.content_hash == chunk_hash
                    ).first()
                    
                    if existing_chunk:
                        continue
                    
                    chunk_model = ChunkModel(
                        document_id=doc_id,
                        chunk_index=chunk_index,
                        text=chunk,
                        content_hash=chunk_hash,
                        page_number=None,
                        token_count=len(chunk.split())
                    )
                    session.add(chunk_model)
                    chunk_index += 1
                
                session.commit()
                logger.info(f"Created {chunk_index} chunks for document {doc_id}")
                total_chunks += chunk_index
                
            except Exception as e:
                logger.error(f"Failed to chunk document {doc_id}: {str(e)}")
    
    logger.info(f"Total chunks created: {total_chunks}")
    return {'chunks': total_chunks}</pre>



<p>This task joins all pages into a single text string, then calls <code data-enlighter-language="python" class="EnlighterJSRAW">chunk_text()</code> to split it into overlapping segments. The default chunk size is 512 words (we use whitespace-separated words as an approximate proxy for tokens) with 50-word overlap. Think of this like cutting a long rope into segments with intentional overlap at the ends — if an important concept spans a boundary, the overlap ensures it appears fully in at least one segment.</p>



<p>For each chunk, we compute a content hash and check if that exact text already exists in the database. This is duplicate detection at the chunk level. If the same sentence appears in multiple documents, we store it once. This saves storage and embedding compute later.</p>



<p>Notice we track <code data-enlighter-language="python" class="EnlighterJSRAW">chunk_index</code> to maintain ordering within a document. This is important for reconstruction or citation purposes.</p>



<p>The task again has idempotency checks. If the document already has chunks in the database, we skip it. This lets us safely retry the task.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Task-4-Validate-Chunks"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Task-4-Validate-Chunks">Task 4: Validate Chunks</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="15">def validate_chunks(**context) -> Dict[str, int]:
    """
    Task 4: Validate chunk quality.
    
    Checks for empty chunks, excessive length, etc.
    """
    logger.info("Validating chunks...")
    
    doc_ids_file = context['task_instance'].xcom_pull(
        key='document_ids_file',
        task_ids='fetch_documents'
    )
    doc_ids = read_data_from_file(doc_ids_file)
    
    valid_count = 0
    invalid_count = 0
    
    with session_scope() as session:
        for doc_id in doc_ids:
            chunks = session.query(ChunkModel).filter(
                ChunkModel.document_id == doc_id
            ).all()
            
            for chunk in chunks:
                # Too short
                if len(chunk.text) &lt; 50:
                    logger.warning(f"Chunk {chunk.id} too short: {len(chunk.text)} chars")
                    invalid_count += 1
                    continue
                
                # Too long
                if len(chunk.text) > 2000:
                    logger.warning(f"Chunk {chunk.id} too long: {len(chunk.text)} chars")
                    invalid_count += 1
                    continue
                
                # Empty or whitespace only
                if not chunk.text.strip():
                    logger.warning(f"Chunk {chunk.id} is empty or whitespace only")
                    invalid_count += 1
                    continue
                
                valid_count += 1
    
    logger.info(f"Validation complete: {valid_count} valid, {invalid_count} invalid chunks")
    return {'valid': valid_count, 'invalid': invalid_count}</pre>



<p>This task performs quality checks on chunks. It checks for chunks that are too short (less than <code data-enlighter-language="python" class="EnlighterJSRAW">50</code> characters), too long (more than <code data-enlighter-language="python" class="EnlighterJSRAW">2000</code> characters), or empty. In production, you might delete invalid chunks or mark them in a separate table. Here, we just log warnings.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Task-5-Mark-Complete"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Task-5-Mark-Complete">Task 5: Mark Complete</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="17">def mark_documents_complete(**context) -> Dict[str, int]:
    """
    Task 5: Mark documents as complete.
    
    Updates document status and creates pipeline run record.
    """
    logger.info("Marking documents complete...")
    
    doc_ids_file = context['task_instance'].xcom_pull(
        key='document_ids_file',
        task_ids='fetch_documents'
    )
    doc_ids = read_data_from_file(doc_ids_file)
    
    chunks_result = context['task_instance'].xcom_pull(task_ids='chunk_documents')
    total_chunks = chunks_result.get('chunks', 0)
    
    with session_scope() as session:
        for doc_id in doc_ids:
            doc = session.query(DocumentModel).filter(
                DocumentModel.id == doc_id
            ).first()
            
            if doc and doc.status == DocumentStatus.PROCESSING:
                doc.status = DocumentStatus.COMPLETED
        
        run_id = context['dag_run'].run_id
        pipeline_run = PipelineRunModel(
            pipeline_type='airflow',
            run_id=run_id,
            status=PipelineRunStatus.COMPLETED,
            started_at=context['dag_run'].start_date,
            completed_at=datetime.utcnow(),
            documents_processed=len(doc_ids),
            chunks_created=total_chunks,
            embeddings_created=0,
            run_metadata={
                'dag_id': context['dag'].dag_id,
                'execution_date': str(context['execution_date'])
            }
        )
        
        session.add(pipeline_run)
        session.commit()
        
        logger.info(f"Pipeline run {run_id} completed: {len(doc_ids)} docs, {total_chunks} chunks")
    
    return {'documents_completed': len(doc_ids)}</pre>



<p>The final task updates document status to <code data-enlighter-language="python" class="EnlighterJSRAW">COMPLETED</code> and creates a <code data-enlighter-language="python" class="EnlighterJSRAW">PipelineRunModel</code> record. This record captures metrics about the entire DAG run. Later, you can query this table to track throughput, find bottlenecks, or generate reports.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-This-DAG-Structure-Works"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-This-DAG-Structure-Works">Why This DAG Structure Works</a></h3>



<p>This 5-task structure enforces 4 critical principles. </p>



<p>First, each task has a single responsibility. Fetch finds work. Parse extracts text. Chunk splits text. Validate checks quality. Mark complete updates status. This makes debugging easier. If chunking fails, you know exactly which task to inspect.</p>



<p>Second, each task is idempotent. You can retry tasks without creating duplicate data or corrupting state. This is essential for reliability.</p>



<p>Third, we have observability at every step. Each task logs its progress. You can see exactly how many documents were parsed, how many chunks were created, and which documents failed.</p>



<p>Fourth, failure handling is granular. The pipeline is designed to continue processing other documents when individual documents fail, rather than aborting the entire batch. We catch exceptions at the document level, mark failed documents with <code data-enlighter-language="python" class="EnlighterJSRAW">FAILED</code> status, and let the task continue with the remaining documents.</p>



<p>In an upcoming lesson, we will implement the shared parsing and chunking logic and see how these tasks operate on real documents end to end.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you built the foundation of a production-grade document ingestion pipeline using Apache Airflow. You learned how to design a FastAPI service for uploading PDF documents with built-in deduplication, how to model ingestion state in PostgreSQL, and how to define a reliable Airflow DAG to orchestrate document processing.</p>



<p>You saw how to separate ingestion from processing, use content hashing for idempotency, and construct a task graph that represents each stage of the pipeline. By the end of this part, you had a complete orchestration design for moving documents from raw uploads into a scheduled workflow.</p>



<p>This architecture forms the control plane of your ingestion pipeline and prepares you to implement the parsing and chunking logic in the next part.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Singh, V</strong><strong>. </strong>“Apache Airflow Document Ingestion Pipeline for RAG Systems,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/8b2ey" target="_blank" rel="noreferrer noopener">https://pyimg.co/8b2ey</a></p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Apache Airflow Document Ingestion Pipeline for RAG Systems" data-enlighter-group="19">@incollection{Singh_2026_apache-airflow-document-ingestion-pipeline-rag-systems,
  author = {Vikram Singh},
  title = {{Apache Airflow Document Ingestion Pipeline for RAG Systems}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/8b2ey},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/06/01/apache-airflow-document-ingestion-pipeline-for-rag-systems/">Apache Airflow Document Ingestion Pipeline for RAG Systems</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</title>
		<link>https://pyimagesearch.com/2026/05/25/manual-tracing-scores-and-evaluation-with-langfuse-self-hosted/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 25 May 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Generative AI]]></category>
		<category><![CDATA[Langfuse]]></category>
		<category><![CDATA[LLMOps]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Monitoring]]></category>
		<category><![CDATA[Observability]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[agent observability]]></category>
		<category><![CDATA[evaluation metrics]]></category>
		<category><![CDATA[langfuse]]></category>
		<category><![CDATA[langfuse tracing]]></category>
		<category><![CDATA[latency monitoring]]></category>
		<category><![CDATA[llm diagnostics]]></category>
		<category><![CDATA[llm evaluation]]></category>
		<category><![CDATA[llm evaluation metrics]]></category>
		<category><![CDATA[llm monitoring]]></category>
		<category><![CDATA[llm observability]]></category>
		<category><![CDATA[manual tracing]]></category>
		<category><![CDATA[observability dashboard]]></category>
		<category><![CDATA[openai compatible api]]></category>
		<category><![CDATA[quality scoring]]></category>
		<category><![CDATA[rag observability]]></category>
		<category><![CDATA[self-hosted langfuse]]></category>
		<category><![CDATA[token usage tracking]]></category>
		<category><![CDATA[tracing pipelines]]></category>
		<category><![CDATA[tracing spans]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[vllm]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53941</guid>

					<description><![CDATA[<p>Table of Contents Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted) Why Manual Tracing Matters for LLM Observability Decorator vs Manual Tracing: When to Use Which Manual Tracing with the Langfuse Low-Level API Why Manual Tracing Matters (Even If You&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/25/manual-tracing-scores-and-evaluation-with-langfuse-self-hosted/">Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Manual-Tracing-Scores-Evaluation-Langfuse-Self-Hosted"><a rel="noopener" target="_blank" href="#h1-Manual-Tracing-Scores-Evaluation-Langfuse-Self-Hosted">Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</a></li>

    <li id="TOC-h2-Why-Manual-Tracing-Matters-LLM-Observability"><a rel="noopener" target="_blank" href="#h2-Why-Manual-Tracing-Matters-LLM-Observability">Why Manual Tracing Matters for LLM Observability</a></li>

    <li id="TOC-h2-Decorator-vs-Manual-Tracing-When-Use-Which"><a rel="noopener" target="_blank" href="#h2-Decorator-vs-Manual-Tracing-When-Use-Which">Decorator vs Manual Tracing: When to Use Which</a></li>

    <li id="TOC-h2-Manual-Tracing-Langfuse-Low-Level-API"><a rel="noopener" target="_blank" href="#h2-Manual-Tracing-Langfuse-Low-Level-API">Manual Tracing with the Langfuse Low-Level API</a></li>
    <ul>
        <li id="TOC-h3-Why-Manual-Tracing-Matters-Even-If-Use-Decorators"><a rel="noopener" target="_blank" href="#h3-Why-Manual-Tracing-Matters-Even-If-Use-Decorators">Why Manual Tracing Matters (Even If You Use Decorators)</a></li>
        <li id="TOC-h3-Full-Manual-Tracing-Implementation-Langfuse"><a rel="noopener" target="_blank" href="#h3-Full-Manual-Tracing-Implementation-Langfuse">Full Manual Tracing Implementation with Langfuse</a></li>
        <li id="TOC-h3-Code-Walkthrough-Langfuse-Manual-Tracing-Pipeline"><a rel="noopener" target="_blank" href="#h3-Code-Walkthrough-Langfuse-Manual-Tracing-Pipeline">Code Walkthrough: Langfuse Manual Tracing Pipeline</a></li>
        <li id="TOC-h3-Creating-Manual-Traces-Langfuse"><a rel="noopener" target="_blank" href="#h3-Creating-Manual-Traces-Langfuse">Creating Manual Traces in Langfuse</a></li>
        <li id="TOC-h3-Running-Langfuse-Manual-Tracing-Script"><a rel="noopener" target="_blank" href="#h3-Running-Langfuse-Manual-Tracing-Script">Running the Langfuse Manual Tracing Script</a></li>
        <li id="TOC-h3-Viewing-Manual-Traces-Langfuse-Dashboard"><a rel="noopener" target="_blank" href="#h3-Viewing-Manual-Traces-Langfuse-Dashboard">Viewing Manual Traces in the Langfuse Dashboard</a></li>
        <li id="TOC-h3-Manual-vs-Decorator-Tracing-Langfuse"><a rel="noopener" target="_blank" href="#h3-Manual-vs-Decorator-Tracing-Langfuse">Manual vs Decorator Tracing in Langfuse</a></li>
    </ul>

    <li id="TOC-h2-LLM-Evaluation-Metrics-Quality-Scoring-Langfuse"><a rel="noopener" target="_blank" href="#h2-LLM-Evaluation-Metrics-Quality-Scoring-Langfuse">LLM Evaluation Metrics and Quality Scoring with Langfuse</a></li>
    <ul>
        <li id="TOC-h3-Adding-LLM-Evaluation-Metrics-Beyond-Manual-Tracing"><a rel="noopener" target="_blank" href="#h3-Adding-LLM-Evaluation-Metrics-Beyond-Manual-Tracing">Adding LLM Evaluation Metrics Beyond Manual Tracing</a></li>
        <li id="TOC-h3-Code-Walkthrough-evaluation-metrics-py"><a rel="noopener" target="_blank" href="#h3-Code-Walkthrough-evaluation-metrics-py">Code Walkthrough: evaluation_metrics.py</a></li>
        <li id="TOC-h3-Running-LLM-Evaluation-Metrics-Pipeline"><a rel="noopener" target="_blank" href="#h3-Running-LLM-Evaluation-Metrics-Pipeline">Running the LLM Evaluation Metrics Pipeline</a></li>
        <li id="TOC-h3-Conceptual-Mockup-Evaluation-Trace-Langfuse"><a rel="noopener" target="_blank" href="#h3-Conceptual-Mockup-Evaluation-Trace-Langfuse">Conceptual Mockup: Evaluation Trace in Langfuse</a></li>
        <li id="TOC-h3-Real-Trace-Self-Hosted-Langfuse-Dashboard"><a rel="noopener" target="_blank" href="#h3-Real-Trace-Self-Hosted-Langfuse-Dashboard">Real Trace from Our Self-Hosted Langfuse Dashboard</a></li>
        <li id="TOC-h3-Why-LLM-Evaluation-Metrics-Matter"><a rel="noopener" target="_blank" href="#h3-Why-LLM-Evaluation-Metrics-Matter">Why LLM Evaluation Metrics Matter</a></li>
    </ul>

    <li id="TOC-h2-vLLM-Diagnostics-Health-Checks-LLM-Observability"><a rel="noopener" target="_blank" href="#h2-vLLM-Diagnostics-Health-Checks-LLM-Observability">vLLM Diagnostics and Health Checks for LLM Observability</a></li>
    <ul>
        <li id="TOC-h3-What-vLLM-Health-Check-Script-Validates"><a rel="noopener" target="_blank" href="#h3-What-vLLM-Health-Check-Script-Validates">What the vLLM Health Check Script Validates</a></li>
        <li id="TOC-h3-Code-Walkthrough-health-check-py"><a rel="noopener" target="_blank" href="#h3-Code-Walkthrough-health-check-py">Code Walkthrough: health_check.py</a></li>
        <li id="TOC-h3-Why-vLLM-Health-Checks-Matter-LLM-Observability"><a rel="noopener" target="_blank" href="#h3-Why-vLLM-Health-Checks-Matter-LLM-Observability">Why vLLM Health Checks Matter for LLM Observability</a></li>
    </ul>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Manual-Tracing-Scores-Evaluation-Langfuse-Self-Hosted"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Manual-Tracing-Scores-Evaluation-Langfuse-Self-Hosted">Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</a></h2>



<p>In this lesson, you will learn how to take full control of LLM observability using the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> manual tracing API. While Lesson 1 demonstrated the benefits of decorator-based tracing, real-world LLM systems often require deeper visibility. This includes custom spans, step-level metadata, evaluation scores, and multi-stage inspection for RAG pipelines and agent workflows. </p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?lossy=2&strip=1&webp=1" alt="manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png" class="wp-image-53961" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/manual-tracing-scores-evaluation-langfuse-self-hosted-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>In this lesson, you will build a fully instrumented pipeline where every step, every decision, and every model output is recorded with precision inside your self-hosted <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard.</p>



<p>This lesson is the 2nd in a 3-part series on <strong>LLM observability with Langfuse</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/tadoh" target="_blank" rel="noreferrer noopener">LLM Observability with Self-Hosted Langfuse and vLLM</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/24p06" target="_blank" rel="noreferrer noopener">Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 3</em></li>
</ol>



<p><strong>To learn how to build manual traces, attach custom spans, and evaluate LLM outputs with scoring metadata, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Manual-Tracing-Matters-LLM-Observability"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Manual-Tracing-Matters-LLM-Observability">Why Manual Tracing Matters for LLM Observability</a></h2>



<p>In Lesson 1, we built the foundations of LLM observability with a fully self-hosted <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> stack, a local <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> server, and a complete decorator-based tracing pipeline. With just a few <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorators, we captured prompts, outputs, latency, token usage, and nested spans, all visualized instantly in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard. That approach was simple, powerful, and ideal for most LLM applications.</p>



<p>However, real production systems require more control than a decorator can provide.</p>



<p>Decorator-based tracing works well when function boundaries align with observability boundaries. Once a pipeline becomes dynamic, for example by involving multiple retrieval steps, conditional branches, tool calls, retries, validations, re-ranking, scoring, or multi-agent planning, you must explicitly decide what gets traced, how traces are grouped, and what metadata is recorded at each stage. In these scenarios, manual tracing becomes essential.</p>



<p>Manual tracing allows you to open and close spans at will, attach arbitrary metadata, log intermediate states, record evaluation scores, and capture execution steps that do not live inside a function, including loops, conditionals, streaming tokens, or retry logic. In short, decorator tracing provides automation, while manual tracing provides precision.</p>



<p>This lesson shows you how to construct traces explicitly, starting from creating the root trace and continuing through building child spans and attaching fine-grained metadata and custom evaluation signals. You will also integrate <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation_metrics.py</code>, which introduces lightweight scoring for model generations. This makes it possible to track correctness, response length, latency thresholds, or any domain-specific metric directly inside <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> as structured metadata.</p>



<p>By the end of this section, you will understand not only why manual tracing matters, but also when it becomes indispensable. Common use cases include debugging RAG pipelines, analyzing retrieval failures, tracking hallucination hotspots, validating agent actions, and building complex multi-step LLM systems where you need complete visibility into what happened and why.</p>



<p>If you are ready to take full control of your observability pipeline, including manual spans and rich evaluation metadata, the following sections will guide you through the process step by step.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Decorator-vs-Manual-Tracing-When-Use-Which"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Decorator-vs-Manual-Tracing-When-Use-Which">Decorator vs Manual Tracing: When to Use Which</a></h2>



<p>In Lesson 1, the <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator gave us an elegant and almost magical tracing experience. You wrapped a function, ran your pipeline, and <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> automatically produced a structured trace with child spans, latency, token usage, and full metadata. This approach works well when your application is composed of clean, well-defined functions, such as a simple “generate answer” pipeline or a single LLM call with minimal branching.</p>



<p>However, decorators have an important limitation. They observe function boundaries, not logic boundaries.</p>



<p>If your real pipeline involves conditional flows, loops, retries, branching, retrieval, ranking, tool invocation, or agent-style decision-making, tracing only the outer function hides much of the interesting behavior. The decorator cannot see inside reasoning steps, iterative refinements, or internal calls unless those steps are wrapped in separate functions. As systems become more dynamic and non-linear, decorator-based tracing begins to fall short.</p>



<p>This is where manual tracing becomes essential.</p>



<p>Manual spans allow you to mark exactly where a step begins and ends, even when that step is not a function. You can record intermediate artifacts such as retrieved documents, scoring signals, latency thresholds, or model reasoning stages. You can attach custom metadata to any span and build a detailed step-by-step view of how your LLM pipeline behaves, rather than only seeing which functions were invoked.</p>



<p>In practice, the most effective approach is hybrid. Use decorators for high-level structure, and use manual spans when precision is required.</p>



<p>This lesson focuses on building that precision.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-69.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="340" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53963" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69.png?size=126x69&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69-300x163.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69.png?size=378x206&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69.png?size=504x275&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-69.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Decorators trace function calls; manual spans trace logic. Together, they give you complete control over LLM observability.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Manual-Tracing-Langfuse-Low-Level-API"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Manual-Tracing-Langfuse-Low-Level-API">Manual Tracing with the Langfuse Low-Level API</a></h2>



<p>In Lesson 1, you used <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorators to add observability with almost no effort: just annotate your functions and <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> automatically created traces, spans, usage metadata, and latency metrics.</p>



<p>In this lesson, we take the opposite approach: full manual control.</p>



<p>Manual tracing exposes the entire underlying API used by <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> itself. You decide:</p>



<ul class="wp-block-list">
<li>when traces are created</li>



<li>how spans relate to each other</li>



<li>what metadata you attach</li>



<li>how token usage is recorded</li>



<li>how latencies are measured</li>



<li>how deeply nested your pipeline becomes</li>
</ul>



<p>This approach is critical for advanced LLM workflows where decorators are either too restrictive or too magical.</p>



<p>You will see exactly how <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> stores a trace internally, and why this skill becomes essential when building complex RAG, evaluation, or multi-agent systems.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Manual-Tracing-Matters-Even-If-Use-Decorators"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Manual-Tracing-Matters-Even-If-Use-Decorators">Why Manual Tracing Matters (Even If You Use Decorators)</a></h3>



<p>The decorator API is elegant but sometimes too simple.</p>



<p>Manual tracing is required when you need:</p>



<ul class="wp-block-list">
<li><strong>Full control over trace structure:</strong> Define parent → child → subchild relationships explicitly.</li>



<li><strong>Dynamic spans:</strong> When you do not know upfront how many steps your pipeline will generate.</li>



<li><strong>Conditional traces:</strong> e.g., only log LLM calls above 2 seconds latency.</li>



<li><strong>Custom metadata injection:</strong> Dynamic context, retrieval sources, ranking scores, chain-of-thought summaries, etc.</li>



<li><strong>Advanced RAG + agent observability:</strong> Where each tool call needs explicit naming and structure.</li>
</ul>



<p>In short:</p>



<p>The decorator API is the convenience layer.</p>



<p>Manual tracing is the power-user layer.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Full-Manual-Tracing-Implementation-Langfuse"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Full-Manual-Tracing-Implementation-Langfuse">Full Manual Tracing Implementation with Langfuse</a></h3>



<p>Below is your complete script, <code data-enlighter-language="python" class="EnlighterJSRAW">src/tracing_manual.py</code>, unmodified and shown entirely so readers can reference it line-by-line.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="1">"""
Manual Tracing with Low-Level Langfuse API

Shows explicit trace creation and management using Langfuse SDK directly.
This gives you full control but requires more code compared to decorators.
"""

from langfuse import Langfuse
from llm_utils import get_llm_client
from config import get_llm_config
import time

# Initialize Langfuse client
langfuse = Langfuse()

# Initialize vLLM client
client, model = get_llm_client(load_model_from_config=True)

# Get configuration
llm_config = get_llm_config()
temperature = llm_config.get("temperature", 0.7)
max_tokens = llm_config.get("max_tokens", 300)


def generate_with_manual_tracing(question: str) -> str:
    """
    Generate answer WITH manual trace creation.
   
    This gives you full control over every trace property:
    - Custom trace names and IDs
    - Granular span creation
    - Manual token counting
    - Custom metadata
    """
   
    print("Calling LLM with manual tracing...")
   
    # 1. Create trace manually
    trace = langfuse.trace(
        name="manual_llm_call",
        metadata={"method": "manual", "question": question}
    )
   
    # 2. Create span for LLM generation
    start_time = time.time()
   
    generation = trace.generation(
        name="llm_generation",
        model=model,
        input=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": question}
        ],
        metadata={
            "temperature": temperature,
            "max_tokens": max_tokens
        }
    )
   
    # 3. Make the actual LLM call
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": question}
        ],
        temperature=temperature,
        max_tokens=max_tokens
    )
   
    latency_ms = (time.time() - start_time) * 1000
    answer = response.choices[0].message.content
   
    # 4. Update generation with results
    generation.update(
        output=answer,
        usage={
            "input": response.usage.prompt_tokens,
            "output": response.usage.completion_tokens,
            "total": response.usage.total_tokens
        },
        metadata={
            "latency_ms": round(latency_ms, 2)
        }
    )
   
    print(f"   Tokens used: {response.usage.total_tokens}")
    print(f"   Latency: {latency_ms:.2f}ms")
    print(f"   ✅ Manually logged to Langfuse")
    print(f"   🔍 Trace ID: {trace.id}\n")
   
    return answer


if __name__ == "__main__":
    print("\n" + "="*70)
    print("Manual Tracing Demo")
    print("="*70 + "\n")
   
    question = "What is deep learning?"
    print(f"Question: {question}\n")
    print("-" * 70 + "\n")
   
    # Generate with manual tracing
    answer = generate_with_manual_tracing(question)
    print(f"Answer: {answer}\n")
   
    print("=" * 70)
    print("\n📊 Manual Tracing vs Decorators:")
    print("   Manual (this file):")
    print("   • Full control over trace structure")
    print("   • More verbose code")
    print("   • Good for complex custom logging")
    print()
    print("   Decorators (recommended):")
    print("   • Clean @observe annotation")
    print("   • Less boilerplate")
    print("   • Automatic nesting")
    print("   • See: src/tracing_decorator.py")
    print("\n🔍 Check your dashboard: https://cloud.langfuse.com")
    print("=" * 70 + "\n")
   
    # Flush traces
    langfuse.flush()
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Code-Walkthrough-Langfuse-Manual-Tracing-Pipeline"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Code-Walkthrough-Langfuse-Manual-Tracing-Pipeline">Code Walkthrough: Langfuse Manual Tracing Pipeline</a></h3>



<p>Let us break this down into meaningful building blocks.</p>



<h4 class="wp-block-heading">Initializing Langfuse + vLLM</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="2">langfuse = Langfuse()
client, model = get_llm_client(load_model_from_config=True)
llm_config = get_llm_config()
</pre>



<p>Here, we:</p>



<ul class="wp-block-list">
<li>connect to the self-hosted <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code></li>



<li>initialize a <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> OpenAI-compatible client</li>



<li>load generation parameters such as <code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code></li>
</ul>



<p>Nothing happens yet. This is just configuration.</p>



<p>The real magic begins once we create a trace.</p>



<p><em><strong>Important: </strong></em><em>Manual tracing gives you full control over the trace lifecycle.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Creating-Manual-Traces-Langfuse"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Creating-Manual-Traces-Langfuse">Creating Manual Traces in Langfuse</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="3">trace = langfuse.trace(
    name="manual_llm_call",
    metadata={"method": "manual", "question": question}
)
</pre>



<p>A <strong>trace</strong> is the root object that represents the entire request.</p>



<p>You define:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> name</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">context</code></li>
</ul>



<p>This is equivalent to <code data-enlighter-language="python" class="EnlighterJSRAW">@observe(name="llm_pipeline")</code>, but explicit.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-70.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="305" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53966" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70.png?size=126x62&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70-300x147.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70.png?size=378x185&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70.png?size=504x246&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-70.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> A manual trace begins with an explicit call to <code>langfuse.trace()</code>, giving you full control over naming, IDs, metadata, and context.</figcaption></figure></div>


<h4 class="wp-block-heading">Creating a Generation Span</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="4">generation = trace.generation(
    name="llm_generation",
    model=model,
    input=[ ... ],
    metadata={ ... }
)
</pre>



<p>This is the part decorators automatically create.</p>



<p>A <strong>generation span</strong>:</p>



<ul class="wp-block-list">
<li>represents a single LLM model call</li>



<li>stores the prompt</li>



<li>stores parameters (<code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code>)</li>



<li>links itself as a <em>child</em> of the main trace</li>
</ul>



<p>This is a foundational building block for RAG and agent pipelines.</p>



<h4 class="wp-block-heading">Making the Actual LLM Call</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="5">response = client.chat.completions.create(...)
</pre>



<p>Here, the raw LLM execution happens.</p>



<p>No tracing occurs automatically; the span must be updated manually afterward.</p>



<h4 class="wp-block-heading">Recording Results (Tokens, Latency, Outputs)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="6">generation.update(
    output=answer,
    usage={...},
    metadata={ "latency_ms": round(latency_ms, 2) }
)
</pre>



<p>In manual mode, <strong>you choose what to log</strong>.</p>



<p>This is how you capture:</p>



<ul class="wp-block-list">
<li>latency</li>



<li>token usage</li>



<li>answer text</li>



<li>any additional metadata</li>



<li>final span status</li>
</ul>



<p>This is where evaluators, reward functions, safety signals, etc., get attached.</p>



<h4 class="wp-block-heading">Flushing Traces</h4>



<p>Short scripts exit before <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> can finish sending data.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="7">langfuse.flush()
</pre>



<p>This guarantees the trace appears in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard immediately.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-Langfuse-Manual-Tracing-Script"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-Langfuse-Manual-Tracing-Script">Running the Langfuse Manual Tracing Script</a></h3>



<p>Right after the “run this script” block:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="8">$ python src/tracing_manual.py
</pre>



<p>You should see the output, as shown in <strong>Figure 3</strong>:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-71-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="846" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-1024x846.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53969" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71.png?size=126x104&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-300x248.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71.png?size=378x312&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71.png?size=504x416&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71.png?size=630x520&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-768x635.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-1024x846.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-71-1536x1269.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Actual terminal output from running <code>tracing_manual.py</code>, showing manual <code>trace</code> creation, token usage, <code>latency</code>, and the generated <code>answer</code>.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Viewing-Manual-Traces-Langfuse-Dashboard"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Viewing-Manual-Traces-Langfuse-Dashboard">Viewing Manual Traces in the Langfuse Dashboard</a></h3>



<p>After running the manual tracing script, open the printed trace URL in your browser.</p>



<p>You should see a page similar to the screenshot below, showing the full structure of your manually created trace.</p>



<p>This view includes:</p>



<ul class="wp-block-list">
<li><strong>Root trace:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">manual_llm_call</code></li>



<li><strong>Child span:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">llm_generation</code></li>



<li><strong>Token usage summary:</strong> 32 → 300 (332 total)</li>



<li><strong>Metadata:</strong>
<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">method: "manual"</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">question: "What is deep learning?"</code></li>
</ul>
</li>



<li><strong>Input and output placeholders:</strong>
<ul class="wp-block-list">
<li>(These appear as <code data-enlighter-language="python" class="EnlighterJSRAW">null</code> until the generation span updates, since the child span holds the actual LLM data.)</li>
</ul>
</li>
</ul>



<p>This is the clearest demonstration of what manual tracing gives you: explicit control over the <code data-enlighter-language="python" class="EnlighterJSRAW">structure</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">nesting</code> of your trace.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-72-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="384" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-1024x384.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53972" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72.png?size=126x47&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-300x112.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72.png?size=378x142&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72.png?size=504x189&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72.png?size=630x236&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-768x288.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-1024x384.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-72-1536x575.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Manual trace in <code>Langfuse</code> showing a custom root trace, a generation span, metadata, and token usage logged via explicit API calls.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Manual-vs-Decorator-Tracing-Langfuse"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Manual-vs-Decorator-Tracing-Langfuse">Manual vs Decorator Tracing in Langfuse</a></h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-73.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="423" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73-1024x423.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53974" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73.png?size=126x52&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73-300x124.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73.png?size=378x156&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73.png?size=504x208&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73.png?size=630x260&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73-768x317.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73-1024x423.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-73.png?lossy=2&amp;strip=1&amp;webp=1 1039w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Comparison of decorator-based tracing versus manual instrumentation across usability, control, and pipeline complexity.</figcaption></figure></div>


<p>In this section, you learned how to build an entire trace manually:</p>



<ul class="wp-block-list">
<li>creating a root <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code></li>



<li>adding a generation <code data-enlighter-language="python" class="EnlighterJSRAW">span</code></li>



<li>logging <code data-enlighter-language="python" class="EnlighterJSRAW">prompts</code></li>



<li>recording <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code></li>



<li>logging token usage</li>



<li>updating <code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code></li>



<li>flushing <code data-enlighter-language="python" class="EnlighterJSRAW">results</code></li>
</ul>



<p>Manual tracing is verbose, but incredibly powerful for custom workflows, evaluation, and multi-step LLM applications.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-LLM-Evaluation-Metrics-Quality-Scoring-Langfuse"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-LLM-Evaluation-Metrics-Quality-Scoring-Langfuse">LLM Evaluation Metrics and Quality Scoring with Langfuse</a></h2>



<p>Observability is more than <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">tokens</code>. In real LLM systems, you also need to evaluate:</p>



<ul class="wp-block-list">
<li><strong>“Was the answer good?”</strong></li>



<li><strong>“Was it long enough?”</strong></li>



<li><strong>“Was it too slow?”</strong></li>



<li><strong>“Did model quality silently degrade?”</strong></li>
</ul>



<p>This section introduces evaluation metrics, custom scoring, and decorator-based tracing for quality analysis. You will learn how to attach accuracy/quality metadata to traces, visualize scores inside <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>, and detect degraded model outputs in real time.</p>



<p>We will do this using the file <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation_metrics.py</code>, which combines:</p>



<ul class="wp-block-list">
<li>the <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator</li>



<li>custom <code data-enlighter-language="python" class="EnlighterJSRAW">scoring</code> logic</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> checks</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> scoring</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> pipeline wrapper</li>
</ul>



<p>By the end, you will have a complete <code data-enlighter-language="python" class="EnlighterJSRAW">scoring pipeline</code> with metrics displayed inside the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Adding-LLM-Evaluation-Metrics-Beyond-Manual-Tracing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Adding-LLM-Evaluation-Metrics-Beyond-Manual-Tracing">Adding LLM Evaluation Metrics Beyond Manual Tracing</a></h3>



<p>This file builds on everything from Sections 2 and 3:</p>



<p>This script adds 4 major <code data-enlighter-language="python" class="EnlighterJSRAW">improvements</code>:</p>



<ul class="wp-block-list">
<li><strong>Automated tracing</strong> using <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code></li>



<li><strong>Custom </strong><code data-enlighter-language="python" class="EnlighterJSRAW">quality</code><strong> metric</strong> (using <code data-enlighter-language="python" class="EnlighterJSRAW">answer_length</code> as a proxy)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency</code><strong> threshold warnings</strong></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">score</code><strong> logging</strong> inside <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> (visible as a numerical “<code data-enlighter-language="python" class="EnlighterJSRAW">quality</code>” score)</li>
</ul>



<p>This turns your traces from “LLM diagnostics” into <strong>LLM evaluation and monitoring</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Code-Walkthrough-evaluation-metrics-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Code-Walkthrough-evaluation-metrics-py">Code Walkthrough: evaluation_metrics.py</a></h3>



<p>Below is the full annotated walkthrough.</p>



<h4 class="wp-block-heading">Initialize Langfuse + LLM Client</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="9">langfuse = Langfuse()
client, model = get_llm_client(load_model_from_config=True)
</pre>



<p>We initialize 2 systems:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> (manual scoring only): decorators handle <code data-enlighter-language="python" class="EnlighterJSRAW">tracing</code>, but <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse()</code> is needed for scoring.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> client: same <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible API as Lesson 1.</li>
</ul>



<h4 class="wp-block-heading">The Main Function: generate_and_score()</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="10">@observe(name="generate_and_score")
def generate_and_score(question: str) -> tuple[str, float]:
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator automatically creates a trace and an associated observation.</p>



<p>The rest of the function focuses on:</p>



<ul class="wp-block-list">
<li>LLM call</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> measurement</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> scoring</li>



<li>updating the observation</li>



<li>recording a <code data-enlighter-language="python" class="EnlighterJSRAW">score</code></li>
</ul>



<h4 class="wp-block-heading">Load Configurations</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="11">llm_config = get_llm_config()
eval_config = get_evaluation_config()
temperature = llm_config.get("temperature", 0.7)
max_tokens = llm_config.get("max_tokens", 300)
   
min_length = eval_config.get("min_length", 20)
good_length_threshold = eval_config.get("good_length_threshold", 100)
max_latency_ms = eval_config.get("max_latency_ms", 5000)
</pre>



<p>From <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code>, we load:</p>



<p><strong>LLM Parameters</strong></p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code></li>
</ul>



<p><strong>Evaluation Parameters</strong></p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">min_length</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">good_length_threshold</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_latency_ms</code></li>
</ul>



<p>This means your scoring logic is <strong>configurable</strong> without touching Python code.</p>



<h4 class="wp-block-heading">Log Input</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="12">langfuse_context.update_current_observation(
    input={"question": question, "model": model}
)
</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">langfuse_context.update_current_observation(...)</code> is used to attach new information to the current observation in a <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> trace.</p>



<p>Think of a <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> as one full request, and an <code data-enlighter-language="python" class="EnlighterJSRAW">observation</code> as one step inside that request (e.g., LLM call, embedding call, retrieval step).</p>



<h4 class="wp-block-heading">Perform the LLM Call + Measure Latency</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="13">start_time = time.time()
# Make LLM call
response = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": question}
    ],
    temperature=temperature,
    max_tokens=max_tokens
)
# Calculate latency
latency_ms = (time.time() - start_time) * 1000
</pre>



<p>This gives us:</p>



<ul class="wp-block-list">
<li>Real wall-clock <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code></li>



<li>First-token + completion <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> combined</li>



<li>Values used for <code data-enlighter-language="python" class="EnlighterJSRAW">threshold</code> checking</li>
</ul>



<h4 class="wp-block-heading">Compute Answer Length + Quality Score</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="14">answer_length = len(answer)
# Calculate quality score
if answer_length &lt; min_length:
    quality_score = 0.3
elif answer_length >= good_length_threshold:
    quality_score = 1.0
else:
    quality_score = 0.3 + (
        0.7 * (answer_length - min_length) /
        (good_length_threshold - min_length)
    )
</pre>



<p>This snippet measures the length of the generated <code data-enlighter-language="python" class="EnlighterJSRAW">answer</code> and uses it to compute a simple <code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> score: if the answer is too short (below <code data-enlighter-language="python" class="EnlighterJSRAW">min_length</code>), it assigns a low score of <code data-enlighter-language="python" class="EnlighterJSRAW">0.3</code>; if it exceeds the <code data-enlighter-language="python" class="EnlighterJSRAW">good_length_threshold</code>, it gives a perfect score of <code data-enlighter-language="python" class="EnlighterJSRAW">1.0</code>. Otherwise, it linearly scales the score between <code data-enlighter-language="python" class="EnlighterJSRAW">0.3</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">1.0</code> based on how close the <code data-enlighter-language="python" class="EnlighterJSRAW">answer_length</code> is to the ideal range. This provides a lightweight heuristic for judging response completeness without requiring complex evaluation logic.</p>



<h4 class="wp-block-heading">Update the Observation (Output + Usage + Metadata)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="15"># Update observation with results and custom metrics
langfuse_context.update_current_observation(
    output={"answer": answer, "quality_score": quality_score},
    usage={
        "input": response.usage.prompt_tokens,
        "output": response.usage.completion_tokens,
        "total": response.usage.total_tokens
    },
    metadata={
        "latency_ms": round(latency_ms, 2),
        "answer_length": answer_length
    }
)
</pre>



<p>This block updates the current <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> observation with everything needed to record the model’s performance: it logs the generated <code data-enlighter-language="python" class="EnlighterJSRAW">answer</code> and its quality score, tracks token usage from the model response (input, output, and total), and attaches custom metadata such as request <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> and the length of the returned answer. Together, these fields give you a complete view of each evaluation run, including what the model produced, how much it cost, and how efficiently it responded, making it easier to analyze and compare results across experiments.</p>



<p>What this adds to <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>:</p>



<ul class="wp-block-list">
<li>Answer text</li>



<li>Quality score</li>



<li>Token usage</li>



<li>Latency</li>



<li>Derived <code data-enlighter-language="python" class="EnlighterJSRAW">metrics</code> (<code data-enlighter-language="python" class="EnlighterJSRAW">answer_length</code>)</li>
</ul>



<p>This gives you the same view you would see in enterprise-grade observability tools.</p>



<h4 class="wp-block-heading">Attach a Score to the Trace</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="16"># Score the trace
langfuse_context.score_current_observation(
    name="quality",
    value=quality_score,
    comment=f"Based on answer length ({answer_length} chars)"
)
</pre>



<p>This line evaluates the current <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> observation by attaching a custom score named &#8220;quality&#8221; to the trace. It records the numerical <code data-enlighter-language="python" class="EnlighterJSRAW">quality_score</code>, your own metric for evaluating the model’s answer, and adds a short comment explaining the basis of that score, in this case referencing the <code data-enlighter-language="python" class="EnlighterJSRAW">answer_length</code>. Scoring observations like this makes it easy to compare model responses, analyze performance over time, and visualize quality trends directly in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard.</p>



<p>In short, this creates a <em>visible, numeric score</em> inside the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard.</p>



<p>This is extremely powerful for:</p>



<ul class="wp-block-list">
<li>model comparisons</li>



<li>regression testing</li>



<li>degradation alerts</li>



<li>ranking model performance</li>
</ul>



<h4 class="wp-block-heading">Running the Evaluation Pipeline</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="17">@observe(name="evaluation_pipeline")
def run_evaluation(question: str):
    """Wrapper to create a trace context for the evaluation."""
    from datetime import datetime
   
    # Add timestamp to make each run unique
    langfuse_context.update_current_trace(
        metadata={"run_time": datetime.now().isoformat()}
    )
   
    answer, score = generate_and_score(question)
   
    print(f"\n✅ Answer: {answer}\n")
    print(f"📊 Quality Score: {score:.2f}\n")
   
    trace_id = langfuse_context.get_current_trace_id()
    if trace_id:
        print(f"🔍 View trace with scores: https://cloud.langfuse.com/trace/{trace_id}")
        print(f"📋 Trace ID: {trace_id}")
    print("="*50 + "\n")
   
    return answer, score
</pre>



<p>This function defines an evaluation pipeline using the <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator, which tells <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> to treat every call as a traced, observable run. When the function starts, it imports datetime and immediately updates the active <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> trace with a timestamp so each evaluation run is uniquely identifiable. This metadata is helpful when you are comparing multiple experiments, debugging behavior, or tracking quality trends over time.</p>



<p>The core of the function calls <code data-enlighter-language="python" class="EnlighterJSRAW">generate_and_score(question)</code>, which returns an AI-generated <code data-enlighter-language="python" class="EnlighterJSRAW">answer</code> along with a numerical quality score. Both values are printed in a human-friendly format, and the function then retrieves the current <code data-enlighter-language="python" class="EnlighterJSRAW">trace_id</code> from <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>. If a trace exists, it prints a direct link to view the full run, including <code data-enlighter-language="python" class="EnlighterJSRAW">metrics</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">scores</code>, in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard. </p>



<p>Finally, the function returns the answer and score so they can be used downstream, while also visually marking the end of the run in the terminal output.</p>



<p><strong>It adds:</strong></p>



<ul class="wp-block-list">
<li>timestamp <code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code></li>



<li>parent-level <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> context</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">output</code> printing</li>



<li>a link to view the <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-LLM-Evaluation-Metrics-Pipeline"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-LLM-Evaluation-Metrics-Pipeline">Running the LLM Evaluation Metrics Pipeline</a></h3>



<p>A typical terminal run will show:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="18">==================================================
Evaluation with Custom Scoring
==================================================

Question: What are neural networks?

📊 Quality Score: 0.82 (answer length: 112 chars)
📊 Latency: 212.45ms
📊 Tokens: 14 → 72

🔍 View trace with scores: http://localhost:3000/trace/01HY3SJQH9...
==================================================
⏳ Flushing traces to Langfuse...
✅ Traces sent!
</pre>



<p>This output must appear in the lesson. It helps the reader validate correctness.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Conceptual-Mockup-Evaluation-Trace-Langfuse"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Conceptual-Mockup-Evaluation-Trace-Langfuse">Conceptual Mockup: Evaluation Trace in Langfuse</a></h3>



<p>Before looking at the real dashboard output, here is a clean conceptual view of what an evaluation trace looks like inside <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-74-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="1021" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-1024x1021.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53976" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-150x150.png?lossy=2&amp;strip=1&amp;webp=1 150w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-300x300.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74.png?size=378x377&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74.png?size=504x503&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74.png?size=630x628&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-768x766.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-1024x1021.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-74-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> <code>Langfuse UI</code> mockup showing the evaluation pipeline, complete with the parent trace (<code>evaluation_pipeline</code>), child span (<code>generate_and_score</code>), token usage, <code>latency</code>, model <code>metadata</code>, <code>answer</code> output, and the computed <code>quality</code> score.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Real-Trace-Self-Hosted-Langfuse-Dashboard"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Real-Trace-Self-Hosted-Langfuse-Dashboard">Real Trace from Our Self-Hosted Langfuse Dashboard</a></h3>



<p>Now, let us look at the <strong>actual trace</strong> generated by our evaluation script.</p>



<p>This is exactly what you should see when running:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="19">$ python src/evaluation_metrics.py
</pre>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard will show:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">evaluation_pipeline</code>: as the parent trace</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">generate_and_score</code>: as the nested span</li>



<li>full <strong>inputs</strong> (question, system message, model config)</li>



<li>full <strong>outputs</strong> (LLM answer + quality score)</li>



<li><strong>token usage</strong> (input, output, total)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> measured manually</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code> from <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">score</code> <strong>badge</strong> showing the computed <code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> metric</li>
</ul>



<p>While <strong>Figure 6</strong> shows the actual <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> trace captured during execution, the diagram below abstracts the same process into a clear evaluation pipeline. It highlights how the <code data-enlighter-language="python" class="EnlighterJSRAW">LLM</code> response is generated, how <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> metrics are computed, and how both the raw <code data-enlighter-language="python" class="EnlighterJSRAW">outputs</code> and derived <code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> scores are attached to a single <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> before being logged to <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-75-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="490" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-1024x490.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53978" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75.png?size=126x60&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-300x143.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75.png?size=378x181&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75.png?size=504x241&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75.png?size=630x301&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-768x367.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-1024x490.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-75-1536x734.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Real evaluation trace from the self-hosted <code>Langfuse</code> dashboard showing <code>metadata</code>, full <code>answer</code> output, <code>latency</code> breakdown, token usage, and the custom <code>quality</code> score registered by our <code>evaluation_metrics.py</code> script.</figcaption></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-76-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="128" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-1024x128.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53980" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76.png?size=126x16&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-300x38.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76.png?size=378x47&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76.png?size=504x63&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76.png?size=630x79&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-768x96.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-1024x128.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-76-1536x192.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> The <code>evaluation_pipeline</code> generates an LLM answer, computes <code>metrics</code>, attaches a <code>quality</code> score, and logs everything into <code>Langfuse</code>.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-LLM-Evaluation-Metrics-Matter"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-LLM-Evaluation-Metrics-Matter">Why LLM Evaluation Metrics Matter</a></h3>



<p>By adding <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> metrics:</p>



<ul class="wp-block-list">
<li>You detect <code data-enlighter-language="python" class="EnlighterJSRAW">model</code> degradation</li>



<li>You compare <code data-enlighter-language="python" class="EnlighterJSRAW">models</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">prompts</code></li>



<li>You measure <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> regressions</li>



<li>You track <code data-enlighter-language="python" class="EnlighterJSRAW">token</code> cost spikes</li>



<li>You get quality insights per <code data-enlighter-language="python" class="EnlighterJSRAW">request</code></li>
</ul>



<p>This pushes your system beyond “debuggable” into <strong>evaluated</strong>, which is critical for anything involving RAG, agents, or multi-step pipelines.</p>



<p>In this section, you learned how to:</p>



<ul class="wp-block-list">
<li>Instrument <code data-enlighter-language="python" class="EnlighterJSRAW">LLM</code> calls with decorators</li>



<li>Compute custom <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> metrics</li>



<li>Attach <code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> scores to <code data-enlighter-language="python" class="EnlighterJSRAW">traces</code></li>



<li>Visualize <code data-enlighter-language="python" class="EnlighterJSRAW">scores</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">tokens</code> inside <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code></li>



<li>Wrap everything inside an <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation_pipeline</code></li>
</ul>



<p>With this, tracing evolves from simple diagnostics into actual <code data-enlighter-language="python" class="EnlighterJSRAW">LLM</code> evaluation.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-vLLM-Diagnostics-Health-Checks-LLM-Observability"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-vLLM-Diagnostics-Health-Checks-LLM-Observability">vLLM Diagnostics and Health Checks for LLM Observability</a></h2>



<p>Before we evaluate model outputs or analyze <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> traces, we need to make sure the underlying engine <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is alive, reachable, and responding correctly. If <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is down, every script in this lesson fails. If the model is still loading, requests time out. If ports are wrong, you will get cryptic errors that look like <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> problems but are actually <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> issues.</p>



<p>To prevent all of that, we use <code data-enlighter-language="python" class="EnlighterJSRAW">health_check.py</code>, a dedicated diagnostic tool that validates your entire local <code data-enlighter-language="python" class="EnlighterJSRAW">LLM</code> runtime before you run any <code data-enlighter-language="python" class="EnlighterJSRAW">tracing</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">scoring</code> scripts.</p>



<p>This script confirms 3 things:</p>



<ul class="wp-block-list">
<li>Is the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> server running and responding?</li>



<li>Are models actually loaded?</li>



<li>Can the model generate text?</li>
</ul>



<p>If all 3 pass, your observability stack is ready.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-vLLM-Health-Check-Script-Validates"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-vLLM-Health-Check-Script-Validates">What the vLLM Health Check Script Validates</a></h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">health_check.py</code> performs 3 layers of validation:</p>



<h4 class="wp-block-heading">Layer 1: Infrastructure health</h4>



<ul class="wp-block-list">
<li>Calls <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code> endpoint</li>



<li>Checks whether the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> server is reachable</li>



<li>Confirms that the port and base URL match your config</li>
</ul>



<h4 class="wp-block-heading">Layer 2: Model readiness</h4>



<ul class="wp-block-list">
<li>Calls <code data-enlighter-language="python" class="EnlighterJSRAW">/v1/models</code></li>



<li>Ensures at least one model is loaded</li>



<li>Detects if <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is still downloading or initializing the model</li>
</ul>



<h4 class="wp-block-heading">Layer 3: LLM generation test</h4>



<ul class="wp-block-list">
<li>Sends a simple prompt: <em>“Say ‘OK’ if you’re working.”</em></li>



<li>Ensures the model produces an actual <code data-enlighter-language="python" class="EnlighterJSRAW">response</code></li>
</ul>



<p>This prevents 95% of “It’s not working” confusion.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Code-Walkthrough-health-check-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Code-Walkthrough-health-check-py">Code Walkthrough: health_check.py</a></h3>



<p>We now walk through the entire script, grouped logically rather than line by line, following typical PyImageSearch style.</p>



<h4 class="wp-block-heading">Configuration and Imports</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="20">import sys
import httpx
from llm_utils import get_llm_client
from config import get_llm_config
</pre>



<p>The script uses:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">httpx</code>: for fast <code data-enlighter-language="python" class="EnlighterJSRAW">HTTP</code> checks</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_llm_client()</code>: to issue a test generation</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_llm_config()</code>: to load the base URL from your YAML config</li>
</ul>



<p>No hard-coded URLs, which keeps the system in sync with <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code>.</p>



<h4 class="wp-block-heading">Checking vLLM Health</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="21">def check_vllm_health(base_url: str = None, timeout: int = 5) -> bool:
    """
    Check if vLLM server is healthy.
   
    Args:
        base_url: vLLM server base URL (defaults to config.yaml)
        timeout: Request timeout in seconds
       
    Returns:
        True if server is healthy, False otherwise
    """
    # Load base_url from config if not provided
    if base_url is None:
        llm_config = get_llm_config()
        base_url = llm_config.get("base_url", "http://localhost:8000/v1")
        base_url = base_url.rstrip("/v1")
   
    health_url = f"{base_url}/health"
    models_url = f"{base_url}/v1/models"
   
    print(f"🔍 Checking vLLM server at {base_url}...")
</pre>



<p>If <code data-enlighter-language="python" class="EnlighterJSRAW">base_url</code> is not provided, the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> URL is loaded from <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code>.</p>



<p>Next:</p>



<h4 class="wp-block-heading">Health endpoint check</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="22">try:
        # Check health endpoint
        with httpx.Client(timeout=timeout) as client:
            response = client.get(health_url)
            if response.status_code == 200:
                print(f"  ✅ Health check passed")
            else:
                print(f"  ❌ Health check failed (status: {response.status_code})")
                return False
</pre>



<p>A healthy <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> server returns:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="23">{"status": "ok"}
</pre>



<p>If this fails, <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is down, so no tracing or scoring will work.</p>



<h4 class="wp-block-heading">Models endpoint check</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="24">       # Check models endpoint
        with httpx.Client(timeout=timeout) as client:
            response = client.get(models_url)
            if response.status_code == 200:
                models = response.json().get("data", [])
                if models:
                    print(f"  ✅ Models available: {[m['id'] for m in models]}")
                else:
                    print(f"  ⚠️  No models loaded yet (still initializing?)")
                    return False
            else:
                print(f"  ❌ Models endpoint failed (status: {response.status_code})")
                return False
       
        return True
</pre>



<p>A healthy <code data-enlighter-language="python" class="EnlighterJSRAW">response</code> contains:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="25">{
  "data": [
     {"id": "meta-llama/Llama-2-7b-chat-hf"}
   ]
}
</pre>



<p>If this list is empty, the model is still loading.</p>



<h4 class="wp-block-heading">Error handling</h4>



<p>The script gracefully handles:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">connection</code> failure</li>



<li>timeouts</li>



<li>unexpected <code data-enlighter-language="python" class="EnlighterJSRAW">JSON</code></li>



<li>wrong <code data-enlighter-language="python" class="EnlighterJSRAW">ports</code></li>



<li>wrong <code data-enlighter-language="python" class="EnlighterJSRAW">base_url</code></li>
</ul>



<p>And prints clear, actionable fixes.</p>



<h4 class="wp-block-heading">Testing LLM Generation</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="26">def test_llm_generation() -> bool:
    """Test simple LLM generation."""
    print("\n🔍 Testing LLM generation...")
   
    try:
        client = get_llm_client(timeout=30)
        response = client.chat.completions.create(
            model="meta-llama/Llama-2-7b-chat-hf",
            messages=[{"role": "user", "content": "Say 'OK' if you're working."}],
            max_tokens=10
        )
       
        answer = response.choices[0].message.content
        print(f"  ✅ Generation successful: {answer[:50]}...")
        return True
       
    except Exception as e:
        print(f"  ❌ Generation failed: {e}")
        return False
</pre>



<p>This test:</p>



<ul class="wp-block-list">
<li>Instantiates the OpenAI client</li>



<li>Sends a tiny one-line <code data-enlighter-language="python" class="EnlighterJSRAW">prompt</code></li>



<li>Validates the model <code data-enlighter-language="python" class="EnlighterJSRAW">answers</code> with at least <em>something</em></li>
</ul>



<p>If the model cannot generate, your entire <code data-enlighter-language="python" class="EnlighterJSRAW">tracing</code> pipeline will also fail.</p>



<h4 class="wp-block-heading">The Entry Point</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="27">if __name__ == "__main__":
     main()
</pre>



<p>This is the command you will run before every other script.</p>



<p>It prints:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> health</li>



<li>Model <code data-enlighter-language="python" class="EnlighterJSRAW">availability</code></li>



<li>Generation <code data-enlighter-language="python" class="EnlighterJSRAW">test</code></li>
</ul>



<p>And guides you through failures with friendly hints:</p>



<p>“Start vLLM: docker-compose up -d”</p>



<p>“Wait 2-3 minutes for model download”</p>



<p>“Check docker logs”</p>



<p>This makes beginner troubleshooting seamless.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-77-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="682" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77-1024x682.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53983" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77.png?size=126x84&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77-300x200.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77.png?size=378x252&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77.png?size=504x336&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77.png?size=630x420&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77-768x511.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77-1024x682.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-77-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 8:</strong> The <code>vLLM</code> health check verifies that the server is running, the model is loaded, and generation works end-to-end.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-vLLM-Health-Checks-Matter-LLM-Observability"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-vLLM-Health-Checks-Matter-LLM-Observability">Why vLLM Health Checks Matter for LLM Observability</a></h3>



<p>If <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is unhealthy, <strong>every</strong> tracing script fails.</p>



<p>This script prevents:</p>



<ul class="wp-block-list">
<li>Running manual tracing while <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is down</li>



<li>Chasing <code data-enlighter-language="python" class="EnlighterJSRAW">decorator</code> errors that are actually <code data-enlighter-language="python" class="EnlighterJSRAW">connection</code> errors</li>



<li>Confusing <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> ingestion errors with model-loading delays</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">token</code> errors caused by uninitialized models</li>



<li>Timeouts that look like <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> bugs</li>
</ul>



<p>It gives readers a clean, deterministic start before diving into observability.</p>



<p>In this section, you learned:</p>



<ul class="wp-block-list">
<li>How to verify <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> health</li>



<li>Why the <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">/v1/models</code> endpoints matter</li>



<li>How to test real <code data-enlighter-language="python" class="EnlighterJSRAW">generation</code></li>



<li>How to diagnose common <code data-enlighter-language="python" class="EnlighterJSRAW">startup</code> issues</li>



<li>How to ensure the entire <code data-enlighter-language="python" class="EnlighterJSRAW">tracing</code> pipeline will work</li>
</ul>



<p>With your environment confirmed healthy, you are ready to score model <code data-enlighter-language="python" class="EnlighterJSRAW">outputs</code> and analyze <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> traces in <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you moved beyond simply capturing <code data-enlighter-language="python" class="EnlighterJSRAW">traces</code> and learned how to measure, <code data-enlighter-language="python" class="EnlighterJSRAW">score</code>, and diagnose the quality of your <code data-enlighter-language="python" class="EnlighterJSRAW">LLM</code> pipeline. Lesson 1 gave you observability; Lesson 2 gave you interpretation.</p>



<p>You began by understanding why manual tracing still matters even when decorators exist. Manual <code data-enlighter-language="python" class="EnlighterJSRAW">spans</code> give you full control over <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> structure, <code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code>, and custom <code data-enlighter-language="python" class="EnlighterJSRAW">logging</code>, making them essential for debugging <code data-enlighter-language="python" class="EnlighterJSRAW">agent</code> loops, multi-step pipelines, and retrieval-heavy systems. You then revisited the <code data-enlighter-language="python" class="EnlighterJSRAW">decorator</code> pattern and learned when to use each approach so your real-world projects can choose the right <code data-enlighter-language="python" class="EnlighterJSRAW">instrumentation</code> strategy.</p>



<p>Next, you implemented true <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation-driven</code> observability using the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> scoring interface. You wrapped LLM calls with <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code>, computed a custom “<code data-enlighter-language="python" class="EnlighterJSRAW">quality</code> score,” tracked <code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> and token usage, and attached structured <code data-enlighter-language="python" class="EnlighterJSRAW">metrics</code> directly to your traces. This transformed your dashboard from a simple <code data-enlighter-language="python" class="EnlighterJSRAW">trace</code> viewer into a performance <code data-enlighter-language="python" class="EnlighterJSRAW">analytics</code> console.</p>



<p>Finally, you validated your infrastructure using a robust <code data-enlighter-language="python" class="EnlighterJSRAW">health-check</code> system. Before any tracing or scoring happens, <code data-enlighter-language="python" class="EnlighterJSRAW">health_check.py</code> ensures <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> is running, the model is loaded, and real <code data-enlighter-language="python" class="EnlighterJSRAW">generation</code> works end-to-end. This eliminates guesswork and gives you a reliable foundation for more advanced workflows.</p>



<p>By the end of this lesson, your observability pipeline now supports:</p>



<ul class="wp-block-list">
<li>manual low-level <code data-enlighter-language="python" class="EnlighterJSRAW">traces</code></li>



<li>decorator-based nested <code data-enlighter-language="python" class="EnlighterJSRAW">traces</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency</code> instrumentation</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">token</code> usage insights</li>



<li>custom <code data-enlighter-language="python" class="EnlighterJSRAW">evaluation</code> scores</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">metadata</code>-rich pipeline summaries</li>



<li>infrastructure-level <code data-enlighter-language="python" class="EnlighterJSRAW">diagnostics</code></li>
</ul>



<p>Together, these upgrades elevate your system from “traced” to measured, from “visible” to actionable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Singh, V</strong><strong>. </strong>“Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted),” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/24p06" target="_blank" rel="noreferrer noopener">https://pyimg.co/24p06</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)" data-enlighter-group="28">@incollection{Singh_2026_manual-tracing-scores-evaluation-langfuse-self-hosted,
  author = {Vikram Singh},
  title = {{Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/24p06},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/25/manual-tracing-scores-and-evaluation-with-langfuse-self-hosted/">Manual Tracing, Scores, and Evaluation with Langfuse (Self-Hosted)</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>LLM Observability with Self-Hosted Langfuse and vLLM</title>
		<link>https://pyimagesearch.com/2026/05/18/llm-observability-with-self-hosted-langfuse-and-vllm/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 18 May 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[Generative AI]]></category>
		<category><![CDATA[LLMOps]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Observability]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[docker compose]]></category>
		<category><![CDATA[generative ai]]></category>
		<category><![CDATA[langfuse]]></category>
		<category><![CDATA[langfuse dashboard]]></category>
		<category><![CDATA[latency monitoring]]></category>
		<category><![CDATA[llm monitoring]]></category>
		<category><![CDATA[llm observability]]></category>
		<category><![CDATA[llm pipeline]]></category>
		<category><![CDATA[llm tracing]]></category>
		<category><![CDATA[llmops]]></category>
		<category><![CDATA[local llm inference]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[observability stack]]></category>
		<category><![CDATA[openai compatible api]]></category>
		<category><![CDATA[postgresql]]></category>
		<category><![CDATA[prompt tracing]]></category>
		<category><![CDATA[self-hosted llm]]></category>
		<category><![CDATA[token usage]]></category>
		<category><![CDATA[trace visualization]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[vllm]]></category>
		<category><![CDATA[vllm docker]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53755</guid>

					<description><![CDATA[<p>Table of Contents LLM Observability with Self-Hosted Langfuse and vLLM Introduction to LLM Observability with Langfuse How Langfuse Fits into an LLM Observability Stack Langfuse Architecture for LLM Observability Why Understanding LLM Observability Architecture Matters Setting Up a Self-Hosted Langfuse&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/18/llm-observability-with-self-hosted-langfuse-and-vllm/">LLM Observability with Self-Hosted Langfuse and vLLM</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-LLM-Observability-Self-Hosted-Langfuse-vLLM"><a rel="noopener" target="_blank" href="#h1-LLM-Observability-Self-Hosted-Langfuse-vLLM">LLM Observability with Self-Hosted Langfuse and vLLM</a></li>

  <li id="TOC-h2-Introduction-LLM-Observability-Langfuse"><a rel="noopener" target="_blank" href="#h2-Introduction-LLM-Observability-Langfuse">Introduction to LLM Observability with Langfuse</a></li>

    <li id="TOC-h2-How-Langfuse-Fits-LLM-Observability-Stack"><a rel="noopener" target="_blank" href="#h2-How-Langfuse-Fits-LLM-Observability-Stack">How Langfuse Fits into an LLM Observability Stack</a></li>

    <li id="TOC-h2-Langfuse-Architecture-LLM-Observability"><a rel="noopener" target="_blank" href="#h2-Langfuse-Architecture-LLM-Observability">Langfuse Architecture for LLM Observability</a></li>

    <li id="TOC-h2-Why-Understanding-LLM-Observability-Architecture-Matters"><a rel="noopener" target="_blank" href="#h2-Why-Understanding-LLM-Observability-Architecture-Matters">Why Understanding LLM Observability Architecture Matters</a></li>

    <li id="TOC-h2-Setting-Up-Self-Hosted-Langfuse-vLLM-Stack"><a rel="noopener" target="_blank" href="#h2-Setting-Up-Self-Hosted-Langfuse-vLLM-Stack">Setting Up a Self-Hosted Langfuse and vLLM Stack</a></li>

    <li id="TOC-h2-Baseline-LLM-Application-Before-Observability"><a rel="noopener" target="_blank" href="#h2-Baseline-LLM-Application-Before-Observability">Baseline LLM Application (Before Observability)</a></li>

    <li id="TOC-h2-Adding-LLM-Observability-Langfuse-observe-Decorator"><a rel="noopener" target="_blank" href="#h2-Adding-LLM-Observability-Langfuse-observe-Decorator">Adding LLM Observability with the Langfuse @observe Decorator</a></li>

    <li id="TOC-h2-Running-Verifying-Self-Hosted-Langfuse-Observability-Stack"><a rel="noopener" target="_blank" href="#h2-Running-Verifying-Self-Hosted-Langfuse-Observability-Stack">Running and Verifying a Self-Hosted Langfuse Observability Stack</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-LLM-Observability-Self-Hosted-Langfuse-vLLM"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-LLM-Observability-Self-Hosted-Langfuse-vLLM">LLM Observability with Self-Hosted Langfuse and vLLM</a></h2>



<p>In this lesson, you will finally demystify what Large Language Model (LLM) observability actually is. It is not just logs or print statements. It is a full, end-to-end view of how your AI system behaves in real-world conditions.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?lossy=2&strip=1&webp=1" alt="llm-observability-self-hosted-langfuse-vllm-featured.png" class="wp-image-53802" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/llm-observability-self-hosted-langfuse-vllm-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>You will learn why modern LLM apps need more than “it works on my machine,” and how traces, token usage, latency, and model interactions become powerful tools for debugging and optimization.</p>



<p>Next, you will roll up your sleeves and self-host <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> locally, connect it to a <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM server</code>, and run your first fully instrumented LLM pipeline from prompt to response.</p>



<p>By the end, you will be exploring live traces in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse UI</code>, inspecting individual requests, understanding where time is spent, and building a solid foundation for debugging, improving, and scaling every LLM workflow you create.</p>



<p>This lesson is the 1st in a 3-part series on <strong>LLM observability with Langfuse</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/tadoh" target="_blank" rel="noreferrer noopener">LLM Observability with Self-Hosted Langfuse and vLLM</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 2</em></li>



<li><em>Lesson 3</em></li>
</ol>



<p><strong>To learn how to self-host Langfuse, connect it to vLLM, and build end-to-end LLM observability from the ground up,</strong><em><strong> just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction-LLM-Observability-Langfuse"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction-LLM-Observability-Langfuse">Introduction to LLM Observability with Langfuse</a></h2>



<p>Modern LLM applications behave very differently from traditional software. They are probabilistic, non-deterministic, sensitive to prompt phrasing, and often expensive to run. Debugging them requires far more than print statements or simple application logs — <em>you need visibility into how your entire LLM pipeline behaves at runtime.</em></p>



<p>This section introduces the foundations of LLM observability, explains why classical ML monitoring tools fall short, and sets the stage for building a complete <strong>self-hosted </strong><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code><strong> + </strong><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code><strong> observability stack</strong>.</p>



<h3 class="wp-block-heading">What Problem Does LLM Observability Solve?</h3>



<p>LLMs fail in ways ordinary software doesn’t:</p>



<ul class="wp-block-list">
<li>They <strong>hallucinate</strong> confidently.</li>



<li>They produce different answers for the same input.</li>



<li>They slow down under load due to tokenizer/model server issues.</li>



<li>They cost real money per token.</li>



<li>They silently degrade when context windows overflow.</li>



<li>They chain multiple steps, making errors hard to pinpoint.</li>
</ul>



<p>Without observability, you are essentially debugging blind.</p>



<p>LLM observability gives you visibility into:</p>



<ul class="wp-block-list">
<li>What prompt was sent?</li>



<li>What did the LLM actually output?</li>



<li>How long did it take?</li>



<li>How many tokens did it use?</li>



<li>Where did a pipeline fail?</li>



<li>Was this output good or bad?</li>



<li>What downstream component was impacted?</li>
</ul>



<p>In short: Observability turns your LLM pipeline from a black box into a glass box.</p>



<h3 class="wp-block-heading">Logs vs Metrics vs Traces (Why Logs Alone Fail)</h3>



<p>Modern systems use 3 observability pillars:</p>



<h4 class="wp-block-heading">Logs</h4>



<p>Unstructured text messages. Good for errors; terrible for understanding multi-step LLM pipelines.</p>



<h4 class="wp-block-heading">Metrics</h4>



<p>Numerical time-series (e.g., latency, tokens/sec). Good for dashboards and alerts.</p>



<h4 class="wp-block-heading">Traces</h4>



<p>End-to-end structured records of what happened across a pipeline.</p>



<p>Traces are <strong>THE critical component for LLM apps</strong> because a single request may produce:</p>



<ul class="wp-block-list">
<li>multiple sub-steps</li>



<li>multiple model calls</li>



<li>embeddings</li>



<li>retrieval calls</li>



<li>tool invocations</li>



<li>agent planning</li>



<li>scoring</li>



<li>post-processing</li>
</ul>



<p><strong>Logs tell you what happened. Metrics tell you how often. Traces tell you why.</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-36.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="332" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53804" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36.png?size=126x67&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36-300x160.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36.png?size=378x201&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36.png?size=504x268&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-36.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Logs tell you what happened, metrics tell you how your system behaves over time, but traces show you the entire LLM pipeline step by step.</figcaption></figure></div>


<h3 class="wp-block-heading">Why LLM Apps Require Traces, Not Just Logs</h3>



<p>LLM-specific debugging demands visibility into things you cannot get from logging alone:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Prompt tracking</code>: See every prompt, <code data-enlighter-language="python" class="EnlighterJSRAW">system</code> message, and <code data-enlighter-language="python" class="EnlighterJSRAW">user</code> message.</li>



<li><strong>Chain-of-thought</strong><strong> structure: </strong>(Even if hidden, you can capture high-level execution steps.)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Latency breakdown</code>: Where time is spent: tokenization? forward pass? retrieval?</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Token usage visibility</code>: Cost control + throughput estimation.</li>



<li><strong>Hallucination hotspots: </strong>Which prompts or contexts fail most?</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Pipeline correctness</code>: Observations from retrieval → reasoning → generation.</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-37.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="332" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53806" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37.png?size=126x67&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37-300x160.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37.png?size=378x201&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37.png?size=504x268&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-37.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> LLM pipelines fail in subtle ways, including hallucinations, slowdowns, bad retrievals, and token spikes. Observability exposes these problems before users do.</figcaption></figure></div>


<h3 class="wp-block-heading">What Is Langfuse? (And Why It Is the Right Tool)</h3>



<p><strong><a href="https://langfuse.com" target="_blank" rel="noreferrer noopener">Langfuse</a></strong> is an open-source observability platform designed specifically for LLM apps. It captures:</p>



<ul class="wp-block-list">
<li>Traces</li>



<li>Spans</li>



<li>Prompt metadata</li>



<li>Inputs and outputs</li>



<li>Token usage</li>



<li>Latencies</li>



<li>Scores (quality, correctness, safety)</li>
</ul>



<p>…and displays them in a clean, production-grade UI.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-38.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="249" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53807" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38.png?size=126x50&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38-300x120.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38.png?size=378x151&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38.png?size=504x201&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-38.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> <code>Langfuse</code> Preview</figcaption></figure></div>


<p>You can think of it as:</p>



<p>“<code data-enlighter-language="python" class="EnlighterJSRAW">Prometheus</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">Grafana</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">MLflow</code>, but specifically for LLM pipelines.”</p>



<h4 class="wp-block-heading">Why Not MLflow or Weights &amp; Biases?</h4>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-39.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="340" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53809" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39.png?size=126x69&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39-300x163.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39.png?size=378x206&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39.png?size=504x275&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-39.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> LLM applications require observability during inference rather than training, which is where <code>Langfuse</code> provides the most value.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Langfuse-Fits-LLM-Observability-Stack"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Langfuse-Fits-LLM-Observability-Stack">How Langfuse Fits into an LLM Observability Stack</a></h2>



<p>Before building anything, consider the mental model:</p>



<ul class="wp-block-list">
<li><strong>Your Python LLM app: </strong>Sends prompts and metadata</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse SDK</code>: Records traces locally inside your code</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM Server</code> (port <code data-enlighter-language="python" class="EnlighterJSRAW">8000</code>)<strong>: </strong>Handles the actual model inference</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> (port <code data-enlighter-language="python" class="EnlighterJSRAW">3000</code>): Receives trace data from the SDK</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>: Aggregates, transforms, and prepares data for the dashboard</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code> database: Stores all traces, spans, scores, and token counts</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse UI</code> dashboard: Displays everything in real time</li>
</ul>



<p>This flow is the backbone of LLM observability.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-40-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="492" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-1024x492.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53811" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40.png?size=126x61&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-300x144.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40.png?size=378x182&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40.png?size=504x242&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40.png?size=630x303&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-768x369.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-1024x492.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-40-1536x738.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> The <code>Langfuse SDK</code> logs trace data inside your Python app, the <code>Langfuse Server</code> stores it in <code>PostgreSQL</code>, and the <code>Worker</code> powers the real-time dashboard.</figcaption></figure></div>


<h3 class="wp-block-heading">Why Self-Hosted Langfuse Instead of Cloud?</h3>



<p>When we first integrated <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Cloud</code> during development, we immediately ran into:</p>



<ul class="wp-block-list">
<li>trace delivery delays</li>



<li>out-of-order spans</li>



<li>slow UI updates</li>



<li>unreliable real-time feedback</li>
</ul>



<p>This is a <em>problem</em> when you are developing an agent or <code data-enlighter-language="python" class="EnlighterJSRAW">RAG</code> system and need to see:</p>



<ul class="wp-block-list">
<li>the exact prompt</li>



<li>the exact context</li>



<li>the exact output</li>



<li>the exact cost</li>



<li><strong>immediately</strong> after running your script.</li>
</ul>



<p>So we switched to:</p>



<h4 class="wp-block-heading">Self-Hosted Langfuse + Local vLLM</h4>



<p>Benefits:</p>



<ul class="wp-block-list">
<li>Real-time, near-instant traces</li>



<li>Fully local development</li>



<li>No Internet dependency</li>



<li>Faster iteration loops</li>



<li>Full control of database and dashboard</li>



<li>Ideal for agent debugging and RAG evaluation</li>
</ul>



<p>📌 <strong>OPTIONAL CALLOUT</strong></p>



<p><em><strong>One short bullet note: </strong></em><em>We still show the Cloud API flow briefly, but everything you build in this module uses the self-hosted setup for real-time performance.</em></p>



<h3 class="wp-block-heading">What You Will Build in This Lesson</h3>



<p>By the end of Lesson 1, you will have a complete local observability foundation:</p>



<h3 class="wp-block-heading">Infrastructure</h3>



<ul class="wp-block-list">
<li>Self-hosted <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code> (required for dashboards)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code> database</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> model server (<code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible API)</li>
</ul>



<h3 class="wp-block-heading">Tracing Skills</h3>



<ul class="wp-block-list">
<li>How to instrument an LLM call</li>



<li>How to build hierarchical traces (pipeline → model call)</li>



<li>How to log prompts, outputs, latencies, and token usage</li>



<li>How to visualize traces instantly in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> User Interface (UI) </li>
</ul>



<h3 class="wp-block-heading">What You Will Actually Run</h3>



<ul class="wp-block-list">
<li>Decorator-based tracing (<code data-enlighter-language="python" class="EnlighterJSRAW">tracing_decorator.py</code>)</li>



<li>Baseline app with <strong>no tracing</strong> (<code data-enlighter-language="python" class="EnlighterJSRAW">basic_llm_app.py</code>)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>-connected LLM client (<code data-enlighter-language="python" class="EnlighterJSRAW">llm_utils.py</code>)</li>



<li>Config loaders (<code data-enlighter-language="python" class="EnlighterJSRAW">config.py</code>)</li>
</ul>



<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-41-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="462" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-1024x462.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53814" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41.png?size=126x57&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-300x135.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41.png?size=378x171&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41.png?size=504x227&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41.png?size=630x284&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-768x346.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-1024x462.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-41-1536x693.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Our self-hosted stack: <code>vLLM</code> handles inference, the <code>Langfuse</code> Software Development Kit (SDK) records traces, and the <code>Langfuse Server</code> + <code>Langfuse Worker</code> + <code>PostgreSQL</code> power the observability dashboard.</figcaption></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Langfuse-Architecture-LLM-Observability"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Langfuse-Architecture-LLM-Observability">Langfuse Architecture for LLM Observability</a></h2>



<p>Before we start installing anything, let us zoom out and understand the architecture of the observability stack you are about to build. <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> is not just a dashboard. It is a coordinated system of services that receives traces, stores them, aggregates them, and displays them in real time. Your LLM app, <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code> all work together to form a complete observability pipeline.</p>



<p>Think of this section as building your mental model. Once you understand these flows, all the <code data-enlighter-language="python" class="EnlighterJSRAW">Docker</code> configuration, YAML files, keys, and scripts will make perfect sense.</p>



<h3 class="wp-block-heading">The High-Level Architecture</h3>



<p>At the core, your pipeline is simple:</p>



<ul class="wp-block-list">
<li>Your Python LLM app: executes inference and logs traces</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> Python SDK: captures all observability data</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM Server</code>: handles the actual LLM generation</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code>: receives trace, span, and token data</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code>: stores all traces, metadata, and scores</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>: aggregates data for dashboards</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse UI</code>: visualizes everything instantly</li>
</ul>



<p>This architecture ensures that every LLM call becomes a structured trace that you can drill into, including latencies, inputs, outputs, steps, errors, and token details.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-42-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="367" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-1024x367.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53816" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42.png?size=126x45&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-300x107.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42.png?size=378x135&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42.png?size=504x181&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42.png?size=630x226&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-768x275.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-1024x367.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-42-1536x550.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> Your Python app calls <code>vLLM</code> for inference and the <code>Langfuse SDK</code> for tracing. The <code>Langfuse Server</code> stores data in <code>PostgreSQL</code>, the <code>Langfuse Worker</code> processes it, and the UI displays it.</figcaption></figure></div>


<h3 class="wp-block-heading">How a Single LLM Request Turns Into a Trace</h3>



<p>Every time your code calls <code data-enlighter-language="python" class="EnlighterJSRAW">client.chat.completions.create(...)</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> performs 3 major steps behind the scenes:</p>



<ul class="wp-block-list">
<li><strong>Observe the call:</strong> capture input, parameters, metadata.</li>



<li><strong>Record the output:</strong> LLM response, tokens, shapes, errors.</li>



<li><strong>Create </strong><strong>and </strong><strong>update a trace hierarchy:</strong> pipeline spans, child spans, nested steps.</li>
</ul>



<p>For example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="1">llm_pipeline (trace)
    ├── retrieve_context (span)
    ├── rerank_candidates (span)
    └── generate_answer (span)
</pre>



<p>Even in Lesson 1 (where we only use decorators), you will already produce parent → child traces automatically.</p>



<p>Without this structure, debugging multi-step LLM pipelines becomes guesswork.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-43.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="337" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53818" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43.png?size=126x68&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43-300x162.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43.png?size=378x204&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43.png?size=504x272&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-43.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 8:</strong> Every LLM request becomes a structured trace: your app → <code>Langfuse SDK</code> → <code>Langfuse Server</code> → stored in <code>PostgreSQL</code> → visualized in real time.</figcaption></figure></div>


<h3 class="wp-block-heading">The Four Core Components You Will Deploy</h3>



<p>You will deploy <strong>4</strong> <strong>services</strong> using <code data-enlighter-language="python" class="EnlighterJSRAW">Docker Compose</code>:</p>



<h3 class="wp-block-heading">1. vLLM Server (Port 8000)</h3>



<p>Your local LLM inference engine.</p>



<p>It exposes an <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible Application Programming Interface (API) endpoint:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="2">http://localhost:8000/v1
</pre>



<p>Your Python scripts send prompts here.</p>



<h3 class="wp-block-heading">2. Langfuse Server (Port 3000)</h3>



<p>The brains of the observability system.</p>



<p>It receives traces from the Python SDK, stores them, and exposes the dashboard.</p>



<h3 class="wp-block-heading">3. Langfuse Worker</h3>



<p>Most tutorials miss this, but you cannot get dashboards without the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>.</p>



<p>It processes:</p>



<ul class="wp-block-list">
<li>aggregations</li>



<li>analytics</li>



<li>score updates</li>



<li>background tasks</li>
</ul>



<p>Without the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>:</p>



<p><strong>you will see traces, but your dashboard will be empty.</strong></p>



<h3 class="wp-block-heading">4. PostgreSQL (Port 5433 → 5432)</h3>



<p>Stores everything:</p>



<ul class="wp-block-list">
<li>traces</li>



<li>spans</li>



<li>metadata</li>



<li>scores</li>



<li>projects</li>



<li>settings</li>
</ul>



<p>It provides the persistence layer that the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> depends on.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-44.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="619" height="317" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53821" style="width:639px;height:auto" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44.png?size=126x65&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44-300x154.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44.png?size=378x194&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44.png?size=504x258&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-44.png?lossy=2&amp;strip=1&amp;webp=1 619w" sizes="(max-width: 619px) 100vw, 619px" /></a><figcaption class="wp-element-caption"><strong>Figure 9:</strong> The self-hosted <code>Langfuse</code> stack includes <code>vLLM</code> for inference, <code>Langfuse Server</code> for ingestion, <code>Langfuse Worker</code> for dashboards, and <code>PostgreSQL</code> for storage.</figcaption></figure></div>


<h3 class="wp-block-heading">How These Components Communicate (Data Flow)</h3>



<p>Let us make the full pipeline explicit:</p>



<ul class="wp-block-list">
<li><strong>Your script</strong> sends an inference request to <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse SDK</code> in your script sends trace info to <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> writes raw trace data into <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code> processes raw data to generate:
<ul class="wp-block-list">
<li>analytics</li>



<li>histograms</li>



<li>span trees</li>



<li>scores</li>
</ul>
</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Web UI</code> reads processed data and displays:
<ul class="wp-block-list">
<li>full trace trees</li>



<li>input/output pairs</li>



<li>token usage</li>



<li>latency heatmaps</li>



<li>error stacks</li>
</ul>
</li>
</ul>



<p>This is the “observability heartbeat” that runs for every request.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-45-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="461" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-1024x461.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53823" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45.png?size=126x57&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-300x135.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45.png?size=378x170&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45.png?size=504x227&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45.png?size=630x284&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-768x346.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-1024x461.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-45-1536x691.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 10:</strong> A complete view of how inference and tracing flow through your stack, from your Python script to the final <code>Langfuse</code> dashboard.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Understanding-LLM-Observability-Architecture-Matters"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Understanding-LLM-Observability-Architecture-Matters">Why Understanding LLM Observability Architecture Matters</a></h2>



<p>Before diving into code, it is important to visualize this system because:</p>



<ul class="wp-block-list">
<li>It prevents confusion when running <code data-enlighter-language="python" class="EnlighterJSRAW">Docker</code> for the first time.</li>



<li>You will instantly understand errors like “Worker not running” or “Database unavailable”.</li>



<li>You will know exactly where to look when traces do not appear.</li>



<li>You will develop intuition about how requests become saved spans.</li>
</ul>



<p>Once this architectural layer clicks, every file in <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code>, every script in <code data-enlighter-language="python" class="EnlighterJSRAW">src/</code>, and every dashboard panel will feel obvious.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Setting-Up-Self-Hosted-Langfuse-vLLM-Stack"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Setting-Up-Self-Hosted-Langfuse-vLLM-Stack">Setting Up a Self-Hosted Langfuse and vLLM Stack</a></h2>



<p>Before we can trace a single LLM call, we need to set up a clean project skeleton and a fully functioning self-hosted observability stack. In this section, you will configure the environment, install dependencies, review the project layout, understand each configuration file, and bring up the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> infrastructure using <code data-enlighter-language="python" class="EnlighterJSRAW">Docker Compose</code>.</p>



<p>Everything that comes later (tracing, scoring, evaluation, debugging) depends on getting this foundation right.</p>



<h3 class="wp-block-heading">Project Structure Overview</h3>



<p>Here is the complete repository structure we will use throughout this lesson:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="3">├── configs
│   └── config.yaml
├── docker-compose.yml
├── README.md
├── requirements.txt
└── src
    ├── basic_llm_app.py
    ├── config.py
    ├── evaluation_metrics.py
    ├── health_check.py
    ├── llm_utils.py
    ├── run_all_examples.py
    ├── tracing_decorator.py
    └── tracing_manual.py
</pre>



<p>At a high level:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/</code>: stores global configuration used by every example.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">src/</code>: contains the LLM application scripts, utilities, and tracing examples.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code>: defines the entire <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> infrastructure.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code>: defines Python dependencies.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code>: defines required environment variables.</li>
</ul>



<p>We will walk through each piece, focusing not on the logic inside every file, but on <em>how the system is designed</em> and <em>how everything connects</em>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-46-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="544" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-1024x544.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53827" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46.png?size=126x67&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-300x159.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46.png?size=378x201&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46.png?size=504x268&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46.png?size=630x335&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-768x408.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-1024x544.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-46-1536x816.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 11:</strong> The project separates configuration, infrastructure, and application modules to keep <code>Langfuse</code> observability reusable across different LLM workflows.</figcaption></figure></div>


<h3 class="wp-block-heading">Installing Dependencies</h3>



<p>Install the required Python packages:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="4">pip install -r requirements.txt
</pre>



<p>The key dependencies in this project are intentionally minimal. We use the following packages:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">langfuse&gt;=2.0.0</code>: provides the observability SDK and the <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">openai&gt;=1.0.0</code>: is required because <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> exposes an <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible API endpoint</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">python-dotenv</code>: loads <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> environment variables</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">pyyaml</code>: reads configuration values from <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">httpx</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">requests</code>: handle health checks and HTTP communication</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">numpy</code>: supports scoring and numeric utilities</li>
</ul>



<p>Together, these packages form the lightweight foundation for our self-hosted observability stack.</p>



<h3 class="wp-block-heading">Configuring Environment Variables</h3>



<p>Copy <code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code> into <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="5">cp .env.example .env
</pre>



<p>Then update the following values after starting <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="6">LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
LANGFUSE_SECRET_KEY="sk-lf-xxxx"
LANGFUSE_HOST=http://localhost:3000

OPENAI_BASE_URL=http://localhost:8000/v1
OPENAI_API_KEY=dummy
</pre>



<p>A few key points:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> <strong>keys</strong> come from your local dashboard once you create a project.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> <strong>does not require authentication</strong>, but the <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code> client still requires an API key value, so <code data-enlighter-language="python" class="EnlighterJSRAW">"dummy"</code> works.</li>



<li>If you use <code data-enlighter-language="python" class="EnlighterJSRAW">Hugging Face</code> models that are not cached, you may need a token.</li>
</ul>



<p>This <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> file becomes the backbone for all examples.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-47-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="317" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-1024x317.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53830" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47.png?size=126x39&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-300x93.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47.png?size=378x117&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47.png?size=504x156&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47.png?size=630x195&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-768x237.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-1024x317.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-47-1536x475.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 12:</strong> <code>Langfuse</code> keys come from the local dashboard, while <code>vLLM</code> uses an <code>OpenAI</code>-compatible endpoint, with everything funneling into the <code>.env</code> file read by your Python scripts.</figcaption></figure></div>


<h3 class="wp-block-heading">Centralized Configuration (configs/config.yaml)</h3>



<p>Instead of scattering options across scripts, everything is configured through one <code data-enlighter-language="python" class="EnlighterJSRAW">YAML</code> file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="7">llm:
  base_url: "http://localhost:8000/v1"
  model: "meta-llama/Llama-2-7b-chat-hf"
  temperature: 0.7
  max_tokens: 300

langfuse:
  host: "http://localhost:3000"
  project_name: "llm-observability-selfhosted"

evaluation:
  enable_scoring: true
  max_latency_ms: 5000
  min_length: 20
  good_length_threshold: 100
</pre>



<p>This allows you to:</p>



<ul class="wp-block-list">
<li>Switch models without changing code</li>



<li>Tune evaluation logic centrally</li>



<li>Redirect LLM traffic to remote endpoints if needed</li>



<li>Adjust <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> location</li>
</ul>



<p>Every script loads from this file automatically.</p>



<h3 class="wp-block-heading">Utility Modules (src/config.py and src/llm_utils.py)</h3>



<p>These 2 utilities prevent duplication across all examples.</p>



<h4 class="wp-block-heading">config.py: Central Configuration Loader</h4>



<p>This module provides:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">load_config()</code>: returns parsed <code data-enlighter-language="python" class="EnlighterJSRAW">YAML</code> config</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_llm_config()</code>: returns <code data-enlighter-language="python" class="EnlighterJSRAW">model</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">temp</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_langfuse_config()</code>: returns <code data-enlighter-language="python" class="EnlighterJSRAW">host</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">project_name</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_evaluation_config()</code>: returns scoring <code data-enlighter-language="python" class="EnlighterJSRAW">thresholds</code></li>
</ul>



<p>This keeps every script flexible and model-agnostic.</p>



<h4 class="wp-block-heading">llm_utils.py: Consistent vLLM Client Factory</h4>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> supports the <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code> Python client natively:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="8">client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
</pre>



<p>This module wraps it into a reusable function:</p>



<ul class="wp-block-list">
<li>Validates environment variables</li>



<li>Loads model name from <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code></li>



<li>Handles default <code data-enlighter-language="python" class="EnlighterJSRAW">base_url</code></li>



<li>Sets request timeouts</li>



<li>Returns the <code data-enlighter-language="python" class="EnlighterJSRAW">(client, model)</code> tuple when requested</li>
</ul>



<p>Every tracing example uses this function.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-48-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="239" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-1024x239.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53833" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48.png?size=126x29&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-300x70.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48.png?size=378x88&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48.png?size=504x118&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48.png?size=630x147&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-768x179.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-1024x239.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-48-1536x358.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 13:</strong> <code>config.py</code> reads <code>YAML</code> → <code>llm_utils.py</code> builds a <code>vLLM</code> client → example scripts use both modules for consistent behavior.</figcaption></figure></div>


<h3 class="wp-block-heading">The Self-Hosted Stack (docker-compose.yml)</h3>



<p>This is the heart of the system.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code> defines:</p>



<h3 class="wp-block-heading">Langfuse Server</h3>



<ul class="wp-block-list">
<li>Runs the frontend + API</li>



<li>Exposes port <code data-enlighter-language="python" class="EnlighterJSRAW">3000</code></li>



<li>Performs authentication, API key creation, and trace storage</li>
</ul>



<h3 class="wp-block-heading">Langfuse Worker</h3>



<ul class="wp-block-list">
<li>Mandatory for dashboards</li>



<li>Processes traces</li>



<li>Updates analytics, charts, latency heatmaps</li>
</ul>



<h3 class="wp-block-heading">PostgreSQL</h3>



<ul class="wp-block-list">
<li>Persistence layer for traces, spans, scores</li>



<li>Exposed on port <code data-enlighter-language="python" class="EnlighterJSRAW">5433</code> (to avoid conflicts)</li>
</ul>



<h3 class="wp-block-heading">vLLM Model Server (GPU or CPU)</h3>



<ul class="wp-block-list">
<li>Exposes <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible API at <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:8000/v1</code></li>



<li>Runs <code data-enlighter-language="python" class="EnlighterJSRAW">Llama 2</code> by default</li>



<li>Enables fast, local inference for testing</li>
</ul>



<p>You can start everything with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="9">docker-compose --profile gpu up -d
</pre>



<p>Or if you don’t have a GPU:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="10">docker-compose --profile cpu up -d
</pre>



<p>Verify services:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="11">docker-compose ps
</pre>



<p>Visit the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard: <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:3000</code></p>



<p><em><strong>Note:</strong></em><em> If you are running the server on a remote machine</em><em>,</em><em> do not forget to </em><em>use </em><em>SSH port forwarding</em><em>. O</em><em>therwise</em><em>,</em><em> you w</em><em>ill no</em><em>t be able to access the Langfuse UI dashboard from your local machine.</em></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-49.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="624" height="340" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53835" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49.png?size=126x69&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49-300x163.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49.png?size=378x206&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49.png?size=504x275&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-49.png?lossy=2&amp;strip=1&amp;webp=1 624w" sizes="(max-width: 624px) 100vw, 624px" /></a><figcaption class="wp-element-caption"><strong>Figure 14:</strong> The <code>docker-compose</code> setup includes <code>Langfuse Server</code>, <code>Langfuse Worker</code>, <code>PostgreSQL</code>, and <code>vLLM</code>, with each container handling a distinct responsibility within the observability stack.</figcaption></figure></div>


<h3 class="wp-block-heading">Bringing Up the Entire Observability Stack</h3>



<p>Once configuration is in place:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="12">docker-compose --profile gpu up -d
</pre>



<p>Then:</p>



<ul class="wp-block-list">
<li>Go to <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:3000</code></li>



<li>Create a project</li>



<li>Copy your <strong>public</strong> and <strong>secret</strong> keys</li>



<li>Paste them into <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code></li>



<li>Restart your Python script</li>
</ul>



<p>You now have:</p>



<ul class="wp-block-list">
<li>A live model server (<code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>)</li>



<li>A local observability platform (<code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>)</li>



<li>A database storing every trace</li>



<li>Real-time dashboards</li>



<li>A clean Python project ready for tracing</li>
</ul>



<p>The foundation is complete. Next, we will write and trace our first LLM call.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Baseline-LLM-Application-Before-Observability"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Baseline-LLM-Application-Before-Observability">Baseline LLM Application (Before Observability)</a></h2>



<p>Before we wire in <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>, we need a clean baseline: a tiny LLM app that talks to <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>, prints an answer, and knows nothing about traces, latency, or tokens.</p>



<p>This section walks through <code data-enlighter-language="python" class="EnlighterJSRAW">src/basic_llm_app.py</code> end-to-end so we have a clear “before” picture of life <strong>without </strong>observability.</p>



<h3 class="wp-block-heading">The Full Baseline Script</h3>



<p>Here is the full file we will dissect:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="13">"""
Basic LLM Application (No Tracing Baseline)

Simple pipeline using local vLLM server.
This version has NO tracing - compare with tracing_decorator.py
"""

from llm_utils import get_llm_client
from config import get_llm_config

# Initialize vLLM client with model from config
client, model = get_llm_client(load_model_from_config=True)
</pre>



<p>The docstring sets the tone very clearly:</p>



<p>this is a <strong>“no tracing”</strong> baseline that we will later compare against a traced version.</p>



<p>We import:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_llm_client</code>: a reusable helper that knows how to connect to <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> using <code data-enlighter-language="python" class="EnlighterJSRAW">OPENAI_BASE_URL</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">OPENAI_API_KEY</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">get_llm_config</code>: a small wrapper around <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code> so we don’t hardcode model parameters in the code.</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">client, model = get_llm_client(load_model_from_config=True)</code> gives us:</p>



<ul class="wp-block-list">
<li>an <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-compatible client already pointed at <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:8000/v1</code></li>



<li>the model name loaded from <code data-enlighter-language="python" class="EnlighterJSRAW">configs/config.yaml</code>.</li>
</ul>



<p>At this point, the app can already talk to <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>, but we still have <em>zero</em> observability.</p>



<h3 class="wp-block-heading">Generating an Answer (With No Tracing at All)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="14">def generate_answer(question: str) -> str:
    """Generate answer using vLLM - NO tracing."""
    # Load config
    llm_config = get_llm_config()
    temperature = llm_config.get("temperature", 0.7)
    max_tokens = llm_config.get("max_tokens", 300)
   
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": question}
            ],
            temperature=temperature,
            max_tokens=max_tokens
        )
        return response.choices[0].message.content
    except Exception as e:
        print(f"Error: {e}")
        print("Tip: Make sure vLLM is running (docker-compose up -d)")
        raise
</pre>



<h4 class="wp-block-heading">Loading config per call</h4>



<p>Inside <code data-enlighter-language="python" class="EnlighterJSRAW">generate_answer</code>, we first pull generation settings from <code data-enlighter-language="python" class="EnlighterJSRAW">YAML</code>:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">llm_config = get_llm_config()</code> loads the <code data-enlighter-language="python" class="EnlighterJSRAW">llm</code>: section from <code data-enlighter-language="python" class="EnlighterJSRAW">configs/config.yaml</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code> are read with sensible defaults (<code data-enlighter-language="python" class="EnlighterJSRAW">0.7</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">300</code>) in case the config is missing keys.</li>
</ul>



<p>This keeps your generation parameters <strong>config-driven</strong>, not hardcoded, which is great for experiments, but still does not give you any tracing.</p>



<h4 class="wp-block-heading">Making the chat completion request</h4>



<p>The try block does a standard <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code>-style chat completion call:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">model=model</code> uses the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>-hosted Llama model from your config.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">messages=[...]</code> constructs a simple conversation with:
<ul class="wp-block-list">
<li>a <code data-enlighter-language="python" class="EnlighterJSRAW">system</code> message: <code data-enlighter-language="python" class="EnlighterJSRAW">"You are a helpful assistant."</code></li>



<li>a <code data-enlighter-language="python" class="EnlighterJSRAW">user</code> message: the question string passed into the function.</li>
</ul>
</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code> control creativity and output length.</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> behaves like the <code data-enlighter-language="python" class="EnlighterJSRAW">OpenAI</code> API here, so <code data-enlighter-language="python" class="EnlighterJSRAW">response.choices[0].message.content</code> gives us the generated answer, which is then returned.</p>



<h4 class="wp-block-heading">Error handling (still without observability)</h4>



<p>If anything goes wrong (<code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> not running, bad network, misconfiguration), the <code data-enlighter-language="python" class="EnlighterJSRAW">except</code> block:</p>



<ul class="wp-block-list">
<li>Prints the raw error message.</li>



<li>Prints a helpful hint: <code data-enlighter-language="python" class="EnlighterJSRAW">Make sure vLLM is running (docker-compose up -d)</code>.</li>



<li>Re-raises the exception so the script fails loudly.</li>
</ul>



<p>This is <strong>basic error handling</strong>, but notice what is still missing:</p>



<ul class="wp-block-list">
<li>No trace of which prompt failed.</li>



<li>No structured record of latency or context.</li>



<li>No way to inspect this error later in a dashboard.</li>
</ul>



<p>Even errors are invisible beyond your terminal scrollback.</p>



<h3 class="wp-block-heading">Running the “Invisible” Pipeline</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="15">def run_simple_pipeline(question: str):
    """Simple pipeline without tracing - baseline example."""
    print(f"\n{'='*50}")
    print(f"Question: {question}")
    print(f"{'='*50}\n")
   
    print("Generating answer (no tracing)...")
    answer = generate_answer(question)
   
    print(f"✅ Answer:\n{answer}\n")
    print(f"{'='*50}\n")
</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">run_simple_pipeline</code> is deliberately small and linear:</p>



<ul class="wp-block-list">
<li>It prints a visual separator and echoes the question.</li>



<li>It calls <code data-enlighter-language="python" class="EnlighterJSRAW">generate_answer(question)</code>, the black-box LLM call.</li>



<li>It prints the answer and another separator.</li>
</ul>



<p>This gives you a <strong>nice terminal UX</strong>, but again, it is only surface-level:</p>



<ul class="wp-block-list">
<li>You see the <em>question</em> and <em>final answer</em>.</li>



<li>You do not see any internal steps.</li>



<li>You do not know how long it took.</li>



<li>You do not know how many tokens it used or how much it cost.</li>



<li>You cannot compare this run with previous ones.</li>
</ul>



<p>For anything beyond a toy demo, this is not enough.</p>



<h3 class="wp-block-heading">The __main__ Block</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="16">if __name__ == "__main__":
    question = "What is machine learning?"
    run_simple_pipeline(question)
</pre>



<p>The entry point is intentionally as minimal as possible:</p>



<ul class="wp-block-list">
<li>It defines a simple default question: <code data-enlighter-language="python" class="EnlighterJSRAW">"What is machine learning?"</code></li>



<li>It calls <code data-enlighter-language="python" class="EnlighterJSRAW">run_simple_pipeline(question)</code></li>
</ul>



<p>This makes <code data-enlighter-language="python" class="EnlighterJSRAW">basic_llm_app.py</code> runnable as a one-shot script:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="17">python src/basic_llm_app.py
</pre>



<p>It is perfect for quick manual testing and serves as a <strong>control group</strong> when we later add <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> tracing and see how much more we can observe.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-50-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="339" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-1024x339.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53837" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50.png?size=126x42&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-300x99.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50.png?size=378x125&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50.png?size=504x167&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50.png?size=630x209&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-768x254.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-1024x339.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-50-1536x508.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 15:</strong> The baseline <code>vLLM</code> pipeline returns answers but offers zero insight into prompts, latency, token usage, or internal steps.</figcaption></figure></div>


<h3 class="wp-block-heading">Why This Baseline Is Not Enough</h3>



<p>With this script, your entire view of the system is:</p>



<ul class="wp-block-list">
<li>one printed question</li>



<li>one printed answer</li>



<li>and maybe an error line if something crashes</li>
</ul>



<p>You cannot answer:</p>



<ul class="wp-block-list">
<li>“Why was this slow?”</li>



<li>“What exact prompt + params did we send?”</li>



<li>“How many tokens did we consume?”</li>



<li>“Where did the pipeline fail?”</li>



<li>“Why is today’s behavior different from yesterday’s?”</li>
</ul>



<p>For serious LLM work involving <code data-enlighter-language="python" class="EnlighterJSRAW">RAG</code> systems, agents, evaluation runs, and A/B testing, this is <strong>debugging in the dark</strong>.</p>



<p>That is exactly what <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> is going to fix.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Adding-LLM-Observability-Langfuse-observe-Decorator"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Adding-LLM-Observability-Langfuse-observe-Decorator">Adding LLM Observability with the Langfuse @observe Decorator</a></h2>



<p>At this point, you have seen how an uninstrumented LLM pipeline behaves: it works, but it hides everything that matters. Now it is time to unlock <strong>real observability</strong> using the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator, the cleanest and most powerful way to add tracing in <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> 2.x.</p>



<p>In this section, we will transform the baseline pipeline into a fully observable workflow, capturing:</p>



<ul class="wp-block-list">
<li>prompts</li>



<li>outputs</li>



<li>latency</li>



<li>token usage</li>



<li>metadata</li>



<li>hierarchy of steps (pipeline → model call)</li>



<li>trace IDs you can click and inspect instantly in <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code></li>
</ul>



<p>This is where everything finally becomes visible.</p>



<h3 class="wp-block-heading">Imports, Initialization, and Configuration Logging</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="18">import os
from langfuse.decorators import observe, langfuse_context
from llm_utils import get_llm_client
from config import get_llm_config
</pre>



<p>We import:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">observe</code> → adds tracing automatically</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">langfuse_context</code> → lets us update spans programmatically</li>



<li>our reusable LLM client and config loaders</li>
</ul>



<p>Before anything happens, the script prints the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> configuration:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="19">print("\n" + "="*70)
print("🔧 LANGFUSE CONFIGURATION")
print("="*70)
print(f"📍 LANGFUSE_HOST: {os.getenv('LANGFUSE_HOST', 'NOT SET')}")
print(f"🔑 LANGFUSE_PUBLIC_KEY: {os.getenv('LANGFUSE_PUBLIC_KEY', 'NOT SET')[:20]}...")
print(f"🔐 LANGFUSE_SECRET_KEY: {os.getenv('LANGFUSE_SECRET_KEY', 'NOT SET')[:20]}...")
print("="*70 + "\n")
</pre>



<p>This is extremely practical.</p>



<p>It confirms:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> host</li>



<li>truncated keys</li>



<li>environment setup correctness</li>
</ul>



<p>If anything is misconfigured, this block saves you debugging time before you even send a single request.</p>



<p>Finally, we initialize the LLM client:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="20">client, model = get_llm_client(load_model_from_config=True)
</pre>



<p>The model name and base URL automatically load from the <code data-enlighter-language="python" class="EnlighterJSRAW">YAML</code> config.</p>



<h3 class="wp-block-heading">Tracing a Single LLM Call with @observe</h3>



<p>Here is the traced model-call function:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="21">@observe(name="generate_answer")
def generate_answer(question: str) -> str:
</pre>



<p>This single decorator:</p>



<ul class="wp-block-list">
<li>creates a <strong>new observation</strong></li>



<li>wraps the function execution</li>



<li>automatically timestamps execution</li>



<li>links child spans to parent spans</li>
</ul>



<h4 class="wp-block-heading">Step 1: Recording Inputs</h4>



<p>Inside the function, the first thing we do is explicitly log the input:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="22">langfuse_context.update_current_observation(
    input={"question": question, "model": model}
)
</pre>



<p>This ensures <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> displays:</p>



<ul class="wp-block-list">
<li>full question</li>



<li>selected model</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">temperature</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code> (we will update outputs later)</li>
</ul>



<h4 class="wp-block-heading">Step 2: Tracking Latency Manually</h4>



<p>Although <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> timestamps spans automatically, we want explicit latency measurement:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="23">import time
start_time = time.time()
</pre>



<p>Then we perform the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> call:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="24">response = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": question}
    ],
    temperature=temperature,
    max_tokens=max_tokens
)
</pre>



<h4 class="wp-block-heading">Step 3: Computing Latency + Extracting Answer</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="25">latency_ms = (time.time() - start_time) * 1000
answer = response.choices[0].message.content
</pre>



<h3 class="wp-block-heading">Adding Outputs, Token Usage, and Metadata</h3>



<p>This is the heart of observability:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="26">langfuse_context.update_current_observation(
    output={"answer": answer},
    usage={
        "input": response.usage.prompt_tokens,
        "output": response.usage.completion_tokens,
        "total": response.usage.total_tokens
    },
    metadata={"latency_ms": round(latency_ms, 2)}
)
</pre>



<p>With a single update call, you give <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>:</p>



<h4 class="wp-block-heading">Outputs</h4>



<ul class="wp-block-list">
<li>final LLM response</li>
</ul>



<h4 class="wp-block-heading">Usage</h4>



<ul class="wp-block-list">
<li>prompt tokens</li>



<li>completion tokens</li>



<li>total tokens</li>
</ul>



<p>Essential for:</p>



<ul class="wp-block-list">
<li>cost analysis</li>



<li>throughput understanding</li>



<li>debugging prompt inflation</li>
</ul>



<h4 class="wp-block-heading">Metadata</h4>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">latency_ms</code> (explicit + human-readable)</li>
</ul>



<p>This is exactly what the baseline pipeline could <em>not</em> show.</p>



<p>Print statements reinforce visibility:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="27">print(f"📊 Latency: {latency_ms:.2f}ms")
print(f"📊 Tokens: {response.usage.prompt_tokens} → {response.usage.completion_tokens} (total: {response.usage.total_tokens})")
</pre>



<h3 class="wp-block-heading">Building Nested Traces with run_pipeline()</h3>



<p>The pipeline function also uses <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="28">@observe(name="llm_pipeline")
def run_pipeline(question: str):
</pre>



<p>This creates a <em>parent span</em>.</p>



<p>Any traced function called inside <code data-enlighter-language="python" class="EnlighterJSRAW">run_pipeline()</code> automatically becomes a <em>child span</em>.</p>



<h4 class="wp-block-heading">Updating the Trace Metadata</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="29">langfuse_context.update_current_trace(
    name="decorator_pipeline",
    metadata={"method": "decorator"}
)
</pre>



<p>This changes the trace title in the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse UI</code> and adds custom metadata so you always know which instrumentation method you used.</p>



<h4 class="wp-block-heading">Calling the Nested Span</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="30">answer = generate_answer(question)
</pre>



<p>This produces:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="31">llm_pipeline (parent)
└── generate_answer (child)
</pre>



<p>The tree structure appears instantly in <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>.</p>



<h4 class="wp-block-heading">Linking Back to the UI</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="32">trace_id = langfuse_context.get_current_trace_id()
print(f"🔍 View trace: {langfuse_host}/trace/{trace_id}")
</pre>



<p>This clickable URL directly opens the exact trace and is extremely useful while iterating locally.</p>



<h3 class="wp-block-heading">Flushing Traces Before Exit</h3>



<p>Short-lived scripts often exit before <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> sends data.</p>



<p>This line ensures nothing is lost:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="33">langfuse_context.flush()
print("✅ Traces sent!\n")
</pre>



<p>Without flushing, traces may appear incomplete or missing entirely.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-51-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="555" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-1024x555.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53839" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51.png?size=126x68&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-300x163.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51.png?size=378x205&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51.png?size=504x273&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51.png?size=630x341&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-768x417.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-1024x555.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-51-1536x833.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 16:</strong> The <code>@observe</code> decorator automatically builds a hierarchical trace. The pipeline becomes the parent span, and the model call becomes a child span with full visibility into latency, tokens, and outputs.</figcaption></figure></div>


<h3 class="wp-block-heading">Why the Decorator Approach Is the Best Default</h3>



<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-53.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="424" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53-1024x424.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53845" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53.png?size=126x52&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53-300x124.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53.png?size=378x157&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53.png?size=504x209&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53.png?size=630x261&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53-768x318.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53-1024x424.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-53.png?lossy=2&amp;strip=1&amp;webp=1 1033w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Comparison of manual tracing implementation versus <code>Langfuse</code>’s <code>@observe</code> decorator for automatic observability and trace management in LLM pipelines.</figcaption></figure>



<p>This is why nearly every modern <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> tutorial and production workflow recommends decorators as the <strong>first instrumentation layer</strong>.</p>



<h3 class="wp-block-heading">What You Just Built</h3>



<p>Your LLM pipeline now has:</p>



<ul class="wp-block-list">
<li>Clickable traces</li>



<li>Per-step metadata</li>



<li>Latency and token breakdown</li>



<li>Nested trace hierarchy</li>



<li>Real-time <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse UI</code> updates</li>



<li>Automatic error propagation</li>
</ul>



<p>This completes the transformation from:</p>



<p><strong>a blind LLM script → a fully observable workflow.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Verifying-Self-Hosted-Langfuse-Observability-Stack"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Verifying-Self-Hosted-Langfuse-Observability-Stack">Running and Verifying a Self-Hosted Langfuse Observability Stack</a></h2>



<p>By now, we have all the moving parts ready:</p>



<ul class="wp-block-list">
<li>the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code></li>



<li>the <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> model server</li>



<li>our traced LLM pipeline using the <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator</li>
</ul>



<p>In this section, we will bring everything online, verify the system health, and run the traced pipeline end-to-end. By the end, you will see your <em>first real traces</em> appear instantly inside the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard.</p>



<h3 class="wp-block-heading">Start the Self-Hosted Stack</h3>



<p>All core services, including <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code>, run through your project’s <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code>.</p>



<p>To start everything with GPU acceleration:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="34">docker compose --profile gpu up -d
</pre>



<p>Or, if you don’t have a GPU:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="35">docker compose --profile cpu up -d
</pre>



<p>This launches:</p>



<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-54.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="306" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54-1024x306.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53847" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54.png?size=126x38&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54-300x90.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54.png?size=378x113&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54.png?size=504x151&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54.png?size=630x188&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54-768x229.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54-1024x306.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-54.png?lossy=2&amp;strip=1&amp;webp=1 1038w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 2:</strong> Core <code>Langfuse</code> deployment services and their roles in trace collection, metric computation, storage, and local LLM inference.</figcaption></figure>



<p>You can check everything is healthy using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="36">docker compose ps
</pre>



<p><strong>Expected output (sample):</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="37">NAME                 STATUS              PORTS
langfuse-server      healthy             0.0.0.0:3000->3000/tcp
langfuse-worker      running            
langfuse-postgres    healthy             0.0.0.0:5433->5432/tcp
vllm-server          healthy             host:8000->8000/tcp
</pre>



<p><strong>Tip:</strong></p>



<p>If <code data-enlighter-language="python" class="EnlighterJSRAW">langfuse-worker</code> is not running, your dashboard will be empty.</p>



<p>If <code data-enlighter-language="python" class="EnlighterJSRAW">vllm-server</code> is not healthy, your LLM calls will fail.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-55-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="254" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55-1024x254.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53850" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55.png?size=126x31&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55-300x74.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55.png?size=378x94&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55.png?size=504x125&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55.png?size=630x156&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55-768x190.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55-1024x254.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-55-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 17:</strong> The full observability stack running locally using <code>Docker Compose</code>.</figcaption></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-56-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="239" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-1024x239.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53851" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56.png?size=126x29&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-300x70.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56.png?size=378x88&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56.png?size=504x118&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56.png?size=630x147&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-768x179.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-1024x239.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-56-1536x359.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 18:</strong> <code>Docker</code> containers running the local <code>Langfuse</code> observability stack, including the <code>Langfuse Server</code>, <code>Langfuse Worker</code>, <code>PostgreSQL</code> database, and <code>vLLM</code> inference service.</figcaption></figure></div>


<h3 class="wp-block-heading">Verify Each Component Individually</h3>



<h4 class="wp-block-heading">Langfuse Server (UI)</h4>



<p>Open:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="38">http://localhost:3000
</pre>



<p>You should see:</p>



<ul class="wp-block-list">
<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> login screen</li>



<li>The dashboard panel</li>



<li>Empty traces (for now)</li>
</ul>



<h4 class="wp-block-heading">vLLM Health</h4>



<p>Visit:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="39">http://localhost:8000/health
</pre>



<p>Expected JSON:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="40">{"status": "ok"}
</pre>



<p>If this endpoint fails, no LLM calls will work.</p>



<h4 class="wp-block-heading">PostgreSQL Health (optional)</h4>



<p>Inside <code data-enlighter-language="python" class="EnlighterJSRAW">Docker</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="41">docker compose logs langfuse-postgres
</pre>



<p>Look for:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="42">database system is ready to accept connections
</pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-57-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="236" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-1024x236.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53855" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57.png?size=126x29&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-300x69.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57.png?size=378x87&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57.png?size=504x116&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57.png?size=630x145&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-768x177.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-1024x236.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-57-1536x353.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 19:</strong> <code>Langfuse UI</code> Home Page</figcaption></figure></div>


<h3 class="wp-block-heading">Run Your First Traced Pipeline</h3>



<p>Now run the decorator-instrumented script:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="43">python src/tracing_decorator.py
</pre>



<p>You should see terminal output like:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="44">==================================================
Question: Explain neural networks briefly
==================================================

Generating answer with tracing...
📊 Latency: 312.45ms
📊 Tokens: 12 → 88 (total: 100)
🔍 View trace: http://localhost:3000/trace/01HXF...

⏳ Flushing traces to Langfuse...
✅ Traces sent!
</pre>



<p>This confirms:</p>



<ul class="wp-block-list">
<li>the decorator worked</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> received the trace</li>



<li>the worker processed it</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-58-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="794" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-1024x794.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53858" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58.png?size=126x98&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-300x232.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58.png?size=378x293&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58.png?size=504x391&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58.png?size=630x488&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-768x595.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-1024x794.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-58-1536x1190.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 20:</strong> Running the traced pipeline prints latency, token usage, and a direct link to the trace.</figcaption></figure></div>


<h3 class="wp-block-heading">View the Trace in Langfuse</h3>



<p>Open the printed URL, for example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="45">http://localhost:3000/trace/01HXFG23P9...
</pre>



<p>You will see:</p>



<h4 class="wp-block-heading">The parent trace</h4>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">decorator_pipeline</code></p>



<h4 class="wp-block-heading">A nested span</h4>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">generate_answer</code></p>



<h4 class="wp-block-heading">Full metadata</h4>



<ul class="wp-block-list">
<li>prompt</li>



<li>output</li>



<li>latency</li>



<li>token usage</li>



<li>model</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">system</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">user</code> messages</li>
</ul>



<p>This is the moment where the entire pipeline becomes visible.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-59-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="520" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-1024x520.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53860" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59.png?size=126x64&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-300x152.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59.png?size=378x192&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59.png?size=504x256&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59.png?size=630x320&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-768x390.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-1024x520.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-59-1536x779.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 21: </strong>The <code>Langfuse</code> trace view showing the full <code>decorator_pipeline</code> execution, including the parent trace, nested <code>generate_answer</code> span, inputs, outputs, and metadata captured automatically via the <code>@observe</code> decorator.</figcaption></figure></div>


<h3 class="wp-block-heading">Your Observability Stack Is Live</h3>



<p>By the end of this section, you now have:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code> + <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code> running locally</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> inference server healthy at port <code data-enlighter-language="python" class="EnlighterJSRAW">8000</code></li>



<li>traced LLM requests flowing into the dashboard</li>



<li>real-time visibility into latency, prompts, outputs, and token usage</li>
</ul>



<p>This forms the foundation for everything in Lesson 2:</p>



<ul class="wp-block-list">
<li>scores</li>



<li>evaluations</li>



<li>diagnostics</li>



<li>advanced tracing patterns</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you built the core foundation for modern LLM observability. You began by understanding why LLM applications need far more than traditional logs or metrics. They require visibility into prompts, responses, latency, token usage, and multi-step pipelines. This led naturally to <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code>, a tool purpose-built for tracing and monitoring LLM workloads.</p>



<p>You then deployed a fully self-hosted observability stack using <code data-enlighter-language="python" class="EnlighterJSRAW">Docker Compose</code>: <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Server</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse Worker</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">PostgreSQL</code>, and a local <code data-enlighter-language="python" class="EnlighterJSRAW">vLLM</code> model server. With the project structure, configuration files, and environment variables in place, your development environment became capable of real-time local trace analysis.</p>



<p>Next, you examined your baseline LLM script, a simple “send a question, print an answer” pipeline that works but offers zero visibility. No prompts, no timing, no token counts, and no traceability. This served as the perfect starting point to highlight why observability is essential.</p>



<p>With the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> <code data-enlighter-language="python" class="EnlighterJSRAW">@observe</code> decorator, you then transformed that invisible pipeline into a fully instrumented one. Every request now captures structured traces: inputs, outputs, latency, token usage, and parent-child spans. Running the script produced your first real trace inside the <code data-enlighter-language="python" class="EnlighterJSRAW">Langfuse</code> dashboard, revealing exactly what the model did and how the pipeline behaved.</p>



<p>By the end of the lesson, your LLM application evolved from a black box into a transparent, debuggable system running locally with self-hosted components.</p>



<p>In the next lesson, you will go deeper by adding manual tracing, scoring, evaluation logic, latency checks, and health diagnostics, building on the foundation you created today.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V. </strong>“LLM Observability with Self-Hosted Langfuse and vLLM,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/tadoh" target="_blank" rel="noreferrer noopener">https://pyimg.co/tadoh</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="LLM Observability with Self-Hosted Langfuse and vLLM" data-enlighter-group="46">@incollection{Singh_2026_llm-observability-self-hosted-langfuse-vllm,
  author = {Vikram Singh},
  title = {{LLM Observability with Self-Hosted Langfuse and vLLM}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/tadoh},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/18/llm-observability-with-self-hosted-langfuse-and-vllm/">LLM Observability with Self-Hosted Langfuse and vLLM</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components</title>
		<link>https://pyimagesearch.com/2026/05/11/building-and-training-a-kimi-k2-model-using-deepseek-v3-components/</link>
		
		<dc:creator><![CDATA[Puneet Mangla]]></dc:creator>
		<pubDate>Mon, 11 May 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Deep Learning]]></category>
		<category><![CDATA[Generative AI]]></category>
		<category><![CDATA[LLMs]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[agentic ai]]></category>
		<category><![CDATA[attention logits]]></category>
		<category><![CDATA[deepseek v3]]></category>
		<category><![CDATA[deepseek-v3]]></category>
		<category><![CDATA[hugging face transformers]]></category>
		<category><![CDATA[kimi k2]]></category>
		<category><![CDATA[kimi-k2]]></category>
		<category><![CDATA[llm training]]></category>
		<category><![CDATA[mixture of experts]]></category>
		<category><![CDATA[mla]]></category>
		<category><![CDATA[moe]]></category>
		<category><![CDATA[multi-head latent attention]]></category>
		<category><![CDATA[muonclip]]></category>
		<category><![CDATA[open source llm]]></category>
		<category><![CDATA[pytorch]]></category>
		<category><![CDATA[qk-clip]]></category>
		<category><![CDATA[synthetic data generation]]></category>
		<category><![CDATA[token efficiency]]></category>
		<category><![CDATA[transformer architecture]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53671</guid>

					<description><![CDATA[<p>Table of Contents Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components Kimi-K2 vs DeepSeek-V3: Key Architecture Differences in LLM Design Mixture of Experts Scaling in Kimi-K2: Model Size, Sparsity, and Efficiency Attention Head Optimization in Kimi-K2 for Efficient Long-Context&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/11/building-and-training-a-kimi-k2-model-using-deepseek-v3-components/">Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Building-Training-Kimi-K2-Model-Using-DeepSeek-V3-Components"><a rel="noopener" target="_blank" href="#h1-Building-Training-Kimi-K2-Model-Using-DeepSeek-V3-Components">Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components</a></li>

    <li id="TOC-h2-Kimi-K2-vs-DeepSeek-V3-Key-Architecture-Differences-LLM-Design"><a rel="noopener" target="_blank" href="#h2-Kimi-K2-vs-DeepSeek-V3-Key-Architecture-Differences-LLM-Design">Kimi-K2 vs DeepSeek-V3: Key Architecture Differences in LLM Design</a></li>
    <ul>
        <li id="TOC-h3-Mixture-Experts-Scaling-Kimi-K2-Model-Size-Sparsity-Efficiency"><a rel="noopener" target="_blank" href="#h3-Mixture-Experts-Scaling-Kimi-K2-Model-Size-Sparsity-Efficiency">Mixture of Experts Scaling in Kimi-K2: Model Size, Sparsity, and Efficiency</a></li>
        <li id="TOC-h3-Attention-Head-Optimization-Kimi-K2-Efficient-Long-Context-LLMs"><a rel="noopener" target="_blank" href="#h3-Attention-Head-Optimization-Kimi-K2-Efficient-Long-Context-LLMs">Attention Head Optimization in Kimi-K2 for Efficient Long-Context LLMs</a></li>
    </ul>

    <li id="TOC-h2-MuonClip-Optimizer-Stabilizing-Large-Scale-LLM-Training-Kimi-K2"><a rel="noopener" target="_blank" href="#h2-MuonClip-Optimizer-Stabilizing-Large-Scale-LLM-Training-Kimi-K2">MuonClip Optimizer: Stabilizing Large-Scale LLM Training in Kimi-K2</a></li>
    <ul>
        <li id="TOC-h3-Token-Efficiency-LLM-Training-Why-It-Matters-Kimi-K2"><a rel="noopener" target="_blank" href="#h3-Token-Efficiency-LLM-Training-Why-It-Matters-Kimi-K2">Token Efficiency in LLM Training: Why It Matters for Kimi-K2</a></li>
        <li id="TOC-h3-Attention-Logit-Explosion-LLMs-Training-Instability-Challenges"><a rel="noopener" target="_blank" href="#h3-Attention-Logit-Explosion-LLMs-Training-Instability-Challenges">Attention Logit Explosion in LLMs: Training Instability and Challenges</a></li>
        <li id="TOC-h3-QK-Clip-Preventing-Attention-Logit-Explosion-Kimi-K2-Training"><a rel="noopener" target="_blank" href="#h3-QK-Clip-Preventing-Attention-Logit-Explosion-Kimi-K2-Training">QK-Clip: Preventing Attention Logit Explosion in Kimi-K2 Training</a></li>
    </ul>

    <li id="TOC-h2-Training-Data-Optimization-Kimi-K2-Improving-Token-Utility-LLMs"><a rel="noopener" target="_blank" href="#h2-Training-Data-Optimization-Kimi-K2-Improving-Token-Utility-LLMs">Training Data Optimization for Kimi-K2: Improving Token Utility in LLMs</a></li>
    <ul>
        <li id="TOC-h3-Token-Utility-LLM-Training-Maximizing-Learning-per-Token"><a rel="noopener" target="_blank" href="#h3-Token-Utility-LLM-Training-Maximizing-Learning-per-Token">Token Utility in LLM Training: Maximizing Learning per Token</a></li>
        <li id="TOC-h3-Knowledge-Data-Rephrasing-LLMs-Improving-Training-Data-Quality"><a rel="noopener" target="_blank" href="#h3-Knowledge-Data-Rephrasing-LLMs-Improving-Training-Data-Quality">Knowledge Data Rephrasing for LLMs: Improving Training Data Quality</a></li>
    </ul>

    <li id="TOC-h2-Kimi-K2-Implementation-Training-Open-Source-LLM-DeepSeek-V3"><a rel="noopener" target="_blank" href="#h2-Kimi-K2-Implementation-Training-Open-Source-LLM-DeepSeek-V3">Kimi-K2 Implementation: Training an Open-Source LLM with DeepSeek-V3</a></li>
    <ul>
        <li id="TOC-h3-Multi-Head-Latent-Attention-MLA-Max-Logit-Tracking-Kimi-K2"><a rel="noopener" target="_blank" href="#h3-Multi-Head-Latent-Attention-MLA-Max-Logit-Tracking-Kimi-K2">Multi-Head Latent Attention (MLA) with Max Logit Tracking in Kimi-K2</a></li>
        <li id="TOC-h3-Implementing-MuonClip-Optimizer-Stable-LLM-Training"><a rel="noopener" target="_blank" href="#h3-Implementing-MuonClip-Optimizer-Stable-LLM-Training">Implementing the MuonClip Optimizer for Stable LLM Training</a></li>
        <li id="TOC-h3-Complete-Kimi-K2-Training-Pipeline-Setup-Config-Optimization"><a rel="noopener" target="_blank" href="#h3-Complete-Kimi-K2-Training-Pipeline-Setup-Config-Optimization">Complete Kimi-K2 Training Pipeline: Setup, Config, and Optimization</a></li>
    </ul>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Building-Training-Kimi-K2-Model-Using-DeepSeek-V3-Components"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Building-Training-Kimi-K2-Model-Using-DeepSeek-V3-Components">Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components</a></h2>



<p>The landscape of large language models (LLMs) is undergoing a fundamental transformation toward <strong>agentic intelligence</strong>, where models can autonomously perceive, plan, reason, and act within complex and dynamic environments. This paradigm shift moves beyond traditional static imitation learning toward models that actively learn through interaction, acquire skills beyond their training distribution, and adapt their behavior based on experience. Agentic intelligence represents a critical capability for the next generation of foundation models, with transformative implications for tool use, software development, and real-world autonomy.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?lossy=2&strip=1&webp=1" alt="building-training-kimi-k2-model-using-deepseek-v3-featured.png" class="wp-image-53723" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/building-training-kimi-k2-model-using-deepseek-v3-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>Kimi-K2 stands at the forefront of this revolution. As a 1.04 trillion-parameter Mixture-of-Experts (MoE) language model with 32 billion activated parameters, Kimi-K2 was purposefully designed to address the core challenges of agentic capability development. The model achieves remarkable performance across diverse benchmarks:</p>



<ul class="wp-block-list">
<li>66.1 on Tau2-bench</li>



<li>76.5 on ACEBench (en)</li>



<li>65.8 on SWE-bench Verified</li>



<li>53.7 on LiveCodeBench v6</li>



<li>75.1 on GPQA-Diamond</li>
</ul>



<p>On the LMSYS (Large Model Systems Organization) Arena leaderboard, Kimi-K2 ranks as the top open-source model and 5th overall, competing closely with Claude 4 Opus and Claude 4 Sonnet.</p>



<p>In this lesson, we dive deep into the technical innovations behind Kimi-K2, focusing on its architectural differences from DeepSeek-V3, the revolutionary MuonClip optimizer, and training data improvements. We also provide a complete implementation guide using DeepSeek-V3 components as building blocks.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Kimi-K2-vs-DeepSeek-V3-Key-Architecture-Differences-LLM-Design"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Kimi-K2-vs-DeepSeek-V3-Key-Architecture-Differences-LLM-Design">Kimi-K2 vs DeepSeek-V3: Key Architecture Differences in LLM Design</a></h2>



<p>While Kimi-K2 builds on DeepSeek-V3&#8217;s architecture, several strategic modifications were made to optimize agentic capabilities and inference efficiency. Understanding these architectural differences is crucial for implementing the model effectively (<strong>Table 1</strong>).</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-8.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="930" height="416" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53695" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?size=126x56&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8-300x134.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?size=378x169&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?size=504x225&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?size=630x282&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8-768x344.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-8.png?lossy=2&amp;strip=1&amp;webp=1 930w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Kimi-K2 vs DeepSeek-V3 Configurations (source: <a href="https://arxiv.org/pdf/2507.20534" target="_blank" rel="noreferrer noopener">Kimi Team, 2026</a>).</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Mixture-Experts-Scaling-Kimi-K2-Model-Size-Sparsity-Efficiency"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Mixture-Experts-Scaling-Kimi-K2-Model-Size-Sparsity-Efficiency">Mixture of Experts Scaling in Kimi-K2: Model Size, Sparsity, and Efficiency</a></h3>



<p>The most significant architectural departure lies in Kimi-K2&#8217;s aggressive sparsity scaling. Through carefully controlled small-scale experiments, the Kimi team developed a <strong>sparsity scaling law</strong> that demonstrated a clear relationship: with the number of activated parameters held constant (i.e., constant FLOPs), increasing the total number of experts consistently lowers both training and validation loss. This finding led to a dramatic increase in model sparsity.</p>



<p>Kimi-K2 employs <strong>384 experts</strong> compared to DeepSeek-V3&#8217;s 256 experts, representing a 50% increase. Despite this, the model maintains 8 active experts per token, resulting in a sparsity ratio of 48 (384/8) versus DeepSeek-V3&#8217;s 32 (256/8). This increased sparsity comes with a trade-off: while total parameters grow to 1.04 trillion (54% more than DeepSeek-V3&#8217;s 671B), the number of activated parameters actually <em>decreases</em> to 32.6B (13% less than DeepSeek-V3&#8217;s 37B). This design choice optimizes the compute-performance frontier, achieving superior model quality while maintaining efficient inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Attention-Head-Optimization-Kimi-K2-Efficient-Long-Context-LLMs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Attention-Head-Optimization-Kimi-K2-Efficient-Long-Context-LLMs">Attention Head Optimization in Kimi-K2 for Efficient Long-Context LLMs</a></h3>



<p>A critical optimization for agentic applications involves the number of attention heads. DeepSeek-V3 sets the number of attention heads to roughly twice the number of model layers (128 heads for 61 layers) to better utilize memory bandwidth. However, as context length increases, this design incurs significant inference overhead.</p>



<p>For agentic applications requiring efficient long-context processing, this becomes prohibitive. With a 128k sequence length, increasing attention heads from 64 to 128 (while keeping 384 total experts) leads to an <strong>83% increase in inference FLOPs</strong>. Through controlled experiments, the Kimi team found that doubling the number of attention heads yields only modest improvements in validation loss (0.5% to 1.2%) under iso-token training conditions.</p>



<p>Given that sparsity 48 already provides strong performance, the marginal gains from doubling attention heads do not justify the inference cost. Kimi-K2 therefore uses <strong>64 attention heads</strong> (half of DeepSeek-V3&#8217;s 128), dramatically reducing inference costs for long-context agentic workloads while maintaining competitive performance.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-MuonClip-Optimizer-Stabilizing-Large-Scale-LLM-Training-Kimi-K2"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-MuonClip-Optimizer-Stabilizing-Large-Scale-LLM-Training-Kimi-K2">MuonClip Optimizer: Stabilizing Large-Scale LLM Training in Kimi-K2</a></h2>



<p>The MuonClip optimizer represents one of the most significant innovations in Kimi-K2&#8217;s development, addressing the fundamental challenge of training stability at trillion-parameter scale while maintaining token efficiency. Understanding MuonClip requires examining both the underlying Muon optimizer and the novel QK-Clip mechanism that makes it stable for large-scale training.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Token-Efficiency-LLM-Training-Why-It-Matters-Kimi-K2"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Token-Efficiency-LLM-Training-Why-It-Matters-Kimi-K2">Token Efficiency in LLM Training: Why It Matters for Kimi-K2</a></h3>



<p>Given the increasingly limited availability of high-quality human data, <strong>token efficiency</strong> has emerged as a critical factor in LLM scaling. Token efficiency refers to how much performance improvement is achieved per token consumed during training. The Muon optimizer, introduced by <a href="https://kellerjordan.github.io/posts/muon/" target="_blank" rel="noreferrer noopener">Jordan et al. (2024)</a>, substantially outperforms AdamW under the same compute budget, model size, and training data volume.</p>



<p>Previous work in Moonlight demonstrated that Muon&#8217;s token efficiency gains make it an ideal choice for maximizing the intelligence extracted from limited high-quality tokens. However, scaling Muon to trillion-parameter models revealed a critical challenge: <strong>training instability due to exploding attention logits</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Attention-Logit-Explosion-LLMs-Training-Instability-Challenges"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Attention-Logit-Explosion-LLMs-Training-Instability-Challenges">Attention Logit Explosion in LLMs: Training Instability and Challenges</a></h3>



<p>During medium-scale training runs using vanilla Muon, attention logits rapidly exceeded magnitudes of 1000, leading to numerical instabilities and occasional training divergence (<strong>Figure 1</strong>). This phenomenon occurred more frequently with Muon than with AdamW, suggesting that Muon&#8217;s aggressive optimization dynamics amplify instabilities in the attention mechanism.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-9.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="889" height="545" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53698" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?size=126x77&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9-300x184.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?size=378x232&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?size=504x309&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?size=630x386&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9-768x471.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-9.png?lossy=2&amp;strip=1&amp;webp=1 889w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Attention logits rapidly exceed 1000, which could lead to potential numerical instabilities and even training divergence (source: <a href="https://arxiv.org/pdf/2507.20534" target="_blank" rel="noreferrer noopener">Kimi Team, 2026</a>).</figcaption></figure></div>


<p>Existing mitigation strategies proved insufficient:</p>



<ul class="wp-block-list">
<li><strong>Logit soft-capping</strong> (used in Gemma) directly clips attention logits, but the dot products between queries and keys can still grow excessively <em>before</em> capping is applied</li>



<li><strong>Query-Key Normalization (QK-Norm)</strong> (<a href="https://arxiv.org/abs/2302.05442" target="_blank" rel="noreferrer noopener">Dehghani</a><a href="https://arxiv.org/abs/2302.05442" target="_blank" rel="noreferrer noopener"> et al., 2023</a>) is incompatible with Multi-head Latent Attention (MLA) because full key matrices are not explicitly materialized during inference</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-QK-Clip-Preventing-Attention-Logit-Explosion-Kimi-K2-Training"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-QK-Clip-Preventing-Attention-Logit-Explosion-Kimi-K2-Training">QK-Clip: Preventing Attention Logit Explosion in Kimi-K2 Training</a></h3>



<p>To address this fundamental challenge, the Kimi team proposed <strong>QK-Clip</strong>, a novel weight-clipping mechanism that explicitly constrains attention logits by rescaling the query and key projection weights post-update. The elegance of QK-Clip lies in its simplicity: it does not alter forward and backward computation in the current step but instead uses maximum logits as a guiding signal to control weight growth (<strong>Figure 2</strong>).</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-2.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="900" height="553" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-53700" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?size=126x77&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-300x184.jpeg?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?size=378x232&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?size=504x310&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?size=630x387&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-768x472.jpeg?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.jpeg?lossy=2&amp;strip=1&amp;webp=1 900w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Maximum logits for Kimi-K2 with MuonClip and τ = 100 over the entire training run. The max logits rapidly increase to the capped value of 100 before decaying to a stable range (source: <a href="https://arxiv.org/pdf/2507.20534" target="_blank" rel="noreferrer noopener">Kimi Team, 2026</a>).</figcaption></figure></div>


<p>For each attention head <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/251/2510c39011c5be704182423e3a695e91-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='h' title='h' class='latex' />, the attention mechanism computes:</p>



<p class="has-text-align-center"><img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/257/25724548027ce7dd7fabfff6a26a14cb-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='Q^h = X W_q^h, \quad K^h = X W_k^h, \quad V^h = X W_v^h' title='Q^h = X W_q^h, \quad K^h = X W_k^h, \quad V^h = X W_v^h' class='latex' srcset='https://b2633864.smushcdn.com/2633864/wp-content/latex/257/25724548027ce7dd7fabfff6a26a14cb-ffffff-000000-0.png?lossy=2&strip=1&webp=1 295w,https://b2633864.smushcdn.com/2633864/wp-content/latex/257/25724548027ce7dd7fabfff6a26a14cb-ffffff-000000-0.png?size=126x9&lossy=2&strip=1&webp=1 126w' sizes='(max-width: 295px) 100vw, 295px' /></p>



<p>The attention output is:</p>



<p class="has-text-align-center"><img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/514/51409ecdc9e1c7546bbb02d0c5f46616-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='O^h = \text{softmax}\left(\dfrac{1}{\sqrt{d}} Q^h (K^h)^\top\right) V^h' title='O^h = \text{softmax}\left(\dfrac{1}{\sqrt{d}} Q^h (K^h)^\top\right) V^h' class='latex' srcset='https://b2633864.smushcdn.com/2633864/wp-content/latex/514/51409ecdc9e1c7546bbb02d0c5f46616-ffffff-000000-0.png?lossy=2&strip=1&webp=1 239w,https://b2633864.smushcdn.com/2633864/wp-content/latex/514/51409ecdc9e1c7546bbb02d0c5f46616-ffffff-000000-0.png?size=126x22&lossy=2&strip=1&webp=1 126w' sizes='(max-width: 239px) 100vw, 239px' /></p>



<p>QK-Clip defines the <strong>max logit</strong> per head as:</p>



<p class="has-text-align-center"><img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/41b/41b103dd14298456087cb885c9b0ea34-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='S_{\max}^h = \dfrac{1}{\sqrt{d}} \max_{X \in B} \max_{i,j} Q_i^h (K_j^h)^\top' title='S_{\max}^h = \dfrac{1}{\sqrt{d}} \max_{X \in B} \max_{i,j} Q_i^h (K_j^h)^\top' class='latex' srcset='https://b2633864.smushcdn.com/2633864/wp-content/latex/41b/41b103dd14298456087cb885c9b0ea34-ffffff-000000-0.png?lossy=2&strip=1&webp=1 255w,https://b2633864.smushcdn.com/2633864/wp-content/latex/41b/41b103dd14298456087cb885c9b0ea34-ffffff-000000-0.png?size=126x19&lossy=2&strip=1&webp=1 126w' sizes='(max-width: 255px) 100vw, 255px' /></p>



<p>where <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/9d5/9d5ed678fe57bcca610140957afab571-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='B' title='B' class='latex' /> is the current batch and <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/f54/f540942e195ca3ac12148363180a7912-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='i, j' title='i, j' class='latex' /> index different tokens.</p>



<p>When <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/fbb/fbbfe0d629823f9635abc37a80d44390-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='S_{\max}^h' title='S_{\max}^h' class='latex' /> exceeds a threshold <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/a6f/a6f317b268ae825d94f832f970af607c-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\tau' title='\tau' class='latex' /> (set to 100 for Kimi-K2), QK-Clip rescales the weights. Critically, the rescaling is applied <strong>per-head</strong> rather than globally, minimizing intervention on heads that remain stable:</p>



<p class="has-text-align-center"><img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/dbb/dbba3baf5b2edebc882c9a597b2fce7b-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\gamma_h = \min(1, \tau / S_{\max}^h)' title='\gamma_h = \min(1, \tau / S_{\max}^h)' class='latex' />.</p>



<p>This per-head, component-aware clipping represents a substantial refinement over naive global clipping strategies.</p>



<p><strong>Figure 3 </strong>describes the complete algorithm for MuonClip Optimizer.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-3-scaled.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="502" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-1024x502.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-53702" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.jpeg?size=126x62&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-300x147.jpeg?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.jpeg?size=378x185&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.jpeg?size=504x247&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.jpeg?size=630x309&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-768x377.jpeg?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-1024x502.jpeg?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-scaled.jpeg?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> MuonClip Algorithm (source: <a href="https://arxiv.org/pdf/2507.20534" target="_blank" rel="noreferrer noopener">Kimi Team, 2026</a>).</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Training-Data-Optimization-Kimi-K2-Improving-Token-Utility-LLMs"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Training-Data-Optimization-Kimi-K2-Improving-Token-Utility-LLMs">Training Data Optimization for Kimi-K2: Improving Token Utility in LLMs</a></h2>



<p>Beyond architectural and optimizer innovations, Kimi-K2&#8217;s superior performance stems significantly from strategic improvements in training data. With high-quality human-generated data becoming increasingly scarce, the focus shifts to <strong>increasing token utility</strong>, defined as the effective learning signal each token contributes to model updates.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Token-Utility-LLM-Training-Maximizing-Learning-per-Token"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Token-Utility-LLM-Training-Maximizing-Learning-per-Token">Token Utility in LLM Training: Maximizing Learning per Token</a></h3>



<p>Token efficiency in pre-training encompasses 2 related but distinct concepts:</p>



<ul class="wp-block-list">
<li><strong>Optimizer efficiency:</strong> How effectively the optimizer extracts signal from each gradient update (addressed by MuonClip)</li>



<li><strong>Token utility:</strong> The inherent information density and learning signal in each token</li>
</ul>



<p>Increasing token utility directly improves token efficiency. A naive approach involves repeated exposure to the same tokens across multiple epochs, but this leads to overfitting and reduced generalization. The key innovation in Kimi-K2 lies in a sophisticated <strong>synthetic data generation strategy</strong> that amplifies high-quality tokens without inducing overfitting.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Knowledge-Data-Rephrasing-LLMs-Improving-Training-Data-Quality"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Knowledge-Data-Rephrasing-LLMs-Improving-Training-Data-Quality">Knowledge Data Rephrasing for LLMs: Improving Training Data Quality</a></h3>



<p>Pre-training on knowledge-intensive text presents a fundamental trade-off: a single epoch is insufficient for comprehensive knowledge absorption, while multi-epoch repetition yields diminishing returns. To resolve this tension, Kimi-K2 employs a synthetic rephrasing framework with the following 3 key components.</p>



<h4 class="wp-block-heading">Style- and Perspective-Diverse Prompting</h4>



<p>To enhance linguistic diversity while maintaining factual integrity, carefully engineered prompts guide a large language model to generate faithful rephrasings in varied styles and perspectives. This approach ensures that while surface-level linguistic features change, the underlying factual content remains consistent. The diversity of expressions forces the model to learn robust representations of the same knowledge across multiple linguistic realizations.</p>



<h4 class="wp-block-heading">Chunk-wise Autoregressive Generation</h4>



<p>Long documents pose a challenge for standard LLM-based rewriting due to implicit output length limitations. Kimi-K2 addresses this through a chunk-based autoregressive strategy: documents are segmented, each segment is rephrased individually with preserved context, and segments are stitched back together to form complete passages. This methodology prevents information loss and maintains global coherence across extended texts (<strong>Figure 4</strong>).</p>



<h4 class="wp-block-heading">Fidelity Verification</h4>



<p>To ensure consistency between original and rewritten content, fidelity checks compare the semantic alignment of each rephrased passage with its source. This quality control step prevents the introduction of hallucinations or factual errors during the rephrasing process.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-10.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="913" height="410" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53704" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?size=126x57&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10-300x135.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?size=378x170&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?size=504x226&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?size=630x283&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10-768x345.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-10.png?lossy=2&amp;strip=1&amp;webp=1 913w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Auto-regressive chunk-wise rephrasing pipeline for long input excerpts (source: <a href="https://arxiv.org/pdf/2507.20534" target="_blank" rel="noreferrer noopener">Kimi Team, 2026</a>).</figcaption></figure></div>


<h4 class="wp-block-heading">Mathematics Data Rephrasing</h4>



<p>To enhance mathematical reasoning capabilities, high-quality mathematical documents are rewritten into a &#8220;learning-note&#8221; style following SwallowMath methodology (<strong>Figure 5</strong>). This transformation converts dense mathematical exposition into more pedagogical formats that better support learning. Additionally, data diversity is increased through the translation of high-quality mathematical materials from other languages into English, effectively multiplying the available high-quality mathematical training data.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-11-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="298" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11-1024x298.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53706" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11.png?size=126x37&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11-300x87.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11.png?size=378x110&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11.png?size=504x147&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11.png?size=630x183&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11-768x223.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11-1024x298.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-11-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Four-stage pipeline for constructing SwallowMath (source: <a href="https://arxiv.org/pdf/2505.02881" target="_blank" rel="noreferrer noopener">Fujii et al., 2026</a>).</figcaption></figure></div>


<h4 class="wp-block-heading">Overall Pre-training Corpus</h4>



<p>The complete Kimi-K2 pre-training corpus comprises <strong>15.5 trillion tokens</strong> of curated, high-quality data spanning 4 primary domains:</p>



<ul class="wp-block-list">
<li><strong>Web Text:</strong> General knowledge and natural language understanding</li>



<li><strong>Code:</strong> Programming and structured reasoning</li>



<li><strong>Mathematics:</strong> Quantitative reasoning and formal problem-solving</li>



<li><strong>Knowledge:</strong> Domain-specific expertise and factual information</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Kimi-K2-Implementation-Training-Open-Source-LLM-DeepSeek-V3"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Kimi-K2-Implementation-Training-Open-Source-LLM-DeepSeek-V3">Kimi-K2 Implementation: Training an Open-Source LLM with DeepSeek-V3</a></h2>



<p>In this section, we walk through the key implementation details for training Kimi-K2, focusing specifically on the components that differ from the standard DeepSeek-V3 implementation. We&#8217;ll examine the enhanced Multi-head Latent Attention with max logit tracking, the MuonClip optimizer implementation, and the custom training setup.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Multi-Head-Latent-Attention-MLA-Max-Logit-Tracking-Kimi-K2"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Multi-Head-Latent-Attention-MLA-Max-Logit-Tracking-Kimi-K2">Multi-Head Latent Attention (MLA) with Max Logit Tracking in Kimi-K2</a></h3>



<p>The Multi-head Latent Attention (MLA) mechanism in Kimi-K2 extends DeepSeek-V3&#8217;s implementation with critical modifications to support QK-Clip. The key enhancement is <strong>per-head max-logit tracking</strong> during the forward pass, which provides the signal needed for weight clipping by the optimizer.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="1">class MultiheadLatentAttention(nn.Module):
    
    def __init__(self, config: DeepSeekConfig):
        super().__init__()
        self.config = config
        self.n_embd = config.n_embd
        self.n_head = config.n_head
        self.head_dim = config.n_embd // config.n_head

        # Compression dimensions
        self.kv_lora_rank = config.kv_lora_rank
        self.q_lora_rank = config.q_lora_rank
        self.rope_dim = config.rope_dim

        # KV compression
        self.kv_proj = nn.Linear(self.n_embd, self.kv_lora_rank, bias=False)
        self.kv_norm = RMSNorm(self.kv_lora_rank)

        # KV decompression
        self.k_decompress = nn.Linear(self.kv_lora_rank, self.n_head * self.head_dim, bias=False)
        self.v_decompress = nn.Linear(self.kv_lora_rank, self.n_head * self.head_dim, bias=False)

        # Query compression
        self.q_proj = nn.Linear(self.n_embd, self.q_lora_rank, bias=False)
        self.q_decompress = nn.Linear(self.q_lora_rank, self.n_head * self.head_dim, bias=False)

        # RoPE projections
        self.k_rope_proj = nn.Linear(self.n_embd, self.n_head * self.rope_dim, bias=False)
        self.q_rope_proj = nn.Linear(self.q_lora_rank, self.n_head * self.rope_dim, bias=False)

        # Output projection
        self.o_proj = nn.Linear(self.n_head * self.head_dim, self.n_embd, bias=config.bias)

        # Dropout
        self.attn_dropout = nn.Dropout(config.dropout)
        self.resid_dropout = nn.Dropout(config.dropout)

        # RoPE
        self.rope = RotaryEmbedding(self.rope_dim, config.block_size)

        # Causal mask
        self.register_buffer(
            "causal_mask",
            torch.tril(torch.ones(config.block_size, config.block_size)).view(
                1, 1, config.block_size, config.block_size
            )
        )

        self.max_logits = 0.0  # Track maximum attention logits

</pre>



<p><strong>O</strong><strong>n Lines 1-47</strong>, we define the MLA architecture following DeepSeek-V3&#8217;s design with compression and decompression of queries and key-values through low-rank projections. The key innovation appears on <strong>Line 49</strong>, where we initialize <code data-enlighter-language="python" class="EnlighterJSRAW">self.max_logits = 0.0</code>, a critical state variable that tracks the maximum attention logits across heads. This tracking mechanism is essential for QK-Clip to function properly.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="52" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="2">    def forward(self, x: torch.Tensor, attention_mask: Optional[torch.Tensor] = None):
        B, T, C = x.size()

        # Compression phase
        kv_compressed = self.kv_norm(self.kv_proj(x))
        q_compressed = self.q_proj(x)

        # Decompression phase
        k_content = self.k_decompress(kv_compressed)
        v = self.v_decompress(kv_compressed)
        q_content = self.q_decompress(q_compressed)

        # RoPE components
        k_rope = self.k_rope_proj(x)
        q_rope = self.q_rope_proj(q_compressed)

        # Reshape for multi-head attention
        k_content = k_content.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
        v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
        q_content = q_content.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
        k_rope = k_rope.view(B, T, self.n_head, self.rope_dim).transpose(1, 2)
        q_rope = q_rope.view(B, T, self.n_head, self.rope_dim).transpose(1, 2)

        # Apply RoPE
        cos, sin = self.rope(x, T)
        q_rope = apply_rope(q_rope, cos, sin)
        k_rope = apply_rope(k_rope, cos, sin)

        # Concatenate content and rope parts
        q = torch.cat([q_content, q_rope], dim=-1)
        k = torch.cat([k_content, k_rope], dim=-1)
     
</pre>



<p><strong>On Lines 52-82</strong>, we implement the standard forward pass through the compression-decompression pipeline. The input undergoes compression via <code data-enlighter-language="python" class="EnlighterJSRAW">kv_proj</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">q_proj</code>, followed by decompression through dedicated linear layers. We then reshape tensors for multi-head processing and apply Rotary Position Embeddings (RoPE) separately to content and positional components. This separation allows per-head QK-Clip to target only the appropriate components without affecting shared rotary embeddings.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="84" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="3">        # Concatenate content and rope parts
        q = torch.cat([q_content, q_rope], dim=-1)
        k = torch.cat([k_content, k_rope], dim=-1)

        # Attention computation
        scale = 1.0 / math.sqrt(q.size(-1))
        scores = torch.matmul(q, k.transpose(-2, -1)) * scale

        with torch.no_grad():
            # self.max_logits = torch.max(scores, dim=1).item()
            self.max_logits = list(torch.max(scores.transpose(1, 0).contiguous().view(scores.shape[1], -1), dim=-1)[0])

        # Apply causal mask
        scores = scores.masked_fill(self.causal_mask[:, :, :T, :T] == 0, float('-inf'))

        # Apply padding mask if provided
        if attention_mask is not None:
            padding_mask_additive = (1 - attention_mask).unsqueeze(1).unsqueeze(2) * float('-inf')
            scores = scores + padding_mask_additive

        # Softmax and dropout
        attn_weights = F.softmax(scores, dim=-1)
        attn_weights = self.attn_dropout(attn_weights)

        # Apply attention to values
        out = torch.matmul(attn_weights, v)

        # Reshape and project
        out = out.transpose(1, 2).contiguous().view(B, T, self.n_head * self.head_dim)
        out = self.resid_dropout(self.o_proj(out))

        return out
</pre>



<p>On <strong>Lines 89-94</strong>, we compute attention scores and implement the crucial max logit tracking. The score computation follows standard scaled dot-product attention. However, <strong>Lines 92-94</strong> represent a key departure from vanilla DeepSeek-V3: we track the maximum attention logit <strong>per head</strong> using <code data-enlighter-language="python" class="EnlighterJSRAW">torch.no_grad()</code> to avoid affecting gradients. The <code data-enlighter-language="python" class="EnlighterJSRAW">scores</code> tensor has shape <code data-enlighter-language="python" class="EnlighterJSRAW">[batch, num_heads, seq_len, seq_len]</code>, and we transpose and reshape to extract per-head maximum values. This per-head granularity enables targeted intervention only on heads exhibiting logit explosion, minimizing disruption to stable heads.</p>



<p>On <strong>Lines </strong><strong>97-113</strong>, we complete the attention mechanism with causal masking, optional padding masks, softmax normalization, and dropout. The final output projection maintains the standard MLA architecture. The elegance of this implementation lies in its non-invasiveness: max logit tracking adds minimal computational overhead (a single max operation under <code data-enlighter-language="python" class="EnlighterJSRAW">torch.no_grad</code>) while providing the critical signal for optimizer-level weight clipping.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Implementing-MuonClip-Optimizer-Stable-LLM-Training"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Implementing-MuonClip-Optimizer-Stable-LLM-Training">Implementing the MuonClip Optimizer for Stable LLM Training</a></h3>



<p>The MuonClip optimizer represents the core innovation enabling stable trillion-parameter training. Our implementation integrates Newton-Schulz orthogonalization, RMS matching, weight decay, and per-head QK-Clip into a unified optimizer.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="4">def apply_qk_clip_per_head(
    query_weights: torch.Tensor,
    key_weights: torch.Tensor,
    max_logits_per_head: Union[List[float], torch.Tensor],
    tau: float = 100.0
) -> None:
        if isinstance(max_logits_per_head, list):
        max_logits_per_head = torch.tensor(
            max_logits_per_head,
            device=query_weights.device,
            dtype=query_weights.dtype
        )
    apply_qk_clip_vectorized(query_weights, key_weights, max_logits_per_head, tau)

</pre>



<p>On <strong>Lines 1-13</strong>, we define the entry point for the QK-Clip application. The function accepts query and key projection weights along with per-head max logits and a threshold <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/a6f/a6f317b268ae825d94f832f970af607c-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\tau' title='\tau' class='latex' /> (defaulting to <code data-enlighter-language="python" class="EnlighterJSRAW">100</code>). We handle both list and tensor inputs for flexibility, converting lists to tensors on the appropriate device with matching <code data-enlighter-language="python" class="EnlighterJSRAW">dtype</code>. The critical design choice here is <strong>in-place modification</strong>: we directly modify weight tensors to avoid memory allocation overhead during optimization.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="15" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="5">def apply_qk_clip_per_head(
    query_weights: torch.Tensor,
    key_weights: torch.Tensor,
    max_logits_per_head: Union[List[float], torch.Tensor],
    tau: float = 100.0
) -> None:
        if isinstance(max_logits_per_head, list):
        max_logits_per_head = torch.tensor(
            max_logits_per_head,
            device=query_weights.device,
            dtype=query_weights.dtype
        )
    apply_qk_clip_vectorized(query_weights, key_weights, max_logits_per_head, tau)

@torch.no_grad()
def apply_qk_clip_vectorized(
    query_weights: torch.Tensor,
    key_weights: torch.Tensor,
    max_logits_per_head: torch.Tensor,
    tau: float = 100.0
) -> None:
    
    q_out, q_in = query_weights.shape[0], query_weights.shape[1]
    k_out, k_in = key_weights.shape[0], key_weights.shape[1]
    num_heads = len(max_logits_per_head)
    d_k = q_out // num_heads

    # Ensure tensor type
    if not isinstance(max_logits_per_head, torch.Tensor):
        max_logits_per_head = torch.tensor(
            max_logits_per_head,
            device=query_weights.device,
            dtype=query_weights.dtype
        )

    # Compute scaling factors: gamma = tau / max_logit where max_logit > tau
    needs_clip = max_logits_per_head > tau
</pre>



<p>On <strong>Lines 15-48</strong>, we extract dimensions and ensure tensor type compatibility. We first extract dimensions and compute the per-head scaling factor <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/dbb/dbba3baf5b2edebc882c9a597b2fce7b-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\gamma_h = \min(1, \tau / S_{\max}^h)' title='\gamma_h = \min(1, \tau / S_{\max}^h)' class='latex' /> only for heads where <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/f0a/f0a17224c7274f174055214500c5c70e-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='S_{\max}^h &gt; \tau' title='S_{\max}^h &gt; \tau' class='latex' />.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="52" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="6">@torch.no_grad()
def apply_qk_clip_vectorized(
    query_weights: torch.Tensor,
    key_weights: torch.Tensor,
    max_logits_per_head: torch.Tensor,
    tau: float = 100.0
) -> None:
    
    q_out, q_in = query_weights.shape[0], query_weights.shape[1]
    k_out, k_in = key_weights.shape[0], key_weights.shape[1]
    num_heads = len(max_logits_per_head)
    d_k = q_out // num_heads

    # Ensure tensor type
    if not isinstance(max_logits_per_head, torch.Tensor):
        max_logits_per_head = torch.tensor(
            max_logits_per_head,
            device=query_weights.device,
            dtype=query_weights.dtype
        )

    # Compute scaling factors: gamma = tau / max_logit where max_logit > tau
    needs_clip = max_logits_per_head > tau

    # If no clipping needed, return early
    if not needs_clip.any():
        return

    gamma = torch.where(
        needs_clip,
        tau / max_logits_per_head.clamp(min=1e-8),
        torch.ones_like(max_logits_per_head)
    )
    sqrt_gamma = torch.sqrt(gamma)

    # Reshape weights to [d_model, num_heads, d_k] for per-head scaling
    # Views share underlying storage, so in-place ops modify original tensor
    q_reshaped = query_weights.view(q_out // num_heads, num_heads, q_in)
    k_reshaped = key_weights.view(k_out // num_heads, num_heads, k_in)

    # Apply per-head scaling IN-PLACE: broadcast sqrt_gamma [num_heads] over [d_model, num_heads, d_k]
    q_reshaped.mul_(sqrt_gamma.view(1, num_heads, 1))
    k_reshaped.mul_(sqrt_gamma.view(1, num_heads, 1))

    q_reshaped = q_reshaped.view(q_out, q_in)
    k_reshaped = k_reshaped.view(k_out, k_in)
</pre>



<p>On <strong>Lines 80-97</strong>, we perform the actual weight clipping through careful tensor reshaping and in-place multiplication. The weights are reshaped from <code data-enlighter-language="python" class="EnlighterJSRAW">[d_model, d_model]</code> to <code data-enlighter-language="python" class="EnlighterJSRAW">[d_model/num_heads, num_heads, d_k]</code> to expose the head dimension. We then apply <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/1b9/1b93d4fbff6b33f401722350670a419d-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\sqrt{\gamma_h}' title='\sqrt{\gamma_h}' class='latex' /> scaling using in-place multiplication (<code data-enlighter-language="python" class="EnlighterJSRAW">mul_</code>) with broadcasting. The square root scaling ensures that when query and key both receive <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/68e/68e92bd7d9878c99406d6f534f99f10a-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\sqrt{\gamma}' title='\sqrt{\gamma}' class='latex' />, their dot product receives the full <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/ae5/ae539dfcc999c28e25a0f3ae65c1de79-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\gamma' title='\gamma' class='latex' /> scaling. This elegant mathematical property allows us to clip attention logits by rescaling the weights that produce them, rather than clipping logits directly after they&#8217;re computed.</p>



<p><strong>Lines</strong> <strong>77</strong><strong> and </strong><strong>78</strong> implement early exit if no head requires clipping, which becomes a common case later in training when attention logits stabilize. This optimization avoids unnecessary computation when the model is well-behaved.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="7">class MuonClip(torch.optim.Optimizer):
    def __init__(
        self,
        params,
        lr: float = 1e-3,
        momentum: float = 0.95,
        weight_decay: float = 0.01,
        tau: float = 100.0,
        ns_steps: int = 5,
        eps: float = 1e-7
    ):
        if lr &lt; 0.0:
            raise ValueError(f"Invalid learning rate: {lr}")
        if not 0.0 &lt;= momentum &lt;= 1.0:
            raise ValueError(f"Invalid momentum value: {momentum}")
        if weight_decay &lt; 0.0:
            raise ValueError(f"Invalid weight_decay value: {weight_decay}")
        if tau &lt;= 0.0:
            raise ValueError(f"Invalid tau value: {tau}")

        defaults = dict(
            lr=lr,
            momentum=momentum,
            weight_decay=weight_decay,
            tau=tau,
            ns_steps=ns_steps,
            eps=eps
        )
        super().__init__(params, defaults)

        # For QK-Clip functionality
        self.model = None
        self.attention_layers = []

    def set_model(self, model: nn.Module):
        self.model = model
        if hasattr(model, 'get_attention_layers'):
            self.attention_layers = model.get_attention_layers()

</pre>



<p>On <strong>Lines 1-33</strong>, we define the MuonClip optimizer class, inheriting from PyTorch&#8217;s base <code data-enlighter-language="python" class="EnlighterJSRAW">Optimizer</code>. The constructor accepts standard hyperparameters (learning rate, momentum, weight decay) plus QK-Clip-specific parameters (<img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/a6f/a6f317b268ae825d94f832f970af607c-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\tau' title='\tau' class='latex' /> and Newton-Schulz steps). We validate all parameters and initialize state tracking. Critically, <strong>Lines 35-38</strong> implement model registration through <code data-enlighter-language="python" class="EnlighterJSRAW">set_model()</code>, which extracts attention layers for later QK-Clip application. This design separates optimizer logic from model architecture, allowing the optimizer to operate on any model exposing a <code data-enlighter-language="python" class="EnlighterJSRAW">get_attention_layers()</code> method.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="40" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="8">    @torch.no_grad()
    def step(self, closure: Optional[Callable] = None) -> Optional[float]:
        loss = None
        if closure is not None:
            with torch.enable_grad():
                loss = closure()

        for group in self.param_groups:
            lr = group['lr']
            momentum = group['momentum']
            weight_decay = group['weight_decay']
            ns_steps = group['ns_steps']
            eps = group['eps']

            for p in group['params']:
                if p.grad is None:
                    continue

                grad = p.grad
                state = self.state[p]

                # Initialize momentum buffer
                if len(state) == 0:
                    state['momentum_buffer'] = torch.zeros_like(p)

                buf = state['momentum_buffer']

                # Apply momentum: Mt = μMt−1 + Gt
                buf.mul_(momentum).add_(grad)

                if p.ndim >= 2:  # 2D+ parameters - use Muon
                    # Apply Newton-Schulz orthogonalization
                    if p.ndim > 2:
                        original_shape = buf.shape
                        buf_2d = buf.view(buf.shape[0], -1)
                        orthogonal_update = newton_schulz(buf_2d, ns_steps, eps)
                        orthogonal_update = orthogonal_update.view(original_shape)
                    else:
                        orthogonal_update = newton_schulz(buf, ns_steps, eps)

                    # RMS matching factor: √(max(n,m) × 0.2)
                    n, m = p.shape[0], p.shape[1] if p.ndim > 1 else 1
                    rms_factor = math.sqrt(max(n, m) * 0.2)
                    orthogonal_update = orthogonal_update * rms_factor

                    # Update: Wt = Wt−1 − η(Ot + λWt−1)
                    p.add_(orthogonal_update + weight_decay * p, alpha=-lr)
                else:
                    # 1D parameters - standard momentum
                    p.add_(buf + weight_decay * p, alpha=-lr)

        # Apply QK-Clip
        self._apply_qk_clip()

        return loss

</pre>



<p>On <strong>Lines 41-94</strong>, we implement the core optimization step integrating Muon updates with QK-Clip. The step begins with standard closure handling and parameter group iteration. <strong>Lines 41-68</strong> implement momentum accumulation (<img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/a94/a94eaf68f4dd0ecd984fe2a564e63f2f-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='M_t = \mu M_{t-1} + G_t' title='M_t = \mu M_{t-1} + G_t' class='latex' />) using in-place operations for memory efficiency. The critical branching occurs at <strong>Line 70</strong>: parameters with 2+ dimensions receive Muon treatment. </p>



<p>On <strong>Lines 72-83</strong>, we apply the Muon update for matrix parameters. Newton-Schulz orthogonalization produces an orthogonal approximation of the momentum buffer, which we then scale by <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/1ed/1ed57c6055afe2435bbee42ba89e80be-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='\sqrt{\max(n,m)} \times 0.2' title='\sqrt{\max(n,m)} \times 0.2' class='latex' /> to match AdamW&#8217;s RMS characteristics. This scaling ensures Muon&#8217;s updates have similar magnitudes to AdamW, enabling easier hyperparameter transfer. Finally, <strong>Line 86</strong> applies the update with weight decay: <img src='https://b2633864.smushcdn.com/2633864/wp-content/latex/265/265fbabff429aa2668545a63e371a4c7-ffffff-000000-0.png?lossy=2&strip=1&webp=1' alt='W_t = W_{t-1} - \eta(O_t + \lambda W_{t-1})' title='W_t = W_{t-1} - \eta(O_t + \lambda W_{t-1})' class='latex' srcset='https://b2633864.smushcdn.com/2633864/wp-content/latex/265/265fbabff429aa2668545a63e371a4c7-ffffff-000000-0.png?lossy=2&strip=1&webp=1 200w,https://b2633864.smushcdn.com/2633864/wp-content/latex/265/265fbabff429aa2668545a63e371a4c7-ffffff-000000-0.png?size=126x11&lossy=2&strip=1&webp=1 126w' sizes='(max-width: 200px) 100vw, 200px' />. <strong>Line 89</strong> applies standard momentum updates to 1D parameters such as biases and normalization layers.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="96" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="9">    def _apply_qk_clip(self):
        """Apply QK-Clip to attention layers to prevent logit explosion."""
        if not self.attention_layers:
            return

        tau = self.param_groups[0]['tau']

        for attention_layer in self.attention_layers:
            if not hasattr(attention_layer, 'max_logits'):
                continue

            max_logits = attention_layer.max_logits
            if not max_logits:
                continue


            # Handle both scalar and per-head max logits
            if isinstance(max_logits, (int, float)):
                max_logits = [max_logits]


            apply_qk_clip_per_head(
                    attention_layer.k_decompress.weight.data,
                    attention_layer.q_decompress.weight.data,
                    max_logits,
                    tau
            )
</pre>



<p>On <strong>Lines 96-122</strong>, we apply QK-Clip after all weight updates. The <code data-enlighter-language="python" class="EnlighterJSRAW">_apply_qk_clip()</code> method iterates through all registered attention layers, extracts their <code data-enlighter-language="python" class="EnlighterJSRAW">max_logits</code> attribute (populated during forward pass), and applies per-head clipping to the query and key decompression weights. This post-update clipping ensures weights don&#8217;t grow unboundedly across training steps while preserving gradient information within each step.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Complete-Kimi-K2-Training-Pipeline-Setup-Config-Optimization"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Complete-Kimi-K2-Training-Pipeline-Setup-Config-Optimization">Complete Kimi-K2 Training Pipeline: Setup, Config, and Optimization</a></h3>



<p>Finally, we bring everything together in a complete training configuration:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="10">config = DeepSeekConfig()
config.multi_token_predict = 0
config.n_experts = 8
config.n_head = 4

training_args = TrainingArguments(
    output_dir="./kimik2_checkpoints",
    num_train_epochs=2,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=4,
    learning_rate=5e-4,
    warmup_steps=10,
    weight_decay=0.01,
    logging_dir="./kimik2_checkpoints/logs",
    logging_steps=50,
    save_steps=50,
    save_total_limit=3,
    eval_steps=50,
    eval_strategy="steps",
    save_strategy="steps",
    metric_for_best_model="eval_loss",
    greater_is_better=False,
    gradient_accumulation_steps=4,
    fp16=True,
    dataloader_num_workers=2,
    remove_unused_columns=False,
    report_to="none",
    push_to_hub=False,
    save_safetensors=False,
)
</pre>



<p>On <strong>Lines 1-4</strong>, we configure the model architecture. Kimi-K2 does not use Multi-Token Prediction, so we disable multi-token prediction (<code data-enlighter-language="python" class="EnlighterJSRAW">multi_token_predict=0</code>) to simplify training and focus on core capabilities. We use <code data-enlighter-language="python" class="EnlighterJSRAW">8</code> experts for this educational implementation rather than the hundreds used in production-scale Kimi-K2 and DeepSeek-V3 models. We also use <code data-enlighter-language="python" class="EnlighterJSRAW">4</code> attention heads for this small-scale educational implementation, compared to the production-scale configurations used in DeepSeek-V3 and Kimi-K2.</p>



<p>On <strong>Lines 6-30</strong>, we define training arguments following best practices for small-scale experiments. We use gradient accumulation (<code data-enlighter-language="python" class="EnlighterJSRAW">4</code> steps) to simulate larger batch sizes with limited GPU memory, enable mixed-precision training (<code data-enlighter-language="python" class="EnlighterJSRAW">fp16=True</code>) for speed and memory efficiency, and configure regular evaluation and checkpointing every <code data-enlighter-language="python" class="EnlighterJSRAW">50</code> steps. The learning rate of <code data-enlighter-language="python" class="EnlighterJSRAW">5e-4</code> is conservative for stable training, with a brief <code data-enlighter-language="python" class="EnlighterJSRAW">10</code>-step warmup.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="31" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="11">model = DeepSeek(config)

data_collator = DeepSeekDataCollator(tokenizer)

optimizer = MuonClip(model.parameters(), lr=5e-3)
optimizer.set_model(model)

# Create trainer
trainer = DeepSeekTrainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    data_collator=data_collator,
    optimizers=(optimizer, None)
)

print("✓ Trainer created. Starting training...")
print("=" * 80)

# Train!
trainer.train()

print("=" * 80)
print("✓ Training complete!")

# Save final model
trainer.save_model("./kimik2_final")
tokenizer.save_pretrained("./kimik2_final")
print("✓ Model saved to ./kimik2_final")
</pre>



<p>On <strong>Lines 31-36</strong>, we initialize the model and create a MuonClip <code data-enlighter-language="python" class="EnlighterJSRAW">optimizer</code>. Critically, <strong>Line 36</strong> registers the model with the optimizer using <code data-enlighter-language="python" class="EnlighterJSRAW">set_model()</code>, enabling QK-Clip to access attention layers. This registration must occur before training begins.</p>



<p>On <strong>Lines 39-60</strong>, we instantiate the custom <code data-enlighter-language="python" class="EnlighterJSRAW">trainer</code> with all components and launch training. The <code data-enlighter-language="python" class="EnlighterJSRAW">optimizers=(optimizer, None)</code> argument provides our custom optimizer to Hugging Face Trainer, overriding its default optimizer creation. After training completes, we save both the model weights and tokenizer for later inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>We began by detailing how to train Kimi-K2 from scratch using DeepSeek-V3 components, emphasizing the architectural differences that set Kimi-K2 apart. We explored the model’s scale and sparsity, showing that reducing the number of attention heads allowed us to balance efficiency and performance. A key part of this journey was the introduction of the MuonClip optimizer, which stabilizes training while pushing the limits of large-scale language modeling.</p>



<p>We then turned to the challenges of token efficiency and the attention logit explosion problem. To address these, we introduced the QK-Clip innovation, which helped us control runaway logits and improve overall stability. Alongside this, we refined our training data pipeline, focusing on token utility and knowledge data rephrasing to ensure that every token contributed meaningfully to the model’s learning process. These improvements allowed us to maximize the value of the data while keeping training efficient.</p>



<p>Finally, we described the implementation details, including enhanced multi-head latent attention with max logit tracking and the practical integration of the MuonClip optimizer. We concluded with a complete training setup, showing how all these innovations came together to make Kimi-K2 a robust, efficient, and scalable model. By combining architectural refinements, optimizer breakthroughs, and data improvements, this lesson demonstrated how these techniques push the boundaries of what’s possible in modern language model training.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Mangla, P</strong><strong>. </strong>“Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/d3tge" target="_blank" rel="noreferrer noopener">https://pyimg.co/d3tge</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components" data-enlighter-group="12">@incollection{Mangla_2026_building-training-kimi-k2-model-using-deepseek-v3,
  author = {Puneet Mangla},
  title = {{Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/d3tge},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/11/building-and-training-a-kimi-k2-model-using-deepseek-v3-components/">Building and Training a Kimi-K2 Model Using DeepSeek-V3 Components</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</title>
		<link>https://pyimagesearch.com/2026/05/04/semantic-caching-for-llms-ttls-confidence-and-cache-safety/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 04 May 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[LLMOps]]></category>
		<category><![CDATA[Machine Learning]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[cache poisoning]]></category>
		<category><![CDATA[cache ttl]]></category>
		<category><![CDATA[confidence scoring]]></category>
		<category><![CDATA[deduplication]]></category>
		<category><![CDATA[fastapi]]></category>
		<category><![CDATA[llm caching]]></category>
		<category><![CDATA[llm optimization]]></category>
		<category><![CDATA[llmops]]></category>
		<category><![CDATA[production llm]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[semantic caching]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53619</guid>

					<description><![CDATA[<p>Table of Contents Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety Why Semantic Caching for LLMs Requires Production Hardening Cache TTL in Semantic Caching: Preventing Stale LLM Responses MLOps Project Structure for Semantic Caching with FastAPI and Redis How&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/04/semantic-caching-for-llms-ttls-confidence-and-cache-safety/">Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Semantic-Caching-LLMs-TTLs-Confidence-Cache-Safety"><a rel="noopener" target="_blank" href="#h1-Semantic-Caching-LLMs-TTLs-Confidence-Cache-Safety">Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</a></li>

    <li id="TOC-h2-Why-Semantic-Caching-LLMs-Requires-Production-Hardening"><a rel="noopener" target="_blank" href="#h2-Why-Semantic-Caching-LLMs-Requires-Production-Hardening">Why Semantic Caching for LLMs Requires Production Hardening</a></li>

    <li id="TOC-h2-Cache-TTL-Semantic-Caching-Preventing-Stale-LLM-Responses"><a rel="noopener" target="_blank" href="#h2-Cache-TTL-Semantic-Caching-Preventing-Stale-LLM-Responses">Cache TTL in Semantic Caching: Preventing Stale LLM Responses</a></li>

    <li id="TOC-h2-MLOps-Project-Structure-Semantic-Caching-FastAPI-Redis"><a rel="noopener" target="_blank" href="#h2-MLOps-Project-Structure-Semantic-Caching-FastAPI-Redis">MLOps Project Structure for Semantic Caching with FastAPI and Redis</a></li>

    <li id="TOC-h2-How-Implement-Cache-TTL-Validation-Python-Redis"><a rel="noopener" target="_blank" href="#h2-How-Implement-Cache-TTL-Validation-Python-Redis">How to Implement Cache TTL Validation in Python and Redis</a></li>

    <li id="TOC-h2-Confidence-Scoring-Semantic-Caching-Beyond-Similarity-LLMs"><a rel="noopener" target="_blank" href="#h2-Confidence-Scoring-Semantic-Caching-Beyond-Similarity-LLMs">Confidence Scoring in Semantic Caching: Beyond Similarity for LLMs</a></li>

    <li id="TOC-h2-Implementing-Confidence-Scoring-LLM-Cache-Optimization-Code-Walkthrough"><a rel="noopener" target="_blank" href="#h2-Implementing-Confidence-Scoring-LLM-Cache-Optimization-Code-Walkthrough">Implementing Confidence Scoring for LLM Cache Optimization (Code Walkthrough)</a></li>

    <li id="TOC-h2-Query-Normalization-Deduplication-Efficient-Semantic-Caching"><a rel="noopener" target="_blank" href="#h2-Query-Normalization-Deduplication-Efficient-Semantic-Caching">Query Normalization and Deduplication for Efficient Semantic Caching</a></li>

    <li id="TOC-h2-Preventing-Cache-Poisoning-Semantic-Caching-LLM-Systems"><a rel="noopener" target="_blank" href="#h2-Preventing-Cache-Poisoning-Semantic-Caching-LLM-Systems">Preventing Cache Poisoning in Semantic Caching for LLM Systems</a></li>

    <li id="TOC-h2-End-to-End-Semantic-Cache-Hardening-TTL-Confidence-Safety-Demos"><a rel="noopener" target="_blank" href="#h2-End-to-End-Semantic-Cache-Hardening-TTL-Confidence-Safety-Demos">End-to-End Semantic Cache Hardening: TTL, Confidence, and Safety Demos</a></li>

    <li id="TOC-h2-Semantic-Caching-Limitations-Trade-Offs-LLM-Optimization-Systems"><a rel="noopener" target="_blank" href="#h2-Semantic-Caching-Limitations-Trade-Offs-LLM-Optimization-Systems">Semantic Caching Limitations: Trade-Offs in LLM Optimization Systems</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Semantic-Caching-LLMs-TTLs-Confidence-Cache-Safety"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Semantic-Caching-LLMs-TTLs-Confidence-Cache-Safety">Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</a></h2>



<p>In this lesson, you will learn how to harden a semantic cache for LLMs, one of the most important LLMOps patterns for reducing redundant inference costs, and move from a working semantic caching prototype to a system that can survive real-world usage with TTL validation, confidence scoring, deduplication, and cache poisoning prevention.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?lossy=2&strip=1&webp=1" alt="semantic-caching-llms-ttls-confidence-cache-safety-feature.png" class="wp-image-53650" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/semantic-caching-llms-ttls-confidence-cache-safety-feature.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>This lesson is the last in a 2-part series on <strong>Semantic Caching for LLMs</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/yso6f" target="_blank" rel="noreferrer noopener">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a></strong></em></li>



<li><strong><em><a href="https://pyimg.co/ahr3p" target="_blank" rel="noreferrer noopener">Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</a></em></strong><strong> (this tutorial)</strong></li>
</ol>



<p><strong>To learn how to harden a semantic cache for LLMs and make it safe, reliable, and production-ready, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Semantic-Caching-LLMs-Requires-Production-Hardening"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Semantic-Caching-LLMs-Requires-Production-Hardening">Why Semantic Caching for LLMs Requires Production Hardening</a></h2>



<p>In Lesson 1, we built a semantic cache that works end-to-end. It correctly avoids redundant LLM calls, reuses responses for identical queries, and even handles paraphrased inputs via semantic similarity. For many tutorials, that would be the end of the story.</p>



<p>In real systems, however, working is only the starting point.</p>



<p>A semantic cache that works under ideal conditions can still fail in subtle and dangerous ways when exposed to real users, long-running processes, and evolving information. These failures do not usually appear as crashes or explicit errors. Instead, they show up as <strong>silent correctness issues</strong>, degraded user trust, and unpredictable behavior over time.</p>



<h3 class="wp-block-heading">What Lesson 1 Solved — and What It Didn’t</h3>



<p>Lesson 1 focused on the <strong>correctness of flow</strong>:</p>



<ul class="wp-block-list">
<li>Requests move through exact match → semantic match → LLM fallback (generation)</li>



<li>Cached responses are reused when appropriate</li>



<li>The system is observable and debuggable</li>



<li>Nothing is hidden behind abstractions</li>
</ul>



<p>What it intentionally did not address was <strong>long-term safety</strong>.</p>



<p>We did not ask:</p>



<ul class="wp-block-list">
<li><em>How old is this cached response, and should we still trust it?</em></li>



<li><em>What happens if the LLM returns an error or partial output?</em></li>



<li><em>What if the cache slowly fills with duplicates?</em></li>



<li><em>What if similarity is high but the answer is no longer valid?</em></li>
</ul>



<p>Those questions only matter once the system runs for days or weeks, not minutes.</p>



<h3 class="wp-block-heading">Real-World Failure Modes in Semantic Caching</h3>



<p>Semantic caching introduces failure modes that rarely exist in traditional exact-match caches.</p>



<p>For example:</p>



<ul class="wp-block-list">
<li>A cached answer with very high similarity may still be <strong>stale</strong></li>



<li>An error response may be accidentally cached and reused</li>



<li>Slight variations of the same query may create <strong>duplicate entries</strong></li>



<li>Old but similar answers may appear correct while being subtly wrong</li>
</ul>



<p>None of these issues breaks the system outright. Instead, they quietly degrade correctness and user trust over time.</p>



<p>These are the hardest bugs to detect because the system continues to respond quickly and confidently.</p>



<h3 class="wp-block-heading">Why “It Works” Does Not Mean “It’s Safe”</h3>



<p>A semantic cache sits directly in the decision path of an LLM system. When it makes a mistake, that mistake is amplified through reuse.</p>



<p>If an unsafe response enters the cache:</p>



<ul class="wp-block-list">
<li>It can be served repeatedly</li>



<li>It can outlive the conditions that made it valid</li>



<li>It can be returned with high confidence</li>
</ul>



<p>This is why semantic caching requires <strong>more discipline</strong>, not less, than direct LLM calls.</p>



<p>In this lesson, we will take the working system from Lesson 1 and begin hardening it. We will introduce explicit safeguards for staleness, confidence, duplication, and safety — without changing the core architecture.</p>



<p>The goal is not to make the system perfect, but to make its failures <strong>controlled, visible, and predictable</strong>.</p>



<p>That is the difference between a demo and a system you can trust.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Cache-TTL-Semantic-Caching-Preventing-Stale-LLM-Responses"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Cache-TTL-Semantic-Caching-Preventing-Stale-LLM-Responses">Cache TTL in Semantic Caching: Preventing Stale LLM Responses</a></h2>



<p>Once a semantic cache is deployed and begins reusing LLM responses, a new question immediately arises:</p>



<p><em>How long should a cached response be trusted?</em></p>



<p>Unlike traditional caches that store deterministic outputs, semantic caches store model-generated answers. These answers are only valid within a certain window of time and context. Without explicit controls, a semantic cache can continue serving responses that are technically valid but practically wrong.</p>



<p>This section explains <strong>why cached LLM responses become stale</strong>, <strong>how TTLs help</strong>, and <strong>what it means for a cache entry to be unsafe</strong>.</p>



<h3 class="wp-block-heading">Why Cached LLM Responses Become Stale</h3>



<p>LLM responses are not timeless.</p>



<p>They are influenced by:</p>



<ul class="wp-block-list">
<li>evolving APIs and libraries</li>



<li>changing business logic or documentation</li>



<li>updated prompts or system behavior</li>



<li>newly introduced edge cases</li>
</ul>



<p>A cached answer that was correct an hour ago may no longer reflect the current state of the world.</p>



<p>Semantic caching amplifies this risk because:</p>



<ul class="wp-block-list">
<li>responses are reused aggressively</li>



<li>high similarity can mask outdated content</li>



<li>cached answers are returned with confidence</li>
</ul>



<p>Without staleness controls, the cache slowly becomes a <strong>museum of old truths</strong>.</p>



<h3 class="wp-block-heading">TTL as a Safety Mechanism</h3>



<p>A <strong>time-to-live (TTL)</strong> specifies how long a cache entry remains valid.</p>



<p>Once the TTL expires:</p>



<ul class="wp-block-list">
<li>the entry is treated as unsafe</li>



<li>it should no longer be reused</li>



<li>a fresh LLM response must be generated</li>
</ul>



<p>TTL does not guarantee correctness, but it <strong>limits the blast radius of staleness</strong>.</p>



<p>In semantic caching, TTL is not an optimization. It is a <strong>correctness safeguard</strong>.</p>



<h3 class="wp-block-heading">Application-Level TTL vs Redis: EXPIRE</h3>



<p>There are 2 common ways to implement TTLs when using Redis:</p>



<h4 class="wp-block-heading">Redis EXPIRE</h4>



<ul class="wp-block-list">
<li>Redis automatically deletes keys after a fixed duration</li>



<li>Expired entries are removed entirely</li>



<li>The application has no visibility into expired data</li>
</ul>



<h4 class="wp-block-heading">Application-Level TTL (Used Here)</h4>



<ul class="wp-block-list">
<li>Entries remain stored in Redis</li>



<li>Expiration is checked at read time by the application</li>



<li>The application decides whether an entry is safe to reuse</li>
</ul>



<p>In this system, TTL is enforced at the application layer rather than using Redis TTL via the native EXPIRE command, a deliberate choice that prioritizes observability over automation.</p>



<p>This choice allows us to:</p>



<ul class="wp-block-list">
<li>inspect expired entries during debugging</li>



<li>apply custom expiration logic</li>



<li>combine TTL with other safety signals (such as confidence)</li>
</ul>



<p>We trade automatic deletion for <strong>control and observability</strong>.</p>



<h3 class="wp-block-heading">When a Cache Entry Becomes Unsafe</h3>



<p>In this system, a cache entry is considered unsafe when <strong>any</strong> of the following are true:</p>



<ul class="wp-block-list">
<li>its TTL has expired</li>



<li>its content is malformed or erroneous</li>



<li>its confidence score falls below an acceptable threshold</li>
</ul>



<p>TTL is the first and most basic of these checks.</p>



<p>If an entry fails the TTL check, semantic similarity is irrelevant.</p>



<p>Reusing it would prioritize speed over correctness.</p>



<h3 class="wp-block-heading">Designing TTLs for LLM Workloads</h3>



<p>There is no universal “correct” TTL for LLM responses.</p>



<p>Instead, TTLs should be chosen based on:</p>



<ul class="wp-block-list">
<li>how fast the underlying information changes</li>



<li>how costly incorrect answers are</li>



<li>how frequently similar queries appear</li>
</ul>



<p>Short TTLs:</p>



<ul class="wp-block-list">
<li>reduce staleness risk</li>



<li>increase LLM calls</li>
</ul>



<p>Long TTLs:</p>



<ul class="wp-block-list">
<li>improve cache hit rate</li>



<li>increase risk of outdated responses</li>
</ul>



<p>In Lesson 1, we used a conservative default TTL to keep behavior predictable. In this lesson, we will focus on <strong>how TTLs are enforced</strong> rather than on tuning them for a specific domain.</p>



<p>TTL design is a policy decision. TTL enforcement is a correctness requirement.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-MLOps-Project-Structure-Semantic-Caching-FastAPI-Redis"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-MLOps-Project-Structure-Semantic-Caching-FastAPI-Redis">MLOps Project Structure for Semantic Caching with FastAPI and Redis</a></h2>



<p>Before diving into individual components, let’s take a moment to understand how the project is organized.</p>



<p>A clear directory structure is especially important in LLM-backed systems, where responsibilities span API orchestration, caching, embeddings, model calls, and observability. In this project, each concern is isolated into its own module so the request flow remains easy to trace and reason about.</p>



<p>After downloading the source code from the “Downloads” section, your directory structure should look like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="1">.
├── app
│   ├── api
│   │   ├── __init__.py
│   │   └── ask.py
│   ├── cache
│   │   ├── __init__.py
│   │   ├── poisoning.py
│   │   ├── schemas.py
│   │   ├── semantic_cache.py
│   │   └── ttl.py
│   ├── config
│   │   ├── __init__.py
│   │   └── settings.py
│   ├── embeddings
│   │   ├── __init__.py
│   │   └── embedder.py
│   ├── llm
│   │   ├── __init__.py
│   │   └── ollama_client.py
│   ├── main.py
│   └── observability
│       └── metrics.py
├── complete-codebase.txt
├── docker-compose.yml
├── Dockerfile
├── README.md
└── requirements.txt
</pre>



<p>Let’s break this down at a high level.</p>



<h3 class="wp-block-heading">The app/ Package</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">app/</code> directory contains all runtime application code. Nothing outside this folder is imported at runtime.</p>



<p>This keeps the service self-contained and makes it easy to reason about deployment and dependencies.</p>



<h3 class="wp-block-heading">app/main.py: Application Entry Point</h3>



<p>This file defines the FastAPI application and registers all routers.</p>



<p>It contains <strong>no business logic</strong> — only service wiring. Every request to the system enters through this file.</p>



<h3 class="wp-block-heading">app/api/: API Layer</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">api/</code> package defines HTTP-facing endpoints.</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">ask.py</code>: Implements the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint and acts as the orchestration layer for the entire semantic caching pipeline.</li>
</ul>



<p>The API layer is responsible for:</p>



<ul class="wp-block-list">
<li>validating input</li>



<li>enforcing cache ordering</li>



<li>coordinating cache, embeddings, and LLM calls</li>



<li>returning structured debug information</li>
</ul>



<p>It does not implement caching or similarity logic directly.</p>



<h3 class="wp-block-heading">app/cache/: Caching Logic</h3>



<p>This package contains all cache-related functionality.</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">semantic_cache.py</code>: Core semantic cache implementation (exact match, semantic match, Redis storage, similarity search).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">schemas.py</code>: Defines the cache entry schema used for Redis storage.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ttl.py</code>: Application-level TTL configuration and expiration checks.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">poisoning.py</code>: Safety checks to prevent invalid or error responses from being reused.</li>
</ul>



<p>By isolating caching logic here, the API layer stays clean and reusable.</p>



<h3 class="wp-block-heading">app/embeddings/: Embedding Generation</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">embedder.py</code>: Handles embedding generation via Ollama’s embedding endpoint.</li>
</ul>



<p>This module has a single responsibility: converting text into semantic vectors.</p>



<p>It does not cache, rank, or validate embeddings.</p>



<h3 class="wp-block-heading">app/llm/: LLM Client</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">ollama_client.py</code>: Wraps calls to the Ollama text-generation endpoint.</li>
</ul>



<p>Isolating LLM interaction allows the rest of the system to remain model-agnostic.</p>



<h3 class="wp-block-heading">app/observability/: Metrics</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">metrics.py</code>: Implements simple in-memory counters for cache hits, misses, and LLM calls.</li>
</ul>



<p>These metrics are intentionally lightweight and meant for learning and debugging, not production monitoring.</p>



<h3 class="wp-block-heading">Configuration and Infrastructure</h3>



<p>Outside the <code data-enlighter-language="python" class="EnlighterJSRAW">app/</code> directory:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">config/settings.py</code>: Centralizes environment-based configuration (Redis host, TTLs, model names).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Dockerfile</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code>: Define a reproducible runtime environment for the API and Redis.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code>: Lists all Python dependencies required to run the service.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Implement-Cache-TTL-Validation-Python-Redis"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Implement-Cache-TTL-Validation-Python-Redis">How to Implement Cache TTL Validation in Python and Redis</a></h2>



<p>In the previous section, we discussed <em>why</em> cached LLM responses become stale and <em>why</em> TTLs are necessary. In this section, we move from concept to code and look at <strong>how TTL validation is enforced in practice</strong>.</p>



<p>The key idea is simple but important:</p>



<p><strong>Cache entries are not deleted automatically. They are validated at read time.</strong></p>



<p>This design choice keeps cache behavior explicit, observable, and safe.</p>



<h3 class="wp-block-heading">The Default TTL Configuration</h3>



<p>TTL configuration is centralized in a single helper function:</p>



<p><strong>File:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">app/cache/ttl.py</code></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="2">def default_ttl():
    return settings.CACHE_TTL_SECONDS
</pre>



<p>Rather than hardcoding a value, the TTL is loaded from configuration. This allows different environments to use different TTLs without changing the code.</p>



<p>At this stage, the specific TTL value is not important. What matters is that:</p>



<ul class="wp-block-list">
<li>every cache entry receives a TTL at creation time</li>



<li>TTL is treated as metadata, not as a Redis feature</li>
</ul>



<h3 class="wp-block-heading">Checking Whether an Entry Has Expired</h3>



<p>TTL enforcement happens through a dedicated validation function:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="3">def is_expired(entry):
    try:
        created_at = int(entry["created_at"])
        ttl = int(entry["ttl"])
        now = int(time.time())
        return now > (created_at + ttl)
    except (KeyError, ValueError, TypeError):
        return True
</pre>



<p>This function answers 1 question:</p>



<p><strong>Is this cache entry still safe to reuse?</strong></p>



<p>If the current time exceeds <code data-enlighter-language="python" class="EnlighterJSRAW">created_at + ttl</code>, the entry is considered expired and must not be reused.</p>



<h3 class="wp-block-heading">Fail-Safe Expiration Behavior</h3>



<p>Notice the exception handling at the end of <code data-enlighter-language="python" class="EnlighterJSRAW">is_expired()</code>.</p>



<p>If the entry:</p>



<ul class="wp-block-list">
<li>is missing required fields</li>



<li>contains malformed values</li>



<li>cannot be parsed safely</li>
</ul>



<p>…it is treated as <strong>expired by default</strong>.</p>



<p>This is a deliberate fail-safe design.</p>



<p>When dealing with cached LLM responses, <strong>silently trusting malformed data is more dangerous than recomputing a response</strong>. If the system is unsure, it expires the entry and falls back to the LLM.</p>



<p>Correctness always wins over reuse.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-2-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="439" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-1024x439.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53631" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.png?size=126x54&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-300x129.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.png?size=378x162&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.png?size=504x216&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2.png?size=630x270&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-768x329.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-1024x439.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-2-1536x659.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Application-level TTL validation for semantic cache entries. Cached responses are reused only within their TTL window and are rejected at read time once expired (source: image by the author).</figcaption></figure></div>


<h3 class="wp-block-heading">Best-Effort Cleanup During Cache Reads</h3>



<p>TTL validation does more than reject expired entries — it also performs <strong>opportunistic cleanup</strong> during cache searches.</p>



<p>Inside the semantic cache search logic:</p>



<ul class="wp-block-list">
<li>expired entries are detected</li>



<li>expired keys are removed from Redis</li>



<li>the cache continues scanning remaining entries</li>
</ul>



<p>This cleanup happens:</p>



<ul class="wp-block-list">
<li>without background workers</li>



<li>without scheduled jobs</li>



<li>without blocking the request</li>
</ul>



<p>This is not a full garbage collector. It is a <strong>best-effort hygiene mechanism</strong> that keeps the cache from accumulating junk over time.</p>



<h3 class="wp-block-heading">Why We Validate on Read, Not Delete on Write</h3>



<p>At this point, a natural question arises:</p>



<p><em>Why not just use Redis EXPIRE and let Redis delete entries automatically?</em></p>



<p>There are 3 reasons this system validates TTLs <strong>on read</strong> instead:</p>



<ul class="wp-block-list">
<li><strong>Visibility: </strong>Expired entries remain inspectable during debugging.</li>



<li><strong>Control: </strong>The application decides what “expired” means, not Redis.</li>



<li><strong>Composability: </strong>TTL checks can be combined with confidence scoring, poisoning detection, and other safety signals.</li>
</ul>



<p>By validating at read time, TTL becomes part of the decision-making pipeline rather than an invisible background mechanism.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Confidence-Scoring-Semantic-Caching-Beyond-Similarity-LLMs"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Confidence-Scoring-Semantic-Caching-Beyond-Similarity-LLMs">Confidence Scoring in Semantic Caching: Beyond Similarity for LLMs</a></h2>



<p>Up to this point, semantic caching decisions have relied heavily on <strong>semantic similarity</strong>. If a cached response is similar enough to a new query, it feels reasonable to reuse it.</p>



<p>In practice, this assumption breaks down.</p>



<p>High similarity answers an important question — <em>“Is this response about the same thing?” </em>— but it does <strong>not</strong> answer an equally important one:</p>



<p><em>“Is this response still safe to reuse right now?”</em></p>



<p>Confidence scoring exists to bridge that gap.</p>



<h3 class="wp-block-heading">Why High Similarity Can Still Be Wrong</h3>



<p>Semantic similarity measures closeness in meaning, not correctness over time.</p>



<p>Consider a cached response that:</p>



<ul class="wp-block-list">
<li>has very high embedding similarity to the current query</li>



<li>was generated hours or days ago</li>



<li>refers to information that has since changed</li>
</ul>



<p>From a vector perspective, the response still appears “correct.”</p>



<p>From a system perspective, it may no longer be trustworthy.</p>



<p>This problem is subtle because:</p>



<ul class="wp-block-list">
<li>similarity scores remain high</li>



<li>responses look fluent and confident</li>



<li>failures are silent rather than catastrophic</li>
</ul>



<p>Without an additional signal, the cache has no way to distinguish <em>relevant but stale</em> from <em>relevant and safe</em>.</p>



<h3 class="wp-block-heading">Combining Semantic Similarity with Freshness</h3>



<p>Confidence scoring introduces a second dimension: <strong>freshness</strong>.</p>



<p>Rather than deciding reuse based on similarity alone, the cache evaluates a combined signal that reflects:</p>



<ul class="wp-block-list">
<li>how semantically close the response is</li>



<li>how recently the response was generated</li>
</ul>



<p>At a high level, confidence answers the question:</p>



<p><em>“How comfortable are we reusing this response right now?”</em></p>



<p>Fresh responses with high similarity score high confidence.</p>



<p>Old responses, even with high similarity, gradually lose confidence as they age.</p>



<p>This ensures that time acts as a natural decay mechanism.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/05/image-3-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="553" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-1024x553.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53633" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.png?size=126x68&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-300x162.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.png?size=378x204&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.png?size=504x272&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3.png?size=630x340&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-768x415.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-1024x553.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/05/image-3-1536x830.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Confidence scoring combines semantic similarity with freshness. Even highly similar cached responses lose confidence over time and are eventually rejected (source: image by the author).</figcaption></figure></div>


<h3 class="wp-block-heading">Understanding the Confidence Score (High-Level)</h3>



<p>In this system, confidence is a <strong>weighted combination</strong> of:</p>



<ul class="wp-block-list">
<li>semantic similarity</li>



<li>freshness relative to TTL</li>
</ul>



<p>You do not need to think about exact formulas at this stage. What matters is the behavior:</p>



<ul class="wp-block-list">
<li>Confidence starts high when an entry is created</li>



<li>Confidence decreases as the entry ages</li>



<li>Confidence is capped by semantic similarity</li>



<li>Expired entries always fail confidence checks</li>
</ul>



<p>Confidence is not a probability. It is a <strong>reuse heuristic</strong> designed to favor correctness over speed.</p>



<h3 class="wp-block-heading">How Confidence Affects Cache Reuse Decisions</h3>



<p>Confidence scoring acts as a <strong>gatekeeper</strong> in the cache pipeline.</p>



<p>Even if:</p>



<ul class="wp-block-list">
<li>the entry is not expired</li>



<li>the semantic similarity is above threshold</li>
</ul>



<p>…the cache will <strong>reject reuse</strong> if confidence falls below an acceptable level.</p>



<p>When this happens:</p>



<ul class="wp-block-list">
<li>the cache treats the entry as unsafe</li>



<li>the request falls back to the LLM</li>



<li>a fresh response is generated and stored</li>
</ul>



<p>This behavior ensures that the cache degrades gracefully.</p>



<p>As uncertainty increases, the system automatically shifts work back to the LLM rather than returning questionable results.</p>



<h3 class="wp-block-heading">Why Confidence Belongs in the Cache (Not the LLM)</h3>



<p>It’s tempting to push this logic downstream and let the LLM “fix” stale responses.</p>



<p>That approach fails for two reasons:</p>



<ul class="wp-block-list">
<li>the LLM has no context about cache age</li>



<li>the LLM cannot distinguish reused content from fresh inference</li>
</ul>



<p>Confidence must be enforced <strong>before reuse</strong>, not after generation.</p>



<p>By embedding confidence checks directly into the cache, we ensure that reuse decisions are explicit, explainable, and controllable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Implementing-Confidence-Scoring-LLM-Cache-Optimization-Code-Walkthrough"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Implementing-Confidence-Scoring-LLM-Cache-Optimization-Code-Walkthrough">Implementing Confidence Scoring for LLM Cache Optimization (Code Walkthrough)</a></h2>



<p>In the previous section, we introduced confidence scoring as a conceptual safeguard: a way to prevent semantically similar but stale responses from being reused.</p>



<p>In this section, we make that idea concrete by implementing it.</p>



<p>We will walk through <strong>where confidence is computed</strong>, <strong>where it is enforced</strong>, and <strong>what happens when a cached entry is rejected</strong>.</p>



<h3 class="wp-block-heading">Where Confidence Is Computed</h3>



<p>Confidence is computed inside the semantic cache, alongside similarity scoring.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="4">def compute_confidence(similarity: float, created_at: int, ttl: int) -> float:
    age = time.time() - created_at

    if ttl &lt;= 0:
        freshness = 1.0
    else:
        freshness = max(0.0, 1.0 - (age / ttl))

    confidence = (0.7 * similarity) + (0.3 * freshness)
    return round(confidence, 3)
</pre>



<p>This function combines 2 signals:</p>



<ul class="wp-block-list">
<li><strong>Semantic similarity:</strong> how close the meanings are</li>



<li><strong>Freshness:</strong> how recent the response is relative to its TTL</li>
</ul>



<p>The exact weights are not important here. What matters is the behavior:</p>



<ul class="wp-block-list">
<li>Fresh, similar responses score high confidence</li>



<li>Old responses lose confidence over time</li>



<li>Expired entries collapse to low confidence</li>
</ul>



<p>Confidence is therefore <strong>bounded</strong>, <strong>decaying</strong>, and <strong>explicitl</strong><strong>y defined</strong>.</p>



<h3 class="wp-block-heading">Why Confidence Is Computed in the Cache</h3>



<p>Notice that confidence is computed <strong>inside the cache layer</strong>, not in the API.</p>



<p>This ensures:</p>



<ul class="wp-block-list">
<li>all reuse decisions are centralized</li>



<li>confidence logic is applied consistently</li>



<li>the API remains an orchestration layer, not a policy engine</li>
</ul>



<p>The API does not need to understand <em>how</em> confidence is computed — only <em>whether</em> it is acceptable.</p>



<h3 class="wp-block-heading">Where Confidence Is Enforced</h3>



<p>Confidence enforcement happens in the request pipeline in <code data-enlighter-language="python" class="EnlighterJSRAW">ask.py</code>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="5">elif cached.get("confidence", 0.0) &lt; 0.7:
    miss_reason = "low_confidence"
</pre>



<p>This check occurs <strong>after</strong>:</p>



<ul class="wp-block-list">
<li>exact or semantic matching</li>



<li>TTL validation</li>



<li>poisoning checks</li>
</ul>



<p>And <strong>before</strong> a cached response is returned.</p>



<p>If confidence is below the threshold:</p>



<ul class="wp-block-list">
<li>the cache entry is rejected</li>



<li>the request is treated as a cache miss</li>



<li>the pipeline falls back to the LLM</li>
</ul>



<p>This ensures that reuse happens only when confidence meets an acceptable threshold.</p>



<h3 class="wp-block-heading">Why Rejection Is Safer Than Reuse</h3>



<p>When confidence is low, the system has 2 choices:</p>



<ul class="wp-block-list">
<li>reuse a response it does not fully trust</li>



<li>generate a fresh response</li>
</ul>



<p>This implementation always chooses the second option.</p>



<p>The cost of an extra LLM call is predictable.</p>



<p>The cost of serving an incorrect response is not.</p>



<p>By rejecting low-confidence entries, the cache degrades <strong>gracefully</strong> instead of failing silently.</p>



<h3 class="wp-block-heading">What Happens After Rejection</h3>



<p>Once a cached entry is rejected:</p>



<ul class="wp-block-list">
<li>the request proceeds to the LLM</li>



<li>a new response is generated</li>



<li>the new response is stored with a fresh timestamp and TTL</li>
</ul>



<p>Over time, this naturally refreshes the cache without requiring explicit invalidation logic.</p>



<h3 class="wp-block-heading">Making Rejections Observable</h3>



<p>Confidence-based rejections are not hidden.</p>



<p>They are surfaced via:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">miss_reason = "low_confidence"</code></li>



<li>debug metadata returned to the client</li>



<li>cache miss metrics</li>
</ul>



<p>This makes it possible to understand <em>why</em> the cache did not reuse a response — a critical property when tuning thresholds later.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Query-Normalization-Deduplication-Efficient-Semantic-Caching"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Query-Normalization-Deduplication-Efficient-Semantic-Caching">Query Normalization and Deduplication for Efficient Semantic Caching</a></h2>



<p>At this point, our semantic cache is safe against stale and low-confidence responses. However, there is another failure mode that appears once the system runs for longer periods of time:</p>



<p><strong>The cache slowly fills with duplicate entries</strong> <strong>represent</strong><strong>ing</strong><strong> the same query.</strong></p>



<p>This problem does not break correctness, but it can silently degrade cache quality and efficiency.</p>



<h3 class="wp-block-heading">Why Duplicate Cache Entries Are a Problem</h3>



<p>In natural language systems, users rarely type queries the same way twice.</p>



<p>Consider the following inputs:</p>



<ul class="wp-block-list">
<li>What is semantic caching?</li>



<li>What is semantic caching</li>



<li>What   is   semantic   caching?</li>
</ul>



<p>From a human perspective, these queries are identical.</p>



<p>From a naïve cache’s perspective, they are completely different strings.</p>



<p>If we store each variation separately:</p>



<ul class="wp-block-list">
<li>cache size grows unnecessarily</li>



<li>similarity scans become slower</li>



<li>cache hit rate decreases</li>



<li>identical LLM work is repeated</li>
</ul>



<p>This is not a semantic problem — it is a <strong>normalization problem</strong>.</p>



<h3 class="wp-block-heading">Normalizing Queries Before Caching</h3>



<p>To prevent this, the cache normalizes queries before storing them.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="6">def _hash_query(query: str) -> str:
    normalized = " ".join(query.lower().split())
    return hashlib.sha256(normalized.encode()).hexdigest()
</pre>



<p>This function performs 3 important steps:</p>



<ul class="wp-block-list">
<li><strong>Lowercasing: </strong>Ensures case-insensitive matching</li>



<li><strong>Whitespace normalization: </strong>Collapses extra spaces and removes leading/trailing whitespace</li>



<li><strong>Hashing: </strong>Produces a fixed-length identifier for fast comparison</li>
</ul>



<p>The result is a stable representation of the query’s <em>structure</em>, not its formatting.</p>



<h3 class="wp-block-heading">Deduplication at Store Time</h3>



<p>Deduplication happens when a new cache entry is about to be written.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="7">query_hash = self._hash_query(query)

for key in self.r.smembers(f"{self.namespace}:keys"):
    data = self.r.hgetall(key)
    if data and data.get("query_hash") == query_hash:
        return
</pre>



<p>Before storing a new entry, the cache checks whether an entry with the same normalized hash already exists in the cache.</p>



<p>If it does:</p>



<ul class="wp-block-list">
<li>the new entry is <strong>not stored</strong></li>



<li>the cache avoids creating a duplicate</li>



<li>storage space and future scans are preserved</li>
</ul>



<p>This approach ensures that <strong>identical queries map to a single cache entry</strong>, regardless of how they were formatted.</p>



<h3 class="wp-block-heading">Why Deduplication Happens in the Cache Layer</h3>



<p>Deduplication is enforced inside the cache rather than in the API layer.</p>



<p>This design ensures:</p>



<ul class="wp-block-list">
<li>all cache writes are normalized consistently</li>



<li>deduplication logic lives next to storage logic</li>



<li>API code remains simple and declarative</li>
</ul>



<p>The API does not need to care <em>how</em> deduplication works — only that the cache remains clean.</p>



<h3 class="wp-block-heading">Why Hash-Based Deduplication Works Well Here</h3>



<p>Using a hash instead of raw strings provides several advantages:</p>



<ul class="wp-block-list">
<li>fixed-length comparisons</li>



<li>efficient storage</li>



<li>no dependency on query length</li>



<li>practical collision resistance</li>
</ul>



<p>For this system, SHA-256 is more than sufficient. The goal is stability and simplicity, not cryptographic security.</p>



<h3 class="wp-block-heading">What Deduplication Does Not Solve</h3>



<p>It’s important to understand the limits of this approach.</p>



<p>Hash-based deduplication:</p>



<ul class="wp-block-list">
<li>prevents exact duplicates after normalization</li>



<li>does <strong>not</strong> merge semantically similar queries</li>



<li>does <strong>not</strong> replace semantic caching</li>
</ul>



<p>In other words:</p>



<ul class="wp-block-list">
<li>deduplication keeps the cache clean</li>



<li>semantic similarity keeps the cache useful</li>
</ul>



<p>They solve different problems and complement each other.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Preventing-Cache-Poisoning-Semantic-Caching-LLM-Systems"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Preventing-Cache-Poisoning-Semantic-Caching-LLM-Systems">Preventing Cache Poisoning in Semantic Caching for LLM Systems</a></h2>



<p>So far, we’ve protected the semantic cache against <em>staleness</em>, <em>low confidence</em>, and <em>duplicate entries</em>. There is one more failure mode that can silently undermine the entire system if left unchecked:</p>



<p><strong>Cache poisoning — storing responses that should never be reused.</strong></p>



<p>Cache poisoning does not usually crash the system. Instead, it causes the cache to confidently serve <strong>bad answers repeatedly</strong>, amplifying a single failure into many incorrect responses.</p>



<h3 class="wp-block-heading">What Cache Poisoning Looks Like in LLM Systems</h3>



<p>In the context of LLM-backed systems, cache poisoning typically happens when:</p>



<ul class="wp-block-list">
<li>the LLM returns an error message</li>



<li>the response is empty or incomplete</li>



<li>the output is malformed due to a timeout or partial generation</li>
</ul>



<p>If these responses are cached, every future “hit” returns the same failure instantly — fast, but incorrect.</p>



<p>This is especially dangerous because:</p>



<ul class="wp-block-list">
<li>the cache appears to be working</li>



<li>responses are returned quickly</li>



<li>the system looks healthy from the outside</li>
</ul>



<h3 class="wp-block-heading">Poisoning Prevention Strategy</h3>



<p>Rather than trying to detect every possible bad response, this system uses a <strong>simple, conservative heuristic</strong>:</p>



<p><em>If a response looks unsafe, do not cache it.</em></p>



<p>This keeps the logic easy to reason about and avoids false positives.</p>



<h3 class="wp-block-heading">Detecting Poisoned Entries</h3>



<p>Poisoning detection is implemented in a dedicated helper function.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="8">def is_poisoned(entry):
    resp = entry.get("response", "")
    if not resp or resp.startswith("[LLM Error]"):
        return True
    return False
</pre>



<p>This function flags an entry as poisoned if:</p>



<ul class="wp-block-list">
<li>the response is empty, or</li>



<li>the response is an explicit LLM error</li>
</ul>



<p>These conditions are intentionally strict. When in doubt, the entry is treated as unsafe.</p>



<h3 class="wp-block-heading">Where Poisoning Is Enforced</h3>



<p>Poisoning checks are applied <strong>before</strong> any cached response is reused in <code data-enlighter-language="python" class="EnlighterJSRAW">ask.py</code>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="9">elif is_poisoned(cached):
    miss_reason = "poisoned"
</pre>



<p>If a cached entry is poisoned:</p>



<ul class="wp-block-list">
<li>it is rejected immediately</li>



<li>the request is treated as a cache miss</li>



<li>the pipeline falls back to the LLM</li>
</ul>



<p>This ensures that invalid responses are never reused, even if they have high similarity or appear fresh.</p>



<h3 class="wp-block-heading">Why Poisoned Entries Are Rejected, Not Repaired</h3>



<p>The cache does not attempt to “fix” poisoned entries.</p>



<p>Trying to repair cached LLM output introduces:</p>



<ul class="wp-block-list">
<li>ambiguity</li>



<li>hidden transformations</li>



<li>unpredictable behavior</li>
</ul>



<p>Instead, the system takes the safest possible action:</p>



<ul class="wp-block-list">
<li>reject the entry</li>



<li>generate a fresh response</li>



<li>overwrite with a clean result</li>
</ul>



<p>This keeps the cache behavior explicit and predictable.</p>



<h3 class="wp-block-heading">Making Poisoning Visible</h3>



<p>Just like low-confidence rejections, poisoning is not silent.</p>



<p>The reason is surfaced via:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">miss_reason = "poisoned"</code></li>



<li>debug metadata returned to the client</li>



<li>cache miss metrics</li>
</ul>



<p>This makes it possible to distinguish between:</p>



<ul class="wp-block-list">
<li>semantic misses</li>



<li>safety rejections</li>



<li>forced fallbacks</li>
</ul>



<p>Visibility is a critical part of safety.</p>



<h3 class="wp-block-heading">What This Approach Does Not Cover</h3>



<p>This poisoning strategy is intentionally simple.</p>



<p>It does not attempt to:</p>



<ul class="wp-block-list">
<li>analyze response quality</li>



<li>validate structured output</li>



<li>detect hallucinations</li>



<li>score semantic correctness</li>
</ul>



<p>Those checks are domain-specific and belong outside the cache.</p>



<p>The cache’s responsibility is narrow:</p>



<p><strong>Do not reuse responses that are obviously unsafe.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-End-to-End-Semantic-Cache-Hardening-TTL-Confidence-Safety-Demos"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-End-to-End-Semantic-Cache-Hardening-TTL-Confidence-Safety-Demos">End-to-End Semantic Cache Hardening: TTL, Confidence, and Safety Demos</a></h2>



<p>In Lesson 1, we verified that semantic caching works.</p>



<p>In this lesson, we harden that system by watching each <strong>safety mechanism activate in practice</strong>.</p>



<p>The goal of these demos is not performance testing.</p>



<p>The goal is <strong>behavioral verification</strong>.</p>



<p>Each demo isolates one hardening feature and makes its effect visible through the response payload.</p>



<h3 class="wp-block-heading">Demo Case 1: TTL Expiration Forces a Cache Miss</h3>



<p>Start by sending a query and populating the cache:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="10">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Explain semantic caching for LLMs"}'
</pre>



<p>This first request falls back to the LLM and stores a new cache entry.</p>



<p>After waiting <strong>longer than the configured TTL</strong>, send the same request again:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="11">sleep 61
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Explain semantic caching for LLMs"}'
</pre>



<p><strong>Expected Behavior</strong></p>



<ul class="wp-block-list">
<li>Exact-match lookup finds an entry</li>



<li>TTL validation fails</li>



<li>Entry is rejected</li>



<li>LLM is called again</li>
</ul>



<p><strong>Example response</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="12">{
  "from_cache": false,
  "debug": {
    "hit": false,
    "miss_reason": "no_match"
  }
}
</pre>



<p>This confirms that stale responses are not reused.</p>



<h3 class="wp-block-heading">Demo Case 2: Semantic Reuse When Confidence Remains High</h3>



<p>Now consider a cached response that is still within TTL and retains sufficient confidence.</p>



<p>Send a semantically similar query:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="13">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "How does semantic caching reduce LLM calls?"}'
</pre>



<p><strong>Expected Behavior</strong></p>



<ul class="wp-block-list">
<li>Semantic similarity match found</li>



<li>Confidence computed</li>



<li>Confidence above threshold</li>



<li>Cached response reused</li>
</ul>



<p><strong>Example response</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="14">{
  "from_cache": true,
  "debug": {
    "hit": true,
    "cache_path": "semantic_match",
    "confidence": 0.81
  }
}
</pre>



<p>This demonstrates that semantic reuse is allowed when both relevance and freshness remain acceptable.</p>



<h3 class="wp-block-heading">Demo Case 3: Failed LLM Responses Are Never Cached</h3>



<p>A safe semantic cache must ensure that failed LLM responses are never reused. This demo demonstrates <em>write-time</em> cache poisoning prevention.</p>



<p>This system enforces that rule at <strong>write time</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="15">if not response.startswith("[LLM Error]"):
    cache.store(...)
</pre>



<p>Only valid responses are ever written to Redis.</p>



<h4 class="wp-block-heading">How We Demonstrate This</h4>



<p>We <strong>do not</strong> shut down Ollama or the embedding service.</p>



<p>Network failures abort the request before caching logic runs and are not suitable demos.</p>



<p>Instead, we simulate an LLM failure.</p>



<h4 class="wp-block-heading">Step 1: Temporarily Simulate an LLM Error</h4>



<p>In <code data-enlighter-language="python" class="EnlighterJSRAW">generate_llm_response()</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="16">if "simulate_error" in prompt.lower():
    return "[LLM Error] Simulated failure"
</pre>



<h4 class="wp-block-heading">Step 2: Send a Query</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="17">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Simulate error in semantic caching"}'
</pre>



<p><strong>Expected Behavior</strong></p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">from_cache = false</code></li>



<li>Cache miss</li>



<li>Error response returned</li>
</ul>



<h4 class="wp-block-heading">Step 3: Send the Same Query Again</h4>



<p><strong>Expected </strong><strong>Result</strong></p>



<ul class="wp-block-list">
<li>Cache miss again</li>



<li>LLM called again</li>



<li>No cached response reused</li>
</ul>



<h4 class="wp-block-heading">Why the Miss Reason Is no_match</h4>



<ul class="wp-block-list">
<li>Failed responses are <strong>never stored</strong></li>



<li>No cache entry exists to reject or evaluate</li>



<li>Cache poisoning checks apply only to existing entries</li>
</ul>



<p>This is intentional and correct.</p>



<h3 class="wp-block-heading">Demo Case 4: Deduplication Under Query Variations</h3>



<p>Send a query with unusual spacing:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="18">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "   What   is   semantic   caching?   "}'
</pre>



<p>Then send the normalized version:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="19">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "What is semantic caching?"}'
</pre>



<p><strong>Expected Behavior</strong></p>



<ul class="wp-block-list">
<li>Both queries map to the same normalized hash</li>



<li>Only one cache entry exists</li>



<li>Exact-match reuse occurs</li>
</ul>



<p><strong>Example response</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="20">{
  "from_cache": true,
  "debug": {
    "hit": true,
    "cache_path": "exact_match"
  }
}
</pre>



<p>This confirms deduplication is working correctly.</p>



<h3 class="wp-block-heading">Demo Case 5: Observing Metrics After Hardening</h3>



<p>After running several demos, inspect the metrics endpoint:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="21">curl http://localhost:8000/internal/metrics
</pre>



<p><strong>Example response</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="22">{
  "hits": 3,
  "misses": 4,
  "llm_calls": 4,
  "_note": "In-memory metrics. Reset on restart. Not production-ready."
}
</pre>



<p>Metrics help you verify that:</p>



<ul class="wp-block-list">
<li>safety rejections increase misses</li>



<li>LLM calls rise when reuse is unsafe</li>



<li>the system degrades gracefully</li>
</ul>



<h3 class="wp-block-heading">What These Demos Prove</h3>



<p>Across these scenarios, we verified that:</p>



<ul class="wp-block-list">
<li>Stale entries are rejected</li>



<li>Low-confidence reuse is prevented</li>



<li>Poisoned responses are never cached</li>



<li>Duplicate entries are avoided</li>



<li>Cache behavior is observable and explainable</li>
</ul>



<p>The cache no longer optimizes for speed alone.</p>



<p>It optimizes for <strong>safe reuse</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Semantic-Caching-Limitations-Trade-Offs-LLM-Optimization-Systems"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Semantic-Caching-Limitations-Trade-Offs-LLM-Optimization-Systems">Semantic Caching Limitations: Trade-Offs in LLM Optimization Systems</a></h2>



<p>By this point, we’ve built a semantic cache that is not only functional, but also hardened against common failure modes: staleness, low confidence, poisoning, duplication, and silent reuse.</p>



<p>However, no system design is complete without clearly stating <strong>what it does not attempt to solve</strong>.</p>



<p>This section makes those boundaries explicit.</p>



<h3 class="wp-block-heading">Why This Cache Still Uses O(N) Scans</h3>



<p>All semantic lookups in this implementation perform a <strong>linear scan</strong> over cached entries.</p>



<p>That means:</p>



<ul class="wp-block-list">
<li>every semantic search compares the query embedding against all stored embeddings</li>



<li>time complexity grows linearly with cache size</li>
</ul>



<p>This is not an oversight.</p>



<p>It is a <strong>deliberate design choice</strong> made for:</p>



<ul class="wp-block-list">
<li>teaching clarity</li>



<li>transparency</li>



<li>small-to-medium cache sizes</li>
</ul>



<p>By avoiding ANN indexes or vector databases, every decision remains visible and debuggable. You can trace exactly why a match was selected or rejected.</p>



<p>For educational systems and low-volume services, this trade-off is acceptable — and often desirable.</p>



<h3 class="wp-block-heading">What We Intentionally Did Not Implement</h3>



<p>To keep the system focused and understandable, several production features were intentionally left out:</p>



<ul class="wp-block-list">
<li>Approximate nearest neighbor (ANN) indexing</li>



<li>Redis Vector Search or RediSearch</li>



<li>Background garbage collection workers</li>



<li>Distributed locks for thundering herd prevention</li>



<li>Request coalescing or single-flight patterns</li>



<li>Multi-process or persistent metrics</li>



<li>Cache warming strategies</li>
</ul>



<p>Each of these adds complexity that would obscure the core ideas being taught.</p>



<p>This cache is designed to <strong>explain semantic caching</strong>, not to compete with specialized retrieval infrastructure.</p>



<h3 class="wp-block-heading">When This Design Is “Good Enough”</h3>



<p>This architecture works well when:</p>



<ul class="wp-block-list">
<li>cache size is modest (hundreds to low thousands of entries)</li>



<li>traffic is low to moderate</li>



<li>correctness and explainability matter more than raw throughput</li>



<li>you are experimenting with semantic reuse behavior</li>



<li>you want to understand cache dynamics before scaling</li>
</ul>



<p>Typical examples include:</p>



<ul class="wp-block-list">
<li>internal tools</li>



<li>developer-facing APIs</li>



<li>research prototypes</li>



<li>educational systems</li>



<li>early-stage LLM applications</li>
</ul>



<p>In these contexts, the simplicity of the design is a strength, not a weakness.</p>



<h3 class="wp-block-heading">When You Need a Vector Database or ANN Index</h3>



<p>As usage grows, linear scans eventually become the bottleneck.</p>



<p>You should consider a dedicated vector search solution when:</p>



<ul class="wp-block-list">
<li>cache size grows into tens or hundreds of thousands of entries</li>



<li>latency requirements become strict</li>



<li>multiple workers or services share the same cache</li>



<li>semantic search dominates request time</li>
</ul>



<p>At that point, technologies such as the following:</p>



<ul class="wp-block-list">
<li>FAISS (Facebook AI Similarity Search)</li>



<li>Milvus</li>



<li>Pinecone</li>



<li>Redis Vector Search</li>
</ul>



<p>become appropriate.</p>



<p>Importantly, the <strong>hardening concepts from this lesson still apply</strong>. TTLs, confidence scoring, poisoning prevention, and observability remain relevant even when the storage backend changes.</p>



<h3 class="wp-block-heading">The Core Trade-Off, Revisited</h3>



<p>This lesson deliberately favors:</p>



<ul class="wp-block-list">
<li>clarity over cleverness</li>



<li>explicit decisions over hidden automation</li>



<li>safety over aggressive reuse</li>
</ul>



<p>That makes it an ideal foundation, not a final destination.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, we took a working semantic cache and made it safe, bounded, and explainable.</p>



<p>Rather than focusing on improving cache hit rates at all costs, we introduced guardrails to ensure cached LLM responses are reused only when they are trustworthy. </p>



<p>We added application-level TTL validation to prevent stale responses from persisting indefinitely, combined semantic similarity with freshness through confidence scoring, and enforced explicit rejection paths for low-confidence and expired entries.</p>



<p>We also addressed subtle but dangerous failure modes that appear in real systems over time. Query normalization and deduplication prevent silent cache bloat, and poisoning checks ensure that error responses are never reused. </p>



<p>Observability signals make every cache decision inspectable rather than implicit. Together, these changes transform the cache from a performance optimization into a reliability component.</p>



<p>Finally, we made the system’s limitations explicit. This design favors clarity, correctness, and debuggability over raw scalability. It deliberately avoids ANN indexes, vector databases, and distributed coordination, making it suitable for small-to-medium systems and educational use cases.</p>



<p>As workloads grow, the same hardening principles apply even when the underlying storage or retrieval strategy changes.</p>



<p>With this lesson, semantic caching is no longer just fast. It is defensive, explainable, and production-aware.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/ahr3p" target="_blank" rel="noreferrer noopener">https://pyimg.co/ahr3p</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety" data-enlighter-group="23">@incollection{Singh_2026_semantic-caching-llms-ttls-confidence-cache-safety,
  author = {Vikram Singh},
  title = {{Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/ahr3p},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/05/04/semantic-caching-for-llms-ttls-confidence-and-cache-safety/">Semantic Caching for LLMs: TTLs, Confidence, and Cache Safety</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</title>
		<link>https://pyimagesearch.com/2026/04/27/semantic-caching-for-llms-fastapi-redis-and-embeddings/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[LLMOps]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[caching]]></category>
		<category><![CDATA[cosine similarity]]></category>
		<category><![CDATA[embeddings]]></category>
		<category><![CDATA[fastapi]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[llm optimization]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[semantic caching]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[vector search]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53546</guid>

					<description><![CDATA[<p>Table of Contents Semantic Caching for LLMs: FastAPI, Redis, and Embeddings Introduction: Why Semantic Caching Matters for LLM Systems How Semantic Caching Works for LLMs: Embeddings and Similarity Search Explained Semantic Caching Architecture and Request Flow Configuring Your Environment for&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/27/semantic-caching-for-llms-fastapi-redis-and-embeddings/">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Semantic-Caching-LLMs-FastAPI-Redis-Embeddings"><a rel="noopener" target="_blank" href="#h1-Semantic-Caching-LLMs-FastAPI-Redis-Embeddings">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a></li>

    <li id="TOC-h2-Introduction-Why-Semantic-Caching-Matters-LLM-Systems"><a rel="noopener" target="_blank" href="#h2-Introduction-Why-Semantic-Caching-Matters-LLM-Systems">Introduction: Why Semantic Caching Matters for LLM Systems</a></li>

    <li id="TOC-h2-How-Semantic-Caching-Works-LLMs-Embeddings-Similarity-Search-Explained"><a rel="noopener" target="_blank" href="#h2-How-Semantic-Caching-Works-LLMs-Embeddings-Similarity-Search-Explained">How Semantic Caching Works for LLMs: Embeddings and Similarity Search Explained</a></li>

    <li id="TOC-h2-Semantic-Caching-Architecture-Request-Flow"><a rel="noopener" target="_blank" href="#h2-Semantic-Caching-Architecture-Request-Flow">Semantic Caching Architecture and Request Flow</a></li>

    <li id="TOC-h2-Configuring-Your-Environment-Semantic-Caching-FastAPI-Redis-Ollama-Setup"><a rel="noopener" target="_blank" href="#h2-Configuring-Your-Environment-Semantic-Caching-FastAPI-Redis-Ollama-Setup">Configuring Your Environment for Semantic Caching: FastAPI, Redis, and Ollama Setup</a></li>

    <li id="TOC-h2-Project-Structure"><a rel="noopener" target="_blank" href="#h2-Project-Structure">Project Structure</a></li>

    <li id="TOC-h2-FastAPI-Entry-Point-Semantic-Caching-Wiring-API-Service"><a rel="noopener" target="_blank" href="#h2-FastAPI-Entry-Point-Semantic-Caching-Wiring-API-Service">FastAPI Entry Point for Semantic Caching: Wiring the API Service</a></li>

    <li id="TOC-h2-FastAPI-Ask-Endpoint-End-to-End-Semantic-Caching-Request-Flow"><a rel="noopener" target="_blank" href="#h2-FastAPI-Ask-Endpoint-End-to-End-Semantic-Caching-Request-Flow">FastAPI Ask Endpoint: End-to-End Semantic Caching Request Flow</a></li>

    <li id="TOC-h2-Embeddings-Turning-Text-into-Semantic-Vectors"><a rel="noopener" target="_blank" href="#h2-Embeddings-Turning-Text-into-Semantic-Vectors">Embeddings: Turning Text into Semantic Vectors</a></li>

    <li id="TOC-h2-Semantic-Cache-Cosine-Similarity-Redis-Storage-Reusing-Meaning"><a rel="noopener" target="_blank" href="#h2-Semantic-Cache-Cosine-Similarity-Redis-Storage-Reusing-Meaning">The Semantic Cache: Cosine Similarity, Redis Storage, and Reusing Meaning</a></li>

    <li id="TOC-h2-Cache-Entries-What-Exactly-Gets-Stored"><a rel="noopener" target="_blank" href="#h2-Cache-Entries-What-Exactly-Gets-Stored">Cache Entries: What Exactly Gets Stored?</a></li>

    <li id="TOC-h2-End-to-End-Demo-Verifying-Core-Cache-Behavior"><a rel="noopener" target="_blank" href="#h2-End-to-End-Demo-Verifying-Core-Cache-Behavior">End-to-End Demo: Verifying Core Cache Behavior</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Semantic-Caching-LLMs-FastAPI-Redis-Embeddings"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Semantic-Caching-LLMs-FastAPI-Redis-Embeddings">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a></h2>



<p>In this lesson, you will learn how to build a semantic cache for LLM applications using FastAPI, Redis, and embedding-based similarity search, and how requests flow from exact matches to semantic matches before falling back to the LLM.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?lossy=2&strip=1&webp=1" alt="semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png" class="wp-image-53571" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-for-llms-fastapi-redis-and-embeddings-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>This lesson is the 1st in a 2-part series on <strong>Semantic Caching for LLMs</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/yso6f" target="_blank" rel="noreferrer noopener">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to build a semantic cache for LLM applications using embeddings and Redis, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction-Why-Semantic-Caching-Matters-LLM-Systems"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction-Why-Semantic-Caching-Matters-LLM-Systems">Introduction: Why Semantic Caching Matters for LLM Systems</a></h2>



<h3 class="wp-block-heading">Cost, Latency, and Redundant LLM Calls</h3>



<p>Large language models are powerful, but they are not cheap. Every request to an LLM involves tokenization, inference, decoding, and network overhead. Even when models are hosted locally, response times are measured in hundreds of milliseconds or seconds rather than microseconds.</p>



<p>In real applications, this cost compounds quickly. Users often ask similar questions repeatedly, either across sessions or within the same workflow. Each request is treated as a fresh LLM invocation, even when the underlying intent has already been handled before.</p>



<p>This leads to 3 systemic problems:</p>



<ul class="wp-block-list">
<li><strong>High latency:</strong> Users wait for responses that could have been reused instantly</li>



<li><strong>Increased cost:</strong> Identical reasoning is paid for multiple times</li>



<li><strong>Wasted capacity:</strong> LLM throughput is consumed by redundant requests</li>
</ul>



<p>These issues become especially visible under load, where repeated paraphrased queries can overwhelm an otherwise well-sized system.</p>



<h3 class="wp-block-heading">Why Exact-Match Caching Breaks Down for Natural Language</h3>



<p>Traditional caching assumes that identical inputs produce identical outputs. This works well for APIs, database queries, and deterministic functions. It fails for natural language.</p>



<p>From a string-matching perspective, the following queries are completely unrelated:</p>



<ul class="wp-block-list">
<li>“What is semantic caching?”</li>



<li>“Can you explain how semantic caching works?”</li>



<li>“How does caching based on embeddings work for LLMs?”</li>
</ul>



<p>A traditional cache keyed on raw strings will miss all three. As a result, the system calls the LLM three times, even though a human would expect the same answer.</p>



<p>This brittleness causes exact-match caches to have extremely low hit rates in LLM-backed systems. Worse, it gives a false sense of optimization. The cache exists, but it almost never helps in practice.</p>



<h3 class="wp-block-heading">Where Semantic Caching Fits in Real Systems</h3>



<p>Semantic caching addresses this mismatch by caching <em>meaning</em> instead of exact text.</p>



<p>Rather than asking “have I seen this string before?”, a semantic cache asks “have I answered something <strong>semantically similar</strong> before?”. It does this by converting queries into embeddings and comparing them using a similarity metric such as cosine similarity.</p>



<p>In a real system, semantic caching sits between the application layer and the LLM:</p>



<ul class="wp-block-list">
<li>The application sends a query</li>



<li>The cache evaluates whether a prior response is reusable</li>



<li>Only true cache misses reach the LLM</li>
</ul>



<p>When designed correctly, this layer is invisible to the user. Responses feel faster, costs drop, and the system scales more gracefully without changing the frontend or prompt logic.</p>



<p>This lesson focuses on building that layer explicitly and transparently, using FastAPI, Redis, and embeddings, without hiding the mechanics behind heavy abstractions.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/semantic-caching-fig1.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="512" height="224" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig1.png?lossy=2&strip=1&webp=1" alt="Figure 1: Why semantic caching matters for LLM systems. Exact-match caching treats paraphrased queries as unique requests, resulting in repeated LLM calls. Semantic caching groups queries by meaning, reducing latency and redundant inference." class="wp-image-53552" style="object-fit:cover" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig1.png?size=126x55&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig1-300x131.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig1.png?size=378x165&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig1.png?lossy=2&amp;strip=1&amp;webp=1 512w" sizes="(max-width: 512px) 100vw, 512px" /></a><figcaption class="wp-element-caption"><strong>Figure 1: </strong>Why semantic caching matters for LLM systems. Exact-match caching treats paraphrased queries as unique requests, resulting in repeated LLM calls. Semantic caching groups queries by meaning, reducing latency and redundant inference (source: image by the author).</figcaption></figure></div>


<p>Exact-match caching treats paraphrased queries as unique requests, resulting in repeated LLM calls. Semantic caching groups similar queries by meaning, allowing responses to be reused and reducing both latency and cost.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Semantic-Caching-Works-LLMs-Embeddings-Similarity-Search-Explained"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Semantic-Caching-Works-LLMs-Embeddings-Similarity-Search-Explained">How Semantic Caching Works for LLMs: Embeddings and Similarity Search Explained</a></h2>



<p><a href="#h2-Introduction-Why-Semantic-Caching-Matters-LLM-Systems" target="_blank" rel="noreferrer noopener">Section 1</a> explained <em>why</em> semantic caching exists.</p>



<p>This section explains <strong>how it works</strong>, conceptually, before we touch any FastAPI, Redis, or code.</p>



<p>The goal here is to give the reader a <strong>mental execution model</strong> they can keep in their head while reading the implementation.</p>



<h3 class="wp-block-heading">From Text to Meaning: Embeddings as the Cache Key</h3>



<p>Semantic caching replaces raw text comparison with <strong>vector similarity</strong>.</p>



<p>Instead of caching responses under the literal query string, the system converts each query into an <strong>embedding</strong>: a high-dimensional numeric vector that captures semantic meaning. Queries that are worded differently but mean the same thing produce embeddings that are close together in vector space.</p>



<p>This is what allows the cache to recognize paraphrases as equivalent:</p>



<ul class="wp-block-list">
<li>“How do I reset my password?”</li>



<li>“I forgot my password, what should I do?”</li>



<li>“Guide me through password recovery”</li>
</ul>



<p>Exact strings differ. Embeddings do not.</p>



<p>At a high level, semantic caching works by:</p>



<ul class="wp-block-list">
<li>Generating an embedding for the incoming query</li>



<li>Comparing it against embeddings stored in the cache</li>



<li>Reusing a cached response if similarity is high enough</li>
</ul>



<p>The similarity metric used in this lesson is <strong>cosine similarity</strong>, which measures the angle between two vectors rather than their raw magnitude.</p>



<h3 class="wp-block-heading">Why a Layered Cache Beats Semantic-Only Caching</h3>



<p>While semantic matching is powerful, it is also <strong>computationally expensive</strong>.</p>



<p>Embedding generation requires a model call. Similarity search requires vector math. Doing this for every request, even when the exact same query has already been seen, would be wasteful.</p>



<p>That is why this lesson uses a <strong>layered caching strategy</strong>.</p>



<h4 class="wp-block-heading">Layer 1: Exact Match (Fast Path)</h4>



<p>The query is normalized and hashed.</p>



<p>If the same query has already been answered, the response is returned immediately.</p>



<ul class="wp-block-list">
<li>No embedding generation</li>



<li>No similarity computation</li>



<li>Minimal latency</li>
</ul>



<p>This handles repeated identical queries efficiently.</p>



<h4 class="wp-block-heading">Layer 2: Semantic Match (Flexible Path)</h4>



<p>If no exact match exists, the query is embedded and compared against cached embeddings.</p>



<p>This layer catches:</p>



<ul class="wp-block-list">
<li>paraphrases</li>



<li>minor wording differences</li>



<li>reordered phrases</li>
</ul>



<p>Semantic matches trade compute cost for much higher cache hit rates.</p>



<h4 class="wp-block-heading">Layer 3: LLM Fallback (Slow Path)</h4>



<p>If neither exact nor semantic matches succeed, the request is forwarded to the LLM.</p>



<p>The response is then stored in the cache so future requests can reuse it.</p>



<p>This layered approach ensures:</p>



<ul class="wp-block-list">
<li>the cheapest checks happen first</li>



<li>expensive operations are only used when necessary</li>
</ul>



<h3 class="wp-block-heading">Confidence, Freshness, and Cache Safety</h3>



<p>Semantic similarity alone is not enough to decide whether a cached response should be reused.</p>



<p>This lesson introduces the idea of <strong>confidence scoring</strong>, which combines:</p>



<ul class="wp-block-list">
<li><strong>Similarity:</strong> how close the embeddings are</li>



<li><strong>Freshness:</strong> how old the cached entry is</li>
</ul>



<p>A highly similar but stale response should not necessarily be trusted. Likewise, a fresh response with low similarity should be rejected.</p>



<p>In addition, cached entries are validated to prevent:</p>



<ul class="wp-block-list">
<li>expired responses</li>



<li>poisoned entries (errors, empty outputs)</li>
</ul>



<p>These checks ensure the cache improves correctness and performance rather than degrading them.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-22-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="554" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-1024x554.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53576" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22.png?size=126x68&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-300x162.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22.png?size=378x205&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22.png?size=504x273&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22.png?size=630x341&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-768x415.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-1024x554.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-22-1536x830.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 2: </strong>Layered semantic caching request flow (source: image by the author).</figcaption></figure></div>


<p>Incoming queries first attempt an exact-match lookup, then fall back to semantic similarity search using embeddings, and finally call the LLM only on cache miss. This ordering minimizes latency and unnecessary model calls.</p>



<p><em><strong>Note:</strong></em><em> In this lesson, we implement this flow using Redis as a simple embedding store with linear similarity scans, rather than a dedicated vector database.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Semantic-Caching-Architecture-Request-Flow"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Semantic-Caching-Architecture-Request-Flow">Semantic Caching Architecture and Request Flow</a></h2>



<p>In <a href="#h2-How-Semantic-Caching-Works-LLMs-Embeddings-Similarity-Search-Explained" target="_blank" rel="noreferrer noopener">Section 2</a>, you learned how semantic caching works conceptually.</p>



<p>In this section, we map that mental model to a <strong>real request flow</strong> in an LLM-backed service.</p>



<p>The goal is to answer one question clearly:</p>



<p><em>What happens, step by step, when a user sends a request to this system?</em></p>



<p>We will stay implementation-aware, but not code-specific yet. That comes next.</p>



<h3 class="wp-block-heading">High-Level System Components</h3>



<p>At a high level, the system consists of 5 logical components:</p>



<ul class="wp-block-list">
<li><strong>API layer: </strong>Receives user requests and orchestrates the caching pipeline.</li>



<li><strong>Exact-match cache: </strong>Performs fast hash-based lookups for identical queries.</li>



<li><strong>Embedding model: </strong>Converts text queries into semantic vectors when needed.</li>



<li><strong>Semantic cache: </strong>Stores embeddings and responses and performs similarity matching.</li>



<li><strong>LLM: </strong>Acts as the final fallback when no cache entry is suitable.</li>
</ul>



<p>Each component has a narrowly defined responsibility. This separation is intentional and keeps the system easy to reason about and extend.</p>



<p>In this implementation:</p>



<ul class="wp-block-list">
<li>The API layer is built using FastAPI and acts as the orchestration point.</li>



<li>Redis is used as the backing store for both exact-match and semantic cache layers.</li>



<li>Ollama provides both embedding generation and LLM inference locally.</li>
</ul>



<p>These choices keep the system lightweight, self-contained, and easy to reason about while still reflecting real production patterns.</p>



<h3 class="wp-block-heading">End-to-End Request Flow</h3>



<p>When a user sends a query, the system processes it in the following order.</p>



<h4 class="wp-block-heading">Step 1: Request enters the API</h4>



<p>The API receives a text query along with optional flags, such as whether to use the <code data-enlighter-language="python" class="EnlighterJSRAW">bypass_cache</code>. Input validation happens immediately to prevent meaningless or malformed queries from entering the pipeline.</p>



<p>This ensures the cache is not polluted with empty or invalid entries.</p>



<h4 class="wp-block-heading">Step 2: Exact-match cache lookup</h4>



<p>The query is normalized and hashed.</p>



<p>The system checks whether an identical query has already been answered.</p>



<ul class="wp-block-list">
<li>If an exact match exists and is valid, the response is returned immediately.</li>



<li>No embeddings are generated.</li>



<li>The LLM is not touched.</li>
</ul>



<p>This is the fastest possible path through the system.</p>



<h4 class="wp-block-heading">Step 3: Embedding generation</h4>



<p>If the exact-match lookup fails, the query is passed to the embedding model.</p>



<p>The model converts the text into a numeric vector that captures semantic meaning. This vector becomes the key for semantic comparison.</p>



<p>This step is intentionally skipped when an exact match succeeds.</p>



<h4 class="wp-block-heading">Step 4: Semantic cache lookup</h4>



<p>The embedding is compared against cached embeddings using a similarity metric.</p>



<p>A cached response is reused only if:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">similarity</code> exceeds a defined threshold</li>



<li>the entry has not expired</li>



<li>the entry is not poisoned</li>



<li>the computed <code data-enlighter-language="python" class="EnlighterJSRAW">confidence</code> is high enough</li>
</ul>



<p>If a suitable match is found, the response is returned to the user without calling the LLM.</p>



<h4 class="wp-block-heading">Step 5: LLM fallback and cache population</h4>



<p>If both cache layers miss, the request is forwarded to the LLM.</p>



<p>Once a response is generated:</p>



<ul class="wp-block-list">
<li>it is returned to the user</li>



<li>it is stored in the cache with metadata, timestamps, and TTL (Time To Live)</li>
</ul>



<p>This ensures future requests can reuse the result.</p>



<h3 class="wp-block-heading">Why This Architecture Works Well</h3>



<p>This architecture is intentionally conservative and explicit.</p>



<ul class="wp-block-list">
<li>Cheap operations happen first.</li>



<li>Expensive operations are deferred.</li>



<li>Every step is observable and debuggable.</li>



<li>No component hides complexity behind opaque abstractions.</li>
</ul>



<p>Most importantly, the system degrades gracefully. Even when the cache provides no benefit, the request still succeeds via the LLM.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/semantic-caching-fig3.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="512" height="248" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig3.png?lossy=2&strip=1&webp=1" alt="Figure 3: Architecture and request flow for a layered semantic caching system." class="wp-image-53556" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig3.png?size=126x61&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig3-300x145.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig3.png?size=378x183&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/semantic-caching-fig3.png?lossy=2&amp;strip=1&amp;webp=1 512w" sizes="(max-width: 512px) 100vw, 512px" /></a><figcaption class="wp-element-caption"><strong>Figure 3: </strong>Architecture and request flow for a layered semantic caching system (source: image by the author).</figcaption></figure></div>


<p>User queries enter the API, attempt an exact-match lookup, fall back to semantic similarity search using embeddings, and call the LLM only when both cache layers miss. Successful LLM responses are stored for future reuse.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Your-Environment-Semantic-Caching-FastAPI-Redis-Ollama-Setup"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Your-Environment-Semantic-Caching-FastAPI-Redis-Ollama-Setup">Configuring Your Environment for Semantic Caching: FastAPI, Redis, and Ollama Setup</a></h2>



<p>To follow this guide, you need a small set of Python libraries and system services that support API orchestration, vector similarity, and LLM interaction. The goal is to keep the environment lightweight, reproducible, and easy to reason about.</p>



<p>At a minimum, you will need:</p>



<ul class="wp-block-list">
<li>Python 3.10 or newer</li>



<li>Redis (used as the cache backing store)</li>



<li>An LLM + embedding provider (Ollama in this tutorial)</li>
</ul>



<p>All required Python dependencies are <code data-enlighter-language="python" class="EnlighterJSRAW">pip</code>-installable.</p>



<h3 class="wp-block-heading">Installing Python Dependencies</h3>



<p>Create and activate a virtual environment (recommended), then install the required packages:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="1">$ pip install fastapi uvicorn redis httpx python-dotenv numpy
</pre>



<p>These libraries provide the following functionality:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">fastapi</code>: API layer and request orchestration</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">uvicorn</code>: ASGI server for running the service</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">redis</code>: client Communication with the cache store</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">httpx</code>: HTTP client for embedding and LLM calls</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">numpy</code>: Vector math for cosine similarity</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">python-dotenv</code>: Environment-based configuration</li>
</ul>



<h3 class="wp-block-heading">Verifying Redis</h3>



<p>This lesson assumes Redis is running locally on the default port.</p>



<p>You can verify Redis is available with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="2">$ redis-cli ping
PONG
</pre>



<p>If Redis is not installed, you can start it quickly using Docker (but you also can spin it up using the <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code> we provide in the code zip):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="3">$ docker run -p 6379:6379 redis:7
</pre>



<h3 class="wp-block-heading">Setting Up Ollama</h3>



<p>This system uses <strong>Ollama</strong> for both embedding generation and LLM inference. Make sure Ollama is installed and running, and that the required models are available.</p>



<p>For example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="4">$ ollama pull nomic-embed-text
$ ollama pull llama3.2
</pre>



<p>Once running, Ollama exposes local HTTP endpoints that the application will call directly for embeddings and text generation.</p>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure">Project Structure</a></h2>



<p>Before diving into individual components, let’s take a moment to understand how the project is organized.</p>



<p>A clear directory structure is especially important in LLM-backed systems, where responsibilities span API orchestration, caching, embeddings, model calls, and observability. In this project, each concern is isolated into its own module so the request flow remains easy to trace and reason about.</p>



<p>After downloading the source code from the “Downloads” section, your directory structure should look like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="5">.
├── app
│   ├── api
│   │   ├── __init__.py
│   │   └── ask.py
│   ├── cache
│   │   ├── __init__.py
│   │   ├── poisoning.py
│   │   ├── schemas.py
│   │   ├── semantic_cache.py
│   │   └── ttl.py
│   ├── config
│   │   ├── __init__.py
│   │   └── settings.py
│   ├── embeddings
│   │   ├── __init__.py
│   │   └── embedder.py
│   ├── llm
│   │   ├── __init__.py
│   │   └── ollama_client.py
│   ├── main.py
│   └── observability
│       └── metrics.py
├── complete-codebase.txt
├── docker-compose.yml
├── Dockerfile
├── README.md
└── requirements.txt
</pre>



<p>Let’s break this down at a high level.</p>



<h3 class="wp-block-heading">The app/ Package</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">app/</code> directory contains all runtime application code. Nothing outside this folder is imported at execution time.</p>



<p>This keeps the service self-contained and makes it easy to reason about deployment and dependencies.</p>



<h3 class="wp-block-heading">app/main.py: Application Entry Point</h3>



<p>This file defines the FastAPI application and registers all routers.</p>



<p>It contains <strong>no business logic</strong> — only service wiring. Every request into the system enters through this file.</p>



<h3 class="wp-block-heading">app/api/: API Layer</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">api/</code> package defines HTTP-facing endpoints.</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">ask.py</code>: Implements the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint and acts as the orchestration layer for the entire semantic caching pipeline.</li>
</ul>



<p>The API layer is responsible for:</p>



<ul class="wp-block-list">
<li>input validation</li>



<li>enforcing cache ordering</li>



<li>coordinating cache, embeddings, and LLM calls</li>



<li>returning structured debug information</li>
</ul>



<p>It does <em>not</em> implement caching or similarity logic directly.</p>



<h3 class="wp-block-heading">app/cache/: Caching Logic</h3>



<p>This package contains all cache-related functionality.</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">semantic_cache.py</code>: Core semantic cache implementation (exact match, semantic match, Redis storage, similarity search).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">schemas.py</code>: Defines the cache entry schema used for Redis storage.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ttl.py</code>: Application-level TTL configuration and expiration checks.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">poisoning.py</code>: Safety checks to prevent invalid or error responses from being reused.</li>
</ul>



<p>By isolating caching logic here, the API layer stays clean and reusable.</p>



<h3 class="wp-block-heading">app/embeddings/: Embedding Generation</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">embedder.py</code>: Handles embedding generation via Ollama’s embedding endpoint.</li>
</ul>



<p>This module has a single responsibility: convert text into semantic vectors.</p>



<p>It does not cache, rank, or validate embeddings.</p>



<h3 class="wp-block-heading">app/llm/: LLM Client</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">ollama_client.py</code>: Wraps calls to the Ollama text-generation endpoint.</li>
</ul>



<p>Keeping LLM interaction isolated allows the rest of the system to remain model-agnostic.</p>



<h3 class="wp-block-heading">app/observability/: Metrics</h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">metrics.py</code>: Implements simple in-memory counters for cache hits, misses, and LLM calls.</li>
</ul>



<p>These metrics are intentionally lightweight and meant for learning and debugging, not production monitoring.</p>



<h3 class="wp-block-heading">Configuration and Infrastructure</h3>



<p>Outside the <code data-enlighter-language="python" class="EnlighterJSRAW">app/</code> directory:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">config/settings.py</code>: Centralizes environment-based configuration (Redis host, TTLs, model names).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Dockerfile</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">docker-compose.yml</code>: Define a reproducible runtime environment for the API and Redis.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code>: Lists all Python dependencies required to run the service.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-FastAPI-Entry-Point-Semantic-Caching-Wiring-API-Service"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-FastAPI-Entry-Point-Semantic-Caching-Wiring-API-Service">FastAPI Entry Point for Semantic Caching: Wiring the API Service</a></h2>



<p>Before we look at caching logic, embeddings, or Redis, it’s important to understand how the service itself is wired together. Every request to the semantic cache enters the system through a single FastAPI application, defined in <code data-enlighter-language="python" class="EnlighterJSRAW">app/main.py</code>.</p>



<p>This file acts as the <strong>entry point</strong> of the service. Its responsibility is not to implement business logic, but to connect the application components and expose HTTP routes.</p>



<h3 class="wp-block-heading">Application Entry Point (app/main.py)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="6">from fastapi import FastAPI
from api.ask import router as ask_router

app = FastAPI(title="Semantic Cache Basics")
app.include_router(ask_router)
</pre>



<p>Let’s break this down.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">FastAPI()</code> call creates the application object. This object represents the entire web service and is what the ASGI (Asynchronous Server Gateway Interface) server (<code data-enlighter-language="python" class="EnlighterJSRAW">uvicorn</code>) runs when the container starts.</p>



<p>The application itself contains no knowledge of caching, embeddings, or LLMs. It simply defines a runtime container that will host those capabilities.</p>



<h3 class="wp-block-heading">Router Registration</h3>



<p>Instead of defining endpoints directly in <code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code>, the application imports a router from <code data-enlighter-language="python" class="EnlighterJSRAW">api/ask.py</code> and registers it using <code data-enlighter-language="python" class="EnlighterJSRAW">include_router()</code>.</p>



<p>This pattern serves several purposes:</p>



<ul class="wp-block-list">
<li><strong>Separation of concerns: </strong>Routing and request handling live outside the application entry point.</li>



<li><strong>Scalability: </strong>As the system grows, additional routers (for health checks, metrics, or admin endpoints) can be added without modifying core application wiring.</li>



<li><strong>Readability: </strong><code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code> remains easy to understand at a glance, even as the codebase expands.</li>
</ul>



<p>At runtime, FastAPI merges the routes defined in <code data-enlighter-language="python" class="EnlighterJSRAW">ask_router</code> into the main application. When a request arrives at the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint, FastAPI resolves it through the registered router and forwards it to the appropriate handler function.</p>



<h3 class="wp-block-heading">Why This Matters</h3>



<p>Keeping the entry point minimal is intentional. It ensures that:</p>



<ul class="wp-block-list">
<li>The application startup process is predictable</li>



<li>Routing logic is easy to trace</li>



<li>Core functionality can evolve independently of service wiring</li>
</ul>



<p>With the application structure in place, we can now focus on what actually happens when a request reaches the system.</p>



<p>In the next section, we will walk through the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint and see how it orchestrates exact-match caching, semantic search, and LLM fallback step by step.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-FastAPI-Ask-Endpoint-End-to-End-Semantic-Caching-Request-Flow"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-FastAPI-Ask-Endpoint-End-to-End-Semantic-Caching-Request-Flow">FastAPI Ask Endpoint: End-to-End Semantic Caching Request Flow</a></h2>



<p>This section makes the architecture concrete. We now walk through the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint, which orchestrates the entire semantic caching pipeline from request arrival to response delivery.</p>



<p>The goal here is not to memorize code, but to understand <strong>why each step exists</strong>, <strong>where it lives</strong>, and <strong>how it protects performance, cost, and correctness</strong>.</p>



<h3 class="wp-block-heading">The Role of the Ask Endpoint</h3>



<p>The Ask endpoint is the <strong>control plane</strong> of the system.</p>



<p>It does <strong>not</strong>:</p>



<ul class="wp-block-list">
<li>Compute similarity</li>



<li>Store embeddings</li>



<li>Talk directly to Redis internals</li>
</ul>



<p>Instead, it:</p>



<ul class="wp-block-list">
<li>Validates input</li>



<li>Decides which cache layers to consult</li>



<li>Enforces ordering between cheap and expensive operations</li>



<li>Collects observability signals</li>



<li>Guarantees a response even on cache failure</li>
</ul>



<p>This separation is intentional. Cache logic remains reusable and testable, while orchestration logic stays explicit at the API boundary.</p>



<h3 class="wp-block-heading">Defining the API Contract</h3>



<p>We begin by defining the request and response models.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="7">class AskRequest(BaseModel):
    query: str
    bypass_cache: bool = False
</pre>



<p>The request consists of a user <code data-enlighter-language="python" class="EnlighterJSRAW">query</code> and an optional <code data-enlighter-language="python" class="EnlighterJSRAW">bypass_cache</code> flag. This flag allows us to force a cache miss during debugging or testing, ensuring that the LLM and embedding pipeline still function correctly.</p>



<p>Before the request ever reaches the cache, the <code data-enlighter-language="python" class="EnlighterJSRAW">query</code> field is validated.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="8">@field_validator('query')
@classmethod
def validate_query(cls, v: str) -> str:
    if not v or not v.strip():
        raise ValueError("Query cannot be empty or whitespace-only")
    return v.strip()
</pre>



<p>This validation step protects the system at the boundary. Rejecting empty or whitespace-only queries prevents:</p>



<ul class="wp-block-list">
<li>wasted embedding computation</li>



<li>cache pollution with meaningless entries</li>



<li>unnecessary LLM calls</li>
</ul>



<p>This is a recurring pattern in production systems: <strong>fail fast, before expensive operations are triggered</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="9">class AskResponse(BaseModel):
    response: str
    from_cache: bool
    similarity: float
    debug: dict
</pre>



<p>The response model intentionally exposes diagnostic information through fields such as <code data-enlighter-language="python" class="EnlighterJSRAW">from_cache</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">similarity</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">debug</code>. During development, this makes cache behavior transparent rather than opaque.</p>



<h3 class="wp-block-heading">Initializing the Cache</h3>



<p>Before handling requests, we create a <code data-enlighter-language="python" class="EnlighterJSRAW">SemanticCache</code> instance:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="10">cache = SemanticCache()
</pre>



<p>The endpoint itself remains stateless. All persistence and reuse live inside the cache layer.</p>



<h3 class="wp-block-heading">Step 1: Entering the Endpoint</h3>



<p>The endpoint is registered using FastAPI’s routing mechanism:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="11">@router.post("/ask", response_model=AskResponse)
def ask_endpoint(request: AskRequest):
</pre>



<p>FastAPI automatically validates incoming requests and outgoing responses using the schemas defined earlier. If invalid data enters or exits the system, FastAPI raises an error instead of silently failing.</p>



<p>Inside the handler, we extract the query and initialize tracking state:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="12">query = request.query
miss_reason = None
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">miss_reason</code> variable exists purely for observability. Rather than treating cache misses as a black box, we explicitly track <em>why</em> a miss occurred.</p>



<h3 class="wp-block-heading">Step 2: Exact-Match Cache Lookup (Fast Path)</h3>



<p>The first decision point is the exact-match cache lookup:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="13">if not request.bypass_cache:
    cached = cache.search(None, exact_query=query)
</pre>



<p>This is the <strong>cheapest path</strong> through the system.</p>



<p>If the same query has already been answered, the response can be returned immediately:</p>



<ul class="wp-block-list">
<li>no embeddings are generated</li>



<li>no similarity computation occurs</li>



<li>the LLM is not touched</li>
</ul>



<p>If a cached entry is found, it is validated:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="14">if is_expired(cached):
    miss_reason = "expired"
elif is_poisoned(cached):
    miss_reason = "poisoned"
elif cached.get("confidence", 0.0) &lt; 0.7:
    miss_reason = "low_confidence"
</pre>



<p>Only entries that are fresh, valid, and confident are allowed to short-circuit the pipeline.</p>



<p>When all checks pass, the endpoint returns immediately:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="15">metrics.cache_hit()
return AskResponse(...)
</pre>



<p>This path typically completes in milliseconds and handles repeated identical queries efficiently.</p>



<h3 class="wp-block-heading">Step 3: Embedding Generation (Escalation Point)</h3>



<p>If the exact-match lookup fails or is bypassed, the endpoint escalates:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="16">embedding = embed_text(query)
</pre>



<p>Embedding generation is expensive, even when running locally. For this reason, it is intentionally delayed until all cheaper options have been exhausted.</p>



<p>This single design choice has a significant impact on system efficiency.</p>



<h3 class="wp-block-heading">Step 4: Semantic Cache Lookup</h3>



<p>With the embedding available, the endpoint attempts a semantic search:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="17">cached = cache.search(embedding)
</pre>



<p>This path catches paraphrased and reworded queries. As before, cached entries are validated to ensure they are safe to reuse.</p>



<p>If a suitable match is found, the response is returned without calling the LLM.</p>



<h3 class="wp-block-heading">Step 5: Explicit Cache Bypass</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">bypass_cache</code> flag is handled explicitly:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="18">if request.bypass_cache:
    miss_reason = "bypass"
</pre>



<p>This allows controlled testing and debugging without modifying code or disabling cache logic globally.</p>



<h3 class="wp-block-heading">Step 6: LLM Fallback and Cache Population</h3>



<p>If both cache layers miss, the request is forwarded to the LLM:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="19">metrics.cache_miss()
response = generate_llm_response(query)
metrics.llm_call()
</pre>



<p>This is the slowest path through the system, but it guarantees correctness.</p>



<p>Successful responses are stored in the cache:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="20">if not response.startswith("[LLM Error]"):
    cache.store(query, embedding, response, metadata=metadata)
</pre>



<p>Responses beginning with <code data-enlighter-language="python" class="EnlighterJSRAW">[LLM Error]</code> are intentionally not cached, preventing cache poisoning and ensuring failures do not propagate to future requests.</p>



<h3 class="wp-block-heading">Control Flow Summary</h3>



<p>The endpoint follows a simple, explicit sequence:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-23-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="738" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23-1024x738.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53580" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23.png?size=126x91&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23-300x216.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23.png?size=378x272&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23.png?size=504x363&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23.png?size=630x454&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23-768x554.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23-1024x738.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-23-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> LLM API Control Flow with Layered Semantic Caching (source: image by the author).</figcaption></figure></div>


<p>Every expensive operation is deferred until absolutely necessary.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Embeddings-Turning-Text-into-Semantic-Vectors"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Embeddings-Turning-Text-into-Semantic-Vectors">Embeddings: Turning Text into Semantic Vectors</a></h2>



<p>Up to this point, we have treated embeddings as a black box: something expensive that we try to avoid unless absolutely necessary.</p>



<p>In this section, we will open that box just enough to understand <strong>what embeddings are</strong>, <strong>when they are generated</strong>, and <strong>why they enable semantic caching</strong> without diving into vector math or model internals.</p>



<h3 class="wp-block-heading">Why Embeddings Exist in This System</h3>



<p>Exact-match caching works only when queries are identical at the string level. As soon as wording changes, exact matching breaks down.</p>



<p>Embeddings solve this problem by converting text into a numeric representation that captures <strong>meaning rather than surface form</strong>.</p>



<p>Queries that mean the same thing tend to produce vectors that are close together in vector space, even if their wording differs significantly.</p>



<p>This is the foundation that makes semantic caching possible.</p>



<h3 class="wp-block-heading">Embedding Generation Happens on Demand</h3>



<p>In our implementation, embeddings are generated <strong>only after</strong> the exact-match cache fails.</p>



<p>This decision is intentional.</p>



<p>Embedding generation involves:</p>



<ul class="wp-block-list">
<li>a model invocation</li>



<li>network overhead</li>



<li>serialization and deserialization</li>



<li>non-trivial latency</li>
</ul>



<p>Because of this cost, embeddings are treated as an <strong>escalation step</strong>, not a default operation.</p>



<p>This is why the <code data-enlighter-language="python" class="EnlighterJSRAW">/ask</code> endpoint first attempts an exact-match lookup before calling <code data-enlighter-language="python" class="EnlighterJSRAW">embed_text()</code>.</p>



<h3 class="wp-block-heading">The embed_text Function</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="21">def embed_text(text: str):
</pre>



<p>This function has one responsibility: <strong>Convert input text into a semantic vector representation.</strong></p>



<p>It does not perform caching, similarity search, or validation. Those concerns live elsewhere.</p>



<h3 class="wp-block-heading">Calling the Embedding Model</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="22">url = f"http://{settings.OLLAMA_HOST}:{settings.OLLAMA_PORT}/api/embeddings"
</pre>



<p>Here, we construct the Ollama embedding endpoint using configuration values (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">settings.OLLAMA_HOST</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">settings.OLLAMA_PORT</code>, etc.).</p>



<p>This allows the embedding service to run locally, inside Docker, or on a remote host without changing code.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="23">resp = httpx.post(
    url,
    json={"model": settings.EMBEDDING_MODEL, "prompt": text},
    timeout=10.0
)
</pre>



<p>This request sends 2 key pieces of information to the embedding service:</p>



<ul class="wp-block-list">
<li>the <strong>embedding model name</strong> (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">nomic-embed-text</code>)</li>



<li>the <strong>input text</strong> to embed</li>
</ul>



<p>The timeout ensures the request does not hang indefinitely. Embedding generation is expensive, but it should still fail fast if something goes wrong.</p>



<h3 class="wp-block-heading">Handling the Response</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="24">resp.raise_for_status()
return resp.json().get("embedding", [])
</pre>



<p>If the request succeeds, the embedding model returns a numeric vector — typically a list of floating-point values.</p>



<p>This vector represents the <strong>semantic meaning</strong> of the input text and becomes the key used for similarity comparison in the cache.</p>



<p>At this stage, we treat the vector as an opaque object. We do not inspect its dimensionality or normalize it here.</p>



<h3 class="wp-block-heading">Error Handling Strategy</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="25">except Exception as e:
    raise RuntimeError(f"Failed to generate embedding: {e}")
</pre>



<p>If embedding generation fails for any reason (network issues, model errors, timeouts), the function raises an exception.</p>



<p>This is intentional.</p>



<p>If embeddings cannot be generated, the system cannot safely perform semantic matching. Silently continuing would lead to unpredictable behavior, so we fail loudly instead.</p>



<h3 class="wp-block-heading">Why the Embedder Is Intentionally Simple</h3>



<p>Notice what this function <strong>does not do</strong>:</p>



<ul class="wp-block-list">
<li>it does not store embeddings</li>



<li>it does not perform similarity search</li>



<li>it does not retry failed requests</li>



<li>it does not fall back to alternative models</li>
</ul>



<p>Those decisions are deliberate.</p>



<p>For Lesson 1, the embedder exists purely to convert text into vectors. Keeping it small and focused makes the system easier to understand and test.</p>



<h3 class="wp-block-heading">How the Embedder Is Used in the Pipeline</h3>



<p>At runtime, the embedder is called only when necessary:</p>



<ul class="wp-block-list">
<li>Exact-match cache fails</li>



<li>The query is passed to <code data-enlighter-language="python" class="EnlighterJSRAW">embed_text()</code></li>



<li>The returned vector is sent to the semantic cache</li>



<li>Similarity is computed against stored embeddings</li>
</ul>



<p>This ensures embeddings are generated <strong>only when cheaper paths have already failed</strong>.</p>



<h3 class="wp-block-heading">Key Takeaways</h3>



<ul class="wp-block-list">
<li>Embeddings are generated via a simple HTTP call to a local model</li>



<li>The embedder has a single responsibility</li>



<li>Errors are surfaced immediately</li>



<li>Embeddings act as semantic keys for cache lookup</li>
</ul>



<p>With embedding generation understood, we are now ready to look at the <strong>semantic cache itself</strong>, how embeddings and responses are stored, scanned, and matched.</p>



<p>In the next section, we will walk through the semantic cache implementation, starting with a deliberately naive but correct linear scan approach.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Semantic-Cache-Cosine-Similarity-Redis-Storage-Reusing-Meaning"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Semantic-Cache-Cosine-Similarity-Redis-Storage-Reusing-Meaning">The Semantic Cache: Cosine Similarity, Redis Storage, and Reusing Meaning</a></h2>



<p>At this point, we understand how queries enter the system and how text is converted into embeddings. What remains is the component that ties everything together: the semantic cache itself.</p>



<p>The semantic cache is responsible for 2 things:</p>



<ul class="wp-block-list">
<li><strong>Storing</strong> past queries, embeddings, and responses</li>



<li><strong>Retrieving</strong> the best reusable response for a new query</li>
</ul>



<p>In Lesson 1, we intentionally implement the cache in the simplest correct way possible: a <strong>linear scan over cached entries</strong>. This keeps the implementation easy to reason about and makes the request flow fully transparent.</p>



<h3 class="wp-block-heading">The Semantic Cache Module</h3>



<p>The cache logic lives in <code data-enlighter-language="python" class="EnlighterJSRAW">semantic_cache.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="26">class SemanticCache:
</pre>



<p>This class encapsulates all Redis interaction and similarity logic. The API layer never talks to Redis directly.</p>



<h3 class="wp-block-heading">Initializing the Cache</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="27">def __init__(self):
    self.r = redis.Redis(
        host=settings.REDIS_HOST,
        port=settings.REDIS_PORT,
        decode_responses=True
    )
    self.similarity_threshold = 0.85
    self.namespace = "semantic_cache:v1"
</pre>



<p>Here we establish a Redis connection and configure 2 important parameters:</p>



<ul class="wp-block-list">
<li><strong>Similarity threshold: </strong>Only responses with sufficiently high semantic similarity are eligible for reuse.</li>



<li><strong>Namespace prefix: </strong>All Redis keys are namespaced to avoid collisions and allow future versioning.</li>
</ul>



<p>For Lesson 1, the exact threshold value is not important. What matters is that a threshold exists and is applied consistently.</p>



<h3 class="wp-block-heading">Storing Cache Entries</h3>



<p>The first core operation is storing new entries.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="28">def store(self, query, embedding, response, metadata=None):
</pre>



<p>This method is called only after a successful LLM response.</p>



<h3 class="wp-block-heading">Creating a Cache Entry</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="29">entry = CacheEntry(
    id=entry_uuid,
    query=query,
    query_hash=query_hash,
    embedding=json.dumps(embedding),
    response=response,
    created_at=int(time.time()),
    ttl=default_ttl(),
    metadata=metadata or {}
)
</pre>



<p>Each cache entry stores:</p>



<ul class="wp-block-list">
<li>the original query</li>



<li>a normalized query hash (used for exact matching)</li>



<li>the embedding (serialized for Redis storage)</li>



<li>the LLM response</li>



<li>timestamps and TTL</li>



<li>optional metadata for observability</li>
</ul>



<p>This structure allows the cache to support both exact-match and semantic lookups.</p>



<h3 class="wp-block-heading">Writing to Redis</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="30">self.r.hset(redis_key, mapping=entry.dict())
self.r.sadd(f"{self.namespace}:keys", redis_key)
</pre>



<p>Each cache entry is stored as a Redis hash, and all entry keys are tracked in a Redis set.</p>



<p>This allows the cache to iterate over all entries during search operations.</p>



<p>For Lesson 1, this approach is intentionally simple and explicit.</p>



<h3 class="wp-block-heading">Searching the Cache</h3>



<p>The second core operation is lookup.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="31">def search(self, embedding, exact_query=None):
</pre>



<p>This method supports <strong>2 search modes</strong>, which map directly to the layered cache strategy used in the API.</p>



<h3 class="wp-block-heading">Exact-Match Lookup (Fast Path)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="32">if exact_query:
    query_hash = self._hash_query(exact_query)
</pre>



<p>When an exact query is provided, the cache first attempts a hash-based lookup.</p>



<p>Each cached entry is scanned until a matching hash is found. If found, the entry is returned immediately with a similarity score of 1.0.</p>



<p>No embeddings are involved in this path.</p>



<h3 class="wp-block-heading">Semantic Lookup (Flexible Path)</h3>



<p>If no exact match is found and an embedding is provided, the cache performs a semantic search:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="33">sim = self.cosine_similarity(query_embedding, cached_embedding)
</pre>



<p>Each cached embedding is compared against the query embedding using cosine similarity.</p>



<p>Only entries that exceed the configured similarity threshold are considered candidates.</p>



<h3 class="wp-block-heading">Selecting the Best Match</h3>



<p>During the scan, the cache tracks the highest similarity score and returns the best matching entry.</p>



<p>This ensures that even when multiple entries are similar, the most relevant response is reused.</p>



<h3 class="wp-block-heading">Why This Implementation Is O(N)</h3>



<p>Every search scans all cached entries.</p>



<p>This is not an accident.</p>



<p>For Lesson 1, a linear scan has 3 advantages:</p>



<ul class="wp-block-list">
<li>the behavior is easy to understand</li>



<li>the logic is fully visible</li>



<li>debugging is straightforward</li>
</ul>



<p>More advanced indexing strategies belong in later lessons.</p>



<h3 class="wp-block-heading">Why Expired Entries Are Cleaned During Search</h3>



<p>While scanning entries, expired items are removed opportunistically.</p>



<p>This prevents stale data from accumulating indefinitely without introducing background workers or schedulers.</p>



<h3 class="wp-block-heading">Key Takeaways</h3>



<ul class="wp-block-list">
<li>The semantic cache owns all <code data-enlighter-language="python" class="EnlighterJSRAW">Redis</code> interactions</li>



<li>Exact-match lookup is attempted before semantic matching</li>



<li>Semantic similarity is computed using embeddings</li>



<li>A linear scan trades performance for clarity</li>



<li>The cache returns the <em>best</em> reusable response, not just the first match</li>
</ul>



<p>At this stage, the system is fully functional: queries can be answered, cached, and reused.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Cache-Entries-What-Exactly-Gets-Stored"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Cache-Entries-What-Exactly-Gets-Stored">Cache Entries: What Exactly Gets Stored?</a></h2>



<p>So far, we’ve treated the cache as a logical concept: something that stores queries, embeddings, and responses.</p>



<p>In this section, we’ll make that concrete by looking at <strong>the structure of a cache entry</strong>. Understanding this structure is important because it explains <em>why</em> the cache can support both exact-match and semantic lookup — without duplicating data or logic.</p>



<h3 class="wp-block-heading">The Cache Entry Schema</h3>



<p>Cache entries are defined using a Pydantic model:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="34">class CacheEntry(BaseModel):
    id: str
    query: str
    query_hash: str
    embedding: str
    response: str
    created_at: int
    ttl: int
    metadata: Optional[Dict] = Field(default_factory=dict)
</pre>



<p>Each field exists for a specific reason. Let’s walk through them one by one.</p>



<h3 class="wp-block-heading">Identity and Query Fields</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="35">id: str
query: str
query_hash: str
</pre>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">id</code>: uniquely identifies the cache entry and is used to construct the Redis key.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">query</code>: stores the original user input. This is useful for debugging and inspection.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">query_hash</code>: stores a normalized hash of the query and enables <strong>exact-match lookup</strong>.</li>
</ul>



<p>At this stage, it’s enough to know that the hash ensures identical queries can be matched quickly. We’ll revisit <em>how</em> and <em>why</em> this normalization matters in a later lesson.</p>



<h3 class="wp-block-heading">Embedding Storage</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="36">embedding: str
</pre>



<p>Embeddings are stored as a <strong>JSON-serialized string</strong>, not as a raw Python list.</p>



<p>This choice is deliberate:</p>



<ul class="wp-block-list">
<li>Redis stores strings efficiently</li>



<li>Serialization keeps the schema simple</li>



<li>Deserialization happens only when similarity needs to be computed</li>
</ul>



<p>For Lesson 1, the important takeaway is that embeddings are stored <strong>once</strong>, alongside the response they produced.</p>



<h3 class="wp-block-heading">Response and Timing Information</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="37">response: str
created_at: int
ttl: int
</pre>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">response</code>: is the text returned by the LLM.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">created_at</code>: records when the entry was generated.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ttl</code>: defines how long the entry is considered valid.</li>
</ul>



<p>The cache does not rely on Redis expiration here. Instead, validity is checked at read time. This gives the application full control over when an entry should be reused or rejected.</p>



<p>We intentionally avoid deeper TTL semantics in this lesson.</p>



<h3 class="wp-block-heading">Metadata and Safety</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="38">metadata: Optional[Dict] = Field(default_factory=dict)
</pre>



<p>Metadata allows the cache to store contextual information such as:</p>



<ul class="wp-block-list">
<li>pipeline name</li>



<li>model identifier</li>



<li>request origin</li>
</ul>



<p>The use of <code data-enlighter-language="python" class="EnlighterJSRAW">default_factory=dict</code> avoids shared mutable state across cache entries — a subtle but important correctness detail.</p>



<p>At this stage, metadata is informational rather than functional.</p>



<h3 class="wp-block-heading">Why This Schema Works Well</h3>



<p>This schema supports the layered caching strategy naturally:</p>



<ul class="wp-block-list">
<li><strong>Exact match</strong> uses <code data-enlighter-language="python" class="EnlighterJSRAW">query_hash</code></li>



<li><strong>Semantic match</strong> uses embedding</li>



<li><strong>Freshness checks</strong> use <code data-enlighter-language="python" class="EnlighterJSRAW">created_at</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">ttl</code></li>



<li><strong>Safety checks</strong> use response and metadata</li>
</ul>



<p>All required information is co-located in a single cache entry, making lookup and validation straightforward.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-End-to-End-Demo-Verifying-Core-Cache-Behavior"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-End-to-End-Demo-Verifying-Core-Cache-Behavior">End-to-End Demo: Verifying Core Cache Behavior</a></h2>



<p>In this section, we will verify that the semantic cache behaves as expected under a small set of controlled scenarios.</p>



<p>These examples are meant to be <strong>run locally by the reader</strong>. The responses shown below are <strong>representative</strong> and may vary slightly depending on the model and configuration.</p>



<h3 class="wp-block-heading">Demo Case 1: Cold Request (LLM Fallback)</h3>



<p>We begin with a query that has not been seen before.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="39">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "What is semantic caching?"}'
</pre>



<p><strong>Expected behavior</strong></p>



<ul class="wp-block-list">
<li>Exact-match cache miss</li>



<li>Semantic cache miss</li>



<li>LLM call</li>



<li>Cache population</li>
</ul>



<p><strong>Response</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-24-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="463" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-1024x463.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53582" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24.png?size=126x57&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-300x135.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24.png?size=378x171&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24.png?size=504x228&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24.png?size=630x285&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-768x347.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-1024x463.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-24-1536x694.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Cold request flow showing a cache miss at both the exact-match and semantic cache layers, triggering an LLM fallback. The response is generated by the model and stored for future reuse (source: image by the author).</figcaption></figure></div>


<p>The key signal here is <code data-enlighter-language="python" class="EnlighterJSRAW">"from_cache": false</code>, confirming the request fell back to the LLM.</p>



<h3 class="wp-block-heading">Demo Case 2: Exact-Match Cache Hit</h3>



<p>Now we send the <strong>same query again</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="40">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "What is semantic caching?"}'
</pre>



<p><strong>Expected behavior</strong></p>



<ul class="wp-block-list">
<li>Exact-match cache hit</li>



<li>No embedding generation</li>



<li>No LLM call</li>
</ul>



<p><strong>Example response</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-25-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="494" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-1024x494.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53584" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25.png?size=126x61&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-300x145.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25.png?size=378x182&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25.png?size=504x243&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25.png?size=630x304&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-768x371.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-1024x494.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-25-1536x741.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Exact-match cache behavior. The repeated query is served directly from the cache via an exact string match, bypassing embedding generation and the LLM entirely (source: image by the author).</figcaption></figure></div>


<p>Here, the cache reused the response immediately using an exact-match lookup.</p>



<h3 class="wp-block-heading">Optional Demo: Whitespace Normalization</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="41">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "   What   is   semantic   caching?   "}'
</pre>



<p>This will hit the exact-match cache due to query normalization.</p>



<h3 class="wp-block-heading">Demo Case 3: Semantic Cache Hit (Paraphrased Query)</h3>



<p>Next, we send a paraphrased version of the original query.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="42">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Can you explain how semantic caching works?"}'
</pre>



<p><strong>Expected behavior</strong></p>



<ul class="wp-block-list">
<li>Exact-match cache miss</li>



<li>Embedding generation</li>



<li>Semantic cache hit</li>



<li>No LLM call</li>
</ul>



<p><strong>Example response</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-26-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="480" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-1024x480.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53586" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26.png?size=126x59&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-300x141.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26.png?size=378x177&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26.png?size=504x236&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26.png?size=630x295&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-768x360.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-1024x480.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-26-1536x720.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> Semantic cache hit for a paraphrased query. Although the input text differs, the cached response is reused based on embedding similarity, avoiding a new LLM call (source: image by the author).</figcaption></figure></div>


<p>Even though the query text is different, the cache successfully reused the response based on semantic similarity.</p>



<h3 class="wp-block-heading">Demo Case 4: Forcing a Cache Miss with bypass_cache</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">bypass_cache</code> flag allows us to force the system to skip both cache layers.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="43">curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "What is semantic caching?", "bypass_cache": true}'
</pre>



<p><strong>Expected behavior</strong></p>



<ul class="wp-block-list">
<li>Exact-match cache skipped</li>



<li>Semantic cache skipped</li>



<li>LLM called unconditionally</li>
</ul>



<p><strong>Example response</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-27-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="488" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27-1024x488.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53587" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27.png?size=126x60&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27-300x143.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27.png?size=378x180&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27.png?size=504x240&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27.png?size=630x300&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27-768x366.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27-1024x488.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-27-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 8: </strong>Cache bypass behavior. The request explicitly skips all cache layers via <code>bypass_cache</code>, ensuring the LLM pipeline executes independently of cached responses (source: image by the author).</figcaption></figure></div>


<p>This is useful for debugging and validating that the LLM pipeline still works independently of the cache.</p>



<h3 class="wp-block-heading">Observing Cache Metrics (Optional)</h3>



<p>You can inspect basic cache statistics using the <code data-enlighter-language="python" class="EnlighterJSRAW">/internal/metrics</code> endpoint:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="44">curl http://localhost:8000/internal/metrics
</pre>



<p><strong>Example response</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-28-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="262" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28-1024x262.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53589" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28.png?size=126x32&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28-300x77.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28.png?size=378x97&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28.png?size=504x129&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28.png?size=630x161&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28-768x196.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28-1024x262.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-28-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 9:</strong> Internal cache metrics showing hit, miss, and bypass counters, enabling lightweight observability of cache behavior during development and debugging (source: image by the author).</figcaption></figure></div>


<p>These metrics make cache behavior observable without requiring external tooling.</p>



<p>If you can reproduce these behaviors locally, you’ve successfully implemented a working semantic cache.</p>



<p>In the next lesson, we will take this system and begin hardening it for real-world use.</p>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, we built a complete semantic caching system for LLM applications from the ground up. We started by wiring a FastAPI service and defining a clean request–response contract, then implemented a layered caching strategy that prioritizes cheap exact-match lookups before escalating to semantic similarity and, finally, LLM inference.</p>



<p>We walked through how text queries are converted into embeddings on demand, how cached responses and embeddings are stored in Redis, and how the cache decides whether a prior response can be safely reused. By keeping the implementation intentionally simple and explicit, every step in the request flow remains observable and easy to reason about.</p>



<p>Finally, we verified the system end-to-end by running controlled demos: a cold request falling back to the LLM, an exact-match cache hit, a semantic cache hit for a paraphrased query, and an explicit cache bypass. At this point, you have a working semantic cache that behaves correctly, makes its decisions visible, and serves as a solid foundation for further hardening and optimization.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“Semantic Caching for LLMs: FastAPI, Redis, and Embeddings,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/yso6f" target="_blank" rel="noreferrer noopener">https://pyimg.co/yso6f</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Semantic Caching for LLMs: FastAPI, Redis, and Embeddings" data-enlighter-group="45">@incollection{Singh_2026_semantic-caching-for-llms-fastapi-redis-and-embeddings,
  author = {Vikram Singh},
  title = {{Semantic Caching for LLMs: FastAPI, Redis, and Embeddings}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/yso6f},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/27/semantic-caching-for-llms-fastapi-redis-and-embeddings/">Semantic Caching for LLMs: FastAPI, Redis, and Embeddings</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</title>
		<link>https://pyimagesearch.com/2026/04/20/pytest-tutorial-mlops-testing-fixtures-and-locust-load-testing/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[FastAPI]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Pytest]]></category>
		<category><![CDATA[Software Testing]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[fastapi testing]]></category>
		<category><![CDATA[locust load testing]]></category>
		<category><![CDATA[mlops pipeline]]></category>
		<category><![CDATA[mlops testing]]></category>
		<category><![CDATA[pytest]]></category>
		<category><![CDATA[pytest fixtures]]></category>
		<category><![CDATA[python load testing]]></category>
		<category><![CDATA[software testing pyramid]]></category>
		<category><![CDATA[testing pyramid]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53470</guid>

					<description><![CDATA[<p>Table of Contents Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing Introduction to MLOps Testing: Building Reliable ML Systems with Pytest Why Testing Is Non-Negotiable in MLOps What You Will Learn: Pytest, Fixtures, and Load Testing for MLOps From&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/20/pytest-tutorial-mlops-testing-fixtures-and-locust-load-testing/">Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Pytest-Tutorial-MLOps-Testing-Fixtures-Locust-Load-Testing"><a rel="noopener" target="_blank" href="#h1-Pytest-Tutorial-MLOps-Testing-Fixtures-Locust-Load-Testing">Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</a></li>

    <li id="TOC-h2-Introduction-MLOps-Testing-Building-Reliable-ML-Systems-Pytest"><a rel="noopener" target="_blank" href="#h2-Introduction-MLOps-Testing-Building-Reliable-ML-Systems-Pytest">Introduction to MLOps Testing: Building Reliable ML Systems with Pytest</a></li>

    <li id="TOC-h2-Why-Testing-Non-Negotiable-MLOps"><a rel="noopener" target="_blank" href="#h2-Why-Testing-Non-Negotiable-MLOps">Why Testing Is Non-Negotiable in MLOps</a></li>
    <ul>
        <li id="TOC-h3-What-You-Will-Learn-Pytest-Fixtures-Load-Testing-MLOps"><a rel="noopener" target="_blank" href="#h3-What-You-Will-Learn-Pytest-Fixtures-Load-Testing-MLOps">What You Will Learn: Pytest, Fixtures, and Load Testing for MLOps</a></li>
        <li id="TOC-h3-From-FastAPI-Testing-Extending-MLOps-Pipeline-Validation"><a rel="noopener" target="_blank" href="#h3-From-FastAPI-Testing-Extending-MLOps-Pipeline-Validation">From FastAPI to Testing: Extending Your MLOps Pipeline with Validation</a></li>
    </ul>

    <li id="TOC-h2-Test-Driven-MLOps-Applying-Software-Testing-Best-Practices-ML-Pipelines"><a rel="noopener" target="_blank" href="#h2-Test-Driven-MLOps-Applying-Software-Testing-Best-Practices-ML-Pipelines">Test-Driven MLOps: Applying Software Testing Best Practices to ML Pipelines</a></li>
    <ul>
        <li id="TOC-h3-What-Test-MLOps-Pipelines-Models-APIs-Configurations"><a rel="noopener" target="_blank" href="#h3-What-Test-MLOps-Pipelines-Models-APIs-Configurations">What to Test in MLOps Pipelines: Models, APIs, and Configurations</a></li>
        <li id="TOC-h3-Unit-vs-Integration-vs-Performance-Testing"><a rel="noopener" target="_blank" href="#h3-Unit-vs-Integration-vs-Performance-Testing">Unit vs Integration vs Performance Testing</a></li>
        <li id="TOC-h3-Software-Testing-Pyramid-MLOps-Unit-Integration-Load-Testing"><a rel="noopener" target="_blank" href="#h3-Software-Testing-Pyramid-MLOps-Unit-Integration-Load-Testing">The Software Testing Pyramid for MLOps: Unit, Integration, and Load Testing</a></li>
    </ul>

    <li id="TOC-h2-Project-Structure-Test-Layout"><a rel="noopener" target="_blank" href="#h2-Project-Structure-Test-Layout">Project Structure and Test Layout</a></li>
    <ul>
        <li id="TOC-h3-Test-Directory-Structure-MLOps-unit-integration-performance"><a rel="noopener" target="_blank" href="#h3-Test-Directory-Structure-MLOps-unit-integration-performance">Test Directory Structure for MLOps: unit, integration, and performance</a></li>
        <li id="TOC-h3-Understanding-Pytest-Fixtures-Using-conftest-py-Reusable-Test-Setup"><a rel="noopener" target="_blank" href="#h3-Understanding-Pytest-Fixtures-Using-conftest-py-Reusable-Test-Setup">Understanding Pytest Fixtures: Using conftest.py for Reusable Test Setup</a></li>
        <li id="TOC-h3-Where-Place-Tests-MLOps-Projects-Unit-vs-Integration-vs-Performance"><a rel="noopener" target="_blank" href="#h3-Where-Place-Tests-MLOps-Projects-Unit-vs-Integration-vs-Performance">Where to Place Tests in MLOps Projects: Unit vs Integration vs Performance</a></li>
    </ul>

    <li id="TOC-h2-Unit-Testing-MLOps-Pytest"><a rel="noopener" target="_blank" href="#h2-Unit-Testing-MLOps-Pytest">Unit Testing in MLOps with Pytest</a></li>
    <ul>
        <li id="TOC-h3-Code-Under-Test-Inference-Service-Dummy-Model"><a rel="noopener" target="_blank" href="#h3-Code-Under-Test-Inference-Service-Dummy-Model">The Code Under Test: Inference Service and Dummy Model</a></li>
        <li id="TOC-h3-services-inference-service-py"><a rel="noopener" target="_blank" href="#h3-services-inference-service-py">services/inference_service.py</a></li>
        <li id="TOC-h3-models-dummy-model-py"><a rel="noopener" target="_blank" href="#h3-models-dummy-model-py">models/dummy_model.py</a></li>
        <li id="TOC-h3-Writing-Pytest-Unit-Tests-MLOps-test-inference-service-py"><a rel="noopener" target="_blank" href="#h3-Writing-Pytest-Unit-Tests-MLOps-test-inference-service-py">Writing Pytest Unit Tests for MLOps: test_inference_service.py</a></li>
        <li id="TOC-h3-Testing-Inference-Service-Pytest-MLOps-Unit-Tests"><a rel="noopener" target="_blank" href="#h3-Testing-Inference-Service-Pytest-MLOps-Unit-Tests">Testing the Inference Service with Pytest (MLOps Unit Tests)</a></li>
        <li id="TOC-h3-Testing-ML-Models-Isolation-Pytest"><a rel="noopener" target="_blank" href="#h3-Testing-ML-Models-Isolation-Pytest">Testing ML Models in Isolation with Pytest</a></li>
        <li id="TOC-h3-How-Run-Pytest-Unit-Tests-MLOps-Projects"><a rel="noopener" target="_blank" href="#h3-How-Run-Pytest-Unit-Tests-MLOps-Projects">How to Run Pytest Unit Tests for MLOps Projects</a></li>
    </ul>

    <li id="TOC-h2-Integration-Testing-MLOps"><a rel="noopener" target="_blank" href="#h2-Integration-Testing-MLOps">Integration Testing in MLOps</a></li>
    <ul>
        <li id="TOC-h3-Using-FastAPI-TestClient-Integration-Testing-Pytest"><a rel="noopener" target="_blank" href="#h3-Using-FastAPI-TestClient-Integration-Testing-Pytest">Using FastAPI TestClient for Integration Testing with Pytest</a></li>
        <li id="TOC-h3-How-FastAPI-TestClient-Works-API-Testing"><a rel="noopener" target="_blank" href="#h3-How-FastAPI-TestClient-Works-API-Testing">How FastAPI TestClient Works for API Testing</a></li>
        <li id="TOC-h3-Testing-API-Endpoints-health-predict"><a rel="noopener" target="_blank" href="#h3-Testing-API-Endpoints-health-predict">Testing API Endpoints (/health, /predict)</a></li>
        <li id="TOC-h3-What-Integration-Tests-Verify-MLOps-API"><a rel="noopener" target="_blank" href="#h3-What-Integration-Tests-Verify-MLOps-API">What Integration Tests Verify in an MLOps API</a></li>
        <li id="TOC-h3-Testing-predict-Endpoint-MLOps-API"><a rel="noopener" target="_blank" href="#h3-Testing-predict-Endpoint-MLOps-API">Testing the /predict Endpoint in an MLOps API</a></li>
        <li id="TOC-h3-Testing-Documentation-Endpoints-docs-openapi-json"><a rel="noopener" target="_blank" href="#h3-Testing-Documentation-Endpoints-docs-openapi-json">Testing Documentation Endpoints (/docs, /openapi.json)</a></li>
        <li id="TOC-h3-What-This-Ensures"><a rel="noopener" target="_blank" href="#h3-What-This-Ensures">What This Ensures</a></li>
        <li id="TOC-h3-Testing-Error-Handling-FastAPI-APIs-Pytest"><a rel="noopener" target="_blank" href="#h3-Testing-Error-Handling-FastAPI-APIs-Pytest">Testing Error Handling in FastAPI APIs with Pytest</a></li>
        <li id="TOC-h3-Integration-Test-Breakdown-What-Each-Test-Validates"><a rel="noopener" target="_blank" href="#h3-Integration-Test-Breakdown-What-Each-Test-Validates">Integration Test Breakdown: What Each Test Validates</a></li>
        <li id="TOC-h3-How-Run-Integration-Tests-Pytest-MLOps"><a rel="noopener" target="_blank" href="#h3-How-Run-Integration-Tests-Pytest-MLOps">How to Run Integration Tests with Pytest in MLOps</a></li>
    </ul>

    <li id="TOC-h2-Performance-Load-Testing-Locust"><a rel="noopener" target="_blank" href="#h2-Performance-Load-Testing-Locust">Performance and Load Testing with Locust</a></li>
    <ul>
        <li id="TOC-h3-Why-Load-Testing-Essential-MLOps-ML-APIs"><a rel="noopener" target="_blank" href="#h3-Why-Load-Testing-Essential-MLOps-ML-APIs">Why Load Testing Is Essential for MLOps and ML APIs</a></li>
        <li id="TOC-h3-Locust-Load-Testing-Concepts-Users-Spawn-Rate-Tasks-Explained"><a rel="noopener" target="_blank" href="#h3-Locust-Load-Testing-Concepts-Users-Spawn-Rate-Tasks-Explained">Locust Load Testing Concepts: Users, Spawn Rate, and Tasks Explained</a></li>
        <li id="TOC-h3-Writing-locustfile-py"><a rel="noopener" target="_blank" href="#h3-Writing-locustfile-py">Writing the locustfile.py</a></li>
        <li id="TOC-h3-What-This-Locust-Load-Test-Validates-MLOps-API"><a rel="noopener" target="_blank" href="#h3-What-This-Locust-Load-Test-Validates-MLOps-API">What This Locust Load Test Validates in an MLOps API</a></li>
        <li id="TOC-h3-Running-Locust-Headless-Mode-vs-Web-UI-Dashboard"><a rel="noopener" target="_blank" href="#h3-Running-Locust-Headless-Mode-vs-Web-UI-Dashboard">Running Locust: Headless Mode vs Web UI Dashboard</a></li>
        <li id="TOC-h3-Generating-Locust-Load-Testing-Reports-ML-APIs"><a rel="noopener" target="_blank" href="#h3-Generating-Locust-Load-Testing-Reports-ML-APIs">Generating Locust Load Testing Reports for ML APIs</a></li>
        <li id="TOC-h3-Understanding-Test-Metrics-RPS-failures-latency-P95-P99"><a rel="noopener" target="_blank" href="#h3-Understanding-Test-Metrics-RPS-failures-latency-P95-P99">Understanding Test Metrics (RPS, failures, latency, P95/P99)</a></li>
    </ul>

    <li id="TOC-h2-MLOps-Test-Configuration-YAML-Environment-Variables"><a rel="noopener" target="_blank" href="#h2-MLOps-Test-Configuration-YAML-Environment-Variables">MLOps Test Configuration: YAML and Environment Variables</a></li>
    <ul>
        <li id="TOC-h3-Understanding-test-config-yaml-MLOps-Testing"><a rel="noopener" target="_blank" href="#h3-Understanding-test-config-yaml-MLOps-Testing">Understanding test_config.yaml for MLOps Testing</a></li>
        <li id="TOC-h3-What-test-config-yaml-Controls-MLOps-Pipelines"><a rel="noopener" target="_blank" href="#h3-What-test-config-yaml-Controls-MLOps-Pipelines">What test_config.yaml Controls in MLOps Pipelines</a></li>
        <li id="TOC-h3-Overriding-Application-Configuration-Test-Mode"><a rel="noopener" target="_blank" href="#h3-Overriding-Application-Configuration-Test-Mode">Overriding Application Configuration in Test Mode</a></li>
        <li id="TOC-h3-How-Configuration-Overrides-Work-YAML-Environment-Variables"><a rel="noopener" target="_blank" href="#h3-How-Configuration-Overrides-Work-YAML-Environment-Variables">How Configuration Overrides Work: YAML and Environment Variables</a></li>
        <li id="TOC-h3-Why-Configuration-Management-Matters-MLOps-Testing"><a rel="noopener" target="_blank" href="#h3-Why-Configuration-Management-Matters-MLOps-Testing">Why Configuration Management Matters in MLOps Testing</a></li>
        <li id="TOC-h3-Using-Environment-Variables-Test-Isolation"><a rel="noopener" target="_blank" href="#h3-Using-Environment-Variables-Test-Isolation">Using Environment Variables for Test Isolation</a></li>
    </ul>

    <li id="TOC-h2-Code-Quality-MLOps-Linting-Formatting-Static-Analysis-Tools"><a rel="noopener" target="_blank" href="#h2-Code-Quality-MLOps-Linting-Formatting-Static-Analysis-Tools">Code Quality in MLOps: Linting, Formatting, and Static Analysis Tools</a></li>
    <ul>
        <li id="TOC-h3-Linting-Python-Code-flake8"><a rel="noopener" target="_blank" href="#h3-Linting-Python-Code-flake8">Linting Python Code with flake8</a></li>
        <li id="TOC-h3-Formatting-Python-Code-Black-Pipelines"><a rel="noopener" target="_blank" href="#h3-Formatting-Python-Code-Black-Pipelines">Formatting Python Code with Black Pipelines</a></li>
        <li id="TOC-h3-Using-isort-Manage-Python-Imports"><a rel="noopener" target="_blank" href="#h3-Using-isort-Manage-Python-Imports">Using isort to Manage Python Imports</a></li>
        <li id="TOC-h3-How-Run-isort-Clean-Python-Imports"><a rel="noopener" target="_blank" href="#h3-How-Run-isort-Clean-Python-Imports">How to Run isort for Clean Python Imports</a></li>
        <li id="TOC-h3-Static-Type-Checking-MyPy-MLOps-Codebases"><a rel="noopener" target="_blank" href="#h3-Static-Type-Checking-MyPy-MLOps-Codebases">Static Type Checking with MyPy for MLOps Codebases</a></li>
        <li id="TOC-h3-Using-Makefile-Automate-MLOps-Testing-Code-Quality"><a rel="noopener" target="_blank" href="#h3-Using-Makefile-Automate-MLOps-Testing-Code-Quality">Using a Makefile to Automate MLOps Testing and Code Quality</a></li>
    </ul>

    <li id="TOC-h2-Automating-Testing-Pytest-Test-Runner-Script"><a rel="noopener" target="_blank" href="#h2-Automating-Testing-Pytest-Test-Runner-Script">Automating Testing with a Pytest Test Runner Script</a></li>
    <ul>
        <li id="TOC-h3-Running-Automated-Tests-run-tests-sh"><a rel="noopener" target="_blank" href="#h3-Running-Automated-Tests-run-tests-sh">Running Automated Tests with run_tests.sh</a></li>
        <li id="TOC-h3-Understanding-Pytest-Output-Test-Results"><a rel="noopener" target="_blank" href="#h3-Understanding-Pytest-Output-Test-Results">Understanding Pytest Output and Test Results</a></li>
        <li id="TOC-h3-Why-Automated-Testing-Workflows-Matter-MLOps"><a rel="noopener" target="_blank" href="#h3-Why-Automated-Testing-Workflows-Matter-MLOps">Why Automated Testing Workflows Matter in MLOps</a></li>
        <li id="TOC-h3-Integrating-Pytest-CI-CD-Pipelines"><a rel="noopener" target="_blank" href="#h3-Integrating-Pytest-CI-CD-Pipelines">Integrating Pytest into CI/CD Pipelines</a></li>
    </ul>

    <li id="TOC-h2-Automating-Load-Testing-MLOps-Locust-Scripts"><a rel="noopener" target="_blank" href="#h2-Automating-Load-Testing-MLOps-Locust-Scripts">Automating Load Testing in MLOps with Locust Scripts</a></li>
    <ul>
        <li id="TOC-h3-Running-Automated-Locust-Load-Tests-run-locust-sh"><a rel="noopener" target="_blank" href="#h3-Running-Automated-Locust-Load-Tests-run-locust-sh">Running Automated Locust Load Tests with run_locust.sh</a></li>
        <li id="TOC-h3-Automatically-Generating-Load-Testing-Reports-ML-APIs"><a rel="noopener" target="_blank" href="#h3-Automatically-Generating-Load-Testing-Reports-ML-APIs">Automatically Generating Load Testing Reports for ML APIs</a></li>
        <li id="TOC-h3-Preparing-Load-Testing-CI-CD-Cloud-MLOps-Pipelines"><a rel="noopener" target="_blank" href="#h3-Preparing-Load-Testing-CI-CD-Cloud-MLOps-Pipelines">Preparing Load Testing for CI/CD and Cloud MLOps Pipelines</a></li>
    </ul>

    <li id="TOC-h2-Test-Coverage-MLOps-Measuring-Improving-Code-Coverage"><a rel="noopener" target="_blank" href="#h2-Test-Coverage-MLOps-Measuring-Improving-Code-Coverage">Test Coverage in MLOps: Measuring and Improving Code Coverage</a></li>
    <ul>
        <li id="TOC-h3-Using-pytest-cov-Measure-Test-Coverage"><a rel="noopener" target="_blank" href="#h3-Using-pytest-cov-Measure-Test-Coverage">Using pytest-cov to Measure Test Coverage</a></li>
        <li id="TOC-h3-How-Measure-Code-Coverage-MLOps-Projects"><a rel="noopener" target="_blank" href="#h3-How-Measure-Code-Coverage-MLOps-Projects">How to Measure Code Coverage in MLOps Projects</a></li>
        <li id="TOC-h3-How-Increase-Test-Coverage-MLOps-Pipelines"><a rel="noopener" target="_blank" href="#h3-How-Increase-Test-Coverage-MLOps-Pipelines">How to Increase Test Coverage in MLOps Pipelines</a></li>
        <li id="TOC-h3-Recommended-Test-Coverage-Targets-MLOps-Systems"><a rel="noopener" target="_blank" href="#h3-Recommended-Test-Coverage-Targets-MLOps-Systems">Recommended Test Coverage Targets for MLOps Systems</a></li>
    </ul>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Pytest-Tutorial-MLOps-Testing-Fixtures-Locust-Load-Testing"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Pytest-Tutorial-MLOps-Testing-Fixtures-Locust-Load-Testing">Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</a></h2>



<p>In this lesson, you will learn how to make ML systems reliable, correct, and production-ready through structured testing and validation. You will walk through unit tests, integration tests, load and performance checks, fixtures, code quality tools, and automated test runs, giving you everything you need to ensure your ML API behaves predictably under real-world conditions.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?lossy=2&strip=1&webp=1" alt="pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png" class="wp-image-53483" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/pytest-tutorial-mlops-testing-fixtures-locust-load-testing-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>This lesson is the last of a 2-part series on Software Engineering for Machine Learning Operations (MLOps):</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/yn8a5" target="_blank" rel="noreferrer noopener">FastAPI for MLOps: Python Project Structure and API Best Practices</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/4ztdu" target="_blank" rel="noreferrer noopener">Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</a></strong></em><strong> (this tutorial)</strong></li>
</ol>



<p><strong>To learn how to test, validate, and stress-test your ML services like a professional MLOps engineer, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction-MLOps-Testing-Building-Reliable-ML-Systems-Pytest"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction-MLOps-Testing-Building-Reliable-ML-Systems-Pytest">Introduction to MLOps Testing: Building Reliable ML Systems with Pytest</a></h2>



<p>Testing is the backbone of reliable MLOps. A model might look great in a notebook, but once wrapped in services, APIs, configs, and infrastructure, dozens of things can break silently: incorrect inputs, unexpected model outputs, missing environment variables, slow endpoints, and downstream failures. This lesson ensures you never ship those problems into production.</p>



<p>In this lesson, you will learn the complete testing workflow for machine learning (ML) systems: from small, isolated unit tests to full API integration checks and load testing your endpoints under real traffic conditions. You will also understand how to structure your tests, how each type of test fits into the MLOps lifecycle, and how to design a test suite that grows cleanly as your project evolves.</p>



<p>To learn how to validate, benchmark, and harden your ML applications for production, just keep reading.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Testing-Non-Negotiable-MLOps"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Testing-Non-Negotiable-MLOps">Why Testing Is Non-Negotiable in MLOps</a></h2>



<p>Machine learning adds layers of unpredictability on top of regular software engineering. Models drift, inputs vary, inference latency can increase, and small code changes can ripple into major behavioral shifts. Without testing, you have no safety net. Proper tests make your system observable, predictable, and safe to deploy.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-You-Will-Learn-Pytest-Fixtures-Load-Testing-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-You-Will-Learn-Pytest-Fixtures-Load-Testing-MLOps">What You Will Learn: Pytest, Fixtures, and Load Testing for MLOps</a></h3>



<p>You will walk through a practical testing workflow tailored for ML applications: writing unit tests for inference logic, validating API endpoints end-to-end, using fixtures to isolate environments, verifying configuration behavior, and running load tests to understand real-world performance. Each example connects directly to the codebase you built earlier.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-From-FastAPI-Testing-Extending-MLOps-Pipeline-Validation"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-From-FastAPI-Testing-Extending-MLOps-Pipeline-Validation">From FastAPI to Testing: Extending Your MLOps Pipeline with Validation</a></h3>



<p>Previously, you learned how to structure a clean ML codebase, configure environments, separate services, and expose reliable API endpoints. Now, you will stress-test that foundation. This lesson transforms your structured application into a validated, production-ready system with tests that catch issues before users ever see them.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Test-Driven-MLOps-Applying-Software-Testing-Best-Practices-ML-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Test-Driven-MLOps-Applying-Software-Testing-Best-Practices-ML-Pipelines">Test-Driven MLOps: Applying Software Testing Best Practices to ML Pipelines</a></h2>



<p>Test-driven development (TDD) matters even more in ML because models introduce uncertainty on top of normal software complexity. A single mistake in preprocessing, an incorrect model version, or a slow endpoint can break your application in ways that are hard to detect without a structured testing strategy. Test-driven MLOps gives you a predictable workflow: write tests, run them often, and let failures guide improvements.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-Test-MLOps-Pipelines-Models-APIs-Configurations"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-Test-MLOps-Pipelines-Models-APIs-Configurations">What to Test in MLOps Pipelines: Models, APIs, and Configurations</a></h3>



<p>ML systems require testing across multiple layers because issues can appear anywhere: in preprocessing logic, service code, configuration loading, API endpoints, or the model itself. You should verify that your inference service behaves correctly with both valid and invalid inputs, that your API returns consistent responses, that your configuration behaves as expected, and that the entire pipeline works end-to-end. Even when using a dummy model, testing ensures that the structure of your system remains correct as the real model is swapped in later.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Unit-vs-Integration-vs-Performance-Testing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Unit-vs-Integration-vs-Performance-Testing">Unit vs Integration vs Performance Testing</a></h3>



<p>Unit tests focus on the smallest pieces of your system: functions, helper modules, and the inference service. They run fast and break quickly when a small change introduces an error. Integration tests validate how components work together: routes, services, configs, and the FastAPI layer. They ensure your API behaves consistently no matter what changes inside the codebase. Performance tests simulate real user traffic, evaluating latency, throughput, and failure rates under load. Together, these 3 types of tests create full confidence in your ML application.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Software-Testing-Pyramid-MLOps-Unit-Integration-Load-Testing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Software-Testing-Pyramid-MLOps-Unit-Integration-Load-Testing">The Software Testing Pyramid for MLOps: Unit, Integration, and Load Testing</a></h3>



<p>The testing pyramid helps prioritize effort: many unit tests at the bottom, fewer integration tests in the middle, and a small number of heavy performance tests at the top. ML systems especially benefit from this structure because most failures occur in smaller utilities and service functions, not in the final API layer. By weighting your test suite correctly, you get fast feedback during development while still validating the entire system before deployment.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure-Test-Layout"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure-Test-Layout">Project Structure and Test Layout</a></h2>



<p>A clean testing layout makes your ML system predictable, scalable, and easy to maintain. By separating tests into clear categories (e.g., unit, integration, and performance), you ensure that each kind of test has a focused purpose and a natural home inside the repository. This structure also mirrors how real production MLOps teams organize their work, making your project easier to extend as your system grows.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Test-Directory-Structure-MLOps-unit-integration-performance"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Test-Directory-Structure-MLOps-unit-integration-performance">Test Directory Structure for MLOps: unit, integration, and performance</a></h3>



<p>Your Lesson 2 repository includes a dedicated <code data-enlighter-language="python" class="EnlighterJSRAW">tests/</code> directory with 3 subfolders:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="1">tests/
│── unit/
│── integration/
└── performance/</pre>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">unit/</code>: holds small, fast tests that validate individual pieces such as the <code data-enlighter-language="python" class="EnlighterJSRAW">DummyModel</code>, the inference service, or helper functions.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">integration/</code>: contains tests that spin up the FastAPI app and verify endpoints like <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code>, and the OpenAPI docs.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">performance/</code>: includes Locust load testing scripts that simulate real traffic hitting your API to measure latency, throughput, and error rates.</li>
</ul>



<p>This layout ensures that each type of test is separated by intent and runtime cost, giving you a clean way to scale your test suite over time.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-Pytest-Fixtures-Using-conftest-py-Reusable-Test-Setup"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-Pytest-Fixtures-Using-conftest-py-Reusable-Test-Setup">Understanding Pytest Fixtures: Using conftest.py for Reusable Test Setup</a></h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">conftest.py</code> file is the backbone of your testing environment. Pytest automatically loads fixtures defined here and makes them available across all test files without explicit imports.</p>



<p>Your project uses <code data-enlighter-language="python" class="EnlighterJSRAW">conftest.py</code> to provide:</p>



<ul class="wp-block-list">
<li><strong>FastAPI TestClient fixture:</strong> allows integration tests to call your API exactly the way a real HTTP client would.</li>



<li><strong>Sample input data:</strong> keeps repeated values out of your test files.</li>



<li><strong>Expected outputs:</strong> help tests stay focused on behavior rather than setup.</li>
</ul>



<p>This shared setup reduces duplication, keeps tests clean, and ensures consistent test behavior across the entire suite.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Where-Place-Tests-MLOps-Projects-Unit-vs-Integration-vs-Performance"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Where-Place-Tests-MLOps-Projects-Unit-vs-Integration-vs-Performance">Where to Place Tests in MLOps Projects: Unit vs Integration vs Performance</a></h3>



<p>A simple rule-of-thumb keeps your test organization disciplined:</p>



<ul class="wp-block-list">
<li><strong>Put tests in unit/ when the code under test does not require a running API or external system.<br></strong>Example: testing that the <code data-enlighter-language="python" class="EnlighterJSRAW">DummyModel.predict()</code> returns “positive” for the word <em>great</em>.</li>



<li><strong>Put tests in integration/ when the test needs the full FastAPI app running.<br></strong>Example: calling <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> and checking that the API returns a JSON response.</li>



<li><strong>Put tests in performance/ when measuring speed, concurrency limits, or error behavior under load.<br></strong>Example: Locust scripts simulating dozens of users sending <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> requests at once.</li>
</ul>



<p>Following this pattern ensures your tests remain stable, fast, and easy to reason about as the project grows.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Unit-Testing-MLOps-Pytest"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Unit-Testing-MLOps-Pytest">Unit Testing in MLOps with Pytest</a></h2>



<p>Unit tests are your first safety net in MLOps. Before you hit the API, spin up Locust, or ship to production, you want to know: <em>Does my core prediction code behave exactly the way I think it does?</em></p>



<p>In this lesson, you do that by testing 2 things in isolation:</p>



<ul class="wp-block-list">
<li><strong>inference service:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">services/inference_service.py</code></li>



<li><strong>dummy model:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">models/dummy_model.py</code></li>
</ul>



<p>All of that is captured in <code data-enlighter-language="python" class="EnlighterJSRAW">tests/unit/test_inference_service.py</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Code-Under-Test-Inference-Service-Dummy-Model"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Code-Under-Test-Inference-Service-Dummy-Model">The Code Under Test: Inference Service and Dummy Model</a></h3>



<p>First, recall what you are testing.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-services-inference-service-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-services-inference-service-py">services/inference_service.py</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="2">"""
Simple inference service for making model predictions.
"""
from models.dummy_model import DummyModel
from core.logger import logger

# Initialize model
model = DummyModel()
logger.info(f"Loaded model: {model.model_name}")


def predict(input_text: str) -> str:
    """
    Make a prediction using the loaded model.
   
    Args:
        input_text: Input text for prediction
       
    Returns:
        Prediction result as string
    """
    logger.info(f"Making prediction for input: {input_text[:50]}...")
   
    try:
        prediction = model.predict(input_text)
        logger.info(f"Prediction result: {prediction}")
        return prediction
    except Exception as e:
        logger.error(f"Error during prediction: {str(e)}")
        raise
</pre>



<p>This file does 3 things:</p>



<ul class="wp-block-list">
<li><strong>Initializes</strong> a <code data-enlighter-language="python" class="EnlighterJSRAW">DummyModel</code> once at import time and logs that it loaded.</li>



<li>Exposes a <code data-enlighter-language="python" class="EnlighterJSRAW">predict(input_text: str) -&gt; str</code> function that:
<ul class="wp-block-list">
<li>Logs the incoming input (truncated to 50 chars).</li>



<li>Calls <code data-enlighter-language="python" class="EnlighterJSRAW">model.predict(...)</code>.</li>



<li>Logs and returns the prediction.</li>
</ul>
</li>



<li>Catches any exception, logs the error, and re-raises it so failures are visible.</li>
</ul>



<p>You are not testing FastAPI here, just pure Python logic: given some text, does this function consistently return the correct label?</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-models-dummy-model-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-models-dummy-model-py">models/dummy_model.py</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="3">"""
Placeholder dummy model class.
"""
from typing import Any


class DummyModel:
    """
    A placeholder ML model class that returns fixed predictions.
    """
   
    def __init__(self) -> None:
        """Initialize the dummy model."""
        self.model_name = "dummy_classifier"
        self.version = "1.0.0"
   
    def predict(self, input_data: Any) -> str:
        """
        Make a prediction (returns a fixed string for demonstration).
       
        Args:
            input_data: Input data for prediction
           
        Returns:
            Fixed prediction string
        """
        text = str(input_data).lower()
        if "good" in text or "great" in text:
            return "positive"
        return "negative"
</pre>



<p>This model is deliberately simple:</p>



<ul class="wp-block-list">
<li>The constructor sets <code data-enlighter-language="python" class="EnlighterJSRAW">model_name</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">version</code> for logging and version tracking.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> method:
<ul class="wp-block-list">
<li>Converts any input to lowercase text.</li>



<li>Returns <code data-enlighter-language="python" class="EnlighterJSRAW">"positive"</code> if it sees <code data-enlighter-language="python" class="EnlighterJSRAW">"good"</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">"great"</code> in the text.</li>



<li>Returns <code data-enlighter-language="python" class="EnlighterJSRAW">"negative"</code> otherwise.</li>
</ul>
</li>
</ul>



<p>Your unit tests will assert that both the <strong>service</strong> and <strong>model</strong> behave exactly like this.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Writing-Pytest-Unit-Tests-MLOps-test-inference-service-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Writing-Pytest-Unit-Tests-MLOps-test-inference-service-py">Writing Pytest Unit Tests for MLOps: test_inference_service.py</a></h3>



<p>Here is the full unit test module:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="4">"""
Unit tests for the inference service.
"""
import pytest
from services.inference_service import predict
from models.dummy_model import DummyModel


class TestInferenceService:
    """Test class for inference service."""
   
    def test_predict_returns_string(self):
        """Test that predict() returns a string."""
        result = predict("some input text")
        assert isinstance(result, str)
   
    def test_predict_positive_input(self):
        """Test prediction with positive input."""
        result = predict("This is good")
        assert result == "positive"
   
    def test_predict_negative_input(self):
        """Test prediction with negative input."""
        result = predict("This is bad")
        assert result == "negative"


class TestDummyModel:
    """Test class for DummyModel."""
   
    def test_model_initialization(self):
        """Test that the model initializes correctly."""
        model = DummyModel()
        assert model.model_name == "dummy_classifier"
        assert model.version == "1.0.0"
   
    def test_predict_with_good_word(self):
        """Test that the model returns positive for 'good'."""
        model = DummyModel()
        result = model.predict("This is good")
        assert result == "positive"
   
    def test_predict_with_great_word(self):
        """Test that the model returns positive for 'great'."""
        model = DummyModel()
        result = model.predict("This is great")
        assert result == "positive"
   
    def test_predict_without_keywords(self):
        """Test that the model returns negative without keywords."""
        model = DummyModel()
        test_inputs = ["test", "random text", "negative sentiment"]
        for input_text in test_inputs:
            result = model.predict(input_text)
            assert result == "negative"
</pre>



<p>Let us break it down.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-Inference-Service-Pytest-MLOps-Unit-Tests"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-Inference-Service-Pytest-MLOps-Unit-Tests">Testing the Inference Service with Pytest (MLOps Unit Tests)</a></h3>



<p>The first test class focuses on the <strong>service function</strong>, not the API:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="5">class TestInferenceService:
    """Test class for inference service."""
   
    def test_predict_returns_string(self):
        """Test that predict() returns a string."""
        result = predict("some input text")
        assert isinstance(result, str)
</pre>



<ul class="wp-block-list">
<li>This test ensures <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> always returns a <strong>string</strong>, no matter what you pass in.</li>



<li>If someone later changes <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> to return a dict, tuple, or Pydantic model, this test will fail immediately.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="6">    def test_predict_positive_input(self):
        """Test prediction with positive input."""
        result = predict("This is good")
        assert result == "positive"
   
    def test_predict_negative_input(self):
        """Test prediction with negative input."""
        result = predict("This is bad")
        assert result == "negative"
</pre>



<p>These 2 tests verify the <strong>happy-path behavior</strong>:</p>



<ul class="wp-block-list">
<li>Text containing <code data-enlighter-language="python" class="EnlighterJSRAW">"good"</code> should be classified as <code data-enlighter-language="python" class="EnlighterJSRAW">"positive"</code>.</li>



<li>Text without <code data-enlighter-language="python" class="EnlighterJSRAW">"good"</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">"great"</code> should default to <code data-enlighter-language="python" class="EnlighterJSRAW">"negative"</code>.</li>
</ul>



<p>Notice what’s <em>not</em> happening here:</p>



<ul class="wp-block-list">
<li>No FastAPI client.</li>



<li>No HTTP calls.</li>



<li>No environment or config loading.</li>
</ul>



<p>This is pure, fast, deterministic testing of the core service logic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-ML-Models-Isolation-Pytest"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-ML-Models-Isolation-Pytest">Testing ML Models in Isolation with Pytest</a></h3>



<p>The second test class targets the model directly:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="7">class TestDummyModel:
    """Test class for DummyModel."""
   
    def test_model_initialization(self):
        """Test that the model initializes correctly."""
        model = DummyModel()
        assert model.model_name == "dummy_classifier"
        assert model.version == "1.0.0"
</pre>



<ul class="wp-block-list">
<li>This verifies that your model is <strong>initialized correctly</strong>.</li>



<li>In real projects, this might include loading weights, setting up devices, or configuration. Here, it is just <code data-enlighter-language="python" class="EnlighterJSRAW">model_name</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">version</code>, but the pattern is the same.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="8">    def test_predict_with_good_word(self):
        """Test that the model returns positive for 'good'."""
        model = DummyModel()
        result = model.predict("This is good")
        assert result == "positive"
   
    def test_predict_with_great_word(self):
        """Test that the model returns positive for 'great'."""
        model = DummyModel()
        result = model.predict("This is great")
        assert result == "positive"
</pre>



<ul class="wp-block-list">
<li>These tests assert that the <strong>keyword-based classification</strong> logic works: both <code data-enlighter-language="python" class="EnlighterJSRAW">"good"</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">"great"</code> map to <code data-enlighter-language="python" class="EnlighterJSRAW">"positive"</code>.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="9">    def test_predict_without_keywords(self):
        """Test that the model returns negative without keywords."""
        model = DummyModel()
        test_inputs = ["test", "random text", "negative sentiment"]
        for input_text in test_inputs:
            result = model.predict(input_text)
            assert result == "negative"
</pre>



<ul class="wp-block-list">
<li>This test loops over several neutral and negative phrases to make sure the model consistently returns &#8220;negative&#8221; when no positive keywords are present.</li>



<li>This is your <strong>guardrail</strong> against accidental changes to the keyword logic.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Run-Pytest-Unit-Tests-MLOps-Projects"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Run-Pytest-Unit-Tests-MLOps-Projects">How to Run Pytest Unit Tests for MLOps Projects</a></h3>



<p>To run just these tests:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="10">pytest tests/unit/ -v
</pre>



<p>Or with Poetry:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="11">poetry run pytest tests/unit/ -v
</pre>



<p>You will see output similar to:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="12">tests/unit/test_inference_service.py::TestInferenceService::test_predict_returns_string PASSED
tests/unit/test_inference_service.py::TestInferenceService::test_predict_positive_input PASSED
tests/unit/test_inference_service.py::TestInferenceService::test_predict_negative_input PASSED
tests/unit/test_inference_service.py::TestDummyModel::test_model_initialization PASSED
...
</pre>



<p>When everything is green, you know:</p>



<ul class="wp-block-list">
<li>Your <strong>core prediction logic</strong> is stable.</li>



<li>The <strong>dummy model</strong> behaves exactly as designed.</li>



<li>You can now safely move on to <strong>integration tests</strong> and <strong>performance tests</strong> in later sections.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Integration-Testing-MLOps"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Integration-Testing-MLOps">Integration Testing in MLOps</a></h2>



<p>Unit tests validate your core Python logic, but integration tests answer a different question:</p>



<p><strong>“Does the entire application behave correctly when all components work together?”</strong></p>



<p>This means testing:</p>



<ul class="wp-block-list">
<li><strong>FastAPI app</strong></li>



<li><strong>routing layer</strong></li>



<li><strong>service functions</strong></li>



<li><strong>model</strong></li>



<li><strong>configuration loaded at runtime</strong></li>
</ul>



<p>All of this happens using FastAPI’s <code data-enlighter-language="python" class="EnlighterJSRAW">TestClient</code> and your actual running application object (<code data-enlighter-language="python" class="EnlighterJSRAW">app</code> from <code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code>).</p>



<p>Let’s break it down.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-FastAPI-TestClient-Integration-Testing-Pytest"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-FastAPI-TestClient-Integration-Testing-Pytest">Using FastAPI TestClient for Integration Testing with Pytest</a></h3>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">conftest.py</code> defines a reusable client fixture:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="13">from fastapi.testclient import TestClient
from main import app

@pytest.fixture
def client():
    """Create a test client for the FastAPI app."""
    return TestClient(app)
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-FastAPI-TestClient-Works-API-Testing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-FastAPI-TestClient-Works-API-Testing">How FastAPI TestClient Works for API Testing</a></h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">TestClient(app)</code> spins up an <strong>in-memory FastAPI instance</strong>.</li>



<li>No server is launched, no networking occurs.</li>



<li>Every test receives a fresh client that behaves exactly like a real HTTP client or API consumer.</li>
</ul>



<p>This lets you write code such as:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="14">response = client.get("/health")
</pre>



<p>as if you were calling a real deployed API, but entirely offline and deterministic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-API-Endpoints-health-predict"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-API-Endpoints-health-predict">Testing API Endpoints (/health, /predict)</a></h3>



<p>Here is the integration test code from your repo:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="15">class TestHealthEndpoint:
    def test_health_check_returns_ok(self, client):
        response = client.get("/health")

        assert response.status_code == 200
        assert response.json() == {"status": "ok"}
   
    def test_health_check_has_correct_content_type(self, client):
        response = client.get("/health")

        assert response.status_code == 200
        assert "application/json" in response.headers["content-type"]
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-Integration-Tests-Verify-MLOps-API"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-Integration-Tests-Verify-MLOps-API">What Integration Tests Verify in an MLOps API</a></h3>



<ul class="wp-block-list">
<li>Your <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code> route is reachable.</li>



<li>It always returns a 200 response.</li>



<li>It returns valid JSON.</li>



<li>The content type is correct.</li>
</ul>



<p>Here is the <strong>real FastAPI code</strong> being tested (<code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code>):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="16">@app.get("/health")
async def health_check():
    logger.info("Health check requested")
    return {"status": "ok"}
</pre>



<p>This alignment is exactly correct.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-predict-Endpoint-MLOps-API"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-predict-Endpoint-MLOps-API">Testing the /predict Endpoint in an MLOps API</a></h3>



<p>Your integration tests call the prediction endpoint:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="17">class TestPredictEndpoint:

    def test_predict_endpoint(self, client):
        response = client.post("/predict", params={"input": "good movie"})
        assert response.status_code == 200
        assert "prediction" in response.json()
   
    def test_predict_positive(self, client):
        response = client.post("/predict", params={"input": "This is a great movie!"})
        assert response.status_code == 200
        assert response.json()["prediction"] == "positive"
   
    def test_predict_negative(self, client):
        response = client.post("/predict", params={"input": "This is bad"})
        assert response.status_code == 200
        assert response.json()["prediction"] == "negative"
</pre>



<p><strong>This tests:</strong></p>



<ul class="wp-block-list">
<li>The endpoint exists and accepts POST requests.</li>



<li>The parameter is correctly passed using <code data-enlighter-language="python" class="EnlighterJSRAW">params={"input": ...}</code>.</li>



<li>The internal inference logic (service → model) behaves correctly end-to-end.</li>
</ul>



<p>Here is the <strong>actual API endpoint</strong> in your <code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="18">@app.post("/predict")
async def predict_route(input: str):
    return {"prediction": predict_service(input)}
</pre>



<p>Perfect 1:1 match.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-Documentation-Endpoints-docs-openapi-json"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-Documentation-Endpoints-docs-openapi-json">Testing Documentation Endpoints (/docs, /openapi.json)</a></h3>



<p>These are built into FastAPI and must exist for production ML systems.</p>



<p>Your tests:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="19">class TestAPIDocumentation:
    def test_openapi_schema_accessible(self, client):
        response = client.get("/openapi.json")

        assert response.status_code == 200
        schema = response.json()
        assert "openapi" in schema
        assert "info" in schema
   
    def test_swagger_ui_accessible(self, client):
        response = client.get("/docs")

        assert response.status_code == 200
        assert "text/html" in response.headers["content-type"]
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-This-Ensures"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-This-Ensures">What This Ensures</a></h3>



<ul class="wp-block-list">
<li>The OpenAPI schema is generated.</li>



<li>Swagger UI loads successfully.</li>



<li>No misconfiguration broke the docs.</li>



<li>Consumers (frontend teams, other ML services, monitoring) can introspect your API.</li>
</ul>



<p>This is standard for production ML systems.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-Error-Handling-FastAPI-APIs-Pytest"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-Error-Handling-FastAPI-APIs-Pytest">Testing Error Handling in FastAPI APIs with Pytest</a></h3>



<p>Your code includes error tests that verify robustness:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="20">class TestErrorHandling:
    def test_nonexistent_endpoint_returns_404(self, client):
        response = client.get("/nonexistent")
        assert response.status_code == 404
   
    def test_invalid_method_on_health_endpoint(self, client):
        response = client.post("/health")
        assert response.status_code == 405  # Method Not Allowed
   
    def test_malformed_requests_handled_gracefully(self, client):
        response = client.get("/health")
        assert response.status_code == 200
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Integration-Test-Breakdown-What-Each-Test-Validates"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Integration-Test-Breakdown-What-Each-Test-Validates">Integration Test Breakdown: What Each Test Validates</a></h3>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-11.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1018" height="236" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53487" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?size=126x29&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11-300x70.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?size=378x88&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?size=504x117&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?size=630x146&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11-768x178.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-11.png?lossy=2&amp;strip=1&amp;webp=1 1018w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Key API edge case tests and their importance in ensuring system reliability</figcaption></figure></div>


<p>These tests ensure your service behaves consistently even when clients behave incorrectly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Run-Integration-Tests-Pytest-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Run-Integration-Tests-Pytest-MLOps">How to Run Integration Tests with Pytest in MLOps</a></h3>



<p>To run only the integration tests:</p>



<h4 class="wp-block-heading">Using pytest directly</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="21">pytest tests/integration/ -v
</pre>



<h4 class="wp-block-heading">With Poetry</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="22">poetry run pytest tests/integration/ -v
</pre>



<h4 class="wp-block-heading">With Makefile</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="23">make test-integration
</pre>



<p>You will see output like:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="24">tests/integration/test_api_routes.py::TestHealthEndpoint::test_health_check_returns_ok PASSED
tests/integration/test_api_routes.py::TestPredictEndpoint::test_predict_positive PASSED
tests/integration/test_api_routes.py::TestAPIDocumentation::test_swagger_ui_accessible PASSED
...
</pre>



<p>Green = your API works correctly end-to-end.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Performance-Load-Testing-Locust"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Performance-Load-Testing-Locust">Performance and Load Testing with Locust</a></h2>



<p>Performance testing is critical for ML systems because even a lightweight model can become slow, unstable, or unresponsive when many users hit the API at once. With Locust, you can simulate hundreds or thousands of concurrent users calling your ML inference endpoints and measure how your API behaves under pressure.</p>



<p>This section explains why load testing matters, how Locust works, how your actual test file is structured, and how to interpret its results.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Load-Testing-Essential-MLOps-ML-APIs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Load-Testing-Essential-MLOps-ML-APIs">Why Load Testing Is Essential for MLOps and ML APIs</a></h3>



<p>ML inference services have unique scaling behaviors:</p>



<ul class="wp-block-list">
<li><strong>Model loading</strong> requires significant memory.</li>



<li><strong>Inference latency</strong> grows non-linearly under load.</li>



<li><strong>CPU/GPU bottlenecks</strong> show up only when multiple users hit the system.</li>



<li><strong>Thread starvation</strong> can cause cascading failures.</li>



<li><strong>Autoscaling decisions</strong> depend on real-world load patterns.</li>
</ul>



<p>A service that performs well for one user may fail miserably at 50 users.</p>



<p>Load testing ensures:</p>



<ul class="wp-block-list">
<li>The API stays <strong>responsive</strong> under traffic.</li>



<li>Latency stays under acceptable thresholds.</li>



<li>No unexpected <strong>failures</strong> or timeouts occur.</li>



<li>You understand the system’s <strong>scaling limits</strong> before going to production.</li>
</ul>



<p>Locust is perfect for this because it is lightweight, Python-based, and designed for web APIs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Locust-Load-Testing-Concepts-Users-Spawn-Rate-Tasks-Explained"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Locust-Load-Testing-Concepts-Users-Spawn-Rate-Tasks-Explained">Locust Load Testing Concepts: Users, Spawn Rate, and Tasks Explained</a></h3>



<p>Locust simulates user behavior using simple Python classes.</p>



<h4 class="wp-block-heading">Users</h4>



<p>A “user” is an independent client that continuously makes requests to your API.</p>



<p>Example:</p>



<ul class="wp-block-list">
<li>10 users = 10 active clients repeatedly calling <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code>.</li>
</ul>



<h4 class="wp-block-heading">Spawn rate</h4>



<p>How quickly Locust ramps up users.</p>



<p>Example:</p>



<ul class="wp-block-list">
<li>spawn rate 2 = add 2 users per second until target is reached.</li>
</ul>



<p>This helps simulate realistic traffic spikes instead of instantly launching all users.</p>



<h4 class="wp-block-heading">Tasks</h4>



<p>Each simulated user executes a set of tasks (e.g., repeatedly calling the <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> endpoint).</p>



<p>Every task can have a weight:</p>



<ul class="wp-block-list">
<li>Higher weight = more frequent calls.</li>
</ul>



<p>This lets you mimic real user patterns like:</p>



<ul class="wp-block-list">
<li>90% predict calls</li>



<li>10% health checks</li>
</ul>



<p>Your project does exactly this.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Writing-locustfile-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Writing-locustfile-py">Writing the locustfile.py</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="25">from locust import HttpUser, task, between

class MLAPIUser(HttpUser):
    """
    Locust user class for testing the ML API.
   
    Simulates a user making requests to the API endpoints.
    """
   
    # Wait between 1 and 3 seconds between requests
    wait_time = between(1, 3)
   
    @task(10)
    def test_predict(self):
        """
        Test the predict endpoint.
       
        This task has weight 10, making it the most frequently called.
        """
        payload = {"input": "The movie was good"}
        with self.client.post("/predict", params=payload, catch_response=True) as response:
            if response.status_code == 200:
                response_data = response.json()
                if "prediction" in response_data:
                    response.success()
                else:
                    response.failure(f"Missing prediction in response: {response_data}")
            else:
                response.failure(f"HTTP {response.status_code}")
   
    def on_start(self):
        """
        Called when a user starts testing.
       
        Used for setup tasks like authentication.
        """
        # Verify the API is reachable
        response = self.client.get("/health")
        if response.status_code != 200:
            print(f"Warning: API health check failed with status {response.status_code}")
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-This-Locust-Load-Test-Validates-MLOps-API"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-This-Locust-Load-Test-Validates-MLOps-API">What This Locust Load Test Validates in an MLOps API</a></h3>



<ul class="wp-block-list">
<li>Creates a simulated user (<code data-enlighter-language="python" class="EnlighterJSRAW">MLAPIUser</code>) that calls <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code>.</li>



<li>Gives the <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> task a <strong>weight of 10</strong>, making it the dominant request.</li>



<li>Sends realistic input (&#8220;The movie was good&#8221;).</li>



<li>Validates:
<ul class="wp-block-list">
<li>Response code is 200.</li>



<li>JSON contains &#8220;prediction&#8221;.</li>
</ul>
</li>



<li>Marks failures explicitly for clean reporting.</li>



<li>On startup, each user verifies that <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code> works.</li>
</ul>



<p>This matches your API perfectly:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> is POST with query parameter <code data-enlighter-language="python" class="EnlighterJSRAW">input=...</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">/health</code> is GET and returns status OK</li>
</ul>



<p>Nothing needs to be changed; this is production-quality.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-Locust-Headless-Mode-vs-Web-UI-Dashboard"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-Locust-Headless-Mode-vs-Web-UI-Dashboard">Running Locust: Headless Mode vs Web UI Dashboard</a></h3>



<p>Locust supports <strong>two modes</strong>.</p>



<h4 class="wp-block-heading">A. Web UI Mode (Interactive Dashboard)</h4>



<p>Launch Locust:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="26">locust -f tests/performance/locustfile.py --host=http://localhost:8000
</pre>



<p>Then open:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="27">http://localhost:8089
</pre>



<p>You will see a dashboard where you can:</p>



<ul class="wp-block-list">
<li>Set number of users</li>



<li>Set spawn rate</li>



<li>Start/stop tests</li>



<li>View real-time stats</li>
</ul>



<h4 class="wp-block-heading">B. Headless Mode (Automated CI/CD or scripting)</h4>



<p>You already have a script:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="28">software-engineering-mlops-lesson2/scripts/run_locust.sh
</pre>



<p>Run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="29">./scripts/run_locust.sh http://localhost:8000 10 2 5m
</pre>



<p>This executes:</p>



<ul class="wp-block-list">
<li>10 users</li>



<li>spawn rate 2 users per second</li>



<li>run time 5 minutes</li>



<li>save HTML report</li>
</ul>



<p>No UI; perfect for pipelines.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Generating-Locust-Load-Testing-Reports-ML-APIs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Generating-Locust-Load-Testing-Reports-ML-APIs">Generating Locust Load Testing Reports for ML APIs</a></h3>



<p>Your script uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="30">--html="reports/locust_reports/locust_report_&lt;timestamp>.html"
</pre>



<p>Which produces files like:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="31">reports/locust_reports/locust_report_20251030_031331.html
</pre>



<p>Each report includes:</p>



<ul class="wp-block-list">
<li>Requests per second (RPS)</li>



<li>Failure stats</li>



<li>Full latency distribution</li>



<li>Percentiles (50th, 95th, 99th)</li>



<li>Charts of active users and response times</li>
</ul>



<p>These HTML reports are great for:</p>



<ul class="wp-block-list">
<li>Comparing deployments</li>



<li>Regression testing API performance</li>



<li>Flagging slow model versions</li>



<li>Archiving performance history</li>
</ul>



<p>Everything is already correctly set up in your repo.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-Test-Metrics-RPS-failures-latency-P95-P99"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-Test-Metrics-RPS-failures-latency-P95-P99">Understanding Test Metrics (RPS, failures, latency, P95/P99)</a></h3>



<p>Locust gives several performance metrics you must understand for ML systems.</p>



<h4 class="wp-block-heading">Requests per Second (RPS)</h4>



<p>How many inference calls your API can handle per second.</p>



<ul class="wp-block-list">
<li>CPU-bound models lead to low RPS</li>



<li>Simple models lead to high RPS</li>
</ul>



<p>Increasing users will show where your model and server saturates.</p>



<h4 class="wp-block-heading">Failures</h4>



<p>Locust marks a request as failed when:</p>



<ul class="wp-block-list">
<li>Status code ≠ 200</li>



<li>Response JSON does not contain <code data-enlighter-language="python" class="EnlighterJSRAW">"prediction"</code></li>



<li>Timeout occurs</li>



<li>Server returns an internal error</li>
</ul>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">catch_response=True</code> logic handles this explicitly.</p>



<p>This prevents “hidden” failures.</p>



<h4 class="wp-block-heading">Latency (ms)</h4>



<p>Response time per request, typically measured in milliseconds.</p>



<p>For ML, latency is the most important metric.</p>



<p>You will see:</p>



<ul class="wp-block-list">
<li><strong>Average latency</strong></li>



<li><strong>Median (P50)</strong></li>



<li><strong>Slowest (max latency)</strong></li>
</ul>



<h4 class="wp-block-heading">P95 / P99 (Tail Latency)</h4>



<p>The 95th and 99th percentile response times.</p>



<p>These capture <strong>worst-case</strong> behavior.</p>



<p>Example:</p>



<ul class="wp-block-list">
<li>P50 = 40 ms</li>



<li>P95 = 210 ms</li>



<li>P99 = 540 ms</li>
</ul>



<p>This means:</p>



<p>Most users see fast responses, but a small % experience major slowdowns.</p>



<p>This is common in ML workloads due to:</p>



<ul class="wp-block-list">
<li>Model warmup</li>



<li>Thread contention</li>



<li>Python GIL blockage</li>



<li>Model cache misses</li>
</ul>



<p>Production Service Level Objectives (SLOs) usually track P95 and P99, not averages.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-MLOps-Test-Configuration-YAML-Environment-Variables"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-MLOps-Test-Configuration-YAML-Environment-Variables">MLOps Test Configuration: YAML and Environment Variables</a></h2>



<p>ML systems behave differently across production, development, and testing environments.</p>



<p>Your Lesson 2 codebase separates these environments cleanly using:</p>



<ul class="wp-block-list">
<li>A <strong>test-specific YAML config</strong></li>



<li>A <strong>modified BaseSettings loader</strong></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> overrides for test mode</li>
</ul>



<p>This ensures that tests run quickly, deterministically, and without polluting real environment settings.</p>



<p>Let’s break down how this works.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-test-config-yaml-MLOps-Testing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-test-config-yaml-MLOps-Testing">Understanding test_config.yaml for MLOps Testing</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="32"># Test Configuration
environment: "test"
log_level: "DEBUG"

# API Configuration
api_host: "127.0.0.1"
api_port: 8000
debug: true

# Performance Testing
performance:
  baseline_users: 10
  spawn_rate: 2
  test_duration: "5m"

# Model Configuration
model:
  name: "dummy_classifier"
  version: "1.0.0"
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-test-config-yaml-Controls-MLOps-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-test-config-yaml-Controls-MLOps-Pipelines">What test_config.yaml Controls in MLOps Pipelines</a></h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-12.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="399" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12-1024x399.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53490" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12.png?size=126x49&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12-300x117.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12.png?size=378x147&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12.png?size=504x196&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12.png?size=630x245&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12-768x299.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12-1024x399.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-12.png?lossy=2&amp;strip=1&amp;webp=1 1039w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 2:</strong> Configuration keys and their roles in test environment setup</figcaption></figure></div>


<p>This config prevents tests from accidentally picking up production configs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Overriding-Application-Configuration-Test-Mode"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Overriding-Application-Configuration-Test-Mode">Overriding Application Configuration in Test Mode</a></h3>



<p>Your test environment uses a special configuration loader inside:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="33">core/config.py
</pre>



<p>Here is the real code:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="34">def load_config() -> Settings:
    # Load base settings from environment
    settings = Settings()
   
    # Load additional configuration from YAML if it exists
    config_path = "configs/test_config.yaml"
    if os.path.exists(config_path):
        yaml_config = load_yaml_config(config_path)
       
        # Override settings with YAML values if they exist
        for key, value in yaml_config.items():
            if hasattr(settings, key):
                setattr(settings, key, value)
   
    return settings
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Configuration-Overrides-Work-YAML-Environment-Variables"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Configuration-Overrides-Work-YAML-Environment-Variables">How Configuration Overrides Work: YAML and Environment Variables</a></h3>



<ul class="wp-block-list">
<li><strong>Step 1</strong><strong>:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">BaseSettings</code><strong> loads environment variables<br></strong>(<code data-enlighter-language="python" class="EnlighterJSRAW">.env</code>, operating system (OS) variables, defaults)</li>



<li><strong>Step 2</strong><strong>:</strong><strong> YAML configuration overrides them<br></strong><code data-enlighter-language="python" class="EnlighterJSRAW">test_config.yaml</code> <em>replaces any matching fields</em> in <code data-enlighter-language="python" class="EnlighterJSRAW">Settings</code>.</li>



<li><strong>Final output:<br></strong>The application is now in <strong>test mode</strong>, completely isolated from development and production environments.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Configuration-Management-Matters-MLOps-Testing"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Configuration-Management-Matters-MLOps-Testing">Why Configuration Management Matters in MLOps Testing</a></h3>



<ul class="wp-block-list">
<li>Integration tests always use the same port, host, and log settings.</li>



<li>Tests are <strong>repeatable</strong> and <strong>deterministic</strong>.</li>



<li>You never accidentally load production API keys or endpoints.</li>



<li>CI/CD pipelines get consistent behavior.</li>
</ul>



<p>This pattern is very common in real-world MLOps systems.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-Environment-Variables-Test-Isolation"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-Environment-Variables-Test-Isolation">Using Environment Variables for Test Isolation</a></h3>



<p>Your test environment uses a <code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code> file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="35"># API Configuration
API_PORT=8000
API_HOST=0.0.0.0
DEBUG=true

# Environment
ENVIRONMENT=test

# Logging
LOG_LEVEL=DEBUG
</pre>



<p>During setup, users run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="36">cp .env.example .env
</pre>



<p>This creates the <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> used during tests.</p>



<h4 class="wp-block-heading">Why Test-Specific .env Variables Matter</h4>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-13.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="308" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13-1024x308.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53491" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13.png?size=126x38&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13-300x90.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13.png?size=378x114&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13.png?size=504x152&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13.png?size=630x189&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13-768x231.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13-1024x308.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-13.png?lossy=2&amp;strip=1&amp;webp=1 1035w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 3:</strong> Environment variables and their impact on test execution</figcaption></figure></div>


<h4 class="wp-block-heading">Combined with YAML Overrides</h4>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> → applies defaults</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">test_config.yaml</code> → overrides final values</p>



<p>This gives you a flexible and safe configuration stack.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Code-Quality-MLOps-Linting-Formatting-Static-Analysis-Tools"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Code-Quality-MLOps-Linting-Formatting-Static-Analysis-Tools">Code Quality in MLOps: Linting, Formatting, and Static Analysis Tools</a></h2>



<p>Testing ensures correctness, but <strong>code quality tools</strong> ensure that your ML system remains maintainable as it grows.</p>



<p>In Lesson 2, you introduce a full suite of professional-quality tooling:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">flake8</code> for linting</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Black</code> for auto-formatting</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">isort</code> for import ordering</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">MyPy</code> for static typing</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Makefile</code> for automation consistency</li>
</ul>



<p>Together, they enforce the same engineering discipline used on real production ML teams at scale.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Linting-Python-Code-flake8"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Linting-Python-Code-flake8">Linting Python Code with flake8</a></h3>



<p>Linting catches code smells, stylistic issues, and subtle bugs before they hit production.</p>



<p>Your repository includes a real <code data-enlighter-language="python" class="EnlighterJSRAW">.flake8</code> file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="37">[flake8]
max-line-length = 88
extend-ignore = E203, W503
exclude =
    .git,
    __pycache__,
    .venv,
    venv,
    env,
    build,
    dist,
    *.egg-info,
    .pytest_cache,
    .mypy_cache
per-file-ignores =
    __init__.py:F401
max-complexity = 10
</pre>



<h4 class="wp-block-heading">What your flake8 setup enforces</h4>



<ul class="wp-block-list">
<li><strong>88-character line limit</strong> (matches Black)</li>



<li>Ignores stylistic warnings that Black also overrides (E203, W503)</li>



<li>Avoids checking generated or virtual-env directories</li>



<li>Allows unused imports only in <code data-enlighter-language="python" class="EnlighterJSRAW">__init__.py</code> files</li>



<li>Enforces a <strong>maximum complexity score of 10</strong></li>
</ul>



<h4 class="wp-block-heading">Run flake8 manually</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="38">poetry run flake8 .
</pre>



<h4 class="wp-block-heading">Or via Makefile</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="39">make lint
</pre>



<p>Linting becomes part of your day-to-day workflow and prevents style drift across your ML services.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Formatting-Python-Code-Black-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Formatting-Python-Code-Black-Pipelines">Formatting Python Code with Black Pipelines</a></h3>



<p>Black is an automatic code formatter; it rewrites Python code into a consistent style.</p>



<p>Your Lesson 2 <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code> includes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="40">[tool.black]
line-length = 88
target-version = ['py39']
include = '\.pyi?$'
</pre>



<p>This means:</p>



<ul class="wp-block-list">
<li>All Python files (<code data-enlighter-language="python" class="EnlighterJSRAW">.py</code>) are formatted.</li>



<li>Max line length is 88 chars.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">py39</code> syntax is allowed.</li>
</ul>



<h4 class="wp-block-heading">Format all code:</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="41">poetry run black .
</pre>



<p>Or using the Makefile shortcut:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="42">make format
</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Black</code> removes tedious decisions about spacing, commas, and line breaks, ensuring all contributors share the same style.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-isort-Manage-Python-Imports"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-isort-Manage-Python-Imports">Using isort to Manage Python Imports</a></h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">isort</code> automatically manages import sorting and grouping.</p>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code> contains:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="43">[tool.isort]
profile = "black"
multi_line_output = 3
</pre>



<p>This aligns <code data-enlighter-language="python" class="EnlighterJSRAW">isort</code>’s output with <code data-enlighter-language="python" class="EnlighterJSRAW">Black</code>’s formatting rules, avoiding conflicts.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Run-isort-Clean-Python-Imports"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Run-isort-Clean-Python-Imports">How to Run isort for Clean Python Imports</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="44">poetry run isort .
</pre>



<p>Or via Makefile:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="45">make format
</pre>



<p><strong>Why This Matters</strong></p>



<p>As ML services grow, import lists become messy. <code data-enlighter-language="python" class="EnlighterJSRAW">isort</code> keeps them clean and consistent, improving readability exponentially.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Static-Type-Checking-MyPy-MLOps-Codebases"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Static-Type-Checking-MyPy-MLOps-Codebases">Static Type Checking with MyPy for MLOps Codebases</a></h3>



<p>Static typing is increasingly important in MLOps systems, especially when passing models, configs, and data structures between services.</p>



<p>Your repo contains a full <code data-enlighter-language="python" class="EnlighterJSRAW">mypy.ini</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="46">[mypy]
python_version = 3.9
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = False
ignore_missing_imports = True

[mypy-tests.*]
disallow_untyped_defs = False

[mypy-locust.*]
ignore_missing_imports = True
</pre>



<h4 class="wp-block-heading">What This Config Enforces</h4>



<ul class="wp-block-list">
<li>Flags functions that return Any</li>



<li>Warns about unused config options</li>



<li>Does <em>not</em> require type hints everywhere (reasonable for ML codebases)</li>



<li>Skips type-checking external packages (common in ML pipelines)</li>



<li>Allows untyped defs in tests</li>
</ul>



<h4 class="wp-block-heading">Run MyPy</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="47">poetry run mypy .
</pre>



<p>Or via Makefile:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="48">make type-check
</pre>



<h4 class="wp-block-heading">Why MyPy Is Critical in ML Systems</h4>



<ul class="wp-block-list">
<li>Prevents silent type errors (e.g., passing a list where a tensor is expected)</li>



<li>Catches config mistakes before runtime</li>



<li>Improves refactor safety for large ML codebases</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-Makefile-Automate-MLOps-Testing-Code-Quality"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-Makefile-Automate-MLOps-Testing-Code-Quality">Using a Makefile to Automate MLOps Testing and Code Quality</a></h3>



<p>Your Makefile automates all key development tasks:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="49">make test          # Run all tests
make test-unit     # Unit tests only
make test-integration
make format        # Black + isort
make lint          # flake8
make type-check    # mypy
make load-test     # Locust performance tests
make clean         # Reset environment
</pre>



<p>This ensures:</p>



<ul class="wp-block-list">
<li>Every developer uses the <strong>same commands</strong></li>



<li>CI/CD pipelines can call the same interface</li>



<li>Tooling stays consistent across machines</li>
</ul>



<p><strong>Example workflow for contributors:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="50">make format
make lint
make type-check
make test
</pre>



<p>If all commands pass, you know your code is clean, consistent, and ready for production.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Automating-Testing-Pytest-Test-Runner-Script"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Automating-Testing-Pytest-Test-Runner-Script">Automating Testing with a Pytest Test Runner Script</a></h2>



<p>As your ML system grows, running dozens of unit, integration, and performance tests manually becomes tedious and error-prone.</p>



<p>Lesson 2 includes a fully automated test runner (<code data-enlighter-language="python" class="EnlighterJSRAW">scripts/run_tests.sh</code>) that enforces a predictable, repeatable workflow for your entire test suite.</p>



<p>This script acts like a miniature CI pipeline that you can run locally. It prints structured logs, enforces failure conditions, and ensures that no test is accidentally skipped.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-Automated-Tests-run-tests-sh"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-Automated-Tests-run-tests-sh">Running Automated Tests with run_tests.sh</a></h3>



<p>Your repository includes a fully functional test runner:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="51">#!/bin/bash

# Test Runner Script for MLOps Lesson 2

set -e

echo "🧪 Running MLOps Lesson 2 Tests..."

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

print_status() {
    echo -e "${GREEN}✅ $1${NC}"
}

print_warning() {
    echo -e "${YELLOW}⚠️  $1${NC}"
}

print_error() {
    echo -e "${RED}❌ $1${NC}"
}

# Run unit tests
echo ""
echo "📝 Running unit tests..."
poetry run pytest tests/unit/ -v
if [ $? -eq 0 ]; then
    print_status "Unit tests passed"
else
    print_error "Unit tests failed"
    exit 1
fi

# Run integration tests
echo ""
echo "🔗 Running integration tests..."
poetry run pytest tests/integration/ -v
if [ $? -eq 0 ]; then
    print_status "Integration tests passed"
else
    print_error "Integration tests failed"
    exit 1
fi

echo ""
print_status "All tests completed successfully!"
</pre>



<h4 class="wp-block-heading">How to Run It</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="52">./scripts/run_tests.sh
</pre>



<p>or, via Makefile:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="53">make test
</pre>



<h4 class="wp-block-heading">What It Does</h4>



<ul class="wp-block-list">
<li>Runs <em>unit tests</em></li>



<li>Runs <em>integration tests</em></li>



<li>Stops immediately (set <code data-enlighter-language="python" class="EnlighterJSRAW">-e</code>) if anything fails</li>



<li>Prints colored output for clarity</li>



<li>Provides a clear pass/fail summary</li>
</ul>



<p>This mirrors real CI pipelines where a failing test stops deployment.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-Pytest-Output-Test-Results"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-Pytest-Output-Test-Results">Understanding Pytest Output and Test Results</a></h3>



<p>When you run the script, you will typically see output like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="54">🧪 Running MLOps Lesson 2 Tests...

📝 Running unit tests...
============================= test session starts ==============================
collected 7 items

tests/unit/test_inference_service.py::TestInferenceService::test_predict_returns_string PASSED
tests/unit/test_inference_service.py::TestInferenceService::test_predict_positive_input PASSED
tests/unit/test_inference_service.py::TestInferenceService::test_predict_negative_input PASSED
tests/unit/test_inference_service.py::TestDummyModel::test_model_initialization PASSED
tests/unit/test_inference_service.py::TestDummyModel::test_predict_with_good_word PASSED
tests/unit/test_inference_service.py::TestDummyModel::test_predict_with_great_word PASSED
tests/unit/test_inference_service.py::TestDummyModel::test_predict_without_keywords PASSED

============================== 7 passed in 0.45s ===============================
✅ Unit tests passed
</pre>



<p>Then integration tests:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="55">🔗 Running integration tests...

tests/integration/test_api_routes.py::TestHealthEndpoint::test_health_check_returns_ok PASSED
tests/integration/test_api_routes.py::TestPredictEndpoint::test_predict_positive PASSED
tests/integration/test_api_routes.py::TestAPIDocumentation::test_swagger_ui_accessible PASSED
tests/integration/test_api_routes.py::TestErrorHandling::test_nonexistent_endpoint_returns_404 PASSED

============================== 8 passed in 0.78s ===============================
✅ Integration tests passed
</pre>



<p>Finally:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="56">✅ All tests completed successfully!
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Automated-Testing-Workflows-Matter-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Automated-Testing-Workflows-Matter-MLOps">Why Automated Testing Workflows Matter in MLOps</a></h3>



<ul class="wp-block-list">
<li>You see exactly which tests failed.</li>



<li>You immediately know whether the API is healthy.</li>



<li>You build the habit of treating tests as a gatekeeper before shipping ML code.</li>
</ul>



<p>This is foundational MLOps workflow discipline.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Integrating-Pytest-CI-CD-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Integrating-Pytest-CI-CD-Pipelines">Integrating Pytest into CI/CD Pipelines</a></h3>



<p>Your test runner is already written <em>as if it were part of CI</em>.</p>



<p>Very soon, you will plug this into:</p>



<ul class="wp-block-list">
<li><strong>GitHub Actions</strong></li>



<li><strong>GitLab CI</strong></li>



<li><strong>CircleCI</strong></li>



<li><strong>AWS CodeBuild</strong></li>



<li><strong>Azure DevOps</strong></li>
</ul>



<p>A typical GitHub Actions step would look like:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="57">- name: Run Tests
  run: ./scripts/run_tests.sh
</pre>



<p>Since your script exits with non-zero status on failures, the CI job fails automatically.</p>



<p><strong>What this enables in production ML workflows:</strong></p>



<ul class="wp-block-list">
<li>No pull request gets merged unless tests pass</li>



<li>Deployments are blocked if integration tests fail</li>



<li>Load testing can be added as a gated step</li>



<li>Test failures provide early feedback on regressions</li>



<li>Teams enforce consistent standards across developers</li>
</ul>



<p><strong>You already have everything CI needs:</strong></p>



<ul class="wp-block-list">
<li>A deterministic test runner</li>



<li>A strict exit-on-fail system</li>



<li>Separate unit and integration test layers</li>



<li>Makefile wrappers for automation</li>



<li>Poetry ensuring repeatable environments</li>
</ul>



<p>Once you introduce CI/CD in later lessons, these scripts plug in seamlessly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Automating-Load-Testing-MLOps-Locust-Scripts"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Automating-Load-Testing-MLOps-Locust-Scripts">Automating Load Testing in MLOps with Locust Scripts</a></h2>



<p>Performance testing becomes essential once an ML API starts supporting real traffic. You want confidence that your inference service will not collapse under load, that p95/p99 latencies remain acceptable, and that the system behaves predictably when scaling horizontally.</p>



<p>Manually running Locust is fine for experimentation, but production MLOps requires <strong>automated, repeatable load tests</strong>. Lesson 2 provides a dedicated script (<code data-enlighter-language="python" class="EnlighterJSRAW">run_locust.sh</code>) which allows you to run performance tests in a single line and automatically generate HTML reports for analysis.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-Automated-Locust-Load-Tests-run-locust-sh"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-Automated-Locust-Load-Tests-run-locust-sh">Running Automated Locust Load Tests with run_locust.sh</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="58">#!/bin/bash

# Simple Locust Load Testing Script for MLOps Lesson 2

set -e

echo "🚀 Starting Locust Load Testing..."

# Configuration
HOST=${1:-"http://localhost:8000"}
USERS=${2:-10}
SPAWN_RATE=${3:-2}
RUN_TIME=${4:-"5m"}

echo "🔧 Configuration: $USERS users, spawn rate $SPAWN_RATE, run time $RUN_TIME"

# Create reports directory
mkdir -p reports/locust_reports

# Check if the API is running
echo "🏥 Checking if API is running..."
if ! curl -s "$HOST/health" > /dev/null; then
    echo "❌ API is not reachable at $HOST"
    echo "Please start the API server first with: python main.py"
    exit 1
fi

echo "✅ API is reachable"

# Run Locust load test
echo "🧪 Starting load test..."

TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
HTML_REPORT="reports/locust_reports/locust_report_$TIMESTAMP.html"

poetry run locust \
    -f tests/performance/locustfile.py \
    --host="$HOST" \
    --users="$USERS" \
    --spawn-rate="$SPAWN_RATE" \
    --run-time="$RUN_TIME" \
    --html="$HTML_REPORT" \
    --headless

echo "✅ Load test completed!"
echo "📊 Report: $HTML_REPORT"
</pre>



<h4 class="wp-block-heading">How to Run It</h4>



<p>Basic load test:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="59">./scripts/run_locust.sh
</pre>



<p>10 users, spawn rate 2 users/sec, run for 5 minutes.</p>



<p>Custom parameters:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="60">./scripts/run_locust.sh http://localhost:8000 30 5 2m
</pre>



<p>This means:</p>



<ul class="wp-block-list">
<li><strong>30 users</strong> total</li>



<li><strong>5 users per second spawn rate</strong></li>



<li><strong>2-minute runtime</strong></li>



<li>Tests <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> endpoint repeatedly (because of <code data-enlighter-language="python" class="EnlighterJSRAW">locustfile.py</code>)</li>
</ul>



<h4 class="wp-block-heading">What This Script Automates</h4>



<ul class="wp-block-list">
<li>API health check before running</li>



<li>Creates timestamped report directories</li>



<li>Runs Locust in headless mode</li>



<li>Stores HTML reports for analysis</li>



<li>Fails gracefully when API is unreachable</li>
</ul>



<p>This gives you a <em>push-button reproducible performance test</em>, a key requirement in professional MLOps.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Automatically-Generating-Load-Testing-Reports-ML-APIs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Automatically-Generating-Load-Testing-Reports-ML-APIs">Automatically Generating Load Testing Reports for ML APIs</a></h3>



<p>Every run creates a unique HTML report:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="61">reports/locust_reports/
    locust_report_20251203_031331.html
    locust_report_20251203_041215.html
    ...
</pre>



<p>This file includes:</p>



<ul class="wp-block-list">
<li>Requests per second (RPS)</li>



<li>Response time percentiles (<code data-enlighter-language="python" class="EnlighterJSRAW">p50</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">p90</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">p95</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">p99</code>)</li>



<li>Failure rates</li>



<li>Total requests</li>



<li>Charts for concurrency vs performance</li>



<li>Per-endpoint performance metrics</li>
</ul>



<p>You can open the report in your browser:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="62">open reports/locust_reports/locust_report_20251203_031331.html
</pre>



<p>(Windows)</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="63">start reports\locust_reports\locust_report_XXXX.html
</pre>



<p><strong>Why This Is Important</strong></p>



<p>Performance regressions are one of the most common ML service failures:</p>



<ul class="wp-block-list">
<li>model upgrades slow down inference unintentionally</li>



<li>logging overhead increases latency</li>



<li>new preprocessing increases CPU usage</li>



<li>hardware changes alter throughput</li>
</ul>



<p><strong>By keeping each test run stored, you can compare historical performance.</strong></p>



<p>This is the foundation of automatic performance regression detection.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Preparing-Load-Testing-CI-CD-Cloud-MLOps-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Preparing-Load-Testing-CI-CD-Cloud-MLOps-Pipelines">Preparing Load Testing for CI/CD and Cloud MLOps Pipelines</a></h3>



<p>Your load testing script is already CI-ready.</p>



<p>Here is how it fits into a production MLOps pipeline.</p>



<h4 class="wp-block-heading">Option 1 — GitHub Actions</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="64">- name: Run Load Tests
  run: ./scripts/run_locust.sh http://localhost:8000 20 5 1m
</pre>



<p>Since the script exits non-zero on error, it becomes a gated step:</p>



<ul class="wp-block-list">
<li>Deployment is blocked if the API cannot sustain the expected load.</li>



<li>Only performant builds reach production.</li>
</ul>



<h4 class="wp-block-heading">Option 2 — Nightly Performance Jobs</h4>



<p>Teams often run Locust nightly to catch degradations early:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">baseline</code>: 20 users</li>



<li>alert if <code data-enlighter-language="python" class="EnlighterJSRAW">p95</code> &gt; 300 ms</li>



<li>alert if failures &gt; 1%</li>
</ul>



<p>Reports are archived automatically via your script.</p>



<h4 class="wp-block-heading">Option 3 — Cloud Load Testing (AWS/GCP/Azure)</h4>



<p>Your script can run inside:</p>



<ul class="wp-block-list">
<li>AWS CodeBuild</li>



<li>Azure Pipelines</li>



<li>Google CloudBuild</li>
</ul>



<p>Simply modify the host:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="65">./scripts/run_locust.sh https://staging.mycompany.com/api 50 10 10m
</pre>



<h4 class="wp-block-heading">Why CI Load Tests Matter</h4>



<ul class="wp-block-list">
<li>Prevents slow releases from being deployed</li>



<li>Ensures model swaps do not tank performance</li>



<li>Protects SLAs (Service Level Agreements)</li>



<li>Helps capacity planning and autoscaling decisions</li>



<li>Detects bottlenecks before customers do</li>
</ul>



<p>Your repository already contains everything needed to industrialize performance testing.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Test-Coverage-MLOps-Measuring-Improving-Code-Coverage"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Test-Coverage-MLOps-Measuring-Improving-Code-Coverage">Test Coverage in MLOps: Measuring and Improving Code Coverage</a></h2>



<p>Even with strong unit, integration, and performance testing, you still need a way to quantify how much of your codebase is actually exercised. This is where <strong>test coverage</strong> comes in. Coverage tools show you which lines are tested, which are skipped, and where hidden bugs may still be lurking. This is especially important in ML systems, where subtle code paths (error handling, preprocessing, retry logic) can easily be missed.</p>



<p>Your Lesson 2 environment includes <code data-enlighter-language="python" class="EnlighterJSRAW">pytest-cov</code>, allowing you to generate detailed coverage reports in a single command.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-pytest-cov-Measure-Test-Coverage"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-pytest-cov-Measure-Test-Coverage">Using pytest-cov to Measure Test Coverage</a></h3>



<p>Coverage is enabled simply by adding <code data-enlighter-language="python" class="EnlighterJSRAW">--cov</code> flags to <code data-enlighter-language="python" class="EnlighterJSRAW">pytest</code>.</p>



<p>Basic usage:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="66">pytest --cov=.
</pre>



<p>Your repo’s <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code> installs <code data-enlighter-language="python" class="EnlighterJSRAW">pytest-cov</code> automatically under [<code data-enlighter-language="python" class="EnlighterJSRAW">tool.poetry.group.dev.dependencies</code>], so coverage works out of the box.</p>



<p>A more detailed command:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="67">pytest --cov=. --cov-report=term-missing
</pre>



<p>This reports:</p>



<ul class="wp-block-list">
<li>total coverage percentage</li>



<li>which lines were executed</li>



<li>which lines were missed</li>



<li>hints for improving coverage</li>
</ul>



<p>Example output you might see:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="68">---------- coverage: platform linux, python 3.9 ----------
Name                                Stmts   Miss  Cover
--------------------------------------------------------
services/inference_service.py          22      0   100%
models/dummy_model.py                  16      0   100%
core/config.py                         40      8    80%
core/logger.py                         15      0   100%
tests/unit/test_inference_service.py   28      0   100%
--------------------------------------------------------
TOTAL                                 121      8    93%
</pre>



<p>This gives immediate visibility into which modules need more test attention.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Measure-Code-Coverage-MLOps-Projects"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Measure-Code-Coverage-MLOps-Projects">How to Measure Code Coverage in MLOps Projects</a></h3>



<p>To formally measure coverage for Lesson 2, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="69">pytest -v --cov=. --cov-report=html
</pre>



<p>This generates a full HTML report inside:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="70">htmlcov/index.html
</pre>



<p>Open it in your browser:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="71">open htmlcov/index.html
</pre>



<p>(Windows)</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="72">start htmlcov\index.html
</pre>



<p>The HTML report visualizes:</p>



<ul class="wp-block-list">
<li>executed vs missed lines</li>



<li>branch coverage</li>



<li>per-module summaries</li>



<li>clickable source code with line highlighting</li>
</ul>



<p>This is the gold standard report format used in industry pipelines.</p>



<h4 class="wp-block-heading">Integrating Coverage into Your Workflow</h4>



<p>Your Makefile could easily support it:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="73">make coverage
</pre>



<p>But even without that, <code data-enlighter-language="python" class="EnlighterJSRAW">pytest-cov</code> gives you everything you need to evaluate test completeness.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Increase-Test-Coverage-MLOps-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Increase-Test-Coverage-MLOps-Pipelines">How to Increase Test Coverage in MLOps Pipelines</a></h3>



<p>ML systems often have unusual testing challenges:</p>



<ul class="wp-block-list">
<li>multiple code paths depending on data</li>



<li>dynamic model loading</li>



<li>error cases that only appear in production</li>



<li>preprocessing/postprocessing steps</li>



<li>branching logic based on config values</li>



<li>retry and timeout logic</li>



<li>logging behavior that might hide bugs</li>
</ul>



<p>To increase coverage meaningfully:</p>



<h4 class="wp-block-heading">1. Test failure modes</h4>



<p>Example: model not loaded, invalid input, exceptions in service layer.</p>



<h4 class="wp-block-heading">2. Test alternative branches</h4>



<p>For example., your dummy model has:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="74">if "good" in text or "great" in text:
    return "positive"
return "negative"
</pre>



<p>Coverage increases when you test:</p>



<ul class="wp-block-list">
<li>positive branch</li>



<li>fallback branch</li>



<li>edge cases like empty strings</li>
</ul>



<h4 class="wp-block-heading">3. Test configuration-dependent behavior</h4>



<p>Since your system loads from:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">.env</code></li>



<li>YAML</li>



<li>runtime values</li>
</ul>



<p>Try testing scenarios where each layer overrides the next.</p>



<h4 class="wp-block-heading">4. Test logging paths</h4>



<p>Logging is crucial in MLOps, and ensuring logs appear where expected also contributes to coverage.</p>



<h4 class="wp-block-heading">5. Test the API under different payloads</h4>



<p>Missing parameters, malformed types, unexpected values.</p>



<h4 class="wp-block-heading">6. Test integration between modules</h4>



<p>Even simple ML systems can break across module boundaries, so testing interactions raises coverage dramatically.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Recommended-Test-Coverage-Targets-MLOps-Systems"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Recommended-Test-Coverage-Targets-MLOps-Systems">Recommended Test Coverage Targets for MLOps Systems</a></h3>



<p>High coverage is good, but perfection is unrealistic and unnecessary.</p>



<p>Here are <strong>industry-grade ML-specific targets</strong>:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-14.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="407" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14-1024x407.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53492" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14.png?size=126x50&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14-300x119.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14.png?size=378x150&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14.png?size=504x200&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14.png?size=630x250&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14-768x305.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14-1024x407.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-14.png?lossy=2&amp;strip=1&amp;webp=1 1037w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Table 4:</strong> Recommended test coverage ranges across system components</figcaption></figure></div>


<h4 class="wp-block-heading">Why You Do Not Aim for 100%</h4>



<ul class="wp-block-list">
<li>ML models are often treated as black boxes</li>



<li>Some branches (especially failure conditions) are difficult to simulate</li>



<li>Performance code paths are not always practical to test</li>
</ul>



<p>A strong MLOps system targets:</p>



<p><strong>Overall coverage: 80-90%</strong></p>



<p>This ensures the most important logic is covered while avoiding diminishing returns.</p>



<p><strong>Critical paths: 100%</strong></p>



<p>Inference, preprocessing, conversion, routing, safety checks.</p>



<p><strong>Performance-sensitive code: covered via load tests</strong></p>



<p>This is why Locust complements <code data-enlighter-language="python" class="EnlighterJSRAW">pytest</code> rather than replacing it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you learned how to make ML systems safe, correct, and production-ready through a full testing and validation workflow. You started by understanding why ML services need far more than “just unit tests,” and how a layered approach (unit, integration, and performance tests) creates confidence in both the code and the behavior of the system. You then explored a real test layout with dedicated folders, fixtures, and isolation, and saw how each type of test validates a different piece of the pipeline.</p>



<p>From there, you implemented unit tests for the inference service and dummy model, followed by integration tests that exercise real FastAPI endpoints, documentation routes, and error handling. You also learned how to perform load testing with Locust, simulate concurrent users, generate performance reports, and interpret latency and failure metrics. This is an essential skill for production ML APIs.</p>



<p>Finally, you covered the tools that keep an ML codebase clean and maintainable: linting, formatting, static typing, and the Makefile commands that tie everything together. You closed with automated test runners, load-test scripts, and coverage reporting, giving you an end-to-end workflow that mirrors real MLOps engineering practice.</p>



<p>By now, you have seen how professional ML systems are tested, validated, measured, and maintained. This sets you up for the next module, where we will begin building data pipelines and reproducible ML workflows.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Singh, V</strong><strong>. </strong>“Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/4ztdu" target="_blank" rel="noreferrer noopener">https://pyimg.co/4ztdu</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing" data-enlighter-group="75">@incollection{Singh_2026_pytest-tutorial-mlops-testing-fixtures-locust-load-testing,
  author = {Vikram Singh},
  title = {{Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/4ztdu},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/20/pytest-tutorial-mlops-testing-fixtures-and-locust-load-testing/">Pytest Tutorial: MLOps Testing, Fixtures, and Locust Load Testing</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>FastAPI for MLOps: Python Project Structure and API Best Practices</title>
		<link>https://pyimagesearch.com/2026/04/13/fastapi-for-mlops-python-project-structure-and-api-best-practices/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[FastAPI]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Python Development]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[backend development]]></category>
		<category><![CDATA[fastapi]]></category>
		<category><![CDATA[fastapi mlops]]></category>
		<category><![CDATA[ml api]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[python poetry]]></category>
		<category><![CDATA[python project structure]]></category>
		<category><![CDATA[software engineering]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53431</guid>

					<description><![CDATA[<p>Table of Contents FastAPI for MLOps: Python Project Structure and API Best Practices Introduction What You Will Build and Learn Why Software Engineering Comes First in MLOps Best Practices Where This Fits in the Overall Curriculum Python Project Structure Best&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/13/fastapi-for-mlops-python-project-structure-and-api-best-practices/">FastAPI for MLOps: Python Project Structure and API Best Practices</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-FastAPI-MLOps-Python-Project-Structure-API-Best-Practices"><a rel="noopener" target="_blank" href="#h1-FastAPI-MLOps-Python-Project-Structure-API-Best-Practices">FastAPI for MLOps: Python Project Structure and API Best Practices</a></li>

    <li id="TOC-h2-Introduction"><a rel="noopener" target="_blank" href="#h2-Introduction">Introduction</a></li>
    <ul>
        <li id="TOC-h3-What-You-Will-Build-Learn"><a rel="noopener" target="_blank" href="#h3-What-You-Will-Build-Learn">What You Will Build and Learn</a></li>
        <li id="TOC-h3-Why-Software-Engineering-Comes-First-MLOps-Best-Practices"><a rel="noopener" target="_blank" href="#h3-Why-Software-Engineering-Comes-First-MLOps-Best-Practices">Why Software Engineering Comes First in MLOps Best Practices</a></li>
        <li id="TOC-h3-Where-This-Fits-Overall-Curriculum"><a rel="noopener" target="_blank" href="#h3-Where-This-Fits-Overall-Curriculum">Where This Fits in the Overall Curriculum</a></li>
    </ul>

    <li id="TOC-h2-Python-Project-Structure-Best-Practices-MLOps"><a rel="noopener" target="_blank" href="#h2-Python-Project-Structure-Best-Practices-MLOps">Python Project Structure Best Practices for MLOps</a></li>
    <ul>
        <li id="TOC-h3-How-Structure-Python-Project-src-Layout"><a rel="noopener" target="_blank" href="#h3-How-Structure-Python-Project-src-Layout">How to Structure a Python Project with src/ Layout</a></li>
        <li id="TOC-h3-Python-Project-Structure-Explained-Repository-Walkthrough"><a rel="noopener" target="_blank" href="#h3-Python-Project-Structure-Explained-Repository-Walkthrough">Python Project Structure Explained: Repository Walkthrough</a></li>
        <li id="TOC-h3-Python-Project-Structure-Best-Practices-Directory-Breakdown"><a rel="noopener" target="_blank" href="#h3-Python-Project-Structure-Best-Practices-Directory-Breakdown">Python Project Structure Best Practices: Directory Breakdown</a></li>
        <li id="TOC-h3-How-This-Structure-Scales-Larger-ML-Systems"><a rel="noopener" target="_blank" href="#h3-How-This-Structure-Scales-Larger-ML-Systems">How This Structure Scales to Larger ML Systems</a></li>
    </ul>

    <li id="TOC-h2-Managing-Python-Dependencies-Poetry-ML-Projects"><a rel="noopener" target="_blank" href="#h2-Managing-Python-Dependencies-Poetry-ML-Projects">Managing Python Dependencies with Poetry for ML Projects</a></li>
    <ul>
        <li id="TOC-h3-Python-Poetry-vs-PDM-vs-UV-Choosing-Package-Manager-MLOps"><a rel="noopener" target="_blank" href="#h3-Python-Poetry-vs-PDM-vs-UV-Choosing-Package-Manager-MLOps">Python Poetry vs PDM vs UV: Choosing a Package Manager for MLOps</a></li>
        <li id="TOC-h3-Understanding-pyproject-toml-Python-Project-Configuration"><a rel="noopener" target="_blank" href="#h3-Understanding-pyproject-toml-Python-Project-Configuration">Understanding pyproject.toml for Python Project Configuration</a></li>
        <li id="TOC-h3-Installing-Dependencies-Poetry-PDM-UV"><a rel="noopener" target="_blank" href="#h3-Installing-Dependencies-Poetry-PDM-UV">Installing Dependencies (Poetry, PDM, UV)</a></li>
        <li id="TOC-h3-Managing-Python-Virtual-Environments-Reproducible-MLOps"><a rel="noopener" target="_blank" href="#h3-Managing-Python-Virtual-Environments-Reproducible-MLOps">Managing Python Virtual Environments for Reproducible MLOps</a></li>
        <li id="TOC-h3-Automating-MLOps-Setup-Python-Environment-Scripts"><a rel="noopener" target="_blank" href="#h3-Automating-MLOps-Setup-Python-Environment-Scripts">Automating MLOps Setup with Python Environment Scripts</a></li>
    </ul>

    <li id="TOC-h2-Configuration-Management-MLOps-YAML-env-Pydantic"><a rel="noopener" target="_blank" href="#h2-Configuration-Management-MLOps-YAML-env-Pydantic">Configuration Management in MLOps: YAML, .env, and Pydantic</a></li>
    <ul>
        <li id="TOC-h3-Using-Pydantic-Settings-MLOps-Configuration-Management"><a rel="noopener" target="_blank" href="#h3-Using-Pydantic-Settings-MLOps-Configuration-Management">Using Pydantic Settings for MLOps Configuration Management</a></li>
        <li id="TOC-h3-What-This-Means-MLOps-Configuration-System-Design"><a rel="noopener" target="_blank" href="#h3-What-This-Means-MLOps-Configuration-System-Design">What This Means for MLOps Configuration and System Design</a></li>
        <li id="TOC-h3-Loading-YAML-Merging-Layers"><a rel="noopener" target="_blank" href="#h3-Loading-YAML-Merging-Layers">Loading YAML and Merging Layers</a></li>
        <li id="TOC-h3-Designing-YAML-Configs-Scalable-MLOps-Pipelines"><a rel="noopener" target="_blank" href="#h3-Designing-YAML-Configs-Scalable-MLOps-Pipelines">Designing YAML Configs for Scalable MLOps Pipelines</a></li>
        <li id="TOC-h3-Using-env-Files-Secure-MLOps-Configuration"><a rel="noopener" target="_blank" href="#h3-Using-env-Files-Secure-MLOps-Configuration">Using .env Files for Secure MLOps Configuration</a></li>
        <li id="TOC-h3-Why-Configuration-Management-Matters-MLOps-Systems"><a rel="noopener" target="_blank" href="#h3-Why-Configuration-Management-Matters-MLOps-Systems">Why Configuration Management Matters in MLOps Systems</a></li>
        <li id="TOC-h3-How-App-Uses-Configuration-src-main-py"><a rel="noopener" target="_blank" href="#h3-How-App-Uses-Configuration-src-main-py">How the App Uses Configuration (src/main.py)</a></li>
        <li id="TOC-h3-How-FastAPI-Uses-Configuration-Production-MLOps-Systems"><a rel="noopener" target="_blank" href="#h3-How-FastAPI-Uses-Configuration-Production-MLOps-Systems">How FastAPI Uses Configuration in Production MLOps Systems</a></li>
        <li id="TOC-h3-Extending-MLOps-Configuration-Safely-Python-Projects"><a rel="noopener" target="_blank" href="#h3-Extending-MLOps-Configuration-Safely-Python-Projects">Extending MLOps Configuration Safely in Python Projects</a></li>
    </ul>

    <li id="TOC-h2-Logging-Best-Practices-MLOps-FastAPI-Applications"><a rel="noopener" target="_blank" href="#h2-Logging-Best-Practices-MLOps-FastAPI-Applications">Logging Best Practices for MLOps and FastAPI Applications</a></li>
    <ul>
        <li id="TOC-h3-Why-Logging-Critical-ML-Systems"><a rel="noopener" target="_blank" href="#h3-Why-Logging-Critical-ML-Systems">Why Logging Is Critical for ML Systems</a></li>
        <li id="TOC-h3-Logger-Initialization"><a rel="noopener" target="_blank" href="#h3-Logger-Initialization">Logger Initialization</a></li>
        <li id="TOC-h3-Log-Formatting-Levels"><a rel="noopener" target="_blank" href="#h3-Log-Formatting-Levels">Log Formatting and Levels</a></li>
        <li id="TOC-h3-Logging-Across-App"><a rel="noopener" target="_blank" href="#h3-Logging-Across-App">Logging Across the App</a></li>
        <li id="TOC-h3-Structured-Traceable-Behavior-Across-App"><a rel="noopener" target="_blank" href="#h3-Structured-Traceable-Behavior-Across-App">Together, This Gives Us Structured, Traceable Behavior Across the App</a></li>
    </ul>

    <li id="TOC-h2-FastAPI-MLOps-Building-Production-ML-API"><a rel="noopener" target="_blank" href="#h2-FastAPI-MLOps-Building-Production-ML-API">FastAPI for MLOps: Building a Production ML API</a></li>
    <ul>
        <li id="TOC-h3-Why-FastAPI-Ideal-MLOps-API-Development"><a rel="noopener" target="_blank" href="#h3-Why-FastAPI-Ideal-MLOps-API-Development">Why FastAPI Is Ideal for MLOps API Development</a></li>
        <li id="TOC-h3-Creating-FastAPI-Application-Machine-Learning-APIs"><a rel="noopener" target="_blank" href="#h3-Creating-FastAPI-Application-Machine-Learning-APIs">Creating a FastAPI Application for Machine Learning APIs</a></li>
        <li id="TOC-h3-Implementing-Health-Check-Endpoints-FastAPI-MLOps"><a rel="noopener" target="_blank" href="#h3-Implementing-Health-Check-Endpoints-FastAPI-MLOps">Implementing Health Check Endpoints in FastAPI (MLOps)</a></li>
        <li id="TOC-h3-Building-FastAPI-Prediction-Endpoint-ML-Models"><a rel="noopener" target="_blank" href="#h3-Building-FastAPI-Prediction-Endpoint-ML-Models">Building a FastAPI Prediction Endpoint for ML Models</a></li>
        <li id="TOC-h3-Behind-This-Endpoint-Prediction-Engine"><a rel="noopener" target="_blank" href="#h3-Behind-This-Endpoint-Prediction-Engine">Behind This Endpoint Is Your Prediction Engine</a></li>
        <li id="TOC-h3-Deploying-FastAPI-Uvicorn-MLOps-Applications"><a rel="noopener" target="_blank" href="#h3-Deploying-FastAPI-Uvicorn-MLOps-Applications">Deploying FastAPI with Uvicorn for MLOps Applications</a></li>
        <li id="TOC-h3-Auto-Generated-API-Docs-Swagger-ReDoc"><a rel="noopener" target="_blank" href="#h3-Auto-Generated-API-Docs-Swagger-ReDoc">Auto-Generated API Docs (Swagger, ReDoc)</a></li>
    </ul>

    <li id="TOC-h2-MLOps-Architecture-Service-Layer-Design-Patterns"><a rel="noopener" target="_blank" href="#h2-MLOps-Architecture-Service-Layer-Design-Patterns">MLOps Architecture: Service Layer Design Patterns</a></li>
    <ul>
        <li id="TOC-h3-Why-Separate-Services-Routes"><a rel="noopener" target="_blank" href="#h3-Why-Separate-Services-Routes">Why We Separate Services from Routes</a></li>
        <li id="TOC-h3-Designing-ML-Inference-Service"><a rel="noopener" target="_blank" href="#h3-Designing-ML-Inference-Service">Designing an ML Inference Service</a></li>
        <li id="TOC-h3-Scaling-MLOps-Systems-Modular-Service-Architecture"><a rel="noopener" target="_blank" href="#h3-Scaling-MLOps-Systems-Modular-Service-Architecture">Scaling MLOps Systems with Modular Service Architecture</a></li>
    </ul>

    <li id="TOC-h2-Model-Abstraction-MLOps-Decoupling-ML-APIs"><a rel="noopener" target="_blank" href="#h2-Model-Abstraction-MLOps-Decoupling-ML-APIs">Model Abstraction in MLOps: Decoupling ML from APIs</a></li>
    <ul>
        <li id="TOC-h3-Designing-Python-ML-Model-Class-MLOps"><a rel="noopener" target="_blank" href="#h3-Designing-Python-ML-Model-Class-MLOps">Designing a Python ML Model Class for MLOps</a></li>
        <li id="TOC-h3-Replace-Dummy-Models-Production-ML-Models"><a rel="noopener" target="_blank" href="#h3-Replace-Dummy-Models-Production-ML-Models">How to Replace Dummy Models with Production ML Models</a></li>
        <li id="TOC-h3-Versioning-Model-Class"><a rel="noopener" target="_blank" href="#h3-Versioning-Model-Class">Versioning the Model Class</a></li>
    </ul>

    <li id="TOC-h2-Building-Reusable-Utilities-Python-MLOps-Projects"><a rel="noopener" target="_blank" href="#h2-Building-Reusable-Utilities-Python-MLOps-Projects">Building Reusable Utilities in Python MLOps Projects</a></li>
    <ul>
        <li id="TOC-h3-Loading-YAML-Configs"><a rel="noopener" target="_blank" href="#h3-Loading-YAML-Configs">Loading YAML Configs</a></li>
        <li id="TOC-h3-Adding-New-Helper-Functions"><a rel="noopener" target="_blank" href="#h3-Adding-New-Helper-Functions">Adding New Helper Functions</a></li>
    </ul>

    <li id="TOC-h2-Running-FastAPI-MLOps-Application-Locally"><a rel="noopener" target="_blank" href="#h2-Running-FastAPI-MLOps-Application-Locally">Running a FastAPI MLOps Application Locally</a></li>
    <ul>
        <li id="TOC-h3-Running-via-Poetry"><a rel="noopener" target="_blank" href="#h3-Running-via-Poetry">Running via Poetry</a></li>
        <li id="TOC-h3-Running-via-UV"><a rel="noopener" target="_blank" href="#h3-Running-via-UV">Running via UV</a></li>
        <li id="TOC-h3-Running-Python-MLOps-Projects-PDM"><a rel="noopener" target="_blank" href="#h3-Running-Python-MLOps-Projects-PDM">Running Python MLOps Projects with PDM</a></li>
        <li id="TOC-h3-Testing-FastAPI-Endpoints-Health-Check-Prediction-API"><a rel="noopener" target="_blank" href="#h3-Testing-FastAPI-Endpoints-Health-Check-Prediction-API">Testing FastAPI Endpoints: Health Check and Prediction API</a></li>
    </ul>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-FastAPI-MLOps-Python-Project-Structure-API-Best-Practices"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-FastAPI-MLOps-Python-Project-Structure-API-Best-Practices">FastAPI for MLOps: Python Project Structure and API Best Practices</a></h2>



<p>In this lesson, you will learn how to structure a Machine Learning (ML) project like a real production system, complete with a <code data-enlighter-language="python" class="EnlighterJSRAW">src</code> directory layout, layered configuration, environment management, logging, and a FastAPI service that exposes your model through clean Application Programming Interface (API) routes.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?lossy=2&strip=1&webp=1" alt="fastapi-for-mlops-python-project-structure-featured.png" class="wp-image-53444" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/fastapi-for-mlops-python-project-structure-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>This lesson is the 1st of a 2-part series on Software Engineering for Machine Learning Operations (MLOps):</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/yn8a5" target="_blank" rel="noreferrer noopener">FastAPI for MLOps: Python Project Structure and API Best Practices</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to build reliable, scalable ML software the right way,</strong><em><strong> just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction">Introduction</a></h2>



<p>Modern ML systems do not succeed because of models alone — they succeed because of the <em>software engineering wrapped</em> around them. Most real-world failures in MLOps come from poor structure, missing configuration, messy environments, unclear APIs, or nonexistent logging, not from bad ML.</p>



<p>This lesson gives you the engineering foundation you need to build ML systems that are stable, testable, and production-ready. You’ll learn how to structure your project, manage environments, load configurations, build APIs, and prepare your system for future modules like testing, deployment, and automation.</p>



<p>To learn how solid software engineering underpins every ML workflow, just keep reading.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-You-Will-Build-Learn"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-You-Will-Build-Learn">What You Will Build and Learn</a></h3>



<p>In this lesson, you’ll build the backbone of a real ML application: a clean repository layout, environment management with modern tooling, configuration loading via Pydantic, structured logging, a FastAPI interface, and a simple service layer to power prediction.</p>



<p>These concepts form the “foundation layer” every MLOps system relies on — regardless of the model you eventually plug in.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Software-Engineering-Comes-First-MLOps-Best-Practices"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Software-Engineering-Comes-First-MLOps-Best-Practices">Why Software Engineering Comes First in MLOps Best Practices</a></h3>



<p>ML projects fail not because the model is wrong, but because the <em>plumbing</em> around the model collapses. Scripts turn into spaghetti, notebooks become unmaintainable, configs get scattered, and environments drift until the system becomes impossible to debug.</p>



<p>Good software engineering fixes this by introducing structure, consistency, and predictable behavior. When your API, config, logs, and model code work together cleanly, everything built on top (e.g., testing, serving, scaling, monitoring) suddenly becomes reliable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Where-This-Fits-Overall-Curriculum"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Where-This-Fits-Overall-Curriculum">Where This Fits in the Overall Curriculum</a></h3>



<p>This lesson is the foundation of the entire MLOps series. Everything that comes next — testing, model integration, deployment workflows, Continuous Integration/Continuous Delivery (CI/CD) automation, monitoring, and scaling — builds on the engineering habits you establish here.</p>



<p>Think of this as your “software engineering base layer.” Once you master this structure, adding real models, adding load testing, or plugging the system into cloud infrastructure becomes far easier.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Python-Project-Structure-Best-Practices-MLOps"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Python-Project-Structure-Best-Practices-MLOps">Python Project Structure Best Practices for MLOps</a></h2>



<p>A well-structured repository is the first sign of a healthy ML system. Before we write any API code or load a model, we need a layout that cleanly separates configuration, services, models, and utilities. This not only prevents chaos — it makes testing, scaling, and future modules dramatically easier.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-Structure-Python-Project-src-Layout"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-Structure-Python-Project-src-Layout">How to Structure a Python Project with src/ Layout</a></h3>



<p>ML projects quickly become messy if everything sits at the root level. The <code data-enlighter-language="python" class="EnlighterJSRAW">src/</code> layout prevents naming collisions, enforces imports that match production structure, and makes it clear where application code actually lives.</p>



<p>This is the same structure used in mature Python services deployed in production environments.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Python-Project-Structure-Explained-Repository-Walkthrough"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Python-Project-Structure-Explained-Repository-Walkthrough">Python Project Structure Explained: Repository Walkthrough</a></h3>



<p>Here’s the repository layout we’re working with in this module (the exact tree will be shown later when you provide it):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="1">sw-eng-mlops/
│
├── src/
│   ├── core/
│   ├── models/
│   ├── services/
│   ├── api/
│   ├── utils/
│   └── config/
│
├── tests/
│   ├── unit/
│   ├── integration/
│   └── performance/
│
├── pyproject.toml
├── README.md
├── setup_env.sh
└── .env.example
</pre>



<p>This structure is intentionally clean: <code data-enlighter-language="python" class="EnlighterJSRAW">core/</code> contains primitives, <code data-enlighter-language="python" class="EnlighterJSRAW">models/</code> stores your ML logic, <code data-enlighter-language="python" class="EnlighterJSRAW">services/</code> contains business logic, and <code data-enlighter-language="python" class="EnlighterJSRAW">api/</code> exposes everything through FastAPI routes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Python-Project-Structure-Best-Practices-Directory-Breakdown"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Python-Project-Structure-Best-Practices-Directory-Breakdown">Python Project Structure Best Practices: Directory Breakdown</a></h3>



<h4 class="wp-block-heading">core/ — The Application Base Layer</h4>



<p>This folder contains shared components such as logging setup, base classes, or utility abstractions. Everything here is meant to be reusable across the whole system.</p>



<h4 class="wp-block-heading">models/ — ML or Dummy Model Code</h4>



<p>Even if you’re starting with a dummy model, isolating model code here makes it easy to swap in real models later.</p>



<h4 class="wp-block-heading">services/ — The Business Logic Layer</h4>



<p>This is where you place the logic that actually powers <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code>, not inside the API route. This separation keeps production-grade APIs maintainable.</p>



<h4 class="wp-block-heading">api/ — FastAPI Endpoints</h4>



<p>Routes live here. Each endpoint calls a service, which calls a model.</p>



<p>Tight, clean, and testable.</p>



<h4 class="wp-block-heading">utils/ — Shared Helpers</h4>



<p>Config loaders, yaml readers, or general-purpose helper functions sit here.</p>



<p>If it isn’t domain logic or a model, it goes here.</p>



<h4 class="wp-block-heading">config/ — Configuration Files</h4>



<p>YAML configs, <code data-enlighter-language="python" class="EnlighterJSRAW">BaseSettings</code> classes, validation logic, and environment overrides.</p>



<p>Centralizing config makes behavior predictable and testable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-This-Structure-Scales-Larger-ML-Systems"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-This-Structure-Scales-Larger-ML-Systems">How This Structure Scales to Larger ML Systems</a></h3>



<p>This layout scales easily as your ML workload grows:</p>



<ul class="wp-block-list">
<li>Add a new model → create a folder inside <code data-enlighter-language="python" class="EnlighterJSRAW">models/</code>.</li>



<li>Add a new prediction workflow → add a service in <code data-enlighter-language="python" class="EnlighterJSRAW">services/</code>.</li>



<li>Add new API functionality → add a route in <code data-enlighter-language="python" class="EnlighterJSRAW">api/</code>.</li>



<li>Add data pipelines or vector DB logic → expand <code data-enlighter-language="python" class="EnlighterJSRAW">core/</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">services/</code>.</li>
</ul>



<p>This way, the project grows <strong>horizontally</strong>, not chaotically.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Managing-Python-Dependencies-Poetry-ML-Projects"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Managing-Python-Dependencies-Poetry-ML-Projects">Managing Python Dependencies with Poetry for ML Projects</a></h2>



<p>Modern MLOps projects rely on predictable, repeatable environments — and this section teaches you how to create exactly that. Before we build APIs or load models, we need a clean, isolated workspace where dependencies are installed, versions are pinned, and tools behave consistently across machines.</p>



<p>To learn how to manage dependencies, virtual environments, and setup scripts in real-world ML projects, just keep reading.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Python-Poetry-vs-PDM-vs-UV-Choosing-Package-Manager-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Python-Poetry-vs-PDM-vs-UV-Choosing-Package-Manager-MLOps">Python Poetry vs PDM vs UV: Choosing a Package Manager for MLOps</a></h3>



<p>There are 3 modern Python toolchains worth knowing:</p>



<ul class="wp-block-list">
<li><strong>Poetry:</strong> full-featured dependency + environment + packaging manager.</li>



<li><strong>PDM</strong><strong> (Python Dependency Manager)</strong><strong>:</strong> simpler and faster than Poetry, with PEP-582 support.</li>



<li><strong><a href="https://docs.astral.sh/uv/" target="_blank" rel="noreferrer noopener">UV</a></strong><strong>:</strong> an extremely fast Rust-based package manager from Astral.</li>
</ul>



<p>All 3 support <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code>, the modern Python standard for dependencies and metadata.</p>



<p>Teams often standardize on a single tool, but your project supports <em>all three</em>, so students can use whichever they prefer.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Understanding-pyproject-toml-Python-Project-Configuration"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Understanding-pyproject-toml-Python-Project-Configuration">Understanding pyproject.toml for Python Project Configuration</a></h3>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code> defines:</p>



<ul class="wp-block-list">
<li>project <code data-enlighter-language="python" class="EnlighterJSRAW">name</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">version</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">description</code></li>



<li>dependencies like <code data-enlighter-language="python" class="EnlighterJSRAW">fastapi</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">pydantic</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">pyyaml</code></li>



<li>dev tools like <code data-enlighter-language="python" class="EnlighterJSRAW">pytest</code> (Lesson 2)</li>



<li>optional entrypoints (<code data-enlighter-language="python" class="EnlighterJSRAW">start-server = "src.main:main"</code>)</li>
</ul>



<p>In other words, it is the <strong>single source of truth</strong> for installation and build metadata.</p>



<p>Any tool (Poetry, PDM, UV, pip) reads this file to install exactly what the project needs.</p>



<p>This is how professional ML systems avoid “works on my machine” issues.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Installing-Dependencies-Poetry-PDM-UV"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Installing-Dependencies-Poetry-PDM-UV">Installing Dependencies (Poetry, PDM, UV)</a></h3>



<h4 class="wp-block-heading">Using Poetry (recommended)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="2">poetry install
poetry shell
poetry run python src/main.py
</pre>



<p>Poetry creates an isolated virtual environment and resolves all versions deterministically.</p>



<h4 class="wp-block-heading">Using UV (lightweight + blazing fast)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="3">uv venv
source .venv/bin/activate
uv pip install -e .
python src/main.py
</pre>



<p>UV is perfect for fast installs and CI systems where speed matters.</p>



<h4 class="wp-block-heading">Using PDM (simple + modern)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="4">pdm install
pdm run python src/main.py
</pre>



<p>PDM feels like <code data-enlighter-language="python" class="EnlighterJSRAW">npm</code> — no <code data-enlighter-language="python" class="EnlighterJSRAW">venv</code> folder by default; lightweight and straightforward.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Managing-Python-Virtual-Environments-Reproducible-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Managing-Python-Virtual-Environments-Reproducible-MLOps">Managing Python Virtual Environments for Reproducible MLOps</a></h3>



<p>Regardless of what tool you choose, the goal is the same: isolate project dependencies from the system Python installation.</p>



<ul class="wp-block-list">
<li>Poetry creates its own environment automatically.</li>



<li>UV uses <code data-enlighter-language="python" class="EnlighterJSRAW">.venv/</code> inside your project.</li>



<li>PDM can create or avoid virtual environments depending on the configuration.</li>
</ul>



<p>The important principle:</p>



<p><strong>Never install ML dependencies globally.</strong></p>



<p>Environments keep your project reproducible and safe.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Automating-MLOps-Setup-Python-Environment-Scripts"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Automating-MLOps-Setup-Python-Environment-Scripts">Automating MLOps Setup with Python Environment Scripts</a></h3>



<p>Your project includes a helper script:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="5">./scripts/setup_env.sh
</pre>



<p>This script:</p>



<ul class="wp-block-list">
<li>Detects whether <strong>Poetry</strong>, <strong>UV</strong>, or plain <strong>pip</strong> is available</li>



<li>Installs dependencies using the detected tool</li>



<li>Creates or activates the <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> file</li>



<li>Shows the next steps to start the API</li>
</ul>



<p>This is extremely helpful for teams because it removes all “setup guessing” and gives new developers a consistent starting point.</p>



<p>You now know how environments, dependency managers, and <code data-enlighter-language="python" class="EnlighterJSRAW">pyproject.toml</code> work together to create a stable foundation for ML systems. With everything installed and configured, you’re ready to build and serve a real API.</p>



<p>Up next, we’ll create your first ML service with FastAPI and connect it to your project’s service layer.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuration-Management-MLOps-YAML-env-Pydantic"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuration-Management-MLOps-YAML-env-Pydantic">Configuration Management in MLOps: YAML, .env, and Pydantic</a></h2>



<p><em>How the entire ML system loads, merges, and applies configuration at runtime.</em></p>



<p>Configuration is one of the most important engineering foundations in any ML system. In Lesson 1, we want students to walk away understanding not only <strong>why</strong> configuration matters but <strong>exactly how this project loads and merges config values</strong>. That means stepping through the real code inside <code data-enlighter-language="python" class="EnlighterJSRAW">src/core/config.py</code>, the <code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">configs/config.yaml</code>.</p>



<p>We also want to show how the API, model, and services consume configuration. So when students replace the dummy model with a real one, the pattern already scales.</p>



<p>Let’s walk through it piece by piece.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-Pydantic-Settings-MLOps-Configuration-Management"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-Pydantic-Settings-MLOps-Configuration-Management">Using Pydantic Settings for MLOps Configuration Management</a></h3>



<p>Your configuration system starts with a <code data-enlighter-language="python" class="EnlighterJSRAW">Settings</code> class:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="6">class Settings(BaseSettings):
    api_host: str = "0.0.0.0"
    api_port: int = 8000
    debug: bool = False
    environment: str = "development"
    log_level: str = "INFO"

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-What-This-Means-MLOps-Configuration-System-Design"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-What-This-Means-MLOps-Configuration-System-Design">What This Means for MLOps Configuration and System Design</a></h3>



<ul class="wp-block-list">
<li>Pydantic’s <code data-enlighter-language="python" class="EnlighterJSRAW">BaseSettings</code> automatically reads:
<ul class="wp-block-list">
<li>environment variables</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> file</li>



<li>any overrides you pass at runtime</li>
</ul>
</li>



<li>Defaults are provided <em>in code</em> so the system always works, even if <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> is missing.</li>



<li>Type safety ensures that if someone writes <code data-enlighter-language="python" class="EnlighterJSRAW">API_PORT=hello</code>, the app will fail fast.</li>
</ul>



<p>This is the right pattern for ML systems where dozens of environment variables must be synchronized across dev, test, staging, and production.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Loading-YAML-Merging-Layers"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Loading-YAML-Merging-Layers">Loading YAML and Merging Layers</a></h3>



<p>Next comes one of the most important parts of your system:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="7">def load_config() -> Settings:
    settings = Settings()

    config_path = "configs/config.yaml"
    if os.path.exists(config_path):
        yaml_config = load_yaml_config(config_path)

        for key, value in yaml_config.items():
            if hasattr(settings, key):
                setattr(settings, key, value)

    return settings
</pre>



<p><strong>Why This Is Powerful</strong></p>



<p>You now have <strong>layered configuration</strong>, which production ML systems use everywhere:</p>



<p><strong>Layer 1: Code defaults</strong></p>



<p>Ensures the app always runs.</p>



<p><strong>Layer 2: YAML</strong> (<code data-enlighter-language="python" class="EnlighterJSRAW">configs/config.yaml</code>)</p>



<p>Great for team-shared configs, model settings, cache sizes, service parameters.</p>



<p><strong>Layer 3:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> <strong>file</strong></p>



<p>Local overrides (ports, debug mode, secrets).</p>



<p><strong>Layer 4: Runtime environment variables</strong></p>



<p>Final source of truth in cloud deployments.</p>



<p>This layered system prevents the “hard-coded value” trap and keeps ML infra consistent across environments.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Designing-YAML-Configs-Scalable-MLOps-Pipelines"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Designing-YAML-Configs-Scalable-MLOps-Pipelines">Designing YAML Configs for Scalable MLOps Pipelines</a></h3>



<p>Your YAML file contains deeper structural config:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="8">api_host: "0.0.0.0"
api_port: 8000
debug: true
environment: "development"

log_level: "INFO"

model:
  name: "dummy_classifier"
  version: "1.0.0"
  cache_size: 100

service:
  timeout: 30
  max_retries: 3
</pre>



<p>Even though <code data-enlighter-language="python" class="EnlighterJSRAW">Settings</code> does not yet support nested objects for models or services, YAML allows you to introduce new structured configuration later. This is how real ML teams configure:</p>



<ul class="wp-block-list">
<li>model version</li>



<li>tokenizer version</li>



<li>max batch size</li>



<li>timeouts</li>



<li>cache settings</li>



<li>experiment IDs</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Using-env-Files-Secure-MLOps-Configuration"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Using-env-Files-Secure-MLOps-Configuration">Using .env Files for Secure MLOps Configuration</a></h3>



<p>You also provide <code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="9">API_PORT=8000
API_HOST=0.0.0.0
DEBUG=true
ENVIRONMENT=development
LOG_LEVEL=INFO
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Configuration-Management-Matters-MLOps-Systems"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Configuration-Management-Matters-MLOps-Systems">Why Configuration Management Matters in MLOps Systems</a></h3>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code> acts as documentation and a template.</li>



<li>You copy it to <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code>, fill values, and the system boots.</li>



<li>This is a best practice in every production ML repo.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-App-Uses-Configuration-src-main-py"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-App-Uses-Configuration-src-main-py">How the App Uses Configuration (src/main.py)</a></h3>



<p>Your FastAPI entrypoint reads config like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="10">logger.info(f"Starting server on {settings.api_host}:{settings.api_port}")

uvicorn.run(
    "main:app",
    host=settings.api_host,
    port=settings.api_port,
    reload=settings.debug
)
</pre>



<p>Meaning:</p>



<ul class="wp-block-list">
<li>Change <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> to <code data-enlighter-language="python" class="EnlighterJSRAW">API_PORT=9000</code>: Your app automatically runs on port 9000.</li>



<li>Change YAML to <code data-enlighter-language="python" class="EnlighterJSRAW">debug: false</code>: Hot reload turns off.</li>
</ul>



<p>This is the <strong>practical benefit</strong> of structured configuration: no hard-coded values are buried inside the code.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-How-FastAPI-Uses-Configuration-Production-MLOps-Systems"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-How-FastAPI-Uses-Configuration-Production-MLOps-Systems">How FastAPI Uses Configuration in Production MLOps Systems</a></h3>



<p>Today, your inference service is simple, but in real projects, you might use:</p>



<ul class="wp-block-list">
<li>model name</li>



<li>version</li>



<li>batch size</li>



<li>latency budget</li>



<li>max retries</li>



<li>cache settings</li>



<li>rate limits</li>
</ul>



<p>All of these come from settings, not hardcoded logic.</p>



<p>In this lesson, you teach the <em>pattern</em>, so when the dummy model is eventually replaced with an Open Neural Network Exchange (ONNX) model, a Hugging Face model, or a custom PyTorch model, the service already has the right structure.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Extending-MLOps-Configuration-Safely-Python-Projects"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Extending-MLOps-Configuration-Safely-Python-Projects">Extending MLOps Configuration Safely in Python Projects</a></h3>



<p>Suppose tomorrow you want:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="11">MODEL_PATH=models/checkpoint.pt
ENABLE_CACHE=true
CACHE_TTL=300
</pre>



<p>You add:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="12">model_path: str = "models/dummy.pt"
enable_cache: bool = False
cache_ttl: int = 120
</pre>



<p>Then update <code data-enlighter-language="python" class="EnlighterJSRAW">.env.example</code>. Then, optionally override in YAML.</p>



<p>The app instantly supports new behavior — no rewrites, no refactoring, no confusion.</p>



<p>This is the level of <strong>software engineering maturity</strong> we want students to learn.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Logging-Best-Practices-MLOps-FastAPI-Applications"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Logging-Best-Practices-MLOps-FastAPI-Applications">Logging Best Practices for MLOps and FastAPI Applications</a></h2>



<p>Logging is one of the most underappreciated parts of an ML system. A model prediction might take milliseconds, but diagnosing a production issue without proper logs can take hours. Good logs reduce that time to minutes. In this section, we’ll look at how our lesson’s project initializes a logger, formats log messages, and uses logs consistently across the entire API.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Logging-Critical-ML-Systems"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Logging-Critical-ML-Systems">Why Logging Is Critical for ML Systems</a></h3>



<p>ML systems fail in ways traditional software does not.</p>



<p>A model might produce an unexpected prediction, a dependency might break silently, or the environment might load the wrong configuration. Logging gives you the breadcrumbs needed to understand:</p>



<ul class="wp-block-list">
<li>What inputs reached the API</li>



<li>What model version was used</li>



<li>What the service did before failing</li>



<li>How often errors occur</li>



<li>Whether latency is increasing</li>
</ul>



<p>Logs are your “black box recorder” when something goes wrong, and they’re equally important when everything seems to be working — because they tell you <em>why</em> things are working.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Logger-Initialization"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Logger-Initialization">Logger Initialization</a></h3>



<p>The project defines a single shared logger in <code data-enlighter-language="python" class="EnlighterJSRAW">src/core/logger.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="13">import logging
import sys

logger = logging.getLogger("mlops-lesson1")
logger.setLevel(logging.INFO)

handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

if not logger.handlers:
    logger.addHandler(handler)
</pre>



<p>Here’s what this setup accomplishes:</p>



<ul class="wp-block-list">
<li><strong>A named logger</strong> (<code data-enlighter-language="python" class="EnlighterJSRAW">mlops-lesson1</code>) groups logs for later aggregation (e.g., in Datadog, ELK (Elasticsearch, Logstash, Kibana), OpenTelemetry).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">INFO</code> <strong>as the default level</strong> ensures we capture meaningful operational details without spamming output.</li>



<li><strong>A </strong><code data-enlighter-language="python" class="EnlighterJSRAW">StreamHandler</code> writes logs to <code data-enlighter-language="python" class="EnlighterJSRAW">stdout</code> — the standard for containerized deployments (Docker, Kubernetes).</li>



<li><strong>A simple timestamped formatter</strong> makes logs human-readable while remaining machine-parseable.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">if not logger.handlers:</code> guard prevents duplicate logs if modules are reloaded.</li>
</ul>



<p>This small file gives us a production-friendly logger with minimal overhead.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Log-Formatting-Levels"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Log-Formatting-Levels">Log Formatting and Levels</a></h3>



<p>The logger uses this format:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="14">2025-01-01 12:34:56 - INFO - Prediction result: positive
</pre>



<p>Each part of the log line matters:</p>



<ul class="wp-block-list">
<li><strong>Timestamp:</strong> crucial for correlating logs with events or latency spikes.</li>



<li><strong>Log level:</strong> signals severity: <code data-enlighter-language="python" class="EnlighterJSRAW">INFO</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">WARNING</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">ERROR</code>.</li>



<li><strong>Message:</strong> the human-readable explanation.</li>
</ul>



<p>In MLOps systems, you’ll most commonly use:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">INFO</code> for model loading, API calls, predictions</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">WARNING</code> for slow responses, unexpected patterns</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ERROR</code> when something fails</li>
</ul>



<p>Because FastAPI reloads modules during development, you may see log duplication without safeguards — which is why we include the <code data-enlighter-language="python" class="EnlighterJSRAW">if not logger.handlers:</code> check.</p>



<p>If you later want structured JSON logs (for cloud log ingestion), this same module is the place to upgrade.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Logging-Across-App"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Logging-Across-App">Logging Across the App</a></h3>



<p>The logger is used in multiple places, showing a consistent logging strategy.</p>



<h4 class="wp-block-heading">Health endpoint (src/main.py)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="15">@app.get("/health")
async def health_check():
    logger.info("Health check requested")
    return {"status": "ok"}
</pre>



<p>This gives visibility into uptime checks — important when a load balancer or Kubernetes performs probes.</p>



<h4 class="wp-block-heading">Prediction endpoint (src/services/inference_service.py)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="16">logger.info(f"Making prediction for input: {input_text[:50]}...")
prediction = model.predict(input_text)
logger.info(f"Prediction result: {prediction}")
</pre>



<p>Here we log:</p>



<ul class="wp-block-list">
<li>The incoming input (truncated to avoid leaking full user data)</li>



<li>The model’s output</li>



<li>Any errors</li>
</ul>



<p>If something goes wrong:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="17">except Exception as e:
    logger.error(f"Error during prediction: {str(e)}")
    raise
</pre>



<p>This ensures errors appear in the logs <strong>before</strong> FastAPI converts them into HTTP exceptions.</p>



<h4 class="wp-block-heading">Server startup (main.py)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="18">logger.info(f"Starting server on {settings.api_host}:{settings.api_port}")
</pre>



<p>This is important for:</p>



<ul class="wp-block-list">
<li>verifying the config loaded correctly</li>



<li>ensuring the correct port is used</li>



<li>debugging environments with conflicting overrides</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Structured-Traceable-Behavior-Across-App"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Structured-Traceable-Behavior-Across-App">Together, This Gives Us Structured, Traceable Behavior Across the App</a></h3>



<p>If a user reports:</p>



<p>“The API feels slow today.”</p>



<p>You can immediately look at:</p>



<ul class="wp-block-list">
<li>prediction request timestamps</li>



<li>whether model loading was triggered again</li>



<li>whether latency warnings appear</li>



<li>whether certain inputs correlate with errors</li>
</ul>



<p>Without logs, you’re flying blind.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-FastAPI-MLOps-Building-Production-ML-API"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-FastAPI-MLOps-Building-Production-ML-API">FastAPI for MLOps: Building a Production ML API</a></h2>



<p>APIs are the interface between your ML system and the outside world. Whether the consumer is a mobile app, a batch job, another microservice, or a human developer testing in Postman, every interaction eventually flows through an API. In MLOps, your API becomes the stable contract that hides internal details (model type, version, preprocessing, logging) — allowing you to upgrade models without breaking clients.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-FastAPI-Ideal-MLOps-API-Development"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-FastAPI-Ideal-MLOps-API-Development">Why FastAPI Is Ideal for MLOps API Development</a></h3>



<p>FastAPI gives you a fast, typed, and production-ready way to expose ML predictions.</p>



<p>It handles validation, serialization, documentation, and error responses, so your ML logic stays clean and modular.</p>



<p>The goal is simple: <strong>your API should stay stable even when everything behind it changes </strong>— models, configs, logging, monitoring, infrastructure.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Creating-FastAPI-Application-Machine-Learning-APIs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Creating-FastAPI-Application-Machine-Learning-APIs">Creating a FastAPI Application for Machine Learning APIs</a></h3>



<p>Your project defines the API inside <code data-enlighter-language="python" class="EnlighterJSRAW">src/main.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="19">from fastapi import FastAPI
app = FastAPI(
    title="ML Service API",
    description="Code Foundations &amp; API Engineering for MLOps",
    version="0.1.0"
)
</pre>



<p>This initializes a fully documented ML service with:</p>



<ul class="wp-block-list">
<li>A <code data-enlighter-language="python" class="EnlighterJSRAW">title</code> for the UI</li>



<li>A <code data-enlighter-language="python" class="EnlighterJSRAW">description</code> that shows up in Swagger</li>



<li>A semantic <code data-enlighter-language="python" class="EnlighterJSRAW">version</code></li>



<li>Automatically generated schemas</li>
</ul>



<p>FastAPI instantly gives you API docs and a clean, declarative way to add endpoints.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Implementing-Health-Check-Endpoints-FastAPI-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Implementing-Health-Check-Endpoints-FastAPI-MLOps">Implementing Health Check Endpoints in FastAPI (MLOps)</a></h3>



<p>A health endpoint is the first thing any production system needs.</p>



<p>Kubernetes, AWS Application Load Balancer (ALB), Docker Compose, Jenkins, and uptime monitors all rely on it.</p>



<p>Your implementation:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="20">@app.get("/health")
async def health_check():
    logger.info("Health check requested")
    return {"status": "ok"}
</pre>



<p>This performs 2 critical functions:</p>



<ul class="wp-block-list">
<li><strong>Confirms the API server is alive</strong></li>



<li><strong>Confirms logs are working</strong></li>
</ul>



<p>It also gives you a simple smoke test to verify the environment.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Building-FastAPI-Prediction-Endpoint-ML-Models"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Building-FastAPI-Prediction-Endpoint-ML-Models">Building a FastAPI Prediction Endpoint for ML Models</a></h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> endpoint is where real ML work happens.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="22">@app.post("/predict")
async def predict_route(input: str):
    return {"prediction": predict_service(input)}
</pre>



<p>This endpoint:</p>



<ul class="wp-block-list">
<li>Accepts a simple string input</li>



<li>Passes it into the inference service</li>



<li>Returns a structured JSON prediction</li>
</ul>



<p>Because prediction logic is isolated in <code data-enlighter-language="python" class="EnlighterJSRAW">services/inference_service.py</code>, the API stays lightweight and focused on HTTP behavior — not business logic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Behind-This-Endpoint-Prediction-Engine"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Behind-This-Endpoint-Prediction-Engine">Behind This Endpoint Is Your Prediction Engine</a></h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="23">from models.dummy_model import DummyModel

model = DummyModel()

def predict(input_text: str) -> str:
    logger.info(f"Making prediction for input: {input_text[:50]}...")
    prediction = model.predict(input_text)
    logger.info(f"Prediction result: {prediction}")
    return prediction
</pre>



<p>Even though this is a dummy model, the structure mirrors real production design:</p>



<ul class="wp-block-list">
<li>The service layer owns the prediction logic</li>



<li>The model is instantiated once</li>



<li>Logging wraps the input and output</li>
</ul>



<p>When you upgrade to a real transformer or classifier, the API <strong>does not need to change</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Deploying-FastAPI-Uvicorn-MLOps-Applications"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Deploying-FastAPI-Uvicorn-MLOps-Applications">Deploying FastAPI with Uvicorn for MLOps Applications</a></h3>



<p>The server entrypoint lives at the bottom of <code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="24">def main():
    logger.info(f"Starting server on {settings.api_host}:{settings.api_port}")
    uvicorn.run(
        "main:app",
        host=settings.api_host,
        port=settings.api_port,
        reload=settings.debug
    )
</pre>



<p>A few details matter:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">reload=True</code> reloads on code changes → perfect for development</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">host</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">port</code> come from config → ideal for containers/cloud</li>



<li><strong>logging is integrated</strong> → so you can trace server start behavior</li>
</ul>



<p>You can run the server with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="25">poetry run start-server
</pre>



<p>or</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="26">uvicorn src.main:app --reload
</pre>



<p>Both give you a live API with hot reload.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Auto-Generated-API-Docs-Swagger-ReDoc"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Auto-Generated-API-Docs-Swagger-ReDoc">Auto-Generated API Docs (Swagger, ReDoc)</a></h3>



<p>FastAPI automatically exposes:</p>



<ul class="wp-block-list">
<li><strong>Swagger UI:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:8000/docs</code></li>



<li><strong>ReDoc:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:8000/redoc</code></li>



<li><strong>OpenAPI schema:</strong> <code data-enlighter-language="python" class="EnlighterJSRAW">http://localhost:8000/openapi.json</code></li>
</ul>



<p>These docs are invaluable in ML workflows because:</p>



<ul class="wp-block-list">
<li>You can test predictions interactively</li>



<li>Product, QA, and frontend engineers can explore endpoints</li>



<li>Payload schemas are always up to date</li>



<li>No one needs to ask “What does this endpoint expect?”</li>
</ul>



<p>FastAPI generates this from your Python type hints, which makes documentation essentially free.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-MLOps-Architecture-Service-Layer-Design-Patterns"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-MLOps-Architecture-Service-Layer-Design-Patterns">MLOps Architecture: Service Layer Design Patterns</a></h2>



<p>The service layer is where your application’s real business logic lives. In an ML system, this includes preprocessing, model selection, inference, error handling, postprocessing, and logging. By keeping this logic out of your API routes, you ensure that your codebase remains modular, testable, and ready for future model upgrades.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Separate-Services-Routes"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Separate-Services-Routes">Why We Separate Services from Routes</a></h3>



<p>FastAPI routes should only handle <strong>HTTP concerns</strong>: input validation, request parsing, and response formatting.</p>



<p>They should not know how your model works internally.</p>



<p>Separating logic into a <code data-enlighter-language="python" class="EnlighterJSRAW">services/</code> folder gives you:</p>



<ul class="wp-block-list">
<li><strong>Cleaner API routes:</strong> easier to read and maintain</li>



<li><strong>Better testability:</strong> you can unit test the inference logic without starting a server</li>



<li><strong>Loose coupling:</strong> upgrading models doesn’t require rewriting routes</li>



<li><strong>Clear ownership:</strong> one layer handles HTTP, the other handles ML logic</li>
</ul>



<p>This separation is one of the most critical software engineering patterns in MLOps — you want your system flexible enough that models can change, scale, or switch frameworks without touching your API.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Designing-ML-Inference-Service"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Designing-ML-Inference-Service">Designing an ML Inference Service</a></h3>



<p>Your inference logic lives in:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="27">src/services/inference_service.py
</pre>



<p>Let’s look at how it’s structured:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="28">from models.dummy_model import DummyModel
from core.logger import logger

# Initialize model
model = DummyModel()
logger.info(f"Loaded model: {model.model_name}")
</pre>



<p>This loads the model once at startup. In a real ML system, this is where:</p>



<ul class="wp-block-list">
<li>You load a transformer model</li>



<li>You warm up a GPU</li>



<li>You hydrate a vector store</li>



<li>You initialize the tokenizer/preprocessor state</li>
</ul>



<p>Then comes the prediction function:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="29">def predict(input_text: str) -> str:
    logger.info(f"Making prediction for input: {input_text[:50]}...")
   
    try:
        prediction = model.predict(input_text)
        logger.info(f"Prediction result: {prediction}")
        return prediction
    except Exception as e:
        logger.error(f"Error during prediction: {str(e)}")
        raise
</pre>



<p>This function represents the <em>business logic</em> of your ML service:</p>



<ul class="wp-block-list">
<li>It trims the input for logging</li>



<li>Calls the model’s <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code></li>



<li>Logs errors and output cleanly</li>



<li>Returns only the result — not HTTP details</li>
</ul>



<p>This is exactly why we keep services separate: <strong>inference is not an HTTP concern</strong>, so it does not belong in a route.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Scaling-MLOps-Systems-Modular-Service-Architecture"/>



<h3 class="wp-block-heading"><a href="#TOC-h2-Model-Abstraction-MLOps-Decoupling-ML-APIs">Scaling MLOps Systems with Modular Service Architecture</a></h3>



<p>A great design scales. Tomorrow, your system might need:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">SentimentService</code>: for NLP</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">RecommendationService</code>: for personalization</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">VisionService</code>: that loads YOLO or CLIP</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">BatchService</code>: for async workflows</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">RetrievalService</code>: for Retrieval-Augmented Generation (RAG) pipelines</li>
</ul>



<p>You don’t modify <code data-enlighter-language="python" class="EnlighterJSRAW">main.py</code> or existing endpoints.</p>



<p>You simply add more files under:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="30">src/services/
├── inference_service.py  
├── recommendation_service.py  
├── vision_service.py  
└── retrieval_service.py  
</pre>



<p>Each service becomes independent, testable, and reusable.</p>



<p>Later in Lesson 2, this design becomes even more powerful because:</p>



<ul class="wp-block-list">
<li><strong>Unit tests:</strong> target individual services</li>



<li><strong>Integration tests:</strong> validate routes and services working together</li>



<li><strong>Load tests:</strong> measure the throughput of the <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code> pipeline</li>
</ul>



<p>By the time you add real ML models, this service layer becomes the heart of your system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Model-Abstraction-MLOps-Decoupling-ML-APIs"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Model-Abstraction-MLOps-Decoupling-ML-APIs">Model Abstraction in MLOps: Decoupling ML from APIs</a></h2>



<p>Models change constantly in MLOps. Today you may be serving a dummy classifier; tomorrow it might be a 7B LLM or a YOLOv12 object detector. A good software engineering foundation treats the model as a <em>pluggable, versioned component</em> that can be replaced with minimal friction.</p>



<p>Your current <code data-enlighter-language="python" class="EnlighterJSRAW">models/</code> directory demonstrates exactly how this abstraction works.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Designing-Python-ML-Model-Class-MLOps"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Designing-Python-ML-Model-Class-MLOps">Designing a Python ML Model Class for MLOps</a></h3>



<p>Your lesson uses a simple placeholder model located at:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="31">src/models/dummy_model.py
</pre>



<p>The goal of this class isn’t to perform “real” ML — it’s to give you a clean structure that mimics how production model classes are written.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="32">class DummyModel:
    def __init__(self) -> None:
        self.model_name = "dummy_classifier"
        self.version = "1.0.0"
   
    def predict(self, input_data: Any) -> str:
        text = str(input_data).lower()
        if "good" in text or "great" in text:
            return "positive"
        return "negative"
</pre>



<p>Even in this tiny model, you already see foundational patterns:</p>



<ul class="wp-block-list">
<li>A <strong>constructor</strong> to load or initialize model state</li>



<li>A <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> method that defines the inference interface</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">model_name</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">version</code> fields for introspection and tracking</li>
</ul>



<p>This interface is intentionally minimal: it forces your service and API layers to depend on an abstraction, not on implementation details.</p>



<p>In real MLOps systems, this exact pattern makes it easy to introduce new models without breaking your API.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Replace-Dummy-Models-Production-ML-Models"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Replace-Dummy-Models-Production-ML-Models">How to Replace Dummy Models with Production ML Models</a></h3>



<p>Here’s where the abstraction shines.</p>



<p>If tomorrow you decide to replace the dummy model with:</p>



<ul class="wp-block-list">
<li>A Hugging Face transformer</li>



<li>A PyTorch Lightning checkpoint</li>



<li>A TensorRT engine</li>



<li>An ONNX Runtime session</li>



<li>A vLLM text-generation server</li>



<li>A YOLO detection model</li>
</ul>



<p>…all you need to do is drop a new file into:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="33">src/models/
</pre>



<p>For example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="34">src/models/
├── dummy_model.py
├── sentiment_model.py
├── llm_generation_model.py
└── object_detector.py
</pre>



<p>And update your service:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="35">from models.sentiment_model import SentimentModel
model = SentimentModel()
</pre>



<p>Nothing else changes.</p>



<p>Your FastAPI routes stay the same.</p>



<p>Your service interface stays the same.</p>



<p>Your tests stay the same (except for new model-specific tests).</p>



<p>This is <em>model decoupling</em>.</p>



<p>This is how ML systems avoid turning into tangled spaghetti when models evolve.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Versioning-Model-Class"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Versioning-Model-Class">Versioning the Model Class</a></h3>



<p>Model versioning is a real production concern, and your dummy model subtly teaches the pattern.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="36">self.version = "1.0.0"
</pre>



<p>Model versioning matters because:</p>



<ul class="wp-block-list">
<li>You may deploy multiple models at once</li>



<li>Clients might depend on specific behaviors</li>



<li>A/B testing needs separate versions</li>



<li>Rollbacks require deterministic reproducibility</li>



<li>Monitoring tools (e.g., Prometheus or Langfuse) track model changes</li>
</ul>



<p>In production, versioning happens in several places:</p>



<ul class="wp-block-list">
<li><strong>version field in the class</strong></li>



<li><strong>model registry tag</strong> (MLflow, SageMaker, Hugging Face Hub)</li>



<li><strong>Docker image tag</strong></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code><strong> entry</strong></li>



<li><strong>model card metadata</strong></li>
</ul>



<p>Your project follows the simplest, clearest entrypoint: a version attribute that propagates everywhere the model is used.</p>



<p>Later in Lesson 2, test cases and load tests will automatically pick up this version, mimicking real-world CI/CD systems that validate each model release.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Building-Reusable-Utilities-Python-MLOps-Projects"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Building-Reusable-Utilities-Python-MLOps-Projects">Building Reusable Utilities in Python MLOps Projects</a></h2>



<p>A well-designed ML system always contains a dedicated utilities layer — small, reusable functions that solve cross-cutting problems without polluting your core logic, service layer, or API routes.</p>



<p>In this project, the <code data-enlighter-language="python" class="EnlighterJSRAW">src/utils/</code> folder gives you a clean space to organize those helpers, starting with configuration loading, and is ready to grow as your system becomes more complex.</p>



<p>This layer keeps your codebase maintainable, testable, and extensible.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Loading-YAML-Configs"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Loading-YAML-Configs">Loading YAML Configs</a></h3>



<p>Your primary helper is <code data-enlighter-language="python" class="EnlighterJSRAW">load_yaml_config()</code> found in:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="37">src/utils/helpers.py
</pre>



<p>Here’s the implementation:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="38">def load_yaml_config(path: str) -> Dict[str, Any]:
    config_path = Path(path)
   
    if not config_path.exists():
        return {}
   
    try:
        with open(config_path, 'r', encoding='utf-8') as file:
            config = yaml.safe_load(file)
            return config if config is not None else {}
    except yaml.YAMLError as e:
        print(f"Error loading YAML config from {path}: {e}")
        return {}
    except Exception as e:
        print(f"Unexpected error loading config from {path}: {e}")
        return {}
</pre>



<p>This function may look simple, but it embodies 3 production-level lessons:</p>



<h4 class="wp-block-heading">Separation of concerns</h4>



<p>Your application logic (FastAPI, inference services) should not know <em>how</em> a YAML file is parsed. They should only receive clean configuration objects.</p>



<h4 class="wp-block-heading">Fault tolerance</h4>



<p>In real deployments:</p>



<ul class="wp-block-list">
<li>configs may be missing</li>



<li>YAML indentation may break</li>



<li>a misconfigured CI pipeline may pass an empty file</li>
</ul>



<p>Returning <code data-enlighter-language="python" class="EnlighterJSRAW">{}</code> instead of crashing gives you graceful degradation.</p>



<h4 class="wp-block-heading">Extensibility</h4>



<p>Tomorrow you may add:</p>



<ul class="wp-block-list">
<li>JSON config support</li>



<li>remote config loading (S3, Google Cloud Storage (GCS), Azure Blob)</li>



<li>encrypted secrets</li>



<li>multiple config layers</li>
</ul>



<p>This helper becomes the foundation.</p>



<p>Inside <code data-enlighter-language="python" class="EnlighterJSRAW">core/config.py</code>, you saw how <code data-enlighter-language="python" class="EnlighterJSRAW">load_yaml_config()</code> merges YAML values into your Pydantic settings. This is a real-world pattern used in production MLOps stacks like Airflow, FastAPI microservices, Ray Serve, and MLflow.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Adding-New-Helper-Functions"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Adding-New-Helper-Functions">Adding New Helper Functions</a></h3>



<p>The utilities layer is designed to grow organically as your system grows.</p>



<p>Common helpers you may introduce later include:</p>



<h4 class="wp-block-heading">String helpers</h4>



<ul class="wp-block-list">
<li>text normalization</li>



<li>input cleaning</li>



<li>token counting</li>
</ul>



<h4 class="wp-block-heading">File helpers</h4>



<ul class="wp-block-list">
<li>safe file writes</li>



<li>temporary directory management</li>



<li>checksum calculation for model files</li>
</ul>



<h4 class="wp-block-heading">Model helpers</h4>



<ul class="wp-block-list">
<li>downloading artifacts from cloud storage</li>



<li>caching models on disk</li>



<li>validating model signatures</li>
</ul>



<h4 class="wp-block-heading">API helpers</h4>



<ul class="wp-block-list">
<li>request validation</li>



<li>standardized error responses</li>



<li>retry/backoff wrappers around external calls</li>
</ul>



<h4 class="wp-block-heading">Monitoring helpers</h4>



<ul class="wp-block-list">
<li>timing decorators</li>



<li>metrics emitters (Prometheus, StatsD, OpenTelemetry)</li>



<li>latency buckets</li>
</ul>



<p>All of these belong in one place:</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">src/utils/</code></p>



<p>This prevents your service layer or route handlers from becoming cluttered and ensures that common functionality is implemented once and reused everywhere.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-FastAPI-MLOps-Application-Locally"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-FastAPI-MLOps-Application-Locally">Running a FastAPI MLOps Application Locally</a></h2>



<p>At this point, you have a fully structured ML application: configuration, logging, models, service layer, and a clean FastAPI interface. Now it’s time to actually <em>run</em> the system locally.</p>



<p>This section walks you through running the API with <strong>Poetry</strong>, <strong>UV</strong>, or <strong>PDM</strong>, depending on your setup. We’ll conclude with a quick validation test to ensure everything works end-to-end.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-via-Poetry"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-via-Poetry">Running via Poetry</a></h3>



<p>If you’re using Poetry (recommended for most workflows), your steps are:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="39"># Install dependencies
poetry install

# Activate the environment
poetry shell

# Start the API server
poetry run python src/main.py
</pre>



<p>You should see log lines like:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="40">INFO - Starting server on 0.0.0.0:8000
INFO - Loaded model: dummy_classifier
</pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-7-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="273" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-1024x273.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53447" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7.png?size=126x34&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-300x80.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7.png?size=378x101&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7.png?size=504x134&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7.png?size=630x168&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-768x205.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-1024x273.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-7-1536x410.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Running ML API using Poetry</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-via-UV"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-via-UV">Running via UV</a></h3>



<p>If you prefer <strong>UV</strong> (super-fast installer by Astral), run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="41"># Create and activate a virtual environment
uv venv
source .venv/bin/activate

# Install project in editable mode
uv pip install -e .

# Start the API
python src/main.py
</pre>



<p>This path is great for users who want lightweight dependency management without Poetry’s abstraction.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Running-Python-MLOps-Projects-PDM"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Running-Python-MLOps-Projects-PDM">Running Python MLOps Projects with PDM</a></h3>



<p>If your workflow uses <strong>PDM</strong>, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="43"># Install dependencies
pdm install

# Start the server
pdm run python src/main.py
</pre>



<p>PDM offers a cleaner pyproject-first workflow and works well for CI/CD pipelines that prefer explicit environment setup.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-8-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="282" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-1024x282.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53450" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8.png?size=126x35&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-300x83.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8.png?size=378x104&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8.png?size=504x139&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8.png?size=630x173&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-768x211.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-1024x282.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-8-1536x422.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Terminal showing a successful server started via PDM dependency resolution.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Testing-FastAPI-Endpoints-Health-Check-Prediction-API"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Testing-FastAPI-Endpoints-Health-Check-Prediction-API">Testing FastAPI Endpoints: Health Check and Prediction API</a></h3>



<p>Once the server is running, validate the system with 2 quick API calls.</p>



<h4 class="wp-block-heading">Health Check</h4>



<p>Open:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="44">http://localhost:8000/health
</pre>



<p>Expected response:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="45">{"status": "ok"}
</pre>



<p>This confirms:</p>



<ul class="wp-block-list">
<li>the API is reachable</li>



<li>config and logger initialized</li>



<li>FastAPI routes are registered</li>
</ul>



<h4 class="wp-block-heading">Prediction Test</h4>



<p>Send a prediction request:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="46">curl -X POST "http://localhost:8000/predict?input=This+is+good"
</pre>



<p>Expected response:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="47">{"prediction": "positive"}
</pre>



<p>Under the hood:</p>



<ul class="wp-block-list">
<li>the service layer logs the request</li>



<li>the dummy model classifies sentiment</li>



<li>the API returns structured JSON</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-9-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="431" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-1024x431.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53452" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9.png?size=126x53&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-300x126.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9.png?size=378x159&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9.png?size=504x212&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9.png?size=630x265&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-768x323.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-1024x431.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-9-1536x646.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Auto-generated documentation for the ML API.</figcaption></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-10-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="392" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-1024x392.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53453" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10.png?size=126x48&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-300x115.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10.png?size=378x145&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10.png?size=504x193&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10.png?size=630x241&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-768x294.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-1024x392.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-10-1536x588.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Real terminal output from running the <code>/predict</code> endpoint, validating the end-to-end workflow of the ML API.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you learned how to build a clean, scalable foundation for ML systems using real software-engineering practices. You now understand why ML projects must be structured like production services — not experiments — if they are ever going to ship reliably.</p>



<p>We began by exploring the <em>why</em>: ML code becomes maintainable only when you enforce clear boundaries between configuration, logic, services, and I/O. That idea naturally led to the <code data-enlighter-language="python" class="EnlighterJSRAW">src/</code> layout, which gave our project a predictable and extensible shape.</p>



<p>You then learned how to manage dependencies using Poetry, UV, or PDM — ensuring that every ML environment is reproducible, isolated, and easy to rebuild. This solved the classic “it works on my machine” trap that haunts ML teams.</p>



<p>Next, we built a robust configuration system using Pydantic <code data-enlighter-language="python" class="EnlighterJSRAW">BaseSettings</code>, merging defaults, YAML files, and <code data-enlighter-language="python" class="EnlighterJSRAW">.env</code> variables into a single typed interface. You now have a configuration pattern used by real-world production ML systems.</p>



<p>We also implemented structured <strong>logging</strong>, enabling the application to communicate what it’s doing internally — a prerequisite for debugging, observability, and monitoring.</p>



<p>From there, you built your first production-style ML API with <strong>FastAPI</strong>, complete with <code data-enlighter-language="python" class="EnlighterJSRAW">/health</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">/predict</code>, and auto-generated documentation. You learned how to expose ML logic cleanly, and why APIs are the interface between ML systems and the real world.</p>



<p>We introduced the <strong>Service Layer</strong>, showing how routes should delegate to independent business logic so APIs stay thin and models stay swappable. This design decision is what makes the system testable and future-proof.</p>



<p>You then explored <strong>model abstraction</strong>, using a simple dummy model to illustrate how real models (PyTorch, TensorFlow, ONNX, vLLM, Transformers) can be slotted in without changing the API layer.</p>



<p>Finally, you saw how helper utilities make the system cleaner, and how to run the full application with Poetry, UV, or PDM. The result is a working ML service that looks, behaves, and organizes itself like production-grade software.</p>



<p>By completing this lesson, you’ve built the foundation required for every advanced MLOps practice: testing, performance monitoring, CI/CD, orchestration, and deployment.</p>



<p>You’re now ready for <strong>Lesson 2</strong>, where we transform this service into a fully tested, validated, and performance-monitored ML system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Singh, V</strong><strong>. </strong>“FastAPI for MLOps: Python Project Structure and API Best Practices,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/yn8a5" target="_blank" rel="noreferrer noopener">https://pyimg.co/yn8a5</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="FastAPI for MLOps: Python Project Structure and API Best Practices" data-enlighter-group="48">@incollection{Singh_2026_fastapi-for-mlops-python-project-structure,
  author = {Vikram Singh},
  title = {{FastAPI for MLOps: Python Project Structure and API Best Practices}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/yn8a5},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/13/fastapi-for-mlops-python-project-structure-and-api-best-practices/">FastAPI for MLOps: Python Project Structure and API Best Practices</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</title>
		<link>https://pyimagesearch.com/2026/04/06/agentic-ai-vision-system-object-segmentation-with-sam-3-and-qwen/</link>
		
		<dc:creator><![CDATA[Piyush Thakur]]></dc:creator>
		<pubDate>Mon, 06 Apr 2026 13:03:56 +0000</pubDate>
				<category><![CDATA[Agentic AI]]></category>
		<category><![CDATA[Computer Vision]]></category>
		<category><![CDATA[Multimodal AI]]></category>
		<category><![CDATA[Qwen]]></category>
		<category><![CDATA[SAM]]></category>
		<category><![CDATA[Segmentation]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[agentic ai]]></category>
		<category><![CDATA[ai agents]]></category>
		<category><![CDATA[computer vision]]></category>
		<category><![CDATA[deep learning]]></category>
		<category><![CDATA[image segmentation]]></category>
		<category><![CDATA[multimodal ai]]></category>
		<category><![CDATA[open vocabulary segmentation]]></category>
		<category><![CDATA[qwen vl]]></category>
		<category><![CDATA[sam 3]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[vision language model]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=53357</guid>

					<description><![CDATA[<p>Table of Contents Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen Why Agentic AI Outperforms Traditional Vision Pipelines Why Agentic AI Improves Computer Vision and Segmentation Tasks What We Will Build: An Agentic AI Vision and Segmentation&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/06/agentic-ai-vision-system-object-segmentation-with-sam-3-and-qwen/">Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<script src="https://fast.wistia.com/player.js" async></script><script src="https://fast.wistia.com/embed/bj2sx8eu3j.js" async type="module"></script><style>wistia-player[media-id='bj2sx8eu3j']:not(:defined) { background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/bj2sx8eu3j/swatch'); display: block; filter: blur(5px); padding-top:56.25%; }</style> <wistia-player media-id="bj2sx8eu3j" aspect="1.7777777777777777"></wistia-player>



<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Agentic-AI-Vision-System-Object-Segmentation-with-SAM-3-and-Qwen"><a rel="noopener" target="_blank" href="#h1-Agentic-AI-Vision-System-Object-Segmentation-with-SAM-3-and-Qwen">Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</a></li>
    <li id="TOC-h2-Why-Agentic-AI-Outperforms-Traditional-Vision-Pipelines"><a rel="noopener" target="_blank" href="#h2-Why-Agentic-AI-Outperforms-Traditional-Vision-Pipelines">Why Agentic AI Outperforms Traditional Vision Pipelines</a></li>
    <li id="TOC-h2-Why-Agentic-AI-Improves-Computer-Vision-and-Segmentation-Tasks"><a rel="noopener" target="_blank" href="#h2-Why-Agentic-AI-Improves-Computer-Vision-and-Segmentation-Tasks">Why Agentic AI Improves Computer Vision and Segmentation Tasks</a></li>
    <li id="TOC-h2-What-We-Will-Build-An-Agentic-AI-Vision-and-Segmentation-System"><a rel="noopener" target="_blank" href="#h2-What-We-Will-Build-An-Agentic-AI-Vision-and-Segmentation-System">What We Will Build: An Agentic AI Vision and Segmentation System</a></li>
    <li id="TOC-h2-Agentic-AI-Workflow-Vision-Language-Reasoning-and-Segmentation-Loop"><a rel="noopener" target="_blank" href="#h2-Agentic-AI-Workflow-Vision-Language-Reasoning-and-Segmentation-Loop">Agentic AI Workflow: Vision-Language Reasoning and Segmentation Loop</a></li>
    <li id="TOC-h2-Agentic-AI-Architecture-Combining-VLMs-and-SAM-3-for-Vision"><a rel="noopener" target="_blank" href="#h2-Agentic-AI-Architecture-Combining-VLMs-and-SAM-3-for-Vision">Agentic AI Architecture: Combining VLMs and SAM 3 for Vision</a></li>
    <ul>
        <li id="TOC-h3-Vision-Language-Model-VLM-The-Reasoning-Component"><a rel="noopener" target="_blank" href="#h3-Vision-Language-Model-VLM-The-Reasoning-Component">Vision-Language Model (VLM): The Reasoning Component</a></li>
        <li id="TOC-h3-SAM-3-Segmentation-Model-Open-Vocabulary-Object-Segmentation"><a rel="noopener" target="_blank" href="#h3-SAM-3-Segmentation-Model-Open-Vocabulary-Object-Segmentation">SAM 3: Open-Vocabulary Object Segmentation</a></li>
        <li id="TOC-h3-The-Agentic-Feedback-Loop-Reasoning-Verification-and-Refinement"><a rel="noopener" target="_blank" href="#h3-The-Agentic-Feedback-Loop-Reasoning-Verification-and-Refinement">The Agentic Feedback Loop: Reasoning, Verification, and Refinement</a></li>
        <li id="TOC-h3-Why-Agentic-Segmentation-Outperforms-One-Shot-Models"><a rel="noopener" target="_blank" href="#h3-Why-Agentic-Segmentation-Outperforms-One-Shot-Models">Why Agentic Segmentation Outperforms One-Shot Models</a></li>
    </ul>
    <li id="TOC-h2-Final-Output-Agentic-Vision-System-with-Segmentation-and-Reasoning"><a rel="noopener" target="_blank" href="#h2-Final-Output-Agentic-Vision-System-with-Segmentation-and-Reasoning">Final Output: Agentic Vision System with Segmentation and Reasoning</a></li>
    <li id="TOC-h2-Key-Takeaway-VLM-SAM-3-Intelligent-Vision-Agent"><a rel="noopener" target="_blank" href="#h2-Key-Takeaway-VLM-SAM-3-Intelligent-Vision-Agent">Key Takeaway: VLM + SAM 3 = Intelligent Vision Agent</a></li>
    <li id="TOC-h2-Configuring-Your-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Your-Development-Environment">Configuring Your Development Environment</a></li>
    <li id="TOC-h2-Python-Setup-and-Imports-for-Agentic-AI-Vision-System"><a rel="noopener" target="_blank" href="#h2-Python-Setup-and-Imports-for-Agentic-AI-Vision-System">Python Setup and Imports for Agentic AI Vision System</a></li>
    <li id="TOC-h2-Loading-SAM-3-and-Qwen-Vision-Language-Models-in-Transformers"><a rel="noopener" target="_blank" href="#h2-Loading-SAM-3-and-Qwen-Vision-Language-Models-in-Transformers">Loading SAM 3 and Qwen Vision-Language Models in Transformers</a></li>
    <li id="TOC-h2-Implementing-VLM-Inference-for-Agentic-Vision-Reasoning-with-Qwen25-VL"><a rel="noopener" target="_blank" href="#h2-Implementing-VLM-Inference-for-Agentic-Vision-Reasoning-with-Qwen25-VL">Implementing VLM Inference for Agentic Vision Reasoning with Qwen2.5-VL</a></li>
    <li id="TOC-h2-Implementing-the-SAM-3-Text-Prompted-Segmentation-Function"><a rel="noopener" target="_blank" href="#h2-Implementing-the-SAM-3-Text-Prompted-Segmentation-Function">Implementing the SAM 3 Text-Prompted Segmentation Function</a></li>
    <li id="TOC-h2-Implementing-the-Agentic-AI-Segmentation-Pipeline-with-Iterative-Refinement"><a rel="noopener" target="_blank" href="#h2-Implementing-the-Agentic-AI-Segmentation-Pipeline-with-Iterative-Refinement">Implementing the Agentic AI Segmentation Pipeline with Iterative Refinement</a></li>
    <li id="TOC-h2-Visualizing-and-Saving-the-Segmentation-Results"><a rel="noopener" target="_blank" href="#h2-Visualizing-and-Saving-the-Segmentation-Results">Visualizing and Saving the Segmentation Results</a></li>
    <li id="TOC-h2-Running-the-Agentic-AI-Vision-System-on-Real-Images"><a rel="noopener" target="_blank" href="#h2-Running-the-Agentic-AI-Vision-System-on-Real-Images">Running the Agentic AI Vision System on Real Images</a></li>
    <li id="TOC-h2-Agentic-Segmentation-Output-Iterative-Prompt-Refinement-in-Action"><a rel="noopener" target="_blank" href="#h2-Agentic-Segmentation-Output-Iterative-Prompt-Refinement-in-Action">Agentic Segmentation Output: Iterative Prompt Refinement in Action</a></li>
    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Agentic-AI-Vision-System-Object-Segmentation-with-SAM-3-and-Qwen"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Agentic-AI-Vision-System-Object-Segmentation-with-SAM-3-and-Qwen">Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</a></h2>



<p>This lesson is the <strong>4th and final part</strong> of our series on <strong>SAM 3</strong>. In the previous parts, we built a strong foundation for concept-aware segmentation.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?lossy=2&strip=1&webp=1" alt="building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png" class="wp-image-53381" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?size=126x105&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured-300x249.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?size=378x314&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?size=504x418&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?size=630x523&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured-768x637.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/building-an-agentic-ai-vision-system-with-sam-3-and-qwen-featured.png?lossy=2&amp;strip=1&amp;webp=1 940w" sizes="(max-width: 630px) 100vw, 630px" /></a></figure></div>


<p>In <strong><a href="https://pyimg.co/uming" target="_blank" rel="noreferrer noopener">Part 1</a></strong>, we introduced the fundamentals of SAM 3 and explored how it enables <strong>concept-based visual understanding and segmentation</strong>. We moved beyond fixed labels and used natural language to describe objects.</p>



<p>In <strong><a href="https://pyimg.co/5c4ag" target="_blank" rel="noreferrer noopener">Part 2</a></strong>, we extended this idea by introducing <strong>multi-modal prompting and interactive segmentation</strong>. We combined text, points, and bounding boxes to gain more precise control over segmentation.</p>



<p>In <strong><a href="https://pyimg.co/luxfd" target="_blank" rel="noreferrer noopener">Part 3</a></strong>, we extended this into the temporal domain. We applied SAM 3 to videos and built systems for <strong>concept-aware segmentation and object tracking across frames</strong>.</p>



<p>In this final part, we take a major step forward. Instead of treating segmentation as a single-step prediction, we introduce an <strong>agentic AI system</strong> that can reason, verify, and iteratively refine its outputs.</p>



<p>This lesson is the last of a 4-part series on <strong>SAM 3</strong>:</p>



<ol class="wp-block-list">
<li><em><a href="https://pyimg.co/uming" target="_blank" rel="noreferrer noopener">SAM 3: Concept-Based Visual Understanding and Segmentation</a></em></li>



<li><em><a href="https://pyimg.co/5c4ag" target="_blank" rel="noreferrer noopener">Advanced SAM 3: Multi-Modal Prompting and Interactive Segmentation</a></em></li>



<li><em><a href="https://pyimg.co/luxfd" target="_blank" rel="noreferrer noopener">SAM 3 for Video: Concept-Aware Segmentation and Object Tracking</a></em></li>



<li><em><strong><a href="https://pyimg.co/ohlwd" target="_blank" rel="noreferrer noopener">Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</a></strong></em> <strong>(this tutorial)</strong></li>
</ol>



<p><strong>To learn how to build an Agentic AI Vision System with SAM</strong> <strong>3 and Qwen, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Agentic-AI-Outperforms-Traditional-Vision-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Agentic-AI-Outperforms-Traditional-Vision-Pipelines">Why Agentic AI Outperforms Traditional Vision Pipelines </a></h2>



<p>Modern computer vision systems are evolving beyond traditional pipelines.</p>



<p>We designed systems where:</p>



<ul class="wp-block-list">
<li>an image is passed to a vision model</li>



<li>the model produces a prediction</li>



<li>the pipeline ends there</li>
</ul>



<p>This approach works well for clearly defined tasks. However, it struggles when tasks require <strong>understanding intent, </strong><strong>handling </strong><strong>ambiguity, or refin</strong><strong>ing outputs</strong>.</p>



<p>To address this, we now transition toward <strong>agentic AI systems</strong>.</p>



<p>Agentic systems are not limited to a single prediction. Instead, they behave more like an iterative reasoning loop.</p>



<p>They can:</p>



<ul class="wp-block-list">
<li>interpret a user request</li>



<li>select the appropriate models or tools</li>



<li>evaluate intermediate outputs</li>



<li>refine their decisions over multiple steps</li>
</ul>



<p>This shift allows us to build systems that are <strong>adaptive, iterative, and self-correcting</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Agentic-AI-Improves-Computer-Vision-and-Segmentation-Tasks"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Agentic-AI-Improves-Computer-Vision-and-Segmentation-Tasks">Why Agentic AI Improves Computer Vision and Segmentation Tasks </a></h2>



<p>Vision tasks are often ambiguous.</p>



<p>For example, consider the instruction:</p>



<ul class="wp-block-list">
<li><em>“the bag on the leftmost side”</em></li>
</ul>



<p>A traditional segmentation model cannot directly handle this:</p>



<ul class="wp-block-list">
<li>it expects fixed labels like <em>“bag”</em></li>



<li>it does not understand spatial reasoning like <em>“leftmost”</em></li>
</ul>



<p>This is where agentic design becomes powerful.</p>



<p>We introduce a <strong>Vision-Language Model (VLM)</strong> to:</p>



<ul class="wp-block-list">
<li>understand the instruction</li>



<li>extract the correct intent</li>



<li>translate it into a form usable by a segmentation model</li>
</ul>



<p>Then, instead of trusting the output blindly, we:</p>



<ul class="wp-block-list">
<li>verify the result</li>



<li>refine the input if needed</li>



<li>retry the process</li>
</ul>



<p>This creates a loop where the system continuously improves.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-What-We-Will-Build-An-Agentic-AI-Vision-and-Segmentation-System"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-What-We-Will-Build-An-Agentic-AI-Vision-and-Segmentation-System">What We Will Build: An Agentic AI Vision and Segmentation System</a></h2>



<p>In this lesson, we build an <strong>agentic segmentation system</strong> that combines reasoning with perception.</p>



<p>The system takes:</p>



<ul class="wp-block-list">
<li>an image</li>



<li>a natural language instruction</li>
</ul>



<p>and produces:</p>



<ul class="wp-block-list">
<li>segmentation masks</li>



<li>bounding boxes</li>



<li>confidence scores</li>



<li>a final overlay visualization</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Agentic-AI-Workflow-Vision-Language-Reasoning-and-Segmentation-Loop"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Agentic-AI-Workflow-Vision-Language-Reasoning-and-Segmentation-Loop">Agentic AI Workflow: Vision-Language Reasoning and Segmentation Loop</a></h2>



<p>The pipeline follows these steps:</p>



<ul class="wp-block-list">
<li><strong>User Input: </strong>First, we provide an image along with a natural language instruction.</li>



<li><strong>Instruction Understanding (VLM): </strong>Next, the VLM processes both the image and the text. It extracts the core intent and converts it into a short concept.</li>



<li><strong>Concept Simplification: </strong>The system converts complex instructions into concise phrases. For example:
<ul class="wp-block-list">
<li><em>“the bag on the leftmost side” → “leftmost bag”</em></li>
</ul>
</li>



<li><strong>Segmentation </strong><strong>(SAM3): </strong>Then, SAM3 uses this concept to generate:
<ul class="wp-block-list">
<li>segmentation masks</li>



<li>bounding boxes</li>



<li>confidence scores</li>
</ul>
</li>



<li><strong>Verification (VLM): </strong>After segmentation, the VLM evaluates whether the output matches the instruction.</li>



<li><strong>Refinement Loop: </strong>If the result is incorrect:
<ul class="wp-block-list">
<li>the VLM refines the concept</li>



<li>SAM3 runs again</li>



<li>the process repeats</li>
</ul>
</li>



<li>This loop continues until the result aligns with the user’s intent.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Agentic-AI-Architecture-Combining-VLMs-and-SAM-3-for-Vision"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Agentic-AI-Architecture-Combining-VLMs-and-SAM-3-for-Vision">Agentic AI Architecture: Combining VLMs and SAM 3 for Vision</a></h2>



<p>Before implementing the code, we break down the system into its core components.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Vision-Language-Model-VLM-The-Reasoning-Component"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Vision-Language-Model-VLM-The-Reasoning-Component">Vision-Language Model (VLM): The Reasoning Component</a></h3>



<p>The VLM is the <strong>reasoning component</strong> of our system. It performs 3 key roles:</p>



<p><strong>Instruction Understanding.</strong> It interprets the natural language input in the context of the image.</p>



<p><strong>Concept Generation.</strong> It converts long instructions into short, structured phrases. For example:</p>



<ul class="wp-block-list">
<li><em>“the person wearing a red shirt” → “person red shirt”</em></li>



<li><em>“the car in the background” → “background car”</em></li>
</ul>



<p>This step is critical because segmentation models perform better with:</p>



<ul class="wp-block-list">
<li>short</li>



<li>object-centric</li>



<li>unambiguous phrases</li>
</ul>



<p><strong>Result Verification.</strong> After segmentation, the VLM checks:</p>



<ul class="wp-block-list">
<li>whether the correct object was segmented</li>



<li>whether spatial or contextual constraints are satisfied</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-SAM-3-Segmentation-Model-Open-Vocabulary-Object-Segmentation"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-SAM-3-Segmentation-Model-Open-Vocabulary-Object-Segmentation">SAM 3: Open-Vocabulary Object Segmentation</a></h3>



<p>SAM3 acts as the <strong>perception component</strong>.</p>



<p>Unlike traditional segmentation models, SAM3 supports:</p>



<ul class="wp-block-list">
<li>flexible prompts</li>



<li>open-vocabulary segmentation</li>
</ul>



<p>This means we are not restricted to predefined classes.</p>



<p>Given a concept phrase, SAM3 produces:</p>



<ul class="wp-block-list">
<li>pixel-level segmentation masks</li>



<li>bounding boxes</li>



<li>confidence scores</li>
</ul>



<p>This makes SAM3 ideal for integration with a language-based reasoning system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-The-Agentic-Feedback-Loop-Reasoning-Verification-and-Refinement"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-The-Agentic-Feedback-Loop-Reasoning-Verification-and-Refinement">The Agentic Feedback Loop: Reasoning, Verification, and Refinement</a></h3>



<p>The most important part of this system is the <strong>agentic loop</strong>.</p>



<p>Instead of a linear pipeline, we build a <strong>feedback-driven process</strong>.</p>



<p><strong>Step-by-step:</strong></p>



<ul class="wp-block-list">
<li>Generate a segmentation concept</li>



<li>Run segmentation using SAM3</li>



<li>Evaluate the output using the VLM</li>
</ul>



<p>If the output is incorrect:</p>



<ul class="wp-block-list">
<li>identify what went wrong</li>



<li>refine the concept</li>



<li>retry segmentation</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Why-Agentic-Segmentation-Outperforms-One-Shot-Models"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Why-Agentic-Segmentation-Outperforms-One-Shot-Models">Why Agentic Segmentation Outperforms One-Shot Models</a></h3>



<p>This loop introduces several important capabilities:</p>



<ul class="wp-block-list">
<li><strong>Self-correction: </strong>The system can recover from incorrect predictions</li>



<li><strong>Robustness: </strong>It handles ambiguous or complex instructions better</li>



<li><strong>Generalization: </strong>It works with open-ended language instead of fixed labels</li>



<li><strong>Improved alignment: </strong>Outputs better match user intent over iterations</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Final-Output-Agentic-Vision-System-with-Segmentation-and-Reasoning"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Final-Output-Agentic-Vision-System-with-Segmentation-and-Reasoning">Final Output: Agentic Vision System with Segmentation and Reasoning</a></h2>



<p>By the end of this tutorial, we build a system that:</p>



<ul class="wp-block-list">
<li>understands natural language instructions</li>



<li>converts them into structured segmentation concepts</li>



<li>performs open-vocabulary segmentation</li>



<li>verifies its own outputs</li>



<li>improves results through iterative refinement</li>
</ul>



<p>This represents a shift </p>



<p>from:</p>



<ul class="wp-block-list">
<li>static, one-shot predictions</li>
</ul>



<p>to:</p>



<ul class="wp-block-list">
<li><strong>dynamic, reasoning-driven vision systems</strong></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Key-Takeaway-VLM-SAM-3-Intelligent-Vision-Agent"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Key-Takeaway-VLM-SAM-3-Intelligent-Vision-Agent">Key Takeaway: VLM + SAM 3 = Intelligent Vision Agent</a></h2>



<p>The real power of this system is not just segmentation.</p>



<p>It is the <strong>collaboration between models</strong>:</p>



<ul class="wp-block-list">
<li>the VLM provides reasoning</li>



<li>SAM3 provides perception</li>



<li>the loop provides intelligence</li>
</ul>



<p>Together, they form an <strong>agentic vision system</strong> that can think, act, and improve.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Your-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Your-Development-Environment">Configuring Your Development Environment</a></h2>



<p>To follow this guide, you need to have the following libraries installed on your system.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="1">!pip install -q transformers accelerate pillow torch torchvision bitsandbytes
</pre>



<p>First, we install the <code data-enlighter-language="python" class="EnlighterJSRAW">transformers</code> library. This library provides access to a wide range of pretrained models, including the Vision-Language Model we will use in this project.</p>



<p>Next, we install <code data-enlighter-language="python" class="EnlighterJSRAW">accelerate</code>, which helps efficiently run large models across GPUs and manage device placement automatically.</p>



<p>After that, we install <code data-enlighter-language="python" class="EnlighterJSRAW">pillow</code>, a lightweight Python library used for image loading and processing. We will use this library to read images and prepare them for model inference.</p>



<p>We also install <code data-enlighter-language="python" class="EnlighterJSRAW">torch</code>, which serves as the core deep learning framework for this project. Both the Vision-Language Model and the segmentation model rely on <code data-enlighter-language="python" class="EnlighterJSRAW">torch</code> for tensor computations and GPU acceleration.</p>



<p>Along with <code data-enlighter-language="python" class="EnlighterJSRAW">torch</code>, we install <code data-enlighter-language="python" class="EnlighterJSRAW">torchvision</code>, which provides datasets, transforms, and model utilities for computer vision tasks.</p>



<p>Finally, we install <code data-enlighter-language="python" class="EnlighterJSRAW">bitsandbytes</code>. This library enables efficient memory usage when working with large models by supporting quantization and optimized GPU kernels.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">-q</code> flag runs the installation in quiet mode, reducing unnecessary output in the notebook.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Python-Setup-and-Imports-for-Agentic-AI-Vision-System"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Python-Setup-and-Imports-for-Agentic-AI-Vision-System">Python Setup and Imports for Agentic AI Vision System</a></h2>



<p>Now that our environment is ready, we import the libraries required to build our agentic vision system. These libraries will help us perform deep learning inference, process images, visualize segmentation outputs, and load the models.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="2">import torch
import numpy as np
import os
import json
from PIL import Image, ImageDraw
import matplotlib
import matplotlib.pyplot as plt
from transformers import (
      AutoProcessor,
   Qwen2_5_VLForConditionalGeneration,
   Sam3Model,
   Sam3Processor,
)
</pre>



<p>First, we import <code data-enlighter-language="python" class="EnlighterJSRAW">torch</code>. This is the primary deep learning framework used to run both the Vision-Language Model and the segmentation model. PyTorch handles tensor computations and GPU acceleration during inference.</p>



<p>Next, we import <code data-enlighter-language="python" class="EnlighterJSRAW">numpy</code>, a popular library for numerical computing in Python. We will use NumPy when working with arrays such as segmentation masks and bounding boxes returned by the segmentation model.</p>



<p>After that, we import the <code data-enlighter-language="python" class="EnlighterJSRAW">os</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">json</code> libraries. The <code data-enlighter-language="python" class="EnlighterJSRAW">os</code> module helps us manage file paths and directories, while the <code data-enlighter-language="python" class="EnlighterJSRAW">json</code> module allows us to parse structured responses generated by the Vision-Language Model.</p>



<p>Next, we import <code data-enlighter-language="python" class="EnlighterJSRAW">Image</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">ImageDraw</code> from the <strong>Pillow</strong> library. Pillow is a lightweight image processing library that allows us to load, manipulate, and display images. In this project, we will use it to read input images and create segmentation overlays.</p>



<p>Then, we import <code data-enlighter-language="python" class="EnlighterJSRAW">matplotlib</code>, which we will use to visualize the results. Specifically, we use <code data-enlighter-language="python" class="EnlighterJSRAW">matplotlib.pyplot</code> to create figures that display the original image, bounding boxes, and segmentation masks.</p>



<p>Finally, we import several classes from the <code data-enlighter-language="python" class="EnlighterJSRAW">transformers</code> library. These classes allow us to load and run the models used in our system.</p>



<ul class="wp-block-list">
<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor</code> class automatically prepares inputs for multimodal models by handling both text and image preprocessing.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">Qwen2_5_VLForConditionalGeneration</code> class loads the <strong>Qwen2.5-VL Vision-Language Model</strong>, which will interpret user instructions and generate segmentation prompts.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">Sam3Model</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">Sam3Processor</code> classes load the <strong>SAM3 segmentation model</strong> and prepare its inputs.</li>
</ul>



<p>Before loading the models, we configure PyTorch to use optimized GPU settings. These settings help improve inference performance, especially when running large multimodal models.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="3">torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype  = torch.bfloat16 if device == "cuda" else torch.float32
print(f"Using device: {device}, dtype: {dtype}")
</pre>



<p>First, we enable <strong>TensorFloat-32 (TF32)</strong> support in PyTorch. TF32 is a numerical format supported by modern NVIDIA GPUs. It allows faster matrix multiplications during deep learning inference while maintaining good numerical stability. Since large models perform many matrix operations, enabling TF32 can significantly improve performance.</p>



<p>Next, we determine which device will be used for inference. Here, we check whether a CUDA-enabled GPU is available. If a GPU is detected, the system runs on <code data-enlighter-language="python" class="EnlighterJSRAW">"cuda"</code>. Otherwise, it falls back to the CPU.</p>



<p>After that, we configure the <strong>tensor precision</strong>. When running on a GPU, we use <strong>bfloat16 precision</strong>. This reduces memory usage and speeds up computation while preserving enough numerical accuracy for inference tasks.</p>



<p>If the system runs on a CPU, we instead use the standard <strong>float32 precision</strong>, which ensures compatibility with CPU computations.</p>



<p>Finally, we print the device configuration. This helps confirm whether the system is using the GPU and which precision mode is active. This information is useful when debugging performance or memory issues during model inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Loading-SAM-3-and-Qwen-Vision-Language-Models-in-Transformers"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Loading-SAM-3-and-Qwen-Vision-Language-Models-in-Transformers">Loading SAM 3 and Qwen Vision-Language Models in Transformers</a></h2>



<p>Now that the environment is configured, we load the two core models used in our agentic vision system: a <strong>Vision-Language Model (VLM)</strong> and a <strong>segmentation model</strong>.</p>



<p>The VLM will interpret the user’s instruction and generate a clean segmentation concept. The segmentation model will then use that concept to detect and segment objects in the image.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="4">VLM_MODEL_ID = "Qwen/Qwen2.5-VL-7B-Instruct"  # swap for Qwen/Qwen3-VL-8B once released in transformers
SAM_MODEL_ID = "facebook/sam3"

print("Loading VLM...")
vlm_processor = AutoProcessor.from_pretrained(VLM_MODEL_ID, trust_remote_code=True)
vlm_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
   VLM_MODEL_ID,
   device_map="auto",
   torch_dtype=dtype,
   trust_remote_code=True,
)
vlm_model.eval()
print("VLM loaded.")

print("Loading SAM3...")
sam_processor = Sam3Processor.from_pretrained(SAM_MODEL_ID)
sam_model = Sam3Model.from_pretrained(SAM_MODEL_ID, torch_dtype=dtype).to(device)
sam_model.eval()
print("SAM3 loaded.")
</pre>



<p>First, we define the model identifiers. These identifiers correspond to the pretrained models hosted on the Hugging Face model hub.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">Qwen2.5-VL-7B-Instruct</code> model is a <strong>Vision-Language Model</strong> capable of understanding both images and text instructions. We will use this model to interpret the user’s request and generate segmentation prompts.</p>



<p>The second model, <strong>SAM3</strong>, is an open-vocabulary segmentation model that can segment objects based on text prompts.</p>



<p>Next, we load the Vision-Language Model. We first load the <strong>processor</strong> associated with the model. The processor prepares the inputs required by the VLM, including tokenizing text prompts and preprocessing images.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">trust_remote_code=True</code> argument allows the Transformers library to load custom processing code provided by the model repository.</p>



<p>Next, we load the model itself. The <code data-enlighter-language="python" class="EnlighterJSRAW">from_pretrained()</code> method downloads the pretrained model weights and initializes the model architecture.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">device_map="auto"</code> argument automatically distributes the model across available devices, which is useful when working with large models that require GPU memory.</p>



<p>We also specify <code data-enlighter-language="python" class="EnlighterJSRAW">torch_dtype=dtype</code>, which ensures the model runs using the precision we configured earlier: <strong>bfloat16 on GPU</strong> or <strong>float32 on CPU</strong>.</p>



<p>After loading the model, we switch it to evaluation mode. Evaluation mode disables training-specific behaviors such as dropout, ensuring consistent inference results.</p>



<p>Next, we load the segmentation model. Similar to the VLM, we first load the <code data-enlighter-language="python" class="EnlighterJSRAW">Sam3Processor</code>. This processor handles preprocessing tasks such as preparing the input image and formatting segmentation prompts.</p>



<p>Next, we load the SAM3 model. The <code data-enlighter-language="python" class="EnlighterJSRAW">from_pretrained()</code> function loads the segmentation model weights, and we move the model to the appropriate device using <code data-enlighter-language="python" class="EnlighterJSRAW">.to(device)</code>.</p>



<p>Finally, we set the model to evaluation mode. At this point, both models are fully initialized. The Vision-Language Model will interpret user instructions, while SAM3 will perform open-vocabulary segmentation based on those instructions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Implementing-VLM-Inference-for-Agentic-Vision-Reasoning-with-Qwen25-VL"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Implementing-VLM-Inference-for-Agentic-Vision-Reasoning-with-Qwen25-VL">Implementing VLM Inference for Agentic Vision Reasoning with Qwen2.5-VL</a></h2>



<p>Now that our models are loaded, we implement a helper function that allows us to run inference using the Vision-Language Model. This function will take an image and a list of chat messages as input and return the model’s response.</p>



<p>In our agentic pipeline, this function plays a very important role. We will use it to:</p>



<ul class="wp-block-list">
<li>extract a clean segmentation prompt from the user instruction</li>



<li>refine prompts if segmentation fails</li>



<li>verify whether the segmentation results match the user intent</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="5">def vlm_generate(image: Image.Image, messages: list, max_new_tokens: int = 512) -> str:
   """
   Mirrors: send_generate_request()
   Runs VLM inference given a list of chat messages and returns the reply string.
   """
   text_input = vlm_processor.apply_chat_template(
       messages, tokenize=False, add_generation_prompt=True
   )
   inputs = vlm_processor(
       text=[text_input],
       images=[image],
       return_tensors="pt",
   )
   inputs = {k: v.to(vlm_model.device) for k, v in inputs.items()}
   input_len = inputs["input_ids"].shape[1]

   with torch.no_grad():
       generated_ids = vlm_model.generate(
           **inputs,
           max_new_tokens=max_new_tokens,
           do_sample=False,
       )

   new_tokens = generated_ids[0][input_len:]
   return vlm_processor.tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
</pre>



<p>First, we define the function <code data-enlighter-language="python" class="EnlighterJSRAW">vlm_generate</code>. This function takes three inputs:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">image</code>: the input image that the model will analyze</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">messages</code>: a list of chat-style prompts used to guide the model</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_new_tokens</code>: the maximum number of tokens the model can generate</li>
</ul>



<p>The function returns a <strong>string response produced by the Vision-Language Model</strong>.</p>



<p>Next, we convert the chat messages into the format expected by the model. Many modern Vision-Language Models use a <strong>chat-style interface</strong> similar to conversational AI systems. The <code data-enlighter-language="python" class="EnlighterJSRAW">apply_chat_template()</code> method converts the list of messages into a properly formatted text prompt that the model understands.</p>



<p>The argument <code data-enlighter-language="python" class="EnlighterJSRAW">add_generation_prompt=True</code> tells the processor that the model should generate a response after the provided messages.</p>



<p>Next, we prepare the inputs for the model. Here, we pass both the text prompt and the image to the processor. The processor converts these inputs into tensors that can be processed by the model. The argument <code data-enlighter-language="python" class="EnlighterJSRAW">return_tensors="pt"</code> ensures the outputs are returned as <strong>PyTorch tensors</strong>.</p>



<p>Next, we move the tensors to the same device as the model. This step ensures that both the model and the input tensors reside on the same device, either the GPU or CPU.</p>



<p>After that, we store the length of the input tokens. This value helps us determine which tokens belong to the <strong>model&#8217;s generated response</strong>, rather than the original prompt.</p>



<p>Next, we perform inference using the model. We use <code data-enlighter-language="python" class="EnlighterJSRAW">torch.no_grad()</code> to disable gradient computations. Since we are only performing inference, this reduces memory usage and improves performance.</p>



<p>Inside this block, we generate the model’s output. The <code data-enlighter-language="python" class="EnlighterJSRAW">generate()</code> function performs autoregressive text generation. The parameter <code data-enlighter-language="python" class="EnlighterJSRAW">max_new_tokens</code> limits the length of the generated response. We also set <code data-enlighter-language="python" class="EnlighterJSRAW">do_sample=False</code>, which ensures deterministic outputs instead of random sampling.</p>



<p>Next, we extract only the tokens generated by the model. This removes the original prompt tokens, leaving only the newly generated tokens.</p>



<p>Finally, we convert the generated tokens into readable text. The <code data-enlighter-language="python" class="EnlighterJSRAW">decode()</code> method converts token IDs back into text. We also remove special tokens and strip unnecessary whitespace.</p>



<p>At this point, the function returns the <strong>final response generated by the Vision-Language Model</strong>.</p>



<p>This function will serve as the core interface between our agentic system and the Vision-Language Model. In the next sections, we will use it to extract segmentation prompts and evaluate the outputs produced by the segmentation model.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Implementing-the-SAM-3-Text-Prompted-Segmentation-Function"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Implementing-the-SAM-3-Text-Prompted-Segmentation-Function">Implementing the SAM 3 Text-Prompted Segmentation Function</a></h2>



<p>Now, we implement a helper function that runs segmentation using the SAM3 model. This function will take an input image and optional prompts, run the SAM3 model, and return the segmentation results.</p>



<p>In our agentic pipeline, this function serves as the <strong>tool used by the agent</strong> to perform segmentation.</p>



<p>Specifically, it returns three important outputs:</p>



<ul class="wp-block-list">
<li>segmentation masks</li>



<li>bounding boxes</li>



<li>confidence scores</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="6">def call_sam(
   image: Image.Image,
   text_prompt: str   = None,
   input_boxes        = None,   # list of [x1,y1,x2,y2]
   input_boxes_labels = None,   # list of 0/1 labels per box
   threshold: float   = 0.5,
) -> dict:
   """
   Mirrors: call_sam_service()
   Returns dict with keys: masks, boxes, scores (all as numpy arrays).
   """
   kwargs = dict(images=image, return_tensors="pt")
   if text_prompt:
       kwargs["text"] = text_prompt
   if input_boxes is not None:
       kwargs["input_boxes"] = [input_boxes]
       kwargs["input_boxes_labels"] = [input_boxes_labels or [1] * len(input_boxes)]

   inputs = sam_processor(**kwargs).to(device)

   with torch.no_grad():
       outputs = sam_model(**inputs)

   results = sam_processor.post_process_instance_segmentation(
       outputs,
       threshold=threshold,
       mask_threshold=0.5,
       target_sizes=inputs.get("original_sizes").tolist(),
   )[0]

   return {
       "masks":  results["masks"].cpu().numpy(),                          # [N, H, W] bool
       "boxes":  results["boxes"].cpu().to(torch.float32).numpy(),        # [N, 4]    xyxy
       "scores": results["scores"].cpu().to(torch.float32).numpy(),       # [N]
   }
</pre>



<p>First, we define the function <code data-enlighter-language="python" class="EnlighterJSRAW">call_sam</code>. This function accepts several inputs:</p>



<ul class="wp-block-list">
<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">image</code> parameter is the input image that we want to segment.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">text_prompt</code> parameter allows us to perform <strong>concept-based segmentation</strong>. SAM3 can segment objects using natural language prompts such as <code data-enlighter-language="python" class="EnlighterJSRAW">"bag"</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">"leftmost bag"</code>.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">input_boxes</code> parameter allows us to guide the segmentation model using bounding boxes. Each box is defined by four coordinates: [x1, y1, x2, y2]</li>



<li>Similarly, <code data-enlighter-language="python" class="EnlighterJSRAW">input_boxes_labels</code> specifies whether each box corresponds to a <strong>positive or negative prompt</strong>.</li>



<li>Finally, the <code data-enlighter-language="python" class="EnlighterJSRAW">threshold</code> parameter determines the confidence threshold used when filtering segmentation results.</li>
</ul>



<p>Next, we prepare the inputs required by the SAM3 processor.</p>



<p>Here, we create a dictionary containing the image input. The <code data-enlighter-language="python" class="EnlighterJSRAW">return_tensors="pt"</code> argument ensures that the processed outputs are returned as <strong>PyTorch tensors</strong>.</p>



<p>If a text prompt is provided, we include it in the input dictionary. This allows SAM3 to perform <strong>text-guided segmentation</strong>.</p>



<p>Next, we check whether bounding boxes are provided. If bounding boxes exist, we pass them to the processor along with their labels. If no labels are specified, we automatically assign <strong>positive labels (1)</strong> to all boxes.</p>



<p>Next, we preprocess the inputs using the SAM3 processor. The processor converts the image, prompts, and bounding boxes into tensors that the model can understand. We also move these tensors to the selected device (GPU or CPU).</p>



<p>Now we perform inference using SAM3. We wrap the inference step inside <code data-enlighter-language="python" class="EnlighterJSRAW">torch.no_grad()</code> to disable gradient calculations. Since we are performing inference only, this improves performance and reduces memory usage. The model returns raw segmentation outputs.</p>



<p>Next, we convert the raw model outputs into usable segmentation results. The <code data-enlighter-language="python" class="EnlighterJSRAW">post_process_instance_segmentation()</code> function performs several important tasks:</p>



<ul class="wp-block-list">
<li>filters predictions using the confidence threshold</li>



<li>converts predicted masks to the correct image resolution</li>



<li>extracts bounding boxes and scores</li>
</ul>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">[0]</code> index retrieves the results corresponding to the input image.</p>



<p>Finally, we return the segmentation results. The function returns a dictionary containing three elements.</p>



<ul class="wp-block-list">
<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">masks</code> array contains the segmentation masks with shape: [N, H, W] where <strong>N</strong> represents the number of detected objects.</li>



<li>The <code data-enlighter-language="python" class="EnlighterJSRAW">boxes</code> array contains the bounding box coordinates in the format: [x1, y1, x2, y2]</li>



<li>Finally, the <code data-enlighter-language="python" class="EnlighterJSRAW">scores</code> array contains the confidence score for each detected object.</li>
</ul>



<p>We also move the tensors to the CPU and convert them into <strong>NumPy arrays</strong>. This makes them easier to process and visualize in later steps.</p>



<p>At this point, the <code data-enlighter-language="python" class="EnlighterJSRAW">call_sam()</code> function provides a simple interface for running <strong>SAM3 segmentation</strong> within our agentic vision pipeline.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Implementing-the-Agentic-AI-Segmentation-Pipeline-with-Iterative-Refinement"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Implementing-the-Agentic-AI-Segmentation-Pipeline-with-Iterative-Refinement">Implementing the Agentic AI Segmentation Pipeline with Iterative Refinement</a></h2>



<p>Now we implement the <strong>core function of our system</strong>. This function orchestrates the entire agentic workflow by combining the Vision-Language Model and the segmentation model.</p>



<p>Instead of running segmentation only once, the system follows an <strong>agentic loop</strong> where the Vision-Language Model interprets the user request, runs segmentation, verifies the result, and refines the prompt if needed.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="7">def run_single_image_inference(
   image_path: str,
   user_prompt: str,
   max_agent_rounds: int = 3,
   seg_threshold: float  = 0.5,
   output_dir: str       = "agent_output",
   debug: bool           = True,
) -> str | None:
   """
   Mirrors: run_single_image_inference() from sam3.agent.inference

   Agentic loop:
     Round 1 — VLM reads image + user prompt → produces a concise SAM3 concept phrase
     Round 2 — SAM3 segments with that phrase → VLM verifies / refines if needed
     Round N — repeat until VLM is satisfied or max_agent_rounds reached
   Returns path to the saved output image (or None on failure).
   """
   os.makedirs(output_dir, exist_ok=True)
   image = Image.open(image_path).convert("RGB")

   # ── Round 1: VLM extracts a clean SAM3 text prompt ──────────────────────
   extraction_messages = [
       {
           "role": "system",
           "content": (
               "You are a precise vision assistant. "
               "Your job is to convert a user's free-form description into a SHORT, "
               "clean object concept phrase suitable for an open-vocabulary segmentation model. "
               "Reply with ONLY a JSON object: {\"sam_prompt\": \"&lt;phrase>\"}. "
               "No explanation, no markdown, just the JSON."
           ),
       },
       {
           "role": "user",
           "content": [
               {"type": "image", "image": image},
               {"type": "text",  "text": f"User description: \"{user_prompt}\""},
           ],
       },
   ]

</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">run_single_image_inference</code> function serves as the <strong>main entry point of our agentic vision system</strong>. It accepts several inputs:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">image_path</code>: the path to the image we want to analyze</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">user_prompt</code>: the natural language description of the object to segment</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_agent_rounds</code>: the maximum number of refinement iterations</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">seg_threshold</code>: the confidence threshold for segmentation</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">output_dir</code>: the directory where the output image will be saved</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">debug</code>: a flag that enables detailed logging</li>
</ul>



<p>The function returns the <strong>path of the saved output image</strong> or <code data-enlighter-language="python" class="EnlighterJSRAW">None</code> if segmentation fails.</p>



<p>First, we create the output directory and load the image. The <code data-enlighter-language="python" class="EnlighterJSRAW">os.makedirs()</code> function ensures that the output directory exists. If the directory already exists, the <code data-enlighter-language="python" class="EnlighterJSRAW">exist_ok=True</code> argument prevents an error. Next, we open the input image using Pillow and convert it to RGB format.</p>



<p>Here, we define a <strong>system message</strong> that instructs the Vision-Language Model to convert the user description into a short concept phrase. The SAM3 model performs better with <strong>short noun-style prompts</strong> such as: </p>



<ul class="wp-block-list">
<li>leftmost bag</li>



<li>red apple</li>



<li>wooden chair</li>
</ul>



<p>rather than long sentences.</p>



<p>We also include the user input. This message contains both the image and the user instruction. </p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="42" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="8">if debug:
       print(f"\n[Agent] Round 1 — extracting SAM3 prompt from: '{user_prompt}'")

   vlm_reply = vlm_generate(image, extraction_messages)
   if debug:
       print(f"[Agent] VLM raw reply: {vlm_reply}")

   # Parse the JSON; fall back to raw reply if needed
   try:
       clean = vlm_reply.strip().lstrip("```json").rstrip("```").strip()
       sam_prompt = json.loads(clean)["sam_prompt"]
   except Exception:
       sam_prompt = user_prompt  # graceful fallback
   if debug:
       print(f"[Agent] SAM3 prompt → '{sam_prompt}'")
</pre>



<p>Next, we call the VLM inference function. The Vision-Language Model analyzes the image and generates a <strong>clean segmentation prompt</strong>.</p>



<p>For example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="9">User prompt: "the bag on the leftmost side"
Model output: {"sam_prompt": "leftmost bag"}
</pre>



<p>Next, we extract the segmentation prompt from the JSON response. This step removes formatting artifacts and converts the JSON string into a Python dictionary.</p>



<p>If the response cannot be parsed, we fall back to the original user prompt.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="58" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="10"># ── Agentic segmentation loop ────────────────────────────────────────────
   sam_result = None
   final_prompt = sam_prompt

   for round_idx in range(max_agent_rounds):
       if debug:
           print(f"\n[Agent] Round {round_idx + 2} — calling SAM3 with '{final_prompt}'")

       sam_result = call_sam(image, text_prompt=final_prompt, threshold=seg_threshold)
       n_masks = len(sam_result["masks"])
       if debug:
           print(f"[Agent] SAM3 found {n_masks} instance(s)")

</pre>



<p>Now we begin the <strong>agentic segmentation loop</strong>. Here, we initialize two variables:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">sam_result</code>: stores the segmentation output</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">final_prompt</code>: stores the prompt used for segmentation</li>
</ul>



<p>Next, we enter the iterative loop. This loop allows the system to refine segmentation prompts up to a maximum number of rounds. </p>



<p>Inside the loop, we call the SAM3 segmentation function. This function returns segmentation results including masks, bounding boxes, and confidence scores.</p>



<p>Next, we count the number of detected objects. This value helps determine whether the segmentation succeeded.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="71" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="11">       # ── Verification: ask VLM if the result looks right ─────────────────
       if n_masks == 0:
           # No masks found — ask VLM to rephrase
           refine_messages = [
               {
                   "role": "system",
                   "content": (
                       "You are a vision assistant helping refine segmentation prompts. "
                       "The segmentation model found NO objects. "
                       "Suggest a simpler or broader alternative concept phrase. "
                       "Reply ONLY with JSON: {\"sam_prompt\": \"&lt;phrase>\"}."
                   ),
               },
               {
                   "role": "user",
                   "content": [
                       {"type": "image", "image": image},
                       {"type": "text",  "text": (
                           f"Original user intent: \"{user_prompt}\". "
                           f"Failed prompt: \"{final_prompt}\". "
                           "Suggest a better phrase."
                       )},
                   ],
               },
           ]
           vlm_reply = vlm_generate(image, refine_messages)
           if debug:
               print(f"[Agent] VLM refine reply: {vlm_reply}")
           try:
               clean = vlm_reply.strip().lstrip("```json").rstrip("```").strip()
               final_prompt = json.loads(clean)["sam_prompt"]
           except Exception:
               break  # give up if we can't parse
       </pre>



<p>If SAM3 fails to detect any objects, we ask the Vision-Language Model to refine the segmentation prompt. We construct a new prompt asking the model to generate a <strong>simpler or broader concept phrase</strong>.</p>



<p>For example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="12">Original prompt: "leftmost brown grocery bag"
Suggested prompt: "bag"
</pre>



<p>The VLM then generates a new segmentation prompt, and the loop repeats.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="105" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="13">else:
           # We have masks — ask VLM to verify they match the user intent
           verify_messages = [
               {
                   "role": "system",
                   "content": (
                       "You are a vision QA assistant. "
                       "Given the original user intent and the segmentation result metadata, "
                       "decide if the segmentation is correct. "
                       "Reply ONLY with JSON: {\"ok\": true/false, \"reason\": \"...\", \"sam_prompt\": \"&lt;refined phrase if not ok>\"}."
                   ),
               },
               {
                   "role": "user",
                   "content": [
                       {"type": "image", "image": image},
                       {"type": "text",  "text": (
                           f"User intent: \"{user_prompt}\".\n"
                           f"SAM3 was given prompt: \"{final_prompt}\".\n"
                           f"Result: {n_masks} mask(s) found, "
                           f"scores: {sam_result['scores'].tolist()}, "
                           f"boxes: {sam_result['boxes'].tolist()}.\n"
                           "Is this correct? If yes, ok=true. If not, provide a better sam_prompt."
                       )},
                   ],
               },
           ]
           vlm_reply = vlm_generate(image, verify_messages, max_new_tokens=256)
           if debug:
               print(f"[Agent] VLM verify reply: {vlm_reply}")
           try:
               clean = vlm_reply.strip().lstrip("```json").rstrip("```").strip()
               verdict = json.loads(clean)
               if verdict.get("ok", True):
                   if debug:
                       print("[Agent] VLM verified result ✓ — stopping.")
                   break
               else:
                   final_prompt = verdict.get("sam_prompt", final_prompt)
                   if debug:
                       print(f"[Agent] VLM says not ok → retrying with '{final_prompt}'")
           except Exception:
               break  # can't parse verdict, accept current result</pre>



<p>If SAM3 successfully detects objects, we verify whether the result matches the user intent.</p>



<p>In this step, we ask the Vision-Language Model to evaluate the segmentation results.</p>



<p>The model receives:</p>



<ul class="wp-block-list">
<li>the original user instruction</li>



<li>the segmentation prompt used</li>



<li>the number of detected masks</li>



<li>the confidence scores</li>



<li>the bounding boxes</li>
</ul>



<p>Based on this information, the model decides whether the segmentation result is correct.</p>



<p>The model returns a JSON response such as:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="14">{
"ok": true,
"reason": "correct object detected"
}
</pre>



<p>or</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="15">{
"ok": false,
"sam_prompt": "bag"
}
</pre>



<p>If the segmentation is incorrect, the system updates the segmentation prompt. The loop then repeats using the new prompt. If the segmentation result is correct, the loop stops. This verification step allows the system to <strong>self-correct its segmentation decisions</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="149" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="16">   # ── Render and save output ───────────────────────────────────────────────
   if sam_result is None or len(sam_result["masks"]) == 0:
       print("[Agent] No masks produced — check your prompt or image.")
       return None

   output_path = os.path.join(
       output_dir,
       os.path.splitext(os.path.basename(image_path))[0] + "_segmented.png"
   )
   _save_overlay(image, sam_result, output_path, title=f'"{user_prompt}"')
   print(f"\n[Agent] Output saved → {output_path}")
   return output_path
</pre>



<p>After the agentic loop finishes, we check whether segmentation succeeded. If no objects were detected, the function returns <code data-enlighter-language="python" class="EnlighterJSRAW">None</code>. Otherwise, we generate the output image path.</p>



<p>Finally, we visualize the segmentation results. This function creates an image containing the segmentation masks and bounding boxes. The result is saved to disk.</p>



<p>This function implements the <strong>agentic reasoning loop</strong> that makes our system powerful.</p>



<p>Instead of relying on a single segmentation attempt, the system:</p>



<ul class="wp-block-list">
<li>interprets the user request</li>



<li>generates a segmentation prompt</li>



<li>runs segmentation</li>



<li>evaluates the results</li>



<li>refines the prompt if necessary</li>
</ul>



<p>This iterative process allows the system to produce more accurate results and demonstrates how multiple AI models can collaborate within an <strong>agentic vision pipeline</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Visualizing-and-Saving-the-Segmentation-Results"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Visualizing-and-Saving-the-Segmentation-Results">Visualizing and Saving the Segmentation Results</a></h2>



<p>After running the agentic segmentation pipeline, we want to visualize the results in a clear and interpretable way. For this purpose, we implement a helper function that overlays the segmentation masks and bounding boxes on top of the original image.</p>



<p>This function generates a side-by-side visualization showing both the detected bounding boxes and the segmentation masks.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="17">def _save_overlay(image: Image.Image, sam_result: dict, output_path: str, title: str = ""):
   masks  = sam_result["masks"]
   boxes  = sam_result["boxes"]
   scores = sam_result["scores"]

   fig, axes = plt.subplots(1, 2, figsize=(16, 8))

   # Left: original + boxes
   axes[0].imshow(image)
   axes[0].set_title(f"Detected boxes  |  {title}", fontsize=11)
   axes[0].axis("off")
   cmap = matplotlib.colormaps.get_cmap("rainbow").resampled(max(len(masks), 1))
   for i, (box, score) in enumerate(zip(boxes, scores)):
       x1, y1, x2, y2 = box
       color = cmap(i)[:3]
       rect = plt.Rectangle(
           (x1, y1), x2 - x1, y2 - y1,
           linewidth=2, edgecolor=color, facecolor="none"
       )
       axes[0].add_patch(rect)
       axes[0].text(x1, y1 - 4, f"{score:.2f}", color=color, fontsize=9, fontweight="bold")

   # Right: mask overlay
   composite = image.convert("RGBA")
   for i, mask in enumerate(masks):
       color = tuple(int(c * 255) for c in cmap(i)[:3])
       mask_img = Image.fromarray((mask * 255).astype(np.uint8))
       overlay  = Image.new("RGBA", composite.size, color + (0,))
       overlay.putalpha(mask_img.point(lambda v: int(v * 0.5)))
       composite = Image.alpha_composite(composite, overlay)

   axes[1].imshow(composite)
   axes[1].set_title(f"SAM3 masks  ({len(masks)} instance(s))", fontsize=11)
   axes[1].axis("off")

   plt.tight_layout()
   plt.savefig(output_path, dpi=150, bbox_inches="tight")
   plt.close()
</pre>



<p>We begin by defining the <code data-enlighter-language="python" class="EnlighterJSRAW">_save_overlay</code> function, which takes the original image, the segmentation output from SAM3, the output path, and an optional title. From the segmentation results, we extract the masks, bounding boxes, and confidence scores. The masks represent pixel-level regions for each detected object, the boxes define object boundaries, and the scores indicate how confident the model is for each detection.</p>



<p>To visualize these results, we create a figure with two side-by-side panels. The left panel displays the original image along with bounding boxes, while the right panel shows the segmentation masks overlaid on the image.</p>



<p>The process starts by rendering the original image and assigning a distinct color to each detected object using a colormap. For every detection, we draw a rectangle corresponding to its bounding box and place the confidence score near it. This provides a quick overview of what the model has detected and how reliable those detections are.</p>



<p>For the mask visualization, the image is first converted to RGBA format so that transparent overlays can be applied. Each segmentation mask is then assigned a color, converted into an image, and used to create a semi-transparent overlay. These overlays are composited onto the original image, allowing the segmented regions to stand out while still preserving the underlying content.</p>



<p>The final composite is displayed in the second panel, along with the number of detected instances. The visualization is then saved to disk using a resolution of 150 DPI for clarity, with <code data-enlighter-language="python" class="EnlighterJSRAW">tight_layout()</code> ensuring proper spacing and <code data-enlighter-language="python" class="EnlighterJSRAW">bbox_inches="tight"</code> removing unnecessary margins. The figure is closed afterward to free up memory.</p>



<p>This results in a clean and intuitive visualization that combines bounding boxes, confidence scores, and segmentation masks, making it easy to verify the model’s predictions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-the-Agentic-AI-Vision-System-on-Real-Images"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-the-Agentic-AI-Vision-System-on-Real-Images">Running the Agentic AI Vision System on Real Images</a></h2>



<p>Now that we have implemented all the components of our pipeline, we can run the complete agentic vision system on an example image.</p>



<p>In this step, we provide an image along with a natural language instruction and let the system handle the rest.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="18">output_image_path = run_single_image_inference(
   image_path  = "/content/groceries.jpg",
   user_prompt = "the bag on the leftmost side",
   max_agent_rounds = 3,
   seg_threshold    = 0.5,
   output_dir       = "agent_output",
   debug            = True,
)

if output_image_path:
   img = Image.open(output_image_path)
   img.show()
</pre>



<p>We begin by calling the <code data-enlighter-language="python" class="EnlighterJSRAW">run_single_image_inference()</code> function, which executes the complete agentic pipeline. The input image is provided through the <code data-enlighter-language="python" class="EnlighterJSRAW">image_path</code> parameter, and in this example, we use <code data-enlighter-language="python" class="EnlighterJSRAW">groceries.jpg</code>. Along with the image, we pass a natural language instruction — <em>&#8220;the bag on the leftmost side&#8221;</em>. This instruction is intentionally written in free-form language to demonstrate how the system can interpret human-like queries.</p>



<p>The pipeline is configured to allow up to three refinement iterations using <code data-enlighter-language="python" class="EnlighterJSRAW">max_agent_rounds=3</code>. A confidence threshold of <code data-enlighter-language="python" class="EnlighterJSRAW">0.5</code> is used to filter segmentation results, and the final output is saved to the <code data-enlighter-language="python" class="EnlighterJSRAW">agent_output</code> directory. Debugging is enabled to log intermediate steps such as prompt generation, segmentation outputs, and verification decisions.</p>



<p>Once the pipeline runs, it returns the path to the output image if segmentation is successful. We then load this image using Pillow and display it. The final visualization includes bounding boxes around detected objects, segmentation masks overlaid on the image, and confidence scores for each detection.</p>



<p>Under the hood, the system follows an iterative process. The Vision-Language Model first analyzes the image and converts the user’s instruction into a concise segmentation prompt. This prompt is passed to SAM3, which generates segmentation masks. The result is then evaluated by the Vision-Language Model to determine whether it matches the user’s intent. If the output is not satisfactory, the prompt is refined and the process repeats. Once the result is verified, the system produces the final visualization and saves it to disk.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Agentic-Segmentation-Output-Iterative-Prompt-Refinement-in-Action"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Agentic-Segmentation-Output-Iterative-Prompt-Refinement-in-Action">Agentic Segmentation Output: Iterative Prompt Refinement in Action</a></h2>



<p>The input image <strong>(Figure 1)</strong> shows multiple grocery bags placed inside the trunk of a car.</p>



<p>We provide the following natural language instruction:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="19">"the bag on the leftmost side"
</pre>



<p>This instruction is <strong>not a fixed label</strong>. Instead, it includes <strong>spatial reasoning</strong>, which makes the task more challenging for standard segmentation models.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-2.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="800" height="534" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-53398" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?size=126x84&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2-300x200.jpeg?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?size=378x252&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?size=504x336&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?size=630x421&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2-768x513.jpeg?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-2.jpeg?lossy=2&amp;strip=1&amp;webp=1 800w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Input Image (source: <a href="https://github.com/facebookresearch/sam3/blob/main/assets/images/groceries.jpg" target="_blank" rel="noreferrer noopener">Sam3 Official Repo assets</a>)</figcaption></figure></div>


<p>Now let’s examine how the system processes this instruction.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="20">[Agent] Round 1 — extracting SAM3 prompt from: 'the bag on the leftmost side'
[Agent] VLM raw reply: {"sam_prompt": "leftmost paper bag"}
</pre>



<p>First, the Vision-Language Model interprets the instruction and generates an initial segmentation prompt:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="21">[Agent] SAM3 prompt -> 'leftmost paper bag'

[Agent] Round 2 — calling SAM3 with 'leftmost paper bag'
[Agent] SAM3 found 0 instance(s)
</pre>



<p>Next, SAM3 attempts segmentation using this prompt.</p>



<p>However, <strong>no objects are detected</strong>.</p>



<p>This shows an important limitation: <strong>SAM3 is sensitive to how the prompt is phrased.</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="22">[Agent] VLM refine reply: {"sam_prompt": "leftmost brown paper bag"}
</pre>



<p>The system does not stop here.</p>



<p>Instead, the Vision-Language Model <strong>refines the prompt</strong> by adding more descriptive information.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="23">[Agent] Round 3 — calling SAM3 with 'leftmost brown paper bag'
[Agent] SAM3 found 0 instance(s)
</pre>



<p>Again, SAM3 fails to detect any objects.</p>



<p>At this point, we observe something important: <strong>More detailed prompts do not always improve segmentation.</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="24">[Agent] VLM refine reply: {"sam_prompt": "leftmost bag"}
</pre>



<p>Now, the model simplifies the prompt.</p>



<p>This step is critical. Instead of making the prompt more complex, the system makes it <strong>more general</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="25">[Agent] Round 4 — calling SAM3 with 'leftmost bag'
[Agent] SAM3 found 1 instance(s)
</pre>



<p>This time, SAM3 successfully detects the object.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="26">[Agent] VLM verify reply: {
 "ok": true,
 "reason": "The segmentation correctly identifies the leftmost bag as per the user's intent."
 "sam_prompt": ""
}
</pre>



<p>Finally, the Vision-Language Model verifies the result and confirms that the segmentation is correct.</p>



<p>The agentic loop stops here, and the system saves the final output image with a bounding box and segmentation mask overlaid on the input image.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-5-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="488" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-1024x488.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53403" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5.png?size=126x60&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-300x143.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5.png?size=378x180&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5.png?size=504x240&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5.png?size=630x300&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-768x366.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-1024x488.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-5-1536x732.png?lossy=2&amp;strip=1&amp;webp=1 1536w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Agentic AI Iterative Refinement Output (source: image by the author)</figcaption></figure></div>


<p>The output image <strong>(Figure 3)</strong> shows:</p>



<ul class="wp-block-list">
<li>the detected bounding box around the leftmost bag</li>



<li>the segmentation mask highlighted in color</li>



<li>the correct object selected based on the user’s instruction</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/04/image-6-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="371" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6-1024x371.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-53406" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6.png?size=126x46&amp;lossy=2&amp;strip=1&amp;webp=1 126w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6-300x109.png?lossy=2&amp;strip=1&amp;webp=1 300w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6.png?size=378x137&amp;lossy=2&amp;strip=1&amp;webp=1 378w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6.png?size=504x183&amp;lossy=2&amp;strip=1&amp;webp=1 504w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6.png?size=630x228&amp;lossy=2&amp;strip=1&amp;webp=1 630w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6-768x278.png?lossy=2&amp;strip=1&amp;webp=1 768w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6-1024x371.png?lossy=2&amp;strip=1&amp;webp=1 1024w, https://b2633864.smushcdn.com/2633864/wp-content/uploads/2026/04/image-6-scaled.png?lossy=2&amp;strip=1&amp;webp=1 1080w" sizes="(max-width: 630px) 100vw, 630px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Generated Output with bounding box, mask, confidence score (source: image by the author).</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		86+ total classes • 115+ hours hours of on-demand code walkthrough videos • Last updated: June 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>86+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>86 Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, we built an <strong>agentic AI vision system</strong> that combines a Vision-Language Model with a segmentation model to solve a real-world problem.</p>



<p>Instead of relying on a single model, we designed a pipeline where multiple components work together in a loop. This allows the system to not only perform segmentation, but also <strong>understand instructions, evaluate results, and improve itself automatically</strong>.</p>



<p>First, we used a Vision-Language Model to interpret the user’s natural language query and convert it into a clean segmentation prompt.</p>



<p>Next, we used SAM3 to perform <strong>open-vocabulary segmentation</strong> using that prompt.</p>



<p>Then, we introduced an agentic loop where the Vision-Language Model verifies the segmentation output and refines the prompt if necessary.</p>



<p>Finally, we visualized the results by overlaying bounding boxes and segmentation masks on the original image.</p>



<p>This approach highlights an important shift in computer vision. Instead of building static pipelines, we are now moving toward <strong>interactive and self-correcting systems</strong> that can adapt to user intent.</p>



<p>Such systems can be extended to a wide range of applications, including:</p>



<ul class="wp-block-list">
<li>interactive image editing</li>



<li>robotics and autonomous perception</li>



<li>visual assistants</li>



<li>multimodal search systems</li>
</ul>



<p>In the future, we can further improve this system by:</p>



<ul class="wp-block-list">
<li>adding support for multiple images or video inputs</li>



<li>integrating more tools into the agent loop</li>



<li>introducing memory for long-term reasoning</li>



<li>optimizing inference for real-time applications</li>
</ul>



<p>By combining Vision-Language Models with powerful segmentation models, we take a step closer to building <strong>intelligent visual systems that can understand and act on human instructions</strong>.</p>



<p>This represents the foundation of next-generation AI systems.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Thakur, P. </strong>“Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen,” <em>PyImageSearch</em>, S. Huot, G. Kudriavtsev, and A. Sharma, eds., 2026, <a href="https://pyimg.co/ohlwd" target="_blank" rel="noreferrer noopener">https://pyimg.co/ohlwd</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen" data-enlighter-group="27">@incollection{Thakur_2026_building-an-agentic-ai-vision-system-with-sam-3-and-qwen,
  author = {Piyush Thakur},
  title = {{Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Georgii Kudriavtsev and Aditya Sharma},
  year = {2026},
  url = {https://pyimg.co/ohlwd},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.smushcdn.com/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/04/06/agentic-ai-vision-system-object-segmentation-with-sam-3-and-qwen/">Agentic AI Vision System: Object Segmentation with SAM 3 and Qwen</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
