在上一章节中,我们深入分析了Spring AI的阻塞式请求与响应机制,并探讨了如何增强其记忆能力。今天,我们将重点讲解流式响应的概念与实现。毕竟,AI的流式回答功能与其交互体验密切相关,是提升用户满意度的重要组成部分。

基本用法

基本用法非常简单,只需增加一个 stream 方法即可实现所需功能。接下来,我们将通过代码示例来展示这一过程,帮助您更清晰地理解如何在实际应用中进行操作。请看以下代码:

@GetMapping(value = "/ai-stream",produces = MediaType.APPLICATION_OCTET_STREAM_VALUE + ";charset=UTF-8")
Flux<String> generationByStream(@RequestParam("userInput") String userInput) {
Flux<String> output = chatClient.prompt()
.user(userInput)
.stream()
.content();
return output;
}

在我们增加 stream 方法之后,返回的对象类型将不再是原来的阻塞式 CallResponseSpec,而是转换为非阻塞的 StreamResponseSpec。与此同时,返回的数据类型也由之前的 String 变更为 Flux

在深入探讨其具体应用之前,首先让我来介绍一下 Flux 的概念与特性。

Spring WebFlux的处理器实现

首先,在 WebFlux 中,处理器已经实现了非阻塞式的功能。这意味着,只要我们的代码返回一个 Flux 对象,就能轻松实现响应功能。通过这种方式,应用程序能够高效地处理并发请求,而不会因阻塞操作而影响整体性能。

    @Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.onErrorResume(ex -> handleResultMono(exchange, Mono.error(ex)))
.flatMap(handler -> handleRequestWith(exchange, handler));
}

这里简单介绍一下 Spring WebFlux,虽然这不是我们的重点,但了解其基本概念还是很有帮助的。Spring WebFlux 是 Spring 框架的一部分,专为构建反应式应用而设计。它支持异步和非阻塞的编程模型,使得处理高并发请求变得更加高效。以下是 WebFlux 的几个关键特性:

  1. 反应式编程:WebFlux 基于反应式编程模型,使用 MonoFlux 类型来处理数据流。Mono 表示零或一个元素,而 Flux 则表示零个或多个元素。这种模型使得我们可以轻松处理异步数据流,从而提高代码的可读性和可维护性。
  2. 非阻塞 I/O:WebFlux 通过非阻塞的 I/O 操作(如 Netty 或 Servlet 3.1+ 容器)来实现高效的资源利用。与传统的阻塞 I/O 不同,WebFlux 在等待响应时能够释放线程,这样一来,就可以显著提高应用的并发能力,支持更多的同时请求而不增加线程开销。

了解这些特性将为后续的非阻塞式响应设计奠定基础,帮助我们更好地利用 WebFlux 的能力来提升应用性能。

源码分析

现在我们来详细看看我们的 content 是如何操作的。接下来的代码示例将展示具体的实现方式,帮助我们理解在 WebFlux 中如何处理数据流和响应:

public Flux<String> content() {
return doGetFluxChatResponse(this.request).map(r -> {
if (r.getResult() == null || r.getResult().getOutput() == null
|| r.getResult().getOutput().getContent() == null) {
return "";
}
return r.getResult().getOutput().getContent();
}).filter(StringUtils::hasLength);
}

这里的实现相对简单,主要是传入了一个函数。接下来,我们将深入分析 doGetFluxChatResponse 的代码实现,以便更好地理解其具体逻辑和运作方式:

private Flux<ChatResponse> doGetFluxChatResponse2(DefaultChatClientRequestSpec inputRequest) {
//此处省略重复代码
var fluxChatResponse = this.chatModel.stream(prompt);
//此处省略重复代码
return advisedResponse;
}

这里的代码逻辑与阻塞回答基本相同,唯一的不同之处在于它调用了 chatModel.stream(prompt) 方法。接下来,我们将深入探讨 chatModel.stream(prompt) 方法的具体实现和其背后的设计思路:

public Flux<ChatResponse> stream(Prompt prompt) {
return Flux.deferContextual(contextView -> {
//此处省略重复代码
Flux<OpenAiApi.ChatCompletionChunk> completionChunks = this.openAiApi.chatCompletionStream(request,
getAdditionalHttpHeaders(prompt));
//此处省略重复代码
Flux<ChatResponse> chatResponse = completionChunks.map(this::chunkToChatCompletion)
.switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> {
//此处省略重复代码
return new ChatResponse(generations, from(chatCompletion2, null));
}
}));
//此处省略重复代码
return new MessageAggregator().aggregate(flux, observationContext::setResponse); });
}

同样的逻辑在这里就不再赘述,我们将重点关注其中的区别。在这一部分,我们使用了 chatCompletionStream,而且与之前不同的是,这里不再使用 retryTemplate,而是引入了 webClient,这是一个能够接收事件流的工具类。

public Flux<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest chatRequest,
MultiValueMap<String, String> additionalHttpHeader) { Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(chatRequest.stream(), "Request must set the stream property to true."); AtomicBoolean isInsideTool = new AtomicBoolean(false); return this.webClient.post()
.uri(this.completionsPath)
.headers(headers -> headers.addAll(additionalHttpHeader))
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
.retrieve()
.bodyToFlux(String.class)
// cancels the flux stream after the "[DONE]" is received.
.takeUntil(SSE_DONE_PREDICATE)
// filters out the "[DONE]" message.
.filter(SSE_DONE_PREDICATE.negate())
.map(content -> ModelOptionsUtils.jsonToObject(content, ChatCompletionChunk.class))
//此处省略一堆代码

这段代码的主要目的是通过 webClient 向指定路径发起一个 POST 请求,同时设置合适的请求头和请求体。在获取响应数据时,使用了事件流的方式(通过 bodyToFlux 方法)来接收响应内容,并对数据进行过滤和转换,最终将其转化为 ChatCompletionChunk 对象。

尽管其余的业务逻辑与之前相似,但有一点显著的区别,即整个流程的返回类型以及与 OpenAI API 的调用方式都是非阻塞式的。

总结

在当今的数字时代,流式响应机制不仅提升了系统的性能,还在用户体验上扮演了关键角色。通过引入 Flux 类型,Spring WebFlux 的设计理念使得应用能够以非阻塞的方式处理并发请求,从而有效利用资源并减少响应延迟。

我们终于全面讲解了Spring AI的基本操作,包括阻塞式回答、流式回答以及记忆增强功能。这些内容为我们深入理解其工作机制奠定了基础。接下来,我们将继续深入探索源码,重点分析回调函数、实体类映射等重要功能。

这将帮助我们更好地理解Spring AI的内部运作原理,并为进一步的优化和定制化提供指导。


我是努力的小雨,一名 Java 服务端码农,潜心研究着 AI 技术的奥秘。我热爱技术交流与分享,对开源社区充满热情。同时也是一位腾讯云创作之星、阿里云专家博主、华为云云享专家、掘金优秀作者。

我将不吝分享我在技术道路上的个人探索与经验,希望能为你的学习与成长带来一些启发与帮助。

欢迎关注努力的小雨!

深入探索Spring AI:源码分析流式回答的更多相关文章

  1. Spring AOP 源码分析 - 拦截器链的执行过程

    1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...

  2. 精尽Spring MVC源码分析 - MultipartResolver 组件

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  3. Spring Security 源码分析(四):Spring Social实现微信社交登录

    社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...

  4. spring事务源码分析结合mybatis源码(一)

    最近想提升,苦逼程序猿,想了想还是拿最熟悉,之前也一直想看但没看的spring源码来看吧,正好最近在弄事务这部分的东西,就看了下,同时写下随笔记录下,以备后查. spring tx源码分析 这里只分析 ...

  5. spring AOP源码分析(三)

    在上一篇文章 spring AOP源码分析(二)中,我们已经知道如何生成一个代理对象了,那么当代理对象调用代理方法时,增强行为也就是拦截器是如何发挥作用的呢?接下来我们将介绍JDK动态代理和cglib ...

  6. Spring AOP 源码分析 - 创建代理对象

    1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...

  7. Spring AOP 源码分析 - 筛选合适的通知器

    1.简介 从本篇文章开始,我将会对 Spring AOP 部分的源码进行分析.本文是 Spring AOP 源码分析系列文章的第二篇,本文主要分析 Spring AOP 是如何为目标 bean 筛选出 ...

  8. Spring AOP 源码分析系列文章导读

    1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...

  9. Spring IOC 源码分析

    Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器.既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文 ...

  10. Spring AMQP 源码分析 08 - XML 配置

    ### 准备 ## 目标 通过 XML 配置文件使用 Spring AMQP ## 前置知识 <Spring AMQP 源码分析 07 - MessageListenerAdapter> ...

随机推荐

  1. 【Java】项目采用的设计模式案例

    先说一下业务需要: 做电竞酒店后台系统,第一期功能有一个服务申请的消息通知功能 就是酒店用户在小程序点击服务功能,可以在后台这边查到用户的服务需要 原本设计是只需要一张表存储这些消息,但是考虑设计是S ...

  2. 【JDBC】Extra02 SqlServer-JDBC

    官网驱动获取地址: https://www.microsoft.com/zh-cn/download/details.aspx Maven仓库获取: https://mvnrepository.com ...

  3. xshell终端——多个窗格同步输入——xshell同时控制多个窗口的快捷方式

    参考: https://blog.csdn.net/m0_58347801/article/details/129551382 ======================== 突发发现了终端的另类用 ...

  4. MySQL手动执行rollback,内部实现分析

    -- 测试手动回滚操作 -- 1手动开启事务 START TRANSACTION -- 2执行更新操作语句 UPDATE FraBakNtuAnalysize SET IsDeleted = 0 WH ...

  5. A. Flipping Game

    A. Flipping Game 本质上是让我们找出一段区间内\(0\)的个数大于\(1\)的个数的最多的区间,且必须进行一次操作,所以可以考虑区间\(dp\),或者最小子序列和 1 最小子序列和 \ ...

  6. 你还在手动操作仓库?这款 CLI 工具让你效率飙升300%!

    前言 作为一名开发者,我经常会在 GitHub 和 Gitee 上 fork 各种项目.时间一长,这些仓库就会堆积如山,变成了"垃圾仓库".每次打开代码托管平台,看到那些不再需要的 ...

  7. SearXNG与LLM强强联合:打造用户隐私保护的智能搜索解答流程,隐私无忧,搜索无忧

    SearXNG与LLM强强联合:打造用户隐私保护的智能搜索解答流程,隐私无忧,搜索无忧 SearXNG 是一个免费的互联网元搜索引擎,整合了各种搜索服务的结果.用户不会被跟踪,也不会被分析. gith ...

  8. Ubuntu APT sources.list 文件格式解释

    单行风格(传统) 传统的 sourses.list 文件使用单行风格配置,下面是两条单行风格的配置项: deb http://archive.ubuntu.com/ubuntu jammy main ...

  9. JMeter手机app录制

    在移动应用的性能测试中,如何准确.全面地捕捉用户操作并生成可复用的测试脚本,始终是测试工程师面临的一大挑战.而JMeter,作为一款功能强大的开源性能测试工具,不仅在Web测试中表现优异,在手机App ...

  10. Kubernetes-4:kubectl常用命令总结

    kubectl常用命令 1.kubectl get 显示资源 ## 查看pod列表,若pod后不指定名称空间(namespace),则默认为default名称空间 kubectl get pod ## ...