☰
生成式AI重塑设计模式:经典与新范式的实战融合
2026/11/4 20:36:59 网站建设 项目流程

1. 从“调用确定函数”到“编排概率模型”:生成式AI对设计模式的根本冲击

先说一段我自己的经历。去年我接手一个客服工单分类项目,按照过去写业务系统的经验,上来就用经典的策略模式加工厂模式把代码搭得整整齐齐。各个模块各司其职,接口定义得清清楚楚。结果一联调,问题全出在最核心的那个环节——模型调用。同一个Prompt,有时候能正确分类,有时候会把“退款”判成“投诉”,而且不同供应商的返回格式五花八门,有的给JSON,有的在JSON外面包一层Markdown代码块,有的干脆把一大段解释性文本直接丢回来。我那些设计模式在模型输出的不确定性面前,几乎帮不上忙。

后来我意识到一个关键问题:传统设计模式的隐含前提是确定性。调用一个函数,给定同样的输入,返回结果是确定的,所以可以把功能拆成稳定的小模块,用接口去约束彼此协作。但生成式AI不一样,你面对的是一个概率系统,同样的上下文可能得出不同的结果,性能有波动,成本有波动,连模型能力本身也在快速迭代。这种情况下,设计模式并没有失效,而是它的“含义”变了——从“封装确定性的实现”变成了“封装不确定性之外的稳定性”。

换句话说,生成式AI时代的架构设计,核心目标变成了三件事:把模型变化隔离掉、把业务稳定性沉淀下来、把不可控的输出纳入可控的流程。这也是我在这个系列里反复强调的:设计模式的本质是“解决反复出现的结构性问题”,而生成式AI引入的恰恰是一批全新的结构性问题,比如上下文怎么管理、多模型怎么切换、失败怎么重试、输出怎么校验、成本怎么控制。这些问题用旧模式硬套会很难受,但把它们拆开看,其实每一个背后都能找到对应的模式思想。

这篇文章是系列第十篇。到了这个位置,我不想再单纯罗列“AI设计模式清单”,而是想把前九篇的内容整合起来,站在实战角度做一次系统性复盘:哪些经典设计模式被AI重构了、哪些真正属于AI时代的新模式、以及一套可以落到代码里的完整案例。适合正在做RAG、智能客服、内容生成工具、AI Agent的同学参考,也适合那些刚看完设计模式理论、想知道“这东西在AI项目里到底怎么用”的读者。

2. 经典设计模式在AI场景下的“重新发明”

旧模式没死,只是换了副面孔。下面这几个是AI项目里我用得最多,也最值得重新理解的经典模式。

2.1 工厂模式:从“对象生产”到“模型供应商管理”

传统工厂模式负责创建对象,调用方不需要关心具体实现类是哪个,只需要一个统一接口。在AI场景下,这个思想直接对应到模型供应商管理——你的代码可能同时接入OpenAI、国产大模型、本地部署的模型,每个供应商的API地址、鉴权方式、模型名称、限流策略都不一样。如果这些细节散落在业务代码里,换模型就是一场灾难。

工厂模式在这里的最佳实践是做一个“模型客户端工厂”:

class LLMClientFactory: _registry = {} @classmethod def register(cls, provider: str, builder): cls._registry[provider] = builder @classmethod def create(cls, provider: str, model_name: str, **kwargs): builder = cls._registry.get(provider) if not builder: raise ValueError(f"Unknown provider: {provider}") return builder(model_name=model_name, **kwargs) # 使用方式 client = LLMClientFactory.create("openai", "gpt-4o", timeout=30)

这样业务代码只依赖create()方法,底层换供应商、加新模型,都不需要改业务逻辑。值得注意的是,注册过程最好在配置加载阶段完成,而不是运行时再动态判断,这样启动阶段就能暴露配置错误。

我实测下来的一个优化是:工厂不只是创建客户端,还可以顺带把模型能力元信息带上。比如哪些模型支持JSON输出、上下文窗口多大、每千token单价多少。这些信息在后面的路由和成本控制里非常有用,属于“把参数对象升格为能力描述对象”的变体。

2.2 策略模式:从算法切换到Prompt策略族

传统策略模式用来解决“一组算法可以互相替换”的问题,比如排序策略、加密策略。在生成式AI场景里,最自然的映射是Prompt策略族——针对同一个问题,可以有零样本、少样本、思维链、反思式等多种策略,它们的成本不同、效果不同、时延不同。

很多团队的Prompt散落在各个调用点,改一版提示词要全局搜索替换。用策略模式重新组织之后,结构会清爽很多:

class ExtractionStrategy(ABC): @abstractmethod def build_messages(self, text: str) -> list[dict]: ... class ZeroShotExtraction(ExtractionStrategy): def build_messages(self, text): return [ {"role": "system", "content": "从用户反馈中提取退款原因。"}, {"role": "user", "content": text} ] class FewShotExtraction(ExtractionStrategy): examples = [...] # 预先筛选好的示例 def build_messages(self, text): return [ {"role": "system", "content": "你是工单分类助手,参考示例输出。"}, *self.examples, {"role": "user", "content": text} ]

这里的关键不只是“把Prompt封装起来”,而是给策略加上效果元数据。我在生产环境里的做法是,在策略对象上记录准确的评估分数和平均响应时间,这样路由层可以基于历史表现动态选择策略。这在传统策略模式里不太会发生,但在AI场景里,策略本身的质量是一个需要持续监控的变量。

2.3 观察者模式:从事件通知到流式输出处理

观察者模式在传统GUI里很常见,一个按钮被点击,多个监听器响应。在AI应用里,它的新形态是流式输出的事件分发——大模型生成内容是一个token一个token返回的,不同的订阅者需要实时处理不同的片段。

比如一个AI写作助手,前端要在流式返回时渲染Markdown,后台需要在生成过程中检测敏感词,日志系统要记录每个token的时间戳用于延迟分析。这时候如果所有逻辑都写在同一个流式回调里,代码会迅速腐化。用观察者模式处理:

class TokenStream: def __init__(self): self._subscribers = [] def subscribe(self, handler): self._subscribers.append(handler) async def emit(self, token: str): for handler in self._subscribers: await handler(token)

看上去很简单,但实际生产要考虑几个关键点:订阅者之间的异常隔离、处理器的执行顺序、以及背压处理。我在项目里遇到过一个问题:渲染处理器跑得慢,拖慢了整条流式链路,后来改成“每个订阅者独立消费一个异步队列”才解决。这相当于给观察者模式加了一层缓冲,代价是内存占用上升,但换来了主链路的稳定。

2.4 门面模式:统一入口掩盖多模型混乱

门面模式的意义是给复杂子系统提供一个简单入口。在AI应用里,这个思想几乎是刚需——因为一个有规模的AI项目,内部往往同时存在多个模型服务、多套Prompt流程、多个后处理模块,调用方如果直接面对这些东西,协作成本极高。

我在工单系统里做的一个门面是“CompletionService”:

class CompletionService: def __init__(self, router, fallback_handler, validator): self._router = router self._fallback_handler = fallback_handler self._validator = validator async def complete(self, request: CompletionRequest) -> CompletionResult: provider = self._router.route(request) try: raw = await provider.complete(request) except ProviderError: raw = await self._fallback_handler.fallback(request) validated = self._validator.validate(raw) return CompletionResult(validated=validated, raw=raw, provider=provider.name())

调用方只需要调用complete(),完全不感知内部有路由、有重试、有校验。这个门面的价值在于,它把“模型调用的复杂度”从业务逻辑里剥离出来了。业务方不需要关心“这个请求应该发给哪个模型”“失败了怎么办”“返回格式对不对”,只需要说“我要完成这个任务”。

这里有个设计教训:门面不要做成“万能门面”。我见过有人把Prompt管理、上下文存储、成本统计全部塞进门面里,结果这个门面变成了上帝类。合理的边界是——门面只负责编排,不负责具体实现。上下文管理应该由独立的模块承担,成本统计应该走日志管道,门面只是协调它们。

2.5 适配器模式:把不同LLM的接口差异磨平

适配器模式在AI项目里的角色,比在任何传统系统里都重要。原因很简单:不同LLM供应商的接口风格差异太大了。OpenAI用/chat/completions,Anthropic用/messages,本地部署的模型可能是完全自定义的协议。更麻烦的是,同一个模型在不同时间返回的结构也可能不一致。

适配器要做的事情有两层:第一层是协议适配,把不同供应商的请求参数和返回结果转换成统一结构;第二层是异常语义适配——有的供应商超时抛这个异常,有的抛另一个,有的限流返回429,有的用429但不是限流,这些差异必须统一成内部错误类型。

class OpenAIAdapter: def __init__(self, client): self._client = client async def complete(self, request: InternalRequest) -> InternalResponse: try: resp = await self._client.chat.completions.create( model=request.model, messages=request.messages, temperature=request.temperature, ) return InternalResponse( content=resp.choices[0].message.content, usage=TokenUsage(resp.usage.prompt_tokens, resp.usage.completion_tokens) ) except RateLimitError: raise ProviderRateLimit(provider="openai") except TimeoutError: raise ProviderTimeout(provider="openai", retryable=True)

做适配器的时候,我建议连token统计格式都统一,而不是各用各的。后续做成本核算时,你会发现“有的供应商返回的token数包含缓存命中、有的不算缓存”这种坑,如果没有统一结构,成本数据会非常混乱。

3. AI应用真正需要的“新设计模式”

除了经典模式的变形,AI应用里还涌现出了一批传统设计模式没有覆盖的新结构。这些结构不是理论推演出来的,而是从大量实际项目里反复沉淀出来的“事实标准”。

3.1 上下文注入器(Context Injector)

RAG是AI应用最常见的架构,而RAG的核心问题就是“怎么把检索到的内容塞进Prompt里”。没经验的团队直接在代码里做字符串拼接,结果上下文越拼越乱,token数超限、关键信息被截断、注入内容格式不一致,各种问题层出不穷。

上下文注入器的职责是:统一决定上下文怎么组装、按什么顺序排列、哪些该保留哪些该丢弃。

class ContextInjector: def __init__(self, max_tokens: int, formatter: ContextFormatter): self._max_tokens = max_tokens self._formatter = formatter def inject(self, system_prompt: str, user_question: str, retrieved: list[Document]) -> list[dict]: ranked = self._formatter.rank_documents(retrieved) budget = self._max_tokens - estimate_tokens(system_prompt) - estimate_tokens(user_question) selected = self._truncate_to_budget(ranked, budget) return [...]

这个模式最大的价值是“上下文预算管理”。生成式AI的关键约束之一就是token窗口,上下文注入器把“内容重要程度排序”和“token预算分配”这两个关注点封装起来,业务代码就不需要再关心细节。我在项目中往往会把元数据注入也放进来做:比如当前时间、用户画像、历史交互摘要,这些信息对模型回答质量影响很大,但如果没有统一注入器,每个业务方各加一段,很快上下文就会爆炸。

3.2 意图路由器(Intent Router)

当系统接入多个模型时,必然出现“什么请求该走什么模型”的问题。我见过最朴素的方案是if-else写死,这样的问题是没有弹性、不可灰度、不能基于实时状态做调整。

意图路由器的核心逻辑是:把请求分类,映射到不同的处理链路。分类的依据可以是显式的意图标签,也可以是嵌入向量的相似度,甚至是模型自身给出的判断。

class IntentRouter: def __init__(self, rules: list[RouteRule], default_route: str): self._rules = rules self._default_route = default_route def route(self, request: CompletionRequest) -> Route: for rule in self._rules: if rule.matches(request): return rule.target_route return self._default_route

我做路由时一般会引入“模型能力矩阵”,而不是简单映射。比如:简单分类任务走小模型,代码生成走代码专用模型,复杂推理走强推理模型,敏感内容走带审核的模型。路由规则必须支持运行时更新,因为模型能力和成本策略变化太快。有条件的团队可以直接把路由规则做成配置中心里的动态条件,这样灰度切换模型就不需要发版。

3.3 自我修正循环(Self-Correction Loop)

传统软件里,代码出错是bug,修一次就好。但生成式AI输出质量不佳是常态,甚至同一个Prompt这次成功下次失败。所以AI应用需要一种“输出质量验证+自动修正”的循环结构,我称之为自我修正循环。

这个模式的骨架是:生成 → 校验 → 不通过则修正 → 再生成 → 达到最大轮次后降级。核心在于校验器设计——校验器不能只做格式检查,还要做语义层面的检查。

async def self_correcting_generate(generator, validator, max_attempts=3): last_output = None for attempt in range(max_attempts): output = await generator.generate() result = await validator.validate(output) if result.is_valid: return output, result last_output = output # 将校验失败原因反馈给生成器做修正 return last_output, ValidationResult(is_valid=False, errors=["max_attempts_exceeded"])

这里容易踩的坑是:修正轮数不能太多。每轮修正都意味着额外的token成本和延迟,实测下来很多场景2到3轮就够了,超过3轮,成功率提升就非常有限,成本却线性上升。另一个值得重视的点是,校验器本身也可能出错(比如漏判了关键信息),所以修正反馈里最好带上面向生成器的“为什么失败”的具体说明,而不是简单的一句“输出不符合要求”。

3.4 可观测性代理(Observability Proxy)

传统系统有日志、有监控。但AI应用的特殊之处在于,同样的输入输出,质量问题往往无法从代码逻辑上完全解释,必须依赖对Prompt、模型版本、参数配置、运行成本的细致观测。我见过太多AI项目排障靠“重新跑一次试试”,这种排障方式的效率太低了。

可观测性代理做的事情是:在模型调用链路上,透明地记录完整上下文——请求和响应的完整内容、token消耗、时延、模型版本、当时的Prompt策略、命中哪些缓存、有无重试。这样当线上出现质量问题时,可以回到过去复现和分析。

class ObservabilityProxy: def __init__(self, inner_service, recorder): self._inner = inner_service self._recorder = recorder async def complete(self, request): start = time.perf_counter() try: response = await self._inner.complete(request) self._recorder.record_success(request, response, latency=time.perf_counter() - start) return response except Exception as e: self._recorder.record_failure(request, error=e, latency=time.perf_counter() - start) raise

这个模式看起来简单,难在“记录什么、怎么存储、怎么检索”。我的经验是:至少要支持按request_id追踪一条完整链路,从用户请求进入系统开始,到最终响应返回,中间经过了多少次模型调用、每次调用用了哪个模型、Prompt是什么、输出是什么,全部串成一条trace。这样出了问题,不需要用户复述,直接看trace就能定位。

4. 完整案例:多模型客服工单处理系统

说了一堆模式,把它们串起来才是真正的功夫。我用一个实际做过的多模型客服工单处理系统为例子,把前面所有模式落一遍。

4.1 需求与约束

系统的功能是:接收用户提交的工单文本,自动完成意图分类、紧急度判断、回复建议生成。约束条件很现实:

  • 预算敏感,不能什么请求都用大模型;需要有一个分级路由策略。
  • 部分工单涉及用户隐私,必须走本地部署模型,不能走外部API。
  • 模型会频繁升级,代码不能跟着模型版本频繁改。
  • 线上偶发供应商超时,需要有降级方案。
  • 运营方需要看到每个工单的完整处理链路(用了哪个模型、为什么用这个模型、耗时多久)。

4.2 整体结构

整个系统的结构分为四层:

第一层是接入层,通过门面模式的CompletionService统一接收内部各业务方的处理请求。业务方完全不感知下游模型细节。

第二层是路由决策层,核心是意图路由器。路由器会读取工单内容,先做一个轻量级分类,再根据分类结果和敏感标记决定走哪个模型链路。比如简单工单走本地小模型,复杂投诉走外部强模型,敏感信息走本地专用模型。

第三层是模型适配层,每个供应商都有对应的适配器,统一转换成内部响应格式。这一层只负责“把模型差异抹平”。

第四层是增强处理层,包括上下文注入器(把历史工单、用户信息注入到Prompt里)、自我修正循环(生成结果校验不通过时自动修正)、可观测性代理(全链路记录)。

4.3 关键代码片段

路由决策是系统最核心的部分,我贴一段精简代码:

class TicketRouter: def __init__(self, classifier, factories, sensitive_detector, recorder): self._classifier = classifier self._factories = factories self._sensitive_detector = sensitive_detector self._recorder = recorder async def route(self, ticket: Ticket) -> Route: # 1. 先做意图分类(走便宜的本地模型) intent = await self._classifier.classify(ticket.text) # 2. 敏感信息检测 is_sensitive = self._sensitive_detector.contains(ticket.text) # 3. 路由决策 if is_sensitive: return Route(provider="local-secure", model="local-7b", priority="normal") elif intent == "complex_complaint": return Route(provider="openai", model="gpt-4o", priority="high") elif intent == "simple_query": return Route(provider="local", model="local-3b", priority="low") else: return Route(provider="fallback", model="medium", priority="normal")

这段代码的精髓在于:分类动作本身就走的是小模型,因为它只需要判断意图的粗粒度方向,不需要生成内容,用便宜模型就够。很多团队一上来就把工单文本发给大模型做“分类+生成+建议”三合一,结果成本立刻失控。

模型调用主链路:

class CompletionService: def __init__(self, router, adapter_factory, validator_factory, recorder): self._router = router self._adapter_factory = adapter_factory self._validator_factory = validator_factory self._recorder = recorder async def handle_ticket(self, ticket: Ticket) -> TicketResult: route = await self._router.route(ticket) # 构建上下文 docs = await retrieve_docs(ticket.id) messages = ContextInjector(...).inject( system_prompt="你是客服助手,回答格式为JSON。", user_question=ticket.text, retrieved=docs ) adapter = self._adapter_factory.create(route.provider) # 带自我修正的生成 generator = PartialGenerator(adapter, messages) validator = self._validator_factory.create(route.task_type) output, result = await self_correcting_generate(generator, validator, max_attempts=2) # 记录全链路信息 self._recorder.record_ticket_trace(ticket.id, route, messages, output, result) return TicketResult(...)

这段代码体现的就是门面、路由、适配器、注入器、自我修正循环的协作。每一层只做自己的事,出了问题能很快定位到具体层。

4.4 实测效果与成本表现

这个系统上线后我统计了两周数据,几个数字比较有参考价值:

工单类型路由目标平均延迟单条成本准确率
简单查询本地3B模型0.8秒极低92%
普通工单中等模型2.1秒中87%
复杂投诉GPT-4o4.5秒高95%
敏感内容本地7B模型1.5秒低90%

混合路由策略和纯大模型方案相比,整体成本大约下降了60%,延迟下降了45%,而最关键工单的准确率没有明显下降。不是所有请求都需要最强模型,需求分级本身就能省一大半钱。这是意图路由最实在的收益。

当时线上也遇到过意外情况:外部模型有一次持续了二十分钟的故障,网关层超时大量抛出。因为这个系统每个环节都有超时控制和降级策略,复杂投诉自动降到本地模型处理,虽然回答质量略降,但整个系统没有宕机。这就是门面加路由加降级配合起来的效果。

5. 我在实战中踩过的坑与边界

模式提供了思路,但真正落地时有一堆细节问题。这一章写几个我亲测吃亏后总结出来的教训。

5.1 过度设计是真实存在的陷阱

有一个项目,我一开始就把所有模式全部套上去:工厂、门面、代理、观察者、策略……结果代码量翻了三倍,团队同事接手时一脸茫然。后来我意识到,设计模式不是装饰品,是为了解决真实存在的复杂度。如果系统只接一个模型、调用点只有两处,门面模式和适配器模式带来的收益就非常有限。

我的判断标准是:当“变更的代价”明显升高时,才值得引入模式。比如第一次需要在A模型和B模型之间做切换时,再做工厂和适配器;当第三个调用方出现时,再做门面;当Prompt策略开始出现差异化效果时,再把策略模式引进来。提前设计的成本往往比事后再重构要高。

5.2 没有模拟器,测试就是空中楼阁

AI应用的测试是老大难问题。每次调用真实模型,结果都是概率性的,不可能用传统断言做回归测试。我的做法是构建一个模型模拟器:在测试环境里,用预先录制好的响应来替代真实模型调用,这样核心业务逻辑可以稳定测试,CI才能跑得起来。

class MockAdapter: def __init__(self, response_map): self._response_map = response_map async def complete(self, request): key = request.messages[-1]["content"] return self._response_map.get(key, InternalResponse(content="mock", usage=TokenUsage(1, 1)))

模拟器解决了“逻辑正确性”的回归问题,但真实模型的质量验证还是需要单独的评测集。我的做法是维护一个三五十条真实样本的评测集,每次模型或Prompt有变更,就批量跑一遍,人工抽查质量分。这个成本不高,但能大幅减少线上翻车概率。

5.3 模式会失效的时刻

有几个场景我明确建议放弃复杂模式:

第一个是原型验证阶段。这个阶段代码就是用来快速验证“这个功能到底可不可行”的,先直接用最简单的方式调模型,跑通了再谈结构化。我见过不少团队连MVP都没跑通,就在纠结适配器接口怎么定义,这不是工程素养,是资源浪费。

第二个是单模型单链路的工具类脚本。比如一个离线数据清洗脚本,只调一个模型一次,不需要路由、门面、观察者这些结构,函数式写法反而清晰。

第三个是团队没有足够熟悉模式的人维护时。设计模式是“结构是一种沟通”,但如果团队里没人看得懂这套结构,反而成了维护负担。这个判断虽然听起来不够“技术”,但在实际项目里非常重要。

5.4 给后来者的三个操作建议

如果让我给正在建设AI应用的团队提三个建议,我会这么说:

第一,先把可观测性做起来,再谈优化。没有全链路trace,你根本不知道质量下降是模型换代导致的、还是上下文排序变化导致的、还是路由规则改错了。可观测性代理的搭建成本并不高,但它决定了你能不能从“靠运气”变成“靠数据”。

第二,把模型变更当发布事件管理,而不是代码变更。模型的Prompt策略、版本升级都要走评审和灰度流程。我见过太多线上问题,最后定位到“昨天顺手改了一个system prompt”。给Prompt配置一个版本号和全链路trace,是AI项目稳定性的基石。

第三,用成本指标驱动架构演进。不要只看准确率,要把单次调用成本和整体预算一起纳入设计考量。当你把成本指标挂到路由决策层,你会发现许多架构上的优化方向会自己浮现出来。

坦白说,生成式AI的出现确实让一部分经典设计模式显得“老派”,但它真正改变的不是“要不要用模式”,而是“模式要解决的问题变了”。从函数调用的确定性世界,走进概率模型的非确定性世界,所幸软件工程沉淀下来的抽象分层思想没有过时,只是需要换一批具体的实现方式。如果你也在做AI应用,不妨对照上面的模式,重新审视一下自己的代码——哪些复杂度被隐藏了,哪些复杂度被放大了,这比纠结“用没用对设计模式”要更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询