首页 / 资讯中心 / 文章详情

生成式AI设计模式实战:Java与Spring Boot落地指南

生成式AI设计模式实战:Java与Spring Boot落地指南 ★ FEATURED ARTICLE
在生成式AI落地到业务系统的这几年我最大的感触是设计模式不但没有过时反而成了驾驭AI不确定性的关键工具。很多同学一开始觉得“AI就是调接口”等真正把大模型接入生产的Java服务才发现提示词怎么管理、模型怎么切换、上下文怎么组织全是坑。这时候回头看经典的23种设计模式思路一下就能打开。这篇文章是“生成式AI设计模式”系列的第四篇继续聊那些在真实项目里反复用到的模式组合尤其是围绕提示词工厂、上下文管理和多模型适配的落地方式。无论你是已经在做AI应用的后端开发还是刚准备把大模型接入公司现有系统这篇内容适合你参考我会尽量把场景拆清楚再配上可以在Spring Boot项目里直接改改就用的Java代码。1. 生成式AI设计模式概览当经典遇见智能1.1 为什么生成式AI需要专属设计模式先说一个老问题设计模式解决的是什么是“变化”。传统软件的变化来自业务规则、数据库、外部接口而生成式AI应用里的变化更多来自模型版本、提示词效果、输出格式、甚至模型供应商。你不可能把大模型的调用逻辑写硬编码那样每换一次模型或每改一次提示词都要动整个核心流程。设计模式的价值就是把这些变化点隔离出来让系统在AI带来的高不确定性下还能保持稳定、可测试、可维护。举个例子我们公司之前做智能客服后端是用Java写的。一开始直接把OpenAI的API调用写在Controller里运行了两个月换模型、调提示词、处理多轮上下文代码改得一团乱。后来我们把系统重构用工厂模式管理所有提示词、用策略模式处理不同模型参数、用适配器模式统一多供应商接口整个结构瞬间清楚了。所以生成式AI不是不需要设计模式而是比传统项目更需要因为AI的输出本身就不可控如果代码结构再不可控那就全完蛋了。1.2 经典23种设计模式到AI场景的映射很多时候大家背过23种设计模式的记忆口诀但总感觉用不上。其实把这些模式往AI场景里一套马上就活了。我整理了一张映射表方便你对照参考经典设计模式传统场景生成式AI场景工厂模式创建不同类型对象根据需求生成不同结构的提示词策略模式不同算法自由切换切换不同模型或提示词策略适配器模式统一不同接口统一不同AI供应商的API差异外观模式简化复杂子系统隐藏模型调用、重试、缓存等细节组合模式树形结构组织多轮对话上下文责任链模式请求逐级处理对用户的输入进行敏感信息过滤、压缩等单例模式全局唯一实例管理全局AI配置模板方法模式固定算法骨架固定提示词固定步骤子类扩展细节建造者模式复杂对象分步构建逐步构建多模态请求这张表只是引入后面会挑使用频率最高的多个模式拆开讲。注意模式不是银弹关键在于你是否识别出了真实的变化点。对于AI应用来说最常见的三个变化点是提示词怎么来、上下文怎么存、模型怎么换。接下来我们就围绕这三点逐一展开。2. 提示词工程中的设计模式从工厂到策略2.1 提示词工厂模式统一管理提示词生成做AI应用做久了你会发现提示词就是你系统里的“SQL语句”你绝对不会容忍把SQL到处拼。同理提示词也绝对不能散落在各个业务代码里。我见过很多项目每个Controller自己拼一段system prompt结果改一个需求要全局搜索替换太痛苦了。我们用一个提示词工厂来统一管理。核心思路是定义一个Prompt接口每个业务场景对应一个实现类工厂根据场景类型返回对应的Prompt对象。这样提示词版本、模板、参数填充逻辑都收口在一个包内想改哪块直接定位改动也只影响一个类。// 提示词接口 public interface Prompt { String build(MapString, String params); } // 客服场景专属提示词 Component(customerServicePrompt) public class CustomerServicePrompt implements Prompt { private static final String TEMPLATE 你是一名企业客服请根据以下用户问题给出简洁回复。\n用户问题: {question}\n回复要求: {constraints}; Override public String build(MapString, String params) { return TEMPLATE.replace({question}, params.get(question)) .replace({constraints}, params.get(constraints)); } } // 工厂类根据场景类型获取提示词构建器 Component public class PromptFactory { private final MapString, Prompt promptMap; public PromptFactory(ListPrompt prompts) { this.promptMap prompts.stream() .collect(Collectors.toMap(p - p.getClass().getAnnotation(Component.class).value(), p - p)); } public Prompt getPrompt(String type) { return promptMap.get(type); } }这段代码里有几个细节值得说明。第一构造函数注入了所有Prompt实现类然后收集到一个Map里这样新增场景时只需新增一个组件不用改工厂代码符合开闭原则。第二模板用字符串常量来写便于管理版本但在生产环境我更建议把模板放到外部配置或数据库这样连产品经理都能优化提示词不用重新发版。工厂模式的直接收益就是提示词统一、变更可控、新人接手快。我们做过一次切换把所有客服话术从“不讲温度”改成“有温度但不说废话”只改动了一个模板类测试一下线上就生效了这种感觉爽到不行。不过要提醒一点别把提示词工厂做成“万能工厂”如果每个场景的提示词结构差异巨大那就要考虑拆分为多个工厂或者用建造者模式处理复杂结构。2.2 策略模式灵活切换指令与参数提示词模板解决了“怎么写”但业务里还需要根据用户行为、会员等级、外部条件来决定“用哪个提示词”和“用什么模型参数”。如果这些分支用if-else写死后面每次新增规则都是灾难。策略模式在这里天然合适每个策略封装一套“指令 参数”组合运行时动态选择。举个具体场景电商客服要区分普通用户和VIP用户。对VIP用户我们希望回复更详细、带一些专属关怀对普通用户回复要简洁、效率优先。同时不同模型比如旗舰模型和轻量模型的响应时间、成本完全不同我们还想根据用户等级自动选择模型。public interface ConversationStrategy { String buildPrompt(UserContext user); String modelName(); double temperature(); } Slf4j Component public class VipConversationStrategy implements ConversationStrategy { Override public String buildPrompt(UserContext user) { return String.format(VIP用户%s询问%s。请给出详细解答并附上专属优惠说明, user.getUserId(), user.getMessage()); } Override public String modelName() { return gpt-4o; } Override public double temperature() { return 0.7; } } Component public class NormalConversationStrategy implements ConversationStrategy { Override public String buildPrompt(UserContext user) { return String.format(用户%s询问%s。请直接给出步骤化解答不要废话, user.getUserId(), user.getMessage()); } Override public String modelName() { return gpt-4o-mini; } Override public double temperature() { return 0.3; } }在运行时用一个StrategyContext根据UserContext提取用户标签直接决定使用哪个策略。策略模式的优势在于把“逻辑判断”和“具体提示词实现”完全解耦以后要是添加“新用户试用策略”或“高投诉风险用户安抚策略”都只是新增一个类不会影响写好的业务代码。它还顺带帮我们管理了模型参数——每个策略直接指定模型和温度做降本增效时只需要调整策略类的参数值非常方便。但我要特别强调一点策略模式里的“策略”不要设计得太细否则会产生策略类爆炸。我们当初按“用户等级”抽象就够了后来有人非要把“天气情况”也做成策略结果一周就加出了十多个类维护成本直线上升。识别真正需要变化的维度才是设计模式的核心不是把所有东西都拆成策略。3. 上下文管理组合模式与责任链模式的实践3.1 组合模式构建结构化上下文多轮对话是所有对话场景都逃不开的问题。最初我们简单地在List里堆历史消息一股脑全部传给模型。结果消息一长既费token模型响应也变慢。后来发现上下文本质上是一种树形结构一个会话根节点下面有多个分支每个分支包含“用户输入”、“系统回复”、甚至“工具调用结果”。组合模式正好可以解决这种树形结构的组织问题。我们的做法是定义ContextNode抽象类底下分为两个子类LeafMessage叶子消息和MessageGroup消息分组。一个MessageGroup可以挂载多个LeafMessage也可以继续挂载子Group从而形成一棵树。这个树可以在进入大模型之前做裁剪比如只保留最近N轮只保留某个分支。在代码上我们对组合节点和叶子节点统一处理统一序列化成模型输入格式。public abstract class ContextNode { public abstract String render(); } public class LeafMessage extends ContextNode { private String role; // user, assistant, system private String content; public LeafMessage(String role, String content) { this.role role; this.content content; } Override public String render() { return {\role\: \ role \, \content\: \ content \}; } } public class MessageGroup extends ContextNode { private ListContextNode children new ArrayList(); public MessageGroup addChild(ContextNode node) { children.add(node); return this; } Override public String render() { StringBuilder sb new StringBuilder(); for (ContextNode child : children) { sb.append(child.render()).append(,); } return sb.toString(); } }组合模式在这里的真正价值不是让我们写出一段漂亮的树结构而是让我们可以在不同层级做后处理。比如我们要做“检索增强生成”时可以把检索出来的文档作为一个叶子节点挂载到一个特定分组里命名成“knowledge_group”。这样上下文里就天然区分了“对话历史”和“外部知识”既方便模型理解也方便我们做效果审计——我们还能知道最终发给模型的到底是哪些内容。实操中有一个坑组合模式使用不当容易造成性能下降。因为每次递归render都生成一个新字符串如果上下文特别长内存占用会明显升高。我建议在关键路径上做一次缓存或者干脆用Stream操作直接聚合别在树结构上过度封装。3.2 责任链模式实现上下文增强上下文里往往混杂着大量噪音比如用户输入的手机号、身份证信息或者那种一屏都放不下的超长日志。直接全部发给模型不仅是token浪费还会让模型注意力分散。我们引入责任链模式建立一条处理链每个节点负责一种增强或过滤操作节点顺序可以配置每个节点决定是否继续向后传递。设想一个典型的处理链敏感信息过滤 - 超长文本截断 - 语义向量化 - 构建最终Prompt。每一步都是独立组件实现了相同的Handler接口。链的装配在配置文件里完成方便调整顺序。public interface ContextHandler { void handle(InternalContext context); void setNext(ContextHandler next); } public class SensitiveDataFilter implements ContextHandler { private ContextHandler next; Override public void setNext(ContextHandler next) { this.next next; } Override public void handle(InternalContext context) { String cleaned context.getUserContent() .replaceAll(1[3-9]\\d{9}, [手机号]); context.setUserContent(cleaned); if (next ! null) next.handle(context); } } public class LengthCompressor implements ContextHandler { private static final int MAX_LENGTH 2000; private ContextHandler next; Override public void setNext(ContextHandler next) { this.next next; } Override public void handle(InternalContext context) { String content context.getAllContent(); if (content.length() MAX_LENGTH) { content content.substring(0, MAX_LENGTH) ...; } context.setAllContent(content); if (next ! null) next.handle(context); } }责任链模式的好处是你可以非常灵活地扩展处理节点。比如我们后来接入某大厂新出的多模态模型需要把用户上传的图片做预处理就直接新加了一个ImagePreHandle节点插在链上其他节点不用动。这比写一个巨型if-else链要干净得多。再说一遍不要滥用如果处理节点只有两个那直接写顺序调用也完全可以模式是为了维护性不是为了炫技。4. 多模型适配与降级适配器模式与外观模式4.1 适配器模式统一不同模型的调用接口现在市面上模型供应商太多了每个厂商的API格式、超时策略、错误码、计费逻辑都不一样。如果你在业务代码里直接写“OpenAI调用”或者“通义千问调用”一旦要增加一个供应商或者做灾备切换就必须改动所有调用方这是最典型的耦合问题。适配器模式解决的是接口不兼容问题。我们抽象出一个统一接口LLMProvider内部包含chat方法。然后为每个供应商写一个适配器把各自差异的API封装成统一签名。业务层只依赖LLMProvider完全不感知背后是哪家模型。public interface LLMProvider { LLMResponse chat(LLMRequest request); } public class OpenAIProvider implements LLMProvider { private final OpenAiClient client; Override public LLMResponse chat(LLMRequest request) { // 将统一请求转为OpenAI的ChatCompletionRequest // 将OpenAI的响应转为LLMResponse return convertedResponse; } } public class WenxinProvider implements LLMProvider { private final WenxinClient client; Override public LLMResponse chat(LLMRequest request) { // 将统一请求转为文心的ErnieBotRequest // 将文心的响应转为LLMResponse return convertedResponse; } }这套适配器实现的关键不只在方法签名统一还在于把各个供应商的超时、重试、流式输出机制也一并适配掉。我们当初踩过一个大坑OpenAI的流式返回走的是SSE而某云厂商走的是WebSocket如果不做适配上层代码就无法用统一方式接收流式结果。后来我们在适配器里隔离了传输层差异对外提供统一的Publisher 流式接口才让业务层真正稳定下来。适配器模式与策略模式有一点相似但区分很简单策略模式是针对“同一接口不同算法”适配器是针对“不同接口转成统一接口”。在AI应用里我们经常两者同时用——先通过适配器统一外部供应商接口再用策略模式在内部选择用哪个适配器各司其职。4.2 外观模式为复杂AI系统提供简单门面当你同时拥有提示词工厂、上下文处理链、多模型适配器、缓存、重试、反馈采集这些组件之后业务方的调用者会被复杂依赖搞疯。正常业务代码根本不需要关心“今天的提示词来自哪个工厂”、“上下文是否要压缩”、“该调哪个供应商的哪个模型”。外观模式的价值就在这里它提供一个高层的、极简的API门面把整套AI子系统封装在内。我们可以做一个AIService门面类只暴露两个方法generate(String prompt)和generateWithContext(ConversationContext ctx)。内部自动完成调用提示词工厂获取提示词调用责任链做预处理调用适配器选择模型然后返回最终文本。同时负责把异常翻译成友好错误、把耗时数据记录到日志。业务方看到的就是一个“傻瓜式”AI服务。Service public class AIServiceFacade { private final PromptFactory promptFactory; private final ContextHandler pipeline; private final LLMProviderRouter router; public String generate(String sceneType, MapString, String params) { Prompt prompt promptFactory.getPrompt(sceneType); String rawPrompt prompt.build(params); LLMRequest req new LLMRequest(rawPrompt); LLMResponse resp router.route(req); return resp.getText(); } }这个门面看起来简单但做起来有几个细节必须注意。第一外观里不要承载业务规则比如“如果用户来自上海则使用某模型”这种规则应该下沉到策略层外观只做编排。第二外观模式不适合硬塞所有方法如果门面上的方法越来越多将来反而变成“上帝类”那就违背了模式的初衷。我们当时规定门面最多只保留5个公开方法超过就要重新考虑分层。在外观层我们还会统一收口所有的异常类型。开源的模型API时不时抽风返回超时、限流、甚至奇奇怪怪的格式错误。如果不做异常翻译500错误会直接打到Controller层用户看到的是一堆堆栈。通过外观层做一个ExceptionMapper把底层异常映射成业务可读的code和message整个系统健壮性会瞬间提升一个台阶。5. 实操案例在Java后端集成生成式AI的常见问题与排查5.1 环境准备与依赖配置所有设计模式都要落进工程才能见到效果。我们用的技术栈是Spring Boot 3 Java 17如果你打算复现这篇文章里的代码建议先把依赖准备好。你需要引入openai-java客户端或者某云SDK最好再配一个连接池。我这里不贴完整pom就提一个关键配置把API key放到环境变量而不是配置文件里避免提交到Git仓库泄露。另外一定要配置超时时间大模型接口慢起来能让你怀疑人生我们默认连接超时5秒读超时30秒。如果你用的是官方的OpenAI SDK还需要注意模型名称的版本管理。现在我们经常同时用多个模型名称做灰度测试比如“gpt-4o-2024-08-06”和“gpt-4o-mini-2024-07-18”不要把这些名称硬编码在业务代码里建议放到配置中心或者环境变量方便随时切流量。这一步看起来和设计模式无关实际上直接影响你后面使用策略模式时的灵活性。5.2 典型错误与调试收获我在项目里遇到最多的问题排名第一的就是“上下文超长被模型拒绝”——这个问题不是靠设计模式能直接解决的但责任链帮我提前处理掉了。排名第二的是JSON输出解析失败。大模型到你手里的东西永远是个字符串你想要结构化数据必须自己锁定输出格式。我们在提示词里用通俗语言要求必须返回JSON但模型就是偶尔在JSON前面多加一个“这里是结果”这种前缀。后来我们直接用了一个OutputParser组件外观模式里集成了它专门负责从非严格格式中提取JSON子串这才彻底封住问题。典型现象可能原因设计模式改进方案响应超时网络抖动或模型负载高外观层统一设置重试机制token消耗过高上下文未裁剪责任链增加截断节点模型回答风格不一致提示词散落各业务代码提示词工厂统一管理换供应商要改大量代码调用方直连SDK适配器模式统一接口会话上下文错位在List里手动拼接组合模式组织上下文树接口返回500堆栈底层异常未翻译外观层增加异常映射器还有一个特别值得说的坑我们曾经在责任链的“敏感信息过滤”节点里用正则把“18612345678”识别成手机号并脱敏。结果测试的时候发现用户输入的验证码包含“18612345678”后面又跟着很多数字正则匹配把不该脱敏的内容也替换了。最后我们改成白名单机制只对明确标记为敏感字段的内容做脱敏不在全面扫描。这个经验告诉我们设计模式解决的是代码组织问题但规则本身需要谨慎设计再好的模式也救不了错误规则。在调试AI应用的时候日志一定要结构化。我们在日志里用MDC记录了sceneType、modelName、promptHash、responseTime这些字段。这让我在定位问题时非常爽直接拿promptHash去对比不同提示词模板效果。推荐大家也这样做。这个技巧虽然不属于设计模式但对维护一个高质量AI系统来说重要程度完全不输给任何模式。6. 从23种设计模式到AI原生模式我的体会我个人在实际使用中最大的体会是经典设计模式从来没有过时只是需要换个姿势去用它。23种设计模式里创建型模式帮你管理提示词和请求对象结构型模式帮你组织多模型和上下文行为型模式帮你处理推理链路和对话策略每一个位置都严丝合缝。只要你把AI系统当作一个正常的后端服务来设计而不是当作一个黑盒脚本模式的价值就会慢慢体现。不过也不是说直接套完就万事大吉。AI时代确实诞生了一些新需求比如“上下文压缩模式”、“记忆衰减模式”等等这些是经典模式清单里还没有的。我自己的习惯是先把现有模式用熟遇到新问题就拿模式思维去拆解找出变化点、定义接口、封装差异。这套思维才是设计模式留给我们最宝贝的东西。最后再分享一个小技巧不要一开始就上全套模式。我见过太多团队第一个版本就上了工厂策略责任链适配器外观结果写完后谁也看不懂。务实的路径是先写直接的调用代码等系统里出现了明显的重复和耦合再针对性地引入模式。模式是为了降低复杂度的不是为了增加复杂度。你在重构的时候试着把变化点画出来再对照这篇文章里的案例一步一步来。只要守住“识别变化、隔离变化”这个核心你的AI应用结构一定会越写越舒坦。
阅读完成 · 觉得有帮助?
咨询建站