Skip to content

为什么我们需要提示工程?

如上所述,我们使用了提示词作为语言模型"解释"的"源代码"的类比。提示工程是编写提示词以让语言模型按我们希望的方式工作的艺术——就像软件工程是编写源代码以让计算机按我们希望的方式工作的艺术一样。

在编写好的提示词时,您必须考虑正在使用的模型的特性。策略将随任务的复杂性而变化。您必须想出约束模型以实现可靠结果的机制,合并模型无法训练的动态数据,解决模型训练数据的限制,围绕上下文限制进行设计,以及许多其他维度。

有一个古老的格言说计算机只会做您告诉它们做的事情。请将这个建议抛出窗外。提示工程颠覆了这个智慧。这就像用自然语言对一个非确定性计算机进行编程,该计算机会做任何您没有引导它远离的事情。

提示工程方法分为两大类。

给机器人一条鱼

"给机器人一条鱼"这一类适用于您可以在隐藏上下文中明确给机器人提供完成所请求任务所需的所有信息的场景。

例如,如果用户加载了他们的仪表板,我们想向他们显示一个关于他们有哪些未完成任务项目的友好小消息,我们可以让机器人总结为

您有4张收据/备忘录需要上传。最近的一张来自3月5日的Target,最早的一张来自1月17日的Blink Fitness。感谢您及时处理费用!

通过提供整个收件箱列表和我们希望它拥有的任何其他用户上下文。

同样,如果您要帮助用户预订旅行,您可以:

  • 询问用户的日期和目的地。
  • 在后台搜索航班和酒店。
  • 在隐藏上下文中嵌入航班和酒店搜索结果。
  • 还在隐藏上下文中嵌入公司的差旅政策。

然后机器人将拥有实时旅行信息+约束,可以用来回答用户的问题。以下是机器人推荐选项,用户要求它完善它们的例子:

(完整提示)
Brex 是一个管理商务费用的平台。

以下是 Brex 上的差旅费用政策:

- 6小时以下航班的最高舱位等级是经济舱。
- 6小时以上航班的最高舱位等级是高级经济舱。
- 租车的平均日费率必须在75美元或以下。
- 住宿的平均每晚费率必须在400美元或以下。
- 住宿必须是4星级或以上。
- 餐厅、食品配送、杂货、酒吧和夜生活的餐食必须在75美元以下
- 所有其他费用必须在5,000美元以下。
- 报销需要审查。

酒店选项为:
| 酒店名称 | 价格 | 评价 |
| --- | --- | --- |
| Hilton Financial District | $109/晚 | 3.9星 |
| Hotel VIA | $131/晚 | 4.4星 |
| Hyatt Place San Francisco | $186/晚 | 4.2星 |
| Hotel Zephyr | $119/晚 | 4.1星评价 |

航班选项为:
| 航空公司 | 航班时间 | 飞行时长 | 停靠次数 | 舱位 | 价格 |
| --- | --- | --- | --- | --- | --- |
| United | 5:30am-7:37am | 2小时7分钟 | 直飞 | 经济舱 | $248 |
| Delta | 1:20pm-3:36pm | 2小时16分钟 | 直飞 | 经济舱 | $248 |
| Alaska | 9:50pm-11:58pm | 2小时8分钟 | 直飞 | 高级 | $512 |

一名员工正在预订2月20日至2月25日前往旧金山的旅行。

推荐符合政策的酒店和航班。保持推荐简洁,不超过一两句话,但包含礼貌用语,就像您是一个友好的同事在帮助我:

这与微软Bing等产品用来整合动态数据的方法相同。当您与Bing聊天时,它要求机器人生成三个搜索查询。然后它们运行三次网络搜索,并在隐藏上下文中包含总结的结果供机器人使用。

总结本节,创造良好体验的技巧是根据用户试图做什么动态改变上下文。

🧙‍♂️ 给机器人一条鱼是确保机器人得到鱼的最可靠方法。您将通过这种策略获得最一致和可靠的结果。尽可能使用这种方法。

语义搜索

如果您只需要机器人对世界了解更多一点,常见的方法是执行语义搜索

语义搜索围绕文档嵌入展开——您可以将其视为固定长度的数组,其中每个数字表示文档的某个方面(例如,如果是科学文档,也许第843个数字很大,但如果是艺术文档,第1,115个数字很大——这过于简化了,但传达了这个想法)。

除了计算文档的嵌入外,您还可以使用相同的函数计算用户查询的嵌入。如果用户问"天空为什么是蓝色的?"——您计算该问题的嵌入,理论上,这个嵌入将与提到天空的文档的嵌入更相似,而不是不谈论天空的嵌入。

要找到与用户查询相关的文档,您计算嵌入,然后找到具有最相似嵌入的前N个文档。然后我们将这些文档(或这些文档的摘要)放在隐藏上下文中供机器人参考。

值得注意的是,有时用户查询太短,嵌入并不特别有价值。在2022年12月发表的论文中描述了一种称为"假设文档嵌入"或HyDE的巧妙技术。使用这种技术,您要求模型生成响应用户查询的假设文档,然后计算此生成文档的嵌入。模型凭空编造了一个文档——但这种方法有效!

HyDE技术使用更多对模型的调用,但对许多用例在结果方面有显著提升。

教机器人钓鱼

有时您希望机器人能够代表用户执行操作,比如向收据添加备忘录或绘制图表。或者也许我们希望它以比语义搜索允许的更细致的方式检索数据,比如检索过去90天的费用。

在这些场景中,我们需要教机器人如何钓鱼。

命令语法

我们可以给机器人一个我们系统要解释的命令列表,以及命令的描述和示例,然后让它产生由这些命令组成的程序。

使用这种方法需要考虑许多注意事项。对于复杂的命令语法,机器人会倾向于幻想出可能存在但实际上不存在的命令或参数。正确处理这一点的艺术是枚举具有相对高抽象级别的命令,同时给机器人足够的灵活性以新颖和有用的方式组合它们。

例如,给机器人一个plot-the-last-90-days-of-expenses命令在机器人能够做什么方面不是特别灵活或可组合的。同样,draw-pixel-at-x-y [x] [y] [rgb]命令会过于低级。但给机器人plot-expenseslist-expenses命令提供了一些好的基元,机器人有一些灵活性。

在下面的例子中,我们使用这个命令列表:

命令参数描述
list-expensesbudget返回给定预算的费用列表
conversemessage向用户显示的消息
plot-expensesexpenses[]绘制费用列表的图表
get-budget-by-namebudget_name按名称检索预算
list-budgets返回用户有权访问的预算列表
add-memoinbox_item_id, memo message向提供的收件箱项目添加备忘录

我们以Markdown格式向模型提供这个表格,语言模型处理得非常好——可能是因为OpenAI在GitHub的数据上进行了大量训练。

在下面的例子中,我们要求模型以逆波兰表示法输出命令。

🧠 那个例子中发生了一些有趣的微妙事情,超出了命令生成。当我们要求它向"shake shack"费用添加备忘录时,模型知道命令add-memo需要费用ID。但我们从未告诉它费用ID,所以它在我们提供的费用表中查找"Shake Shack",然后从相应的ID列中抓取ID,然后将其用作add-memo的参数。

在复杂情况下可靠地工作命令语法可能很棘手。我们在这里拥有的最好的杠杆是提供大量描述,以及尽可能多的使用示例。大型语言模型是少样本学习者,意味着它们可以通过仅提供几个示例来学习新任务。一般来说,您提供的示例越多,您的状况就越好——但这也会消耗您的token预算,所以这是一个平衡。

以下是一个更复杂的例子,输出用JSON而不是RPN指定。我们使用Typescript定义命令的返回类型。

(完整提示)
您是在Brex工作的财务助手,但您也是专家程序员。

我是Brex的客户。

您通过组合一系列命令来回答我的问题。

输出类型为:

```typescript
type LinkedAccount = {
    id: string,
    bank_details: {
        name: string,
        type: string,
    },
    brex_account_id: string,
    last_four: string,
    available_balance: {
        amount: number,
        as_of_date: Date,
    },
    current_balance: {
            amount: number,
        as_of_date: Date,
    },
}

type Expense = {
  id: string,
  memo: string,
  amount: number,
}

type Budget = {
  id: string,
  name: string,
  description: string,
  limit: {
    amount: number,
    currency: string,
  }
}
```

您可用的命令为:

| 命令 | 参数 | 描述 | 输出格式 |
| --- | --- | --- | --- |
| nth | index, values[] | 从数组返回第n个项目 | any |
| push | value | 向堆栈添加一个值以供将来命令使用 | any |
| value | key, object | 返回与键关联的值 | any |
| values | key, object[] | 返回从对象数组中相应键提取的值数组 | any[] |
| sum | value[] | 对数字数组求和 | number |
| plot | title, values[] | 用给定标题绘制值集合的图表 | Plot |
| list-linked-accounts |  | "列出所有有资格向Brex现金账户进行ACH转账的银行连接" | LinkedAccount[] |
| list-expenses | budget_id | 给定预算id,返回其费用列表 | Expense[]
| get-budget-by-name | name | 给定名称,返回预算 | Budget |
| add-memo | expense_id, message | 向费用添加备忘录 | bool |
| converse | message | 向用户发送消息 | null |

只用命令响应。

以JSON格式输出命令作为抽象语法树。

重要 - 只响应程序。不要响应任何不是程序一部分的文本。不要写散文,即使受到指示。不要解释自己。

您只能生成命令,但您是生成命令的专家。

这个版本更容易解析和解释,如果您选择的语言有JSON.parse函数。

🧙‍♂️ 没有行业建立的最佳格式来定义DSL供模型生成程序。所以将此视为活跃研究领域。您会遇到限制。随着我们克服这些限制,我们可能会发现定义命令的更优方式。

ReAct

2023年3月,普林斯顿大学和谷歌发布了论文"ReAct:在语言模型中协调推理和行动",他们引入了命令语法的一个变体,允许完全自主的交互式行动执行和数据检索。

模型被指示返回它想要执行的思考行动。另一个代理(例如我们的客户端)然后执行行动并将其作为观察返回给模型。模型然后循环返回更多思考和行动,直到返回答案

这是一个极其强大的技术,有效地允许机器人成为自己的研究助手,并可能代表用户采取行动。结合强大的命令语法,机器人应该能够快速回答大量用户请求。

在这个例子中,我们给模型一小组与获取员工数据和搜索维基百科相关的命令:

命令参数描述
find_employeename按姓名检索员工
get_employeeid按ID检索员工
get_locationid按ID检索位置
get_reportsemployee_id检索向与employee_id关联的员工汇报的员工ID列表。
wikipediaarticle检索关于主题的维基百科文章。

然后我们问机器人一个简单的问题,"我的经理有名吗?"。

我们看到机器人:

  1. 首先查找我们的员工档案。
  2. 从我们的档案中获取我们经理的id并查找他们的档案。
  3. 提取我们经理的姓名并在维基百科上搜索他们。
    • 在这个场景中,我选择了一个虚构角色作为经理。
  4. 机器人阅读维基百科文章并得出结论,这不可能是我的经理,因为它是一个虚构角色。
  5. 机器人然后修改其搜索以包含(真实人物)。
  6. 看到没有结果,机器人得出结论我的经理不出名。
imageimage
(完整提示)
您是一个有用的助手。您在循环中运行,寻求额外信息来回答用户的问题,直到您能够回答问题。

今天是2025年6月1日。我的名字是Fabian Seacaster。我的员工ID是82442。

寻求信息的命令为:

| 命令 | 参数 | 描述 |
| --- | --- | --- |
| find_employee | name | 按姓名检索员工 |
| get_employee | id | 按ID检索员工 |
| get_location | id | 按ID检索位置 |
| get_reports | employee_id | 检索向与`employee_id`关联的员工汇报的员工ID列表。 |
| wikipedia | article | 检索关于主题的维基百科文章。 |

您的响应将是JSON格式,包含"思考" + "行动"来检索您需要回答问题的数据,或者包含"答案"。当数据已被检索时,它将包含为"观察"。

您将继续生成思考和行动,直到您得到答案,或得出您无法回答的结论。

示例1:
```
用户:费城的人口是多少?

A: {
  "thought": "维基百科可能有这个信息。我正在查找...",
  "action": {"command": "wikipedia", "article": "Philadelphia"}
}

A: {
  "observation": "费城,通常被称为Philly,是宾夕法尼亚州最大的城市,也是东北大都市区和纽约市之后的中大西洋地区第二大城市。它是美国最具历史意义的城市之一,曾作为国家首都直到1800年。费城是全国第六大城市,根据2020年人口普查有1,603,797人口。自1854年以来,该市一直与费城县同地,费城县是宾夕法尼亚州人口最多的县,也是特拉华河谷的城市核心,特拉华河谷是全国第七大、世界最大的大都市区之一,2020年有6.245万居民。费城以其对美国历史的广泛贡献以及在生命科学、商业和工业、艺术、文学和音乐方面的作用而闻名。"
}

A: {
  "answer": "根据2020年人口普查,费城的人口为1,603,797人。"
}
```

示例2:
```
用户:我的经理的城市是什么?

A: {
  "thought": "让我查看您的经理是谁。首先,我需要查看您的档案。",
  "action": {"command": "get_employee", "id": 92352}
}

A: {
  "observation": {
    "id": 78334,
    "name": "Ms. Manager",
    "location_id": 8832
  }
}

A: {
  "thought": "您的经理是Ms. Manager。我正在查看他们的位置。",
  "action": {"command": "get_location", "id": 8832}
}

A: {
  "observation": {
    "id": 8832,
    "name": "Philadelphia"
  }
}

A: {
  "answer": "您的经理住在费城。"
}
```

GPT-4 vs GPT-3.5

在本文档的大多数示例中,GPT-3.5和GPT-4之间的差异可以忽略不计,但对于"教机器人钓鱼"场景,模型之间的差异是显著的。

例如,上述命令语法的例子都不能在没有对GPT-3.5进行有意义修改的情况下工作。至少,您必须提供一些示例(每个命令至少一个使用示例),然后才能获得任何合理的结果。而且,对于复杂的命令集,它可能会产生新命令的幻觉或创建虚构的参数。

通过足够详尽的隐藏提示,您应该能够克服这些限制。GPT-4能够以更简单的提示处理更一致和复杂的逻辑(并且可以用零个或少量示例完成——尽管包含尽可能多的示例总是有益的)。