ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

在生成式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(Map<String, String> params); } // 客服场景专属提示词 @Component("customerServicePrompt") public class CustomerServicePrompt implements Prompt { private static final String TEMPLATE = "你是一名企业客服,请根据以下用户问题给出简洁回复。\n用户问题: {question}\n回复要求: {constraints}"; @Override public String build(Map<String, String> params) { return TEMPLATE.replace("{question}", params.get("question")) .replace("{constraints}", params.get("constraints")); } } // 工厂类,根据场景类型获取提示词构建器 @Component public class PromptFactory { private final Map<String, Prompt> promptMap; public PromptFactory(List<Prompt> 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 List<ContextNode> 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, Map<String, 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应用结构一定会越写越舒坦。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进