Featured image of post 程序员的提示工程手册

程序员的提示工程手册

原文来自于 Addy Osmani,他是 Google Chrome 的工程主管,在 Google 工作多年。

开发人员越来越依赖 AI 编程助手来加速我们的日常工作流程。这些工具可以自动完成函数、建议修复错误,甚至生成整个模块或 MVP。然而,正如我们许多人所了解的那样,AI 输出的质量很大程度上取决于你提供的提示的质量。换句话说,提示工程已经成为一项必备技能。一个措辞不当的请求可能会产生不相关或笼统的答案,而一个精心设计的提示可以产生周到、准确甚至富有创造性的代码解决方案。本文将实际探讨如何系统地构建适用于常见开发任务的提示。

AI 结对程序员很强大但并非神奇——除了你告诉他们的或包含在上下文中的信息外,对你特定的项目或意图没有先验知识。你提供的信息越多,输出就越好。我们将提炼出开发者反响热烈的提示模式、可重复的框架和难忘的示例。你将看到好与坏提示的并排比较以及实际的 AI 响应,并附有评论以了解为什么一个成功而另一个失败。以下是入门小贴士:

技巧提示模板目的
1. 角色提示“你是一名资深的{语言名}开发者。请审查这个函数以实现{目标}。”模拟专家级的代码审查、调试或重构
2. 明确上下文设定“问题如下:{问题说明}。下面是代码。它本应做到{预期行为},但实际上做了 {实际行为}。为什么?”明确框定问题,避免泛泛而谈的回答
3. 输入/输出示例“这个函数在输入{输入内容}时应返回{预期输出}。你能编写或修复这段代码吗?”通过示例引导 AI 理解意图
4. 迭代分步“首先生成这个组件的骨架。接下来我们添加状态。然后处理 API 调用。”将大任务拆解为步骤,避免模糊或过载的提示
5. 模拟调试“逐行走查这个函数。变量值是什么?哪里可能出错?”引导 AI 模拟运行时行为,发现隐藏的 bug
6. 功能蓝图“我正在构建{功能特性}。需求如下:{具体说明}。技术栈为:{技术栈说明}。请搭建初始组件并解释你的选择。”启动 AI 主导的功能规划与脚手架搭建
7. 重构指导“请重构这段代码以提升{目标},比如(如:可读性、性能、符合语言习惯的风格)。请使用注释解释变更。”让 AI 的重构符合你的目标,而不是随意更动
8. 请求替代方案“你能用函数式风格重写这段代码吗?递归版本会是什么样?”探索多种实现方式,扩展工具箱
9. 橡皮鸭调试“我觉得这个函数的作用是:{你的解释}。我遗漏了什么吗?有没有 bug?”让 AI 质疑你的理解并发现潜在问题
10. 约束锚定“请避免(例如:递归),并遵守(例如:ES6 语法、无外部库)。优化方向为(例如:内存)。这是函数代码:”防止 AI 过度发挥或引入不兼容的模式

高效代码提示基础

向 AI 编码工具提示与与一个非常有原则、有时会知识渊博的合作者交流有点像。为了获得有用的结果,您需要清楚地设置场景,并指导 AI 您想要什么以及如何实现。

以下是本指南中所有示例的基础原则:

  • 提供丰富的上下文。始终假设 AI 除提供的信息外,对您的项目一无所知。请包括相关的详细信息,例如编程语言、框架和库,以及具体的功能或代码片段。如果出现错误,请提供确切的错误消息,并描述代码应该做什么。具体性和上下文是模糊建议和精确、可操作解决方案之间的区别。在实践中,这意味着您的提示可能包括一个简短的设置,例如:“我有一个使用 Express 和 Mongoose 的 Node.js 函数,应该通过 ID 获取用户,但它抛出了一个 TypeError。以下是代码和错误……”。您提供的设置提示词越多,AI 需要猜测的内容就越少。
  • 明确你的目标或问题。模糊的查询会导致模糊的答案。与其问“我的代码为什么不能工作?”不如精确指出你需要什么见解。例如:“这个 JavaScript 函数返回了未定义值而不是预期的结果。根据下面的代码,你能帮我找出原因以及如何修复吗?”这样的问题更有可能得到有帮助的答案。一个用于调试的提示公式是:“预期它会做[预期行为],但实际它给[示例输入]时却在做[当前行为]。错误在哪里?”类似地,如果你想要优化,请要求特定类型的优化(例如,“如何提高这个排序函数对 10k 个项目的运行时性能?”)。明确性引导 AI 的焦点。
  • 分解复杂任务。在实现新功能或处理多步骤问题时,不要将整个问题一次性输入一个巨大的提示词中。将工作分解成更小的块并迭代通常更有效。例如,“首先,为产品列表页面生成一个 React 组件骨架。接下来,我们将添加状态管理。然后,我们将集成 API 调用。”每个提示都建立在之前的基础上。通常不建议一次性请求一个完整的大型功能;相反,从一个高级目标开始,然后迭代地请求每一部分。这种方法不仅使 AI 的响应保持专注和可管理,而且也反映了人类逐步构建解决方案的方式。
  • 包含输入/输出的示例或预期行为。如果你能用示例来说明你想要什么,那就这样做。例如,“给定数组 [3,1,4],这个函数应该返回 [1,3,4]。”在提示中提供一个具体的示例有助于 AI 理解你的意图并减少歧义。这就像给初级开发人员一个快速测试用例——它阐明了需求。在提示工程术语中,这有时被称为“少样本提示”,其中你向 AI 展示一个要遵循的模式。即使一个正确的行为示例也能显著指导模型的响应。
  • 利用角色或身份。一种在许多病毒式提示示例中流行的强大技术是要求 AI“扮演”某个特定身份或角色。这可以影响答案的风格和深度。例如,“扮演一名资深 React 开发者,审查我的代码以查找潜在错误”或“你是一名 JavaScript 性能专家。优化以下函数。”通过设定一个角色,你引导助手采用相关的语气——无论是严格的代码审查员、初级开发者,还是寻找漏洞的安全分析师。社区共享的提示显示这种方法很成功,例如“扮演 JavaScript 错误处理程序,为我调试这个函数。从 API 调用中数据无法正确渲染。”在我们的实际使用中,我们仍然需要提供代码和问题细节,但角色扮演提示可以产生更结构化和专家级的指导。

  • 迭代并优化对话。提示工程是一个交互式过程,不是一次性的事情。开发者通常需要查看 AI 的第一个答案,然后提出后续问题或进行更正。如果解决方案不太对,你可能会说,“那个解决方案使用了递归,但我更喜欢迭代方法——你能再试一次不使用递归吗?”或者,“太好了,现在你能改进变量名并添加注释吗?”AI 会记住聊天会话中的上下文,所以你可以逐步引导它达到期望的结果。关键是要将 AI 视为一个你可以指导的伙伴——重在第一次尝试的进步而非完美。
  • 保持代码的清晰和一致性。最后一个原则有点间接,但非常重要,特别是对于在代码上下文中工作的工具。即使在 AI 介入之前,也要编写干净、结构良好的代码和注释。有意义的函数和变量名、一致的格式和文档字符串不仅使代码更易于人类理解,也为 AI 提供了更强的线索,了解你在做什么。如果你展示了一致的模式或风格,AI 会继续这样做。将这些工具视为极其专注的初级开发者——它们从你的代码和注释中获取每一个线索。

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

调试代码的提示词模式

调试非常适合 AI 助手。这就像有一个会说话的橡皮鸭,不仅会听,还会给出建议。然而,成功很大程度上取决于你如何向 AI 呈现问题。以下是系统地提示 AI 帮助查找和修复错误的方法:

  1. 清晰地描述问题和症状。开始你的提示词时,描述出错了什么以及代码应该做什么。始终包含确切的错误消息或不正确的行为。例如,不要只说“我的代码不工作”,你可以提示:“我有一个 JavaScript 函数,应该计算一个数字数组的和,但它返回的是 NaN(不是数字)而不是实际的和。以下是代码:[包含代码]。它应该对一个数字数组(如[1,2,3])输出一个数字(和),但我得到的是 NaN。这个 bug 可能是什么原因?”这个提示指定了语言、预期行为、观察到的错误输出,并提供了代码上下文——所有这些都是关键信息。提供一个结构化的上下文(代码+错误+预期结果+你尝试过的方法)为 AI 提供了一个坚实的基础。相比之下,一个泛泛的问题“我的函数为什么不工作?”只能得到微乎其微的结果——模型只能在缺乏上下文的情况下提供最一般的猜测。

  2. 对于棘手的错误,使用逐步或逐行的方法。对于更复杂的逻辑错误(没有抛出明显的错误消息,但输出不正确),您可以提示 AI 逐步执行代码。例如:“逐行遍历此函数并跟踪 total 在每个步骤中的值。它没有正确累加——逻辑错误出在哪里?”这是一个橡皮鸭调试提示的例子——本质上您是在要求 AI 模拟人类使用打印或调试器进行的调试过程。这种提示通常会揭示微妙的错误,例如变量未重置或不正确的条件逻辑,因为 AI 会逐步说明状态。如果您怀疑代码的某个部分,可以放大:“解释这里的过滤器调用是什么做的,它是否可能排除比应该更多的项。”让 AI 扮演解释角色可以在解释过程中揭示错误。

  3. 尽可能提供最小化的可复现示例。有时你的实际代码库很大,但问题可以由一小段代码演示。如果你能提取或简化仍然能复现问题的代码,那就这样做,并将其输入给 AI。这不仅能帮助 AI 集中注意力,还能迫使你澄清问题(这本身就是一个有用的练习)。例如,如果你在一个深层嵌套的函数调用中遇到 TypeError,尝试用几行可分享的代码来复现它。目标是使用最少的代码来隔离问题,做出关于错误的假设,进行测试,并迭代。你可以通过说:“这是一个简化后的示例,仍然会触发错误[包含代码片段]。为什么会出现这个错误?”来让 AI 参与其中。通过简化,你移除了噪音,帮助 AI 定位问题。(这种技巧反映了许多资深工程师的建议:如果你不能立即找到 bug,就简化问题空间。如果你向 AI 展示一个更小的案例,它可以帮助你进行分析。)

  4. 提出专注的问题和后续问题。在提供背景信息后,直接询问你需要什么通常很有效,例如:“这个问题可能是什么原因引起的,我该如何解决?”这会邀请 AI 进行诊断并提出解决方案。如果 AI 的第一个回答不清晰或部分有帮助,不要犹豫提出后续问题。你可以说,“这个解释有道理。你能展示如何修复代码吗?请提供修正后的代码。”在聊天环境中,AI 有对话历史,所以它可以直接输出修改后的代码。如果你使用的是 VS Code 中的 Copilot 或 Cursor 等内联工具,没有聊天功能,你可以在代码上方写一个注释,如// BUG: 返回 NaN,修复这个函数,看看它如何自动补全——但总的来说,交互式聊天能提供更全面的解释。另一个后续问题模式:如果 AI 提供了一个修复方案但你不知道为什么,可以问“你能解释为什么这个更改解决了问题吗?”这样你下次就能学到,并双重检查 AI 的推理是否合理。

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

调试示例:糟糕的提示与改进的提示词

想象我们有一个简单的 Node.js 函数,用于将用户对象列表转换为按用户 ID 查找的映射。但是,它正在抛出错误。以下是有问题的代码:

// Buggy function: converts array of users to a map by ID
function mapUsersById(users) {
  const userMap = {};
  for (let i = 0; i <= users.length; i++) {
    // Note: using <= will go out of bounds on the last iteration
    const user = users[i];
    userMap[user.id] = user;
  }
  return userMap;
}

// Example usage:
const result = mapUsersById([{ id: 1, name: "Alice" }]);

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

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

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

没有更多信息很难说。你确定函数被用了一个正确的用户数组调用吗?也许问题在于用户数组为空或者不是数组,导致循环无法正常执行。也可能是设置 userMap 属性时出现了错误。确保每个用户都有一个 id 属性。如果用户未定义或为 null,那也会导致无法工作。

分析:这个答案相当模糊。提示中除了函数名外没有提供任何详细信息,因此 AI 在猜测通用原因(“也许数组为空或不是数组”、“确保每个用户都有一个 id”)。它没有识别出实际错误。当问题不明确时,你会得到这种类型的回答——模型只能重复常见的调试技巧。作为一名开发者,这并不是很有帮助,因为它没有解决真正的错误。提示中缺乏上下文和具体信息会导致通用答案(本质上就是 AI 版本的“你试过关机再开机吗?”)。这与社区的建议一致,即模糊的提示会导致不相关或通用的答案。

✅ 改进后的提示:

我有一个名为 mapUsersById 的 JavaScript 函数,它应该将用户对象数组转换为以用户 ID 为键的映射(对象)。但是,当我运行它时,它会抛出错误。例如,当我传递 [{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;
}

It should return { "1": {id: 1, name: "Alice"} }. What is the bug and how can I fix it?”
它应该返回 { "1": {id: 1, name: "Alice"} }。有什么错误,我该如何修复?”

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

错误提示“Cannot read property ‘id’ of undefined”表明在某个时刻用户是未定义的,然后你尝试访问用户 id。查看你的循环,问题在于循环的边界。你使用的是 i <= users.length。如果 users.length 是 1(即数组中只有一个用户),循环会以 i = 0 和 i = 1 运行。当 i = 1 时,users[1] 是未定义的(因为数组中唯一的索引是 0)。因此在该迭代中用户变为未定义的,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,然后循环停止。有了这个更改,你的函数将不再匹配未定义的用户,并且应该按预期工作,对于给定的输入返回 {“1”: {id: 1, name: “Alice”}}。

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

附带调试技巧:除了识别明显的错误之外,您还可以使用提示工程进行更深入的调试辅助:

  • 询问潜在原因。如果你真的卡住了,可以把问题稍微扩展一下:“对于这段代码中的 TypeError: cannot read property ‘foo’ of undefined,有哪些可能的原因?”并附上代码。模型可能会列举几个场景(例如对象未初始化、竞争条件、变量作用域错误等)。这能给你提供一些你之前未曾考虑过的调查角度。这就像和同事一起头脑风暴。
  • “向橡皮鸭提问”——即向 AI 解释你的代码。这可能听起来有些反直觉(为什么向助手解释?),但编写解释的过程可以澄清你自己的理解,然后你可以让 AI 验证或批评它。例如:“我将解释这个函数的作用:[你的解释]。基于这一点,我的推理是否正确,并且是否揭示了错误所在?” AI 可能会指出你解释中的缺陷,从而指向实际错误。这种技巧利用 AI 作为一只积极的橡皮鸭,不仅倾听,还做出回应。
  • 让 AI 创建测试用例。你可以问:“你能提供一些可能破坏这个函数的测试用例(输入)吗?” 助手可能会想到你没有考虑到的边缘情况(空数组、极大的数字、空值等)。这对于调试和为未来的健壮性生成测试都很有用。
  • 扮演代码审查者。作为直接“调试这个”提示的替代方案,你可以这样说:“扮演代码审查者。这里有一段代码无法按预期工作。审查它并指出任何可能导致问题的错误或不良实践:[代码]”。这会使 AI 进入批判模式。许多开发者发现,将请求表述为代码审查会得到非常彻底的分析,因为模型会评论代码的每一部分(而且,在这样做的时候,它通常会发现错误)。事实上,一个提示工程技巧是明确要求 AI 表现得像一位一丝不苟的审查者。这不仅能揭示当前的问题,还能揭示其他问题(例如遗漏的潜在空值检查),这些可能很有用。

总之,在使用人工智能助手进行调试时,详细和明确的方向是你的好朋友。提供场景、症状,然后提出有针对性的问题。与“它不起作用,帮忙!”这样漫无目的的提示相比,手术刀般的调试提示差别是 night and day,就像上面看到的那样。接下来,我们将进入另一个主要用例:重构和改进现有代码。

重构和优化的提示词模式

重构代码——使其更简洁、更快或更符合习惯用法,而不改变其功能——是人工智能助手可以大显身手的地方。它们在大量代码上进行了训练,其中包括许多结构良好、优化过的解决方案的例子。然而,为了有效地利用这些知识,你的提示必须明确“更好”对你来说意味着什么。以下是用于重构任务的提示方法:

  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 与 require),这是正确重构的一部分。如果您想确保它不会引入不兼容的内容,请说明您的限制。

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

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

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

重构示例:糟糕与改进的提示词

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

// Original function: Fetches two lists and processes them (needs refactoring)
async function getCombinedData(apiClient) {
  // Fetch list of users
  const usersResponse = await apiClient.fetch("/users");
  if (!usersResponse.ok) {
    throw new Error("Failed to fetch users");
  }
  const users = await usersResponse.json();
  // Fetch list of orders
  const ordersResponse = await apiClient.fetch("/orders");
  if (!ordersResponse.ok) {
    throw new Error("Failed to fetch orders");
  }
  const orders = await ordersResponse.json();
  // Combine data (match user to orders)
  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) {
  // Fetch users and orders in parallel
  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(),
  ]);
  // Map orders by userId for efficient lookup
  const ordersByUser = {};
  for (const order of orders) {
    if (!ordersByUser[order.userId]) {
      ordersByUser[order.userId] = [];
    }
    ordersByUser[order.userId].push(order);
  }
  // Combine data
  return users.map((user) => ({
    user,
    orders: ordersByUser[user.id] || [],
  }));
}

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

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

使用通用“重构这个”提示的风险在于,AI 可能会做出改变,从而以意想不到的方式改变行为。在这里,结合错误处理可能可以接受,也可能不可以,这取决于需求。助手还进行了一次结构上的改变(使用字典来分组订单)——这很好,而且可能更好,但如果我们有特定的约束(比如想要流式传输结果或保持内存使用率低),我们没有通知它。所以虽然结果看起来很好,但 AI 自己对“重构”的解释与我们所期望的一致,这有点幸运。这表明,一个定义不明确的提示有时在简单情况下可以得到好的结果,但对于复杂的代码,它可能会朝着不期望的方向发展,或者错过你关心的某些改进。

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

AI 对提示 2 的响应:

好的!这是根据要求重构后的 getCombinedData 函数:

async function getCombinedData(apiClient) {
  // 1. Fetch users and orders in parallel to improve performance
  const [usersResponse, ordersResponse] = await Promise.all([
    apiClient.fetch("/users"),
    apiClient.fetch("/orders"),
  ]);

  // 2. Handle errors separately to know which call failed
  if (!usersResponse.ok) {
    throw new Error("Failed to fetch users");
  }
  if (!ordersResponse.ok) {
    throw new Error("Failed to fetch orders");
  }

  // Parse JSON responses in parallel
  const [users, orders] = await Promise.all([
    usersResponse.json(),
    ordersResponse.json(),
  ]);

  // 3. Use a lookup map for orders by userId for efficient matching
  const ordersByUser = orders.reduce((map, order) => {
    if (!map[order.userId]) {
      map[order.userId] = [];
    }
    map[order.userId].push(order);
    return map;
  }, {});

  // 4. Combine data using the lookup map
  return users.map((user) => ({
    user,
    orders: ordersByUser[user.id] || [], // empty array if no orders for user
  }));
}

更改说明: 并行获取:我使用 Promise.all 同时获取用户和订单(要点 1)。 保留单独的错误处理:我保留了针对 usersResponse.ok 和 ordersResponse.ok 的单独检查,以便任何错误都能清楚地指示哪个获取操作失败(要点 2)。 并行 JSON 解析:同样使用 Promise.all 进行 response.json() 调用,这是一个微小的改进。 高效组合:我创建了一个 ordersByUser 映射(要点 3),这可以在一次遍历中按 userId 对订单进行分组。然后我遍历用户来构建结果,从映射中以 O(1) 时间复杂度获取每个用户的订单(要点 4)。这更高效且简洁。

分析:这个响应直接回应了所有指定目标。代码被重构得更简洁、更快,并按照要求保持了独立的错误消息。AI 的解释确认了我们列出的每个点,这表明它仔细遵循了提示说明。这是一个很好的结果,因为我们作为提示者,在这个上下文中定义了“重构”的含义。这样做指导 AI 以最少的来回沟通产生符合我们需求的解决方案。如果 AI 遗漏了某个点(比如仍然合并了错误处理),我们可以轻松地再次提示:“看起来不错,但请确保用户和订单的错误消息保持独立。”——不过在这种情况下并不需要,因为我们的提示很全面。

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

额外的重构技巧:

  • 分步重构:如果代码非常大或者有很多修改项,可以一次处理一个。例如,首先让 AI“重构以提高可读性”(专注于重命名、拆分函数),然后稍后“优化这个函数的算法”。这可以防止一次向模型发出太多指令,并允许逐步验证每个更改。
  • 询问替代方法:也许 AI 的第一个重构效果很好,但你好奇不同的角度。你可以问,“你能用另一种方式重构它吗,比如使用函数式编程风格(例如,使用数组方法而不是循环)?”或者“这里用递归而不是迭代方法怎么样,只是为了比较?”这样,你可以评估不同的解决方案。这就像和同事一起头脑风暴多个重构选项一样。
  • 结合重构与解释来学习模式:我们之前提到过,但值得强调的是——将 AI 作为学习工具。如果它以巧妙的方式重构代码,研究输出和解释。你可能会发现一个新的 API 或技术(比如使用 reduce 来构建一个 map),这是你之前未曾使用过的。这就是要求解释的一个原因:它将一个答案变成一个迷你教程,强化你对最佳实践的理解。
  • 验证和测试:在 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]); // Problem is here

return loading ? <div>Loading...</div> : <div>{user?.name}</div>;
};
预期行为:当 userId 变化时,应该只获取一次用户数据 实际行为:组件无限重新渲染 错误信息:"警告:最大更新深度超出"
这是什么导致无限循环,我该如何解决依赖数组问题?

为什么这个有效:提供确切的代码、错误消息、预期与实际行为,并专注于一个在 React 中常见的易误解的模式。

现代状态架构

❌ 糟糕的提示:“为我构建 Next.js 电商应用的状态管理” ✅ 增强提示:

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

组件:
产品列表页面(需要:产品, 过滤器,分页)
购物车(需要:购物车商品,总计,运费信息)
用户认证(需要:用户资料、认证状态、偏好设置)
实时通知(需要:toast 消息,错误状态)

技术限制:
Next.js 14 带有 App Router 和服务器组件
TypeScript 严格模式
服务器端数据获取以利于 SEO
客户端交互以处理购物车/用户操作
状态应在导航后持续存在

我应该使用:
Zustand 为每个领域(购物车、认证、通知)存储
使用 React Query/TanStack Query 进行服务器状态管理,使用 Zustand 进行客户端状态管理
使用单个 Zustand 存储并使用切片

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

为什么有效:具有特定技术栈、明确需求,并要求提供架构指导及实现细节的真实场景。

实现新功能的提示词模式

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

  1. 从高层级指令开始,然后深入细节。首先用文字语言概述你想构建的内容,可能将其分解为更小的任务(类似于我们之前关于分解复杂任务的建议)。例如,如果你想为一个现有的 web app 添加一个搜索栏功能。你可能首先提示:“概述一个在 React app 中添加搜索功能的计划,该功能通过名称过滤产品列表。产品从 API 获取。” AI 可能会给你一个逐步的计划:“1. 为搜索查询添加一个输入字段。2. 添加状态来保存查询。3. 根据查询过滤产品列表。4. 确保它不区分大小写等。”一旦你有了这个计划(你可以借助 AI 的帮助来完善它),你就可以用专注的提示来处理每个要点。 例如:“好的,实现步骤 1:创建一个 SearchBar 组件,其输入框更新 searchQuery 状态。” 然后,“实现步骤 3:给定 searchQuery 和一个产品数组,过滤产品(名称不区分大小写匹配)。通过划分功能,您可以确保每个提示都是具体的,并且响应是可控的。这也反映了迭代开发——您可以测试构建的每个部分。

  2. 提供相关的上下文或参考代码。如果你正在为现有项目添加功能,展示 AI 在该项目中如何处理类似的事情会非常有帮助。例如,如果你已经有一个与你想要创建的组件相似的组件,你可以说:“这里有一个现有的 UserList 组件(代码……)。现在创建一个与它类似但包含搜索栏的 ProductList 组件。” AI 会识别模式(比如你使用某些库或风格约定),并应用它们。打开相关文件或在提示中引用它们可以提供上下文,从而产生更针对项目且一致的代码建议。另一个技巧:如果你的项目使用特定的编码风格或架构(比如 Redux 用于状态管理或某个特定的 CSS 框架),请提及这一点。“我们使用 Redux 进行状态管理——将搜索状态集成到 Redux 存储中。” 一个训练好的模型将随后生成符合 Redux 模式等的代码。本质上,你是在教 AI 关于你项目环境的信息,以便它能定制输出。一些助手甚至可以使用你的整个仓库作为上下文来参考;如果使用这些助手,请确保将其指向你仓库中类似的模块或文档。

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

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

  4. 提供预期输入/输出或使用示例。类似于我们之前讨论的,如果你要求 AI 实现一个新函数,请包含一个快速示例说明其如何使用或一个简单的测试用例。例如:“在 JavaScript 中实现一个名为 formatPrice 的函数,该函数接受一个数字(如 2.5)并返回一个以美元(如$2.50)格式化的字符串。例如,formatPrice(2.5)应该返回’$2.50’。”

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

  1. 当结果不符合预期时,用更详细的信息或约束条件重写提示。生成新功能的第一次尝试通常不会完美成功。也许代码能运行但不够规范,或者遗漏了某些要求。不要感到沮丧,把 AI 当作一个给出初稿的初级开发者——现在你需要给出反馈。例如,“解决方案是可行的,但我更希望你能使用内置的数组过滤方法而不是 for 循环。”或者,“你能重构生成的组件,使其使用 React Hooks 来管理状态而不是类组件吗?我们的代码库都是函数式组件。”你也可以添加新的约束条件:“此外,确保函数运行在 O(n)时间复杂度或更好,因为 n 可能会很大。”这种迭代式提示非常强大。一个真实的场景:一位开发者让 LLM 使用 JS 画布库生成绘制冰淇淋圆锥的代码,但它一直给出无关的输出,直到他们用更具体和详细的提示和上下文进行完善。教训是,不要在第一次尝试后就放弃。找出提示中缺失或被误解的部分,并加以澄清。 这是提示工程的精髓——每一次调整都能引导模型更接近你所设想的。

让我们用一个示例场景来说明特征实现提示:

功能实现示例:使用 AI 辅助构建 React 组件

假设我们有一个 React 应用程序,并希望添加一个新组件:一个带有搜索过滤和排序下拉菜单的产品列表。这是一个典型的 Web 应用程序功能。为了简洁起见,我们将专注于为此组件中的搜索功能进行提示。

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

这是一个很长的提示,但它很具体。让我们假设我们将其提供给聊天界面中的 LLM。

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);
  };

  // Filter products by search query (case-insensitive)
  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;

(如果需要,代码中可能还会包含一些注释来解释每个部分。)

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

  • 假设上面的代码返回了,但我们的项目使用上下文或全局存储来获取数据,而不是在组件内部获取。这是我们之前没有指定的。AI 做出了在 useEffect 中获取的选择。如果我们更喜欢使用自定义钩子或上下文,我们应该给出暗示。因此,我们可以改进提示:“实际上,在我们的应用中我们使用一个自定义钩子 useProducts(),它已经处理了获取。请重构组件以使用 useProducts 钩子而不是直接调用 fetch。”然后 AI 会调整,可能假设 useProducts 返回{ products, loading, error },并相应地简化组件。
  • 另一种改进:也许我们意识到我们还想要一个排序下拉菜单(这是我们最初没有提到的)。我们现在可以扩展对话:“太好了,现在添加一个下拉菜单来按名称对产品进行排序(A-Z 或 Z-A)。下拉菜单应该允许用户选择升序或降序,并且列表应根据过滤结果相应地排序。”由于 AI 具有现有代码的上下文,它可以插入一个排序状态并调整渲染。我们提供了一个清晰的新要求,它将尝试满足它,可能会添加类似的东西:
const [sortOrder, setSortOrder] = useState("asc");
// ... a select input for sortOrder ...
// and sort the filteredProducts before rendering:
const sortedProducts = [...filteredProducts].sort((a, b) => {
  if (sortOrder === "asc") return a.name.localeCompare(b.name);
  else return b.name.localeCompare(a.name);
});
  • (以及下拉界面)。 通过逐个迭代功能,我们模拟了与 AI 的开发周期。这比一开始就尝试一次性提示所有复杂的功能要有效得多。它减少了错误,并在需求变得清晰时允许中途修正。
  • 如果 AI 犯了一个微小的错误(比如它忘记使搜索过滤器不区分大小写),我们只需指出:“使搜索不区分大小写。”它将调整过滤器以使用小写比较(在我们的伪输出中它已经这样做了,如果没有,它会进行修正)。

这个例子表明,使用 AI 实现功能全在于渐进式开发和提示优化。一条 Twitter 线程可能会感叹某人通过不断向 LLM 提示每个部分来构建了一个小应用——这就是基本方法:构建、审查、优化、扩展。每个提示就像你开发过程中的一个提交。

额外的功能实现技巧:

  • 让 AI 搭建框架,然后你填充细节:有时候让 AI 生成一个大致的结构,然后你进行微调很有用。例如,“生成一个带有验证和错误处理的 Node.js Express 用户注册路由的骨架。”它可能会生成一个带有占位符的通用路由。然后你可以填充实际的应用特定的验证规则或数据库调用。AI 帮你避免了编写重复代码,如果你需要处理敏感逻辑,你负责处理自定义逻辑。

  • 询问边缘情况处理:在生成功能时,您可以提示 AI 考虑边缘情况:“对于此功能,我们应该考虑哪些边缘情况(并且能否在代码中处理它们)?”例如,在搜索示例中,一个边缘情况可能是“当用户输入时,产品尚未加载”(尽管我们的代码通过加载状态处理这种情况)或“如果两个产品有相同的名称”(这不是一个大问题,但也许可以提及)。AI 可能会提到空结果处理、非常大的列表(可能需要对搜索输入进行防抖处理)等。这是利用 AI 在常见陷阱方面的训练的一种方式。

  • 文档驱动开发:一些人有的一种巧妙的方法是先编写一个文档字符串或使用示例,然后让 AI 实现与之匹配的功能。例如:

    /**
    - Returns the nth Fibonacci number.
    - @param {number} n - The position in Fibonacci sequence (0-indexed).
    - @returns {number} The nth Fibonacci number.
    -
    - Example: fibonacci(5) -> 5 (sequence: 0,1,1,2,3,5,…)
      */
    function fibonacci(n) {
      // ... implementation
    }
    

    如果你写下上述注释和函数签名,一个 LLM 可能会正确地填写实现代码,因为注释准确地描述了该做什么,甚至给出了一个示例。这种技术确保你先用语言明确地阐明功能(这通常是一种好习惯),然后 AI 将其用作规范来编写代码。

目前我们已经涵盖了用于调试、重构和新代码生成的提示策略,让我们将注意力转向编码中提示工程的一些常见陷阱和反模式。了解这些将帮助你避免在不产生生产力的交互上浪费时间,并在 AI 没有给你需要的东西时快速调整。

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

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

这里有一些常见的提示失败及其解决方法:

  • 反模式:模糊的提示词。这是经典的“它不起作用,请修复它”或“写一些能做 X 的事情”但没有足够细节的情况。我们看到了一个这样的例子,当问题“我的函数为什么不能工作?”得到一个无用的回答时。模糊的提示迫使 AI 猜测上下文,并经常导致通用建议或不相关的代码。修复方法很简单:添加上下文和具体细节。如果你发现自己问了一个问题,而答案感觉像是一个魔法 8 球回应(“你试过检查 X 了吗?”),停下来,用更多细节重新表述你的问题(错误消息、代码片段、预期与实际结果等)。一个好方法是阅读你的提示词,并问自己,“这个问题是否适用于几十种不同的场景?”如果适用,它就太模糊了。把它变得足够具体,以至于它只能适用于你的场景。
  • 反模式:过载提示词。这是一个相反的问题:一次向 AI 请求做太多事情。例如,“生成一个完整的 Node.js 应用程序,包含身份验证、使用 React 的前端和部署脚本。”或者在小规模上,“一次性修复这 5 个错误,并添加这 3 个功能。”AI 可能会尝试这样做,但你很可能会得到一个混乱或不完整的結果,或者它可能会忽略请求的某些部分。即使它处理了所有内容,响应也会很长且更难验证。解决方法是拆分任务。优先级:一次做一件事,就像我们之前强调的那样。这使得更容易发现错误,并确保模型保持专注。如果你发现自己写了一个包含多个“和”的指令段落,考虑将其拆分为单独的提示或顺序步骤。
  • 反模式:缺少问题。有时用户会提供很多信息,但从未明确提出问题或说明他们需要什么。例如,粘贴一大段代码,然后只说“这是我的代码”。这可能会让 AI 感到困惑——它不知道你想要什么。始终包含一个明确的要求,例如“找出上述代码中的任何错误”、“解释这段代码的作用”或“完成代码中的 TODO”。提示应该有目的。如果你只是提供文本而没有问题或指令,AI 可能会做出错误的假设(例如,总结代码而不是修复它等)。确保 AI 知道你为什么展示它一些代码。即使是一个简单的补充,例如“这段代码有什么问题?”或“请继续实现这个函数”,也能给它方向。
  • 反模式:模糊的成功标准。这是一个微妙的问题——有时你可能要求优化或改进,但你没有定义成功看起来像什么。例如,“让这个函数更快。”更快用什么指标衡量?如果 AI 不知道你的性能限制,它可能会微优化一些无关紧要的东西,或者使用理论上更快但实际上可以忽略不计的方法。或者“让这段代码更干净”——“更干净”是主观的。我们通过明确陈述目标来处理这个问题,例如“减少重复”或“改进变量名”等。修复方法:量化或描述改进。例如,“优化这个函数以线性时间运行(当前版本是二次方)”或“重构这段代码以移除全局变量并使用类代替。”基本上,要明确说明你通过重构或功能解决了什么问题。如果你让它太开放,AI 可能会解决一个你不在乎的问题。
  • 反模式:忽视 AI 的澄清或输出。有时 AI 可能会用澄清问题或假设来回应。例如:“您在使用 React 类组件还是函数组件?”或“我假设输入是字符串——请确认。”如果您忽视这些并只是重申您的要求,您就错失了改进提示词的机会。AI 正在发出它需要更多信息信号。始终回答它的问题或改进您的提示词以包含这些详细信息。此外,如果 AI 的输出明显错误(比如它误解了问题),不要只是原封不动地重试相同的提示词。花点时间调整您的措辞。也许您的提示词中有模棱两可的短语或遗漏了重要的信息。把它当作对话——如果人类误解了,您会以不同的方式解释;对 AI 也这样做。
  • 反模式:风格多变或不一致。如果你不断改变提问方式或一次性混合不同格式,模型可能会感到困惑。例如,在指令中切换第一人称和第三人称,或以令人困惑的方式混合伪代码和实际代码。尽量在单个提示词中保持一致的风格。如果你提供示例,请确保它们被清晰地区分(使用 Markdown 三反引号表示代码,使用引号表示输入/输出示例等)。一致性有助于模型正确解析你的意图。此外,如果你有偏好的风格(比如 ES6 与 ES5 语法),请始终如一地提及它,否则模型可能会在一个提示词中建议一种方式,在另一个提示中建议另一种方式。
  • 反模式:模糊的引用,如“上面的代码”。在使用聊天时,如果你说“上面的函数”或“之前的输出”,请确保引用清晰。如果对话很长,你再说“重构上面的代码”,AI 可能会失去跟踪或选择错误的代码片段进行重构。更安全的方法是重新引用代码或具体命名你想重构的函数。模型有一个有限的注意力窗口,尽管许多 LLMs 可以引用对话的前面部分,但再次给它明确的上下文可以帮助避免混淆。如果代码展示后过了一段时间(或几条消息),这一点尤其正确。

最后,这里有一种在事情出错时重写提示词的策略:

  • 确定 AI 的回应中缺少或错误了什么。它解决的是不同的问题吗?它产生了一个错误或不合适的解决方案吗?例如,也许你要求用 TypeScript 编写解决方案,但它给出了普通的 JavaScript。或者它写了一个递归解决方案,而你明确要求的是迭代方案。找出差异。
  • 在新的提示中添加或强调该要求。你可以这样写,“解决方案应该是 TypeScript,而不是 JavaScript。请包含类型注解。”或者,“我提到我想一个迭代式的解决方案——请避免递归,而是使用循环。”有时候,在提示中直接使用“注意:”或“重要:”这样的短语来突出关键约束是有帮助的(模型没有情绪,但它确实会权衡某些措辞是否表示重要性)。例如:“重要:不要使用任何外部库。”或者“注意:代码必须在浏览器中运行,所以不要使用 Node 特定的 API。”
  • 如果需要,进一步分解请求。如果 AI 在处理复杂请求时反复失败,可以尝试先请求一个更小的部分。或者问一个可能阐明情况的问题:“你明白我所说的 X 是什么意思吗?”模型可能会复述它认为你的意思,如果它错了,你可以纠正它。这是一种元提示——讨论提示本身——有时可以解决误解。
  • 如果对话陷入僵局,考虑重新开始。在多次尝试后,对话可能会变得混乱。重新开始会话(或暂时清除聊天记录)并从你根据之前的失败制定的一个更精细的请求开始,这会有所帮助。模型不介意重复,新的上下文可以消除之前消息积累的任何混乱。

通过了解这些反模式及其解决方案,你将能够更快地在过程中调整你的提示词。对于开发者来说,提示工程是一个非常迭代、反馈驱动的过程(就像任何编程任务一样!)。好消息是,你现在有很多模式和示例可以借鉴。

总结

提示工程有点像艺术,也有点像科学——正如我们所见,它正迅速成为与 AI 代码助手合作的开发人员必备技能。通过编写清晰、内容丰富的提示,你实际上是在教 AI 你需要什么,就像你入职一个人类团队成员或向同伴解释一个问题时一样。在本文中,我们探讨了如何系统地处理用于调试、重构和功能实现的提示:

  • 我们学会了在请求帮助时向 AI 提供与向同事提供相同的信息:代码应该做什么,它如何表现不佳,相关的代码片段等等——从而获得更有针对性的帮助。
  • 我们看到了与 AI 迭代的力量,无论是逐行调试函数的逻辑,还是通过多次提示来优化解决方案(比如将递归解决方案转换为迭代方案,然后改进变量名)。耐心和迭代让 AI 成为一个真正的配对编程伙伴,而不是一次性代码生成器。
  • 我们利用角色扮演和角色设定来提升 AI 的响应质量——将 AI 视为一个代码审查者、导师或某个特定技术栈的专家。这通常会产生更严格、解释更丰富的输出,不仅解决问题,还能在这个过程中教育我们。
  • 在重构和优化方面,我们强调定义“好”的标准(无论是更快、更简洁、更符合习惯用法等),并且 AI 在引导下展示了它能够应用已知最佳实践(比如并行化调用、消除重复、正确处理错误)。这就像拥有无数代码审查者的集体智慧——但你必须问正确的问题才能利用它。
  • 我们也演示了在 AI 协助下逐步构建新功能,展示了即使是复杂任务也可以分解并一次一个提示来处理。AI 可以搭建基础代码框架,提出实现建议,甚至在提示时突出边缘情况——充当一个随时可用的知识渊博的协作者。
  • 在此过程中,我们识别出了一些需要避免的陷阱:保持提示既不模糊也不过载,始终明确我们的意图和约束,并在 AI 的输出不符合目标时准备好进行调整。我们引用了具体的坏提示示例,并看到了微小的改变(如包含错误消息或预期输出)如何能显著改善结果。

随着你将这些技巧融入你的工作流程,你会发现与 AI 合作变得更加直观。你会逐渐感受到什么样的措辞能获得最佳结果,以及如何在模型偏离轨道时引导它。请记住,AI 是它训练数据的产物——它见过许多代码和问题解决的例子,但提供方向的是你,告诉它哪些例子现在相关。本质上,你负责设定细节,AI 负责实现

值得注意的是,提示工程是一个不断发展的实践。开发者社区不断发现新的技巧——一个巧妙的单行提示或一个结构化的模板可能会突然在社交媒体上走红,因为它解锁了人们之前没有意识到的能力。关注那些讨论(如在 Hacker News、Twitter 上),因为它们可以启发你自己的技巧。但也要勇于自己尝试。将 AI 视为一个灵活的工具——如果你有想法(“如果我让它绘制架构的 ASCII 图表会怎样?”),就试试看。你可能会对结果感到惊讶,如果它失败了,也没关系——你学到了关于模型局限或需求的知识。

总之,提示工程使开发者能够充分利用 AI 助手。这是令人沮丧的体验(“这个工具毫无用处,它给了我胡说八道的东西”)和富有成效的体验(“这感觉像与专家进行结对编程,他为我编写了样板代码”)之间的区别。通过应用我们涵盖的策略——从提供详尽上下文到引导 AI 的风格和思考——你可以将这些以代码为中心的 AI 工具转变为你开发工作流程的真正扩展。最终结果不仅是你能更快地编写代码,而且你通常还能在这个过程中获得新的见解和模式(当 AI 解释事情或提出替代方案时),从而提升你自己的技能水平。

作为最后的要点,请记住提示词是一个迭代对话。用与其他工程师交流时相同的清晰度、耐心和彻底性来对待它。这样做,你会发现 AI 助手可以显著增强你的能力——帮助你更快地调试,更聪明地重构,以及更轻松地实现功能。

使用 Hugo 构建
主题 StackJimmy 设计