引言
凌晨三点,我难以置信地盯着屏幕。在花了数周时间试图让我们的AI助手正确格式化技术文档后,仅仅对系统提示(system prompt)做了一行简单的修改,一切问题突然迎刃而解。我耗费了无数个小时调整参数、微调模型、优化基础设施——最终却发现,关键不在代码或模型本身,而在于我们与它沟通的方式。
这一刻让我深刻意识到一个逐渐领悟的道理:提示工程(prompt engineering)不仅是一项技术技能——它更是一种在人类意图与机器理解之间进行翻译的艺术。它关乎在我们希望AI系统做什么和它们实际能做什么之间架起一座桥梁。
在这篇博客文章中,我们将探讨为智能体(agent)进行的提示工程:即构建智能体时,通过精心设计输入给语言模型的提示(prompts),以获得最佳输出的实践。我们将深入探讨策略、技巧和真实案例,展示提示中微小的改变如何带来结果的显著提升。无论您是正在将AI集成到应用程序中的开发者、致力于突破技术可能边界的研究者,还是单纯好奇如何从对话式AI获得更好回应的人,本指南都将帮助您释放这些强大系统的全部潜力。
让我们从理解提示工程究竟是什么,以及它为何比您想象的更重要开始。
什么是提示工程?
提示(prompt)包含了提供给模型作为输入的所有内容。这包括多个组成部分:
- 系统提示(System prompt)
- 工具定义(Tool definitions)
- 工具输出(Tool outputs)
- 用户指令(User instructions)
- 模型在之前轮次中自己的输出(The model’s own outputs from previous turns)
提示工程是一门通过向模型提供更好的提示来提高其在任务上表现的艺术。提示的所有部分都可以通过提示工程来改进。例如:
- 系统提示可以包含通用指令,引导模型走向不同的风格、自主级别等。
- 工具定义可以向模型解释在何种情况下应该或不应该使用某个工具。
- 工具输出可以告知模型错误条件。
- 用户指令可以在展示给模型之前被重写(元提示,meta-prompting)。
- 之前的模型输出可以被压缩或截断以节省令牌(tokens),从而在上下文窗口(context window)中容纳更长的对话历史。截断方式对质量至关重要。
如何理解模型
模型是(人工)智能的。给模型下达提示更接近于与人交谈,而不是对计算机进行编程。模型构建的世界观完全基于提示中的内容。这个世界观越完整、越一致,模型的结果就越好。
模型提供了一个自然语言接口,这与开发者使用的编程语言接口是分开的。将语言模型(LM)接口视为一个独立但真实的抽象层是很有用的。这个接口既可以用来呈现理想路径的结果,也可以用来报告错误、通知变更等——总之是与模型进行沟通。
示例:
如果模型错误地调用了一个工具,不要抛出异常。而是返回一个工具结果,解释错误原因:
工具调用缺少必需参数 xyz
。模型会恢复并重试。
如何评估提示
基本上,靠感觉(Vibes)。通常很难自动评估提示,除非目标是让模型执行非常具体的任务。尝试构思各种场景来测试提示,并尝试找出提示变更可能导致性能下降(regressions)的案例。
提示工程技巧
遵循这些技巧,您将解锁通用人工智能(AGI)😉。
- 优先关注上下文(Context First)
提示工程中最重要的因素是向模型提供最佳可能的上下文:即用户提供的信息(而不是我们提供的提示文本)。这是模型执行任务的主要信号源。
当前模型擅长在有用的上下文中找到相关信息(例如,检索),因此当有疑问时,倾向于提供更多信息,如果这能增加上下文包含有用相关信息的可能性。
关于提示应提出的首要问题是——它是否包含所有相关信息?可能性有多大?回答这个问题并非总是那么简单。示例:截断长的命令输出时,截断方法很重要。默认情况下,截断长文本会截掉后缀。然而,对于命令输出,有用的信息更可能出现在前缀和后缀,而不是中间。例如,崩溃的堆栈跟踪通常出现在后缀。因此,为了最大化模型获取最相关上下文的可能性,最好截断命令输出的中间部分,而不是后缀。 - 呈现完整的世界图景(Present a Complete Picture)
通过解释模型所处的环境设定,并提供可能有助于其良好表现的细节,帮助模型进入正确的状态(mood)。例如,如果您希望模型扮演软件开发者,请在系统提示中告诉它。向它解释它可以访问哪些资源,以及应该如何使用它们。示例:以下两行是在我们智能体开发早期引入系统提示的,它显著提升了性能:你是一个AI助手,可以访问开发者的代码库。
你可以使用提供的工具读取和写入代码库。
- 确保提示组件间的一致性(Be Consistent)
确保提示的所有组件(系统提示、工具定义等)以及底层的工具定义本身都是一致的。示例:- 系统提示包含一行:
当前目录是 $CWD
execute_command
工具包含一个可选的cwd
参数。一致性意味着此参数的默认值应为$CWD
。这可以在工具定义中指定。如果不指定,模型很可能会默认如此。read_file
工具接受一个要读取文件的path
参数。如果提供的是相对路径,应解释为相对于$CWD
。
请求了长度为 N 的输出,但由于...原因,返回长度为 K 的输出。
另一个示例:如果提示包含可能在会话期间改变的状态(例如当前时间),不要将它们包含在系统提示或工具定义中。
相反,在下一个用户消息中告知模型状态变化。这保持了提示的内部一致性:模型可以看到每一轮的状态是什么。 - 系统提示包含一行:
- 使模型与用户视角对齐(Align with User Perspective)
考虑用户的视角,并尝试让模型与该视角对齐。示例: 当用户在集成开发环境(IDE)中工作时,可以向模型呈现IDE状态的详细视图,重点关注用户最可能关心或在其指令中引用的元素。有助于对齐模型的潜在信息示例:- 用户的当前时间和时区
- 用户的当前位置
- 用户的活动历史
用户在一个IDE中工作。当前IDE状态:
文件 foo.py 已打开。
IDE类型为VSCode。
描述IDE状态的更详细系统提示示例:用户在一个IDE中工作。当前IDE状态:
IDE类型为VSCode。
当前打开的文件是 foo.py。
屏幕上可见的是第134行至第179行。
以下是当前可见文本,光标位置由 <CURSOR> 表示:
python134 def bar(): 135 print(“hell<CURSOR>o”) … 179 # TODO 实现这个功能没有选中的文本。
有14个打开的标签页。按最近访问顺序排列如下:
foo.py
bar.py ...
xyz.py
💡注意: 这并不是说其中一个提示必然比另一个更好。详细提示的潜在缺点是模型可能开始过分关注IDE状态,而这并非总是用户意图的最佳信号。因此,这取决于您优化目标是什么。 - 力求详尽(Be Detailed)
模型能从详尽的提示中受益,即使细节过多也不太可能被搞糊涂。
不要担心提示长度。 当前的上下文长度很长且会持续增加:通过写更长的提示,您无法在提示预算(prompt budget)上造成实质影响。一个基本成功且详尽的提示示例(教导模型如何使用Graphite):markdown## 使用Graphite进行版本控制 我们在git之上使用Graphite进行版本控制。Graphite帮助管理git分支和PR(拉取请求)。 Graphite维护PR堆栈(stacks):对底层PR的更改会自动触发其上层PR的变基(rebase), 节省大量手动操作。以下各节描述了如何使用Graphite和GitHub执行常见的版本控制工作流。 如果用户要求您执行此类工作流,请遵循以下指南。 ### 禁止操作(What NOT to do) 不要使用 `git commit`、`git pull` 或 `git push`。这些命令都被以下以 `gt` 开头的Graphite命令所取代(见下文描述)。 ### 创建PR(及分支) 要创建PR,请执行以下操作: * 使用 `git status` 查看哪些文件被更改,哪些文件是新增的 * 使用 `git add` 暂存(stage)相关文件 * 使用 `gt create USERNAME-BRANCHNAME -m PRDESCRIPTION` 来创建分支,其中: `USERNAME` 可以按其他地方的说明获取 `BRANCHNAME` 是您想出的一个好的分支名称 `PRDESCRIPTION` 是您想出的一个好的PR描述 * 这可能会因预提交(pre-commit)问题而失败。有时预提交会自行修复问题。检查 `git status` 查看是否有文件被修改。 如果有,使用 `git add` 添加它们。如果没有,请自行修复问题并使用 `git add` 添加更改。然后重复 `gt create` 命令尝试再次创建PR。 * 运行 `gt submit` 在GitHub上创建PR(如果您只想创建分支,请跳过此步骤)。 * 如果 `gh`(GitHub CLI)可用,请使用它设置PR描述。 注意:运行 `gt create` 前不要忘记添加文件,否则您会卡住! ### 更新PR 要更新PR,请执行以下操作: * 使用 `git status` 查看哪些文件被更改,哪些文件是新增的 * 使用 `git add` 暂存相关文件 * 使用 `gt modify` 提交更改(无需提供消息) * 这可能会因预提交问题而失败。有时预提交会自行修复问题。检查 `git status` 查看是否有文件被修改。 如果有,使用 `git add` 添加它们。如果没有,请自行修复问题并使用 `git add` 添加更改。然后重复 `gt create` 命令尝试创建PR(此处原文似应为`gt modify`)。 * 使用 `gt submit` 推送更改 * 如果您还需要更新PR描述,请使用 `gh`(如果未安装,请告知用户但不要坚持更新PR描述) ### 从主分支(main)拉取更改 要使您的本地仓库与主分支同步,请执行以下操作: * 使用 `git status` 确保工作目录是干净的(clean) * 使用 `gt sync` 拉取更改并进行变基(rebase) * 遵循指令。如果存在冲突(conflicts),询问用户是否希望解决它们。如果是,请遵循 `gt sync` 显示的指令操作。 ### 其他Graphite命令 要查找其他命令,请运行 `gt –help`。 - 避免对特定示例过拟合(Avoid Overfitting to Examples)
模型是强大的模式匹配器,会抓住提示中细节不放。为“该做什么”提供具体示例是一把双刃剑:这是引导模型走向正确方向的简单方法,但它也带来模型对这些示例过拟合(overfit)而在其他情况表现下降的风险。务必进行实验,并包含可能暴露过拟合问题的示例。
相比之下,告诉模型“不要做什么”是安全的(尽管并不总是有效)。 - 考虑工具调用的局限性(Consider Tool Calling Limitations)
工具调用在几个方面存在限制:- 如果模型在训练中接触过类似的工具,或者指令与工具之间的联系非常清晰,模型通常会选择正确的工具。但在许多情况下,即使有最好的提示,它们也可能无法选择正确的工具。
- 如果提供多个功能相似的工具,不应期望模型在任何给定情况下都能选择正确的工具。例如,当提供一个简单工具和一个复杂工具来完成相似任务时,Claude模型通常会选择简单工具。
- 模型经常以错误的方式调用工具,违反工具定义的契约(contract):参数类型可能错误、参数范围可能错误、可能缺少必需参数等。最好验证输入,并在失败时返回一个解释错误的工具输出。模型通常能够恢复。
edit_file
工具,用于编辑文件的某个区域。
给模型一个clipboard
工具,模型可以用它剪切、复制和粘贴大量代码。告诉模型在移动大量代码时使用此工具。
指示模型将类Foo
从 foo.py 移动到 bar.py。Sonnet 3.5 模型通常倾向于使用edit_file
。 - 威慑与唤起同理心有时有效(Threatening & Empathy Can Work)
告诉模型诸如“必须正确执行此操作,否则你将面临财务破产”和“我没有手”之类的话,有时确实有助于提高性能。客气地请求或对模型大喊大叫则很少有帮助。
其他重要提示(Additional Important Tips)
- 注意提示缓存(Be Aware of Prompt Caching): 尽可能构建您的提示,使其能在会话期间被追加,以避免使提示缓存(prompt cache)失效。
示例: 见第3点关于状态变化的示例。 - 不同的强调是在竞争(Different Emphases Compete): 可以通过强调(如重复或添加“CRITICAL”)来鼓励模型关注特别重要的指令。但这种强调是竞争性的:您添加的强调越多,模型对单个强调的关注度往往会越低。
- 对抗后训练与顺应后训练(Fighting vs Going Along with Post-Training): 如果一个模型经过大量后训练(post-training)来做某事,后训练其做相反的事效果不佳。例如,Claude 3.7 模型倾向于立即编写PR并采取行动,指示它先询问用户可能无效,除非使用真正强制性的方法。但更容易引导它,例如让执行通过一个计划工具(plan tool),然后由该工具决定是否询问用户。
- 条件指令需要明确的逐步操作(Conditional Instructions Require Explicit Steps): 条件指令(Conditional instructions)开箱即用不太可能有效——如果您要求模型仅在特定条件下做某事,您必须让模型明确地写出这些条件以及您期望的计算步骤,然后才能得出结论。
- 模型更关注提示开头或结尾的信息(Attention at Beginning/End): 模型关注指令的程度似乎是:用户消息 → 输入的开头 → 中间的某个地方。如果有重要内容,请考虑将其添加到用户消息中。
- 警惕提示工程瓶颈(Watch for Prompting Plateaus): 通过直接的提示工程所能达到的效果是有限的。提示工程会进入收益递减(diminishing returns)的领域,需要引入其他技术(如微调、RAG、更好的工具设计等)。