Spring AI 与 Langchain4j 混用实战:构建旅游行程规划智能体
简介面向Java后端与AI应用开发者基于Spring AI和Langchain4j打造的旅游行程规划智能体项目用于实现个性化行程规划、实时旅行咨询与回忆分享等能力。资源包内含tourism-agent-client、tourism-weather-server和tourism-agent-ui三个核心模块分别对应智能体后端、天气服务与Vue前端覆盖完整前后端工程链路。压缩包共69个文件以Java与Vue构成前后端主体JSON/Properties负责配置PNG设计图展示系统设计PDF/DOCX文档提供说明整体约5.74MB目录划分清晰。随包还提供类图、用例图、部署图、活动图等系统设计材料以及大作业文档PDF/DOCX便于理解整体架构并复现运行。目前已有125人学习适合想掌握Spring AI与Langchain4j实际集成方法、并落地旅游类智能体项目的开发者参考。1. 旅游行程规划智能体为什么偏偏用 Java 技术栈做大多数人听到“智能体”三个字第一反应是 Python、Dify、Coze 这类快速搭建的玩法。但如果公司已有的订单、酒店库存、供应商报价全在 Java 系统里让模型自己编一个行程再手动录进 ERP等于没做。这个题目里的 zip 之所以值得拆开看是因为它把 Spring AI 和 Langchain4j 两个 Java 侧的 AI 框架拧成了一条能跑通的链路LLM 只负责“规划和决策”数据查询、预算校验、交通耗时这些脏活全部交给 Java 工具函数去执行。适合谁Java 后端团队想把一个能对话、能调用内部 API、能输出结构化行程的智能体嵌进现有业务系统而不是再造一个演示用的 ChatBot。即使你手上只有这一份 zip按下面的思路也能把它理顺成能跑、能改、能上生产的工程。2. Spring AI 和 Langchain4j 不是二选一边界、定位与混用2.1 两个框架到底各自管什么Spring AI 是 Spring 官方出的 AI 抽象层核心价值是“模型接入标准化”。你用它的 starter 配好 base-url 和 api-key 之后不管是通义千问、智谱还是其他兼容 OpenAI 协议的模型在 Java 代码里拿到的都是一个 ChatClient。Spring AI 2.0.1 这条线还强化了两件事一是 Function Calling 的机制更顺Tool 标注的 Java 方法可以直接注册给模型二是 BeanOutputConverter 这类结构化输出工具能把模型回答直接映射成 DTO不用自己写 JSON 解析。Langchain4j 则是社区驱动的 Java LLM 框架它更像智能体的“运行时”。AiServices 是它的核心抽象你定义一个 Java 接口接口方法上用注解描述框架自动把方法绑定给大模型并且把工具调用、记忆、重试这个循环替你管好。特别是 ChatMemory多轮对话里上下文窗口怎么裁剪它在 builder 里直接配置不用自己拼 Prompt。再加上 ContentRetriever 这一套 RAG 组件知识库检索、向量召回、重新排序都能串起来。所以这两个不是竞争对手而是分层关系。我的常见做法是Spring AI 管模型接入、结构化输出Langchain4j 管智能体编排、工具循环、对话记忆。如果你只做单轮问答只用 Spring AI 就够了如果你只想快速验证智能体链路、不在乎 Spring Boot 版本对齐只用 Langchain4j 也顺。但这个题目要的是“旅游行程规划”既有实时数据检索又有多日对话上下文还要求输出可落地的行程单两个一起用才不别扭。2.2 选型对比一张表看清各自边界对比维度Spring AI 2.0.xLangchain4j模型接入starter 丰富连接阿里云百炼 qwen3.7 这类模型只需配置端点也能接但多走兼容 OpenAI 协议端点工具调用Tool 注解 ChatClient 手动编排AiServices 自动绑定工具循环更完整对话记忆通过 Advisors 体系做消息裁剪ChatMemory 开箱即用MessageWindow 直接配窗口多路召回有向量库抽象但检索、合并、排序得自己拼ContentRetriever 串一条链RAG 组件更省事结构化输出BeanOutputConverter 映射 DTO 最稳也有 JSON Schema 支持但要自己多写两步与 Spring Boot 集成Spring 官方出品配置最顺有 starter也能融入 Boot但版本要卡准这张表对应到旅游行程规划这个场景结论很清楚模型接入和最终结果反序列化用 Spring AI工具循环和对话记忆用 Langchain4j。两边的 Bean 在同一个 Spring 容器里各管一段互不抢活。2.3 混用时的依赖关系怎么摆既然是混用maven 里就要同时引入两套依赖。这里最容易踩的坑是版本号对不齐。Langchain4j 对 Spring Boot 的主版本很敏感Spring AI 2.0.x 又要求 Boot 3.x 这条线所以 pom 里先锁死 Spring Boot 版本再让两个框架的 starter 跟随 Boot 版本走。常见做法是这么组织依赖spring-ai-starter模型接入、langchain4j-spring-boot-starter智能体运行时、langchain4j 的 spring-ai 适配模块把 Spring AI 的 ChatClient 包装成 Langchain4j 能识别的 ChatLanguageModel。加依赖的时候把两个框架的版本写到 properties 里统一管理避免手里一个 2.0.1、一个 0.35 这种错配组合。我第一次混用的时候就是没管版本结果 Spring AI 的自动配置把 Langchain4j 的工具扫描也抢过去了后面避坑章会细说。3. 落一个多路召回的行程规划智能体架构与领域模型3.1 为什么行程规划必须做多路召回旅游行程规划智能体跟普通问答最大的区别在于它不能靠模型“凭记忆”生成行程。POI 的营业时间是今天的事天气是明天的事交通耗时是实时的事这些数据 LLM 训练时没见过。让模型直接编一个“故宫上午、颐和园下午”的行程很可能把周一闭馆的景点排进去。所以第一步必须是召回而且不能只有一路。多路召回指的是把用户的一句话拆成结构化参数然后并行从 POI 数据库、天气接口、交通耗时服务、预算池四个来源取真实数据再合并成候选清单交给模型做决策。热门搜索里“langchain4j 多路召回”指的就是这条链路。我一般会这样设置召回源第一路POI 库按城市、偏好、区域过滤带评分和价格第二路天气接口取未来三天的天气、温度、降水概率第三路交通耗时算景点与住宿地之间的通勤时间第四路预算池把酒店、门票、餐饮的预算上限查出来。四路数据合并之后模型拿到的是“事实”而不是“想象”。这也是智能体和 ChatBot 拉开差距的地方。3.2 领域模型召回的是事实规划的是结论为了不让模型自由发挥我建议先把数据结构定义清楚。召回的 POI 只保留模型做决策真正需要的字段别把一个几十列的大对象丢给模型字段越多模型越容易挑错。public record TripRequest(String city, int days, int budget, ListString preferences) {} public record PoiDto(String id, String name, String category, double rating, double price, String region, boolean mondayClosed) {} public record WeatherDto(String date, String condition, int high, int low, int rainProbability) {} public record TransitDto(String fromRegion, String toRegion, int minutes, String mode) {} public record AccommodationDto(String name, int pricePerNight, String region, String status) {}TripRequest 是用户意图抽取的结果其他四个是各路召回的产物。这里有一个容易被忽略的设计点POI 里加了一个mondayClosed字段就是用来承接“周一闭馆”这类真实约束的。如果不把这个字段显式给到模型模型在规划时很可能会忽略闭馆日把周一的行程排成满的。这就是“让数据替模型做决定”的思路。3.3 数据流从一句话到一份行程单整个智能体的执行流程可以这样描述这也是我们后面代码要照着实现的框架用户输入一句话比如“北京 3 天 2 晚预算 5000喜欢博物馆和美食”模型先抽取结构化参数得到 TripRequest参数分发给四个召回源并行执行合并结果按“城市匹配优先、偏好关键词加权、评分降序”做初步排序把合并后的候选清单交给模型模型结合用户偏好做行程编排编排结果触发工具链校验比如预算是否超支、交通时间是否合理校验通过后结构化输出 TimetableOutput也就是最终行程单整个对话过程写入 Langchain4j 的 ChatMemory下一次追问“第二天想轻松点”时模型还记得前因后果。这八步里最长最难的是第 5 步到第 7 步。模型并不总是第一次就能排出合理行程经常需要“规划—校验—修正”循环几轮。下一章就写怎么用代码把这套流程跑起来。4. 用 Spring AI 和 Langchain4j 跑通最小行程编排代码与参数4.1 先建好依赖和配置两个框架各拿各的模型句柄最小可跑工程只需要三组依赖。版本号建议统一放到 properties 里Spring Boot 用 3.x 这条线Spring AI 用 2.0.1 这条线Langchain4j 选择与 Boot 3.x 对齐的 release。不要混用 Boot 3.2 和 Boot 3.4 的旧版 starter。properties java.version21/java.version spring-boot.version3.3.5/spring-boot.version spring-ai.version2.0.1/spring-ai.version langchain4j.version1.0.0-beta3/langchain4j.version /properties dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter/artifactId version${spring-ai.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-ai/artifactId version${langchain4j.version}/version /dependency /dependencies配置层面Spring AI 管模型接入。连接“百炼 qwen3.7”这类兼容 OpenAI 协议的模型时只需要配置 base-url 和 api-key不需要额外引入模型服务商的 SDK。下面是核心配置片段按你实际拿到的服务商信息填就行。spring: ai: model: chat: base-url: ${AI_BASE_URL:https://your-model-endpoint.example} api-key: ${AI_API_KEY:} options: temperature: 0.2 max-tokens: 2048temperature 调低到 0.2 是有意的。行程编排属于“有标准答案倾向”的任务温度太高会让模型在“颐和园还是圆明园”之间反复横跳。max-tokens 给 2048 是因为一次行程单往往要输出日期、景点、交通、预算四部分太小会被截断。4.2 组装模型句柄Spring AI 当接入层Langchain4j 当运行时关键一步是把 Spring AI 的 ChatClient 包装成 Langchain4j 的 ChatLanguageModel。这样 Langchain4j 的 AiServices 不认识 Spring AI 的类型也能正常调用两个框架各管一段互不干扰。Configuration public class ModelConfig { Bean public ChatClient springAiChatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是旅游行程规划助手。拿到用户需求后先抽取参数 再调用工具获取真实数据最后按天输出行程。不要编造景点和价格。) .build(); } Bean public ChatLanguageModel chatLanguageModel(ChatClient springAiChatClient) { // langchain4j-spring-ai 适配器把 Spring AI 的 ChatClient 转成 Langchain4j 模型句柄 SuppressWarnings(unchecked) ChatLanguageModel model SpringAiChatModel.builder() .chatClient(springAiChatClient) .build(); return model; } }这段代码的逻辑是Spring AI ChatClient 通过 Builder 构建时注入了系统提示词和模型端点信息然后适配器把它转成 Langchain4j 模型。参数说明两点一是系统提示词不要写得过长把“先抽取参数再调用工具”这个约束写清楚就行二是如果你不想用适配器也可以在 Langchain4j 侧直接用 OpenAiChatModel.builder() 配同一个 base-url两个框架各自连接同一模型服务。适配方式更省事因为 Spring AI 已经帮你管好了连接池和重试。4.3 把旅游能力写成工具POI 搜索、天气、交通、预算校验智能体的核心是工具。我用 Langchain4j 的 Tool 注解把四个能力注册给模型。注意工具描述里必须写清楚“什么时候调用、什么时候别调用”这是减少模型乱调工具的关键。Component public class TripTools { private final PoiRepository poiRepository; private final WeatherClient weatherClient; private final TransitClient transitClient; Tool(搜索指定城市的 POI。入参 city 必填preference 可选。 不要在 city 为空时调用不要用本工具查询天气或交通。) public ListPoiDto searchPoi( P(城市名) String city, P(偏好关键词如博物馆、美食、亲子) String preference) { return poiRepository.search(city, preference); } Tool(查询某城市未来三天的天气。只在用户明确提到出行日期时调用。) public ListWeatherDto getWeather( P(城市名) String city) { return weatherClient.forecast(city); } Tool(计算两个区域之间的交通耗时。用于判断两个景点是否适合排在同一天。) public TransitDto getTransit( P(出发区域) String from, P(到达区域) String to) { return transitClient.estimate(from, to); } Tool(校验行程预算是否超支。传入总预算和当前预估花费返回是否可行。) public BudgetCheckDto checkBudget( P(总预算) int totalBudget, P(当前预估花费) int currentCost) { return new BudgetCheckDto(currentCost totalBudget, currentCost - totalBudget); } }这套工具设计的核心是“单一职责”和“边界描述”。searchPoi 只搜 POIgetWeather 只查天气checkBudget 只做预算校验。如果让一个工具既搜 POI 又算交通模型很容易在传参时发懵。还有一个细节Tool 方法签名里每个参数都用 P 写明了中文含义Langchain4j 会把方法名、描述、参数描述拼进给模型的 function schema描述写得越清楚模型越不会传错参数。4.4 多路召回CompletableFuture 并行拉取四路数据单路串行地查 POI、天气、交通、预算最慢的那一路决定了整体延迟。我一般把四个召回源丢进一个固定大小的线程池并行执行等全部回来再合并。下面是多路召回的合并入口为了复用我把“并发发送请求”和“合并结果”分开了。Service public class MultiSourceRetriever { private final PoiRepository poiRepository; private final WeatherClient weatherClient; private final TransitClient transitClient; private final BudgetClient budgetClient; private final ExecutorService executor Executors.newFixedThreadPool(4); public CandidateBundle retrieve(TripRequest request) { CompletableFutureListPoiDto poiFuture CompletableFuture.supplyAsync( () - poiRepository.search(request.city(), request.preferences()), executor); CompletableFutureListWeatherDto weatherFuture CompletableFuture.supplyAsync( () - weatherClient.forecast(request.city()), executor); CompletableFutureListTransitDto transitFuture CompletableFuture.supplyAsync( () - transitClient.estimateByRegion(request.city()), executor); CompletableFutureBudgetDto budgetFuture CompletableFuture.supplyAsync( () - budgetClient.getBudget(request.city()), executor); CandidateBundle bundle CompletableFuture .allOf(poiFuture, weatherFuture, transitFuture, budgetFuture) .thenApply(v - new CandidateBundle( poiFuture.join(), weatherFuture.join(), transitFuture.join(), budgetFuture.join())) .join(); return reRank(bundle); } private CandidateBundle reRank(CandidateBundle bundle) { ListPoiDto ranked bundle.pois().stream() .sorted(Comparator.comparingDouble(PoiDto::rating).reversed()) .toList(); return new CandidateBundle(ranked, bundle.weather(), bundle.transits(), bundle.budget()); } }线程池大小设为 4 是有讲究的四个召回源各占一个线程既不互相阻塞也不会因为线程太多造成接口压力。如果你接了更多召回源按“接口数量”而不是“并发请求数”来定线程数。合并后的 reRank 目前只做了按评分降序它其实是一个扩展点如果用户偏好“美食”在排序时就可以给 category 为餐饮的 POI 加分这是后面调优最常动的地方。这里要用虚拟线程的团队Java 21 下把newFixedThreadPool(4)换成Executors.newVirtualThreadPerTaskExecutor()性能更好但要注意虚拟线程不适合池化的长连接操作。4.5 AiServices 组装把工具和记忆绑到智能体上召回是把信息拉回来编排还需要模型跑工具循环。Langchain4j 的 AiServices 把这一切串起来。下面的代码定义了一个 TripAssistant 接口通过 AiServices.builder 绑定模型、工具和记忆。public interface TripAssistant { String planTrip(MemoryId String sessionId, UserMessage String userInput); } Service public class TripPlannerService { private final ChatLanguageModel model; private final TripTools tools; private final MultiSourceRetriever retriever; public TripAssistant buildAssistant() { return AiServices.builder(TripAssistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.builder() .maxMessages(20) .build()) .tools(tools) .build(); } public String plan(String sessionId, String userInput) { TripAssistant assistant buildAssistant(); CandidateBundle bundle retriever.retrieve(extractRequest(userInput)); String context formatCandidate(bundle); return assistant.planTrip(sessionId, userInput \n参考数据:\n context); } }这段代码有三个关键参数。第一个是maxMessages(20)这是窗口大小20 条消息对“3 天 2 晚”这种多日行程足够又能把超过窗口的旧消息丢掉防止 token 涨个不停。第二个是tools(tools)把 TripTools 这个 Spring Bean 注入进去AiServices 启动时会扫描里面所有 Tool 方法注册给模型。第三个是MemoryId注解它把 sessionId 映射到 ChatMemory 的具体会话多个用户同时用也不会串上下文。把召回结果拼进用户消息里再传给模型而不是单独拼成 system prompt原因是系统提示词在很多实现里会被忽略或截断但用户消息一定参与生成召回结果放这里模型才会真正读到。4.6 结构化输出用 Spring AI 的 BeanOutputConverter 锁死行程单格式旅游行程规划最终要给前端渲染如果模型输出的是散文下游解析必炸。我用 Spring AI 2.0 的 BeanOutputConverter 来解决。原理是把目标 DTO 的类型描述塞进系统提示词让模型严格按 JSON 结构输出然后用 Jackson 反序列化成对象。public record DayPlan(String date, String region, ListString morning, ListString afternoon, int estimatedCost) {} public record TimetableOutput(ListDayPlan days, int totalCost) {} Service public class PlanExporter { private final ChatClient chatClient; private final BeanOutputConverterTimetableOutput converter new BeanOutputConverter(TimetableOutput.class); public TimetableOutput export(String tripContext) { String response chatClient.prompt() .system(converter.getInstruction()) .user(tripContext) .call() .content(); return converter.convert(response); } }converter.getInstruction()会生成一段“请严格返回以下 JSON Schema”的约束文本converter.convert(response)负责把模型输出映射成 TimetableOutput。关键点是让模型的输出结构对应到 DayPlan 里的 List 而不是复杂嵌套对象——嵌套越深模型输出的字段名越容易错。宁愿输出后在 Java 代码里做二次组装也别让模型直接产出一个十层嵌套的 JSON。5. 避坑工具扫描、中文召回、幻觉调用与上下文膨胀5.1 两个框架的工具扫描互相抢同一个 Tool 被注册两次现象Spring AI 和 Langchain4j 同时引入后一个 Tool 方法在调用日志里出现两次或者模型反复收到“该工具已存在”的错误。原因两个框架的自动配置都扫描了 Spring 容器里带工具注解的 Bean。Spring AI 认自己的 ToolLangchain4j 也认自己的注解。当你把一个类同时标记成两个框架的工具类或者让两个框架的组件扫描落在同一个包路径下就会发生重复注册。解决明确分工——工具类只标 Langchain4j 的 ToolSpring AI 侧不注册任何工具或者反过来让 Spring AI 负责工具调用Langchain4j 只用它的 ChatMemory。我实践下来最稳的组合是“Spring AI 当模型接入层不碰工具Langchain4j 当智能体运行时负责工具循环”这样注解归属没有交集。如果不想改代码还可以在 Spring Boot 的自动配置里排除掉其中一个框架的工具扫描器但排除项的类名随着版本变动很频繁不如从设计上规避。5.2 中文 POI 检索召回为空默认向量分词把中文拆得太碎现象用户输入“北京 美食 烤鸭”考核里 POI 库明明有“全聚德(前门店)”但召回结果为空或者只能召回“美食城”这种泛泛的结果。原因默认的 embedding 模型在中文短语上的切分确实不够细“北京美食烤鸭”可能被切成“北京/美食/烤鸭”但库里的“全聚德”三个字和“烤鸭”没有直接的字面重叠纯向量检索相关度上不去。这是纯向量召回在中文场景下的典型短板。解决多路召回里加一路关键词召回作为兜底。常见做法是在 POI 表上建一个 name/category/region 的复合索引先用数据库的 LIKE 或全文索引匹配“城市 偏好词”再把召回结果和向量召回结果合并。合并时的加权规则我一般这么设关键词命中的记录权重 0.3向量相似度高于阈值的记录权重 0.5两路都命中的权重叠加。这样“全聚德”即使向量分数一般也能被关键词一路捞回来。注意关键词召回和向量召回的结果去重要用 POI 的 id不能用名称。同名 POI 在不同区域很常见。5.3 模型幻觉调用工具三天的行程把天气查了八遍现象用户只要一个 3 天行程模型却在编排过程中反复调用 getWeather(“北京”)每次返回的数据还一样白白消耗 token整体响应时间拖到十几秒。原因工具描述里只写了“能查天气”没写“什么时候不用查”。模型一旦进入工具循环拿不准后续步骤时就会倾向于“再确认一次”把工具调用当成保险动作。解决在 Tool 描述里写禁止条件。我当时改成了这样“查询某城市未来三天的天气。只在用户首次提出出行日期时调用一次如果本轮对话中已经查询过不要重复调用。”同时在 AiServices 的 builder 上设置最大迭代次数Langchain4j 的 AiServices 可以通过maxIterations限制工具循环的轮数。我一般设 10超出就返回当前已生成的部分计划并提示用户“信息不完整但已按经验排了一个可用版本”。这个降级策略比无限循环好得多。5.4 上下文窗口被召回结果撑爆POI 详情全灌进了记忆现象对话到第三天用户问“早餐吃什么”模型的回答开始出现第一天 POI 列表里的字段错乱甚至回答变慢API 返回 400 提示 token 超限。原因每一轮对话都把完整的召回结果拼进用户消息而这些消息又通过 MemoryId 写进了 ChatMemory。三轮对话下来候选 POI 的完整描述、价格表、天气详情全在窗口里20 条消息窗口几乎是废的。解决两个手段配合。一是只把“候选清单的摘要”放进用户消息而不是原始记录。我给每个 POI 只保留“名称、区域、评分、一句话理由”比如“全聚德前门店前门区域评分 4.8适合晚餐”。二是工具结果不写入记忆。做法是在 AiServices 的 builder 上把工具返回结果配置成只保留摘要或者在工具方法内部就直接返回摘要对象。核心原则记忆里只留“结论”不留“过程”。5.5 模型输出的行程 JSON 字段对不上结构化输出也要给够上下文现象BeanOutputConverter.convert() 偶发抛 Jackson 解析异常或者 days 数组里的 morning 变成了字符串而不是字符串数组。原因模型在生成 JSON 时没有把 TimetableOutput 的类型说明和实际候选数据关联起来。当候选 POI 太多时模型会试图把整个列表塞进 text 字段而不是规规矩矩返回 id 列表。解决一是确认 DTO 字段名和给模型的 Schema 一致不要在 DTO 里用驼峰、在系统提示词里又写 snake_case二是把字段约束写进工具描述比如“morning 字段最多放两个 POI 名称必须是候选清单里真实存在的名称”三是给 converter 加 fallback解析失败时重试一次并降低温度到 0.1。这个 fallback 在阿里云百炼这类模型服务上效果很明显低温输出更稳定。6. 从工作流平台迁到 Java 代码一段行程编排的迁移思路很多人问我“Dify 里画着很顺为什么要迁到 Spring AI 和 Langchain4j”因为工作流平台擅长的是“固定节点编排”但旅游行程规划这种动态决策场景节点之间相互依赖条件分支动不动五六层在画布上维护的成本比写 Java 代码高得多。迁移不是逐节点复刻而是按“职责”重新划分。Dify 工作流节点迁到 Java 之后的落地位置开始节点用户输入TripRequest 参数抽取交给模型或规则解析知识库检索节点Langchain4j 的 ContentRetriever或者多路召回里的 POI 检索LLM 节点ChatLanguageModelSpring AI 的 ChatClient 就当接入层条件分支预算不足→换住宿Tool 方法里的 if/else模型触发工具后自行决策HTTP 请求节点Tool 封装的 WeatherClient / TransitClient变量赋值ChatMemory 里保存当前预算、城市、日期等关键上下文举个例子。Dify 里画一个“预算不足就换青旅”的条件分支需要加两个节点。在 Java 里我只是在 TripTools 里加了一个方法Tool(根据预算修正住宿建议。预算充足返回原方案预算不足返回更便宜的住宿并标注差价。) public AccommodationDto adjustAccommodation( P(城市) String city, P(当前酒店) String currentHotel, P(当前价格) int currentPrice, P(总预算剩余) int remainingBudget) { if (currentPrice remainingBudget) { return new AccommodationDto(currentHotel, currentPrice, OK); } return new AccommodationDto(city 青年旅舍, (int) (currentPrice * 0.4), 超支 (currentPrice - remainingBudget) 元已替换为更便宜选项); }模型在编排时发现“预估花费 剩余预算”就会主动调这个工具而不是在 Prompt 里等别人提醒它。这就是迁移的价值平台里的显式分支变成了模型可感知的“可选动作”整个智能体反而更灵活。我个人的习惯是先把一条 10 步的 Dify 工作流压成 3 个工具加一段系统提示跑通主干再根据错误日志拆细。因为智能体项目里链路每多一个节点模型犯错的面就多一圈。这份 zip 里最值得保存的也不是某个类而是这套“接口即工具、结论进记忆、输出走 Schema”的处理方式。希望你下次拿起它的时候也能少走几个坑一次就把行程规划跑顺。本文还有配套的精品资源点击获取