让AI编程助手成为更可靠的开发伙伴
神译局是36氪旗下编译团队,关注科技、商业、职场、生活等领域,重点介绍国外的新技术、新观点、新风向。
编者按:开发者们越来越依赖AI编程助手来加速日常工作流程,但AI编程助手输出质量取决于提示词水平。本文系统拆解调试、重构、开发三大场景的提示工程框架,并附上可复用的模板及对比案例,助开发者释放AI协作潜能。文章来自编译。
开发者们越来越依赖AI编程助手来加速日常工作流程。这些工具可以自动补全函数、建议修复bug,甚至生成整个模块或最小可行产品(MVP)。不过,正如我们许多人已经了解到的那样,AI输出的质量在很大程度上取决于你提供的提示(prompt)的质量。换句话说,提示工程已经成为一项必备技能。一个措辞不当的请求可能会得到无关或泛泛的答案,而一个精心设计的提示则可能生成深思熟虑、准确甚至富有创意的代码解决方案。本文旨在从实用角度探讨如何系统地为常见开发任务设计有效的提示。
AI结对编程工具很强大,但并非魔法——它们缺乏有关你的项目或意图的先验知识,只知道你告诉它们或作为上下文所包含的内容。你提供的信息越丰富,输出结果就越好。我们将提炼出关键的提示模式、可复用的框架以及让开发者产生共鸣的难忘示例。你将看到好提示与坏提示及其实际AI响应的对比,并附有评论说明为什么一个成功了而另一个却失败了。以下是一份入门速查指南:
向AI编码工具发出提示,有点像是跟一位非常刻板(有时候还知识渊博)的合作者沟通。要想获得有用的结果,你需要清晰设定场景,并引导AI了解你想要什么以及你想要的方式。
以下是支撑本指南所有示例的基本原则:
牢记这些基本原则后,让我们深入探讨具体场景。我们将从调试开始,这可能是最直接的用例:你的代码行为异常,你想让AI帮忙找出原因。
调试天然适合AI助手。这就像拥有一个橡皮鸭,它不仅会听你说话,还会提出建议。不过,成功与否很大程度上取决于你如何向AI呈现问题。以下是系统性地请求帮助查找和修复bug的方法:
1.清晰描述问题和症状。首先在提示中描述哪里出了问题,以及代码本应实现什么功能。务必包含确切的错误信息或错误行为。比方说,与其只是说“我的代码不工作”,你可以这样写提示:“我有一个JavaScript函数,它应该计算一个数字数组的总和,但它返回的是NaN(非数字)而不是实际的总和。代码如下:[包含代码]。对于像[1,2,3]这样的数字数组,它应该输出一个数字(总和),但我得到的是NaN。这个bug可能是什么原因造成的?” 这个提示明确了编程语言、预期行为、观察到的错误输出,并提供了代码上下文——所有这些都是关键信息。提供结构化的上下文(代码 + 错误 + 预期结果 + 你尝试过的方法)为AI奠定了坚实的基础。相比之下,像“为什么我的函数不工作?”这种泛泛的问题收效甚微——模型在没有上下文的情况下只能提供最笼统的猜测。
2.对棘手的bug采用分步或逐行方法。对于更复杂的逻辑错误(没有抛出明显的错误消息但输出错误),你可以提示AI逐步执行代码。比方说:“逐行分析这个函数,并在每一步跟踪`total`的值。这个值累加不正确——逻辑是在哪里出错的?” 这是一个橡皮鸭调试提示的示例——本质上是在要求AI模拟人类用打印语句或调试器进行的调试过程。这类提示常常能揭示一些微妙的问题,比如变量未重置或条件逻辑错误,因为AI会详细说明每一步的状态。如果你怀疑代码的某个特定部分,可以放大关注点:“解释一下这里的`filter`调用在做什么,它排除掉的项是否比预期的要多?” 让AI扮演解释者的角色,可以在解释过程中暴露bug。
3.尽可能提供最小可复现示例。有时候你的实际代码库很大,但bug可以在一个小片段中复现。如果能提取或简化仍然能复现问题的代码,就把它提供给AI。这不仅让AI更容易集中注意力,也迫使你澄清问题(这本身通常就是一次有用的练习)。比方说,如果你在一个深度嵌套的函数调用中得到了一个TypeError,尝试用几行可分享的代码来复现它。目标是用最少的代码隔离掉bug,对问题所在做出假设,进行测试并迭代。你可以让AI参与这个过程:“这是一个简化后的示例,它仍然会触发错误[包含代码片段]。为什么会出现这个错误?” 通过简化,可消除干扰,帮助AI精确定位问题。(这种技术反映了许多资深工程师的建议:如果不能立即找到bug,就简化问题空间。如果你向AI呈现一个更小的案例,它可以协助进行分析。)
4.提出有针对性的问题和后续跟进。在提供上下文后,直接询问你需要的内容通常很有效,比方说:“可能导致这个问题的原因是什么,以及我该如何修复它?”。这会促使AI既进行诊断又提出解决方案。如果AI的首次回答不清楚或只部分有用,不要犹豫,继续追问。你可以说:“这个解释有道理。你能告诉我如何修复代码吗?请提供修正后的代码。” 在聊天设置中,AI是掌握对话历史的,因此它可以直接输出修改后的代码。如果你使用的是像VS Code中的Copilot或Cursor这样的内联工具(没有聊天功能),你可能需要在代码上方写一条注释,如`// BUG: returns NaN, fix this function`,然后看看它如何自动补全——但通常来说,互动式聊天能产生更详尽的解释。另一种跟进模式:如果AI给出了修复方案但你不理解原因,就问:“你能解释一下为什么这个改动能解决问题吗?” 这样你就能学到东西以备下次使用,同时也复核了AI的推理是否合理。
现在,让我们用一个具体示例来说明这些调试提示原则,展示一个糟糕提示与改进提示的对比以及AI响应的差异:
调试示例:糟糕提示 vs 改进提示
想象我们有一个简单的Node.js函数,目的是将用户对象列表转换为按用户ID索引的查找映射(lookup map)。但是,它抛出了一个错误。以下是包含bug的代码:
// 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。不过,存在一个bug:循环使用了`i <= users.length`而不是`i < users.length`。这会导致在最后一次迭代时`users[i]`是undefined,很可能引发运行时错误(比方说,尝试访问undefined的属性`id`)。让我们看看AI助手可能如何回应关于此问题的两个不同提示。
糟糕提示:“为什么我的mapUsersById函数不工作?”
AI对糟糕提示的响应(模拟):
> *没有更多信息很难说。你确定函数是用一个有效的用户数组调用的吗?可能是`users`为空或不是数组,导致循环无法正常执行。也可能是在`userMap`上设置属性时出错。确保每个用户都有`id`属性。如果`users`是undefined或null,也会导致它不工作。*
分析:这个答案相当含糊。提示只提供了函数名,没有任何细节,所以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对改进提示的响应(模拟):
> *错误`Cannot read property 'id' of undefined`表明在某个时刻`user`是undefined,然后你尝试访问`user.id`。查看你的循环,问题出在循环边界上。你使用了`i <= users.length`。如果`users.length`是1(数组里有一个用户),循环会在i=0和i=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的user,并且应该按预期工作,对于给定的输入返回`{"1": {id: 1, name: "Alice"}}`。*
分析:这个响应直接指出了bug(在循环中使用了`<=`),并解释了它为什么导致`user`变成undefined。然后它提供了正确的代码解决方案(`i < users.length`)并确认了预期结果。改进提示之所以成功,是因为它遵循了我们的原则:明确了语言(JavaScript),描述了函数目的,包含了确切的错误消息和示例输入,甚至还提供了相关的代码片段。注意AI是如何利用错误消息作为线索,将注意力集中在循环边界上的——一个有明确目标的提示使AI能够进行真正的问题解决,有效地模拟了人类调试者的思考方式:“undefined可能从哪里来?很可能来自循环索引”。这具体展示了详细提示的好处。
额外的调试策略:除了识别明显的bug,你还可以利用提示工程辅助进行更深层次的调试:
总之,在使用AI助手进行调试时,细节和方向性是关键。提供场景、症状,然后提出有针对性的问题。正如我们上面所看到那样,慌乱无措地提示“它不工作了,救命!”和一个精准的手术式调试提示之间有着天壤之别。接下来,我们将转向另一个主要用例:重构和改进现有代码。
重构代码——在不改变其功能的前提下使其更清晰、更快或更符合语言习惯——是AI助手可以大放异彩的领域。它们基于海量代码进行训练,里面就有很多结构良好、优化解决方案的示例。不过,要有效利用这些知识,你的提示必须明确说明在你的情况下“更好”意味着什么。以下是如何为重构任务编写提示:
1、明确说明你的重构目标。 光说“重构这段代码”就太过开放了。你是想提高可读性?降低复杂度?优化性能?使用不同的范式或库?AI需要一个目标。一个好的提示会框定任务,比方说:“重构以下函数以提高其可读性和可维护性(减少重复,使用更清晰的变量名)。”或者“优化这个算法来提高速度——它在处理大型输入时太慢了。”通过说明具体目标,你帮助模型决定应用哪些转换。比方说,告诉它你关心性能可能会让它使用更高效的排序算法或缓存,而关注可读性则可能会让它将一个函数拆分成更小的函数或添加注释。如果你有多个目标,把它们列出来。来自Strapi指南的一个提示模板甚至建议列举问题:“我想解决的问题:1) [性能问题], 2) [代码重复], 3) [过时的API使用]。” 这样,AI就知道具体要修复什么。记住,它本身并不知道你觉得代码中存在的问题是什么——你必须告诉它。
2、提供必要的代码上下文。 重构时,你通常会在提示中纳入需要改进的代码片段。重要的是把你想要重构的函数或代码完整放进来,有时候如果相关的话,还需要把周边的上下文放进来(比如函数的用法或相关代码,这可能会影响你重构的方式)。同时要介绍语言和框架,因为“符合语言习惯”的代码在不同环境(比方说,符合Node.js习惯 vs 符合Deno习惯,或者React类组件 vs 函数组件)下是不一样的。比方说:“我有一个用类写的React组件。请将其重构为使用Hooks的函数组件。”然后AI会应用典型的步骤(使用useState, useEffect等)。如果你只是说“重构这个React组件”而没有说明风格,AI可能不知道你特指想要Hooks。
3、鼓励附带代码的解释。 从AI主导的重构中学习(并验证其正确性)的一个好方法是要求对更改进行解释。比方说:“请建议一个重构后的代码版本,并解释你所做的改进。”这甚至被内置在我们引用的提示模板里面:“……建议重构后的代码并附上对更改的解释。” 当AI提供解释时,你可以评估它是否理解了代码并满足了你的目标。解释可能会是这样:“我将两个相似的循环合并为一个以减少重复,并且我使用了一个字典来加速查找”,等等。如果解释有任何听起来不对劲的地方,那就是需要仔细检查代码的危险信号。简而言之,利用AI的解释能力作为保障——这就像让AI对其自身的重构进行代码审查。
4、 利用角色扮演设定高标准。如前所述,要求AI扮演代码审查员或资深工程师可能非常有效。对于重构,你可以说:“扮演一位经验丰富的TypeScript专家,重构这段代码以符合最佳实践和现代标准。”结果往往不仅是肤浅的更改,还会带来更有见地的改进,因为AI试图符合“专家”的形象。提示指南有个流行的例子是让AI扮演导师:“扮演一位经验丰富的Python开发者指导初级开发者。提供解释并编写文档字符串(docstrings)。重写代码来优化。” 在那个案例里,结果是AI使用了更高效的数据结构(用集合set去重),并为原本使用循环的函数提供了一行式的解决方案。角色扮演不仅帮助它进行了重构,还解释了为什么新方法更好(在那种情况下,使用集合是一种众所周知的用于唯一性的优化)。
现在,让我们通过一个重构示例来看看提示是如何影响结果的。我们将使用一个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;
}
这段代码的问题:我们看到获取用户和订单的逻辑重复(可以重构为辅助函数或循环),错误信息缺乏细节(只是通用错误消息),并且可以并行执行获取操作以提高速度。让我们看看两种提示方法:
**提示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碰巧做出了好的选择,但由于提示是开放式的,它做了很多假设。它假设性能应该被改进(并行请求,这是一个聪明的改动,但如果在这里并行不安全呢?),并且它将错误处理改成了一个通用的消息“Failed to fetch data”,这**丢失了**具体是哪个调用失败的**特异性**。
使用通用的“重构这个”提示的风险在于,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解析:也对response.json()调用使用了Promise.all,这是一个小改进。*
> *高效组合:我没有使用嵌套循环,而是创建了一个ordersByUser映射(第3点)。这通过单次遍历将订单按userId分组。然后我遍历users构建结果,在O(1)时间内从映射中提取每个用户的订单(第4点)。这更高效且简洁。*
分析:这个响应直接处理了**所有**指定的目标。代码被重构得更清晰、更快,并且按要求保留了单独的错误消息。AI的解释确认了我们列出的每一点,这表明它仔细遵循了提示指令。这是一个很好的结果,因为作为提示者,我们定义了在这个上下文中“重构”意味着什么。如此,我们引导AI产生了一个符合我们需求的解决方案,让来回沟通最小化。如果AI忽略了其中一点(比如说它仍然合并了错误处理),我们很容易就可以再次提示:“看起来不错,但请确保区分用户和订单的错误消息。”——不过,在这个案例中,因为我们的提示很详尽,所以不需要这样做。
这个例子展示了一个关键教训:**当你知道你想要改进什么时,要明确说出来。** AI擅长遵循指令,但它不会读心术。一句宽泛的“让它更好”可能适用于简单的事情,但对于复杂的代码,通过列举“更好”对你意味着什么,才能获得最佳结果。这符合社区的见解:清晰、结构化的提示能显著改善结果。
额外的重构技巧:
至此,我们已经讲完了调试和重构——对现有代码的改进。下一个合乎逻辑的步骤是使用AI辅助来实现新功能或生成新代码。我们将探讨如何有效地为这种场景编写提示。
**React Hook依赖项问题**
**糟糕提示:** “我的useEffect工作不正常”
**增强提示:**
I have a React component that fetches user data, but it's causing infinite re-renders. Here's my code:
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变化时获取一次用户数据
实际行为:组件无限重新渲染
控制台错误:"Warning: Maximum update depth exceeded"
是什么导致了这个无限循环?如何修复依赖项数组?
为什么有效:提供了确切的代码、错误消息、预期与实际行为,并聚焦于一个常被误解的特定React模式。
现代的状态架构
**糟糕提示:** “为我的Next.js电商应用构建状态管理”
**增强提示:**
我正在构建一个Next.js 14电商应用,需要设计状态管理架构。以下是我的要求:
组件
* 产品列表页(需要:products[], 筛选器, 分页)
* 购物车(需要:购物车商品, 总计, 配送信息)
* 用户认证(需要:用户资料, 认证状态, 偏好设置)
* 实时通知(需要:toast消息, 错误状态)
技术约束
* Next.js 14 带App Router和服务器组件
* TypeScript严格模式
* 服务端数据获取(用于SEO)
* 购物车/用户操作的客户端交互性
* 状态应在导航间保持
我应该使用:
* 每个域(cart, auth, notifications)使用独立的Zustand store
* React Query/TanStack Query用于服务器状态 + Zustand用于客户端状态
* 一个带切片(slices)的Zustand store
请提供一个推荐的架构,并附带代码示例,展示如何构建store以及如何与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 介绍你的项目环境,这样它才能定制化输出。有些助手甚至可以将你的整个代码库作为上下文来源;如果使用这类助手,确保你将其指向代码库中相似的模块或文档。
3. 使用注释和 TODO 作为内联提示。如果是直接在带有 Copilot 的 IDE 来开发,一个有效的工作流是:写一条描述你接下来所需要的代码块的注释,然后让 AI 自动补全它。比方说,在 Node.js 后端,你可以写:`// TODO: 验证请求负载(确保提供了姓名和邮箱)`,然后开始新的一行。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 数组),将它们存储在状态中,并允许用户在搜索框中输入内容以按名称过滤显示的产品(不区分大小写匹配)。在一个简单的 `<ul>` 列表中显示过滤后的产品名称。包含基本的错误处理(比方说,如果 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;
(如果我们提出要求,代码中可能还会包含一些解释各部分的注释。)
现在,我们来分析这个结果,并考虑如何优化提示:
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 实现功能的核心在于渐进式开发和提示优化。一篇 Twitter 帖子可能会惊叹于某人如何通过持续、针对每一部分向 LLM 发出提示来构建一个小型应用——这本质上就是这种方法:开发、审查、优化、扩展。每个提示就像是开发过程中的一次提交(commit)。
实现功能的额外技巧:
* 让 AI 搭建脚手架,然后你填充细节:有时候让 AI 生成一个粗略的结构,然后你再进行调整是很有用的。比方说,“生成一个用于用户注册的 Node.js Express 路由的骨架,包含验证和错误处理。”它可能会生成一个带有占位符的通用路由。然后你可以填入特定于你的应用程序的实际验证规则或数据库调用。AI 省去了你编写样板代码的麻烦,而你可以处理那些敏感的自定义逻辑。
* 要求处理边界情况:在生成功能时,你可以提示 AI 考虑边界情况:**“我们应该为这个功能考虑哪些边界情况(并且你能在代码中处理它们吗)?”** 比方说,在搜索示例中,边界情况可能是“如果用户输入时产品数据还没加载完怎么办?”(尽管我们的代码通过加载状态处理了)或者“如果两个产品同名怎么办”(不是什么大问题,但也许可以提一下)。AI 可能会提到空结果处理、非常大的列表(搜索输入可能需要防抖)等。这是利用 AI 对常见陷阱训练的一种方式。
* 文档驱动开发:有些人的做法很妙,就是先写一个文档字符串(docstring)或用法示例,然后让 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
}
在介绍了用于调试、重构和新代码生成的提示策略之后,让我们把注意力转向编码提示工程中的一些常见陷阱和反模式。理解这些将帮助你避免在无效的交互上浪费时间,并在 AI 没有给出你需要的东西时快速调整。
不是所有提示的有效性都一样的。我们已经看过许多高效提示的案例,但识别反模式——那些导致AI表现不佳的常见错误——同样具有指导意义。
以下是几种常见的提示失误及其改进方法:
当出现问题时的提示重写策略:
了解这些反模式及解决方案后,调整实时提示就能更快。对开发者而言,提示工程本质是迭代的、反馈驱动的过程(如同所有编程任务!)。好消息是你现在拥有丰富的模式案例可供参考。
提示工程既是艺术也是科学——正如我们所见,它正迅速成为开发者使用AI编程助手的必备技能。通过构建清晰、上下文丰富的提示,你本质上是在教导AI你的需求,如同引导新团队成员或向同事解释问题。本文系统探讨了如何构建用于调试、重构和功能实现的提示:
将这些技巧融入工作流后,你会发现AI协作变得更直观。你将逐步掌握获取最佳结果的表达方式,以及模型偏离时如何引导。记住AI是其训练数据的产物——它见过无数代码和问题解决案例,但由你决定哪些案例与当前相关。本质上,你设定上下文,AI负责执行。
值得注意的是,提示工程是不断发展的实践。开发者社区持续发现新技巧——一个聪明的单行提示或结构化模板可能突然在社交媒体爆火,因为它解锁了人们未曾意识到的能力。关注这些讨论(如Hacker News、Twitter等)可激发个人技术灵感。同时勇于自我实验:将AI视为灵活工具——若有想法(如"让它用ASCII字符画架构图会怎样?"),尽管尝试。结果可能令你惊喜,即便失败也无妨——你由此了解了模型的局限或需求。
总之,提示工程可赋能开发者更高效利用AI助手。有没有提示工程,就是挫败体验("这工具没用,尽说废话")与高效体验("如同专家结对编程帮我写基础代码")之别。应用本文的策略手册——从提供完整上下文到引导AI风格思路——你就能将这些代码导向的AI工具转化为开发流程的真正延伸。最终结果不仅是编码提速,更常在过程中收获新见解和模式(当AI解释或建议替代方案时),从而提升自身技能水平。
最后一个关键点:记住提示是一场循序渐进的对话。要像跟其他工程师沟通时那么清晰、耐心和严谨地对待这场对话。如此你将发现AI助手能极大增强你的能力——助你更快地调试、更智能地重构、更轻松地实现功能。
祝你享受编写提示和代码的过程!
文章来自于“36氪网”。
【开源免费】n8n是一个可以自定义工作流的AI项目,它提供了200个工作节点来帮助用户实现工作流的编排。
项目地址:https://github.com/n8n-io/n8n
在线使用:https://n8n.io/(付费)
【开源免费】DB-GPT是一个AI原生数据应用开发框架,它提供开发多模型管理(SMMF)、Text2SQL效果优化、RAG框架以及优化、Multi-Agents框架协作、AWEL(智能体工作流编排)等多种技术能力,让围绕数据库构建大模型应用更简单、更方便。
项目地址:https://github.com/eosphoros-ai/DB-GPT?tab=readme-ov-file
【开源免费】VectorVein是一个不需要任何编程基础,任何人都能用的AI工作流编辑工具。你可以将复杂的工作分解成多个步骤,并通过VectorVein固定并让AI依次完成。VectorVein是字节coze的平替产品。
项目地址:https://github.com/AndersonBY/vector-vein?tab=readme-ov-file
在线使用:https://vectorvein.ai/(付费)
【开源免费】ai-town是MIT授权的一个AI虚拟小镇,该项目可以让研发人员轻松构建和定制你自己的AI小镇版本,其中居住在小镇的AI角色可以进行交流和社交。该项目受到研究论文《生成代理:人类行为的交互模拟》的启发。
项目地址:https://github.com/a16z-infra/ai-town
【开源免费】LangGPT 是一个通过结构化和模板化的方法,编写高质量的AI提示词的开源项目。它可以让任何非专业的用户轻松创建高水平的提示词,进而高质量的帮助用户通过AI解决问题。
项目地址:https://github.com/langgptai/LangGPT/blob/main/README_zh.md
在线使用:https://kimi.moonshot.cn/kimiplus/conpg00t7lagbbsfqkq0