8. Spring Ai之入门到精通(超级详细)

简介

2024年5月30号Spring AI 的 1.0.0 里程碑 1 版本发布。表明版本已正在巩固,并且大部分主要错误和问题已经解决,API基本已确定,不会发生很大的变化。

在与大模型集成方面,继LangChain4j之后,又一重大的框架诞生。标志着在人工智能的浪潮中,Java不会因此被边缘化,让生成式人工智能应用程序不仅适用于Python,也适用于Java。

Spring Ai官网: Spring AI

前置准备

Spring Ai除了支持国外的大模型外,也支持国内很多大模型,比如清华的智普Ai,百度的千帆和月之暗面的 kimi。集成Spring Ai需要用到 api-key,大家按照自己的需要,去Ai开放平台申请。

下面我主要用到OpenAi和智普Ai来讲解案例代码,OpenAi是国外的,需要我们要有个国外手机号(亚洲很多被封了,用不了),登录OpenAi官网创建apikey(需要用到魔法软件科学上网)。下面给出各个注册渠道.

Open-AI:

ZhiPu-AI:

概念和案例实践

新建SpringBoot工程,然后添加以下依赖:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.gorgor</groupId>
    <artifactId>spring-ai-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>


    <repositories>

        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
</project>

添加application.yml配置文件

需要配置智普api-key和openai api-key.

Lua 复制代码
server:
  port: 10096
spring:
  application:
    name: ai-demo
  ai:
    zhipuai:
      api-key: ${ZHIPUAI_API_KEY}
      chat:
        options:
          model: glm-3-turbo
      embedding:
        enabled: false
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.openai-hk.com
      chat:
        options:
          model: gpt-4o-mini
      embedding:
        enabled: true
1. ChatClient 和 ChatModel

ChatClient是SpringAI 0.8.0版本的概念,到1.0.0版本变成了ChatModel,但同时保留了ChatClient,ChatClient底层还是调用ChatModel,ChatClient支持Fluent Api,ChatModel不支持。两者都是表示某个模型,具体是什么模型,需要看配置。

基于ChatClient 和 ChatModel 实现聊天效果:

java 复制代码
@Configuration
public class ChatConfig {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    @Bean
    public ChatClient chatClient() {
        return ChatClient
                .builder(openAiChatModel)
                .build();
    }
}

Controller层代码:

java 复制代码
@RestController
public class ChatDemoController {
    @Autowired
    private ChatClient chatClient;
    @Autowired
    private ZhiPuAiChatModel chatModel;
    @Autowired
    private OpenAiChatModel openAiChatModel;
    
    /**
     * openAi 聊天
     *
     * @param message
     * @return
     */
    @GetMapping("/ai/openAiChat")
    public Map openAiChat(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", openAiChatModel.call(message));
    }
    /**
     * zhipuAi 聊天
     *
     * @param message
     * @return
     */
    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatModel.call(message));
    }
    /**
     * ChatClient使用(流式调用)
     * @param message
     * @param voice
     * @return
     */
    @GetMapping("/ai/chatClient")
    Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
        return Map.of(
                "completion",
                chatClient.prompt()
                        .system(sp -> sp.param("voice", voice))
                        .user(message)
                        .call()
                        .content());
    }
}
2. 文生图
java 复制代码
@RestController
public class ChatDemoController {

    @Autowired
    private OpenAiImageModel imageModel;
    /**
     * 图片生成(文生图)
     *
     * @param message
     * @return
     */
    @GetMapping("/ai/imageGeneration")
    public Map imageGeneration(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        OpenAiImageOptions imageOptions = OpenAiImageOptions.builder()
                .withQuality("hd")
                .withN(1)
                .withHeight(1024)
                .withWidth(1024).build();
        ImagePrompt imagePrompt = new ImagePrompt(message, imageOptions);
        ImageResponse response = imageModel.call(imagePrompt);
        return Map.of("generation", response.getResult().getOutput().getUrl());
    }
}
3. 多模态

多模态(Multimodal)指的是数据或信息的多种表现形式。在人工智能领域,我们经常会听到这个词,尤其是在近期大型模型(如GPT-4)开始支持多模态之后。

  • 模态:模态是指数据的一种形式,例如文本、图像、音频等。每一种形式都是一种模态。
  • 多模态:多模态就是将不同模态的数据结合起来,以更全面、更丰富的方式来处理信息。比如,我们可以同时处理文本、图像、语音等多种类型的数据。

举个例子,如果我想告诉你"我有一个苹果",我可以用文字写出来,也可以用语言说出来,还可以用图片画出来。这就是相同信息的多种模态表现形式。

同样地,给大模型一副图片,可以上大模型对这张图片进行详细地描述。给大模型一段文本,可以让大模型进行概要提取,内容总结等。

java 复制代码
@RestController
public class ChatDemoController {

    @Autowired
    private OpenAiChatModel openAiChatModel;
    /**
     * 多模态
     *
     * @param message
     * @return
     * @throws MalformedURLException
     */
    @GetMapping("/ai/multimodal")
    public String Multimodal(@RequestParam(value = "message", defaultValue = "解释一下你在这张图片上看到了什么?") String message) throws MalformedURLException {
        var userMessage = new UserMessage(message,
                List.of(new Media(MimeTypeUtils.IMAGE_PNG,
                        new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));

        ChatResponse response = openAiChatModel.call(new Prompt(List.of(userMessage),
                OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()).build()));
        return response.getResult().getOutput().getContent();
    }
}
4. 语音转文字

语音文件需要在spring-ai中下载spring-ai/models/spring-ai-openai/src/test/resources/speech at main · spring-projects/spring-ai · GitHubAn Application Framework for AI Engineering. Contribute to spring-projects/spring-ai development by creating an account on GitHub.https://github.com/spring-projects/spring-ai/tree/main/models/spring-ai-openai/src/test/resources/speech

java 复制代码
@RestController
public class ChatDemoController {

    @Autowired
    private OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel;
    @Value("classpath:/speech/jfk.flac")
    private Resource audioFile;
    /**
     * 语音转文字
     */
    @GetMapping("/ai/audioTranscription")
    private String audioTranscription(){
        OpenAiAudioTranscriptionOptions transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
                .withResponseFormat(OpenAiAudioApi.TranscriptResponseFormat.TEXT)
                .withTemperature(0f)
                .build();
        AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
        AudioTranscriptionResponse response = openAiAudioTranscriptionModel.call(transcriptionRequest);
        return response.getResult().getOutput();
    }
}
5. Function Calling 工具调用

大模型是基于历史数据进行训练的,回答我们的问题也是基于历史数据进行回复, 如果你想要大模型具备获取最新消息的能力, 此时,就需要用到工具机制,它能帮助大模型获取最新的数据消息.

Function Calling 工作原理图:

执行流程,如下图:

首先,当我们发送问题给大模型的时候,比如"今天是几号?",大模型会响应一个结果给我们,这个结果不是问题的答案,而是大模型告诉我们需要执行哪个工具。我们执行工具后,才能得到问题的答案,但这个答案可能不太像人回复的,不太符合自然语言的样子,比如工具结果是"2024-07-13 11:23:00",此时我们需要把问题,执行工具请求和工具执行结果一起发给大模型,得到最好的答案。

工具定义:

  • 实现 java.util.function.Function 接口

  • @Description注解: 注释是可选的,它提供了一个函数描述,帮助模型理解何时调用函数。它是一个重要的属性,可以帮助AI模型确定要调用的客户端函数。

  • @JsonClassDescription注解: 对方法进行描述.

  • @JsonPropertyDescription注解: 对参数进行描述.

代码实现:

java 复制代码
@Component
@Description("先获取指定地点,再获取当前时间")
public class DateService implements Function<DateService.Request, DateService.Response> {

    @JsonClassDescription("地点请求")
    public record Request(@JsonPropertyDescription("地点")String address) { }

    public record Response(String date) { }
   
    @Override
    public Response apply(Request request) {
        System.out.println(request.address);
        return new Response(String.format("%s的当前时间是%s", request.address, LocalDateTime.now()));
    }
}

工具调用:

java 复制代码
@RestController
public class ChatDemoController {

    @Autowired
    private OpenAiChatModel openAiChatModel;
    /**
     * 工具调用
     */
    @GetMapping("/ai/function")
    public String function(@RequestParam String message) {
        Prompt prompt = new Prompt(message, OpenAiChatOptions.builder().withFunction("dateService").build());
//        Prompt prompt = new Prompt(message, OpenAiChatOptions.builder().withFunctionCallbacks(
//                List.of(FunctionCallbackWrapper.builder(new DateService())
//                        .withName("dateService")
//                        .withDescription("获取指定地点的当前时间").build())
//        ).build());
        Generation generation = openAiChatModel.call(prompt).getResult();
        return (generation != null) ? generation.getOutput().getContent() : "";
    }
}
6. Embeddings文本向量化

什么叫向量? 向量可以理解为平面坐标中的一个坐标点(x,y),在编程领域,一个二维向量就是一个大小为float类型的数组。也可以用三维坐标系中的向量表示一个空间中的点. 而文本向量化是指,利用大模型可以把一个字,一个词或一段话映射为一个多维向量.

为什么要向量化? 当我们把所有的文本生成向量后, 就可以利用向量的特点,进行相似度搜索.这种搜索算法比elasticsearch的分词算法更好.

Spring AI 支持的向量数据库:

以下我们使用Redis作为向量数据库

然后需要注意的是,普通的Redis是不支持向量存储和查询的,需要额外的redisearch模块,我这边是直接使用docker来运行一个带有redisearch模块的redis容器的,命令为:

Go 复制代码
docker run -p 6379:6379 redis/redis-stack-server:latest

注意端口6379不要和你现有的Redis冲突了。

引入redis依赖

XML 复制代码
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-redis-store</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>5.1.0</version>
        </dependency>

定义向量模型和Redis向量数据库 Bean

java 复制代码
@Configuration
public class RedisConfig {
    @Autowired
    private EmbeddingModel openAiEmbeddingModel;

    @Bean
    public RedisVectorStore vectorStore() {
        RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig.builder()
                .withURI("redis://127.0.0.1:6379")
//                .withIndexName("rag_index")
//                .withPrefix("rag:")
                .withMetadataFields(
                        RedisVectorStore.MetadataField.text("filename"),
                        RedisVectorStore.MetadataField.text("question"))
                .build();

        return new RedisVectorStore(config, openAiEmbeddingModel,true);
    }

    @Bean
    public EmbeddingModel openAiEmbeddingModel() {
        // Can be any other EmbeddingModel implementation.
        return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
    }
}

定义向量存储和搜索核心逻辑代码

文本读取,解析和存储,SpringAi提供了以下核心概念:

Document

  1. DocumentReader :用来读取TXT、PDF等文件内容

    • JsonReader:读取JSON格式的文件
    • TextReader:读取txt文件
    • PagePdfDocumentReader:使用Apache PdfBox读取PDF文件
    • TikaDocumentReader:使用Apache Tika来读取PDF, DOC/DOCX, PPT/PPTX, and HTML等文件
  2. DocumentTransformer :用来解析文件内容

    • **tokenTextSplitter:**按照token进行解析。
  3. DocumentWriter :用来写入文件内容到向量数据库

    • **VectorStore:**DocumentWriter的子类。

流程如下:

java 复制代码
/**
*
*文本解析
*/
public class CustomerTextSplitter extends TextSplitter {

    @Override
    protected List<String> splitText(String text) {
        return List.of(split(text));
    }

    public String[] split(String text) {
        return text.split("\\s*\\R\\s*\\R\\s*");
    }
}
java 复制代码
@Component
public class DocumentService {

    @Value("classpath:meituan-qa.txt")
    private Resource resource;
    @Autowired
    private RedisVectorStore vectorStore;

    /**
     * 向量存储
     * @return
     */
    public List<Document> loadText() {
        //文本读取
        TextReader textReader = new TextReader(resource);
        textReader.getCustomMetadata().put("filename", "meituan-qa.txt");
        List<Document> documents = textReader.get();

        CustomerTextSplitter customerTextSplitter= new CustomerTextSplitter();
        List<Document> list = customerTextSplitter.apply(documents);
        // 把问题存到元数据中
        list.forEach(document -> document.getMetadata().put("question", document.getContent().split("\\n")[0]));
        // 向量存储(文本存储)
        vectorStore.add(list);
        return list;
    }

    /**
     * 向量搜索
     * @param message
     * @return
     */
    public List<Document> search(String message) {
        List<Document> documents = vectorStore.similaritySearch(message);
        return documents;
    }

    /**
     * 元数据搜索
     * @param message
     * @param question
     * @return
     */
    public List<Document> metadataSearch(String message, String question) {
        return vectorStore.similaritySearch(
                SearchRequest
                        .query(message)
//                        .withTopK(5)
                        .withSimilarityThreshold(0.1)
                        .withFilterExpression(String.format("question in ['%s']", question)));
    }
}

需要向量的文本 meituan-qa.txt

bash 复制代码
Q:在线支付取消订单后钱怎么返还?
订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。

Q:怎么查看退款是否成功?
退款会在一个工作日之内到美团账户余额,可在"账号管理------我的账号"中查看是否到账。

Q:美团账户里的余额怎么提现?
余额可到美团网(meituan.com)------"我的美团→美团余额"里提取到您的银行卡或者支付宝账号,另外,余额也可直接用于支付外卖订单(限支持在线支付的商家)。

Q:余额提现到账时间是多久?
1-7个工作日内可退回您的支付账户。由于银行处理可能有延迟,具体以账户的到账时间为准。

Q:申请退款后,商家拒绝了怎么办?
申请退款后,如果商家拒绝,此时回到订单页面点击"退款申诉",美团客服介入处理。

Q:怎么取消退款呢?
请在订单页点击"不退款了",商家还会正常送餐的。

Q:前面下了一个在线支付的单子,由于未付款,订单自动取消了,这单会计算我的参与活动次数吗?
不会。如果是未支付的在线支付订单,可以先将订单取消(如果不取消需要15分钟后系统自动取消),订单无效后,此时您再下单仍会享受活动的优惠。

Q:为什么我用微信订餐,却无法使用在线支付?
目前只有网页版和美团外卖手机App(非美团手机客户端)订餐,才能使用在线支付,请更换到网页版和美团外卖手机App下单。

Q:如何进行付款?
美团外卖现在支持货到付款与在线支付,其中微信版与手机触屏版暂不支持在线支付。

Controller层代码实现

java 复制代码
@RestController
public class ChatDemoController {

    @Autowired
    private DocumentService documentService;
    /**
     * 向量存储
     */
    @GetMapping("/ai/vectorStore")
    public Map vectorStore() {
        List<Document> documents = documentService.loadText();
        return Map.of("generation", documents);
    }
    /**
     * 向量搜索
     * @param message
     * @return
     */
    @GetMapping("/ai/documentSearch")
    public List<Document> documentSearch(@RequestParam String message) {
        return documentService.search(message);
    }
    /**
     * 元数据搜索
     * @param message
     * @param question
     * @return
     */
    @GetMapping("/ai/metadataSearch")
    public List<Document> documentMetadataSearch(@RequestParam String message, @RequestParam String question) {
        return documentService.metadataSearch(message, question);
    }
}
7. RAG 检索增强生成

RAG是什么?检索增强生成又是什么意思?大模型的知识仅限于它所训练的数据,如果你问大模型,你们公司的xxx产品有什么作用,大模型肯定会回答不出来。如果你想让大模型拥有你们公司知识库的数据, 此时就可以用到RAG。

简单的讲,RAG的原理是,根据用户输入的问题,先从你们公司的知识库查询出答案,再把用户输的问题和搜索出来的答案,让大模型根据我们的答案回复用户的问题。

而根据用户问题,从知识库搜索问题,需要用到上面所说的文本向量化。根据文本的相识度,从知识库中搜索出符合用户问题的答案出来。

RAG的工作原理

RAG的工作原理可以分为以下几个步骤:

1.接收请求:首先,系统接收到用户的请求(例如提出一个问题)。

2.信息检索(R):系统从一个大型文档库中检索出与查询最相关的文档片段。这一步的目标是找到那些可能包含答案或相关信息的文档。

3.生成增强(A):将检索到的文档片段与原始查询一起输入到大模型(如chatGPT)中,注意使用合适的提示词,比如原始的问题是XXX,检索到的信息是YYY,给大模型的输入应该类似于:请基于YYY回答XXXX。

4.输出生成(G):大模型基于输入的查询和检索到的文档片段生成最终的文本答案,并返回给用户。

RAG代码实现

java 复制代码
@RestController
public class ChatDemoController {
    @Autowired
    private OpenAiChatModel openAiChatModel;
    @Autowired
    private DocumentService documentService;
    /**
     * RAG
     * @param message
     * @return
     */
    @GetMapping("/ai/customerService")
    public String customerService(@RequestParam String message) {

        // 向量搜索
        List<Document> documentList = documentService.search(message);

        // 提示词模板
        PromptTemplate promptTemplate = new PromptTemplate("{userMessage}\n\n 用以下信息回答问题:\n {contents}");

        // 组装提示词
        Prompt prompt = promptTemplate.create(Map.of("userMessage", message, "contents", documentList));

        // 调用大模型
        return openAiChatModel.call(prompt).getResult().getOutput().getContent();
    }
}
8. Advisor机制

Advisor是Spring AOP中的概念, 一个Advisor表示一个切面, 由Advice和PointCut组成,Advice表示切面的逻辑, PointCut表示切点, 也就是切那些方法.而Spring AI也用了Advisor的设计思想, 也具备前置切面和后置切面.

8.1 QuestionAnswerAdvisor

QuestionAnswerAdvisor的作用是对问题请求进行增强,增强逻辑为:

  1. 根据原始问题进行相似度搜索,得到匹配知识点
  2. 拼接RAG提示词模板

评估模型代码如下:

java 复制代码
@RestController
public class ChatDemoController {
    @Autowired
    private ChatClient chatClient;
    @Autowired
    private RedisVectorStore vectorStore;
    @Autowired
    private OpenAiChatModel chatModel;
    /**
     * 模型评估
     */
    @GetMapping("/ai/evaluation")
    public EvaluationResponse evaluation(String message) {
        //RAG
        ChatResponse response = chatClient.prompt()
                .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
                .user(message)
                .call()
                .chatResponse();

        // 评估器
        var relevancyEvaluator = new RelevancyEvaluator(ChatClient.builder(chatModel));
        // 评估请求
        EvaluationRequest evaluationRequest = new EvaluationRequest(message,
                (List<Content>) response.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS), response);
        // 评估结果
        EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
        return evaluationResponse;
    }
}

后续Spring AI会根据增强后的请求进行提示词模版的变量填充,得到请求最终的提示词,并将请求发送给大模型,得到大模型的返回结果,QuestionAnswerAdvisor也会对返回结果进行增强,会把匹配的知识点放入ChatResponse的metadata中。

8.2 MessageChatMemoryAdvisor

是一种Advisor,也是用来增强问答请求和响应的,而其中另外一个概念就是ChatMemory,默认实现为InMemoryChatMemory,它可以用来按conversationId进行历史对话记录的存储。

java 复制代码
@RestController
public class ChatDemoController {
    @Autowired
    private ChatClient chatClient;
    
    private InMemoryChatMemory chatMemory= new InMemoryChatMemory();
    /**
     * ChatMemory
     */
    @GetMapping("/ai/chatMemory")
    private String chatMemory(String message,String userId){
        ChatResponse response = chatClient.prompt()
                .advisors(new MessageChatMemoryAdvisor(chatMemory,userId,100))
                .system(sp -> sp.param("voice", "律师"))
                .user(message)
                .call()
                .chatResponse();
        return response.getResult().getOutput().getContent();
    }
}

因此MessageChatMemoryAdvisor的作用就是将原始请求和向量添加到ChatMemory中。

8.3 PromptChatMemoryAdvisor

也是用来记录历史对话记录的,和MessageChatMemoryAdvisor的不同点在于,MessageChatMemoryAdvisor是把每个历史请求和响应封装为Message增强到请求中,而PromptChatMemoryAdvisor是把所有请求和响应也会存到ChatMemory中,但是会把所有内容合并一条Message增强到请求中。

8.4 VectorStoreChatMemoryAdvisor

这个就更加强大了,它既会进行RAG,也会把存储历史对话,只不过会把对话记录封装为Document存到向量数据库中。

相关推荐
Jarlen几秒前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
大耳朵爱学习4 分钟前
掌握Transformer之注意力为什么有效
人工智能·深度学习·自然语言处理·大模型·llm·transformer·大语言模型
TAICHIFEI5 分钟前
目标检测-数据集
人工智能·目标检测·目标跟踪
蓑 羽6 分钟前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
qq_153214526411 分钟前
【2023工业异常检测文献】SimpleNet
图像处理·人工智能·深度学习·神经网络·机器学习·计算机视觉·视觉检测
Reese_Cool14 分钟前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
洛阳泰山14 分钟前
如何使用Chainlit让所有网站快速嵌入一个AI聊天助手Copilot
人工智能·ai·llm·copilot·网站·chainlit·copliot
儿创社ErChaungClub24 分钟前
解锁编程新境界:GitHub Copilot 让效率翻倍
人工智能·算法
工业甲酰苯胺25 分钟前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis