ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI实战:基于智谱AI构建餐饮智能点餐助手

Spring AI实战:基于智谱AI构建餐饮智能点餐助手 Spring AI是最近一年多Java圈子里绕不开的话题。先说结论如果你所在团队技术栈是Java/Spring Boot又想着把大模型能力接进业务系统那Spring AI基本就是最顺手的路。它不是用来做大模型训练或者微调的它的定位是让你用写普通Spring Boot接口的方式去调用大模型、做提示词模板、搞结构化输出、甚至让AI自己调用你业务代码里的方法。这篇文章我会把从“引入依赖”到“在餐饮SaaS场景里落地一个点餐助手”的完整过程拆开讲包括我实际踩过的坑和版本选择的经验。这套内容我默认你是想认真落地的不是看完图个热闹。我会尽量少讲虚的架构概念多讲能直接抄的配置和代码。考虑到不少团队因为网络环境和合规要求会选择国内的模型服务后面我就以智谱AI为例来讲接入Spring AI官方的starter对它是全链路支持的换OpenAI或者其他几家国产模型也就是改依赖和配置的功夫。1. Spring AI到底解决什么问题1.1 大模型接入的老路与痛点在Spring AI出现之前Java项目接入大模型无非是两条路一是直接用HTTP客户端调模型厂商的REST API自己拼JSON、自己管流式响应、自己刷token二是接一些社区封装的SDK但这类SDK质量参差不齐绑定某个厂商之后想切换模型服务商就很痛苦。我见过不少项目为了在业务代码里加一个“AI总结”的功能硬生生写出几百行HTTP调用代码里面还夹着大量字符串拼接出来的提示词后面维护的人根本不敢动那一段。更麻烦的是当项目里十几个地方都要调大模型每个地方的鉴权、超时、重试、token统计逻辑都各自写一套那基本就是灾难。还有一个痛点是没有统一抽象。今天用的模型服务是A明天想换成便宜一点的B或者想拿同一个提示词快速对比不同模型的效果你会发现代码里到处是A厂商SDK的类型根本换不动。Spring AI干的事情就是把这层统一掉它定义了一套面向Spring生态的抽象接口让你像写数据访问层一样写AI调用。1.2 Spring AI的定位与核心设计Spring AI从名字就能看出来它是Spring生态针对AI应用做的官方集成层。核心思路和Spring Boot一贯的风格非常一致约定大于配置、自动装配、通过starter快速接入。它提供了几个关键能力统一的ChatModel接口、可装配的ChatClient、提示词模板、结构化输出解析、向量数据库抽象以及对函数调用Function Calling的支持。这一点和Spring Boot把数据库访问统一到JdbcTemplate/Spring Data是一样的道理。你面向接口编程底层具体接哪家模型由依赖和配置决定。今天用智谱AI明天想切到其他厂商只要两边对模型的支持能力一致业务代码基本不用改。所以我的建议是新项目直接用Spring AI别自己封装HTTP调用老项目如果已经有自己的AI调用封装也值得评估逐步迁移到Spring AI上省掉后续的维护成本。下面我直接讲怎么把它跑起来。2. 环境准备与依赖引入2.1 版本选择与JDK要求先泼一盆冷水Spring AI对Spring Boot版本有硬性要求用它之前一定要确认版本组合。目前稳定路线是Java 17 Spring Boot 3.2/3.3/3.4JDK版本低于17直接在启动时报错。Spring AI本身经历了里程碑版本阶段版本的命名里带MMilestone或者RCRelease Candidate比如1.0.0-M6这种是预发布版本API可能后面还会调整。我自己的建议是直接用GA版本比如1.0.0正式版以及后续的小版本。原因很简单网上很多教程还停留在M系列版本里边的API跟正式版差异不小照着写经常编译不过。以我当前的实践来看推荐组合是JDK17或21Spring Boot3.3.x以上Spring AI1.0.0 GA及以上你别图新鲜去追Spring Boot最新大版本因为Spring AI的兼容更新会稍微滞后我见过有人用Spring Boot 4.0预览版搭Spring AI折腾了一整天最后发现是兼容问题。2.2 Maven坐标与智谱AI接入配置引入依赖这块很多新手容易懵的是到底该引哪个artifactId。Spring AI的模块非常多有spring-ai-openai、spring-ai-ollama、spring-ai-zhipuai、spring-ai-alibaba等等。注意这些不仅是模型厂商SDK更是Spring自动装配的starter引进来之后Spring上下文里会自动多出对应的Bean。我平时用智谱AIMaven坐标是这么写的dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-zhipuai/artifactId version1.0.0/version /dependency对artifact名里带starter这才是Spring Boot风格的自动装配入口。如果你看到别人的代码里引的是spring-ai-zhipuai这种不带starter的那属于手动配置模式需要自己new model对象一般不建议除非你的场景非常特殊。版本号怎么确认别瞎猜去Maven中央仓库搜这个坐标看最新的release版本。也可以直接在IDE的Maven工具窗口里搜会自动列出所有版本。Spring AI的版本更新比较勤小版本之间会有API兼容性微调比如1.0.0和1.0.1之间基本没事但M系列和GA之间就可能有断崖式变化。另外如果你的网络环境访问Maven中央仓库慢换成阿里云镜像一样能用坐标不用改。2.3 配置文件怎么写依赖引入后剩下就是配api-key和模型相关参数。智谱AI的配置在application.yml里长这样spring: application: name: spring-ai-demo ai: zhipuai: api-key: ${ZHIPU_API_KEY} base-url: https://open.bigmodel.cn/api/paas/v4 chat: options: model: glm-4-flash temperature: 0.7 max-tokens: 2048这里我多说几句。api-key千万不要硬编码在配置文件里提交到Git仓库用环境变量注入才是稳妥做法。我在团队里见过不止一次因为测试key写到application.yml里然后推到公开仓库结果被人刷爆额度这种坑一次都别踩。base-url需要和你选的模型服务商匹配智谱AI一般是上面这个地址。不同厂商的base-url差异很大比如阿里云百炼是另一个域名接的时候别张冠李戴不然报401找半天都找不到原因。model这个参数也很关键哪怕同一个厂商不同模型的能力、价格、上下文长度差别很大。比如智谱的glm-4-flash是轻量模型便宜适合简单对话glm-4-plus能力更强适合复杂推理。刚开始调试用便宜模型就好等逻辑跑通了再换强模型。Spring Boot会自动创建一个ChatModel的Bean接下来就能直接干活了。3. 从零跑通第一个聊天模型3.1 核心概念ChatClient / ChatModel / Prompt先理清Spring AI里几个高频概念不然看代码容易懵。ChatModel是最底层的抽象代表一个能聊天的模型客户端它的方法比较基础给一个Prompt返回一个ModelResponse。平时我们很少直接用ChatModel因为可用的功能比较少比如拿不到流式便利封装、提示词模板等。Prompt和Message是模型的输入封装。大白话理解Prompt就是一次完整的对话请求体里面可能包含多个Message每条Message有role角色比如system系统指令、user用户消息、assistantAI回复。比如你让AI扮演客服那个“扮演客服”的设定就是system消息用户实际问的话是user消息。ChatClient是把ChatModel再包了一层的高阶客户端这才是日常开发的主力。它的设计风格很像Spring WebFlux里的WebClient用流式API一层层拼装请求。ChatClient的优势在于支持同步、流式、响应式三种调用方式支持Prompt模板还能直接把AI返回的文本转换成Java对象。我建议你所有业务代码都走ChatClient别碰底层ChatModel。3.2 写一个最简单的聊天接口用Spring Boot的方式一个Controller就能把聊天能力暴露出去。示例代码如下RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/chat) public String chat(RequestParam(defaultValue 你好介绍一下你自己) String message) { return chatClient.prompt() .user(message) .call() .content(); } GetMapping(/chat/stream) public SseEmitter chatStream(RequestParam String message) { SseEmitter emitter new SseEmitter(); chatClient.prompt() .user(message) .stream() .content() .doOnComplete(emitter::complete) .subscribe(emitter::send); return emitter; } }这段代码里ChatClient.Builder是Spring AI自动装配好的你只需构造函数注入。有人问为什么不直接注入ChatClient原因在于每次需求可能不同样式的客户端配置比如有的接口温度低一些、有的接口要自定义超时通过Builder按场景创建更灵活。call()是同步阻塞调用content()是拿AI回复的文本内容。这是最基础的链路适合先跑通。如果你在浏览器里访问得确保配置里的api-key是有效的不然会直接抛异常。3.3 同步与流式调用的取舍同步调用和流式调用要分场景。后端内部处理、结果要入库、或者要拿完整结果再往下走逻辑的用call()就好了。但凡是面向用户的交互场景比如网页聊天框、客服窗口、智能点餐面板强烈建议用流式。因为大模型生成一段完整回复往往要好几秒让用户干等白屏体验很差。我在实操中的感受是流式响应用SseEmitter方案是最省事的后端把AI吐出来的内容逐个token推到前端用户看到的就是打字机效果。上面的示例代码是一个最简版本实际项目里还需要考虑前端断开连接时取消订阅、异常时通过emitter的completeWithError回调通知前端这些细节做好体验才完整。另外要注意流式模式下的chatClient.prompt()用法和同步基本一样区别只是call换成了stream返回的是Flux也就是响应式流。4. 进阶玩法模板、结构化输出与函数调用4.1 Prompt模板参数化提示词业务开发里最忌讳把提示词硬编码散落在代码各处尤其是那种拼接用户输入的提示词写起来容易、维护起来想哭。Spring AI提供了类似MyBatis里动态SQL的PromptTemplate机制可以把提示词模板放到resources目录再通过参数填充。在resources/prompts目录下建一个文本文件比如order-assistant.st内容大致是你是一名智能点餐助手。 门店名称{storeName} 营业时间{businessHours} 请根据用户需求从菜单中推荐合适的餐品。 如果用户表达不清晰请先询问确认。然后代码里这样用PromptTemplate promptTemplate new PromptTemplate( new ClassPathResource(prompts/order-assistant.st) ); Message systemMessage promptTemplate.createMessage(Map.of( storeName, 老王汉堡店, businessHours, 10:00-22:00 )); Prompt prompt new Prompt(List.of(systemMessage, userMessage)); String response chatClient.prompt(prompt).call().content();这种做法的好处很明显运营和产品可以直接改提示词文件不用动代码提示词和业务代码分离结构清晰。模板文件用.st后缀是Spring AI里的常见约定你也可以用.txt不影响功能。有人问参数里如果包含用户输入怎么办可以继续用Map传值。但要特别注意提示词注入的问题如果用户输入里有别有用心的指令比如“忽略之前的规则”单靠模板是拦不住的。所以涉及用户输入的提示词最好在system消息里加点防御性提示同时在业务上对敏感能力做权限控制语言模型不是这么容易“锁死”的。4.2 结构化输出别让AI裸奔回字符串大模型默认返回纯文本但如果你的业务需要拿结果去处理比如提取订单信息、解析意图、转换数据对象那就得让输出结构化。最直接的方式是通过提示词要求AI返回JSON然后在代码里解析。但这样很脆AI偶尔会在JSON里夹注释、加Markdown标记解析直接崩。Spring AI的结构化输出功能能把这个过程规范起来它利用模型对JSON Schema的理解把结果直接映射到Java对象。我以一个点餐意图识别为例我要从用户的一句话里提取菜品列表和桌号定义如下recordpublic record OrderIntent( ListOrderItem items, String tableNo ) { public record OrderItem(String name, Integer quantity, String note) {} }然后调用OrderIntent intent chatClient.prompt() .system(你是点餐助手请从用户输入中提取菜品名、数量和备注如果信息缺失对应字段用空值或默认值。) .user(userInput) .call() .entity(OrderIntent.class);这个方法内部会引导模型输出符合目标类型的JSON然后完成反序列化。我在实际使用中遇到的坑是AI偶尔会脑补字段比如用户没说桌号它硬编一个“A01”进去或者数量给错。这没法从技术上完全规避我的经验是提示词里明确约定“缺失字段必须给null/默认值不许猜测”同时下游业务对缺失字段要有兜底。4.3 函数调用让AI去查询数据库函数调用是这几轮AI应用里最有工程价值的能力。它让模型不再局限于静态知识而是能在对话过程中动态调用你定义的Java方法获取实时数据然后再组织回答。比如用户问“现在有哪些麻辣口味的菜”AI如果只靠训练数据是回答不了实时菜单的。有了函数调用它可以先调用你提供的“根据口味查询菜品”的方法拿到真实菜品列表再自然语言回答用户。Spring AI的实现方式相对友好。定义一个Java方法加上Tool注解然后注册到ChatClient的Tool回调里模型在对话中判断需要时就会触发调用。Component public class MenuTools { Tool(description 根据口味描述查询当前菜单中的菜品列表) public ListDish searchDishesByTaste(String taste) { return menuRepository.findByTaste(taste); } }注册代码如下chatClient builder.defaultTools(menuTools).build();这里要注意函数调用对模型有要求不是任何模型都支持。目前智谱AI的glm-4系列是支持的但从接口到注解的兼容性每个小版本可能会有细微出入建议你写完后用本地最简单的一个测试用例触发一下确认通了再叠加复杂逻辑。5. 餐饮SaaS场景实战智能点餐助手5.1 场景拆解与流程设计现在把这些能力组合起来做一个餐饮SaaS场景里非常典型的“智能点餐助手”。这个场景我接触过好多次商户希望顾客在扫码点餐时能像一个真人服务员一样对话比如“帮我看看有什么不辣的鸡肉菜品再来杯冰柠檬茶”。AI在这里不仅仅是聊天它需要接到真实的菜单数据上最终把用户的需求转化为能下单的结构化订单。整个链路拆成四步用户输入自然语言由AI识别点餐意图。AI通过函数调用查询菜单拿到当前门店真实的在售菜品。AI根据菜单数据和用户需求推荐菜品并生成结构化订单。后端把订单内容推送确认页用户确认后创建订单。在这套设计中最关键的不是某个模型而是提示词和工具的配合。菜单数据必须走实时查询不能让AI根据训练数据脑补菜品否则商户菜单换了你还在卖下架菜那可就闹笑话了。5.2 集成实现的关键代码第一步定义工具类封装菜单查询逻辑。注意这个方法里要过滤掉下架菜品返回的是商户门店维度的实时菜单。Component public class StoreMenuTools { private final DishRepository dishRepository; public StoreMenuTools(DishRepository dishRepository) { this.dishRepository dishRepository; } Tool(description 根据门店ID查询当前在售菜单) public ListDish getAvailableMenu(String storeId) { return dishRepository.findByStoreIdAndStatus(storeId, Status.ON_SALE); } Tool(description 根据门店ID和菜品名称查询菜品详情及库存状态) public Dish getDishDetail(String storeId, String dishName) { return dishRepository.findByStoreIdAndName(storeId, dishName); } }第二步在Controller里把工具注册进ChatClient构造点餐助手的system提示词。这一步的提示词我建议沉淀成独立文件方便以后调整话术。RestController public class AiOrderController { private final ChatClient chatClient; public AiOrderController(ChatClient.Builder builder, StoreMenuTools menuTools) { this.chatClient builder .defaultTools(menuTools) .defaultSystem( 你是一个餐饮连锁品牌的智能点餐助手。 你的职责是理解顾客的语言结合实时菜单推荐菜品并引导下单。 你的输出必须是一个JSON对象包含items数组和remark字段。 如果顾客要求的菜品不在菜单中必须如实告知不能虚构菜品。 可以适当推荐门店招牌菜但不能强行推销。 ) .build(); } PostMapping(/api/demo/ai-order) public OrderIntent createOrderIntent(RequestBody OrderRequest request) { OrderIntent intent chatClient.prompt() .user(request.userText()) .call() .entity(OrderIntent.class); return intent; } }这里的关键是defaultSystem和defaultTools会在每次请求时自动带上你不用每次调用都重复写。实际调试时我发现一个常见问题AI有时候把顾客一句话里的多个菜品识别成重复项或者漏掉“不要葱花香菜”这种备注。解决方式是提示词里增加一句“顾客的所有需求都必须体现在结构化输出中包括数量、口味、做法备注且每一项只能出现一次。”5.3 链路优化与成本控制这个场景上线前有几点优化建议给你都是真金白银换来的经验。第一给大模型套缓存或分流。顾客问的最多的问题往往集中在几个常见菜品的辣度、招牌推荐、营业时间上。对这种高频且答复相对固定的对话可以在应用层加一个简短的意图判断命中固定问答就直接返回不调用大模型。省下来的tokens很可观。第二流式输出必须做。餐饮点餐触屏上如果卡个两三秒才出一行字顾客会以为设备坏了。用前面讲的SseEmitter或者WebFlux推送把AI输出实时显示出来配合一个“正在生成”的loading状态体验会好很多。第三限制上下文长度。一次点餐对话如果又臭又长历史消息全往模型里塞token消耗会爆炸。Spring AI里可以设置系统提示词和用户消息的最大长度或者在业务层做对话历史裁剪。我的策略是只保留最近两轮对话多了就丢弃。第四唯一幂等。如果AI输出的结构里有桌号但用户没报桌号不要让它猜。后端要有默认桌号或提醒人工处理订单绝对不能因为AI瞎猜而落错桌。6. 常见问题排查与避坑实录6.1 高频报错速查表把我和身边团队踩过的高频问题整理成一张表对照排查会快很多。现象可能原因解决办法启动报错Unable to load class ChatModelSpring AI版本与Spring Boot版本不匹配确认Spring Boot 3.3.x以上 Spring AI 1.0.0 GA请求返回401 Unauthorizedapi-key错误或未配置检查环境变量ZHIPU_API_KEY确认没拼错、没过期返回404 Not Foundbase-url路径不对核对厂商文档里的API路径智谱AI通常以/v4结尾调用entity()转换报JsonMappingExceptionAI返回的结构与Java类型不匹配检查record字段名和提示词约束字段缺失给默认值函数调用没触发模型不支持或Tool没注册确认模型支持Function Calling确认defaultTools已注入中文乱码控制台编码或HTTP响应编码问题检查Spring Boot的server.servlet.encoding配置为UTF-8请求超时模型响应时间长默认超时太短调整连接超时和读取超时智谱AI这种大模型场景建议读取超时60秒以上提示词里带了空参数PromptTemplate创建时的Map缺失key给模板参数设置默认值或提前校验这张表里我强调一下超时问题。很多人默认HTTP超时设3秒、5秒结果一调大模型就超时然后怀疑是不是代码写错了。大模型生成一段话网络往返加推理时间10秒左右是常态你按同步调用的方式设个5秒超时那基本就没法用了。6.2 几个容易忽略的细节依赖传递冲突是我遇到比较多的坑。Spring AI内部依赖了Spring 6.1、Spring Boot 3.x版本对应的核心库如果你的项目里还有其他老版本Spring依赖启动时会出现NoSuchMethodError、ClassNotFoundException这种神鬼难测的报错。排查思路很简单删掉本地仓库里多余的Spring相关依赖统一版本或者用mvn dependency:tree看冲突。还有api-key千万别提交到Git。不仅仅是安全问题某些扫描工具会自动检测并告警处理起来非常被动。我现在的做法是本地用IDE的环境变量配置CI/CD里用密钥管理服务注入配置文件里只留占位符。模型名称也别看教程里写什么就抄什么。同一个厂商的模型更新很快旧名字可能已经下线或者能力有变化。最稳的做法是去厂商官网查当前可用模型列表把配置里的model参数改成实际存在的模型ID。另外再提一个经验在线下调试时我给ChatClient加了一个简单的日志拦截器把它发出的请求体和返回结果打到日志里。排查问题的时候这比看后端异常栈高效得多。Spring AI也提供了比较完整的观测机制项目上线后可以接上。最后说一个我自己的习惯每次升级Spring AI版本我都会先把现有样例项目跑一遍全量测试而不是直接用生产分支去升。这个小习惯救过我很多次因为Spring AI活跃度高版本API说改就改盯紧兼容性才能少加班。
返回列表