这一节,我们将深入@AiService最强大的特性之一:方法返回值可以是任意 Java 类型。你不再需要手动解析 JSON,LangChain4j 会自动完成序列化与反序列化,直接返回 Java 对象给你使用。
回想一下在 Spring AI 中如何获取结构化输出:每次都要调用chatClient.prompt().user(...).call().entity(Xxx.class)。而 LangChain4j 的方式更加彻底——接口定义好返回类型,调用就跟普通 Java 方法一样,完全感知不到背后是 AI 在生成内容。
一、返回 Java 对象
你只需要把方法的返回类型从String换成具体的 Java 类(Record 或 POJO),其他什么都不用改。LangChain4j 会自动:
将目标类的结构转换成 JSON Schema 注入到 Prompt 中;
要求模型按照该 Schema 输出 JSON;
将模型输出的 JSON 反序列化为目标对象。
定义返回类型(Record)
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 服务接口
package com.jichi.langchain4j.service; import com.jichi.langchain4j.model.SentimentResult; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.spring.AiService; @AiService public interface SentimentAnalyzer { @SystemMessage(""" 你是情感分析专家。 分析用户评论的情感,给出情感类别、判断依据和评分。 """) SentimentResult analyze(String review); }Controller 中直接使用
@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); } }测试一下:
curl "http://localhost:8080/structured/sentiment?review=东西不错但快递太慢了" # 返回:{"sentiment":"MIXED","reasons":["商品质量好","物流速度慢"],"score":6}二、返回枚举
当输出结果必须是固定选项之一时,直接返回枚举类型最为合适。
定义枚举
package com.jichi.langchain4j.model; public enum TicketCategory { BILLING, TECH_SUPPORT, FEATURE_REQUEST, ACCOUNT, OTHER }AI 服务接口
@AiService public interface TicketClassifier { @SystemMessage(""" 对客户工单进行分类。 BILLING:账单/付款问题 TECH_SUPPORT:技术故障 FEATURE_REQUEST:功能建议 ACCOUNT:账号/权限问题 OTHER:其他 """) TicketCategory classify(String ticket); }调用示例
@RestController @RequestMapping("/structured/ticket") public class TicketController { private final TicketClassifier classifier; @GetMapping public TicketCategory classify(@RequestParam String ticket) { return classifier.classify(ticket); } }测试:
curl "http://localhost:8080/structured/ticket?ticket=我的信用卡被扣了两次" # 返回:"BILLING"三、嵌套复杂结构
返回类型支持任意复杂的嵌套结构,比如合同信息提取场景。
定义嵌套 Record
// 合同方 public record ContractParty(String role, String name, String contactPerson) {} // 合同信息 public record ContractInfo( String contractNumber, List<ContractParty> parties, String signDate, Double amount, String currency, List<String> keyObligations, List<String> warnings ) {}AI 服务接口
@AiService public interface ContractExtractor { @SystemMessage(""" 你是合同信息提取专家。 只提取文本中明确表述的信息,不推断不猜测。 日期统一转为 YYYY-MM-DD 格式。 无法确定的字段填 null,列表无内容填空列表。 """) ContractInfo extract(String contractText); }调用示例
@RestController @RequestMapping("/structured/contract") public class ContractController { private final ContractExtractor extractor; @PostMapping public ContractInfo extract(@RequestBody String contractText) { return extractor.extract(contractText); } }测试:
curl -X POST "http://localhost:8080/structured/contract" \ -H "Content-Type: text/plain" \ -d "合同编号:HT-2025-001,甲方:鸡翅科技有限公司(联系人:张总),乙方:鸡哥服务公司(联系人:鸡哥),签署日期:2025年3月1日,合同金额:人民币50000元。主要义务:乙方提供AI技术培训服务,共20课时。"四、流式输出(返回 TokenStream)
结构化输出因为需要完整的 JSON 才能解析,所以不支持流式。但是普通的String返回值可以配合TokenStream实现流式输出。
package com.stduying.controller.aiservice; 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 中配合SseEmitter使用:
package com.stduying.controller; import com.stduying.controller.aiservice.StreamingAssistant; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @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; } }测试:
curl -N "http://localhost:8080/stream/write?topic=Spring虚拟线程的原理和使用"五、参数传递总结
@AiService方法支持多种参数类型和注解,灵活控制输入:
| 参数类型/注解 | 作用 |
|---|---|
普通String参数(无注解) | 直接作为 User 消息内容 |
@UserMessage标注的 String | 明确标注该参数为 User 消息(与默认行为一致,但更清晰) |
@V("name") | 注入到@SystemMessage或@UserMessage的{{name}}占位符 |
@MemoryId | 会话标识,用于区分不同对话的记忆(下一节详细讲解) |
| Record / 对象参数 | 自动展开其字段,字段名作为变量名,配合@V使用 |
组合使用示例:
@AiService public interface AdvancedAssistant { @SystemMessage("你是{{role}},为{{company}}公司服务") @UserMessage("{{taskDescription}}:\n{{content}}") String process( @V("role") String role, @V("company") String company, @V("taskDescription") String taskDesc, @V("content") String content, @MemoryId String sessionId // 记忆 ID,不进 Prompt ); }注意:使用了@MemoryId后,需要在 Spring 配置中提供一个ChatMemoryProviderBean:
@Configuration public class ChatMemoryConfig { @Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory.withMaxMessages(10); } }Controller 调用示例:
@GetMapping public String process(@RequestParam String role, @RequestParam String company, @RequestParam String taskDesc, @RequestParam String content, @RequestHeader("X-Session-Id") String sessionId) { return assistant.process(role, company, taskDesc, content, sessionId); }六、对比 Spring AI 的结构化输出
两者都能很好地支持结构化输出,但写法差异明显:
Spring AI(每次都需要手动调用 entity):
SentimentResult result = chatClient.prompt() .user("分析:" + review) .call() .entity(SentimentResult.class);LangChain4j @AiService(就像调用普通方法):
SentimentResult result = analyzer.analyze(review);LangChain4j 的方式更加简洁,调用方完全感知不到背后是 AI 在生成内容。所有的 Prompt 模板、Schema 注入、JSON 反序列化都被框架隐藏了,开发者只需要关心业务接口定义。
七、小结
通过@AiService的结构化返回能力,我们可以:
让 AI 直接返回Java 对象、枚举、嵌套结构,无需手动解析 JSON;
利用
TokenStream实现流式输出(适用于 String 类型);灵活组合
@V、@UserMessage、@MemoryId等注解,满足复杂输入需求;与 Spring AI 等框架相比,代码更加简洁、声明式、易于维护。