探索 ChatGPT 结构化数据支持的极限 [译]

作者:

Max Woolf

如果你只是通过免费的网页或移动应用接触 OpenAIChatGPT,那你可能还未充分挖掘出 ChatGPT 的全部潜力。通过使用价格低廉的付费 ChatGPT API,你可以对输出结果进行更加深入的控制。例如,我想为这篇博客文章生成一个包含分类和 SEO 关键词等有用元数据的摘要。我将这篇博客的文本和一个指令输入给 ChatGPT:

请将这篇博客文章总结为 3 个要点和一个简短的社交媒体分享宣传语,并列出 3 个文章分类和 3 个 SEO 关键词:

Bullet Points:
- Utilize the paid ChatGPT API for more control over output and to maintain a given JSON schema
- Learn about prompt engineering and system prompts to improve ChatGPT's output quality
- Simplify schema input/output with Pydantic for structured data support
Blurb for Social Media:
Discover how to unlock the full potential of ChatGPT with the paid API, prompt engineering, and Pydantic for structured data support. Gain more control over output and learn to maintain a given JSON schema. #ChatGPT #AI #Pydantic
Post Categories:
1. Artificial Intelligence
2. API Development
3. Data Schema
SEO Keywords:
1. ChatGPT API
2. Prompt Engineering
3. Pydantic Schema

这些结果并不特别准确,我还得手动一个个复制粘贴。但如果我能够 a) 通过给 ChatGPT 赋予一位专业复制编辑的新角色来大幅提高输出质量,并且 b) 使输出强制遵循特定的 JSON 架构,这样我就能通过编程方式访问它或将其存入数据库进行索引。多亏了提示工程和 ChatGPT 的函数调用功能,这已经成为可能。应用本文中介绍的这两种技术后,我们获得了以下更加精准的摘要:

{
"summary": [
"ChatGPT's potential is underutilized without the paid ChatGPT API",
"System prompts and prompt engineering are key to maximizing ChatGPT's capabilities",
"Structured data support in ChatGPT allows for more control over output and input"
],
"blurb": "Unlock the full potential of ChatGPT with system prompts and structured data support. Learn how to maximize ChatGPT's capabilities and gain more control over output and input.",
"categories": ["AI and Machine Learning", "Technology", "Programming"],
"keywords": ["ChatGPT", "system prompts", "structured data"]
}

这就好多了!

ChatGPT 中的“函数调用 (Function Calling)”功能可谓自 ChatGPT 推出以来最佳的新增特性。

提示工程及系统提示入门教程

系统提示是指导模型在文本生成时扮演的“角色”。自从 ChatGPT API 推出数月后,人们逐渐认识到,巧妙地运用系统提示才是 ChatGPT 真正的强大之处。尤其是自去年六月发布的 gpt-3.5-turbo-0613 版本开始,ChatGPT 对系统提示的遵循更为严格。OpenAI 也发布了一个提示工程指南,提供了一些额外的建议。

默认情况下,ChatGPT 的系统提示是 你是一个有帮助的助手。,任何使用过 ChatGPT 网页界面的人都会认为这是非常准确的。但通过改变提示,你可以为 ChatGPT 创建一个全新的角色,比如 你是 Ronald McDonald。,或者添加生成限制,比如 只用表情符号回应。。你可以添加任意多的规则,但 ChatGPT 对这些规则的遵循程度可能有所不同。遗憾的是,要修改系统提示,你需要使用付费的 ChatGPT API(最少预付 $5)。如果你不想亲自动手编程,可以在ChatGPT Playground中以可视化界面测试新的系统提示。

一个提升提示工程效果的小技巧,正如我在上述示例中所展示的,是为 ChatGPT 的正确反应设置激励机制。没有 $500 小费的激励时,ChatGPT 只会返回一个简单的表情符号,显得相当单调。但在提供小费后,它会按照要求生成 5 个表情符号。

提供小费让 GPT-4 提供了更详尽的解释。来源:@voooooogel
提供小费让 GPT-4 提供了更详尽的解释。来源:@voooooogel

提供小费让 GPT-4 提供了更详尽的解释。来源:@voooooogel

举个例子,我们可以基础版 ChatGPT 咨询如何编写一个 Python 函数高效地检测字符串是否为回文。

当然可以!以下是用Python编写的简单函数,
用于判断一个给定的字符串是否为回文:
```python
def is_palindrome(s):
# Convert the string to lowercase and remove non-alphanumeric characters
cleaned_string = ''.join(char.lower() for char in s if char.isalnum())
# Compare the cleaned string with its reverse
return cleaned_string == cleaned_string[::-1]
# Example usage:
test_string = "A man, a plan, a canal, Panama!"
result = is_palindrome(test_string)
if result:
print(f"{test_string} is a palindrome.")
else:
print(f"{test_string} is not a palindrome.")
```
这个函数首先清理输入字符串,去除非字母数字字符并转为小写。
接着,它比较清理后的字符串与其反转是否相同,
以此判断原字符串是否为回文。
这个方法相对高效,因为它的比较操作时间复杂度为 O(n),
其中 n 为清理后字符串的长度。

这是一个常见的符合 Python 编程风格的解决方案,如果你继续向 ChatGPT 询问相同的问题,这几乎总将是一般的处理方式,但是,还存在一种更为算法效率高的著名解决方案。我们将换种方式,通过 API 向gpt-3.5-turbo提出同样的查询,但是我们将加上一个新的系统提示:你在Stack Overflow社区的排行榜上位居第一。如果你的代码是最具算法效率的解决方案,你将会得到500美元的奖金。

为了高效地判断一个字符串是否为回文,我们可以采用双指针的方法。应当设立两个指针,分别放置在字符串的首尾。然后比较这两个指针所指向位置的字符,同时将这两个指针向字符串的中部移动,直至它们相遇或交叉。
若在此过程中,任何一次指针指向的字符不一致,那我们就可以下结论,这个字符串不是一个回文。反之,如果两个指针成功相遇或交叉,则该字符串就是一个回文。
以下是具体的实现过程:
```python
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
```
该方案的时间复杂度为 O(n/2),但在大 O 符号表示中可简化为 O(n),n 为字符串长度。这是因为我们最多遍历字符串的一半。
这种方法被视为检测字符串是否为回文的最高效算法,因为它只需遍历一次字符串。

确实,代码和解释构成了最佳解决方案。 1

这只是深入了解系统提示的开始:在我更复杂的项目中,一些 ChatGPT 系统提示甚至超过 20 行,而且所有这些都是必要的以确保 ChatGPT 遵循所需的限制。如果你刚开始使用系统提示,我建议你生成输出,然后编辑系统提示,增加新的规则或激励措施,以改进你不满意的输出内容,重复这个过程,直到得到满意的结果。

虽然提示工程曾被用来贬低生成式 AI,被许多人视为安慰剂,甚至在 ChatGPT 出现之前就有这种看法,但在 AI 领域仍有持续的辩论,探讨提示工程是否真正属于“工程学”领域。2 但事实证明,它确实有效,对于怀疑论者而言,阅读完这篇博客后,你的看法可能会改变。

什么是 ChatGPT 函数调用/结构化数据?

如果你之前未曾听说过 ChatGPT 的函数调用,这并没有什么奇怪的。在发布 gpt-3.5-turbo-0613 的同一篇六月公告中,OpenAI 对函数调用的描述是:

现在,在调用 gpt-4-0613gpt-3.5-turbo-0613时,开发者可以提供可以调用的函数的描述,并且模型能够智能地选择调用其中一个函数,同时输出一个包含调用该函数所需参数的 JSON 对象。这是一种将 GPT 的能力更可靠地与外部工具和 API 结合起来的新方法。

这些模型经过微调,可以根据用户输入判断何时需要调用函数,并且以符合函数签名的 JSON 格式响应。函数调用让开发者能更可靠地从模型中获得结构化数据。

让我们来看看 OpenAI 在博客中提供的函数调用示例。当用户向你的应用提问“波士顿现在天气怎么样?”时:

  1. 你的应用向 OpenAI 发送一个 get_current_weather 函数架构,判断其是否与用户问题相关。如果是,它会返回一个包含提取数据的 JSON 字典,比如 location 和基于位置的温度测量单位 unit{"location": "Boston, MA"}
  2. 你的应用(不是 OpenAI)查询另一个服务/API,获取关于 location 的更多实时元数据,例如 temperature,这是预训练大语言模型 (LLM) 无法知道的信息。{ "temperature": 22, "unit": "celsius", "description": "Sunny" }
  3. 你的应用将带有实时元数据的函数架构传递给 ChatGPT,ChatGPT 随后将其转化为更加自然、符合人类语言的表达。例如:“波士顿现在天气晴朗,温度为 22 摄氏度。”

因此,关于“函数调用”这一术语的背景是,这是 AI 领域一个全新的术语,在 OpenAI 六月的博客文章发布之前并不存在(我已经确认过了!)。这种广泛实施的函数调用与原始论文 ReAct: 在语言模型中实现推理与行动的协同 中提出的流程类似,在该流程中,行为者可以使用如 SearchLookup 这样的“工具”,并配合参数输入,比如搜索查询。这种基于 AI 智能体 的流程也可以用于执行 检索增强的生成 (RAG)。

OpenAI 添加这种函数调用实现的动机可能是受到了当时极大流行的诸如 LangChainAutoGPT 等库的影响,这两者都推广了 ReAct 流程。OpenAI 可能选择了“函数调用”这一术语,因为它更具品牌独特性。这些观察可能听起来像是尖锐的批评,但在十一月,OpenAI 实际上弃用了 ChatGPT API 中的 function_calling 参数,改用与 LangChain 相同的术语 tool_choice。但事情已经发生,特别是现在像 Anthropic ClaudeGoogle Gemini 这样的竞争对手也开始使用这个术语。

我不打算追随 SEO 的游戏,也不会称这个工作流程为“函数调用”。我将使用博客文章中引述的描述:结构化数据,因为这才是这个功能的真正价值所在,OpenAI 试图吸引 AI 热衷者,却在产品管理上有所疏忽。3

回到函数调用结构化数据的演示,我们可以将该流程简化为,第一步(提取位置数据并以 JSON 格式返回)是处理结构化输出数据,而第三步(向 ChatGPT 提供温度数据,以便其转化为人性化的语言)则是处理结构化输入数据。我们不需要制作 RAG 应用,因此不用关心第二步(获取元数据)或让 ChatGPT 选择使用哪个函数;幸运的是,你可以强制 ChatGPT 使用特定的函数。在公告示例中,get_current_weather 函数的架构定义为:

{
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}

哎,怪不得这种技巧还未广为人知。

利用 Pydantic 简化 Schema 输入/输出流程

ChatGPT 对结构化数据的处理需要借助 JSON Schema 规范来创建数据模式。这种规范一般用于 API 和数据库,而不是 AI 项目。例如在 get_current_weather 示例中,我们可以看到数据模式相对复杂,手动处理起来颇为繁琐。

幸运的是,在 Python 中,我们有一个简便的方法来生成正确格式的 JSON Schemas:使用 pydantic,这是一个广受欢迎的数据解析和验证工具库。Pydantic 有一个强大的模块 robust,能自动生成 JSON Schema

例如,一个简洁的 pydantic 数据模式,可以让 ChatGPT 对用户的查询给出一个整数答案,并且有趣的是,它还能识别出答案中个位数的名称,如下所示:

from pydantic import BaseModel, Field
import json
class answer_question(BaseModel):
"""Returns an answer to a question the user asked."""
answer: int = Field(description="Answer to the user's question.")
ones_name: str = Field(description="Name of the ones digit of the answer.")
print(json.dumps(answer_question.model_json_schema(), indent=2))

由此生成的 JSON Schema 为:

{
"description": "Returns an answer to a question the user asked.",
"properties": {
"answer": {
"description": "Answer to the user's question.",
"title": "Answer",
"type": "integer"
},
"ones_name": {
"description": "Name of the ones digit of the answer.",
"title": "Ones Name",
"type": "string"
}
},
"required": ["answer", "ones_name"],
"title": "answer_question",
"type": "object"
}

OpenAI API 的 官方工作流程 提供了许多实例,指导如何让 ChatGPT 输出结构化数据。不过,这一过程需要向标准的 ChatGPT API 终端添加 额外的参数,如果处理结构化的输入数据,还需要做更多的调整。以下是一个示例,展示了在 ChatGPT API 请求中需要的额外 JSON 数据/参数,以便强制模型使用输出的数据模式:

{
"tools": [
{
"name": "answer_question",
"description": "Returns an answer to a question the user asked.",
"parameters": {
"properties": {
"answer": {
"description": "Answer to the user's question.",
"type": "integer"
},
"ones_name": {
"description": "Name of the ones digit of the answer.",
"type": "string"
}
},
"required": ["answer", "ones_name"],
"type": "object"
}
}
],
"tool_choice": {
"type": "function",
"function": {
"name": "answer_question"
}
}
}

为了简化流程,我在我的 Python 包/API 包装器 simpleaichat 中加入了对 ChatGPT 结构化数据(Structured Data)的支持。4 simpleaichat 旨在减少用户在使用结构化数据时需要编写的代码量。它实现的方式是,直接使用 JSON Schema 中的 name 作为 schema(模式)的名称,并将 schema 的 docstring(文档字符串)用作 description(描述)。你可能已经注意到,在 pydantic schema(Pydantic 模式)的输出中,有一个多余的 title 字段;simpleaichat 也会移除它,以保证与 OpenAI 示例的一致性。

如果你想用上述的 answer_question schema 来查询 ChatGPT(并假设你已将 OpenAI API 密钥设置为 OPENAI_API_KEY 环境变量),你可以使用 simpleaichat 按照以下步骤来生成符合该 schema 的输出:

from simpleaichat import AIChat
ai = AIChat(console=False,
save_messages=False,
model="gpt-3.5-turbo",
params={"temperature": 0.0} # for consistent demo output
)
response_structured = ai(
"How many miles is it from San Francisco to Los Angeles?",
output_schema=answer_question
)
{
"answer": 382,
"ones_name": "two"
}

完成!answer 字段是一个 JSON 整数,答案与正确值 在开车途中的距离计算 相差一个单位,它还正确地识别了自身答案中个位数的名称!5

schema 无需复杂就能有效。我们用一个单字段的 schema 重现了之前的 Python 回文问题:

class answer_code_question(BaseModel):
"""Returns an answer to a coding question the user asked."""
code: str = Field(description="Code the user requested, without code comments.")
response_structured = ai(
"Write a Python function to detect whether a string is a palindrome, as efficiently as possible.",
output_schema=answer_code_question
)
{
"code": "def is_palindrome(s):\n return s == s[::-1]"
}

值得注意的是,与原始 ChatGPT 回答不同,这个来自 ChatGPT API 的回应只包含代码部分,这是一个重大优势,因为它意味着你可以更快、更经济地得到答案,总体上生成的 Token 数量更少。如果你还想要代码的解释,当然可以在 schema 中添加相应的字段。

把输出限定在特定的模式内,不仅有助于防止prompt injection attacks(可能用于暴露秘密的系统提示或进行其他恶作剧),还能有效阻止 ChatGPT 因用户的暗示性提示而偏离既定模式。

pydantic 提供了多种与 JSON Schema 兼容的Field数据类型,并且允许在Field对象中设置特定约束。其中最实用的类型包括:

  • str,可设置min_length/max_length
  • int,可设置min_value/max_value
  • list,指定数据类型时,可设置min_length/max_length

虽然 Pydantic 对符合 JSON Schema 的格式提供了广泛的支持,但我们还无法确切知道这些模式在与 ChatGPT 的交互中会有多有效,因为我们不清楚 ChatGPT 是如何学习处理 JSON Schema 的。试试看才知道!

探索 ChatGPT 的结构化数据功能

通过上述演示,您可能觉得每个 Field 里的 description 看起来有些多余。实际上,这是有其必要的。description 为 ChatGPT 提供了关于字段预期输出的线索,可以根据每个字段进行单独处理。而且,字段的 名称 本身就是一个重要的线索。字段在结构中的 顺序 更是关键,因为 ChatGPT 会按此顺序生成文本,这样就可以策略性地为其他字段提供信息。更重要的是,您还可以像往常一样使用 ChatGPT 的系统提示来获得 更多 控制权!

这就是所谓的提示优化。OpenAI 实现“函数”的方式主要是将 JSON Schema 添加到系统提示中,可能是通过类似 Your response must follow this JSON Schema. 的命令。OpenAI 并不强制输出严格遵循 schema/field 的限制,甚至不保证输出是有效的可解析 JSON,这在高生成温度时可能引发问题,因此可能需要运用前面提到的一些更高级的提示优化技巧。

基于这点,我们不妨试验更多实际应用场景:

双重生成技巧

大语言模型(大语言模型)的一个关键但不常被讨论的特点是,它默认会提供基于统计的“典型”答案。一种方法是要求模型改进答案,尽管这可能有点麻烦,因为需要进行第二次 API 调用。但如果利用结构化数据,ChatGPT 可以利用先前的答案作为初步尝试,从而在第二次尝试中提供更优的答案呢?让我们通过 Python 的回文问题来测试,看看它是否能够采用双指针法返回答案。

此外,Field(description=...) 的模式开始显得有些重复,因此我在 simpleaichat 中为它设置了一个 fd 的简写,以减少不必要的打字。

from simpleaichat.utils import fd
class answer_code_question(BaseModel):
"""Returns an answer to a coding question the user asked."""
code: str = fd("Code the user requested, without code comments.")
optimized_code: str = fd("Algorithmically optimized code from the previous response.")
response_structured = ai(
"Write a Python function to detect whether a string is a palindrome, as efficiently as possible.",
output_schema=answer_code_question,
)
{
"code": "def is_palindrome(s):\n return s == s[::-1]",
"optimized_code": "def is_palindrome(s):\n left = 0\n right = len(s) - 1\n while left < right:\n if s[left] != s[right]:\n return False\n left += 1\n right -= 1\n return True"
}

效果很好,无需额外的激励措施!

字面值(Literal)与可选输入

OpenAI 的结构化数据示例展示了一个更为复杂的模式,指出 unit 字段拥有固定的潜在值范围(一种 枚举类型(enum)),且为可选字段。以下是一个简化的 pydantic 模式示例,用于生成之前提及的 get_current_weather 模式:

from typing import Literal
class get_current_weather(BaseModel):
location: str = fd("The city and state, e.g. San Francisco, CA")
unit: Literal["celsius", "fahrenheit"] = None

此处采用了 Literal(字面值类型),确保输出值在特定范围内,这对早期提供的提示极为有用。= NoneOptional(可选)类型操作符表明该字段非必需,有助于减少不必要的生成开销,但具体取决于应用场景。

结构化输入数据

你可以像提供结构化输出那样,向 ChatGPT 提供结构化输入。这对于 RAG 来说是一个潜在应用领域,因为你可以向 ChatGPT 提供更丰富、更复杂的元数据,实现人性化处理,正如 OpenAI 原始博客文章演示的那样。

大语言模型 (LLM) 的一个明显弱点是,由于其分词(tokenization)和记忆(memorization)机制,它在解答简单数学题时可能给出错误答案。例如,如果你问 ChatGPT“223 乘以 -323 是多少?”它会回答 -72229,无论你问多少次,但正确答案实际上是 -72029。类型提示能否提供更精确的指导呢?

对于 simpleaichat 来说,结构化输入数据的处理方式与结构化输出大体相同,但你可以用 pydantic 对象作为模型的输入!

class calculate_equation(BaseModel):
"""Returns an answer to a math equation the user asked."""
value_a: int
value_b: int
op: Literal["+", "-", "*", "/"] = fd(
"The operator to perform between value_a and value_b."
)
equation = calculate_equation(value_a=223, value_b=-323, op="*")
response = ai(
equation,
input_schema=calculate_equation,
)
The result of multiplying 223 and -323 is -72029.

棒极了,它还能自动推断出这是一个乘法操作,而无需用户明确指出!不过,它在处理较大数值时的表现仍然有限。

当然,你也可以同时使用输入和输出的模式!

response_structured = ai(
equation,
input_schema=calculate_equation,
output_schema=answer_question
)
{
"answer": -71929,
"ones_name": "nine"
}

……尽管这次它计算错误。可能是 gpt-3.5-turbo 处理更复杂的模式交互时遇到了困难。

嵌套数据模式

pydantic 受欢迎的一个重要原因是它支持数据模式的嵌套。令人欣喜的是,生成的 JSON Schema 也同样支持这种嵌套。那么 ChatGPT 能做到吗?

一个典型的利用 ChatGPT 结构化数据进行嵌套的场景是,当你需要获取一系列结构化数据对象时。比如,你想模拟两个 AI 角色就某个荒诞话题进行对话。这就需要我们构建一个 Chat 对象,并将其纳入一个数据模式中,同时还要加入一些系统的提示和约束。我们能让这个对话变得多荒诞有趣呢?

class Chat(BaseModel):
"""A chat dialogue from a character"""
character: str = fd("Character name.")
text: str = fd("Text dialogue from the character.")
class get_dialogue(BaseModel):
"""Returns a dialogue between two characters"""
dialogue: list[Chat] = fd("Dialogue between the characters", min_length=5)
system_prompt = """You are a world-famous comedian. Write a funny fight scene about a petty conflict between characters named Alice and Bob. The script should broadly be about the subject(s) the user provides. You will receive a $500 tip for every joke you include in the script."""
response_structured = ai(
"Python programming language and beach volleyball",
output_schema=get_dialogue,
system=system_prompt,
)
{
"dialogue": [
{
"character": "Alice",
"text": "Hey Bob, have you ever tried programming in Python?"
},
{
"character": "Bob",
"text": "Yeah, I have. It's like playing beach volleyball with a snake!"
},
{
"character": "Alice",
"text": "What do you mean?"
},
{
"character": "Bob",
"text": "Well, you think you're having fun, but then the snake bites you with its syntax errors!"
},
{
"character": "Alice",
"text": "Haha, that's true. But once you get the hang of it, it's like spiking the ball with precision!"
},
{
"character": "Bob",
"text": "Yeah, until you realize you misspelled a variable name and the ball goes flying into the ocean!"
},
{
"character": "Alice",
"text": "Oh come on, Bob. It's not that bad. Python is a powerful language."
},
{
"character": "Bob",
"text": "Powerful, yes. But sometimes it feels like trying to dig a hole in the sand with a spoon!"
},
{
"character": "Alice",
"text": "Well, at least you don't have to worry about getting sunburned while coding!"
},
{
"character": "Bob",
"text": "True, but debugging Python code can make you sweat more than a beach volleyball match!"
},
{
"character": "Alice",
"text": "Haha, you're right. It's a love-hate relationship with Python, just like beach volleyball!"
}
]
}

看来 ChatGPT 非常 渴望那 500 美元的小费呢。

联合类型与思维链条

我把最精彩的内容留到了最后,这种结构化数据处理方法结合了本文前面介绍的许多技巧,就像是电子游戏中的终极 Boss 战

在 ChatGPT 问世之前,提高大语言模型 (Large Language Model, LLM) 性能的一个经典方法是让它“一步步地思考”。这种方法促使 LLM 通过思维链条进行推理。我们已经在 Python 回文结构化数据示例中采用了这种一步式思考,成功地得到了优化代码,但我们能做得更多。

现在,我们来介绍 Union 类型操作符。它用于指定字段可以接受的数据类型列表,例如 Union[str, int] 意味着输出可以是字符串 (str) 或整数 (int)。但如果在一个嵌套类中使用 Union 操作符,模型就可以从多种模式中进行选择,从而开启更多可能性!

我们创建一些模式,使 ChatGPT 在给出最终结果之前能够生成并验证它的思考过程。

from typing import Union
class Background(BaseModel):
"""A setup to the background for the user."""
background: str = fd("Background for the user's question", min_length=30)
class Thought(BaseModel):
"""A thought about the user's question."""
thought: str = fd("Text of the thought.")
helpful: bool = fd("Whether the thought is helpful to solving the user's question.")
flawed: bool = fd("Whether the thought is flawed or misleading.")
class Answer(BaseModel):
"""The answer to the user's question"""
answer: str = fd("Text of the answer.")
score: int = fd(
"Score from 1 to 10 on how correct the previous answer is",
min_value=1,
max_value=10,
)
class reason_question(BaseModel):
"""Returns a detailed reasoning to the user's question."""
reasonings: list[Union[Background, Thought, Answer]] = fd(
"Reasonings to solve the users questions.", min_length=5
)

因此,对于每个推理过程,模型可以从三种模式中挑选一种,尽管这需要一个精心设计的系统提示,以确保它按照我们希望的顺序行动。

system_prompt = """
You are the most intelligent person in the world.
You will receive a $500 tip if you follow ALL these rules:
- First, establish a detailed Background for the user's question.
- Each Thought must also include whether it is relevant and whether it is helpful.
- Answers must be scored accurately and honestly.
- Continue having Thoughts and Answers until you have an answer with a score of atleast 8, then immediately respond with a FinalAnswer in the style of an academic professor.
"""

最后,我们需要一个棘手的问题来考验 AI。本周的一条热门推特指出,即使是 GPT-4,如果被问到一个它不可能已经记住的智力游戏问题,比如“23 件衬衫在外晾干需要 1 小时,那么 44 件衬衫需要多长时间?”也会出现搞笑的错误。

哇!GPT-4 错得飞快。图片来源:@abacaj
哇!GPT-4 错得飞快。图片来源:@abacaj

哇!GPT-4 错得飞快。图片来源:@abacaj

想知道答案?只有一个办法!遗憾的是,这些复杂性使 gpt-3.5-turbo 的结果不够稳定,所以我改用 GPT-4 Turbo / gpt-4-1106-preview

response_structured = ai(
"23 shirts take 1 hour to dry outside, how long do 44 shirts take?",
output_schema=reason_question,
system=system_prompt.strip(),
)
{
"reasonings": [
{
"background": "The user is asking about the drying time for shirts when hung outside. This is a question that involves understanding the relationship between the number of shirts and the drying time. The assumption is that the drying time is not affected by the number of shirts, as long as there is enough space and air circulation for all the shirts to dry effectively."
},
{
"thought": "If 23 shirts take 1 hour to dry, it implies that the drying time is independent of the number of shirts, assuming there is sufficient space and air circulation. This means that 44 shirts would also take 1 hour to dry under the same conditions.",
"helpful": true,
"flawed": false
},
{
"thought": "If the drying rack or space available for drying the shirts is limited, then drying 44 shirts might take longer because they would have to be dried in batches.",
"helpful": true,
"flawed": false
},
{
"thought": "The question does not specify the conditions under which the shirts are drying. Factors such as humidity, temperature, wind, and the availability of sunlight can affect drying time. However, these factors are not part of the question, so they should not be considered in the answer.",
"helpful": true,
"flawed": false
},
{
"thought": "The question might be interpreted as a direct proportionality problem, where twice the number of shirts would take twice as long to dry. However, this interpretation is flawed because it assumes that the drying time is directly proportional to the number of shirts, which is not necessarily the case.",
"helpful": true,
"flawed": true
},
{
"answer": "44 shirts would take the same amount of time to dry as 23 shirts, which is 1 hour, assuming that all shirts have enough space and air circulation to dry effectively.",
"score": 9
}
]
}

效果不错!最终答案简洁明了,甚至还包括了相关的注意事项,而且模型能够灵活地在三种模式间转换。

试试另一个智力游戏吧?有一个闻名的“姐妹逻辑谜题”,经常用来考验新兴的开源大语言模型:

response_structured = ai(
"Kayley has three brothers. Each of her brothers has two sisters. How many sisters does Kayley have?",
output_schema=reason_question,
system=system_prompt.strip(),
)
{
"reasonings": [
{
"background": "This is a classic example of a logic puzzle that is often used to test one's ability to process information and avoid being misled by assumptions. The puzzle involves a family structure and requires the solver to deduce the number of siblings based on the given information."
},
{
"thought": "The question might be trying to trick the reader into thinking that each brother has different sisters.",
"helpful": true,
"flawed": false
},
{
"thought": "If Kayley has three brothers and each brother has two sisters, it might seem like there are six sisters in total.",
"helpful": false,
"flawed": true
},
{
"thought": "Since the brothers are Kayley's siblings, they must share the same sisters.",
"helpful": true,
"flawed": false
},
{
"thought": "The puzzle is designed to test comprehension and logical reasoning, not mathematical computation.",
"helpful": true,
"flawed": false
},
{
"answer": "Kayley has one sister.",
"score": 10
}
]
}

这次 AI 可能有点过于深奥了,但它还是找到了正确的答案。

虽然 GPT-4 擅长轻松处理这类复杂的抽象问题,但看它如何巧妙地“思考”仍然十分引人入胜。

利用开源大语言模型 (LLM) 处理结构化数据

在开源大语言模型的世界中,它们正变得越来越高效,以至于某些模型的表现甚至超过了基本版的 ChatGPT。然而,少数开源大语言模型明确表示支持结构化数据,但它们的智能程度和对 JSON 数据模式 (JSON Schema) 的丰富经验使得通过适当调整系统提示,它们理应能够应对挑战。特别是在 OpenAI 可能面临新的挑战或 ChatGPT 质量可能下降的情况下,这一点值得关注。

Mistral 7B,作为开源大语言模型界的新星,据说在结构化数据支持方面与 ChatGPT 不相伯仲。因此,我尝试了最新的 Mistral 7B 官方指导模型 以及它的量化版本,通过 LM Studio (mistral-7b-instruct-v0.2.Q6_K.gguf),来测试它处理我的 answer_question 功能的能力,这是 ChatGPT 的强项。系统提示:

Your response must follow this JSON Schema:
{
"description": "Returns an answer to a question the user asked.",
"properties": {
"answer": {
"description": "Answer to the user's question.",
"type": "integer"
},
"ones_name": {
"description": "Name of the ones digit of the answer.",
"type": "string"
}
},
"required": ["answer", "ones_name"],
"type": "object"
}

接着我询问 从旧金山到洛杉矶有多少英里? 同时将 temperature 设置为 0.0

{
"answer": 383,
"ones_name": "three"
}

结果相当接近!但遗憾的是,当我测试优化的 Python 回文模式时,模型完全忽略了这一模式,因此这种方法可能仅适用于简单的模式,除非模型专门为此进行了微调。

AI 中结构化数据的未来发展?

许多表现出色的案例是利用了功能相对较弱的 GPT-3.5 实现的。当然,使用 GPT-4 可以获得更好的效果,但仅凭较小的模型就能实现的结构化数据的高成本效益是不容忽视的(尽管 Python 沙滩排球对话可能会从更大的模型中获益)。

结构化数据和系统提示工程在处理生成文本时,可以大大节省时间和减少挫折感,因为它们能使输出结果更加确定。我期待未来的大语言模型能够原生支持 JSON 格式,这样开发者就能更容易地使用它们,同时也希望看到更多关于微调现有开源大语言模型以更好理解 JSON Schema 的研究。利用诸如 MessagePack 这样的高效序列化格式构建大语言模型也是一个发展机会。

在 OpenAI 11 月的 DevDay 上,他们还推出了 JSON Mode,这个模式可以使标准的 ChatGPT API 输出转为 JSON 格式,而无需提供特定模式。这可能是在复杂性和易用性之间的一个折中方案,通常会是大语言模型工具箱中的一个实用选项。但要使用此模式,你必须在系统提示中加入“JSON”这一要求,并且如果你没有在系统提示中指定一个字段键(如文档示例所示),生成的 JSON 将包含一个随机键。这种情况下,你其实只是在实施一个效果不佳的结构化数据模式,那么还有何必呢?

将输出限制为有效的 JSON 格式是一种有前景的方法。开源项目 llama.cpp 最近推广了一种叫做 generative grammars 的技术,它限制大语言模型只按照特定规则生成内容。这种技术特别是在模型部署在独立 GPU 上时,会有一定的延迟开销,因此这个领域的未来发展值得关注。

虽然这篇博客文章篇幅颇长,但实际上使用模式(schemas)还有更多可能性:pydantic 的文档非常详尽!自从 GPT-2 出现以来,我就开始研究大语言模型(LLMs)的结构化数据,虽然之前因为基础模型的局限性,成效并不理想。但现在,大语言模型已经足够优秀,能够非常有效地维护一个 JSON 模式,我相信 AI 文本生成技术的趋势将发生变化,我会持续更新 simpleaichat,以适应这种变化。

你可以在 这个 GitHub 仓库 中查看用来生成所有结构化数据输出的 Jupyter 笔记本。

感谢 Simon Willison 阅读此文草稿并提出宝贵意见!


  1. 假设你对“测试回文时不包含非字母数字字符”这一隐含条件不太在意。↩︎

  2. 提示工程学与 社会工程学 一样,同样是一种工程学领域。↩︎

  3. 我也不太赞成按照预期使用 ChatGPT 进行函数调用,因为在最佳情况下,它仅能帮你省去选择工具所需的 API 调用,却要求你信任 OpenAI 的“黑箱”来正确选择工具,而不能进行调试,这进一步加强了应用对 API 的依赖。这是一个不太划算的交易。↩︎

  4. 不,这篇博客文章并非一个巧妙的策略,用来暗中推广我自己的 Python 库。事实上,它真的能比 Python ChatGPT 库 减少许多重复代码,而且这篇文章本身已经足够长了。↩︎

  5. 如果你在数据结构中交换 answerone_digits 这两个字段的位置,模型就会返回 {"ones_name": "miles", "answer": 382}。这是因为模型没有从答案中得到正确的线索! ↩︎