news 2026/9/3 22:28:13

JSON结构化输出实战:用Spring AI打造可靠的简历解析助手

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON结构化输出实战:用Spring AI打造可靠的简历解析助手

很多同学在接触大模型应用开发时,都会遇到一个绕不开的话题:结构化输出。尤其是最近 AI 简历助手、AI 文档解析、Agent 工具调用这类应用越来越火,大家会发现,同样是让大模型干活,有的人做出来的功能稳定可靠,有的人做出来的功能只能“看运气”。这个差距,很大程度上就来自于是否做好了结构化输出。

我在做简历解析助手这个功能时,就踩过不少坑。最开始是直接把一段候选人的工作经历文本丢给大模型,然后在提示词里写“请帮我提取姓名、电话、工作经历”,模型确实能返回内容,但返回格式非常不稳定——有时候是 Markdown 表格,有时候是一段带星号的文本,还有时候在 JSON 外面额外补一句“以下是提取结果”。为了兼容这些情况,解析层被迫写了一大堆补丁逻辑,代码又乱又难维护。

后来我把方案改成了 JSON 结构化输出,再配合实体类和校验逻辑,整个流程一下子变得清晰可控。这篇文章我会从“为什么需要结构化输出”讲起,解释 JSON 结构化输出的核心概念,然后带你完整实现一个“简历助手”项目实战,帮你理解实体类如何定义、提示词如何设计、返回结果如何验证,最后分享常见报错的排查思路和工程化建议。

本文适合两类读者:一类是刚接触大模型开发,想搞清楚结构化输出到底是怎么回事的新手;另一类是有一定开发经验,准备在项目中落地 LLM 信息抽取、文档解析、报表生成等功能的开发者。读完你不仅能理解结构化输出的原理,还能直接复制代码跑通一个简历解析助手。

1. 为什么需要结构化输出?

1.1 大模型原生输出的痛点

大模型本质上是一个文本生成模型,它接收文本,也输出文本。对于闲聊、问答、写文章这类场景来说,自然语言输出是最合适的。但当你需要把模型输出交给程序继续处理时,自然语言反而是最大的障碍。

举个很简单的例子,你想让模型从一段简历文本中提取出姓名和电话。模型可能输出:

候选人叫张三,联系电话是 13800138000,邮箱是 zhangsan@example.com。

这段文字人类一眼就能看懂,但程序想要把“张三”和“13800138000”从这句话里准确切出来,就需要写正则、做关键词匹配,而且一旦模型换了一种表达方式,比如“这位候选人是张三,可以通过 13800138000 联系到他”,正则就很有可能匹配失败。

这就是大模型原生输出的三个典型痛点:

  • 格式不统一,同一份输入每次返回的表达方式都可能不同;
  • 经常夹带解释性文字,把数据混在自然语言里;
  • 程序解析成本高,容易出错,且难以穷尽所有情况。

在项目开发中,我们希望模型“输出的内容能直接被程序消费”,也就是说,程序拿到结果后可以一行代码解析成对象,直接落库、渲染或者传给下游系统。结构化输出就是为了解决这个问题。

1.2 结构化输出是什么

结构化输出,简单来说,就是让大模型按照我们预先定义好的结构和格式来返回内容。最常见的形式是 JSON,也可以是 XML、YAML 或者一段符合特定模板的文本。

在 LLM 应用开发语境下,结构化输出通常包含两层含义:

第一层是“格式约束”,也就是要求模型只输出合法 JSON,不要输出解释性文字,不要使用 Markdown 代码块包裹。

第二层是“结构约束”,也就是我们提前定义好 JSON 里有哪些字段、每个字段的类型是什么、哪些是必填项,模型必须按照这个 Schema 来输出。

只做到第一层,模型偶尔还是会漏字段、填错类型;做到第二层,输出质量才会真正稳定。你可以把结构化输出理解为给大模型发了一张“填空题答题卡”,而不是让它自由发挥写作文。

1.3 典型应用场景

结构化输出在真实项目中的应用非常广,这里列出几个最常见的场景:

  • 信息抽取:从简历、合同、发票、物流单等文本中提取关键字段,这是本文实战项目的核心场景;
  • 数据转换:把一段非结构化的描述转换成标准格式,再写入数据库或发送给下游系统;
  • Agent 工具调用:让模型决定调用哪个函数、传入什么参数,必须严格遵循工具定义的 JSON Schema;
  • RAG 查询条件抽取:在知识库问答中,从用户问题里提取查询条件,转成结构化查询参数;
  • 报表生成:让模型把统计数据转换成前端可以直接渲染的 JSON 结构。

你会发现,这些场景都有一个共同点:模型只是处理链路中的一环,处理结果要被后续程序继续使用。只要程序需要处理模型输出,结构化输出就不是“可选项”,而是“必选项”。

2. JSON 结构化输出的核心概念

2.1 JSON 格式基础

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,结构简单,几乎所有编程语言都内置了解析支持,所以它成了大模型结构化输出的事实标准。

一个 JSON 对象由键值对组成,键是字符串,值可以是字符串、数字、布尔值、数组、嵌套对象或者 null。比如:

{ "name": "张三", "age": 28, "skills": ["Java", "Spring Boot"], "address": { "city": "北京", "street": "朝阳区某街道" } }

在 Java 中可以用 Jackson、Gson 解析,在 Python 中可以用json模块解析,前端更是原生支持。这也是为什么大家都选择 JSON 作为大模型输出的载体——解析成本几乎为零,且与业务对象可以非常方便地相互转换。

2.2 结构化输出的几种实现方式

在具体实现上,让大模型输出结构化内容主要有三种方式,它们的约束强度依次递增。

第一种是纯提示词约束。你在 system prompt 里写“请只输出 JSON,不要输出其他内容”,然后自己用JSON.parseObjectMapper解析。这种方式实现成本最低,任何模型都支持,但稳定性最差,模型偶尔还是会“不听话”,输出一些额外文字。适合原型验证和临时脚本。

第二种是开启 JSON Mode。OpenAI 的 API 支持response_format={"type": "json_object"}参数,强制模型输出合法 JSON;OpenAI 兼容接口的厂商大多也支持这个参数。这种方式能保证输出是合法 JSON,但不能保证 JSON 里面真的有你要的所有字段。

第三种是 Function Calling 或 Structured Output。模型根据你定义的函数参数 Schema 来输出结构化内容,平台会尽可能严格地按 Schema 校验结果。这是目前生产环境最推荐的方式。在 Spring AI 这类框架中,我们直接传一个实体类进去,框架会自动把实体类转换成 Schema 描述,并解析模型返回的内容。

下表对比了三种方式的差异:

方式输出稳定性字段约束实现成本推荐场景
纯提示词较低临时脚本、原型验证
JSON Mode较高信息抽取、数据转换
Function Calling / Structured Output生产环境、Agent 工具调用

2.3 选型建议

在实际项目中,我的建议是:如果模型平台支持 JSON Mode 或 Structured Output,优先使用,不要把稳定性赌在提示词上。提示词仍然要写,但它是“兜底约束”,不是“唯一约束”。

另外要注意,“结构化输出”不等于“输出格式是 JSON 就行”。模型返回了合法 JSON,但字段名和你定义的不一致,或者数组元素类型不对,程序解析时一样会出错。所以在设计阶段就要把实体类、字段名、字段类型想清楚,这也是后面实战部分要重点讲的内容。

3. 项目实战:简历助手环境准备

3.1 项目需求与功能拆分

本文要实现的“简历助手”功能很简单:用户输入一段纯文本简历,系统调用大模型,自动提取出候选人姓名、联系方式、技能、工作经历、教育经历等信息,并输出结构化的 JSON 数据。

功能拆分如下:

  • 接收原始简历文本;
  • 调用大模型进行信息抽取;
  • 按照预定义的实体类解析模型输出;
  • 把解析结果返回给调用方。

整个项目不涉及数据库和前端,聚焦在“结构化输出”这条主线上,方便你只看核心逻辑。

3.2 技术栈与版本说明

本文示例使用 Java + Spring Boot + Spring AI 实现,这套组合在目前的企业级项目中很常见,而且 Spring AI 提供了比较完善的结构化输出支持。

版本说明如下:

  • Java 17;
  • Spring Boot 3.x;
  • Spring AI 1.x(以你实际引入的版本为准);
  • Maven 3.8+;
  • 支持 OpenAI 兼容协议的大模型接口。

如果你的项目使用的是 Python 技术栈,也不用担心,我在后面会单独给出一段 Python + OpenAI SDK 的参考实现,原理完全一致。实际开发时版本需要根据你的项目情况调整,本文重点演示的是实现思路。

3.3 创建项目并引入依赖

先创建一个 Spring Boot 项目,并在pom.xml中引入 Web 和 Spring AI 的 OpenAI Starter 依赖:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>resume-assistant</artifactId> <version>1.0.0</version> <name>resume-assistant</name> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> </dependencies> </project>

Spring AI 的版本迭代比较快,不同版本间的 API 命名可能有差异,引入依赖时建议以官方 Maven 仓库中的最新稳定版本为准。

接着在application.yml中配置模型接口信息:

server: port: 8080 spring: ai: openai: base-url: https://api.openai.com api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2

这里需要说明几点:

  • api-key通过环境变量OPENAI_API_KEY注入,不要把密钥硬编码在配置文件里;
  • 如果你接入的是阿里云百炼、DeepSeek、智谱、Kimi 等平台,只要它们提供 OpenAI 兼容接口,就把base-url换成对应地址即可;
  • temperature设置为 0.2,是为了让模型输出更稳定,减少随机性,信息抽取类任务建议保持较低温度。

4. 定义输出契约:实体类设计

4.1 为什么先定义实体而不是先写提示词

很多初学者做结构化输出时,习惯先写提示词,告诉模型“你要输出姓名、电话、邮箱……”,然后再写解析逻辑。这个顺序其实是反的。

更推荐的做法是:先把实体类定义好,让实体类成为“输出契约”。理由有三个:

第一,实体类本身就是 Schema,字段名、字段类型一目了然,模型和程序共用同一份契约;

第二,使用 Spring AI 的entity()方法时,框架会读取实体类结构,自动生成格式说明注入到提示词中,你不需要在提示词里手写一遍 JSON 示例;

第三,实体类可以直接交给 Jackson 解析,模型输出和 Java 对象之间的映射关系完全由注解控制,出错时也容易排查。

4.2 简历实体类完整代码

src/main/java/com/example/resumeassistant/model目录下创建三个实体类。

首先是简历主类Resume.java

package com.example.resumeassistant.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public record Resume( @JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("years_of_experience") Integer yearsOfExperience, @JsonProperty("skills") List<String> skills, @JsonProperty("work_experience") List<WorkExperience> workExperience, @JsonProperty("education") List<Education> education ) { }

然后是工作经历类WorkExperience.java

package com.example.resumeassistant.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public record WorkExperience( @JsonProperty("company") String company, @JsonProperty("title") String title, @JsonProperty("start_date") String startDate, @JsonProperty("end_date") String endDate, @JsonProperty("description") String description ) { }

最后是教育经历类Education.java

package com.example.resumeassistant.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public record Education( @JsonProperty("school") String school, @JsonProperty("degree") String degree, @JsonProperty("major") String major, @JsonProperty("start_date") String startDate, @JsonProperty("end_date") String endDate ) { }

这里使用 Java record 来定义实体类,代码非常简洁。如果你的项目还在使用传统 JavaBean,效果也是一样的,重点是每个字段上的@JsonProperty注解,它决定了模型输出的 JSON 字段名。

4.3 字段命名与 JSON 映射注意事项

在字段命名上,有一个很常见的坑:你的 Java 实体类字段名通常使用 camelCase,但模型输出的 JSON 字段名如果也使用 camelCase,在中文大模型场景下问题不大。不过,如果你希望 JSON 字段名使用 snake_case(下划线风格),就必须显式地用@JsonProperty声明。

举个例子,如果实体类里写:

public record WorkExperience( String company, String startDate ) { }

Jackson 默认会把startDate序列化成startDate,但很多模型在训练数据里更习惯start_date这种表达。如果不对齐,模型输出的start_date就无法正确映射到 Java 字段上,解析结果会是 null。

另外一个和 Jackson 命名有关的经典问题是:如果 JavaBean 的字段以大写字母开头,比如URLQRCode,Jackson 默认命名策略会把首字母小写,导致 JSON 里变成urlqrCode。遇到这种情况,同样是用@JsonProperty显式指定 JSON 字段名来解决。

所以我建议:字段命名策略一旦确定,就在实体类上用@JsonProperty写清楚,不要让框架猜测。

还有一点要注意,@JsonIgnoreProperties(ignoreUnknown = true)可以忽略模型输出中多出来的字段。模型有时候会自作主张多输出一个字段,如果不加这个注解,Jackson 解析时可能直接报错,加了之后就能安全忽略多余字段,增强兼容性。

5. 核心实现:提示词设计与调用 LLM

5.1 提示词设计

虽然有了实体类作为 Schema,提示词仍然需要精心设计。Spring AI 的entity()方法会自动把实体类格式说明注入到提示词里,但提取规则、缺失值处理、日期格式这些“业务语义”还是需要我们自己写清楚。

看一下本项目使用的 system prompt:

private static final String SYSTEM_PROMPT = """ 你是一名专业的简历解析助手。 用户会提供一段候选人简历文本,你需要从中提取以下字段: 姓名、手机号、邮箱、工作年限、技能列表、工作经历列表、教育经历列表。 要求: 1. 如果字段在原文本中不存在,统一填空字符串或空数组。 2. 只输出 JSON 本身,不要输出 Markdown 代码块,不要输出任何解释文字。 3. 日期格式统一为 yyyy-MM,例如 2020-07;如果只有年份,则补充为 yyyy-01。 """;

这段提示词有几个关键点:

  • 明确告诉模型“缺失字段填空字符串或空数组”,避免模型编造数据;
  • 强调“只输出 JSON 本身”,这是和“纯提示词方案”之间的最后一道防线;
  • 对日期格式做了归一化要求,方便下游存储和排序。

注意,System prompt 里不需要重复实体类的每个字段,因为 Spring AI 的entity()会帮我们把字段结构加入提示词。你只需要补充提取规则即可。

5.2 编写 ResumeAnalysisService

src/main/java/com/example/resumeassistant/service目录下创建ResumeAnalysisService.java

package com.example.resumeassistant.service; import com.example.resumeassistant.model.Resume; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class ResumeAnalysisService { private static final String SYSTEM_PROMPT = """ 你是一名专业的简历解析助手。 用户会提供一段候选人简历文本,你需要从中提取以下字段: 姓名、手机号、邮箱、工作年限、技能列表、工作经历列表、教育经历列表。 要求: 1. 如果字段在原文本中不存在,统一填空字符串或空数组。 2. 只输出 JSON 本身,不要输出 Markdown 代码块,不要输出任何解释文字。 3. 日期格式统一为 yyyy-MM,例如 2020-07;如果只有年份,则补充为 yyyy-01。 """; private final ChatClient chatClient; private final ObjectMapper objectMapper; public ResumeAnalysisService(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) { this.chatClient = chatClientBuilder.build(); this.objectMapper = objectMapper; } public Resume analyze(String rawResumeText) { return chatClient.prompt() .system(SYSTEM_PROMPT) .user(rawResumeText) .call() .entity(Resume.class); } public String analyzeToJson(String rawResumeText) { Resume resume = analyze(rawResumeText); try { return objectMapper.writeValueAsString(resume); } catch (Exception e) { throw new IllegalStateException("序列化解析结果失败", e); } } }

核心代码只有analyze方法这一小段。entity(Resume.class)是 Spring AI 结构化输出的关键方法,它会自动完成两件事:把Resume类的结构描述注入到提示词中,然后把模型返回的 JSON 反序列化成Resume对象。

这里有一个重要的设计细节:analyze返回的是类型安全的Resume对象,而不是一个字符串。这意味着业务代码里不需要再写任何 JSON 解析逻辑,直接调用对象上的 getter(对 record 来说是 accessor 方法)就能拿到数据。

5.3 接入控制器与运行入口

为了让接口可以被外部调用,我们再加一个简单的 REST 控制器。先定义一个请求体 DTO:

package com.example.resumeassistant.model; public record AnalyzeRequest(String text) { }

然后创建ResumeAssistantController.java

package com.example.resumeassistant.controller; import com.example.resumeassistant.model.AnalyzeRequest; import com.example.resumeassistant.model.Resume; import com.example.resumeassistant.service.ResumeAnalysisService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/resume") public class ResumeAssistantController { private final ResumeAnalysisService resumeAnalysisService; public ResumeAssistantController(ResumeAnalysisService resumeAnalysisService) { this.resumeAnalysisService = resumeAnalysisService; } @PostMapping("/analyze") public Resume analyze(@RequestBody AnalyzeRequest request) { return resumeAnalysisService.analyze(request.text()); } }

启动 Spring Boot 应用后,调用POST /api/resume/analyze,请求体是一个 JSON 对象,里面包含待解析的简历文本。

5.4 Python 参考实现

如果你的项目是 Python 技术栈,可以参考下面这段代码。它使用 OpenAI SDK,开启 JSON Mode,并手动解析结果:

import json from openai import OpenAI client = OpenAI() # 自动读取环境变量 OPENAI_API_KEY SYSTEM_PROMPT = """ 你是一名专业的简历解析助手。 用户会提供一段候选人简历文本,你需要从中提取指定字段。 如果字段在原文本中不存在,统一填空字符串或空数组。 只输出 JSON 本身,不要输出 Markdown 代码块,不要输出任何解释文字。 """ def analyze_resume(raw_text: str) -> dict: resp = client.chat.completions.create( model="gpt-4o-mini", temperature=0.2, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": raw_text}, ], ) content = resp.choices[0].message.content return json.loads(content)

两者思路完全一致:先定义输出契约,再设计提示词,最后解析成结构化数据。差别只在于 Java 用实体类定义 Schema,Python 用response_format参数约束输出为 JSON。

6. 运行与验证

6.1 准备测试输入

下面给出一段模拟的简历文本,方便你直接测试:

姓名:张三 手机:13800138000 邮箱:zhangsan@example.com 工作年限:5年 技能:Java、Spring Boot、MySQL、Redis、Kubernetes、Docker 工作经历: 2020.07 - 至今 某互联网科技有限公司 高级后端开发工程师 负责核心交易系统的设计与开发,主导订单服务重构,将接口性能提升 40%。 2017.06 - 2020.06 某某软件有限公司 后端开发工程师 参与企业内部 ERP 系统的开发和维护,负责库存模块与报表模块。 教育经历: 2013.09 - 2017.06 某某大学 本科 计算机科学与技术

调用接口的方式如下:

curl -X POST http://localhost:8080/api/resume/analyze \ -H "Content-Type: application/json" \ -d '{"text":"姓名:张三\n手机:13800138000\n邮箱:zhangsan@example.com\n工作年限:5年\n技能:Java、Spring Boot、MySQL、Redis、Kubernetes、Docker\n\n工作经历:\n2020.07 - 至今 某互联网科技有限公司 高级后端开发工程师\n负责核心交易系统的设计与开发,主导订单服务重构,将接口性能提升 40%。\n2017.06 - 2020.06 某某软件有限公司 后端开发工程师\n参与企业内部 ERP 系统的开发和维护,负责库存模块与报表模块。\n\n教育经历:\n2013.09 - 2017.06 某某大学 本科 计算机科学与技术"}'

6.2 预期 JSON 输出

正常情况下,接口会返回类似下面的 JSON:

{ "name": "张三", "phone": "13800138000", "email": "zhangsan@example.com", "years_of_experience": 5, "skills": ["Java", "Spring Boot", "MySQL", "Redis", "Kubernetes", "Docker"], "work_experience": [ { "company": "某互联网科技有限公司", "title": "高级后端开发工程师", "start_date": "2020-07", "end_date": "至今", "description": "负责核心交易系统的设计与开发,主导订单服务重构,将接口性能提升 40%。" }, { "company": "某某软件有限公司", "title": "后端开发工程师", "start_date": "2017-06", "end_date": "2020-06", "description": "参与企业内部 ERP 系统的开发和维护,负责库存模块与报表模块。" } ], "education": [ { "school": "某某大学", "degree": "本科", "major": "计算机科学与技术", "start_date": "2013-09", "end_date": "2017-06" } ] }

这就是结构化输出的价值:程序收到这串 JSON 后,不需要任何额外解析,直接就能映射到实体对象,存数据库、渲染到前端都可以。

6.3 怎么判断解析质量

判断一次解析是否成功,不能只看“有没有返回 JSON”,还要看字段级质量。建议你做三件事:

第一,检查字段完整性。namephone是简历的核心字段,缺失率应该接近 0,如果经常缺失,说明提示词或 Schema 设计有问题。

第二,检查字段类型。years_of_experience应该是整数而不是字符串“5年”,这也是为什么实体类里用Integer而不是String

第三,构建一个包含 20 份不同格式简历的测试集,跑一遍统计每个字段的缺失率和错误率。数据比感觉可靠,如果你准备把简历助手做到生产环境,这一步不能省。

7. 常见问题与排查思路

7.1 高频错误一览

结合我自己的开发经验,结构化输出最常见的几类问题如下:

问题现象常见原因解决思路
解析 JSON 时报错,提示 Content 不是合法 JSON模型输出了 Markdown 代码块,或夹带了说明文字在提示词中强调“只输出 JSON 本身”;开启 JSON Mode;忽略外层 ```json 标记
某个字段经常为 null原文本没有该信息,或字段语义不明确在提示词中规定缺失值处理方式;检查实体类字段名与提示词描述是否一致
返回字段名和实体类不一致字段名大小写问题、命名策略不一致统一使用@JsonProperty显式声明 JSON 字段名
数值字段被识别成字符串Schema 中未说明字段类型,或模型对文本理解有误实体类中使用IntegerLong等强类型;在提示词中补充格式说明
中文乱码请求响应编码不一致确保消息请求和响应统一使用 UTF-8 编码
输出不稳定,相同输入返回不同结果temperature 设置过高信息抽取类任务建议temperature设为 0 到 0.2
JavaBean 大写字母开头的字段在 JSON 中变成小写Jackson 默认命名策略导致通过@JsonProperty显式指定 JSON 字段名

7.2 排查清单

如果你遇到解析失败,可以按下面顺序排查:

  1. 先看模型原始返回内容。记住一定不要只记录解析失败后的异常信息,要把模型最原始的返回值打到日志里。很多时候,问题一眼就能看出来——返回了 Markdown、字段名不对、多了注释文字。
  2. 确认 JSON 字段名和实体类注解是否完全一致,特别注意大小写和下划线。
  3. 确认实体类字段类型是否合理。模型把“5年”识别成字符串,而实体类期望数字,解析就会失败。
  4. 确认是否开启了 JSON Mode 或 Structured Output。如果只是依赖提示词,稳定性会明显差很多。
  5. 检查 temperature。太高会让模型“自由发挥”,建议降到 0.2 以下。

8. 最佳实践与工程建议

8.1 输出 Schema 设计

实体类就是输出 Schema,设计时要注意几点:

字段要尽量扁平,不要过度嵌套,嵌套层级越多,模型出错的概率越高。一个招聘系统需要候选人技能列表,直接用List<String>就够,不要设计成List<Skill>加一堆子属性。

字段名要见名知义,避免用abtmp这类无法理解的命名。因为模型会读取字段名来理解“要提取什么”,字段名越清晰,提取越准确。

给字段添加合理的类型约束。固定枚举值尽量用字符串,并可以在提示词里限定取值范围,比如“degree 只允许填写:高中、大专、本科、硕士、博士”。

8.2 提示词工程

不要因为用了 Structured Output 就忽略提示词。框架能保证格式,但不能保证业务规则。以下三条建议非常重要:

第一,把 system prompt 和 user 输入分开。不要让用户输入直接拼接进 system prompt,否则容易引入提示词注入风险。

第二,对缺失值做明确约定。你希望缺失时填空字符串还是 null?希望日期归一化成什么格式?这些都要写清楚,否则模型会自行发挥。

第三,在提示词里加一个“只输出 JSON 本身”的强调。即使有 JSON Mode 兜底,这句话也能减少模型输出说明文字的概率。

8.3 异常处理与重试

结构化输出不是 100% 可靠的,生产环境必须处理解析失败的情况。

解析失败时,建议先捕获异常,记录模型原始输出日志,然后重试一次。重试时可以稍微调整提示词,比如强调“不要输出代码块”。如果连续两次失败,就走人工兜底或返回友好错误提示,不要让调用方直接看到堆栈。

同时,对解析成功的结果也要做业务校验。比如电话字段要符合手机号格式,邮箱要包含@符号,必填字段不能为空。校验失败时同样要记录日志并处理。

8.4 安全与隐私

简历数据属于个人敏感信息,涉及隐私合规,生产环境中必须注意:

  • 不要记录完整的原始简历文本日志,必要时对姓名、电话、邮箱做脱敏处理;
  • 最小化存储,只保留业务真正需要的字段;
  • 调用大模型时,如果使用的是第三方 API,要遵循最小化原则,避免传输与解析任务无关的敏感字段;
  • 密钥通过环境变量或配置中心管理,严禁提交到代码仓库。

8.5 性能与成本

信息抽取类任务建议把temperature调低,这样得到稳定结果的概率更高,也减少了重试带来的额外成本。除此之外,还可以考虑以下优化:

  • 如果模型支持更小的型号,先用小模型跑通流程,再评估是否需要升级;
  • 对相同或相似的输入做缓存,避免重复调用;
  • 精简 system prompt,减少输入 token;
  • 在业务低峰期做批量解析,而不是实时逐个调用。

9. 下一步可以继续做的事

本文的简历助手只是一个最小可运行版本,跑通之后你可以沿着下面几个方向继续扩展。

首先是接入数据库。把解析出来的Resume对象落库,然后提供查询候选人列表、搜索技能匹配的接口,这就从一个解析工具变成了真正的简历管理系统。

其次是支持文件上传。把接口入参从纯文本改成 PDF、Word 简历文件上传,先用文件解析工具提取文本,再交给大模型做结构化抽取,这个能力在招聘系统里非常实用。

然后是增加多轮追问。如果解析结果缺失了手机号,可以基于已有的结构化结果,向用户追问“请补充手机号”,再做一次补全,而不是直接返回空值。

最后,你还可以把这段经验迁移到其他信息抽取场景,比如合同字段抽取、发票识别、工单解析。原理

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 22:23:28

前端录制回放优化:解决JSON体积、回放卡顿与密码明文泄露问题

从用户那里拿到一个有点“惨烈”的需求&#xff1a;做一个用户操作录制回放&#xff0c;目的是复现问题、分析用户行为。结果系统上线后遇到三个现象——录出来的 JSON 文件比录屏视频还大&#xff1b;回放时浏览器卡成幻灯片&#xff1b;更让人冒冷汗的是&#xff0c;用户在某…

作者头像 李华
网站建设 2026/9/3 22:18:19

论文AI工具怎么选?初稿用大模型,定稿我交给毕业之家

又到毕业季&#xff0c;身边学弟学妹问得最多的一句话是&#xff1a;“论文到底用什么AI工具改&#xff1f;” 但2026年的现实是&#xff0c;这个问题早就没有统一答案了。现在高校和期刊卡的是两道线&#xff1a;重复率 AIGC率。多少同学重复率好不容易磨到8%&#xff0c;AIG…

作者头像 李华
网站建设 2026/9/3 22:14:55

GTA5兵不厌诈攻略:除虫大师布点与双人拿画消防员离场流程

之前做名钻赌场豪劫的“兵不厌诈”时&#xff0c;我们固定队里最怕两个环节&#xff1a;一个是除虫大师信号干扰器没放好导致全程被摄像头锁定&#xff0c;另一个是双人拿画阶段总是慢半拍触发警报。后来把细节理顺之后&#xff0c;整场下来基本可以做到无警撤离&#xff0c;连…

作者头像 李华
网站建设 2026/9/3 22:13:45

别再手动改参考文献❗OKBIYE一键规范|彻底告别格式报错✅

谁懂参考文献才是论文最折磨人的地方&#xff01;&#x1f62d; 正文写得再完美&#xff0c;最后全栽在参考文献上&#xff1a;格式乱七八糟、标点错乱、中英文不统一、页码缺失、引用格式不对、导师反复打回。 手动一条条改、逐条核对&#xff0c;耗一下午时间&#xff0c;改…

作者头像 李华
网站建设 2026/9/3 22:12:27

站长必备SQLiteBrowser数据库浏览器单文件免安装绿色无广告202608

站长必备SQLiteBrowser数据库浏览器单文件免安装绿色无广告-20260831 下载&#xff1a;https://download.csdn.net/download/YUJIANYUE/93365116 这是一款**Win7风格、免安装**的SQLite数据库浏览工具&#xff0c;主打“开箱即用”。无需配置环境&#xff0c;下载后双击即可运…

作者头像 李华