01-Prompt工程基本原则

要点

  • 第 12 章把聊天接口搭起来了,但 system_prompt 里写什么、怎么写,还停在「你是一个助手」这种笼统话上
  • Prompt 不是文案,是一份给模型的任务说明书——它决定模型输出质量的下限
  • Prompt 工程的核心问题是:怎么把模糊的人类意图翻译成模型能稳定执行的指令
  • 五个基本原则:明确任务、给足上下文、约束输出、提供示例、可迭代验证
  • Prompt 不是写完就结束——它需要版本管理、测试集和回归验证,和第 20 章的测试体系是同一套思路
  • 这一章先把原则和全景铺开,后续 13 节会逐层展开每个原则的具体做法

1. 从第 12 章的一个细节说起

第 12 章写聊天接口时,我们留了一个口子:

// src/routes/chat.ts — 第 12 章的骨架
chatApp.post('/chat', authMiddleware, rateLimit, async (c) => {
  const body = await c.req.json()
  const { model, messages, stream, system_prompt } = body
 
  const context = buildContextWindow(messages, getModelContextLimit(model))
  if (system_prompt) context.unshift({ role: 'system', content: system_prompt })
 
  const result = await callModelAPI({ model, messages: context })
  // ...
})

这里 system_prompt 是前端传过来的。实际项目里,这个值通常不该由前端决定——前端不该关心模型应该怎么「思考」。更合理的做法是后端根据业务场景拼出 system prompt,前端只传用户消息和业务参数。

但这就引出一个问题:后端拼什么内容进去?

「你是一个专业的 AI 助手,请准确回答用户的问题。」——这种话写了等于没写。模型读完之后该怎么做,和没读差不多。

换一个稍微具体点的:

「你是一个代码审查助手。用户会提交 TypeScript 代码片段。你需要:检查类型安全、找出潜在 bug、给出修改建议。输出用 Markdown 格式。」

方向对了,但还会遇到这些情况:

  • 用户提交了一段 500 行的代码,模型逐行评论,输出 3000 字,根本没法用
  • 用户提交了一段没有 bug 的代码,模型硬凑了几条「建议」
  • 同一段代码,每次审查的格式和重点不一样

问题出在哪?不是模型能力不够,是指令写得不够好。这就是 Prompt 工程要解决的问题。

2. Prompt 不是文案

很多人第一次写 Prompt 时,会按写文案的思路来:措辞优美一点、语气友善一点、多用排比句。

这个方向从一开始就偏了。

模型不关心你的措辞是否优美。它关心的是:要做什么、有哪些约束、输出应该长什么样。

一个更准确的类比是:Prompt 是一份任务说明书。你交给一个实习生做事,说明书写得越清楚,结果越稳定。写得越模糊,结果越看运气。

来看一个对比:

// 文案思路的 Prompt ❌
const prompt = `
你是一个非常聪明的 AI 助手,拥有丰富的编程经验。
请用你的专业知识帮助用户解决编程问题,给出详细而有深度的回答。
记住,要尽可能帮助用户理解问题的本质。
`
 
// 任务说明书思路的 Prompt ✅
const prompt = `
你是一个 TypeScript 代码审查助手。
 
输入:用户提交的 TypeScript 代码片段。
 
你需要完成以下检查:
1. 类型安全问题(any 滥用、类型断言、空值未处理)
2. 逻辑错误(边界条件、循环终止、异步错误处理)
3. 性能问题(不必要的循环、内存泄漏风险)
 
输出格式:
- 如果没有问题,输出「代码审查通过,无需修改」
- 如果有问题,按严重程度排序,每个问题用以下格式:
  **[严重/警告/建议]** 问题描述
  代码位置:第 N 行
  修改方案:(代码块)
 
不要编造不存在的问题。如果代码量超过 200 行,只报告严重和警告级别的问题。
`

第二个 Prompt 更长,但每一句都在回答模型的一个具体问题:该做什么、做到什么程度、输出什么格式、遇到边界情况怎么处理。

3. 五个基本原则

把 Prompt 工程的经验收拢,可以归纳成五条原则。后续 13 章的每一节,基本都是在回答这五条原则中某一条的具体做法。

原则一:明确任务

模型需要知道它要完成的是什么任务。任务越具体,输出越稳定。

// 模糊 ❌
const prompt = '分析一下这段代码'
 
// 具体 ✅
const prompt = '检查以下 TypeScript 函数是否存在类型安全问题。只报告 any 类型滥用和未处理的 null/undefined 两种情况。'

「分析」是一个开放任务——模型可以分析风格、结构、性能、可读性,每次选的方向可能不同。「检查类型安全」是一个封闭任务,模型知道该看什么。

原则二:给足上下文

模型不知道你的业务背景、数据格式、上下游关系。你需要告诉它。

// 缺少上下文 ❌
const prompt = '把这个字段名改成更合适的名字'
 
// 补全上下文 ✅
const prompt = `
以下是一个电商订单系统的 TypeScript 类型定义。
字段命名规范:
- 使用 camelCase
- 金额字段后缀 Amount,单位是分(integer)
- 时间字段后缀 At,使用 ISO 8601 字符串
- 布尔字段用 is/has/can 前缀
 
请根据以上规范,修改以下类型定义中的不规范字段名:
${typeDefinition}
`

没有上下文时,模型会按自己的理解来。有了上下文,模型才能按你的规范来。

原则三:约束输出

你不约束输出格式,模型就会自由发挥。在工程场景里,自由发挥意味着下游代码无法稳定解析。

// 不约束格式 ❌ — 模型可能输出散文、列表、带标题的 Markdown
const prompt = '列出这段代码的三个问题'
 
// 约束格式 ✅ — 下游 JSON.parse 能稳定执行
const prompt = `
列出这段代码的问题,以 JSON 数组输出,每个元素结构如下:
{
  "severity": "error" | "warning" | "suggestion",
  "line": number,
  "message": string,
  "fix": string | null
}
只输出 JSON,不要添加其他说明。
`

第 06 节会专门讲输出格式约束的设计。

原则四:提供示例

Few-shot 是最稳定的质量提升手段。给模型看一两个符合预期的输入输出对,比多写一段解释更有效。

const prompt = `
将用户的自然语言需求翻译成 SQL 查询。
 
示例:
用户需求:「最近 7 天注册的用户数」
SQL:SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL '7 days'
 
用户需求:「订单金额超过 1000 元的用户」
SQL:SELECT DISTINCT user_id FROM orders WHERE amount > 100000
 
用户需求:${userRequirement}
SQL:
`

示例不需要多,两三个就能把格式和粒度对齐。第 07 节会展开讲示例的选择和组织策略。

原则五:可迭代验证

Prompt 不是一次写定的产品。模型会升级、业务会变、边界情况会冒出来。

没有测试集的 Prompt 等于盲飞。你需要:

  1. 一组覆盖正常情况和边界情况的测试输入
  2. 每个输入对应的期望输出(或期望满足的条件)
  3. 每次修改 Prompt 后跑一遍,确认没有回归

这套思路看起来眼熟——和软件测试是同一套逻辑。第 11 和 12 节会讲怎么搭 Prompt 测试集和回归测试。

4. 一个最小可运行的例子

把五条原则合在一起,写一个实际能在 Hono 项目里跑的 Prompt 服务:

// src/services/prompt/code-review.ts
import { Hono } from 'hono'
 
// 把五条原则落到一个具体 Prompt 上
const CODE_REVIEW_PROMPT = `你是一个 TypeScript 代码审查助手。
 
## 输入
用户提交的 TypeScript 代码片段。
 
## 检查范围
按优先级排序:
1. 类型安全问题:any 滥用、未标注返回值类型、不安全的类型断言
2. 空值处理:未处理的 null/undefined、可选链缺失
3. 异步错误:await 缺少 try-catch、Promise 未处理 rejection
 
## 输出格式
JSON 数组,每个元素:
{
  "severity": "error" | "warning" | "suggestion",
  "line": number,
  "column": number | null,
  "message": string,
  "fix": string | null
}
 
## 约束
- 没有问题时输出空数组 []
- 不要编造不存在的问题
- 最多报告 10 个问题,按严重程度排序
- 只输出 JSON,不要 Markdown 代码块标记`
 
export type ReviewResult = {
  severity: 'error' | 'warning' | 'suggestion'
  line: number
  column: number | null
  message: string
  fix: string | null
}
 
export async function reviewCode(code: string): Promise<ReviewResult[]> {
  const messages = [
    { role: 'system', content: CODE_REVIEW_PROMPT },
    { role: 'user', content: code },
  ]
 
  const response = await callModelAPI({
    model: 'gpt-4o',
    messages,
    temperature: 0,  // 审查任务要确定性,不要随机性
    response_format: { type: 'json_object' },
  })
 
  // response_format 保证了输出是合法 JSON,但内容结构还要自己校验
  return parseReviewResult(response.choices[0].message.content)
}
 
function parseReviewResult(raw: string): ReviewResult[] {
  try {
    const parsed = JSON.parse(raw)
    if (!Array.isArray(parsed)) return []
    return parsed.filter(isValidReviewResult)
  } catch {
    return []
  }
}
 
function isValidReviewResult(item: unknown): item is ReviewResult {
  if (typeof item !== 'object' || item === null) return false
  const obj = item as Record<string, unknown>
  return (
    (obj.severity === 'error' || obj.severity === 'warning' || obj.severity === 'suggestion') &&
    typeof obj.line === 'number' &&
    typeof obj.message === 'string'
  )
}
 
// Hono 路由
const app = new Hono()
 
app.post('/review', async (c) => {
  const { code } = await c.req.json()
  if (typeof code !== 'string' || code.length === 0) {
    return c.json({ error: 'code is required' }, 400)
  }
  if (code.length > 5000) {
    return c.json({ error: 'code too long, max 5000 chars' }, 400)
  }
 
  const results = await reviewCode(code)
  return c.json({ results })
})
 
export default app

这段代码把五条原则都落到了具体实现里:

原则落地方式
明确任务Prompt 开头声明「你是 TypeScript 代码审查助手」,列出三类检查范围
给足上下文检查范围按优先级排序,告诉模型该看什么、不该看什么
约束输出JSON schema + response_format 双重保障
提供示例这个例子里没有放 few-shot,第 07 节会补上
可迭代验证Prompt 是常量,方便后续加测试集和版本管理

5. 一些常见的误区

在展开每一条原则之前,先列出几个容易踩的坑。后面各节会逐个拆。

误区一:Prompt 越长越好

Prompt 的长度和效果没有线性关系。一个 3000 字的 Prompt 不一定比 300 字的好。

关键不是长,是该有的信息有、不该有的信息没有。信息冗余反而会稀释关键指令的权重。

误区二:把 Prompt 当代码写

有些人会写出这样的 Prompt:

IF 用户输入包含代码 THEN 执行代码审查
ELSE IF 用户输入包含问题 THEN 回答问题
ELSE 闲聊

模型不是解释器,它不按 if-else 分支执行。更自然的写法是列出场景和对应的处理方式:

根据用户输入的类型,执行对应任务:

- 代码片段:执行代码审查,检查类型安全和空值处理
- 编程问题:解释原理,给出示例代码
- 其他:礼貌地引导用户回到编程话题

误区三:改 Prompt 靠感觉

「感觉这次回答不太好,改几个词试试。」——这是最常见的 Prompt 迭代方式,也是最不可靠的。

感觉不可复现、不可度量。更好的方式是:

  1. 准备 10-20 个测试输入
  2. 修改 Prompt
  3. 跑一遍测试集,看通过率有没有变化
  4. 有变化就保留,没变化就回滚

第 11 节会讲怎么搭这个测试集。

误区四:Prompt 一次写定

模型供应商会更新模型版本。同一个 Prompt 在 GPT-4o-2024-05-13 和 GPT-4o-2024-08-06 上的表现可能不同。

Prompt 需要跟着模型版本一起管理。这就是第 10 节要解决的问题。

6. 本章在整体中的位置

第 12 章把聊天接口搭好了——认证、限流、流式响应、上下文截断,管道已经通了。

但管道里流的是什么,取决于 Prompt。这一章就是来解决这个问题的。

后续章节的展开顺序是:

02 任务描述设计   →  怎么把模糊需求翻译成明确指令
03 角色设定设计   →  system prompt 怎么写
04 上下文注入     →  怎么给模型喂背景信息
05 约束条件设计   →  怎么限制模型的行为边界
06 输出格式约束   →  怎么让输出稳定可解析
07 Few-shot      →  怎么用示例提升质量
08 Chain of Thought → 什么时候该让模型「想一想」
09 结构化模板     →  怎么把 Prompt 做成可复用的工程组件
10 版本管理      →  怎么管理 Prompt 的变更
11 测试集        →  怎么度量 Prompt 质量
12 回归测试      →  怎么保证修改不引入退化
13 安全防护      →  怎么防止 Prompt 被注入
14 管理平台      →  怎么把以上能力整合成平台

前三章(02-04)解决的是「怎么写一个好 Prompt」,中间三章(05-08)解决的是「怎么把 Prompt 写得更精确」,后面七章(09-14)解决的是「怎么把 Prompt 当工程组件管理」。

总结

回顾一下这篇的要点:

  • Prompt 是任务说明书,不是文案——它决定输出质量的下限
  • 五个基本原则:明确任务、给足上下文、约束输出、提供示例、可迭代验证
  • 常见误区:越长越好、当代码写、靠感觉改、一次写定
  • Prompt 工程和软件工程共享同一套思路:版本管理、测试集、回归验证
  • 第 12 章搭好了管道,第 13 章决定管道里流什么

下一篇开始拆第一条原则:任务描述设计——怎么把模糊的人类意图翻译成模型能稳定执行的指令。