第一课我们讲了一件看起来很简单但其实决定了整个 Agent 技术栈的事情LLM 只会产生输出它不会直接改变外部世界。你让一个裸模型删除 report.txt它可以回答好的report.txt 已删除。甚至可以生成os.remove(report.txt)但只要外面没有程序真正执行os.remove()你的文件就还好好地躺在硬盘里。所以第一课最后我们把 Model 和 Runtime 分开了Model决定应该做什么 Runtime真正把事情做掉到这里其实已经很接近 Agent 了。但如果你真的开始自己写代码会立刻撞上第二个问题。而且这个问题比“怎么调用 Tool”更早出现。假设用户说帮我查一下上海今天的天气 看看下午要不要带伞。模型判断完之后回答我觉得这个问题最好先查一下上海今天的天气。作为人你当然看得懂。但现在不要站在人类的角度看这句话。站在代码的角度看。你的程序拿到的其实只是response 我觉得这个问题最好先查一下上海今天的天气。 然后呢程序怎么知道模型到底想干什么这就是第二课真正要解决的问题。人能听懂的话不一定是一个好的软件接口假设我们的 Runtime 现在有三个能力查询天气 查询订单 直接回答用户那么模型每一次做完判断之后程序真正关心的其实只有一件事你到底选择了哪一个但模型偏偏特别擅长“说人话”。它可能说我需要先看看上海天气。下一次可能说为了回答这个问题我需要获取上海当前的气象信息。还可能说最好先确认一下上海今天有没有雨。换个模型也许直接变成Let me check the weather in Shanghai first.对人来说这四句话表达的是同一个意思。对程序来说这是四个完全不同的字符串。于是很多人第一次自己写这种程序时会很自然地干一件事if 天气 in response: get_weather()看起来居然还能跑。然后有一天模型回答我需要看看上海今天会不会下雨。没有“天气”两个字。代码失效。于是你补if 天气 in response or 下雨 in response: get_weather()过几天模型又回答先获取一下当地气象情况吧。于是你继续补if ( 天气 in response or 下雨 in response or 气象 in response ): get_weather()如果再复杂一点你很快就会开始写正则表达式。然后再给正则表达式打补丁。再给补丁打补丁。做到这里应该已经能感觉到哪里不对了。问题并不在于模型“不够聪明”。真正的问题是我们正在拿自然语言当软件协议用。而自然语言恰恰是一个极度不稳定的软件协议。人类喜欢自然语言是因为我们非常擅长容忍模糊。程序恰恰相反。程序最喜欢的是字段叫什么是确定的。 类型是什么是确定的。 允许出现哪些值是确定的。 缺少字段怎么办也是确定的。于是 Agent 工程里第一个非常自然的问题出现了能不能不要让 Runtime 猜模型是什么意思而是让模型直接按照程序约定的格式表达自己的决定这就是 Structured Output 真正要解决的问题。第一次关键转变从“说一句话”变成“返回一个数据结构”还是刚才的问题。用户帮我查一下上海今天的天气。以前模型返回我认为应该先查询上海天气。现在我们要求模型返回{ action: get_weather, arguments: { city: Shanghai } }看起来只是换成了 JSON。但从软件工程角度看事情已经发生了本质变化。以前 Runtime 拿到我认为应该先查询上海天气。它必须先理解一句自然语言。现在 Runtime 拿到decision[action]得到get_weather再读取decision[arguments][city]得到ShanghaiRuntime 不需要理解中文。不需要 NLP。不需要关键词。也不需要猜。这时候LLM 第一次从一个自然语言生成器开始变成了一个可以被软件消费的决策接口这才是 Structured Output 真正重要的地方。它解决的从来不是“让输出看起来整齐一点”。它解决的是怎样把一个概率模型的输出接进一个确定性的软件系统。这一点非常重要。因为后面整个 Agent 都建立在这条桥上。但这里有一个坑会输出 JSON不等于真正有了 Structured Output很多人学到这里会觉得事情已经解决了。Prompt 里面写一句请严格返回 JSON不要输出其他内容。然后告诉模型格式 { action: ..., arguments: {} }看起来很合理。但你真正跑几次就会发现问题来了。模型可能返回当然可以下面是结果 { action: get_weather, arguments: { city: Shanghai } }这时候json.loads(response)直接失败。你说没关系我再强调一次“只能返回 JSON”。然后下一次模型很听话{ action: weather, arguments: { city: Shanghai } }这是合法 JSON。但 Runtime 根本没有一个叫weather的 Action。Runtime 只有get_weather再下一次{ action: get_weather, arguments: { city: 100 } }还是合法 JSON。但city为什么变成数字了甚至可能{ action: get_weather, arguments: {} }JSON 依然完全合法。但最关键的city不见了。这时候有一个很值得记住的区别Valid JSON ≠ Valid Decision这句话最好记住。JSON 解决的只是语法。但一个软件接口真正需要的是契约。Schema 才是这里真正重要的东西假设我们希望模型返回{ action: get_weather, arguments: { city: Shanghai } }真正需要定义的其实远远不只是“大括号怎么写”。我们需要告诉模型和 Runtime最外层必须是 object。 必须存在 action。 action 必须是 string。 action 只能取几个指定值。 必须存在 arguments。 arguments 必须是 object。 如果 action 是 get_weather 那么 arguments 里必须有 city。 city 必须是 string。这就是 Schema 在干的事情。例如我们可以表达成类似这样的约束{ type: object, properties: { action: { type: string, enum: [ get_weather, query_order, final_answer ] }, arguments: { type: object } }, required: [ action, arguments ] }具体以后你用 JSON Schema、类型系统还是某个模型 SDK 提供的 Structured Output API并不是这一课最值得记住的事情。真正重要的是脑子里要建立一个概念Schema 是 Model 和 Runtime 之间的 Contract。Runtime 相当于在告诉模型你可以负责判断。 但如果你希望软件理解你的判断 请按照这个协议说。这就是一个非常标准的软件工程思想。只不过协议的一端不再是另一个确定性的服务。而是一个 LLM。第二个关键转变我们实际上正在限制模型的“动作空间”这里有一个我认为比“学会 JSON Schema”重要得多的理解。假设我们告诉模型action 只能是 get_weather query_order final_answer这意味着什么意味着模型不能突然自己发明一个search_weather_from_google也不能发明open_browser更不能突然来一个delete_database从 Runtime 的角度我们实际上定义了Action Space { get_weather, query_order, final_answer }这个概念特别重要。因为到这里LLM 已经不再只是“根据上下文继续生成一段文字”从 Agent Engineering 的视角我们可以开始换一种方式理解它给定当前 Context ↓ 模型判断当前应该做什么 ↓ 从允许的 Action Space 里选择一个 Action如果写得更抽象一点Context / State ↓ LLM ↓ Choose Action这已经开始非常像 Agent 了。而且你会发现Schema 不只是定义数据格式。它还在定义这个 Agent 到底被允许做出哪些类型的决定。这是这一课非常重要的一个“恍然大悟”。Schema 设计本身就是 Agent 设计我们继续往下推。最开始只有一个动作get_weather所以很简单{ action: get_weather, arguments: { city: Shanghai } }后来系统又增加一个能力query_order它需要的参数不是city而是order_id于是{ action: query_order, arguments: { order_id: PO-10086 } }再后来增加search_web它需要query于是{ action: search_web, arguments: { query: Agent Memory } }到这里你会慢慢看到一个结构出现了Decision │ ├── action │ └── argumentsaction回答我准备做什么arguments回答完成这个动作需要什么参数这已经和函数调用非常像了。例如 Pythonget_weather(cityShanghai)和结构化 Decision{ action: get_weather, arguments: { city: Shanghai } }两者之间几乎已经可以一一对应。注意这个时候我们仍然没有执行函数。但是 Model 和 Runtime 之间已经建立好了“如何描述一个函数调用”的语言。这就是为什么 Structured Output 应该放在 Tool Calling 前面讲。否则你第一次看到 Tool Calling 的时候很容易误以为模型里面有一个什么神秘的函数执行系统。其实完全没有必要这么理解。把前面的东西一步一步推出来以后你会发现Tool Calling 马上就要变得非常朴素了。到这里先停一下模型还是没有调用任何东西这个边界必须再强调一次。模型返回{ action: get_weather, arguments: { city: Shanghai } }请问上海天气查了吗没有。一点都没有。现在真实发生的事情仍然只有Context ↓ LLM ↓ Tokens ↓ Structured Data模型只是用一种程序容易理解的方式表达“我认为下一步应该查询上海天气。”以前它用中文表达。现在它用结构化数据表达。本质没有变化这仍然只是 Decision。真正的天气查询必须等 Runtime 做get_weather(cityShanghai)才会发生。所以这里可以建立一个以后非常有用的边界Structured Output 如何表达 Decision Tool Execution 如何执行 Action这两件事情不要混。一旦混了你后面学 Tool Calling、MCP、Agent Loop 时会越来越乱。我们现在真的写一个02_structured_output.py这一课仍然不要 LangChain。不要 LangGraph。也不要任何 Agent Framework。我们的项目继续保持agent-from-zero/ │ ├── 01_llm.py └── 02_structured_output.py第一课User ↓ LLM ↓ Text这一课只增加一件东西User ↓ LLM ↓ Structured Decision ↓ Validation我们的目标非常克制不是做 Agent。只是让模型学会输出 Runtime 能可靠理解的 Decision。假设系统目前允许四种决定get_weather query_order ask_user final_answer为什么多了一个ask_user等一下就会知道。我们先约定一个统一格式{ action: ..., arguments: {} }例如{ action: get_weather, arguments: { city: Shanghai } }或者{ action: query_order, arguments: { order_id: PO-10086 } }程序拿到模型返回之后第一件事不是执行。而是import json def parse_decision(raw): return json.loads(raw)这是 Parser。它只解决一个问题模型返回的东西 能不能被解析成 JSON例如{ action: get_weather, arguments: { city: Shanghai } }可以。而好的我建议查询天气。不可以。但只有 Parser 远远不够。我们还需要 Validator。比如ALLOWED_ACTIONS { get_weather, query_order, ask_user, final_answer, } def validate_decision(decision): if action not in decision: raise ValueError(missing action) if arguments not in decision: raise ValueError(missing arguments) if decision[action] not in ALLOWED_ACTIONS: raise ValueError(unknown action) if not isinstance(decision[arguments], dict): raise ValueError(arguments must be an object) return decision现在raw llm(messages) decision parse_decision(raw) decision validate_decision(decision)先不用纠结llm()具体是哪一家 API。我们现在研究的是结构。这几行代码其实已经把一个非常重要的东西拆出来了Model Output ↓ Parse ↓ Validate ↓ Decision以后做大型 Agent 时你会发现这几层依然存在。只不过实现得复杂得多。Parser 和 Validator千万不要混成一件事这个区别值得专门讲一下。假设模型输出{ action: destroy_planet, arguments: {} }这是合法 JSON 吗当然是。所以ParserPASS但这是我们允许的 Decision 吗不是。因为destroy_planet根本不属于我们的 Action Space。所以ValidatorFAIL这就是为什么Valid JSON ≠ Valid Decision再比如{ action: get_weather, arguments: {} }JSON 完全正确。Action 也存在。但get_weather明明需要city。所以更严格的 Validator 还应该检查if decision[action] get_weather: if city not in decision[arguments]: raise ValueError(get_weather requires city)这时候你开始进入真正的软件接口设计了。你不是在问“模型有没有大概理解我的意思”你是在问“这个输出是否满足系统契约”这两个问题完全不是一个层次。一个特别值得做的实验故意不给模型足够信息现在用户说帮我查一下天气。注意。没有城市。如果你的 Schema 只有get_weather final_answer模型会陷入一个很尴尬的状态。它想调用get_weather。但参数不完整。怎么办一个非常自然的解决办法是增加ask_user于是 Action Space 变成get_weather query_order ask_user final_answer模型可以返回{ action: ask_user, arguments: { question: 你想查询哪个城市的天气 } }这个例子非常值得反复琢磨。因为这里第一次可以清楚地看到我们怎样设计 Schema会直接决定 Agent 能采取什么行为。如果没有ask_user模型没有一种正式方式表达“信息不够我需要问用户。”增加ask_user以后这种行为突然成为了系统允许的一等 Action。也就是说Schema 并不只是描述输出长什么样它实际上参与定义 Agent 的能力边界。这件事到了复杂 Agent 里会越来越明显。再看一个企业场景你会更容易理解为什么这件事重要假设以后我们做一个企业内部 Agent。用户说帮我查一下 PO-20260807-001 的状态。一种设计是让模型直接生成 SQL{ sql: SELECT * FROM orders WHERE id PO-20260807-001 }看起来很灵活。但是这种设计很快会带来另外一个问题为什么模型一定会生成SELECT它也可能生成DELETE FROM orders ...甚至生成一段你完全没有预料到的 SQL。于是更合理的设计通常不是让 LLM 自由创造底层操作而是 Runtime 先定义安全的高层能力query_order模型只负责{ action: query_order, arguments: { order_id: PO-20260807-001 } }真正的 SQLSELECT ...由 Runtime 内部自己决定。这其实延续了第一课那个非常重要的思想Model 决定 What Runtime 控制 How模型可以判断我要查询这个订单但数据库应该怎么连、SQL 怎么写、权限怎么控制、哪些字段能返回不应该默认全部交给模型自由发挥。所以 Structured Output 表面是在讲输出格式。继续往下看你会发现它已经开始碰到权限边界 API 设计 安全边界 系统职责划分这才是它真正有价值的地方。为什么我说 Structured Output 是 Model 和 Runtime 的“边界协议”到这里我们终于可以把整个结构画完整。第一课只有Context ↓ LLM ↓ Output第二课变成Context ↓ LLM ↓ Structured Output ↓ Parser ↓ Validator ↓ Decision再往下未来才会是Decision ↓ Runtime ↓ Tool ↓ Environment所以 Structured Output 恰好站在一个非常特殊的位置概率世界 │ LLM │ ↓ Structured Output │ ───────────┼─────────── Model / Runtime Boundary ───────────┼─────────── │ ↓ Validator │ Runtime │ Tool │ Environment 确定性世界上面是概率模型。下面是传统软件系统。中间必须有一个双方都能理解的 Contract。否则下面的软件永远在猜模型刚才那句话到底什么意思所以如果一定要用一句话总结这一课我更喜欢这样说Structured Output 的真正价值是把 LLM 的概率性输出压缩成软件系统可以验证和消费的确定性接口。这比“让模型输出 JSON”准确得多。还有一个很重要的原则Schema 不是越复杂越好当大家第一次意识到 Schema 可以控制模型输出之后很容易开始设计这种东西{ thought: ..., reasoning: ..., analysis: ..., confidence: 0.87, intent: ..., action: ..., arguments: {}, explanation: ..., summary: ... }看上去信息特别丰富。但先问一个问题Runtime 真正需要什么如果 Runtime 最终只消费action arguments那么其他字段为什么一定要存在接口设计有一个非常朴素的原则只暴露真正需要暴露的东西。Structured Output 同样如此。你不是在试图把模型所有“思考”全部结构化而是在定义软件下一步真正需要消费什么数据这两个目标差别很大。好的 Schema 往往不是最大的 Schema。而是足够表达决策 足够验证 没有多余耦合这也会成为后面设计 Tool Schema 时非常重要的原则。到这里你其实已经摸到 Tool Calling 的本质了现在再看{ action: get_weather, arguments: { city: Shanghai } }有没有一种非常熟悉的感觉它其实已经长得很像get_weather(cityShanghai)差的只剩下一步到底谁把前面的数据结构变成后面的真实函数执行答案当然不是模型。而是 Runtime。也就是说下一课我们只需要继续增加一层Structured Decision ↓ Runtime ↓ 根据 action 找到函数 ↓ 传入 arguments ↓ 真正执行例如概念上tools { get_weather: get_weather, query_order: query_order, }然后tool tools[decision[action]] result tool( **decision[arguments] )这时候才发生真正的Action而今天整整一课我们只解决Decision 应该怎样被可靠地表达这个顺序非常重要。因为一旦这层理解了下一课所谓的 Tool Calling 会突然祛魅。你会发现它并不是LLM 神奇地拥有了调用函数的能力而是Model 产生一个符合协议的 Tool Decision ↓ Runtime 解释这个 Decision ↓ 真正执行函数而已。第二课学到这里应该形成一个新的脑内模型第一课结束时我们脑子里的 LLM 是LLM 根据 Context 生成 Output第二课结束之后应该再多一层理解LLM 可以根据 Context 生成符合特定 Contract 的 Decision于是 Agent 的结构开始慢慢长出来User ↓ Context ↓ LLM ↓ Structured Decision ↓ Parser / Validator注意。目前还不是 Agent。因为我们甚至还没有真正执行任何 Tool。但一个非常重要的地基已经完成了Model 和 Runtime 终于有办法可靠通信了。下一课我们只需要把 Runtime 接上去。第二课结束前我建议你真的想明白这几个问题不用背定义。如果下面这些问题能够不看文章自己解释出来这一课基本就过了。模型回答我觉得应该先查询上海天气。为什么人类很容易理解但不适合作为 Runtime 的接口模型返回{ action: get_weather }为什么这并不意味着天气已经被查询为什么一个输出可以是Valid JSON却依然不是Valid DecisionParser 和 Validator 分别解决了什么问题当我们规定action ∈ { get_weather, query_order, ask_user, final_answer }实际上是在给 Agent 定义什么为什么让模型返回query_order(order_id)通常比直接让模型自由生成 SQL 更容易控制如果用户只说帮我查天气为什么ask_user本身也值得成为一个 Action最后也是最重要的一个Structured Output 和 Tool Calling 到底差在哪一步如果最后这个问题真正想清楚了第三课基本已经学会一半。最后把第二课压缩成四句话第一句自然语言适合人与人交流但不是可靠的软件协议。第二句Structured Output 不是“让模型输出 JSON”而是在 Model 和 Runtime 之间建立可验证的 Contract。第三句Schema 不只是定义数据格式它还在定义模型允许选择的 Action Space。第四句Structured Output 只负责表达 Decision真正把 Decision 变成 Action 的是 Runtime。现在再回头看我们整个系列的进度第一课LLM ↓ Output第二课LLM ↓ Structured Decision下一课终于轮到LLM ↓ Structured Decision ↓ Runtime ↓ Tool ↓ Environment也就是下一课《Agent 原理三Tool Calling——模型根本没有调用函数真正动手的是 Runtime》到了第三课我们第一次真正执行一个 Tool。然后把 Tool 的返回结果重新交给模型。再下一步一个真正的 Agent Loop 就会自然长出来。你会发现我们从头到尾都没有“发明”Agent。只是每遇到一个解决不了的工程问题就不得不增加一个组件。最后回过头时一个 Agent 自己出现了。这也是这个系列真正想讲清楚的东西。