Appearance
课 2 · Prompt 分层与模板
本课目标
理解 Meta Prompt(元提示词)、System Prompt(系统提示词)、User Prompt(用户提示词)的分层关系,掌握 Prompt 模板的工程化写法。课后你会拿到一个可复用的 Prompt 模板引擎。
课 1 我们学了怎么写好一段 System Prompt。但实际开发中,你很快会遇到两个问题:
- System Prompt 越写越长,改一处怕影响别处——能不能用 AI 来写 Prompt?
- 不同用户、不同场景需要不同的 Prompt——能不能做成模板动态拼装?
这一课解决这两个问题。
三种 Prompt 的定位
在一个 AI 应用中,"提示词"其实不是一个东西,而是三层:
| 名称 | 谁写的 | 什么时候用 | 干什么 |
|---|---|---|---|
| Meta Prompt(元提示词) | 开发者 | 开发阶段 | 用 AI 生成 / 优化 System Prompt |
| System Prompt(系统提示词) | 开发者 | 运行时,整个会话 | 定义角色、规则、输出格式 |
| User Prompt(用户提示词) | 终端用户 | 运行时,单轮 | 提出问题或给出指令 |
三层的关系是:Meta Prompt 产出 System Prompt,System Prompt 约束 User Prompt 的处理方式。
先看关系图:
三层 Prompt 的关系
Meta Prompt开发阶段:生成或优化 System Prompt
→
System Prompt运行时:定义角色、边界、格式
→
User Prompt运行时:用户当前问题或指令
→
模型输出
Meta Prompt 产出或改进 System Prompt;System Prompt 决定模型如何处理每一轮 User Prompt。
System Prompt:产品说明书
课 1 已经讲了 System Prompt 的 XML 结构化写法。这里补充一个认知:System Prompt 本质上是产品说明书——它定义了这个 AI 产品"是什么、能做什么、不能做什么、怎么做"。
它和用户输入的根本区别:
- System Prompt 是开发者写的,用户看不到,整个会话只设一次
- User Message 是终端用户的输入,每轮都不同
这意味着 System Prompt 的质量直接决定产品表现。写得好,模型就是一个称职的专家;写得差,模型就是一个什么都答、什么都不确定的全能但无用的 AI。
User Prompt:不只是用户随便打字
在很多 AI 产品中,User Prompt 看起来是用户"自由输入"的,但产品侧可以做很多引导:
typescript
// 搜索框的 Placeholder 引导
const placeholder = '试试问:React 和 Vue 的区别是什么?'
// 预设问题建议
const suggestions = [
'帮我审查这段代码',
'解释一下 useEffect 的依赖数组',
'对比 REST 和 GraphQL',
]
// 在用户输入前自动拼接上下文
function buildUserMessage(userInput: string, context: { fileName?: string }) {
if (context.fileName) {
return `我正在编辑文件 ${context.fileName},我的问题是:${userInput}`
}
return userInput
}这些都属于 User Prompt 层面的工程——让用户更容易问出好问题。
Meta Prompt:用 AI 写 Prompt
Meta Prompt 是最上层的 Prompt——它的输出不是回答用户问题,而是生成另一个 Prompt。
什么时候需要 Meta Prompt:
- 你要为一个新场景写 System Prompt,但不知道从哪下手
- 你写了一版 System Prompt,但效果不好,想让 AI 帮你优化
- 你有一批测试用例,想让 AI 根据失败 case 自动调整 Prompt
Meta Prompt 实战
用 AI 生成 System Prompt
最直接的用法:告诉 AI 你的业务需求,让它帮你生成一个结构化的 System Prompt。
typescript
import { generateText, Output } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { z } from 'zod'
const model = createOpenAI({
apiKey: process.env.CHAT_API_KEY,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}).chat('qwen3.6-plus')
// 定义 System Prompt 的结构
const SystemPromptSchema = z.object({
role: z.string().describe('角色定义,一句话说明这个 AI 是谁'),
capabilities: z.array(z.string()).describe('能力列表,3-5 项'),
rules: z.array(z.string()).describe('行为规则,5-8 条'),
outputFormat: z.string().describe('输出格式要求'),
refusalTemplate: z.string().describe('无法回答时的回复模板'),
})
const metaPrompt = `你是一个 Prompt 工程专家。
根据用户描述的业务场景,生成一个结构化的 System Prompt。
要求:
- 角色定义要具体,不要泛泛的"你是一个 AI 助手"
- 规则要可执行,不要模糊的"尽量做好"
- 输出格式要明确到具体的结构
- 必须包含拒答模板`
async function generateSystemPrompt(businessRequirement: string) {
const { output } = await generateText({
model,
output: Output.object({ schema: SystemPromptSchema }),
system: metaPrompt,
prompt: businessRequirement,
})
// 把结构化结果拼成 XML 格式的 System Prompt
return `
<role>
${output.role}
</role>
<capabilities>
${output.capabilities.map(c => `- ${c}`).join('\n')}
</capabilities>
<rules>
${output.rules.map(r => `- ${r}`).join('\n')}
</rules>
<output_format>
${output.outputFormat}
</output_format>
<refusal>
${output.refusalTemplate}
</refusal>
`.trim()
}用法:
typescript
const prompt = await generateSystemPrompt(
'我在做一个前端组件库的文档站,需要一个 AI 助手帮用户查组件用法、属性说明、代码示例。'
)
console.log(prompt)Meta Prompt 迭代:用失败 case 改进
生成一版 System Prompt 后,跑几个测试用例。把失败的结果喂回去让 AI 改进:
typescript
const iteratePrompt = `你是一个 Prompt 优化专家。
当前 System Prompt:
<current_prompt>
{currentPrompt}
</current_prompt>
以下是用这个 Prompt 跑出来的失败用例:
<failed_cases>
{failedCases}
</failed_cases>
请分析失败原因,输出改进后的 System Prompt。
改进要求:
- 只修改导致失败的部分,不要重写整个 Prompt
- 每处修改说明改了什么、为什么改`这就是 Meta Prompt 的核心用法:把 Prompt 开发从手工试错变成有反馈的迭代循环。
Prompt 模板
当产品有多种场景、多类用户时,写一堆独立的 System Prompt 既难维护又容易不一致。解决方案是模板化——把可复用的结构固定下来,变化的部分用变量填充。
变量插值
最基本的模板能力——把运行时信息插入 Prompt:
typescript
interface PromptVariables {
userName: string
currentDate: string
language: string
productName: string
}
function buildSystemPrompt(vars: PromptVariables): string {
return `
<role>
你是「${vars.productName}」的智能助手,正在为 ${vars.userName} 提供服务。
</role>
<rules>
- 使用${vars.language}回答
- 今天是 ${vars.currentDate},涉及时间相关的问题以此为准
- 称呼用户为"${vars.userName}"
</rules>
<output_format>
- 先给结论,再展开
- 代码示例使用 TypeScript
</output_format>
`.trim()
}条件段落
有些 Prompt 段落只在特定条件下才需要——用条件拼接取代写多份完整 Prompt:
typescript
interface PromptConfig {
role: 'user' | 'admin' | 'developer'
features: {
codeReview: boolean
docSearch: boolean
debugging: boolean
}
language: string
}
function buildSystemPrompt(config: PromptConfig): string {
const sections: string[] = []
// 基础角色(所有人都有)
sections.push(`
<role>
你是一个前端技术助手。
</role>`)
// 能力:根据功能开关动态拼接
const capabilities: string[] = ['回答前端技术问题']
if (config.features.codeReview) capabilities.push('审查代码质量')
if (config.features.docSearch) capabilities.push('基于文档检索回答问题')
if (config.features.debugging) capabilities.push('协助排查 Bug')
sections.push(`
<capabilities>
${capabilities.map(c => `- ${c}`).join('\n')}
</capabilities>`)
// 权限:根据角色动态拼接
if (config.role === 'admin') {
sections.push(`
<permissions>
- 可以查看系统配置信息
- 可以查看所有用户的使用记录
</permissions>`)
}
if (config.role === 'developer') {
sections.push(`
<permissions>
- 可以执行代码片段
- 可以访问 API 文档
- 可以查看错误日志
</permissions>`)
}
sections.push(`
<rules>
- 使用${config.language}回答
- 不确定的信息标注"不确定"
</rules>`)
return sections.join('\n')
}这样做的好处:
- 一份模板覆盖所有场景,改规则只改一处
- 按需加载,普通用户不会看到 admin 相关的能力描述,避免浪费 Token
- 类型安全,TypeScript 会检查 config 的合法性
Dynamic Few-shot(动态少样本提示)
课 1 讲了 Few-shot 的写法,但示例是写死在 Prompt 里的。实际产品中,示例应该根据用户的问题动态选取:
typescript
// 示例库:按领域分类
const exampleLibrary = {
react: [
{
input: 'useState 的初始值可以是函数吗?',
output: '可以。传入函数时,React 只在首次渲染时调用它(惰性初始化)……',
},
{
input: 'useEffect 的清理函数什么时候执行?',
output: '在组件卸载时,以及下一次 effect 执行前……',
},
],
typescript: [
{
input: 'interface 和 type 有什么区别?',
output: '主要区别:interface 可以声明合并,type 支持联合类型……',
},
],
nodejs: [
{
input: 'require 和 import 的区别?',
output: 'require 是 CommonJS,同步加载;import 是 ESM,静态分析……',
},
],
}
// 根据用户问题选取相关示例
function selectExamples(
question: string,
maxExamples: number = 2
): string {
// 简单关键词匹配(生产环境可以用 embedding 做语义匹配)
const keywords: Record<string, string[]> = {
react: ['react', 'hook', 'useState', 'useEffect', 'component', '组件'],
typescript: ['typescript', 'ts', 'type', 'interface', '类型'],
nodejs: ['node', 'require', 'import', 'express', '服务端'],
}
const lowerQ = question.toLowerCase()
const matchedDomain = Object.entries(keywords).find(([, words]) =>
words.some(w => lowerQ.includes(w))
)?.[0]
if (!matchedDomain || !exampleLibrary[matchedDomain as keyof typeof exampleLibrary]) {
return '' // 没有匹配到领域,不插入示例
}
const examples = exampleLibrary[matchedDomain as keyof typeof exampleLibrary]
.slice(0, maxExamples)
return `
<examples>
${examples.map(e => `输入:${e.input}\n输出:${e.output}`).join('\n\n')}
</examples>`
}整合到完整的 Prompt 构建中:
typescript
function buildFullPrompt(
config: PromptConfig,
vars: PromptVariables,
userQuestion: string
): string {
const base = buildSystemPrompt(config)
const examples = selectExamples(userQuestion)
return `${base}
${examples}`
}模板维护的原则
- 变量命名明确 —
{userName}好过{name},读代码的人一眼知道填什么 - 必填和选填分开 — 用 TypeScript 的类型标记哪些变量是可选的
- 段落顺序固定 — role → capabilities → rules → output_format → examples → security,保持一致性
- 版本控制 — Prompt 模板跟代码一样放在 Git 里,每次修改有记录
并入主线项目
本模块能力会在基础项目 basic/project/ 中收束,当前课内 Demo 见下方运行方式。
课 1 教了怎么写好一段 System Prompt,这一课在此基础上增加了两个维度:
- Meta Prompt — 用 AI 帮你写 Prompt,然后用失败 case 迭代改进。它更适合作为开发工作流能力,用来持续打磨最终 Agentic RAG 的各类 Prompt。
- 模板化 — 把 System Prompt 做成参数化模板,一份代码覆盖多种场景。当前
basic/project先落地单一结构化 Prompt,后续主线会把这套模板化能力扩展到 planner、retriever、answerer、reviewer 等不同角色。
下一课我们会把视角拉高一层——看看一个 AI 应用的 Prompt System 全貌,System Prompt 只是其中的一环。
本课产物
- ✅ Meta Prompt / System Prompt / User Prompt 的分层定位
- ✅ Meta Prompt 生成 System Prompt 的方法
- ✅ Meta Prompt 迭代改进的流程
- ✅ Prompt 模板:变量插值、条件段落、Dynamic Few-shot
- ✅ 模板维护的工程化原则
完整代码在 basic/examples/02-prompt/02-prompt-layers/index.ts。
试试看
bash
cd daqi-ai-agent
pnpm exec tsx basic/examples/02-prompt/02-prompt-layers/index.ts- 用
meta命令输入一个业务场景描述,观察 AI 生成的 System Prompt 结构 - 用
template命令切换不同的角色和功能开关,对比生成的 Prompt 差异 - 用
chat命令验证生成的 Prompt 实际效果
面试追问
Q:Meta Prompt 和 System Prompt 有什么区别?
Meta Prompt 是"写 Prompt 的 Prompt",作用于开发阶段,输出是一段 System Prompt 文本;System Prompt 作用于运行时,输出是对用户问题的回答。两者工作在不同阶段——Meta Prompt 属于开发工具,System Prompt 属于产品运行时配置。
Q:Prompt 模板用字符串拼接可以吗?需不需要用模板引擎?
字符串拼接完全可以,TypeScript 的模板字符串加上条件函数就够用。引入模板引擎(如 Handlebars、Mustache)会增加依赖但不会带来本质提升,因为 Prompt 模板的复杂度远低于 HTML 模板。关键是要有类型约束——用 TypeScript 的 interface 定义变量结构,确保不会遗漏必填字段。
Q:Dynamic Few-shot 怎么选示例?
简单场景用关键词匹配,从预定义的示例库中按领域选取。进阶做法是把示例做成 Embedding 存向量数据库,用户问题到来时做语义检索,选最相关的 2-3 个示例插入 Prompt。这和 RAG 的检索逻辑一样——本质上少样本提示示例也是一种"知识",可以用检索增强的方式管理。