每个 AI 编码工具都跑着同一个核心循环:你给出文本,模型生成代码。听上去简单,直到你意识到你给出的文本——prompt——是你对产出唯一的杠杆。模型是固定的;上下文窗口是有限的;你唯一能控制的变量,是你往里放什么。然而多数工程师把 prompting 当作事后想起的小事,敲一行字,然后纳闷输出为什么不对劲。

本文专门深入剖析面向代码生成与 agentic 编码任务的 prompt engineering。它覆盖“为什么 prompt 决定输出质量”的机理、那些能稳定抬高水准的具体技巧——上下文、约束、规划、few-shot 示例、迭代精修、测试驱动 prompting——以及那些悄悄产出平庸或破碎代码的反模式。读完你应该拥有一套可复用的心智模型,去 prompt 任何 AI 编码工具,从 Copilot 补全到 Claude Code 的 agentic 任务。

⚡ 速览要点
  • 上下文是倍增器。给模型对的文件、真实的报错、真实的约束,胜过任何其他单项技巧。
  • 先规划再写码。让模型先勾勒它的做法,能在设计错误被嵌进 200 行实现之前抓住它们。
  • few-shot 示例瓦解歧义。一个具体的“给定 X、产出 Y”示例,比一整段散文描述同样的东西更清楚。
  • 陈述验收标准,而非只有目标。“加认证”是目标;“加一个 JWT middleware,拒绝无有效 token 的请求并返回带 JSON 错误体的 401”是可测试的规格。
  • 用有针对性的追问来迭代。一个超长 mega-prompt 很少胜过一个干净的初始 prompt 加几轮聚焦的纠正。
  • 含糊的 prompt 产出看似合理的错误代码。模型永远不会告诉你 prompt 有歧义——它只会幻想出一个看似合理的答案。
tldr

为代码写 prompt 是一门技能,不是窍门。核心公式是:对的上下文 + 清晰的规格 + 验收标准 + 先规划 + few-shot 示例。把这五样配齐,模型输出会显著改善。略过它们,你得到看似合理、却悄悄违反你约束的代码。

为什么 prompt 决定代码质量

大语言模型本质上是一个以上下文窗口内一切为条件的下一 token 预测器。它对你的 repo、你团队的约定、你在 Slack 里提过的生产约束、上个 sprint 烧到你的边界情况,毫无背景知识。它知道的,只有你此刻给它的。

这有个深刻含义:模型的输出受其输入质量所限。一个前沿模型配上烂 prompt,会比一个小模型配上极好的 prompt 产出更糟的代码,因为小模型至少在用准确、完整的信息工作。上下文组装——选择往 prompt 里放什么——是输出质量的主导因素,而它完全在你掌控之中。

还有一个你需要理解的失败不对称性:模型总会产出点什么。它不会告诉你 prompt 太含糊;它会用统计上合理的补全填补空白。在代码语境里,那意味着看似合理的代码——可能编译、能过表面阅读,却仍以只在生产中浮现的方式微妙地错。这正是为什么一个激进 vibe-coding 的初级工程师,能在技术债凝结前显得高产好几周。

因此,面向代码的 prompt engineering 无关魔法咒语。它关乎系统性地消除模型的不确定性:给它需要的文件、它必须修的报错、它必须尊重的约束、以及展示它该匹配什么风格的示例。本文每个技巧都减少一种不同的不确定性。

给模型足够的上下文

你能做的最高杠杆的事,也是最机械的:贴对代码。多数工程师贴得太少。他们该贴函数时用散文描述函数;他们该贴完整栈追踪时只提一句报错;他们该从“auth 模块”里贴出相关五十行时只说一句“auth 模块”。

该纳入什么

不该纳入什么

一个有用的启发法:若一位新队友正与你结对做这个确切任务,你会往共享屏幕上放什么?贴那个。不少于它,也不无端多于它。

prompt — 糟糕的上下文
# ❌ 含糊 —— 模型必须猜“the auth middleware”长什么样
Fix the bug in our auth middleware where sometimes
tokens are accepted even when expired.
prompt — 良好的上下文
# ✅ 模型有了做出精确修复所需的一切
Here is our JWT middleware (Go):

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenStr := r.Header.Get("Authorization")
        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenStr, claims, keyFunc)
        if err != nil || !token.Valid {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Bug: tokens that are expired (Claims.ExpiresAt in the past) are
sometimes accepted. jwt.ParseWithClaims does check expiry, but we
strip the "Bearer " prefix inconsistently — see the raw header value
in the failing test output below:

    Authorization: Bearer eyJhbGci...   <-- has "Bearer " prefix
    jwt: token is malformed              <-- parse error swallowed, falls through

Fix the prefix stripping and make sure an expired token always 401s.

用验收标准规约需求

目标规格之间存在类别差异。“加限流”是目标。规格告诉模型“完成”长什么样:哪些端点、什么限额、哪个 header 携带剩余计数、耗尽时什么状态码、重置窗口多长、限额是按 IP 还是按用户。模型读不到你的心思;你不规约,就会得到模型的默认假设,而那可能不匹配你的。

好的验收标准有两个性质:它们具体(名称、数字、状态码、字段名),它们可测试(你能写出一个当且仅当标准被满足时才通过的测试)。若你为它写不出测试,这条标准多半太含糊。

含糊的目标可测试的规格
给 user 端点加输入校验 POST /users 在 email 缺失或非 RFC 5322 格式时必须返回 422 带 {"error":"email_invalid"};name > 100 字符时返回 422 {"error":"name_too_long"}
让它更快 listProducts 查询在 1 万行时 p99 必须 <50 ms;若缺失则在 (category_id, created_at DESC) 上加索引
更好地处理错误 包装所有 db.Query 调用,在 ERROR 级记录 query=<sql> err=<msg> duration=<ms>;把原始错误向上传播,绝不吞掉
加缓存 把 GET /products/:id 缓存到 Redis,TTL 300 s;用 key product:<id>;缓存未命中时从 DB 取并回填;DB 返回 404 时不缓存

右列字更多,但它产出可度量为正确的代码。左列产出貌似正确的代码——那是非常不同的两回事。

先要计划

对非平凡任务,最可靠的技巧之一是两步序列:先让模型勾勒其做法,再让它实现。这能在设计问题被嵌进代码之前抓住它们,也迫使模型对问题做推理,而非模式匹配到最近的样板。

规划 prompt 通常很短:“写任何代码之前,勾勒你实现 X 要采取的步骤。列出你做的任何假设。指出我该留意的任何边界情况。”读这个计划。若计划错了——做法错、库错、误解了需求——在实现前纠正它。两分钟的计划评审省下二十分钟的调试。

两步 prompting
## Step 1 — 计划 prompt
I need to add distributed rate limiting to our Go API gateway.
Requirements:
- 100 req/min per API key, sliding window
- Limits stored in Redis; gateway pods are stateless
- On limit exceeded: 429 with Retry-After header (seconds until window resets)
- Keys are passed in X-API-Key header

Before writing any code, outline:
1. The algorithm you'll use (token bucket? sliding log? fixed window?)
2. The Redis data structure and key schema
3. The middleware interface in Go
4. Any edge cases (key missing, Redis down, clock skew)

## Step 2 — 实现 prompt(评审计划之后)
The plan looks good. Implement it.
Use go-redis v9. The middleware should be chainable with our existing
http.Handler chain. Do not introduce a global singleton — accept a
*redis.Client as a parameter so it can be injected in tests.

先规划对模型将自主执行多步的 agentic 任务尤其有价值。一个立刻开始实现的 agent 会在你注意到之前深入一条错误路径。一个先呈现计划的 agent,让你在可能最便宜的时刻重定向。

Few-Shot 示例:展示,而不只是讲述

few-shot prompting——在要真东西之前提供一个或多个输入/输出示例——是 prompt engineering 里最古老、最可靠的技巧之一。对代码它尤其强大,因为代码无歧义:单个示例同时钉死命名约定、缩进、错误处理风格、返回类型模式、日志格式,这是成段描述永远做不到的。

few-shot 何时回报最大

few-shot — 生成风格一致的 handler
// 示例 handler(现有代码 —— 作为 few-shot 示例贴上)
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user, err := h.store.GetUser(r.Context(), id)
    if errors.Is(err, store.ErrNotFound) {
        h.writeError(w, http.StatusNotFound, "user_not_found")
        return
    }
    if err != nil {
        h.writeError(w, http.StatusInternalServerError, "internal_error")
        return
    }
    h.writeJSON(w, http.StatusOK, user)
}

// 贴完示例后的 prompt:
// Following exactly the same pattern above (chi router, h.store,
// h.writeError / h.writeJSON, errors.Is for not-found),
// write GetProduct and DeleteProduct handlers.

关键纪律:贴你代码库里的真实示例,而非杜撰的。杜撰的示例会意外引入你其实不用的约定。

规约语言、风格与边界

AI 编码模型是多语言的。没有明确指示,它们会挑它们认为对该任务最常见的语言、库和风格。那可能不是你的语言、你的库、你的风格。这些要紧时,永远显式陈述。

语言与运行时

版本要紧时规约语言版本:Go 1.22Python 3.12 with type annotationsTypeScript 5 strict modeJava 21 with records and sealed interfaces。模型知道每个版本有哪些特性可用,会据此避免或使用它们。

库与框架

指名具体的库:use pgx/v5 not database/sqluse Zod for validation not Joiuse React Query v5 not SWR。没有这个,模型会挑它训练得最多的那个,而那可能与你现有的依赖树冲突。

什么在范围之外

边界约束和正向需求一样重要。“不要加任何新依赖”“不要改这个函数的公开接口”“不要加数据库迁移——这必须在应用层处理”“不要碰测试文件”。这些负向约束,防止模型“热心地”重构你没让它碰的东西——一个非常常见的失败模式。

风格与边界约束
# 显式约束防止“热心”漂移
Add a retry wrapper around the S3 upload call in upload.go.

Constraints:
- Language: Go 1.22
- Use only stdlib (context, time, errors) — do NOT add a retry library
- Max 3 attempts, exponential backoff starting at 100 ms, cap at 2 s
- Do not change the function signature of UploadFile
- Do not modify upload_test.go
- Log each retry attempt at WARN level with attempt=N err=<msg> using
  our existing slog.Default() logger

精确地迭代与追问

好的 prompting 是对话,不是独白。第一个响应很少完美;问题是如何高效纠正它。最糟的做法是用一个更长的 mega-prompt 重头来过。最好的做法是一个简短、外科手术式的追问,精确点名哪里错了。

一个好的纠正的解剖

一个有价值的元技巧:拿到输出后,让模型批评自己的工作。“这个实现漏了哪些边界情况?”或“你做了哪些可能不成立的假设?”被直接问到时,模型出奇地擅长找出自己的漏洞。

测试驱动 prompting

最严格的 prompting 工作流借鉴 TDD:先规约测试,再让模型让它们通过。这迫使规格精确(测试无歧义),并给模型一个它能用来验证自身输出的自动反馈回路。

测试驱动 prompting 工作流
## Step 1: 自己写测试(或先 prompt 出测试)
func TestParseISO8601Duration(t *testing.T) {
    cases := []struct{ input string; want time.Duration; wantErr bool }{
        {"PT30S",   30 * time.Second,  false},
        {"PT1M30S", 90 * time.Second,  false},
        {"P1D",     24 * time.Hour,    false},
        {"P1Y",     0,               true},  // years not supported
        {"",        0,               true},
        {"garbage", 0,               true},
    }
    for _, c := range cases {
        got, err := ParseISO8601Duration(c.input)
        if (err != nil) != c.wantErr {
            t.Errorf("%q: wantErr=%v got err=%v", c.input, c.wantErr, err)
        }
        if !c.wantErr && got != c.want {
            t.Errorf("%q: want %v got %v", c.input, c.want, got)
        }
    }
}

## Step 2: prompt 对照测试实现
// Implement ParseISO8601Duration(s string) (time.Duration, error)
// in duration.go so all cases above pass. Do not add dependencies.

用 Claude Code 这类 agentic 工具时,你可以更进一步:“实现后跑测试并迭代,直到全部通过。”agent 自动闭合反馈回路,你只在测试变绿时审查最终 diff。

为模型分解大改动

上下文窗口有限,且注意力在长而复杂的任务上退化。一个触及十五个文件、重组数据模型、更新三个 API 面的改动不是一个 prompt——它是五六个。把大改动拆成聚焦、可独立审查的步骤,产出更好,也让每步更易验证。

一个分解启发法

当改动确实大时,把分解明确写出来并贴进第一个 prompt:“我要分四步做这个改动。这是第 1 步。只实现第 1 步。”这防止模型投机地实现第 2–4 步,造出你审不动的 diff。

常见反模式

理解失败模式和理解最佳实践一样有用。这些是稳定产出糟糕输出的 prompting 反模式。

好高骛远的含糊 prompt

“重构这个让它更干净、更可维护。”这给模型无限自由去重组任何它认为次优的东西。你会得到大量改动,可能合也可能不合你的约定,碰到你本不想碰的代码。要具体说明“更干净”是什么意思:“把 processPayment 里三层嵌套的 if 块抽成有名字的辅助函数;别改任何其他逻辑。”

复制粘贴的 cargo cult

贴一大块代码加一句“修 bug”。没有症状描述、复现用例、或错误出现在哪一行,模型只能猜。它可能猜对,也可能“修”了代码别的部分并引入回归。永远纳入可观察的失败:栈追踪、失败测试输出、错误的返回值。

缺失的负向约束

要新功能却不规约什么不能变。模型常会“改进”相邻代码、重命名它觉得困惑的变量、或加它认为标准的依赖。这些没要的改动搅浑你的 diff,还能引入隐蔽的破坏。对任何需要保持稳定的东西,永远纳入“不要改 X”。

一次性 mega-prompt

试图在单个巨型 prompt 里完整规约一个复杂 feature。这超载了模型的指令遵循能力;靠后的约束相对靠前的被赋予过低权重。对复杂工作,迭代:prompt 出计划、批准、prompt 第 1 步、审查、继续。总质量更高,即便总回合数更多。

反模式 vs. 正确的 prompt
## ❌ 反模式:含糊 + 无上下文 + 无约束
Add pagination to the API.

## ✅ 正确:上下文 + 规格 + 约束 + 验收标准
File: handlers/products.go (pasted below)
Current GET /products returns all rows, which causes OOM at scale.

Add cursor-based pagination:
- Query params: limit (int, default 20, max 100) and cursor (opaque string)
- Response: add "next_cursor" field to the existing JSON envelope;
  null if no more pages
- Cursor encodes the last row's (created_at, id) as base64 JSON —
  do not use offset
- Return 400 {"error":"invalid_limit"} if limit < 1 or > 100
- Do not change the shape of the Product objects in the response
- Do not add any new SQL queries beyond what listProducts already uses;
  add the WHERE clause to the existing query

[paste handlers/products.go here]
[paste store/products.go here]

不加审查就接受第一个输出

这本身不算 prompting 反模式——它是工作流反模式——但值得在此点名,因为它正是那个让上述所有技巧失效的失败模式。每一块 AI 生成的代码都应被阅读、理解、并有意识地接受。若你讲不清一个函数做什么,你就还没准备好 merge 它。模型是你的结对程序员,不是你的代码审查者。

随时间建立 prompting 直觉

prompting 是一门会复利的技能。用了一年 AI 编码工具的工程师,写出的 prompt 比起步时好得多,因为他们已内化了哪些技巧闭合哪些缺口、要预防哪些失败模式。

几个加速这条学习曲线的习惯:

prompt engineering 最好的心智模型:你在写一份将由一位极有能力但完全天真的承包商执行的规格。他们会严格照你说的做,以最常见的方式解读歧义,且从不要求澄清。据此写规格——无歧义、完整、并对“不要做什么”有明确约束。

takeaway

面向代码的 prompt engineering 其实是规格工程。这些技巧——上下文、验收标准、先规划、few-shot 示例、分解、精确追问——都服务于一个目标:减少模型的不确定性,使它可观的能力被精确瞄准你真正的问题。把规格做到位,代码质量随之而来。

🎯 面试尖锐观点

为什么在 AI 编码工具里上下文比模型大小更重要?上下文窗口有限;模型只能对窗口里的东西做推理。给它对的文件和报错,胜过一个用含糊散文工作的更大模型。
什么是先规划 prompting,为什么有帮助?让模型在写码前勾勒做法,在可能最便宜的时刻浮现设计错误——在它们被嵌进实现之前——并迫使模型推理而非模式匹配。
最危险的 AI 编码反模式是什么?不加审查就接受第一个输出。即便模型已微妙地违反你的约束,它也会产出看似合理的代码,而“貌似合理却错误”比明显的错误更难抓。

← 上一篇
Copilot vs Cursor vs Claude Code