
































April 17, 2026, 9:32am 1
It seems like when creating agents using create_agent() it is only possible to give the agent a single response format.
For my specific project, I would like my agent to respond in different formats depending on the situation.
Currently I achieve this by using seperate create_agent() instances, but this doesn’t feel very satisfactory.
Is there some way I can have a single agent, that is capable of following multiple response formats?
Or any ideas to achieve a similar effect without multiple create_agent() instances?
hi @Bergholdt
I think you don’t need multiple agent instances. create_agent’s response_format accepts one strategy object, but that object can wrap many schemas. Three patterns:
Caveat: only ToolStrategy expands unions. ProviderStrategy / AutoStrategy take a single schema, so to get provider-native validation you must pick one variant (option 3 is how).
1 Like
Hi @pawel-twardziak
Thanks for the reply!
It is indeed me that decides the format. I know the format I want beforehand.
So if I understand you right I could create a wrap_model_call middleware that directly changes the response_format of the agent?
So it does something like this?:
def wrap_model_call(request, handler):
request.response_format = insert_current_format_here
return handler(request)
yes - that exact pattern works, with one important caveat about ToolStrategy that the docs don’t spell out.
What the source code actually does after your middleware runs
After wrap_model_call fires, _get_bound_model re-reads request.response_format (langchain/agents/factory.py:1198) and rebuilds the bound model from it. So mutating request.response_format inside the handler is enough - no other plumbing is required.
Caveat: ToolStrategy swaps must be a subset of the upfront declaration
This is buried in the factory but it will bite you if you don’t know it. When the final (post-middleware) format is a ToolStrategy, the factory validates that every schema variant was declared when the agent was built:
# sources/langchain/libs/langchain_v1/langchain/agents/factory.py
# Middleware is allowed to change the response format
# to a subset of the original structured tools when using ToolStrategy,
# but not to add new structured tools that weren't declared upfront.
for tc in effective_response_format.schema_specs:
if tc.name not in structured_output_tools:
msg = (
f"ToolStrategy specifies tool '{tc.name}' "
"which wasn't declared in the original "
"response format when creating the agent."
)
raise ValueError(msg)
Concretely:
response_format=ToolStrategy(A) and the middleware swaps in ToolStrategy(B), you get ValueError - B wasn’t declared up front.ProviderStrategy. If you swap to ProviderStrategy(X) where the model supports native structured output, the new schema is accepted without the upfront-declaration requirement.Since you said you already know which format you want at call time, pass it via context_schema - that’s the read-only per-invocation channel the middleware docs recommend, and it’s exactly what runtime.context is for (see docs/src/oss/langchain/context-engineering.mdx):
from dataclasses import dataclass
from typing import Callable, Union
from pydantic import BaseModel
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.agents.structured_output import ToolStrategy
class ProductReview(BaseModel): ...
class CustomerComplaint(BaseModel): ...
class SupportTicket(BaseModel): ...
SCHEMAS = {
"review": ProductReview,
"complaint": CustomerComplaint,
"ticket": SupportTicket,
}
@dataclass
class Context:
response_format_key: str # predetermined by the caller
@wrap_model_call
def select_format(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
key = request.runtime.context.response_format_key
request.response_format = ToolStrategy(SCHEMAS[key]) # subset of the Union
return handler(request)
agent = create_agent(
model="gpt-5",
tools=tools,
# Declare the superset upfront so the subset swap above is always valid.
response_format=ToolStrategy(Union[ProductReview, CustomerComplaint, SupportTicket]),
middleware=[select_format],
context_schema=Context,
)
agent.invoke(
{"messages": [...]},
context=Context(response_format_key="complaint"),
)
Why this shape:
create_agent pre-registers every tool that any invocation might need - that clears the factory’s subset check.context_schema is the right carrier for “the caller already decided”: it’s read-only, per-invocation, and doesn’t pollute state.wrap_model_call runs once per model step, so retries inside the same invoke keep the format you picked.If instead you want to skip the Union gymnastics and your model has native structured output, swap to ProviderStrategy(SCHEMAS[key]) in the middleware - no upfront declaration needed, though you lose the Union fallback for providers without native support.
langchain/libs/langchain_v1/langchain/agents/factory.pyresponse_format re-read after middleware, and the ToolStrategy subset validation.langchain/libs/langchain_v1/langchain/agents/middleware/types.py
ModelRequest.response_format is a writable dataclass field.wrap_model_callcontext_schema.context_schema + runtime.context as the per-invocation channel.1 Like
Wow thank you for the detailed response and guidance!
It’s much appreciated.
no problem - let me know if it works for you
1 Like
Worked like a charm! ![]()
1 Like
Hmm, actually I am not certain it is working.
I am experiencing some issues.
I will provide snippets of my code this time for context.
# Response formats:
class AgentDecision(BaseModel): ...
class AgentJobSolution(BaseModel): ...
class AgentVote(BaseModel): ...
RESPONSE_FORMATS = {
"decision": AgentDecision,
"job_solve": AgentJobSolution,
"vote": AgentVote,
}
@dataclass
class ResponseFormatContext:
response_format_key: Literal["decision", "job_solve", "vote"]
class EnergyMiddleware(AgentMiddleware):
...
def wrap_model_call(self, request, handler):
key = request.runtime.context.response_format_key
request.response_format = ToolStrategy(RESPONSE_FORMATS[key])
return handler(request)
class EnergyAgent:
...
self._agent = create_agent(
model=model,
tools=tools,
middleware=[EnergyMiddleware(model_size=model_size)],
response_format=ToolStrategy(Union[AgentDecision, AgentJobSolution, AgentVote]),
context_schema=ResponseFormatContext
)
That is the setup.
And then I am calling the agent down the road passing state info and setting the reponse_format_key accordingly. An example:
# Invoke agent to make a decision
response = self._agent.invoke(
{"messages": decision_messages,
"energy": self.energy,
"max_energy": self.max_energy,
"phase_log": [],
},
context=ResponseFormatContext(response_format_key="decision"),
)
What I am noticing is that the agents don’t seem to have one specific response format as if I had called an agent that was just initialized with a single response format when using this approach.
I am having problems where the agent is returning one of the other response formats.
For instance in a call from the invoke example above, the agent called the AgentDecision ToolStrategy instead and returned that format.
When inspecting the EnergyMiddleware.wrap_model_call trace in LangSmith it also seems like the response format is not changed from the Union:
I’m not sure if I am missing something?
hi @Bergholdt
alright, lemme follow up on this - thanks for reporting the issues! ![]()
1 Like
would it be possible for you to share the langsmith thread with me?
in the meantime
instead of
@wrap_model_call
def select_format(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
key = request.runtime.context.response_format_key
request.response_format = ToolStrategy(SCHEMAS[key]) # subset of the Union
return handler(request)
try this
class EnergyMiddleware(AgentMiddleware):
def wrap_model_call(self, request, handler):
key = request.runtime.context.response_format_key
return handler(
request.override(response_format=ToolStrategy(RESPONSE_FORMATS[key]))
)
Two changes: .override(…) returns a new ModelRequest, and you must forward that new request to handler. If you mutate in place and forward the original, the factory may still see the Union because of dataclass aliasing plus the deprecation path.
Also worth double-checking: that every invoke() passes context=ResponseFormatContext(response_format_key=“…”), and that the key actually exists in RESPONSE_FORMATS. Full known-good snippet is in the doc.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。