资讯动态

LangChain4j 结构化输出:让模型吐出 Java 对象,JSON 不再手写解析

发布时间:2026/10/22 2:12:09 来源:尧图企业网站定制
LangChain4j 结构化输出让模型吐出 Java 对象JSON 不再手写解析作者鱼宵 LangChain4j实战精通营 · 第 3 篇前两课你拿到的回答都是chatModel.chat(...)返回的一段String。聊天场景这没问题——用户看一段话就够了。但真实业务不是这么玩的让你从一段简历自我介绍里抽出姓名、技能、年限你要的是一个Resume对象好塞进数据库、传给下游接口不是一段小作文。我第一回干这事是跟模型说请输出 JSON然后拿 Gson 自己 parse。结果模型高兴起来拿三反引号代码块把答案一包Gson 直接炸给我看。这一课就把这两条路放在一起实测一条是我手写解析的原始人路线一条是AiServices定义个接口就直接拿回 Java 对象的优雅人路线。代码全在仓库lesson-03/clone 下来跑一遍你会看懂为什么生产里没人愿意手写第二条。一、核心原理自由文本怎么变成 Java 对象一句话模型只会吐文本业务要的是对象中间必须有一道解析。这道解析你可以手写也可以让框架帮你写。1. 痛点实习生写字潦草你让实习生模型整理一份简历他给你交回一段小作文我叫王小明干了五年 Java……——你要的不是作文是一张填好的表格。你得自己从作文里把名字、年限抠出来。这就是手写解析的脏活模型输出是自由文本字段格式还不保证。2. 三道保险各防什么生产里让模型吐 JSON一般叠三道保险保险在哪设防什么temperature(0.0)builder模型自由发挥写出不合规 JSON——抽取要确定事实不要文采responseFormat(ResponseFormat.JSON)builder服务端在解码层强制吐合法 JSON不合法就重采样提示词写死字段说明prompt告诉模型有哪些字段、什么类型name/skills/years注意一个坑DeepSeek 开 JSON 模式时提示词里必须出现 “json” 这个词否则接口直接报 400。本课两处提示词都写了输出 JSON就是这个原因。还要清醒一点JSON 模式只保证语法合法不保证语义正确——年限抽成 6 而不是 5它管不了那靠提示词和 few-shot 兜底。3. AiServices你只定义接口框架派个临时工最优雅的玩法是AiServices——你只写一个 Java 接口不写实现类框架在运行时用**动态代理JDK Proxy**给你生成实现interfaceResumeExtractor{SystemMessage(你是简历信息抽取器...只输出 JSON...)Resumeextract(UserMessage(抽取{{intro}})V(intro)Stringintro);}ResumeExtractoreAiServices.builder(ResumeExtractor.class).chatModel(chatModel).build();Resumere.extract(我叫王小明...);// 直接拿对象打个比方你业务代码只写一份岗位描述接口 注解外包公司AiServices派个临时工来干活——拼提示词、调模型、把 JSON 解析成 Resume全是临时工的事。魔法在于返回类型是 Resume 不是 String框架一看你要 POJO就自动套用PojoOutputParser帮你解析。你一行 Gson 都不用写。二、动手跑通两条路线环境沿用前两课JDK 17、DEEPSEEK_API_KEY已配好。cd langchain4j-journey\lesson-03$env:JAVA_HOMEC:\Program Files\Java\jdk-17mvn clean install-DskipTests java-Dfile.encodingUTF-8-jar target\lesson-03-1.0.0.jar想单跑某条路线java-Dfile.encodingUTF-8-jar target\lesson-03-1.0.0.jar manual# 只跑手写java-Dfile.encodingUTF-8-jar target\lesson-03-1.0.0.jar aisservice# 只跑 AiServices三、关键代码四个文件两条路线对照本课模型配置LlmConfig有两个和聊天课不一样的地方先贴它packagecom.langchain4j.lesson03;importdev.langchain4j.model.chat.request.ResponseFormat;importdev.langchain4j.model.openai.OpenAiChatModel;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/** * 模型配置本课两个特殊点——temperature 压到 0.0打开 JSON 模式。 */ConfigurationpublicclassLlmConfig{BeanpublicOpenAiChatModelopenAiChatModel(){returnOpenAiChatModel.builder().baseUrl(System.getenv().getOrDefault(LLM_BASE_URL,https://api.deepseek.com)).apiKey(System.getenv(DEEPSEEK_API_KEY)).modelName(System.getenv().getOrDefault(LLM_MODEL,deepseek-chat)).maxTokens(400)// 温度 0.0信息抽取要确定性不要发散聊天课是 0.7对照记忆.temperature(0.0)// JSON 模式服务端强制吐合法 JSONDeepSeek 要求提示词里必须出现 json.responseFormat(ResponseFormat.JSON).build();}}目标 JavaBeanResume被自动解析的 POJO 三条件public 类 无参构造 getter/setter字段名和 JSON key 对上packagecom.langchain4j.lesson03;importjava.util.List;/** 简历信息我们希望模型最终吐出的形状。 */publicclassResume{privateStringname;// 姓名privateListStringskills;// 技能列表privateintyears;// 工作年限publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}publicListStringgetSkills(){returnskills;}publicvoidsetSkills(ListStringskills){this.skillsskills;}publicintgetYears(){returnyears;}publicvoidsetYears(intyears){this.yearsyears;}OverridepublicStringtoString(){returnResume{namename, skillsskills, yearsyears};}}AiServices 接口——注意它没有实现类packagecom.langchain4j.lesson03;importdev.langchain4j.service.SystemMessage;importdev.langchain4j.service.UserMessage;importdev.langchain4j.service.V;/** * 简历抽取器接口你只定义长什么样框架运行时动态生成实现。 * 返回类型是 Resume不是 String——框架一看要 POJO自动用 PojoOutputParser 解析。 */publicinterfaceResumeExtractor{SystemMessage(你是简历信息抽取器。从用户给的自我介绍文本中抽取结构化信息字段name(姓名字符串)、skills(技能列表字符串数组)、years(工作年限整数单位年)。严格输出 JSON不要输出任何多余解释。)Resumeextract(UserMessage(从下面的自我介绍中抽取简历信息JSON\n{{intro}})V(intro)Stringintro);}主程序里两条路线并排跑脏活全在左边packagecom.langchain4j.lesson03;importcom.google.gson.Gson;importdev.langchain4j.service.AiServices;importdev.langchain4j.model.chat.ChatModel;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * 第 3 课主程序同一任务两条路线对照。 * manual —— 手写chat() 拿回 String → 剥三反引号代码块壳 → Gson 解析 * aisservice —— AiServices定义接口框架动态代理直接拿 Resume */SpringBootApplicationpublicclassResumeExtractApplicationimplementsCommandLineRunner{privatefinalChatModelchatModel;publicResumeExtractApplication(ChatModelchatModel){this.chatModelchatModel;}publicstaticvoidmain(String[]args){SpringApplication.run(ResumeExtractApplication.class,args);}Overridepublicvoidrun(String...args){Stringintro我叫王小明做Java后端开发5年熟悉Spring Boot、MySQL、Redis。;System.out.println(【待抽取的自我介绍】intro);Stringmodeargs.length0?args[0]:all;switch(mode){casemanual-manualDemo(intro);caseaisservice-aiServicesDemo(intro);default-{manualDemo(intro);aiServicesDemo(intro);}}System.out.println( LangChain4j 第 3 课演示结束 );}/** 路线一手写解析。三步脏活全在这。 */privatevoidmanualDemo(Stringintro){System.out.println( 玩法一手写 JSON 解析String 返回 Gson );// 提示词必须出现 jsonDeepSeek JSON 模式的硬要求Stringprompt你是简历信息抽取器。请从下面自我介绍中抽取信息输出 JSON字段name(姓名,字符串)、skills(技能,字符串数组)、years(工作年限,整数)。只输出 JSON。\n自我介绍intro;// ① 拿回自由文本 StringStringrawchatModel.chat(prompt);System.out.println(【模型原始输出String】\nraw);// ② 模型爱用三反引号代码块包答案Gson 不认这个壳先剥掉脏活StringjsonstripCodeFence(raw);// ③ 自己用 Gson 解析成对象ResumeresumenewGson().fromJson(json,Resume.class);System.out.println(【Gson 解析成对象】resume);}/** 剥掉模型可能输出的三反引号代码块包裹只留中间 JSON 本体。 */privateStringstripCodeFence(Stringraw){Stringfence;// 拼成三反引号避免源码里直接出现三个反引号字面量Stringsraw.trim();if(s.startsWith(fence)){// 去掉开头的围栏可能带 json 字样ss.replaceFirst(^java.util.regex.Pattern.quote(fence)(json|JSON)?,).trim();// 去掉结尾的围栏if(s.endsWith(fence)){ss.substring(0,s.length()-fence.length()).trim();}}returns;}/** 路线二AiServices。不碰 String、不碰 Gson直接拿对象。 */privatevoidaiServicesDemo(Stringintro){System.out.println( 玩法二AiServices 自动解析接口直接返回 Resume );// build() 那一刻框架用动态代理生成 ResumeExtractor 的实现类ResumeExtractorextractorAiServices.builder(ResumeExtractor.class).chatModel(chatModel).build();// 像调普通 Java 方法一样调——背后拼提示词、调模型、解析 JSON 全包了Resumeresumeextractor.extract(intro);System.out.println(【AiServices 直接返回的 Resume 对象】resume);}}看出差别了吗手写路线有三步拿 String → 剥壳 → GsonAiServices 路线就一行extractor.extract(intro)。四、实测输出两条路线拿到一模一样的对象以下为 2026-10-05 本机真实运行DeepSeek 实测已省略启动 banner。【待抽取的自我介绍】我叫王小明做Java后端开发5年熟悉Spring Boot、MySQL、Redis。 玩法一手写 JSON 解析String 返回 Gson 【模型原始输出String】 {name: 王小明, skills: [Java, Spring Boot, MySQL, Redis], years: 5} 【Gson 解析成对象】Resume{name王小明, skills[Java, Spring Boot, MySQL, Redis], years5} 玩法二AiServices 自动解析接口直接返回 Resume 【AiServices 直接返回的 Resume 对象】Resume{name王小明, skills[Java, Spring Boot, MySQL, Redis], years5} LangChain4j 第 3 课演示结束 三个观察两条路线结果一模一样name王小明 / skills[Java, Spring Boot, MySQL, Redis] / years5——自我介绍里做Java后端开发5年被正确抽成整数5技能拆成了数组。手写路线这次模型很配合直接吐了裸 JSON连三反引号代码块壳都没包stripCodeFence没派上用场。但生产里模型一任性包个壳没这步 Gson 就炸——这就是为什么剥壳这步不能省。AiServices 路线你没写一行 Gson、没剥一个代码块框架直接把对象递到你手里。这就是声明式的舒服。排查提示报 400 多半是开了 JSON 模式但提示词里没写 “json”Gson 报JsonSyntaxException是模型吐了散文或代码块检查stripCodeFenceAiServices 返回的字段是 null检查 POJO 是不是 public、有没有无参构造和 getter/setter。五、挑战题改参数看看会怎样⭐换个输入把intro改成我是李雷3 年前端主要做 React 和 Vue重跑两条路线看years是不是抽成 3、skills 是不是 React/Vue。答案就在ResumeExtractApplication那个intro字符串里。⭐⭐故意制造格式漂移把stripCodeFence那行注释掉再临时把提示词里只输出 JSON删掉看 Gson 怎么炸——体会手写路线有多脆弱。答案在manualDemo那三步里。⭐⭐给 Resume 加个字段加一个String city城市同时改ResumeExtractor的SystemMessage和手写路线的提示词重跑验证 AiServices 自动带上新字段。答案在Resume.java和那两处提示词。六、生产环境进阶三个加分项1. 格式漂移必须兜底。开了 JSON 模式也不代表 100% 不出错——生产要剥壳 解析失败重试/降级。模型偶尔包代码块、加废话没有兜底线上就 500。2. maxTokens 别太小。JSON 被截断成半截Gson 必炸。抽取复杂对象时把 maxTokens 调大本课 400别抠那点 token 钱。3. JSON 模式只管语法不管语义。字段抽错年限 5 抽成 6它管不了靠提示词写清楚字段定义 few-shot 给示例兜底。七、面试回答模板面试官把 LLM 输出变成 Java 对象有哪几条路一句话手写路线提示词要 JSON 自己 Gson 解析可控但脏JSON 模式 response_format 让服务端强制吐合法 JSONAiServices 声明返回 POJO框架动态代理自动解析最优雅。展开手写要自己剥三反引号代码块壳、自己处理格式漂移AiServices 你只定义接口和注解返回类型是 POJO 时框架自动套 PojoOutputParser。指向本课第一节 lesson-03 两条路线实测追问AiServices 的动态代理原理你只定义接口AiServices.builder(接口.class).chatModel(模型).build()那一刻用 JDK Proxy 生成实现类。调方法时它在背后拼 SystemMessage UserMessage 模板变量 → 调模型 → 看返回类型是 POJO 就用 PojoOutputParser 反射映射成对象。你一行解析代码都不用写。指向本课第 3 小节 ResumeExtractor.java追问模型输出不按 JSON 格式来怎么办三道保险叠temperature0 压确定性、responseFormat(JSON) 服务端强制合法、提示词写死字段。再加兜底剥代码块壳 解析失败重试/降级。注意 JSON 模式只保语法合法不保语义正确字段抽错靠提示词和 few-shot。指向本课第 2 小节 第六节追问DeepSeek 开 JSON 模式有什么特殊要求提示词里必须出现 “json” 这个词否则直接 400。本课两处提示词都写了输出 JSON。指向本课LlmConfig 两处提示词八、总结表坑现象解法模型输出带三反引号代码块壳Gson 直接解析炸手写路线先stripCodeFence剥壳开 JSON 模式但提示词没写 jsonDeepSeek 直接 400提示词里必须出现 “json” 字样POJO 不规范AiServices 字段给 nullpublic 类 无参构造 getter/setter 字段名和 JSON key 对上temperature 太高模型自由发挥写出不合规 JSON抽取任务压到 0.0maxTokens 太小JSON 半截Gson 必炸抽取复杂对象调大本课 400旧教程签名对不上编译报错1.21.0 用AiServices.builder(类).chatModel(模型).build()九、关于这个系列本文是「Java 后端实战精通营」系列第 3 篇原则实战驱动、由浅到深、面试向每篇文章的结论都可以亲手验证。LangChain4j 实战精通营10 课https://gitee.com/j67mk2/langchain4j-journey本文对应源码位置lesson-03/控制台工程内含ResumeJavaBean ResumeExtractorAiServices 接口 两条路线对照的ResumeExtractApplication系列文章一览按发布顺序篇主题1LangChain4j 第一个对话程序手写 LLM 调用链路看清模型怎么开口2LangChain4j 提示词工程模板 few-shot 温度提示词从写死到复用3LangChain4j 结构化输出让模型吐出 Java 对象JSON 不再手写解析4LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比5LangChain4j 流式输出打字机效果 SSE回答不再干等三秒6LangChain4j 工具调用Function Calling 让模型动手查数据多工具连调实测7LangChain4j 向量检索文本怎么变成可搜索的坐标8LangChain4j RAG 流水线手写问答全流程给 AI 开卷考试9LangChain4j Agent多步推理自主调用放手让实习生自己安排工作10LangChain4j 企业智能客服RAG 工具 记忆 流式 兜底验收 5 项全过下一篇预告《LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比》——到现在每次调用都是一问一答、问完就忘你上一句说过的话下一句它根本不记得。下一课用三个对照实验实测带记忆、无记忆、窗口溢出看清记忆到底是怎么回事。跑完有任何报错把终端输出发评论区一起排查。标签建议LangChain4j、结构化输出、AiServices摘要建议≤256 字模型只会吐自由文本业务要的却是能塞进数据库的 Java 对象。本文把两条路线放一起实测手写路线要拿 String、剥代码块壳、Gson 解析脏活一堆AiServices 只定义一个接口框架动态代理直接返回 Resume 对象。讲清 temperature0 responseFormat(JSON) 三道保险各防什么附 3 道改参数挑战题与面试回答模板源码在 lesson-03 可 clone 直接跑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑