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 等于盲飞。你需要:
- 一组覆盖正常情况和边界情况的测试输入
- 每个输入对应的期望输出(或期望满足的条件)
- 每次修改 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 迭代方式,也是最不可靠的。
感觉不可复现、不可度量。更好的方式是:
- 准备 10-20 个测试输入
- 修改 Prompt
- 跑一遍测试集,看通过率有没有变化
- 有变化就保留,没变化就回滚
第 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 章决定管道里流什么
下一篇开始拆第一条原则:任务描述设计——怎么把模糊的人类意图翻译成模型能稳定执行的指令。