惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
V
V2EX
S
Security Affairs
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
J
Java Code Geeks
The Register - Security
The Register - Security
U
Unit 42
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
Project Zero
Project Zero
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
DataBreaches.Net
博客园 - 司徒正美
V
Vulnerabilities – Threatpost
T
Tor Project blog
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
小众软件
小众软件
L
LangChain Blog
Attack and Defense Labs
Attack and Defense Labs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
A
Arctic Wolf
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
MongoDB | Blog
MongoDB | Blog
Webroot Blog
Webroot Blog
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta

XINDOO

关于内卷,几个值得深想的洞察 当创作被 Skill 化:我用 AI 写了一部 320 章的长篇网文 AI第一剑,先斩程序员 AI 也会偷懒?这个 PUA 工具专治“摸鱼式编程” Gstack 深度解析:YC CEO 开源的 AI 工程团队 GitHub Trending霸榜!深度解析AI Coding辅助神器 Superpowers 深度探讨:从 OpenClaw 爆火,看 AI Agent 的真相与程序员的未来 我复刻了NotebookLM的信息图功能 我用AI写了部小说,这里是整个过程 [翻译]我在谷歌14年学到的21堂课 2万字吊打40万字:为什么我的“牛马Agent”比“数字分身”更聪明? 最近AI领域爆火的 Agent Skills 是什么? 从计算机科学的视角来看拖延症 一周改6个库后我悟了:AI时代,程序员正在退化成“甲方” 使用n8n做一个自动同步更新的Github项目问答机器人 Agent设计模式——第 20 章:优先级排序 Agent设计模式——第 1 章:提示词链 Agent设计模式——附录 D - 使用 AgentSpace 构建 Agent Agent设计模式——第 19 章:评估和监控 Agent设计模式——第 10 章:模型上下文协议 (MCP) Agent设计模式——第 21 章:探索和发现 Agent设计模式——智能体设计模式 Agent设计模式——第 11 章:目标设定和监控 Agent设计模式——第 9 章:学习和适应 Agent设计模式——第 16 章:资源感知优化 Agent设计模式——附录 G - 编码 Agent Agent设计模式——第 13 章:人机协同 Agent设计模式——第 17 章:推理技术 Agent设计模式——附录 F - 深入剖析:Agent 推理引擎的内部运作机制 Agent设计模式——章节目录 Agent设计模式—— Agent设计模式——第 7 章:多 Agent 协作 Agent设计模式——附录 E - 命令行界面中的 AI Agent Agent设计模式——附录 C - Agentic 框架快速概览 Agent设计模式——第 3 章:并行化 Agent设计模式——**常见问题解答:Agentic 设计模式** Agent设计模式——第 14 章:知识检索(RAG) Agent设计模式——第 18 章:Guardrails/安全模式 Agent设计模式——第 15 章:Agent 间通信(A2A) Agent设计模式——第 8 章:内存管理 Agent设计模式——第 12 章:异常处理和恢复 Agent设计模式——第 4 章:反思 Agent设计模式——附录 B - AI Agentic 交互:从图形界面到现实世界环境 Agent设计模式——第 5 章:工具使用(函数调用) Agent设计模式——结论 Agent设计模式——第 6 章:规划 Agent设计模式——第 2 章:路由 从经验主义到贝叶斯理论:如何排查线上问题 我用AI为自己造了一把安全的开发者“瑞士军刀”” 从LLM和MCP的协同过程看如何做优化 打通Dify与AI工具生态:将Workflow转为MCP工具的实践 一文了解知识库背后的技术RAG AI应用的五个级别:从入门到专家的进阶之路 一文入门AI圈最近爆火的MCP协议 HTTP/3:性能改进(第 2 部分) deepseek-r1祛魅:从过度热捧到理性认知⁠ 为什么AI智能体需要工作流 如何用GPT-4o解读视频 json命令行处理神器jq介绍 从大模型的原理到提示词优化 从经济学原理看团队分工合作 [翻译]关于人工智能的30个思考 从马斯洛需求层次理论谈职场激励 知识与智慧 如何使用大语言模型绘制专业图表 两个开源项目打造自己的大模型聚合平台 我让gpt4o给我推荐了一千多次书 得到了这些数据 用Langchain创建一个可以总结网页内容的Agent 推荐一个好用的命令行工具ShellGPT 关于ffmpeg height not divisible by 2的错误 使用Certbot解决https证书自动更新的问题 Spring Cache简明教程 软件开发中的抓大放小vs极致细节思维 OpenAI Assistants-API简明教程 OpenAI的多函数调用(Multiple Function Calling)简介 如何使用ffmpeg制作透明背景的视频 spring-kafka中ContainerProperties.AckMode详解 如何在地图上寻找最密集点的位置? IO密集型服务提升性能的三种方法 职场中的基本归因错误和自利归因 使用javax.validation.constraints校验参数合法性 Java Optional:让你的代码更优雅 ChatGPT函数调用初体验:让ChatGPT具备抓取网页文本的能力 如何使用ChatGPT提升自己的“码”力? 使用ffmpeg拼接两张图片 ThreadPoolExecutor——高效处理并发任务的必备良器 从CPU的视角看 多线程代码为什么那么难写! 使用ffmpeg缩小视频体积的几种方式 Linux parallel 命令使用手册 为什么说过早优化是万恶之源? Linux xargs命令介绍 深入理解Spring的事件通知机制 Java高并发之CyclicBarrier简介 聊一聊过度设计! 详解Redisson分布式限流的实现原理 Java中使用HashMap时指定初始化容量性能一定会更好吗? 如何用ffmpeg截取视频片段&截取时间不准确的坑 XINDOO的2022年年终总结 使用ffmpeg将视频转成HLS(m3u8)格式 谷歌Guava LoadingCache介绍
OpenAI的结构化浅析
xindoo · 2024-10-27 · via XINDOO


  OpenAI于2024年8月6日在其新模型gpt-4o-2024-08-06上推出了结构化输出功能(Structured Outputs)。截至本文撰写日期(2024年8月25日),gpt-4o仍指向上一版本gpt-4o-2024-05-13,尚不支持结构化输出。有趣的是,gpt-4o-mini反而已经支持了这一功能,这点值得大家注意。那么,结构化输出究竟是什么?为什么OpenAI要专门发布一篇博客来详细介绍它呢?接下来,让我们一起深入了解这个话题。

  结构化输出可以简单地描述为让大模型生成特定格式JSON的能力。OpenAI在其博客中指出,使用大语言模型(LLM)将非标准数据转化为特定格式的结构化数据是LLM的核心应用场景之一。然而,在早期阶段,让LLM直接输出合法的JSON字符串并非易事。某些模型在被要求输出JSON字符串时,会以Markdown代码块的形式呈现结果。正因如此,著名的LLM开发框架Langchain特意提供了JSON输出解析器(SimpleJsonOutputParser)来解决这一问题。

  让模型输出JSON看似简单,有人可能会说:"直接在提示中要求大模型输出JSON不就行了吗?"然而,这种方法并非万无一失。如前所述,模型有时会以Markdown代码块的形式返回结果,有时又会直接给出纯JSON。若要使用这些输出,你还需要兼容这两种情况。更棘手的是,在处理复杂的JSON格式时,模型可能会生成不合法的JSON字符串。在这种情况下,这条数据就完全无法使用了。

  OpenAI 后来推出了 json_object 输出模式(DeepSeek 也跟进了)。使用这种输出模式时,prompt 中必须包含json字样。json_object 模式解决了输出不一定是 JSON 字符串的问题。为了便于理解,让我们用一个从非结构化文本中提取结构化数据的场景为例,来演示这个简单操作。

初探Json生成

  假设我们要从一个人的自我介绍中提取各个维度的信息,并将提取的结果以 JSON 形式组织起来。用prompt抽取的代码如下:

import openai
import json
client = openai.OpenAI()

system_prompt = """
请提取出内容中的姓名,地址,手机号,兴趣爱好,对应的字段名分别是‘name’,‘address’,‘phoneNumber’,‘interests’。
用json字符串返回,格式参考下面这个json
{
   "name":"张三",
   "addres":"北京市朝阳区大望路108号",
   "phoneNumber":"17000098734",
   "interests":"打游戏"
}
"""

user_prompt = "我叫李四,家住在杭州西湖区xx路18号,我的手机号是19876496862,我平时喜欢钓鱼。"

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    response_format={
        'type': 'json_object'
    }
)

print(json.loads(response.choices[0].message.content))

  我个人测试这个简单案例,运行效果相当不错。对于我示例中这种功能简单的数据结构化,json_object输出模式完全足够应对。然而,在处理复杂的JSON结构时,这种方法就显得力不从心了。首先,用prompt准确描述复杂的JSON结构本身就是一个挑战。其次,让大语言模型严格遵循prompt格式输出数据也是一个难题。为了具体说明这一点,让我们来看一个树形结构JSON的例子。

  我现在有如下的组织架构,需要生成与之对应的json数据,每个节点有orgCode orgName 和children 三个字段,我们尝试用json_object生成试试。

orgCode: BJ1, orgName: 北京大区, parentOrgCode: null
orgCode: BJ2, orgName: 京北大区, parentOrgCode: BJ1
orgCode: BJ3, orgName: 京南大区, parentOrgCode: BJ1
orgCode: BJ4, orgName: 京北一部, parentOrgCode: BJ2
orgCode: BJ5, orgName: 京北二部, parentOrgCode: BJ2
orgCode: BJ6, orgName: 京北三部, parentOrgCode: BJ2
orgCode: BJ7, orgName: 京北四部, parentOrgCode: BJ2
orgCode: BJ8, orgName: 京南一部, parentOrgCode: BJ3
orgCode: BJ9, orgName: 京南二部, parentOrgCode: BJ3
orgCode: BJ10, orgName: 京南三部, parentOrgCode: BJ3
orgCode: BJ11, orgName: 京南四部, parentOrgCode: BJ3
orgCode orgName parentOrgCode
BJ1 北京大区 null
BJ2 京北大区 BJ1
BJ3 京南大区 BJ1
BJ4 京北一部 BJ2
BJ5 京北二部 BJ2
BJ6 京北三部 BJ2
BJ7 京北四部 BJ2
BJ8 京南一部 BJ3
BJ9 京南二部 BJ3
BJ10 京南三部 BJ3
BJ11 京南四部 BJ3
import openai
import json
client = openai.OpenAI()

system_prompt = """
我会给一些组织架构中的节点信息,其中parentOrgCode是当前节点的父节点,如果是null表示没有父节点。
请将这些节点信息用JsonArray表示出来, 每个节点有orgCode、orgName、level、children四个字段,
其中level是在组织树中的层级,children是其所有子节点,没有就不输出这个字段。 
参考下面格式:
[{
   "orgCode":"BJ1",
   "orgName":"北京大区",
   "children":{}
},{…}]
"""

user_prompt = """
orgCode: BJ1, orgName: 北京大区, parentOrgCode: null
orgCode: SH1, orgName: 上海大区, parentOrgCode: null
orgCode: BJ2, orgName: 京北大区, parentOrgCode: BJ1
orgCode: BJ3, orgName: 京南大区, parentOrgCode: BJ1
orgCode: BJ4, orgName: 京北一部, parentOrgCode: BJ2
orgCode: BJ5, orgName: 京北二部, parentOrgCode: BJ2
orgCode: BJ6, orgName: 京北三部, parentOrgCode: BJ2
orgCode: BJ7, orgName: 京北四部, parentOrgCode: BJ2
orgCode: BJ8, orgName: 京南一部, parentOrgCode: BJ3
orgCode: BJ9, orgName: 京南二部, parentOrgCode: BJ3
orgCode: BJ10, orgName: 京南三部, parentOrgCode: BJ3
orgCode: BJ11, orgName: 京南四部, parentOrgCode: BJ3
"""

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    response_format={
        'type': 'json_object'
    }
)
res=json.loads(response.choices[0].message.content)

  从上述代码的prompt中,我们可以看出我原本想要最外层返回一个JsonArray。然而,在多次尝试中,gpt-4o始终未能给出正确答案。它返回的是一个JsonObject,而且还遗漏了上海大区这个节点。此外,它的输出格式也不太稳定,有时会在最外层莫名其妙地包裹一个"orgs"或"nodes",如下所示:

{
    "orgCode": "BJ1",
    "orgName": "北京大区",
    "level": 1,
    "children": [{…}, {…}]
}
或者 
{
    "orgs": [{
        "orgCode": "BJ1",
        "orgName": "北京大区",
        "children": {…}
    }, {…}]
}

  经过进一步研究,我发现OpenAI实际上无法直接输出JsonArray。模型为了输出JsonObject而吞掉节点或强行在外层添加包装,这就导致了之前提到的问题。通过验证,我发现只需在上述数据中添加一个根节点,输出就能符合预期了。

orgCode: root, orgName: 根节点
orgCode: BJ1, orgName: 北京大区, parentOrgCode: root
orgCode: SH1, orgName: 上海大区, parentOrgCode: root
orgCode: BJ2, orgName: 京北大区, parentOrgCode: BJ1
…………

   接下来,让我们看看在 JSON Schema 模式下的效果。当我将输出切换为 JSON Schema 的严格模式后,结果达到了 100% 的准确率。具体代码如下:

import openai
import json
client = openai.OpenAI()

system_prompt = """
我会给一些组织架构中的节点信息,请将这些数据用json格式输出出来
"""

user_prompt = """
orgCode: BJ1, orgName: 北京大区, parentOrgCode: null
orgCode: SH1, orgName: 上海大区, parentOrgCode: null
orgCode: BJ2, orgName: 京北大区, parentOrgCode: BJ1
orgCode: BJ3, orgName: 京南大区, parentOrgCode: BJ1
orgCode: BJ4, orgName: 京北一部, parentOrgCode: BJ2
orgCode: BJ5, orgName: 京北二部, parentOrgCode: BJ2
orgCode: BJ6, orgName: 京北三部, parentOrgCode: BJ2
orgCode: BJ7, orgName: 京北四部, parentOrgCode: BJ2
orgCode: BJ8, orgName: 京南一部, parentOrgCode: BJ3
orgCode: BJ9, orgName: 京南二部, parentOrgCode: BJ3
orgCode: BJ10, orgName: 京南三部, parentOrgCode: BJ3
orgCode: BJ11, orgName: 京南四部, parentOrgCode: BJ3
"""

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=messages,
    response_format={
        "type": "json_schema",
        "json_schema":
        {
            "name": "my_schema",
            "schema":
            {
                "type": "object",
                "properties":
                {
                    "nodes":
                    {
                        "type": "array",
                        "description": "所有的子节点",
                        "items":
                        {
                            "$ref": "#/definitions/organization"
                        }
                    }
                },
                "required":["nodes"],
                "additionalProperties": False,
                "definitions":
                {
                    "organization":
                    {
                        "type": "object",
                        "properties":
                        {
                            "orgCode":
                            {
                                "type": "string",
                                "description": "orgCode"
                            },
                            "orgName":
                            {
                                "type": "string",
                                "description": "orgName"
                            },
                            "level":
                            {
                                "type": "integer",
                                "description": "在组织树中的层级"
                            },
                            "children":
                            {
                                "type": "array",
                                "description": "所有的子节点",
                                "items":
                                {
                                    "$ref": "#/definitions/organization"
                                }
                            }
                        },
                        "required":
                        [
                            "orgCode", "orgName", "level", "children"
                        ],
                        "additionalProperties": False
                    }
                }
            },
            "strict": True
        }
    }
)
res2=json.loads(response.choices[0].message.content)

  上面代码中prompt部分就很少了,仅包含一句简单的需求描述和一些数据,主要的代码都在schema的定义上

注意:OpenAI要求json的root层必须是JsonObject,所以我在上面额外加了个nodes层将结果封装起来了,还有一些其他的限制比如additionalProperties必须是false……,具体可以查阅下官网文档 https://platform.openai.com/docs/guides/structured-outputs

OpenAI在博客中给出的数据显示,gpt-4-0613使用prompt抽取复杂JSON格式数据时,结构的准确率仅为40%。即便是当前最强的gpt-4o模型,准确率也只达到85%。在线上系统中,哪怕只有1%的错误率,你也必须考虑这部分异常的处理逻辑,更何况是15%。在实际场景中,处理这15%的异常数据所花费的成本可能会超过处理另外85%正常数据的成本。

然而,OpenAI的强大之处在于gpt-4o-2024-08-06模型在JSON输出方面的表现。在严格模式下,它的准确率能达到100%——没错,就是100%。这意味着你完全不需要为数据格式异常考虑任何处理逻辑,只需专注于实际的业务数据处理。

image.png

如何使用

OpenAI的结构化输出调用相当简单。核心在于使用JSON Schema描述你所需的输出格式。虽然这需要一些学习,但成本不高,而且你还可以让大语言模型帮你编写JSON Schema。让我们回到之前的场景,给出一个结构化输出的代码示例。

import openai
import json
client = openai.OpenAI()

system_prompt = """
请提取出内容中的姓名,地址,手机号
"""

user_prompt = "我叫李四,家住在杭州西湖区xx路18号,我的手机号是19876496862,我平时喜欢钓鱼。"

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    response_format={
        "type": "json_schema",
        "json_schema":{
            "name": "my_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description":"姓名"
                    },
                    "address": {
                        "type": "string",
                        "description":"地址"
                    },
                    "phoneNumber": {
                        "type": "string",
                        "description":"11位的手机号"
                    }
                },
                "required": ["name", "address", "phoneNumber"],
                "additionalProperties": False
            },
            "strict": True
        }
    }
)

print(json.loads(response.choices[0].message.content))

OpenAI是如何实现的

  OpenAI在官方博客中表示,他们使用了上下文无关文法(CFG)来实现结构化输出。有限状态自动机(FSM)也可以实现,但表达能力较弱,无法支持递归结构定义。相比于编程语言,JSON的语法表示相对简单。以下是使用ANTLR4表示的JSON语法:

grammar JSON;

json
   : value
   ;

obj
   : '{' pair (',' pair)* '}'
   | '{' '}'
   ;

pair
   : STRING ':' value
   ;

array
   : '[' value (',' value)* ']'
   | '[' ']'
   ;

value
   : STRING
   | NUMBER
   | obj
   | array
   | 'true'
   | 'false'
   | 'null'
   ;

STRING
   : '"' (ESC | SAFECODEPOINT)* '"'
   ;

fragment ESC
   : '\\' (["\\/bfnrt] | UNICODE)
   ;
fragment UNICODE
   : 'u' HEX HEX HEX HEX
   ;
fragment HEX
   : [0-9a-fA-F]
   ;
fragment SAFECODEPOINT
   : ~ ["\\\u0000-\u001F]
   ;

NUMBER
   : '-'? INT ('.' [0-9] +)? EXP?
   ;

fragment INT
   : '0' | [1-9] [0-9]*
   ;

// no leading zeros

fragment EXP
   : [Ee] [+\-]? INT
   ;

// \- since - means "range" inside [...]

WS
   : [ \t\n\r] + -> skip
   ;

  熟悉AI的同学都知道,LLM的工作过程就是根据已有内容不断预测下一个token,这与我们使用的输入法预测下一个候选词本质上相似。不过,LLM能利用更丰富的上下文信息,从而推测出更多符合"逻辑"的可能性。

  在预测过程中,如果仅关注当前token与之前token的语义关系,我们会得到一段符合前文语义的内容。但如果同时考虑与前文的结构关系,就能生成既符合语义又符合结构的内容。

  这个概念可能有点抽象,让我们用一个简单的例子来说明。还记得大学时学习数据结构中的栈吗?有一个经典示例就是判断括号的合法性。如果你忘记了,不妨重温一下LeetCode第20题。以LeetCode第20题Valid Parentheses为例,我们可以用Antlr4来表示合法的括号输入:

expr:  '(' expr ')'
    |  '{' expr '}'
    |  '[' expr ']'
    |  /* empty */
    ;

  这实际上是一个上下文无关文法定义(CFG)。它可以用状态转移图来表示,其中每条边代表一个输入符号:

  上图包含递归定义,其中expr边的定义就是上图本身。在这个图中,只要一个输入能从start节点顺利到达end节点,就表示这个输入是合法的括号表达式。在LeetCode第20题中,我们可以将上图的递归展开,得到下面这个更复杂的图(其中"|"表示"或")。虽然看起来略微复杂,但我们可以轻易看出:合法输入的第一个字符只能是"("、"["或"{"三者之一。随着输入的持续,只要是合法输入,状态转换一定在q0到q8之间进行,且在每个节点上都有明确的下一个预期合法输入字符。

  根据OpenAI的博客介绍,我推测其实现原理与上述例子类似,但JSON结构的状态转移图复杂度远高于LeetCode第20题。OpenAI可能会将输入的JSON Schema预编译成类似的状态图。在预测每个新token时,系统处于特定状态节点,通过状态图可以确定下一步的合法输入。最终,系统会从这些合法输入中选择概率最高的作为下一个token。

结语

  通过这篇文章,我们了解了OpenAI结构化输出的基本用法,并深入探讨了其可能的实现原理。希望这些内容对大家有所帮助。结构化输出功能无疑是AI与现有系统对接的关键依赖,因为目前所有系统的输入都有特定的格式要求。在没有结构化输出能力之前,我们不得不使用各种奇技淫巧来完成数据格式化。显然,有了结构化输出,这部分工作就会简单得多。 不过,我还要提醒大家一点:不要把结构化输出当成万能工具。俗话说,"拿着锤子看什么都像钉子",可别落入这个陷阱。根据我的实际测试,大多数数据格式相对简单,使用json_object模式通常就足够了。所以,要根据实际需求选择合适的方法。

参考资料