ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI全栈开发实战:Java+Vue3+大模型打造旅游推荐助手

2026/8/31 21:37:26 拓冰建站 浏览量
AI全栈开发实战:Java+Vue3+大模型打造旅游推荐助手 今年聊“AI全栈”的人很多但多数人只是停留在“会用 AI 生成代码”的层面。真正值得关注的变化是AI 能力正在变成业务系统的标准模块而不再只是开发者的辅助工具。如果你一直做 Java、Spring Boot 或者 Vue3 这类前后端开发会发现网上关于“AI 全栈”的资料往往只讲大模型本身很少有人把Java 后端 Vue3 前端 大模型接口串成一条完整链路讲清楚。这篇文章要做的就是用“AI 旅游智能推荐助手”这个真实项目把这条链路完整拆解一遍。为什么选旅游推荐因为它天然适合验证 AI 的能力边界用户输入是模糊的自然语言输出需要结合目的地数据、用户偏好和时间预算等结构化信息。传统推荐系统很难处理“我想找个适合带父母去、不要太累、预算 5000 以内的南方城市”这种请求而大模型擅长理解这种语义又需要落到真实数据上否则就会胡编。这个项目就是典型的“业务系统 大模型能力”复合场景也是 2026 年前后端工程师转型 AI 全栈最值得动手的方向。先给结论AI 全栈不是让你去训练大模型而是让你具备独立交付“包含 AI 能力的完整业务系统”的能力。真正的核心是如何在稳定的 Java 后端里接入大模型如何用 Vue3 做出好的交互如何让前后端数据流清晰可靠。下面开始正文。1. 这篇文章真正要解决的问题很多前后端工程师在转型时会先遇到一个认知困境学大模型要学 PyTorch、要懂 Transformer、要会微调这些和当前工作离得太远学完也用不上。但企业里真正稀缺的其实是另一类人——能把大模型能力“插进”现有业务系统的人。这背后的原因是大模型本身不产生业务价值只有和业务数据、用户场景、交互界面组合在一起才形成可用产品。比如你要做一个旅游推荐助手光有 ChatGPT 式对话框不够它必须知道数据库里有哪些真实景点、价格、评分、开放时间必须推荐完成后能下单、能收藏、能规划行程这些能力都属于 Java 后端和 Vue3 前端的工程范畴。回到技术侧这个项目要解决的问题有几个大模型接口如何被 Spring Boot 可靠调用包括超时、限流、错误处理如何把用户的自然语言转换为结构化查询条件再结合数据库数据生成推荐结果Vue3 前端如何优雅地展示推荐流并支持多轮修改需求前后端分离交互时的数据格式、跨域、状态管理问题。文章会按一条完整的项目实践路径展开先讲架构设计和概念边界再讲后端实现、前端实现、联调验证最后给出一套可复用的排错清单和工程建议。你可以把这份内容当作一份“从零到一”的 AI 旅游推荐助手项目源码笔记也可以当作转型 AI 全栈后的地图工具。1.1 什么样的读者最适合这篇文章已经掌握 Java 基础、Spring Boot 基础想学习如何接入大模型的读者熟悉 Vue2/Vue3想找一个“前后端分离 AI 能力”综合项目的读者准备面试 AI 应用开发、全栈开发相关岗位需要项目经历的读者对“AI 全栈”概念感兴趣但还不清楚实际工作内容是什么的读者。如果你是零基础建议先补完 Java 核心语法和 Vue3 基础再开始本文项目。项目并不要求你懂训练模型但后端 Spring Boot、前端 Vue3 的基本功必须有否则你会在环境问题上消耗掉大量时间。2. AI全栈开发与大模型全栈开发两个容易混淆的方向在这两年招聘市场里“AI 全栈开发工程师”和“大模型全栈工程师”经常被放在同一岗位描述中但二者侧重点并不相同。如果你准备转型先搞清楚概念边界能少走很多弯路。“大模型全栈工程师”的焦点更偏模型侧从数据清洗、微调、量化、推理服务部署到 prompt engineering 和 RAG整个过程围绕“如何构建一个会工作的模型服务”。而“AI 全栈开发工程师”的焦点是应用侧在已有模型服务的前提下完成业务系统集成、数据模型设计、接口开发、前端体验和部署运维。二者有交集但工作重心差别很大。用旅游推荐项目来对比会更直观方向典型工作对应本项目内容大模型全栈构建推荐模型、微调、搭建推理服务可选后续接入 RAG、向量检索AI 全栈把模型能力嵌入业务系统设计推荐接口、调用大模型、前端展示本文默认你走的是AI 全栈开发工程师路径也就是“使用模型能力”而不是“设计模型”。2.1 为什么技术栈选 Java Spring Boot Vue3技术选型上旅游推荐助手没有采用 Python 后端主要有三个原因第一在真实的企业级项目中Java Spring Boot 承载了海量稳定的交易系统、管理后台和开放接口。旅游推荐涉及订单、支付、用户体系时Java 生态的事务管理和成熟组件优势很大。第二对已经在做前后端开发的人而言Java Vue3 的平滑迁移成本比 Python 全栈低得多。你不需要一边学模型一边学一门新后端语言。第三大模型调用本质上就是一个 HTTP API 调用。Spring Boot 的 RestTemplate、WebClient、OpenFeign 都能胜任不存在“必须用 Python 才能调大模型”的门槛。这套组合的真正变化在于原来 Service 层写的是固定业务逻辑现在 Service 层需要多一个“与大模型对话”的环节原来前端只是展示数据库数据现在前端要能处理流式输出和多轮上下文。这不改变前后端分离的架构却改变了业务处理的复杂度分布。2.2 项目的核心难点不在 AI在“工程化”很多人上手 AI 项目后会发现调用大模型 API 太简单了难的是怎么让它在真实业务里稳定工作。比如大模型返回了超出数据库范围的景点怎么处理用户要求“冷门”“小众”模型编造了一个不存在的景点怎么兜底多个条件组合时比如“带老人小孩 不要爬山 南方”如何把语义准确性提升起来大模型响应慢前端要不要流式展示API Key 泄漏到前端了怎么办这些都不是模型训练问题而是经典的工程问题。本项目会在后面逐步给出解法核心策略是模型负责语义理解与推荐文案生成规则和数据库负责约束答案边界。3. 项目架构设计AI 旅游智能推荐助手的完整分层在写代码之前先设计架构。项目整体采用前后端分离后端提供 REST API前端使用 Vue3 渲染大模型作为后端服务中的一个远程能力被调用。整体流程可以概括为用户输入自然语言需求 ↓ Vue3 前端发送请求到 Spring Boot 后端 ↓ 后端解析请求 → 提取目的地/预算/天数/偏好 ↓ 查询数据库候选景点 → 拼接 Prompt ↓ 调用大模型 API 生成推荐结果 ↓ 后端对结果做校验与结构化转换 ↓ 返回 JSON 给前端展示这个流程看似简单但每一层都有独立的职责。3.1 后端模块职责后端规划为以下几个层级Controller 层接收前端请求做参数校验返回统一格式的 RestResultService 层业务编排先调 Dao 层查候选数据再调 LMMClient 生成推荐最后做结果校验LMMClient 层封装大模型 API 调用负责超时控制、错误重试、流式/非流式切换Dao 层访问 MySQL 中的目的地表、景点表、用户偏好表数据模型TripPlanRecommendRequestScenicSpot 等。这样分层的好处是如果未来换一个模型供应商只需要修改 LMMClient 层如果推荐算法要优化只需要改 Service 层如果展示字段要调整改 Controller 返回即可。3.2 前端模块职责前端的核心页面建议拆成两个视图推荐首页用户输入出行人数、天数、预算、偏好关键词点击“生成推荐”推荐结果页展示目的地卡片、景点列表、行程建议支持“重新生成”和“追问修改”。Vue3 中建议使用 Composition API用 ref/reactive 组织响应式数据用 fetch 或 axios 调用后端接口。推荐结果强调可视化卡片信息包括目的地名称、推荐理由、预估花费、景点列表、适合人群。3.3 为什么是“大模型 规则兜底”策略这里有必要强调一个判断纯大模型生成的推荐结果不适合直接作为最终输出。原因是模型存在幻觉可能推荐不存在的景点也可能推荐用户所在城市之外的区域。生产级项目必须引入规则兜底模型生成的推荐必须能在数据库中找到对应记录预算与天数必须有数字校验如果模型返回非法 JSON后端要有默认方案对于“冷门地点”优先从数据库中的“小众标签”里选而不是让模型自由发挥。这个策略会让系统在 90% 的场景下输出可控结果同时保留大模型的语义灵活性。4. 环境准备与前置条件开始写代码前先把环境准备好。下面给出本文项目使用的环境清单标注说明的版本请以实际为准。4.1 后端环境JDK17 或 21Spring Boot 3.x 对 JDK17 支持最稳定 构建工具Maven 3.8 Spring Boot3.x MySQL8.x也可以用 H2 内存库做演示 IDEIntelliJ IDEA 或 Eclipse如果你之前用的还是 JDK8 Spring Boot 2.x需要先稍微适应一下 Spring Boot 3.x 的变化例如 javax 包名变为 jakarta。4.2 前端环境Node.js18 或 20 包管理器npm 或 pnpm Vue3.x Vite4/5 均可4.3 大模型 API 准备由于大模型厂商较多本文不绑定具体商家。在代码设计上会抽象一个LlmClient接口你只需要按照厂商 SDK 替换实现即可。实际操作时你需要准备API Base URL、API Key、模型名称。安全提示API Key 绝不能写在前端代码里。所有调用必须走后端由后端携带密钥发起请求。5. Spring Boot 后端实现接入大模型的推荐服务这一节我们逐步编码。先创建工程再写 Model 层、LlmClient 层、Service 层和 Controller 层。5.1 创建 Spring Boot 工程推荐使用 Spring Initializr 创建选择依赖Spring Web Spring Data JPA或 MyBatis MySQL Driver Lombok Validationpom.xml核心依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesapplication.yml中配置数据源和自定义的 LLM 参数server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/trip_ai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true app: llm: api-base: https://your-llm-provider.example.com/v1 api-key: ${LLM_API_KEY} model: your-chat-model max-tokens: 800 temperature: 0.7请把 api-key 放入环境变量LLM_API_KEY中不要硬编码到配置文件并提交到 Git。5.2 定义推荐请求与返回模型先定义前端传来的请求结构// 文件路径src/main/java/com/example/tripai/model/RecommendRequest.java package com.example.tripai.model; import jakarta.validation.constraints.*; import lombok.Data; import java.util.List; Data public class RecommendRequest { NotBlank(message 目的地偏好不能为空) private String keywords; private String city; // 可选限定城市 private Integer days; // 出行天数 private BigDecimal budget; // 总体预算单位元 private Integer travelers; // 出行人数 private ListString preferences; // 例如 [美食, 亲子, 不累] }再定义返回给前端的推荐结果// 文件路径src/main/java/com/example/tripai/model/TripPlan.java package com.example.tripai.model; import lombok.Builder; import lombok.Data; import java.math.BigDecimal; import java.util.List; Data Builder public class TripPlan { private String destination; private String summary; private BigDecimal estimatedCost; private ListScenicSpot spots; private String remark; }5.3 封装大模型调用为了让项目不被具体大模型厂商绑定定义一个接口// 文件路径src/main/java/com/example/tripai/llm/LlmClient.java package com.example.tripai.llm; import java.util.List; public interface LlmClient { /** * 非流式对话 * * param systemPrompt 系统提示词 * param userMessage 用户消息 * return 模型回复文本 */ String chat(String systemPrompt, String userMessage); /** * 要求模型返回 JSON 时的对话入口 */ String chatForJson(String systemPrompt, String userMessage); }提供一个基于 RestTemplate 的通用实现// 文件路径src/main/java/com/example/tripai/llm/RestLlmClient.java package com.example.tripai.llm; import com.example.tripai.config.LlmProperties; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.Map; Slf4j Component RequiredArgsConstructor public class RestLlmClient implements LlmClient { private final RestTemplate restTemplate; private final LlmProperties llmProperties; private final ObjectMapper objectMapper; Override public String chat(String systemPrompt, String userMessage) { MapString, Object body Map.of( model, llmProperties.getModel(), messages, List.of( Map.of(role, system, content, systemPrompt), Map.of(role, user, content, userMessage) ), max_tokens, llmProperties.getMaxTokens(), temperature, llmProperties.getTemperature() ); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(llmProperties.getApiKey()); HttpEntityMapString, Object request new HttpEntity(body, headers); try { ResponseEntityString response restTemplate.exchange( llmProperties.getApiBase() /chat/completions, HttpMethod.POST, request, String.class ); JsonNode root objectMapper.readTree(response.getBody()); return root.path(choices).path(0).path(message).path(content).asText(); } catch (Exception e) { log.error(LLM call failed, e); throw new RuntimeException(大模型调用失败, e); } } Override public String chatForJson(String systemPrompt, String userMessage) { return chat(systemPrompt, userMessage); } }在实际项目中建议把restTemplate.exchange方法的超时时间调出来并在失败时增加重试或降级逻辑。Spring Boot 中可以通过RestTemplateBuilder配置连接和读取超时。5.4 推荐服务层把数据库数据和大模型拼接起来Service 层的核心逻辑是根据用户关键词查询数据库候选景点把候选数据、用户输入、数据库约束一起拼进 Prompt调用大模型生成推荐结果校验返回的 JSON转换成 TripPlan如果模型结果非法触发降级策略。先写一个简化版 Service// 文件路径src/main/java/com/example/tripai/service/TripRecommendService.java package com.example.tripai.service; import com.example.tripai.llm.LlmClient; import com.example.tripai.model.RecommendRequest; import com.example.tripai.model.ScenicSpot; import com.example.tripai.model.TripPlan; import com.example.tripai.repository.ScenicSpotRepository; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.stream.Collectors; Slf4j Service RequiredArgsConstructor public class TripRecommendService { private static final String SYSTEM_PROMPT 你是一位专业的旅游规划师。 请根据用户提供的候选景点数据生成一份合理的旅游推荐计划。 要求 1. 只能从候选景点中选择禁止编造不存在的景点 2. 推荐理由要结合用户偏好 3. 必须返回JSON格式{destination:目的地,summary:概述,spots:[{name:景点名,reason:推荐理由}],remark:备注} ; private final ScenicSpotRepository scenicSpotRepository; private final LlmClient llmClient; private final ObjectMapper objectMapper; public TripPlan recommend(RecommendRequest request) { ListScenicSpot candidates findCandidates(request); if (candidates.isEmpty()) { return buildFallbackPlan(暂未找到完全匹配的景点请尝试放宽条件。); } String candidateText candidates.stream() .map(s - String.format(景点%s城市%s特色%s门票%s, s.getName(), s.getCity(), s.getTags(), s.getTicketPrice())) .collect(Collectors.joining()); String userMessage String.format( 用户需求%s 出行天数%d 预算%s 人数%d 候选景点数据 %s 请根据候选数据生成推荐计划。 , request.getKeywords(), request.getDays(), request.getBudget(), request.getTravelers(), candidateText); try { String content llmClient.chatForJson(SYSTEM_PROMPT, userMessage); MapString, Object resultMap objectMapper.readValue(content, new TypeReference() { }); return convertToPlan(resultMap); } catch (Exception e) { log.warn(LLM response parse failed, use fallback, e); return buildFallbackPlan(AI 推荐暂时不可用为你展示候选景点。); } } private ListScenicSpot findCandidates(RecommendRequest request) { return scenicSpotRepository.findByKeyword(request.getKeywords()); } private TripPlan convertToPlan(MapString, Object resultMap) { // 实际项目中建议在这里做字段校验和默认值填充 return TripPlan.builder() .destination(String.valueOf(resultMap.getOrDefault(destination, 未命名目的地))) .summary(String.valueOf(resultMap.getOrDefault(summary, ))) .remark(String.valueOf(resultMap.getOrDefault(remark, ))) .build(); } private TripPlan buildFallbackPlan(String message) { return TripPlan.builder() .destination(候选方案) .summary(message) .remark(系统兜底结果) .build(); } }这里的关键点是Prompt 里放进了候选景点数据并严格要求模型只能选择候选列表中的景点。这样可以把模型的幻觉约束在可控范围内。5.5 Controller 层对外提供 REST API// 文件路径src/main/java/com/example/tripai/controller/TripController.java package com.example.tripai.controller; import com.example.tripai.model.RecommendRequest; import com.example.tripai.model.TripPlan; import com.example.tripai.service.TripRecommendService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/recommend) RequiredArgsConstructor public class TripController { private final TripRecommendService tripRecommendService; PostMapping public TripPlan recommend(Valid RequestBody RecommendRequest request) { return tripRecommendService.recommend(request); } GetMapping(/health) public String health() { return ok; } }启动 Spring Boot 后后端部分就初步跑通了。可以用 curl 测试curl -X POST http://localhost:8080/api/recommend \ -H Content-Type: application/json \ -d {keywords:南方 亲子 不累,days:3,budget:5000,travelers:3,preferences:[美食,亲子]}5.6 配置统一响应与全局异常处理为了前端联调方便推荐把返回统一为RestResultT并增加全局异常处理器。示例// 文件路径src/main/java/com/example/tripai/common/RestResult.java package com.example.tripai.common; import lombok.Getter; Getter public class RestResultT { private final int code; private final String message; private final T data; private RestResult(int code, String message, T data) { this.code code; this.message message; this.data data; } public static T RestResultT success(T data) { return new RestResult(0, success, data); } public static T RestResultT error(int code, String message) { return new RestResult(code, message, null); } }全局异常处理不是本文核心但一定要加上否则大模型接口异常时前端会收到很不友好的错误体。6. Vue3 前端实现从提问到结果展示后端接口就绪后前端开始接入。这一节会创建 Vue3 项目并实现推荐请求与结果展示。6.1 创建 Vue3 项目npm create vitelatest trip-ai-web -- --template vue cd trip-ai-web npm install npm install axios npm run dev建议在src/api/index.js中统一封装 axios 实例// 文件路径src/api/index.js import axios from axios const request axios.create({ baseURL: /api, timeout: 60000, }) request.interceptors.response.use( (response) response.data, (error) { console.error(接口请求失败:, error) return Promise.reject(error) } ) export default request6.2 编写推荐页面推荐页面包含输入区关键词、天数、预算、人数、按钮、结果区。按照 Vue3 的script setup写!-- 文件路径src/views/HomeView.vue -- template div classhome h2AI 旅游智能推荐助手/h2 div classsearch-panel el-input v-modelform.keywords placeholder请输入你的旅行偏好例如南方 亲子 不累 / el-input-number v-modelform.days :min1 :max30 placeholder天数 / el-input-number v-modelform.budget :min100 :step500 placeholder预算 / el-input-number v-modelform.travelers :min1 :max20 placeholder人数 / el-checkbox-group v-modelform.preferences el-checkbox label美食 / el-checkbox label亲子 / el-checkbox label古镇 / el-checkbox label自然风光 / /el-checkbox-group el-button typeprimary :loadingloading clickhandleRecommend 生成推荐 /el-button /div div v-iftripPlan classresult-panel h3{{ tripPlan.destination }}/h3 p{{ tripPlan.summary }}/p p{{ tripPlan.remark }}/p /div /div /template script setup import { reactive, ref } from vue import request from ../api const loading ref(false) const tripPlan ref(null) const form reactive({ keywords: 南方 亲子 不累, days: 3, budget: 5000, travelers: 3, preferences: [美食, 亲子], }) async function handleRecommend() { loading.value true try { const res await request.post(/recommend, form) tripPlan.value res.data } finally { loading.value false } } /script6.3 开发环境代理配置开发阶段前后端端口不同会遇到跨域问题。推荐在 Vite 里配置代理而不是在 Spring Boot 里放开跨域// 文件路径vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, })这样前端请求/api/recommend时会由 Vite 开发服务器转发到后端 8080 端口避免浏览器直接跨域。7. 前后端交互与功能验证7.1 启动后端在 IDEA 中直接运行 Spring Boot 启动类或者命令行mvn spring-boot:run看到类似下面的日志说明启动成功Tomcat started on port(s): 8080 (http)7.2 启动前端npm run dev浏览器访问http://localhost:5173在页面输入偏好点击“生成推荐”观察请求是否成功返回。7.3 预期结果与判断标准正常情况下的数据流前端POST /api/recommend携带 JSON 请求体后端查询数据库拿到候选景点列表后端拼接 Prompt调用大模型大模型返回 JSON后端转换成TripPlan返回前端tripPlan被赋值页面展示目的地与推荐理由。如果这一步跑通了说明你已经完成了一个“前后端分离 AI 能力”的最小闭环。判断标准可以定为1. 数据库中有候选景点时推荐结果来自候选列表而不是模型随意编造 2. 多次请求同一个输入时结果内容会有合理变化而不是完全一样 3. 大模型接口故意关闭时后端能返回兜底方案前端不白屏。7.4 验证兜底策略你可以临时把大模型 API Key 改错再发起请求观察后端日志是否出现LLM response parse failed前端是否展示系统兜底结果。这个验证非常重要因为它决定了你的应用在生产环境中是否具备基本的容错能力。8. 常见问题与排查思路下面整理本项目最常见的几个问题覆盖环境、前后端、大模型三个层面。问题现象可能原因排查方式解决方案Spring Boot 启动失败报Error creating bean with name entityManagerFactory数据库连接失败或 JPA 实体扫描错误检查 MySQL 是否启动查看 application.yml 用户名密码修正数据源配置确认数据库已创建前端请求/api/recommend返回 404Vite 代理没生效或者后端接口路径不匹配打开浏览器 Network 面板看请求实际地址检查 vite.config.js 的 proxy 配置确认 Controller 的 RequestMapping后端报 CORS 跨域错误浏览器直接请求了 8080 端口看浏览器 Network 中接口的 Request URL使用 Vite proxy或 Spring Boot 配置 CORS 策略大模型返回内容无法解析为 JSONPrompt 约束不严格或模型输出包含多余内容打印 LLM 返回原文查看日志在 Prompt 强调“只返回 JSON”解析前先截取{...}区间推荐结果中出现了数据库不存在的景点候选数据没有传入 Prompt或模型忽略约束检查 Service 中 candidateText 是否拼接成功在 Service 中对结果景点名做二次校验不在候选列表中的删掉大模型接口超时模型响应时间长HTTP 客户端超时设置太短查看后端日志的 Remaining Time增加 RestTemplate 超时时间或改用流式输出提升首字体验前端页面卡住没有结果也没有报错axios 请求未设置超时后端挂起重试打开 Network 面板看 Pending 状态给 axios 设置timeout: 60000中文乱码编码不一致检查 application.yml 是否配置server.servlet.encoding统一使用 UTF-8前端请求头声明charsetutf-88.1 重点排查大模型返回 JSON 解析失败这是接入大模型时最高频的错误。推荐在 LlmClient 层统一处理打印模型返回原文尝试用正则提取 JSON 片段String json content.substring(content.indexOf({), content.lastIndexOf(}) 1);解析失败时抛出异常Service 层降级。8.2 重点排查Spring Boot 版本太高导致的依赖问题如果你的 Spring Boot 版本过高一些第三方依赖可能还没有适配。最稳妥的做法是查看当前 Spring Boot 对应的 Spring Cloud 版本、查看 Lombok 版本是否兼容 JDK 17。如果出现 Lombok 不生效先在 IDE 中 Enable Annotation Processing再检查 Lombok 版本是否过旧。9. 最佳实践与工程建议了解了基础实现之后这里给出一些生产级项目的改进建议。这些点也是面试中体现经验深度的地方。9.1 密钥与配置管理API Key 属于敏感信息严禁提交到 Git 仓库。推荐方式export LLM_API_KEY你的密钥然后在application.yml中使用app: llm: api-key: ${LLM_API_KEY}9.2 超时与重试策略大模型接口可能慢也可能不稳定。建议为 LLM 调用设计两层机制第一层HTTP 连接超时 3 秒读取超时 30 秒超时后不再等待第二层对网络抖动类错误增加最多 2 次重试重试间隔逐步扩大对业务类错误如额度不足、鉴权失败不要重试直接进入降级逻辑。这里的关键判断是不能因为大模型暂不可用就让整个推荐业务不可用。降级策略要作为一个必要条件而不是可选增强。9.3 Prompt 与数据边界把数据库候选数据放入 Prompt明确告诉模型“只能从中选择”在返回结果解析后再做一次“结果是否在候选集合中”的校验对于金额、天数等数字使用 Java 侧 BigDecimal 和 Integer 校验不依赖模型的数学计算。9.4 响应方式流式 vs 非流式本文为了演示使用了非流式接口体验上会有等待时间。生产项目中更推荐流式输出SSE让用户先看到第一个字再看到完整结果。前端使用EventSource或fetch的ReadableStream接收流式数据。流式实现的复杂度更高但也是 AI 应用体验提升的关键一步。9.5 日志与可观测性给每一次 LLM 调用增加 requestId记录 Prompt 关键片段与模型返回原文便于排查记录候选景点数量、模型耗时、降级是否发生。日志建议格式[requestIdabcd1234] LLM call cost1523ms, candidateCount8, fallbackfalse9.6 数据安全与权限如果推荐系统涉及用户注册、收藏等操作后端要做权限校验。推荐在 Spring Boot 中使用拦截器或 Spring Security 校验 JWT前端将 Token 放在 HTTP Header 的Authorization字段。不要把用户身份信息传给大模型必要时只传脱敏后的偏好字段。9.7 前后端协作规范前后端分离项目最容易出现接口定义不一致。建议在项目启动前先定义好 OpenAPI 文档或者使用 Apifox/Postman 维护接口集。后端返回数据格式统一为RestResultT前端不要在业务层猜测字段类型。这样能显著减少联调时间。10. 如何把项目扩展到简历与面试层面一个“AI 旅游推荐助手”项目如果只是把大模型接口调通面试时很难加分。真正拉开差距的地方在于下面这些扩展点10.1 RAG让推荐内容基于实时知识景点数据可能有变化比如临时闭园、门票涨价。可以把景区公告、攻略文档做向量化存入向量数据库。推荐生成前先从向量库检索相关知识再拼入 Prompt。这就是 RAG 的落地场景也是大模型全栈工程师与实际业务结合的典型路径。10.2 多轮对话与上下文管理目前的接口是一问一答。生产项目中用户可能需要“再改成 5 天”“预算减少到 3000”。这时需要前端维护 conversationId后端存储多轮上下文大模型基于历史对话调整推荐结果。需要注意控制上下文长度避免 Token 超限。10.3 结果缓存与性能优化对相同或近似的推荐请求可以做结果缓存减少大模型调用成本。可以是 Redis 缓存也可以本地 Caffeine 缓存。但要注意用户偏好和景点数据更新后缓存要能失效。10.4 部署后端 前端 数据库全栈项目的价值不只体现在本地运行还要能部署上线。推荐用 Docker Compose 编排 MySQL、Spring Boot 后端、Nginx 静态前端。这个过程并不难但能证明你具备完整的工程交付能力。前端构建后输出到dist目录由 Nginx 提供静态资源和反向代理。这里给出一个最小的 Nginx 配置思路server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }把 Vue3 的dist放到 Nginx 的html目录后端通过 Docker 网络访问就完成了前后端分离部署的基础形态。10.5 面试时可以重点表达的判断如果面试官问到“你为什么选择 Java Vue3 做 AI 项目”可以这样回答AI 应用全栈的核心在于“模型能力 业务系统”的稳定集成Java 在事务、权限、部署生态上有优势Vue3 的组件化能快速搭建推荐交互界面尤其适合结果卡片、流式输出这类 UI 场景项目中的难点不是调用大模型而是如何让模型输出可校验、可降级、可维护我通过候选数据约束 JSON 校验 兜底策略把模型幻觉控制在可接受范围后续通过 RAG 和流式输出可以进一步提升推荐准确性和用户体验。这些表达不是说辞而是项目里真实发生过的设计决策所以能经得住追问。11. 总结与下一步实践建议这篇文章围绕“AI 旅游智能推荐助手”项目把 AI 全栈开发的核心链路完整拆了一遍。你可以看到这套项目的技术本质不是多高深的模型算法而是把 Spring Boot、Vue3 和大模型接口组合成一个能落地、能验证、能兜底的业务系统。真正值得反复练习的是候选数据约束、JSON 校验、降级策略和前后端联调这些工程细节。模型能力会不断迭代但工程能力永远不可替代。建议你按下面顺序实践先按本文目录结构把 Spring Boot 后端跑通用 curl 验证推荐接口再创建 Vue3 前端把页面和接口对接上完成最小闭环接着故意制造“大模型不可用”的故障验证兜底逻辑然后加入流式输出、缓存、RAG、Docker 部署逐个扩展最后整理项目文档、截图、README形成完整的作品集。如果你之前一直做传统前后端开发这个项目就是转型 AI 全栈最合适的起点。它不要求你立刻成为模型专家但能让你第一次亲手把“智能”变成业务系统的一部分。从这一个项目开始再往 RAG、Agent、流式交互和复杂业务编排走路会顺畅得多。建议收藏本文动手实现时遇到问题随时对照第 8 节的排查清单。