The most surprising truth about structured output in LLMs is that it’s not about telling the model what to do, but showing it.
Let’s see this in action. Imagine we want to extract structured data from this messy text:
Customer: John Doe
Email: john.doe@example.com
Order ID: #12345
Items: 2x Widget A, 1x Gadget B
Notes: Please gift wrap Gadget B.
A common, but less effective, approach is to just ask: "Extract the customer name, email, order ID, and items from the text. Format as JSON." The model might get it right, but it’s a coin flip.
A more robust method is few-shot prompting, where we provide examples of the desired input and output.
{
"customer_name": "Jane Smith",
"email": "jane.smith@example.com",
"order_id": "#67890",
"items": [
{"name": "Thingamajig", "quantity": 1},
{"name": "Doodad", "quantity": 3}
],
"notes": "Ship to alternate address."
}
Now, let’s apply this to our John Doe example:
Input Text:
Customer: John Doe
Email: john.doe@example.com
Order ID: #12345
Items: 2x Widget A, 1x Gadget B
Notes: Please gift wrap Gadget B.
Desired Output (JSON):
{
"customer_name": "John Doe",
"email": "john.doe@example.com",
"order_id": "#12345",
"items": [
{"name": "Widget A", "quantity": 2},
{"name": "Gadget B", "quantity": 1}
],
"notes": "Please gift wrap Gadget B."
}
By showing the model exactly what we expect, we dramatically increase the likelihood of a correct, structured response.
This pattern works because LLMs are fundamentally pattern-matching machines. When you provide examples, you’re not just giving instructions; you’re providing a template that the model can learn to replicate. The more consistent and clear your examples are, the better the model will understand the desired structure, data types, and even nuances like how to parse item quantities.
The "Show, Don’t Tell" Patterns:
-
Few-Shot Prompting (as above): The foundational pattern. Provide 1-5 examples of input/output pairs. This is your go-to for most structured output tasks.
- Why it works: Directly demonstrates the desired schema and data transformation.
- Example Command (conceptual):
prompt = """ Extract customer order details. Example 1: Input: Customer: Jane Smith Email: jane.smith@example.com Order ID: #67890 Items: 1x Thingamajig, 3x Doodad Notes: Ship to alternate address. Output: {"customer_name": "Jane Smith", "email": "jane.smith@example.com", "order_id": "#67890", "items": [{"name": "Thingamajig", "quantity": 1}, {"name": "Doodad", "quantity": 3}], "notes": "Ship to alternate address."} Example 2: Input: Customer: John Doe Email: john.doe@example.com Order ID: #12345 Items: 2x Widget A, 1x Gadget B Notes: Please gift wrap Gadget B. Output: """ # Then send prompt to LLM API
-
Instruction Tuning with Output Schema: For models that support it (like many OpenAI models via
response_format={"type": "json_object"}or function calling), you can explicitly state the desired output format and provide a JSON schema.- Why it works: The LLM’s API or internal mechanisms are designed to guide the output towards the specified schema, often using pre-trained capabilities for JSON generation.
- Example Config (OpenAI API):
(Note: This specific example relies on the model inferring the schema from the system message and user input. For stricter control, you’d define a JSON schema object and pass it.)from openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o", response_format={ "type": "json_object" }, messages=[ {"role": "system", "content": "Extract order details into a JSON object."}, {"role": "user", "content": "Customer: John Doe\nEmail: john.doe@example.com\nOrder ID: #12345\nItems: 2x Widget A, 1x Gadget B\nNotes: Please gift wrap Gadget B."} ] ) print(response.choices[0].message.content)
-
XML/Markdown Formatting: Sometimes, JSON is overkill or harder for the model to adhere to strictly. Using XML tags or Markdown lists can be simpler.
- Why it works: LLMs are highly proficient at parsing and generating structured text formats like XML and Markdown.
- Example Prompt:
Extract the following information from the text and format it using XML tags: <customer_name>...</customer_name> <email>...</email> <order_id>...</order_id> <items> <item> <name>...</name> <quantity>...</quantity> </item> </items> <notes>...</notes> Text: Customer: John Doe Email: john.doe@example.com Order ID: #12345 Items: 2x Widget A, 1x Gadget B Notes: Please gift wrap Gadget B.
-
YAML Output: Similar to JSON, YAML is another human-readable data serialization format that LLMs can generate effectively, often with simpler syntax for lists and nested structures.
- Why it works: YAML’s indentation-based structure is easily understood and replicated by LLMs.
- Example Prompt:
Extract the following information and output it as YAML: customer_name: John Doe email: john.doe@example.com order_id: '#12345' items: - name: Widget A quantity: 2 - name: Gadget B quantity: 1 notes: Please gift wrap Gadget B.
-
"Tool Use" / Function Calling: Advanced models can be instructed to call functions with specific arguments, where the function signature defines the desired structured output.
- Why it works: This leverages the LLM’s ability to parse intent and map it to predefined executable actions, with the arguments acting as the structured data.
- Example (conceptual): You define a
process_order(customer_name: str, email: str, order_id: str, items: list[dict], notes: str)function. The LLM, when given the input text, will output a JSON object representing a call to this function with the extracted arguments.
-
Constrained Generation: Some libraries and APIs offer ways to constrain the LLM’s output generation to a specific grammar or format (e.g., using libraries like
guidanceoroutlines).- Why it works: This imposes a hard structure on the generation process, ensuring the output conforms to a predefined template or schema, preventing malformed outputs.
- Example (using
guidancelibrary):
(Thisimport guidance llm = guidance.llms.OpenAI("gpt-4o") # Or your preferred LLM grammar = guidance(""" {{#system}}You are a helpful assistant that extracts order information. Output the information as a JSON object.{{/system}} {{#user}} Customer: John Doe Email: john.doe@example.com Order ID: #12345 Items: 2x Widget A, 1x Gadget B Notes: Please gift wrap Gadget B. {{/user}} {{#assistant}} ```json { "customer_name": "{{regex '[^\\n]+'}}", "email": "{{regex '[^\\n]+'}}", "order_id": "{{regex '[^\\n]+'}}", "items": [ {{#each (split (replace (regex 'Items: (.*?) Notes:' $$0) ", " "\\n") )}} {{#unless @first}},{{/unless}} { "name": "{{regex '(\\d+x )?(.*)' $$0}}", "quantity": {{to_integer (regex '(\\d+)x' $$0)}} } {{/each}} ], "notes": "{{regex 'Notes: (.*)' $$0}}" } ```json {{/assistant}} """) executed_grammar = grammar(llm=llm) print(executed_grammar)guidanceexample is illustrative and requiresguidancelibrary installation and setup. The regex and logic would be refined for robustness.)
The most subtle aspect of structured output is how the LLM handles ambiguity in the input text that maps to your desired output structure. For instance, if an order has "1 Widget A and 2 Doodads" versus "2 Doodads and 1 Widget A", a robust few-shot example showing how to parse and order these items consistently within your JSON items array is crucial. Without it, the model might output them in the order they appear in the text, or alphabetically, or in some other unpredictable fashion, breaking your downstream processing.
The next challenge is handling multi-turn conversations where the structured output needs to be updated or refined based on follow-up questions.