您的位置:首頁(yè) > 軟件教程 > 教程 > 深度解析Spring AI:請(qǐng)求與響應(yīng)機(jī)制的核心邏輯

深度解析Spring AI:請(qǐng)求與響應(yīng)機(jī)制的核心邏輯

來(lái)源:好特整理 | 時(shí)間:2024-10-10 10:18:36 | 閱讀:84 |  標(biāo)簽: a 邏輯 Ri 核心 S in AI   | 分享到:

我們?cè)谇懊娴膬蓚(gè)章節(jié)中基本上對(duì)Spring Boot 3版本的新變化進(jìn)行了全面的回顧,以確保在接下來(lái)研究Spring AI時(shí)能夠避免任何潛在的問(wèn)題。今天,我們終于可以直接進(jìn)入主題:Spring AI是如何發(fā)起請(qǐng)求并將信息返回給用戶的。 在接下來(lái)的內(nèi)容中,我們將專注于這一過(guò)程,而流式回答和函數(shù)回調(diào)的相

我們?cè)谇懊娴膬蓚(gè)章節(jié)中基本上對(duì)Spring Boot 3版本的新變化進(jìn)行了全面的回顧,以確保在接下來(lái)研究Spring AI時(shí)能夠避免任何潛在的問(wèn)題。今天,我們終于可以直接進(jìn)入主題:Spring AI是如何發(fā)起請(qǐng)求并將信息返回給用戶的。

在接下來(lái)的內(nèi)容中,我們將專注于這一過(guò)程,而流式回答和函數(shù)回調(diào)的相關(guān)內(nèi)容我們可以在下次的講解中詳細(xì)探討。

開始解析

首先,對(duì)于還沒(méi)有項(xiàng)目的同學(xué),請(qǐng)務(wù)必安裝所需的POM依賴項(xiàng)。請(qǐng)注意,JDK的版本要求為17。因此,你可以在IDEA中輕松下載和配置這個(gè)版本。


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)覆蓋過(guò),因此這里就不再詳細(xì)說(shuō)明。為了更好地理解這一概念,我們將通過(guò)兩個(gè)具體的例子來(lái)進(jìn)行演示。

第一個(gè)例子將展示阻塞回答的實(shí)現(xiàn),而第二個(gè)例子則會(huì)涉及帶有上下文信息記憶的回答。這兩種方式將幫助我們更深入地了解如何在實(shí)際應(yīng)用中靈活運(yùn)用這些技術(shù)。

基本用法

這里將提供一個(gè)阻塞回答的用法示例,以便更好地理解其應(yīng)用場(chǎng)景和具體實(shí)現(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;
}

在這個(gè)示例中,我們將展示如何實(shí)現(xiàn)一個(gè)等待 AI 完成回答的機(jī)制,并將結(jié)果直接返回給接口調(diào)用端。這一過(guò)程實(shí)際上非常簡(jiǎn)單,您只需將問(wèn)題傳遞給 user 參數(shù)即可。接下來(lái),我們將進(jìn)行源碼解析。

為了節(jié)省時(shí)間,我們不會(huì)詳細(xì)逐行分析中間過(guò)程的代碼,因?yàn)檫@可能會(huì)顯得冗長(zhǎng)而復(fù)雜。相反,我們將直接聚焦于關(guān)鍵源碼,以便更高效地理解其核心邏輯和實(shí)現(xiàn)細(xì)節(jié)。

源碼解析——構(gòu)建請(qǐng)求

我們現(xiàn)在直接進(jìn)入 content 方法進(jìn)行深入分析。在前面的步驟中,所有方法的參數(shù)調(diào)用主要是為了構(gòu)建一個(gè)對(duì)象,為后續(xù)的操作做準(zhǔn)備。而真正的核心調(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;
        }

這段代碼沒(méi)有任何注釋,確實(shí)令人感到意外,充分說(shuō)明了Spring代碼的設(shè)計(jì)初衷——更多是為開發(fā)者所用,而非為人類閱讀。其核心思想是,能夠有效使用就足夠了。盡管這段代碼顯得簡(jiǎn)潔明了,但其重要性不容忽視。所有的實(shí)現(xiàn)都非常精煉,沒(méi)有冗余的代碼,因此我決定不進(jìn)行刪減,而是將其完整呈現(xiàn)出來(lái)。

為了幫助大家更好地理解其中的邏輯和結(jié)構(gòu),我將使用偽代碼來(lái)進(jìn)行講解。

初始化上下文 :創(chuàng)建一個(gè)空的上下文。

請(qǐng)求調(diào)整 :請(qǐng)求調(diào)整的邏輯是基于上下文對(duì)輸入請(qǐng)求進(jìn)行動(dòng)態(tài)處理。首先,我們需要判斷請(qǐng)求對(duì)象是否已經(jīng)被 advisor 包裝。如果需要那么我們將返回一個(gè)經(jīng)過(guò) advisor 包裝后的請(qǐng)求對(duì)象。

下面是相關(guān)的源碼實(shí)現(xiàn),展示了這一邏輯的具體細(xì)節(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;
        }

在這里,我想詳細(xì)講解一下 advisor.adviseRequest(adviseRequest, context) 這一方法的功能和重要性。由于我們已經(jīng)配置了增強(qiáng)類,比如引入了一個(gè)聊天記憶功能,該方法的作用就顯得尤為關(guān)鍵。具體來(lái)說(shuō),它負(fù)責(zé)對(duì)傳入的請(qǐng)求進(jìn)行增強(qiáng)處理,以滿足特定的業(yè)務(wù)需求。

值得注意的是,這個(gè)增強(qiáng)請(qǐng)求的方法是與增強(qiáng)響應(yīng)方法相對(duì)應(yīng)的,它們通常成對(duì)出現(xiàn)。接下來(lái),深入查看 adviseRequest 方法的具體實(shí)現(xiàn):

String content = this.myChatClientWithSystem.prompt()
                .advisors(new MessageChatMemoryAdvisor(chatMemory))
                .user(userInput)
                .call()
                .content();

我們配置了 MessageChatMemoryAdvisor 類,其核心方法的具體實(shí)現(xiàn)是,在接收到相應(yīng)的信息后,將該信息存儲(chǔ)到一個(gè)聊天記憶中。這樣一來(lái),下一次處理請(qǐng)求時(shí),就可以直接從聊天記憶中提取相關(guā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 方法來(lái)對(duì)用戶的輸入進(jìn)行處理。具體而言,這個(gè)步驟不僅涉及到對(duì)用戶文本的格式化,還需要更新相應(yīng)的用戶參數(shù)。

接下來(lái),我們將展示具體的實(shí)現(xiàn)示例,以便更清晰地理解這一過(guò)程的操作細(xì)節(jié):

.user(u -> u.text("""
                Generate the filmography for a random actor.
                {format}
              """)
            .param("format", converter.getFormat()))

上面的代碼段會(huì)將 {format} 替換為實(shí)際的格式化信息。除了用戶提供的參數(shù)外,系統(tǒng)信息中同樣包含了一些需要解析的參數(shù),這些參數(shù)也必須在處理過(guò)程中正確地傳入。

構(gòu)建消息列表 :根據(jù)系統(tǒng)文本和用戶文本的有效性,構(gòu)建消息的過(guò)程將兩者進(jìn)行整合。我們可以將所有有效的消息添加到一個(gè) List 集合中,以便于后續(xù)處理。此外,系統(tǒng)還會(huì)創(chuàng)建一個(gè)信息對(duì)象,用于保存這些消息的相關(guān)信息,以確保在需要時(shí)可以方便地訪問(wèn)和管理它們。

是否有函數(shù)回調(diào) :如果有,則設(shè)置一下具體的函數(shù)。(下一章節(jié)細(xì)講)

生成聊天提示 :創(chuàng)建一個(gè)提示new Prompt()對(duì)象并調(diào)用聊天模型api獲取返回信息。

返回增強(qiáng) :如果當(dāng)前請(qǐng)求對(duì)象配置了 advisor,那么將會(huì)調(diào)用相應(yīng)的增強(qiáng)方法。此外,系統(tǒng)會(huì)自動(dòng)將對(duì)應(yīng)的問(wèn)答內(nèi)容存儲(chǔ)到信息列表中,因此相應(yīng)的信息也需要被一并記錄下來(lá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é)果 :返回最終的聊天響應(yīng)。

源碼解析——請(qǐng)求OpenAI

接下來(lái),我們將詳細(xì)探討如何通過(guò)請(qǐng)求對(duì)象來(lái)調(diào)用 OpenAI 接口的具體過(guò)程。為此,我們將以 OpenAI 的源碼為基礎(chǔ)進(jìn)行分析。如果您使用的是其他 AI 產(chǎn)品,那么在這一環(huán)節(jié)的流程將會(huì)有所不同,系統(tǒng)會(huì)根據(jù)具體的產(chǎn)品進(jìn)行相應(yīng)的跳轉(zhuǎn)。如圖所示:

深度解析Spring AI:請(qǐng)求與響應(yīng)機(jī)制的核心邏輯

我們將對(duì) OpenAI 的請(qǐng)求調(diào)用過(guò)程進(jìn)行全面的解析,以深入理解其背后的機(jī)制和實(shí)現(xiàn)細(xì)節(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)容都很有價(jià)值,刪減并不是一個(gè)好的選擇,但由于缺乏注釋,我們可能需要仔細(xì)分析。讓我們一起來(lái)看看這些信息,逐步理清其中的邏輯和要點(diǎn)。

createRequest 函數(shù)的主要作用是構(gòu)建在實(shí)際調(diào)用 API 時(shí)所需的請(qǐng)求對(duì)象。由于不同服務(wù)提供商的接口設(shè)計(jì)各有特點(diǎn),因此我們需要根據(jù)具體的 API 規(guī)范自行實(shí)現(xiàn)這一過(guò)程。例如,在調(diào)用 OpenAI 的接口時(shí),我們需要構(gòu)建特定的參數(shù)結(jié)構(gòu),這一過(guò)程大家應(yīng)該已經(jīng)非常熟悉。如下圖所示,我們可以看到構(gòu)建請(qǐng)求時(shí)所需的各項(xiàng)參數(shù)及其格式。

深度解析Spring AI:請(qǐng)求與響應(yīng)機(jī)制的核心邏輯

ChatModelObservationContext 主要用于配置與請(qǐng)求相關(guān)的其他限制和要求。這包括多個(gè)關(guān)鍵參數(shù),例如本次請(qǐng)求的最大 token 數(shù)量限制、所使用的 OpenAI 問(wèn)答模型的具體類型、以及請(qǐng)求的頻率限制等。如代碼所示:

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 大方法負(fù)責(zé)實(shí)際執(zhí)行 API 請(qǐng)求并處理響應(yīng)。在這一過(guò)程中,有幾個(gè)關(guān)鍵細(xì)節(jié)值得注意。

請(qǐng)求對(duì)象使用的是 retryTemplate ,這是一個(gè)具有重試機(jī)制的請(qǐng)求 API 工具。它的設(shè)計(jì)旨在增強(qiáng)請(qǐng)求的可靠性,特別是在面對(duì)暫時(shí)性故障或網(wǎng)絡(luò)問(wèn)題時(shí),能夠自動(dòng)進(jìn)行重試,從而提高成功率。更為靈活的是, retryTemplate 允許用戶進(jìn)行配置,以滿足不同應(yīng)用場(chǎng)景的需求。

用戶可以根據(jù)實(shí)際需要調(diào)整重試次數(shù)、重試間隔時(shí)間以及其他相關(guān)參數(shù),所有這些配置都可以通過(guò) spring.ai.retry 這一前綴進(jìn)行自定義設(shè)置。具體大家可以看這個(gè)類:

@AutoConfiguration
@ConditionalOnClass(RetryTemplate.class)
@EnableConfigurationProperties({ SpringAiRetryProperties.class })
public class SpringAiRetryAutoConfiguration {
  //此處省略一堆代碼
}

接著,如果 OpenAI 的接口正常返回響應(yīng),那么系統(tǒng)將開始格式化回答。在這一過(guò)程中,涉及到多個(gè)關(guān)鍵字段,這些字段對(duì)于程序員們而言應(yīng)該都是相當(dāng)熟悉的,尤其是那些有過(guò)接口對(duì)接經(jīng)驗(yàn)的開發(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 對(duì)象。然而,在這一階段,我們又進(jìn)行了一個(gè)重要的判斷,檢查是否為 isToolCall 。這個(gè)判斷實(shí)際上涉及到函數(shù)回調(diào)的機(jī)制,這一部分的實(shí)現(xiàn)邏輯非常關(guān)鍵,但今天我們就不深入探討這個(gè)細(xì)節(jié),留待下次再進(jìn)行講解。

至此,整個(gè)調(diào)用流程已經(jīng)圓滿完成。我們的接口順利而愉快地將處理后的信息返回給了調(diào)用端,確保了用戶請(qǐng)求的高效響應(yīng)。

總結(jié)

在這次探討中,我們聚焦于Spring AI如何有效地發(fā)起請(qǐng)求并將響應(yīng)信息傳遞給用戶。這一過(guò)程不僅是開發(fā)者與AI交互的橋梁,更是優(yōu)化用戶體驗(yàn)的關(guān)鍵。通過(guò)明確的請(qǐng)求結(jié)構(gòu)和響應(yīng)機(jī)制,Spring AI能夠靈活地處理各種用戶輸入,并根據(jù)上下文調(diào)整回答策略。

然后,我們深入分析了這一機(jī)制的核心,關(guān)注具體實(shí)現(xiàn)與業(yè)務(wù)邏輯。在此過(guò)程中,我們通過(guò)實(shí)例演示阻塞回答與帶上下文記憶的回答如何在實(shí)際應(yīng)用中發(fā)揮作用。這樣的實(shí)操不僅能幫助我們更好地理解Spring AI的工作原理,也為將來(lái)深入探討流式回答和函數(shù)回調(diào)埋下了伏筆。

理解這一過(guò)程的背后邏輯,將為我們?cè)谌粘i_發(fā)中應(yīng)用Spring AI提供有力支持。隨著技術(shù)的不斷進(jìn)步,開發(fā)者們面臨的挑戰(zhàn)也在日益增加,但通過(guò)這種清晰的請(qǐng)求與響應(yīng)架構(gòu),我們可以更從容地應(yīng)對(duì)復(fù)雜性,實(shí)現(xiàn)更加智能化的解決方案。


我是努力的小雨,一名 Java 服務(wù)端碼農(nóng),潛心研究著 AI 技術(shù)的奧秘。我熱愛(ài)技術(shù)交流與分享,對(duì)開源社區(qū)充滿熱情。同時(shí)也是一位騰訊云創(chuàng)作之星、阿里云專家博主、華為云云享專家、掘金優(yōu)秀作者。

? 我將不吝分享我在技術(shù)道路上的個(gè)人探索與經(jīng)驗(yàn),希望能為你的學(xué)習(xí)與成長(zhǎng)帶來(lái)一些啟發(fā)與幫助。

? 歡迎關(guān)注努力的小雨!?

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

a 1.0
a 1.0
類型:休閑益智  運(yùn)營(yíng)狀態(tài):正式運(yùn)營(yíng)  語(yǔ)言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

《alittletotheleft》官網(wǎng)正版是一款備受歡迎的休閑益智整理游戲。玩家的任務(wù)是對(duì)日常生活中的各種雜亂物
RPG Ri序章 0.2.1
RPG Ri序章 0.2.1
類型:角色扮演  運(yùn)營(yíng)狀態(tài):正式運(yùn)營(yíng)  語(yǔ)言: 日文  

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

《RPG_Ri序章》是GameMaker'Child-Dream'制作的一款幻想廢土風(fēng)RPG手游,完全免費(fèi)的幻想廢土風(fēng)RPG登場(chǎng)!元

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2024 haote.com 好特網(wǎng)