Prompt-Engineering入门
> **本文适合谁**
Prompt Engineering 入门:让 LLM 听话的艺术
本文适合谁
已经能调用 API 拿到输出,但效果时好时坏、不知道该如何系统改进的开发者。本文给你一套可重复的 Prompt 设计框架,让调 Prompt 像调 bug 一样有章可循。
同一个模型,换不同的 Prompt,效果差距可以极大。以代码审查为例:
差的 Prompt:
这段代码有没有问题?
好的 Prompt:
你是一名有十年经验的 Java 后端工程师,擅长并发编程。
请分析以下代码是否存在线程安全问题,重点检查:共享变量的访问、锁的使用、可见性问题。
用中文回答,格式:问题描述 + 代码位置 + 修复建议。
差的 Prompt 可能返回一堆泛泛而谈。好的 Prompt 会直接指向具体行号,告诉你哪里没加 volatile,哪里的 HashMap 应该换成 ConcurrentHashMap。
这就是 Prompt Engineering 存在的意义。
1.1 为什么 LLM 是"续写"不是"理解"
1.1.1 先建立这个直觉
要理解为什么 Prompt 格式如此重要,需要先记住 LLM 的本质:它不是在"理解"你的问题,它是在"续写"你的 Prompt。
当你输入"帮我写一段代码",模型预测的是:在这段文字之后,互联网上最可能出现什么样的文字。如果你的 Prompt 和互联网上高质量代码示例的上下文很像,模型就会给你高质量的代码;如果你的 Prompt 和随便一段网络灌水更像,模型就会给你相应质量的输出。
这个认知有两个直接推论:
- 给出示例比描述要求更有效——你在引导模型往哪个"文本空间"续写
- 格式要求放在前面比放在后面有效——模型在生成过程中不断参考之前的内容,越早出现的约束越强
1.1.2 为什么描述角色有效
"你是一名资深 Java 架构师,有十年微服务开发经验"——这句话不是魔法。
它的作用是:把模型接下来的"续写"限定在"专业 Java 架构师写的文字"这个分布里。这种文字通常不会解释什么是 Spring,而是直接讨论 BeanDefinition 或 Netty 的线程模型。
角色设定在统计意义上让模型的输出更贴近那个角色的语言风格和知识体系。对于专业技术问题,这个差距是显著的。
1.2 Prompt 设计的四个维度
一个好的 Prompt 需要在四个维度上都清晰:
1.2.1 维度一:角色定义
为什么有效:帮模型确定"用什么知识分布来回答"。
好的做法:
你是一名有十年经验的 Java 后端工程师,熟悉 Spring Boot、高并发编程和微服务架构。
差的做法:
你是一个 AI 助手,帮助用户解决问题。(太泛,等于没写)
角色定义要具体,最好带上量化信息(年限、技术栈)和专业背景。
1.2.2 维度二:任务描述
为什么有效:缩小模型的输出搜索空间,减少歧义。
好的做法:
请分析以下代码是否存在线程安全问题,重点检查:
1. 共享变量的访问控制
2. 锁的使用是否正确
3. 可见性问题(volatile 的使用)
差的做法:
帮我优化这段代码(优化什么?性能?可读性?内存?)
越具体,模型的发挥空间越小,输出越可预测。
1.2.3 维度三:约束条件
为什么有效:明确说"不要做什么"和说"要做什么"同样重要。
常用约束:
- 字数限制:
不超过 200 字 - 范围限制:
只回答问题,不要引申 - 格式约束:
不要生成测试数据,只生成框架代码 - 不确定性处理:
如果不确定,直接说不知道,不要编造
最后那条非常关键。LLM 有一个著名问题叫幻觉——它会在不确定的情况下自信地编造答案。加上"不确定就说不知道"这条约束,不能完全消除幻觉,但能明显减少。
1.2.4 维度四:输出格式
为什么有效:让 LLM 的输出可以被程序处理,减少解析成本。
当 LLM 的输出需要被程序处理(存入数据库、传给下一个步骤),自由文本是个障碍。
输出格式(严格遵守):
- 如果没有发现问题,只输出:LGTM
- 如果发现问题,用以下 JSON 格式输出,不要添加任何额外说明:
{
"issues": [
{
"type": "问题类型",
"line": "代码行号",
"description": "问题描述,不超过 50 字",
"suggestion": "修复建议,不超过 80 字"
}
]
}
一个完整 Prompt 的五大组成部分——角色定义、任务描述、背景信息、输出格式、示例
1.3 Prompt 速查卡片
1.3.1 基础结构
[角色设定]
你是一名...,有...年经验,熟悉...
[任务描述]
请分析/生成/优化...,重点关注:
1. ...
2. ...
[约束条件]
- 不超过 X 字
- 只回答...,不要...
- 如果不确定,直接说不知道
[输出格式]
用以下格式输出:...
1.3.2 代码审查 System Prompt 完整示例
import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
system_prompt = """你是一名专注于 Java 后端代码审查的工程师,有十年 Spring Boot 项目经验。
你的工作是审查用户提交的 Java 代码,找出以下类型的问题:
- 潜在的 NullPointerException 风险
- 未关闭的资源(数据库连接、文件流等)
- 线程安全问题
- 不必要的性能开销
输出格式要求(严格遵守):
1. 如果没有发现问题,只输出:LGTM
2. 如果发现问题,用以下 JSON 格式输出,不要添加任何额外说明:
{
"issues": [
{
"type": "问题类型",
"line": "代码行号或行范围",
"description": "问题描述,不超过50字",
"suggestion": "修复建议,不超过80字"
}
]
}
注意:不要解释你的思考过程,直接给出结果。"""
user_code = """
public List<User> getUsers(String department) {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE dept = ?"
);
ps.setString(1, department);
ResultSet rs = ps.executeQuery();
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapUser(rs));
}
return users;
}
"""
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请审查以下代码:\n```java\n{user_code}\n```"}
],
temperature=0.1 # 代码审查用低温度,输出更稳定
)
print(response.choices[0].message.content)
你应该看到类似这样的输出:
{
"issues": [
{
"type": "资源未关闭",
"line": "2-10",
"description": "Connection、PreparedStatement、ResultSet 均未关闭,会导致连接池泄漏",
"suggestion": "使用 try-with-resources 语句自动关闭资源"
}
]
}
输出是稳定的 JSON,可以直接被程序解析。
1.4 常见错误和解决方法
1.4.1 错误一:Prompt 太短
差的做法:
翻译这段话
好的做法:
将以下 Java 技术文档翻译为中文,保留所有代码块不翻译,专业术语保留英文原文并在括号内附中文说明
效果完全不同。
1.4.2 错误二:没有约束
不告诉模型不能做什么,它就可能做任何事。它可能在只需要代码的时候写五百字解释。
加上明确的约束:只输出代码,不要解释、不超过 100 字、如果不确定请说不知道。
1.4.3 错误三:格式要求放错位置
格式要求放在 System Prompt 里,而不是放在 User Prompt 里。User Prompt 里的格式要求,模型偶尔会"忘记";System Prompt 里的格式要求,遵从度高得多。
1.4.4 错误四:一次写好就不改了
Prompt 需要迭代。写完跑一遍,看输出哪里不对,针对性修改。通常改三到五轮才能稳定。这和调 bug 是一样的过程——不要期待第一次就写对。
1.4.5 错误五:把所有东西塞进一个 Prompt
任务太复杂时,拆成多个步骤,每步用一个 Prompt,效果远好于一个巨型 Prompt。这是 Prompt Chaining 的思路,第 11 篇会详细讲。
1.5 System Prompt 的写法
System Prompt 是最重要的 Prompt 设计工具。它设定整个对话的基调,优先级比 User Prompt 更高。
一个好的 System Prompt 由三部分组成:
角色定义:我是谁,我擅长什么。
行为约束:我会做什么,我不做什么。
输出格式:我用什么格式回答。
messages = [
{
"role": "system",
"content": """你是一个专业的代码审查助手。
行为规则:
- 只审查 Java 代码,其他语言回答"请提交 Java 代码"
- 始终用中文回答
- 指出问题时,必须同时给出修复建议
输出格式:
- 没有问题:只输出 LGTM
- 有问题:输出 JSON 格式(不要其他任何文字)"""
},
{"role": "user", "content": f"请审查以下代码:\n{code}"}
]
格式要求写死,不留余地。"可以用表格或列表"这种说法会让模型自己决定,结果就是输出不稳定。
1.6 Prompt 是工程,不是玄学
好的 Prompt 有固定的结构:角色 + 任务 + 约束 + 格式。
失败了就分析哪个维度出了问题,针对性修改,就像调 bug 一样。
当把 LLM 接入实际项目,Prompt 的稳定性直接决定系统的稳定性。一个经过充分测试、覆盖各种边界情况的 Prompt,是最重要的工程资产之一。
下一篇讲 Few-shot 和 CoT(思维链),那是让模型在复杂任务上真正靠谱的关键技术。
1.7 本篇速查卡片
| 维度 | 为什么重要 | 好的写法 |
|---|---|---|
| 角色定义 | 确定模型用什么知识分布来回答 | 具体职业 + 年限 + 技术栈 |
| 任务描述 | 缩小输出搜索空间 | 明确目标 + 分点列出关注点 |
| 约束条件 | 防止模型"创意"发挥 | 字数限制 + 禁止行为 + 不确定性处理 |
| 输出格式 | 让输出可被程序处理 | 给出完整的格式模板或示例 |
| 放置位置 | 格式约束在 System Prompt 比 User Prompt 有效 | 格式要求放 System,具体任务放 User |