1. 项目概述当Java遇见声明式AI Agent如果你最近在捣鼓Java生态下的AI应用开发大概率已经绕不开LangChain4j这个框架了。而AiServices无疑是LangChain4j 0.30.0版本后最引人注目的特性之一它把“声明式编程”这个在Spring、MyBatis等框架里玩得炉火纯青的理念引入了AI Agent的开发领域。简单来说它让你能用写接口、加注解的方式就定义出一个能调用大语言模型、使用工具、拥有记忆的智能体而无需关心背后复杂的流程编排和状态管理。这听起来有点像魔法——你定义“做什么”框架负责“怎么做”。今天我们就来彻底拆解这个“魔法”背后究竟是如何运作的从设计思想到源码细节再到实战中的那些“坑”和最佳实践。对于Java开发者而言这意味着一场开发范式的转变。过去构建一个Agent你可能需要手动组装ChatMemory、ToolSpecification、ConversationalChain写一大堆模板代码。现在你只需要一个接口和几个注解。这种转变不仅提升了开发效率更重要的是降低了认知负担让开发者能更专注于业务逻辑和Prompt设计本身。无论是想快速构建一个智能客服接口还是一个能自动分析日志、执行运维命令的AI助手AiServices都提供了一条更优雅的路径。接下来我们将深入其核心看看这层“声明式”的糖衣之下LangChain4j为我们封装了怎样的复杂引擎。2. AiServices核心设计思想与架构拆解2.1 声明式编程范式的引入与价值声明式编程的核心思想是描述目标状态或意图而非具体执行步骤。在AiServices的语境下就是你通过一个Java接口声明性地描述你希望AI Agent具备的能力它能回答什么问题它能调用哪些工具它需要记住哪些对话历史框架在运行时会根据你的声明动态地生成一个代理对象这个对象的方法调用会被拦截并转化为对LangChain4j底层执行引擎的调用。这种模式带来了几个显著优势极简的API开发者体验大幅提升。定义一个智能体变得像定义Spring Bean一样简单。强类型安全接口方法定义了清晰的输入输出契约编译器能在早期发现类型错误避免了动态拼装字符串时容易出现的运行时错误。关注点分离业务逻辑接口定义与AI调用基础设施框架实现完全解耦。你可以像管理普通服务接口一样管理你的AI服务方便测试、维护和替换实现。易于组合与扩展通过注解可以方便地组合不同的能力如记忆、工具、流式响应等。未来框架增加新特性很可能只需增加新的注解即可。它的底层实现本质上是一个复杂的“方法拦截器”或“动态代理”。当你调用AiServices.create()方法时框架会使用字节码增强技术如CGLIB、ByteBuddy或JDK动态代理为你声明的接口生成一个代理实例。这个代理实例会拦截所有接口方法的调用并根据方法上的注解和签名构造出对应的UserMessage、选择合适的ChatModel、加载配置的Tools和ChatMemory最终发起一次或多次对AI模型的请求并将响应结果转换为你定义的返回类型。2.2 核心注解体系深度解析AiServices的魔法很大程度上依赖于一套精心设计的注解。理解每个注解的职责和生效时机是掌握其精髓的关键。SystemMessage这是最基础的Prompt工程注解。用于定义系统的角色、指令和约束条件。它会在每次对话交互中作为第一条消息发送给模型为整个对话设定基调和上下文。一个常见的误区是把它当成一次性的配置实际上它在多轮对话中每次都会发送除非使用ChatMemory并做了特殊处理。因此内容要精炼避免包含每次对话都会变化的动态信息。SystemMessage( 你是一个专业的Java代码审查助手。你的职责是以严谨、清晰的方式分析提供的代码片段指出潜在的性能问题、代码坏味道和安全漏洞并给出具体的改进建议。 请始终使用中文回复并以列表形式组织你的回答。 ) public interface CodeReviewAssistant { String reviewCode(String codeSnippet); }UserMessage这个注解用于标注方法参数中哪个或哪些将作为用户输入的主体。它非常灵活可以标注在String类型的参数上直接将其内容作为用户消息。可以标注在Template类型的参数上支持使用占位符如{{variable}}进行动态内容组装。如果方法只有一个String参数且未标注任何消息注解框架默认会将其视为UserMessage。MemoryId这是实现多租户或会话隔离的核心。它标注一个方法参数通常是String或UUID该参数的值将作为本次对话的“记忆标识符”。框架会根据这个ID从ChatMemory存储如内存、Redis中加载或创建对应的对话历史上下文。这意味着你可以用同一个AiService实例服务成千上万个独立的对话每个对话都有自己的记忆流。V这是一个用于Prompt模板变量替换的注解。当你的SystemMessage或UserMessage中包含类似{{name}}的占位符时你可以使用V(name)来标注方法参数框架会自动完成替换。这比在业务代码中手动拼接字符串要优雅和安全得多。工具集成相关注解虽然AiServices.builder()可以通过.tools()方法全局注册工具但更精细的控制可以通过Tool注解实现。你可以定义一个工具类然后在AiService接口中声明一个返回工具列表的方法并标注Tool框架会自动发现并注册这些工具。这允许你根据不同的AI服务动态装配不同的工具集。2.3 运行时流程与组件交互图一次典型的AiServices方法调用背后的流程可以概括为以下几步代理拦截你调用aiService.someMethod(arg)实际上调用的是动态生成的代理对象的方法。上下文构建代理处理器开始工作。它扫描方法注解和参数执行以下操作组装系统消息来自SystemMessage。组装用户消息来自UserMessage或默认参数并处理模板变量替换V。确定记忆ID来自MemoryId并从全局配置的ChatMemoryStore中获取或创建对应的ChatMemory对象。根据方法返回类型String、Response、Stream确定响应处理器。收集所有可用的工具全局注册的通过Tool方法提供的并生成工具描述。模型调用将构建好的UserMessage、系统提示、历史消息来自ChatMemory以及工具描述列表一并发送给配置的ChatModel如OpenAI GPT、Ollama本地模型等。响应处理与工具执行如果模型返回的是一个普通文本响应则直接进入步骤5。如果模型返回的是一个“工具调用”请求AiMessage中包含ToolExecutionRequest框架会 a. 解析出要调用的工具名和参数。 b. 从注册的工具集中找到对应的工具实例。 c.同步执行该工具方法获取执行结果。 d. 将工具执行结果作为一条新的ToolExecutionResultMessage追加到对话历史中。 e. 将整个更新后的消息历史再次发送给模型让模型基于工具执行结果生成最终回答。这个过程可能会循环多次直到模型返回一个文本响应。记忆更新与返回将模型返回的最终AiMessage添加到ChatMemory中实现记忆的持久化。最后将响应内容转换为方法声明的返回类型如直接返回文本或封装成Response对象返回给调用者。注意工具执行是同步阻塞的。如果你的工具执行一个耗时很长的操作如调用一个慢速外部API整个AI调用线程会被阻塞。在设计工具时务必考虑超时和异步化处理。3. 从零构建一个声明式AI Agent实战3.1 环境准备与基础依赖首先确保你的项目使用Maven或Gradle。这里以Maven为例你需要引入LangChain4j的核心依赖以及对应AI模型的依赖。由于我们要使用AiServices必须确保LangChain4j版本在0.30.0及以上。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.31.0/version !-- 使用最新稳定版 -- /dependency接下来根据你选择的模型添加相应依赖。如果你使用OpenAIdependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.31.0/version /dependency如果你使用本地部署的Ollama推荐用于开发和测试成本低dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama/artifactId version0.31.0/version /dependency对于记忆功能最简单的内存存储已经包含在核心包中。但如果需要持久化如Redis还需引入dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-store-redis/artifactId version0.31.0/version /dependency3.2 定义你的第一个AI服务接口让我们构建一个简单的“旅行规划助手”。它需要理解用户的目的地和偏好并能查询“天气”和“航班”信息通过工具。首先定义AI服务接口import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.V; import java.util.List; public interface TravelPlanningAssistant { SystemMessage( 你是一个专业的旅行规划专家。请根据用户的目的地和偏好提供详细的旅行建议包括行程概览、注意事项和必备物品提醒。 你可以调用工具来获取实时信息以完善你的建议。请始终以友好、热情的语气用中文回答。 ) String planTrip( UserMessage(我想去{{destination}}旅行我的偏好是{{preferences}}) String input, V(destination) String destination, V(preferences) String preferences, MemoryId String userId // 用userId来区分不同用户的对话记忆 ); }这个接口声明了一个planTrip方法。它使用了一个包含占位符{{destination}}和{{preferences}}的模板作为用户消息并通过V注解将方法参数绑定到这些占位符上。MemoryId注解标注了userId参数这意味着对于不同的用户ID对话历史将是独立隔离的。3.3 实现与注册自定义工具AI Agent的强大之处在于能使用工具。我们来定义两个工具WeatherTool和FlightTool。在LangChain4j中一个工具就是一个普通的Java方法使用Tool注解进行标注。首先创建一个工具类TravelToolsimport dev.langchain4j.agent.tool.Tool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.LocalDate; public class TravelTools { private static final Logger log LoggerFactory.getLogger(TravelTools.class); Tool(根据给定的城市名称查询未来三天的天气预报) public String getWeatherForecast(String cityName) { log.info([工具调用] 查询 {} 的天气, cityName); // 这里应该是调用真实天气API如OpenWeatherMap // 为了演示我们返回模拟数据 return String.format(%s未来三天天气第一天晴15-25°C第二天多云16-24°C第三天小雨14-22°C。建议携带雨具。, cityName); } Tool(查询从出发城市到目的地城市在指定日期附近的航班信息) public String searchFlights(String fromCity, String toCity, LocalDate aroundDate) { log.info([工具调用] 查询从 {} 到 {} 在 {} 附近的航班, fromCity, toCity, aroundDate); // 模拟调用航班搜索API return String.format(找到从%s到%s在%s前后的航班航班A09:00-11:30价格1200元航班B14:00-16:45价格950元。, fromCity, toCity, aroundDate); } }Tool注解内的字符串描述至关重要大语言模型依靠这个描述来决定是否以及如何调用该工具。描述应清晰说明工具的功能和参数含义。然后我们需要在创建AiService时注册这些工具。有两种方式全局注册在AiServices.builder()中通过.tools()方法注册。通过接口方法动态提供在AI服务接口中定义一个返回工具集合的方法。这里演示第一种更直接的方式import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.service.AiServices; public class TravelAgentDemo { public static void main(String[] args) { // 1. 创建模型实例 (这里使用本地Ollama运行llama3.1模型) ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(llama3.1) .temperature(0.7) .build(); // 2. 创建工具实例 TravelTools travelTools new TravelTools(); // 3. 创建AI服务实例并注册模型和工具 TravelPlanningAssistant assistant AiServices.builder(TravelPlanningAssistant.class) .chatLanguageModel(model) .tools(travelTools) // 注册工具 .chatMemoryProvider(memoryId - MessageWindowChatMemory.withMaxMessages(10)) // 为每个memoryId提供独立的记忆窗口 .build(); // 4. 使用服务 String userId user_001; String response assistant.planTrip(我想去北京旅行我的偏好是喜欢历史文化古迹预算中等旅行5天, 北京, 喜欢历史文化古迹预算中等旅行5天, userId); System.out.println(助理回复\n response); } }运行这段代码模型在生成建议时如果认为需要天气或航班信息就会自动调用我们注册的工具并将工具返回的结果融入最终的回复中。通过日志你可以清晰地看到工具被调用的过程。3.4 配置记忆与多轮对话实现上面的例子已经通过.chatMemoryProvider()配置了记忆。MessageWindowChatMemory.withMaxMessages(10)创建了一个最多保存10条最新消息的滑动窗口记忆。每次调用planTrip时传入相同的userId模型就能看到之前的对话历史。让我们实现一个更连续的多轮对话场景public class MultiTurnChatDemo { public static void main(String[] args) { ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(llama3.1) .build(); // 定义一个简单的聊天接口这次我们让用户消息直接来自参数 interface ChatBot { SystemMessage(你是一个有帮助的助手。) String chat(MemoryId String sessionId, UserMessage String userMessage); } ChatBot bot AiServices.builder(ChatBot.class) .chatLanguageModel(model) .chatMemoryProvider(id - MessageWindowChatMemory.withMaxMessages(20)) .build(); String sessionId test_session; System.out.println(用户: 你好); System.out.println(AI: bot.chat(sessionId, 你好)); System.out.println(\n用户: 我叫小明); System.out.println(AI: bot.chat(sessionId, 我叫小明)); System.out.println(\n用户: 你还记得我的名字吗); // 由于记忆存在AI应该能回答出名字 System.out.println(AI: bot.chat(sessionId, 你还记得我的名字吗)); } }在这个例子中第二次和第三次调用都使用了相同的sessionId。第三次提问时AI模型收到的上下文里包含了前两轮对话的历史消息因此它能够回答“你叫小明”。这就是MemoryId和ChatMemoryProvider共同作用的结果轻松实现了带状态的会话。4. 高级特性与性能调优指南4.1 流式响应处理对于需要长时间生成内容或希望实现打字机效果的应用流式响应是必备功能。AiServices完美支持这一点。只需将接口方法的返回类型定义为StreamString或ResponseStreamString即可。import java.util.stream.Stream; public interface StreamingAssistant { SystemMessage(你是一个讲故事的高手。) StreamString tellStoryAbout(String topic); } // 使用 StreamingAssistant assistant AiServices.create(StreamingAssistant.class, model); StreamString stream assistant.tellStoryAbout(一只会编程的猫); stream.forEach(chunk - { System.out.print(chunk); // 逐块打印实现流式效果 System.out.flush(); });底层上框架会使用模型提供的流式API如OpenAI的SSE并将返回的token流逐个推送给Stream。这对于构建实时交互的聊天前端至关重要。4.2 结构化输出与复杂类型返回除了返回纯文本AiServices还能自动将模型的响应解析成复杂的Java对象POJO。这极大地简化了后续的数据处理。import lombok.Data; // 使用Lombok简化代码 Data // 自动生成getter, setter class TravelPlan { private String destination; private ListString itinerary; // 行程列表 private ListString packingList; // 行李清单 private Double estimatedBudget; } public interface StructuredTravelPlanner { SystemMessage(根据用户输入生成一个结构化的旅行计划。) TravelPlan generateStructuredPlan(UserMessage String userRequest); } // 使用 StructuredTravelPlanner planner AiServices.create(StructuredTravelPlanner.class, model); TravelPlan plan planner.generateStructuredPlan(我想去东京进行一场5天的美食之旅预算在1万元左右。); System.out.println(目的地 plan.getDestination()); System.out.println(行程 plan.getItinerary());为了实现这一点框架在内部做了两件事在发送给模型的系统指令中会追加要求模型以特定JSON格式输出的指令。收到模型响应后使用JSON解析器如Jackson将文本反序列化成你指定的TravelPlan对象。实操心得模型并不总是严格遵守JSON格式。为了增加鲁棒性建议在Prompt中明确要求并考虑在接口方法上使用Default注解提供一个备用的解析策略或者使用Response包装器其中包含原始的文本响应以供备用处理。4.3 错误处理与重试机制网络调用和AI模型本身具有不确定性错误处理是生产级应用必须考虑的。AiServices本身不提供复杂的重试机制但你可以通过配置底层ChatModel或使用外部库来实现。1. 配置模型客户端的超时和重试 以OpenAI客户端为例你可以通过自定义的HTTP客户端来设置策略。import dev.langchain4j.model.openai.OpenAiChatModel; import okhttp3.OkHttpClient; import java.time.Duration; OkHttpClient httpClient new OkHttpClient.Builder() .connectTimeout(Duration.ofSeconds(30)) .readTimeout(Duration.ofSeconds(60)) .writeTimeout(Duration.ofSeconds(30)) .addInterceptor(new RetryInterceptor(3)) // 自定义重试拦截器 .build(); OpenAiChatModel model OpenAiChatModel.builder() .apiKey(your-key) .modelName(gpt-4) .httpClient(httpClient) .build();2. 在服务层进行降级和兜底 一种更通用的模式是使用Spring AOP或简单的代理模式在AiService外层包裹一个带有重试和降级逻辑的代理。public class ResilientTravelAssistant implements TravelPlanningAssistant { private final TravelPlanningAssistant delegate; private final RetryTemplate retryTemplate; // 例如使用Spring Retry public ResilientTravelAssistant(TravelPlanningAssistant delegate) { this.delegate delegate; this.retryTemplate RetryTemplate.builder() .maxAttempts(3) .exponentialBackoff(1000, 2, 5000) .retryOn(IOException.class) .build(); } Override public String planTrip(String input, String destination, String preferences, String userId) { return retryTemplate.execute(context - delegate.planTrip(input, destination, preferences, userId)); } }3. 处理模型特有的错误 如OpenAI的速率限制429错误、上下文长度超限等。这些错误通常包含在模型返回的异常信息中需要在调用处进行捕获并做相应处理如等待后重试、提示用户缩短输入等。4.4 性能优化关键点工具设计的异步化如前所述工具执行是同步的。如果一个工具需要调用慢速的外部HTTP服务或执行复杂计算会阻塞整个AI调用线程。解决方案是将工具本身设计为异步非阻塞或者使用CompletableFuture包装但需要注意AiServices目前对异步工具返回类型的支持情况。更稳妥的做法是在工具内部使用缓存或者确保外部服务调用有合理的超时设置。记忆存储的选型与优化默认的InMemoryChatMemoryStore仅适用于单实例或测试。生产环境必须使用外部存储如Redis。要关注序列化/反序列化的开销以及存储结构的设计。对于高频对话可以考虑只存储消息的摘要或使用更紧凑的格式。Prompt模板的预编译如果你的SystemMessage或UserMessage是固定的模板框架在每次调用时都会进行解析。对于超高并发场景可以考虑在服务初始化时手动预编译PromptTemplate并在接口方法中直接传入预编译好的模板对象以减少运行时开销。模型的批量与缓存对于内容生成类且对实时性要求不高的场景如批量生成产品描述可以考虑将多个请求合并为一个批量Prompt发送给模型如果模型API支持或者对相同输入的生成结果进行缓存。监控与度量务必为AI调用、工具调用添加详细的日志和指标如耗时、token使用量、工具调用次数。这有助于发现性能瓶颈和进行成本分析。可以使用Micrometer等工具集成到你的监控体系中。5. 常见问题排查与实战避坑指南在实际开发中你肯定会遇到各种问题。下面是一些典型问题及其解决方案。5.1 工具未被调用或调用错误问题现象你明明注册了工具但AI模型在回答时似乎完全忽略了工具或者调用了错误的工具/参数。排查步骤检查工具描述首先检查Tool注解中的描述是否清晰、准确。模型完全依赖这个描述来理解工具功能。描述应像写给另一个开发者的API文档一样清晰。可以尝试让同事阅读描述看是否能准确猜出工具的作用和参数。启用详细日志在创建ChatModel时开启logRequests()和logResponses()。这会打印出实际发送给模型的请求体和收到的响应体是调试的黄金标准。OpenAiChatModel model OpenAiChatModel.builder() .apiKey(key) .logRequests(true) .logResponses(true) .build();查看日志中tools字段是否被正确发送以及模型的响应中是否包含tool_calls。检查模型能力确保你使用的模型支持“函数调用”或“工具调用”功能。并非所有模型都支持。GPT-3.5-turbo及以上、Claude系列、DeepSeek等主流模型都支持。简化测试创建一个最简单的工具如返回当前时间的工具和一个最简单的Prompt如“请使用工具获取当前时间”排除业务逻辑的干扰。避坑技巧描述要具体避免模糊描述。“获取信息”是糟糕的描述“根据城市名称查询该城市当前天气状况和温度”是好描述。参数名要直观工具方法的参数名也会被模型看到使用cityName比arg1好得多。提供示例Few-Shot如果工具使用复杂可以在系统消息中提供一两个工具调用和响应的示例引导模型学习。5.2 记忆不生效或混乱问题现象对话似乎没有历史或者不同用户的记忆串了。排查步骤确认MemoryId确保你的接口方法中有一个参数被MemoryId注解标注并且每次相关对话都传入了相同的值。检查ChatMemoryProvider确保在AiServices.builder()中正确配置了.chatMemoryProvider()。如果你没有配置那么记忆功能不会启用。检查存储后端如果使用Redis等外部存储检查连接是否正常序列化是否正确。尝试直接通过Redis客户端查看对应key下的数据。记忆窗口大小检查MessageWindowChatMemory.withMaxMessages()设置的大小。如果设置得太小比如1历史消息很快就会被丢弃。避坑技巧明确记忆作用域想清楚你的MemoryId是什么。是用户ID会话ID还是某个工单ID这决定了记忆的隔离粒度。定期清理对于内存存储要防范内存泄漏。对于Redis存储可以为记忆Key设置TTL生存时间让过期对话自动清理。谨慎使用全局记忆避免使用一个固定的MemoryId如“global”这会导致所有用户的对话都混在一起除非这是你特意想要的效果如一个公共知识库。5.3 复杂对象返回解析失败问题现象方法定义返回一个TravelPlan对象但调用时抛出JsonProcessingException或返回null。排查步骤查看原始响应开启模型请求/响应日志查看模型返回的原始文本。模型是否真的输出了合规的JSON很多时候模型会在JSON前后加上解释性文字。简化POJO先将返回类型改为最简单的String看看模型返回什么。然后逐步增加POJO的字段确保模型能理解并生成对应结构。强化Prompt指令在SystemMessage中非常明确地要求模型输出格式。例如“你必须将你的输出严格遵循以下JSON格式不要有任何额外的解释或标记{destination: ..., itinerary: [...]}”。使用ResponseT包装器将方法返回类型改为ResponseTravelPlan。这样即使解析失败你也能通过Response对象拿到原始的文本响应(response.content())进行手动处理或降级。避坑技巧为POJO字段提供描述使用Jackson注解JsonPropertyDescription为字段添加描述这有助于模型理解每个字段应该填什么内容。Data class TravelPlan { JsonPropertyDescription(旅行的目的地城市) private String destination; // ... }准备默认值或空对象在调用方代码中做好解析失败的异常处理准备一个默认的或部分为空的对象返回保证服务不崩溃。5.4 并发环境下的线程安全问题问题现象在多线程环境下使用AiService实例出现状态错乱或奇怪的错误。根源分析AiServices创建的代理实例本身通常是线程安全的无状态。但工具类和记忆存储可能是共享的需要特别注意。工具类如果你的工具类中包含了可变的成员变量如计数器、缓存Map并且没有做同步控制那么在并发调用时就会出问题。记忆存储InMemoryChatMemoryStore内部使用ConcurrentHashMap基本是线程安全的。但自定义的存储实现需要自己保证线程安全。解决方案工具类设计为无状态或线程安全最佳实践是将工具类设计为无状态的只有Tool方法没有成员变量。如果必须有状态如连接池、缓存使用线程安全的容器如ConcurrentHashMap或加锁。使用ThreadLocal或为每次调用创建新实例对于非线程安全的工具可以考虑在.tools()注册时传入一个工具供应商Supplier每次调用都创建一个新实例。但这会增加开销需权衡。.tools(() - new NonThreadSafeTool())仔细选择记忆存储生产环境务必使用如Redis这样支持并发访问的外部存储并利用其原子操作特性。5.5 提示词Prompt设计不佳导致效果差问题现象AI的回答偏离预期不遵循指令或者质量不稳定。优化策略角色扮演要具体不要只说“你是一个助手”。要说“你是一个精通Java和系统架构的资深技术专家擅长用简洁清晰的例子解释复杂概念”。指令要清晰、结构化使用编号、分点来列出你的要求。例如“请按以下步骤分析1. ... 2. ... 3. ...”。提供输出格式示例对于结构化输出在Prompt里直接给一个完整的例子。这比单纯描述格式有效得多。使用“负面提示”明确告诉模型不要做什么。例如“不要使用Markdown格式。”“不要对用户的问题进行评价。”迭代和测试Prompt工程是一个实验性过程。准备一组标准测试用例每次修改Prompt后都跑一遍对比效果。可以将不同的Prompt版本管理起来。一个优化前后的例子优化前SystemMessage(帮我分析代码。)优化后SystemMessage( 你是一个专注于Java代码质量和性能的审查专家。你的任务是严格审查用户提供的代码片段。 请按以下结构用中文回复 ## 1. 潜在缺陷 列出可能的空指针、资源未关闭、并发问题等 ## 2. 代码坏味道 列出过长方法、重复代码、魔法数字等 ## 3. 性能优化点 列出低效的算法、不必要的对象创建等 ## 4. 改进建议 针对以上每一点提供具体的代码改进建议 注意只分析代码本身不要假设其运行上下文。如果代码没有问题请说明“未发现明显问题”。 )通过以上五个部分的深度拆解我们从设计理念、基础实战、高级特性到疑难排查完整地透视了LangChain4jAiServices这一声明式Agent编程利器。它的出现确实让Java开发者构建AI应用的门槛降低了一个数量级。但正如我们所看到的魔法背后依然是扎实的软件工程原理清晰的抽象、合理的配置、对底层机制的理解以及对生产环境问题的周全考虑。掌握这些你才能不仅会用这个“魔法”更能驾驭它构建出真正稳定、高效、智能的Java AI应用。