单次模型调用的结构化输出,和 Agent 的结构化输出,不是同一件事。
前者通常是「给模型一段文本,请模型返回一个固定对象」。后者还要允许 Agent 在过程中调用天气、搜索、数据库等真实工具,等工具循环结束后,再把最终结果整理成一个程序可以消费的结构。
如果把这两个层次混在一起,常见结果是,模型一开始就被强行限制成结构化对象,工具调用无法正常进行,或者开发者拿到了中间的伪工具消息,却误以为那是真实业务工具执行结果。
这篇文章从 Agent 的 response_format 开始,解释 ProviderStrategy 和 ToolStrategy 的差别、结构化结果的读取位置、错误反馈与重试机制,以及如何在结构化结果和真实工具调用之间划清边界。
本文解决什么,理解 Agent 工具循环结束后如何生成结构化响应,并区分 Provider 原生约束、ToolStrategy 反馈和应用层预算控制。
本文不展开什么,不展开具体 Provider 的全部能力矩阵、生产级队列和计费系统;文中的递归上限与超时示例展示的是接入位置。
读完能完成什么,读者可以选择 ProviderStrategy 或 ToolStrategy,从 structured_response 读取结果,并为校验失败、循环上限和总超时设置外层兜底。
两种结构化输出的作用对象
#mermaid-svg-C9xvZgydMt0buLBT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C9xvZgydMt0buLBT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C9xvZgydMt0buLBT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C9xvZgydMt0buLBT .error-icon{fill:#552222;}#mermaid-svg-C9xvZgydMt0buLBT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C9xvZgydMt0buLBT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C9xvZgydMt0buLBT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C9xvZgydMt0buLBT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C9xvZgydMt0buLBT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C9xvZgydMt0buLBT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C9xvZgydMt0buLBT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C9xvZgydMt0buLBT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C9xvZgydMt0buLBT .marker.cross{stroke:#333333;}#mermaid-svg-C9xvZgydMt0buLBT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C9xvZgydMt0buLBT p{margin:0;}#mermaid-svg-C9xvZgydMt0buLBT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C9xvZgydMt0buLBT .cluster-label text{fill:#333;}#mermaid-svg-C9xvZgydMt0buLBT .cluster-label span{color:#333;}#mermaid-svg-C9xvZgydMt0buLBT .cluster-label span p{background-color:transparent;}#mermaid-svg-C9xvZgydMt0buLBT .label text,#mermaid-svg-C9xvZgydMt0buLBT span{fill:#333;color:#333;}#mermaid-svg-C9xvZgydMt0buLBT .node rect,#mermaid-svg-C9xvZgydMt0buLBT .node circle,#mermaid-svg-C9xvZgydMt0buLBT .node ellipse,#mermaid-svg-C9xvZgydMt0buLBT .node polygon,#mermaid-svg-C9xvZgydMt0buLBT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C9xvZgydMt0buLBT .rough-node .label text,#mermaid-svg-C9xvZgydMt0buLBT .node .label text,#mermaid-svg-C9xvZgydMt0buLBT .image-shape .label,#mermaid-svg-C9xvZgydMt0buLBT .icon-shape .label{text-anchor:middle;}#mermaid-svg-C9xvZgydMt0buLBT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C9xvZgydMt0buLBT .rough-node .label,#mermaid-svg-C9xvZgydMt0buLBT .node .label,#mermaid-svg-C9xvZgydMt0buLBT .image-shape .label,#mermaid-svg-C9xvZgydMt0buLBT .icon-shape .label{text-align:center;}#mermaid-svg-C9xvZgydMt0buLBT .node.clickable{cursor:pointer;}#mermaid-svg-C9xvZgydMt0buLBT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C9xvZgydMt0buLBT .arrowheadPath{fill:#333333;}#mermaid-svg-C9xvZgydMt0buLBT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C9xvZgydMt0buLBT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C9xvZgydMt0buLBT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9xvZgydMt0buLBT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C9xvZgydMt0buLBT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9xvZgydMt0buLBT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C9xvZgydMt0buLBT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C9xvZgydMt0buLBT .cluster text{fill:#333;}#mermaid-svg-C9xvZgydMt0buLBT .cluster span{color:#333;}#mermaid-svg-C9xvZgydMt0buLBT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C9xvZgydMt0buLBT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C9xvZgydMt0buLBT rect.text{fill:none;stroke-width:0;}#mermaid-svg-C9xvZgydMt0buLBT .icon-shape,#mermaid-svg-C9xvZgydMt0buLBT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9xvZgydMt0buLBT .icon-shape p,#mermaid-svg-C9xvZgydMt0buLBT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C9xvZgydMt0buLBT .icon-shape .label rect,#mermaid-svg-C9xvZgydMt0buLBT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9xvZgydMt0buLBT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C9xvZgydMt0buLBT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C9xvZgydMt0buLBT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户任务
Agent模型循环
真实工具调用
最终结构化响应
模型with_structured_output
单次模型响应
model.with_structured_output() 包装的是一次模型调用。Agent 的 response_format 约束的是 Agent 完成工具循环后的最终状态。
下面是一份从安装到调用的完整示例。天气工具返回的是写在代码里的演示数据,不代表实时天气;结构化结果仍然从 Agent 状态的 structured_response 字段读取。
安装依赖,并准备 OPENAI_API_KEY 环境变量。
powershell
pip install -U "langchain>=1.0,<2" "langchain-openai>=1.0,<2" "langgraph>=1.1,<2.0" "pydantic>=2"
python
from pydantic import BaseModel, Field
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain.tools import tool
from langchain_openai import ChatOpenAI
class TravelPlan(BaseModel):
"""下游程序要消费的旅行建议契约。"""
destination: str = Field(description="最终推荐的目的地")
reason: str = Field(description="基于工具结果给出的简短理由")
warnings: list[str] = Field(
default_factory=list,
description="需要提醒用户的事项",
)
@tool
def get_demo_weather(city: str) -> str:
"""读取演示天气,只读,不执行外部修改。"""
demo_data = {
"北京": "晴,22 摄氏度",
"上海": "多云,25 摄氏度",
}
return demo_data.get(city, f"没有 {city} 的演示天气数据")
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_agent(
model=model,
tools=[get_demo_weather],
system_prompt=(
"先调用 get_demo_weather 获取演示数据,再给出旅行建议。"
"不要把演示数据说成实时数据,也不要凭记忆补全天气。"
),
response_format=ToolStrategy(
TravelPlan,
handle_errors="请检查 destination、reason 和 warnings 的字段类型后重新输出。",
),
)
if __name__ == "__main__":
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "比较北京和上海的演示天气,推荐一个目的地。",
}
]
},
# 这是图运行时的总步数上限,不是 ToolStrategy 的参数。
config={"recursion_limit": 8},
)
plan = result["structured_response"]
print(plan.model_dump())
不要从最后一条消息的文本中手动猜结构化结果。Agent 的结构化响应有自己的状态字段,当前官方文档使用 structured_response。
ProviderStrategy,优先使用模型原生能力
ProviderStrategy 依赖模型供应商提供的原生结构化输出能力。
下面是局部配置片段,承接上面完整示例中的 model、get_demo_weather 和 TravelPlan 定义。
python
from langchain.agents.structured_output import ProviderStrategy
agent = create_agent(
model=model,
tools=[get_demo_weather],
response_format=ProviderStrategy(TravelPlan),
)
原生约束通常更强,模型返回结果也更接近声明的 Schema。但它不是所有模型都支持,嵌套字段、枚举、默认值和严格模式也可能存在差异。
ToolStrategy,把 Schema 变成结构化工具调用
当模型不支持原生结构化输出,但支持 Tool Calling 时,可以使用 ToolStrategy。
下面是局部配置片段,承接本文完整示例中的 model、get_demo_weather 和 TravelPlan 定义。
python
from langchain.agents.structured_output import ToolStrategy
agent = create_agent(
model=model,
tools=[get_demo_weather],
response_format=ToolStrategy(TravelPlan),
)
这里的 Schema 会被包装成一种特殊的结构化工具调用。它的目标是让 Agent 得到 TravelPlan,而不是执行外部副作用。
这类伪工具调用和真实业务工具要分开理解。
| 类型 | 是否访问外部系统 | 作用 |
|---|---|---|
| 天气工具 | 可能是 | 获取工具提供的数据,本例使用演示数据 |
| 订单查询工具 | 是 | 查询业务状态 |
| ToolStrategy 结构化调用 | 否 | 生成最终结构化结果 |
伪工具消息只是 Agent 内部完成结构化响应的一种协议,不应被审计系统当成一次真实支付、发信或写库动作。
自动选择策略与版本边界
当前官方文档支持直接把 Schema 类型传给 response_format,LangChain 会根据模型能力自动选择 ProviderStrategy 或 ToolStrategy。
python
agent = create_agent(
model=model,
tools=[get_demo_weather],
response_format=TravelPlan,
)
这比在代码里硬编码一个所谓的「自动策略类」更稳妥,因为具体内部实现可能变化。不过自动选择并不消除兼容性问题,目标模型是否支持原生能力、集成包如何声明能力,都需要在锁定版本上测试。
策略选择可以理解成下面这条路径。
#mermaid-svg-1ErrMHmgzrJWk25h{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1ErrMHmgzrJWk25h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1ErrMHmgzrJWk25h .error-icon{fill:#552222;}#mermaid-svg-1ErrMHmgzrJWk25h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1ErrMHmgzrJWk25h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1ErrMHmgzrJWk25h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1ErrMHmgzrJWk25h .marker.cross{stroke:#333333;}#mermaid-svg-1ErrMHmgzrJWk25h svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1ErrMHmgzrJWk25h p{margin:0;}#mermaid-svg-1ErrMHmgzrJWk25h .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1ErrMHmgzrJWk25h .cluster-label text{fill:#333;}#mermaid-svg-1ErrMHmgzrJWk25h .cluster-label span{color:#333;}#mermaid-svg-1ErrMHmgzrJWk25h .cluster-label span p{background-color:transparent;}#mermaid-svg-1ErrMHmgzrJWk25h .label text,#mermaid-svg-1ErrMHmgzrJWk25h span{fill:#333;color:#333;}#mermaid-svg-1ErrMHmgzrJWk25h .node rect,#mermaid-svg-1ErrMHmgzrJWk25h .node circle,#mermaid-svg-1ErrMHmgzrJWk25h .node ellipse,#mermaid-svg-1ErrMHmgzrJWk25h .node polygon,#mermaid-svg-1ErrMHmgzrJWk25h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1ErrMHmgzrJWk25h .rough-node .label text,#mermaid-svg-1ErrMHmgzrJWk25h .node .label text,#mermaid-svg-1ErrMHmgzrJWk25h .image-shape .label,#mermaid-svg-1ErrMHmgzrJWk25h .icon-shape .label{text-anchor:middle;}#mermaid-svg-1ErrMHmgzrJWk25h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1ErrMHmgzrJWk25h .rough-node .label,#mermaid-svg-1ErrMHmgzrJWk25h .node .label,#mermaid-svg-1ErrMHmgzrJWk25h .image-shape .label,#mermaid-svg-1ErrMHmgzrJWk25h .icon-shape .label{text-align:center;}#mermaid-svg-1ErrMHmgzrJWk25h .node.clickable{cursor:pointer;}#mermaid-svg-1ErrMHmgzrJWk25h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1ErrMHmgzrJWk25h .arrowheadPath{fill:#333333;}#mermaid-svg-1ErrMHmgzrJWk25h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1ErrMHmgzrJWk25h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1ErrMHmgzrJWk25h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ErrMHmgzrJWk25h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1ErrMHmgzrJWk25h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ErrMHmgzrJWk25h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1ErrMHmgzrJWk25h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1ErrMHmgzrJWk25h .cluster text{fill:#333;}#mermaid-svg-1ErrMHmgzrJWk25h .cluster span{color:#333;}#mermaid-svg-1ErrMHmgzrJWk25h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1ErrMHmgzrJWk25h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1ErrMHmgzrJWk25h rect.text{fill:none;stroke-width:0;}#mermaid-svg-1ErrMHmgzrJWk25h .icon-shape,#mermaid-svg-1ErrMHmgzrJWk25h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ErrMHmgzrJWk25h .icon-shape p,#mermaid-svg-1ErrMHmgzrJWk25h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1ErrMHmgzrJWk25h .icon-shape .label rect,#mermaid-svg-1ErrMHmgzrJWk25h .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ErrMHmgzrJWk25h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1ErrMHmgzrJWk25h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1ErrMHmgzrJWk25h :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
Agent response_format
Provider支持原生结构化输出
ProviderStrategy
模型支持Tool Calling
ToolStrategy
更换模型或采用兼容解析
structured_response
handle_errors 如何处理校验失败
ToolStrategy 默认会捕获结构化输出校验错误,并把可理解的错误信息写入 ToolMessage,交给模型修正。handle_errors 决定哪些异常被捕获、反馈内容是什么;它不提供最大重试次数、总耗时或费用预算参数。
python
agent = create_agent(
model=model,
tools=[get_demo_weather],
response_format=ToolStrategy(
TravelPlan,
handle_errors="请检查字段类型、必填字段和枚举值后重新输出。",
),
)
常见配置包括。
True,使用默认错误消息并尝试重试。False,不捕获错误,直接让异常传播。- 字符串,使用固定错误消息。
- 异常类型,只处理指定类型。
- 函数,根据异常动态生成反馈。
这里的「尝试重试」是把校验错误作为 ToolMessage 反馈给模型,让 Agent 有机会再次生成;具体执行多少轮,不由 handle_errors 单独决定。
重试过程大致如下。
Agent运行时 结构化Schema Agent模型 Agent运行时 结构化Schema Agent模型 #mermaid-svg-h9vIDBhwFrdQPvNK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h9vIDBhwFrdQPvNK .error-icon{fill:#552222;}#mermaid-svg-h9vIDBhwFrdQPvNK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h9vIDBhwFrdQPvNK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h9vIDBhwFrdQPvNK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h9vIDBhwFrdQPvNK .marker.cross{stroke:#333333;}#mermaid-svg-h9vIDBhwFrdQPvNK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h9vIDBhwFrdQPvNK p{margin:0;}#mermaid-svg-h9vIDBhwFrdQPvNK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h9vIDBhwFrdQPvNK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h9vIDBhwFrdQPvNK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-h9vIDBhwFrdQPvNK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-h9vIDBhwFrdQPvNK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-h9vIDBhwFrdQPvNK .sequenceNumber{fill:white;}#mermaid-svg-h9vIDBhwFrdQPvNK #sequencenumber{fill:#333;}#mermaid-svg-h9vIDBhwFrdQPvNK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-h9vIDBhwFrdQPvNK .messageText{fill:#333;stroke:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h9vIDBhwFrdQPvNK .labelText,#mermaid-svg-h9vIDBhwFrdQPvNK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .loopText,#mermaid-svg-h9vIDBhwFrdQPvNK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h9vIDBhwFrdQPvNK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-h9vIDBhwFrdQPvNK .noteText,#mermaid-svg-h9vIDBhwFrdQPvNK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-h9vIDBhwFrdQPvNK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h9vIDBhwFrdQPvNK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h9vIDBhwFrdQPvNK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h9vIDBhwFrdQPvNK .actorPopupMenu{position:absolute;}#mermaid-svg-h9vIDBhwFrdQPvNK .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-h9vIDBhwFrdQPvNK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h9vIDBhwFrdQPvNK .actor-man circle,#mermaid-svg-h9vIDBhwFrdQPvNK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-h9vIDBhwFrdQPvNK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生成结构化调用 解析与校验 校验失败 ToolMessage错误反馈 修正后的结构化调用 再次校验 structured_response
上面的流程只描述一次校验失败如何反馈给模型。它不是一个可以单独配置最大轮数的独立重试器。若模型持续返回错误,图仍会继续执行,直到 Agent 正常结束或触发外层的递归上限。因此,错误消息不要包含内部堆栈、数据库地址或敏感数据。
一个结构化 Agent 的完整读取方式
下面的函数把 Agent 状态和结构化结果分开处理。
python
def run_travel_agent(agent, question: str) -> TravelPlan:
result = agent.invoke(
{"messages": [{"role": "user", "content": question}]}
)
structured = result.get("structured_response")
if structured is None:
raise RuntimeError("Agent没有生成结构化响应")
if not isinstance(structured, TravelPlan):
raise TypeError("结构化响应类型不符合预期")
return structured
真实项目里还应该把消息列表用于观测,把 structured_response 用于业务消费。二者不要互相替代,消息列表告诉你 Agent 做过什么,结构化结果告诉下游最终拿到了什么。
结构化响应和最终自然语言回答可以并存
很多产品既要给前端展示自然语言,又要给后端一个固定对象。推荐让 Agent 先生成结构化结果,再由一个确定性模板或单独的回答步骤生成展示文本。
#mermaid-svg-7h9AuYJVASA7uQ8s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7h9AuYJVASA7uQ8s .error-icon{fill:#552222;}#mermaid-svg-7h9AuYJVASA7uQ8s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7h9AuYJVASA7uQ8s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7h9AuYJVASA7uQ8s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7h9AuYJVASA7uQ8s .marker.cross{stroke:#333333;}#mermaid-svg-7h9AuYJVASA7uQ8s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7h9AuYJVASA7uQ8s p{margin:0;}#mermaid-svg-7h9AuYJVASA7uQ8s .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster-label text{fill:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster-label span{color:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster-label span p{background-color:transparent;}#mermaid-svg-7h9AuYJVASA7uQ8s .label text,#mermaid-svg-7h9AuYJVASA7uQ8s span{fill:#333;color:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s .node rect,#mermaid-svg-7h9AuYJVASA7uQ8s .node circle,#mermaid-svg-7h9AuYJVASA7uQ8s .node ellipse,#mermaid-svg-7h9AuYJVASA7uQ8s .node polygon,#mermaid-svg-7h9AuYJVASA7uQ8s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7h9AuYJVASA7uQ8s .rough-node .label text,#mermaid-svg-7h9AuYJVASA7uQ8s .node .label text,#mermaid-svg-7h9AuYJVASA7uQ8s .image-shape .label,#mermaid-svg-7h9AuYJVASA7uQ8s .icon-shape .label{text-anchor:middle;}#mermaid-svg-7h9AuYJVASA7uQ8s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7h9AuYJVASA7uQ8s .rough-node .label,#mermaid-svg-7h9AuYJVASA7uQ8s .node .label,#mermaid-svg-7h9AuYJVASA7uQ8s .image-shape .label,#mermaid-svg-7h9AuYJVASA7uQ8s .icon-shape .label{text-align:center;}#mermaid-svg-7h9AuYJVASA7uQ8s .node.clickable{cursor:pointer;}#mermaid-svg-7h9AuYJVASA7uQ8s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7h9AuYJVASA7uQ8s .arrowheadPath{fill:#333333;}#mermaid-svg-7h9AuYJVASA7uQ8s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7h9AuYJVASA7uQ8s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7h9AuYJVASA7uQ8s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7h9AuYJVASA7uQ8s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7h9AuYJVASA7uQ8s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7h9AuYJVASA7uQ8s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster text{fill:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s .cluster span{color:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7h9AuYJVASA7uQ8s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7h9AuYJVASA7uQ8s rect.text{fill:none;stroke-width:0;}#mermaid-svg-7h9AuYJVASA7uQ8s .icon-shape,#mermaid-svg-7h9AuYJVASA7uQ8s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7h9AuYJVASA7uQ8s .icon-shape p,#mermaid-svg-7h9AuYJVASA7uQ8s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7h9AuYJVASA7uQ8s .icon-shape .label rect,#mermaid-svg-7h9AuYJVASA7uQ8s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7h9AuYJVASA7uQ8s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7h9AuYJVASA7uQ8s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7h9AuYJVASA7uQ8s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Agent工具循环
structured_response
数据库、路由或审批
确定性模板或展示模型
用户可读回答
这样,前端文案变化不会破坏后端字段契约。也不要把面向用户的长篇自然语言塞进一个本来用于路由的结构化字段中,字段应该服务于下一步动作。
错误反馈要让模型能行动
错误消息太模糊,模型无法修正;错误消息太详细,又可能泄露内部信息。
不推荐。
text
解析失败,请重试。
也不推荐把完整的 Pydantic 堆栈和数据库异常直接传给模型。
更合适的反馈是指出可修正的字段和规则。
text
字段 rating 必须是 1 到 5 的整数,字段 category 只能选择 billing、shipping 或 other,请只修正这些字段。
应用可以在错误处理函数中把内部异常映射成有限的错误码,再生成模型可理解的短消息。内部堆栈保留在受控日志,模型只拿到完成修正所需的最小信息。
真正生效的执行限制放在哪里
结构化输出纠错和执行预算是两层不同的控制。handle_errors 只负责把可处理的 Schema 错误反馈给模型,不能替代总步数、网络超时和费用控制。
图运行时的步数上限
create_agent() 基于 LangGraph 运行。调用时可以通过 config["recursion_limit"] 限制一次执行允许的最大图步数。达到上限时,运行时会抛出递归上限错误,应用应把它转换成可解释失败并记录 Trace。
python
result = agent.invoke(
{"messages": [{"role": "user", "content": "给出旅行建议"}]},
config={"recursion_limit": 8},
)
这限制的是整个 Agent 图的步数,不等于「结构化校验最多重试 8 次」。模型调用、真实工具调用和结构化反馈都可能消耗图步数,具体计数方式应以锁定版本的 LangGraph 文档为准。
请求超时和费用预算
总耗时属于应用服务边界,通常由异步调用的超时、网关超时或任务队列截止时间控制;费用预算则需要根据模型调用次数、输入输出 Token 和工具成本在应用层累计。一个最小的异步超时包装如下。
python
import asyncio
async def run_with_timeout(agent, question: str):
return await asyncio.wait_for(
agent.ainvoke(
{"messages": [{"role": "user", "content": question}]},
config={"recursion_limit": 8},
),
timeout=30,
)
超时能否中断底层 HTTP 请求,还取决于模型集成是否正确支持异步取消;生产环境应同时配置 HTTP 客户端超时和服务网关超时。不要把一个没有被 Agent 调用、没有被外层包装的自定义重试函数当成预算控制。
tool_message_content 只改变历史展示
ToolStrategy 可以定制结构化结果对应的 ToolMessage 文本。
python
response_format = ToolStrategy(
TravelPlan,
tool_message_content="旅行建议已完成,请根据结构化结果生成最终答复。",
)
这只影响 Agent 历史中用于继续推理的消息内容,不会把伪工具变成真实外部工具,也不会替代 structured_response。如果下游需要稳定数据,仍然读取状态中的结构化对象。
Union Schema 的边界
某些任务有多个合法结果类型,可以使用 Union。
python
from typing import Literal, Union
from pydantic import BaseModel
class NeedMoreInfo(BaseModel):
kind: Literal["need_more_info"]
question: str
class FinalAnswer(BaseModel):
kind: Literal["final"]
answer: str
response_format = ToolStrategy(Union[NeedMoreInfo, FinalAnswer])
Union 越复杂,模型选择正确分支的难度越高。每个分支都应该有明显的判别字段,例如 kind,并且避免两个 Schema 拥有大量同名、同类型字段。
模型级和 Agent 级错误要分开
结构化校验失败只是 Agent 运行中的一种错误。还可能出现真实工具超时、权限拒绝、模型网络失败或上下文过长。
#mermaid-svg-ETknhOJVLxxCpRqL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ETknhOJVLxxCpRqL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ETknhOJVLxxCpRqL .error-icon{fill:#552222;}#mermaid-svg-ETknhOJVLxxCpRqL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ETknhOJVLxxCpRqL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ETknhOJVLxxCpRqL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ETknhOJVLxxCpRqL .marker.cross{stroke:#333333;}#mermaid-svg-ETknhOJVLxxCpRqL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ETknhOJVLxxCpRqL p{margin:0;}#mermaid-svg-ETknhOJVLxxCpRqL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ETknhOJVLxxCpRqL .cluster-label text{fill:#333;}#mermaid-svg-ETknhOJVLxxCpRqL .cluster-label span{color:#333;}#mermaid-svg-ETknhOJVLxxCpRqL .cluster-label span p{background-color:transparent;}#mermaid-svg-ETknhOJVLxxCpRqL .label text,#mermaid-svg-ETknhOJVLxxCpRqL span{fill:#333;color:#333;}#mermaid-svg-ETknhOJVLxxCpRqL .node rect,#mermaid-svg-ETknhOJVLxxCpRqL .node circle,#mermaid-svg-ETknhOJVLxxCpRqL .node ellipse,#mermaid-svg-ETknhOJVLxxCpRqL .node polygon,#mermaid-svg-ETknhOJVLxxCpRqL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ETknhOJVLxxCpRqL .rough-node .label text,#mermaid-svg-ETknhOJVLxxCpRqL .node .label text,#mermaid-svg-ETknhOJVLxxCpRqL .image-shape .label,#mermaid-svg-ETknhOJVLxxCpRqL .icon-shape .label{text-anchor:middle;}#mermaid-svg-ETknhOJVLxxCpRqL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ETknhOJVLxxCpRqL .rough-node .label,#mermaid-svg-ETknhOJVLxxCpRqL .node .label,#mermaid-svg-ETknhOJVLxxCpRqL .image-shape .label,#mermaid-svg-ETknhOJVLxxCpRqL .icon-shape .label{text-align:center;}#mermaid-svg-ETknhOJVLxxCpRqL .node.clickable{cursor:pointer;}#mermaid-svg-ETknhOJVLxxCpRqL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ETknhOJVLxxCpRqL .arrowheadPath{fill:#333333;}#mermaid-svg-ETknhOJVLxxCpRqL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ETknhOJVLxxCpRqL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ETknhOJVLxxCpRqL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ETknhOJVLxxCpRqL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ETknhOJVLxxCpRqL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ETknhOJVLxxCpRqL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ETknhOJVLxxCpRqL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ETknhOJVLxxCpRqL .cluster text{fill:#333;}#mermaid-svg-ETknhOJVLxxCpRqL .cluster span{color:#333;}#mermaid-svg-ETknhOJVLxxCpRqL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ETknhOJVLxxCpRqL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ETknhOJVLxxCpRqL rect.text{fill:none;stroke-width:0;}#mermaid-svg-ETknhOJVLxxCpRqL .icon-shape,#mermaid-svg-ETknhOJVLxxCpRqL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ETknhOJVLxxCpRqL .icon-shape p,#mermaid-svg-ETknhOJVLxxCpRqL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ETknhOJVLxxCpRqL .icon-shape .label rect,#mermaid-svg-ETknhOJVLxxCpRqL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ETknhOJVLxxCpRqL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ETknhOJVLxxCpRqL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ETknhOJVLxxCpRqL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 结构化校验
工具临时失败
权限拒绝
模型调用失败
是
否
Agent运行
错误类别
ToolStrategy反馈并有限重试
调用级有限重试或降级
立即停止并审计
调用级有限重试或切换备选
是否达到上限
可解释失败或转人工
不要用一个笼统的 except Exception 把所有错误都转成「请重试」。权限拒绝和参数错误的处理语义完全不同。
上线检查
- 最终结构化对象是否从
structured_response读取。 - ProviderStrategy 和 ToolStrategy 是否在目标模型上实测。
- 是否区分真实工具消息和结构化伪工具消息。
handle_errors是否只用于结构化错误反馈;图递归上限、请求超时和费用预算是否由外层设置。- Union Schema 是否有清晰的判别字段。
- 结构化失败、工具失败、权限拒绝是否分开记录。
- 是否保留 Schema、Prompt、模型和依赖版本。
- 是否对错误消息和 Trace 做脱敏。
Agent 的结构化输出不是把 Agent 变成一个 JSON 接口那么简单。它是先允许 Agent 完成必要的动作,再把最终状态收敛到一份可以被业务系统消费的契约上。
官方文档与版本说明
- LangChain Structured Output
- LangChain Agents
- LangChain Models,Structured Output
- LangGraph Graph API,递归上限
本文按 LangChain 1.x 编写。直接传入 Schema 类型的自动策略选择、ToolStrategy 参数以及 Agent 状态字段可能随版本更新,正式项目应以锁定版本的官方文档和集成测试为准。