原文是"尚未使用Redis",模型返回了技能Redis,引用也正好是这句话。JSON能解析,字段没有缺,引用确实在原文里------可如果功能目标是提取实际使用过的技能,这条结果仍然不成立。
做AI抽取时,结构化输出值得用,但结构正确与业务正确要分开检查。本文先用一段不调用远程模型的Java程序,演示后端可以确定性检查什么,以及哪里会漏掉语义问题。
先明确提取规则
教学功能只接受Java、MySQL、Redis三种技能,要求为每一项返回原文依据。结构可以是:
json
{"skill":"Java","quote":"使用Java处理客户数据"}
它只是输出形状。要认为技能成立,还需要满足业务定义,比如"原文明确描述实际使用",而不只是"文本提到了技术名字"。
Spring AI的结构化输出说明讨论类型映射与模式验证;输出转换器说明提示转换是尽力而为。本文不依赖这些版本新增的调用方法,只处理已经解析成Java对象的候选结果。
把确定能检查的条件写成代码
java
record Candidate(String skill, String quote) {}
第一道检查是字段与范围:技能不能是空值或白名单外的名字,引用不能缺失。第二道检查是引用是否原样出现在当前输入中。后者能拦住"精通Redis集群"这种输入里根本不存在的句子。
检查失败时,不要直接把候选结果写进简历。可以返回具体错误,允许重试或人工修改。日志也应避免默认保存完整敏感原文,按任务需要保留脱敏样本和定位信息。
完整程序将错误收集成列表。实际接口还应限制输入长度和候选条数;本文用几条固定教学数据,未实现接入层限额,也不提供通用模型客户端。
"引用存在"为什么仍然不够
程序特意保留一个反例:Redis + 尚未使用Redis 会通过白名单和子串检查。打印结果里的 negation=passesMechanicalCheck 就是这个现象,不是一个应该删除的测试失败。

这提醒我们要再定义事实层的验收:否定句、计划、学习经历与实际使用是否分别处理?引用是否真的支持所提取字段,而不是只在旁边碰巧出现?多个候选是不是重复或冲突?
单靠搜索关键词"未"也解决不了全部语义:双重否定、跨句指代和复杂表达都会增加情况。高风险的自动决策不应因为前两道检查通过就直接执行。当前小功能可以保留候选和依据,让用户确认,遇到不能支持的字段就允许为空。
用小样本做回归,不编整体准确率
本地断言覆盖:正常技能与引用;白名单外的技能;编造引用;缺失列表;否定句机械检查通过。最后一项记录的是能力边界。
接真实模型后,应固定一批脱敏输入与期望结果,修改提示词、模型或校验规则时逐条比较。五六条输入只能说明这些样本,不能扩写成"准确率达到多少、适用于所有简历"。
把以下内容保存为 EvidenceLab.java,使用JDK21运行:
shell
javac -encoding UTF-8 -d out EvidenceLab.java
java -cp out demo.EvidenceLab
预期:
text
valid=accepted; unknownSkill=rejected; inventedQuote=rejected; missingList=rejected; negation=passesMechanicalCheck; checks=5
java
package demo;
import java.util.*;
public class EvidenceLab {
record Candidate(String skill,String quote) {}
static List<String> validate(String source,List<Candidate> values) {
var errors=new ArrayList<String>();
Set<String> allowed=Set.of("Java","MySQL","Redis");
if(values==null)return List.of("技能列表缺失");
for(int i=0;i<values.size();i++){
var v=values.get(i);
if(v==null){errors.add(i+":条目缺失");continue;}
if(!allowed.contains(v.skill()==null?"":v.skill()))errors.add(i+":技能不在允许范围");
if(v.quote()==null||v.quote().isBlank()||!source.contains(v.quote()))errors.add(i+":引用未在原文找到");
}
return errors;
}
static void eq(Object e,Object a){if(!e.equals(a))throw new AssertionError(e+" != "+a);}
public static void main(String[] args){
String source="使用Java处理客户数据,使用MySQL保存结果。尚未使用Redis。";
eq(List.of(),validate(source,List.of(new Candidate("Java","使用Java处理客户数据"))));
eq(1,validate(source,List.of(new Candidate("Kafka","使用Java处理客户数据"))).size());
eq(1,validate(source,List.of(new Candidate("Redis","精通Redis集群"))).size());
eq(1,validate(source,null).size());
// 这条会通过机械校验,但语义是"未使用",不能解释成掌握该技能。
eq(List.of(),validate(source,List.of(new Candidate("Redis","尚未使用Redis"))));
System.out.println("valid=accepted; unknownSkill=rejected; inventedQuote=rejected; missingList=rejected; negation=passesMechanicalCheck; checks=5");
}
}
Java接入AI以后,接下来值得补的通常是这些可以检查的环节:输入范围、候选结果、证据和失败处理。把它们说明白,项目就不只剩一个"调用模型"的按钮。