Make Your Local AI Actually Reliable: Ollama Structured Outputs for Agents That Don’t Break

Here’s the dirty secret behind half the local-AI automations that quietly fall apart: the model was never the problem. You asked it for “just the JSON,” it gave you a lovely paragraph that starts with Sure! Here's the JSON you asked for:, and your script choked on the first character. Multiply that by a few hundred runs and your clever little agent becomes a coin flip.

The fix isn’t a bigger model or a cleverer prompt. It’s a feature that’s now baked right into Ollama: structured outputs. You hand the model a schema, and it is constrained to return data that matches – every time, no pep talk, no markdown fences, no apologies. If you’ve been running models on your own machine for privacy or cost, this is the upgrade that turns them from fun toys into dependable building blocks.

A developer watching orderly structured data flow out of a local AI running on their own computer
Structured outputs turn a chatty local model into a predictable data source.

What “structured outputs” actually means

A normal language model predicts the next most likely word. That’s great for conversation and terrible for automation, because “most likely” includes all the friendly fluff humans wrote on the internet. Structured outputs flip the control: instead of hoping the model formats things correctly, you give it a JSON schema – a blueprint that says exactly which fields must exist and what type each one is – and the generation is forced to stay inside those lines.

Technically, the model’s next-token choices get masked so only tokens that keep the output valid against your schema are allowed. You don’t need to understand the internals. You just need to know the payoff: if it comes back, it’s valid. No regex scraping, no “strip everything before the first curly brace,” no retry loops praying the second attempt behaves.

Why this is the real unlock for local agents

An “agent” is just a loop: the model looks at a situation, decides what to do, and that decision triggers something in your code – a database write, an API call, a file move. That loop only works if the decision comes back in a shape your code can read. One stray sentence and the whole chain snaps.

Cloud providers solved this a while ago with their own structured-output modes. The news that matters for the self-hosted crowd is that you now get the same guarantee on a model running entirely on your own hardware – no data leaving your machine, no per-token bill, no rate limits. For anyone automating around private information (client records, personal finances, internal docs), that combination of reliable and local is the whole ballgame.

Messy free-form text on the left transformed into clean organized structured data on the right
The difference between hoping the model formats things right and guaranteeing it.

Setting it up with Ollama

If you already run Ollama, you’re about 90 seconds from using this. The magic lives in a single format parameter that accepts a JSON schema. Here’s the shape of a raw API call:

curl http://localhost:11434/api/chat -d '{
  "model": "llama3.2",
  "messages": [{ "role": "user", "content": "Canada became a country in 1867. Return year and country." }],
  "stream": false,
  "format": {
    "type": "object",
    "properties": {
      "country": { "type": "string" },
      "year":    { "type": "integer" }
    },
    "required": ["country", "year"]
  }
}'

That format block is the schema. The response’s content now comes back as clean JSON you can parse directly – {"country":"Canada","year":1867} – with nothing wrapped around it.

In Python it’s even friendlier, because you can describe the schema with a Pydantic model and let the library generate the JSON schema for you:

from ollama import chat
from pydantic import BaseModel

class Fact(BaseModel):
    country: str
    year: int

resp = chat(
    model='llama3.2',
    messages=[{'role': 'user', 'content': 'Canada became a country in 1867.'}],
    format=Fact.model_json_schema(),
)
fact = Fact.model_validate_json(resp.message.content)
print(fact.year)  # 1867

Notice the last two lines: the model returns schema-valid JSON, and Pydantic validates it again on the way in. Belt and suspenders – and you end up with a real typed object instead of a string you have to babysit.

A real example: messy email in, clean task out

Abstract schemas are boring, so let’s do something you’d actually use. Say you want a little assistant that reads an email and turns it into a to-do item. Define what a task looks like:

class Task(BaseModel):
    title: str
    due_date: str | None
    priority: str   # 'low' | 'medium' | 'high'
    action_needed: bool

Feed it a rambling email – “Hey, circling back on the Henderson proposal, can we get the revised numbers over before Friday? Not urgent urgent but the client’s asking…” – with that schema attached, and you reliably get back something like:

{
  "title": "Send revised numbers for Henderson proposal",
  "due_date": "Friday",
  "priority": "medium",
  "action_needed": true
}
An AI assistant turning a cluttered inbox into three neat task cards on a laptop
A practical win: messy email in, clean structured task out.

That object drops straight into your task manager, calendar, or spreadsheet. No human in the loop deciding whether “not urgent urgent” means high or medium – the schema forced a choice from your allowed set, and your code can trust it.

Wiring it into a tool-calling loop

Once outputs are trustworthy, a genuine agent is within reach. The pattern is simple: ask the model to choose an action from a fixed menu, constrained by a schema.

class Decision(BaseModel):
    tool: str        # 'search_files' | 'send_reply' | 'do_nothing'
    query: str | None
    reason: str

Your code reads decision.tool, runs the matching function, feeds the result back into the next turn, and repeats. Because tool can only ever be one of the names you listed, you never hit the classic failure where the model invents a tool that doesn’t exist or wraps its answer in three paragraphs of reasoning. The decision is always actionable. That single property – actionable every time – is the line between a demo and something you’d leave running unattended.

Gotchas worth knowing before you ship

  • Pick a capable instruct model. Tiny models will happily return valid JSON that’s semantically wrong. Schema validity guarantees shape, not truth. A mid-size instruct model (think 7-8B and up) is the sweet spot for local work.
  • Always validate anyway. Constrained decoding is excellent, but re-validating with Pydantic or a schema library costs nothing and catches edge cases. Treat it as a safety net, not an insult.
  • Keep schemas tight. Use enums (fixed string choices) and required fields aggressively. The more you constrain, the less room there is for the model to drift.
  • Watch the first-call latency. Constraining generation adds a little overhead and models load into memory on first use. Warm the model up once and subsequent calls fly.
  • Still write a clear prompt. The schema controls the container; your instructions still shape what goes inside it. Ask for what you want in plain language and give it the schema.

Where this fits in your stack

If you’ve already followed one of the countless “run your own private AI” guides and got Ollama humming on your machine, structured outputs are the natural next step – the thing that moves you from chatting with a model to building with one. Combine it with a bit of local vector search for memory, a handful of Python functions as tools, and a simple loop, and you’ve got a private, free, dependable assistant doing real work on your own hardware.

The headline here isn’t a flashy new model. It’s something quieter and more useful: predictability. AI gets genuinely powerful the moment you can trust what comes out of it enough to let it act on your behalf. Structured outputs are how you earn that trust – and they turn the model on your own computer from a clever conversationalist into a reliable extension of what you can get done.


Sources & further reading:

Related Reading

Leave a Reply

Your email address will not be published. Required fields are marked *