深度剖析Spring AI:哀求与响应机制的焦点逻辑

打印 上一主题 下一主题

主题 928|帖子 928|积分 2784

我们在前面的两个章节中基本上对Spring Boot 3版本的新变革举行了全面的回顾,以确保在接下来研究Spring AI时能够克制任何潜在的问题。本日,我们终于可以直接进入主题:Spring AI是如何发起哀求并将信息返回给用户的。
在接下来的内容中,我们将专注于这一过程,而流式答复和函数回调的相关内容我们可以在下次的讲解中具体探讨。
开始剖析

首先,对于还没有项目的同砚,请务必安装所需的POM依赖项。请注意,JDK的版本要求为17。因此,你可以在IDEA中轻松下载和配置这个版本。
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.     <modelVersion>4.0.0</modelVersion>
  5.     <parent>
  6.         <groupId>org.springframework.boot</groupId>
  7.         <artifactId>spring-boot-starter-parent</artifactId>
  8.         <version>3.3.1</version>
  9.         <relativePath/>
  10.     </parent>
  11.     <groupId>com.example</groupId>
  12.     <artifactId>demo</artifactId>
  13.     <version>0.0.1-SNAPSHOT</version>
  14.     <name>demo</name>
  15.     <description>Demo project for Spring Boot</description>
  16.     <url/>
  17.     <licenses>
  18.         <license/>
  19.     </licenses>
  20.     <developers>
  21.         <developer/>
  22.     </developers>
  23.     <scm>
  24.         <connection/>
  25.         <developerConnection/>
  26.         <tag/>
  27.         <url/>
  28.     </scm>
  29.     <properties>
  30.         <java.version>17</java.version>
  31.         <spring-ai.version>1.0.0-M2</spring-ai.version>
  32.     </properties>
  33.     <dependencies>
  34.         <dependency>
  35.             <groupId>org.springframework.boot</groupId>
  36.             <artifactId>spring-boot-starter-actuator</artifactId>
  37.         </dependency>
  38.         <dependency>
  39.             <groupId>org.springframework.boot</groupId>
  40.             <artifactId>spring-boot-starter-web</artifactId>
  41.         </dependency>
  42.         <dependency>
  43.             <groupId>org.springframework.ai</groupId>
  44.             <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
  45.         </dependency>
  46.         <dependency>
  47.             <groupId>com.github.xiaoymin</groupId>
  48.             <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
  49.             <version>4.1.0</version>
  50.         </dependency>
  51.         <dependency>
  52.             <groupId>javax.servlet</groupId>
  53.             <artifactId>javax.servlet-api</artifactId>
  54.             <version>4.0.1</version>
  55.         </dependency>
  56.         <dependency>
  57.             <groupId>org.projectlombok</groupId>
  58.             <artifactId>lombok</artifactId>
  59.             <optional>true</optional>
  60.         </dependency>
  61.         <dependency>
  62.             <groupId>org.springframework.boot</groupId>
  63.             <artifactId>spring-boot-starter-test</artifactId>
  64.             <scope>test</scope>
  65.         </dependency>
  66.     </dependencies>
  67.     <dependencyManagement>
  68.         <dependencies>
  69.             <dependency>
  70.                 <groupId>org.springframework.ai</groupId>
  71.                 <artifactId>spring-ai-bom</artifactId>
  72.                 <version>${spring-ai.version}</version>
  73.                 <type>pom</type>
  74.                 <scope>import</scope>
  75.             </dependency>
  76.         </dependencies>
  77.     </dependencyManagement>
  78.     <build>
  79.         <plugins>
  80.             <plugin>
  81.                 <groupId>org.graalvm.buildtools</groupId>
  82.                 <artifactId>native-maven-plugin</artifactId>
  83.                 <configuration>
  84.                     
  85.                     <imageName>${project.artifactId}</imageName>
  86.                     
  87.                     <mainClass>com.example.demo.DemoApplication</mainClass>
  88.                     <buildArgs>
  89.                         --no-fallback
  90.                     </buildArgs>
  91.                 </configuration>
  92.                 <executions>
  93.                     <execution>
  94.                         <id>build-native</id>
  95.                         <goals>
  96.                             <goal>compile-no-fork</goal>
  97.                         </goals>
  98.                         <phase>package</phase>
  99.                     </execution>
  100.                 </executions>
  101.             </plugin>
  102.             <plugin>
  103.                 <groupId>org.springframework.boot</groupId>
  104.                 <artifactId>spring-boot-maven-plugin</artifactId>
  105.                 <configuration>
  106.                     <excludes>
  107.                         <exclude>
  108.                             <groupId>org.projectlombok</groupId>
  109.                             <artifactId>lombok</artifactId>
  110.                         </exclude>
  111.                     </excludes>
  112.                 </configuration>
  113.             </plugin>
  114.         </plugins>
  115.     </build>
  116.     <repositories>
  117.         <repository>
  118.             <id>spring-milestones</id>
  119.             <name>Spring Milestones</name>
  120.             <url>https://repo.spring.io/milestone</url>
  121.             <snapshots>
  122.                 <enabled>false</enabled>
  123.             </snapshots>
  124.         </repository>
  125.     </repositories>
  126. </project>
复制代码
基本用法在之前的讲解中已经覆盖过,因此这里就不再具体说明。为了更好地理解这一概念,我们将通过两个具体的例子来举行演示。
第一个例子将展示阻塞答复的实现,而第二个例子则会涉及带有上下文信息记忆的答复。这两种方式将帮助我们更深入地了解如安在实际应用中灵活运用这些技能。
基本用法

这里将提供一个阻塞答复的用法示例,以便更好地理解其应用场景和具体实现方式。
  1. @PostMapping("/ai")
  2. ChatDataPO generationByText(@RequestParam("userInput")  String userInput) {
  3.     String content = this.myChatClientWithSystem.prompt()
  4.                 .user(userInput)
  5.                 .call()
  6.                 .content();
  7.     log.info("content: {}", content);
  8.     ChatDataPO chatDataPO = ChatDataPO.builder().code("text").data(ChildData.builder().text(content).build()).build();;
  9.     return chatDataPO;
  10. }
复制代码
在这个示例中,我们将展示如何实现一个等待 AI 完成答复的机制,并将结果直接返回给接口调用端。这一过程实际上非常简单,您只需将问题通报给 user 参数即可。接下来,我们将举行源码剖析。
为了节省时间,我们不会具体逐行分析中间过程的代码,因为这可能会显得冗长而复杂。相反,我们将直接聚焦于关键源码,以便更高效地理解其焦点逻辑和实现细节。
源码剖析——构建哀求

我们现在直接进入 content 方法举行深入分析。在前面的步调中,全部方法的参数调用主要是为了构建一个对象,为后续的操作做准备。而真正的焦点调用逻辑则集中在 content 方法内部。
  1. private ChatResponse doGetChatResponse(DefaultChatClientRequestSpec inputRequest, String formatParam) {
  2.             Map<String, Object> context = new ConcurrentHashMap<>();
  3.             context.putAll(inputRequest.getAdvisorParams());
  4.             DefaultChatClientRequestSpec advisedRequest = DefaultChatClientRequestSpec.adviseOnRequest(inputRequest,
  5.                     context);
  6.             var processedUserText = StringUtils.hasText(formatParam)
  7.                     ? advisedRequest.getUserText() + System.lineSeparator() + "{spring_ai_soc_format}"
  8.                     : advisedRequest.getUserText();
  9.             Map<String, Object> userParams = new HashMap<>(advisedRequest.getUserParams());
  10.             if (StringUtils.hasText(formatParam)) {
  11.                 userParams.put("spring_ai_soc_format", formatParam);
  12.             }
  13.             var messages = new ArrayList<Message>(advisedRequest.getMessages());
  14.             var textsAreValid = (StringUtils.hasText(processedUserText)
  15.                     || StringUtils.hasText(advisedRequest.getSystemText()));
  16.             if (textsAreValid) {
  17.                 if (StringUtils.hasText(advisedRequest.getSystemText())
  18.                         || !advisedRequest.getSystemParams().isEmpty()) {
  19.                     var systemMessage = new SystemMessage(
  20.                             new PromptTemplate(advisedRequest.getSystemText(), advisedRequest.getSystemParams())
  21.                                 .render());
  22.                     messages.add(systemMessage);
  23.                 }
  24.                 UserMessage userMessage = null;
  25.                 if (!CollectionUtils.isEmpty(userParams)) {
  26.                     userMessage = new UserMessage(new PromptTemplate(processedUserText, userParams).render(),
  27.                             advisedRequest.getMedia());
  28.                 }
  29.                 else {
  30.                     userMessage = new UserMessage(processedUserText, advisedRequest.getMedia());
  31.                 }
  32.                 messages.add(userMessage);
  33.             }
  34.             if (advisedRequest.getChatOptions() instanceof FunctionCallingOptions functionCallingOptions) {
  35.                 if (!advisedRequest.getFunctionNames().isEmpty()) {
  36.                     functionCallingOptions.setFunctions(new HashSet<>(advisedRequest.getFunctionNames()));
  37.                 }
  38.                 if (!advisedRequest.getFunctionCallbacks().isEmpty()) {
  39.                     functionCallingOptions.setFunctionCallbacks(advisedRequest.getFunctionCallbacks());
  40.                 }
  41.             }
  42.             var prompt = new Prompt(messages, advisedRequest.getChatOptions());
  43.             var chatResponse = this.chatModel.call(prompt);
  44.             ChatResponse advisedResponse = chatResponse;
  45.             // apply the advisors on response
  46.             if (!CollectionUtils.isEmpty(inputRequest.getAdvisors())) {
  47.                 var currentAdvisors = new ArrayList<>(inputRequest.getAdvisors());
  48.                 for (RequestResponseAdvisor advisor : currentAdvisors) {
  49.                     advisedResponse = advisor.adviseResponse(advisedResponse, context);
  50.                 }
  51.             }
  52.             return advisedResponse;
  53.         }
复制代码
这段代码没有任何注释,确实令人感到意外,充分说明确Spring代码的计划初志——更多是为开发者所用,而非为人类阅读。其焦点思想是,能够有效使用就足够了。尽管这段代码显得简洁明确,但其重要性不容忽视。全部的实现都非常精炼,没有冗余的代码,因此我决定不举行删减,而是将其完整呈现出来。
为了帮助大家更好地理解其中的逻辑和结构,我将使用伪代码来举行讲解。
初始化上下文:创建一个空的上下文。
哀求调解:哀求调解的逻辑是基于上下文对输入哀求举行动态处理。首先,我们需要判断哀求对象是否已经被 advisor 包装。如果需要那么我们将返回一个经过 advisor 包装后的哀求对象。
下面是相关的源码实现,展示了这一逻辑的具体细节:
  1. public static DefaultChatClientRequestSpec adviseOnRequest(DefaultChatClientRequestSpec inputRequest,
  2.                 Map<String, Object> context) {
  3. //....此处省略一堆代码
  4.         var currentAdvisors = new ArrayList<>(inputRequest.advisors);
  5.                 for (RequestResponseAdvisor advisor : currentAdvisors) {
  6.                     adviseRequest = advisor.adviseRequest(adviseRequest, context);
  7.                 }
  8.                 advisedRequest = new DefaultChatClientRequestSpec(adviseRequest.chatModel(), adviseRequest.userText(),
  9.                         adviseRequest.userParams(), adviseRequest.systemText(), adviseRequest.systemParams(),
  10.                         adviseRequest.functionCallbacks(), adviseRequest.messages(), adviseRequest.functionNames(),
  11.                         adviseRequest.media(), adviseRequest.chatOptions(), adviseRequest.advisors(),
  12.                         adviseRequest.advisorParams(), inputRequest.getObservationRegistry(),
  13.                         inputRequest.getCustomObservationConvention());
  14.             }
  15.             return advisedRequest;
  16.         }
复制代码
在这里,我想具体讲解一下 advisor.adviseRequest(adviseRequest, context) 这一方法的功能和重要性。由于我们已经配置了加强类,比如引入了一个谈天记忆功能,该方法的作用就显得尤为关键。具体来说,它负责对传入的哀求举行加强处理,以满意特定的业务需求。
值得注意的是,这个加强哀求的方法是与加强响应方法相对应的,它们通常成对出现。接下来,深入查看 adviseRequest 方法的具体实现:
  1. String content = this.myChatClientWithSystem.prompt()
  2.                 .advisors(new MessageChatMemoryAdvisor(chatMemory))
  3.                 .user(userInput)
  4.                 .call()
  5.                 .content();
复制代码
我们配置了 MessageChatMemoryAdvisor 类,其焦点方法的具体实现是,在接收到相应的信息后,将该信息存储到一个谈天记忆中。如许一来,下一次处理哀求时,就可以直接从谈天记忆中提取相关内容。
  1. public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
  2.     //此处省略一堆代码
  3.     // 4. Add the new user input to the conversation memory.
  4.     UserMessage userMessage = new UserMessage(request.userText(), request.media());
  5.     this.getChatMemoryStore().add(this.doGetConversationId(context), userMessage);
  6.     return advisedRequest;
  7. }
复制代码
处理用户文本、构建用户参数:需要依据 formatParam 方法来对用户的输入举行处理。具体而言,这个步调不但涉及到对用户文本的格式化,还需要更新相应的用户参数。
接下来,我们将展示具体的实现示例,以便更清晰地理解这一过程的操作细节:
  1. .user(u -> u.text("""
  2.                 Generate the filmography for a random actor.
  3.                 {format}
  4.               """)
  5.             .param("format", converter.getFormat()))
复制代码
上面的代码段会将 {format} 替换为实际的格式化信息。除了用户提供的参数外,系统信息中同样包含了一些需要剖析的参数,这些参数也必须在处理过程中准确地传入。
构建消息列表:根据系统文本和用户文本的有效性,构建消息的过程将两者举行整合。我们可以将全部有效的消息添加到一个 List 集合中,以便于后续处理。此外,系统还会创建一个信息对象,用于生存这些消息的相关信息,以确保在需要时可以方便地访问和管理它们。
是否有函数回调:如果有,则设置一下具体的函数。(下一章节细讲)
天生谈天提示:创建一个提示new Prompt()对象并调用谈天模型api获取返回信息。
返回加强:如果当前哀求对象配置了 advisor,那么将会调用相应的加强方法。此外,系统会主动将对应的问答内容存储到信息列表中,因此相应的信息也需要被一并记载下来。
  1. public ChatResponse adviseResponse(ChatResponse chatResponse, Map<String, Object> context) {
  2.     List<Message> assistantMessages = chatResponse.getResults().stream().map(g -> (Message) g.getOutput()).toList();
  3.     this.getChatMemoryStore().add(this.doGetConversationId(context), assistantMessages);
  4.     return chatResponse;
  5. }
复制代码
返回结果:返回最终的谈天响应。
源码剖析——哀求OpenAI

接下来,我们将具体探讨如何通过哀求对象来调用 OpenAI 接口的具体过程。为此,我们将以 OpenAI 的源码为底子举行分析。如果您使用的是其他 AI 产物,那么在这一环节的流程将会有所不同,系统会根据具体的产物举行相应的跳转。如图所示:

我们将对 OpenAI 的哀求调用过程举行全面的剖析,以深入理解其背后的机制和实现细节:
  1. public ChatResponse call(Prompt prompt) {
  2.     ChatCompletionRequest request = createRequest(prompt, false);
  3.     ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
  4.         .prompt(prompt)
  5.         .provider(OpenAiApiConstants.PROVIDER_NAME)
  6.         .requestOptions(buildRequestOptions(request))
  7.         .build();
  8.     ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
  9.         .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
  10.                 this.observationRegistry)
  11.         .observe(() -> {
  12.             ResponseEntity<ChatCompletion> completionEntity = this.retryTemplate
  13.                 .execute(ctx -> this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));
  14.             var chatCompletion = completionEntity.getBody();
  15.             if (chatCompletion == null) {
  16.                 logger.warn("No chat completion returned for prompt: {}", prompt);
  17.                 return new ChatResponse(List.of());
  18.             }
  19.             List<Choice> choices = chatCompletion.choices();
  20.             if (choices == null) {
  21.                 logger.warn("No choices returned for prompt: {}", prompt);
  22.                 return new ChatResponse(List.of());
  23.             }
  24.             List<Generation> generations = choices.stream().map(choice -> {
  25.         // @formatter:off
  26.                 Map<String, Object> metadata = Map.of(
  27.                         "id", chatCompletion.id() != null ? chatCompletion.id() : "",
  28.                         "role", choice.message().role() != null ? choice.message().role().name() : "",
  29.                         "index", choice.index(),
  30.                         "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "",
  31.                         "refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "");
  32.                 // @formatter:on
  33.                 return buildGeneration(choice, metadata);
  34.             }).toList();
  35.             // Non function calling.
  36.             RateLimit rateLimit = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
  37.             ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody(), rateLimit));
  38.             observationContext.setResponse(chatResponse);
  39.             return chatResponse;
  40.         });
  41.     if (response != null && isToolCall(response, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
  42.             OpenAiApi.ChatCompletionFinishReason.STOP.name()))) {
  43.         var toolCallConversation = handleToolCalls(prompt, response);
  44.         // Recursively call the call method with the tool call message
  45.         // conversation that contains the call responses.
  46.         return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
  47.     }
  48.     return response;
  49. }
复制代码
虽然这些内容都很有代价,删减并不是一个好的选择,但由于缺乏注释,我们可能需要仔细分析。让我们一起来看看这些信息,逐步理清其中的逻辑和要点。
createRequest 函数的主要作用是构建在实际调用 API 时所需的哀求对象。由于不同服务提供商的接口计划各有特点,因此我们需要根据具体的 API 规范自行实现这一过程。例如,在调用 OpenAI 的接口时,我们需要构建特定的参数结构,这一过程大家应该已经非常认识。如下图所示,我们可以看到构建哀求时所需的各项参数及其格式。

ChatModelObservationContext 主要用于配置与哀求相关的其他限制和要求。这包括多个关键参数,例如本次哀求的最大 token 数量限制、所使用的 OpenAI 问答模型的具体类型、以及哀求的频率限制等。如代码所示:
  1. private ChatOptions buildRequestOptions(OpenAiApi.ChatCompletionRequest request) {
  2.     return ChatOptionsBuilder.builder()
  3.         .withModel(request.model())
  4.         .withFrequencyPenalty(request.frequencyPenalty())
  5.         .withMaxTokens(request.maxTokens())
  6.         .withPresencePenalty(request.presencePenalty())
  7.         .withStopSequences(request.stop())
  8.         .withTemperature(request.temperature())
  9.         .withTopP(request.topP())
  10.         .build();
  11. }
复制代码
剩下的 ChatResponse 大方法负责实际执行 API 哀求并处理响应。在这一过程中,有几个关键细节值得注意。
哀求对象使用的是 retryTemplate,这是一个具有重试机制的哀求 API 工具。它的计划旨在加强哀求的可靠性,特别是在面对暂时性故障或网络问题时,能够主动举行重试,从而提高乐成率。更为灵活的是,retryTemplate 允许用户举行配置,以满意不同应用场景的需求。
用户可以根据实际需要调解重试次数、重试隔断时间以及其他相关参数,全部这些配置都可以通过 spring.ai.retry 这一前缀举行自定义设置。具体大家可以看这个类:
  1. @AutoConfiguration
  2. @ConditionalOnClass(RetryTemplate.class)
  3. @EnableConfigurationProperties({ SpringAiRetryProperties.class })
  4. public class SpringAiRetryAutoConfiguration {
  5.   //此处省略一堆代码
  6. }
复制代码
接着,如果 OpenAI 的接口正常返反响应,那么系统将开始格式化答复。在这一过程中,涉及到多个关键字段,这些字段对于程序员们而言应该都是相当认识的,尤其是那些有过接口对接经验的开发者。
  1. Map<String, Object> metadata = Map.of(
  2.                             "id", chatCompletion.id() != null ? chatCompletion.id() : "",
  3.                             "role", choice.message().role() != null ? choice.message().role().name() : "",
  4.                             "index", choice.index(),
  5.                             "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "",
  6.                             "refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "");
复制代码
接着,在接收到全部返回参数后,系统将这些参数整合并返回给 response 对象。然而,在这一阶段,我们又举行了一个重要的判断,检查是否为 isToolCall。这个判断实际上涉及到函数回调的机制,这一部分的实现逻辑非常关键,但本日我们就不深入探讨这个细节,留待下次再举行讲解。
至此,整个调用流程已经圆满完成。我们的接口顺利而舒畅地将处理后的信息返回给了调用端,确保了用户哀求的高效响应。
总结

在这次探讨中,我们聚焦于Spring AI如何有效地发起哀求并将响应信息通报给用户。这一过程不但是开发者与AI交互的桥梁,更是优化用户体验的关键。通过明确的哀求结构和响应机制,Spring AI能够灵活地处理各种用户输入,并根据上下文调解答复策略。
然后,我们深入分析了这一机制的焦点,关注具体实现与业务逻辑。在此过程中,我们通过实例演示阻塞答复与带上下文记忆的答复如安在实际应用中发挥作用。如许的实操不但能帮助我们更好地理解Spring AI的工作原理,也为将来深入探讨流式答复和函数回调埋下了伏笔。
理解这一过程的背后逻辑,将为我们在一样平常开发中应用Spring AI提供有力支持。随着技能的不断进步,开发者们面临的挑衅也在日益增加,但通过这种清晰的哀求与响应架构,我们可以更从容地应对复杂性,实现更加智能化的办理方案。
我是努力的小雨,一名 Java 服务端码农,潜心研究着 AI 技能的奥秘。我热爱技能互换与分享,对开源社区充满热情。同时也是一位腾讯云创作之星、阿里云专家博主、华为云云享专家、掘金优秀作者。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

汕尾海湾

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表