程序员的提示工程实战手册

将 AI 编程助手转变为更可靠的开发伙伴

开发者们正日益依赖 AI 编程助手来加速日常工作流程。这些工具可以自动补全函数、建议错误修复,甚至能生成整个模块或最小可行产品(MVP)。然而,我们中许多人已经体会到,AI 输出的质量在很大程度上取决于你提供的提示词质量。换言之,提示工程已成为一项必备技能。一句措辞不当的请求可能只会得到无关或泛泛的答案,而一个精心设计的提示则能产出深思熟虑、准确甚至富有创意的代码解决方案。本文将从实践角度出发,探讨如何系统地为常见的开发任务构建有效的提示。

AI 结对程序员虽然强大,但并非无所不能——除了你告知或作为上下文包含的信息外,它们对你的具体项目或意图一无所知。你提供的信息越多,输出的效果就越好。我们将提炼出关键的提示模式、可复用的框架以及那些在开发者中引起共鸣且易于记忆的例子。你将看到好提示与坏提示的并排对比,以及它们得到的真实 AI 回应,并附有评论,以理解为何一个成功而另一个失败。这是一份快速入门的备忘单:

给 AI 代码助手的提示词技巧 - @addyosmani

技巧

提示词模板

目的

1. 角色提示 (Role Prompting)

"你是一位资深的 {某种语言} 开发者。请为了 {某个目标} 来审查这个函数。"

模拟专家级的代码审查、调试或重构,获得更高质量的建议。

2. 明确上下文 (Explicit Context Setup)

"问题是:{问题摘要}。代码如下。它本应 {预期行为},但现在却 {实际行为}。这是为什么?"

清晰地框定问题,避免 AI 给出泛泛而谈、流于表面的回答。

3. 输入/输出示例 (Input/Output Examples)

"当输入为 {某个输入} 时,这个函数应该返回 {预期输出}。你能编写或修复这段代码吗?"

通过具体示例来展示你的意图,引导 AI 智能体准确理解需求。

4. 迭代式链条 (Iterative Chaining)

"第一步,生成组件的骨架代码。下一步,我们来添加状态管理。最后,处理 API 调用。"

将复杂的大任务分解成连续的小步骤,避免因提示过于庞大或含糊而导致 AI 混乱。

5. 模拟调试 (Debug with Simulation)

"请逐行过一遍这个函数。每个变量的值是什么?代码最有可能在哪里出错?"

让 AI 智能体模拟代码的运行时行为,从而帮助你发现那些隐藏很深的 bug。

6. 功能蓝图 (Feature Blueprinting)

"我正在构建 {某个功能}。需求是:{几点核心需求}。技术栈是:{所用技术}。请搭建出初始组件的脚手架,并解释你的选择。"

借助 AI 主导的规划和脚手架能力,高效启动新功能的开发。

7. 重构指导 (Code Refactor Guidance)

"请重构这段代码以提升 {某个目标},例如 {可读性、性能、代码风格等}。请用注释来解释你做了哪些更改。"

确保 AI 的代码重构与你的核心目标对齐,而不是进行随意的、不必要的修改。

8. 寻求替代方案 (Ask for Alternatives)

"你能用函数式风格重写这段代码吗?如果用递归版本会是什么样子?"

探索多种不同的实现路径,开阔思路,并丰富你的技术工具箱。

9. 小黄鸭调试法 (Rubber Ducking)

"我是这样理解这个函数功能的:{你的解释}。我有什么遗漏吗?这个解释能暴露出什么 bug 吗?"

让 AI 扮演“小黄鸭”的角色,通过向它解释来挑战你自己的理解,并发现逻辑上的矛盾之处。

10. 约束锚定 (Constraint Anchoring)

"请避免使用 {例如:递归},并严格遵守 {例如:ES6 语法,不使用外部库}。请为 {例如:内存占用} 进行优化。函数如下:"

给 AI 设定明确的边界和限制,防止它“自由发挥”过度,或引入与项目不兼容的代码模式。

高效代码提示的基础

向 AI 编码工具提问,有点像与一个极其刻板、但有时知识渊博的合作者沟通。为了得到有用的结果,你需要清晰地设定场景,并引导 AI 明白你想要什么以及你希望它如何做

以下是贯穿本手册所有示例的基础原则:

  • 提供丰富的上下文。 始终假设 AI 对你的项目一无所知,除了你提供的信息。包括相关细节,如编程语言、框架和库,以及具体的函数或代码片段。如果出现错误,请提供确切的错误信息,并描述代码应该做什么。明确性上下文是区分模糊建议和精确、可操作解决方案的关键。在实践中,这意味着你的提示可能包含一个简短的背景介绍,例如:“我有一个使用 Express 和 Mongoose 的 Node.js 函数,它应该通过 ID 获取用户,但抛出了一个 TypeError。这是代码和错误信息……”。你提供的背景越多,AI 需要猜测的就越少。

  • 明确你的目标或问题。 模糊的查询导致模糊的答案。与其问“我的代码为什么不工作?”,不如精确指出你需要什么洞见。例如:“这个 JavaScript 函数返回了 undefined,而不是预期的结果。根据下面的代码,你能帮忙找出原因并修复吗?”这样的提问更有可能得到有用的回答。一个调试的提示公式是:“它预期会[预期行为],但在给定[示例输入]时,它却[当前行为]。错误在哪里?”。同样,如果你想要优化,请要求特定类型的优化(例如,“如何提高这个排序函数处理一万个项目时的运行时性能?”)。明确性引导 AI 的注意力。

  • 分解复杂任务。 在实现新功能或处理多步骤问题时,不要把整个问题塞进一个巨大的提示里。将工作分成更小的部分并进行迭代通常更有效。例如,“首先,为产品列表页面生成一个 React 组件的骨架。接下来,我们添加状态管理。然后,我们再集成 API 调用。” 每个提示都建立在前一个的基础上。通常不建议一次性要求完成一个大型功能;相反,从一个高层目标开始,然后迭代地要求每个部分。这种方法不仅使 AI 的回应保持专注和可管理,也模仿了人类逐步构建解决方案的方式。

  • 包含输入/输出示例或预期行为。 如果你能用一个例子来说明你想要什么,就这么做。例如,“给定数组 [3,1,4],这个函数应该返回 [1,3,4]。” 在提示中提供具体例子有助于 AI 理解你的意图并减少歧义。这类似于给一个初级开发者一个快速的测试用例——它澄清了需求。在提示工程术语中,这有时被称为“少样本提示(few-shot prompting)”,即你向 AI 展示一个模式让它遵循。即使只有一个正确行为的例子,也能显著地引导模型的响应。

  • 利用角色或身份。 在许多病毒式传播的提示示例中,一个强大的技巧是要求 AI“扮演”某个特定角色或身份。这可以影响答案的风格和深度。例如,“扮演一位资深的 React 开发者,审查我的代码中可能存在的错误”或“你是一位 JavaScript 性能专家。优化以下函数。” 通过设定一个角色,你让助手预先进入了相关的状态——无论是成为一个严格的代码审查员,一个帮助初级开发者的老师,还是一个寻找漏洞的安全分析师。社区分享的提示已经证明了这种方法的成功,例如*“扮演一个 JavaScript 错误处理器,帮我调试这个函数。API 调用的数据没有正确渲染。”*。在我们自己的使用中,我们仍然需要提供代码和问题细节,但角色扮演提示可以产生更结构化和专家级的指导。

  • 迭代并优化对话。 提示工程是一个互动过程,而非一蹴而就。开发者通常需要审查 AI 的第一个回答,然后提出后续问题或进行修正。如果解决方案不完全正确,你可能会说,“那个解决方案使用了递归,但我更喜欢迭代的方法——你能不用递归再试一次吗?” 或者,“很好,现在你能改进一下变量名并添加注释吗?” AI 会记住聊天会话的上下文,所以你可以逐步引导它达到期望的结果。关键在于将 AI 视为一个你可以指导的伙伴——追求进步而非第一次就完美

  • 保持代码的清晰和一致性。 这最后一个原则有点间接,但对于处理你代码上下文的工具来说非常重要。即使在 AI 介入之前,也要编写清晰、结构良好的代码和注释。有意义的函数和变量名、一致的格式以及文档字符串不仅使你的代码对人类来说更容易理解,也为 AI 提供了更强的线索来了解你在做什么。如果你展示了一种一致的模式或风格,AI 将会延续它。把这些工具当作极其专注的初级开发者——它们会从你的代码和注释中获取每一个线索。

有了这些基础原则,让我们深入到具体场景中。我们将从调试开始,这可能是最直接的用例:你有一段行为异常的代码,你希望 AI 帮助找出原因。

调试代码的提示模式

调试是 AI 助手的天然应用场景。这就像拥有一个不仅会倾听,还会用建议回应你的“小黄鸭”。然而,成功与否很大程度上取决于你如何向 AI 呈现问题。以下是如何系统地为寻求和修复错误提供帮助的提示方法:

1. 清晰描述问题和症状。 在提示的开头,描述哪里出了问题以及代码应该做什么。始终包含确切的错误信息或不正确的行为。例如,不要只说“我的代码不工作”,你可以这样提示:“我有一个 JavaScript 函数,它应该计算一个数字数组的总和,但它返回的是 NaN (Not a Number),而不是实际的总和。这是代码:[包含代码]。对于像 [1,2,3] 这样的数字数组,它应该输出一个数字(总和),但我得到了 NaN。这个错误的原因可能是什么?” 这个提示指明了语言、预期行为、观察到的错误输出,并提供了代码上下文——这些都是至关重要的信息。提供结构化的上下文(代码 + 错误 + 预期结果 + 你尝试过的方法)给了 AI 一个坚实的起点。相比之下,一个泛泛的问题如“我的函数为什么不工作?”只会得到微不足道的结果——模型在没有上下文的情况下只能提供最普遍的猜测。

2. 对棘手的错误使用分步或逐行的方法。 对于更复杂的逻辑错误(即没有明显错误信息抛出,但输出错误),你可以提示 AI 逐行执行代码。例如:“逐行执行这个函数,并跟踪每一步 total 变量的值。它没有正确累加——逻辑错在哪里了?” 这是一个小黄鸭调试法提示的例子——你实际上是要求 AI 模拟人类使用打印语句或调试器进行的调试过程。这类提示常常能揭示一些细微的问题,比如变量未重置或条件逻辑不正确,因为 AI 会详细说明每一步的状态。如果你怀疑代码的某个特定部分,可以深入追问:“解释一下这里的 filter 调用在做什么,以及它是否可能排除了比预期更多的项。” 让 AI 扮演解释者的角色,可以在解释过程中发现错误。

3. 尽可能提供最小可复现示例。 有时你的实际代码库很大,但错误可以在一小段代码中复现。如果你能提取或简化代码并仍然能复现问题,就这么做,并把它提供给 AI。这不仅让 AI 更容易集中注意力,也迫使你澄清问题(这本身就是一个有用的练习)。例如,如果你在一个深层嵌套的函数调用中遇到了 TypeError,试着用几行代码来复现它,并分享出来。目标是用最少的代码隔离错误,对错误原因做出假设,进行测试,然后迭代。你可以让 AI 参与进来,说:“这是一个仍然能触发错误的精简示例[包含代码片段]。为什么会出现这个错误?” 通过简化,你排除了噪音,帮助 AI 精准定位问题。(这项技术呼应了许多资深工程师的建议:如果你不能立即找到错误,就简化问题空间。如果你向 AI 展示一个更小的情景,它可以协助进行分析。)

4. 提出集中的问题和后续追问。 提供上下文后,直接询问你需要什么通常很有效,例如:“可能是什么原因导致了这个问题,我该如何修复?”。这会邀请 AI 同时进行诊断和提出解决方案。如果 AI 的第一个回答不清楚或只是部分有用,不要犹豫,提出后续问题。你可以说,“那个解释很有道理。你能给我展示一下如何修复代码吗?请提供修正后的代码。” 在聊天环境中,AI 有对话历史,所以它可以直接输出修改后的代码。如果你使用的是像 VS Code 中的 Copilot 或 Cursor 这样的内联工具而没有聊天功能,你可以在代码上方写一个注释,比如 // BUG: returns NaN, fix this function,然后看它如何自动补全——但总的来说,交互式聊天能提供更详尽的解释。另一个后续追问的模式是:如果 AI 给出了一个修复方案但你不理解为什么,可以问*“你能解释一下为什么那个改动解决了问题吗?”* 这样下次你就能学到东西,并且可以再次确认 AI 的推理是否合理。

现在,让我们用一个具体的例子来说明这些调试提示原则,展示一个糟糕的提示 vs. 改进的提示以及 AI 响应的差异:

调试示例:糟糕的提示 vs. 改进的提示

假设我们有一个简单的 Node.js 函数,旨在将用户对象列表转换为按用户 ID 查找的映射表。然而,它抛出了一个错误。这是有问题的代码:

// 有问题的函数:将用户数组转换为按 ID 索引的 map
function mapUsersById(users) {
  const userMap = {};
  for (let i = 0; i <= users.length; i++) {
    // 注意:使用 <= 会在最后一次迭代中越界
    const user = users[i];
    userMap[user.id] = user;
  }
  return userMap;
}

// 示例用法:
const result = mapUsersById([{ id: 1, name: "Alice" }]);

这段代码本应遍历 users 数组并构建一个以用户 ID 为键的对象。然而,存在一个错误:循环使用了 i <= users.length 而不是 i < users.length。这会导致在最后一次迭代中 users[i] 变为 undefined,很可能导致运行时错误(例如,尝试访问 undefinedid 属性)。让我们看看 AI 助手对关于这个问题的两个不同提示会如何回应。

糟糕的提示:“为什么我的 mapUsersById 函数不工作?”

AI 对糟糕提示的回应(模拟):

没有更多信息很难说。你确定函数是用一个合法的用户数组调用的吗?也许问题在于 users 是空的或者不是一个数组,导致循环没有正确执行。也可能是设置 userMap 属性时出错。请确保每个用户都有一个 id 属性。如果 usersundefinednull,也会导致它不工作。

分析: 这个回答相当模糊。提示除了函数名外没有提供任何细节,所以 AI 只能猜测一些常见原因(“也许数组是空的或不是数组”,“确保每个用户都有一个 id”)。它没有识别出真正的错误。当你提问不够具体时,你就会得到这种回应——模型只能复述一些通用的调试技巧。作为开发者,这没什么帮助,因为它没有解决真正的错误。提示中缺乏上下文和明确性导致了泛泛的回答(基本上相当于 AI 版的“你试过重启吗?”)。这与社区的建议一致,即模糊的提示会导致无关或泛泛的答案。

改进的提示:“我有一个 JavaScript 函数 mapUsersById,它应该将一个用户对象数组转换为一个以用户 ID 为键的 map(对象)。然而,当我运行它时,它抛出了一个错误。例如,当我传入 [ {id: 1, name: "Alice"} ] 时,我得到 TypeError: Cannot read property 'id' of undefined。这是函数代码:

function mapUsersById(users) {
  const userMap = {};
  for (let i = 0; i <= users.length; i++) {
    const user = users[i];
    userMap[user.id] = user;
  }
  return userMap;
}

它应该返回 { "1": {id: 1, name: "Alice"} }。错误是什么,我该如何修复它?”

AI 对改进提示的回应(模拟):

*错误 Cannot read property 'id' of undefined 表明在某个时刻,userundefined,然后你试图访问 user.id。查看你的循环,问题出在循环边界上。你使用了 i <= users.length。如果 users.length 是 1(数组中有一个用户),循环会以 i = 0i = 1 运行。当 i 是 1 时,users[1]undefined(因为数组中唯一的索引是 0)。因此,在那次迭代中 user 变为 undefined,而 user.id 抛出了一个 TypeError。修复方法是将循环条件改为 i < users.length,这样 i 的范围就是从 0 到 users.length-1。例如:

for (let i = 0; i < users.length; i++) {
  const user = users[i];
  userMap[user.id] = user;
}

这样,对于第一个(也是唯一一个)元素,i 将是 0,然后循环停止。做了这个改动后,你的函数将不再遇到 undefined 的用户,并且应该能按预期工作,对于给定的输入返回 {"1": {id: 1, name: "Alice"}}。*

分析: 这个回应直接指出了错误(在循环中使用了 <=)并解释了为什么这会导致 user 变为 undefined。然后,它提供了正确的代码解决方案(i < users.length)并确认了预期的结果。改进的提示之所以成功,是因为它遵循了我们的原则:它指明了语言(JavaScript),描述了函数的目的,包含了确切的错误信息和示例输入,甚至提供了相关的代码片段。注意 AI 如何利用错误信息作为线索,将注意力集中在循环边界上——一个有针对性的提示让 AI 能够进行真正的问题解决,有效地模拟了人类调试者的思考过程:“undefined 可能来自哪里?很可能是循环索引的问题”。这是一个具体的例子,展示了详细提示的好处。

额外的调试策略: 除了识别明显的错误,你还可以使用提示工程来进行更深层次的调试辅助:

  • 询问潜在原因。 如果你真的束手无策,可以稍微扩大问题范围:“在这段代码中,出现 TypeError: cannot read property 'foo' of undefined 的可能原因有哪些?” 并附上代码。模型可能会列出几种情况(例如,对象未初始化、竞态条件、变量作用域错误等)。这可以为你提供一些你未曾考虑过的调查角度。这就像和同事进行头脑风暴。

  • “向小黄鸭提问”——即,向 AI 解释你的代码。这听起来可能有些反直觉(为什么要向助手解释?),但写解释的过程可以澄清你自己的理解,然后你可以让 AI 来验证或批评它。例如:“我来解释一下这个函数在做什么:[你的解释]。基于此,我的推理正确吗?它是否揭示了错误所在?” AI 可能会在你的解释中发现一个指向实际错误的缺陷。这项技术将 AI 作为一个不仅会倾听还会回应的积极的小黄鸭来利用。

  • 让 AI 创建测试用例。 你可以问:“你能提供几个可能让这个函数崩溃的测试用例(输入)吗?” 助手可能会想出你没想到的边缘情况(空数组、极大数字、null 值等)。这对于调试和为未来的健壮性生成测试都很有用。

  • 角色扮演代码审查员。 作为直接“调试这个”提示的替代方案,你可以说:“扮演一个代码审查员。这是一段没有按预期工作的代码片段。请审查它并指出任何可能导致问题的错误或不良实践:[代码]”。这将 AI 置于一种批判模式。许多开发者发现,将请求表述为代码审查会得到非常详尽的分析,因为模型会评论代码的每个部分(并且通常在这样做时,它会发现错误)。事实上,一个提示工程技巧是明确要求 AI 表现得像一个细致的审查员。这不仅能发现手头的错误,还能发现其他问题(例如,可能缺少空值检查),这可能很有用。

总之,在使用 AI 助手进行调试时,细节和方向是你的朋友。提供场景、症状,然后提出有针对性的问题。正如我们上面看到的,一个含糊的“它不工作,帮帮我!”提示和一个精准的调试提示之间的差别是天壤之别。接下来,我们将转向另一个主要用例:重构和改进现有代码。

重构和优化的提示模式

重构代码——在不改变其功能的前提下,使其更清晰、更快或更符合语言习惯——是 AI 助手可以大放异彩的领域。它们接受了大量代码的训练,其中包括许多结构良好、经过优化的解决方案的例子。然而,要有效地利用这些知识,你的提示必须阐明“更好”对你的情况意味着什么。以下是如何为重构任务提出提示:

1. 明确陈述你的重构目标。 单独的“重构这段代码”太开放了。你是想提高可读性?降低复杂性?优化性能?还是使用不同的范式或库?AI 需要一个目标。一个好的提示会框定任务,例如:“重构以下函数以提高其可读性和可维护性(减少重复,使用更清晰的变量名)。” 或者 “优化这个算法的速度——它在处理大输入时太慢了。” 通过陈述具体目标,你帮助模型决定应用哪些转换。例如,告诉它你关心性能可能会让它使用更高效的排序算法或缓存,而专注于可读性则可能让它将一个函数分解成更小的函数或添加注释。如果你有多个目标,请一一列出。Strapi 指南中的一个提示模板甚至建议列举问题:“我想解决的问题:1) [性能问题], 2) [代码重复], 3) [过时的 API 使用]。”。这样,AI 就确切地知道要修复什么。记住,它不会天生就知道你认为代码中的问题是什么——你必须告诉它。

2. 提供必要的代码上下文。 在重构时,你通常会在提示中包含需要改进的代码片段。重要的是要包含你想要重构的完整函数或部分,有时如果相关的话,还包括一些周围的上下文(比如函数的使用方式或相关代码,这可能会影响你如何重构)。同时也要提及语言和框架,因为“符合语言习惯”的代码在不同的环境中是不同的,比如说,符合 Node.js 习惯的代码与符合 Deno 习惯的代码,或者 React 类组件与函数式组件。例如:“我有一个用类编写的 React 组件。请将其重构为使用 Hooks 的函数式组件。” AI 随后会应用典型的步骤(使用 useState、useEffect 等)。如果你只说“重构这个 React 组件”而没有阐明风格,AI 可能不知道你特指想要用 Hooks。

  • 如果相关,请包含版本或环境细节。 例如,“这是一个 Node.js v14 的代码库”“我们正在使用 ES6 模块”。这会影响 AI 是否使用某些语法(比如 import/export vs. require),这是正确重构的一部分。如果你想确保它不会引入不兼容的东西,请提及你的限制。

3. 鼓励在提供代码的同时附上解释。 从 AI 主导的重构中学习(并验证其正确性)的一个好方法是要求对更改进行解释。例如:“请建议一个重构版本的代码,并解释你所做的改进。” 这甚至被内置到了我们引用的提示模板中:“……建议重构后的代码并附上对你更改的解释。”。当 AI 提供解释时,你可以评估它是否理解了代码并达到了你的目标。解释可能会说:“我将两个相似的循环合并为一个以减少重复,并使用字典进行更快的查找,”等等。如果解释中有什么听起来不对劲,那就是仔细检查代码的警示信号。简而言之,利用 AI 的解释能力作为一种保障——这就像让 AI 对自己的重构进行代码审查。

4. 使用角色扮演来设定高标准。 如前所述,要求 AI 扮演代码审查员或高级工程师的角色可能非常有效。对于重构,你可能会说:“扮演一位经验丰富的 TypeScript 专家,重构这段代码以符合最佳实践和现代标准。” 这通常不仅会产生表面的更改,还会带来更有洞察力的改进,因为 AI 会努力达到“专家”的身份。一个来自提示指南的流行例子是让 AI 扮演导师的角色:“像一位经验丰富的 Python 开发者指导初级开发者一样。提供解释并编写文档字符串。重写代码以优化它。”。在这种情况下,结果是 AI 使用了更高效的数据结构(用集合去重)并为一个原本使用循环的函数提供了一行解决方案。角色扮演不仅帮助它重构,还帮助它解释了为什么新方法更好(在这种情况下,使用集合是众所周知的去重优化)。

现在,让我们通过一个重构的例子来看看提示如何影响结果。我们将使用一个 JavaScript (Node.js) 的场景,我们有一些不太理想的代码,并希望改进它。

重构示例:糟糕的提示 vs. 改进的提示

假设我们有一个函数,它进行两次数据库调用并做一些处理。它能工作,但不够美观——有重复的代码,而且难以阅读。我们希望为了清晰和效率而重构它。这是原始代码:

// 原始函数:获取两个列表并处理它们(需要重构)
async function getCombinedData(apiClient) {
  // 获取用户列表
  const usersResponse = await apiClient.fetch('/users');
  if (!usersResponse.ok) {
    throw new Error('Failed to fetch users');
  }
  const users = await usersResponse.json();
  // 获取订单列表
  const ordersResponse = await apiClient.fetch('/orders');
  if (!ordersResponse.ok) {
    throw new Error('Failed to fetch orders');
  }
  const orders = await ordersResponse.json();
  // 合并数据(将用户与订单匹配)
  const result = [];
  for (let user of users) {
    const userOrders = orders.filter(o => o.userId === user.id);
    result.push({ user, orders: userOrders });
  }
  return result;
}

这段代码中的问题:我们看到了用户和订单的重复获取逻辑(可以重构为一个辅助函数或循环),错误细节不足(只有一个通用的错误消息),而且它或许可以并行运行获取操作以提高速度。让我们看看两种不同的提示方法:

提示 1 (不具体):“重构上面的 getCombinedData 函数。”

AI 对提示 1 的回应:

*当然。这是 getCombinedData 的一个重构版本:

async function getCombinedData(apiClient) {
  // 并行获取用户和订单
  const [usersResponse, ordersResponse] = await Promise.all([
    apiClient.fetch('/users'),
    apiClient.fetch('/orders')
  ]);
  if (!usersResponse.ok || !ordersResponse.ok) {
    throw new Error('Failed to fetch data');

  }
  const [users, orders] = await Promise.all([
    usersResponse.json(),
    ordersResponse.json()
  ]);
  // 按 userId 映射订单以提高查找效率
  const ordersByUser = {};
  for (const order of orders) {
    if (!ordersByUser[order.userId]) {
      ordersByUser[order.userId] = [];
    }
    ordersByUser[order.userId].push(order);
  }
  // 合并数据
  return users.map(user => ({
    user,
    orders: ordersByUser[user.id] || []
  }));
}

我并行获取了用户和订单以提高性能。我还整合了错误处理,并使用了一个 map (ordersByUser) 来按用户分组订单,这使得合并步骤更清晰,也可能更快。*

分析: 这实际上是一个相当不错的重构,实现了几个改进:并行获取、为提高效率按用户分组订单、使用 .map 进行更清晰的合并并默认为空数组。AI 也解释了它做了什么。那么我们为什么将这个提示标记为不具体或“糟糕”呢?在这种情况下,AI 碰巧做出了好的选择,但它假设了很多,因为提示是开放式的。它假设性能应该被提高(并行请求,这是一个聪明的改变,但如果在这里并行不安全呢?),并且它将错误处理改为一个通用的消息“Failed to fetch data”,这在合并错误时失去了具体是哪个调用失败的信息。

使用一个泛泛的“重构这个”提示的风险在于,AI 可能会做出以意想不到的方式改变行为的更改。在这里,合并错误处理可能是可接受的,也可能不是,这取决于需求。助手还做了一个结构性改变(使用字典来分组订单)——这很好,可能更好,但如果我们有特定的限制(比如想要流式处理结果或保持低内存使用),我们没有告知它。所以虽然结果看起来不错,但 AI 对“重构”的自我解释与我们想要的相符,这有些幸运。这说明一个不具体的提示有时在简单的案例中可以得到好的结果,但对于复杂的代码,它可能会走向一个不希望的方向或错过你关心的某些改进。

提示 2 (目标导向):“重构上面的 getCombinedData 函数以消除重复代码并提高性能。具体来说:(1) 避免重复用户和订单的获取逻辑——也许使用一个辅助函数或将它们一起获取。(2) 如果可能,并行获取两个列表。(3) 保留每次获取的错误处理(我们想知道是哪个调用失败了)。(4) 改进数据合并,可能通过使用更高效的查找结构而不是嵌套循环。提供重构后的代码,并附上解释更改的注释。”

AI 对提示 2 的回应:

*当然!这是 getCombinedData 的一个改进版本,带有请求的重构:

async function getCombinedData(apiClient) {
  // 1. 并行获取用户和订单以提高性能
  const [usersResponse, ordersResponse] = await Promise.all([
    apiClient.fetch('/users'),
    apiClient.fetch('/orders')
  ]);

  // 2. 分别处理错误以知道是哪个调用失败了
  if (!usersResponse.ok) {
    throw new Error('Failed to fetch users');
  }
  if (!ordersResponse.ok) {
    throw new Error('Failed to fetch orders');
  }

  // 并行解析 JSON 响应
  const [users, orders] = await Promise.all([
    usersResponse.json(),
    ordersResponse.json()
  ]);

  // 3. 使用按 userId 的查找映射表以实现高效匹配
  const ordersByUser = orders.reduce((map, order) => {
    if (!map[order.userId]) {
      map[order.userId] = [];
    }
    map[order.userId].push(order);
    return map;
  }, {});

  // 4. 使用查找映射表合并数据
  return users.map(user => ({
    user,
    orders: ordersByUser[user.id] || []  // 如果用户没有订单,则为空数组
  }));
}

更改说明:

  • 并行获取: 我使用 Promise.all 来同时获取用户和订单(第 1 点)。

  • 保留单独的错误处理: 我保留了对 usersResponse.okordersResponse.ok 的单独检查,这样任何错误都能清楚地指出是哪个获取失败了(第 2 点)。

  • 并行 JSON 解析: 也对 response.json() 调用使用了 Promise.all,这是一个微小的改进。

  • 高效合并: 我没有使用嵌套循环,而是创建了一个 ordersByUser 映射表(第 3 点)。这在一个单次遍历中按 userId 分组订单。然后我遍历用户来构建结果,以 O(1) 的时间从映射表中拉取每个用户的订单(第 4 点)。这更高效、更简洁。*

分析: 这个回应直接解决了所有指定的目标。代码被重构得更清晰、更快,并且按要求保留了单独的错误消息。AI 的解释确认了我们列出的每一点,这表明它仔细遵循了提示的指示。这是一个很好的结果,因为我们作为提示者,定义了在这种情况下“重构”意味着什么。通过这样做,我们引导 AI 产生了一个符合我们需求的解决方案,而且几乎不需要来回沟通。如果 AI 忽略了其中一点(比如说它仍然合并了错误处理),我们可以很容易地再次提示:“看起来不错,但请确保用户和订单的错误消息保持不同。”——然而,在这种情况下,这没有必要,因为我们的提示很详尽。

这个例子展示了一个关键的教训:当你知道你想要改进什么时,把它说清楚。 AI 擅长遵循指令,但它不会读心术。一个宽泛的“让这个更好”可能对简单的事情有效,但对于不平凡的代码,通过列举“更好”对你意味着什么,你会得到最好的结果。这与社区的见解一致,即清晰、结构化的提示会产生显著改善的结果。

额外的重构技巧:

  • 分步重构: 如果代码非常大或者你有一个很长的更改列表,你可以一次只处理一个。例如,首先要求 AI“为了可读性重构”(专注于重命名、拆分函数),然后稍后“优化这个函数中的算法”。这可以防止一次性给模型太多指令而使其不知所措,并让你逐步验证每个更改。

  • 询问替代方法: 也许 AI 的第一次重构有效,但你对不同的角度感到好奇。你可以问,“你能用另一种方式重构它吗,也许使用函数式编程风格(例如,使用数组方法而不是循环)?” 或者 “用递归代替迭代方法怎么样,只是为了比较一下?” 这样,你就可以评估不同的解决方案。这就像和同事一起头脑风暴多个重构选项。

  • 结合重构与解释来学习模式: 我们已经提到了这一点,但值得强调——把 AI 当作一个学习工具。如果它用一种巧妙的方式重构了代码,研究输出和解释。你可能会发现一个你以前没用过的新 API 或技术(比如使用 reduce 来构建一个映射表)。这是要求解释的一个原因:它将一个答案变成了一个迷你教程,加深了你对最佳实践的理解。

  • 验证和测试: 在任何 AI 生成的重构之后,一定要运行你的测试或用示例输入尝试代码。AI 可能会无意中引入细微的错误,特别是如果提示没有指定一个重要的约束。例如,在我们的重构中,如果原始代码有意为日志记录而分离获取错误,但我们没有提到日志记录,那么合并后的错误可能就不那么有用了。我们的工作是在审查中发现这一点。AI 也可以通过编写测试来提供帮助——你可以问*“为重构后的函数生成几个单元测试”*,以确保它在预期输入上的行为与之前相同。

至此,我们已经涵盖了调试和重构——改进现有代码。合乎逻辑的下一步是使用 AI 辅助实现新功能或生成新代码。我们将探讨如何有效地为那种场景提出提示。

现代调试场景

React Hook 依赖问题

糟糕的提示: "我的 useEffect 不正常"

增强的提示:

// 我有一个 React 组件,它获取用户数据,但导致了无限次的重新渲染。这是我的代码:

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(setUser).finally(() => setLoading(false));
  }, [userId, setUser, setLoading]); // 问题在这里

  return loading ? <div>Loading...</div> : <div>{user?.name}</div>;
};

预期行为: 当 userId 改变时,应该只获取一次用户数据。 实际行为: 组件无限次地重新渲染。 控制台错误: "Warning: Maximum update depth exceeded"

是什么导致了这个无限循环,我该如何修复依赖数组?

为什么这样有效: 提供了确切的代码、错误信息、预期与实际行为的对比,并专注于一个通常被误解的特定 React 模式。

现代状态架构

糟糕的提示: "为我的 Next.js 电商应用构建状态管理"

增强的提示:

我正在构建一个 Next.js 14 的电商应用,需要设计状态管理架构。以下是我的需求:

组件:

  • 产品列表页 (需要: products[], filters, pagination)

  • 购物车 (需要: cart items, totals, shipping info)

  • 用户认证 (需要: user profile, auth status, preferences)

  • 实时通知 (需要: toast messages, error states)

技术限制:

  • Next.js 14,使用 App Router 和 Server Components

  • TypeScript 严格模式

  • 为 SEO 进行服务器端数据获取

  • 购物车/用户操作的客户端交互性

  • 状态应在导航之间持久化

我应该使用:

  1. 为每个领域(购物车、认证、通知)使用 Zustand stores

  2. React Query/TanStack Query 用于服务器状态 + Zustand 用于客户端状态

  3. 一个带有切片(slices)的单一 Zustand store

请提供一个推荐的架构,并附上代码示例,展示如何构建 stores 并与 Next.js App Router 模式集成。

为什么这样有效: 这是一个真实世界的场景,具有特定的技术栈、清晰的需求,并要求提供包含实现细节的架构指导。

实现新功能的提示模式

AI 代码助手最令人兴奋的用途之一是帮助你从头开始编写新代码或将新功能集成到现有代码库中。这可以是从为 React 组件生成样板代码到在 Express 应用中编写新的 API 端点。这里的挑战通常是这些任务是开放式的——实现一个功能有很多种方法。用于代码生成的提示工程旨在引导 AI 生成符合你需求和风格的代码。以下是实现这一目标的一些策略:

1. 从高层指令开始,然后逐步深入。 首先用通俗的语言概述你想要构建什么,可能将其分解成更小的任务(类似于我们之前关于分解复杂任务的建议)。例如,假设你想为一个现有的 Web 应用添加一个搜索栏功能。你可能首先提示:“概述一个计划,在我的 React 应用中添加一个搜索功能,该功能可以按名称过滤产品列表。产品是从一个 API 获取的。”

AI 可能会给你一个分步计划:“1. 添加一个用于搜索查询的输入字段。2. 添加状态来保存查询。3. 根据查询过滤产品列表。4. 确保搜索不区分大小写,等等。” 一旦你有了这个计划(你可以在 AI 的帮助下对其进行完善),你就可以用集中的提示来处理每个要点。

例如:“好的,实现第 1 步:创建一个 SearchBar 组件,带有一个可以更新 searchQuery 状态的输入框。” 之后,“实现第 3 步:给定 searchQuery 和一个产品数组,过滤产品(对名称进行不区分大小写的匹配)。” 通过分解功能,你确保了每个提示都是具体的,并且响应是可管理的。这也模仿了迭代开发——你可以在构建每个部分时对其进行测试。

2. 提供相关上下文或参考代码。 如果你要向现有项目添加功能,向 AI 展示该项目中类似功能是如何完成的会非常有帮助。例如,如果你已经有一个与你想要的组件相似的组件,你可以说:“这是一个现有的 UserList 组件(代码…)。现在创建一个类似的 ProductList 组件,但要包括一个搜索栏。”

AI 将会看到这些模式(也许你使用了某些库或风格约定)并应用它们。在你的提示中打开相关文件或引用它们,可以提供上下文,从而产生更具项目针对性和一致性的代码建议。另一个技巧是:如果你的项目使用特定的编码风格或架构(比如使用 Redux 进行状态管理或某个 CSS 框架),请提及它。“我们使用 Redux 进行状态管理——将搜索状态集成到 Redux store 中。”

一个训练有素的模型随后会生成与 Redux 模式等一致的代码。从本质上讲,你是在向 AI 传授你项目的环境,以便它能量身定制输出。一些助手甚至可以利用你的整个仓库作为上下文来借鉴;如果使用这些工具,请确保将其指向你仓库中类似的模块或文档。

  • 如果开始一个新项目但你有偏好的方法,你也可以提及:“我想用函数式编程风格来实现这个(没有外部状态,使用数组方法)。” 或者,“确保遵循 MVC 模式,将逻辑放在控制器中,而不是视图中。” 这些都是高级工程师可能会提醒初级工程师的细节,而在这里你就是告诉 AI 的高级工程师

3. 使用注释和 TODO 作为内联提示。 当直接在带有 Copilot 的 IDE 中工作时,一个有效的工作流程是编写一条描述你需要的下一块代码的注释,然后让 AI 自动补全它。例如,在 Node.js 后端,你可能会写:// TODO: 验证请求负载(确保提供了 name 和 email),然后开始下一行。Copilot 通常会捕捉到意图并生成执行该验证的代码块。这是因为你的注释实际上是一个自然语言提示。然而,如果 AI 误解了,要准备好编辑生成的代码——一如既往,验证其正确性。

4. 提供预期输入/输出或用法的示例。 与我们之前讨论的类似,如果你要求 AI 实现一个新函数,请包含一个关于它将如何被使用的快速示例或一个简单的测试用例。例如:“用 JavaScript 实现一个函数 formatPrice(amount),它接收一个数字(如 2.5)并返回一个格式化为美元的字符串(如 2.50)。例如,formatPrice(2.5) 应该返回 '2.50'。”

通过给出那个例子,你限制了 AI 生成与之一致的函数。没有这个例子,AI 可能会假设其他一些格式或货币。差异可能很微妙但很重要。另一个在 Web 上下文中的例子:“实现一个记录请求的 Express 中间件。例如,对 /users 的 GET 请求应该在控制台记录 ‘GET /users’。” 这清楚地表明了输出应该是什么样子。在提示中包含预期行为就像一个 AI 会尝试满足的测试。

5. 当结果不符合你的期望时,用更多细节或约束重写提示。 生成新功能的第一次尝试没有成功是很常见的。也许代码可以运行但不够地道,或者它遗漏了一个要求。不要感到沮丧,把 AI 当作一个提交了初稿的初级开发者——现在你需要给出反馈。例如,“这个解决方案能用,但我更希望你使用内置的数组 filter 方法而不是 for 循环。” 或者,“你能将生成的组件重构为使用 React Hooks 进行状态管理而不是类组件吗?我们的代码库都是函数式组件。” 你还可以添加新的约束:“另外,确保函数在 O(n) 或更好的时间内运行,因为 n 可能很大。” 这种迭代式提示非常强大。一个真实世界的场景:一个开发者要求一个大语言模型生成代码来用 JS canvas 库画一个冰淇淋甜筒,但它一直给出不相关的输出,直到他们用更多具体细节和上下文来完善提示。教训是,不要一次尝试就放弃。找出提示中缺少或被误解了什么,并加以澄清。这就是提示工程的精髓——每一次微调都能引导模型更接近你的设想。

让我们用一个示例场景来说明功能实现的提示:

功能实现示例:在 AI 辅助下构建 React 组件

假设我们有一个 React 应用,我们想添加一个新组件:一个带有搜索过滤器和排序下拉菜单的产品列表。这是一个 Web 应用中相当典型的功能。为简便起见,我们将重点放在这个组件内的搜索功能的提示上。

初始提示(功能请求): “创建一个名为 ProductList 的 React 函数式组件,用于显示产品列表,并包含一个文本输入框以按名称过滤产品。该组件应:从 /api/products 获取产品数组(假设它返回 {id, name, ...} 的 JSON 数组),将其存储在状态中,并允许用户在搜索框中键入以按名称过滤显示的产品(不区分大小写匹配)。在一个简单的 <ul> 中显示过滤后的列表,并显示产品名称。包含基本的错误处理(例如,如果 API 调用失败,则显示错误消息)和获取数据时的加载状态。”

这是一个很长的提示,但很具体。我们假设我们把这个交给一个聊天式的大语言模型。

AI 的回应(它可能产生的内容摘要): AI 可能会输出一个类似这样的 ProductList 组件:

import React, { useState, useEffect } from 'react';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [search, setSearch] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchProducts() {
      try {
        const res = await fetch('/api/products');
        if (!res.ok) throw new Error('Network response was not ok');
        const data = await res.json();
        setProducts(data);
      } catch (err) {
        console.error('Failed to fetch products:', err);
        setError('Failed to load products.');
      } finally {
        setLoading(false);
      }
    }
    fetchProducts();
  }, []);

  const handleSearchChange = (e) => {
    setSearch(e.target.value);
  };

  // 按搜索查询过滤产品(不区分大小写)
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(search.toLowerCase())
  );

  if (loading) return <div>Loading products...</div>;
  if (error) return <div>{error}</div>;

  return (
    <div className="product-list">
      <input
        type="text"
        placeholder="Search products"
        value={search}
        onChange={handleSearchChange}
      />
      <ul>
        {filteredProducts.map(prod => (
          <li key={prod.id}>{prod.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;

(如果我们要求的话,代码中可能还会有一些解释各部分的注释。)

现在,让我们分析这个结果并考虑提示的改进:

  • 假设返回了上面的代码,但我们的项目使用 context 或全局 store 来获取数据,而不是在组件内部获取。这是我们没有指明的一点。AI 做出了在 useEffect 中获取的选择。如果我们更喜欢使用自定义钩子或 context,我们应该对此有所提示。所以我们可以改进我们的提示:“实际上,在我们的应用中,我们使用一个自定义钩子 useProducts() 来处理数据获取。请将组件重构为使用 useProducts 钩子,而不是直接调用 fetch。” AI 随后会进行调整,可能会假设 useProducts 返回 { products, loading, error } 并相应地简化组件。

  • 另一个改进:也许我们意识到我们还想要一个排序下拉菜单(我们最初没有提到)。我们现在可以扩展对话:“很好,现在添加一个下拉菜单来按名称(A-Z 或 Z-A)对产品进行排序。下拉菜单应该让用户选择升序或降序,并且列表应该在过滤的基础上相应地排序。” 因为 AI 有现有代码的上下文,它可以插入一个排序状态并调整渲染。我们提供了一个清晰的新要求,它会尝试实现它,很可能会添加类似这样的东西:

const [sortOrder, setSortOrder] = useState('asc');
// ... 一个用于 sortOrder 的 select 输入框 ...
// 并在渲染前对 filteredProducts 进行排序:
const sortedProducts = [...filteredProducts].sort((a, b) => {
  if (sortOrder === 'asc') return a.name.localeCompare(b.name);
  else return b.name.localeCompare(a.name);
});
  • (以及下拉菜单的 UI)。

通过这样逐个功能地迭代,我们与 AI 模拟了一个开发周期。这远比试图一次性提示整个复杂组件的所有功能要有效得多。它减少了错误,并允许在需求变得更清晰时进行中途修正。

  • 如果 AI 犯了一个细微的错误(比如它忘记让搜索过滤器不区分大小写),我们只需指出:“让搜索不区分大小写。” 它会调整过滤器以使用小写比较(在我们的伪输出中它已经这样做了,但如果没有,它会修复它)。

这个例子表明,用 AI 实现功能完全是关于增量开发和提示优化。一条 Twitter 帖子可能会惊呼某人如何通过不断地向一个大语言模型提示每个部分来构建一个小应用——这本质上就是这种方法:构建、审查、优化、扩展。每个提示就像你开发过程中的一次提交。

实现功能的额外技巧:

  • 让 AI 搭建骨架,然后你填充具体内容: 有时让 AI 生成一个粗略的结构,然后你再进行微调会很有用。例如,“生成一个 Node.js Express 用户注册路由的骨架,包括验证和错误处理。” 它可能会产生一个带有占位符的通用路由。然后你可以填写特定于你应用的实际验证规则或数据库调用。AI 为你省去了编写样板代码的麻烦,而你则处理敏感的自定义逻辑。

  • 要求处理边缘情况: 在生成功能时,你可以提示 AI 思考边缘情况:“对于这个功能,我们应该考虑哪些边缘情况(你能在代码中处理它们吗)?” 例如,在搜索示例中,一个边缘情况可能是“如果用户输入时产品还没有加载怎么办?”(尽管我们的代码通过加载状态处理了这一点)或者“如果两个产品有相同的名称怎么办”(不是大问题,但也许可以提一下)。AI 可能会提到诸如空结果处理、非常大的列表(可能需要对搜索输入进行防抖处理)等。这是一种利用 AI 在常见陷阱上的训练经验的方法。

  • 文档驱动开发: 一些人采取了一种巧妙的方法,即先编写文档字符串或用法示例,然后让 AI 实现函数以匹配。例如:

/**
 * 返回第 n 个斐波那契数。
 * @param {number} n - 斐波那契数列中的位置(从 0 开始索引)。
 * @returns {number} 第 n 个斐波那契数。
 *
 * 示例: fibonacci(5) -> 5 (数列: 0,1,1,2,3,5,…)
 */
function fibonacci(n) {
  // ... 实现
}
  • 如果你编写了上面的注释和函数签名,一个大语言模型可能会正确地填充实现,因为注释确切地描述了要做什么,甚至给出了一个例子。这种技术确保你先用文字澄清功能(这通常是一个好习惯),然后 AI 以此为规范来编写代码。

在介绍了调试、重构和新代码生成的提示策略之后,让我们将注意力转向在编码提示工程中的一些常见陷阱和反模式。理解这些将帮助你避免在低效的互动中浪费时间,并在 AI 没有给你想要的东西时迅速调整。

常见的提示反模式及其避免方法

并非所有提示都是平等的。到目前为止,我们已经看到了许多有效提示的例子,但认识到反模式——那些导致 AI 响应不佳的常见错误——同样具有启发性。

以下是一些常见的提示失败案例及其修复方法:

  • 反模式:模糊的提示。 这是典型的*“它不工作,请修复它”“写一个做 X 的东西”而没有足够细节的例子。我们之前看到了一个例子,当问题“为什么我的函数不工作?”得到一个无用的回答时。模糊的提示迫使 AI 猜测上下文,通常导致泛泛的建议或不相关的代码。解决方法很简单:添加上下文和具体细节。如果你发现自己提出的问题得到的回答像是一个魔力八号球的响应(“你试过检查 X 吗?”),停下来,用更多细节(错误消息、代码摘录、预期与实际结果等)重新组织你的查询。一个好的做法是读一下你的提示,然后问自己,“这个问题能适用于几十种不同的场景吗?”* 如果是,那它就太模糊了。让它变得具体到只能适用于你的场景。

  • 反模式:超载的提示。 这是相反的问题:要求 AI 一次做太多的事情。例如,“生成一个完整的 Node.js 应用,包括身份验证、React 前端和部署脚本。” 或者在更小的规模上,“一次性修复这 5 个错误并添加这 3 个功能。” AI 可能会尝试,但你很可能会得到一个混乱或不完整的结果,或者它可能会忽略请求的某些部分。即使它处理了所有事情,响应也会很长,难以验证。补救措施是拆分任务。排定优先级:一次只做一件事,正如我们之前强调的。这使得捕捉错误更容易,并确保模型保持专注。如果你发现自己写的段落中包含多个“和”的指令,考虑将其分解为单独的提示或顺序步骤。

  • 反模式:缺失问题。 有时用户会提供大量信息,但从未清楚地提出问题或指明他们需要什么。例如,扔下一大段代码片段,只说“这是我的代码”。这会迷惑 AI——它不知道你想要什么。始终包含一个明确的要求,例如*“识别上述代码中的任何错误”“解释这段代码是做什么的”“完成代码中的 TODO。”* 一个提示应该有一个目的。如果你只提供文本而没有问题或指令,AI 可能会做出错误的假设(比如总结代码而不是修复它等)。确保 AI 知道你为什么给它看了一些代码。即使是简单的补充,如*“这段代码有什么问题?”“请继续实现这个函数。”*也能给它方向。

  • 反模式:模糊的成功标准。 这是一个微妙的问题——有时你可能会要求优化或改进,但你没有定义成功是什么样子。例如,“让这个函数更快。” 快是以什么标准衡量?如果 AI 不知道你的性能限制,它可能会对一些无关紧要的东西进行微优化,或者使用一种理论上更快但实际上可以忽略不计的方法。或者*“让这段代码更清晰”*——“更清晰”是主观的。我们通过明确说明目标来处理这个问题,比如“减少重复”或“改进变量名”等。解决方法是:量化或限定改进。例如,“优化这个函数,使其以线性时间运行(当前版本是二次的)”或“重构这个函数以移除全局变量并改用类”。基本上,要明确你通过重构或功能解决的是什么问题。如果你让它太开放,AI 可能会解决一个与你关心的问题不同的问题。

  • 反模式:忽略 AI 的澄清或输出。 有时 AI 可能会以一个澄清问题或一个假设来回应。例如:“你使用的是 React 类组件还是函数式组件?”或“我假设输入是一个字符串——请确认。” 如果你忽略这些,只是重申你的请求,你就错过了一个改进提示的机会。AI 在示意它需要更多信息。一定要回答它的问题或完善你的提示以包含这些细节。此外,如果 AI 的输出明显错误(比如它误解了问题),不要只是逐字重试相同的提示。花点时间调整你的措辞。也许你的提示中有个模糊的短语或遗漏了什么必要的东西。把它当作一次对话——如果一个人类误解了,你会用不同的方式解释;对 AI 也一样。

  • 反模式:风格多变或不一致。 如果你一直改变提问方式或在一次请求中混合不同的格式,模型可能会感到困惑。例如,在指令中在第一人称和第三人称之间切换,或者以一种令人困惑的方式混合伪代码和实际代码。尝试在单个提示中保持一致的风格。如果你提供示例,确保它们被清晰地划分(使用 Markdown 的三反引号表示代码,引号表示输入/输出示例等)。一致性有助于模型正确解析你的意图。另外,如果你有偏好的风格(比如,ES6 vs ES5 语法),要始终如一地提及它,否则模型可能会在一个提示中建议一种方式,在另一个提示中建议另一种。

  • 反模式:模糊的引用,如“上面的代码”。 在使用聊天时,如果你说“上面的函数”或“之前的输出”,要确保引用是清晰的。如果对话很长,你说“重构上面的代码”,AI 可能会跟丢或选择错误的代码片段进行重构。更安全的方法是再次引用代码或具体指明你想要重构的函数。模型有一个有限的注意力窗口,尽管许多大语言模型可以引用对话的前面部分,但再次给它明确的上下文可以帮助避免混淆。如果代码展示后已经过了一段时间(或几条消息),这一点尤其正确。

最后,这里有一个在事情出错时重写提示的战术方法

  • 识别 AI 响应中缺失或不正确的地方。 它解决的是另一个问题吗?它产生的错误或解决方案不适用吗?例如,也许你要求一个 TypeScript 的解决方案,但它给的是纯 JavaScript。或者它写了一个递归的解决方案,而你明确想要迭代的。找出差异所在。

  • 在一个新的提示中添加或强调那个要求。 你可以说,“解决方案应该是 TypeScript,而不是 JavaScript。请包含类型注解。” 或者,“我提到我想要一个迭代的解决方案——请避免递归,改用循环。” 有时在你的提示中字面上使用像*“注意:”“重要:”这样的短语来强调关键约束会很有帮助(模型没有情感,但它确实会认为某些措辞表示重要性)。例如:重要: 不要为此使用任何外部库。”* 或 注意: 代码必须在浏览器中运行,所以不要使用 Node 特定的 API。”

  • 如果需要,进一步分解请求。 如果 AI 在一个复杂的请求上屡次失败,试着先要求一个更小的部分。或者问一个可能有助于澄清情况的问题:“你明白我说的 X 是什么意思吗?” 模型可能会转述它认为你的意思,如果它错了,你可以纠正它。这是元提示——讨论提示本身——有时可以解决误解。

  • 如果对话卡住了,考虑重新开始。 有时在多次尝试后,对话可能会进入一个混乱的状态。开始一个新的会话(或暂时清除聊天历史)并用你根据之前失败经验制定的更精炼的请求从头开始提问会有所帮助。模型不介意重复,一个全新的上下文可以消除之前消息中累积的任何困惑。

通过意识到这些反模式及其解决方案,你将能更快地随时调整你的提示。对于开发者来说,提示工程在很大程度上是一个迭代的、反馈驱动的过程(就像任何编程任务一样!)。好消息是,你现在工具箱里已经有很多模式和例子可以借鉴了。

结论

提示工程既是一门艺术,也是一门科学——正如我们所见,它正迅速成为使用 AI 代码助手的开发者必备的技能。通过精心设计清晰、富含上下文的提示,你实际上是在 AI 你需要什么,就像你引导一个新的人类团队成员或向同事解释一个问题一样。在本文中,我们探讨了如何系统地为调试、重构和功能实现构建提示:

  • 我们学会了向 AI 提供与向同事求助时相同的信息:代码应该做什么、它如何行为异常、相关的代码片段等等——从而获得更有针对性的帮助。

  • 我们看到了与 AI 迭代的威力,无论是逐行遍历函数逻辑,还是通过多个提示优化解决方案(比如将递归方案改为迭代方案,然后改进变量名)。耐心和迭代将 AI 从一个一次性的代码生成器转变为真正的结对程序员。

  • 我们利用角色扮演和身份设定来提升响应的水平——将 AI 视为代码审查员、导师或某个技术栈的专家。这通常会产生更严谨、解释更丰富的输出,不仅解决了问题,还在过程中教育了我们。

  • 对于重构和优化,我们强调了定义“好”的标准(无论是更快、更清晰、更符合语言习惯等),而 AI 也表明,在引导下,它可以应用已知的最佳实践(如并行化调用、消除重复、正确处理错误)。这就像拥有了无数代码审查员的集体智慧——但你必须提出正确的问题才能挖掘出来。

  • 我们还演示了在 AI 辅助下逐步构建新功能,表明即使是复杂的任务也可以分解并一次一个提示地解决。如果提示得当,AI 可以搭建样板代码、建议实现方案,甚至高亮边缘情况——扮演一个随时待命、知识渊博的共同开发者。

  • 在此过程中,我们识别了要避免的陷阱:保持提示既不太模糊也不太超载,始终明确我们的意图和约束,并在 AI 输出不准确时随时准备调整。我们引用了糟糕提示的具体例子,并看到了微小的改变(如包含错误消息或预期输出)如何能极大地改善结果。

当你将这些技巧融入你的工作流程时,你可能会发现与 AI 的合作变得更加直观。你会培养出一种感觉,知道什么样的措辞能得到最好的结果,以及在模型偏离轨道时如何引导它。请记住,AI 是其训练数据的产物——它看过许多代码和解决问题的例子,但提供方向,指明哪些例子在当下是相关的,这正是的工作。本质上,你设定上下文,AI 跟进执行

同样值得注意的是,提示工程是一种不断发展的实践。 开发者社区在不断发现新的技巧——一个巧妙的一行提示或一个结构化模板可能会突然在社交媒体上走红,因为它解锁了人们之前没有意识到的能力。请关注这些讨论(在 Hacker News、Twitter 等平台上),因为它们可以激发你自己的技巧。但也不要害怕自己进行实验。把 AI 当作一个灵活的工具——如果你有一个想法(“如果我让它画一个我的架构的 ASCII 图会怎么样?”),就去试试。你可能会对结果感到惊讶,如果失败了,也没什么损失——你对模型的局限性或需求有了新的了解。

总而言之,提示工程使开发者能够从 AI 助手中获得更多。 这是 frustrating experience(“这个工具没用,它给了我一堆胡说八道”)和 productive one(“这感觉就像和一个为我写样板代码的专家结对编程”)之间的区别。通过应用我们所涵盖的策略手册——从提供详尽的上下文到微调 AI 的风格和思维——你可以将这些专注于代码的 AI 工具变成你开发工作流程的真正延伸。最终结果不仅是你编码更快,而且你常常会在这个过程中学到新的见解和模式(因为 AI 会解释事物或建议替代方案),从而提升你自己的技能水平。

作为最后的总结,请记住提示是一个迭代的对话。用你与另一位工程师沟通时所使用的同样清晰、耐心和彻底的态度来对待它。这样做,你会发现 AI 助手可以显著放大你的能力——帮助你更快地调试、更聪明地重构,并更轻松地实现功能。

祝你提示愉快,编码快乐!