3、结构化输出
结构化输出
什么是结构化输出
结构化输出(Structed Outputs)是指让 LLM 输出符合机器可解析的格式,典型的是 JSON 结构。有三条技术路径:
- JSON mode - 在 前面Prompt Engineering章节 有介绍,也就是通过prompt告诉大模型生成什么样的格式
- Function Calling
- JSON Schema
接口(Interface)
两种常见接口:
- 人机交互接口,User Interface,简称 UI
- 应用程序编程接口,Application Programming Interface,简称 API
接口能「通」的关键,是两边都要遵守约定。
- 人要按照 UI 的设计来操作。UI 的设计要符合人的习惯
- 程序要按照 API 的设计来调用。API 的设计要符合程序惯例
接口的进化
UI 进化的趋势是:越来越适应人的习惯,越来越自然
- 命令行,Command Line Interface,简称 CLI(DOS、Unix/Linux shell, Windows Power Shell)
- 图形界面,Graphical User Interface,简称 GUI(Windows、MacOS、iOS、Android)
- 语言界面,Conversational User Interface,简称 CUI,或 Natural-Language User Interface,简称 LUI ← 我们在这里
- 脑机接口,Brain–Computer Interface,简称 BCI

API:
- 从本地到远程,从同步到异步,媒介发生很多变化,但本质一直没变:程序员的约定
- 现在,开始进化到自然语言接口,Natural-Language Interface,简称 NLI
自然语言连接一切(Natural Language Interface)
用户操作习惯的迁移,会逼所有软件,都得提供「自然语言界面(Natural Language Interface,简称 NLI)」。
不仅用户界面要 NLI,API 也要 NLI 化。这是因为用户发出的宏观指令,往往不会是一个独立软件能解决的,它需要很多软件、设备的配合。
一种实现思路是,入口 AI(比如 Siri、小爱同学,机器人管家)非常强大,能充分了解所有软件和设备的能力,且能准确地把用户任务拆解和分发下去。这对入口 AI 的要求非常高。
另一种实现思路是,入口 AI 收到自然语言指令,把指令通过 NLI 广播出去(也可以基于某些规则做有选择的广播,保护用户隐私),由各个软件自主决策接不接这个指令,接了要怎么做,该和谁配合。
最自然的接口,就是自然语言接口:
以前因为计算机处理不对自然语言,所以有了那么多编程语言,那么多接口,那么多协议,那么多界面风格。而且,它们每一次进化,都是为了「更自然」。现在,终极的自然,到来了。我们终于可以把计算机当人看了!
为什么要大模型连接外部世界?
- 并非知晓一切
- 训练数据不可能什么都有。垂直、非公开数据必有欠缺
- 不知道最新信息。大模型的训练周期很长,且更新一次耗资巨大,还有越训越傻的风险。所以 ta 不可能实时训练。OpenAI 模型知识截止日期:
- GPT-3.5 知识截至 2021 年 9 月
- GPT-4-turbo 知识截至 2023 年 12 月
- GPT-4o-mini 知识截至 2023 年 10 月
- GPT-4o 知识截至 2023 年 10 月
- GPT-4 知识截至 2021 年 9 月
- 没有「真逻辑」。它表现出的逻辑、推理,是训练文本的统计规律,而不是真正的逻辑,所以有幻觉。
所以:大模型需要连接真实世界,并对接真逻辑系统执行确定性任务。
比如算加法:
- 把 100 以内所有加法算式都训练给大模型,ta 就能回答 100 以内的加法算式,但仍有概率出错
- 如果问 ta 更大数字的加法,出错概率就会更大
- 因为 ta 并不懂「加法」,只是记住了 100 以内的加法算式的统计规律
- Ta 是用字面意义做数学
ChatGPT 用 Actions 连接外部世界
第一次尝试:Plugins
- 2023 年 3 月 24 日发布 Plugins,模型可以调用外部 API
- 2024 年 4 月 9 日正式下线,宣告失败
我们在第 1 期(2023 年 7 月)就告诉大家,Plugins 会失败,不用投入精力了解细节。
第二次尝试:Actions
Actions,内置在 GPTs 中,解决了落地场景问题,但没能成功商业化。
工作流程:

- 通过 Actions 的 schema,GPT 能读懂各个 API 能做什么、怎么调用(相当于人读 API 文档)
- 拿到 prompt,GPT 分析出是否要调用 API 才能解决问题(相当于人读需求)
- 如果要调用 API,生成调用参数(相当于人编写调用代码)
- ChatGPT(注意,不是 GPT)调用 API(相当于人运行程序)
- API 返回结果,GPT 读懂结果,整合到回答中(相当于人整理结果,输出结论)
把 AI 当人看!
这个过程中,GPT 已经是个 agent 了。
Actions 开发对接
把 API 对接到 GPTs 里,只需要配置一段 API 描述信息:
1 | openapi: 3.1.0 |
这里的所有 name
、description
都是 prompt,决定了 GPT 会不会调用你的 API,调用得是否正确。
GPTs 与它的平替们
- 无需编程,就能定制个性对话机器人的平台
- 可以放入自己的知识库,实现 RAG
- 可以通过 actions 对接专有数据和功能
- 内置 DALL·E 3 文生图和 Code Interpreter 能力
- 只有 ChatGPT Plus 会员可以使用
推荐两款平替:
- 中国版发展势头很猛,支持豆包、Moonshot 等国产大模型
- 功能很强大,支持工作流、API
- 开源,中国公司开发
- 可以本地部署,支持几乎所有大模型
- 有 GUI,也有 API
有这类无需开发的工具,为什么还要学大模型开发技术呢?
- 并不是所有事情都适合用对话解决
- 它们都无法针对业务需求做极致调优
一个常见的研发场景:先在扣子/Dify 验证原型可行性,再编程落地实现。
Function Calling 技术可以把大模型和业务系统连接,实现更丰富的功能。
Function Calling 的机制
原理和 Actions 一样,只是使用方式有区别。

示例 1:调用本地函数
需求:实现一个回答问题的 AI。题目中如果有加法,必须能精确计算。
1 | # 初始化 |
1 | def get_completion(messages, model="gpt-4o-mini"): |
1 | from math import * |
=====最终 GPT 回复=====
The sum of the numbers 1 through 10 is 55.
=====对话历史=====
{
"role": "system",
"content": "你是一个数学家"
}
{
"role": "user",
"content": "Tell me the sum of 1, 2, 3, 4, 5, 6, 7, 8, 9, 10."
}
{
"content": null,
"refusal": null,
"role": "assistant",
"audio": null,
"function_call": null,
"tool_calls": [
{
"id": "call_DaOLrmICI4uhR9ZwPIfj9CA0",
"function": {
"arguments": "{\"numbers\":[1,2,3,4,5,6,7,8,9,10]}",
"name": "sum",
"parameters": null
},
"type": "function"
}
]
}
{
"tool_call_id": "call_DaOLrmICI4uhR9ZwPIfj9CA0",
"role": "tool",
"name": "sum",
"content": "55"
}
{
"content": "The sum of the numbers 1 through 10 is 55.",
"refusal": null,
"role": "assistant",
"audio": null,
"function_call": null,
"tool_calls": null
}
- Function Calling 中的函数与参数的描述也是一种 prompt
- 这种 prompt 也需要调优,否则会影响函数的召回、参数的准确性,甚至让大模型产生幻觉,调用不存在的函数
示例 2:多 Function 调用
需求:查询某个地点附近的酒店、餐厅、景点等信息。即,查询某个 POI 附近的 POI。
1 | def get_completion(messages, model="gpt-4o-mini"): |
1 | import requests |
1 | prompt = "我想在五道口附近喝咖啡,给我推荐几个" |
=====GPT回复=====
{
"content": null,
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_BEjN2hy7nriCqmWFGGvoyNmt",
"function": {
"arguments": "{\"location\":\"五道口\",\"city\":\"北京\"}",
"name": "get_location_coordinate"
},
"type": "function"
}
]
}
函数参数展开:
{
"location": "五道口",
"city": "北京"
}
Call: get_location_coordinate
=====函数返回=====
{
"parent": "",
"address": "海淀区",
"distance": "",
"pcode": "110000",
"adcode": "110108",
"pname": "北京市",
"cityname": "北京市",
"type": "地名地址信息;热点地名;热点地名",
"typecode": "190700",
"adname": "海淀区",
"citycode": "010",
"name": "五道口",
"location": "116.338611,39.992552",
"id": "B000A8WSBH"
}
函数参数展开:
{
"longitude": "116.338611",
"latitude": "39.992552",
"keyword": "咖啡"
}
Call: search_nearby_pois
=====函数返回=====
PAGEONE CAFE(五道口购物中心店)
成府路28号五道口购物中心(五道口地铁站B南口步行190米)
距离:9米
星巴克(北京五道口购物中心店)
成府路28号1层101-10B及2层201-09号
距离:39米
luckin coffee 瑞幸咖啡(五道口购物中心店)
成府路28号五道口购物中心负一层101号
距离:67米
=====最终回复=====
在五道口附近有以下几家咖啡店推荐:
1. **PAGEONE CAFE(五道口购物中心店)**
- 地址:成府路28号五道口购物中心(五道口地铁站B南口步行190米)
- 距离:9米
2. **星巴克(北京五道口购物中心店)**
- 地址:成府路28号1层101-10B及2层201-09号
- 距离:39米
3. **luckin coffee 瑞幸咖啡(五道口购物中心店)**
- 地址:成府路28号五道口购物中心负一层101号
- 距离:67米
希望你能找到一个满意的地方享受咖啡时光!
=====对话历史=====
{
"role": "system",
"content": "你是一个地图通,你可以找到任何地址。"
}
{
"role": "user",
"content": "我想在五道口附近喝咖啡,给我推荐几个"
}
{
"content": null,
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_BEjN2hy7nriCqmWFGGvoyNmt",
"function": {
"arguments": "{\"location\":\"五道口\",\"city\":\"北京\"}",
"name": "get_location_coordinate"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_BEjN2hy7nriCqmWFGGvoyNmt",
"role": "tool",
"name": "get_location_coordinate",
"content": "{'parent': '', 'address': '海淀区', 'distance': '', 'pcode': '110000', 'adcode': '110108', 'pname': '北京市', 'cityname': '北京市', 'type': '地名地址信息;热点地名;热点地名', 'typecode': '190700', 'adname': '海淀区', 'citycode': '010', 'name': '五道口', 'location': '116.338611,39.992552', 'id': 'B000A8WSBH'}"
}
{
"content": null,
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_PuOC0rCTct8cSbHoT3rAHTee",
"function": {
"arguments": "{\"longitude\":\"116.338611\",\"latitude\":\"39.992552\",\"keyword\":\"咖啡\"}",
"name": "search_nearby_pois"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_PuOC0rCTct8cSbHoT3rAHTee",
"role": "tool",
"name": "search_nearby_pois",
"content": "PAGEONE CAFE(五道口购物中心店)\n成府路28号五道口购物中心(五道口地铁站B南口步行190米)\n距离:9米\n\n星巴克(北京五道口购物中心店)\n成府路28号1层101-10B及2层201-09号\n距离:39米\n\nluckin coffee 瑞幸咖啡(五道口购物中心店)\n成府路28号五道口购物中心负一层101号\n距离:67米\n\n"
}
{
"content": "在五道口附近有以下几家咖啡店推荐:\n\n1. **PAGEONE CAFE(五道口购物中心店)**\n - 地址:成府路28号五道口购物中心(五道口地铁站B南口步行190米)\n - 距离:9米\n\n2. **星巴克(北京五道口购物中心店)**\n - 地址:成府路28号1层101-10B及2层201-09号\n - 距离:39米\n\n3. **luckin coffee 瑞幸咖啡(五道口购物中心店)**\n - 地址:成府路28号五道口购物中心负一层101号\n - 距离:67米\n\n希望你能找到一个满意的地方享受咖啡时光!",
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": null
}
示例 3:通过 Function Calling 查询数据库
需求:从订单表中查询各种信息,比如某个用户的订单数量、某个商品的销量、某个用户的消费总额等等。
1 | # 描述数据库表结构 |
1 | def get_sql_completion(messages, model="gpt-4o-mini"): |
1 | import sqlite3 |
1 | def ask_database(query): |
====Function Calling====
{
"content": "",
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_aeZKffHgmyzOzcNKOXcRe3ha",
"function": {
"arguments": "{\"query\":\"SELECT SUM(price) AS total_sales FROM orders WHERE strftime('%Y-%m', create_time) = '2023-10' AND status = 1;\"}",
"name": "ask_database"
},
"type": "function"
}
]
}
====SQL====
SELECT SUM(price) AS total_sales FROM orders WHERE strftime('%Y-%m', create_time) = '2023-10' AND status = 1;
====DB Records====
[(86.0,)]
====最终回复====
10月的销售额为86.00元。
=====对话历史=====
{
"role": "system",
"content": "你是一个数据分析师,基于数据库的数据回答问题"
}
{
"role": "user",
"content": "10月的销售额"
}
{
"content": "",
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_aeZKffHgmyzOzcNKOXcRe3ha",
"function": {
"arguments": "{\"query\":\"SELECT SUM(price) AS total_sales FROM orders WHERE strftime('%Y-%m', create_time) = '2023-10' AND status = 1;\"}",
"name": "ask_database"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_aeZKffHgmyzOzcNKOXcRe3ha",
"role": "tool",
"name": "ask_database",
"content": "[(86.0,)]"
}
{
"content": "10月的销售额为86.00元。",
"refusal": null,
"role": "assistant",
"function_call": null,
"tool_calls": null
}
示例 4:Stream 模式
流式(stream)输出不会一次返回完整 JSON 结构,所以需要拼接后再使用。
1 | def get_completion(messages, model="gpt-4o-mini"): |
====Streaming====
sum
{"
numbers
":[
1
,
2
,
3
]}
====done!====
sum
{"numbers":[1,2,3]}
Function Calling 的注意事项
- 函数声明是消耗 token 的。要在功能覆盖、省钱、节约上下文窗口之间找到最佳平衡
- Function Calling 不仅可以调用读函数,也能调用写函数。但官方强烈建议,在写之前,一定要有真人做确认
国产大模型 Function Calling 能力仍不足
- 国产大模型基本都支持 Function Calling 了
- 实现稳定的 FC 能力,难度挺大。需要模型推理能力强,指令遵从强,格式控制能力强,以及有好的中间层
另一种声音
- 有人不喜欢用 FC,更愿意用 prompt 请求 JSON 结果的方式手动实现 FC 的能力。原因:
- 省 token
- 更可控
- 更容易切换基础大模型
- 并没有足够证据表明一定孰优孰劣
几条经验总结
- 详细拆解业务 SOP,形成任务工作流。每个任务各个击破,当前别幻想模型一揽子解决所有问题
- 不是所有任务都适合用大模型解决。传统方案,包括传统 AI 方案,可能更合适
- 一定要能评估大模型的准确率(所以要先有测试集,否则别问「能不能做」)
- 评估 bad case 的影响面
- 大模型永远不是 100% 正确的,建立在这个假设基础上推敲产品的可行性
用 json_schema 控制回复格式
- 这是 OpenAI 2024 年 8 月 6 日发布的新 API
- 未见国产大模型跟进,因为没那么容易跟进
- 但很可能又成为一个标准
- 比 JSON mode 更稳定,更容易控制
1 | from pydantic import BaseModel |
原理
把 JSON 的结构定义一并给到大模型,所以能更稳定。
下面是调用时传的参数:
1 | { |