Information Extraction › Structured Output Generation with LLMs

Michael BrenndoerferFebruary 4, 20269 min read
Language AI HandbookAll chapters
Part 63 of 67LLM Applications
Chapter 481Information Extraction › Structured Output Generation with LLMs
Reading section 5 of 10 · Structured Output Generation with LLMs · 9 minSection 5 of 10 · 9 min

Book contents

Section 5 of 10 · 9 min read

Information Extraction › Structured Output Generation with LLMsOne section · Sections in this chapter5 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The traditional IE pipeline (NER, RE, EE) is powerful but brittle. Each stage introduces its own errors, and adapting to a new domain or a new schema requires retraining multiple models. Large language models offer a different approach: instruct the model to extract all relevant information in one pass and return a structured JSON object.

This approach uses the LLM's broad world knowledge and flexible reasoning. A model that has read millions of news articles already "knows" that acquisitions involve buyers, sellers, prices, and dates. It understands that "deal," "transaction," and "purchase" are all synonyms for an acquisition event, and that "five billion dollars," "$5B," and "USD 5,000,000,000" all express the same monetary value. You can directly prompt it to structure this knowledge without annotating training data.

The tradeoffs are significant. LLM-based IE is easier to adapt to new schemas and domains, but each inference call costs more than running a fine-tuned sequence tagger, and the model may hallucinate fields that are not present in the source text. For high-volume production workloads processing millions of documents, the cost difference matters. For exploratory work or low-volume high-complexity extraction, LLM-based approaches are often the practical choice. A common production pattern is to use an LLM to validate and develop the schema on a sample of documents, then train a fine-tuned model for high-volume extraction, and use rule-based post-processing to enforce hard constraints on both.

Prompting for Structured Extraction

The key to reliable structured extraction is a precise, schema-defining prompt. The schema should specify the exact fields expected, their types, and what to include when information is absent. Vague prompts produce inconsistent output. A prompt that says "Extract the key information" may return a narrative summary; a prompt that defines a specific JSON schema returns a consistently structured object.

Equally important is the instruction for handling missing information. If you say "Extract the deal value," a model that finds no deal value in the text may either return null or invent a plausible number. Explicitly instructing the model to "Set any field to null if the information is not present in the text" substantially reduces hallucination, because the model is given a valid, expected output for the case where information is absent.

In[9]:
Code
# Demonstration using a mock LLM call pattern
# In production, replace with actual API call to an LLM


def extract_information(text: str, schema_description: str) -> str:
    """
    Simulates structured extraction via LLM prompt.
    Returns a JSON string.
    """
    # This is a mock response illustrating the expected output structure.
    # In production, this would be: response = client.chat.completions.create(...)
    mock_response = """{
  "event_type": "acquisition",
  "acquirer": {"name": "Apple", "type": "organization"},
  "target": {"name": "Beats Electronics", "type": "organization"},
  "deal_value": {"amount": 3000000000, "currency": "USD"},
  "announcement_date": "2014-05-01",
  "announcement_person": {"name": "Tim Cook", "title": "CEO", "organization": "Apple"},
  "location": {"city": "Cupertino", "state": "California", "country": "USA"}
}"""
    return mock_response


text = (
    "Apple acquired Beats Electronics for $3 billion in May 2014. "
    "Tim Cook announced the deal at Apple's headquarters in Cupertino, California."
)

schema_description = """
Extract a JSON object with the following fields:
- event_type: type of business event
- acquirer: {name, type} of acquiring entity
- target: {name, type} of acquired entity
- deal_value: {amount (integer in USD), currency}
- announcement_date: ISO date (YYYY-MM-DD) or null
- announcement_person: {name, title, organization} or null
- location: {city, state, country} or null
Set any field to null if the information is not present in the text.
"""

result = extract_information(text, schema_description)
Out[10]:
Console
{
  "event_type": "acquisition",
  "acquirer": {
    "name": "Apple",
    "type": "organization"
  },
  "target": {
    "name": "Beats Electronics",
    "type": "organization"
  },
  "deal_value": {
    "amount": 3000000000,
    "currency": "USD"
  },
  "announcement_date": "2014-05-01",
  "announcement_person": {
    "name": "Tim Cook",
    "title": "CEO",
    "organization": "Apple"
  },
  "location": {
    "city": "Cupertino",
    "state": "California",
    "country": "USA"
  }
}

The extracted JSON is machine-readable and maps directly to a database schema. This one-pass approach is particularly powerful when the schema is complex or when fields are expressed in varied natural language patterns that would require many separate pattern rules to capture. Notice that the LLM has normalized "May 2014" to a proper ISO date format ("2014-05-01"), converted "$3 billion" to an integer number of USD, and added the implied title "CEO" for Tim Cook, all without being explicitly instructed to do so.

This normalization behavior is one of the most practically valuable properties of LLM-based extraction. Traditional rule-based systems require explicit normalization rules for every surface form: separate patterns for "May 2014," "05/2014," "the fifth month of 2014," and "Q2 2014." An LLM understands all of these as referring to the same time period and normalizes them to ISO format if the schema specifies an ISO date field. The same applies to monetary values expressed as "$3B," "$3 billion," "three billion dollars," or "USD 3,000,000,000."

JSON Mode and Function Calling

Modern LLM APIs provide structured output features that guarantee syntactically valid JSON regardless of what the model generates. OpenAI's function calling mechanism and structured output mode, and similar features in Anthropic's API and Google's Gemini, work by constraining the model's token sampling to only produce outputs that conform to a provided JSON Schema.

The mechanism is elegant: during decoding, the sampling process is constrained so that only tokens compatible with the current parse state of the expected JSON Schema can be generated. If the schema says the "amount" field must be an integer, the model cannot generate a string in that position. This is implemented via constrained decoding with a finite state machine derived from the JSON Schema. As we saw in the chapter on constrained decoding, this approach guarantees syntactic validity while allowing the model's full semantic capability to operate within the constraint.

The practical benefit is that you no longer need to handle JSON parse errors in your application code. Without structured output mode, a simple retry loop catches the occasional malformed JSON. With structured output mode, every response is guaranteed to be valid JSON that conforms to the schema. This is especially valuable for production systems where a malformed response that crashes a parsing pipeline at 2 AM is a reliability incident.

In[11]:
Code
# Schema definition for structured extraction using OpenAI-style JSON Schema
# In production, pass this schema to the API's response_format or tools parameter

acquisition_schema = {
    "type": "object",
    "properties": {
        "event_type": {
            "type": "string",
            "enum": [
                "acquisition",
                "merger",
                "investment",
                "partnership",
                "other",
            ],
        },
        "acquirer": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "type": {
                    "type": "string",
                    "enum": ["organization", "person", "government"],
                },
            },
            "required": ["name", "type"],
        },
        "target": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "type": {"type": "string"},
            },
            "required": ["name", "type"],
        },
        "deal_value": {
            "type": ["object", "null"],
            "properties": {
                "amount": {"type": "integer"},
                "currency": {"type": "string"},
            },
        },
        "announcement_date": {"type": ["string", "null"]},
        "location": {
            "type": ["object", "null"],
            "properties": {
                "city": {"type": "string"},
                "country": {"type": "string"},
            },
        },
    },
    "required": ["event_type", "acquirer", "target"],
}


# Validate our mock extraction result against the schema
def validate_extraction(extracted: dict, schema: dict) -> list:
    """Simple field-level validation."""
    errors = []
    for required_field in schema.get("required", []):
        if required_field not in extracted:
            errors.append(f"Missing required field: {required_field}")
    return errors


parsed = json.loads(result)
validation_errors = validate_extraction(parsed, acquisition_schema)
Out[12]:
Console
Extraction valid. All required fields present.
  Required fields found: ['event_type', 'acquirer', 'target']
  Optional fields populated: ['deal_value', 'announcement_date', 'announcement_person', 'location']

When structured output mode is not available, adding "Return only valid JSON. Do not include any explanation." to the prompt and validating with json.loads() with a retry on parse failure achieves similar reliability. A simple retry loop that re-prompts with "The previous response was not valid JSON. Try again." handles most failure cases within two attempts. For the rare case where the model consistently fails to produce valid JSON for a particular input, logging the failure and falling back to a rule-based extraction or manual review is the appropriate response.

Multi-Entity Extraction

Many real documents contain multiple events or multiple entities of the same type. A single news article may mention five acquisitions. The schema must accommodate lists, and the prompt must be explicit about including all instances.

The challenge is completeness: LLMs tend to focus on the most prominent event in a document and undercount secondary events. Explicitly asking for "a list of ALL events" and specifying that the list may contain multiple items reduces this problem. For critical applications, running a separate extraction pass per candidate entity pair (found by an NER scan) and then deduplicating is more reliable than a single all-events extraction.

In[13]:
Code
# Simulate multi-entity extraction from a paragraph
multi_entity_text = (
    "In a busy quarter for tech M&A, Microsoft acquired Activision Blizzard for $68.7 billion, "
    "while Google invested $300 million in Anthropic. "
    "Adobe's attempted $20 billion acquisition of Figma was later abandoned."
)

# Expected output structure for multi-entity extraction
expected_multi_output = {
    "events": [
        {
            "event_type": "acquisition",
            "acquirer": {"name": "Microsoft", "type": "organization"},
            "target": {"name": "Activision Blizzard", "type": "organization"},
            "deal_value": {"amount": 68700000000, "currency": "USD"},
            "status": "completed",
        },
        {
            "event_type": "investment",
            "acquirer": {"name": "Google", "type": "organization"},
            "target": {"name": "Anthropic", "type": "organization"},
            "deal_value": {"amount": 300000000, "currency": "USD"},
            "status": "completed",
        },
        {
            "event_type": "acquisition",
            "acquirer": {"name": "Adobe", "type": "organization"},
            "target": {"name": "Figma", "type": "organization"},
            "deal_value": {"amount": 20000000000, "currency": "USD"},
            "status": "abandoned",
        },
    ]
}

n_events = len(expected_multi_output["events"])
event_types = [e["event_type"] for e in expected_multi_output["events"]]
total_value = sum(
    e["deal_value"]["amount"]
    for e in expected_multi_output["events"]
    if e.get("deal_value")
)
Out[14]:
Console
Events extracted: 3
Event types: ['acquisition', 'investment', 'acquisition']
Total deal value across events: $89.0B

  Microsoft -> Activision Blizzard: $68.7B (completed)
  Google -> Anthropic: $0.3B (completed)
  Adobe -> Figma: $20.0B (abandoned)

The list-based schema forces the model to enumerate all events explicitly. This is more reliable than asking for a "summary" because the structure makes completeness auditable: you can check whether the number of events in the JSON matches the number of acquisition verbs in the source text. Including a "status" field ("completed" vs. "abandoned") captures negation, which is a common failure mode for rule-based systems that trigger on keywords like "acquisition" without detecting the negative context "was later abandoned."

Negation handling deserves special attention. A sentence like "Adobe's attempted acquisition of Figma was later abandoned" contains an acquisition trigger but describes an event that did not complete. Rule-based systems often miss this because they match keywords without understanding the modifier "attempted" and the completion marker "abandoned." LLMs handle this naturally because they understand the sentence semantically, not just lexically. Fine-tuned classifiers can also learn negation patterns if they are represented in training data, but they require negative examples that explicitly mark abandoned, failed, or hypothetical events.