Spring AI Structured-Output源码分析
- 在 Spring AI 中,StructuredOutputConverter(常用的实现类是 BeanOutputConverter)的核心作用是将大模型的非结构化文本输出 转换为 Java 的结构化对象(POJO)。
- StructuredOutputConverter extends Converter<String, T>, FormatProvider
- FormatProvider:负责将你 Java Bean 的 JSON Schema
- Converter:负责将模型返回的字符串 通过 Jackson 反序列化转换为你的Java Bean
- 其实结构化对象本质实现原理也是基于:JSON 解析器 + Prompt 工程 + 结果解析 机制
实现思考&源码分析
如何告知大模型需要转换格式?
- 原理
StructuredOutputConverter 接口有一个方法 getFormat()。当你调用这个方法时,它会生成一段自然语言的指令 ,这段指令中包含了你 Java Bean 的 JSON Schema。你需要手动或自动将这段指令拼接到发给 AI 的 Prompt 中。
- 源码
- 将你 Java Bean 的格式为 JSON Schema,并放到请求上下文中
java
@GetMapping("/chat-format")
public BeanEntity simpleChatFormat(@RequestParam(value = "query", defaultValue = "写诗歌") String query) {
return chatClient.prompt(query).call()
.format(converter.getFormat()) // 显式注入格式指令
.entity(BeanEntity.class) // 这里内部会自动调用 format()
;
}
//org.springframework.ai.chat.client.DefaultChatClient.DefaultCallResponseSpec#entity(java.lang.Class<T>)
public <T> T entity(Class<T> type) {
var outputConverter = new BeanOutputConverter<>(type);
return doSingleWithBeanOutputConverter(outputConverter);
}
@Nullable
private <T> T doSingleWithBeanOutputConverter(StructuredOutputConverter<T> outputConverter) {
//1. 根据泛型 T 的类型,生成标准的 JSON Schema:详见BeanOutputConverter.getFormat()
//2. 将生成的生成标准的 JSON Schema放入到上下文中,key为:"spring.ai.chat.client.output.format"
var chatResponse = doGetObservableChatClientResponse(this.request, outputConverter.getFormat())
.chatResponse();
var stringResponse = getContentFromChatResponse(chatResponse);
//3.大模型返回的字符串反序列化为JavaBean,详见:BeanOutputConverter.convert()
return outputConverter.convert(stringResponse);
}
- 将 JSON Schema添加到Prompt提示词中,在Advisor链中最后一个ChatModelCallAdvisor实现
java
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
//1.增强格式指令,即提示词增强
ChatClientRequest formattedChatClientRequest = augmentWithFormatInstructions(chatClientRequest);
ChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());
return ChatClientResponse.builder()
.chatResponse(chatResponse)
.context(Map.copyOf(formattedChatClientRequest.context()))
.build();
}
private static ChatClientRequest augmentWithFormatInstructions(ChatClientRequest chatClientRequest) {
//2.从请求上下文获取JSON Schema格式指令
String outputFormat = (String) chatClientRequest.context().get(ChatClientAttributes.OUTPUT_FORMAT.getKey());
//3.其实逻辑很简单:用户文本 + 换行 + JSON Schema格式指令
Prompt augmentedPrompt = chatClientRequest.prompt()
.augmentUserMessage(userMessage -> userMessage.mutate()
.text(userMessage.getText() + System.lineSeparator() + outputFormat)
.build());
return ChatClientRequest.builder()
.prompt(augmentedPrompt)
.context(Map.copyOf(chatClientRequest.context()))
.build();
}
大模型返回结果后是如何转换为对应的实体?
- 原理
转换发生在应用层(你的服务内部),而不是模型层。模型返回的仍然是 String(一段 JSON 文本),Spring AI 在接收到这段文本后,调用 convert() 方法将其反序列化,详见:BeanOutputConverter.convert()。
- 源码
java
public T convert(@NonNull String text) {
try {
// 1. 预处理:清洗文本
// 大模型经常喜欢返回 ```json { ... } ```这种 Markdown 格式
// 这里会去除首尾的 Markdown 标记,只保留纯 JSON 字符串
text = this.textCleaner.clean(text);
// 2. 反序列化
// 使用 Jackson 的 ObjectMapper 将 JSON 字符串转为 Java 对象
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
注意事项
- 如果你使用了 StructuredOutputConverter(或者 .entity() 方法),请不要在你的 User Prompt 中再手写任何关于格式(Format)的指令,避免提示词冲突,只关注业务内容即可。