{"slug": "from-software-engineer-to-ai-engineer-part-2-structuring-the-model", "title": "From Software Engineer to AI Engineer - Part 2: Structuring the model", "summary": "A developer's guide to structuring AI model output for software applications covers prompt engineering techniques and enforcing structured output using schemas. The post demonstrates how to use system prompts, role assignment, and few-shot prompting to steer models, and shows how to define a Pydantic schema to parse model responses into typed objects, using a payment dispute assessment example.", "body_md": "Part 1 left us with a model that talks back beautifully but without structure. This is fine, even pretty cool, for a chatbot used by humans. We understand natural language and fuzziness. Unstructured output is, however, terrible for software. You cannot parse the output into a typed object if the shape is different every time.\n\nTo build an application around a model, you must make it respect an interface. This part covers the two skills that turn model output into something you can build on: steering the model with prompts, and enforcing structured output.\n\nA prompt (an input message you give to the model) is best understood as runtime configuration. The model responds to the information, constraints, and examples you provide. Prompting the right way is therefore crucial for taming the model's output. Entire books have been written about prompt engineering. Here are a few quick principles:\n\n**Use the system prompt.** The first message the model receives is often not the user's first message, but a system prompt that precedes the conversation. In chat interfaces such as ChatGPT, the system prompt is invisible to the user, but it is sent to the model on every turn along with the conversation history. Use it to define the model's overall behavior by specifying rules, constraints, and tone that should apply to every request. For example, \"Do not invent fee figures if the knowledge base contains no relevant data\" is a business rule that belongs in the system prompt.\n\n**Assign a role.** Models perform better when they know what expertise to apply. Instead of \"Summarize this contract,\" write \"You are a legal assistant. Summarize this contract for a software engineer, highlighting payment terms, termination clauses, and obligations\". This role assignment can live in the system prompt.\n\n**Show examples.** One of the most effective prompting techniques is to show the model examples of the behavior you want. Instead of explaining a format in detail, provide a few input + output examples. Models are extremely good at recognizing and continuing patterns, and examples often define the desired behavior more precisely than prose. This is also called \"few shot prompting\".\n\n```\n  Input: \"Refund is requested because the item was damaged.\"\n  Output: \"Category: Product damage. Outcome: Refund denied.\"\n\n  Input: \"Refund is requested because item never arrived.\"\n  Output: \"Category: Shipment lost. Outcome: Refund accepted.\"\n```\n\nA model's natural output is text. Humans can interpret text easily, but software systems want contracts. If you need to store a model's result, trigger a workflow, or pass data to another service, dealing with free-form text is difficult. You need an output that can be parsed reliably into a typed object. The solution is the same as in traditional software: define a schema and make the model produce output that conforms to it.\n\nLet's see how PayIQ gets structured output from the model. Create `02_structured_output.py`\n\n:\n\n``` python\nfrom enum import Enum\n\nfrom dotenv import load_dotenv\nfrom pydantic import BaseModel, Field\nfrom langchain.chat_models import init_chat_model\n\nload_dotenv()\n\nclass WinLikelihood(str, Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n\nclass ProbabilityRange(BaseModel):\n    \"\"\"Estimated probability range for winning the dispute.\"\"\"\n\n    low_pct: int = Field(\n        ge=0,\n        le=100,\n        description=\"Lower bound of estimated win probability percentage\"\n    )\n    high_pct: int = Field(\n        ge=0,\n        le=100,\n        description=\"Upper bound of estimated win probability percentage\"\n    )\n\nclass DisputeAssessment(BaseModel):\n    \"\"\"Structured verdict on a card chargeback / payment dispute.\"\"\"\n\n    win_likelihood: WinLikelihood = Field(\n        description=\"Overall likelihood of winning the dispute\"\n    )\n\n    key_evidence: list[str] = Field(\n        min_length=2,\n        max_length=4,\n        description=\"Most important evidence to submit, written as short phrases\"\n    )\n\n    estimated_win_probability: ProbabilityRange\n\n    recommended_next_step: str = Field(\n        description=\"Single most important action to take before the deadline\"\n    )\n\nSYSTEM_PROMPT = (\n    \"You are a payment operations analyst who assesses card chargebacks for merchants. \"\n    \"Judge the case only on the evidence the user provides. \"\n    \"If a decisive piece of evidence is missing, say so in the recommended next step \"\n    \"instead of assuming it exists.\"\n)\n\nmodel = init_chat_model(\"anthropic:claude-sonnet-5\")\nstructured_model = model.with_structured_output(DisputeAssessment)\n\n# A real DisputeAssessment instance, not a string\nresult: DisputeAssessment = structured_model.invoke([\n    {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n    {\n        \"role\": \"user\",\n        \"content\": (\n            \"Assess this chargeback: €480 online card payment, reason code 10.4 \"\n            \"(fraud, card-absent), customer claims they never made the purchase. \"\n            \"We have a 3-D Secure authentication record, an AVS match, and \"\n            \"delivery confirmation to the cardholder's billing address. Response \"\n            \"deadline in 12 days.\"\n        ),\n    },\n])\n\nprint(result.model_dump_json(indent=2))\n```\n\nNote the two principles from above at work: the system prompt assigns a role and defines failure behavior, and we pass a *list* of messages, exactly the interface from part 1.\n\nThe result is not a string but rather an actual `DisputeAssessment`\n\nobject. Under the hood, `with_structured_output`\n\nconverts your Pydantic class into a JSON schema and hands it to the provider, which constrains generation to that shape. LangChain then parses the response back into your class.\n\nSo what happens if the model does not follow the schema and outputs something else? Well, it depends. LangChain raises a validation error by default, and `with_structured_output(..., include_raw=True)`\n\nlets you inspect the raw response instead of raising. Some implementations also support automatic retries: if parsing fails, the framework sends the error back to the model and asks it to produce corrected output.\n\nEither way, it fails fast rather than silently passing invalid data downstream. This follows the same principle used in reliable backend systems: parse once, then work with typed data. Convert untrusted input into a representation your application can reason about, or throw if it doesn't fit.\n\nThe output will look like this:\n\n``` bash\n$ python3 02_structured_output.py\n\n{\n  \"win_likelihood\": \"high\",\n  \"key_evidence\": [\n    \"3-D Secure authentication record (liability shift)\",\n    \"AVS match on billing address\",\n    \"Delivery confirmation to cardholder's billing address\",\n    \"Order/session details linking IP or device to cardholder if available\"\n  ],\n  \"estimated_win_probability\": {\n    \"low_pct\": 70,\n    \"high_pct\": 85\n  },\n  \"recommended_next_step\": \"Confirm the 3-D Secure record shows fully authenticated (frictionless or challenge-completed with ECI value indicating liability shift, e.g., ECI 05/02) rather than attempted-only authentication, since under 10.4 a successful 3DS authentication generally shifts fraud liability to the issuer and is the single most decisive piece of evidence to compile and submit before the 12-day deadline.\"\n}\n```\n\nA schema guarantees structure, not correctness. Therefore, we should also set up additional validation:\n\nOur `ProbabilityRange`\n\nalready rejects a `low_pct`\n\nof `900`\n\n(schema validation), but it happily accepts `low_pct: 90`\n\nwith `high_pct: 10`\n\nwhich describes an impossible range. Pydantic (the library we used above) lets you keep business rules close to your schema. For example, a `ProbabilityRange`\n\nmodel can enforce that percentages are between 0 and 100 *and* that `low_pct`\n\nis never greater than `high_pct`\n\n:\n\n``` python\nfrom pydantic import BaseModel, Field, model_validator\n\nclass ProbabilityRange(BaseModel):\n    low_pct: int = Field(ge=0, le=100)\n    high_pct: int = Field(ge=0, le=100)\n\n    @model_validator(mode=\"after\")\n    def validate_range(self):\n        if self.low_pct > self.high_pct:\n            raise ValueError(\"low_pct must be <= high_pct\")\n        return self\n```\n\nThe goal is not to make the model perfectly reliable. The goal is to build a system where unreliable text generation is surrounded by reliable software boundaries. By harnessing this non-deterministic force, we can build tooling around it.\n\nThe next articles build on the same schema-constrained output to create tools for calculations, knowledge retrieval, and even accessing the internet.", "url": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-2-structuring-the-model", "canonical_source": "https://dev.to/bjornvdlaan/from-software-engineer-to-ai-engineer-part-2-structuring-the-model-3o1m", "published_at": "2026-07-31 22:00:00+00:00", "updated_at": "2026-07-31 22:40:50.314739+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["PayIQ", "ChatGPT", "Pydantic", "LangChain"], "alternates": {"html": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-2-structuring-the-model", "markdown": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-2-structuring-the-model.md", "text": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-2-structuring-the-model.txt", "jsonld": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-2-structuring-the-model.jsonld"}}