Spring 官方的叫法是结构化输出,不过我更喜欢沿用 LangChain 的叫法“提取器(Extractor)”。
官方文档(Structured Output Converter)
https://docs.spring.io/spring-ai/reference/1.0/api/structured-output-converter.html
package net.heimeng.web.test;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.heimeng.common.ai.extractor.ExtractorUtils;
import net.heimeng.web.Agent4jWebApplication;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.ParameterizedTypeReference;
import java.util.List;
@Slf4j
@SpringBootTest(classes = Agent4jWebApplication.class)
@DisplayName("AI 单元测试")
public class AiUnitTest {
@Autowired
private ChatModel model;
@DisplayName("AI 配置项测试")
@Test
void propertiesTest() {
// Given
ChatClient client = ChatClient.builder(model).build();
// Ensure the client is able to use
log.debug(client.prompt().user("你好").call().content());
// Result: 你好👋!我是人工智能助手智谱清言,可以叫我小智🤖,很高兴见到你,欢迎问我任何问题。
}
@DisplayName("Extractor 提取器测试")
@Test
void extractorTest() {
// Given
String textAboutPersons = "蔡徐坤(KUN),1998年8月2日出生于浙江省温州市," +
"户籍湖南省吉首市,中国内地男歌手、演员、原创音乐制作人、MV导演。" +
"朱明春的长子朱立科浙农大毕业三年后,针对温州风俗——家逢喜事分红蛋的习惯,研发出了一鸣利市红蛋," +
"深受广大市民的喜爱,成为温州市民办喜事的必需品。之后,朱立科倡导的新型奶吧," +
"改变了部分温州人糯米饭加紫菜汤的早餐习惯。该模式一经推出大受温州人欢迎。" +
"2002年在市区横渎开出第一家一鸣真鲜奶吧直营店,如今,一鸣真鲜奶吧直营店、加盟连锁店在温州地区已发展至近400家。";
String textAboutActors = "Generate the filmography of 5 movies for Tom Hanks and Bill Murray.";
List<Person> persons = ExtractorUtils.extract(textAboutPersons, new ParameterizedTypeReference<>() {});
log.debug(String.valueOf(persons));
Assertions.assertNotNull(persons);
Person person = ExtractorUtils.extract(textAboutPersons, Person.class);
log.debug(person.toString());
Assertions.assertNotNull(person);
record ActorFilms(String actor, List<String> movies) {}
List<ActorFilms> actorFilms = ExtractorUtils.extract(textAboutActors, new ParameterizedTypeReference<>() {});
log.debug(actorFilms.toString());
Assertions.assertNotNull(actorFilms);
}
@Data
private static class Person {
// 私有属性
private String name;
private int age;
private String address;
private List<String> occupations;
}
}
package net.heimeng.common.ai.extractor;
import net.heimeng.common.ai.model.WithDescription;
import net.heimeng.common.core.util.SpringUtils;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.ParameterizedTypeReference;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
/**
* 提取器工具类
*
* @author InwardFlow
*/
public class ExtractorUtils {
private static final ChatClient CLIENT = SpringUtils.getBean(ChatClient.class);
public static <T extends WithDescription> T extractWithDescription(String text, Class<T> clazz) {
try {
T instance = clazz.getDeclaredConstructor().newInstance();
return CLIENT.prompt()
.user(u -> u.text("""
Extract information from:
{text}
---
Here is the description:
{description}
""")
.params(Map.of("text", text, "description", instance.getDescription())))
.call()
.entity(new ParameterizedTypeReference<>() {});
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException("Unable to create an instance of " + clazz.getName(), e);
}
}
public static <T> T extract(String text, Class<T> clazz) {
String className = clazz.getSimpleName();
return CLIENT.prompt()
.user(u -> u.text("""
Extract information about {className} from {text}
""")
.params(Map.of("className", className, "text", text)))
.call()
.entity(clazz);
}
public static <T> T extract(String text, ParameterizedTypeReference<T> typeReference) {
return CLIENT.prompt()
.user(u -> u.text("Extract information from: " + text))
.call()
.entity(typeReference);
}
}