第1710篇:Domain-Driven Design在AI应用建模中的适配——限界上下文与AI服务边界
第1710篇:Domain-Driven Design在AI应用建模中的适配——限界上下文与AI服务边界
DDD(领域驱动设计)这个词,在Java圈子里已经流行了很多年。但我见过的很多项目,"DDD"用得很形式化:画了上下文图,写了聚合根,然后代码里还是到处 xxxService、xxxManager,本质上还是过程式的。
在AI应用里,DDD的价值更具体也更难用好。"AI"不是一个领域,它是一种能力,它会嵌入到各种不同的领域里。你做的是教育AI、医疗AI还是客服AI,领域知识完全不同,但背后都在用同样的LLM API。
这篇文章我想聊的核心问题是:当AI能力作为一个子域,嵌入到业务领域里,边界应该怎么划?两边如何协作? 这是AI工程里一个很现实但少人系统讲过的话题。
一、一个反例:AI能力泄漏到业务域的后果
先看一个常见的错误架构。
一个在线教育平台,有个"AI智能辅导"功能:学生提问→AI回答+推荐相关知识点→记录学习行为。
错误的做法:
// Service层直接耦合OpenAI
@Service
public class StudyAssistantService {
@Autowired
private OpenAiChatClient openAiClient; // 直接依赖具体AI实现
public StudyResponse help(Student student, String question) {
// 业务逻辑和AI调用混在一起
String prompt = "你是一个数学老师,学生叫" + student.getName() +
",年级" + student.getGrade() + "。学生问:" + question;
ChatResponse response = openAiClient.call(
new Prompt(prompt,
OpenAiChatOptions.builder()
.withModel("gpt-4o")
.withTemperature(0.3)
.build())
);
String answer = response.getResult().getOutput().getContent();
// 在业务方法里解析AI的响应格式
String[] parts = answer.split("【推荐知识点】");
String mainAnswer = parts[0];
List<String> recommendations = parts.length > 1
? Arrays.asList(parts[1].split(","))
: List.of();
// 记录学习行为
studyRecordRepository.save(new StudyRecord(
student.getId(), question, mainAnswer
));
return new StudyResponse(mainAnswer, recommendations);
}
}这段代码有哪些问题?
OpenAiChatClient直接在业务Service里用,换个AI供应商要改业务代码- Prompt模板是硬编码字符串,每次改Prompt都要改业务逻辑
- AI响应的解析逻辑(
split("【推荐知识点】"))和业务逻辑混在一起 - 如果AI服务异常,整个业务方法挂掉,没有降级
- 无法对AI调用做独立的监控、限流、缓存
这就是"AI能力泄漏到业务域"的典型症状。
二、DDD的核心概念在AI场景的映射
先明确几个DDD核心概念在AI场景里对应什么:
限界上下文(Bounded Context)
一个限界上下文是一个语义一致的范围。在AI应用里,我划分出两类主要上下文:
- 业务上下文:教育、医疗、客服……这里的概念是"学生"、"课程"、"诊断"、"工单"等业务概念
- AI能力上下文:语言模型调用、向量检索、内容安全、提示工程……这里的概念是"Prompt"、"嵌入"、"Token"、"响应"
这两个上下文有防腐层(Anti-Corruption Layer)隔离。业务上下文不直接用AI上下文的概念,而是通过翻译层交互。
聚合根(Aggregate Root)
在AI对话场景里,ConversationSession(会话)是一个好的聚合根:
- 它维护了一致性边界(对话的状态、历史、配置)
- 外部通过会话ID引用,不能直接操作内部的消息列表
- 它负责执行业务规则(比如会话不活跃就不能发消息)
领域服务(Domain Service)
不属于任何聚合但包含领域逻辑的操作。比如"跨会话搜索",它涉及多个会话聚合,不属于单个聚合,但包含业务逻辑,适合作为领域服务。
防腐层(Anti-Corruption Layer, ACL)
这是AI应用里最重要的DDD模式。防腐层保护业务域不被AI技术细节污染。
三、限界上下文图设计
各上下文之间的关系:
- 教育业务上下文 是核心域,包含学习规则、进度跟踪等核心业务
- AI能力上下文 是支撑子域,提供智能问答能力
- 防腐层 在两者之间翻译语言,保护业务域不依赖AI实现细节
四、防腐层的具体实现
防腐层的职责是:把业务概念翻译成AI能力上下文的概念,以及把AI的输出翻译回业务概念。
// ===== 业务上下文的接口(业务语言,不涉及AI技术细节)=====
// 业务接口:从业务角度定义"AI辅导能力"
public interface TutoringCapability {
// 回答学生问题(业务语言)
TutoringResponse answerQuestion(Student student, Subject subject, String question);
// 生成练习题
List<ExerciseProblem> generateProblems(Student student, KnowledgePoint knowledgePoint, int count);
// 分析学生的解题过程(评估)
ProblemAnalysis analyzeSolution(Student student, Problem problem, String studentSolution);
}
// 业务概念:辅导响应(不包含任何AI技术术语)
public record TutoringResponse(
String mainExplanation,
List<String> keyPoints,
List<KnowledgePoint> recommendedKnowledgePoints,
DifficultyLevel estimatedLevel,
boolean requiresTeacherReview // 是否需要人工教师介入
) {
public enum DifficultyLevel { BASIC, INTERMEDIATE, ADVANCED }
}
// 业务概念:知识点
public record KnowledgePoint(
String id,
String name,
String subject,
String gradeLevel
) {}
// ===== 防腐层实现(翻译业务语言 <-> AI技术语言)=====
@Service
public class AITutoringACL implements TutoringCapability {
// 依赖AI上下文的接口(不是具体的OpenAI客户端)
private final LanguageModelPort languageModelPort;
private final KnowledgeBasePort knowledgeBasePort;
// Prompt模板管理(在ACL里,不在业务层里)
private final PromptTemplateRepository promptTemplates;
@Override
public TutoringResponse answerQuestion(Student student, Subject subject, String question) {
// 1. 翻译:业务概念 -> AI上下文概念
QueryContext aiContext = buildQueryContext(student, subject, question);
// 2. 检索相关知识(向量搜索,业务层不知道)
List<KnowledgeChunk> relevantChunks = knowledgeBasePort.search(
aiContext.queryEmbedding(), subject.code(), 5
);
// 3. 构建Prompt(在ACL里,业务层不关心)
String prompt = promptTemplates.getTemplate("tutoring_answer")
.render(Map.of(
"student_name", student.getName(),
"grade", student.getGradeLevel(),
"subject", subject.getName(),
"question", question,
"context", formatChunks(relevantChunks)
));
// 4. 调用AI
AIModelResponse aiResponse = languageModelPort.complete(
LanguageModelRequest.builder()
.prompt(prompt)
.maxTokens(1000)
.temperature(0.3)
.build()
);
// 5. 翻译:AI响应 -> 业务概念
return translateToTutoringResponse(aiResponse, relevantChunks);
}
@Override
public List<ExerciseProblem> generateProblems(
Student student, KnowledgePoint knowledgePoint, int count) {
String prompt = promptTemplates.getTemplate("problem_generation")
.render(Map.of(
"knowledge_point", knowledgePoint.name(),
"grade", student.getGradeLevel(),
"difficulty", assessStudentLevel(student, knowledgePoint),
"count", count
));
AIModelResponse response = languageModelPort.complete(
LanguageModelRequest.builder()
.prompt(prompt)
.responseFormat("json")
.maxTokens(2000)
.temperature(0.7) // 出题需要一些创意
.build()
);
return parseProblems(response.content());
}
// 翻译AI响应到业务概念(最关键的翻译逻辑)
private TutoringResponse translateToTutoringResponse(
AIModelResponse aiResponse, List<KnowledgeChunk> relevantChunks) {
// 解析AI返回的结构化内容
TutoringAIOutput aiOutput = parseAIOutput(aiResponse.content());
// 翻译推荐的知识点(从知识库里查找匹配的KnowledgePoint实体)
List<KnowledgePoint> recommendations = aiOutput.recommendedTopics().stream()
.map(topic -> knowledgeBasePort.findKnowledgePointByName(topic))
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
// 判断是否需要人工介入
boolean needsReview = aiOutput.confidence() < 0.7 ||
aiOutput.containsSensitiveContent();
return new TutoringResponse(
aiOutput.mainExplanation(),
aiOutput.keyPoints(),
recommendations,
determineDifficulty(aiOutput.complexity()),
needsReview
);
}
// 内部的AI输出格式(不暴露给业务层)
private record TutoringAIOutput(
String mainExplanation,
List<String> keyPoints,
List<String> recommendedTopics,
double confidence,
int complexity, // 1-10
boolean containsSensitiveContent
) {}
// 内部方法:评估学生水平
private String assessStudentLevel(Student student, KnowledgePoint kp) {
// 查询学生在该知识点的历史表现
return "intermediate"; // 简化实现
}
private TutoringAIOutput parseAIOutput(String content) {
// 解析JSON响应...
// 如果解析失败,返回降级结果
return new TutoringAIOutput(content, List.of(), List.of(), 0.8, 5, false);
}
private TutoringResponse.DifficultyLevel determineDifficulty(int complexity) {
return switch (complexity) {
case 1, 2, 3 -> TutoringResponse.DifficultyLevel.BASIC;
case 4, 5, 6 -> TutoringResponse.DifficultyLevel.INTERMEDIATE;
default -> TutoringResponse.DifficultyLevel.ADVANCED;
};
}
private QueryContext buildQueryContext(Student student, Subject subject, String question) {
return new QueryContext(question, subject.code(), student.getGradeLevel());
}
private String formatChunks(List<KnowledgeChunk> chunks) {
return chunks.stream()
.map(KnowledgeChunk::content)
.collect(Collectors.joining("\n\n---\n\n"));
}
private List<ExerciseProblem> parseProblems(String content) {
// 解析生成的题目...
return List.of();
}
record QueryContext(String query, String subjectCode, String gradeLevel) {}
}这个防腐层有几个关键设计点:
- 业务层调用
TutoringCapability接口,不知道底层是OpenAI还是Claude - Prompt的构建在ACL里,业务层完全不知道Prompt的存在
- AI输出的解析在ACL里,业务层看到的是业务概念(
KnowledgePoint、TutoringResponse) - 降级逻辑(AI失败时如何返回)也在ACL里处理
五、AI能力端口(Port)设计
防腐层内部,用端口(Port)接口隔离具体的AI技术实现:
// ===== AI能力的端口接口(在防腐层内部使用)=====
// 语言模型端口
public interface LanguageModelPort {
AIModelResponse complete(LanguageModelRequest request);
Flux<String> streamComplete(LanguageModelRequest request);
boolean isAvailable();
}
// 请求对象(AI上下文的概念,不是业务概念)
public record LanguageModelRequest(
String prompt,
String systemPrompt,
String responseFormat, // "text" or "json"
int maxTokens,
double temperature,
String preferredModel // 首选模型,不强制要求
) {
public static Builder builder() { return new Builder(); }
public static class Builder {
private String prompt;
private String systemPrompt;
private String responseFormat = "text";
private int maxTokens = 2000;
private double temperature = 0.7;
private String preferredModel;
public Builder prompt(String p) { this.prompt = p; return this; }
public Builder systemPrompt(String s) { this.systemPrompt = s; return this; }
public Builder responseFormat(String f) { this.responseFormat = f; return this; }
public Builder maxTokens(int m) { this.maxTokens = m; return this; }
public Builder temperature(double t) { this.temperature = t; return this; }
public Builder preferredModel(String m) { this.preferredModel = m; return this; }
public LanguageModelRequest build() {
Objects.requireNonNull(prompt, "prompt required");
return new LanguageModelRequest(
prompt, systemPrompt, responseFormat, maxTokens, temperature, preferredModel
);
}
}
}
// AI响应
public record AIModelResponse(
String content,
String modelUsed,
int promptTokens,
int completionTokens,
boolean isComplete, // 是否正常结束(非截断)
boolean wasCached
) {}
// 知识库端口
public interface KnowledgeBasePort {
List<KnowledgeChunk> search(float[] queryEmbedding, String subjectCode, int topK);
Optional<KnowledgePoint> findKnowledgePointByName(String name);
void indexDocument(KnowledgeDocument document);
}
// 知识块(AI上下文的概念)
public record KnowledgeChunk(
String chunkId,
String content,
String subjectCode,
String knowledgePointId,
double relevanceScore
) {}
// ===== 适配器实现(连接AI上下文和具体技术)=====
// Spring AI的适配器实现
@Service
@ConditionalOnProperty(name = "ai.provider", havingValue = "openai")
public class SpringAILanguageModelAdapter implements LanguageModelPort {
private final ChatClient chatClient;
@Override
public AIModelResponse complete(LanguageModelRequest request) {
try {
ChatResponse response = chatClient.prompt()
.system(request.systemPrompt())
.user(request.prompt())
.call()
.chatResponse();
return new AIModelResponse(
response.getResult().getOutput().getContent(),
response.getMetadata().getModel(),
response.getMetadata().getUsage().getPromptTokens().intValue(),
response.getMetadata().getUsage().getCompletionTokens().intValue(),
"stop".equals(response.getResult().getMetadata().getFinishReason()),
false
);
} catch (Exception e) {
throw new AICapabilityException("语言模型调用失败", e);
}
}
@Override
public Flux<String> streamComplete(LanguageModelRequest request) {
return chatClient.prompt()
.system(request.systemPrompt())
.user(request.prompt())
.stream()
.content();
}
@Override
public boolean isAvailable() {
// 可以做健康检查
return true;
}
}
// 本地模型的适配器实现(比如用Ollama)
@Service
@ConditionalOnProperty(name = "ai.provider", havingValue = "ollama")
public class OllamaLanguageModelAdapter implements LanguageModelPort {
private final OllamaChatClient ollamaClient;
@Override
public AIModelResponse complete(LanguageModelRequest request) {
// Ollama的具体实现...
return null;
}
@Override
public Flux<String> streamComplete(LanguageModelRequest request) {
return Flux.empty();
}
@Override
public boolean isAvailable() {
return true;
}
}六、领域事件:跨上下文通信
不同的限界上下文之间通过领域事件通信,而不是直接调用:
// 业务上下文发布的事件
@DomainEvent
public record StudentAskedQuestion(
String studentId,
String questionId,
String subjectCode,
String questionContent,
Instant askedAt
) {}
// AI上下文监听事件,做自己的事情(异步)
@Service
public class AIKnowledgeIndexer {
// 当有新的学习问题时,更新AI的知识图谱(异步)
@EventListener
@Async
public void onStudentAsked(StudentAskedQuestion event) {
// 分析问题,更新学生的知识薄弱点画像
// 这是AI上下文的事,业务上下文不关心
log.info("更新学生{}的知识图谱,问题: {}", event.studentId(), event.questionId());
}
}
// AI辅导完成事件(AI上下文发布)
@DomainEvent
public record TutoringResponseGenerated(
String sessionId,
String studentId,
String subjectCode,
String questionId,
int tokensUsed,
boolean wasHelpful, // 初始假设为helpful,后续通过反馈更新
Instant generatedAt
) {}
// 业务上下文监听AI完成事件(更新学习记录)
@Service
public class StudyProgressTracker {
@EventListener
@Async
public void onTutoringCompleted(TutoringResponseGenerated event) {
// 更新学生的学习进度记录
studyRecordService.recordAIInteraction(
event.studentId(),
event.subjectCode(),
event.questionId()
);
// 更新学习时长统计等业务逻辑
}
}七、聚合根设计:以Student为例
学生聚合是业务上下文的核心,需要保持对AI能力的无感知:
// Student聚合根(业务上下文)
@Entity
@Table(name = "students")
public class Student implements AggregateRoot {
@Id
private String id;
private String name;
private String gradeLevel;
private String schoolId;
// 学习偏好(业务概念,不是AI概念)
@Embedded
private LearningPreferences preferences;
// 各科目掌握水平(业务概念)
@ElementCollection
private Map<String, MasteryLevel> subjectMastery = new HashMap<>();
// 未发布的领域事件
@Transient
private List<Object> domainEvents = new ArrayList<>();
// 学生提问(领域行为,不是调用AI)
public AskQuestionResult ask(Subject subject, String questionContent) {
// 业务规则:每天最多问50个问题(避免滥用)
if (todayQuestionCount() >= 50) {
return AskQuestionResult.dailyLimitReached();
}
// 创建问题实体
Question question = Question.create(this.id, subject, questionContent);
// 发布领域事件(AI上下文监听这个事件来提供辅导)
domainEvents.add(new StudentAskedQuestion(
this.id, question.getId(), subject.code(), questionContent, Instant.now()
));
return AskQuestionResult.success(question);
}
// 记录AI辅导的学习效果(反馈)
public void recordTutoringFeedback(String questionId, FeedbackType feedback) {
// 更新知识掌握度
if (feedback == FeedbackType.UNDERSTOOD) {
Question question = findQuestion(questionId);
updateMastery(question.getSubject(), MasteryLevel.IMPROVED);
}
domainEvents.add(new StudentGaveFeedback(this.id, questionId, feedback));
}
// 各种业务规则方法
private int todayQuestionCount() {
// 查询今日提问数
return 0;
}
private Question findQuestion(String questionId) {
// 查询问题
return null;
}
private void updateMastery(Subject subject, MasteryLevel improvement) {
subjectMastery.merge(subject.code(), improvement, MasteryLevel::combine);
}
@Override
public List<Object> getDomainEvents() { return domainEvents; }
@Override
public void clearDomainEvents() { domainEvents.clear(); }
public enum MasteryLevel { BEGINNER, BASIC, INTERMEDIATE, ADVANCED, EXPERT;
public MasteryLevel combine(MasteryLevel other) {
return this.ordinal() >= other.ordinal() ? this : other;
}
}
public enum FeedbackType { UNDERSTOOD, PARTIALLY_UNDERSTOOD, NOT_UNDERSTOOD }
}
// 提问结果
public sealed interface AskQuestionResult
permits AskQuestionResult.Success, AskQuestionResult.LimitReached {
record Success(Question question) implements AskQuestionResult {}
record LimitReached() implements AskQuestionResult {}
static AskQuestionResult success(Question q) { return new Success(q); }
static AskQuestionResult dailyLimitReached() { return new LimitReached(); }
}八、应用服务层:协调业务和AI
应用服务(Application Service)是协调者,它不包含业务规则,但知道如何协调业务上下文和AI能力上下文:
@Service
@Transactional
public class StudyAssistantApplicationService {
private final StudentRepository studentRepo;
private final TutoringCapability tutoringCapability; // 通过ACL接口
private final SubjectRepository subjectRepo;
private final DomainEventPublisher eventPublisher;
// 应用用例:学生提问
public TutoringResponseDTO getHelp(GetHelpCommand command) {
// 1. 加载聚合
Student student = studentRepo.findById(command.studentId())
.orElseThrow(() -> new StudentNotFoundException(command.studentId()));
Subject subject = subjectRepo.findByCode(command.subjectCode())
.orElseThrow(() -> new SubjectNotFoundException(command.subjectCode()));
// 2. 执行业务规则(通过聚合根)
AskQuestionResult askResult = student.ask(subject, command.question());
switch (askResult) {
case AskQuestionResult.LimitReached l -> {
return TutoringResponseDTO.limitReached("今日提问次数已达上限,明天再来吧");
}
case AskQuestionResult.Success s -> {
// 3. 通过ACL调用AI能力(业务逻辑和AI能力完全分离)
TutoringResponse aiResponse = tutoringCapability.answerQuestion(
student, subject, command.question()
);
// 4. 保存状态
studentRepo.save(student);
// 5. 发布领域事件
eventPublisher.publishAll(student.getDomainEvents());
student.clearDomainEvents();
// 6. 翻译为应用层DTO(屏蔽领域细节)
return TutoringResponseDTO.success(aiResponse, s.question().getId());
}
}
// 不可达,但编译器需要
throw new IllegalStateException();
}
}
// 应用层DTO(不是业务概念,是面向前端的传输对象)
public record TutoringResponseDTO(
boolean success,
String questionId,
String explanation,
List<String> keyPoints,
List<String> recommendedTopics,
String difficultyLevel,
boolean needsTeacherReview,
String limitMessage
) {
public static TutoringResponseDTO success(TutoringResponse resp, String questionId) {
return new TutoringResponseDTO(
true, questionId, resp.mainExplanation(),
resp.keyPoints(),
resp.recommendedKnowledgePoints().stream().map(KnowledgePoint::name).toList(),
resp.estimatedLevel().name(),
resp.requiresTeacherReview(),
null
);
}
public static TutoringResponseDTO limitReached(String message) {
return new TutoringResponseDTO(false, null, null, null, null, null, false, message);
}
}九、DDD在AI项目落地的现实建议
理论讲完了,来说几点现实中的经验:
建议1:不要一上来就全套DDD
中小项目先保证防腐层隔离就够了,不需要聚合根、领域事件一起上。把AI能力封在一个接口后面,业务层不直接依赖AI SDK,这是最有效的一步。
建议2:Prompt是领域知识,要当代码管理
很多团队把Prompt放在配置文件甚至数据库里,改起来方便但版本管理混乱。我建议把核心Prompt作为"代码",放在代码库里,像函数一样有名字、有版本、有测试。
建议3:AI失败要有业务降级,不是技术降级
技术降级是:AI超时了,返回500错误。业务降级是:AI超时了,给学生推荐一个相关的人工辅导预约。后者才是真正有价值的降级,需要在应用服务层设计,防腐层只做技术层面的重试。
建议4:AI上下文的持续演化
随着AI技术进步,你的AI上下文会频繁变化:换模型、改Prompt策略、引入RAG……好的防腐层让这些变化不影响业务逻辑。如果每次换模型都要改业务Service,说明防腐层没做到位。
小结
DDD在AI应用建模中的核心适配点:
- 限界上下文:业务域和AI能力域严格分离,通过防腐层交互
- 防腐层:翻译业务概念和AI技术概念,保护业务域不被AI细节污染
- 端口与适配器:AI能力接口化,具体实现可以随时替换
- 聚合根:封装业务规则,不直接依赖AI能力
- 领域事件:上下文间的松耦合通信,AI上下文监听业务事件,业务上下文监听AI完成事件
最终目的只有一个:当AI技术进步、供应商变更、模型升级的时候,你的业务代码不需要改变。
