用Spring Boot搭建一个可版本化的Prompt模板管理器:加载、缓存、灰度与回滚

📅 发布时间:2026/7/20 18:04:00
用Spring Boot搭建一个可版本化的Prompt模板管理器:加载、缓存、灰度与回滚 文章摘要企业AI项目中的Prompt不能长期散落在Java代码里。随着场景增加需要解决模板版本、环境差异、审批、缓存、灰度、回滚和调用追踪。本文实现一个轻量级Spring Boot Prompt模板管理器使用数据库保存模板元数据通过Resource与StringTemplate渲染变量提供版本发布、缓存和灰度选择能力并将promptId与version写入AI调用日志。一、为什么Prompt不能写死在代码中常见写法StringsystemPrompt 你是企业客服助手。 请准确回答用户问题。 ;项目早期很方便后期会遇到Prompt散落在几十个类不知道线上使用哪个版本修改Prompt必须重新发布产品人员无法参与审核A/B测试困难模型切换后无法快速回滚历史Trace无法还原测试环境与生产环境不一致。Prompt应该成为一种受管理资产。二、目标能力本文实现Prompt ID 版本 状态 模板内容 变量定义 发布 缓存 灰度 回滚 调用追踪状态DRAFT REVIEWING PUBLISHED ARCHIVED三、数据表设计CREATETABLEai_prompt_template(idBIGINTPRIMARYKEYAUTO_INCREMENT,prompt_keyVARCHAR(100)NOTNULL,versionINTNOTNULL,nameVARCHAR(200)NOTNULL,template_typeVARCHAR(30)NOTNULL,contentTEXTNOTNULL,variable_schemaTEXT,statusVARCHAR(30)NOTNULL,traffic_percentINTNOTNULLDEFAULT100,created_byVARCHAR(100)NOTNULL,approved_byVARCHAR(100),created_atTIMESTAMPNOTNULL,published_atTIMESTAMP,UNIQUEKEYuk_prompt_version(prompt_key,version));关键字段prompt_key业务唯一标识version递增版本template_typeSYSTEM、USER等variable_schema变量定义traffic_percent灰度比例status生命周期。四、领域对象publicenumPromptStatus{DRAFT,REVIEWING,PUBLISHED,ARCHIVED}publicrecordPromptTemplateDefinition(StringpromptKey,intversion,Stringname,Stringcontent,PromptStatusstatus,inttrafficPercent,SetStringrequiredVariables){}渲染结果publicrecordRenderedPrompt(StringpromptKey,intversion,Stringcontent,StringcontentHash){}五、Repository接口publicinterfacePromptTemplateRepository{ListPromptTemplateDefinitionfindPublished(StringpromptKey);OptionalPromptTemplateDefinitionfindByKeyAndVersion(StringpromptKey,intversion);voidsave(PromptTemplateDefinitiondefinition);}真实项目可以使用Spring Data JPA、JdbcClient、MyBatis、MongoDB或配置中心。六、版本选择器一个Prompt可以存在v390%流量 v410%流量稳定灰度需要使用确定性哈希否则同一用户每次可能进入不同版本。ComponentpublicclassPromptVersionSelector{publicPromptTemplateDefinitionselect(StringsubjectId,ListPromptTemplateDefinitionversions){if(versions.isEmpty()){thrownewIllegalStateException(没有已发布Prompt);}intbucketMath.floorMod(subjectId.hashCode(),100);intaccumulated0;for(PromptTemplateDefinitiondefinition:versions){accumulateddefinition.trafficPercent();if(bucketaccumulated){returndefinition;}}returnversions.getLast();}}subjectId可以使用userId、tenantId或conversationId。不要使用随机数否则用户体验不稳定。七、模板渲染器ComponentpublicclassEnterprisePromptRenderer{privatefinalTemplateRendererrendererStTemplateRenderer.builder().startDelimiterToken().endDelimiterToken().build();publicStringrender(PromptTemplateDefinitiondefinition,MapString,Objectvariables){validateVariables(definition,variables);returnrenderer.apply(definition.content(),variables);}privatevoidvalidateVariables(PromptTemplateDefinitiondefinition,MapString,Objectvariables){SetStringmissingnewHashSet(definition.requiredVariables());missing.removeAll(variables.keySet());if(!missing.isEmpty()){thrownewIllegalArgumentException(缺少Prompt变量missing);}}}变量使用customerName question context避免与JSON花括号冲突。八、Prompt ServiceServicepublicclassPromptTemplateService{privatefinalPromptTemplateRepositoryrepository;privatefinalPromptVersionSelectorselector;privatefinalEnterprisePromptRendererrenderer;publicPromptTemplateService(PromptTemplateRepositoryrepository,PromptVersionSelectorselector,EnterprisePromptRendererrenderer){this.repositoryrepository;this.selectorselector;this.rendererrenderer;}publicRenderedPromptrender(StringpromptKey,StringsubjectId,MapString,Objectvariables){ListPromptTemplateDefinitionversionsrepository.findPublished(promptKey);PromptTemplateDefinitionselectedselector.select(subjectId,versions);Stringcontentrenderer.render(selected,variables);returnnewRenderedPrompt(selected.promptKey(),selected.version(),content,sha256(content));}privateStringsha256(Stringvalue){// 示例省略MessageDigest异常处理returnInteger.toHexString(value.hashCode());}}生产环境应使用真正SHA-256不要使用hashCode()作为审计哈希。九、增加缓存Prompt读取频率高、更新频率低适合缓存。Cacheable(cacheNamespromptTemplates,key#promptKey)publicListPromptTemplateDefinitionfindPublishedTemplates(StringpromptKey){returnrepository.findPublished(promptKey);}发布或回滚后清除CacheEvict(cacheNamespromptTemplates,key#promptKey)publicvoidevict(StringpromptKey){}可使用Caffeine、Redis或Spring Cache。缓存必须带版本更新机制避免数据库已发布但实例仍使用旧Prompt。十、接入Spring AIServicepublicclassCustomerAiService{privatefinalChatClientchatClient;privatefinalPromptTemplateServicepromptService;publicCustomerAiService(ChatClientchatClient,PromptTemplateServicepromptService){this.chatClientchatClient;this.promptServicepromptService;}publicStringanswer(StringuserId,Stringquestion){RenderedPromptpromptpromptService.render(customer-answer,userId,Map.of(question,question));returnchatClient.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回滚Transactionalpublicvoidrollback(StringpromptKey,inttargetVersion){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一旦影响真实业务就应该像代码和配置一样接受工程治理。