09.ChatClient使用说明

一灰灰blogSpringAISpringSpringAI约 2401 字大约 8 分钟

09.ChatClient使用说明

SpringAI中,ChatModel作为与大模型交互的具体实现,更上一层的应用推荐则是使用ChatClient,特别是在结构化输出、多轮对话的场景,ChatClient提供了更方便的调用方式

如结构化输出,两者的写法对比如下

// 结构化返回场景:
// chatModel方式
BeanOutputConverter<ActorsFilms> beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);
String format = beanOutputConverter.getFormat();

PromptTemplate template = new PromptTemplate("""
        帮我返回五个{actor}导演的电影名
        {format}
    """);
Prompt prompt = template.create(Map.of("actor", actor, "format", format));
Generation generation = chatModel.call(prompt).getResult();
if (generation == null) {
return null;
}
return beanOutputConverter.convert(generation.getOutput().getText());


// ChatClient方式
PromptTemplate template = new PromptTemplate("帮我返回五个{actor}导演的电影名,要求中文返回");
Prompt prompt = template.create(Map.of("actor", actor));
ActorsFilms films = ChatClient.create(chatModel).prompt(prompt).call().entity(ActorsFilms.class);

一、基本使用

1. 创建ChatClient

使用自动配置的ChatClient.Builder

如果你的项目中,只有一个大模型使用,且使用的是官方提供的starter进行的接入,那么你可以直接使用SpringBoot自动装配的ChatClient.Builder来创建ChatClient

如下

@RestController
public class ChatController {
    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        chatClient = builder.build();
    }
}

但是,请注意,当一个应用中需要使用多个聊天模型时,则不能使用上面这种方式了,因为很难知道底层到底用的是哪个模型,此时则建议使用ChatModel进行创建

使用ChatModel创建ChatClient

@RestController
public class ChatController {
    private final ChatClient chatClient;

    public ChatController(ChatModel chatModel) {
        chatClient = ChatClient.builder(chatModel).build();
    }
}

2. OpenAI兼容API的客户端初始化方式

借助 OpenAiApiOpenAiChatModel 类提供的 mutate() 方法,来实现兼容OpenAI API 的调用

@Service
public class MultiModelService {

    private static final Logger logger = LoggerFactory.getLogger(MultiModelService.class);

    private ChatClient groqClient;

    public MultiModelService(OpenAiChatModel baseChatModel, OpenAiApi baseOpenAiApi) {
        try {
            // Derive a new OpenAiApi for Groq (Llama3)
            OpenAiApi groqApi = baseOpenAiApi.mutate()
                    .baseUrl("https://api.groq.com/openai")
                    .apiKey(System.getenv("GROQ_API_KEY"))
                    .build();

            // Derive a new OpenAiChatModel for Groq
            OpenAiChatModel groqModel = baseChatModel.mutate()
                    .openAiApi(groqApi)
                    .defaultOptions(OpenAiChatOptions.builder().model("llama3-70b-8192").temperature(0.5).build())
                    .build();

            groqClient = ChatClient.builder(groqModel).build();
        } catch (Exception e) {
        }
    }
}

3. 提示词传入

创建ChatClient时,需要传入的Prompt对象用于和大模型进行交互,提供了三种方式

直接接收String

chatClient.prompt("为我写首诗").call().content();

这种表示传入的文本,作为用户消息传送给大模型

接收Prompt对象

直接接收Prompt对象,具体的交互信息封装在Prompt对象中,由用户来管控

chatClient.prompt(new Prompt(new UserMessage("为我写首诗"))).call().content();

Fluent式

通过无参方式启动FluentAPI,支持逐步构建系统消息、用户消息提示词

chatClient.prompt()
    .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
    .user("为我写首诗")
    .call().content();

4. 响应

AI 模型返回的 ChatResponse 对象,封装了模型返回的 Generation 对象,以及一些元数据、token统计

ChatResponse response = chatClient.prompt()
    .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
    .user("为我写首诗")
    .call().chatResponse();

如我们希望获取用户的token情况,则可以在元数据中获取

Usage usage = response.getMetadata().getUsage();

获取返回的消息

// ChatResponse中实际以数组的方式承载 Generation 以应对多响应的场景
// 对于大部分场景,只需要获取第一个即可
Generation generation = response.getResult();

结构化输出,如需将返回的String映射为实体类,则可以考虑使用 entity() 来实现

@GetMapping("/ai/generate")
public Object generate(@RequestParam(value = "msg", defaultValue = "你好") String msg) {
    Poem poem = chatClient.prompt()
            .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
            .user(msg)
            .call().entity(Poem.class);
    return poem;
}

record Poem(String title, String content) {
}

当然,前面介绍的结构化输出时,也提到了可以借助 ParameterizedTypeReference 来实现泛型等复杂类型的指定,如

@GetMapping("/ai/batchGen")
public Object batchGen(@RequestParam(value = "msg", defaultValue = "你好") String msg) {
    List<Poem> poem = chatClient.prompt()
            .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
            .user(msg)
            .call().entity(new ParameterizedTypeReference<List<Poem>>() {});
    return poem;
}

5. 流式调用

流式调用,前面介绍的通过call方法实现同步请求大模型,等待模型返回结果,然后进行结果处理。我们平时使用大模型时,更常见的是流式的交互方式,问一个问题,对方一点一点的返回结果

对于ChtClient而言,要想实现流式调用,则需要借助stream()方法,如

@GetMapping(path = "/ai/fluxGen", produces = "text/event-stream")
public Flux<String> fluxGen(@RequestParam(value = "msg", defaultValue = "你好") String msg) {
    return chatClient.prompt()
            .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
            .user(msg).stream().content();
}

访问示例如下:

二、进阶使用

1. 提示词模板

ChatClient Fluent 式 API 支持提供含变量的用户/系统消息模板,运行时进行替换。

@GetMapping("/ai/template")
public String template(@RequestParam(value = "role", defaultValue = "李白") String role,
                       @RequestParam(value = "msg", defaultValue = "你好") String msg) {
    return chatClient.prompt()
            .system(u -> u.text("你现在扮演盛唐著名的诗人{role},接下来我们进行对话")
                    .param("role", role))
            .user(u -> u.text("我是一个现代诗歌爱好者,我的提问是:{msg}").params(Map.of("msg", msg)))
            .call().content();
}

默认使用的是 {} 的模板变量替换,当然如果你有诉求,想用 <> 进行替换(如提示词中包含json时,{}的方式可能不太适合了),可以如下进行调整

@GetMapping("/ai/template")
public String template(@RequestParam(value = "role", defaultValue = "李白") String role,
                       @RequestParam(value = "msg", defaultValue = "你好") String msg) {
    return chatClient.prompt()
            .system(u -> u.text("你现在扮演盛唐著名的诗人{role},接下来我们进行对话")
                    .param("role", role))
            .user(u -> u.text("我是一个现代诗歌爱好者,我的提问是:<msg>").params(Map.of("msg", msg)))
            .templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
            .call().content();
}

2. stream结构化返回

使用 call() 同步调用时,结构化输出比较简单,直接通过 entity() 方法传入对象类型即可;对于流式的场景,由于大模型是逐步返回的,没有获取到完整的内容直接转换为目标对象,基本就是序列化异常了

对于 stream 方式,需要接过话输出时,可以考虑使用下面的方式

public class ChatController {

    @GetMapping(path = "/ai/fluxGenV2")
    public List<Poem> fluxGenV2(@RequestParam(value = "msg", defaultValue = "你好") String msg) {
        var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<Poem>>() {
        });

        Flux<String> flux = chatClient.prompt()
                .system("你现在扮演盛唐著名的诗人李白,接下来我们进行对话")
                .user(u -> u.text("{msg}.\n{format}").param("msg", msg).param("format", converter.getFormat()))
                .stream().content();
        String content = flux.collectList().block().stream().collect(Collectors.joining());
        return converter.convert(content);
    }
}

3. 默认值

我们可以在ChatClient创建时,使用一些默认的系统消息、提示词设置(通过 defaultXxx 的方式)

如下面给出了提供默认的消息提示词(支持带参数) 和默认的模型参数设置

  • 说明:默认的配置,可以通过不带 default 前缀的相同方法进行覆盖
@RestController
public class ChatController {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ChatController.class);

    private final ChatClient chatClient;

    private final ChatClient poemClient;

    public ChatController(ChatModel chatModel) {
        chatClient = ChatClient.builder(chatModel).build();

        poemClient = ChatClient.builder(chatModel)
                .defaultSystem("你现在扮演著名的诗人{role},接下来我们进行对话")
                .defaultOptions(ChatOptions.builder().maxTokens(500).build())
                .build();
    }

    @GetMapping("/ai/poet")
    public Poem poetChat(String role, String msg) {
        return poemClient.prompt().system(sp -> sp.param("role", role))
                .user(msg)
                .call().entity(Poem.class);
    }


    record Poem(String title, String content) {
    }
}

4. Advisor

Advisor API 为 Spring 应用中的 AI 驱动交互提供灵活强大的拦截、修改和增强能力。这个思路基本和AOP 类似,但是 Advisor 允许在运行时动态修改方法调用,从而实现更灵活的逻辑处理。

如我们希望在用户消息基础上追加或增强上下文数据时

  • 可以是RAG技术给大模型喂资料
  • 也可以是集成聊天历史,实现多轮对话

比如之前在介绍聊天上下文时,提到的借助MessageChatMemoryAdvisor来实现多轮对话

@Autowired
private ChatMemory chatMemory;

@GetMapping("/ai/historyChat")
public String historyChat(String msg) {
    return chatClient.prompt()
            .system("你现在扮演盛唐著名诗人李白,我们接下来开启对话")
            .user(msg)
            .advisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
            .call().content();
}

除了上面这个之外,另外一个记录ChatClient请求和返回的 SimpleLoggerAdvisor 也是常用的增强

@GetMapping("/ai/historyChat")
public String historyChat(String msg) {
    return chatClient.prompt()
            .system("你现在扮演盛唐著名诗人李白,我们接下来开启对话")
            .user(msg)
            .advisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build())
            .call().content();
}

要查看日志,需要调整 advisor 包的日志级别设为 DEBUG,如下设置即可

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

如果觉得默认的输出不合心意,也可以在创建时,指定传参、返回的打印方式,如

@GetMapping("/ai/historyChat")
public String historyChat(String msg) {
    return chatClient.prompt()
            .system("你现在扮演盛唐著名诗人李白,我们接下来开启对话")
            .user(msg)
            .advisors(new SimpleLoggerAdvisor(
                            req -> ("[request] " + req),
                            res -> ("[response] " + res),
                            0),
                    MessageChatMemoryAdvisor.builder(chatMemory).build())
            .call().content();
}

请注意,当传入多个 advisor 时,传入的顺序很重要,决定了它们的执行顺序。每个 Advisor 都会以某种方式修改提示词或上下文,且一个 Advisor 所做的更改会传递给链中的下一个 Advisor。

三、小结

本文的内容主要是相对成体系的介绍了一下前面几篇文章示例中的 ChatClient 的使用方式,同时也将前面的内容或多或少都覆盖了一部分。 通常来讲,我们与大模型之间的交互,更推荐的是基于ChatClient来实现,SpringAI对其上层使用,封装的很是齐全了,有兴趣的小伙伴可以赶紧体验一下

文中所有涉及到的代码,可以到项目中获取 https://github.com/liuyueyi/spring-ai-demoopen in new window

Loading...