Spring AI 学习实践笔记(三): 实现企业级自定义智能客服
Spring AI 学习实践笔记三 实现企业级自定义智能客服本文记录在既有 Spring AI 项目中新增「企业级自定义智能客服」的完整过程后端接入 MySQL MyBatis-Plus 存储课程/校区/预约数据通过Tool把数据库操作暴露给大模型Function Calling前端做客服对话页并复盘真实遇到的建库、代码生成、Bean 冲突、接口重复、模型选型等经验与坑。项目原型参考【Spring AI实战】FunctionCalling实现企业级自定义智能客服。本系列前两篇docs/SpringAI学习实践笔记.md仿 DeepSeek 对话、docs/SpringAI哄哄模拟器学习实践笔记.md提示词工程。一、背景与目标1.1 要做什么做一个 7×24 在线、能给用户咨询培训课程并预约线下试听的智能客服。关键在于模型不仅要会聊天还要能读写真实业务数据——查课程、查校区、生成预约单。这就是Function Calling工具调用要解决的问题。1.2 核心思路用户提问 → 大模型判断需要调用哪个工具函数 → Spring AI 根据 Tool 描述生成工具定义发给模型 → 模型返回要调用的函数名 参数JSON → Spring AI 执行该 Java 方法真实查库 / 写库 → 把执行结果回传给模型 → 模型基于真实数据生成最终回答1.3 技术目标目标实现要点业务数据持久化MySQL MyBatis-Plusmysql-connector-jmybatis-plus-spring-boot3-starter工具暴露Tool/ToolParam注解 ChatClient.defaultTools(tools)多轮记忆复用MessageChatMemoryAdvisorInMemoryChatMemory流式体验stream().content()→FluxString会话历史复用ChatHistoryRepositorytypeservice前端Vue 3 客服对话页 推荐问句二、实现效果演示2.1 数据准备先执行src/main/resources/db/schema.sql建库ai-dome及course/course_reservation/school三张表含初始数据。2.2 接口演示GET /ai/service?prompt{问题}chatId{会话ID} 返回text/html;charsetutf-8 流式文本示例 1咨询课程模型自动调用queryCoursecurl-Nhttp://localhost:8080/ai/service?prompt有哪些编程方向的课程chatIddemo-001模型返回表格形式当然可以下面是编程方向的课程快看看有没有心动的 | 课程名称 | 课程类型 | 学习时长 | | --- | --- | --- | | JavaEE | 编程 | 108 天 | | 鸿蒙应用开发 | 编程 | 98 天 | | AI人工智能 | 编程 | 100 天 | | Python大数据开发 | 编程 | 102 天 |示例 2查询校区调用querySchoolcurl-Nhttp://localhost:8080/ai/service?prompt都有哪些校区chatIddemo-001示例 3预约试听调用createCourseReservation写库curl-Nhttp://localhost:8080/ai/service?prompt帮我预约广东校区的新媒体运营试听我叫张三电话13800000000chatIddemo-001OK已为你预约成功预约信息如下 - 预约课程新媒体运营 - 预约校区广东校区 - 学生姓名张三 - 联系电话13800000000 - 预约单号2若开启SimpleLoggerAdvisor与org.springframework.ai: debug可在后端日志中看到工具的调用参数与返回验证“模型确实调用了工具”而非编造。2.3 前端演示浏览器打开http://localhost:5173/顶部导航切到「智能客服」左侧是咨询记录主区有欢迎语和 4 个推荐问句含一个预约示例点击即可对话回答流式展示、支持 Markdown 表格渲染。三、学习路径与详细步骤阶段 1加入数据库依赖与数据源pom.xmldependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdscoperuntime/scope/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactIdversion3.5.10.1/version/dependencyapplication.yaml用环境变量默认值便于本地开发spring:datasource:driver-class-name:com.mysql.cj.jdbc.Driverurl:${DB_URL:jdbc:mysql://localhost:3306/ai-dome?serverTimezoneAsia/ShanghaiuseSSLfalseuseUnicodetruecharacterEncodingutf-8allowPublicKeyRetrievaltrueallowMultiQueriestrue}username:${DB_USERNAME:root}password:${DB_PASSWORD:1234}注意加了数据源后应用启动即依赖 MySQL库连不上会直接启动失败。阶段 2根据 SQL 生成实体 / Mapper / ServiceMyBatis-Plus CRUD针对三张表生成标准三层代码放在com.study.study_ai_client实体Course、CourseReservation、School用TableNameTableId(type IdType.AUTO)下划线字段靠 MyBatis-Plus 默认驼峰映射。MapperXxxMapper extends BaseMapperXxxMapper。ServiceIXxxService extends IServiceXxxXxxServiceImpl extends ServiceImplXxxMapper, Xxx自带全套增删改查。DataTableName(course)publicclassCourse{TableId(valueid,typeIdType.AUTO)privateIntegerid;privateStringname;privateIntegeredu;// 学历要求privateStringtype;// 课程类型privateLongprice;privateIntegerduration;// 学习时长(天)}必须显式开启 Mapper 扫描自动扫描只覆盖启动类所在包SpringBootApplication(scanBasePackagescom.study)MapperScan(com.study.study_ai_client)publicclassStudyAiApplication{...}阶段 3定义工具入参CourseQuery用ToolParam描述每个参数模型才知道怎么填DatapublicclassCourseQuery{ToolParam(requiredfalse,description课程类型编程、设计、自媒体、其它)privateStringtype;ToolParam(requiredfalse,description学历要求0-无、1-初中、2-高中、3-大专、4-本科及本科以上)privateIntegeredu;ToolParam(requiredfalse,description排序方式)privateListSortsorts;DatapublicstaticclassSort{ToolParam(requiredfalse,description排序字段: price或duration)privateStringfield;ToolParam(requiredfalse,description是否是升序: true/false)privateBooleanasc;}}阶段 4编写工具类CourseTools每个业务能力写成一个Tool方法方法体就是普通的 Service 调用RequiredArgsConstructorComponentpublicclassCourseTools{privatefinalICourseServicecourseService;privatefinalISchoolServiceschoolService;privatefinalICourseReservationServicereservationService;Tool(description根据条件查询课程)publicListCoursequeryCourse(ToolParam(description查询的条件,requiredfalse)CourseQueryquery){if(querynull){returncourseService.list();}QueryChainWrapperCoursewrappercourseService.query().eq(query.getType()!null,type,query.getType()).le(query.getEdu()!null,edu,query.getEdu());if(query.getSorts()!null!query.getSorts().isEmpty()){for(CourseQuery.Sortsort:query.getSorts()){wrapper.orderBy(true,Boolean.TRUE.equals(sort.getAsc()),sort.getField());}}returnwrapper.list();}Tool(description查询所有校区)publicListSchoolquerySchool(){returnschoolService.list();}Tool(description生成预约单返回预约单号)publicIntegercreateCourseReservation(ToolParam(description预约课程)Stringcourse,ToolParam(description预约校区)Stringschool,ToolParam(description学生姓名)StringstudentName,ToolParam(description联系电话)StringcontactInfo,ToolParam(description备注,requiredfalse)Stringremark){CourseReservationrnewCourseReservation();r.setCourse(course);r.setSchool(school);r.setStudentName(studentName);r.setContactInfo(contactInfo);r.setRemark(remark);reservationService.save(r);returnr.getId();}}阶段 5把工具注册到 ChatClientConfigurationpublicclassCommonConfiguration{BeanpublicChatClientserviceChatClient(OpenAiChatModelmodel,Qualifier(chatMemory)ChatMemorychatMemory,CourseToolscourseTools){returnChatClient.builder(model).defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT).defaultAdvisors(newSimpleLoggerAdvisor(),newMessageChatMemoryAdvisor(chatMemory)).defaultTools(courseTools)// 关键注册工具启用 Function Calling.build();}}阶段 6编写客服 ControllerRequiredArgsConstructorRestControllerRequestMapping(/ai)publicclassCustomerServiceController{privatefinalChatClientserviceChatClient;privatefinalChatHistoryRepositorychatHistoryRepository;RequestMapping(value/service,producestext/html;charsetutf-8)publicFluxStringservice(RequestParam(prompt)Stringprompt,RequestParam(chatId)StringchatId){chatHistoryRepository.save(service,chatId);returnserviceChatClient.prompt().user(prompt).advisors(a-a.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)).stream().content();}}阶段 7系统提示词角色 流程 防注入SystemConstants.SERVICE_SYSTEM_PROMPT明确角色身份、课程咨询规则、预约流程必须收集姓名/电话/校区、安全防护拒绝 prompt 注入、展示要求用表格且不含 id/价格。阶段 8前端客服页services/api.js增加sendServiceMessage(prompt, chatId)→/ai/service。views/ServiceChat.vue侧边「咨询记录」复用/ai/history/service 欢迎语 推荐问句 流式对话。App.vue导航新增「智能客服」。四、学习经验与踩坑记录重点1. 加数据源后应用“变脆”引入 MyBatis-Plus 后Spring Boot 会自动配置 DataSource没有 MySQL 就启动失败。学习期建议先起库再启动或用环境变量切换不同环境。2. MyBatis-Plus 的 Mapper 自动扫描范围Mapper注解的接口只有在启动类所在的自动配置包下才会被自动扫描。本项目 Mapper 在com.study.study_ai_client不属于启动类的包必须加MapperScan(com.study.study_ai_client)否则报“找不到 Mapper Bean”。3. 工具方法相关的常见问题ToolParam的description要写清楚模型靠它决定参数requiredfalse要标对否则模型可能硬造参数。CourseQuery.Sort.getAsc()是BooleanorderBy需要boolean直接传可能 NPE用Boolean.TRUE.equals(...)兜底。查询条件的.eq(条件?, ...)用法MyBatis-Plus 的条件构造能让“参数为空则不参与 where”。4. 接口/Bean 冲突本项目真实踩到我一度新增了第二个RequestMapping(/ai/service)的 Controller和已有CustomerServiceController映射重复会导致启动失败Ambiguous mapping。同一个路径只能有一个处理方法。之前也遇到过Configuration简单类名相同、Bean方法名相同导致的 Bean 冲突——新增功能前先检查命名是否撞车。5. 验证要跑起来看日志工具到底有没有被调用靠日志。开启logging:level:org.springframework.ai:debug配合SimpleLoggerAdvisor能看到请求轮次、工具调用参数与结果确认不是模型“编造”的数据。五、核心知识点小结知识点一句话结论Function Calling用Tool/ToolParam把 Java 方法暴露给模型模型产出调用参数框架执行并回传defaultToolsChatClient.builder(model).defaultTools(tools)注册工具ToolParam参数描述越清晰模型填参越准required要标对MyBatis-PlusBaseMapperIService/ServiceImpl提供通用 CRUD条件构造用QueryChainWrapperMapperScan自动扫描只覆盖启动类包跨包必须显式声明提示词工程角色 流程约束 防注入 展示要求决定客服的“人设”与稳定性数据源影响引入后应用启动依赖数据库需先建库建表六、项目结构本次新增部分src/main/java/com/study/study_ai_client/ ├─ Course.java / CourseReservation.java / School.java # 实体 ├─ CourseMapper.java / CourseReservationMapper.java / SchoolMapper.java ├─ ICourseService.java / ICourseReservationService.java / ISchoolService.java ├─ CourseServiceImpl.java / CourseReservationServiceImpl.java / SchoolServiceImpl.java ├─ CourseQuery.java # 工具入参 ├─ CourseTools.java # Tool 工具类 ├─ SystemConstants.java # 客服系统提示词 ├─ CommonConfiguration.java # serviceChatClient含 defaultTools └─ CustomerServiceController.java # GET /ai/service src/main/resources/db/schema.sql # 建库建表脚本 frontend/src/ ├─ services/api.js # sendServiceMessage ├─ views/ServiceChat.vue # 客服对话页 └─ App.vue # 导航新增「智能客服」七、总结与可扩展方向7.1 总结Function Calling 是“AI 接入业务系统”的关键模型负责理解与决策Java 负责真实数据读写两者用工具协议连接。先有数据再有智能生成实体/Mapper/Service → 写工具 → 注册到 ChatClient是一条清晰的落地路径。提示词决定上限角色、流程先问学历再查课、先收集信息再下单、防注入全靠 System Prompt。工程细节同样重要MapperScan、接口唯一映射、Bean 命名这些“非 AI”问题反而最容易卡住启动。7.2 可扩展方向工具调用可观测性Micrometer 统计工具被调次数、耗时、token 消耗。结构化输出预约确认可用.entity(ReservationResult.class)直接映射。RAG 增强接入课程大纲/常见问题向量库回答更专业。权限与安全敏感写操作下单加校验、限流、审计SafeGuardAdvisor过滤不当内容。多租户/多客服人设不同ChatClientBean 用Qualifier区分。八、参考文档链接本系列文章【Spring AI实战】FunctionCalling实现企业级自定义智能客服本篇原型【SpringAI 实战】提示词工程实现哄哄模拟器【Spring AI实战】实现仿DeepSeek页面对话机器人Spring AI 完整学习路径与项目实践指南姿势纠正Spring AI 官方项目主页https://spring.io/projects/spring-ai参考文档https://docs.spring.io/spring-ai/reference官方示例https://github.com/spring-projects/spring-ai-examples核心 APIChatClienthttps://docs.spring.io/spring-ai/reference/api/chatclient.htmlTools函数调用https://docs.spring.io/spring-ai/reference/api/tools.htmlAdvisorshttps://docs.spring.io/spring-ai/reference/api/advisors.htmlChat Memoryhttps://docs.spring.io/spring-ai/reference/api/chat-memory.html结构化输出https://docs.spring.io/spring-ai/reference/api/structured-output-converter.htmlRAGhttps://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html数据库 / 前端MyBatis-Plus 官网https://baomidou.comMySQL 下载https://dev.mysql.com/downloads/Vue 3https://cn.vuejs.orgVitehttps://cn.vitejs.devDeepSeek 开放平台https://platform.deepseek.com

相关新闻

锂电串联主动均衡原理与MP2642GR-Z+PIC18F46K20工程实现

锂电串联主动均衡原理与MP2642GR-Z+PIC18F46K20工程实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:09:01 阅读更多 →
AI辅助学术答辩PPT:从论文到演示讲稿的完整指南

AI辅助学术答辩PPT:从论文到演示讲稿的完整指南

答辩这个坎,几乎每个研究生都躲不开。论文写完之后,你以为熬到头了,结果还有一份答辩PPT横在面前。前几年我帮实验室几个师弟师妹改答辩稿,最常见的情况是:论文改了三轮,PPT却拖到答辩前两天才开始动手&…

2026/10/16 3:09:01 阅读更多 →
Kafka 分区数到底设几个:吞吐量、顺序性与 rebalance 代价的取舍

Kafka 分区数到底设几个:吞吐量、顺序性与 rebalance 代价的取舍

我不会起名字322 后端 / 算法 / 数据库 📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL 文章目录Kafka 分区数到底设几个:吞吐量、顺序性与…

2026/10/16 3:09:01 阅读更多 →

最新新闻

CTF音频隐写解题指南:从‘啊哒~’看振幅、频谱与相位三重分析

CTF音频隐写解题指南:从‘啊哒~’看振幅、频谱与相位三重分析

1. 从一道CTF杂项题名看起:为什么“啊哒~”不是卖萌,而是信号“BugkuCTF-Misc:啊哒~”——第一次在某高校CTF训练平台看到这个标题时,我下意识点开想看看是不是新出的搞笑彩蛋题。结果发现它被归在Misc(杂项&#xff0…

2026/10/16 3:52:10 阅读更多 →
CANN一站式算子开发工具:从工程生成到性能优化的完整实践

CANN一站式算子开发工具:从工程生成到性能优化的完整实践

做AI算子开发的朋友,应该都有过这种体验:需求一句话就说完了——“写个自定义算子”,真正动手却要在工程搭建、参数定义、调试、性能调优上来回折腾。如果恰好是在一款新的AI芯片上跑,工具链又不成熟,那痛苦程度直接翻…

2026/10/16 3:52:10 阅读更多 →
Win10下VMware安装CentOS 7.9并配置静态IP与外网访问

Win10下VMware安装CentOS 7.9并配置静态IP与外网访问

1. 为什么要在Win10上折腾CentOS 7.9虚拟机很多刚接触Linux运维或者后端开发的朋友,第一道拦路虎不是命令本身,而是“我该把系统装在哪”。直接把自己的主力Windows 10笔记本格掉装Linux,显然不现实——办公、聊天、看文档都还得靠它。退而求…

2026/10/16 3:52:10 阅读更多 →
深度学习为何持续流行:从特征学习到端到端训练的范式变革

深度学习为何持续流行:从特征学习到端到端训练的范式变革

1. 从一张老照片说起:深度学习到底改变了什么2012年之前,如果你在一场计算机视觉的学术会议上说“我们用一个端到端的神经网络把图像分类错误率砍掉一半”,大概率会被审稿人反问:“你的特征工程做了什么?为什么不用SIF…

2026/10/16 3:52:10 阅读更多 →
WinUI 3 快速上手指南:6 个里程碑做出 Fluent 风格的 Windows 应用

WinUI 3 快速上手指南:6 个里程碑做出 Fluent 风格的 Windows 应用

WinUI 3 快速上手指南:6 个里程碑做出 Fluent 风格的 Windows 应用 【免费下载链接】microsoft-ui-xaml WinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications. 项目地址: https://g…

2026/10/16 3:52:10 阅读更多 →
PL/0编译器C语言实现:从词法分析到解释执行的完整教学级编译流程

PL/0编译器C语言实现:从词法分析到解释执行的完整教学级编译流程

简介:本资源是N.Wirth教授经典PL/0语言编译器的C语言实现源码,面向编译原理初学者、高校计算机专业学生及教学实践者,用于深入理解词法分析、语法分析、语义处理与目标代码生成等编译全流程核心机制。压缩包为11KB的ZIP格式,共含2…

2026/10/16 3:51:09 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →