速成课 · No. 27
原始的语言模型返回的是 free text——对人来说赏心悦目,对你的代码却毫无依靠。要在模型之上构建真正的软件,你需要两样东西:程序真正能信任的输出(structured output),以及让模型越过文本、采取行动的安全方式(tool use)。二者合在一起,就把聊天机器人变成了你系统内部一个可靠的组件。
只讲精髓 · 每个想法一个画面 · 工程胜于魔法
在模型之上构建的第一步,是认清这种错配:它说的是散文,而你的程序需要数据和动作。弥合这道鸿沟,正是整门课要讲的事。
散文是给人看的;代码需要数据
一段手写的、描述某笔订单的文字,对比一张填好的、带标注方框的表格——人能轻松读懂那段文字,但只有表格能被机器归档处理。
默认情况下,语言模型输出的是 free text——一段文字、一段解释、散文。这对人类读者完美无缺,对你的代码却毫无用处,因为代码需要可预测、有结构、能据以行动的值:这里一个数字,那里一个类别,一个是或否。程序无法可靠地从模型随心所欲措辞的句子里,把「这位客户想退款 40 美元」抠出来。散文和数据是两种不同的东西,而代码运行在数据之上。
解析散文既脆弱又会崩
试图从一封信里提取总额,而这封信可能写「$40」、「四十美元」,也可能写「退还 40 块钱」——你的规则一直管用,直到某天措辞一变,整套就全垮了。
最朴素的办法——让模型写出文本,再用你自己的代码去解析——很脆弱。模型是非确定性的;同一件事它会用十种不同的说法表达,加上一段友好的开场白,或者把答案裹进 markdown。你的解析器只能应付见过的情形,碰到没见过的那一个就碎掉。在解析模型散文的字符串之上构建,就是在沙子上盖楼:演示时管用,上线后失灵。
目标:让模型成为可靠的零件
引擎只有装上了标准的安装座和接口,才对汽车有用——那是机器其余部分可以放心拴接上去的、可预测的接口。
要在模型之上构建软件,你必须把它从一个健谈的神谕,变成一个带有可预测接口的组件——代码能信任的输出,以及它能安全采取的动作。这恰好就是本课的两半:structured output(数据,而非散文)和 tool use(行动,而不只是说话)。把这两样拿到手,模型就不再是个演示,而成了一个可靠的零件,你可以像对待任何其他零件那样在它周围做工程。
模型说的是散文;你的代码需要数据和动作。解析 free text 很脆弱——目标是把模型变成一个可靠的组件,输出你信得过、动作它能采取。
把模型变成组件的前半段,是让它每一次都按你的代码定义的形状返回数据——不是要你去猜的散文,而是可预测的结构。
要的是 schema,不是句子
递给别人一张带标注方框的表格,而不是一张白纸——他们恰好填上你需要的字段,按你需要的顺序,多一项都没有。
Structured output 是指要求模型按一种确定的格式返回数据——通常是匹配你所指定 schema 的 JSON:这些字段、这些类型、这种形状。你不再说「跟我讲讲这笔订单」,而是说「返回 {customer, amount, reason}」,于是你拿回的是代码可以直接用的数据。你定义形状,模型把它填满。正是这一步,把模型的输出从散文变成了程序可以依赖的东西。
模型可以被约束到只产生合法输出
一个铁路道岔,物理上只能把列车送上几条既定轨道之一——它没有任何办法让列车开到地图之外去。
现代模型支持 constrained 生成:它们可以被强制产出真正匹配你 schema 的输出——合法的 JSON、正确的字段、正确的类型——而不只是被客客气气地请求这么做。这弥补了一个缺口:模型「大体上」会返回正确的形状,但偶尔会跑偏。把输出 constrained 到 schema 之后,你的代码就能笃定结构必然在那儿,而这正是让模型可被当作真正构件来用的关键。
Structured output 是通往你代码的桥
一位翻译,把客人口头的愿望转换成一张精确、标准化、厨房可以照办的点单——意思不变,只是换成了系统能用的形式。
Structured output 是模型的语言能力与你软件对数据的需求之间的桥。它让模型去做它最擅长的事——理解杂乱的人类输入——再把结果作为干净、带类型的值交给你的代码。分类、抽取、路由、填表:当输出是结构化的,这些都变得可靠。模型读懂混乱,schema 交付秩序。大多数真实的 LLM 功能,恰恰活在这次交接里。
Structured output 意味着模型返回匹配你所定义 schema 的数据,并被约束到合法形状——这座桥把它的语言能力变成你代码能依赖的、带类型的值。
Structured output 给了你一个可预测的形状,但形状不等于正确。在你的代码信任模型返回的东西之前,它必须先检查——和对待任何不可信输入的纪律一样。
形状合法不等于内容合法
一张填得整整齐齐、每个方框都填满的表格——可日期根本不可能存在,总额也对不上。工整不等于正确。
schema 保证的是形状——正确的字段和类型——而不是这些值有没有道理。模型可以返回一份格式完美的 JSON,里头却是个负数的数量、一个根本不存在的类别,或者一个它凭空捏造的金额。Structured output 解决的是「我的代码能不能读它」;它并不解决「它对不对」。把一份格式良好的响应当成一个有待检查的起点,而不是一个正确无误的保证。
在任何东西据以行动之前先校验
两国之间的一道海关检查——任何东西在依据规则受检并被宣告安全之前,都不能跨进下一个系统。
所以你要在代码使用模型的 structured output 之前先去 validate 它:严格执行 schema,检查各个值是否落在允许的范围与集合内,确认任何被引用的东西确实存在。这道检查跑在你的代码里,跑在模型与你系统其余部分之间的边界上,因为模型是一个你无法完全信任的非确定性组件。这和安全里的那条教训一样:在你核验之前,模型的输出对下一阶段而言就是不可信输入。
绝不要把原始输出直接灌进危险的地方
你不会把一股未经过滤的水流直接倒进饮用水供给里——上游有什么,现在就出现在每一个水龙头里,再没机会拦住它。
当输出流向某个有后果的地方时,规矩就变得严苛。绝不要把模型的原始输出未经校验和转义就直接喂给一次数据库写入、一条 shell 命令、一次查询或另一个系统——一次幻觉或一条被注入的指令,正是这样变成真实的 bug 或被攻破。Structured output 加上严格的 validation,才让你能安全地对模型产出的东西采取行动。结构让它可读;validation 让它可信。
合法的 schema 保证的是形状,不是正确性。在边界处 validate 模型的输出——范围、集合、存在性——再让任何代码据以行动,因为模型的输出在被检查之前都不可信。
Structured output 让模型能返回数据。把它变成组件的另一半,是让它能做事——越过文本去搜索、计算、获取,并在真实世界里行动。这就是 tool use。
给模型它可以调用的函数
一位聪明的助手,自己打不开文件柜——但能精确告诉你该抽哪个抽屉、拿哪份文件,并用你取回来的东西。
借助 tool use(也叫 function calling),你描述一组模型被允许请求的函数——search_orders、send_email、get_weather——当模型判断自己需要其中某个时,它返回一个结构化的调用,指明函数及其参数。这是把 structured output 用在了动作上:模型吐出的不是数据,而是一个去做某事的请求。这正是 LLM 越过自身文本、伸进你的系统与世界的方式。
模型决定;你的代码执行
一位工头,指着并精确说出要做什么——但真正操作机器、掌控整个工地的,是工人,不是工头。
分工才是关键:模型决定做什么、用什么参数;**你的代码决定是否以及如何真正去做。**当模型返回一个工具调用时,你的代码运行那个函数(或者不运行),拿到结果,再把它喂回上下文,让模型继续。模型从不直接碰你的系统——它发出请求,你的代码去执行。那条边界,正是你保住掌控与安全的地方。
结果会喂回循环里
一位研究者要来一份文件,读完取回来的内容,再据此决定下一个问题——是一来一回,而不是一锤子买卖。
Tool use 是一个循环:模型请求一个工具,你的代码运行它并把结果返回上下文,模型带着这条新信息继续推理——可能再调用另一个工具。正是这个循环把模型变成了 agent(agents 课会深讲)。工具是手;循环是持续。眼下,关键的想法是:tool use 让模型能一步一步地收集它所需要的东西并行动,而不是只凭记忆盲目作答。
Tool use 让模型能请求函数——这是用在动作上的 structured output。模型决定做什么;你的代码决定是否以及如何去做,并把结果喂回去。
模型是根据你怎么描述工具来挑选和使用它们的,所以工具设计是拿到可靠行为的一部分。几条原则,把好用的工具和让模型犯迷糊的工具区分开来。
清晰的名字和描述引导选择
一个工具箱,每件工具都清楚标注了用途——工人立刻就能伸手拿对的那件,而不必从一排没有标记的手柄里去猜。
模型几乎完全是凭工具的名字和描述来选择调用哪一个。所以它们不是给人看的文档——它们是模型据以推理的指令。一段含糊或带误导的描述,会引向错的工具、错的参数,或者在该用时却被忽略。把工具的名字和描述写得像写一段提示一样用心,因为对模型来说,它们恰恰就是提示。
几件锋利的工具胜过一份巨型菜单
一间厨房,配几件精挑细选、彼此分明的工具,干起活来比配五十件功能重叠、厨师每次都得翻找的小玩意儿要快。
给模型太多工具——或几件互相重叠的工具——会让它每一轮都更难选得好。一小套锋利、界限分明的工具,比一份铺得到处都是的菜单更可靠。如果两件工具做的事几乎一样,模型有时会挑错那个;如果有四十件,选择本身就成了一个出错的来源。像你筛选上下文那样去筛选工具集:只留任务所需的。
把工具的描述当作一个安全面来提防
一位新员工,对每个箱子上的标签都照单全收、从不质疑——于是谁来写标签,谁就实际控制了他做什么。
因为模型信任工具的描述和工具的输出,它们就是你安全面的一部分,而不是中立的管道。一段被投毒的描述,或一个返回攻击者可控文本的工具,能操纵模型的行为——这叫 tool poisoning。审查你接入的工具,把每件都收窄到它所需的最小权限(一个只读工具不该有删除的能力),并把工具返回的东西当作不可信输入来对待。你授予一个工具的能力,就是一个犯迷糊或被劫持的模型可以滥用的能力。
模型凭工具的名字和描述来挑选工具,所以要像写提示一样写它们。让工具数量少而锋利,把每件收窄到 least privilege,并把描述和输出当作一个安全面。
随着工具越来越多,手工把每个模型连到每个系统就难以为继。一种关于模型如何触达工具与数据的标准已经浮现——随之而来的,是一个要去理解和加固的新层。
一种把工具暴露给模型的标准方式
在标准插头和插座出现之前,每件电器都需要自己的定制接线——一个通用标准意味着任何设备都能接上任何插座,无需量身定做。
**Model Context Protocol(MCP)**是一个正在兴起的标准,规定模型如何连接到工具和数据源。MCP 不再用定制代码把每个模型连到每个系统,而是定义了一种暴露工具的通用方式,于是任何兼容的模型都能用上任何兼容的工具。它是 agent 时代的万能插座——一个连接器层,让工具与模型的生态系统能拼接到一起,不必每次都做一次性的集成。
它把 agent 的管道标准化了
一张电网:标准化之后,一件新电器一插就能用,一个新电源也只是给同一张网供电——正是这个标准,让整张网络可以组合。
MCP 之所以重要,是因为它让工具和数据变得可组合:一件工具造一次,就能被许多模型和 agent 使用;一个新 agent 立刻就能用上整个既有工具生态。这很大程度上解释了 agent 为何那么快就强大起来——管道被标准化了,于是能力得以咔嗒一声拼到一起。今天当你给一个 agent 接入你的系统时,越来越多地是 MCP 这一层在做这件事。
连接器层是一个攻击面
新管道飞快地铺遍了整栋房子——而一次勘查发现,相当大一部分阀门从来就没装过锁。
这个新层同时也是一个会被攻破的新地方。MCP 连接器暴露的是真实的动作和数据,而仓促采用它们,已经让许多部署带着脆弱或缺失的安全上线——相当大一部分远程 MCP 服务器在发布时根本没有任何认证。所以,把连接器当成它本来的那扇门:给它做认证,收窄它暴露的东西,除非必须否则别把它放到公网上,并清点你都连了些什么。强大的管道,要求装上最基本的锁。
MCP 把模型如何触达工具与数据标准化,让生态系统变得可组合。同样这个连接器层是一个真实的攻击面——给它认证,收窄它,并清点它。
Structured output 和 tool use 是把模型变成组件的工具,但要用好它们,仍然意味着爬那架梯子——用能解决你问题的、威力最小的那件。
先用 structured output,必须行动时才用工具
当你需要的只是一个答案时,你不会把汽车钥匙交给别人——你只给任务所需的那点能力,多一分都不给。
这里有一架能力的梯子。一段朴素的提示给出答案;structured output 让那个答案能被代码使用;工具让模型去获取和行动;一个完整的 agent 循环让它去追逐一个多步骤的目标。每一级都加上更多威力,也加上新的失败方式。只爬到任务所需的那么高:如果你只需要干净的数据,structured output 就够了——别给模型工具,当一个 constrained 的响应就能解决时,更别给它一个循环。大多数功能从不需要最高那一级。
对一切跨回来的东西都做校验
一个退货台,对每一件退回来的物品都先检查再上架——不管顾客看上去多可信,没有任何东西能不经查验就重新进入系统。
贯穿这两半的统一习惯,是在边界处不轻信:在你的代码使用之前 validate 模型的 structured output,并对模型请求的工具让你的代码掌控是否运行它。模型是一个出色的、非确定性的组件,你把一切从它跨进你系统的东西,都当成有待核验的输入。正是这种边界纪律,让你能在一个本质上会犯错的东西之上可靠地构建。
- 我的代码需要数据吗——我是不是在拿到符合某个 schema 的 structured output,而不是在解析散文? - 输出校验过了吗——在任何东西据以行动之前,值是被检查过的,而不只是格式良好? - 它需要行动吗——我有没有把它暴露成一个工具,并让我的代码掌控是否运行 它? - 工具是否数量少、锋利、描述得好,并收窄到 least privilege? - 如果在 用 MCP,每个连接器是不是都经过认证、收窄、并清点在册? - 我是不是站在能成事的 最低那一级——只在需要时才用输出、工具或一个完整的循环?
- free text / structured output——给人看的散文,对比你代码能依赖的数据。 - schema / JSON / constrained——你定义并强制模型去匹配的形状。 - validation / boundary——在任何代码信任之前,检查值是否正确。 - tool use / function calling——让模型请求由你代码运行的动作。 - tool description / tool poisoning——模型如何挑选工具,以及其中蕴含的安全风险。 - MCP——把工具与数据暴露给模型的 标准连接器层。 - least privilege——把 每件工具收窄到它所需的最小权限。
- 你拿到的是符合某个 schema 的 structured output,而不是去解析模型的散文。 - 你在任何代码据以行动之前, 于边界处 validate 输出。 - 模型请求工具,而你的代码始终掌控是否运行它们。 - 你的工具数量少、锋利、 描述得好,并且 least-privilege。 - 你只爬到任务所需的那一级,并把 MCP 连接器当作一扇已加固的门。
要在模型之上构建,就把它做成一个组件:你代码能信任、并在边界处校验过的 structured output,以及它请求但由你代码掌控的工具——用能成事的、威力最小的那一级。