用Spring Boot搭建一个可版本化的Prompt模板管理器:加载、缓存、灰度与回滚
文章摘要
企业AI项目中的Prompt不能长期散落在Java代码里。随着场景增加,需要解决模板版本、环境差异、审批、缓存、灰度、回滚和调用追踪。本文实现一个轻量级Spring Boot Prompt模板管理器:使用数据库保存模板元数据,通过Resource与StringTemplate渲染变量,提供版本发布、缓存和灰度选择能力,并将promptId与version写入AI调用日志。
一、为什么Prompt不能写死在代码中
常见写法:
String systemPrompt = """
你是企业客服助手。
请准确回答用户问题。
""";
项目早期很方便,后期会遇到:
- Prompt散落在几十个类;
- 不知道线上使用哪个版本;
- 修改Prompt必须重新发布;
- 产品人员无法参与审核;
- A/B测试困难;
- 模型切换后无法快速回滚;
- 历史Trace无法还原;
- 测试环境与生产环境不一致。
Prompt应该成为一种受管理资产。
二、目标能力
本文实现:
Prompt ID
版本
状态
模板内容
变量定义
发布
缓存
灰度
回滚
调用追踪
状态:
DRAFT
REVIEWING
PUBLISHED
ARCHIVED
三、数据表设计
CREATE TABLE ai_prompt_template (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
prompt_key VARCHAR(100) NOT NULL,
version INT NOT NULL,
name VARCHAR(200) NOT NULL,
template_type VARCHAR(30) NOT NULL,
content TEXT NOT NULL,
variable_schema TEXT,
status VARCHAR(30) NOT NULL,
traffic_percent INT NOT NULL DEFAULT 100,
created_by VARCHAR(100) NOT NULL,
approved_by VARCHAR(100),
created_at TIMESTAMP NOT NULL,
published_at TIMESTAMP,
UNIQUE KEY uk_prompt_version (
prompt_key,
version
)
);
关键字段:
prompt_key:业务唯一标识;version:递增版本;template_type:SYSTEM、USER等;variable_schema:变量定义;traffic_percent:灰度比例;status:生命周期。
四、领域对象
public enum PromptStatus {
DRAFT,
REVIEWING,
PUBLISHED,
ARCHIVED
}
public record PromptTemplateDefinition(
String promptKey,
int version,
String name,
String content,
PromptStatus status,
int trafficPercent,
Set requiredVariables
) {
}
渲染结果:
public record RenderedPrompt(
String promptKey,
int version,
String content,
String contentHash
) {
}
五、Repository接口
public interface PromptTemplateRepository {
List
findPublished(String promptKey);
Optional
findByKeyAndVersion(
String promptKey,
int version
);
void save(
PromptTemplateDefinition definition
);
}
真实项目可以使用Spring Data JPA、JdbcClient、MyBatis、MongoDB或配置中心。
六、版本选择器
一个Prompt可以存在:
v3:90%流量
v4:10%流量
稳定灰度需要使用确定性哈希,否则同一用户每次可能进入不同版本。
@Component
public class PromptVersionSelector {
public PromptTemplateDefinition select(
String subjectId,
List versions
) {
if (versions.isEmpty()) {
throw new IllegalStateException(
"没有已发布Prompt"
);
}
int bucket = Math.floorMod(
subjectId.hashCode(),
100
);
int accumulated = 0;
for (
PromptTemplateDefinition definition
: versions
) {
accumulated +=
definition.trafficPercent();
if (bucket ')
.build();
public String render(
PromptTemplateDefinition definition,
Map variables
) {
validateVariables(
definition,
variables
);
return renderer.apply(
definition.content(),
variables
);
}
private void validateVariables(
PromptTemplateDefinition definition,
Map variables
) {
Set missing =
new HashSet(
definition.requiredVariables()
);
missing.removeAll(variables.keySet());
if (!missing.isEmpty()) {
throw new IllegalArgumentException(
"缺少Prompt变量:"
+ missing
);
}
}
}
变量使用:
避免与JSON花括号冲突。
八、Prompt Service
@Service
public class PromptTemplateService {
private final PromptTemplateRepository repository;
private final PromptVersionSelector selector;
private final EnterprisePromptRenderer renderer;
public PromptTemplateService(
PromptTemplateRepository repository,
PromptVersionSelector selector,
EnterprisePromptRenderer renderer
) {
this.repository = repository;
this.selector = selector;
this.renderer = renderer;
}
public RenderedPrompt render(
String promptKey,
String subjectId,
Map variables
) {
List versions =
repository.findPublished(
promptKey
);
PromptTemplateDefinition selected =
selector.select(
subjectId,
versions
);
String content = renderer.render(
selected,
variables
);
return new RenderedPrompt(
selected.promptKey(),
selected.version(),
content,
sha256(content)
);
}
private String sha256(String value) {
// 示例省略MessageDigest异常处理
return Integer.toHexString(
value.hashCode()
);
}
}
生产环境应使用真正SHA-256,不要使用 hashCode() 作为审计哈希。
九、增加缓存
Prompt读取频率高、更新频率低,适合缓存。
@Cacheable(
cacheNames = "promptTemplates",
key = "#promptKey"
)
public List
findPublishedTemplates(String promptKey) {
return repository.findPublished(promptKey);
}
发布或回滚后清除:
@CacheEvict(
cacheNames = "promptTemplates",
key = "#promptKey"
)
public void evict(String promptKey) {
}
可使用Caffeine、Redis或Spring Cache。
缓存必须带版本更新机制,避免数据库已发布但实例仍使用旧Prompt。
十、接入Spring AI
@Service
public class CustomerAiService {
private final ChatClient chatClient;
private final PromptTemplateService promptService;
public CustomerAiService(
ChatClient chatClient,
PromptTemplateService promptService
) {
this.chatClient = chatClient;
this.promptService = promptService;
}
public String answer(
String userId,
String question
) {
RenderedPrompt prompt =
promptService.render(
"customer-answer",
userId,
Map.of(
"question",
question
)
);
return chatClient.prompt()
.advisors(spec -> spec
.param(
"promptKey",
prompt.promptKey()
)
.param(
"promptVersion",
prompt.version()
)
)
.user(prompt.content())
.call()
.content();
}
}
每次调用记录:
prompt_key
prompt_version
prompt_hash
model
request_id
user_id
十一、发布流程
推荐生命周期:
创建DRAFT
→ 自动校验
→ REVIEWING
→ 人工审批
→ PUBLISHED
→ 灰度
→ 全量
→ ARCHIVED
自动校验包括:
- 必填变量;
- 未关闭占位符;
- 超长Prompt;
- 禁止词;
- JSON示例是否合法;
- 输出格式;
- 基础回归测试。
十二、Prompt回归测试
测试数据:
{
"caseId": "P-001",
"variables": {
"question": "如何申请退款?"
},
"expectedKeywords": [
"退款",
"订单"
],
"forbiddenKeywords": [
"百分之百成功",
"无需审核"
]
}
发布前对比当前生产版本和候选版本。
指标:
- 任务成功率;
- 事实准确率;
- 输出格式成功率;
- 平均Token;
- 平均延迟;
- 禁止表达命中;
- 模型评分;
- 人工评分。
十三、回滚设计
发布记录:
prompt_key
from_version
to_version
operator
reason
timestamp
回滚:
@Transactional
public void rollback(
String promptKey,
int targetVersion
) {
archiveCurrent(promptKey);
publishVersion(
promptKey,
targetVersion,
100
);
evict(promptKey);
}
不要删除错误版本,应该归档,保留审计。
十四、多环境管理
不要让测试环境和生产环境直接共用发布状态。
增加:
environment
取值:
DEV
TEST
STAGING
PROD
发布链路:
DEV验证
→ TEST自动化评测
→ STAGING影子流量
→ PROD灰度
十五、权限
| 角色 | 权限 |
|---|---|
| 编辑者 | 创建和修改草稿 |
| 审核者 | 审核内容 |
| 发布者 | 发布和回滚 |
| 查看者 | 查看历史与指标 |
| 管理员 | 权限和环境管理 |
生产Prompt不能由同一人编辑后直接发布,关键业务应实行审批分离。
十六、还可以继续扩展什么
- Prompt对比界面;
- 在线测试;
- 模型A/B;
- 自动优化;
- 变量Schema编辑器;
- 多语言;
- Prompt依赖片段;
- RAG模板;
- Advisor模板;
- Secret引用;
- Git同步;
- CI/CD发布。
总结
一个可用的Prompt模板管理器,不只是把字符串放进数据库。
它必须同时解决:
版本
发布
变量
缓存
灰度
回滚
评测
权限
审计
Prompt一旦影响真实业务,就应该像代码和配置一样接受工程治理。
延伸阅读
如果你正在关注企业级 AI 应用、Agent、RAG、MCP 与大模型工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。