Why this belongs in a node
with_structured_output, from the LangChain course, already showed that a model can return a validated Pydantic object instead of loose text. Nothing about that changes inside a graph, the interesting part is what happens next: a downstream node can read specific fields out of that object and make a decision, exactly like Beginner Lesson 4's conditional routing, except now the thing being routed on was extracted by the model itself, not typed in by hand.
The schema
class FeedbackAnalysis(BaseModel): topic: str = Field(description="The single main subject of the feedback, in a few words.") sentiment: Literal["positive", "negative", "neutral"] = Field( description="The overall sentiment of the feedback." )Same idea as any Pydantic model since the LangChain course, a shape the model has to fill in correctly. Literal[...] constrains sentiment to exactly three allowed values, so the downstream node can safely assume it never sees anything else.
The extraction node
structured_model = model.with_structured_output(FeedbackAnalysis)
def extract(state: State) -> dict: last_user_message = state["messages"][-1].content analysis = structured_model.invoke(last_user_message) return {"topic": analysis.topic, "sentiment": analysis.sentiment}structured_model.invoke(...) returns a FeedbackAnalysis instance directly, not an AIMessage, there's no .tool_calls or .text to unwrap here. The node pulls topic and sentiment off that object and writes them into two new state fields.
The state fields with no reducer
class State(TypedDict): messages: list topic: str sentiment: strtopic and sentiment are plain str fields, no Annotated reducer like add_messages. That means whichever node writes to them simply replaces the previous value, appropriate here since each run should hold exactly one topic and one sentiment, not a growing list of them.
Routing on what the model extracted
def respond(state: State) -> dict: if state["sentiment"] == "negative": reply = f"I'm sorry to hear about the trouble with {state['topic']}. ..." elif state["sentiment"] == "positive": reply = f"Glad to hear {state['topic']} is working well for you!" else: reply = f"Thanks for the note about {state['topic']}." return {"messages": [reply]}respond never looks at the raw feedback text again. It trusts the structured fields extract already wrote, the same separation of concerns as any two-node pipeline: one node's job is to understand, the next node's job is to act on that understanding.
Checkpoint
- structured output in a node:
with_structured_outputworks identically inside a node as it does at the top level, it just writes into state instead of standing alone. - no reducer on a field: plain
str(or any type withoutAnnotated[..., reducer]) means "replace", not "accumulate". - routing on extracted fields: a downstream node can make decisions based on structured data an earlier node derived from free text.
If anything here still feels unclear, ask before moving to Lesson 25.