LangChain4j AiServices:Java AI应用开发的声明式编程实践
1. 从“胶水代码”到“声明式魔法”AiServices 解决了什么痛点如果你在过去一年里尝试过用 Java 集成大语言模型LLM来构建应用大概率经历过这样的场景为了调用一个简单的问答接口你需要写几十行甚至上百行的“胶水代码”。先初始化一个ChatLanguageModel然后手动构建SystemMessage和UserMessage接着调用generate方法最后还得从AiMessage里把文本内容提取出来并处理可能出现的各种异常。这还没完如果你想实现一个简单的工具调用Tool Calling或者让模型根据结构化数据如 JSON来回答代码的复杂度会呈指数级上升各种if-else、类型转换和错误处理逻辑交织在一起代码很快就变得难以维护。这就是传统“命令式”编程方式在 AI 应用开发中的典型困境。开发者需要花费大量精力去处理通信协议、消息组装、结果解析和错误处理等底层细节而不是专注于业务逻辑本身。LangChain4j的AiServices的出现正是为了解决这个核心痛点。它引入了一种“声明式”的编程范式让你能够像定义 Spring 的Service接口一样通过简单的 Java 接口来描述你希望 AI 完成的任务而所有的实现细节——包括与模型的对话、工具的调用、结果的解析——都由框架在背后自动完成。这种转变带来的直接好处是开发效率的飞跃和代码可读性的质变。你不再需要关心Message对象是如何在UserMessage、AiMessage、ToolExecutionResultMessage之间流转的也不需要手动拼接复杂的PromptTemplate。你只需要告诉框架“我有一个接口里面有个方法请用这个模型结合这些工具来回答用户的问题。” 剩下的AiServices会用它的“魔法”帮你搞定。这听起来有点过于美好以至于让人不禁想问这魔法背后到底是怎么运作的它真的能处理所有复杂场景吗会不会有性能开销或者隐藏的坑这篇文章我就结合自己从早期版本一路踩坑过来的实战经验为你深度拆解AiServices的内部机制、最佳实践以及那些官方文档里没写的细节。2. AiServices 核心架构注解驱动的执行引擎要理解AiServices的魔法首先得拆开它的引擎盖看看。整个体系的核心是一个基于动态代理和注解处理的执行引擎。当你调用AiServices.builder()创建一个服务实例时框架并不是在编译期生成实现类而是在运行时通过 Java 的动态代理机制为你定义的接口创建一个代理对象。这个代理对象会拦截所有接口方法的调用并将其路由到一套复杂的、可插拔的处理器链中。2.1 动态代理与执行链的初始化我们从一个最简单的例子开始看看AiServices的骨架是如何搭建的。interface Assistant { String chat(String userMessage); } public class Main { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(demo) .modelName(gpt-3.5-turbo) .build(); Assistant assistant AiServices.create(Assistant.class, model); String answer assistant.chat(你好世界); System.out.println(answer); } }当你执行AiServices.create(Assistant.class, model)时背后发生了以下几件关键事情接口扫描与元数据提取AiServices会使用反射扫描传入的接口这里是Assistant分析其所有方法、参数、返回类型以及方法上的注解如SystemMessage,UserMessage,MemoryId等。这些信息被封装成MethodSpec之类的元数据对象构成了后续执行的“蓝图”。执行器AiServiceExecutor构建这是真正的核心。框架会根据扫描到的元数据以及你提供的ChatLanguageModel、ToolCatalog工具目录、ChatMemory记忆等组件组装一个针对该接口的专用执行器。这个执行器内部维护着一个处理器链Chain of Responsibility模式。代理对象生成最后利用java.lang.reflect.Proxy生成一个实现了Assistant接口的代理对象。当你调用assistant.chat(...)时调用会被代理对象拦截并转发给上一步构建好的AiServiceExecutor来实际处理。这个设计非常巧妙它将稳定的接口定义你的业务契约与多变的实现逻辑AI模型、工具、记忆策略解耦。你可以轻松替换底层的模型从 OpenAI 换到 Anthropic 或本地模型或者增删工具而业务接口代码一行都不用改。2.2 注解系统声明意图的“咒语”注解是AiServices声明式编程的灵魂。它们像是一句句简短的“咒语”告诉框架在方法执行前后应该做什么。理解每个注解的精确语义和生效时机至关重要。SystemMessage: 用于提供系统指令或角色设定。它的内容会在每一次对话交互中作为第一条消息发送给模型。这意味着如果你在方法A中设置了SystemMessage在同一个AiService实例内调用方法B时这个系统消息依然有效。它被存储在对话的上下文记忆中。一个常见的误区是认为它只对当前方法生效。SystemMessage(你是一个专业的Java代码审查助手专注于发现潜在的性能问题和代码坏味道。) String reviewCode(String codeSnippet);UserMessage: 用于标注哪个参数或方法本身提供了用户输入。这是最常用的注解。你可以将其用在方法参数上也可以用在方法本身上通过模板。关键点在于一个方法只能有一个“用户消息”来源。如果方法有多个String参数你必须明确指定其中一个为UserMessage否则框架会报错。// 方式1注解在参数上 String answerQuestion(UserMessage String question, V(context) String providedContext); // 方式2注解在方法上使用模板推荐更灵活 UserMessage(请根据以下上下文{{context}} 回答这个问题{{question}}) String answerQuestion(String question, String context);模板语法{{variableName}}允许你动态拼接消息这是构建复杂提示词的基础。MemoryId: 这是实现多轮对话和会话隔离的关键。MemoryId标注的参数值会被用作ChatMemory的标识符。相同memoryId的调用会共享同一个对话历史上下文。这里有个大坑如果你没有提供ChatMemory实例比如只传了model给AiServices那么即使使用了MemoryId对话也不会被持久化每次调用都是独立的。你必须显式配置一个ChatMemory比如MessageWindowChatMemory。interface ChatBot { String chat(MemoryId String sessionId, UserMessage String message); } // 使用时必须配置 ChatMemory ChatMemory memory MessageWindowChatMemory.withMaxMessages(10); ChatBot bot AiServices.builder(ChatBot.class) .chatLanguageModel(model) .chatMemory(provider - memory) // 关键提供 ChatMemory .build(); // 现在相同 sessionId 的对话会有历史记忆了。Tool注解在工具方法上: 这不是用在AiService接口上的而是用在你想暴露给 AI 的工具类方法上。Tool注解的description属性至关重要AI 模型主要依靠这个描述来决定是否以及如何调用你的工具。描述要清晰、具体说明工具的用途、输入参数的意义和输出是什么。public class Calculator { Tool(计算两个浮点数的和。输入a 是第一个加数b 是第二个加数。) public double add(double a, double b) { return a b; } }2.3. 执行流程一次方法调用的奇幻之旅当你调用一个被代理的接口方法时一次完整的“声明式 Agent”执行就开始了。这个过程可以粗略分为以下几个阶段我将其称为“请求生命周期”参数绑定与上下文准备执行器首先解析方法参数。根据注解将参数值绑定到不同的上下文中。例如MemoryId的值被提取出来用于查找或创建ChatMemoryUserMessage模板中的变量被实际参数值替换生成最终的用户消息文本其他参数可能会被放入一个“变量映射表”VariableMap中供后续的提示词模板或工具使用。历史记忆加载如果配置了ChatMemory并且本次调用提供了MemoryId执行器会从记忆存储中加载当前会话的所有历史消息ListChatMessage。这些消息将成为本次对话的“上下文”。消息列表ListChatMessage组装这是核心步骤。执行器会按照正确的时序组装一个消息列表首先加入来自SystemMessage的SystemMessage如果存在且是对话中的第一条系统消息。然后按顺序加入从ChatMemory中加载的历史消息包括之前的用户消息、AI回复、工具执行结果等。最后加入本次调用生成的UserMessage。 这个列表完整地定义了本次对话的“状态”。模型交互与工具调用循环执行器将这个消息列表发送给ChatLanguageModel。模型可能直接返回一个文本回答AiMessage也可能返回一个请求调用工具的指令AiMessage中包含ToolExecutionRequest。如果模型请求调用工具执行器会 a. 从已注册的ToolCatalog中找到对应的工具。 b. 使用模型提供的参数通常是 JSON调用该工具的实际 Java 方法。 c. 将工具执行的结果成功或异常封装成一个ToolExecutionResultMessage。 d. 将这个结果消息追加到当前的消息列表末尾。 e. 将整个更新后的消息列表再次发送给模型让模型基于工具执行结果进行下一步思考或回答。 这个“模型 - 工具 - 模型”的循环可能会进行多次直到模型返回一个不包含工具调用请求的最终文本回答。这就是 ReActReasoning and Acting模式在AiServices中的实现。响应处理与记忆持久化拿到模型的最终文本回答后执行器需要将其转换成接口方法声明的返回类型。如果是简单的String直接返回即可。如果是复杂的POJO框架会尝试让模型以 JSON 格式输出并利用 Jackson 或 Gson 等 JSON 库自动反序列化。最后本次交互中产生的所有新消息用户消息、工具执行消息、AI的最终回答都会被保存到ChatMemory中供下一次调用使用。这个过程看似复杂但得益于框架的良好封装开发者几乎感知不到。你只需要关注接口定义和工具实现这就是声明式编程的魅力。3. 超越 Hello World复杂场景下的实战与配置理解了基本原理后我们来看看如何用AiServices处理更真实的复杂场景。这些往往是官方 Quick Start 里一笔带过但实际开发中一定会遇到的坎。3.1. 结构化输出从 JSON 到 POJO 的自动映射让 AI 输出结构化的数据而不仅仅是一段文本是构建严肃应用的基础。例如你需要模型分析一段用户反馈并自动分类、提取情感和关键实体。AiServices对此提供了优雅的支持。核心机制当你的接口方法返回类型不是一个简单的String而是一个自定义的类POJO时框架会在内部提示词中隐式地加入指令要求模型以 JSON 格式输出并且这个 JSON 的结构必须与你定义的 POJO 类匹配。然后框架使用内置的JsonCodec默认通常基于 Jackson将返回的 JSON 字符串反序列化成 POJO 对象。// 1. 定义你的数据结构 class FeedbackAnalysis { Description(反馈的主要类别如功能请求、Bug报告、用户体验、咨询) private String category; Description(情感倾向取值为积极、消极、中性) private String sentiment; Description(从反馈中提取的关键词或实体列表) private ListString keywords; // ... getters and setters } // 2. 在 AiService 接口中使用它 interface FeedbackAnalyzer { UserMessage(请分析以下用户反馈{{feedback}}) FeedbackAnalysis analyze(V(feedback) String userFeedback); } // 3. 像调用普通方法一样使用 FeedbackAnalyzer analyzer AiServices.create(FeedbackAnalyzer.class, model); FeedbackAnalysis result analyzer.analyze(这个新按钮的位置太隐蔽了我找了半天都没找到建议放到更显眼的地方。); System.out.println(result.getCategory()); // 可能输出用户体验 System.out.println(result.getSentiment()); // 可能输出消极 System.out.println(result.getKeywords()); // 可能输出[按钮位置, 隐蔽, 建议]实战避坑指南字段描述很重要像上面例子一样使用Description注解为 POJO 的字段添加描述。这会被框架加入到给模型的指令中极大地提高模型输出 JSON 字段的准确性和一致性。没有描述模型可能误解字段含义。处理模型“不听话”的情况即使有指令模型偶尔也可能在 JSON 外包裹一些解释性文字如“好的分析结果如下”。LangChain4j的默认JsonCodec通常具备一定的容错能力会尝试从响应文本中提取 JSON 块。但如果失败你会收到JsonParsingException。一个增强鲁棒性的技巧是使用OutputParser进行后处理。列表和嵌套对象框架完全支持ListYourPojo或嵌套对象的返回类型。确保你的 POJO 结构是 Jackson 友好的有无参构造函数、标准的 getter/setter。3.2. 复杂工具编排让 AI 学会使用你的系统工具调用是 Agent 能力的核心。AiServices使得为 AI 装备工具变得非常简单。// 1. 定义你的工具类 public class DatabaseTools { Tool(根据用户ID查询用户的订单列表。userId 是用户的唯一标识字符串。) public ListOrder queryUserOrders(P(用户ID) String userId) { // 模拟数据库查询 return mockOrderService.findByUserId(userId); } Tool(根据订单号获取订单的详细信息包括商品列表和状态。orderId 是订单号字符串。) public OrderDetail getOrderDetail(P(订单号) String orderId) { return mockOrderService.getDetail(orderId); } } public class ExternalServiceTools { Tool(发送邮件通知。to 是收件人邮箱subject 是邮件主题content 是邮件正文。) public boolean sendEmail(String to, String subject, String content) { // 调用邮件发送服务 return emailClient.send(to, subject, content); } } // 2. 在构建 AiService 时注册这些工具 public class Main { public static void main(String[] args) { ChatLanguageModel model ...; DatabaseTools dbTools new DatabaseTools(); ExternalServiceTools esTools new ExternalServiceTools(); interface CustomerServiceAgent { String handleInquiry(UserMessage String inquiry, MemoryId String customerId); } CustomerServiceAgent agent AiServices.builder(CustomerServiceAgent.class) .chatLanguageModel(model) .tools(dbTools, esTools) // 一次性注册所有工具实例 .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); // 现在 AI 可以自动决定何时查询订单、何时发送邮件了。 String response agent.handleInquiry(帮我查一下我最近的订单状态如果有发货的请邮件通知我。, customer-123); System.out.println(response); } }工具使用的深层机制与调优工具描述是“使用说明书”模型完全依赖Tool注解中的description和P注解用于参数来理解工具的用途。描述要尽可能精确、无歧义。例如“查询数据”就是一个糟糕的描述“根据用户ID从订单表中查询最近30天内未发货的订单”就好得多。工具的选择与冲突当注册的工具很多时模型可能会选错工具。这通常是因为工具描述相似或不够具体。解决方法是细化描述或者在极端情况下考虑对工具进行分层或分类注册。工具执行的安全性这是一个极其重要的考量。你暴露给 AI 的工具相当于给了它操作你系统的 API 密钥。必须实施严格的权限控制。例如上面的queryUserOrders工具在实际实现中必须校验传入的userId是否与当前会话的授权用户匹配防止越权访问。永远不要相信模型传来的参数是安全的。处理工具执行异常如果工具执行时抛出异常这个异常信息会被包装成ToolExecutionResultMessage返回给模型。模型有时能理解异常并调整策略但有时会导致对话失败。建议在工具内部做好健壮性处理对于可预见的错误如“订单不存在”返回明确的错误信息对象而不是抛出异常。3.3. 记忆管理对话状态的持久化与隔离没有记忆的 Agent 就像金鱼只有7秒的“智商”。ChatMemory是AiServices实现多轮对话的基石。MessageWindowChatMemory: 最常用的内存实现。它像一个滑动窗口只保留最近 N 条消息。这可以有效防止上下文过长导致模型 Token 超限或性能下降以及成本无限增长。withMaxMessages(10)是一个不错的起始配置。ChatMemory memory MessageWindowChatMemory.withMaxMessages(10);PersistentChatMemory: 用于需要跨应用重启保持记忆的场景。它需要一个底层的ChatMemoryStore如基于 Redis、数据库的实现来持久化消息。LangChain4j提供了一些内置实现你也可以自己实现。// 示例使用 InMemoryChatMemoryStore (仅用于演示重启后数据丢失) ChatMemoryStore store new InMemoryChatMemoryStore(); ChatMemory memory PersistentChatMemory.builder() .id(unique-session-id) // 这个ID需要你自己管理和传递 .chatMemoryStore(store) .maxMessages(10) .build();记忆的键MemoryIdMemoryId参数的值就是查找ChatMemory的键。对于 Web 应用这通常是用户的 Session ID 或 User ID。你必须确保同一个会话使用相同的 ID不同的会话使用不同的 ID以实现对话隔离。一个常见的陷阱内存泄漏。如果你为每个请求都创建一个新的MessageWindowChatMemory实例并且没有用MemoryId进行复用那么这些内存对象会一直留在堆中直到 GC 回收。在生产环境中务必使用某种形式的缓存如ConcurrentHashMap或 Spring 的Scope(“session”)来管理ChatMemory实例的生命周期。3.4. 高级配置温度、重试与超时AiServices.builder()提供了丰富的配置项让你能精细控制 Agent 的行为。Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel( OpenAiChatModel.builder() .apiKey(apiKey) .modelName(gpt-4) .temperature(0.7) // 控制创造性。0.0更确定1.0更多变。 .topP(0.9) .maxTokens(1000) .timeout(Duration.ofSeconds(30)) // 网络超时 .build() ) .tools(myTools) .chatMemory(memory) .retrySpec(RetrySpec.fixed(3).delay(Duration.ofMillis(500))) // 配置重试 .maxRetries(2) // 工具调用失败的重试次数 .build();temperature和topP这两个参数直接影响模型的输出。对于需要确定性输出的任务如代码生成、数据提取建议设置较低的temperature如 0.1-0.3。对于创意性任务如写作、头脑风暴可以调高如 0.7-0.9。topP核采样是另一种控制随机性的方法通常与temperature配合使用或二选一。重试机制retrySpec主要用于处理网络抖动等暂时性故障。maxRetries则针对工具调用失败。注意重试会增加延迟和潜在的成本对于按Token收费的模型。超时设置务必为模型客户端设置合理的超时防止因网络或模型服务端问题导致线程长时间阻塞。4. 生产环境下的性能、监控与调试将基于AiServices的 Agent 部署到生产环境除了功能正确还需要考虑性能、可靠性和可观测性。4.1. 性能考量与优化点上下文长度与 Token 消耗这是最大的成本和性能影响因素。每次调用发送给模型的“消息列表”都会被计算 Token 数。ChatMemory中积累的历史消息会不断增长这个列表。优化策略1使用MessageWindowChatMemory严格限制历史消息条数。对于长文档对话可以考虑更复杂的记忆压缩策略如SummaryChatMemory但LangChain4j目前可能需自定义实现。优化策略2精简系统提示词和工具描述。在满足清晰度的前提下尽量用简短的文字。优化策略3异步与非阻塞。如果业务允许考虑使用异步模型调用如果底层模型客户端支持避免阻塞业务线程。工具调用的延迟工具执行可能是 I/O 密集型操作查数据库、调外部 API。这会显著增加单次 Agent 交互的总耗时模型思考时间 工具执行时间 可能的多轮循环。优化策略评估工具的必要性。如果某些工具执行很慢考虑能否优化工具本身或者为工具设置独立的超时和熔断机制。对象创建开销AiServices在每次方法调用时会创建不少中间对象Message列表、各种上下文对象。虽然对于单次调用开销不大但在高 QPS 下需要注意 GC 压力。确保ChatMemory等重量级组件被有效复用。4.2. 日志、追踪与监控调试一个“会自己思考”的 Agent 比调试普通代码更具挑战性。你需要看清模型接收了什么、思考了什么、调用了什么工具、返回了什么。启用LangChain4j的详细日志设置日志级别io.langchain4j为DEBUG或TRACE。这会打印出每次发送给模型的完整消息列表、接收到的响应以及工具调用的详细信息。这是最直接的调试手段。# application.properties 或 logback-spring.xml logging.level.io.langchain4jDEBUG结构化日志与关联ID在生产环境中为每一次用户会话或请求生成一个唯一的traceId并将其注入到日志的 MDCMapped Diagnostic Context中。在构建AiServices时你可以通过自定义组件或拦截器将这个traceId添加到系统消息或用户消息的元数据中如果模型支持或者在工具调用时记录下来。这样你可以在日志系统中轻松过滤出某一次完整对话的所有相关日志。关键指标监控耗时记录每次AiService方法调用的总耗时并区分“模型交互耗时”和“工具执行耗时”。Token 使用量如果模型客户端提供记录每次请求的 Prompt Tokens 和 Completion Tokens用于成本分析和优化。工具调用统计记录每个工具被调用的频率、成功率和平均耗时。这有助于发现不常用的工具可以考虑移除或性能瓶颈工具。错误率监控模型调用失败、工具执行异常、JSON 解析失败等错误。4.3. 测试策略如何测试一个非确定性的 Agent测试 AI 应用是新的挑战因为输出不是完全确定的。你不能简单地断言输出等于某个字符串。契约测试测试 Agent 是否遵守基本的“契约”。例如对于返回FeedbackAnalysisPOJO 的方法你可以测试返回的对象不为null且category字段是预定义集合中的一个值sentiment是“积极/消极/中性”之一。Test void testAnalyzerReturnsValidStructure() { FeedbackAnalysis result analyzer.analyze(一些测试反馈); assertNotNull(result); assertThat(result.getCategory()).isIn(功能请求, Bug报告, 用户体验, 咨询); assertThat(result.getSentiment()).isIn(积极, 消极, 中性); assertThat(result.getKeywords()).isInstanceOf(List.class); }基于语义的断言模糊匹配对于文本回答可以使用嵌入模型Embedding Model计算回答与预期答案的余弦相似度设定一个阈值来判断是否通过。LangChain4j未来可能会提供更集成的测试工具。模拟Mocking在单元测试中你应该模拟MockChatLanguageModel和Tool。你可以预设模型的响应来测试你的AiService接口在不同模型输出下的行为是否正确。例如模拟模型返回一个特定的工具调用请求然后验证你的工具是否被以正确的参数调用。集成测试与黄金数据集维护一个“黄金数据集”包含一系列典型的用户输入和可接受的输出范围。定期例如每晚运行集成测试将 Agent 的输出与黄金标准进行比较监控其行为是否有显著漂移。这更像是一种监控手段而非严格的测试。5. 常见“魔法失灵”场景与排查手册即使理解了原理在实际使用中你还是会碰到AiServices“不按套路出牌”的情况。下面是一些典型问题及其排查思路。5.1. 模型不调用工具症状你注册了工具但模型在回答时完全无视它们只用自身知识回答。排查步骤检查工具描述这是最常见的原因。描述是否清晰、无歧义是否准确描述了工具的用途和输入用中文描述时确保语法通顺。可以尝试用英文描述某些模型对英文提示词响应更好。检查系统提示词你是否提供了SystemMessage一个强烈的系统指令如“你只能使用提供给你的工具来回答问题不能使用内部知识”可以极大地影响模型行为。没有系统指令模型倾向于使用自身知识。检查用户问题你的用户问题是否明确触发了工具的使用场景例如你有一个“查询天气”的工具但用户问的是“哲学是什么”模型自然不会调用工具。尝试一个更直接的问题如“用工具查一下北京的天气”。查看 DEBUG 日志打开DEBUG日志查看发送给模型的最终消息列表。确认工具的描述信息是否被正确包含在消息中通常是在一条SystemMessage里。确认你的用户问题是否被正确传递。调整模型参数尝试提高temperature如从 0.1 调到 0.5。过低的temperature有时会让模型过于保守不敢尝试工具调用。5.2. 工具调用参数错误或类型转换失败症状模型请求调用工具但传入的参数是错的比如格式不对、类型不匹配导致工具方法调用时抛出IllegalArgumentException或JsonParseException。排查步骤检查P注解和参数类型确保工具方法参数有P注解提供清晰的描述。确保参数类型是模型容易理解的简单类型String,int,double,boolean,ListString等。避免使用复杂的自定义对象作为工具参数。查看模型提供的参数在DEBUG日志中找到模型发出的ToolExecutionRequest查看里面的argumentsJSON 字符串。模型是否提供了你期望的键key值value的格式是否正确例如期望是数字模型却提供了带引号的字符串。增强提示词在工具描述或系统消息中更明确地指定参数格式。例如“日期参数必须是 ‘YYYY-MM-DD’ 格式的字符串。”“数量参数必须是一个整数。”在工具方法内增加防御性代码对传入的参数进行校验和转换。例如如果期望是int但模型传来String尝试Integer.parseInt并做好异常处理返回清晰的错误信息给模型。5.3. 结构化输出POJO解析失败症状接口返回YourPojo但调用时抛出JsonParsingException。排查步骤查看模型的原始响应打开TRACE或DEBUG日志找到模型返回的原始文本。模型是否真的输出了一段完整的、合法的 JSON还是输出了一些额外的文本包裹着 JSON检查 POJO 定义你的 POJO 是否有默认构造函数字段是否有 public getter/setter 或标记为public字段名是否与 JSON 中的 key 匹配或使用了JsonProperty使用OutputParser如果模型响应总是不纯净可以自定义一个OutputParser来清洗和提取 JSON。OutputParserFeedbackAnalysis parser (response) - { // 尝试从响应文本中提取第一个 JSON 块 String json extractJsonBlock(response.text()); return objectMapper.readValue(json, FeedbackAnalysis.class); }; AiServices.builder(FeedbackAnalyzer.class) .chatLanguageModel(model) .outputParser(parser) .build();强化指令在UserMessage或SystemMessage中用非常强硬和明确的指令要求模型只输出 JSON不要有任何额外解释。例如“你必须且只能输出一个 JSON 对象不要有任何其他文字。JSON 格式必须严格符合以下定义...”5.4. 内存未按预期工作症状使用了MemoryId但对话似乎没有历史每次都是新的开始。排查步骤确认ChatMemory实例已配置这是最可能的原因。检查AiServices.builder()是否调用了.chatMemory(...)方法。如果只传了model记忆功能是关闭的。确认MemoryId值稳定确保在同一个会话中每次调用方法时传入的MemoryId参数值是相同的。如果每次都是新生成的 ID自然没有历史。检查记忆实现如果你用的是PersistentChatMemory检查底层的ChatMemoryStore是否正常工作如数据库连接。查看存储中是否确实保存了消息。查看日志DEBUG日志会显示每次调用时从内存中加载的消息列表。如果列表为空说明记忆没有生效。LangChain4j的AiServices将声明式编程的思想引入了 Java AI 应用开发极大地提升了开发体验和代码质量。它抽象了复杂的交互流程让开发者能聚焦于定义“做什么”而非“怎么做”。然而正如任何强大的魔法理解其咒语注解的原理、知晓其施法材料模型、工具、记忆的特性并准备好应对反噬异常与调试是将其用于生产环境并创造稳定价值的必经之路。从简单的接口声明开始逐步深入工具编排、记忆管理和生产化配置你会发现构建一个智能、健壮的 Agent 不再是一件令人望而生畏的工程挑战。