LOADING
3598 words
18 minutes
Prompt Engineering(二)

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 都封装成了可复用的模板:

src/server/prompts.ts
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 的 JsonOutputParser
import { 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

src/server/memory-strategies.ts
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 返回给前端:

src/server/handlers/stream.ts
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、事实问答「仅根据上下文回答,不知道就说不知道」
对话摘要多轮对话「将以下对话压缩为摘要,保留关键信息」
节点专用 PromptLangGraph 工作流Planner / Executor / Replanner,各司其职

我的工程化原则

  1. 把 LLM 当 API,不当人:输入要严谨,输出要可控
  2. 宁长勿短,宁多勿少:Prompt 写长点没关系,信息不够才是大问题
  3. 示例 > 说明:给几个具体例子,比写十句说明管用
  4. 肯定 > 否定:说「只输出 JSON」,不说「不要输出解释」
  5. 容错是必须的:LLM 一定会出错,准备好重试和降级策略
  6. Prompt 是代码的一部分:要版本化、可测试、可回滚

一句话总结

Prompt Engineering 不是玄学,是「输入输出设计学」。好的 Prompt 就是一份好的 API 文档。

项目源码:learn-langchain.js

上一篇:[RAG 检索增强生成:让大模型从「凭记忆答题」变成「带资料开卷」](RAG 检索增强(一).md) 下一篇预告:[LangChain 工程模块化:从 100 行脚本到可维护的后端服务](LangChain 工程模块化(三).md)

Prompt Engineering(二)
/posts/2026-7-1/2-prompt-engineering/
Author
Atopos
Published at
2026-07-01
License
CC BY-NC-SA 4.0

Some information may be outdated