October 14, 2025

LLMs as Semantic Middleware 2/3 — Where They Belong in the Stack

Placing LLMs inside real systems: how semantic middleware fits between intent, execution, and feedback.

architecturellmmiddlewareintegration

Everything Wants to Talk, Nothing Shares a Language

Most modern systems are modular. Services, APIs, databases, humans — all exchanging data through narrow contracts. But those contracts are brittle. They assume everyone already speaks the same dialect of meaning.

I've seen this break down everywhere. A user says "show me last quarter's performance." The API expects start_date, end_date, metrics[], and group_by. A mobile app calls a field userId, the backend expects user_id, and the analytics service calls it account_number. Same concept, different language.

The moment a request crosses a boundary — from business logic to data, or from a user story to a code change — context starts leaking. That leak is where Large Language Models become useful. Not as a new interface or a bolt-on chatbot, but as a layer inside the architecture that restores context before it is lost.


What "Placement" Means

When we call LLMs middleware, we have to decide where they actually sit in the flow.

An LLM layer can live in three general positions:

  1. At the edge: translating between humans and systems (chat, natural-language query, documentation).
  2. Inside the system: translating between services or models with different schemas or data contracts.
  3. In the feedback loop: translating raw telemetry or logs back into human insight.

Each position does the same job — it moves meaning between incompatible layers — but the design constraints differ.


Position 1: Between Human and System

This is the most obvious case. An LLM parses a natural-language request, injects context, and produces structured intent — a SQL query, API call, or configuration file. It becomes the semantic interface for users who think in problems, not endpoints.

Example: Natural Language to SQL

I built this pattern for a data team that was drowning in Slack requests: "Can someone pull Q3 signups by region?" "What was our churn rate last month?" Each request took 20 minutes to translate into SQL, run, and format.

We added an LLM layer:

# User input
"Show me Q3 signups by region"

# LLM middleware output
{
  "query_type": "aggregation",
  "table": "signups",
  "date_range": {"start": "2024-07-01", "end": "2024-09-30"},
  "group_by": "region",
  "aggregation": "count"
}

# System output (generated SQL)
SELECT region, COUNT(*) as signups
FROM signups
WHERE created_at BETWEEN '2024-07-01' AND '2024-09-30'
GROUP BY region;

The LLM didn't execute the query. It translated the request into a structure the existing analytics API could handle. The result? Request-to-answer time dropped from 20 minutes to 30 seconds.

Design Rule: Treat It Like Input Validation

The LLM layer here is just another input validator. It must:

  • Log everything: prompt, model response, validation result.
  • Have a schema: define what valid output looks like and reject anything else.
  • Fail gracefully: if the LLM produces gibberish, return a helpful error, not a hallucination.
def parse_query(user_input: str) -> Query:
    prompt = build_prompt(user_input)
    response = llm.complete(prompt)
    
    logger.info("llm_query_parse", {
        "input": user_input,
        "prompt_version": PROMPT_VERSION,
        "raw_response": response.text
    })
    
    try:
        structured = validate_query_schema(response.text)
        return structured
    except ValidationError as e:
        logger.warning("llm_parse_failed", {"error": str(e)})
        return fallback_query(user_input)

This makes the LLM layer boring, predictable, and debuggable—exactly what you want in production.


Position 2: Between Services

This is the less visible but more interesting layer. As architectures evolve, teams build microservices that describe similar concepts with slightly different language — "customer," "account," "user." An LLM can translate across those internal dialects.

Example: Schema Alignment Across Services

At a SaaS company I worked with, three teams had built three different user models:

  • Auth service: { "sub": "uuid", "email": "string", "roles": [] }
  • Billing service: { "account_id": "uuid", "primary_email": "string", "plan": "string" }
  • Analytics service: { "user_id": "uuid", "contact_email": "string", "tier": "string" }

Every integration required custom mapping logic. When a new field was added, three codebases had to change.

We introduced an LLM translation layer:

class ServiceTranslator:
    def translate(self, source_service: str, target_service: str, data: dict) -> dict:
        prompt = f"""
        Translate this {source_service} schema to {target_service} schema.
        Source: {json.dumps(data)}
        Target schema: {self.schemas[target_service]}
        Map equivalent fields. Use null for missing fields.
        """
        
        response = llm.complete(prompt)
        translated = json.loads(response.text)
        
        # Cache successful translations
        self.cache.set(hash(source_service, target_service, data), translated)
        
        return self.validate_schema(target_service, translated)

This worked surprisingly well. The LLM understood semantic equivalence: sub in Auth meant the same as account_id in Billing. When a new field appeared, the translator adapted without code changes.

Design Rule: Make It Stateless and Cacheable

The LLM must be stateless and deterministic enough to be predictable:

  • Cache common translations: most service-to-service calls repeat the same patterns.
  • Version prompt templates: when the translation logic changes, version it like an API.
  • Observe performance: log translation latency, cache hit rate, and validation failures.
@cached(ttl=3600)
def translate_user_schema(source: str, target: str, data: dict) -> dict:
    # Translation logic here
    pass

This turns the LLM into a semantic adapter that sits quietly between services, translating on demand.


Position 3: Between System and Human Feedback

Finally, LLM middleware can sit in the feedback loop — summarizing logs, incidents, or metrics into narratives that humans can act on. This is where the model closes the learning loop between systems and their operators.

Example: Incident Summarization

One team I advised had a problem: after every incident, they'd spend an hour combing through logs, Slack threads, and Jira tickets to write a postmortem. The work was tedious and inconsistent.

We added an LLM summarizer:

def summarize_incident(incident_id: str) -> IncidentSummary:
    # Gather context
    logs = fetch_logs(incident_id)
    slack_thread = fetch_slack_thread(incident_id)
    timeline = reconstruct_timeline(incident_id)
    
    prompt = f"""
    Summarize this incident in 3 paragraphs:
    1. What happened (timeline, impact)
    2. Root cause (technical details)
    3. What we learned (actionable insights)
    
    Logs: {logs}
    Discussion: {slack_thread}
    Timeline: {timeline}
    """
    
    response = llm.complete(prompt)
    
    return IncidentSummary(
        text=response.text,
        generated_at=now(),
        sources=[logs, slack_thread, timeline]
    )

The result wasn't perfect, but it was good enough to edit. Instead of starting from scratch, engineers reviewed and refined the LLM's summary. Postmortem time dropped from an hour to 15 minutes.

Design Rule: Outputs Must Be Auditable

When the LLM interprets system data for humans, trust is critical:

  • Store raw inputs: keep the original logs, Slack threads, and timelines.
  • Version the prompt: so you can reproduce the exact summary logic.
  • Show confidence: if the model is uncertain, say so.
{
  "summary": "The API gateway experienced a 30-minute outage...",
  "confidence": 0.85,
  "sources": ["logs/2024-10-14.txt", "slack/incident-42"],
  "prompt_version": "1.2.0",
  "generated_at": "2024-10-14T10:30:00Z"
}

This makes the LLM's reasoning traceable. Humans can verify the chain of logic and correct it when needed.


Research and Emerging Practice

Several frameworks already treat LLMs as composable services rather than chat interfaces:

  • LangChain treats prompts as structured functions and enables chaining across services.
  • Semantic Kernel positions itself explicitly as AI middleware, managing context and orchestration.
  • OpenDevin explores autonomous agents that embed LLM calls inside software workflows.
  • The a16z "Emerging Architectures" map shows the LLM layer sitting between APIs, embeddings, and tools — exactly where middleware belongs.

Each of these frameworks hints at the same idea: LLMs are most reliable when treated as connective infrastructure, not endpoints.


Reflection: Designing for Clarity, Not Magic

Placing an LLM layer inside your system is not about adding intelligence. It is about shortening the semantic distance between components. The same principles that make technical middleware safe and observable apply here too:

  • Every translation must be reversible: you should be able to trace output back to input.
  • Every prompt must be versioned: so you can reproduce, test, and roll back changes.
  • Every failure must degrade gracefully: return a safe default, not a hallucination.

Good middleware makes itself boring. When you integrate LLMs that way, they stop being demos and start being infrastructure.


Try This

Pick one integration point in your system where meaning is lost in translation:

  1. Human to system: a search box, a chatbot, a command parser.
  2. Service to service: an API that transforms data from one schema to another.
  3. System to human: a log aggregator, a reporting tool, a monitoring dashboard.

Add a simple LLM layer with three properties:

  • Input logging: capture what went in.
  • Output validation: ensure the output matches your schema.
  • Fallback logic: if the LLM fails, do something safe.

Run it for a week. Measure how often the LLM succeeds, how often it fails, and where you need to improve prompts or validation. That's how you turn magic into infrastructure.


Next: LLMs as Semantic Middleware 3/3 — How the Same Logic Applies Between Humans.


Sources

Enjoyed this article?

Check out more articles or connect with me