Agent 系统的结构化输出:先别跟 JSON 撕扯
每个写 Agent 的人都经历过——LLM 返回了一坨 JSON 格式的字符串,你小心翼翼地做 json.loads,然后它抛异常了。结构化输出不是"让模型输出格式化的数据"这么简单,它是整个 Agent 可靠性的地基。这篇文章从真实踩坑出发,聊聊结构化输出的三种模式、Pydantic AI 为什么在 2026 年成了默认选项,以及落地时该注意什么。
上周调试一个多 Agent 任务,子 Agent 返回了一段"JSON":
{
"action": "search_knowledge_base",
"query": "RAG chunking strategy",
"top_k": 5
}
看着挺规整的对吧。我把它喂给 json.loads,报错。
仔细一看,模型在 JSON 外面裹了一层 markdown 代码块标记。去掉 ```json 之后又试了一次,这回通了。但下一轮,模型在 query 字段里塞了一个换行符,再下一轮,它把 top_k 写成了字符串 "5"。
这种跟 JSON 格式拉扯的日子,我过了好一阵子。后来发现,结构化输出这件事远不止"格式正确"四个字——它决定了你的 Agent 系统到底是一个可靠系统,还是一根风中摇摆的稻草。
从"模型返回文本"到"系统需要数据"
Agent 系统和普通聊天的本质区别在于:聊天只输出文本给人看,Agent 输出的是数据——给下游系统用的。
下游系统可能是另一个 Agent、一个工具函数、一个数据库写入操作。它们不关心你返回的文字好不好看,它们要求的是:字段名正确、类型正确、值在合法范围内。
这就带来了一个根本矛盾:LLM 天然是文本生成器,而你的系统需要的是结构化数据。你不得不在中间加一层"翻译"——把 LLM 的文本输出转成系统能消费的数据格式。
传统的做法是 prompt 里写一句"请返回 JSON 格式",然后在外面包一个 json.loads。这个做法在 demo 阶段跑得通,但一旦到了生产环境,你会遇到各种各样的意外:
LLM 在 JSON 外面包了 markdown 代码块。JSON 里出现了多余的逗号。某个字段被模型自己改了名字("action" 变成 "tool")。嵌套结构里少了一个层级。字符串该转义的地方没转义。
每一类问题出现的概率都不高,但乘在一起,就是一个让你防不胜防的"小概率军团"。
三种结构化输出的模式
2026 年,结构化输出已经不是一个"要不要"的问题,而是"怎么选"的问题。目前主流的模式有三种。
第一,Tool Calling 模式。这是最可靠的方式。把输出目标注册成模型的一个工具(tool),模型的工具调用机制会天然返回结构化的参数。OpenAI、Anthropic、Google 都原生支持。Pydantic AI 的默认方式就是这个——你把输出类型定义成一个 Pydantic model,框架自动帮你注册成工具,模型调这个"伪工具"来返回数据。
第二,Native Structured Output 模式。部分模型(GPT-4o 系列、Gemini)提供了原生的 JSON Schema 约束能力——你给一个 schema,模型保证只输出符合这个 schema 的 JSON。Google 的 Gemini 在这一点上做得最早,OpenAI 的 structured output 在 2024 年底跟进。优势是严格,劣势是部分模型还不能和工具调用同时启用。
第三,Prompted Output 模式。最原始的方式——在 prompt 里塞一句"请返回 JSON 格式",然后靠后处理解析。没有任何强制力,全看模型心情。但在某些特殊场景下(比如模型本身不支持前两种方式),这是唯一的选择。
选型主线很清楚:能用 Tool Calling 模式就不要退而求其次。它能同时提供类型约束、自动重试和格式校验,是三种模式里防御最完整的一种。
Pydantic AI 为什么在 2026 年成了默认选项
聊到工具调用模式,就绕不开 Pydantic AI。
这个框架是 Pydantic 团队做的——就是那个 Python 生态里的 schema 验证库,FastAPI、LangChain、OpenAI 的 SDK 都在用它。Pydantic 团队做 Agent 框架的逻辑非常直白:既然 Pydantic 已经是 Python 事实上的 schema 层,那用 Pydantic 做 Agent 框架应该是自然的选择。
一个最小可用的例子长这样:
from pydantic import BaseModel
from pydantic_ai import Agent
class Order(BaseModel):
product: str
quantity: int
customer: str
agent = Agent(
"openai:gpt-4o",
output_type=Order,
system_prompt="从用户消息中提取订单信息。",
)
result = agent.run_sync("小明要了三台 14 寸笔记本")
print(result.output)
# Order(product='14寸笔记本', quantity=3, customer='小明')
注意到没有?result.output 是一个真正的 Order 实例。不是 dict,不是 JSON 字符串。IDE 知道它的类型、字段、方法。下游函数接收 Order 参数的地方会做类型检查。
这看着好像是小事,但实际体验上的差距非常大。传统方式写了一堆 JSON 解析、类型转换、字段映射的胶水代码。Pydantic AI 把这些全吞掉了——模型返回的数据先经过 Pydantic 的严格校验,校验不通过会自动告诉模型"重来",直到拿到合法数据为止。
它的重试循环是内置的:模型返回数据 -> Pydantic 校验 -> 校验失败 -> 把失败信息发回给模型修正 -> 模型重试。不需要你自己写 while 循环。
真正值钱的是依赖注入
Pydantic AI 另一个让我觉得"这个框架有人认真做过"的东西,是它的依赖注入设计。
Agent 的工具函数需要上下文——数据库连接、API 客户端、当前用户信息、租户 ID。传统的处理方式是全局变量或者环境变量查找,这会导致一个问题:测试起来非常麻烦,因为你得 mock 掉全局状态。
Pydantic AI 的做法是从 FastAPI 学来的——用一个 Deps 类承载所有依赖,每次调用 agent.run() 时传进去:
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
@dataclass
class Deps:
db: Database
http: AsyncClient
user_id: str
agent = Agent(
"anthropic:claude-sonnet-4-5",
deps_type=Deps,
system_prompt="你是客户支持助手。",
)
@agent.tool
async def get_order(ctx: RunContext[Deps], order_id: str) -> dict:
return await ctx.deps.db.fetch_order(order_id)
好处是什么?测试的时候传一个 mock 的 Deps,工具函数不用改一行代码。多租户场景里每个请求注入不同的 user_id 和 db 连接,没有全局变量污染。这些在单体应用里已经是被验证过的模式,Pydantic AI 把它搬到了 Agent 系统里。
什么时候该用它,什么时候不该
2026 年的 Python Agent 框架,格局已经很清晰了。
如果你的场景是:单 Agent 或少量 Agent 协作、需要可靠的结构化输出、希望代码简洁可读——Pydantic AI 是最短路径。
如果你的场景是:复杂的状态图、多个分支和循环、需要人机交互的暂停点——LangGraph 更适合。但我通常会建议把 Pydantic AI 作为 LangGraph 图里的执行节点,让 LangGraph 管编排,Pydantic AI 管执行和类型安全。
如果场景是多角色团队式协作——CrewAI 的角色扮演模型做这件事比较自然。
Pydantic AI 最大的克制是它不试图做所有事。它只做一件事:让 Agent 调用可靠、类型安全、可观测。然后停在那儿。这种"我不膨胀"的设计哲学,反而是它最大的优点。
最后说一句
结构化输出看起来是个小问题。但 Agent 系统里,每一层小问题的累积就是系统的不可靠性。把 JSON 解析这种脏活交给框架,把精力放在真正的业务逻辑上,系统才有机会从"跑得通"进化到"靠得住"。
好,今天先聊到这。
评论
发表评论