langchain reference

Pinned versions: langchain==1.4.0 (released 2026-09-03), which resolved langchain-core==1.6.2, langgraph==1.2.11 and (for the one live example) langchain-anthropic==1.7.1. Everything below was derived by inspecting these exact installed packages, and every offline example was executed against them on Python 3.13. Each heading links to reference.langchain.com, which is unversioned and always serves the latest 1.x. Links open in a new tab.

Part of a five-page set β€” see how the LangChain stack fits together for how langchain-core, langgraph and langchain fit together.

What langchain is in 1.x

  • It is the prebuilt agent layer on top of langgraph. It no longer executes anything itself; it assembles a StateGraph and hands it to the langgraph runtime.
  • An agent from create_agent is a compiled langgraph StateGraph. The return annotation is literally CompiledStateGraph[AgentState, ...], so invoke, stream, get_state, checkpointers and interrupt all behave exactly as in the langgraph reference.
  • Its own contribution is the agent loop and middleware. The model↔tools cycle, structured output, and hooks for changing agent behaviour without rewriting the graph.
  • It is small. The package has seven subpackages: agents (whose public API is exactly create_agent and AgentState), agents.middleware, chat_models (init_chat_model), messages, tools, embeddings, rate_limiters β€” the last four re-export langchain-core names β€” and, new in 1.4.0, mcp (MCPAdapter, which turns an MCP server's tools into BaseTools for create_agent; it needs the langchain[mcp] extra, i.e. fastmcp>=4, and raises ModuleNotFoundError without it).
  • Layer order: langchain β†’ langgraph β†’ langchain-core. Import from the lowest layer that provides what you need.

Old tutorials warning: AgentExecutor, initialize_agent and the Chain classes (LLMChain, ConversationChain, …) are 0.x patterns now parked in langchain-classic (currently 1.0.8) β€” all are absent from langchain.agents in 1.4.0, and from langchain.agents import AgentExecutor raises ImportError: cannot import name 'AgentExecutor'. If a tutorial uses them, it predates 1.0.

Index

NameWhat it defines / doesMost-used parameters & methods
create_agentBuilds the model β†’ tools β†’ model … graph, binds the tools to the model, applies middleware, and returns it compiledmodel, tools, system_prompt, middleware, response_format, checkpointer
AgentStateThe graph's state TypedDict: messages (appended via add_messages), jump_to (middleware redirect, not persisted), structured_response (output only)messages, structured_response, jump_to
AgentMiddlewareBase class with six hook methods (each with an async twin) the loop calls at fixed points, plus state_schema and tools attributes to extend the agentbefore_model, after_model, wrap_model_call, wrap_tool_call
@before_modelDecorators (before_agent, before_model, after_model, after_agent, wrap_model_call, wrap_tool_call, dynamic_prompt) that wrap one function into an AgentMiddlewarebefore_model, after_model, wrap_model_call
HumanInTheLoopMiddlewareCalls langgraph interrupt() before the tools named in interrupt_on run; resumes on an approve/edit/reject/respond decision per callinterrupt_on, description_prefix
ContextEditingMiddlewareIn wrap_model_call, when the token count passes trigger, replaces old ToolMessage contents with a placeholder in the request only, not in stateedits, token_count_method
ToolStrategyresponse_format= value: exposes your schema to the model as a tool, parses the resulting call into state["structured_response"]schema, handle_errors
init_chat_modelSplits "provider:model", imports langchain-<provider> at call time, and returns that package's chat classmodel, model_provider, configurable_fields

create_agent

The one function most 1.x apps start from. What it does with its arguments:

  • model β€” a BaseChatModel, or a "provider:model" string handed to init_chat_model . It calls model.bind_tools(tools) so the model can emit tool_calls.
  • tools β€” BaseTools, plain functions (wrapped with @tool for you), or provider tool dicts. They become a ToolNode that runs each call in the last AIMessage and appends one ToolMessage per call.
  • system_prompt β€” a str or SystemMessage prepended to every model request (not stored in state).
  • middleware β€” a sequence of AgentMiddleware ; their hooks are spliced into the graph in order.
  • response_format β€” a schema or ToolStrategy /ProviderStrategy; the final answer is parsed into state["structured_response"].
  • state_schema / context_schema β€” extend AgentState with your own keys; type the read-only runtime.context.
  • checkpointer, store, interrupt_before, interrupt_after, name, cache, debug β€” passed straight through to StateGraph.compile().

It returns a CompiledStateGraph implementing the loop: call the model; if the reply has tool_calls, run them and go back to the model; otherwise end. Input and output are {"messages": [...]} dicts.

 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain_core.tools import tool
 4from langchain.agents import create_agent
 5
 6
 7class FakeToolModel(GenericFakeChatModel):
 8    """GenericFakeChatModel plus the bind_tools() the agent loop requires."""
 9
10    def bind_tools(self, tools, **kwargs):
11        return self
12
13
14@tool
15def add(a: int, b: int) -> int:
16    """Add two integers."""
17    return a + b
18
19
20# Scripted replies stand in for a real model: first a tool call, then the answer.
21model = FakeToolModel(messages=iter([
22    AIMessage(content="", tool_calls=[{"name": "add", "args": {"a": 2, "b": 3}, "id": "c1"}]),
23    AIMessage(content="The answer is 5."),
24]))
25
26agent = create_agent(model=model, tools=[add], system_prompt="You are a careful calculator.")
27result = agent.invoke({"messages": [{"role": "user", "content": "what is 2+3?"}]})
28
29for m in result["messages"]:
30    print(type(m).__name__, "|", repr(m.content))
31# HumanMessage | 'what is 2+3?'
32# AIMessage | ''            <- the tool call
33# ToolMessage | '5'
34# AIMessage | 'The answer is 5.'
35
36print(type(agent).__name__)   # CompiledStateGraph
37print(agent.get_graph().draw_mermaid())

The reveal β€” create_agent returns a CompiledStateGraph, so you can print the graph it built for you:

START to model; model to END (conditional); model to tools (conditional); tools to model (conditional).

model -.-> tools and tools -.-> model are the agent loop; model -.-> __end__ is the exit taken when the model replies without tool calls. (The picture is draw_mermaid(with_styles=False) rendered; the styled variant the code prints adds colours and wraps the start/end labels in <p> tags.)

The one live example

Everything else in this file runs offline. This is the single variant that talks to a real provider; it skips itself when ANTHROPIC_API_KEY is absent. Executed with a key on 2026-09-06: it printed 2 + 3 = 5.

 1import os
 2
 3from langchain_core.tools import tool
 4from langchain.agents import create_agent
 5from langchain_anthropic import ChatAnthropic
 6
 7
 8@tool
 9def add(a: int, b: int) -> int:
10    """Add two integers."""
11    return a + b
12
13
14if os.environ.get("ANTHROPIC_API_KEY"):
15    agent = create_agent(model=ChatAnthropic(model="claude-sonnet-5"), tools=[add])
16    result = agent.invoke({"messages": [{"role": "user", "content": "what is 2+3?"}]})
17    print(result["messages"][-1].text)
18else:
19    print("ANTHROPIC_API_KEY not set β€” skipping live example.")

↩ back to index


AgentState

The TypedDict the agent graph runs on β€” a langgraph state with exactly three keys:

  • messages: Required[Annotated[list[AnyMessage], add_messages]] β€” the conversation; nodes return new messages and the reducer appends them (or replaces by id).
  • jump_to: NotRequired[JumpTo | None] β€” set by middleware to redirect control flow ("model", "tools" or "end"). Annotated EphemeralValue and PrivateStateAttr: it is cleared after each step and never appears in the output.
  • structured_response: NotRequired[ResponseT] β€” filled only when response_format is set; annotated OmitFromInput, so callers cannot pass it in.

Subclass it to carry extra keys (class MyState(AgentState): user_id: str) and pass the subclass as state_schema=; middleware can also declare a state_schema and the union of all of them is the graph's real state.

 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain.agents import AgentState, create_agent
 4
 5print(list(AgentState.__annotations__))
 6# ['messages', 'jump_to', 'structured_response']
 7
 8agent = create_agent(model=GenericFakeChatModel(messages=iter([AIMessage("hi back")])), tools=[])
 9result = agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
10print(list(result))                       # ['messages']   <- jump_to is private, structured_response unset
11print(result["messages"][-1].content)     # hi back

↩ back to index


AgentMiddleware

The extension point. Rather than rebuilding the graph, you attach objects that the loop calls at fixed points. The class defines six hooks, each a no-op until overridden and each with an a-prefixed async twin:

HookCalledReceives β†’ returns
before_agent(state, runtime)once, before the first model callstate update dict or None
before_model(state, runtime)before every model callstate update dict, or {"jump_to": "end"} to skip the model
wrap_model_call(request, handler)around every model callhandler(request) runs the model; you may edit request.messages, request.tools, request.system_message, request.model first, or return your own ModelResponse
after_model(state, runtime)after every model call, before tools runstate update dict or None
wrap_tool_call(request, handler)around every tool calledit request.tool_call, call handler, or return a ToolMessage yourself
after_agent(state, runtime)once, after the final replystate update dict or None

Two class attributes extend the agent itself: state_schema (a TypedDict whose keys are merged into AgentState) and tools (extra BaseTools the middleware brings along).

Middleware that ships in 1.4.0, all built from these hooks: SummarizationMiddleware (compress old turns), ContextEditingMiddleware, HumanInTheLoopMiddleware, ModelCallLimitMiddleware and ToolCallLimitMiddleware (stop runaway loops), ModelFallbackMiddleware, ModelRetryMiddleware, ToolRetryMiddleware, ToolErrorMiddleware, PIIMiddleware (redact patterns), TodoListMiddleware, LLMToolSelectorMiddleware and ProviderToolSearchMiddleware (narrow a large tool set), ShellToolMiddleware and FilesystemFileSearchMiddleware (add tools), LLMToolEmulator (fake tools for tests).

 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain.agents import create_agent
 4from langchain.agents.middleware import AgentMiddleware
 5
 6
 7class CountingMiddleware(AgentMiddleware):
 8    def __init__(self):
 9        super().__init__()
10        self.calls = 0
11
12    def before_model(self, state, runtime):
13        self.calls += 1
14        return None
15
16
17counter = CountingMiddleware()
18agent = create_agent(
19    model=GenericFakeChatModel(messages=iter([AIMessage("ok")])),
20    tools=[],
21    middleware=[counter],
22)
23agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
24print("before_model fired:", counter.calls)   # before_model fired: 1

↩ back to index


@before_model

For middleware that needs no state of its own, skip the subclass: decorate a single function and pass it straight to middleware=. Each decorator wraps the function into an AgentMiddleware instance that overrides exactly one hook. The family, one per hook plus one convenience:

  • @before_agent, @before_model, @after_model, @after_agent β€” fn(state, runtime); return None to leave the run unchanged, or a state update dict to change it.
  • @wrap_model_call, @wrap_tool_call β€” fn(request, handler); call handler(request) to proceed (possibly with an edited request) and return its result or your own.
  • @dynamic_prompt β€” fn(request) -> str; the string becomes the system prompt for that call, recomputed every turn.
 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain.agents import create_agent
 4from langchain.agents.middleware import AgentMiddleware, before_model, dynamic_prompt
 5
 6
 7@before_model
 8def log_turn(state, runtime):
 9    print("[middleware] messages so far:", len(state["messages"]))
10    return None
11
12
13@dynamic_prompt
14def prompt_for(request) -> str:
15    return f"You are answering turn {len(request.messages)}."
16
17
18agent = create_agent(
19    model=GenericFakeChatModel(messages=iter([AIMessage("hi back")])),
20    tools=[],
21    middleware=[log_turn, prompt_for],
22)
23print(type(log_turn).__name__, isinstance(log_turn, AgentMiddleware))
24# log_turn True   <- the decorator built an AgentMiddleware subclass named after the function
25print(agent.invoke({"messages": [{"role": "user", "content": "hi"}]})["messages"][-1].content)
26# [middleware] messages so far: 1
27# hi back

↩ back to index


HumanInTheLoopMiddleware

Pauses the agent before named tools run and waits for a human decision. What it does:

  • interrupt_on maps a tool name to True (always ask, all decisions allowed) or an InterruptOnConfig dict: allowed_decisions (subset of approve, edit, reject, respond), description (text or a callable building it), args_schema, and when (a predicate on the tool call, so only some calls pause).
  • In after_model, if the reply contains a call to such a tool, it calls langgraph's interrupt() with {"action_requests": [...], "review_configs": [...]} β€” one action_request per pending call, with its name, args and a description built from description_prefix.
  • It needs a checkpointer, because pausing means persisting the run; invoke returns an __interrupt__ key and you resume with Command(resume={"decisions": [...]}), one decision per action request, in order.
  • Decisions: {"type": "approve"} runs the call as is; {"type": "edit", "edited_action": {"name", "args"}} runs it with your arguments; {"type": "reject", "message": "..."} skips the call and feeds the message back to the model as the tool result; {"type": "respond", ...} answers the model directly without running the tool.
 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain_core.tools import tool
 4from langgraph.checkpoint.memory import InMemorySaver
 5from langgraph.types import Command
 6from langchain.agents import create_agent
 7from langchain.agents.middleware import HumanInTheLoopMiddleware
 8
 9
10class FakeToolModel(GenericFakeChatModel):
11    def bind_tools(self, tools, **kwargs):
12        return self
13
14
15@tool
16def add(a: int, b: int) -> int:
17    """Add two integers."""
18    return a + b
19
20
21model = FakeToolModel(messages=iter([
22    AIMessage(content="", tool_calls=[{"name": "add", "args": {"a": 2, "b": 3}, "id": "c1"}]),
23    AIMessage(content="The answer is 5."),
24]))
25
26agent = create_agent(
27    model=model,
28    tools=[add],
29    middleware=[HumanInTheLoopMiddleware(interrupt_on={"add": True})],
30    checkpointer=InMemorySaver(),
31)
32
33config = {"configurable": {"thread_id": "1"}}
34paused = agent.invoke({"messages": [{"role": "user", "content": "what is 2+3?"}]}, config)
35print(list(paused))                                       # ['messages', '__interrupt__']
36request = paused["__interrupt__"][0].value["action_requests"][0]
37print(request["name"], request["args"])                   # add {'a': 2, 'b': 3}
38
39resumed = agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config)
40print(resumed["messages"][-1].content)                    # The answer is 5.

↩ back to index


ContextEditingMiddleware

Context management. It implements wrap_model_call: before each model request it counts the tokens in the messages (token_count_method="approximate" by default, "model" to ask the model, or your own token_counter) and, if a threshold is crossed, rewrites the copy of the messages being sent to the model β€” the persisted state is untouched, so the full history is still there for later turns and for you. The rewriting is described by edits; the one built-in is ClearToolUsesEdit:

  • trigger=100000 β€” token count at which it acts;
  • keep=3 β€” the most recent tool results to leave alone;
  • clear_at_least=0 β€” keep clearing until at least this many tokens are freed;
  • clear_tool_inputs=False β€” also blank the arguments in the AIMessage.tool_calls;
  • exclude_tools=() β€” tool names never cleared;
  • placeholder="[cleared]" β€” what replaces the content.

Because it acts on the request, not the state, observing it means looking at what the model received β€” which is what the recording model below does.

 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain_core.tools import tool
 4from langchain.agents import create_agent
 5from langchain.agents.middleware import ClearToolUsesEdit, ContextEditingMiddleware
 6
 7seen: list[list] = []
 8
 9
10class RecordingModel(GenericFakeChatModel):
11    """Records the messages handed to the model on each call."""
12
13    def bind_tools(self, tools, **kwargs):
14        return self
15
16    def _generate(self, messages, stop=None, run_manager=None, **kwargs):
17        seen.append(list(messages))
18        return super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
19
20
21@tool
22def fetch_report(topic: str) -> str:
23    """Fetch a long report."""
24    return "LONG REPORT " * 50
25
26
27def second_model_call_sees(middleware):
28    seen.clear()
29    model = RecordingModel(messages=iter([
30        AIMessage(content="", tool_calls=[
31            {"name": "fetch_report", "args": {"topic": "sales"}, "id": "c1"}
32        ]),
33        AIMessage(content="Summarised."),
34    ]))
35    agent = create_agent(model=model, tools=[fetch_report], middleware=middleware)
36    result = agent.invoke({"messages": [{"role": "user", "content": "report?"}]})
37    return repr(seen[1][-1].content)[:40], repr(result["messages"][2].content)[:40]
38
39
40print("without:", second_model_call_sees([]))
41# without: ("'LONG REPORT LONG REPORT LONG REPORT LON", "'LONG REPORT LONG REPORT LONG REPORT LON")
42
43print("with:   ", second_model_call_sees(
44    [ContextEditingMiddleware(edits=[ClearToolUsesEdit(trigger=1, keep=0)])]
45))
46# with:    ("'[cleared]'", "'LONG REPORT LONG REPORT LONG REPORT LON")
47#           ^ what the model saw            ^ what stayed in state

↩ back to index


ToolStrategy

Structured output. Pass response_format= to create_agent and the final answer is parsed into your schema and placed in state["structured_response"] instead of being left as prose. Two strategies exist:

  • ToolStrategy(schema, handle_errors=True, tool_message_content=None) β€” registers schema as an extra tool named after it; when the model calls that tool the agent validates the arguments against the schema, appends a ToolMessage (its content from tool_message_content) and ends. Validation failures are fed back to the model for another try when handle_errors is on. Works with any tool-calling model. schema may be a Pydantic model, a dataclass, a TypedDict, a JSON-schema dict, or a Union of several.
  • ProviderStrategy(schema, strict=None) β€” uses the provider's native JSON-schema response mode instead of a tool; only for providers that have one.

Passing a bare schema (response_format=Answer) lets create_agent pick: ProviderStrategy when the model's profile says it supports native structured output, ToolStrategy otherwise.

 1from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
 2from langchain_core.messages import AIMessage
 3from langchain.agents import create_agent
 4from langchain.agents.structured_output import ToolStrategy
 5from pydantic import BaseModel
 6
 7
 8class Answer(BaseModel):
 9    value: int
10    explanation: str
11
12
13class FakeToolModel(GenericFakeChatModel):
14    def bind_tools(self, tools, **kwargs):
15        return self
16
17
18model = FakeToolModel(messages=iter([
19    AIMessage(content="", tool_calls=[
20        {"name": "Answer", "args": {"value": 5, "explanation": "2+3=5"}, "id": "s1"}
21    ]),
22]))
23
24agent = create_agent(model=model, tools=[], response_format=ToolStrategy(Answer))
25result = agent.invoke({"messages": [{"role": "user", "content": "what is 2+3?"}]})
26
27answer = result["structured_response"]
28print(type(answer).__name__, "|", answer.value, "|", answer.explanation)
29# Answer | 5 | 2+3=5
30print([type(m).__name__ for m in result["messages"]])
31# ['HumanMessage', 'AIMessage', 'ToolMessage']   <- the schema "tool" was called and answered

↩ back to index


init_chat_model

Builds a chat model from a "provider:model" string, so the provider becomes configuration rather than a hard-coded import. What it does: splits the string on : (or takes model_provider=, or infers the provider from well-known model-name prefixes such as claude-/gpt-), maps the provider to a package (anthropic β†’ langchain_anthropic.ChatAnthropic, openai β†’ langchain_openai.ChatOpenAI, ollama, google_genai, bedrock, …), imports that package at call time, and instantiates the class with the remaining kwargs (temperature=, max_tokens=, …). With configurable_fields=, it instead returns a wrapper whose model can be chosen per call through config.

It does not remove your dependency on the provider package. The package must already be installed β€” if it isn't, you get an ImportError telling you what to pip install. The dependency moves from import-time to call-time; it does not disappear. create_agent(model="anthropic:...") calls this function for you.

 1from langchain.chat_models import init_chat_model
 2
 3# langchain-anthropic IS installed here, so this constructs fine (no API key needed to build it).
 4model = init_chat_model("anthropic:claude-sonnet-5")
 5print(type(model).__name__)   # ChatAnthropic
 6
 7# langchain-openai is NOT installed here β€” the dynamic import fails at call time.
 8try:
 9    init_chat_model("openai:gpt-4o-mini")
10except ImportError as e:
11    print("ImportError:", e)
12# ImportError: Initializing ChatOpenAI requires the langchain-openai package.
13# Please install it with `pip install langchain-openai`

↩ back to index