让 Prompt 动起来:从固定模板到动态生成的完整实践

328 阅读5分钟

让 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 是动态的

问题在于:

  1. key 是变量(如 "city"),不能写死
  2. 需要替换所有 {city},而不仅仅是第一个
  3. 字符串替换不支持动态构造

所以我们需要用 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% 的候选人。


参考资料