资讯动态

Java 后端 AI 转型实录 (三)Spring‑AI 结构化输出 entity () 实战,踩坑 Prompt 花括号注入风险

发布时间:2026/10/19 16:24:44 来源:尧图企业网站定制
一名 Java 后端的 AI 转型实录三Day 3让模型 “说给程序听”AI 转型系列・第 3 篇Day 1 跑通模型调用链路Day 2 实现流式输出与对话记忆。今天要解决一个非常现实的痛点就算大模型回答再通顺返回的是自由大段文本Java 代码很难直接消费。结构化输出让模型输出稳定转化为 Java 对象。为什么需要结构化输出前面两天接口返回的都是String字符串这种返回是给人阅读的。但业务系统中模型输出结果后续要交给程序处理数据库入库、业务分支判断、作为下游组件输入。如果手动让模型输出 JSON再自己调用JSON.parse解析会遇到大量现实问题模型经常额外输出前置说明文字例如 “好的以下是您要的结果”或者输出 markdown 代码块包裹 JSON直接导致 JSON 解析异常。Spring AI 提供了非常方便的能力.call().entity(YourRecord.class)。本篇实战验证该能力同时踩坑梳理花括号带来的三重边界问题。三十行代码entity () 的标准编码姿势我们用贴近业务的示例场景从商品描述文本中抽取商品结构化信息。使用 Java Record 作为接收载体。import java.util.List; /** * 商品抽取实体 */ public record ProductInfo( String name, String category, ListString sellingPoints ) {}编写 Controller 接口import org.springframework.ai.chat.client.ChatClient; 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; RestController RequestMapping(/api) public class ExtractController { private final ChatClient chatClient; public ExtractController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(/extract) public ProductInfo extract(RequestParam String q) { return chatClient.prompt() .user(u - u.text( 从下面的商品描述中抽取结构化信息 - name商品名称 - category商品类目 - sellingPoints卖点列表 商品描述 {desc} ).param(desc, q)) .call() .entity(ProductInfo.class); } }curl 测试命令curl -G http://localhost:8080/api/extract \ --data-urlencode q这款不锈钢保温杯500ml双层真空12小时保温适合通勤户外正常返回结果示例{name:不锈钢保温杯,category:水杯/保温容器,sellingPoints:[双层真空,12小时保温,适合通勤户外]}Record entity()是绝佳组合Record 的字段名直接映射 JSON 的 key不可变对象几乎没有样板代码类定义即数据契约。当需要一次性抽取多条商品顶层是集合要使用ParameterizedTypeReference// 一次性抽取多条商品返回 ListProductInfo .call() .entity(new ParameterizedTypeReferenceListProductInfo() {});⚠️ 重要位置细节entity()调用在.call()之后和.content()属于同级调用。旧的 Milestone 版本教程会把 entity 写在 prompt 链中间GA 正式版 API 已经发生调整不要照搬老文档写法。entity () 底层真相它不是魔法很多同学会误以为 entity 能力是大模型原生功能其实底层逻辑根据传入的 Java 实体类自动生成 JSON Schema同时追加强制格式约束提示词告诉模型只返回 JSON不要解释不要 markdown 代码块拿到模型返回内容后做清洗再反序列化为 Java 对象。可以打印框架自动生成的格式约束提示直观看到框架替我们做的工作BeanOutputConverterProductInfo converter new BeanOutputConverter(ProductInfo.class); // 打印自动生成的schema格式指令 System.out.println(converter.getFormat());生产环境重要认知entity()只能提升模型输出合规 JSON 的概率不是 100% 绝对保证。大模型依旧有小概率输出不符合 schema 的内容。✅ 生产环境三个必备习惯返回对象的业务关键字段手动做判空校验捕获结构化解析异常做好业务降级不要直接抛出 500稳定性敏感业务场景增加一次重试策略。本次简单测试输入无关文本 “今天天气不错”模型返回全部字段为空的合法对象没有崩溃但不能因此放松防护意识结构化输出降低出错概率不等于零风险。花括号三重坑不可信内容的边界问题Prompt 模板使用示例.user(u - u.text(分析这段商品描述\n{desc}) .param(desc, q)){desc}属于模板占位符使用param()填充变量。坑来了用户输入 / 检索文档本身就包含花括号例如文本只售{2599}元会发生什么坑 1Prompt 模板层花括号问题使用.param()传入变量值变量内容里面包含{}不会被模板引擎解析安全隔离。禁止直接字符串拼接把原始内容塞进text()。整理三种常见模板写法行为对比表格场景写法说明运行结果占位符多于参数模板定义 2 个占位符param 只传入 1 个直接运行时报错参数多于占位符param 传入参数但模板没有对应占位符静默直接忽略不会报错隐蔽坑字符串拼接原始输入u.text(分析 q)用户输入直接拼模板字符串q 中{xxx}会被识别为未定义占位符直接抛出异常 RAG 开发高危提醒检索返回的文档内容属于完全不受控的外部文本文档里面很可能包含 JSON、代码片段大量花括号。铁律用户输入、检索文档内容一律通过 param 传入禁止直接拼接进模板字符串坑 2URL 请求层面花括号问题同样的文本只售{2599}元如果直接拼接到 URL 请求参数。Tomcat 会直接拒绝请求Invalid character found in the request target ... The valid characters are defined in RFC 7230 and RFC 3986{}不属于 RFC3986 规定的 URL 允许裸传字符没有做 URL 编码直接放在请求路径 / 参数中Web 容器解析直接失败。解决使用--data-urlencode、前端自动 urlencode、SpringRequestParam自动编码。引申认知后端老经验在 AI 时代的延伸表格不可信数据流向处理手段不可信内容进入 HTTP URL必须做 URL 百分号编码RFC3986不可信内容进入 Prompt 模板使用 param 参数化传参禁止字符串直接拼接不可信内容进入 SQL使用预编译参数#{}防止 SQL 注入不可信内容渲染 HTML 页面HTML 转义防御 XSS核心思想不变所有来自外部的不可信数据进入协议 / 模板层都要做隔离处理。AI 应用开发Prompt 成为新的注入攻击面这一点做 RAG 的时候尤其要时刻牢记。entity () 的 3 种重载怎么选Spring AI1.1.0 中entity()提供 3 种重载区分的核心就是类型信息能不能用普通Class?来表达。// ① 普通Java类 / Record单个对象 .entity(ProductInfo.class) // ② 顶层为泛型集合ListT、MapK,V .entity(new ParameterizedTypeReferenceListProductInfo() {}) // ③ 完全自定义自己实现转换器高级场景才使用 .entity(StructuredOutputConverterT)Java 泛型擦除知识点ListProductInfo.class语法非法运行时 Class 丢失泛型参数。ParameterizedTypeReference通过匿名内部子类把泛型类型信息保存在字节码运行时反射读取。和 SpringMVC RestTemplate 的用法完全同源。选型对照表表格需要得到的返回类型使用方案单个对象Record / POJO内部字段可以包含 List①xxx.class返回顶层直接就是集合 / MapListProductInfo、MapString,Object②ParameterizedTypeReference❗容易混淆点实体内部嵌套 List 集合不属于顶层泛型。例如ProductInfo内部有ListString sellingPoints直接用ProductInfo.class即可。只有返回值最外层本身是 List/Map才需要用ParameterizedTypeReference。⚠️ParameterizedTypeReference后面的大括号{}千万不能省略匿名子类靠这个捕获泛型类型去掉花括号会丢失类型信息导致解析失败。Day3 收获清单✅entity()单对象 顶层集合两种实战写法使用 Record 接收结构化输出✅ Record 作为输出载体类定义即数据契约消除样板代码✅ Prompt 模板最佳实践text()param()参数化传参隔离外部不可信文本✅ 吃透花括号两大坑Prompt 模板占位符、URL 非法字符确立 Prompt 安全铁律✅ 建立新认知Prompt 是 AI 应用中新的注入攻击面✅ 明确结构化输出能力边界仅提升合规概率非绝对保证生产环境三大防护手段判空、异常降级、重试✅ 掌握entity()三个重载的选型逻辑理解ParameterizedTypeReference对抗泛型擦除的原理。ChatClient 层面三件核心武器全部集齐说给模型听人设 / Prompt 模板、模型记住对话会话记忆、模型说给程序听结构化输出。写在最后本篇业务代码量并不大但花括号衍生出来的边界与安全思考把后端传统安全思维延伸到 Prompt 工程是前两篇没有涉及的新维度。继续坚持个人学习纪律每天一个闭环宁可少学不可断链—— Day 3闭环。下一篇正式进入 Embedding 向量与语义检索RAG 的底层地基。从单纯调用大模型进入到给大模型喂私有知识库的阶段。如果你也在转型 Java 后端做 AI 开发欢迎评论区交流你业务场景下希望模型输出哪些复杂的结构化对象本文是「Java 后端 AI 转型实录」系列第 3 篇。项目后续会开源在 GitHub每周更新。

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

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

免费获取报价 →
↑