Prompt Engineering:把 LLM 当 API 用,而不是陪聊
Prompt Engineering 被很多人吹成「玄学」,但在工程上它就是一门「输入输出设计学」。
为什么 Prompt Engineering 是工程必修课?
很多初学者觉得:「不就是写几句话给 AI 吗,这有什么难的?」
直到他们遇到这些场景:
| 场景 | 糟糕的 Prompt 导致的结果 |
|---|---|
| 调用 LLM 生成 JSON | 输出了一堆解释文字,JSON 解析直接报错 |
| 让 LLM 做数学题 | 跳过计算步骤,直接给答案,结果还是错的 |
| 多轮对话 | 聊着聊着就忘了用户之前说过什么 |
| RAG 问答 | 明明检索到了答案,但 LLM 还是胡说八道 |
| Agent 工具调用 | 工具参数格式不对,调用链直接断裂 |
核心问题:你把 LLM 当「人」在聊天,而工程上应该把 LLM 当「API」来调用。
💡 工程化思维:Prompt 就是 API 的参数定义,输出格式就是 API 的返回值。输入要严谨,输出要可控。
一、Prompt Engineering 的本质
1.1 一句话定义
Prompt Engineering = 给 LLM 设计一套「明确的输入输出契约」
| 维度 | 说明 |
|---|---|
| 输入契约 | 告诉 LLM:你是谁、你有什么信息、你要做什么 |
| 输出契约 | 告诉 LLM:按什么格式输出、遵循什么规则 |
| 错误处理 | 告诉 LLM:遇到不确定的情况该怎么办 |
1.2 从「聊天思维」到「API 思维」
| 聊天思维(错误) | API 思维(正确) |
|---|---|
| 「帮我写个函数」 | 「你是一位资深前端工程师。请用 TypeScript 写一个防抖函数。要求:1)函数名为 debounce 2)支持泛型 3)返回值类型正确。只输出代码,不要解释。」 |
| 「这个问题怎么解」 | 「请一步步分析以下数学题,把每一步的计算过程写清楚,最后给出答案。如果无法解答,请返回『无法解答』。」 |
| 「转换成 JSON」 | 「将用户输入转换为 JSON 格式,只输出 JSON,不要多余的解释。」 |
1.3 我的 Prompt 模板库
在 learn-langchain.js 项目中,我把常用的 Prompt 都封装成了可复用的模板:
import { PromptTemplate } from "@langchain/core/prompts";
export const promptTemplates: Record<string, PromptTemplate> = { tech: /* 技术导师 */, life: /* 生活导师 */, english: /* 英语老师 */, interview: /* 面试官 */, cot: /* 思维链 */, json: /* Few-shot JSON 转换 */,};为什么要封装成模板?
- ✅ 复用:不用每次都重写
- ✅ 一致:同一个场景输出格式稳定
- ✅ 可配置:通过变量注入动态内容
- ✅ 可测试:写单元测试验证 Prompt 效果
二、六大核心技巧(附完整代码)
技巧 1:角色设定 — 给 LLM 一个「身份」
原理:LLM 是一个概率模型,给它一个具体的角色,它会从这个角色的概率分布中采样输出。
❌ 错误写法:
回答这个问题。✅ 正确写法:
// 模板:技术导师const techPrompt = PromptTemplate.fromTemplate(`你是一位专业的技术导师。请用简洁易懂的语言回答问题。如果涉及技术概念,请配合生活例子说明。
用户问题:{question}`);更多角色示例:
// 生活达人const lifePrompt = PromptTemplate.fromTemplate(`你是一位生活达人,知道各种生活小技巧、收纳妙招、省钱窍门。请用轻松友好的语气分享实用技巧。
用户问题:{question}`);
// 面试官(带参数)const interviewPrompt = PromptTemplate.fromTemplate(`你是一位严格的{role}面试官。请用专业且简洁的语言回答:{question}限制在{limit}字以内。`);效果对比:
| 无角色 | 有「技术导师」角色 |
|---|---|
| React 是一个 JavaScript 库。 | React 就像一个「乐高积木」工厂…(会用生活例子解释) |
工程心得:角色越具体,输出越稳定。不要写「你是一个助手」,要写「你是一位有 10 年经验的前端架构师」。
技巧 2:输出格式约束 — 「只输出 JSON,不要解释」
这是工程化最关键的技巧。LLM 输出必须是机器可解析的。
❌ 错误写法:
帮我把这个转成 JSON。LLM 可能输出:「好的,这是转换后的 JSON:{…}」—— 多了一句话,JSON.parse 直接报错。
✅ 正确写法(Few-shot 示例法):
const jsonPrompt = PromptTemplate.fromTemplate(`将用户输入转换为 JSON 格式,只输出 JSON,不要多余的解释。
例子:输入:苹果5元,香蕉3元,一共多少钱?输出:{{ "items": [{{"name": "苹果", "price": 5}}, {{"name": "香蕉", "price": 3}}], "total": 8 }}
输入:张三25岁,李四30岁输出:{{ "people": [{{"name": "张三", "age": 25}}, {{"name": "李四", "age": 30}}] }}
输入:{question}输出:`);✅ 进阶:结构化输出模板:
export const jsonPromptTemplate = PromptTemplate.fromTemplate(`请根据以下需求,返回结构化的 JSON 数据。要求:1. 只返回 JSON,不要包含多余的解释2. JSON 字段名使用有意义的英文命名3. 数据要完整、准确
需求:{question}
JSON 结果:`);
// 配合 LangChain 的 JsonOutputParserimport { JsonOutputParser } from "@langchain/core/output_parsers";const parser = new JsonOutputParser();const chain = jsonPromptTemplate.pipe(llm).pipe(parser);
// 直接拿到 JS 对象,不用自己解析const result = await chain.invoke({ question: "..." });我的调试经验:
- 如果 LLM 还是输出解释文字,把「只输出 JSON」这句话重复写 3 遍
- 如果格式还是不对,加几个具体的示例(Few-shot 比纯文字说明有效 10 倍)
- 最后加一句:「如果你输出了任何非 JSON 内容,我会直接报错。」
技巧 3:思维链(CoT)— 「请一步步分析」
LLM 做数学题、逻辑推理时,直接要答案很容易错。让它「先思考再回答」。
❌ 错误写法:
123 × 456 等于多少?✅ 正确写法:
const cotPrompt = PromptTemplate.fromTemplate(`{question}
请一步步分析,把推理过程写清楚,最后给出答案。`);效果对比:
| 直接问答案 | 思维链 |
|---|---|
| 123 × 456 = 56088(可能算错) | 第一步:123 × 400 = 49200 第二步:123 × 50 = 6150 第三步:123 × 6 = 738 总和:49200 + 6150 + 738 = 56088 |
为什么有效? LLM 是自回归的,每一步都基于前面的输出。让它先写推理过程,相当于给了它更多的「上下文」,后面的答案会更准确。
进阶:零样本思维链
如果不想写示例,直接在结尾加一句:
让我们一步步思考。就这么简单的一句话,能让推理准确率提升 20%+。
技巧 4:防幻觉约束 — 「不知道就说不知道」
这是 RAG 系统的生命线——宁可答「不知道」,也不能胡说八道。
❌ 错误的 RAG Prompt:
请回答用户的问题。✅ 正确的 RAG Prompt:
const ragPrompt = PromptTemplate.fromTemplate(`你是一个知识库问答助手。请根据提供的上下文回答用户问题。
上下文:{context}
用户问题:{question}
要求:1. 仅根据上述上下文回答,不要编造信息2. 如果上下文中没有相关信息,请回答"抱歉,知识库中没有相关内容"3. 回答要简洁、准确、有条理`);我的三层防幻觉体系:
| 层级 | 约束 |
|---|---|
| 第一层 | 「仅根据上下文回答」 |
| 第二层 | 「如果没有相关信息,请回答 XXX」 |
| 第三层 | 「不要编造信息」 |
调试经验:
- 如果 LLM 还是编造,把「不要编造信息」改成加粗大写:「绝对不要编造任何上下文中没有的内容!」
- 可以加一句惩罚:「如果你编造了信息,用户会失去对我的信任。」
- 最狠的:「如果你编造信息,我会扣你工资。」(亲测有效)
技巧 5:对话摘要 Prompt — 把 100 轮对话压缩成 1 段
多轮对话时,上下文窗口是有限的。需要把旧对话压缩成摘要。
摘要 Prompt:
const summaryPrompt = PromptTemplate.fromTemplate(`请将以下对话历史压缩为一段简洁的摘要,保留所有关键信息(人名、偏好、决策、重要事实等)。
{previous_summary}
对话历史:{history}
请输出压缩后的摘要(只输出摘要内容,不要加任何前缀):`);使用场景:
原始对话(50 轮,5000 tokens) ↓摘要压缩(1 段,200 tokens) ↓放入 System Message,跟最新的 5 轮对话一起发给 LLM关键细节:
- 摘要 Prompt 里要明确列出「保留什么」:人名、偏好、决策、重要事实
- 一定要加「只输出摘要内容,不要加任何前缀」—— 否则 LLM 会写「以下是摘要:」
- 摘要要增量更新,不是每次都重新压缩所有历史
技巧 6:LangGraph 节点专用 Prompt — Planner / Executor / Replanner
复杂工作流需要多个 LLM 节点协作,每个节点有专门的 Prompt。
① Planner Prompt — 制定计划
const plannerPrompt = PromptTemplate.fromTemplate(`你是一个任务规划专家。请将用户的请求分解为具体的执行步骤。
可用工具:{tools}
请输出 JSON 格式的计划,格式如下:{{"steps": ["步骤1的描述", "步骤2的描述", ...]}}
注意:- 每个步骤应该是一个具体、可执行的动作- 步骤之间要有逻辑顺序- 步骤数量不超过 5 个
用户请求:{task}`);② Executor Prompt — 执行单步
const executorPrompt = PromptTemplate.fromTemplate(`你是一个任务执行专家。请执行以下步骤,并返回执行结果。
步骤:{step}
请直接返回执行结果,简洁明了。`);③ Replanner Prompt — 评估进度,决定继续还是完成
const replannerPrompt = PromptTemplate.fromTemplate(`你是一个任务重规划专家。请评估执行结果,并决定下一步。
原始任务:{task}已完成的步骤和结果:{completed}剩余计划:{remaining}
请判断:1. 如果任务已经完成,返回:{{"status": "complete", "result": "最终结果"}}2. 如果需要继续执行,返回:{{"status": "continue"}}
请直接输出 JSON。`);工程心得:
- 每个节点的 Prompt 要非常聚焦,只做一件事
- Planner 的输出必须是机器可解析的(JSON 格式)
- Replanner 是整个流程的「刹车」,必须能准确判断任务是否完成
三、我的 Prompt 调试工具箱
调试 Prompt 比调试代码难——因为 LLM 的输出是概率性的。我总结了一套调试方法论:
3.1 第一层:直接看 Prompt
在 WebSocket 服务中,我把实际发给 LLM 的 Prompt 返回给前端:
ws.send(JSON.stringify({ type: "done", mode: "stream", content: reply, prompt: formatted // ← 把实际 Prompt 传回去}));前端展示:
🤖 AI: 你的答案...
📝 实际 Prompt:你是一位专业的技术导师...(完整的 Prompt 内容)为什么这很重要?
- 很多时候问题出在「变量没正确替换」,不是 Prompt 本身写得不好
- 看到实际 Prompt,能快速定位问题
3.2 第二层:A/B 测试
同一个场景,写两个版本的 Prompt,对比效果:
版本 A(简洁):把这个转成 JSON。版本 B(详细):将用户输入转换为 JSON 格式,只输出 JSON,不要多余的解释。跑 10 次测试,看哪个版本的成功率更高。
3.3 第三层:温度(Temperature)调参
LLM 的 temperature 参数控制输出的随机性:
| temperature | 场景 |
|---|---|
| 0 | 代码生成、JSON 输出、事实回答(需要确定性) |
| 0.3-0.5 | 技术解释、问答(需要一点创造性但不能太离谱) |
| 0.7-1.0 | 创意写作、头脑风暴(需要多样性) |
我的默认配置:
const llm = new ChatOpenAI({ model: "GLM-4.7", temperature: 0.3, // ← 偏保守,适合工程场景});3.4 第四层:失败重试 + 降级策略
LLM 不是 100% 可靠的,工程上必须有容错机制:
async function safeInvoke(chain: any, input: any, retries = 3) { for (let i = 0; i < retries; i++) { try { return await chain.invoke(input); } catch (e) { console.warn(`调用失败(第 ${i + 1} 次),重试中...`); await new Promise(r => setTimeout(r, 1000 * (i + 1))); } } throw new Error(`重试 ${retries} 次后仍然失败`);}降级策略:
- JSON 解析失败 → 用正则提取 JSON 部分
- 正则也提取失败 → 返回一个默认的错误格式
- 连 LLM 都调不通 → 返回「服务暂时不可用」
四、常见踩坑与避坑指南
坑 1:Prompt 太长,超出上下文窗口
现象:RAG 检索了 10 个片段,拼到 Prompt 里就超长度了。
解决方案:
- 控制检索数量(Top-3 ~ Top-5)
- 对话历史用摘要压缩
- 用支持更长上下文的模型(GLM-4-128k、GPT-4-128k)
坑 2:Prompt 太短,信息不够
现象:只给了一句话,LLM 理解错了意图。
解决方案:
- 宁长勿短。写清楚:角色 + 背景 + 要求 + 输出格式
- 用 Few-shot 示例代替文字说明
坑 3:用了「不要」「不能」等否定词
现象:你写「不要输出解释」,LLM 反而输出了解释。
原因:LLM 对否定词不敏感,它更容易注意到「输出解释」这几个字。
解决方案:
- ❌ 不要写:「不要输出解释」
- ✅ 要写:「只输出 JSON」
- ✅ 用肯定句代替否定句
坑 4:Prompt 写在代码里,难以维护
现象:几百行的 Prompt 散落在各个业务文件里。
解决方案:
- 集中管理:所有 Prompt 放到
prompts.ts - 模板化:用
PromptTemplate封装 - 版本化:重要的 Prompt 变更要记录版本号
五、全文总结
核心技巧速查表
| 技巧 | 适用场景 | 关键句 |
|---|---|---|
| 角色设定 | 所有场景 | 「你是一位…」 |
| 输出格式约束 | JSON、代码、结构化输出 | 「只输出 XXX,不要解释」 |
| 思维链(CoT) | 数学题、逻辑推理 | 「请一步步分析…」 |
| 防幻觉约束 | RAG、事实问答 | 「仅根据上下文回答,不知道就说不知道」 |
| 对话摘要 | 多轮对话 | 「将以下对话压缩为摘要,保留关键信息」 |
| 节点专用 Prompt | LangGraph 工作流 | Planner / Executor / Replanner,各司其职 |
我的工程化原则
- 把 LLM 当 API,不当人:输入要严谨,输出要可控
- 宁长勿短,宁多勿少:Prompt 写长点没关系,信息不够才是大问题
- 示例 > 说明:给几个具体例子,比写十句说明管用
- 肯定 > 否定:说「只输出 JSON」,不说「不要输出解释」
- 容错是必须的:LLM 一定会出错,准备好重试和降级策略
- Prompt 是代码的一部分:要版本化、可测试、可回滚
一句话总结
Prompt Engineering 不是玄学,是「输入输出设计学」。好的 Prompt 就是一份好的 API 文档。
项目源码:learn-langchain.js
上一篇:[RAG 检索增强生成:让大模型从「凭记忆答题」变成「带资料开卷」](RAG 检索增强(一).md) 下一篇预告:[LangChain 工程模块化:从 100 行脚本到可维护的后端服务](LangChain 工程模块化(三).md)
Some information may be outdated