【Spring AI】基于SpringAI+Vue3+ElementPlus的QA系统实现一

整理不易,请不要吝啬你的赞和收藏。

1. 前言

这是 SpringAI 系列的第二篇文章,这篇文章将介绍如何基于 RAG 技术,使用 SpringAI + Vue3 + ElementPlus 实现一个 Q&A 系统。本文使用 deepseek 的 DeepSeek-V3 作为聊天模型,使用阿里百炼的 text-embedding-v3 作为向量模型,使用 redis 作为向量库。(PS:近期阿里百炼也上架了DeepSeek-V3  DeepSeek-R1 模型供开发者调用,如果觉得 DeepSeek 官方 AP I比较慢的话,可以去试试)。

什么是 RAG?

RAG(Retrieval-Augmented Generation,检索增强生成)技术是一种结合了检索(Retrieval)和生成(Generation)的自然语言处理方法。它通过检索相关的文档片段来增强语言模型的生成能力,从而提高生成文本的质量和相关性。RAG技术的核心思想是利用检索系统从大规模文档集合中找到与输入问题最相关的文档片段,然后将这些片段作为上下文信息输入到生成模型中,生成更加准确和详细的回答。

注:以下几行内容节选自 spring ai alibaba RAG 部分说明文档。

RAG 的工作流程分为两个阶段:Indexing pipeline 和 RAG 。

  • Indexing pipeline 阶段主要是将结构化或者非结构化的数据或文档进行加载和解析、chunk切分、文本向量化并保存到向量数据库。

  • RAG 阶段主要包括将 promp t文本内容转为向量、从向量数据库检索内容、对检索后的文档 chunk 进行重排和 prompt 重写、最后调用大模型进行结果的生成。

下面是大致流程:

2. 效果展示

让聊天模型基于我上传的文件,进行回答。

3. 设计思路

整个 Q&A 功能的时序图:

4. 前提条件

  • 已有自己的向量库,如 Redis、Faiss。想了解更多参考我的博客:

    【LLM】Redis 作为向量库入门指南_redis 向量数据库-CSDN博客文章浏览阅读1k次,点赞25次,收藏12次。这篇文章将介绍基于 RedisSearch 的Redis向量库实现。通过阅读本篇文章,你将学习到如何创建向量索引,如何存储和更新向量,如何进行向量搜索,如何使用阿里百炼 Embedding Model 文本向量化,如何集成到 spring boot 中并实现向量的存储和搜索等。_redis 向量数据库 https://blog.csdn.net/u013176571/article/details/145122380
  • 已安装 18.3 或更高版本的 Node.js ,未安装进入 官网下载 ,LTS 为长期支持版,Current 为最新功能版。

  • 使用过Spring AI,可参考我的这篇博客:

    【Spring AI】Spring AI Alibaba的简单使用_spring-ai-alibaba-starter-CSDN博客文章浏览阅读696次,点赞7次,收藏6次。项目中引入Spring AI、Spring AI Alibaba踩坑笔记,并实现简单的几种聊天模式。_spring-ai-alibaba-starter https://blog.csdn.net/u013176571/article/details/144488475

需要注意:写这篇文章的时候,我对 spring-ai-core 版本进行了升级,版本为:1.0.0-M5。并且舍弃了 spring-ai-alibaba-starter 包,改为引入使用更广泛的 spring-ai-openai 包,这些修改意味着之前写的文章中的部分代码要做相应的修改,下文将会介绍。

5. 引入 Spring AI

5.1 pom 文件配置

spring-ai 版本为 1.0.0-M5。

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-core</artifactId>
    <version>${spring-ai-core.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
    <version>${spring-ai-core.version}</version>
</dependency>

由于 spring-ai 相关依赖包还没有发布到中央仓库,需要在项目的 pom.xml 依赖中加入如下仓库配置。

<repositories>
    <repository>
        <id>maven2</id>
        <name>maven2</name>
        <url>https://repo1.maven.org/maven2/</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

maven 的 setting.xml 中做出如下更改

<mirror>  
    <id>alimaven</id>  
    <name>aliyun maven</name>  
    <url>https://maven.aliyun.com/repository/public</url> 
    <!-- 表示除了spring-milestones、maven2其它都走阿里云镜像  --> 
    <mirrorOf>*,!spring-milestones,!maven2</mirrorOf>  
</mirror>

5.2 application.yml 配置

聊天模型基于 deepseek,‘your-api-key’ 为 deepseek 上申请的api-key,base-url 设置为 deepseek 的接口 url。

spring:
  ai:
    openai:
      api-key: your-api-key
      base-url: https://api.deepseek.com
      chat:
        model: deepseek-chat
    chat:
      client:
        enabled: false

6. 接口开发

由于我们使用的是 spring-ai-openai 依赖,依赖中的 ChatModel 、 EmbeddingModel 等 Bean 默认配置都是基于 OpenAI 的模型,所以我们都需要根据自己使用的模型重新注册这些 Bean。

6.1 注册 EmbeddingModel

从上文的 RAG 流程中我们知道,实现 Q&A 系统的第一步是将知识库的资源向量化(也就是 Embedding)。

我这里使用阿里百炼的嵌入模型,下图是其主要文本向量模型对比:

这篇文章使用 text-embedding-v3,需要新注册一个 EmbeddingModel Bean。

@Bean
public EmbeddingModel embeddingModel() {
    return new OpenAiEmbeddingModel(
            // embeddingBaseUrl 为阿里百炼的接口 url ,值为:https://dashscope.aliyuncs.com/compatible-mode
            // embeddingApiKey 为阿里百炼上申请的 api-key 。
            new OpenAiApi(embeddingBaseUrl, embeddingApiKey),
            MetadataMode.EMBED,
            OpenAiEmbeddingOptions.builder()
                    .model("text-embedding-v3")  // 选用的嵌入模型
                    .dimensions(1024)            // 向量维度
                    .build(),
            RetryUtils.DEFAULT_RETRY_TEMPLATE);
}

6.2 注册 ChatModel

我使用 DeepSeek 的 DeepSeek-V3 作为聊天模型,这里需要重新注入 ChatModel Bean。

@Bean
public ChatModel chatModel() {
    // baseUrl 为 DeepSeek 接口 url,值为:https://api.deepseek.com
    // apiKey 为申请的秘钥,defaultChatModel 为聊天模型。
    return new OpenAiChatModel(new OpenAiApi(baseUrl, apiKey), OpenAiChatOptions.builder()
            .model("deepseek-chat")    // 选用的聊天模型
            .temperature(0.7d)         // 
            .build());
}

6.3 注册 VectorStore

我的这篇文章

【LLM】RedisSearch 向量相似性搜索在 SpringBoot 中的实现_jedis ftsearch-CSDN博客文章浏览阅读762次,点赞11次,收藏11次。这篇文章将介绍两种实现方式,第一种为使用Jedis中的UnifiedJedis类实现,第二种为使用SpringAI中的VectorStore实现。通过这边文章你将收获,如何使用阿里百炼Embedding模型实现文本向量化,如何通过连接池获取UnifiedJedis对象,如何在SpringBoot中实现向量数据的存储以及使用fTSearch进行向量相似性搜索,如何使用SpringAI的VecotStore。_jedis ftsearch https://blog.csdn.net/u013176571/article/details/145235761

做过详细介绍,这里增加两个元数据配置 docId(文档Id) 和 docName(文档名),用于在回答的时候进行过滤。

@Bean
public VectorStore vectorStore() {
    return RedisVectorStore.builder(jedisPooled, embeddingModel)
            .indexName(indexName)
            .prefix(prefix)
            .embeddingFieldName("embedding")      // 向量字段名,默认 embedding
            .contentFieldName("content")          // 存储的原始文本内容
            .initializeSchema(initializeSchema)   // 是否初始化索引配置信息
            .batchingStrategy(batchingStrategy)
            .metadataFields(
                    RedisVectorStore.MetadataField.text(SpringAIConst.VectorStore.METADATA_DOC_ID),   // 存储的元数据信息
                    RedisVectorStore.MetadataField.text(SpringAIConst.VectorStore.METADATA_DOC_NAME)  // 存储的元数据信息
            ).build();
}

6.4 文档 Embedding

文本文档 Embedding 之前要先对文本文档进行分割,以满足大模型接口的输入字符数或者 Token 限制。

6.4.1 文档分割

文本分割的算法有:字符分割、递归字符文本分割、语义文档分割等等,如果有需要我将在后续文章中介绍这些算法。

本文使用较为常见的:递归字符文本分割。在递归字符文本分割中,通常会按照分隔符的优先级逐步分割文本。以英文常用的分隔符为例 ["\n\n", "\n", " "] ,其表示先用 "\n\n" 分割成段落,然后在每个段落中用 "\n" 分割成行,最后在每行中用 " " 分割成单词,这种逐层分割的方式可以将文本逐步细化为更小的单元。

public class RecursiveCharacterTextSplitter implements TextSplitterIntf {
    /**
     * 分隔符列表,用于拆分文本
     * 中文:"\n\n", "。", "!", "?", ",", ";"
     * 英文:"\n\n", "\n", " "
     */
    private List<String> separators = Arrays.asList("\n\n", "。", "!", "?", ",", ";");
    /**
     * 每个块的最大大小
     */
    private int chunkSize = 250;
    /**
     * 块之间的重叠大小,用来维持上下文关系
     */
    private int chunkOverlap = 30;

    public RecursiveCharacterTextSplitter() {
    }

    public RecursiveCharacterTextSplitter(int chunkSize, int chunkOverlap) {
        this.chunkSize = chunkSize;
        this.chunkOverlap = chunkOverlap;
        if (chunkOverlap >= chunkSize) {
            throw new IllegalArgumentException("chunkOverlap must be smaller than chunkSize");
        }
    }

    public RecursiveCharacterTextSplitter(List<String> separators, int chunkSize, int chunkOverlap) {
        this.separators = separators;
        this.chunkSize = chunkSize;
        this.chunkOverlap = chunkOverlap;
        if (chunkOverlap >= chunkSize) {
            throw new IllegalArgumentException("chunkOverlap must be smaller than chunkSize");
        }
    }

    /**
     * Spring ai Document 拆分
     *
     * @param documents
     * @param metaData  文档元数据信息
     * @return
     */
    @Override
    public List<Document> split(List<Document> documents, List<Map<String, Object>> metaData) {
        List<JSONObject> docList = this.beforeSplit(documents, metaData);
        List<Document> chunkedDocuments = new ArrayList<>();
        docList.forEach(item -> {
            Document document = item.getObject("document", Document.class);
            Map<String, Object> singleMetaData = item.getObject("metaData", Map.class);
            List<String> chunks = new ArrayList<>();
            splitRecursive(document.getContent(), chunks);
            chunks.forEach(chunk -> {
                chunkedDocuments.add(new Document(chunk, singleMetaData));
            });
        });
        return chunkedDocuments;
    }

    /**
     * 递归地拆分文本
     *
     * @param text
     * @param chunks
     */
    private void splitRecursive(String text, List<String> chunks) {
        if (text.length() <= chunkSize) {
            chunks.add(text);
            return;
        }

        String separator = findBestSeparator(text);
        if (separator == null) {
            chunks.add(text);
            return;
        }

        String[] parts = text.split(separator, -1);
        StringBuilder currentChunk = new StringBuilder();
        int currentLength = 0;

        for (String part : parts) {
            if (currentLength + part.length() > chunkSize) {
                if (currentChunk.length() > 0) {
                    chunks.add(currentChunk.toString());
                    // 通过回退chunkOverlap的距离创建来重叠部分
                    int overlapStart = Math.max(0, currentChunk.length() - chunkOverlap);
                    currentChunk = new StringBuilder(currentChunk.substring(overlapStart));
                    currentLength = currentChunk.length();
                }
            }
            currentChunk.append(part).append(separator);
            currentLength += part.length() + separator.length();
        }
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString());
        }
    }

    /**
     * 查找文本中第一个出现的分隔符
     *
     * @param text
     * @return
     */
    private String findBestSeparator(String text) {
        for (String separator : separators) {
            if (text.contains(separator)) {
                return separator;
            }
        }
        return null;
    }
}

6.4.2 BatchingStrategy 介绍

每个 Embedding 模型都有最大处理 token 数限制,对于一个大文档,我们不能够一次性发送给模型处理,为了解决这个问题,Spring AI 实现了一种批处理策略,这个方法将大量文档分解成更小的批次,不仅能够适配 Embedding Model 的最大上下文,还能够提高性能并更有效地利用API速率限制。

Spring AI 通过 BatchingStrategy 接口提供此功能,该接口允许根据文档的 token 数量将文档处理为子批次,这个接口有一个默认实现 TokenCountBatchingStrategy ,这里我们需要根据自己选择的模型重新注册该 Bean。

@Bean
public BatchingStrategy tokenCountBatchingStrategy() {
    return new TokenCountBatchingStrategy(
            EncodingType.CL100K_BASE,  // 指定用于分词的编码类型
            8000,                      // 最大 Token 数量
            0.1                        // 设置预留百分比(默认 10%)
    );
}

参数解释:

  • EncodingType.CL100K_BASE:指定用于分词的编码类型。这种编码类型被 JTokkitTokenCountEstimator 使用,以准确估计token数量。

  • 8000:最大 Token 数量,默认使用 OpenAI 的最大输入token数(8191)。

  • 0.1:设置预留百分比(默认 10%),从最大输入token数中预留的token百分比,这为处理过程中可能的token数量增加创建了一个缓冲区。

6.5 保存文档到向量库

6.5.1 分割后的文本入向量库

为了防止同一文档重复入库,在文档入库前先计算文档的 Hash 值,并在文档保存到向量库后,将文档的信息(文档名,文档大小,分割文本大小等)保存到 Redis 中(key 为文档 Hash 值),方便后续操作中的重复判断。

    /**
     * 文档入库
     *
     * @param inputStream 文档输入流
     * @param docHash     文档Hash
     * @param docName     文档名
     * @throws Exception
     */
    public void addDocument(InputStream inputStream, String docHash, String docName) throws Exception {
        // 校验文档是否已经存在
        if (redisService.hasKey(CommonConst.RAG_KEY_PREFIX + docHash)) {
            return;
        }
        int docSize = inputStream.available();
        // 文件流转为 Document
        TextReader textReader = new TextReader(new InputStreamResource(inputStream));
        List<Document> documents = textReader.get();
        // 文档拆分
        TextSplitterIntf textSplitter = new RecursiveCharacterTextSplitter(250, 30);
        List<Document> newDocuments = textSplitter.split(documents, List.of(Map.of(SpringAIConst.VectorStore.METADATA_DOC_ID, docHash,
                SpringAIConst.VectorStore.METADATA_DOC_NAME, docName)));
        // 向量入库
        vectorStore.add(newDocuments);
        // 将文档信息存入 Redis
        JSONObject docJson = new JSONObject();
        docJson.put(SpringAIConst.VectorStore.METADATA_DOC_NAME, docName);
        docJson.put("docSize", docSize);
        docJson.put("docChunks", newDocuments.size());
        redisService.setObject(CommonConst.RAG_KEY_PREFIX + docHash, docJson);
    }

6.5.2 计算文档的 Hash 值

支持的算法:MD5、SHA-1、SHA-256

    /**
     * 计算文件 Hash 值
     *
     * @param inputStream 输入流
     * @param algorithm   算法,可选,MD5、SHA-1、SHA-256
     * @return
     * @throws Exception
     */
    public static String calculateFileHash(InputStream inputStream, String algorithm) throws Exception {
        // 创建MessageDigest实例
        algorithm = StringUtils.isEmpty(algorithm) ? CommonConst.SHA_256 : algorithm;
        MessageDigest digest = MessageDigest.getInstance(algorithm);

        // 打开文件输入流
        try (inputStream) {
            byte[] buffer = new byte[1024];
            int numRead;
            // 读取文件内容并更新摘要
            while ((numRead = inputStream.read(buffer)) != -1) {
                digest.update(buffer, 0, numRead);
            }
            // 完成哈希计算
            byte[] hashBytes = digest.digest();
            // 将哈希值转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

6.6 聊天接口

话不多说,直接上代码。

    /**
     * 带聊天记忆流式返回
     *
     * @param accessKey 聊天访问key,每次打开聊天页面重置
     * @param inputInfo 聊天输入内容
     * @param docIds    参考文档ID,多个用英文','号隔开
     * @return
     */
    @GetMapping("/memoryStreamWithApi")
    public Flux<ServerSentEvent<String>> memoryStreamWithApi(String accessKey, String inputInfo, String docIds) {
        // 接收并校验参数
        CommonUtil.checkAndThrow(accessKey, "您没有该功能访问权限!");
        CommonUtil.checkAndThrow(inputInfo, "请输入内容!");

        // 调用模型查询
        ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.builder(chatModel)
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
                .build()
                .prompt()
                .user(inputInfo)
                .advisors(advisor -> advisor.param(CHAT_MEMORY_CONVERSATION_ID_KEY, accessKey).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 5));
        // 先从向量库查询近似数据
        if (StringUtils.isNotEmpty(docIds)) {
            // 组装过滤条件,根据文档ID过滤,等价于 SQL 的 WHERE docId IN (docId1,docId2...)
            List<String> docIdList = Arrays.asList(docIds.split(","));
            Filter.Expression expression = new Filter.Expression(Filter.ExpressionType.IN,
                    new Filter.Key(SpringAIConst.VectorStore.METADATA_DOC_ID), new Filter.Value(docIdList));

            Advisor advisor = new QuestionAnswerAdvisor(vectorStore,
                    SearchRequest.builder().query(inputInfo)
                            .filterExpression(expression)
                            .topK(6)
                            .similarityThreshold(0.8d).build());
            chatClientRequestSpec.advisors(advisor);
        }
        return chatClientRequestSpec.stream()
                .chatResponse()
                .map(response -> ServerSentEvent.<String>builder()
                        .data(response.getResult().getOutput().getContent())
                        .build());

    }

7. 快速搭建 Elemenet-Plus 项目

写到这发现需要整理的文档太多,前端实现放到下一篇文章。

8. 参考文档

  • Spring AI RAG 文档
  • Spring AI Alibaba RAG 文档
  • Vue3 文档
  • Element Plus 文档
  • markdown-it 文档

9. 拓展

9.1 如何计算 Token ?

9.1.1 阿里通义

Token 是模型用来表示自然语言文本的基本单位,可以直观地理解为“字”或“词”。

  • 对于中文文本,1个 Token 通常对应一个汉字或词语。例如,“你好,我是通义千问”会被转换成['你好', ',', '我是', '通', '义', '千', '问']。

  • 对于英文文本,1个 Token 通常对应3至4个字母或1个单词。例如,"Nice to meet you."会被转换成['Nice', ' to', ' meet', ' you', '.']。

不同的大模型切分Token 的方法可能不同。可以本地运行 tokenizer 来估计文本的 Token 量。

9.1.2 DeepSeek

一般情况下模型中 token 和字数的换算比例大致如下:

  • 1 个英文字符 ≈ 0.3 个 token。

  • 1 个中文字符 ≈ 0.6 个 token。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/967812.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue】在Vue3中使用Echarts的示例 两种方法

文章目录 方法一template渲染部分js部分方法一实现效果 方法二template部分js or ts部分方法二实现效果 贴个地址~ Apache ECharts官网地址 Apache ECharts示例地址 官网有的时候示例显示不出来&#xff0c;属于正常现象&#xff0c;多进几次就行 开始使用前&#xff0c;记得先…

1.【线性代数】——方程组的几何解释

1.方程组的几何解释 概述举例举例一1. matrix2.row picture3.column picture 概述 三种表示方法 matrixrow picturecolumn picture 举例 举例一 { 2 x − y 0 − x 2 y 3 \begin{cases} 2x - y 0 \\ -x 2y 3 \end{cases} {2x−y0−x2y3​ 1. matrix [ 2 − 1 − 1 2…

KITE提示词框架:引导大语言模型的高效新工具

大语言模型的应用日益广泛。然而&#xff0c;如何确保这些模型生成的内容在AI原生应用中符合预期&#xff0c;仍是一个需要不断探索的问题。以下内容来自于《AI 原生应用开发&#xff1a;提示工程原理与实战》一书&#xff08;京东图书&#xff1a;https://item.jd.com/1013604…

【论文阅读】Revisiting the Assumption of Latent Separability for Backdoor Defenses

https://github.com/Unispac/Circumventing-Backdoor-Defenses 摘要和介绍 在各种后门毒化攻击中&#xff0c;来自目标类别的毒化样本和干净样本通常在潜在空间中形成两个分离的簇。 这种潜在的分离性非常普遍&#xff0c;甚至在防御研究中成为了一种默认假设&#xff0c;我…

基于Springmvc+MyBatis+Spring+Bootstrap+EasyUI+Mysql的个人博客系统

基于SpringmvcMyBatisSpringBootstrapEasyUIMysql的个人博客系统 1.项目介绍 使用Maven3Spring4SpringmvcMybatis3架构&#xff1b;数据库使用Mysql&#xff0c;数据库连接池使用阿里巴巴的Druid&#xff1b;使用Bootstrap3 UI框架实现博客的分页显示&#xff0c;博客分类&am…

香港中文大学 Adobe 推出 MotionCanvas:开启用户掌控的电影级图像视频创意之旅。

简介&#xff1a; 亮点直击 将电影镜头设计引入图像到视频的合成过程中。 推出了MotionCanvas&#xff0c;这是一种简化的视频合成系统&#xff0c;用于电影镜头设计&#xff0c;提供整体运动控制&#xff0c;以场景感知的方式联合操控相机和对象的运动。 设计了专门的运动条…

数据结构 单链表的模拟实现

一、链表的定义 线性表的链式存储就是链表。 它是将元素存储在物理上任意的存储单元中&#xff0c;由于⽆法像顺序表⼀样通过下标保证数据元素之间的逻辑关系&#xff0c;链式存储除了要保存数据元素外&#xff0c;还需额外维护数据元素之间的逻辑关系&#xff0c;这两部分信息…

Avalonia-wpf介绍

文章目录 工程简述窗体样式暗色模式亚克力模糊效果ExperimentalAcrylicBorder” 和 “ExperimentalAcrylicMaterial” 的介绍ExperimentalAcrylicBorderExperimentalAcrylicMaterial按钮排版按钮图标按钮命令响应式命令添加一个新对话框对话框窗口样式对话框的输入与输出显示对…

Node.js开发属于自己的npm包(发布到npm官网)

在 Node.js 中开发并发布自己的 npm 包是一个非常好的练习&#xff0c;可以帮助我们更好地理解模块化编程和包管理工具&#xff0c;本篇文章主要阐述如何使用nodejs开发一个属于自己的npm包&#xff0c;并且将其发布在npm官网。在开始之前确保已经安装了 Node.js 和 npm。可以在…

操作系统常见调度算法的详细介绍

目录 1. 先进先出算法&#xff08;FIFO&#xff09; 2. 前后台调度算法 3. 最短处理机运行期优先调度算法&#xff08;短进程优先算法&#xff09; 4. 最高响应比优先调度算法&#xff08;HRRN&#xff09; 5. 优先级调度算法 6. 时间片轮转调度算法 7. 多级反馈队列轮转…

ADB详细教程

目录 一、ADB简介 二、配置 配置环境变量 验证是否安装成功 三、简单使用 基本命令 设备连接管理 USB连接 WIFI连接&#xff08;需要USB线&#xff09; 开启手机USB调试模式 开启USB调试 四、其他 更换ADB默认启动端口 一、ADB简介 ADB&#xff08;Android Debug…

WEB攻防-第60天:PHP反序列化POP链构造魔术方法流程漏洞触发条件属性修改

目录 一、序列化与反序列化基础 1.1 什么是序列化与反序列化 二、魔术方法的生命周期 2.1 常见的魔术方法 2.2 模式方法的生命周期触发调用 2.2.1 __construct() 2.2.2 __destruct() 2.2.3 __sleep() 2.2.4 __wakeup() 2.2.5 __invoke() 2.2.6 __toS…

SQLMesh系列教程-2:SQLMesh入门项目实战(下篇)

上篇我介绍了环境搭建、duckdb数据准备、sqlmesh数据模型、plan命令运行。本文继续介绍审计、测试、生成血缘关系以及python模型等。 有两种方法可以在SQLMesh中创建宏。一种方法是使用Python&#xff0c;另一种方法是使用Jinja。这里我们创建Python宏。让我们构建简单的Python…

自主项目面试点总结

1、许苑–OJ判题系统 技术栈&#xff1a;Spring BootSpring Cloud AlibabaRedisMybatisMQDocker 项目地址: https://github.com/xuyuan-upward/xyoj-backend-microservice 1.1、项目介绍: 一个基于微服务的OJ系统&#xff0c;具备能够根据管理员预设的题目用例对用户提交的代…

Macbook Pro快速搭建Easysearch学习环境

在学习过程中&#xff0c;我们有时身边没有可用的服务器&#xff0c;这时就需要借助自己的 Mac 来安装和学习 Easysearch。然而&#xff0c;Easysearch 官网并未提供 Mac 版本的安装教程&#xff0c;下面我将详细整理我在 Mac 上安装和使用 Easysearch 的折腾经历。 Easysearc…

Arduino 第十三章:红外接收

Arduino 第十三章&#xff1a;红外接收 一、红外接收概述 红外接收在日常生活和电子制作中十分常见&#xff0c;像电视、空调等家电的遥控器就是利用红外信号来实现远程控制的。在 Arduino 项目里&#xff0c;借助红外接收模块能够让设备接收红外信号&#xff0c;进而实现诸如…

朝天椒USB服务器:解决加密狗远程连接

本文探讨朝天椒USB服务器用Usb Over Network技术&#xff0c;解决加密狗在虚拟机、云主机甚至异地的远程连接问题。 在企业数字化转型的浪潮中&#xff0c;加密狗作为防止软件盗版的重要手段&#xff0c;广泛应用于各类软件授权场景。然而&#xff0c;随着企业超融合进程不断加…

第二篇:电压与电流的“锡安之战”——电路定律在800V高压平台中的应用

——基尔霍夫与戴维南如何破解新能源汽车的“高压密码” 核心隐喻&#xff1a;电路定律的“数字起义” 在《黑客帝国》中&#xff0c;锡安的反抗军通过破解母体协议实现逆袭。而在新能源汽车的800V高压平台中&#xff0c; 基尔霍夫定律 和 戴维南定理 正是工程师手中的“通…

【牛客】动态规划专题一:斐波那契数列

文章目录 DP1 斐波那契数列法1&#xff1a;递归法2&#xff1a;动态规划法3&#xff1a;优化空间复杂度 2.分割连接字符串3. 给定一个字符串s和一组单词dict&#xff0c;在s中添加空格将s变成一个句子 DP1 斐波那契数列 法1&#xff1a;递归 // 递归 #include <iostream>…

innovus如何分步长func和dft时钟

在Innovus工具中&#xff0c;分步处理功能时钟&#xff08;func clock&#xff09;和DFT时钟&#xff08;如扫描测试时钟&#xff09;需要结合设计模式&#xff08;Function Mode和DFT Mode&#xff09;进行约束定义、时钟树综合&#xff08;CTS&#xff09;和时序分析。跟随分…