软件工程第四次作业

构建一个能说会做的智能体

这个作业属于哪个课程 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 主对话流程

  1. 用户在前端输入问题,前端以 memoryId(会话 ID)+ message 发起 GET /api/chat 请求;
  2. ChatController 先将用户消息落库到 chat_history;
  3. 请求进入 AiRagDemoService 前,先经过输入护轨校验(长度、空值、敏感词、Prompt 注入模式);
  4. LangChain4j 的 AiServices 将系统提示词、历史记忆、用户问题一起交给大模型;
  5. 大模型根据问题类型自主决策:
    • 知识类问题 → 触发 RAG 检索,从 Redis 向量库召回相关 PDF 片段;
    • 实时数据类问题 → 触发 Tool Calling,调用录取分数查询工具;
    • 预约类问题 → 触发 Tool Calling,写入或查询预约表;
  6. 大模型基于检索结果/工具返回结果组织自然语言回答;
  7. 回答以 Flux 流式返回,前端边接收边渲染(打字机效果);
  8. 流结束后,完整 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 chat(@UserMessage String userMessage);
}

在工厂中完成装配:

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 中完成:

  1. 加载:用 ClassPathDocumentLoader + ApachePdfBoxDocumentParser 读取 resources/content 下的 8 份 PDF;
  2. 切分:DocumentSplitters.recursive(500, 100),即每段 500 token、重叠 100 token,避免语义被截断;
  3. 增强元数据:为每个文本段拼接文件名前缀,提升检索命中率;
  4. 向量化入库:使用阿里云百炼 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 设计亮点与难点

  1. "模型决策 + 工具执行"的 Agent 架构:模型不直接回答实时分数,而是调用工具拿真实数据,从机制上减少幻觉;
  2. RAG 与工具互补:静态知识(院校规则、专业分析)走 RAG,动态数据(录取分数)走 API,覆盖两类不同问题;
  3. 双层记忆:Redis 提供低延迟读写,MySQL 提供持久化兜底,服务重启不丢上下文;
  4. 多例流式模型解决并发:避免多个会话同时流式输出时状态互相污染;
  5. 外部数据缓存与熔断:既降低第三方接口压力,又保证接口异常时业务可用。

四、使用示例(功能演示)

4.1 查询指定院校分专业录取分数

用户输入"帮我查看一下西北大学的24年录取分数线",智能体调用录取分数工具,返回 2024 年分专业最低分、位次与平均分:
10c3ab22-4d8c-4174-9078-6f4f50c6200b

4.2 查询实时录取数据(外部 API + 工具调用)

用户输入"帮我查看一下广东工业大学今年的录取分数",智能体搜索学校、查询广东省物理类录取数据,并按"高分/热门专业""主流工科专业"分类整理:
112da65b-89bf-40ab-b817-387e15aef8d9

4.3 多轮对话与聊天记录持久化

点击右上角"历史记录"按钮,可从 MySQL 中加载当前会话的完整聊天记录;即使刷新页面、重启服务,历史依然存在:
86bb5851-1a1d-457f-b5ec-bba9d7ee88ec

4.4 暗黑模式

前端支持一键切换暗黑模式,适配不同使用环境:
e785ae83-7ef9-451c-bbed-d4ec2367c6f7

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 知识库、双层记忆、输入防护和可视化前端。

后续可以从三个方向继续演进:

  1. 接入 MCP 协议:把现有 @Tool 工具封装为标准 MCP Server,让智能体具备跨框架、跨语言的工具扩展能力;
  2. 增加语音输入/输出:前端接入 Web Speech API 或第三方 ASR/TTS,进一步降低使用门槛;
  3. 引入多智能体协作:拆分为"院校检索 Agent""分数分析 Agent""志愿
posted @ 2026-10-17 10:34  dl12  阅读(15)  评论(0)    收藏  举报