#复杂的AI业务如何拆解?从时序图到模块划分的领域驱动设计(DDD)实战

📅 2026/7/20 18:48:25 👁️ 阅读次数 📝 编程学习
#复杂的AI业务如何拆解?从时序图到模块划分的领域驱动设计(DDD)实战

当AI项目从Demo走向生产,最大的挑战往往不是模型精度,而是业务逻辑的复杂度

一、为什么AI项目越做越乱?

我见过不少AI项目,初期用Jupyter Notebook跑通了一个高精度模型,团队欢欣鼓舞地开始产品化。三个月后,代码库变成了谁也看不懂的一团乱麻——意图识别、Prompt模板、外部API调用、业务规则判断、数据持久化全部揉在一起,改一个功能要排查半天,加一个新场景要复制粘贴一大段代码。

症状很典型

  • Prompt满天飞:同样的业务规则,在代码的不同位置用不同的Prompt重复实现
  • 逻辑散落各处:一个订单退款流程的判断逻辑,分散在Controller、Service、Agent节点和Prompt指令里
  • 改了不敢动:不确定一个改动的波及范围,因为没有人说得清整个业务流程的完整链路

根本原因:把AI能力当作“魔法黑盒”塞进传统分层架构,忽视了AI应用本身也是一种复杂的业务系统,需要被领域建模、被结构化拆解

解决方案的思路很清晰:先用时序图把业务流程可视化,再用领域驱动设计(DDD)把业务逻辑模块化。这套方法论在传统软件工程中已被验证有效,而AI应用的复杂性恰恰让它更不可或缺。

二、第一步:用时序图把“黑盒”打开

任何复杂系统设计的第一步,都是把流程画出来。时序图(Sequence Diagram)是最合适的工具——它清晰展示了不同组件之间如何交互、谁调用谁、输入输出是什么。

2.1 选一个真实的AI业务场景

假设我们要开发一个**“智能订单助手”**,用户可以通过自然语言完成三类操作:

  1. 商品咨询:“这款耳机支持降噪吗?”
  2. 订单查询:“帮我查一下我最近的订单状态”
  3. 退款申请:“我要退掉昨天买的那个键盘”

这是一个典型的“多意图+多Agent协作”场景。如果用传统方式,代码里会是一堆if-else根据意图类型路由到不同的处理逻辑。这恰恰是最容易写乱的地方。

2.2 绘制交互时序图

先画出系统内部有哪些参与者,以及它们之间的交互关系:

┌────────┐ ┌──────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │ 用户 │ │ API网关 │ │ 意图识别器 │ │ 各业务Agent │ │ 外部服务 │ └────┬───┘ └────┬─────┘ └──────┬──────┘ └──────┬──────┘ └────┬─────┘ │ │ │ │ │ │ 1. 输入自然语言 │ │ │ │────────────>│ │ │ │ │ │ 2. 携带userId/sessionId │ │ │ │────────────────>│ │ │ │ │ │ 3. 调用LLM判断意图 │ │ │ │ (商品咨询/订单查询/退款) │ │ │ │────────────────>│ │ │ │ │ 4. 返回意图+实体 │ │ │ │<────────────────│ │ │ │ 5. 按意图路由 │ │ │ │ │<────────────────│ │ │ │ │ │ │ │ │ │ 6. 调用对应Agent │ │ │ │ │─────────────────────────────────────>│ │ │ │ │ │ 7. 查询数据库/调用外部API │ │ │ │────────────────>│ │ │ │ │ 8. 返回结果 │ │ │ │ │<────────────────│ │ 9. 返回最终回复 │ │ │ │<────────────│ │ │ │

这个时序图的价值在于:

  1. 明确了系统边界——哪些是外部调用方,哪些是内部组件
  2. 看清了调用链路——每个步骤的输入输出是什么
  3. 暴露了拆分点——意图识别和具体业务执行可以拆成独立模块

有了这张图,我们就可以开始模块划分了。

2.3 从时序图到模块边界

观察时序图,自然浮现出几个职责清晰的模块:

模块职责对应时序图中的角色
意图识别模块理解用户输入,判断意图类型和关键实体意图识别器
商品咨询模块处理商品知识问答业务Agent(商品)
订单查询模块查询订单状态和详情业务Agent(订单)
退款处理模块执行退款流程(含规则校验)业务Agent(退款)

这些模块之间的边界在时序图里也一目了然——它们通过“意图路由”这个中间环节解耦,互不直接调用。这正是DDD中限界上下文(Bounded Context)的雏形。

三、第二步:用DDD把模块变成“可维护的代码”

有了时序图建立的全局认知,我们可以进入DDD的战术设计阶段,把模块变成有清晰业务边界、可独立演化的代码单元

3.1 项目结构:按限界上下文组织代码

order-assistant/ ├── src/main/java/com/example/ │ ├── intent/ # 限界上下文:意图识别 │ │ ├── domain/ # 领域层 │ │ │ ├── Intent.java # 聚合根:意图 │ │ │ ├── IntentRepository.java │ │ │ └── IntentClassifier.java # 领域服务 │ │ ├── application/ # 应用层 │ │ │ └── IntentRecognitionService.java │ │ └── infrastructure/ # 基础设施层 │ │ └── LLMIntentClassifierImpl.java │ │ │ ├── product/ # 限界上下文:商品咨询 │ │ ├── domain/ │ │ │ ├── ProductKnowledge.java │ │ │ └── ProductRepository.java │ │ └── application/ │ │ └── ProductConsultationService.java │ │ │ ├── order/ # 限界上下文:订单查询 │ │ ├── domain/ │ │ │ ├── Order.java # 聚合根 │ │ │ └── OrderRepository.java │ │ └── application/ │ │ └── OrderQueryService.java │ │ │ ├── refund/ # 限界上下文:退款处理 │ │ ├── domain/ │ │ │ ├── RefundOrder.java │ │ │ ├── RefundPolicy.java # 值对象 │ │ │ └── RefundRepository.java │ │ └── application/ │ │ └── RefundApplicationService.java │ │ │ └── orchestration/ # 编排层(连接各上下文) │ ├── domain/ │ │ └── ConversationContext.java │ └── application/ │ └── OrderAssistantOrchestrator.java

这种结构的关键在于:每个限界上下文都有自己独立的领域层和应用层,彼此通过编排层协调,不直接依赖。

3.2 领域层:把业务规则写进代码

DDD的核心原则是充血模型——业务逻辑封装在领域对象中,而不是散落在Service层。对于AI业务来说,这意味着:

  • Prompt策略是领域知识的一部分,应该与业务规则放在一起
  • **意图的“业务含义”**由领域对象表达,而不是靠字符串匹配

Intent聚合根为例:

// intent/domain/Intent.javapublicclassIntent{privatefinalIntentIdid;privatefinalIntentTypetype;// CONSULTATION / QUERY / REFUNDprivatefinalStringrawInput;privatefinalMap<String,Object>entities;// 提取的实体privatefinaldoubleconfidence;privateIntent(IntentIdid,IntentTypetype,StringrawInput,Map<String,Object>entities,doubleconfidence){this.id=id;this.type=type;this.rawInput=rawInput;this.entities=entities;this.confidence=confidence;}// 工厂方法:创建意图publicstaticIntentfromRawInput(Stringinput,IntentClassifierclassifier){ClassificationResultresult=classifier.classify(input);returnnewIntent(IntentId.generate(),result.getType(),input,result.getEntities(),result.getConfidence());}// 领域行为:判断意图是否可信publicbooleanisConfident(){returnconfidence>0.7;}// 领域行为:获取必需的实体public<T>TgetEntity(Stringkey,Class<T>type){if(!entities.containsKey(key)){thrownewMissingEntityException("缺少必需实体: "+key);}returntype.cast(entities.get(key));}// getters...}

再看Order聚合根,将订单的业务规则封装在领域对象内:

// order/domain/Order.java@Entity@Table(name="orders")publicclassOrder{@IdprivateStringorderNumber;privateStringcustomerId;privateOrderStatusstatus;privateMoneytotalAmount;privateLocalDateTimecreatedAt;// 领域行为:取消订单publicvoidcancel(){if(status!=OrderStatus.PAID&&status!=OrderStatus.CREATED){thrownewIllegalStateException("订单状态为 "+status+",无法取消");}this.status=OrderStatus.CANCELLED;// 发布领域事件DomainEventPublisher.publish(newOrderCancelledEvent(this.orderNumber));}// 领域行为:确认退款资格publicbooleanisRefundable(){// 只有已支付且在7天内的订单可退款returnstatus==OrderStatus.PAID&&createdAt.isAfter(LocalDateTime.now().minusDays(7));}// getters/setters...}

3.3 应用层:协调领域对象完成用例

应用层不包含业务逻辑,只负责编排——从仓储取聚合根、调用领域方法、再存回去。

以退款处理为例:

// refund/application/RefundApplicationService.java@Service@Transactional@RequiredArgsConstructorpublicclassRefundApplicationService{privatefinalOrderRepositoryorderRepository;privatefinalRefundRepositoryrefundRepository;privatefinalRefundPolicyrefundPolicy;publicRefundResultapplyRefund(StringorderNumber,Stringreason){// 1. 从仓储获取订单聚合根Orderorder=orderRepository.findByOrderNumber(orderNumber).orElseThrow(()->newOrderNotFoundException(orderNumber));// 2. 调用聚合根的领域方法进行业务校验if(!order.isRefundable()){thrownewRefundNotAllowedException("该订单不具备退款资格");}// 3. 构建退款领域对象RefundOrderrefundOrder=RefundOrder.create(order,reason,refundPolicy.calculateRefundAmount(order));// 4. 执行退款(调用领域方法)refundOrder.process();// 5. 持久化refundRepository.save(refundOrder);// 6. 发布领域事件DomainEventPublisher.publish(newRefundProcessedEvent(refundOrder));returnRefundResult.from(refundOrder);}}

注意:整个业务流程中,业务规则都在领域层(Order.isRefundable()RefundOrder.process(),应用层只是把“原材料”准备好、把“流程”串起来。

3.4 基础设施层:隔离技术细节

基础设施层实现领域层定义的接口,把技术细节(LLM调用、数据库访问、外部API)隔离出去。

// intent/infrastructure/LLMIntentClassifierImpl.java@Component@RequiredArgsConstructorpublicclassLLMIntentClassifierImplimplementsIntentClassifier{privatefinalChatClientchatClient;privatestaticfinalStringPROMPT_TEMPLATE=""" 你是一个意图识别专家。分析以下用户输入,判断用户意图属于以下哪种类型: - CONSULTATION: 商品咨询(询问商品功能、规格、价格等) - QUERY: 订单查询(查询订单状态、物流等) - REFUND: 退款申请(要求退货、退款) 同时提取关键实体(如商品名称、订单号)。 用户输入:%s 以JSON格式返回结果。 """;@OverridepublicClassificationResultclassify(Stringinput){Stringresponse=chatClient.prompt().user(String.format(PROMPT_TEMPLATE,input)).call().content();// 解析LLM返回的JSONreturnparseClassificationResult(response);}}

四、第三步:用“编排层”把多Agent协作串起来

对于多意图场景,需要一个编排层(Orchestration Layer)来协调各限界上下文的协作。这里推荐使用工作流(Graph/Workflow)模式,把业务流程表达为一张有向图。

4.1 定义工作流状态

// orchestration/domain/ConversationContext.java@DatapublicclassConversationContext{privateStringsessionId;privateStringuserId;privateStringuserInput;privateStringfinalReply;// 中间结果privateIntentintent;privateList<Order>queriedOrders;privateRefundOrderrefundOrder;// 流程控制privateStringcurrentStep;privateList<String>executionPath;}

4.2 构建状态图(参考Spring AI Alibaba Graph模式)

// orchestration/infrastructure/AssistantGraphConfig.java@ConfigurationpublicclassAssistantGraphConfig{@BeanpublicStateGraph<ConversationContext>orderAssistantGraph(EntryNodeentryNode,IntentRouterNodeintentRouterNode,ProductAgentNodeproductNode,OrderAgentNodeorderNode,RefundAgentNoderefundNode){StateGraph<ConversationContext>graph=newStateGraph<>();// 注册节点graph.addNode("entry",entryNode);graph.addNode("intent_router",intentRouterNode);graph.addNode("product_agent",productNode);graph.addNode("order_agent",orderNode);graph.addNode("refund_agent",refundNode);// 设置入口graph.setStart("entry");// 固定边graph.addEdge("entry","intent_router");// 条件边:根据意图路由到不同Agentgraph.addConditionalEdges("intent_router",context->context.getIntent().getType().name(),Map.of("CONSULTATION","product_agent","QUERY","order_agent","REFUND","refund_agent",StateGraph.END,StateGraph.END));// Agent节点到结束graph.addEdge("product_agent",StateGraph.END);graph.addEdge("order_agent",StateGraph.END);graph.addEdge("refund_agent",StateGraph.END);returngraph;}}

这种图结构的好处是:业务流与代码结构一一对应。新增一个意图类型时,只需添加一个节点和一条条件边,不影响已有逻辑。

4.3 各Agent节点实现(以商品咨询为例)

// orchestration/infrastructure/nodes/ProductAgentNode.java@Component@RequiredArgsConstructorpublicclassProductAgentNodeimplementsNode<ConversationContext>{privatefinalProductConsultationServiceproductService;@OverridepublicConversationContextapply(ConversationContextcontext){StringuserInput=context.getUserInput();// 调用商品咨询限界上下文的应用服务Stringreply=productService.consult(userInput);context.setFinalReply(reply);context.setCurrentStep("product_agent");context.getExecutionPath().add("product_agent");returncontext;}}

五、关键原则与避坑指南

5.1 三条核心原则

  1. 时序图先行,代码后行:任何新功能先画时序图再写代码,保证团队对流程有一致理解
  2. 限界上下文 = 限界Prompt:每个上下文的Prompt策略独立维护,互不干扰
  3. 领域逻辑决不允许泄露到应用层:像if (status == PAID)这种判断,必须写在聚合根内部

5.2 常见陷阱

陷阱症状解决方案
贫血的聚合根所有业务逻辑都在Service层,领域对象只是数据容器把业务方法移到聚合根内
跨上下文直接调用Order上下文直接调用Refund上下文的方法通过领域事件异步解耦
Prompt散落各处同样的Prompt在3个地方出现,改一处忘两处把Prompt集中管理在对应上下文的配置类中
时序图与代码脱节画完图就扔一边,代码和图对不上把时序图作为设计文档,Code Review时对照检查

六、写在最后

回到开头的问题:为什么AI项目容易越做越乱?

因为AI能力的“魔法感”让团队忽视了它作为业务系统的本质。无论是意图识别、Prompt编排还是多Agent协作,本质上都是业务逻辑——它们应该被建模、被组织、被封装,而不是被“塞进”代码的缝隙里。

时序图和DDD的组合拳解决的是认知负载问题:

  • 时序图让团队看清流程全貌,减少沟通偏差
  • DDD的限界上下文把大问题拆成小问题,每个团队/每个人负责一个边界清晰的模块
  • 充血模型让业务规则显式化,新人看代码就能理解业务

这套方法论不是AI专属,但对于AI项目的复杂度来说,它比任何时候都更必要。下次接到新需求时,先画图、再分模块、最后写代码——让复杂业务变得可控,从这两张图开始。