lesson-28

19 MIN READ | UPDATED: 2026-05-07

Welcome back, architects, to our LangGraph Multi-Agent Masterclass. It's your old friend here.

After 27 episodes of hard-fought battles, our "AI Content Agency" has finally taken shape. The Planner strategizes, the Researcher scours the web, and the Writer drafts tirelessly. Watching the endless streams of text scrolling through your terminal, you might even get the illusion that "AI is already ruling the world."

But as a developer with 10 years of experience, I have to pour some cold water on you: if your AI system can only output a blob of plain text, it is practically unusable from an engineering standpoint.

Think about what your downstream systems (like your company's CMS, a database, or a frontend dashboard) actually need. They need a precise Title, a structured Outline, an array of Citations, and an integer Word Count.

If you are still using Regular Expressions (Regex) to parse strings output by large models, or begging the model in your Prompt: "Please, I beg you, swear to only output JSON format, do not include any extra nonsense"... then this episode is here to save your code and your hairline.

Today, we are going to inject a "soul contract" into the final gatekeeper of our Agency—the Editor Agent: Structured Outputs. We will transform the final artifact of our graph from uncontrollable natural language into a data structure with a strict JSON Schema.


🎯 Learning Objectives for this Episode

By the end of this lesson, I expect you to master the following points and be able to apply them immediately in a production environment:

  1. Cognitive Upgrade: Completely understand the underlying logical differences between Prompt Engineering constraints, JSON Mode, and Tool Calling (Structured Outputs).
  2. Core API Mastery: Proficiently use the .with_structured_output() method in LangChain/LangGraph.
  3. Architecture Refactoring: Introduce Pydantic data validation to the AI Content Agency's Editor Agent, outputting a final Payload with strict fields like title, outline, citations, and word count.
  4. State Graph Integration: Seamlessly write structured data into the LangGraph State, perfectly interfacing with traditional software APIs.

📖 Understanding the Concepts

Before we write any code, let's talk theory. Why is extracting complex data so difficult?

Large Language Models (LLMs) are essentially "probability prediction machines." They naturally love to improvise. Traditional software engineering, however, is all about "determinism." Structured Outputs serve as a bridge between probability and determinism.

To get LLMs to output structured data, the industry has gone through three stages:

  • Stage 1: Prompt Constraints (The Stone Age). Writing Output as JSON only in the prompt. The result? The LLM often replies: "Sure, here is your JSON:...", instantly breaking your JSON parser.
  • Stage 2: JSON Mode (Semi-Automatic Weapons). Major APIs introduced response_format={ "type": "json_object" }. This guarantees the output is valid JSON, but it does not guarantee the fields inside the JSON are correct. You might ask for title, and it gives you heading.
  • Stage 3: Function/Tool Calling Forced Schema (Modern Warfare). We define the required JSON structure as a "Function Signature." For the LLM to call this function, it must strictly populate the data according to our defined parameter types (Pydantic Schema). This is currently the most stable and elegant solution!

How does this mechanism work within our AI Content Agency's workflow? Let's look at the architecture diagram below:

graph TD
    subgraph LangGraph State Flow
        A[State: Draft generated by Writer] --> B(Editor Agent Node)
        R[State: Reference Links provided by Researcher] --> B
    end

    subgraph Editor Agent Internal Logic
        B -->|1. Assemble Prompt and Context| C{LLM with Structured Output}
        C -.->|2. Convert to Tool Calling under the hood| D[OpenAI / Anthropic API]
        D -.->|3. Return Schema-compliant JSON| E[Pydantic Validation]
    end

    E -->|Validation Failed: Auto-retry| C
    E -->|Validation Successful| F[State: Final Article Payload]
    
    F --> G[(Downstream Systems: CMS / Database / API)]

    classDef state fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
    classDef agent fill:#fff3e0,stroke:#e65100,stroke-width:2px;
    classDef core fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px;
    
    class A,R,F state;
    class B agent;
    class C,E core;

Diagram Explanation:

  1. The Editor Agent receives the plain text Draft and scattered reference materials.
  2. Instead of calling llm.invoke() directly, we call llm.with_structured_output(ArticleSchema) bound to a Pydantic model.
  3. The underlying LLM is forced to fill the extracted content into the slots defined by the ArticleSchema.
  4. Pydantic performs strict local validation (Are the types correct? Are required fields present?).
  5. It ultimately outputs a perfect Python object, saves it to the Graph State, and makes it directly available for downstream use.

💻 Hands-on Code Practice

Enough talk, show me the code.

We will use Python, LangGraph, and Pydantic to refactor the Editor node. Please ensure you have installed langchain-openai, langgraph, and pydantic.

Step 1: Define a Strict Data Contract (Pydantic Schema)

This is the core of structured outputs. You need to treat the LLM as a "form-filling machine." The more rigorously you design this form, the better the LLM will fill it out.

from pydantic import BaseModel, Field
from typing import List

# Define the structure we want the Editor to ultimately output
class ArticlePayload(BaseModel):
    """Final article data structure delivered to the CMS system"""
    
    title: str = Field(
        description="The final title of the article, must be engaging, SEO-friendly, and under 20 words"
    )
    outline: List[str] = Field(
        description="The outline hierarchy of the article, extract all H2 and H3 headings into an array"
    )
    citations: List[str] = Field(
        description="Citation links or references extracted from the original text and reference materials. Empty array if none."
    )
    word_count: int = Field(
        description="Approximate word count of the main body text (integer)"
    )
    final_content: str = Field(
        description="The final Markdown formatted body content after being polished by the Editor"
    )
    seo_keywords: List[str] = Field(
        description="Extract 3-5 core SEO keywords"
    )

Instructor's Note: Pay attention to the description here! In traditional development, comments are for humans to read; but in LLM development, Pydantic's description is part of the Prompt for the AI! The clearer you write it here, the more accurate the data the AI extracts will be.

Step 2: Define the LangGraph State

We need to reserve a spot in the global state for this structured object.

from typing import TypedDict, Optional
from langgraph.graph import StateGraph, END

class AgencyState(TypedDict):
    writer_draft: str          # Draft passed from the Writer
    research_links: List[str]  # Reference materials passed from the Researcher
    # 👇 Here is the core of this episode: we store the structured object in the State
    final_delivery: Optional[ArticlePayload] 

Step 3: Write the Editor Node Logic

This is where the magic happens. We will use the .with_structured_output() method.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

def editor_node(state: AgencyState):
    print("--- 👨‍⚖️ Editor Agent starting: Extracting and refactoring complex data ---")
    
    draft = state.get("writer_draft", "")
    links = state.get("research_links", [])
    
    # 1. Instantiate the LLM (Recommend using models with excellent Tool Calling support, like GPT-4o)
    # Note: Set temperature to 0, because we need deterministic data extraction now, not divergent creative writing
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    
    # 2. Bind structured output!!! (The core magic)
    # This line converts ArticlePayload into an OpenAI Function Calling Schema under the hood
    structured_llm = llm.with_structured_output(ArticlePayload)
    
    # 3. Write the Editor's Prompt
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are a strict, top-tier AI Editor-in-Chief.
        Your task is to review the Writer's draft, combine it with the reference materials, and output the final structured article data.
        Please strictly follow the requirements to extract the title, outline, citations, word count, SEO keywords, and provide the final polish for the main content."""),
        ("user", "[Draft Content]:\n{draft}\n\n[Reference Materials]:\n{links}")
    ])
    
    # 4. Assemble the Chain and execute
    chain = prompt | structured_llm
    
    # Execute the call. Note: 'result' here is no longer a string, but a native ArticlePayload object!
    result: ArticlePayload = chain.invoke({
        "draft": draft,
        "links": "\n".join(links)
    })
    
    print(f"✅ Extraction complete! Title: {result.title}, Word Count: {result.word_count}")
    
    # 5. Update the State
    return {"final_delivery": result}

Step 4: Assemble the Graph and Simulate a Run

Let's run the graph and see the results.

# Build the graph
workflow = StateGraph(AgencyState)
workflow.add_node("Editor", editor_node)

# For simplicity, we set the Editor directly as the entry and exit point
workflow.set_entry_point("Editor")
workflow.add_edge("Editor", END)

app = workflow.compile()

# === Simulated Run Demo ===
if __name__ == "__main__":
    # Simulate upstream data passed from the Writer and Researcher
    mock_state = {
        "writer_draft": """
        # Why do we need multi-agent systems?
        In today's AI landscape, monolithic large models have hit a bottleneck. Multi-Agent Systems greatly enhance the ability to solve complex tasks through division of labor and collaboration.
        ## Core Advantages
        1. Distributed Computing
        2. Role Specialization
        In short, multi-agent systems are the future.
        """,
        "research_links": [
            "https://arxiv.org/abs/1234.5678",
            "https://github.com/langchain-ai/langgraph"
        ]
    }
    
    # Run the graph
    final_state = app.invoke(mock_state)
    
    # Verify the output result
    delivery = final_state["final_delivery"]
    
    print("\n" + "="*40)
    print("🚀 Final Output JSON Structure (Simulating sending to CMS):")
    print("="*40)
    # Because delivery is a Pydantic object, we can directly call model_dump_json()
    print(delivery.model_dump_json(indent=2))

Expected Console Output:

{
  "title": "Multi-Agent Systems: The Future Beyond Monolithic AI Bottlenecks",
  "outline": [
    "Why do we need multi-agent systems?",
    "Core Advantages"
  ],
  "citations": [
    "https://arxiv.org/abs/1234.5678",
    "https://github.com/langchain-ai/langgraph"
  ],
  "word_count": 45,
  "final_content": "# Why do we need multi-agent systems?\n\nIn today's AI landscape, monolithic large language models (LLMs) have hit a bottleneck when dealing with highly complex business logic. Multi-Agent Systems have emerged as a solution, greatly enhancing the system's capacity to solve complex tasks through division of labor and collaboration.\n\n## Core Advantages\n\n1. **Distributed Computing**: Breaking down complex problems to be processed in parallel by different Agents.\n2. **Role Specialization**: Similar to human teams, the Planner, Researcher, and Writer each have their own duties, reducing hallucinations.\n\nIn short, multi-agent architecture is undoubtedly an important cornerstone towards AGI.",
  "seo_keywords": [
    "Multi-Agent Systems",
    "AI Bottleneck",
    "Distributed Computing",
    "AI Architecture"
  ]
}

Look at that! It's no longer messy plain text, but a perfectly typed JSON data structure ready to be inserted into a database or rendered by a frontend! This is what an industrial-grade AI application should look like.


Pitfalls & Best Practices (High-Level Troubleshooting Experience)

As your mentor, I can't just teach you how to write pretty demos. In real production environments, structured outputs are often fraught with hidden dangers. Here is a troubleshooting guide I've summarized through blood, sweat, and tears:

💣 Pitfall 1: The LLM "Outsmarts" Itself, Causing Schema Validation Failure

Sometimes the LLM wraps the JSON in ```json ... ```, or if it feels it can't find a required field, it simply omits it, causing Pydantic to throw a ValidationError. 🛡️ Strategy:

  1. Use OpenAI's Strict Mode: If you are using the latest OpenAI models, LangChain natively supports OpenAI's Structured Outputs (strict=True) under the hood. This guarantees 100% Schema compliance at the API level.
  2. Fault Tolerance & Retry Mechanisms: In LangGraph, you can catch the ValidationError and route it as an Edge back to the Editor node, feeding the error message to the LLM as a Prompt: "The JSON you just generated threw an error. The reason is a missing 'title' field. Please correct it and output again."

💣 Pitfall 2: Stuffing Too Much Logic into One Massive Schema

I've seen students define a massive Pydantic model with over 50 fields nested 4 levels deep, expecting the LLM to deconstruct a tens-of-thousands-of-words article into this structure all at once. The result is not only slow but also suffers from severe hallucinations. 🛡️ Strategy: Divide and Conquer. Don't make the Editor do everything at once. You can split the nodes:

  • Extract_Meta_Node: Only responsible for extracting SEO keywords and word count.
  • Format_Content_Node: Only responsible for formatting Markdown. Keeping Schemas flat is the secret to stable LLM outputs.

💣 Pitfall 3: Not All Models Support .with_structured_output()

If you deploy a weaker open-source model locally (like Llama-2-7B), calling this method might throw an error or output garbage data that completely misses expectations. 🛡️ Strategy: For models that don't support native Tool Calling, LangChain provides fallback solutions like include_raw=True or using JsonOutputParser. But to be completely honest: when dealing with complex structured outputs, please make sure to use top-tier models like GPT-4o or Claude 3.5 Sonnet. When it comes to data contracts, trying to save money on tokens often leads to massive engineering maintenance costs.


📝 Episode Summary

Today, we completed the most engineering-valuable refactoring in our AI Content Agency architecture.

We learned:

  1. Why structured outputs are the bridge connecting AI with traditional software.
  2. How to use Pydantic to define rigorous data contracts (ArticlePayload).
  3. How to use LangChain's .with_structured_output() magic to force LLMs to output structured data.
  4. How to integrate this process into LangGraph's State to form a closed loop.

Now, our Editor Agent is no longer just a chatbot that "talks nonsense," but a core microservice capable of producing standard data interfaces.

Next Episode Teaser: The data structure might be perfect, but what if the boss isn't satisfied with the title the Editor extracted? AI ultimately needs human supervision. In Episode 29, we will introduce one of LangGraph's most fascinating features: Human-in-the-loop. We will make the graph pause after the Editor outputs the data, waiting for the Editor-in-Chief (you) to click "Approve" or make edits before continuing the workflow.

Architects, type out today's code and experience the thrill of having your data precisely manipulated. See you in the next episode!