记得去年我刚接手公司AI客服系统重构时,看到代码库里密密麻麻的字符串拼接逻辑,差点当场崩溃。光是查询订单状态的Prompt就有20多个版本散落在不同Service类里,每次业务调整都要全局搜索修改。这种开发方式就像用记事本写长篇小说——没有章节结构、没有版本控制,最终必然陷入维护地狱。
Spring AI的PromptTemplate彻底改变了这种局面。它把Prompt从代码中的字符串常量变成了真正的工程化组件。最近在给某电商平台做架构咨询时,我们通过模板分层设计将200多个零散Prompt整合为15个基础模板,维护效率提升了8倍。举个例子,原来处理用户退货的代码是这样的:
java复制// 旧代码:字符串拼接方式
String generateReturnPrompt(String orderId, String reason) {
return "用户订单" + orderId + "申请退货,原因:\"" + reason
+ "\"。请根据退货政策判断是否接受,如接受需提供退货地址。";
}
现在用PromptTemplate重构后:
java复制// 新代码:模板工程化方式
@Repository
public class ReturnTemplate {
private final PromptTemplate template;
public ReturnTemplate() {
this.template = new PromptTemplate(
"用户订单{{orderId}}申请退货,原因:{{reason}}。\n" +
"请根据{{policyVersion}}退货政策判断是否接受," +
"#if({{isPremium}})优先处理VIP用户请求#end"
);
}
public Prompt generate(String orderId, String reason,
String policyVersion, boolean isPremium) {
Map<String, Object> params = Map.of(
"orderId", orderId,
"reason", reason,
"policyVersion", policyVersion,
"isPremium", isPremium
);
return template.create(params);
}
}
这种转变带来三个显著优势:首先,模板与业务逻辑解耦,产品经理可以直接修改模板文件而不用重新部署;其次,通过参数化设计,同一个模板能适应普通用户和VIP用户不同场景;最重要的是,所有Prompt变更现在可以通过Git进行版本管理,配合CI/CD流程实现自动化测试和发布。
很多开发者刚开始使用{{variable}}语法时,容易陷入两个误区:要么把所有参数都塞进一个Map导致难以维护,要么过度拆分模板失去复用性。经过多个项目实践,我总结出参数分层的黄金法则:
java复制// 最佳实践示例
public class PromptParamBuilder {
// 自动注入用户基础信息
private final UserContext userContext;
public Map<String, Object> buildBaseParams() {
return Map.of(
"userId", userContext.getUserId(),
"userTier", userContext.getTier(),
"currentTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
);
}
public Prompt generateOrderPrompt(String orderId, String queryType) {
Map<String, Object> params = new HashMap<>(buildBaseParams());
params.put("orderId", orderId);
params.put("queryType", queryType);
return orderTemplate.create(params);
}
}
#if-#else语法虽然简单,但在复杂业务中容易变成难以维护的"面条代码"。我在金融项目中发现一个模板里嵌套了7层条件判断,后来通过模板组合模式重构:
text复制// 原始复杂模板(难以维护)
{{header}}
#if({{type}}=="A")
{{contentA}}
#elseif({{type}}=="B")
#if({{urgent}})
{{contentBUrgent}}
#else
{{contentBNormal}}
#end
#end
// 重构后的模块化设计
// main-template.st
{{header}}
#include("type-{{type}}.st")
// type-A.st
{{contentA}}
// type-B.st
#include({{urgent}} ? "type-b-urgent.st" : "type-b-normal.st")
配合动态模板加载机制,可以根据type参数值自动选择子模板:
java复制String templatePath = String.format("templates/type-%s.st", type);
Resource resource = new ClassPathResource(templatePath);
PromptTemplate template = new PromptTemplate(resource);
#each语法处理大数据量时容易引发性能问题。上周排查一个生产环境故障时发现,某个批量查询接口当传入1000个商品ID时,Prompt生成耗时达到惊人的3秒。解决方案是采用分块处理+异步生成:
java复制// 分块处理大数据集
List<List<Product>> chunks = ListUtils.partition(products, 50);
List<CompletableFuture<Prompt>> futures = chunks.stream()
.map(chunk -> CompletableFuture.supplyAsync(() -> {
Map<String, Object> params = new HashMap<>(baseParams);
params.put("products", chunk);
return batchTemplate.create(params);
}, executor))
.toList();
List<Prompt> prompts = futures.stream()
.map(CompletableFuture::join)
.toList();
同时建议在模板中添加安全限制,防止恶意传入超大集合:
text复制{{header}}
#if({{products.size}} > 50)
错误:单次查询不能超过50个商品
#else
#each({{products}} as product)
{{product.id}} - {{product.name}}
#end
#end
在中大型项目中,我推荐采用四层模板架构,类似前端组件化的设计思想:
基础层(Base Templates):包含企业统一的风格、安全声明等
领域层(Domain Templates):按业务领域划分
场景层(Scenario Templates):具体使用场景
定制层(Custom Templates):客户/渠道特定定制
通过这种架构,我们在保险行业项目实现了95%的模板复用率,新业务接入时间从2周缩短到2天。
模板的版本管理需要结合Git和运行时双机制。我们的标准做法是:
在resources/templates下建立按日期命名的版本目录
text复制templates/
├── 202405/
│ ├── v1/
│ │ ├── order-query.st
│ └── v2/
│ ├── order-query.st
└── latest -> 202405/v2
应用启动时扫描模板版本:
java复制@PostConstruct
public void initTemplates() {
Path templateRoot = Paths.get("templates");
// 自动加载最新版本
templateCache.loadVersion(templateRoot.resolve("latest"));
// 保留旧版本用于回滚
templateCache.loadVersion(templateRoot.resolve("202405/v1"));
}
java复制@RestController
@RequestMapping("/templates")
public class TemplateController {
@PostMapping("/switch-version")
public void switchVersion(@RequestParam String version) {
templateCache.switchVersion(version);
}
}
建立模板的SLA监控指标至关重要,我们通常跟踪这些维度:
| 指标名称 | 计算方式 | 告警阈值 |
|---|---|---|
| 渲染耗时 | 99线生成耗时 | >200ms |
| 参数缺失率 | 未定义变量数/总变量数 | >5% |
| Token使用效率 | 有效内容长度/总Token数 | <0.7 |
| 注入尝试次数 | 检测到的特殊字符出现频率 | >0 |
通过Spring Actuator暴露自定义指标:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> templateMetrics() {
return registry -> {
Gauge.builder("prompt.template.render.time",
templateCache,
c -> c.getAverageRenderTime())
.register(registry);
Counter.builder("prompt.template.injection.attempt")
.tag("type", "xss")
.register(registry);
};
}
经过多个项目验证,这套优化方案能将模板渲染性能提升10倍以上:
预编译+缓存:启动时编译所有模板为Java字节码
java复制public class CompiledTemplateCache {
private final Map<String, CompiledTemplate> cache;
public void precompileAll() {
Files.walk(templateDir)
.filter(p -> p.toString().endsWith(".st"))
.forEach(p -> {
String key = getTemplateKey(p);
cache.put(key, TemplateCompiler.compile(p));
});
}
}
参数验证前置:在进入模板前过滤非法参数
java复制@Aspect
@Component
public class TemplateParamAspect {
@Before("execution(* com..PromptService.generate*(..)) && args(params))")
public void validateParams(Map<String, Object> params) {
params.forEach((k,v) -> ValidationUtils.validate(k, v));
}
}
智能缓存失效:基于参数指纹的缓存策略
java复制public class ParamFingerprint {
public static String generate(Map<String, Object> params) {
return DigestUtils.md5Hex(
params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey()+":"+e.getValue())
.collect(Collectors.joining("|"))
);
}
}
输入净化层:对所有传入参数进行HTML/JS转义
java复制public class SafeTemplate extends PromptTemplate {
@Override
public Prompt create(Map<String, Object> params) {
Map<String, Object> safeParams = params.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> escape(e.getValue())
));
return super.create(safeParams);
}
}
模板沙箱:限制模板引擎的系统访问权限
java复制STGroup secureGroup = new STGroup('{', '}');
secureGroup.setListener(new TemplateSecurityListener() {
@Override
public void checkMethodAccess(Object target, Method method) {
throw new SecurityException("Method call not allowed");
}
});
运行时监控:检测异常渲染行为
java复制public class TemplateMonitor {
private static final ThreadLocal<Integer> renderDepth = ThreadLocal.withInitial(() -> 0);
public static void enterTemplate() {
if (renderDepth.get() > 10) {
throw new IllegalStateException("Render depth exceeded");
}
renderDepth.set(renderDepth.get() + 1);
}
}
审计日志:记录所有模板渲染操作
java复制@Aspect
@Component
public class TemplateAuditLog {
@AfterReturning(
pointcut = "execution(* com..PromptTemplate.create(..))",
returning = "prompt"
)
public void logTemplateRender(Prompt prompt) {
AuditLogEntry entry = new AuditLogEntry(
"TEMPLATE_RENDER",
prompt.getTemplateText(),
SecurityContext.getCurrentUser()
);
auditLogRepository.save(entry);
}
}
当模板行为不符合预期时,按这个检查清单排查:
参数透视图:打印完整的参数结构和值
java复制params.forEach((k,v) -> log.debug("{} => {} ({})",
k, v, v != null ? v.getClass().getSimpleName() : "null"));
模板解析树:输出引擎内部解析结构
java复制StringTemplate st = new StringTemplate(templateText);
log.debug("Parsed template:\n{}", st.getAST().toStringTree());
逐步渲染测试:拆分复杂模板逐段验证
text复制// 原始模板
{{header}} {{#if cond}} {{content}} {{/if}}
// 测试步骤1:验证header渲染
TEST {{header}} TEST
// 测试步骤2:验证条件判断
TEST {{#if cond}} CONDITION_WORKS {{/if}} TEST
差异对比工具:比较预期与实际输出
java复制String diff = DiffBuilder.compare(expected)
.withActual(actual)
.ignoreWhitespace()
.build()
.toString();
在电商项目实践中,这套调试方法帮助我们将模板相关故障的排查时间从平均4小时缩短到30分钟以内。