· 9 min read

From Software Engineer to AI Engineer: A Pragmatic Blueprint for Building Production AI

How software engineers can bridge the gap to AI engineering without a machine learning PhD: mastering non-deterministic APIs, vector indexing, local RAG architectures, and evaluation-driven development.

Noureddine Driouech
Noureddine Driouech
AI Software Engineer
#AI Engineering #Software Engineering #System Design #RAG #Ollama
From Software Engineer to AI Engineer: A Pragmatic Blueprint for Building Production AI
Table of Contents

There is a pervasive myth circulating across tech circles: that to build modern AI-powered applications, you must hold a Ph.D. in machine learning, spend years tuning loss curves in Jupyter notebooks, or understand the mathematical gradient descent equations inside transformer attention heads.

I did not come from an academic data science background. I am a Software Engineer and Systems Architect. My daily craft has always been distributed system design, API contracts, database normalization, concurrency, and resilient cloud architectures.

When LLMs emerged into the mainstream, I did not abandon software engineering—I expanded into AI Engineering. And what I discovered in the trenches surprised me:

85% of successful AI engineering is robust, disciplined software engineering. The remaining 15% is learning how to constrain, evaluate, and orchestrate non-deterministic models.

If you already know how to write clean code, design microservices, manage databases, and optimize latency, you are already equipped with the hardest skills needed to succeed in AI. Here is the pragmatic blueprint of what you actually need to learn, what you can safely ignore, and how to build production-grade AI systems today.


1. The Mindset Shift: The LLM Is Just a Slow, Non-Deterministic Microservice

In traditional software, functions are deterministic. Given input $x$, you always receive output $y$:

$$f(x) \rightarrow y$$

A Large Language Model, however, is a probabilistic next-token engine. The exact same prompt can yield slightly different responses across executions unless temperature is zero, and even then, floating-point non-determinism across GPU clusters can cause minor variations:

$$f(x) \sim P(Y \mid x)$$

TRADITIONAL MICROSERVICE                      LLM AS A MICROSERVICE
┌───────────────────────────────┐             ┌───────────────────────────────┐
│ • Latency: 5ms - 50ms         │             │ • Latency: 800ms - 4,000ms    │
│ • Deterministic Typed JSON    │    VS       │ • Non-deterministic Tokens    │
│ • Fixed Cost per Request      │             │ • Metered Cost Per Token      │
│ • Idempotent & Cacheable      │             │ • Unpredictable Failures      │
│ • Standard Unit Test Pass/Fail│             │ • Evals & Distributional Tests│
└───────────────────────────────┘             └───────────────────────────────┘

As software engineers, our job is not to build models from scratch. Our job is to build the deterministic scaffolding around probabilistic models:

  • Enforcing strict type contracts (using Pydantic or Zod).
  • Implementing circuit breakers, timeouts, and automatic fallbacks.
  • Caching frequent queries to eliminate unnecessary token expenditures.
  • Constructing test harnesses that measure output quality at scale.

2. The Core AI Engineering Tech Stack

You do not need to memorize PyTorch backpropagation to be a world-class AI engineer. Here is the actual technical stack you must master:

+-----------------------------------------------------------------------+
|                       THE AI ENGINEERING STACK                        |
+-----------------------------------------------------------------------+
|  ORCHESTRATION & RUNTIMES: LangChain, LangGraph, LlamaIndex, Ollama   |
+-----------------------------------------------------------------------+
|  DATA & RETRIEVAL: pgvector, ChromaDB, Qdrant, BM25 Hybrid Search     |
+-----------------------------------------------------------------------+
|  VALIDATION & SCHEMAS: Pydantic (Python), Zod (TypeScript)            |
+-----------------------------------------------------------------------+
|  CACHING & INFRASTRUCTURE: Redis Semantic Cache, Docker, FastAPI      |
+-----------------------------------------------------------------------+
|  OBSERVABILITY & EVALS: Ragas, DeepEval, OpenTelemetry, LangSmith     |
+-----------------------------------------------------------------------+

A. Vector Stores and Dense Embeddings

Instead of searching strings with SQL ILIKE '%query%', an embedding model converts text into high-dimensional vectors (e.g., 1536 floating-point numbers). Semantic similarity is computed using the Cosine Distance:

$$\cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|}$$

As a software engineer, you do not need a standalone niche database for everything. If your stack already runs PostgreSQL, pgvector lets you store embeddings and perform approximate nearest-neighbor indexing (HNSW or IVFFlat) right alongside your relational customer data.

B. Hybrid Search: Dense + Sparse Retrieval

Vector search alone has a critical blind spot: exact keyword matching (SKUs, IDs, phone numbers, exact code identifiers).

Production RAG systems use Hybrid Search:

  1. Dense Vector Search (HNSW) captures conceptual and semantic meaning.
  2. Sparse Keyword Search (BM25) catches exact character tokens and product codes.
  3. Reciprocal Rank Fusion (RRF) merges both result sets, producing a single ranking with higher precision than either method alone.

C. Semantic Caching

Why pay $0.02 and wait 2,500ms for an LLM to answer a question that was asked five minutes ago?

A Semantic Cache embeds the incoming query and checks a Redis vector index. If the cosine similarity exceeds a threshold (e.g., $0.96$), the system returns the cached answer instantly in 8ms with zero LLM API cost.


3. The New Engineering Discipline: Evals as Unit Tests

In standard backend engineering, writing unit tests is straightforward:

def test_calculate_tax():
    assert calculate_tax(100.0) == 12.0

With an LLM, the output is freeform text. How do you test that your customer support agent or SQL generator did not regress after a prompt adjustment?

This is where Eval-Driven Development (EDD) comes in:

┌─────────────────┐       ┌─────────────────┐       ┌─────────────────┐
│   Golden Test   │ ----> │ Target System / │ ----> │  Evaluation     │
│     Dataset     │       │   LLM Pipeline  │       │  Harness (Eval) │
│ (50-200 inputs) │       │                 │       │                 │
└─────────────────┘       └─────────────────┘       └────────┬────────┘
                                                             │
                                   ┌─────────────────────────┴────────────┐
                                   ▼                                      ▼
                        Deterministic Assertions                  LLM-as-a-Judge
                        • Output conforms to JSON                 • Faithfulness
                        • Contains required IDs                   • Context Precision
                        • Latency < 1200ms                        • Answer Relevance
  1. Deterministic Assertions: Verify that JSON schemas validate, mandatory keywords are present, forbidden patterns are absent, and response times remain within SLA.
  2. Model-Based Evaluators (LLM-as-a-Judge): Use a fast, unbiased evaluator model to score outputs against standard RAG metrics:
    • Faithfulness: Did the model derive its answer strictly from the retrieved context without hallucinating?
    • Answer Relevance: Did the answer address the user’s specific prompt?
    • Context Recall: Did the retrieval engine fetch all necessary paragraphs?

4. Production Architecture Blueprint: The Resilient AI Gateway

Below is the architectural blueprint of an enterprise-grade AI gateway designed with software engineering principles:

+-----------------------------------------------------------------------------------+
|                        PRODUCTION AI GATEWAY ARCHITECTURE                         |
+-----------------------------------------------------------------------------------+
                                          │
                                 [Client Application]
                                 (Next.js / Astro UI)
                                          │  HTTP POST / Stream (SSE)
                                          ▼
                               ┌─────────────────────┐
                               │   FastAPI Gateway   │
                               │  - Auth & Rate-Limit│
                               └──────────┬──────────┘
                                          │
                                          ▼
                             [1. Semantic Cache (Redis)]
                             /                         \
                     [Cache Hit]                     [Cache Miss]
                          │                               │
                          │ (Return 8ms)                  ▼
                          │                     [2. Query Rewriter]
                          │                               │
                          │                               ▼
                          │                   [3. Hybrid Retrieval]
                          │                    /                 \
                          │            (Dense Vector)       (BM25 Keyword)
                          │                   \                 /
                          │                    ▼               ▼
                          │                 [4. Reciprocal Rank Fusion]
                          │                               │
                          │                               ▼
                          │                     [5. Context Compactor]
                          │                               │
                          │                               ▼
                          │                   [6. Local / Cloud LLM]
                          │                     (Ollama / Claude)
                          │                               │
                          ▼                               ▼
                 [Stream Tokens via Server-Sent Events (SSE) to User]

Key Architectural Safeguards:

  1. Context Compactor: Never pass 50 raw retrieved chunks into the prompt. Filter out duplicate paragraphs and compact token length to minimize time-to-first-token (TTFT).
  2. Local Fallback via Ollama: If cloud APIs experience outages or rate limits, the gateway routes traffic to an on-premise local Ollama instance running an open-weights model (such as Llama 3.1 8B).
  3. Streaming First: Never buffer multi-paragraph responses on the server. Stream tokens directly to the client over Server-Sent Events (SSE) to ensure sub-second perceived latency.

5. Concrete Code: A Production-Grade FastAPI + Ollama Service

Here is a concrete implementation of an AI service built with software engineering rigor—featuring Pydantic schema validation, local LLM execution via Ollama, and structured outputs:

import os
import json
from typing import List, Optional
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import requests

app = FastAPI(title="Production AI Inference Service", version="1.0.0")

OLLAMA_ENDPOINT = os.getenv("OLLAMA_HOST", "http://localhost:11434/api/generate")

# 1. Define Strict Pydantic Data Contracts
class CodeAnalysisRequest(BaseModel):
    source_code: str = Field(..., min_length=10, description="Source code to analyze.")
    language: str = Field(..., example="python", description="Programming language.")

class SecurityVulnerability(BaseModel):
    severity: str = Field(..., description="CRITICAL, HIGH, MEDIUM, or LOW")
    description: str = Field(..., description="Explanation of the vulnerability.")
    remediation: str = Field(..., description="Concrete code or architectural fix.")

class CodeAnalysisResponse(BaseModel):
    language: str
    vulnerabilities: List[SecurityVulnerability] = []
    overall_risk_score: int = Field(..., ge=0, le=100)
    summary: str

# 2. Build the System Prompt with JSON Schema Enforcement
SYSTEM_INSTRUCTION = """
You are a senior Application Security Engineer.
Analyze the provided code for security flaws (e.g., SQL injections, XSS, unvalidated inputs).
You MUST respond with valid JSON strictly conforming to this schema:
{
  "language": string,
  "vulnerabilities": [
    {"severity": "CRITICAL"|"HIGH"|"MEDIUM"|"LOW", "description": string, "remediation": string}
  ],
  "overall_risk_score": integer (0 to 100),
  "summary": string
}
Do NOT include markdown formatting or conversational commentary outside the JSON.
"""

@app.post("/api/v1/analyze-code", response_model=CodeAnalysisResponse, status_code=status.HTTP_200_OK)
def analyze_code(payload: CodeAnalysisRequest) -> CodeAnalysisResponse:
    prompt = f"Language: {payload.language}\n\nCode to inspect:\n{payload.source_code}"
    
    ollama_payload = {
        "model": "llama3.1:8b",
        "system": SYSTEM_INSTRUCTION,
        "prompt": prompt,
        "format": "json", # Ollama native JSON mode constraint
        "stream": False,
        "options": {
            "temperature": 0.1, # Minimize variance
            "top_p": 0.9,
        }
    }
    
    try:
        response = requests.post(OLLAMA_ENDPOINT, json=ollama_payload, timeout=30.0)
        response.raise_for_status()
        raw_output = response.json().get("response", "{}")
        
        # 3. Deterministic Validation & Parsing
        parsed_data = json.loads(raw_output)
        validated_response = CodeAnalysisResponse(**parsed_data)
        return validated_response

    except requests.exceptions.RequestException as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Inference backend unreachable: {str(exc)}"
        )
    except (json.JSONDecodeError, ValueError) as exc:
        # Graceful handling of malformed generation
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=f"Model emitted non-conforming payload: {str(exc)}"
        )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Look at what this service accomplishes:

  • It treats the model as an internal HTTP service with explicit timeouts and error boundaries.
  • It leverages Ollama’s native JSON mode to force grammar adherence.
  • It validates the response using Pydantic, ensuring that invalid outputs fail fast at the boundary rather than corrupting downstream consumers.

6. Practical Lessons for Developers Entering AI

  1. Type Safety Is Your Superpower: Use Pydantic in Python and Zod in TypeScript. Never pass unvalidated model outputs into your application state or database.
  2. Build for Offline & Privacy First: By running open-weights models locally with tools like Ollama or vLLM, you eliminate third-party API costs, protect confidential IP, and guarantee consistent offline latency.
  3. Observability Is Not Optional: Instrument every prompt with token counts, latency tracking, and input/output logs using OpenTelemetry. You cannot optimize what you do not measure.
  4. Vibes Are Not an Engineering Metric: If you change a prompt, run your automated eval test suite across at least 50 representative cases before pushing to staging.
  5. Software Engineering Fundamentals Still Win: Solid caching, clean database schema design, thoughtful API routing, and defensive error handling are what separate a production-grade AI platform from a fragile weekend demo.