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 aStateGraphand hands it to the langgraph runtime. - An agent from
create_agentis a compiled langgraphStateGraph. The return annotation is literallyCompiledStateGraph[AgentState, ...], soinvoke,stream,get_state, checkpointers andinterruptall 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 exactlycreate_agentandAgentState),agents.middleware,chat_models(init_chat_model),messages,tools,embeddings,rate_limitersβ the last four re-exportlangchain-corenames β and, new in 1.4.0,mcp(MCPAdapter, which turns an MCP server's tools intoBaseTools forcreate_agent; it needs thelangchain[mcp]extra, i.e.fastmcp>=4, and raisesModuleNotFoundErrorwithout it). - Layer order:
langchainβlanggraphβlangchain-core. Import from the lowest layer that provides what you need.
Old tutorials warning:
AgentExecutor,initialize_agentand theChainclasses (LLMChain,ConversationChain, β¦) are 0.x patterns now parked inlangchain-classic(currently 1.0.8) β all are absent fromlangchain.agentsin 1.4.0, andfrom langchain.agents import AgentExecutorraisesImportError: cannot import name 'AgentExecutor'. If a tutorial uses them, it predates 1.0.
Index
| Name | What it defines / does | Most-used parameters & methods |
|---|---|---|
create_agent | Builds the model β tools β model β¦ graph, binds the tools to the model, applies middleware, and returns it compiled | model, tools, system_prompt, middleware, response_format, checkpointer |
AgentState | The graph's state TypedDict: messages (appended via add_messages), jump_to (middleware redirect, not persisted), structured_response (output only) | messages, structured_response, jump_to |
AgentMiddleware | Base 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 agent | before_model, after_model, wrap_model_call, wrap_tool_call |
@before_model | Decorators (before_agent, before_model, after_model, after_agent, wrap_model_call, wrap_tool_call, dynamic_prompt) that wrap one function into an AgentMiddleware | before_model, after_model, wrap_model_call |
HumanInTheLoopMiddleware | Calls langgraph interrupt() before the tools named in interrupt_on run; resumes on an approve/edit/reject/respond decision per call | interrupt_on, description_prefix |
ContextEditingMiddleware | In wrap_model_call, when the token count passes trigger, replaces old ToolMessage contents with a placeholder in the request only, not in state | edits, token_count_method |
ToolStrategy | response_format= value: exposes your schema to the model as a tool, parses the resulting call into state["structured_response"] | schema, handle_errors |
init_chat_model | Splits "provider:model", imports langchain-<provider> at call time, and returns that package's chat class | model, model_provider, configurable_fields |
create_agent
The one function most 1.x apps start from. What it does with its arguments:
modelβ aBaseChatModel, or a"provider:model"string handed toinit_chat_model. It callsmodel.bind_tools(tools)so the model can emittool_calls.toolsβBaseTools, plain functions (wrapped with@toolfor you), or provider tool dicts. They become aToolNodethat runs each call in the lastAIMessageand appends oneToolMessageper call.system_promptβ astrorSystemMessageprepended to every model request (not stored in state).middlewareβ a sequence ofAgentMiddleware; their hooks are spliced into the graph in order.response_formatβ a schema orToolStrategy/ProviderStrategy; the final answer is parsed intostate["structured_response"].state_schema/context_schemaβ extendAgentStatewith your own keys; type the read-onlyruntime.context.checkpointer,store,interrupt_before,interrupt_after,name,cache,debugβ passed straight through toStateGraph.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:
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.")
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 byid).jump_to: NotRequired[JumpTo | None]β set by middleware to redirect control flow ("model","tools"or"end"). AnnotatedEphemeralValueandPrivateStateAttr: it is cleared after each step and never appears in the output.structured_response: NotRequired[ResponseT]β filled only whenresponse_formatis set; annotatedOmitFromInput, 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
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:
| Hook | Called | Receives β returns |
|---|---|---|
before_agent(state, runtime) | once, before the first model call | state update dict or None |
before_model(state, runtime) | before every model call | state update dict, or {"jump_to": "end"} to skip the model |
wrap_model_call(request, handler) | around every model call | handler(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 run | state update dict or None |
wrap_tool_call(request, handler) | around every tool call | edit request.tool_call, call handler, or return a ToolMessage yourself |
after_agent(state, runtime) | once, after the final reply | state 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
@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); returnNoneto leave the run unchanged, or a state update dict to change it.@wrap_model_call,@wrap_tool_callβfn(request, handler); callhandler(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
HumanInTheLoopMiddleware
Pauses the agent before named tools run and waits for a human decision. What it does:
interrupt_onmaps a tool name toTrue(always ask, all decisions allowed) or anInterruptOnConfigdict:allowed_decisions(subset ofapprove,edit,reject,respond),description(text or a callable building it),args_schema, andwhen(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'sinterrupt()with{"action_requests": [...], "review_configs": [...]}β oneaction_requestper pending call, with itsname,argsand adescriptionbuilt fromdescription_prefix. - It needs a
checkpointer, because pausing means persisting the run;invokereturns an__interrupt__key and you resume withCommand(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.
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 theAIMessage.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
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)β registersschemaas an extra tool named after it; when the model calls that tool the agent validates the arguments against the schema, appends aToolMessage(its content fromtool_message_content) and ends. Validation failures are fed back to the model for another try whenhandle_errorsis on. Works with any tool-calling model.schemamay be a Pydantic model, a dataclass, aTypedDict, a JSON-schema dict, or aUnionof 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
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`