Yujun's Blog
Langchain4j(二):AiService
Langchain4j(二):AiService
让我们能够以声明式的方式,定义我们AI助手的行为,能力和交互逻辑。
只需告诉 AI 应该“做什么”,而无需关心它“如何做”——所有底层复杂的编排、调用和管理都由 AiService 在幕后自动完成。
我们将实现一个简单的 AI 助手,它具备以下功能:
- 能够进行多轮对话,记住之前的聊天内容(记忆)。
 - 能够调用外部工具来获取当前日期时间。
 - 能够调用外部工具来计算两个数字的和。
 
我们将分别用两种方式实现这个助手,并对比代码的复杂度和可读性。
那么什么是更高级的功能?
这里可以想像一下我们平时在网页端使用AI 的场景:包括:对话记忆,函数调用,RAG....
链是将多个底层组件组合起来,并协调它们之间的交互的。但是缺点是不灵活。
在langchain4j中,我们使用AIservice完成复杂操作,底层组件将由AIservice封装。
依赖
为了使用 AiService ,我们需要引入langchain4j的高级功能。
<!--langchain4j的高级功能--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>1.0.1-beta6</version> </dependency>
如何使用?所谓的 AiService ,在使用中,其实就是一个注解:(不同版本可能有不同)
@Service @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AiService { AiServiceWiringMode wiringMode() default AiServiceWiringMode.AUTOMATIC; String chatModel() default ""; String streamingChatModel() default ""; String chatMemory() default ""; String chatMemoryProvider() default ""; String contentRetriever() default ""; String retrievalAugmentor() default ""; String moderationModel() default ""; String[] tools() default {}; }
首先看下注解的元注解:
@Service: 这意味着,当 langchain4j-spring-boot-starter 扫描到任何被@AiService注解的接口时,它会同时将其视为一个 Spring 服务组件,并自动为其创建和管理 Bean。这使得 AiService 能够直接被 Spring 的依赖注入机制使用(例如,通过@Autowired注入)。@Target({ElementType.TYPE}): ElementType.TYPE 意味着它可以应用于类、接口(这也是我们主要使用的)、枚举或注解类型。- @Retention(RetentionPolicy.RUNTIME): 这个元注解指定了注解的生命周期。RetentionPolicy.RUNTIME 意味着该注解信息会在运行时保留,因此可以通过 Java 反射机制在运行时被读取和处理(这是 LangChain4j 动态代理生成 AiService 实现的关键)。
 
接下来看下成员变量:
- wiringMode():
- 类型为:
AiServiceWiringMode(一个枚举类型);默认值为:AiServiceWiringMode.AUTOMATIC - 这个参数控制 
AiService如何自动连接其内部组件(如ChatLanguageModel,ChatMemory,Tools等)。 AUTOMATIC: 默认模式。LangChain4j Spring Boot Starter 会尝试自动发现 Spring Context 中的合适 Bean,并将它们注入到 AiService 的构建过程中。例如,它会自动查找ChatLanguageModelBean、ChatMemoryStoreBean,以及所有带有@Tool方法的@ComponentBean。STRICT: 严格模式。如果启用了严格模式,那么@AiService就不会尝试自动发现和注入。你必须通过@AiService注解的参数(例如 chatModel(), tools() 等)或者在AiServices.builder()中明确指定所有依赖。这在某些需要精确控制依赖关系的场景下可能有用。- 大多数情况下保持默认 
AUTOMATIC即可。只有在我们遇到模糊的 Bean 注入问题,或者需要非常精细地控制每个 AiService 的依赖时,才可能考虑将其设置为 STRICT。 - 假如我们在配置文件中配置了多个LLM,比如千问,DeepSeek,Openai,就要用严格模式;只配置了一个的话保持默认即可。
 
 - 类型为:
 - 打
 
原理的感性认识
先来总结一下 AiService 的一个核心工作原理:代理对象 (Proxy Object) 负责所有输入/输出的转换和编排。
AiServices 即 LangChain4j 提供的一个工具类(dev.langchain4j.service.AiServices)。它就是负责创建 @AiService 接口代理实例的工厂。它会组装你的小助手接口和其他组件。并使用反射机制创建一个实现小助手接口的代理对象。具体来说,工作流程如下:
当调用 AiServices.builder() 或 AiServices.create() 时,我们通常会传入 Assistant.class(即 @AiService 接口),并且可能会配置其他依赖组件,例如:
chatLanguageModel(): 告诉 AiService 使用哪个 LLM。chatMemory()或chatMemoryStore(): 告诉 AiService 如何管理对话记忆。tools(): 告诉 AiService 有哪些外部工具可以调用。retriever(): 告诉 AiService 如何进行 RAG 检索。
之后,AiServices 在运行时不会去寻找一个 Assistant 接口的实现类。(也就是说我们不需要创建小助手的实现类。)相反,它会利用 Java 的动态代理(通常是 java.lang.reflect.Proxy 或底层库如 CGLIB),在内存中动态生成一个类,这个类实现了你的 Assistant 接口。当获得 Assistant 接口的实例并调用其方法时,实际上是在与这个动态生成的“代理对象”交互。
实现聊天记忆
使用@AiService 实现聊天记忆很简单,我们就可以从这里开始,让我们的小助手变得有记忆起来。
@AiService( chatMemory = "chatMemory" ) public interface MemoryAssistant { String chat(String msg); } // 配置类 @Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.withMaxMessages(10); }
持久化聊天记忆
对于任何生产级别的 AI 应用,聊天记忆的持久化是必不可少的。它能确保用户对话体验的连续性,支持应用的横向扩展,并有效管理内存资源。
实现方案上涉及以下几点:
聊天历史的持久化与管理 形成有效的聊天记忆上下文 聊天历史大小,避免超过model context上下文大小限制。包括 MessageWindowChatMemory 等,包括一些优化 Chat Summary,有效降低聊天历史成本的一种手段。
@AiService 的底层原理
@AiService 这个注解其背后是很明显是有 动态代理(Dynamic Proxy)机制的体现,但是同时也体现了智能编排(Orchestration)机制。让我们深入理解它如何将 Java 接口调用转化为与 LLM 的复杂交互。
1. 动态代理:拦截与转发
- 核心: 当我们在 Spring Context 中 
@Autowired一个@AiService接口时,Spring 并不会找到一个硬编码的实现类。相反,LangChain4j 会利用 Java 的动态代理技术(如java.lang.reflect.Proxy或 CGLIB)在运行时为接口生成一个代理对象。 - 拦截机制: 当我们调用这个代理对象上的任何方法时(例如 
myAssistant.chat("hello");),实际的调用并不会直接执行任何业务逻辑,而是被这个代理拦截下来。代理对象会检查你调用的方法名、参数以及方法上定义的 LangChain4j 相关注解。 
2. 提示词自动化构建 (Prompt Construction)
代理拦截方法调用后,其首要任务就是根据收集到的信息,自动构建一个结构化的消息列表 (List<ChatMessage>),这个列表将作为完整的提示词发送给 LLM。
@SystemMessage处理:- 代理会查找接口级别或方法级别定义的 
@SystemMessage注解。 - 它会将 
systemMessage的文本内容(可以是多行)作为SystemMessage对象添加到消息列表的开头,为 LLM 设定角色和全局指令。 {{tools}}的魔法: 如果SystemMessage文本中包含{{tools}}占位符,代理会扫描所有已绑定到此AiService的工具(通过tools()参数或 Spring 自动扫描发现的@Component工具类)。它会提取这些工具的元数据(名称、描述、每个参数的名称和类型,以及@P注解提供的额外描述),并将其转换为 LLM 能够理解的 JSON Schema 格式。然后,将这个 JSON Schema 插入到SystemMessage中{{tools}}所在的位置。这就像在告诉 LLM:“这是你所有可用的工具列表和它们的使用说明书!”
- 代理会查找接口级别或方法级别定义的 
 @UserMessage处理:- 代理会识别被 
@UserMessage注解的方法参数。 - 该参数的值将被封装成一个 
UserMessage对象,代表用户当前的输入或请求,并添加到消息列表中。 
- 代理会识别被 
 
3. 记忆管理与上下文维护 (Memory Management)
为了实现多轮对话的上下文感知,@AiService 结合 ChatMemory 和 ChatMemoryStore 自动管理记忆:
@MemoryId识别: 代理会识别带有@MemoryId注解的方法参数。这个参数的值(例如userId)将作为当前对话会话的唯一标识符。ChatMemoryStore协作: 代理会将这个memoryId传递给在 Spring Context 中配置的ChatMemoryStoreBean(例如InMemoryChatMemoryStore、RedisChatMemoryStore)。- 获取/创建 
ChatMemory:ChatMemoryStore会根据memoryId查找对应的ChatMemory实例。如果存在,则返回已有的实例(包含历史消息)。- 如果不存在,
ChatMemoryStore会创建一个新的ChatMemory实例(通常是MessageWindowChatMemory,其行为可在ChatMemoryConfig中配置,如maxMessages),并将其与memoryId关联。 
 - 记忆更新: 代理会将当前的用户消息(
UserMessage)添加到这个特定会话的ChatMemory中。随后 LLM 返回的 AI 响应(AiMessage)以及可能发生的工具调用/输出消息,也会被自动添加到该ChatMemory中。 - 上下文传递: 最终,所有存储在该 
ChatMemory实例中的历史消息都会被包含在发送给 LLM 的List<ChatMessage>中,确保 LLM 始终拥有完整的对话上下文。 
4. 智能工具调用编排 (Tool Calling Orchestration)
这是 @AiService 最具魔力且最复杂的环节,它让 AI 真正具备了“行动力”:
- 发送带工具信息的请求: 代理将完整的 
List<ChatMessage>(包含SystemMessage中注入的工具 JSON Schema)发送给底层的ChatLanguageModel(LLM)。 - LLM 的决策: LLM 接收到请求后,会进行推理。它会结合用户意图和可用的工具描述,判断是否需要调用某个工具来完成任务。
- 如果 LLM 判断不需要工具(可以直接回答),它会直接返回一个文本响应。
 - 如果 LLM 判断需要调用工具(例如,用户问“现在几点了?”,LLM 知道有一个 
getCurrentDateTime工具),它不会直接返回文本,而是返回一个特殊的tool_calls对象(通常是 JSON 格式),其中包含要调用的工具名称和提取出的参数。 
 - 代理解析并执行工具: 代理拦截 LLM 返回的 
tool_calls指令。- 代理解析 JSON,根据工具名称找到对应的 Java 
@Tool方法(在通过tools()参数或 Spring 自动扫描注册的工具类中)。 - 代理将 LLM 提供的参数(JSON)自动映射并转换为 Java 方法的参数类型(如果参数有 
@P注解,其描述也有助于这个过程)。 - 代理通过 Java 反射机制调用这个找到的 Java 方法,执行实际的业务逻辑。
 
 - 代理解析 JSON,根据工具名称找到对应的 Java 
 - 结果反馈与多轮交互: 工具方法执行完毕后,其返回值会被代理封装成一个 
ToolOutputMessage。这个ToolOutputMessage会被添加到当前会话的ChatMemory中,并再次发送回 LLM。LLM 接收到工具的真实输出后,会基于这个输出进行二次推理,生成一个连贯、准确且用户友好的最终文本回复。这个“LLM 决策 -> 代理执行 -> 结果反馈给 LLM -> LLM 再次推理”的循环可以执行多次,直到 LLM 认为任务完成。 
5. 输出解析 (Output Parsing)
LLM 的原始响应是文本。代理会根据 @AiService 接口方法定义的返回类型,尝试将 LLM 的文本响应解析为 Java 对象。
- 如果返回类型是 
String,则直接返回文本。 - 如果返回类型是自定义的 Java 对象(POJO),并且 LLM 的输出是 JSON 格式,LangChain4j 会尝试使用 Jackson 等库进行 JSON 反序列化。
 - 如果需要更复杂的解析,可以结合 
dev.langchain4j.model.output.OutputParser。 
@AiService 接口内部方法的常用注解
这些注解是 AiService 声明式编程的基础,用于定义在接口的各个方法上:
@SystemMessage: 定义 AI 的角色、行为规则和全局指令。可以包含{{tools}}占位符。@UserMessage: 标记方法参数作为用户输入发送给 LLM。@MemoryId: 标记方法参数作为对话会话的唯一标识符,实现记忆隔离。@Tool(在单独的工具类中): 标记一个 Java 方法为可供 AI 调用的工具,并提供其描述。@P(在@Tool方法的参数中): 为工具方法的参数提供额外的自然语言描述,帮助 LLM 准确提取参数。
结语:驾驭 LLM 的未来
@AiService 是 LangChain4j 卓越设计的一个缩影。它不仅仅是一个简单的封装,更是一种对复杂 AI Agent 构建的深度抽象和智能编排。它将 LLM 的文本理解和决策能力,与 Java 应用的业务逻辑和外部操作能力完美结合,让 Java 开发者能够以最熟悉、最优雅的方式,构建出功能强大、智能且高度可维护的 AI 应用程序。