专栏:AI Agent 开发|12
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 前面我们已经分别学过 Structured Output、Tool Calling 和 Tool 选择。现在把视角拉高:当模型输出不再只给人看,而是要交给下一段代码继续执行时,为什么"结构化契约"会越来越重要?这一篇只建立这层工程认识,不提前把 Planning、Memory、Multi-Agent 的具体算法讲完。 |
一、先看一个 Agent 为什么会被"自由文本"卡住
假设模型给出计划:
|--------------------------------------------|
| 第一步,查询北京天气。 第二步,查看路况。 第三步,如果下雨且拥堵,就建议改乘地铁。 |
人一眼就懂。
但如果 Runtime 真要执行,它马上会问:
• 第一步调用哪个 Tool?
• 参数字段叫什么?
• 第二步依赖第一步吗?
• "下雨且拥堵"这个条件要怎样被代码判断?
如果只能靠正则、关键字和字符串切分把这些信息"抠"出来,模型稍微换一种说法,程序就可能失效。
图 1 自由文本适合表达,结构化对象适合成为程序之间稳定的接口
二、Structured Output 真正改变的,是 LLM 和代码之间的边界
上一篇 08 已经讲过:Structured Output 可以让模型返回符合约定结构的数据。
到了 Agent 里,这件事的重要性会放大,因为模型输出经常不是终点,而是下一步程序的输入。
例如:
{
"action": "get_weather",
"arguments": {
"city": "北京"
}
}
Runtime 不需要猜"模型想调用什么",直接读取字段、校验,然后决定是否执行。
所以"基础设施"这几个字,不是说 Agent 的每一块都必须输出 JSON,而是:
凡是 LLM 输出要跨过边界进入程序逻辑,最好先有一份明确、可验证的数据契约。
三、哪些地方最明显地需要结构化契约
图 2 Tool、Plan、State、Agent Message、Memory Metadata 都可能需要结构化,但它们的来源和实现并不完全相同
1. Tool Call:模型输出动作和参数
这是最直观的一类。`name + arguments` 如果没有固定结构,Runtime 就无法可靠分发。Tool Calling 本身已经把这一层做成了结构化接口。
2. Planning:当计划需要被程序执行
如果计划只是展示给用户,自由文本完全可以。
但如果程序要按步骤执行、记录依赖和进度,那么这种结构更有用:
{
"steps": [
{
"id": 1,
"action": "get_weather",
"depends_on": []
},
{
"id": 2,
"action": "check_traffic",
"depends_on": []
}
]
}
重点不是"Plan 必须是 JSON",而是只要 Plan 要进入程序编排,它就需要机器可读的结构。
3. State:状态本身应该有数据模型
这里要和 Structured Output 区分一下。State 不一定由模型生成,所以它不是天然等于"模型结构化输出"。
但任务状态要保存、恢复、检查点续跑时,程序通常仍需要稳定的数据模型,例如:
{
"run_id": "run_123",
"status": "waiting_approval",
"current_step": 3
}
4. Multi-Agent Message:跨组件传递任务时要减少歧义
如果 Agent A 给 Agent B 发送的是一段散文,B 还要重新理解"到底让我干什么"。
对于要自动路由的消息,`type / payload / status` 这类字段会更清晰。
但多 Agent 协议具体怎么设计留到后面的专题,这一篇只建立"跨边界需要契约"的直觉。
5. Memory:正文不一定结构化,元数据常常需要
Memory 也不能简单写成"长期记忆必须 JSON"。
一段用户经历、网页摘要、会议记录完全可以保存成文本;但为了筛选和检索,往往还会配结构化元数据:
{
"user_id": "u_42",
"created_at": "2026-09-05T15:30:00+08:00",
"tags": ["travel", "preference"],
"text": "用户更喜欢早班航班"
}
四、为什么"结构正确"仍然不等于"系统可以执行"
图 3 结构化输出只是第一道门;后面仍然有业务规则、权限和真实动作
假设模型严格返回:
|----------------------------------------------------------------------|
| { "action": "transfer_money", "amount": 1000000, "currency": "CNY" } |
字段齐全、类型正确,并不意味着这笔转账应该执行。
Runtime 仍然要检查:
• amount 是否超过业务上限。
• 用户是否有权限。
• 账户是否存在、余额是否足够。
• 是否需要人工确认。
所以 Structured Output 提高的是**接口可靠性和可验证性**,不是把模型输出自动变成可信事实。
五、一个最小例子:让"计划"真正变成程序对象
Python 可以先用 Pydantic 定义一份简单契约:
from typing import Literal
from pydantic import BaseModel
class Step(BaseModel):
id: int
action: Literal["get_weather", "check_traffic"]
city: str
class Plan(BaseModel):
steps: list[Step]
模型输出拿回来以后:
raw = {
"steps": [
{"id": 1, "action": "get_weather", "city": "北京"},
{"id": 2, "action": "check_traffic", "city": "北京"},
]
}
plan = Plan.model_validate(raw)
for step in plan.steps:
print(step.id, step.action, step.city)
这段代码最重要的不是 Pydantic,而是:下游代码终于不再处理"某段话",而是在处理有类型、有字段的对象。
六、Structured Output、JSON Mode、Function Calling 不要再次混在一起
|----------------------------|---------------------------|--------------------|
| 能力 | 主要解决什么 | 在 Agent 里的典型位置 |
| 普通 JSON 提示 | 让模型尽量按 JSON 写 | Demo / 低要求场景 |
| JSON Mode | 保证有效 JSON(具体能力看 Provider) | 需要 JSON 语法正确 |
| Schema 级 Structured Output | 约束模型输出结构 | Plan、分类结果、结构化业务结果 |
| Function / Tool Calling | 生成 Tool Call 与 arguments | 动作选择与工具参数 |
它们都可能产生结构化数据,但用途不同。
不要因为这一篇说"结构化契约是基础设施",就把所有问题都强行塞进同一个 `response_format`。
七、契约设计应该"够用",而不是越复杂越专业
原稿把 enum、required、版本、重试、线程安全全部一次塞进来,会让初学者误以为契约越严越好。
真正实用的原则是:
• 只定义下游真的要用的字段。
• 字段含义要清楚,不要同一个 `status` 在不同模块里代表不同东西。
• 枚举适合有限集合,但不要为了枚举而枚举。
• 可选字段就明确可选,不要全设 required。
• 契约变化以后,调用方和消费方要一起考虑兼容性。
如果一份 Schema 已经复杂到没人敢改,它就从"基础设施"变成新的负担。
八、失败以后是不是一定"回喂模型重试"
不一定。
如果目标 Provider 已经提供严格 Schema 约束,很多结构错误本来就能在生成阶段减少。
即便出现失败,也要分情况:
• 结构错误:可以重新生成或换降级策略。
• 业务值错误:应该由业务校验处理,不一定让模型重试。
• 权限拒绝:应停止执行,而不是鼓励模型"换个参数再试"。
• 安全拒答:不能当普通解析失败无限重试。
错误处理属于 Runtime 策略,后面会单独深入。
九、为什么这会影响 Eval、Tracing 和版本演进
一旦边界有结构,就更容易测试。
例如 Tool Call 可以检查:
|-------------------------------------------------------------------------|
| expected_action = "get_weather" assert result.action == expected_action |
Tracing 也可以记录:
• 本轮 action 是什么。
• 参数是多少。
• 哪一步校验失败。
• 哪个版本的 Schema 产生了结果。
自由文本当然也能记录,但结构化字段让统计和回归更直接。
十、这一篇最重要的边界
|----------------------------------------------------------------------------------------------------|
| Structured Output 是 LLM → 程序边界上的重要基础设施;"结构化数据契约"则是更大的软件工程概念。State、数据库记录、消息协议等可以本来就由代码定义,不需要都让模型生成。 |
把这两个层次分开,后面的 Agent 架构就不会变成"所有东西都套一个 JSON Schema"的机械设计。
十一、几个常见误区
• 误区 1:程序完全不能处理自由文本。程序当然可以处理文本,只是自由文本不适合作为需要稳定字段语义的自动化接口。
• 误区 2:所有 Agent 能力都必须是 Structured Output。只有 LLM 生成的结构化边界才属于 Structured Output;其他模块也可以使用普通数据模型。
• 误区 3:Schema 越严格越安全。结构约束不能代替权限、安全和业务校验。
• 误区 4:Memory 必须全部存成 JSON。文本正文 + 结构化元数据是很常见的组合。
• 误区 5:结构化输出成功就代表计划一定合理。它只说明数据形状符合契约。
十二、下一篇
下一篇 13《从零写第一个 Agent Loop》会把前面 06~12 的知识真正串起来:Context、模型调用、Structured Output、Tool Call、Tool Result 和停止条件,最终放进一个可以跑起来的最小循环。