Free text vs a validated object

Every QueryEngine so far (Lesson 5, 01_beginner) hands back a Response whose .response is a plain string, the LLM's synthesized answer in its own words. That's fine for a human reading it, but brittle for a program: if you need max_days as an int to feed into a conditional, you'd have to regex it out of a sentence.

LlamaIndex's output_cls parameter fixes this by asking the LLM to fill in a Pydantic model's fields instead of writing free text, then validating the result against that model before returning it. A malformed or missing field raises instead of silently handing you a broken string to parse.

Free-text QueryEngineoutput_cls-constrained QueryEngine
.response typestrAn instance of your Pydantic model
Shape guaranteed?No, whatever the LLM wroteYes, validated against the model
Good forA human reading the answerFeeding the answer into more code
LangChain equivalentmodel.invoke(prompt)model.with_structured_output(Schema)

Two ways to get structured output

This lesson shows both, verified against this repo's installed llama-index-core (0.14.23):

1. index.as_query_engine(output_cls=SomeModel), RAG plus structure: retrieve relevant Nodes, then synthesize an answer shaped like SomeModel instead of a sentence. 2. Settings.llm.as_structured_llm(SomeModel), structure with no index or retrieval: wrap the LLM directly when you already have the text and just want it coerced into a schema.

Both routes go through the same underlying mechanism, LlamaIndex builds a JSON-schema-constrained prompt from your Pydantic model and validates the LLM's response against it with Pydantic itself.

The code, piece by piece

class PolicySummary(BaseModel):
topic: str = Field(description="What policy this summary is about, in a few words")
max_days: int = Field(description="The maximum number of days mentioned in the policy, as an integer")
summary: str = Field(description="A one-sentence plain-English summary of the policy")

A plain Pydantic model describing the *shape of the answer*, not the question. Field descriptions matter here, they get included in the schema the LLM is asked to fill in, so a clear description is doing real prompting work, not just documentation.

query_engine = index.as_query_engine(output_cls=PolicySummary)
response = query_engine.query("Summarize the vacation policy's carryover rule...")

Same as_query_engine() call as Lesson 5, plus one new keyword. Retrieval still happens exactly as before, only the response synthesis step changes shape.

parsed: PolicySummary = response.response
print(parsed.max_days)

response.response is now a PolicySummary instance, not a string. Normal attribute access, normal types, no string-parsing.

structured_llm = Settings.llm.as_structured_llm(PolicySummary)
direct = structured_llm.complete("Vacation policy: employees carry over at most 5 unused vacation days...")
direct_parsed: PolicySummary = direct.raw

The index-free route. .complete() returns a CompletionResponse whose .raw attribute holds the parsed Pydantic object, useful when you already have the source text in hand (e.g. from your own retrieval, or a paragraph pasted into the prompt) and only need the schema-coercion part.

Checkpoint

  • output_cls: pass a Pydantic model to as_query_engine() (or as_structured_llm() on an LLM directly) to get a validated object back instead of free text.
  • index.as_query_engine(output_cls=Model) combines retrieval with structured synthesis, RAG plus a schema.
  • Settings.llm.as_structured_llm(Model) skips retrieval entirely, useful when you already have the source text.
  • Field descriptions in your Pydantic model are part of the prompt, not just documentation, write them for the LLM as much as for future readers of your code.
  • Structured output guarantees *shape*, not which facts get included, that's still governed by retrieval and the source text given to the model.

If anything here still feels unclear, ask before moving to Lesson 11.