上一篇我们搞懂了 LLM 的工作原理------它只会「预测下一个 token」。那么问题来了:既然它的全部能力就是续写,我们怎么让它乖乖按我们的意图输出?答案就是 Prompt 工程。这篇我们来系统拆解:怎么把"和模型说话"变成一门可控的工程手艺。
0. 为什么 Prompt 是你的"第二门编程语言"
上一篇结尾留了一个钩子:作为应用开发者,你控制模型行为的主要手段是 Prompt,不是训练、不是微调。这意味着什么?
意味着 Prompt 就是你写给模型的代码。
写前端时,你用 JavaScript 命令浏览器;做 AI 应用时,你用 Prompt 命令模型。区别在于:JavaScript 是精确的形式语言,少一个分号可能就报错;而 Prompt 是自然语言,它的"语法"是模糊的、概率性的------这恰恰是它难掌握的地方,也是"Prompt 工程"之所以是一门"工程"的原因。
很多前端同学第一次写 Prompt 的体验是:有时候效果好得惊人,有时候又完全跑偏,而且你不知道为什么。这篇要解决的,就是把这个"不知道为什么"变成"我知道为什么,而且能稳定复现"。
1. 先破除一个迷思:Prompt 不是"咒语"
网上有很多「神级 prompt」分享,看起来像魔法咒语------一大段神秘的设定、各种"你必须..."、"你是一个拥有 20 年经验的..."。很多人收藏了一堆,用的时候照抄,却发现效果忽好忽坏。
Prompt 不是咒语,是沟通。 你在和一个非常聪明、但没有上下文、且倾向于"猜你意思"的同事交接任务。想想你在公司里怎么给同事提需求的:
- 说清楚目标(要做什么)
- 给足背景(为什么做、限制条件)
- 提供参考(以前怎么做的、期望的样子)
- 规定交付格式(写成文档还是表格)
给模型写 Prompt,核心就是这四件事。把它当成一次"需求交接",你已经掌握了 80%。
2. Prompt 的骨架:四个组成部分
任何一个高质量 Prompt,拆开来看基本都由四块组成。我用前端的方式来类比:
| 组成部分 | 作用 | 前端类比 |
|---|---|---|
| 指令(Instruction) | 告诉模型要做什么 | 组件的 props / 函数签名 |
| 上下文(Context) | 提供背景信息 | 组件的 context / 全局配置 |
| 示例(Examples) | 用样例示范期望输出 | Storybook 里的 story |
| 输出格式(Format) | 约束返回的结构 | 函数的 return 类型 / TS 接口 |
下面逐个拆解。
2.1 指令:一句话说清要做什么
这是 Prompt 的核心。好的指令有几个特征:
- 动词开头,明确动作:「分类」「提取」「翻译」「总结」「生成」------别让模型猜你要什么。
- 单一目标:一个 Prompt 最好只解决一件事。想让模型同时"总结并翻译并评分",往往会每件都做不好。
- 正向表述:说"做什么"比说"不做什么"更有效。模型对"不要"的理解不如对"要"的理解可靠。
对比一下:
arduino
不好的:"帮我看看这段文字"
(做什么?看什么?看完输出啥?全是谜)
好的:"把下面这段用户反馈分类为「bug 反馈」「功能建议」「使用疑问」「其他」之一,只输出类别名称。"
(动作=分类,范围=四选一,输出=只输出类别名)
2.2 上下文:给模型补足"你才知道的信息"
模型见多识广,但它不知道你的业务。任何跟你的具体场景相关的信息,都需要你喂给它:
- 你在做什么产品、面向谁
- 处理的数据是什么格式、来自哪
- 有哪些约束(字数、语气、敏感词)
前端类比:这就像 React 的 Context Provider------你不注入,子组件(模型)就拿不到这些信息,只能用它的"默认值"(通用知识),而通用知识往往不符合你的业务。
2.3 示例(Few-shot):用样例教学
这是 Prompt 工程里最强大的技巧之一,单独一节讲(见第 3 节)。
2.4 输出格式:定义"返回类型"
这是工程化应用中最关键的一环。如果你的程序要解析模型的输出,你必须严格约束格式,否则模型可能今天返回 JSON、明天返回一段散文。
最基础的方式是直接描述:
xml
请按以下格式输出,不要输出任何其他内容:
类别:<分类结果>
置信度:<0-1的数字>
进阶方式是用结构化输出(Structured Outputs / JSON Schema),这会在第 4 章专门讲。现在你只需要记住一个原则:想要机器可用,就必须定义格式。
3. Few-shot:用示例教模型"照着做"
3.1 Zero-shot vs Few-shot
给模型任务的方式,按"给不给示例"分两类:
- Zero-shot(零样本):只描述任务,不给示例。模型靠自己的理解力去猜。
- Few-shot(少样本):给几个输入-输出的范例,模型"照葫芦画瓢"。
前端类比:Zero-shot 是你给同事一段口头描述就让他干活;Few-shot 是你给他两三个做好的样板,说"照这个来"。后者几乎总是更靠谱,尤其当你要求特定格式或特定风格时。
3.2 一个具体例子
假设我们要做情感分析,把用户评论分成正面/负面/中性。
Zero-shot 版本:
arduino
判断下面评论的情感倾向(正面/负面/中性):
"这个页面加载太慢了,等得我花都谢了"
模型大概率能做对,但输出格式不可控------它可能回"负面",也可能回"这条评论的情感倾向是负面的"。
Few-shot 版本:
arduino
判断评论的情感倾向。
评论:"界面很好看,用起来很顺手" → 正面
评论:"经常闪退,太烂了" → 负面
评论:"功能还行,就是没有夜间模式" → 中性
评论:"这个页面加载太慢了,等得我花都谢了" →
给完三个例子,模型不仅知道任务,还从示例中学会了输出格式(只要一个词)。你甚至不需要显式说"只输出一个词"。
3.3 Few-shot 的几条经验
- 示例数量:通常 2-5 个就够,不是越多越好。太多会占上下文、增加成本,边际收益递减。
- 示例要有多样性:如果全是正面例子,模型可能学会"什么都判正面"。正负中各给一个,覆盖你要区分的情况。
- 示例顺序会影响结果:模型对最后的示例印象更深(这是注意力机制的特性),把和当前输入最相似的示例放后面。
- 示例就是"隐式规范":你想让模型输出的格式、语气、粒度,最好在示例里体现出来,比口头描述有效得多。
3.4 什么时候用 Few-shot
| 场景 | 建议 |
|---|---|
| 任务简单、模型能力强 | Zero-shot 足够,省 token |
| 要求特定输出格式 | Few-shot,示例比描述有效 |
| 任务有特殊规则或风格 | Few-shot,用示例传递"潜规则" |
| 任务是分类/抽取等"有标准答案"的 | Few-shot 效果稳定 |
4. Chain-of-Thought:让模型"打草稿"
4.1 一个反直觉的现象
你让模型算一道稍微复杂的数学题,或者做一个需要推理的判断,它直接给答案时经常出错。但如果你让它先把推理过程写出来,正确率会大幅提升。
这不是玄学,这和 LLM 的本质有关------还记得上一篇说的吗?模型是「预测下一个 token」。当它直接预测"答案是什么"时,它是在"蒙"。但当它先预测"第一步推理是什么",再基于第一步预测"第二步是什么"......每一步都给后续推理铺了路,错误率就降下来了。
前端类比:这就像你让一个人心算 37 乘 24,他直接报答案容易错;但你让他在纸上列竖式一步步算,就不容易错。Chain-of-Thought 就是让模型"列竖式"。
4.2 怎么触发 CoT
最简单的方式,在指令里加一句:
请一步步思考,然后再给出最终答案。
或者用 Few-shot 的方式,给几个带推理过程的范例:
ini
问题:小明有 5 个苹果,给了小红 2 个,又买了 3 个,现在有几个?
推理:小明原来有 5 个,给出 2 个后剩 5-2=3 个,又买 3 个,3+3=6 个。
答案:6
模型看到这种"先推理后作答"的模式,就会照做。
4.3 CoT 的适用边界
CoT 不是万能的,它有明确的适用场景:
- 适合:数学计算、逻辑推理、多步骤决策、需要权衡的问题。
- 不适合:简单的分类、翻译、摘要------这些任务"想太多"反而可能引入错误,还白烧 token。
还有一点要权衡:CoT 会增加输出长度,也就增加成本和延迟。你的应用需不需要它,得拿效果和成本来换算。这也是为什么后面"评测"那一章很重要------你得能量化"加 CoT 到底提升多少"。
5. System Prompt:全局规则的大本营
上一篇我们讲了 system / user / assistant 三个角色。这里展开讲:怎么写好 system prompt。
5.1 System Prompt 该放什么
System prompt 是你应用的"宪法",定义了整个会话的基调。适合放在这里的内容:
- 角色设定:你是谁、服务谁、用什么语气("你是一个严谨的技术客服,回答简洁专业")
- 全局规则:贯穿整个会话的硬约束("不确定时明确说不知道,不要编造")
- 输出规范:默认的格式和长度要求
- 安全边界:不能做什么("不回答与产品无关的问题")
前端类比:它是你应用的根配置或全局 middleware------所有请求都会过一遍这些规则。
5.2 写 System Prompt 的几条原则
1. 具体胜过抽象
arduino
不好:"你是一个好助手"
好:"你是一个前端技术答疑助手,面向有 1-3 年经验的前端开发者。回答时优先给代码示例,解释要简洁,涉及原理时附上 MDN 链接。"
2. 用结构化格式
长 system prompt 建议用分段或列表写,而不是一整坨:
diff
你是一个代码审查助手。
职责:审查用户提交的 JavaScript 代码。
审查维度:
- 安全性(XSS、注入风险)
- 性能(不必要的重渲染、内存泄漏)
- 可读性(命名、结构)
输出格式:按维度列出问题,每条给出代码位置、问题描述、修改建议。
约束:如果代码没有问题,直接回复"代码看起来没问题",不要编造问题。
分段写的好处不只是模型理解得更好,也方便你自己维护和迭代------后面调试 prompt 时,你能精确定位是哪一段出了问题。
3. 设定"不知道"的兜底
LLM 最大的毛病之一是"幻觉"------不知道也硬编一个答案。在 system prompt 里明确约束"不确定时说不知道",能显著降低幻觉率。这一条几乎每个生产应用都该有。
5.3 System 和 User 的边界
一个常见错误是:把本该放在 system 里的全局规则,反复写在每条 user 消息里。这样既浪费 token,又容易不一致。
原则:稳定的、全局的规则进 system;变化的、一次性的信息进 user。
6. 常见反模式:这些坑你一定会踩
6.1 指令冲突:自相矛盾
arduino
不好:"请详细全面地回答,但不超过 50 个字。"
模型收到矛盾指令时会"猜"你更想要哪个,结果不可控。要么放宽字数限制,要么调整"详细"的要求。
6.2 模糊表述:缺少可执行标准
arduino
不好:"帮我优化一下这段文案"
(优化方向?目标受众?保持原意还是可以重写?)
好:"把这段文案改写得更适合社交媒体传播:口语化、每句不超过 15 个字、保留核心卖点。"
6.3 没有输出约束:格式漂移
这是工程化最大的坑。你不约束格式,模型就自由发挥------这次返回 JSON,下次返回"好的,结果是:..."。你的代码一解析就崩。
铁律:只要输出要给程序处理,就必须用格式约束(描述格式、给示例、或用 Structured Outputs)。
6.4 过度堆砌角色设定
arduino
不好:"你是一个拥有 30 年经验的谷歌首席工程师,MIT 博士,精通所有编程语言,获奖无数......"
冗长的头衔堆砌对效果提升有限,反而浪费 token。模型不需要你"夸"它,它需要你明确告诉它做什么、怎么做。
6.5 期望模型"记住"你没说的
arduino
不好:"按我们之前说的格式输出"
(它不记得你脑子里想的格式,你得显式写出来)
模型没有读心术。任何期望它"应该知道"的东西,都要写出来。
7. Prompt 调试方法:像管理代码一样管理 Prompt
Prompt 工程的"工程"二字,体现在你怎么管理和迭代它。这一节帮你建立正确的工作流。
7.1 变量化:把 Prompt 拆成模板
不要把整个 Prompt 写成一大段死文本。把可变的部分抽出来当变量,固定部分当模板------这和你写前端组件是一个思路。
js
// 伪代码:把 Prompt 当组件来设计
const prompt = buildPrompt({
system: SYSTEM_PROMPT, // 固定的全局规则
context: userContext, // 动态的上下文
examples: FEW_SHOT_EXAMPLES, // 固定的示例库
userInput: currentUserInput, // 用户本次输入
format: OUTPUT_FORMAT_SPEC, // 固定的格式约束
});
这样做的好处:改一处不影响全局,不同部分可以独立迭代和测试。
7.2 A/B 对比:一次只改一个变量
调试 Prompt 时最大的陷阱是"同时改了好几处"------效果变了,但你不知道是哪个改动起的作用。
正确做法:控制变量,一次只改一个地方。
- 改了 few-shot 示例?其他都不动,对比前后效果。
- 调了 temperature?prompt 不变,看输出差异。
- 用了 CoT?和不用的版本放一起跑同一批输入。
这也是为什么你需要一批固定的测试输入("黄金集"),每次改完 prompt 都用同一批数据跑一遍,才能客观比较。这一点会在第 10 章"评测"里展开。
7.3 版本管理:Prompt 也是资产
Prompt 不是写完就扔的一次性文本,它是你应用的核心资产,而且一定会反复改。所以:
- 把 Prompt 单独抽出来,不要和业务逻辑混在一起。
- 用 Git 管理你的 prompt,每次改动有 commit message 说明改了什么、为什么。
- 记录每个版本的"测试集表现",这样你能知道改了之后是变好还是变差。
前端类比:你不会把 API 配置硬编码散落在各处还不留版本记录------Prompt 也一样。很多团队甚至会专门搭一个 prompt 管理平台。对你个人来说,至少做到:prompt 独立成文件 + Git 管理。
7.4 一个实用的调试清单
当模型输出不符合预期时,按这个顺序排查:
- 指令清楚吗? 动词是否明确,目标是否单一。
- 格式约束了吗? 有没有明确说"只输出 XXX"。
- 要不要加示例? 试试用 few-shot 替代纯描述。
- 要不要 CoT? 如果是推理任务,加一句"一步步思考"。
- 是不是 temperature 太高? 需要稳定输出时降到 0-0.3。
- 上下文够不够? 模型是不是缺少必要的背景信息。
- 有没有指令冲突? 仔细读一遍,有没有自相矛盾的要求。
8. 小结与下一步
这篇我们把"和模型说话"拆成了一套可控的方法论:
- Prompt = 指令 + 上下文 + 示例 + 输出格式,四块各司其职。
- Few-shot 用示例教模型格式和风格,比口头描述更可靠。
- Chain-of-Thought 让模型先推理再作答,适合复杂推理任务,但要权衡成本。
- System Prompt 是全局规则的大本营,具体、结构化、带兜底。
- 反模式要避开:指令冲突、模糊表述、无格式约束、过度堆砌、期望模型读心。
- 调试要工程化:变量化、控制变量 A/B、版本管理。
核心理念一句话:Prompt 不是写出来的,是调出来的。 像调代码一样调 prompt------有假设、有对照、有度量。
下一篇我们正式进入代码实战:用 Next.js + Vercel AI SDK 把这些 prompt 变成真实可运行的应用,并打通流式输出这条链路。从"在 Playground 里调"到"在代码里跑",这是从"懂概念"到"能做产品"的关键一跃。我们下篇见。