Most engineers treat large language models like glorified search engines or expensive autocomplete tools. They type a loose prompt, copy the resulting function, paste it into a pull request, and hope the test suite passes. This workflow misses the point entirely.
An LLM isn't a database. It's a probabilistic reasoning engine with a massive state space. Treat it like a dictionary and you get brittle code, silent hallucinations, and security holes. Building actual software with AI means shifting from prompting text to engineering control loops.
Traditional software relies on deterministic routines. These routines handle specific inputs and spit out predictable outputs. When you mix machine learning models into that stack, the model handles the ambiguous translation layer. Deterministic code handles the constraints.
Take a simple data extraction task. Ask an LLM for JSON and it usually complies. Change the schema slightly or feed it weird edge cases, and it outputs malformed syntax that breaks your parser. The amateur move is writing a longer prompt begging the model to be careful. The engineering move is wrapping the generation step in a validation loop.
Here is what that looks like in practice (field notes here). You send the prompt, catch the parse error if the output fails validation, and feed that exact error message back into the context alongside the original input. Let the model debug its own output.
import json
from pydantic import BaseModel, ValidationError
class UserExtract(BaseModel):
name: str
age: int
def extract_with_retry(client, prompt, max_retries=3):
messages = [{"role": "user", "content": prompt}]
for _ in range(max_retries):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
response_format={"type": "json_object"}
)
raw_content = response.choices[0].message.content
try:
data = json.loads(raw_content)
validated = UserExtract(**data)
return validated
except (json.JSONDecodeError, ValidationError) as e:
messages.append({"role": "assistant", "content": raw_content})
messages.append({"role": "user", "content": f"Fix this error: {e}"})
raise RuntimeError("Failed to generate valid schema after retries.")
This pattern turns an unreliable string generator into a reliable component. You expect failures on the first try. You architect the system to catch and correct those failures automatically.
Prompt engineering is dying. System architecture is taking its place. Writing clever prose for a chat box is a temporary skill. Designing feedback loops, state machines, and evaluation harnesses around non-deterministic components is the real work now.
If your SaaS relies on AI features, your competitive advantage won't come from a slightly better base model. Everyone hits the exact same API endpoints. Your advantage lives in the wrapper code you build around those models. The orchestration layer is the product.
Stop tweaking your prompts. Build better guardrails, stricter parsing schemas, and automated correction loops. Treat the model like a brilliant, sleep-deprived junior engineer who needs clear boundaries and a strict code reviewer.
Top comments (0)