1. 项目概述
最近在开发一个基于LangChain4j的AI志愿填报顾问系统,这个项目让我对Java生态中的AI应用开发有了全新的认识。作为一名长期从事Java开发的工程师,第一次将大模型能力整合到传统Java应用中,整个过程既充满挑战又收获颇丰。
这个系统主要解决高考志愿填报过程中的信息不对称问题。传统志愿填报依赖人工咨询和纸质资料,效率低下且信息更新不及时。我们的AI顾问能够7×24小时提供专业、及时的志愿填报建议,通过自然语言交互理解考生需求,结合最新院校和专业数据给出个性化推荐。
2. 技术架构解析
2.1 核心组件选型
项目技术栈采用Spring Boot + LangChain4j + 阿里云百炼平台:
- Spring Boot 3.2:基础框架,提供依赖注入、Web接口等核心能力
- LangChain4j 1.0.1:Java版LangChain,封装大模型交互的复杂细节
- 阿里云百炼:提供稳定可靠的国产大模型服务(qwen-plus)
- Redis:实现会话记忆持久化和向量数据存储
- MySQL:存储用户预约信息等结构化数据
选择LangChain4j而非Python生态的LangChain,主要基于以下考虑:
- 团队Java技术栈成熟,迁移成本低
- 与Spring生态无缝集成,注解式开发体验好
- 类型安全,编译期就能发现很多潜在问题
- 性能优势,特别是处理高并发请求时
2.2 系统交互流程
mermaid复制graph TD
A[前端页面] -->|HTTP请求| B[Spring Boot]
B --> C[LangChain4j]
C --> D[大模型API]
C --> E[向量数据库]
C --> F[业务数据库]
实际开发中,这个架构有几个关键优化点:
- 异步处理:大模型响应较慢,所有接口都设计为异步非阻塞
- 缓存策略:高频查询结果缓存到Redis,减轻数据库压力
- 降级方案:大模型不可用时自动切换规则引擎兜底
3. 核心功能实现
3.1 基础会话功能
3.1.1 快速集成
LangChain4j与Spring Boot的整合异常简单,只需几步:
- 添加起步依赖:
xml复制<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
- 配置application.yml:
yaml复制langchain4j:
open-ai:
chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${API_KEY}
model-name: qwen-plus
- 开发聊天接口:
java复制@RestController
public class ChatController {
@Autowired
private OpenAiChatModel model;
@GetMapping("/chat")
public String chat(String message) {
return model.generate(message);
}
}
踩坑记录:API_KEY建议通过环境变量注入,不要硬编码在配置文件中。我们曾因误提交含API_KEY的代码到GitHub导致密钥泄露,不得不重新申请。
3.1.2 流式响应
传统阻塞式响应体验较差,改为流式响应后效果提升明显:
java复制@GetMapping(value = "/chat-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(String message) {
return Flux.create(sink -> {
model.generate(message)
.onNext(sink::next)
.onComplete(sink::complete)
.onError(sink::error)
.start();
});
}
前端配合EventSource接收数据:
javascript复制const eventSource = new EventSource(`/chat-stream?message=${encodeURIComponent(msg)}`);
eventSource.onmessage = (e) => {
// 实时追加到页面
};
3.2 志愿填报专业问答
3.2.1 限定问答范围
通过@SystemMessage注解约束AI只回答志愿相关问题:
java复制@AiService
public interface AdvisorService {
@SystemMessage("你是一名高考志愿填报顾问,只能回答与大学、专业、志愿填报相关的问题")
String advise(String question);
}
更复杂的场景可以使用外部文件定义系统消息:
code复制resources/system-message.txt
内容示例:
code复制你是一名资深高考志愿填报顾问,具备10年指导经验。你需要:
1. 只回答与志愿填报相关的问题
2. 对院校和专业的评价保持中立客观
3. 不知道的内容明确告知"不清楚"
3.2.2 结构化提示词
使用Mustache模板引擎构建动态提示词:
java复制@SystemMessage("""
考生信息:
- 省份:{{province}}
- 预估分数:{{score}}
- 首选科目:{{subject}}
""")
String analyze(@UserMessage String question,
@V("province") String province,
@V("score") int score,
@V("subject") String subject);
3.3 会话记忆管理
3.3.1 基础实现
java复制@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.build();
}
3.3.2 Redis持久化
将会话记忆存入Redis实现持久化:
- 实现ChatMemoryStore接口:
java复制public class RedisChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = redisTemplate.opsForValue().get(memoryId.toString());
return ChatMessageDeserializer.messagesFromJson(json);
}
// 其他方法实现...
}
- 配置使用Redis存储:
java复制@Bean
public ChatMemoryStore chatMemoryStore() {
return new RedisChatMemoryStore(redisTemplate);
}
性能优化:实际测试发现频繁读写Redis会影响响应速度,最终方案是本地缓存+Redis异步持久化,TPS从50提升到300+。
4. RAG知识库增强
4.1 实现原理
RAG(检索增强生成)技术流程:
-
文档处理:
- 加载:PDF/Word等格式的招生简章
- 分割:按段落切分文本
- 向量化:通过Embedding模型转换为向量
-
检索流程:
- 用户问题向量化
- 在向量数据库检索相似内容
- 将检索结果作为上下文注入提示词
-
生成流程:
- 大模型基于上下文生成回答
- 回答中包含引用来源
4.2 具体实现
4.2.1 文档处理
java复制// 加载文档
List<Document> documents = ClassPathDocumentLoader.loadDocuments("admission-data");
// 配置处理管道
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(new DocumentByParagraphSplitter(500, 50))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 处理并存储
ingestor.ingest(documents);
4.2.2 检索增强
java复制@Bean
public ContentRetriever contentRetriever(EmbeddingStore store) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.6)
.build();
}
@AiService
public interface AdvisorService {
@SystemMessage("你是一名志愿填报顾问...")
String advise(@UserMessage String question,
@Retriever ContentRetriever retriever);
}
关键参数:
- chunkSize:500-1000字效果最佳
- overlap:50-100字避免信息割裂
- topK:3-5条相关片段足够
5. 工具调用开发
5.1 预约服务实现
- 定义领域服务:
java复制@Service
public class ReservationService {
public void create(Reservation reservation) {
// 数据库操作
}
public Reservation findByPhone(String phone) {
// 查询逻辑
}
}
- 暴露工具方法:
java复制@Component
public class ReservationTools {
@Autowired
private ReservationService reservationService;
@Tool("创建志愿咨询预约")
public String createReservation(
@P("考生姓名") String name,
@P("手机号") String phone,
@P("预约时间") String time) {
// 参数转换和校验
reservationService.create(...);
return "预约成功";
}
}
5.2 工具注册配置
java复制@Bean
public ToolsProvider toolsProvider(List<Object> toolBeans) {
return new ToolsProvider(toolBeans);
}
@AiService(toolsProvider = "toolsProvider")
public interface AdvisorService {
// 接口方法
}
6. 性能优化实践
6.1 缓存策略
- 向量缓存:
java复制@Cacheable(value = "embeddings", key = "#text")
public Embedding getEmbedding(String text) {
return embeddingModel.embed(text).content();
}
- 响应缓存:
java复制@Cacheable(value = "answers", key = "#question")
public String answerQuestion(String question) {
// 大模型调用
}
6.2 异步处理
使用Reactor实现全异步 pipeline:
java复制public Mono<String> handleQuestion(String question) {
return Mono.fromCallable(() -> embedQuestion(question))
.flatMap(embedding -> searchVectorDB(embedding))
.flatMap(context -> callLLM(question, context))
.subscribeOn(Schedulers.boundedElastic());
}
7. 部署注意事项
7.1 生产环境配置
yaml复制langchain4j:
open-ai:
chat-model:
timeout: 30s
max-retries: 3
temperature: 0.7
log-requests: true
log-responses: true
7.2 监控指标
建议监控以下指标:
- 大模型响应时间P99
- 令牌使用量
- 错误率(特别是限流错误)
- 缓存命中率
8. 项目总结
这个项目让我深刻体会到:
- Java生态成熟:从Spring整合到监控告警,整个工具链非常完善
- LangChain4j设计精妙:注解驱动的方式大幅降低开发复杂度
- 国产大模型可用性强:qwen-plus在中文场景表现优异
几个特别实用的开发技巧:
- 使用@AiService自动代理接口
- 通过@Tool暴露业务能力
- 用@SystemMessage控制AI行为
- Redis实现会话持久化
未来计划:
- 增加多模态能力(院校图片展示)
- 实现志愿表智能生成
- 开发移动端适配界面
整个项目代码已开源在GitHub,欢迎交流讨论。对于想要尝试Java+AI的开发者,我的建议是:
- 从简单场景入手
- 善用LangChain4j的注解能力
- 重视监控和性能优化
- 保持对新技术的好奇心