1. 为什么需要规范化的项目结构设计
当我在2023年第一次接触LangChain4j项目时,面对这个新兴的Java大语言模型集成框架,最让我头疼的不是API的使用复杂度,而是如何组织一个可维护、可扩展的项目结构。当时团队中有三位开发者同时开发不同功能模块,结果一周后就出现了以下典型问题:
- 工具类散落在5个不同层级的包中
- 模型服务实现与业务逻辑深度耦合
- 测试代码与生产代码交叉引用
- 新增一个对话场景需要修改8个文件
这种混乱直接导致我们的迭代效率下降了40%。经过两个月的重构,我们最终形成了一套清晰的分层方案,这也是今天要分享的核心内容。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础分层架构设计
2.1 经典三层架构的局限性
传统的controller-service-dao分层在LLM应用中会遇到三个主要问题:
- 模型交互层的特殊性:需要处理流式响应、token计算等非典型业务逻辑
- 向量存储的异构性:不同数据库的接入方式差异巨大
- 业务编排的复杂性:一个对话场景可能涉及多个模型调用和数据处理步骤
2.2 六层核心架构方案
经过三个生产项目的验证,我们总结出以下分层结构:
code复制src/main/java
├── application # 应用层
├── domain # 领域层
├── infrastructure # 基础设施层
│ ├── client # 模型客户端
│ ├── store # 向量存储
│ └── tool # 工具链
├── interface # 接口层
└── config # 配置层
每层的具体职责:
- interface层:处理HTTP/gRPC请求,做基础参数校验
- application层:业务流程编排,不包含具体业务规则
- domain层:核心业务逻辑和领域模型
- infrastructure层:
- client:封装不同LLM提供商的SDK
- store:实现向量存储的CRUD操作
- tool:自定义工具链(如PDF解析器)
- config层:Spring配置和Bean定义
关键设计原则:上层可以依赖下层,下层绝对不允许依赖上层。例如domain层不应该感知interface层的任何类。
3. 关键目录实现细节
3.1 模型客户端的最佳实践
在infrastructure/client中,我们按提供商划分包结构:
code复制client
├── openai
│ ├── OpenAIClient.java
│ ├── OpenAIConfig.java
│ └── model # DTO定义
├── azure
└── local # 本地模型
每个客户端实现需要处理三个核心问题:
- 异常转换:将提供商特定异常转为统一异常体系
- 指标收集:记录token用量、响应时间等
- 重试机制:对可重试错误自动处理
示例代码片段:
java复制public class OpenAIClient {
private final RetryTemplate retryTemplate;
public CompletionResult chatCompletion(ChatRequest request) {
return retryTemplate.execute(ctx -> {
// 记录原始请求
metrics.recordRequest(request);
try {
CompletionResult result = // 调用SDK
metrics.recordSuccess();
return result;
} catch (OpenAIThrottlingException e) {
metrics.recordThrottling();
throw e;
}
});
}
}
3.2 向量存储的抽象设计
在infrastructure/store中,我们定义了两个关键接口:
java复制public interface VectorStore {
String store(List<Float> vector, Document doc);
List<Document> search(List<Float> vector, int topK);
}
public interface DocumentLoader {
List<Document> load(Resource resource);
}
这样实现不同数据库时,业务代码无需修改:
code复制store
├── redis
├── pgvector
└── milvus
3.3 领域层的核心模式
推荐使用领域服务+能力模式组织domain层:
code复制domain
├── model # 领域对象
├── service # 领域服务
└── ability # 领域能力
例如处理PDF问答的场景:
java复制public class PdfQaAbility {
private final VectorStore store;
private final DocumentLoader loader;
public Answer answerQuestion(File pdf, String question) {
List<Document> docs = loader.load(pdf);
List<Float> embedding = // 生成embedding
List<Document> results = store.search(embedding, 3);
return // 构造提示词并调用LLM
}
}
4. 配置管理的技巧
4.1 多模型配置方案
在config层使用Spring Boot的@ConfigurationProperties:
java复制@ConfigurationProperties(prefix = "llm")
public record LlmProperties(
@NestedConfigurationProperty
OpenAIConfig openai,
@NestedConfigurationProperty
AzureConfig azure
) {}
@Bean
public OpenAIClient openAIClient(LlmProperties props) {
return new OpenAIClient(props.openai());
}
对应的application.yml:
yaml复制llm:
openai:
api-key: ${OPENAI_KEY}
timeout: 10s
azure:
endpoint: https://xxx.openai.azure.com
4.2 环境隔离策略
建议按环境拆分配置文件:
code复制resources
├── application.yml
├── application-dev.yml
└── application-prod.yml
通过spring.profiles.active指定运行环境,关键配置包括:
- 模型提供商开关
- 超时时间
- 重试策略
- 降级方案
5. 常见问题解决方案
5.1 循环依赖问题
当领域服务需要调用基础设施时,推荐使用依赖倒置:
java复制// 在domain层定义接口
public interface EmbeddingService {
List<Float> embed(String text);
}
// 在infrastructure实现
public class OpenAIEmbeddingService implements EmbeddingService {
// 实现具体逻辑
}
5.2 测试代码组织
测试目录应与main结构保持一致:
code复制src/test/java
├── application
├── domain
└── infrastructure
使用分层测试策略:
- 单元测试:覆盖领域模型和工具类
- 集成测试:验证Spring Bean装配
- 契约测试:保证客户端与模型API的兼容性
5.3 多模块项目结构
对于大型项目,建议按功能拆分模块:
code复制project
├── llm-core
├── llm-openai
├── llm-azure
└── llm-app
每个模块内部仍然保持六层结构,通过spring-boot-starter机制提供自动配置。
6. 演进式架构建议
在实际项目中,我建议采用渐进式优化:
- 初期:先确保基础分层(interface/application/domain/infrastructure)
- 中期:完善基础设施的抽象(客户端/存储)
- 后期:细化领域模型和能力划分
监控以下指标判断是否需要结构调整:
- 新增功能平均修改文件数
- 构建时间增长率
- 模块间依赖关系复杂度
