软件工程第四次作业
构建一个能说会做的智能体
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/16760 |
| 这个作业的目标 | 构建一个能说会做的智能体 |
| 项目 | 内容 |
|---|---|
| 姓名 / 学号 | 黄德朗 / 3124004133,朱海碲 / 3124004150,邓兴荣 / 3124004128,何鹏 / 3124004131 |
| Github 项目地址 | https://github.com/delang666/delang666/tree/main/langchain4j-rag-demo |
| 开发环境 | Windows 11 + JDK 21 |
基于 LangChain4j 的"能说会做"高考志愿填报智能体
项目名称:AI 志愿填报顾问
技术栈:Java 21 + Spring Boot 3.5 + LangChain4j + DeepSeek + 阿里云百炼 Embedding + Redis + MySQL + Vue 3
一、需求描述
1.1 项目背景
高考志愿填报是一个典型的信息密集型决策场景:考生和家长需要在短时间内了解大量院校的办学情况、录取规则、专业分数线、招生联系方式,并结合自身分数做出"冲、稳、保"的志愿组合。这些信息分散在各个网站和文档中,检索成本高、专业门槛高。
本项目构建一个具备"能说会做"能力的高考志愿填报智能体:
- 能说:支持自然语言多轮对话,像真人顾问一样理解考生意图并给出建议;
- 会做:能够主动调用外部工具——检索本地知识库、查询掌上高考实时录取数据、向数据库写入预约单,而不是只做"文字接龙"。
1.2 目标用户与核心痛点
| 目标用户 | 核心痛点 | 本项目的解决方式 |
|---|---|---|
| 高三考生 | 不知道自己的分数能上什么学校 | 结合分数与历年录取数据,按冲/稳/保推荐 |
| 考生家长 | 不了解院校规则、专业前景 | RAG 检索院校 PDF 资料后由大模型总结 |
| 咨询机构 | 重复回答相同问题、预约登记繁琐 | 智能体自动答疑并引导完成预约下单 |
1.3 功能需求
| 编号 | 功能 | 能力类型 | 实现方式 |
|---|---|---|---|
| F1 | 查询目标院校简介、录取规则、奖学金、食宿条件、招生联系方式 | 能说 + 知识检索 | RAG 检索本地 PDF 知识库 |
| F2 | 查询某校某年某省分专业录取分数线与位次 | 能做 + 调用外部 API | Tool Calling 调用掌上高考接口 |
| F3 | 查询热门专业、天坑专业 | 能说 + 知识检索 | RAG 知识库 |
| F4 | 根据分数推荐冲/稳/保院校与专业 | 能说 + 综合推理 | 大模型结合检索与工具结果生成 |
| F5 | 志愿填报一对一预约(下单、查询) | 能做 + 写数据库 | Tool Calling 写入/查询 MySQL |
| F6 | 多轮对话记忆 | 加分项 | Redis 对话记忆 + 数据库持久化恢复 |
| F7 | 聊天记录查看、新建会话、暗黑模式 | 加分项(前端) | Vue 3 单页应用 |
| F8 | 输入安全防护 | 安全需求 | 输入护轨(长度/敏感词/注入检测) |
1.4 非功能需求
- 响应体验:回答必须流式输出,首字延迟尽量低;
- 并发安全:流式模型实例需支持多会话并发;
- 可用性:外部接口异常时降级返回,不能导致整个对话中断;
- 可持久化:服务重启或 Redis 过期后,对话记忆能够恢复;
- 可维护性:AI 编排、工具、业务服务、数据访问分层清晰。
二、业务流描述
2.1 总体架构
mermaid
flowchart LR
U[用户] -->|自然语言| FE[Vue 3 前端
流式渲染 / 暗黑模式]
FE -->|GET /api/chat| C[ChatController]
C --> G[输入护轨
PromptSafetyInputGuardrail]
G --> AI[AiRagDemoService
LangChain4j AiServices]
AI <--> MEM[(Redis
对话记忆 20 条)]
AI --> RAG[RAG 检索器
Redis 向量库]
RAG --> PDF[PDF 知识库
院校资料 / 专业分析]
AI --> T1[ReservationTool]
AI --> T2[UniversityAdmissionTool]
T1 --> DB1[(MySQL
reservation)]
T2 --> DB2[(MySQL
university_score_cache)]
T2 --> API[掌上高考公开 API]
AI -->|Flux 流式输出| FE
C --> DB3[(MySQL
chat_history)]
2.2 主对话流程
- 用户在前端输入问题,前端以 memoryId(会话 ID)+ message 发起 GET /api/chat 请求;
- ChatController 先将用户消息落库到 chat_history;
- 请求进入 AiRagDemoService 前,先经过输入护轨校验(长度、空值、敏感词、Prompt 注入模式);
- LangChain4j 的 AiServices 将系统提示词、历史记忆、用户问题一起交给大模型;
- 大模型根据问题类型自主决策:
- 知识类问题 → 触发 RAG 检索,从 Redis 向量库召回相关 PDF 片段;
- 实时数据类问题 → 触发 Tool Calling,调用录取分数查询工具;
- 预约类问题 → 触发 Tool Calling,写入或查询预约表;
- 大模型基于检索结果/工具返回结果组织自然语言回答;
- 回答以 Flux
流式返回,前端边接收边渲染(打字机效果); - 流结束后,完整 AI 回答落库到 chat_history。
2.3 工具调用流程(录取分数查询)
用户:帮我查一下广东工业大学今年的录取分数
│
▼
大模型识别意图 → 选择 getMajorScores 工具
│
▼
UniversityAdmissionTool.getMajorScores(schoolName, year, provinceName, typeName, majorKeyword)
│
▼
UniversityAdmissionService
① 省份名 → 省份 ID(双向包含模糊匹配)
② 学校名 → 学校 ID(搜索接口 + 精确匹配优先排序)
③ 查 MySQL 缓存(school_id + province_id + year)
├─ 命中 → JSON 反序列化 → 返回
└─ 未命中 → 分页拉取掌上高考 API(每页 20 条,最多 50 页)
→ 序列化为 JSON 写入缓存 → 返回
④ 按科类 / 专业关键词二次过滤
│
▼
大模型将结构化分数整理成表格回答用户
2.4 预约业务流程
用户表达预约意愿
│
▼
大模型按系统提示词要求,逐项引导用户提供:
姓名、性别、手机号、沟通时间、所在省份、预估分数
│
▼
信息齐全后调用 addReservation 工具
│
▼
ReservationService → MySQL reservation 表
│
▼
返回预约成功话术
关键约束:系统提示词明确要求大模型不得编造用户信息,必须等用户完整提供六项信息后才能下单。
2.5 记忆与持久化流程
每次创建 AI Service 实例
│
▼
ChatHistoryService.restoreMemory(sessionId)
│
├─ Redis 中已有记忆 → 直接复用
└─ Redis 为空(重启/过期)→ 从 chat_history 查询历史 → 回填 Redis
│
▼
MessageWindowChatMemory(id=sessionId, maxMessages=20, store=RedisChatMemoryStore)
三、实现说明
3.1 技术选型
| 层次 | 技术 | 版本 | 选型理由 |
|---|---|---|---|
| 开发语言 | Java | 21 | 团队技术栈统一,生态成熟 |
| 应用框架 | Spring Boot | 3.5.9 | 快速构建 Web 服务、依赖注入 |
| Agent 框架 | LangChain4j | 1.1.0 | Java 生态最成熟的 LLM 应用框架,原生支持 AiServices、Tool、RAG、Memory、Guardrail |
| 大模型 | DeepSeek Chat | - | 中文能力强,OpenAI 协议兼容,成本低 |
| Embedding | 阿里云百炼 text-embedding-v3 | - | 中文语义向量效果好,OpenAI 协议兼容 |
| 向量库 | Redis(RedisEmbeddingStore) | - | 复用已有 Redis,部署成本低 |
| 对话记忆 | RedisChatMemoryStore | - | 支持 TTL,天然适配多会话 |
| 关系数据库 | MySQL + MyBatis | 8.x / 3.0.3 | 存储聊天记录、预约单、分数缓存 |
| 前端 | HTML | 3.2 / 2.2 | 单文件即可运行,开发成本低 |
| 接口文档 | Knife4j | 4.4.0 | 便于调试与展示 |
3.2 工程结构
src/main/java/com/dl/langchain4jragdemo
├── ai
│ ├── AiRagDemoService.java # AI Service 接口(声明系统提示词 + 流式方法)
│ ├── AiRagDemoServiceFactory.java # AI Service 工厂(装配模型/记忆/RAG/工具/护轨)
│ ├── guardrail
│ │ ├── PromptSafetyInputGuardrail.java # 输入护轨
│ │ └── RetryOutputGuardrail.java # 输出护轨(流式场景未启用)
│ └── tool
│ ├── ReservationTool.java # 预约工具
│ └── UniversityAdmissionTool.java # 录取分数查询工具
├── config
│ ├── RagConfig.java # PDF 加载、切分、向量化、检索器
│ ├── RedisChatMemoryStoreConfig.java # Redis 对话记忆
│ ├── StreamingChatModelConfig.java # 多例流式模型(解决并发)
│ ├── RestClientConfig.java # HTTP 客户端
│ └── CorsConfig.java # 跨域
├── controller
│ ├── ChatController.java # 流式聊天入口
│ └── ChatHistoryController.java # 聊天记录查询/保存
├── service
│ ├── ChatHistoryService.java # 历史落库 + 记忆恢复
│ ├── ReservationService.java # 预约业务
│ └── UniversityAdmissionService.java # 录取数据查询 + 二级缓存
├── mapper # MyBatis Mapper
├── pojo # 实体类
└── typehandler # 枚举类型处理器
src/main/resources
├── content/ # 8 份院校/专业 PDF 知识库
├── prompt/system.txt # 系统提示词(人设 + 业务规则)
├── SQL/ # 建表脚本
└── static/index.html # 前端单页
3.3 核心实现
3.3.1 AI Service 与流式输出
AI 能力通过 LangChain4j 的声明式 AiServices 实现,接口只需要描述"要什么",不用关心底层调用:
public interface AiRagDemoService {
@SystemMessage(fromResource = "prompt/system.txt")
Flux
}
在工厂中完成装配:
return AiServices.builder(AiRagDemoService.class)
.chatModel(chatModel)
.streamingChatModel(openAiStreamingChatModel)
.chatMemory(chatMemory)
.inputGuardrails(new PromptSafetyInputGuardrail())
.contentRetriever(contentRetriever)
.tools(reservationTool, universityAdmissionTool)
.build();
这里有一个值得说明的工程细节:流式模型使用 @Scope("prototype") 多例模式。因为流式 StreamingChatModel 在并发场景下存在共享状态风险,每次创建 AI Service 时都从 Spring 容器取一个新实例,可以避免多个会话同时输出时互相干扰。
3.3.2 RAG 知识库
RAG 的构建分为四步,全部在 RagConfig 中完成:
- 加载:用 ClassPathDocumentLoader + ApachePdfBoxDocumentParser 读取 resources/content 下的 8 份 PDF;
- 切分:DocumentSplitters.recursive(500, 100),即每段 500 token、重叠 100 token,避免语义被截断;
- 增强元数据:为每个文本段拼接文件名前缀,提升检索命中率;
- 向量化入库:使用阿里云百炼 text-embedding-v3 生成向量,写入 Redis 向量库。
检索器配置:
EmbeddingStoreContentRetriever.builder()
.embeddingStore(redisEmbeddingStore)
.minScore(0.5) // 低于阈值的片段直接丢弃,减少无关内容干扰
.maxResults(5) // 最多召回 5 个片段,控制上下文长度
.embeddingModel(embeddingModel)
.build();
3.3.3 工具调用(Tool Calling)
工具通过 @Tool 和 @P 注解声明,模型会根据工具描述自主判断是否调用、如何填参:
@Tool("查询某学校某年某省份的专业录取分数线和位次")
public List<MajorScore> getMajorScores(
@P("学校名称") String schoolName,
@P("年份,例如2024") Integer year,
@P("省份名称,例如广东") String provinceName,
@P(value = "科类,物理类或历史类,可选", required = false) String typeName,
@P(value = "专业名称关键词,可选", required = false) String majorKeyword) {
return universityAdmissionService.getMajorScores(
schoolName, year, provinceName, typeName, majorKeyword);
}
预约工具同理,把"下单"和"查询"两个动作暴露给模型:
@Tool("预约志愿填报服务")
public void addReservation(@P("考生姓名") String name,
@P("考生性别") String gender,
@P("考生手机号") String phone,
@P("预约沟通时间,格式为: yyyy-MM-dd'T'HH:mm") String communicationTime,
@P("考生所在省份") String province,
@P("考生预估分数") Integer estimatedScore) { ... }
@Tool("根据考生手机号查询预约单")
public Reservation findReservation(@P("考生手机号") String phone) { ... }
这正是智能体"会做"的核心:模型负责决策,工具负责执行。模型不直接编造分数,而是先调工具拿真实数据,再组织语言回答。
3.3.4 多轮对话记忆
对话记忆采用 MessageWindowChatMemory + RedisChatMemoryStore:
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.id(memoryId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
- id 使用前端生成的会话 ID,实现不同会话隔离;
- 只保留最近 20 条消息,控制 token 成本;
- Redis 设置 TTL,过期后由数据库兜底恢复。
ChatHistoryService.restoreMemory() 实现了"双保险":
public void restoreMemory(Long sessionId) {
List<ChatMessage> existing = redisChatMemoryStore.getMessages(sessionId);
if (existing != null && !existing.isEmpty()) {
return; // Redis 有记忆,直接复用
}
List<ChatHistory> histories = chatHistoryMapper.listBySessionId(sessionId);
if (histories == null || histories.isEmpty()) {
return;
}
redisChatMemoryStore.updateMessages(sessionId,
histories.stream().map(this::toChatMessage).collect(Collectors.toList()));
}
3.3.5 外部 API 交互与数据库缓存
录取分数查询服务对接掌上高考公开接口,核心设计有三点:(1)Cache-Aside 旁路缓存缓存粒度为 (school_id, province_id, year),value 是完整专业列表的 JSON。这样一次缓存可以服务"物理类/历史类""计算机专业"等多种后续过滤查询,显著提高命中率。
读请求 → 查 MySQL 缓存
├─ 命中 → JSON 反序列化 → 返回
└─ 未命中 → 拉取上游 API → 序列化写入缓存 → 返回
(2)分页拉取的双重熔断
- 业务终止:本页返回条数小于 20,说明是最后一页;
- 安全熔断:最多翻 50 页(1000 条),防止上游接口异常时死循环耗尽连接池。
(3)降级容错
- 缓存反序列化失败 → 降级为直接拉 API;
- 缓存写入失败 → 只记录日志,不影响本次返回;
- 单页请求失败 → 返回已拉取的部分数据;
- 省份配置加载失败 → 返回空 Map,而不是抛异常。
此外,省份名解析采用双向包含模糊匹配("广东" ↔ "广东省"),并用 volatile + 双重检查锁(DCL) 实现线程安全的单例缓存。
3.3.6 输入护轨
PromptSafetyInputGuardrail 在请求进入模型前拦截三类风险:
| 检查项 | 规则 |
|---|---|
| 长度 | 超过 3000 字直接拒绝 |
| 空值 | 空白输入直接拒绝 |
| 敏感词 | 破解、绕过、越狱、jailbreak 等 |
| Prompt 注入 | 正则匹配 "ignore previous instructions"、"system: you are"、"pretend as" 等模式 |
同时实现了 RetryOutputGuardrail 用于输出内容检查,但因为流式输出无法在生成后统一重试,生产中未启用,这是一种明确的取舍。
3.3.7 前端实现
前端是一个 Vue 3 单页应用,核心能力:
- 流式渲染:使用 fetch + ReadableStream 读取后端流,逐块更新消息;
- 打字机效果:以 20ms 间隔逐字显示,体验更自然;
- 中止响应:使用 AbortController 支持用户随时停止生成;
- 会话管理:新建会话时生成新的时间戳作为 memoryId;
- 聊天记录:调用 /chatHistory/list 从数据库加载历史;
- 暗黑模式:跟随系统偏好并持久化到 localStorage。
3.4 设计亮点与难点
- "模型决策 + 工具执行"的 Agent 架构:模型不直接回答实时分数,而是调用工具拿真实数据,从机制上减少幻觉;
- RAG 与工具互补:静态知识(院校规则、专业分析)走 RAG,动态数据(录取分数)走 API,覆盖两类不同问题;
- 双层记忆:Redis 提供低延迟读写,MySQL 提供持久化兜底,服务重启不丢上下文;
- 多例流式模型解决并发:避免多个会话同时流式输出时状态互相污染;
- 外部数据缓存与熔断:既降低第三方接口压力,又保证接口异常时业务可用。
四、使用示例(功能演示)
4.1 查询指定院校分专业录取分数
用户输入"帮我查看一下西北大学的24年录取分数线",智能体调用录取分数工具,返回 2024 年分专业最低分、位次与平均分:

4.2 查询实时录取数据(外部 API + 工具调用)
用户输入"帮我查看一下广东工业大学今年的录取分数",智能体搜索学校、查询广东省物理类录取数据,并按"高分/热门专业""主流工科专业"分类整理:

4.3 多轮对话与聊天记录持久化
点击右上角"历史记录"按钮,可从 MySQL 中加载当前会话的完整聊天记录;即使刷新页面、重启服务,历史依然存在:

4.4 暗黑模式
前端支持一键切换暗黑模式,适配不同使用环境:

4.5 预约流程(文字示例)
用户:我想预约一个一对一指导
智能体:好的,为了帮您安排指导老师,请问考生的姓名是?
用户:张三
智能体:请问考生性别是?
用户:男
……
(依次收集性别、手机号、沟通时间、省份、预估分数)
智能体:恭喜您,一对一志愿指导服务已经预约成功,我们会准时联系您,请注意接听电话!
预约成功后,可在 reservation 表中查到对应记录,证明智能体完成了真实的数据库写入操作。
4.6 本地运行方式
# 1. 初始化数据库(MySQL 8.x)
mysql -u root -p < src/main/resources/SQL/chat_history.sql
mysql -u root -p < src/main/resources/SQL/reservation.sql
mysql -u root -p < src/main/resources/SQL/university_score_cache.sql
# 2. 配置 MySQL、Redis、DeepSeek / 阿里云百炼 API Key
# 默认启用 prod 配置;本地运行可切换为 local 并补齐数据源配置
# 3. 启动项目
./mvnw spring-boot:run
# 4. 浏览器访问
[http://localhost:8088/api/index.html](http://localhost:8088/api/index.html)
------
五、GitHub 链接
https://github.com/delang666/delang666/tree/main/langchain4j-rag-demo
六、小组分工
| 成员 | 学号 | 主要分工 | 具体工作内容 |
|---|---|---|---|
| 黄德朗 | 3124004133 | 后端 / AI 编排 | Spring Boot 工程搭建、LangChain4j AiServices 装配、流式输出、对话记忆、AI Service 工厂 |
| 朱海碲 | 3124004150 | RAG / 知识库 | PDF 资料收集与整理、文档切分与向量化、Redis 向量库接入、检索阈值调优 |
| 邓兴荣 | 3124004128 | 前端 / 测试文档 | Vue 3 聊天界面、流式渲染与打字机效果、暗黑模式、聊天记录、功能测试与博客撰写 |
| 何鹏 | 3124004131 | 工具 / 外部数据 | 掌上高考接口对接、录取分数工具、预约工具、MySQL 缓存与降级策略 |
七、小组成员心得
黄德朗
这次最大的收获是理解了"智能体"和"聊天机器人"的本质区别。以前做对话功能,只是把用户问题丢给大模型然后显示回答;这次通过 AiServices 把记忆、RAG、工具、护轨装配到一起,才真正体会到 Agent 的核心是"模型负责决策,系统负责提供能力"。另一个印象深刻的点是流式模型的并发问题——一开始所有会话共用一个 StreamingChatModel,多开几个窗口就会出现输出错乱,后来改成 prototype 多例才解决。这种问题只有真正跑起来才会暴露,文档里不会写。
朱海碲
我负责 RAG 部分,原以为"把 PDF 丢进去就能用",实际做下来发现每一步都有讲究:PDF 解析可能丢格式,切分粒度太大召回不准、太小语义断裂,检索阈值设低了会引入无关内容、设高了又容易召不回。最后采用 500 token 切分 + 100 重叠,并在每个片段前拼接文件名,命中率明显提升。也第一次理解了 Embedding 和向量检索的工作原理,不再是"调个 API 就完事"。
邓兴荣
前端部分最大的挑战是流式渲染。普通的 fetch 会等整个响应结束才返回,体验很差;改成 ReadableStream 逐块读取后,还要处理打字机效果、自动滚动、用户中止等问题。做完之后明显感觉到,AI 应用的体验很大程度取决于这些"细节工程"。另外通过写这篇文档,我把整个项目的业务流重新梳理了一遍,比只看代码理解得更透彻。
何鹏
工具调用部分让我印象最深的是"模型会不会正确填参数"。一开始工具描述写得很模糊,模型经常把"广东"和"广东省"、年份参数搞错,或者在没有省份信息时硬编一个。后来我们把参数说明写得更具体,并在系统提示词里明确"查不到就如实告知,不要编造",效果才稳定。另外,对接第三方接口时做了缓存和熔断,让我意识到 AI 应用同样需要传统的工程可靠性设计——大模型再聪明,底层数据接口挂了也白搭。
八、学习资源推荐
| 资源 | 链接 | 推荐理由 |
|---|---|---|
| MCP 官方快速入门 | https://modelcontextprotocol.io/quickstart | 理解 MCP 协议与工具定义的标准方式 |
| 动手学多智能体系统实战教程(Hello-Agents) | https://github.com/datawhalechina/hello-agents | 从零理解 Agent、工具调用、多智能体协作 |
| LangChain4j 官方文档 | https://docs.langchain4j.dev/ | 本项目核心框架,AiServices、RAG、Tool、Memory 示例齐全 |
| 阿里云百炼 | https://bailian.console.aliyun.com/ | Embedding 与通义系列模型,国内访问稳定 |
| 文心智能体平台 | https://agents.baidu.com/ | 低代码搭建智能体,适合快速体验 Agent 产品形态 |
| DeepSeek 开放平台 | https://platform.deepseek.com/ | OpenAI 协议兼容,中文对话效果好、成本低 |
| MCP 协议深度解析与实战应用 | 课程提供资料 | 深入理解 MCP 的通信机制与工程落地 |
九、总结与展望
本项目实现了一个面向高考志愿填报场景的"能说会做"智能体:既能通过自然语言进行多轮咨询,又能调用外部 API 获取真实录取数据、写入数据库完成预约,并具备 RAG 知识库、双层记忆、输入防护和可视化前端。
后续可以从三个方向继续演进:
- 接入 MCP 协议:把现有 @Tool 工具封装为标准 MCP Server,让智能体具备跨框架、跨语言的工具扩展能力;
- 增加语音输入/输出:前端接入 Web Speech API 或第三方 ASR/TTS,进一步降低使用门槛;
- 引入多智能体协作:拆分为"院校检索 Agent""分数分析 Agent""志愿
浙公网安备 33010602011771号