目录
Spring AI 介绍
Spring AI 组件介绍
Spring AI 结构化输出
Srping AI 多模态
Spring AI 本地Ollama
Spring AI 源码
Spring AI Advisor机制
Spring AI Tool Calling
Spring AI MCP
Spring AI RAG
Spring AI Agent

一、技术架构与核心流程‌

检索增强生成 (RAG) 的技术,用于解决将相关数据纳入提示以实现准确 AI 模型响应。

该方法涉及批处理风格的编程模型,其中作业从您的文档中读取非结构化数据,对其进行转换,然后将其写入矢量数据库。 概括地说,这是一个 ETL (提取、转换和加载) 管道。 向量数据库用于 RAG 技术的检索部分。

作为将非结构化数据加载到矢量数据库的一部分,最重要的转换之一是将原始文档拆分为更小的部分。 将原始文档拆分为较小部分的过程有两个重要步骤:

  1. 将文档拆分为多个部分,同时保留内容的语义边界。 例如,对于包含段落和表格的文档,应避免在段落或表格的中间拆分文档。 对于代码,请避免在方法实现的中间拆分代码。
  2. 将文档的各个部分进一步拆分为大小占 AI 模型令牌限制的一小部分。

RAG 的下一阶段是处理用户输入。 当 AI 模型要回答用户的问题时,该问题和所有“相似”文档片段都会被放入发送到 AI 模型的提示中。 这就是使用向量数据库的原因。它非常擅长寻找相似的内容。
在这里插入图片描述

1. 动态知识增强机制‌

RAG 通过实时检索外部知识库(如向量数据库)获取相关信息,再将检索结果作为上下文输入生成模型,解决传统大模型的“知识冻结”问题。

  • 对比传统生成模型‌:
维度传统生成模型RAG 架构
知识更新依赖训练数据时效性实时检索最新数据
可解释性黑盒生成可追溯参考文档路径

2. 四大核心步骤‌

  • 文档智能分块‌:
    使用算法(如 TokenTextSplitter)将文档切割为语义连贯的片段,避免破坏表格、代码块等结构化内容。
  • 向量编码‌:
    通过嵌入模型(如 OpenAI Embeddings)将文本转化为数学向量,使语义相近的内容(如“续航时间”与“电池容量”)具有相似向量特征。
  • 相似检索‌:
    将用户问题编码为向量,通过相似度匹配(如余弦相似度)从知识库中检索最相关的文档片段。
  • 生成增强‌:
    将检索结果作为上下文输入生成模型(如 GPT-4),输出附带参考资料溯源路径的回答。

二、Spring AI 的模块化实现

1‌. 核心组件‌

  • 检索模块‌:
    • VectorStoreDocumentRetriever:基于向量数据库(如 Redis、PgVector)实现高效相似性检索。
    • 支持动态过滤(如 similarityThreshold=0.5)和查询重写(RewriteQueryTransformer)。
  • 生成模块‌:
    • QuestionAnswerAdvisor:标准化流程,集成检索与生成,支持多模型切换(如 OpenAI/Qwen)。

2. 模块化扩展‌

  • 预处理阶段‌:自定义文档分块策略(如按章节或语义边界切割)。
  • 后处理阶段‌:对检索结果排序或加权(如时效性优先)。

三、优化策略与挑战

1. 性能调优‌

  • 检索效率‌:
    • 为分块添加标签(如“技术规格”“操作指南”)提升检索精准度。
    • 使用多查询扩展技术,生成问题变体以提高检索覆盖率。
  • 生成质量‌:
    • 设置空上下文处理策略(allowEmptyContext=true)控制无匹配时的行为。

2. 典型挑战‌

  • 语义连贯性‌:分块时需避免割裂上下文关联(如代码块拆分)。
  • 向量对齐‌:不同嵌入模型生成的向量需保证语义空间一致性。

四、应用场景示例

‌1. 智能客服‌:回答时自动引用最新产品说明书或法律条文。
‌2. 内容生成‌:基于检索结果输出带参考文献路径的专业报告。
‌3. 推荐系统‌:如结合 22,000 种啤酒数据集生成精准推荐。

五、详解

1. Advisors

Advisor 工作机制‌

  • 采用类似 WebFilter 的链式处理模式,通过 aroundCall 方法拦截请求/响应流
  • 支持优先级控制(实现 Ordered 接口),默认预留 1000 个优先级槽位供扩展
  • 通过 AdvisorContext 的 Map 结构实现数据在 Advisor 链中的流转

2. QuestionAnswerAdvisor

  • RAG 流程封装‌
    QuestionAnswerAdvisor 是 Spring AI 提供的开箱即用 RAG 组件,封装了检索(向量库查询)→ 上下文拼接 → 生成(LLM 调用)的完整流程,开发者仅需配置基础参数即可实现知识增强问答。
  • 设计目标‌
    通过标准化接口降低 RAG 实现复杂度,特别适合快速构建基于企业知识库的问答系统。
  • 核心配置参数
QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().similarityThreshold(0.5)  // 相似度过滤阈值.topK(3)                   // 返回文档数量.filterExpression("a==b")  // 元数据过滤条件.build()).build();

支持动态参数注入,如运行时通过 advisors(a -> a.param(“filterExpression”, “a==b”)) 修改过滤条件。

技术实现细节

  • 检索阶段‌

    • 自动将用户问题转换为向量查询
    • 支持混合检索策略(语义+关键词)
    • 结果存入响应元数据 RETRIEVED_DOCUMENTS 供后续评估
  • 生成阶段‌

    • 内置提示词模板拼接检索结果与原始问题
    • 支持通过 ChatClient 绑定任意生成模型(如 GPT-4)

3. RetrievalAugmentationAdvisor

核心架构设计
‌1. 拦截器模式‌
继承 BaseAdvisor 接口实现双重拦截能力,同步调用通过 aroundCall 方法处理,异步流式调用通过 aroundStream 方法处理,采用责任链模式传递请求/响应16。

public interface BaseAdvisor extends CallAroundAdvisor, StreamAroundAdvisor {default AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {AdvisedRequest processedRequest = before(request);AdvisedResponse response = chain.nextAroundCall(processedRequest);return after(response);}
}

‌2. 模块化流程‌
支持三阶段自定义扩展:

  • 预检索‌:通过 QueryTransformer 实现查询重写或扩展(如 HyDE 策略)48‌
  • 检索‌:集成向量数据库查询,支持相似度阈值过滤和元数据筛选34
  • 后处理‌:通过 DocumentCompressor 压缩上下文长度,提升生成效率5

关键配置参数

RetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(weaviateVectorStore)  // 绑定向量库.similarityThreshold(0.6)         // 相似度过滤.filterExpression("category=='tech'") // 元数据过滤.build()).queryAugmenter(new HyDEQueryAugmenter()) // 查询增强.build();

支持动态注入运行时参数(如通过 advisors(a -> a.param(“filterExpression”, “status==‘active’”)) 修改过滤条件)。

核心特性
‌1. 优先级控制‌
实现 Ordered 接口,默认优先级为 HIGHEST_PRECEDENCE + 1000,允许通过 setOrder 调整执行顺序。

2‌. 上下文管理‌
自动将检索结果存入 AdvisorContext,可通过 RETRIEVED_DOCUMENTS 键值获取匹配文档列表,供后续评估或日志记录。

3‌. 错误降级机制‌
重写 after 方法可实现检索失败时的默认响应生成,保障服务可用性。

性能优化建议
‌1. 检索阶段‌

  • 对高频查询实施 Redis 缓存,减少向量计算开销
  • 使用 similarityThreshold 和 topK 平衡召回率与响应速度
  1. 生成阶段‌
  • 通过 DocumentCompressor 移除冗余文本,降低 Token 消耗
  • 监控 aroundCall 方法耗时定位瓶颈(如向量查询或 LLM 调用延迟)

扩展开发示例
‌1. 自定义检索器‌

public class HybridRetriever implements DocumentRetriever {@Overridepublic List<Document> retrieve(String query) {// 混合语义检索+关键词检索}
}

2‌. 动态查询增强‌

public class DynamicQueryAugmenter implements QueryAugmenter {@Overridepublic String augment(String query, Map<String,Object> context) {// 根据上下文重写查询}
}

Modules

pring AI 实现了一个模块化 RAG 架构

Pre-Retrieval

预检索模块负责处理用户查询以实现最佳检索结果。
查询转换
一个用于转换输入查询以使其更有效地执行检索任务、解决挑战的组件 例如格式不佳的查询、不明确的术语、复杂的词汇或不支持的语言。

  • CompressionQueryTransformer
    使用大型语言模型来压缩对话历史记录和后续查询 转换为捕获对话本质的独立查询。
Query query = Query.builder().text("And what is its second largest city?").history(new UserMessage("What is the capital of Denmark?"),new AssistantMessage("Copenhagen is the capital of Denmark.")).build();QueryTransformer queryTransformer = CompressionQueryTransformer.builder().chatClientBuilder(chatClientBuilder).build();Query transformedQuery = queryTransformer.transform(query);
  • RewriteQueryTransformer
    使用大型语言模型重写用户查询,以便在 查询目标系统,例如 Vector Store 或 Web 搜索引擎。
Query query = new Query("I'm studying machine learning. What is an LLM?");QueryTransformer queryTransformer = RewriteQueryTransformer.builder().chatClientBuilder(chatClientBuilder).build();Query transformedQuery = queryTransformer.transform(query);
  • TranslationQueryTransformer
    使用大型语言模型将查询转换为支持的目标语言 通过用于生成文档嵌入的嵌入模型。
Query query = new Query("Hvad er Danmarks hovedstad?");QueryTransformer queryTransformer = TranslationQueryTransformer.builder().chatClientBuilder(chatClientBuilder).targetLanguage("english").build();Query transformedQuery = queryTransformer.transform(query);

查询扩展
一个组件,用于将输入查询扩展为查询列表,以解决诸如格式不正确的查询等挑战 通过提供替代查询公式,或将复杂问题分解为更简单的子查询。

  • MultiQueryExpander
    使用大型语言模型将查询扩展为多个语义不同的变体 捕获不同的视角,有助于检索其他上下文信息和增加机会 找到相关结果。
MultiQueryExpander queryExpander = MultiQueryExpander.builder().chatClientBuilder(chatClientBuilder).numberOfQueries(3).build();
List<Query> queries = queryExpander.expand(new Query("How to run a Spring Boot app?"));
Retrieval

检索模块负责查询数据系统(如 vector store)并检索最相关的文档。

文件搜索

负责从底层数据源(如搜索引擎、向量存储、 数据库或知识图谱)。

  • VectorStoreDocumentRetriever
    从向量存储中检索语义上与输入相似的文档 查询。它支持根据元数据、相似性阈值和 top-k 结果进行筛选。
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).similarityThreshold(0.73).topK(5).filterExpression(new FilterExpressionBuilder().eq("genre", "fairytale").build()).build();
List<Document> documents = retriever.retrieve(new Query("What is the main character of the story?"));
文档联接

一个组件,用于将基于多个查询和从多个数据源检索的文档合并到 单个文档集合。作为连接过程的一部分,它还可以处理重复文档和互惠 排名策略。

  • ConcatenationDocumentJoiner
    合并基于多个查询和多个数据源检索的文档 将它们连接到单个文档集合中。如果存在重复的文档,则保留第一次出现。 每个文档的分数保持原样。
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);
Post-Retrieval

检索后模块负责处理检索到的文档,以实现最佳的生成结果。
一个组件,用于根据查询对检索到的文档进行后处理,解决诸如中间丢失、模型中的上下文长度限制以及减少检索信息中的噪声和冗余等挑战。
例如,它可以根据文档与查询的相关性对文档进行排名,删除不相关或冗余的文档,或者压缩每个文档的内容以减少干扰和冗余。

Generation

生成模块负责根据用户查询和检索到的文档生成最终响应。
查询扩充
一个组件,用于使用其他数据来扩充输入查询,可用于提供大型语言模型 替换为回答用户查询所需的上下文。
ContextualQueryAugmenter 使用来自所提供文档内容的上下文数据来增强用户查询。

QueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder().build();

六、ETL

提取、转换和加载 (ETL) 框架是检索增强生成 (RAG) 用例中数据处理的主干。
ETL 管道编排从原始数据源到结构化向量存储的流程,确保数据处于最佳格式,以便 AI 模型进行检索。
RAG 用例是文本,通过从数据主体中检索相关信息来提高生成输出的质量和相关性,从而增强生成模型的功能。

TL 管道创建、转换和存储实例。
在这里插入图片描述
ETL 管道有三个主要组件:

  • DocumentReader实现Supplier<List>
  • DocumentTransformer实现Function<List, List>
  • DocumentWriter实现Consumer<List>
    在这里插入图片描述
    DocumentReader
public interface DocumentReader extends Supplier<List<Document>> {default List<Document> read() {return get();}
}

DocumentTransformer

public interface DocumentTransformer extends Function<List<Document>, List<Document>> {default List<Document> transform(List<Document> transform) {return apply(transform);}
}

DocumentWriter

public interface DocumentWriter extends Consumer<List<Document>> {default void write(List<Document> documents) {accept(documents);}
}

相关类
在这里插入图片描述

DocumentReader

Transformers

TextSplitter

一个抽象基类,可帮助划分文档以适应 AI 模型的上下文窗口。

TokenTextSplitter

使用 CL100K_BASE 编码根据令牌计数将文本拆分为块。

@Component
class MyTokenTextSplitter {public List<Document> splitDocuments(List<Document> documents) {TokenTextSplitter splitter = new TokenTextSplitter();return splitter.apply(documents);}public List<Document> splitCustomized(List<Document> documents) {TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 5000, true);return splitter.apply(documents);}
}
ContentFormatTransformer

这是一个使用生成式 AI 模型从文档内容中提取关键字并将其添加为元数据。

@Component
class MyKeywordEnricher {private final ChatModel chatModel;MyKeywordEnricher(ChatModel chatModel) {this.chatModel = chatModel;}List<Document> enrichDocuments(List<Document> documents) {KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);return enricher.apply(documents);}
}
KeywordMetadataEnricher

KeywordMetadataEnricher是一个DocumentTransformer,它使用生成式AI模型从文档内容中提取关键字并将其添加为元数据。

@Component
class MyKeywordEnricher {private final ChatModel chatModel;MyKeywordEnricher(ChatModel chatModel) {this.chatModel = chatModel;}List<Document> enrichDocuments(List<Document> documents) {KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);return enricher.apply(documents);}
}
SummaryMetadataEnricher

SummaryMetadataEnricher是一个DocumentTransformer,它使用生成式AI模型为文档创建摘要并将其添加为元数据。它可以为当前文档以及相邻文档(上一个和下一个)生成摘要。

@Configuration
class EnricherConfig {@Beanpublic SummaryMetadataEnricher summaryMetadata(OpenAiChatModel aiClient) {return new SummaryMetadataEnricher(aiClient,List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));}
}@Component
class MySummaryEnricher {private final SummaryMetadataEnricher enricher;MySummaryEnricher(SummaryMetadataEnricher enricher) {this.enricher = enricher;}List<Document> enrichDocuments(List<Document> documents) {return this.enricher.apply(documents);}
}

Writers

文件

FileDocumentWriter是一个DocumentWriter实现,它将文档对象列表的内容写入文件。

@Component
class MyDocumentWriter {public void writeDocuments(List<Document> documents) {FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);writer.accept(documents);}
}
VectorStore 矢量数据库

矢量数据库是一种特殊类型的数据库,在 AI 应用程序中起着至关重要的作用。
在矢量数据库中,查询不同于传统的关系数据库。 它们执行相似性搜索,而不是完全匹配。 当给定一个向量作为查询时,向量数据库会返回与查询向量“相似”的向量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/912700.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/912700.shtml
英文地址,请注明出处:http://en.pswp.cn/news/912700.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入Linux开发核心:掌握Vim编辑器与GCCG++编译工具链

文章目录 一、Vim&#xff1a;终端环境下的编辑艺术1.1 Vim设计哲学&#xff1a;模态编辑的终极实践1.2 高效导航&#xff1a;超越方向键的移动艺术1.3 定制化开发环境&#xff1a;从基础到专业IDE1.4 调试集成&#xff1a;Vim作为调试前端 二、GCC/G&#xff1a;Linux编译基石…

阿里云-spring boot接入arms监控

目标&#xff1a;在ecs中启动一个java应用&#xff0c;且携带arms监控 原理&#xff1a;在java应用启动时&#xff0c;同时启动一个agent探针&#xff0c;时刻监控java应用变化&#xff08;如&#xff1a;接口调用、CPU、线程池状态等&#xff09; 1.arms接入中心添加java应用…

昆泰芯3D霍尔磁传感器芯片在汽车零部件中的应用

HUD即抬头显示系统&#xff08;Head-Up Display)&#xff0c;HUD 是一种将重要的车辆或飞行等相关信息(如速度、导航指示、警告信息等)投射到驾驶员或操作员前方视野范围内的透明显示屏或直接投射到风挡玻璃上的技术。 HUD即抬头显示系统&#xff08;Head-Up Display)&#xff…

new Vue() 的底层工作原理

当你调用 new Vue() 时&#xff0c;Vue.js 会执行一系列复杂的初始化过程。让我们深入剖析这个看似简单的操作背后发生的事情&#xff1a; 1. 初始化阶段 (1) 内部初始化 function Vue(options) {if (!(this instanceof Vue)) {warn(Vue is a constructor and should be cal…

最简安装SUSE15SP7导致大部分命令缺失

我嘞个去~~~明明选择Enable了ssh&#xff0c;结果也没给装。 俺习惯使用NetworkManager管理网络&#xff0c;没给装&#xff0c;用不了nmcli和nmtui。不高兴归不高兴&#xff0c;最简安装的话&#xff0c;也情有可原。我嘞个去去~~连ping、vi都没有装&#xff0c;这也太简了。…

Vue-14-前端框架Vue之应用基础嵌套路由和路由传参

文章目录 1 嵌套路由1.1 News.vue1.2 Detail.vue1.3 router/index.ts2 路由传参2.1 query参数2.1.1 News.vue(传递参数)2.1.2 Detail.vue(接收参数)2.2 params参数2.2.1 router/index.ts(需要提前占位)2.2.2 News.vue(传递参数)2.2.3 Detail.vue(接收参数)2.3 props配置2.3.1 r…

Python网安-ftp服务暴力破解(仅供学习)

目录 源码在这里 需要导入的模块 连接ftp&#xff0c;并设置密码本和线程 核心代码 设置线程 源码在这里 https://github.com/Wist-fully/Attack/tree/cracker 需要导入的模块 import ftplib from threading import Thread import queue 连接ftp&#xff0c;并设置密码…

ES6数组的`flat()`和`flatMap()`函数用法

今天给大家分享ES6中两个超实用的数组函数&#xff1a;flat()和flatMap()&#xff0c;学会它们能让数组处理变得更轻松&#xff01; 1. flat()函数 1.1 基本介绍 flat()用于将嵌套数组"拍平"&#xff0c;即将多维数组转换为一维数组。 1.2 语法 const newArray …

upload-labs靶场通关详解:第15-16关

第十五关 getimagesize函数验证 一、分析源代码 function isImage($filename){$types .jpeg|.png|.gif;if(file_exists($filename)){$info getimagesize($filename);$ext image_type_to_extension($info[2]);if(stripos($types,$ext)>0){return $ext;}else{return false…

【Linux】基础IO流

好的代码自己会说话&#xff0c;清晰的逻辑与优雅的结构&#xff0c;是程序员与世界对话的方式。 前言 这是我自己学习Linux系统编程的第五篇笔记。后期我会继续把Linux系统编程笔记开源至博客上。 上一期笔记是关于进程&#xff1a; 【Linux】进程-CSDN博客https://blog.csdn…

【C语言】学习过程教训与经验杂谈:思想准备、知识回顾(二)

&#x1f525;个人主页&#xff1a;艾莉丝努力练剑 ❄专栏传送门&#xff1a;《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题 &#x1f349;学习方向&#xff1a;C/C方向 ⭐️人生格言&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为…

AD8021ARZ-REEL7【ADI】300MHz低噪声运放放大器,高频信号处理的性价比之选!

AD8021ARZ-REEL7&#xff08;ADI&#xff09;产品解析与推广文案 1. 产品概述 AD8021ARZ-REEL7 是 Analog Devices Inc.&#xff08;ADI&#xff09; 推出的一款 高速、低噪声运算放大器&#xff08;Op-Amp&#xff09;&#xff0c;属于 ADI的高性能放大器系列&#xff0c;专为…

WPF学习笔记(11)数据模板DataTemplate与数据模板选择器DataTemplateSelector

数据模板DataTemplate与数据模板选择器DataTemplateSelector 一、DataTemplate1. DataTemplate概述2. DataTemplate详解 二、DataTemplateSelector1. DataTemplateSelector概述2. DataTemplateSelector详解 总结 一、DataTemplate 1. DataTemplate概述 DataTemplate 表示数据…

【V6.0 - 听觉篇】当AI学会“听”:用声音特征捕捉视频的“情绪爽点”

系列回顾&#xff1a; 在上一篇 《AI的“火眼金睛”&#xff1a;用OpenCV和SHAP洞察“第一眼缘”》 中&#xff0c;我们成功地让AI拥有了视觉&#xff0c;它已经能像一个严苛的“质检员”一样&#xff0c;评判我视频的画质和动态感。 但我的焦虑并没有完全消除。因为我发现&a…

(5)pytest-yield操作

1. 简介 上一篇中&#xff0c;我们刚刚实现了在每个用例之前执行初始化操作&#xff0c;那么用例执行完之后如需要清除数据&#xff08;或还原&#xff09;操作&#xff0c;可以使用 yield 来实现。fixture通过scope参数控制setup级别&#xff0c;既然有setup作为用例之前前的操…

C++中的cmath库

在C编程中&#xff0c;数值计算是科学计算、工程应用及算法开发的基础。cmath库作为C标准库的重要组成部分&#xff0c;提供了丰富的数学函数和工具&#xff0c;能够高效处理各种数值计算任务。本文将全面解析cmath库的核心功能&#xff0c;并通过实战案例展示其强大威力。 一…

python包管理工具uv VS pip

在 Python 中&#xff0c;uv 和 pip 都是包管理工具&#xff0c;但它们的定位和特性有所不同。以下是主要区别&#xff1a; 1. pip&#xff08;传统工具&#xff09; 定位&#xff1a;Python 官方的包安装工具&#xff0c;是 Python 生态中最基础的包管理器。特点&#xff1a;…

OpenCv基础(C++)

1.图像读取与显示 #include<opencv2/opencv.hpp> using namespace cv;Mat src imread("C:/Users/16385/Desktop/new/photo/1.jpg");//读取图像 Mat src imread("C:/Users/16385/Desktop/new/photo/1.jpg",IMREAD_GRAYSCALE); //将读取的图像转为灰…

MySQL非阻塞创建索引的方法

文章目录 1. Online DDL (MySQL 5.6)2. pt-online-schema-change 工具3. gh-ost 工具4. 对于MySQL 8.0注意事项 在MySQL中创建大型表索引时&#xff0c;传统方式会阻塞表的写操作&#xff0c;影响生产环境使用。以下是几种非阻塞创建索引的方法&#xff1a; 1. Online DDL (My…

数字雨动画背景

<!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>数字雨动画背景</title><style>* {m…