Watercolor marine scene
← Back to blog

TECHNICAL GUIDE

Integrating Confidence Scores Into Real Agent Frameworks (LangChain, LangGraph, AutoGen, Instructor)

Wiring confidence scores into LangChain, LangGraph, AutoGen, and Instructor — without rebuilding your stack.

Paulina XuApr 30, 202610 min
EngineeringFrameworksAgents

TL;DR

Confidence scoring rarely fails on the math. It fails on where the number lives in your framework's control flow, because LangChain, LangGraph, AutoGen, and Instructor each assume a different shape for that flow. A score that works as a Runnable output in LangChain doesn't map cleanly onto LangGraph's state machine, and neither maps onto AutoGen's multi-agent conversation.

The signal itself is usually simple: a self-reported number, sometimes checked by a second model. We're not covering how to make that number trustworthy here. A companion post on building the raw signal covers self-rating, entropy, and calibration from scratch.

What each framework changes is the wiring: LangChain treats confidence as a field you route on inside a chain, LangGraph makes it an edge-selection condition between nodes, AutoGen turns it into agreement or disagreement between two agents, and Instructor just guarantees the field is always present and valid.

None of that wiring improves the number underneath it. What changes is the distance a bad score has to travel before it reaches a decision.

Pick the pattern that matches your control flow, wire in the field, and log every score next to its eventual outcome, so you can tell later whether the number was worth trusting.

Overview

Take a team three months into building an agent. Nobody is starting from a blank Python file at that point. They're already inside LangChain, LangGraph, AutoGen, or Instructor, and the actual question isn't "how do I build an agent." It's "how do I make the agent I already have admit when it isn't sure," which is a wiring problem, not a modeling problem.

Each of these four frameworks encodes a different assumption about how control flow moves. LangChain composes Runnables end to end. LangGraph is a state machine with named nodes and explicit edges. AutoGen is a conversation between agents that hand messages back and forth. Instructor doesn't touch control flow at all; it just guarantees the shape of what comes back. A confidence score that plugs cleanly into one of these often needs to be rebuilt, not just re-typed, to plug into another.

We think that's the gap underneath all of this: where the score lives, what decides where it routes, and the one place in each ecosystem's API where an older pattern, a manual retry loop, an if confidence < threshold scattered through business logic, tends to survive past the point where the framework already provides something better.

The framework you picked already has an opinion about where a confidence signal belongs. Fight that opinion and you end up bolting a parallel control-flow system onto the one you're already running. Work with it and the score becomes a first-class part of the graph, the chain, or the conversation, instead of an afterthought stapled onto the output.

LangChain: Confidence as an Output Parser + Post-Processing Chain

LangChain's Runnables and structured-output abstractions make confidence scoring straightforward to embed. Because everything is a Runnable, the score is just another field in your output schema, and the parsed result pipes straight into downstream logic with the | operator.

Define a structured output model (Pydantic)

Start with a schema. A description on each field nudges the model toward filling it out deliberately, and the ge/le constraints reject any score outside the 0-1 range before it reaches your code.

python
from pydantic import BaseModel, Field

class AnswerWithConfidence(BaseModel):
    answer: str = Field(..., description="The answer to the question.")
    confidence: float = Field(
        ..., ge=0, le=1,
        description="How confident you are in the answer, from 0 to 1.",
    )

Build a prompt + chain

The cleanest modern approach is with_structured_output, which binds the schema directly to the model and returns a populated Pydantic object: no separate parser step, no brittle JSON post-processing. Note the import: chat models now live in the dedicated langchain_openai package rather than under langchain.chat_models.

python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer the question and rate your confidence (0-1)."),
    ("user", "{question}"),
])

model = ChatOpenAI(model="gpt-4.1", temperature=0)
structured_model = model.with_structured_output(
    AnswerWithConfidence, method="json_schema"
)

chain = prompt | structured_model

If you prefer an explicit parser, for example to fall back gracefully on malformed output, PydanticOutputParser still works and slots in as the final Runnable: prompt | model | parser. Both produce the same typed object.

Call it

result = chain.invoke({"question": "What is the capital of Peru?"})
print(result.answer, result.confidence)

Add a post-processing router based on confidence

Now the score earns its keep. A plain Python function inspects confidence and decides what to surface: the answer directly when the model is sure, a clarifying question when it isn't.

python
def route(result: AnswerWithConfidence):
    if result.confidence < 0.5:
        return "I'm not sure—could you clarify the question?"
    return result.answer

Composite chain

Wrap the router in a RunnableLambda and append it to the chain. Prompt, model, structured parse, confidence gate: the whole pipeline is now one composable Runnable you can invoke, stream, or nest inside a larger graph.

python
from langchain_core.runnables import RunnableLambda

full_chain = chain | RunnableLambda(route)

The agent now answers directly when it's sure and asks for help when it isn't, and both paths live in the same chain rather than in a wrapper function somebody has to remember to call.

LangGraph: Confidence as Edge Routing Logic

LangGraph encodes stateful agent workflows with branching based on confidence thresholds. Where a LangChain router collapses every branch into one function, LangGraph makes each outcome a distinct node, so "confident," "needs clarification," and "fallback" each get their own logic, retries, and downstream edges.

The mechanism is the conditional edge. Confidence goes into the state object, a router function reads that state and returns a string, and add_conditional_edges maps each returned string to a target node.

Define LangGraph State

python
from typing import TypedDict, Optional

class AgentState(TypedDict):
    question: str
    answer: Optional[str]
    confidence: Optional[float]

Step 1: LLM Node

Each node receives the current state and returns a partial update that LangGraph merges in. Here we reuse the confidence-aware chain from the LangChain section.

python
def answer_node(state: AgentState):
    res = chain.invoke({"question": state["question"]})
    return {"answer": res.answer, "confidence": res.confidence}

Step 2: Routing Function

A router function should only read state and return a string. No LLM calls, no side effects. The string it returns is a label the conditional edge resolves to a concrete node.

python
def router(state: AgentState) -> str:
    conf = state["confidence"]
    if conf > 0.8:
        return "confident"
    elif conf > 0.5:
        return "needs_clarification"
    else:
        return "fallback"

Build the Graph

This is where the original pattern most often goes wrong: LangGraph doesn't accept a condition= argument on add_edge. Branching is wired with add_conditional_edges, passing the source node, the router callable, and a path map that translates each returned label into a destination node.

python
from langgraph.graph import StateGraph, END

workflow = StateGraph(AgentState)

workflow.add_node("answer_node", answer_node)
workflow.add_node("respond", lambda s: {"answer": s["answer"]})
workflow.add_node("clarify", lambda s: {"answer": "Can you clarify?"})
workflow.add_node("fallback", lambda s: {"answer": "Not confident enough."})

workflow.set_entry_point("answer_node")

workflow.add_conditional_edges(
    "answer_node",
    router,
    {
        "confident": "respond",
        "needs_clarification": "clarify",
        "fallback": "fallback",
    },
)

workflow.add_edge("respond", END)
workflow.add_edge("clarify", END)
workflow.add_edge("fallback", END)

graph = workflow.compile()

Run

result = graph.invoke({"question": "Explain quantum entanglement"})
print(result)

The same threshold logic that lived in one function under LangChain becomes an inspectable, visualizable graph here, and each branch is free to grow its own multi-step subflow without crowding the router.

AutoGen: Confidence-Aware Assistant + Critic Agents

AutoGen makes multi-agent patterns trivial, which suits cross-model confidence validation well. Rather than trusting a single model's self-report, a second, independent agent grades the first one's answer, and you blend the two signals into a confidence estimate that's harder to fool.

Define the main assistant

The package ships on PyPI as pyautogen but imports as autogen. The assistant is instructed to return its answer and confidence as JSON, so it parses deterministically downstream.

python
from autogen import AssistantAgent, UserProxyAgent

assistant = AssistantAgent(
    name="assistant",
    llm_config={"config_list": [{"model": "gpt-4.1"}]},
    system_message="""Answer the user's question. Reply with ONLY valid JSON:
{
  "answer": "...",
  "confidence": 0.0
}
""",
)

Define a critic agent (cross-check model)

The critic runs on a different, often cheaper, model, so its judgment is independent of the assistant's. Ask it to return a bare number so the score is easy to extract.

critic = AssistantAgent(
    name="critic",
    llm_config={"config_list": [{"model": "gpt-4o-mini"}]},
    system_message="Evaluate the answer's factual accuracy. Reply with ONLY a number 0-1.",
)

Workflow

Generate the answer, parse it with json.loads rather than eval (never eval model output anywhere near production), hand the answer to the critic, and average the two scores into a final confidence.

python
import json

user = UserProxyAgent(name="user", human_input_mode="NEVER")

question = "What is the capital of Peru?"

resp = assistant.generate_reply(
    messages=[{"role": "user", "content": question}]
)
answer_data = json.loads(resp)  # assistant returns JSON

critique = critic.generate_reply(
    messages=[{"role": "user", "content": f"Rate accuracy 0-1:\
{answer_data['answer']}"}]
)
critic_score = float(critique.strip())

final_conf = (answer_data["confidence"] + critic_score) / 2

print("Answer:", answer_data["answer"])
print("Confidence:", final_conf)

AutoGen fits this pattern naturally because it already implements multi-agent cross-checking as its default shape, not a bolt-on. A large gap between the self-reported score and the critic's score is itself worth reading: it usually means the assistant is overconfident, and that gap is worth routing on or logging in its own right, separately from either number alone.

Instructor: Enforcing Structured Confidence via JSON / Pydantic

Instructor guarantees structured outputs, confidence scores included, by combining the provider's JSON or tool modes with Pydantic validation and automatic retries. If the model returns something that doesn't satisfy your schema, Instructor re-asks until it does, so the object that reaches your code is always well-formed.

Define Your Typed Schema

Note the current entry point: you wrap a real OpenAI client with instructor.from_openai rather than the older instructor.patch helper, and OpenAI is imported from the openai SDK, not from Instructor.

python
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field

class Response(BaseModel):
    answer: str
    confidence: float = Field(..., ge=0, le=1)

client = instructor.from_openai(OpenAI())

Prompt

Pass your Pydantic class as response_model. Instructor handles the schema injection, the JSON mode, and validation, then returns a populated Response instance.

result = client.chat.completions.create(
    model="gpt-4.1",
    messages=[
        {"role": "user", "content": "Explain the Doppler Effect."},
    ],
    response_model=Response,  # enforced + validated structure
    max_retries=2,            # re-ask if validation fails
)

Usage

print(result.answer)
print(result.confidence)

Instructor's advantage here is narrow and useful: the score is always present and always a validated float, so it goes straight into a database or metrics backend with no defensive null check standing in the way.

confidence field
0-1 float, one model's self-report

LangChain
Runnable output -> router function

LangGraph
state field -> conditional edge -> node

AutoGen
assistant score + critic score -> blended

Instructor
validated field, always present

same number,
four different control-flow shapes

Figure 1 — The same confidence field takes on a different shape in each framework's control flow. Picking the framework already answers "where does this number live."

Logging and Calibration: Closing the Loop

Routing on confidence is only half the job. The other half is finding out whether those scores mean anything: whether a high score really is more likely to be correct than a low one. That means logging the score next to the eventual outcome and watching the relationship over time.

Emit the score as structured telemetry

Whichever framework you're in, the score is just a number you can attach to a span or a log line. OpenTelemetry is a convenient sink, since most observability backends already understand it, and span attributes let you slice by confidence later.

python
from opentelemetry import trace

tracer = trace.get_tracer("agent.confidence")

def answer_with_telemetry(question: str):
    with tracer.start_as_current_span("answer") as span:
        result = chain.invoke({"question": question})
        span.set_attribute("agent.answer", result.answer)
        span.set_attribute("agent.confidence", result.confidence)
        return result

Check calibration once you have outcomes

Once you can join each logged score to a label, whether the answer was ultimately correct, bucket predictions and compare the average confidence in each bucket to the actual accuracy in that bucket. A well-calibrated agent's line tracks the diagonal: scores near the top of the range correspond to answers that are right nearly all the time, and that relationship holds all the way down the scale, not just at the extremes.

python
from collections import defaultdict

def calibration_report(records, n_bins=10):
    # records: list of (confidence, was_correct)
    bins = defaultdict(list)
    for conf, correct in records:
        idx = min(int(conf * n_bins), n_bins - 1)
        bins[idx].append(correct)

    for idx in sorted(bins):
        outcomes = bins[idx]
        bucket = f"{idx / n_bins:.1f}-{(idx + 1) / n_bins:.1f}"
        accuracy = sum(outcomes) / len(outcomes)
        print(f"conf {bucket}: accuracy {accuracy:.2f} (n={len(outcomes)})")

A framework can guarantee the field. It can't guarantee the number means anything. Structured-output libraries validate that confidence is a float between 0 and 1. Only your own labeled outcomes tell you whether a given score from this pipeline is trustworthy or just consistently overconfident.

If the buckets are badly out of line, high scores landing on answers that are wrong a meaningful fraction of the time, the model is overconfident, and you should either raise your routing thresholds or recalibrate before trusting the raw score. This is the step that turns a self-report into a signal you can stake decisions on.

Final Notes: Which Framework Should You Choose?

There's no single right answer. The best fit depends on the shape of your control flow and how much branching your agent really needs.

LangChain suits a modular pipeline: a classic model-plus-tools-plus-chains shape where the score gates one mostly linear flow. LangGraph is the pick once you need real branching, state machines and agent flows where each outcome deserves its own multi-step subgraph instead of a shared router function. AutoGen already thinks in multiple agents, so ensemble confidence, a critic grading the assistant, comes for free. And Instructor is the narrow tool: strict typed outputs and stable telemetry ingestion, for when the question is just whether the field exists, not how you branch on it.

Conclusion

Whichever framework you're in, the underlying pattern doesn't change: make confidence a first-class field in your output schema, route on it using whatever branching mechanism that framework already gives you, and log it next to outcomes so you can check, later, whether it earned the trust you placed in it.

What does change is where that wiring happens. A router function in LangChain, a conditional edge in LangGraph, a second agent in AutoGen, a validated field in Instructor: four different places for the same idea to live, and picking the wrong one is how a perfectly good confidence score ends up ignored by the rest of the system.

The framework rarely limits what you can build. It limits how much custom plumbing you have to write to get there. Match the pattern to the framework's own idioms, and the confidence signal stops being an add-on and starts being part of how the agent already works.