
简介本资源是一个基于Spring AI与Langchain4j构建的旅游行程规划智能体完整工程面向Java后端开发者、AI应用实践者及高校计算机专业学生解决个性化旅游方案生成、自然语言交互式行程定制等实际问题。压缩包共69个文件涵盖12个核心Java服务模块含Agent编排、LLM调用与工具链集成、15个Vue前端页面行程展示、偏好配置、实时对话界面、5个JSON配置与提示词模板、以及类图/用例图/部署图等5张系统设计图整体大小为5.74MB。已有125人学习下载资源结构清晰分层——tourism-agent-server提供AI推理服务tourism-agent-client封装业务逻辑tourism-agent-ui实现用户交互配套PDF/DOCX大作业文档详述设计思路与实现细节开箱即可运行调试是理解AI智能体在垂直领域落地的典型教学级实战案例。1. 为什么旅游行程规划不能只靠大模型“自由发挥”——Spring AI Langchain4j 智能体的真实落地逻辑你让大模型直接写“帮我规划北京3日游”它大概率会给你一份带故宫、颐和园、长城的模板清单再加几句“建议早起避开人流”——听起来合理但实际根本没法用没考虑你出发地是深圳还是西安、没校验高铁票余量、不识别你带6岁孩子不能爬八达岭陡坡、更不会主动查明天故宫是否闭馆。这不是模型能力弱而是纯LLM调用缺乏结构化约束、外部工具链和状态记忆。而“基于 Spring AI 和 Langchain4j 的旅游行程规划智能体.zip”这个标题恰恰指向一个工程化解法用 Spring 生态做可靠服务编排用 Langchain4j 做可调试的智能体骨架把天气API、交通票务、景点开放时间、用户偏好等真实数据源像插件一样嵌进决策流。它不是炫技的Demo而是面向OTA系统、旅行社SaaS或企业差旅平台的可交付模块——适合正在用 Spring Boot 做后端、又想快速集成AI能力的Java工程师也适合需要把LLM能力封装成标准服务接口的架构师。核心价值不在“用了AI”而在让AI输出可验证、可回溯、可灰度、可运维。2. 从零搭起智能体骨架Spring AI 与 Langchain4j 的分工边界在哪2.1 Spring AI 不是“Spring版LangChain”而是 Java 生态的 AI 基础设施层很多刚接触的开发者误以为 Spring AI 是 Langchain4j 的包装器甚至试图用SpringBootApplication直接启动一个Langchain4j Agent——这会导致依赖冲突和上下文丢失。真相是Spring AI 负责“连接器”和“管道”Langchain4j 负责“决策脑”和“工具调度”。Spring AI 提供统一的AiClient对接OpenAI/Alibaba Qwen/DeepSeek等、EmbeddingClient向量库接入、RetrievalAugmentorRAG基础组件它把不同厂商的SDK差异抹平让你换模型只需改配置而 Langchain4j 的AgentExecutor、Tool、ToolProvider才真正定义“什么时候调哪个工具、怎么组合结果、失败时如何降级”。二者关系类似 Spring JDBC 与 MyBatis前者管连接池和事务后者管SQL组装和映射逻辑。提示Spring AI 2.02024年Q2发布已移除对旧版 Spring Boot 2.x 的支持最低要求 Spring Boot 3.2、Java 17。若项目还在用 Spring Boot 2.7必须先升级——这不是可选项因为旧版 Spring AI 的AiResponse类型在新版本中已被重构为ChatResponseStreamingChatResponse强行混用会导致ClassCastException。2.2 Langchain4j 的 Agent 构建三要素Orchestrator、Tools、MemoryLangchain4j 的智能体不是单个类而是一个协作系统。以旅游行程规划为例必须显式声明三个核心组件Orchestrator编排器选用StructuredChatAgent推荐而非ReactAgent。前者强制要求每个 Tool 返回结构化 JSON如{ type: transport, from: Beijing South, to: Xian North, date: 2024-08-15 }避免LLM自由发挥导致解析失败后者依赖自然语言描述调试成本高。Tools工具集至少包含4个可注入的Tool实现WeatherTool调用和风天气API输入城市名返回未来3天温度/降水概率TrainTicketTool封装12306开放平台需企业资质或第三方票务聚合API校验车次余票AttractionInfoTool对接高德/百度POI接口获取景点开放时间、预约状态、儿童政策ItineraryRefinerTool本地Java逻辑负责合并多日行程、检查时间冲突如“上午爬山下午看展”是否可行。Memory记忆不用ConversationHistory这种简单文本缓存而用InMemoryChatMemory 自定义ChatMessage序列化策略。关键点在于每次AgentExecutor.invoke()前必须将用户原始请求含地理位置、预算、同行人年龄等转为UserMessage并注入SystemMessage强制约束输出格式例如“你只能返回JSON字段必须包含days、activities、warnings禁止任何解释性文字”。下面是最小可运行的 Agent 初始化代码Bean public Agent agent(AiClient aiClient, ToolProvider toolProvider) { // 1. 定义系统提示词硬编码进SystemMessage String systemPrompt 你是一个专业旅游行程规划助手。请严格按以下规则响应 - 输出必须是合法JSON无任何额外字符 - 字段必须包含days整数数组如[1,2,3]、activities对象数组每个含place、time、duration、notes、warnings字符串数组 - 若某日无法安排满保留空数组不可虚构 - 禁止使用“可能”、“建议”等模糊词所有结论必须有工具调用依据 ; // 2. 构建Orchestrator注意必须用StructuredChatAgentBuilder return StructuredChatAgent.builder() .aiClient(aiClient) .tools(toolProvider.tools()) // 注入所有Tool .chatMemory(new InMemoryChatMemory()) // 内存实例 .systemMessage(systemPrompt) .build(); }这段代码的关键参数说明aiClient由 Spring AI 自动装配自动读取application.yml中的spring.ai.openai.api-key或spring.ai.alibaba.qwen.api-keytoolProvider.tools()必须返回ListTool且每个Tool的name()方法返回值要与LLM提示词中提到的工具名完全一致大小写敏感InMemoryChatMemory仅用于开发测试生产环境必须替换为 Redis-backedRedisChatMemory否则集群部署时会话丢失。3. 工具链落地如何让旅游智能体真正“动起来”而不是“瞎说”3.1 WeatherTool用 OpenFeign 封装天气API拒绝裸HTTP调用很多教程直接用RestTemplate调天气接口结果在高并发下线程阻塞、超时熔断失效。正确做法是用 Spring Cloud OpenFeign 做声明式客户端并内置重试与降级FeignClient(name weather-api, url ${weather.api.base-url:https://devapi.qweather.com/v7}) public interface WeatherClient { GetMapping(/weather/3d?location{location}key{key}) WeatherResponse get3DayForecast( PathVariable(location) String location, PathVariable(key) String key ); } Data public class WeatherResponse { private String code; private ListDaily daily; Data public static class Daily { private String date; private String textDay; private String tempMin; private String tempMax; } }对应的WeatherTool实现Component public class WeatherTool implements Tool { private final WeatherClient weatherClient; private final String apiKey; public WeatherTool(WeatherClient weatherClient, Value(${weather.api.key}) String apiKey) { this.weatherClient weatherClient; this.apiKey apiKey; } Override public String name() { return get_weather_forecast; // 必须与LLM提示词中工具名一致 } Override public String description() { return 获取指定城市的3天天气预报输入参数city城市名称如北京; } Override public String execute(String jsonInput) { try { MapString, String input new ObjectMapper().readValue(jsonInput, Map.class); String city input.get(city); if (city null || city.trim().isEmpty()) { return {\error\: \缺少city参数\}; } // 调用Feign Client自动携带重试、熔断 WeatherResponse response weatherClient.get3DayForecast(city, apiKey); if (200.equals(response.getCode())) { return new ObjectMapper().writeValueAsString(response.getDaily()); } else { return String.format({\error\: \天气API返回错误码%s\}, response.getCode()); } } catch (Exception e) { return String.format({\error\: \调用天气API异常%s\}, e.getMessage()); } } }关键设计点FeignClient的url属性用${weather.api.base-url}配置化方便测试/生产环境切换execute()方法必须返回字符串非对象因为 Langchain4j 的 Tool 调用协议要求 JSON 字符串错误处理必须返回结构化JSON含error字段否则LLM无法理解失败原因可能反复重试同一无效请求。3.2 TrainTicketTool解决12306反爬与会话维持难题12306官网无开放API必须模拟浏览器行为。但直接用 Selenium 会吃光服务器内存正确方案是用 OkHttp CookieJar 维持登录态配合极验验证码识别商用API。我们不实现完整登录而是假设已通过企业合作获得 tokenComponent public class TrainTicketTool implements Tool { private final OkHttpClient httpClient; private final String ticketApiUrl https://ticket-api.example.com/query; public TrainTicketTool() { // 启用连接池与超时 this.httpClient new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .cookieJar(new DefaultCookieJar()) // 关键保持JSESSIONID .build(); } Override public String name() { return query_train_tickets; } Override public String description() { return 查询两城市间高铁余票输入参数from出发站、to到达站、dateYYYY-MM-DD; } Override public String execute(String jsonInput) { try { MapString, String input new ObjectMapper().readValue(jsonInput, Map.class); String from input.get(from); String to input.get(to); String date input.get(date); // 构造请求体模拟12306 POST格式 RequestBody body new FormBody.Builder() .add(fromStation, from) .add(toStation, to) .add(trainDate, date) .add(token, System.getenv(TICKET_TOKEN)) // 从环境变量读取 .build(); Request request new Request.Builder() .url(ticketApiUrl) .post(body) .header(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .build(); try (Response response httpClient.newCall(request).execute()) { if (response.isSuccessful()) { return response.body().string(); } else { return String.format({\error\: \票务API返回HTTP %d\}, response.code()); } } } catch (Exception e) { return String.format({\error\: \查询车票异常%s\}, e.getMessage()); } } }血泪经验12306的fromStation/toStation必须是官方编码如“BJP”代表北京南不能传中文。因此AttractionInfoTool在返回景点信息时必须附带所在城市的标准三字码通过高德POI接口的adcode反查否则TrainTicketTool会因参数错误直接失败。4. 避坑指南旅游智能体上线前必须踩过的5个深坑4.1 坑1LLM输出JSON格式错乱导致解析崩溃现象Agent返回内容包含多余换行、中文引号、末尾逗号ObjectMapper.readValue()抛JsonProcessingException。原因LLM生成JSON时未严格遵循语法尤其在流式响应streaming场景下首帧可能只有{末帧才补全}。解决在StructuredChatAgent的systemMessage中强制要求“输出JSON必须是单行、无注释、无多余空格、无中文引号”在Tool.execute()返回前用正则预清洗json.replaceAll(\\s, ).replace(“, ).replace(”, )更稳妥方案用 Jackson 的JsonNode替代ObjectMapper.readValue()它对格式容错更强。4.2 坑2工具调用死循环Agent反复查同一天天气现象用户问“上海3日游”Agent连续3次调用get_weather_forecast参数都是city:上海却不推进到下一步。原因LLM未理解“3日游”需分别查每日天气提示词未明确要求“按日期拆分调用”。解决在systemMessage中追加“若需多日数据必须对每一天单独调用工具不可合并”在WeatherTool.execute()中增加逻辑若输入city含“上海”且无date参数则默认返回3天数据但JSON中daily数组必须含date字段如2024-08-15强制LLM区分日期。4.3 坑3Redis内存爆满RedisChatMemory写入失败现象生产环境报OOM command not allowed when used memory maxmemoryAgent会话全部丢失。原因InMemoryChatMemory切换为RedisChatMemory后未设置TTL历史会话永久堆积。解决在RedisChatMemory构造时指定ttlSecondsnew RedisChatMemory(redisTemplate, Duration.ofHours(24));对Redis Key 加命名空间前缀如agent:chat:避免与其他业务冲突开启Redis LRU淘汰策略maxmemory-policy allkeys-lru。4.4 坑4Spring AI 的 EmbeddingClient 与 Langchain4j RAG 结果不一致现象用EmbeddingClient计算的向量与 Langchain4jInMemoryEmbeddingStore存储的向量余弦相似度低于0.8。原因Spring AI 默认用OpenAiEmbeddingModel而 Langchain4j 示例用HuggingFaceEmbeddingModel两者tokenizer和归一化方式不同。解决统一使用 Spring AI 的EmbeddingClient生成向量并用其embed()方法批量计算Langchain4j 的RetrievalAugmentor必须注入同一个EmbeddingClient实例而非新建验证方法用相同文本调用两次embeddingClient.embed(北京景点)确认返回向量一致。4.5 坑5多轮对话中用户修改需求Agent无法覆盖旧计划现象用户先问“北京3日游”Agent返回计划再问“改成4日加一个环球影城”Agent在原计划上硬加导致第4天行程超负荷。原因InMemoryChatMemory仅存储消息历史未标记“当前行程草案”状态LLM无法识别需重生成。解决在systemMessage中加入状态指令“每次收到新需求必须先清空已有行程草案重新规划全程”在ItineraryRefinerTool中增加校验若输入JSON含days字段且长度变化强制触发全量重排更优方案引入状态机用SessionScopeBean 存储CurrentItineraryStateAgent每次调用前先读取该Bean。5. RAG增强实战用景点知识库解决“冷门地接”问题旅游智能体最大的短板是LLM对小众景点如“贵州肇兴侗寨”的非遗体验项目缺乏细节。纯靠API调用只能返回开放时间无法回答“哪家染布作坊接受游客体验”。这时必须上RAG——但别急着堆Chroma或Milvus先用最轻量的InMemoryEmbeddingStore验证流程。5.1 构建景点知识片段结构化比纯文本更重要不要把整个百度百科页面扔进向量库。正确做法是人工提取结构化三元组每条记录含id: “dongzhai-rafting”title: “肇兴侗寨 canoe rafting”content: “在堂安梯田下游的溪流中由侗族师傅指导乘坐杉木筏时长1.5小时需提前2小时预约费用80元/人儿童限1.2m以上”metadata:{ province: Guizhou, city: Liping, category: cultural_experience, min_age: 6 }用Excel整理后导出为JSONL每行一个JSON对象共237条冷门景点数据。这是知识库质量的生死线——如果content写成“侗寨很美可以玩水”RAG检索必然失效。5.2 Langchain4j 的 RetrievalAugmentor 配置要点Spring AI 的EmbeddingClient与 Langchain4j 的RetrievalAugmentor必须桥接Bean public RetrievalAugmentor retrievalAugmentor(EmbeddingClient embeddingClient) { // 1. 创建内存向量库生产环境换为RedisVectorStore EmbeddingStoreTextSegment embeddingStore InMemoryEmbeddingStore.create(); // 2. 加载知识片段从JSONL文件读取 ListTextSegment segments loadSegmentsFromJsonl(attractions.jsonl); embeddingStore.add(embeddingClient, segments); // 关键用同一embeddingClient // 3. 构建增强器 return RetrievalAugmentor.builder() .embeddingStore(embeddingStore) .retriever(new EmbeddingStoreRetriever(embeddingStore, embeddingClient)) .build(); }然后注入 AgentBean public Agent agent(AiClient aiClient, ToolProvider toolProvider, RetrievalAugmentor retrievalAugmentor) { return StructuredChatAgent.builder() .aiClient(aiClient) .tools(toolProvider.tools()) .retrievalAugmentor(retrievalAugmentor) // ← 关键注入点 .build(); }注意retrievalAugmentor会在每次AgentExecutor.invoke()前自动将用户问题向量化并从embeddingStore中检索Top3相关片段拼接到SystemMessage末尾。因此你的systemMessage必须预留位置例如“参考以下景点知识{retrieved}。请据此规划行程……”5.3 效果验证用“贵州肇兴侗寨”触发RAG的完整链路当用户输入“我要带父母去肇兴侗寨他们腿脚不便有什么轻松体验”Agent 先调用AttractionInfoTool获取基础信息开放时间、门票同时RetrievalAugmentor检索到dongzhai-rafting片段发现其min_age为6且未提老人限制但检索也命中dongzhai-singing片段“侗族大歌表演在鼓楼内进行全程坐椅观看时长40分钟”最终Agent输出中activities包含{ place: 肇兴侗寨鼓楼, time: 14:00-14:40, duration: 40分钟, notes: 室内坐椅观看适合老年人 }而不是凭空编造“坐竹筏”。这就是RAG的价值不替代工具调用而是补足工具无法提供的长尾知识。没有它智能体只是API聚合器有了它才能叫“懂行的旅行顾问”。6. 生产就绪 checklist从 ZIP 包到可交付服务的最后五步拿到“基于 Spring AI 和 Langchain4j 的旅游行程规划智能体.zip”后别急着解压跑通就交差。真正的交付价值在于它能否融入现有系统、被业务方信任、出问题时快速定位。以下是我在三个客户项目中沉淀的 checklist每一条都对应过线上事故步骤检查项验证方法不做的后果1. 环境隔离application-prod.yml中spring.ai.*和langchain4j.*配置项必须与测试环境物理隔离禁止用Profile(test)临时开关检查CI/CD流水线确认prod profile打包时application.yml被完全覆盖测试环境密钥泄露到生产API调用量暴增扣费2. 工具熔断每个Tool必须配置RetryableCircuitBreaker且maxAttempts2,openTimeout30s用curl -X POST http://localhost:8080/actuator/health查看circuitBreakers状态天气API宕机导致整个Agent线程阻塞HTTP请求超时3. 输出审计所有AgentExecutor.invoke()返回的JSON必须经JsonSchemaValidator校验用json-schema-validator库编写单元测试故意传非法JSON确认抛ValidationException前端解析失败白屏客服接到大量投诉4. 会话追踪每次调用生成唯一traceId透传至所有Tool并在日志中打印traceIdxxx, userId123, query北京3日游查看ELK日志搜索traceId是否贯穿全部微服务出问题时无法定位是哪个用户、哪次请求、哪个工具失败5. 降级开关提供/actuator/feature-toggle端点可动态关闭RAG、关闭某个Tool如query_train_tickets返回兜底静态数据调用POST /actuator/feature-toggle?tooltrainenabledfalse再发请求验证春运期间12306接口不稳定需秒级切回“仅展示景点”模式最后分享一个血泪习惯永远在application.yml里留一个debug.modetrue开关开启时让 Agent 输出完整决策链包括每次Tool调用的入参/出参/耗时但日志级别设为DEBUG并只写入独立文件agent-trace.log。上线后把它关掉但千万别删——下次用户说“为什么没推荐环球影城”打开它5分钟内定位到是AttractionInfoTool的高德API返回了{status:0,message:配额超限}而不是模型问题。希望帮到你。本文还有配套的精品资源点击获取