ARTICLE DETAIL

资讯详情

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

Spring AI与Langchain4j构建Java旅游行程规划智能体实战

Spring AI与Langchain4j构建Java旅游行程规划智能体实战 简介这是一套基于 Spring AI 与 Langchain4j 打造的旅游行程规划智能体完整项目源码面向正在学习 Java AI 应用开发的全栈开发者适合想理解大模型在实际业务中落地的读者。资源共 69 个文件、5.74MB前端采用 Vue 实现对话交互界面后端以 Java 编写智能体核心逻辑并包含 JSON、properties、xml 等配置类文件doc 目录提供 PDF 与 docx 大作业文档assets 目录给出类图、用例图、部署图、活动图等设计素材整体结构便于对照学习。目前已有 125 人学习下载。代码保留了 tourism-agent-client、tourism-agent-server、tourism-agent-ui 等清晰模块围绕行程规划、天气服务、智能体交互等场景演示了从用户输入到模型调用再到结果输出的完整链路搭配项目文档与 UML 图可以快速掌握基于 AI 构建个性化旅游助手的工程思路与关键实现细节。1. 旅游行程规划智能体怎么做Spring AI 和 Langchain4j 的 Java 落地方案做旅游行程规划智能体最容易踩坑的不是模型选型而是 Java 团队拿着一堆 Python 生态的参考代码不知道往哪落。这个标题把 Spring AI 和 Langchain4j 放在一起实际上是在回答一个问题Spring 官方的 AI 框架和社区这套 Java 智能体框架到底谁负责接模型、谁负责编排工具才能让“五天四晚带老人”这种需求真正变成可执行的行程单。适合三类人正在做智能体项目但被 Python 技术栈卡住的 Java 工程师、想把现有 Dify 或 Coze 原型工程化的后端团队以及刚接触 LLM 应用开发、想用一套代码同时看懂两种主流框架的入门者。下面按选型、骨架、调参、排错的顺序把整条路走一遍。2. 先选型两套 Java 框架在行程规划里怎么分工少走三个月弯路2.1 Spring AI 和 Langchain4j 的定位差异一个接生态一个接思路把这两个框架放一起很多人第一反应是“二选一”实际项目里它们更常见的角色是分工。Spring AI 是 Spring 官方出的 AI 应用框架设计哲学跟 JdbcTemplate 一脉相承把模型调用、提示词模板、流式输出这些基础能力封装成统一接口天然嵌进 Spring Boot 的自动配置体系。你配好 application.yml 里的模型地址和密钥注入一个 ChatClient 就能开聊切换模型厂商的成本被压得很低。比如 Spring AI 2.0.1 连接百炼 qwen3.7配置一个 OpenAI 兼容的 chat 端点就行这点对国内团队很实用。Langchain4j 的路线完全不同它更像 LangChain 思路的 Java 移植核心抽象是 AiServices围绕智能体常见的“模型 工具 记忆”三件套做编排。你可以用 Tool 注解直接暴露一个 Java 方法给大模型调用用 ChatMemory 管理多轮对话状态这套东西在 Spring AI 里需要自己拼。说句实在话如果你要做带工具调用、多路召回、状态管理的智能体Langchain4j 的抽象更顺如果你只是想把模型接进现有 Spring Boot 接口Spring AI 更省事。两者都支持 OpenAI 兼容协议所以模型层可以共用一份配置不必重复造轮子。2.2 行程规划智能体需要的四个能力拆解旅游行程规划不是简单的一问一答把它拆开看至少四个能力缺一不可。第一是意图识别用户说的“带老人”“孩子暑假”“预算五千”这些信息要能识别成硬约束而不是当成闲聊带过。第二是多路召回一个目的地通常涉及景点、酒店、餐厅三类数据需要并行查询再合并而不是让模型凭训练数据编造一个“本地人都去的小众景点”——那基本就是幻觉重灾区。第三是行程编排把召回结果按照天数、地理位置、游玩时长排成上午下午都有安排的路线。第四是结构化输出前端地图组件要渲染行程后端就需要 JSON而不是一段难以解析的 Markdown。这四个能力对应到框架选型上意图识别和行程编排主要靠提示词与模型能力多路召回靠工具调用框架结构化输出靠输出解析器。Langchain4j 的 AiServices 天生就是为这套流程设计的Tool 方法做召回MessageWindowChatMemory 做多轮记忆OutputParser 约束输出格式。Spring AI 在这个流程里更适合扮演“模型网关”负责统一的模型接入、流式响应和对齐不同厂商的接口差异。2.3 选型结论与最小技术栈我一般给出的选型结论是Langchain4j 做主智能体编排Spring AI 做模型接入层Spring Boot 做 Web 容器两者不冲突。需要注意版本兼容Langchain4j 1.x 基于 Java 17 和 Spring Boot 3.xSpring AI 从 1.0 开始也要求 Spring Boot 3.2 以上。有个容易被忽略的坑两套框架都依赖 Jackson 和 OkHttp传递依赖版本不一致会在启动时抛 NoSuchMethodError后面避坑章节会专门讲。最小技术栈大致如下组件选型职责基础框架Spring Boot 3.2Web 服务、依赖管理模型接入Spring AI 2.xChatClient、流式输出、模型切换智能体编排Langchain4j 1.xAiServices、Tool 工具调用、对话记忆数据存储MySQL / Redis景点库、酒店库、会话状态持久化这套组合的边界在于Spring AI 管“模型怎么连”Langchain4j 管“智能体怎么想”。如果你只有一个简单问答需求只上 Spring AI 就行如果你要写复杂的 Agent 逻辑只靠 Spring AI 会发现自己实现 Tool Calling 和记忆管理很痛苦。反过来Langchain4j 需要自己接 Spring Boot 的配置体系配合 Spring AI 的自动配置反而省心。3. 搭骨架用 Langchain4j 定义工具用 Spring AI 接模型最小可跑代码3.1 工程结构把 Tool、服务、模型配置分开起步工程不建议把代码堆在一个类里我习惯按“入口 → 服务 → 工具 → 配置”四层拆。入口层放 Controller负责接收前端对话请求服务层放行程规划的核心编排逻辑工具层放景点、酒店、餐厅的查询方法每一个都是独立的 Java 类配置层放模型客户端和记忆策略。这样的结构在后面加新工具时不需要动主流程Langchain4j 的 AiServices 会自动扫描注册的工具类。src/main/java/com/example/trip/ ├── controller/TripController.java ├── service/ItineraryService.java ├── agent/ItineraryAgent.java ├── tool/AttractionTool.java ├── tool/HotelTool.java ├── tool/RestaurantTool.java ├── config/ModelConfig.java └── config/MemoryConfig.java主流程我一般放在 ItineraryAgent 里由它持有 AiServices 构建出来的智能体实例。工具类通过 Tool 注解暴露给模型Controller 只做参数接收和响应返回。这个分层的逻辑是让模型只看到工具层的输出不让它碰到底层数据库连接或第三方 API 密钥既安全又方便后续替换数据源。3.2 用 Langchain4j 定义 AiServices 和工具类先写一个最简单的景点查询工具。注意 Tool 注解里的 description 不是写给人看的注释而是给大模型看的函数说明它决定模型在什么场景下调用这个工具描述不清晰模型就会乱调。import dev.langchain4j.agent.tool.Tool; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; Component public class AttractionTool { Tool(根据城市名和游玩天数查询景点列表返回每个景点的名称、门票价格、建议游玩时长和开放时间) public ListMapString, Object queryAttractions(String city, int days) { // 实际项目中这里会查 MySQL 或调用第三方 POI 服务 // 这里用模拟数据演示返回结构 return List.of( Map.of(name, 故宫, ticket, 60, duration, 4, hours, 08:30-17:00), Map.of(name, 颐和园, ticket, 30, duration, 3, hours, 06:30-18:00) ); } }这个工具方法有两个关键点。第一方法入参由模型根据用户对话内容自动填充比如用户说“去北京玩三天”模型会识别出 city“北京”、days3所以参数名要起得语义明确。第二返回值推荐用 ListMapString, Object 或 JSON 字符串模型对 JSON 结构的解析能力远强于对自由文本的理解字段名也要稳定否则模型每次都瞎猜。再看智能体本身怎么构建。在配置类里注入模型和工具列表用 AiServices 把它们组装成一个可以调用的智能体接口。import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.service.AiServices; public class ItineraryAgent { public ItineraryService build(ChatLanguageModel model) { MessageWindowChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); return AiServices.builder(ItineraryService.class) .chatLanguageModel(model) .tools(new AttractionTool(), new HotelTool(), new RestaurantTool()) .chatMemory(memory) .build(); } }其中 ItineraryService 是一个接口你只需要声明方法签名Langchain4j 会在运行时生成实现。注意 maxMessages 这个参数它决定记忆窗口保留多少条历史消息太小会忘记前两天定的酒店太大会把上下文塞满导致模型响应变慢、Token 费用飙升。20 条对于单日行程够用多日行程建议配合摘要压缩后面的章节会细说。3.3 用 Spring AI 接入模型并组装调用链前面说了分工思路现在看 Spring AI 怎么接入模型。以 Spring AI 连接百炼 qwen3.7 为例配置 Open AI 兼容端点模型配置从 application.yml 里读取不硬编码在代码里。spring: ai: chat: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} options: model: qwen3.7 temperature: 0.4Spring AI 对 OpenAI 兼容协议的支持比较完善把 base-url 指向兼容端点模型名换成对应的 qwen 模型标识就行。temperature 参数对行程规划很关键建议设在 0.3 到 0.5 之间太高模型会发挥想象力给你编造景点太低模型会变成复读机回答缺乏灵活性。如果你是私有化部署把 base-url 指向内网的 vLLM 或 Ollama 服务地址同样生效。这里有一个绕不开的适配问题Spring AI 的 ChatClient 和 Langchain4j 的 ChatLanguageModel 不能直接互相转换。我在工程里的常见做法是做一层薄薄的适配器Spring AI 负责接收前端请求、把消息转成 Langchain4j 需要的格式核心智能体调用走 Langchain4j流式响应为了兼容 Spring WebFlux 再转回 Spring AI 的 Flux 类型。import org.springframework.ai.chat.model.ChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ModelConfig { Bean public ChatLanguageModel chatLanguageModel(ChatModel springAiChatModel) { // 适配器把 Spring AI 的 ChatModel 适配成 Langchain4j 的 ChatLanguageModel return new SpringAiChatLanguageModelAdapter(springAiChatModel); } }适配器内部需要实现 chat 方法把 Langchain4j 的 ChatMessage 列表转换成 Spring AI 的 Prompt 结构再接收返回结果。这个转换过程有时候会因为消息角色字段不一致出问题比如 Langchain4j 的 SystemMessage 和 Spring AI 的 SystemMessage 虽然名字一样但构造参数和序列化字段有差异适配时不要偷懒直接映射建议单独写工具方法做转换。如果你的团队不想维护这层适配也可以让 Langchain4j 直接接 OpenAI 兼容端点Spring AI 只负责流式代理两条路都走得通。3.4 多路召回的实现先并行查库再让模型编排多路召回是行程规划智能体区别于普通问答的关键。一个“北京三天游”的需求模型需要同时拿到景点、酒店、餐厅三类候选数据才能给出靠谱的行程。如果串行查询用户等待时间会叠加更好的方式是用 CompletableFuture 并行查询把结果合并后一次性交给模型。import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class RecallService { private final AttractionTool attractionTool; private final HotelTool hotelTool; private final RestaurantTool restaurantTool; public String multiRecall(String city, int days) { CompletableFutureString attractions CompletableFuture .supplyAsync(() - attractionTool.queryAttractions(city, days)) .completeOnTimeout([], 3, TimeUnit.SECONDS); CompletableFutureString hotels CompletableFuture .supplyAsync(() - hotelTool.queryHotels(city, days)) .completeOnTimeout([], 3, TimeUnit.SECONDS); CompletableFutureString restaurants CompletableFuture .supplyAsync(() - restaurantTool.queryRestaurants(city)) .completeOnTimeout([], 3, TimeUnit.SECONDS); CompletableFuture.allOf(attractions, hotels, restaurants).join(); return 【景点数据】%s 【酒店数据】%s 【餐厅数据】%s .formatted(attractions.join(), hotels.join(), restaurants.join()); } }这里做了一个关键的兜底completeOnTimeout 让任何一个数据源超时时返回空数组而不是让整个请求卡死。实际项目中第三方 API 的响应时间波动很大景区接口 2 秒返回、酒店接口 8 秒才响应是常有的事如果不加超时控制用户那边就是无限转圈。合并后的文本用分段标签把三类数据隔开模型能更清楚地知道哪部分数据对应哪种工具结果编排行程时就不容易把酒店信息错当成景点用。4. 让行程能落地结构化输出、多日记忆与预算约束的调参思路4.1 让模型输出结构化行程JSON Schema 约束与提示词兜底模型默认输出的是自然语言前端地图组件要渲染路线必须拿到结构化数据。我的做法是双保险第一在系统提示词里给出 JSON 格式模板第二调用过程中要求模型严格按模板返回。只靠提示词不百分百可靠但加上格式示例后qwen 这类模型的遵循能力已经足够应付行程规划场景。String systemPrompt 你是一个旅游行程规划专家。请根据提供的景点、酒店、餐厅数据 为用户规划每日行程并严格按照以下 JSON 结构输出不要输出任何额外文字 { days: [ { day: 1, date: 2025-07-20, morning: {spot: 景点名, duration: 2小时, note: 安排说明}, afternoon: {spot: 景点名, duration: 3小时, note: 安排说明}, evening: {hotel: 酒店名, restaurant: 餐厅名}, tips: 当天注意事项 } ], budget: {total: 0, detail: 预算明细} } ;提示词模板里有两个容易被忽略的细节。一个是 date 字段如果你不给当前日期模型经常会把今年日期写成去年的同一天或者干脆写一个“2025-01-01”之类的占位符。另一个是 budget 里的 total如果用户没说预算模型会默认填一个大而化之的数最好在召回阶段就把酒店的每晚价格和景点门票放进上下文模型才有依据做预算测算。如果业务有强校验需求比如必须保证所有字段非空可以在提示词里加一句“如果某天没有安排填写自由活动不要省略字段”。4.2 多日记忆与会话窗口哪些该记、哪些该丢行程规划不是一次性对话用户会在同一会话里反复调整比如“第二天酒店换成离故宫近的”“第三天下午想去博物馆”。如果智能体记不住之前定好的安排每次修改都得让用户重述需求体验极差。Langchain4j 的 MessageWindowChatMemory 解决的是“最近几轮聊了什么”但行程规划还要记住“已经确认了什么”。我的常见做法是把记忆分成两层。原始对话轮次放进 MessageWindowChatMemory窗口设为最近 10 轮就够了因为完整的对话原文很快会超出模型上下文限制。已经确认的行程安排单独存一份“行程状态”每次用户确认一个安排就把它写入状态对象下一次调用时把状态对象的摘要和原始消息一起发给模型。这样既保证模型知道当前的完整行程又不至于把所有历史消息都塞进窗口。MessageWindowChatMemory.builder() .maxMessages(10) .chatMemoryStore(new RedisChatMemoryStore()) .build();注意 ChatMemoryStore 这个扩展点。默认的内存实现重启即失忆一旦服务重启用户会话里的历史就全没了。生产环境我建议把记忆持久化到 Redis用 sessionId 做 key超时时间设成 24 小时这样即使用户断线重连行程规划还能接上上下文。这属于“加了不显眼、不加就翻车”的配置项。4.3 预算、时间、老人小孩这些硬约束怎么设硬约束处理不好是行程规划智能体最常见的返工原因。用户说“预算五千以内”模型给你排一个每天住五星酒店的计划这属于没有把约束写进系统提示词。约束不能只在第一轮提示词里提因为多轮对话中它会被后续消息冲淡。正确做法是把约束固化成结构化的用户画像在每一轮调用前都重新注入。public class UserPreference { private int budget 0; // 总预算0 表示未设定 private boolean withElderly false; // 是否有老人同行 private boolean withChildren false; // 是否有儿童同行 private String pace normal; // 行程节奏relaxed / normal / intense }这段数据结构直接拼进系统提示词并且用一句话告诉模型“以上约束条件优先级最高任何推荐都不得违反。如果无法在预算内满足需求明确告诉用户并解释原因。”这一步很关键——模型倾向于讨好用户即使条件冲突也会硬着头皮给方案你要允许它说“不”。实际测试中加了这句授权之后模型会主动提醒“这个行程超出预算约 800 元建议去掉一天温泉行程”而不是继续硬编。同样带老人小孩的场景模型默认的攻略路线经常出现暴走两万步的安排。处理方法不是提示“慢慢走”而是让工具返回的数据里带上“步行距离”和“爬升高度”字段再在提示词里规定“单日步行距离不得超过 8 公里每个景点之间车程不超过 1 小时”。模型只有在数据层面看到这些字段才会真正约束自己的行程编排逻辑。5. 避坑与排查模型幻觉、上下文膨胀、依赖冲突的 5 个真实翻车点5.1 模型编造不存在的“小众景点”推荐完才发现查无此地现象智能体规划行程时推荐了一个没在工具返回结果里的景点理由是“当地人才知道的秘境”用户到了现场发现已经关门停业。原因模型训练数据里确实包含大量景点信息行程编排时模型觉得自己“知道”这个景点就没走工具调用。工具框架给了模型选择权模型在高压场景下更倾向用参数记忆而不是外部检索。解决在系统提示词里写死一条规则“所有景点、酒店、餐厅必须来自【数据来源】列表禁止自行补充。如果候选数据不足明确告知用户数据覆盖有限。”同时在提示词末尾加一句“请检查你输出的地点是否全部在候选列表中”这句话能显著降低幻觉。技术上还能加一道校验每次模型输出后用程序比对返回的景点名称和召回数据发现不在列表中的名称就触发一次重试让模型重新生成。5.2 对话到第三轮就失忆前面定的酒店全被“遗忘”现象用户在第二轮确定了酒店到第三轮说“把第二天酒店换掉”模型回复“我似乎没有看到您之前选择的酒店信息”整个行程规划断裂。原因MessageWindowChatMemory 的 maxMessages 设置太小比如默认 10 条而每轮对话至少包含用户消息、助手消息、工具调用结果三条三轮过后最早的消息已经被挤出了窗口。行程信息恰好在这时丢失。解决把 maxMessages 调到 30 左右同时不要只依赖原始消息窗口。更可靠的做法是像 4.2 节那样把已确认的行程做摘要存到状态对象每轮调用前把摘要和原始消息一起注入。摘要用大白话说就是“把之前所有轮的结论浓缩成几行字”比堆原始消息省钱且抗丢失。提示调参时先估算一轮对话会产生多少条消息再决定窗口大小。带工具调用的对话一轮通常产生 3 到 5 条消息这是新手最常算错的地方。5.3 启动报 NoSuchMethodErrorSpring AI 和 Langchain4j 的传递依赖打架现象项目启动时抛NoSuchMethodError: okhttp3.RequestBody.create或 Jackson 的InvalidDefinitionException堆栈信息指向完全不相关的代码。原因Spring AI 和 Langchain4j 内部都依赖 OkHttp、Jackson、SLF4J 等通用库。两套框架各自锁定的版本不一致Maven 在解析传递依赖时选择了较旧的版本导致运行时找不到新版本才有的方法。解决在 pom.xml 里显式锁定公共依赖版本。我一般引入 Maven 的 dependencyManagement 统一管理。常见需要锁定的有 okhttp统一用 4.12.x、jackson-databind2.15、reactor-core3.6。锁定后跑一遍完整测试重点验证模型调用和流式输出两条链路因为这两处最容易触发 HTTP 客户端兼容问题。5.4 多路召回串行执行用户等 5 秒才看到响应直接流失现象工具调用看起来没毛病但每个接口 1 秒三个工具串行跑完要 3 秒加上模型生成时间用户感知延迟超过 5 秒日志里工具调用时间是叠加的。原因Tool 注解的方法默认是同步串行执行。写的时候没想那么多以为模型会“智能地并行调用”实际上 Langchain4j 默认串行执行工具除非底层模型支持并行工具调用并且框架开启了该特性。解决像 3.4 节那样把多路召回的逻辑从 Tool 方法里抽出来在进入智能体之前先用 CompletableFuture 并行查好数据把结果合并成一个上下文块再让模型基于这段合并数据编排行程。这样模型只需要处理一次编排不需要管理多个工具的调用顺序和返回时间。另一个备选方案是给慢接口做本地缓存比如景点数据按城市 24 小时缓存能省掉一大半的外部调用耗时。5.5 模型返回的 JSON 偶尔多一个逗号解析直接报错现象10 次里有 1 次模型输出被解析器拒绝报 JSON 格式错误。重复请求同样的参数有时候好有时候坏属于典型的概率性问题。原因大型语言模型生成文本存在随机性即使提示词里给了 JSON 模板模型也可能在最后一个字段后多加逗号或者把注释文本混进 JSON。这种情况在长行程输出三天五晚时概率更高因为生成步骤越多越容易出错。解决做两层兜底。第一层提示词末尾加“不要包含 markdown 代码块标记输出纯 JSON”这能挡住最常见的 json 包裹。第二层解析失败时不要把错误直接返回给用户而是捕获异常后把报错信息回传给模型一次让它自行修复 JSON 再输出。很多模型看到“你的 JSON 解析失败原因是第 45 行意外字符”能在第二次尝试中自我纠正。这一招能挽回约六成的解析失败场景。6. 进阶技巧把 Dify 工作流迁到 Spring AI Java 代码的映射方法6.1 Dify 节点与 Java 组件的映射关系很多人先拿 Dify 或 Coze 搭了原型验证了流程再考虑工程化。这些平台的工作流编辑器里拖拽的节点落实到 Spring AI 和 Langchain4j 代码上映射关系比想象中直接。理解这个映射迁移时就不会对着流程图发愁了。Dify 工作流节点Spring AI / Langchain4j 对应组件迁移要点开始节点用户输入Controller 入参 DTO参数名保持一致默认值别漏LLM 节点AiServices 接口方法 系统提示词把每一轮 LLM 的 Prompt 模板抽成常量工具节点Tool 注解的 Java 方法HTTP 调用改成 Spring RestClient 或 WebClient知识库检索节点多路召回 向量检索可以用 spring-ai 的向量存储抽象或直接查库条件分支节点代码里的 if/switch模型输出结构化之后走不同分支变量聚合节点多路召回后的上下文拼接用 3.4 节的模板合并数据6.2 从流程图落到 AiServices 的实践经验迁移时最忌讳把每个节点对应成一个大类那会把工作流翻译得像流水账。我一般先找出流程图里的“决策点”——在 Dify 里是一个条件分支节点在代码里就是一次 AiServices 接口方法的分派。比如一个判断用户是否设定预算的分支落到代码里就是 UserPreference 里的 budget 字段先查出来后续 Prompt 模板根据这个字段选不同版本。public interface TripPlanner { String planTrip(dev.langchain4j.service.UserMessage String userMessage, String context); }这是一个极简的 AiServices 接口定义唯一的入参是用户消息上下文多路召回结果、用户偏好 JSON通过外部拼接收好再传进去。Dify 工作流里那些串联的 LLM 节点迁到代码里往往可以合并让一个大模型调用同时完成“理解需求 编排行程”两个动作省掉一次模型往返延迟下降明显。迁移完第一步不是对拍输出效果而是对比 Token 消耗——Dify 工作流节点多了Token 自然会成倍上涨合并节点后通常能省 30% 到 50% 的 Token这是 Java 化之后最直观的收益。我自己的经验是跑通一个 5 节点 Dify 工作流到 Java 的迁移正常需要一到两个工作日。压垮进度的从来不是代码量而是提示词里隐藏的隐性依赖。有一次我迁移一个带“知识库检索”节点的流程Dify 里默认检索 topK 是 5 条我在代码里只查了 3 条模型生成的行程推荐质量肉眼可见地下降。调参数花了半天最后发现根因是召回数量阈值变了。以后凡是涉及召回参数我会先写进配置中心再写代码。如果你想在这个方向往下走建议从“把 Dify 里的工具节点替换成 Java 工具类”开始先保留平台跑逻辑Java 侧只做转发两边输出对齐后再逐步替换。这条路风险最小每一步都有对照结果可以验证最终迁移完成时也不会出现“代码看着对、效果就是不对”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表