我們在前面的兩個章節(jié)中基本上對Spring Boot 3版本的新變化進行了全面的回顧,以確保在接下來研究Spring AI時能夠避免任何潛在的問題。今天,我們終于可以直接進入主題:Spring AI是如何發(fā)起請求并將信息返回給用戶的。 在接下來的內(nèi)容中,我們將專注于這一過程,而流式回答和函數(shù)回調(diào)的相
我們在前面的兩個章節(jié)中基本上對Spring Boot 3版本的新變化進行了全面的回顧,以確保在接下來研究Spring AI時能夠避免任何潛在的問題。今天,我們終于可以直接進入主題:Spring AI是如何發(fā)起請求并將信息返回給用戶的。
在接下來的內(nèi)容中,我們將專注于這一過程,而流式回答和函數(shù)回調(diào)的相關內(nèi)容我們可以在下次的講解中詳細探討。
首先,對于還沒有項目的同學,請務必安裝所需的POM依賴項。請注意,JDK的版本要求為17。因此,你可以在IDEA中輕松下載和配置這個版本。
http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.3.1
com.example
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
17
1.0.0-M2
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-web
org.springframework.ai
spring-ai-openai-spring-boot-starter
com.github.xiaoymin
knife4j-openapi3-jakarta-spring-boot-starter
4.1.0
javax.servlet
javax.servlet-api
4.0.1
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.ai
spring-ai-bom
${spring-ai.version}
pom
import
org.graalvm.buildtools
native-maven-plugin
${project.artifactId}
com.example.demo.DemoApplication
--no-fallback
build-native
compile-no-fork
package
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
false
基本用法在之前的講解中已經(jīng)覆蓋過,因此這里就不再詳細說明。為了更好地理解這一概念,我們將通過兩個具體的例子來進行演示。
第一個例子將展示阻塞回答的實現(xiàn),而第二個例子則會涉及帶有上下文信息記憶的回答。這兩種方式將幫助我們更深入地了解如何在實際應用中靈活運用這些技術。
這里將提供一個阻塞回答的用法示例,以便更好地理解其應用場景和具體實現(xiàn)方式。
@PostMapping("/ai")
ChatDataPO generationByText(@RequestParam("userInput") String userInput) {
String content = this.myChatClientWithSystem.prompt()
.user(userInput)
.call()
.content();
log.info("content: {}", content);
ChatDataPO chatDataPO = ChatDataPO.builder().code("text").data(ChildData.builder().text(content).build()).build();;
return chatDataPO;
}
在這個示例中,我們將展示如何實現(xiàn)一個等待 AI 完成回答的機制,并將結(jié)果直接返回給接口調(diào)用端。這一過程實際上非常簡單,您只需將問題傳遞給
user
參數(shù)即可。接下來,我們將進行源碼解析。
為了節(jié)省時間,我們不會詳細逐行分析中間過程的代碼,因為這可能會顯得冗長而復雜。相反,我們將直接聚焦于關鍵源碼,以便更高效地理解其核心邏輯和實現(xiàn)細節(jié)。
我們現(xiàn)在直接進入
content
方法進行深入分析。在前面的步驟中,所有方法的參數(shù)調(diào)用主要是為了構(gòu)建一個對象,為后續(xù)的操作做準備。而真正的核心調(diào)用邏輯則集中在
content
方法內(nèi)部。
private ChatResponse doGetChatResponse(DefaultChatClientRequestSpec inputRequest, String formatParam) {
Map context = new ConcurrentHashMap<>();
context.putAll(inputRequest.getAdvisorParams());
DefaultChatClientRequestSpec advisedRequest = DefaultChatClientRequestSpec.adviseOnRequest(inputRequest,
context);
var processedUserText = StringUtils.hasText(formatParam)
? advisedRequest.getUserText() + System.lineSeparator() + "{spring_ai_soc_format}"
: advisedRequest.getUserText();
Map userParams = new HashMap<>(advisedRequest.getUserParams());
if (StringUtils.hasText(formatParam)) {
userParams.put("spring_ai_soc_format", formatParam);
}
var messages = new ArrayList(advisedRequest.getMessages());
var textsAreValid = (StringUtils.hasText(processedUserText)
|| StringUtils.hasText(advisedRequest.getSystemText()));
if (textsAreValid) {
if (StringUtils.hasText(advisedRequest.getSystemText())
|| !advisedRequest.getSystemParams().isEmpty()) {
var systemMessage = new SystemMessage(
new PromptTemplate(advisedRequest.getSystemText(), advisedRequest.getSystemParams())
.render());
messages.add(systemMessage);
}
UserMessage userMessage = null;
if (!CollectionUtils.isEmpty(userParams)) {
userMessage = new UserMessage(new PromptTemplate(processedUserText, userParams).render(),
advisedRequest.getMedia());
}
else {
userMessage = new UserMessage(processedUserText, advisedRequest.getMedia());
}
messages.add(userMessage);
}
if (advisedRequest.getChatOptions() instanceof FunctionCallingOptions functionCallingOptions) {
if (!advisedRequest.getFunctionNames().isEmpty()) {
functionCallingOptions.setFunctions(new HashSet<>(advisedRequest.getFunctionNames()));
}
if (!advisedRequest.getFunctionCallbacks().isEmpty()) {
functionCallingOptions.setFunctionCallbacks(advisedRequest.getFunctionCallbacks());
}
}
var prompt = new Prompt(messages, advisedRequest.getChatOptions());
var chatResponse = this.chatModel.call(prompt);
ChatResponse advisedResponse = chatResponse;
// apply the advisors on response
if (!CollectionUtils.isEmpty(inputRequest.getAdvisors())) {
var currentAdvisors = new ArrayList<>(inputRequest.getAdvisors());
for (RequestResponseAdvisor advisor : currentAdvisors) {
advisedResponse = advisor.adviseResponse(advisedResponse, context);
}
}
return advisedResponse;
}
這段代碼沒有任何注釋,確實令人感到意外,充分說明了Spring代碼的設計初衷——更多是為開發(fā)者所用,而非為人類閱讀。其核心思想是,能夠有效使用就足夠了。盡管這段代碼顯得簡潔明了,但其重要性不容忽視。所有的實現(xiàn)都非常精煉,沒有冗余的代碼,因此我決定不進行刪減,而是將其完整呈現(xiàn)出來。
為了幫助大家更好地理解其中的邏輯和結(jié)構(gòu),我將使用偽代碼來進行講解。
初始化上下文 :創(chuàng)建一個空的上下文。
請求調(diào)整
:請求調(diào)整的邏輯是基于上下文對輸入請求進行動態(tài)處理。首先,我們需要判斷請求對象是否已經(jīng)被
advisor
包裝。如果需要那么我們將返回一個經(jīng)過
advisor
包裝后的請求對象。
下面是相關的源碼實現(xiàn),展示了這一邏輯的具體細節(jié):
public static DefaultChatClientRequestSpec adviseOnRequest(DefaultChatClientRequestSpec inputRequest,
Map context) {
//....此處省略一堆代碼
var currentAdvisors = new ArrayList<>(inputRequest.advisors);
for (RequestResponseAdvisor advisor : currentAdvisors) {
adviseRequest = advisor.adviseRequest(adviseRequest, context);
}
advisedRequest = new DefaultChatClientRequestSpec(adviseRequest.chatModel(), adviseRequest.userText(),
adviseRequest.userParams(), adviseRequest.systemText(), adviseRequest.systemParams(),
adviseRequest.functionCallbacks(), adviseRequest.messages(), adviseRequest.functionNames(),
adviseRequest.media(), adviseRequest.chatOptions(), adviseRequest.advisors(),
adviseRequest.advisorParams(), inputRequest.getObservationRegistry(),
inputRequest.getCustomObservationConvention());
}
return advisedRequest;
}
在這里,我想詳細講解一下
advisor.adviseRequest(adviseRequest, context)
這一方法的功能和重要性。由于我們已經(jīng)配置了增強類,比如引入了一個聊天記憶功能,該方法的作用就顯得尤為關鍵。具體來說,它負責對傳入的請求進行增強處理,以滿足特定的業(yè)務需求。
值得注意的是,這個增強請求的方法是與增強響應方法相對應的,它們通常成對出現(xiàn)。接下來,深入查看
adviseRequest
方法的具體實現(xiàn):
String content = this.myChatClientWithSystem.prompt()
.advisors(new MessageChatMemoryAdvisor(chatMemory))
.user(userInput)
.call()
.content();
我們配置了
MessageChatMemoryAdvisor
類,其核心方法的具體實現(xiàn)是,在接收到相應的信息后,將該信息存儲到一個聊天記憶中。這樣一來,下一次處理請求時,就可以直接從聊天記憶中提取相關內(nèi)容。
public AdvisedRequest adviseRequest(AdvisedRequest request, Map context) {
//此處省略一堆代碼
// 4. Add the new user input to the conversation memory.
UserMessage userMessage = new UserMessage(request.userText(), request.media());
this.getChatMemoryStore().add(this.doGetConversationId(context), userMessage);
return advisedRequest;
}
處理用戶文本、構(gòu)建用戶參數(shù)
:需要依據(jù)
formatParam
方法來對用戶的輸入進行處理。具體而言,這個步驟不僅涉及到對用戶文本的格式化,還需要更新相應的用戶參數(shù)。
接下來,我們將展示具體的實現(xiàn)示例,以便更清晰地理解這一過程的操作細節(jié):
.user(u -> u.text("""
Generate the filmography for a random actor.
{format}
""")
.param("format", converter.getFormat()))
上面的代碼段會將
{format}
替換為實際的格式化信息。除了用戶提供的參數(shù)外,系統(tǒng)信息中同樣包含了一些需要解析的參數(shù),這些參數(shù)也必須在處理過程中正確地傳入。
構(gòu)建消息列表 :根據(jù)系統(tǒng)文本和用戶文本的有效性,構(gòu)建消息的過程將兩者進行整合。我們可以將所有有效的消息添加到一個 List 集合中,以便于后續(xù)處理。此外,系統(tǒng)還會創(chuàng)建一個信息對象,用于保存這些消息的相關信息,以確保在需要時可以方便地訪問和管理它們。
是否有函數(shù)回調(diào) :如果有,則設置一下具體的函數(shù)。(下一章節(jié)細講)
生成聊天提示 :創(chuàng)建一個提示new Prompt()對象并調(diào)用聊天模型api獲取返回信息。
返回增強 :如果當前請求對象配置了 advisor,那么將會調(diào)用相應的增強方法。此外,系統(tǒng)會自動將對應的問答內(nèi)容存儲到信息列表中,因此相應的信息也需要被一并記錄下來。
public ChatResponse adviseResponse(ChatResponse chatResponse, Map context) {
List assistantMessages = chatResponse.getResults().stream().map(g -> (Message) g.getOutput()).toList();
this.getChatMemoryStore().add(this.doGetConversationId(context), assistantMessages);
return chatResponse;
}
返回結(jié)果 :返回最終的聊天響應。
接下來,我們將詳細探討如何通過請求對象來調(diào)用 OpenAI 接口的具體過程。為此,我們將以 OpenAI 的源碼為基礎進行分析。如果您使用的是其他 AI 產(chǎn)品,那么在這一環(huán)節(jié)的流程將會有所不同,系統(tǒng)會根據(jù)具體的產(chǎn)品進行相應的跳轉(zhuǎn)。如圖所示:
我們將對 OpenAI 的請求調(diào)用過程進行全面的解析,以深入理解其背后的機制和實現(xiàn)細節(jié):
public ChatResponse call(Prompt prompt) {
ChatCompletionRequest request = createRequest(prompt, false);
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(OpenAiApiConstants.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.build();
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity completionEntity = this.retryTemplate
.execute(ctx -> this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));
var chatCompletion = completionEntity.getBody();
if (chatCompletion == null) {
logger.warn("No chat completion returned for prompt: {}", prompt);
return new ChatResponse(List.of());
}
List choices = chatCompletion.choices();
if (choices == null) {
logger.warn("No choices returned for prompt: {}", prompt);
return new ChatResponse(List.of());
}
List generations = choices.stream().map(choice -> {
// @formatter:off
Map metadata = Map.of(
"id", chatCompletion.id() != null ? chatCompletion.id() : "",
"role", choice.message().role() != null ? choice.message().role().name() : "",
"index", choice.index(),
"finishReason", choice.finishReason() != null ? choice.finishReason().name() : "",
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "");
// @formatter:on
return buildGeneration(choice, metadata);
}).toList();
// Non function calling.
RateLimit rateLimit = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody(), rateLimit));
observationContext.setResponse(chatResponse);
return chatResponse;
});
if (response != null && isToolCall(response, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()))) {
var toolCallConversation = handleToolCalls(prompt, response);
// Recursively call the call method with the tool call message
// conversation that contains the call responses.
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
}
return response;
}
雖然這些內(nèi)容都很有價值,刪減并不是一個好的選擇,但由于缺乏注釋,我們可能需要仔細分析。讓我們一起來看看這些信息,逐步理清其中的邏輯和要點。
createRequest
函數(shù)的主要作用是構(gòu)建在實際調(diào)用 API 時所需的請求對象。由于不同服務提供商的接口設計各有特點,因此我們需要根據(jù)具體的 API 規(guī)范自行實現(xiàn)這一過程。例如,在調(diào)用 OpenAI 的接口時,我們需要構(gòu)建特定的參數(shù)結(jié)構(gòu),這一過程大家應該已經(jīng)非常熟悉。如下圖所示,我們可以看到構(gòu)建請求時所需的各項參數(shù)及其格式。
ChatModelObservationContext
主要用于配置與請求相關的其他限制和要求。這包括多個關鍵參數(shù),例如本次請求的最大 token 數(shù)量限制、所使用的 OpenAI 問答模型的具體類型、以及請求的頻率限制等。如代碼所示:
private ChatOptions buildRequestOptions(OpenAiApi.ChatCompletionRequest request) {
return ChatOptionsBuilder.builder()
.withModel(request.model())
.withFrequencyPenalty(request.frequencyPenalty())
.withMaxTokens(request.maxTokens())
.withPresencePenalty(request.presencePenalty())
.withStopSequences(request.stop())
.withTemperature(request.temperature())
.withTopP(request.topP())
.build();
}
剩下的 ChatResponse 大方法負責實際執(zhí)行 API 請求并處理響應。在這一過程中,有幾個關鍵細節(jié)值得注意。
請求對象使用的是
retryTemplate
,這是一個具有重試機制的請求 API 工具。它的設計旨在增強請求的可靠性,特別是在面對暫時性故障或網(wǎng)絡問題時,能夠自動進行重試,從而提高成功率。更為靈活的是,
retryTemplate
允許用戶進行配置,以滿足不同應用場景的需求。
用戶可以根據(jù)實際需要調(diào)整重試次數(shù)、重試間隔時間以及其他相關參數(shù),所有這些配置都可以通過
spring.ai.retry
這一前綴進行自定義設置。具體大家可以看這個類:
@AutoConfiguration
@ConditionalOnClass(RetryTemplate.class)
@EnableConfigurationProperties({ SpringAiRetryProperties.class })
public class SpringAiRetryAutoConfiguration {
//此處省略一堆代碼
}
接著,如果 OpenAI 的接口正常返回響應,那么系統(tǒng)將開始格式化回答。在這一過程中,涉及到多個關鍵字段,這些字段對于程序員們而言應該都是相當熟悉的,尤其是那些有過接口對接經(jīng)驗的開發(fā)者。
Map metadata = Map.of(
"id", chatCompletion.id() != null ? chatCompletion.id() : "",
"role", choice.message().role() != null ? choice.message().role().name() : "",
"index", choice.index(),
"finishReason", choice.finishReason() != null ? choice.finishReason().name() : "",
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "");
接著,在接收到所有返回參數(shù)后,系統(tǒng)將這些參數(shù)整合并返回給
response
對象。然而,在這一階段,我們又進行了一個重要的判斷,檢查是否為
isToolCall
。這個判斷實際上涉及到函數(shù)回調(diào)的機制,這一部分的實現(xiàn)邏輯非常關鍵,但今天我們就不深入探討這個細節(jié),留待下次再進行講解。
至此,整個調(diào)用流程已經(jīng)圓滿完成。我們的接口順利而愉快地將處理后的信息返回給了調(diào)用端,確保了用戶請求的高效響應。
在這次探討中,我們聚焦于Spring AI如何有效地發(fā)起請求并將響應信息傳遞給用戶。這一過程不僅是開發(fā)者與AI交互的橋梁,更是優(yōu)化用戶體驗的關鍵。通過明確的請求結(jié)構(gòu)和響應機制,Spring AI能夠靈活地處理各種用戶輸入,并根據(jù)上下文調(diào)整回答策略。
然后,我們深入分析了這一機制的核心,關注具體實現(xiàn)與業(yè)務邏輯。在此過程中,我們通過實例演示阻塞回答與帶上下文記憶的回答如何在實際應用中發(fā)揮作用。這樣的實操不僅能幫助我們更好地理解Spring AI的工作原理,也為將來深入探討流式回答和函數(shù)回調(diào)埋下了伏筆。
理解這一過程的背后邏輯,將為我們在日常開發(fā)中應用Spring AI提供有力支持。隨著技術的不斷進步,開發(fā)者們面臨的挑戰(zhàn)也在日益增加,但通過這種清晰的請求與響應架構(gòu),我們可以更從容地應對復雜性,實現(xiàn)更加智能化的解決方案。
我是努力的小雨,一名 Java 服務端碼農(nóng),潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社區(qū)充滿熱情。同時也是一位騰訊云創(chuàng)作之星、阿里云專家博主、華為云云享專家、掘金優(yōu)秀作者。
? 我將不吝分享我在技術道路上的個人探索與經(jīng)驗,希望能為你的學習與成長帶來一些啟發(fā)與幫助。
? 歡迎關注努力的小雨!?
本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)