n4nAI

LangChain function calling for data extraction

Hands-on langchain function calling structured extraction tutorial: use Pydantic and with_structured_output to parse text into typed objects.

n4n Team2 min read457 words

Audio narration

Coming soon — every post will get a voice note here.

This langchain function calling structured extraction tutorial walks through extracting typed records from raw text using LangChain’s with_structured_output and Pydantic. You’ll build a small pipeline that parses emails, support tickets, or job posts into validated Python objects without hand-written regex.

Prerequisites

  • Python 3.10 or newer
  • langchain, langchain-openai, pydantic v2, python-dotenv
  • An OpenAI-compatible API key (OpenAI, or a gateway that exposes the same surface)
pip install langchain langchain-openai pydantic python-dotenv

Create a .env with your key:

OPENAI_API_KEY=sk-...
# or for a gateway:
# OPENAI_API_KEY=your-gateway-key
# OPENAI_BASE_URL=https://api.n4n.ai/v1
from dotenv import load_dotenv
load_dotenv()

Define the extraction schema

Function calling works by giving the model a JSON schema. LangChain converts a Pydantic model into that schema automatically. Start with a flat structure:

from pydantic import BaseModel, Field
from typing import List, Optional

class Person(BaseModel):
    name: str = Field(description="Full name of the person")
    age: Optional[int] = Field(default=None, description="Age in years if stated")
    emails: List[str] = Field(
        default_factory=list,
        description="Associated email addresses"
    )

class DocumentExtraction(BaseModel):
    people: List[Person]

The description fields are not optional. The model uses them to decide which text spans map to which fields. Omit them and you’ll get weaker extraction.

Bind the model to the schema

ChatOpenAI exposes with_structured_output. This wraps the model in a Runnable that sends a function definition alongside the prompt and parses the returned arguments.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(DocumentExtraction)

If you are routing through a gateway, set base_url on the constructor. For example, n4n.ai provides one OpenAI-compatible endpoint covering 240+ models and will forward provider cache-control hints, so the same with_structured_output code works unchanged.

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    base_url="https://api.n4n.ai/v1",  # optional gateway
)

Checkpoint: first extraction

Run a short block of text:

text = """
From: John Doe <john@example.com>
Jane Smith (age 34) reached out via jane@work.org. Also met Bob, no email.
"""

result = structured_llm.invoke(text)
print(result.model_dump())

Expected output (field order may vary):

{
  "people": [
    {"name": "John Doe", "age": null, "emails": ["john@example.com"]},
    {"name": "Jane Smith", "age": 34, "emails": ["jane@work.org"]},
    {"name": "Bob", "age": null, "emails": []}
  ]
}

The model inferred John Doe from the From: line and attached his email. Bob has no email, so the list is empty rather than null.

Extract from a realistic support ticket

Real documents are noisier. Extend the schema to capture the ticket metadata:

from enum import Enum

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class Ticket(BaseModel):
    id: str = Field(description="Ticket identifier like TK-1234")
    subject: str
    priority: Priority
    requester: Person
    mentions: List[Person] = Field(
        default_factory=list,
        description="Other people named in the ticket"
    )

class TicketExtraction(BaseModel):
    ticket: Ticket

Bind and run:

ticket_llm = llm.with_structured_output(TicketExtraction)

ticket_text = """
TK-4821: VPN drops every 10 minutes
Priority: high
From: Alice Nguyen <alice@corp.com>
CC: Bob (bob@corp.com), Carol
The VPN tunnel collapses under load. Carol said she sees the same on WiFi.
"""

out = ticket_llm.invoke(ticket_text)
print(out.ticket.priority, out.ticket.requester.name)

Output:

Priority.HIGH Alice Nguyen

out.ticket.mentions will contain Bob with his email and Carol with none. The enum forces the model to pick one of the three strings; if it returns urgent the parse step raises before your code sees it.

Strict mode and validation

In this langchain function calling structured extraction tutorial we enable strict mode to lock down the schema. For models that support strict JSON schema (recent OpenAI and some open weights), pass strict=True:

strict_llm = llm.with_structured_output(TicketExtraction, strict=True)

Strict mode disables “additional properties” and requires all fields present, which reduces hallucinated keys. If the model still returns a missing required field, LangChain raises OutputParserException. Catch it and fall back to a cheaper model or a retry with a clearer prompt:

from langchain_core.exceptions import OutputParserException

try:
    data = strict_llm.invoke(ticket_text)
except OutputParserException as e:
    print("parse failed:", e)
    data = ticket_llm.invoke(ticket_text)  # non-strict fallback

Why function calling beats prompt-only JSON

Earlier LangChain versions relied on asking the model to “return JSON” and then parsing with json.loads. That breaks when the model wraps output in markdown fences or adds commentary. Function calling moves the contract into the API layer: the model returns arguments to a named function, and the provider guarantees well-formed JSON matching the schema. You lose the temptation to write response.text.strip().lstrip("```json") in production.

Also, function calling lets you register multiple schemas and let the model pick. For a router that decides between TicketExtraction and PersonExtraction, bind a union or use a two-step classify-then-extract pattern.

Debugging extraction failures

When strict=True raises, print the raw message:

raw = llm.invoke(prompt.format_messages(input=ticket_text))
print(raw.additional_kwargs.get("function_call"))

This shows what the model actually attempted. Common fixes: tighten field descriptions, add examples in the system prompt, or split the document. If you see the model returning emails: null instead of [], set default_factory=list and avoid Optional[List] unless you truly need tri-state.

Handling nested lists at scale

When you expect many entities, cap the work per call. Split a 50-page PDF into sections and extract per section, then merge:

def extract_sections(sections: List[str]) -> List[DocumentExtraction]:
    results = []
    for sec in sections:
        r = structured_llm.invoke(sec)
        results.append(r)
    return results

If a section returns zero people, that’s a valid empty list, not an error. Log the section id and move on.

Prompt engineering still matters

Function calling is not a substitute for a clear instruction. Prepend a system message:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "Extract every person and their contact info. If age is not stated, omit it."),
    ("user", "{input}")
])

chain = prompt | structured_llm
chain.invoke({"input": text})

The schema tells the model how to format; the system message tells it what to look for.

Production considerations

  • Set temperature=0 for deterministic extraction.
  • Cache schema definitions; rebuilding Pydantic models per request wastes CPU.
  • Meter token usage. If you use a gateway with per-token usage metering, inspect response.usage on the raw call to track cost per document type.
  • Validate downstream. Trust but verify: a Person with age: 999 passed Pydantic but fails business logic.

Wrapping up

You now have a runnable langchain function calling structured extraction tutorial pattern: define Pydantic, bind with with_structured_output, and pipe text through a prompted chain. Swap the model name or base URL without touching the schema, and keep strict mode on for high-value pipelines.

Tagslangchainfunction-callingstructured-outputextraction

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain structured output & parsers posts →