让 Prompt 动起来:从固定模板到动态生成的完整实践
在大模型(LLM)应用开发中,我们经常需要将用户输入与预设指令结合,构造出合适的 Prompt。但如果你还在硬编码拼接字符串,那你已经落后了!本文带你深入理解「Prompt 固定」问题的本质,并手把手教你用 Prompt Template + 正则表达式 实现灵活、安全、可复用的动态 Prompt 构建系统。
一、面试官的灵魂拷问:你怎么解决 Prompt 固定的问题?
“你写的 Prompt 都是写死的吗?怎么根据用户输入动态调整?”
这是我在一次 AI 工程师面试中被突然问到的问题。当时我愣住了——是啊,很多教程里的 Prompt 看似强大,实则全是硬编码,根本无法应对真实业务场景。
比如:
const prompt = `你是专业的旅游顾问,请帮我在西安玩三天,重点推荐美食。`
这样的 Prompt 虽然有效,但完全不通用。换个城市、换天数、换偏好,就得重新写一遍。这显然不是工程化的做法。
于是,我开始思考:如何让 Prompt “动”起来?
答案就是:使用 Prompt Template(提示词模板)机制,结合变量注入和正则替换,实现动态 Prompt 生成。
二、什么是 Prompt Template?
Prompt Template(提示词模板) 是一种将静态指令与动态变量分离的设计模式。它允许我们定义一个通用的结构化模板,在运行时根据用户输入填充具体值。
✅ 优势:
- 提高 Prompt 复用性
- 减少重复代码
- 易于维护和调试
- 支持多语言、多场景扩展
三、动手实现:一个简单的 PromptTemplate 类
我们可以用 JavaScript 快速构建一个基础版的 PromptTemplate 类:
class PromptTemplate {
constructor(template) {
this.template = template;
}
format(variables) {
let result = this.template;
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp(`{${key}}`, 'g'), value);
}
return result;
}
}
使用示例:
const promptTemplate = new PromptTemplate(`
你是一位专业的旅游顾问。
请帮用户规划在{city}的{days}天旅游行程。
要求:突出{preference},并给出每天的详细安排。
`);
const userInput = {
city: '西安',
days: '3',
preference: '美食'
};
const finalPrompt = promptTemplate.format(userInput);
console.log(finalPrompt);
输出结果:
你是一位专业的旅游顾问。
请帮用户规划在西安的3天旅游行程。
要求:突出美食,并给出每天的详细安排。
✅ 成功实现了 动态 Prompt 生成!
四、核心武器:正则表达式(Regular Expression)
result = result.replace(new RegExp(`{${key}}`, 'g'), value);
这行代码是实现 动态 Prompt 模板替换的核心,它的作用是:在模板字符串中,把形如 {city} 的占位符,替换成用户传入的实际值(如 "西安")。
下面我们从内到外,一层一层拆解。
🔍 完整代码回顾
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp(`{${key}}`, 'g'), value);
}
这段代码的目的是遍历用户输入的所有变量(比如 city: "西安"),然后在模板字符串中找到对应的 {city} 占位符,并替换成 "西安"。
1、外层:string.replace(searchValue, replaceValue)
JavaScript 的 replace() 方法用于替换字符串中的内容。
str.replace(searchValue, replaceValue)
searchValue:要查找的内容(可以是字符串或正则表达式)replaceValue:用来替换的新内容
✅ 关键点:如果
searchValue是字符串,只会替换第一个匹配项;
如果是正则表达式,就可以通过/g标志实现全局替换(替换所有匹配项)。
2、为什么要用 new RegExp()?不能直接写 {${key}} 吗?
你可能会想:为什么不这样写?
result.replace("{city}", "西安") // ❌ 只替换第一个,且 key 是动态的
问题在于:
key是变量(如"city"),不能写死- 需要替换所有
{city},而不仅仅是第一个 - 字符串替换不支持动态构造
所以我们需要用 new RegExp() 动态创建一个正则表达式。
3、new RegExp(pattern, flags) 详解
new RegExp(`{${key}}`, 'g')
这是动态创建正则表达式的语法:
| 参数 | 说明 |
|---|---|
第一个参数 pattern | 正则的匹配模式(字符串形式) |
第二个参数 flags | 修饰符,如 'g' 表示全局匹配 |
3.1. 模式部分:{${key}}
我们使用了 ES6 的模板字符串(Template Literal)来拼接正则的匹配模式:
`{${key}}`
假设 key = "city",那么这部分就变成了:
"{city}"
也就是我们要匹配的文本。
3.2. 修饰符:'g'
g= global(全局)- 表示替换所有匹配项,而不是只替换第一个
没有
g,只会替换第一个{city};加上g,所有{city}都会被替换。
总结:这行代码到底做了什么?
| 部分 | 作用 |
|---|---|
for...of | 遍历所有变量 |
{${key}} | 构造占位符 {city} |
new RegExp(..., 'g') | 创建全局匹配的正则对象 |
replace(...) | 替换所有匹配项 |
❗ escapeRegExp | 防止正则注入,确保安全 |
🔍 什么是正则表达式?
正则表达式是一种强大的文本匹配工具,它通过特定语法描述字符串的“模式”,从而实现查找、替换、提取等功能。
示例:匹配手机号
const str = '我的手机号是13812345678,有空打给我';
const reg = /1[3-9][0-9]{9}/; // 匹配以1开头,第二位是3-9,后面跟9个数字
console.log(str.match(reg)[0]); // 输出:13812345678
示例:匹配 {name} 这类占位符
const str = '你好,{name}!';
console.log(str.replace(/\{name\}/, '小明'));
// 输出:你好,小明!
⚠️ 注意:
{和}在正则中有特殊含义(表示重复次数),所以需要用\{和\}转义。
五、为什么不能直接用 string.replace('{city}', value)?
你可能会问:为什么不直接用字符串替换?
"Hello {name}".replace("{name}", "Tom") // 看似没问题
但存在几个致命问题:
| 问题 | 说明 |
|---|---|
| ❌ 不支持全局替换 | 只替换第一个匹配项 |
| ❌ 不支持变量名动态拼接 | 比如 {${key}} 需要动态构造正则 |
| ❌ 特殊字符未转义 | 如果 key = "name}",会导致正则错误或意外匹配 |
✅ 所以我们必须使用 new RegExp() 动态构造正则,并对变量名进行转义处理。
六、⚠️ 潜在问题:正则特殊字符怎么办?
上面的实现有一个安全隐患:如果用户输入包含正则特殊字符(如 .、*、+、(、) 等),可能导致正则报错或行为异常。
🚨 问题示例:
如果用户输入:
{ city: "北京{3}" }
那么 key = "city",但 value = "北京{3}",我们生成的正则是:
new RegExp("{city}", "g") → 匹配 "{city}"
看起来没问题。但如果 key 本身包含正则特殊字符呢?比如:
variables = { "user.name": "张三" }
我们生成的正则是:
new RegExp("{user.name}", "g")
这里的 . 在正则中表示“任意字符”,所以 {user.name} 实际上会匹配:
{userXname}{user1name}{useraname}等等……
这就出错了!
✅ 正确做法:对 key 进行正则转义
为了安全,我们必须对 key 中的正则特殊字符进行转义。
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& 表示整个匹配的子串
}
// 使用时:
const regex = new RegExp(`\\{${escapeRegExp(key)}\\}`, 'g');
🔍 拆解这个转义函数:
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
[.*+?^${}()|[\]\\]:匹配所有正则特殊字符\\$&:替换为\+ 原字符(例如.→\.)$&表示“匹配到的整个子串”
✅ 这样就能安全处理
key中的特殊字符。
完整安全版代码
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
class PromptTemplate {
constructor(template) {
this.template = template;
}
format(variables) {
let result = this.template;
for (const [key, value] of Object.entries(variables)) {
const escapedKey = escapeRegExp(key);
const regex = new RegExp(`\\{${escapedKey}\\}`, 'g');
result = result.replace(regex, value);
}
return result;
}
}
🔍 关键变化:
\\{${escapedKey}\\}:\\{→ 匹配字面量{${escapedKey}→ 安全的变量名\\}→ 匹配字面量}
七、更优雅的写法:使用 tagged template literals(可选)
如果你追求更现代的语法,也可以使用 ES6 的 模板字符串标签函数 实现类似效果:
function prompt(strings, ...keys) {
return function (variables) {
let result = '';
keys.forEach((key, i) => {
result += strings[i] + variables[key];
});
result += strings[strings.length - 1];
return result;
};
}
const travelPrompt = prompt`
请为我在 ${'city'} 玩 ${'days'} 天制定行程,偏好 ${'preference'}。
`;
console.log(travelPrompt({ city: '杭州', days: '5', preference: '古镇' }));
⚠️ 缺点:不够灵活,无法重复使用同一模板多次。
八、工业级方案参考:LangChain 的 PromptTemplate
在实际项目中,推荐使用成熟的框架,如 LangChain 提供的 PromptTemplate:
import { PromptTemplate } from "langchain/prompts";
const prompt = PromptTemplate.fromTemplate(
"请为我在 {city} 玩 {days} 天制定行程,偏好 {preference}。"
);
const result = await prompt.format({
city: "成都",
days: "4",
preference: "火锅"
});
它内部已经处理了:
- 正则安全转义
- 模板验证
- 异步支持
- 多种输入格式(JSON、YAML 等)
九、最佳实践总结
| 实践 | 建议 |
|---|---|
| ✅ 使用模板 + 变量分离 | 避免硬编码 |
| ✅ 使用正则替换 | 支持全局匹配 |
| ✅ 转义变量名 | 防止正则注入 |
| ✅ 支持默认值 | 如 {city: "北京"} |
| ✅ 模板校验 | 格式是否正确?变量是否存在? |
| ✅ 抽象成类或工具函数 | 提高复用性 |
十、结语:Prompt 工程是 AI 应用的基石
“Prompt 固定”看似是个小问题,实则是 Prompt Engineering(提示词工程) 的起点。
掌握 PromptTemplate + 正则表达式 的组合拳,不仅能帮你轻松应对面试官的挑战,更能让你在构建 AI 应用时游刃有余。
下一次,当面试官再问你:“你怎么管理 Prompt?” 你可以自信地说:
“我用的是动态模板引擎,支持变量注入、正则安全替换,甚至可以集成到 LangChain 中。”
——那一刻,你就已经超越了 80% 的候选人。
参考资料
- MDN 正则表达式文档
- LangChain.js Prompt Templates
- 《Prompt Engineering Guide》