开发者们正越来越多地借助 AI 编程助手来加速日常工作流程。这些工具能自动补全函数、建议 bug 修复方案,甚至能生成整个模块或最小可行产品 (MVP)。然而,正如我们许多人已经体会到的,AI 输出的质量在很大程度上取决于你所提供提示的质量。换句话说,提示工程已成为一项必备技能。一句措辞不当的请求可能会得到毫不相关或泛泛而谈的回答,而一条精心设计的提示则能产出经过深思熟虑、准确甚至富有创意的代码解决方案。本文将以实践为导向,探讨如何系统地为常见的开发任务打造高效的提示。
AI 结对程序员虽然强大,但并非无所不能——它们对你的具体项目或意图一无所知,所有信息都来源于你告知它们或作为上下文提供的内容。你提供的信息越多,产出的结果就越好。我们将提炼出那些能与开发者产生共鸣的关键提示模式、可复用的框架以及令人印象深刻的例子。你将看到好提示与坏提示的并排对比,以及真实的 AI 回复,并附上分析,以理解为何一个能成功而另一个却会失败。这里有一份快速入门的备忘单:
| 技巧 | 提示模板 | 目的 |
| :--- | :--- | :--- |
| 1. 角色设定 | "你是一位资深的 {language} 开发者。请为实现 {goal} 目标而审查此函数。" | 模拟专家级的代码审查、调试或重构。 |
| 2. 明确上下文设定 | "问题是这样的:{summary}。代码如下。它本应实现 {expected behavior},但实际却表现出 {actual behavior}。请问是为什么?" | 清晰地框定问题,以避免泛泛而谈、流于表面的回答。 |
| 3. 输入/输出示例 | "当给定输入 {input} 时,此函数应返回 {expected output}。你能编写或修复这段代码吗?" | 通过示例展示意图,从而引导 AI 助手。 |
| 4. 链式迭代 | "首先,生成组件的骨架。下一步,我们添加状态。然后处理 API 调用。" | 将大任务分解为小步骤,以避免提示过于繁重或模糊。 |
| 5. 模拟调试 | "请逐行分析此函数。各个变量的值是什么?它可能会在哪里中断?" | 让 AI 助手模拟运行时行为,并发现隐藏的 bug。 |
| 6. 功能蓝图规划 | "我正在构建 {feature}。需求是:{bullets}。使用的技术栈是:{tech stack}。请搭建初始组件的骨架,并解释你的选择。" | 通过 AI 主导的规划和脚手架搭建来启动功能开发。 |
| 7. 代码重构指导 | "请重构此代码以改进 {goal},例如 {可读性、性能、惯用风格}。请用注释解释所做的更改。" | 让 AI 的重构与你的目标保持一致,而不是进行随意的更改。 |
| 8. 询问替代方案 | "你能用函数式风格重写它吗?递归版本会是什么样子?" | 探索多种实现路径,扩展你的技术工具箱。 |
| 9. 小黄鸭法 | "我是这样认为这个函数的功能的:{your explanation}。我遗漏了什么吗?这能揭示出任何 bug 吗?" | 让 AI 挑战你的理解,并发现其中的不一致之处。 |
| 10. 约束锚定 | "请避免使用 {如:递归},并坚持使用 {如:ES6 语法、无外部库}。请为 {如:内存} 进行优化。函数如下。" | 防止 AI 超出范围或引入不兼容的模式。 |
高效代码提示的基础
给 AI 编程工具发指令,有点像在和一个极其较真、但有时又知识渊博的同事沟通。要想获得有用的结果,你需要清晰地铺陈背景,并明确指导 AI 你想要什么以及希望它如何做。
以下是贯穿本实战宝典所有示例的基础原则:
提供丰富的上下文。永远假定 AI 对你的项目一无所知,除非你明确提供信息。请包含相关细节,例如编程语言、框架、库,以及你正在处理的特定函数或代码片段。如果出现错误,请提供确切的错误信息,并描述代码本应*实现的功能。明确性和上下文是区分模糊建议与精准可行方案的关键。实践中,这意味着你的提示可能需要一段简短的背景描述,例如:“我有一个基于 Node.js、使用 Express 和 Mongoose 的函数,它本应根据 ID 获取用户,但抛出了 TypeError。这是我的代码和错误信息……”。你给的背景信息越多,AI 需要猜测的内容就越少。
具体说明你的目标或问题。模糊的提问只会得到模糊的回答。不要问“为什么我的代码跑不起来?”这种问题,而应精准地指出你需要什么帮助。例如:“这个 JavaScript 函数返回了 undefined,而不是预期的结果。根据下面的代码,你能帮忙分析原因并提供修复方案吗?”这样的提问更有可能获得有用的答复。一个调试场景的提示公式是:“我期望它执行 [预期行为],但在输入 [示例输入] 时,它却表现出 [当前行为]。Bug 在哪里?”。同理,如果你想进行优化,请说明需要哪种特定类型的优化 (例如,“对于一万个项目,我该如何提升这个排序函数的运行时性能?”*)。具体性能够引导 AI 的注意力。
拆解复杂任务。在实现新功能或处理多步骤问题时,不要试图用一个庞大的提示把所有问题一次性抛给 AI。更有效的方法是,将任务拆分成小块,然后逐步迭代。例如,“首先,为产品列表页生成一个 React 组件的基本骨架。下一步,我们来添加状态管理。然后,再集成 API 调用。”*每个提示都建立在前一个的基础上。通常不建议一次性要求实现一个庞大的功能;而是应该从一个高层目标开始,然后迭代地请求实现每个部分。这种方法不仅能让 AI 的回答保持专注和可控,也恰好模仿了人类开发者增量构建解决方案的过程。
提供输入/输出示例或预期行为。如果能用例子来说明你的需求,务必这样做。例如,“给定数组 [3,1,4],这个函数应该返回 [1,3,4]。”*在提示中给出一个具体例子,有助于 AI 理解你的意图,减少歧义。这就像给一位初级开发者一个简明的测试用例——它能清晰地阐明需求。在提示工程领域,这有时被称为“少样本提示 (few-shot prompting)”,也就是你给 AI 展示一个可供模仿的模式。哪怕只有一个正确的行为示例,也能极大地引导模型的响应方向。
善用角色或人设。在许多广为流传的提示案例中,一个强大的技巧是要求 AI “扮演”某个特定角色或人设。这能影响回答的风格和深度。例如,“请扮演一位资深的 React 开发者,审查我的代码并找出潜在的 bug”或者“你是一位 JavaScript 性能优化专家,请优化以下函数。”通过设定角色,你能引导 AI 助手采用相应的口吻——无论是扮演严格的代码审查员、为初级开发者提供帮助的导师,还是寻找安全漏洞的分析师。社区分享的提示已经证明了这种方法的有效性,例如“扮演一名 JavaScript 错误排查专家,为我调试这个函数。从 API 调用返回的数据没有正确渲染。”*。在实际使用中,我们仍然需要提供代码和问题细节,但角色扮演提示能够产出更结构化、更具专家水准的指导。

通过对话迭代和优化。提示工程是一个互动的过程,而非一蹴而就。开发者常常需要审阅 AI 的初次回答,然后提出追问或修正。如果解决方案不尽如人意,你可以说:“这个方案用了递归,但我更倾向于迭代实现——你能不用递归再试一次吗?”或者,“太棒了,现在能帮我优化一下变量名并加上注释吗?”*在聊天会话中,AI 会记住上下文,因此你可以逐步引导它走向你期望的结果。关键在于,要把 AI 看作一个可以指导的伙伴——追求持续进步,而非一步到位。
* 保持代码的清晰与一致。这最后一条原则看似间接,但对于那些需要理解你代码上下文的工具而言至关重要。即便在 AI 介入之前,也要编写清晰、结构良好且带有注释的代码。有意义的函数名和变量名、一致的格式化以及文档字符串,不仅能让人类更容易理解你的代码,也能为 AI 提供更明确的线索来推断你的意图。如果你展现出一种固定的模式或风格,AI 也会遵循。可以把这些工具看作是极其认真的初级开发者——它们会从你的代码和注释中捕捉每一个信号。
牢记这些基础原则后,让我们深入具体的应用场景。我们将从调试 (debugging) 开始,这或许是最直接的应用场景:你有一段代码行为异常,希望 AI 帮忙找出原因。
代码调试的提示模式
调试是 AI 助手的天然主场。这就像拥有了一只会说话的“小黄鸭”,它不仅倾听,还能给出建议。然而,成功与否很大程度上取决于你如何向 AI 呈现问题。以下是如何系统地提出请求,以寻求查找和修复 bug 的帮助:
1. 清晰描述问题和症状。在提示的开头,就说明哪里出了问题以及代码本应如何工作。务必包含确切的错误信息或不正确的行为表现。例如,不要只说“我的代码坏了”,而应该这样提问:“我有一个 JavaScript 函数,本应计算一个数字数组的总和,但它返回了 NaN (Not a Number),而不是正确的结果。代码如下:[在此附上代码]。对于像 [1,2,3] 这样的数组,它应输出一个数字 (即总和),但我却得到了 NaN。这个 bug 的原因可能是什么?”这个提示明确了语言、预期行为、观察到的错误输出,并提供了代码上下文——这些都是至关重要的信息。提供结构化的上下文 (代码 + 错误 + 预期结果 + 你已尝试的方法) 能给 AI 一个坚实的出发点。相比之下,像“我的函数为什么不工作?”这样的泛泛之问只会得到寥寥无几的成果——模型在没有上下文的情况下,只能给出最普遍的猜测。
2. 对棘手的 bug 采用逐步或逐行分析法。对于更复杂的逻辑 bug (即没有明显报错,但输出结果错误),你可以让 AI 逐行过一遍代码的执行过程。例如:“请逐行分析这个函数,并追踪每一步 total 变量的值。它的累加不正确——逻辑究竟错在哪里?”这是一个小黄鸭调试法 (rubber duck debugging) 的典型提示——你实际上是在要求 AI 模拟人类用打印语句或调试器进行的调试过程。这类提示常常能揭示一些细微的问题,比如变量未被重置或条件逻辑错误,因为 AI 会把每一步的状态都清晰地描述出来。如果你怀疑代码的某个特定部分,可以聚焦于此:“请解释一下这里的 filter 调用在做什么,它是否可能排除了超出预期的项?”让 AI 扮演解释者的角色,往往能在解释的过程中发现 bug。
3. 尽可能提供最小可复现示例。有时,你实际的代码库非常庞大,但 bug 可以在一小段代码中复现。如果你能提取或简化出仍能触发问题的代码,就把它提供给 AI。这不仅能让 AI 更容易聚焦问题,也能迫使你理清问题的本质 (这本身就是一个非常有益的练习)。例如,如果你在一个深层嵌套的函数调用中遇到了 TypeError,试着用几行代码来重现它,并分享出来。目标是用最少的代码来隔离 bug,然后对错误原因做出假设、进行测试并迭代。你可以让 AI 参与这个过程,比如:“这是一个精简后仍能触发错误的示例 [附上代码片段]。为什么会发生这个错误?”通过简化,你排除了干扰,帮助 AI 精准定位问题。(这个技巧也呼应了许多资深工程师的建议:如果无法立刻找到 bug,就简化问题域。如果你向 AI 展示一个更小的案例,它可以辅助你进行分析。)
4. 提出聚焦的问题和追问。在提供了上下文之后,直接询问你的需求通常很有效,例如:“可能是什么导致了这个问题?我该如何修复它?”。这会促使 AI 同时进行诊断并提出解决方案。如果 AI 的首次回答不够清晰或只有部分帮助,不要犹豫,继续追问。你可以说:“这个解释我明白了。能展示一下如何修复代码吗?请提供修正后的代码。”在聊天模式下,AI 拥有对话历史,所以它可以直接输出修改后的代码。如果你使用的是 VS Code 或 Cursor 中像 Copilot 这样的内联工具而非聊天窗口,你可能需要在代码上方写一条注释,比如 // BUG: returns NaN, fix this function,然后看它如何自动补全——但总的来说,交互式聊天能提供更详尽的解释。另一个追问模式是:如果 AI 给了修复方案但你不理解原因,就问“你能解释一下为什么这个改动能解决问题吗?”这样你下次就能学到东西,同时也能二次确认 AI 的推理是否站得住脚。
现在,让我们通过一个具体例子来说明这些调试提示原则,展示一个糟糕提示 vs. 优化提示,以及它们在 AI 回复上的差异:
调试示例:糟糕提示 vs. 优化提示
假设我们有一个简单的 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" }]);
这段代码本应遍历 users 数组,并构建一个以用户 ID 为键的对象。然而,这里有一个 bug:循环条件使用了 i <= users.length 而不是 i < users.length。这会导致在最后一次迭代中,users[i] 的值为 undefined,从而很可能引发运行时错误 (例如,尝试访问 undefined 的 id 属性)。让我们看看 AI 助手对关于这个问题的两个不同提示会如何回应。
❌ 糟糕的提示:“为什么我的 mapUsersById 函数不工作?”
AI 对糟糕提示的回应 (模拟):
分析:这个回答非常模糊。提示除了函数名之外没有提供任何细节,所以 AI 只能猜测一些常见原因 (“也许数组是空的或不是数组”,“请确保每个用户都有 id 属性”)。它并没有找出真正的 bug。当你问题描述不充分时,就会得到这类回复——模型只能复述一些通用的调试技巧。作为开发者,这没什么用,因为它没有解决真正的问题。提示中缺乏上下文和具体性导致了一个泛泛的回答 (基本上是 AI 版的“你试过重启大法吗?”)。这与社区的共识一致,即模糊的提示会导致不相关或泛泛的回答。
✅ 优化的提示:“我有一个 JavaScript 函数 mapUsersById,它应该将一个用户对象数组转换成一个以用户 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;
}
它本应返回 { "1": {id: 1, name: "Alice"} }。请问 bug 在哪里,以及我该如何修复它?”
AI 对优化提示的回应 (模拟):
for (let i = 0; i < users.length; i++) {
const user = users[i];
userMap[user.id] = user;
}
分析:这个回应直接指出了 bug 所在 (在循环中使用了 <=),并解释了为什么这会导致 user 变为 undefined。接着,它提供了正确的代码解决方案 (i < users.length) 并确认了预期的结果。这个优化后的提示之所以成功,是因为它遵循了我们的原则:指明了语言 (JavaScript),描述了函数的用途,包含了确切的错误信息和示例输入,甚至还提供了相关的代码片段。注意 AI 是如何利用错误信息作为线索,将注意力集中在循环边界上的——一个目标明确的提示使得 AI 能够进行真正的问题解决,有效地模拟了人类调试员的思维过程:“undefined 可能从哪里来?很可能是循环索引出了问题”。这是详细提示带来好处的一个具体例证。
更多调试策略:除了识别明显的 bug,你还可以利用提示工程进行更深层次的调试辅助:
询问可能的原因。如果你真的束手无策,可以稍微扩大问题的范围:“在这段代码中,出现 TypeError: cannot read property 'foo' of undefined 有哪些可能的原因?”*并附上代码。模型可能会列出几种情况 (例如,对象未初始化、存在竞态条件、变量作用域错误等)。这可以为你提供一些未曾考虑过的排查角度,就像和同事进行头脑风暴一样。
“请教小黄鸭”——即向 AI 解释你的代码。这听起来可能有些反直觉 (为什么要向助手解释?),但书写解释的过程能帮助你理清自己的思路,之后你还可以让 AI 验证或评判你的解释。例如:“我来解释一下这个函数是做什么的:[你的解释]。基于此,我的逻辑是否正确?这是否能揭示 bug 所在?”*AI 可能会在你的解释中发现一个指向实际 bug 的漏洞。这个技巧将 AI 当作一个积极的“小黄鸭”,它不仅倾听,还会回应。
让 AI 创建测试用例。你可以问:“你能提供几个可能会让这个函数出错的测试用例 (输入) 吗?”*助手可能会想出你没考虑到的边界情况 (空数组、极大或极小的数字、null 值等)。这对于调试和为未来的代码健壮性生成测试都很有用。
扮演代码审查员。作为直接“调试这段代码”提示的替代方案,你可以说:“请扮演一名代码审查员。这里有一个未按预期工作的代码片段。请审查它,并指出任何可能导致问题的错误或不良实践:[附上代码]”。*这会让 AI 进入一种批判性思维模式。许多开发者发现,将请求表述为代码审查会得到非常详尽的分析,因为模型会评论代码的每个部分 (并且通常在这样做时,它会发现 bug)。事实上,一个提示工程技巧就是明确要求 AI 表现得像一个一丝不苟的审查员。这不仅能发现手头的 bug,还可能揭示其他问题 (例如,可能缺少空值检查),这或许也很有用。
总而言之,当使用 AI 助手进行调试时,细节和方向是你的制胜法宝。提供场景、描述症状,然后提出有针对性的问题。一个手足无措的“它不工作,救命!”和一个外科手术般精准的调试提示,其效果有天壤之别,正如我们上面所见。接下来,我们将转向另一个主要应用场景:重构和优化现有代码。
重构与优化的提示模式
重构代码——即在不改变其功能的前提下,使其更简洁、更快或更符合语言习惯——是 AI 助手大显身手的领域。它们经过海量代码的训练,其中包括大量结构良好、优化过的解决方案。然而,要有效地利用这些知识,你的提示必须明确在你的情境下“更好”意味着什么。以下是如何为重构任务编写提示:
1. 明确陈述你的重构目标。“重构这段代码”本身太过宽泛。你是想提升可读性?降低复杂度?优化性能?还是想换用不同的编程范式或库?AI 需要一个明确的目标。一个好的提示会框定任务范围,例如:“重构以下函数,以提升其可读性和可维护性 (减少重复代码,使用更清晰的变量名)。”或者“请优化这个算法的速度——它在处理大量输入时太慢了。”通过陈述具体目标,你帮助模型决定应该应用哪种转换。例如,告诉它你关心性能,可能会促使它使用更高效的排序算法或缓存机制;而如果侧重于可读性,它可能会将一个函数拆分成几个小函数或添加注释。如果你有多个目标,请一一列出。一份来自 Strapi 指南的提示模板甚至建议枚举问题:“我想解决的问题:1) [性能问题],2) [代码重复],3) [使用了过时的 API]。”。这样,AI 就确切地知道要修复什么。记住,它不会凭空知道你认为代码中存在什么问题——你必须告诉它。
2. 提供必要的代码上下文。在进行重构时,你通常会在提示中附上需要改进的代码片段。重要的是要包含你想要重构的完整函数或代码块,有时如果相关的话,还需提供一些周边上下文 (比如函数的使用场景或相关代码,这可能会影响重构的方式)。同时,也要提及语言和框架,因为“符合习惯的”代码在不同技术栈之间差异很大,例如,符合 Node.js 习惯的代码与符合 Deno 习惯的代码不同,React 的类组件与函数式组件也大相径庭。例如:“我有一个用类 (class) 编写的 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. 优化提示
假设我们有一个函数,它会进行两次数据库调用并做一些处理。代码能跑,但不够优雅——存在重复代码,而且可读性差。我们希望为了清晰度和效率对其进行重构。这是原始代码:
// 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;
}
这段代码的问题在于:我们看到了针对用户和订单的重复 fetch 逻辑 (可以重构为一个辅助函数或放入循环)、缺乏详细的错误信息 (只有一个泛泛的错误消息),而且或许可以通过并行执行 fetch 来提升速度。让我们看看两种不同的提示方法:
❌ 提示 1 (描述不详):“重构上面的 getCombinedData 函数。”
AI 对提示 1 的回应:
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] || []
}));
}
分析:这其实是一次相当不错的重构,实现了多项改进:并行获取、为提高效率按用户 ID 对订单进行分组、使用 .map 并默认空数组使数据组合更简洁。AI 也解释了它做了什么。那我们为什么还把这个提示标记为描述不详或“糟糕”呢?在这种情况下,AI 碰巧做出了正确的选择,但因为它收到的提示是开放式的,所以它假设了很多东西。它假设了性能需要提升 (并行请求是个聪明的改动,但如果在这里并行是不安全的呢?),并且它将错误处理改为了一个泛泛的“Failed to fetch data”消息,这在合并错误时丢失了具体是哪个调用失败了的信息。
使用泛泛的“重构这个”提示的风险在于,AI 可能会做出一些意想不到的、改变了行为的改动。在这里,合并错误处理是否可接受,取决于具体需求。AI 助手还做了一个结构性改动 (使用字典对订单进行分组)——这很好,可能更好,但如果我们有特定约束 (比如希望流式处理结果或保持低内存占用),我们并没有告知它。所以,虽然结果看起来不错,但 AI 对“重构”的自我解读恰好与我们想要的一致,这多少有些侥幸。这说明,一个描述不详的提示在简单情况下有时能得到好结果,但对于复杂的代码,它可能会偏离我们期望的方向,或错过我们关心的某些改进。
✅ 提示 2 (目标导向):“请重构上面的 getCombinedData 函数,以消除重复代码并提升性能。具体要求如下:(1) 避免用户和订单的 fetch 逻辑重复——可以考虑使用辅助函数或将它们一起获取。(2) 如果可能,并行获取两个列表。(3) 保留每个 fetch 的独立错误处理 (我们想知道是哪个调用失败了)。(4) 改进数据组合的方式,可以考虑使用更高效的查找结构,而不是嵌套循环。请提供重构后的代码,并附上解释改动的注释。”
AI 对提示 2 的回应:
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.ok 和 ordersResponse.ok 的单独检查,这样任何错误都能清晰地指明是哪个 fetch 失败了 (要点 2)。
并行 JSON 解析:*同样对 response.json() 调用使用了 Promise.all,这是一个微小的改进。
高效的数据组合:我创建了一个 ordersByUser 映射,而不是使用嵌套循环 (要点 3)。这可以在一次遍历中按 userId 对订单进行分组。然后我遍历用户来构建结果,以 O (1) 的时间复杂度从映射中提取每个用户的订单 (要点 4)。这种方式更高效、更简洁。
分析:这个回应直接解决了所有指定的目标。代码被重构得更清晰、更快,并且根据请求保留了独立的错误消息。AI 的解释确认了我们列出的每一点,表明它仔细地遵循了提示的指令。这是一个非常好的结果,因为作为提问者,我们在这个上下文中定义了“重构”的含义。通过这样做,我们引导 AI 生成了一个符合我们需求的解决方案,而且几乎不需要来回沟通。如果 AI 忽略了其中某一点 (比如它仍然合并了错误处理),我们可以很容易地再次提示:“看起来不错,但请确保用户和订单的错误信息保持独立。”——不过,在这种情况下,由于我们的提示足够详尽,所以没这个必要。
这个例子揭示了一个关键教训:当你知道想改进什么时,就明确地把它写出来。AI 擅长遵循指令,但它不会读心术。一个宽泛的“让这个更好”对于简单的事情可能有效,但对于非凡的代码,只有列举出你心中“更好”的标准,才能得到最佳结果。这与社区的见解不谋而合:清晰、结构化的提示能带来显著改善的结果。
更多重构技巧:
分步重构:*如果代码量非常大,或者你有一长串的修改清单,可以一次只解决一个。例如,先让 AI “为提高可读性进行重构”(专注于重命名、拆分函数),稍后再“优化这个函数中的算法”。这可以避免一次性给模型过多的指令而让它不堪重负,并让你能逐步验证每一项改动。
询问替代方案:也许 AI 的第一次重构可行,但你对其他角度感到好奇。你可以问:“你能用另一种方式重构吗?比如使用函数式编程风格 (例如,用数组方法代替循环)?”或者“如果用递归代替迭代方法会怎么样?只是为了比较一下。”*这样,你就可以评估不同的解决方案,就像和同事一起头脑风暴多种重构方案一样。
结合重构与解释来学习模式:*我们之前提到过这一点,但值得再次强调——把 AI 当作一个学习工具。如果它用一种巧妙的方式重构了代码,仔细研究它的输出和解释。你可能会发现一个以前没用过的新 API 或技巧 (比如用 reduce 来构建一个映射表)。这也是为什么要索要解释的原因之一:它能把一个答案变成一个迷你教程,巩固你对最佳实践的理解。
验证与测试:在任何由 AI 生成的重构之后,一定要运行你的测试或用示例输入来验证代码。AI 可能会无意中引入细微的 bug,特别是当提示没有指明某个重要约束时。例如,在我们的重构中,如果原始代码为了日志记录而特意分开了 fetch 错误,但我们没有提及这一点,那么合并后的错误信息可能就没那么有用了。我们的职责就是在审查中发现这类问题。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 ?
Loading... : {user?.name};
};
预期行为:当 userId 变化时,应该只获取一次用户数据。
实际行为:组件无限次重渲染。
控制台报错:“Warning: Maximum update depth exceeded”
是什么导致了这个无限循环?我该如何修正依赖项数组?
为什么这样有效:提供了确切的代码、错误信息、预期与实际行为的对比,并聚焦于一个常见且容易被误解的 React 模式。
现代状态架构
❌ 糟糕的提示:“帮我为我的 Next.js 电商应用构建状态管理”
✅ 优化的提示:
我正在构建一个 Next.js 14 的电商应用,需要设计状态管理架构。我的需求如下:
组件需求:
* 产品列表页 (需要:products[]、筛选器、分页)
* 购物车 (需要:购物车商品、总计、配送信息)
* 用户认证 (需要:用户资料、认证状态、偏好设置)
* 实时通知 (需要:toast 消息、错误状态)
技术限制:
* 使用 Next.js 14 的 App Router 和服务器组件
* TypeScript 严格模式
* 为 SEO 进行服务器端数据获取
* 为购物车/用户操作提供客户端交互性
* 状态需要在页面导航间持久化
我应该选择哪种方案:
1. 为每个领域 (购物车、认证、通知) 创建独立的 Zustand store
2. 使用 React Query/TanStack Query 管理服务器状态 + Zustand 管理客户端状态
3. 使用一个包含多个 slice 的单一 Zustand store
请提供一个推荐的架构方案,并附上代码示例,展示如何组织 store 以及如何与 Next.js App Router 模式集成。
为什么这样有效:这是一个真实场景,包含了具体的技术栈和清晰的需求,并且要求提供包含实现细节的架构指导。
实现新功能的提示模式
AI 代码助手最激动人心的用途之一,莫过于帮助我们从零开始编写新代码,或将新功能集成到现有代码库中。这可以小到生成一个 React 组件的样板代码,大到在 Express 应用中编写一个全新的 API 端点。这里的挑战通常在于,这些任务是开放式的——实现一个功能有多种方式。代码生成的提示工程,其核心就是引导 AI 产出符合你需求和编码风格的代码。以下是一些实现这一目标的策略:
1. 从高层指令开始,然后逐步深入。首先用通俗易懂的语言概述你想要构建什么,并可以将其分解为更小的任务 (这与我们之前关于分解复杂任务的建议类似)。例如,假设你想给一个现有的 web 应用添加一个搜索栏功能。你可以先这样提问:“请为我的 React 应用设计一个方案,以添加一个可以按名称筛选产品列表的搜索功能。产品列表是从一个 API 获取的。”
AI 可能会给你一个分步计划:“1. 添加一个用于搜索查询的输入框。2. 添加一个状态来保存查询词。3. 根据查询词过滤产品列表。4. 确保搜索是大小写不敏感的,等等。”一旦你有了这个计划 (你还可以在 AI 的帮助下进行完善),你就可以针对每个要点,用更聚焦的提示来逐一攻克。
例如:“好的,现在实现第一步:创建一个 SearchBar 组件,它包含一个能更新 searchQuery 状态的输入框。”然后是:“接下来实现第三步:给定 searchQuery 和一个产品数组,过滤出产品 (对名称进行不区分大小写的匹配)。”通过分解功能,你确保了每个提示都是具体的,并且返回的响应也是可控的。这也模仿了迭代开发的流程——你可以在构建每个部分时对其进行测试。
2. 提供相关上下文或参考代码。如果你要为一个现有项目添加功能,向 AI 展示项目中类似功能是如何实现的,会非常有帮助。例如,如果你已经有一个与你想要的功能相似的组件,你可以说:“这里有一个现有的 UserList 组件 (代码…)。现在,请创建一个与之类似的 ProductList 组件,但要额外包含一个搜索栏。”
AI 会观察并应用你代码中的模式 (比如你使用的特定库或代码风格约定)。打开相关文件或在提示中引用它们,能够提供上下文,从而生成更贴近项目、风格更一致的代码建议。另一个技巧是:如果你的项目使用了特定的编码风格或架构 (比如用 Redux 做状态管理,或某个特定的 CSS 框架),请明确指出。“我们使用 Redux 进行状态管理——请将搜索状态集成到 Redux store 中。”
一个训练有素的模型随后会生成符合 Redux 模式的代码。本质上,你是在向 AI 介绍你的项目环境,以便它能量身定制输出。有些 AI 助手甚至能将你的整个代码仓库作为上下文来参考;如果使用这类工具,请确保你指明了仓库中相似的模块或文档。
如果你是从零开始,但有偏好的实现方式,也可以提出来:“我希望用函数式编程的风格来实现这个功能 (无外部状态,使用数组方法)。”或者,“请确保遵循 MVC 模式,将业务逻辑放在控制器 (controller) 中,而不是视图 (view) 中。”*这些都是资深工程师可能会提醒初级工程师的细节,而在这里,你就是那位指导 AI 的资深工程师。
3. 使用注释和 TODO 作为内联提示。当在 IDE 中直接使用像 Copilot 这样的工具时,一个高效的工作流是:先写一条注释来描述你需要的下一段代码,然后让 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 可能会很大。”这种迭代式的提示方式非常强大。一个真实世界的例子是:一位开发者让一个大语言模型 (LLM) 用 JS canvas 库画一个冰淇淋蛋筒,但模型一直给出不相关的输出,直到他用更具体的细节和上下文完善了提示。教训就是,不要浅尝辄止。弄清楚提示中哪里有欠缺或被误解了,然后加以澄清。这正是提示工程的精髓——每一次微调都能引导模型更接近你的设想。
让我们通过一个具体的场景,来说明如何提示以实现新功能:
功能实现示例:在 AI 辅助下构建 React 组件
假设我们有一个 React 应用,想要添加一个新组件:一个带有搜索过滤器和排序下拉菜单的产品列表。这是 Web 应用中一个相当典型的功能。为简明起见,我们将重点放在提示该组件的搜索功能上。
初始提示 (功能请求):“请创建一个名为 ProductList 的 React 函数式组件,用于展示一个产品列表,并包含一个文本输入框以按名称筛选产品。该组件应:从 /api/products 获取一个产品数组 (假设返回 {id, name, ...} 格式的 JSON 数组),将其存储在 state 中,并允许用户在搜索框中输入文字来筛选显示的产品 (不区分大小写)。用一个简单的
- 列表展示筛选后的产品名称。请包含基本的错误处理 (例如,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
Loading products...;
if (error) return{error};return (
type="text"
placeholder="Search products"
value={search}
onChange={handleSearchChange}
/>
{filteredProducts.map(prod => (
- {prod.name}
))}
);
}
export default ProductList;
(如果我们要求,代码中可能还会附带解释各部分的注释。)
现在,让我们分析这个结果,并考虑如何优化提示:
假设返回了上面的代码,但我们的项目实际上是使用一个 context 或全局 store 来进行数据获取,而不是在组件内部直接 fetch。这是我们之前没有指明的一点。AI 自主选择了在 useEffect 中获取数据。如果我们更倾向于使用自定义 hook 或 context,就应该在提示中加以暗示。所以我们可以这样优化提示:“实际上,在我们的应用中,我们使用一个自定义 hook useProducts() 来处理数据获取。请将组件重构为使用 useProducts hook,而不是直接调用 fetch。”*AI 随后会进行调整,可能会假设 useProducts 返回 { products, loading, error },并相应地简化组件。
另一个优化点:也许我们意识到还想要一个排序下拉菜单 (我们一开始没提)。我们现在可以扩展对话:“太棒了,现在请添加一个下拉菜单,用于按名称对产品进行排序 (A-Z 或 Z-A)。下拉菜单应允许用户选择升序或降序,并且列表应在筛选的基础上进行相应的排序。”*由于 AI 拥有现有代码的上下文,它可以插入一个排序状态并调整渲染逻辑。我们提供了一个清晰的新需求,它会尝试去实现,很可能会添加类似下面的代码:
const [sortOrder, setSortOrder] = useState('asc');
// ... 一个用于选择 sortOrder 的