导读 :如果你用过 Spring Data JPA,一定对"只定义接口就能查数据库"这种体验印象深刻。LangChain4j 的
@AiService做的事情如出一辙------只定义接口,就能调大模型。本文将从@AiService的设计理念出发,深入讲解 Prompt 注解体系(@SystemMessage、@UserMessage、@V)和结构化输出能力,帮你彻底掌握 LangChain4j 中最核心、最优雅的开发范式。读完本文,你会发现:与 Spring AI 相比,LangChain4j 在接口抽象层面确实做到了更少的代码、更清晰的语义。
一、@AiService:用接口定义 AI 能力
1.1 一个类比搞懂核心思想
LangChain4j 最有特色的设计就是 @AiService。它的思路和 Spring Data JPA 一模一样:
| 框架 | 你做的事 | 框架帮你做的事 |
|---|---|---|
| Spring Data JPA | 定义 Repository 接口 | 自动生成 SQL、执行查询、返回实体 |
| LangChain4j | 定义 AI Service 接口 | 自动构建 Prompt、调用大模型、解析返回值 |
换句话说,JPA 是"定义接口自动查数据库",@AiService 是"定义接口自动调大模型"。
1.2 最简示例:一个接口搞定 AI 对话
来看项目中的 SimpleAssistant(com.jichi.langchain4j.service.SimpleAssistant):
package com.jichi.langchain4j.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
@AiService // 告诉 LangChain4j:这是一个 AI 服务,帮我生成实现
public interface SimpleAssistant {
@SystemMessage("你是一个友好的 AI 助手,用简洁的语言回答问题")
String chat(String userMessage);
}
Controller 中直接注入调用(com.jichi.langchain4j.controller.aiservice.AssistantController):
@RestController
@RequestMapping("/assistant")
public class AssistantController {
private final SimpleAssistant assistant;
public AssistantController(SimpleAssistant assistant) {
this.assistant = assistant;
}
@GetMapping
public String ask(@RequestParam String question) {
return assistant.chat(question);
}
}
没有 chatModel.call(),没有 getResult().getContent(),一切都被代理层屏蔽了。你写的代码看起来就像在调一个普通的 Java 方法。
1.3 工作原理
Spring Boot 启动时,LangChain4j 会扫描所有带有 @AiService 注解的接口,为它们动态生成代理对象并注入到 Spring 容器。当你调用接口方法时,代理内部做了以下几件事:
- 读取角色设定 :解析
@SystemMessage注解,构建 System Prompt - 包装用户消息:将方法参数封装为 User Message
- 构建消息列表:如果有历史对话(多轮记忆),会一并构建完整的 Message 列表
- 调用大模型:通过 ChatModel 发起实际请求
- 解析返回值:将模型的响应自动转换为方法声明的返回类型
这正是代理模式的经典应用------把所有"脏活累活"封装在代理层,开发者只需关注接口契约。
1.4 一个接口挂载多个能力
一个 @AiService 接口可以定义多个方法,每个方法拥有独立的 System Prompt。来看项目中的 MultiCapabilityAssistant(com.jichi.langchain4j.service.MultiCapabilityAssistant):
package com.jichi.langchain4j.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface MultiCapabilityAssistant {
@SystemMessage("你是一个 Java 技术助手,专注于代码质量和性能优化")
String reviewCode(String code);
@SystemMessage("""
你是一个技术文档写作专家。
把技术内容转化为清晰易懂的文档,有条理,有示例。
""")
String writeDoc(String techContent);
@SystemMessage("你是一个 SQL 专家,帮助优化数据库查询")
String optimizeSql(String sql);
}
对应的 Controller(com.jichi.langchain4j.controller.aiservice.DevAssistantController):
@RestController
@RequestMapping("/dev")
public class DevAssistantController {
private final MultiCapabilityAssistant assistant;
public DevAssistantController(MultiCapabilityAssistant assistant) {
this.assistant = assistant;
}
@PostMapping("/review")
public String reviewCode(@RequestBody String code) {
return assistant.reviewCode(code);
}
@PostMapping("/doc")
public String writeDoc(@RequestBody String techContent) {
return assistant.writeDoc(techContent);
}
@PostMapping("/sql")
public String optimizeSql(@RequestBody String sql) {
return assistant.optimizeSql(sql);
}
}
调用不同方法时,会自动走对应的 System Prompt,互不干扰。一目了然地知道这个接口有哪些 AI 能力,不需要翻看实现细节------这就是接口抽象的魅力。
1.5 多模型切换:让贵的干复杂活,便宜的干简单活
生产环境中,我们通常会配置多个模型------功能强但成本高的主模型,以及速度快、价格低的备用模型。LangChain4j 支持在 @AiService 级别指定使用哪个模型。
首先,将备用模型注册为 Spring Bean:
@Configuration
public class ModelConfig {
@Bean("cheapModel")
public ChatLanguageModel cheapModel() {
return OpenAiChatModel.builder()
.baseUrl("https://api.example.com")
.apiKey("your-api-key")
.modelName("gpt-3.5-turbo")
.build();
}
@Bean("mainModel")
public ChatLanguageModel mainModel() {
return OpenAiChatModel.builder()
.baseUrl("https://api.example.com")
.apiKey("your-api-key")
.modelName("gpt-4")
.build();
}
}
然后在 @AiService 中通过 chatModel 属性指定:
@AiService(chatModel = "cheapModel")
public interface SimpleTaskService {
@SystemMessage("你是一个简单问答助手")
String answer(String question);
}
@AiService(chatModel = "mainModel")
public interface ComplexTaskService {
@SystemMessage("你是一个资深架构师,擅长分析复杂技术问题")
String analyze(String topic);
}
注意 :当容器中存在多个 ChatModel Bean 时,所有
@AiService都必须显式指定chatModel,否则 Spring 会因为无法确定注入哪个 Bean 而报错。
1.6 手动构建:AiServices.builder()
除了注解方式,LangChain4j 也提供了编程式的构建方式,适合需要动态配置的场景。来看项目中的 TenantChatAssistant(com.jichi.langchain4j.service.TenantChatAssistant),它是一个不加 @AiService 的纯接口,由 AiServices.builder() 编程式构建:
package com.jichi.langchain4j.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
// TenantChatAssistant 是不加 @AiService 的纯接口,由 AiServices.builder() 编程式构建
public interface TenantChatAssistant {
String chat(@MemoryId String sessionId, @UserMessage String message);
}
构建方式(摘自 TenantAwareAssistantService):
AiServices.builder(TenantChatAssistant.class)
.chatModel(chatModel)
// @MemoryId 场景必须用 chatMemoryProvider,不能用 chatMemory
.chatMemoryProvider(memId -> MessageWindowChatMemory.withMaxMessages(10))
.systemMessageProvider(memId -> systemPrompt)
.build();
这种方式和注解方式功能完全一致,只是从声明式变成了命令式,在需要运行时动态创建 AI Service 的场景下更加灵活。
1.7 对比 Spring AI:代码量少一半
实现同样的"代码审查 + 文档生成 + SQL 优化"三个能力,Spring AI 需要:
// Spring AI 写法 - 每个能力都要手写调用逻辑
String result = chatClient.prompt()
.system("你是一个Java开发工程师...")
.user(code)
.call()
.content();
而 LangChain4j 只需要定义接口:
// LangChain4j 写法 - 定义即能力(摘自 MultiCapabilityAssistant)
@SystemMessage("你是一个 Java 技术助手,专注于代码质量和性能优化")
String reviewCode(String code);
代码量几乎减半,而且接口本身就是能力清单,可读性极强。
二、Prompt 注解体系:@SystemMessage、@UserMessage 与 @V
有了 @AiService 的基础,接下来我们深入 Prompt 注解体系。LangChain4j 提供了三个核心注解来管理 Prompt:@SystemMessage、@UserMessage 和 @V。
2.1 @SystemMessage:角色设定
@SystemMessage 用于设定 AI 的角色和行为规则,加在方法(或接口)上。
单行写法 (摘自 SimpleAssistant):
@SystemMessage("你是一个友好的 AI 助手,用简洁的语言回答问题")
String chat(String userMessage);
多行写法 ------使用文本块(摘自 MultiCapabilityAssistant):
@SystemMessage("""
你是一个技术文档写作专家。
把技术内容转化为清晰易懂的文档,有条理,有示例。
""")
String writeDoc(String techContent);
2.2 @V 注解:Prompt 变量注入
很多时候 System Prompt 不能写死,需要动态传入参数。@V 注解就是做这件事的------将方法参数绑定到 Prompt 模板中的占位符。来看项目中的 FileBasedAssistant(com.jichi.langchain4j.service.FileBasedAssistant):
@AiService
public interface FileBasedAssistant {
@SystemMessage(fromResource = "prompts/customer-service.txt")
String chat(@V("companyName") String company,
@V("serviceScope") String scope,
@UserMessage String message);
}
调用时:
service.chat("鸡哥商城", "商品、订单、售后", "我的订单在哪?");
实际发送给模型的 System Prompt 就变成了:"你是"鸡哥商城"的智能客服助手。服务范围:商品、订单、售后..."。
@V 的使用规则很简单:@V("name") 中的 name 必须与 Prompt 模板中 {``{name}} 一一对应。
2.3 @UserMessage:控制用户消息格式
默认情况下,方法中的 String 参数就是 User Message。但如果你想对用户消息做更精细的控制,比如组合多个变量,就需要 @UserMessage 注解。
方式一:模板化用户消息 (摘自 Translator,com.jichi.langchain4j.service.Translator):
@AiService
public interface Translator {
@SystemMessage("你是一个专业翻译")
@UserMessage("将以下文字翻译成{{language}}:\n{{text}}")
String translate(@V("language") String language, @V("text") String text);
}
方式二:标注在参数上 (摘自 TenantChatAssistant):
public interface TenantChatAssistant {
String chat(@MemoryId String sessionId, @UserMessage String message);
}
此时 message 参数会被明确标记为用户消息,语义更加清晰。
2.4 @V 支持对象映射
当变量很多时,逐个用 @V 标注显得冗余。LangChain4j 支持直接传入对象,框架会自动将对象的字段映射到 Prompt 模板的占位符。来看项目中的 CodeReviewer(com.jichi.langchain4j.service.CodeReviewer):
首先定义请求对象(com.jichi.langchain4j.model.CodeReviewRequest):
package com.jichi.langchain4j.model;
public record CodeReviewRequest(
String language,
String code,
String focusAreas // "性能、空指针、事务"
) {}
然后在接口中直接使用对象参数:
@AiService
public interface CodeReviewer {
@SystemMessage("你是代码审查专家")
@UserMessage("""
请 review 以下 {{language}} 代码:
```{{language}}
{{code}}
```
重点关注:{{focusAreas}}
""")
String review(CodeReviewRequest request);
}
只要对象的字段名与 {``{}} 中的变量名一致,就能自动映射,无需额外注解。
2.5 从文件加载 Prompt
当 Prompt 较长或需要非开发人员维护时,把 Prompt 写在代码里就不合适了------每次修改都要重新编译。LangChain4j 支持从资源文件加载 Prompt。
项目中 resources/prompts/customer-service.txt 的内容:
你是"{{companyName}}"的智能客服助手。
服务范围:{{serviceScope}}
约束:
- 只回答与{{companyName}}相关的问题
- 不确定的说"我帮您确认一下",不要猜测
- 回复不超过 150 字
然后在接口中通过 fromResource 引用(摘自 FileBasedAssistant):
@AiService
public interface FileBasedAssistant {
// fromResource 指定加载的文件路径
@SystemMessage(fromResource = "prompts/customer-service.txt")
String chat(@V("companyName") String company,
@V("serviceScope") String scope,
@UserMessage String message);
}
这样产品经理或运营人员可以直接修改 txt 文件来调整 AI 的行为,无需改代码、无需重新编译。
2.6 动态 System Prompt:多租户 SaaS 场景
在 SaaS 系统中,不同租户可能需要完全不同的 AI 人设------有的要严谨客服风格,有的要幽默助手风格,有的要推销员风格。这种场景下,静态的 @SystemMessage 就不够用了。
项目中通过 TenantPrompt 实体将 Prompt 存储在数据库中(com.jichi.langchain4j.model.TenantPrompt):
@Entity
@Table(name = "tenant_prompt")
@Data
public class TenantPrompt {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String tenantId;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
}
然后通过 TenantAwareAssistantService(com.jichi.langchain4j.service.TenantAwareAssistantService)实现运行时动态切换:
@Service
public class TenantAwareAssistantService {
private final ChatModel chatModel;
private final TenantPromptRepository promptRepo;
private final Map<String, TenantChatAssistant> assistantCache = new ConcurrentHashMap<>();
public TenantAwareAssistantService(ChatModel chatModel, TenantPromptRepository promptRepo) {
this.chatModel = chatModel;
this.promptRepo = promptRepo;
}
public String chat(String tenantId, String sessionId, String message) {
TenantChatAssistant assistant = assistantCache.computeIfAbsent(tenantId, id -> {
String systemPrompt = promptRepo.findByTenantId(id)
.map(TenantPrompt::getContent)
.orElse("你是一个通用助手");
return AiServices.builder(TenantChatAssistant.class)
.chatModel(chatModel)
// @MemoryId 场景必须用 chatMemoryProvider,不能用 chatMemory
.chatMemoryProvider(memId -> MessageWindowChatMemory.withMaxMessages(10))
.systemMessageProvider(memId -> systemPrompt)
.build();
});
return assistant.chat(sessionId, message);
}
}
核心思路:将 Prompt 存储在数据库中,运行时根据租户 ID 动态查询并注入 。配合 @MemoryId 可以实现会话级别的隔离,每个租户拥有独立的对话记忆。
2.7 注解优先级
当多个注解同时存在时,优先级规则如下:
- 方法级
@SystemMessage> 类级@SystemMessage(方法粒度更细,优先级更高) @UserMessage注解 > 默认参数作为用户消息(显式声明优先于隐式约定)
记住一个原则:越具体的声明,优先级越高。
三、结构化输出:告别手动 JSON 解析
3.1 直接返回 Java 对象
这是 LangChain4j 最让人惊喜的能力之一。在 @AiService 的方法中,返回值可以是任意 Java 类型------不需要手动解析 JSON,不需要调用 .entity() 方法。
来看项目中的情感分析。先定义返回类型(com.jichi.langchain4j.model.SentimentResult):
package com.jichi.langchain4j.model;
import java.util.List;
public record SentimentResult(
String sentiment, // POSITIVE / NEGATIVE / MIXED / NEUTRAL
List<String> reasons, // 判断依据
int score // 1-10 分
) {}
然后定义 AI Service 接口(com.jichi.langchain4j.service.SentimentAnalyzer):
@AiService
public interface SentimentAnalyzer {
@SystemMessage("""
你是情感分析专家。
分析用户评论的情感,给出情感类别、判断依据和评分。
""")
SentimentResult analyze(String review);
}
Controller 中直接调用(com.jichi.langchain4j.controller.structed.SentimentController):
@RestController
@RequestMapping("/structured/sentiment")
public class SentimentController {
private final SentimentAnalyzer analyzer;
public SentimentController(SentimentAnalyzer analyzer) {
this.analyzer = analyzer;
}
@GetMapping
public SentimentResult analyze(@RequestParam String review) {
return analyzer.analyze(review);
}
}
完全像调用一个普通方法一样------传入字符串,返回对象。你甚至感知不到背后有大模型在工作。
3.2 背后的魔法
框架在背后做了三件事:
- Schema 生成 :将 Java 类(如
SentimentResultrecord)转换为 JSON Schema - Prompt 注入:将 Schema 注入到 Prompt 中,要求模型按照指定格式输出 JSON
- 反序列化:将模型返回的 JSON 字符串反序列化为 Java 对象
这个过程对开发者完全透明,你只需要定义好返回类型的类结构即可。
3.3 枚举类型:固定选项输出
当 AI 的输出是有限选项之一时,可以直接返回枚举。来看项目中的工单分类。先定义枚举(com.jichi.langchain4j.model.TicketCategory):
package com.jichi.langchain4j.model;
public enum TicketCategory {
BILLING, TECH_SUPPORT, FEATURE_REQUEST, ACCOUNT, OTHER
}
然后定义分类器接口(com.jichi.langchain4j.service.TicketClassifier):
@AiService
public interface TicketClassifier {
@SystemMessage("""
对客户工单进行分类。
BILLING:账单/付款问题
TECH_SUPPORT:技术故障
FEATURE_REQUEST:功能建议
ACCOUNT:账号/权限问题
OTHER:其他
""")
TicketCategory classify(String ticket);
}
TicketCategory category = classifier.classify("我的信用卡被扣了两次");
// category -> TicketCategory.BILLING
枚举输出在意图识别、状态分类等场景中非常实用。
3.4 复杂嵌套结构
LangChain4j 的结构化输出支持任意深度的嵌套。来看项目中的合同信息提取。涉及三个 record 类:
ContractInfo(com.jichi.langchain4j.model.ContractInfo):
public record ContractInfo(
String contractNumber,
List<ContractParty> parties,
String signDate,
Double amount,
String currency,
List<String> keyObligations,
List<String> warnings
) {}
ContractParty(com.jichi.langchain4j.model.ContractParty):
public record ContractParty(String role, String name, String contactPerson) {}
对应的提取器接口(com.jichi.langchain4j.service.ContractExtractor):
@AiService
public interface ContractExtractor {
@SystemMessage("""
你是合同信息提取专家。
只提取文本中明确表述的信息,不推断不猜测。
日期统一转为 YYYY-MM-DD 格式。
无法确定的字段填 null,列表无内容填空列表。
""")
ContractInfo extract(String contractText);
}
只要定义好类的层级关系,框架就能将模型的输出正确地映射到嵌套对象中。String、int、Double、List、嵌套 record,统统支持。
项目中还有联系人提取(返回 Optional<ContactInfo>)和命名实体提取(返回 List<NamedEntity>),展示了更多返回类型的灵活性:
// ContactExtractor - 返回 Optional,找不到时为空
@AiService
public interface ContactExtractor {
@SystemMessage("从文本中提取联系人信息,如果某个字段找不到,对应字段为 null")
Optional<ContactInfo> extract(String text);
}
// EntityExtractor - 返回 List,提取多个实体
@AiService
public interface EntityExtractor {
@SystemMessage("从文本中提取所有命名实体(人名、地名、公司名、产品名)")
List<NamedEntity> extract(String text);
}
3.5 流式输出:TokenStream
对于需要实时展示生成过程的场景(如打字机效果),可以将返回类型改为 TokenStream。来看项目中的 StreamingAssistant(com.jichi.langchain4j.service.StreamingAssistant):
package com.jichi.langchain4j.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface StreamingAssistant {
@SystemMessage("你是一个写作助手")
TokenStream write(String topic);
}
Controller 中通过 SSE 返回(com.jichi.langchain4j.controller.structed.StreamingWriteController):
@RestController
@RequestMapping("/stream")
public class StreamingWriteController {
private final StreamingAssistant streamingAssistant;
public StreamingWriteController(StreamingAssistant streamingAssistant) {
this.streamingAssistant = streamingAssistant;
}
@GetMapping(value = "/write", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter write(@RequestParam String topic) {
SseEmitter emitter = new SseEmitter(60_000L);
streamingAssistant.write(topic)
.onPartialResponse(token -> {
try {
emitter.send(token);
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onCompleteResponse(response -> emitter.complete())
.onError(emitter::completeWithError)
.start();
return emitter;
}
}
只需将返回类型从 String 改为 TokenStream,其他调用方式完全不变。TokenStream 是 LangChain4j 内置的类型,开箱即用。
3.6 对比 Spring AI
Spring AI 的结构化输出需要显式调用 .entity() 并传入目标类:
// Spring AI 写法
SentimentResult result = chatClient.prompt()
.system("分析情感倾向")
.user(review)
.call()
.entity(SentimentResult.class);
LangChain4j 则完全隐藏了这个过程:
// LangChain4j 写法(摘自 SentimentAnalyzer)
SentimentResult result = analyzer.analyze(review);
一行代码搞定,类型安全由编译器保证,开发体验显著提升。
四、参数传递规则总结
LangChain4j 的参数传递遵循以下规则:
| 参数类型 | 行为 |
|---|---|
普通 String 参数 |
直接作为 User Message 内容 |
@UserMessage String |
明确标记为 User Message |
@V("name") String |
绑定到 Prompt 模板中的 {``{name}} 变量 |
@MemoryId String |
对话记忆隔离 ID(用于多轮对话) |
| 对象参数 | 自动将字段映射到模板变量 |
一个综合示例------项目中的 MultiScenarioAssistant(com.jichi.langchain4j.service.MultiScenarioAssistant)完整展示了这些用法:
@AiService
public interface MultiScenarioAssistant {
// 场景一:简单问答
@SystemMessage("你是一个 Java 技术助手,回答简洁")
String techChat(String question);
// 场景二:有格式要求的翻译
@SystemMessage("你是专业翻译,保持原文风格,不添加解释")
@UserMessage("将以下内容翻译成{{language}}:\n\n{{content}}")
String translate(@V("language") String lang, @V("content") String content);
// 场景三:从文件加载复杂 Prompt
@SystemMessage(fromResource = "prompts/code-review.txt")
String reviewCode(@V("language") String lang, @UserMessage String code);
// 场景四:多变量 + 对象参数
@SystemMessage("你是数据分析专家")
@UserMessage("分析以下{{period}}的销售数据,重点关注{{focus}}:\n{{data}}")
String analyzeData(@V("period") String period,
@V("focus") String focus,
@V("data") String data);
}
五、总结
本文围绕 LangChain4j 三大核心能力展开:
@AiService 是 LangChain4j 最具特色的设计。它借鉴了 Spring Data JPA "定义接口即实现功能"的思路,通过动态代理将大模型调用封装在接口背后。开发者只需关注接口契约,不需要关心调用细节。支持单接口多能力、多模型切换、手动构建等灵活用法。
Prompt 注解体系 提供了 @SystemMessage、@UserMessage、@V 三个核心注解,覆盖了从静态配置到动态注入、从单行到多行、从硬编码到文件加载的全部场景。在多租户 SaaS 场景下,还可以结合数据库实现运行时动态切换 System Prompt。
结构化输出 让 AI 的返回值和普通 Java 方法一样类型安全。支持 record、枚举、嵌套对象、Optional、List、流式 TokenStream 等多种返回类型,框架自动完成 JSON Schema 生成、Prompt 注入和反序列化的全流程。
一句话概括:LangChain4j 的核心哲学是"用接口描述意图,让框架处理细节" 。如果你的项目是 Java 技术栈,正在选型大模型集成框架,LangChain4j 的 @AiService 绝对值得一试。