端侧RAG实战指南

作者:稀有猿诉日期:2026/3/6

本文译自「On-Device RAG for App Developers: Embeddings, Vector Search, and Beyond」,原文链接medium.com/google-deve…,由Sasha Denisov发布于2026年2月21日。

我们已经探讨了离线 AI 代理的重要性如何通过函数调用赋予它们工具。现在,让我们通过赋予它们记忆——即使用 RAG(检索增强生成)搜索和检索你的私有数据——来完善整个图景。

当我开始构建 Flutter Gemma 时,开发者们提出的第一个问题是:“如何让 AI 了解我的数据?” 不是维基百科,也不是通用知识——而是他们应用的数据。用户、联系人、文档。这正是 RAG 填补的空白。想象一下,你正在构建一个带有 AI 助手的个人 CRM 应用。你的用户问:“我应该跟进上周会议中的哪些人?” 挑战在于:AI 需要访问你的数据——你的联系人、你的对话记录、你的业务背景——而不是它训练时获得的通用知识。

问题:大型语言模型不了解你的数据

大型语言模型基于海量的公共数据(维基百科、书籍、网站)进行训练。它们在通用推理方面表现出色。但他们对你的联系人、会议记录,甚至昨天与客户的对话都一无所知。

如果你问一个普通的LLM(客户关系管理)“我应该跟进上周联系的哪些人?”,你得到的要么是“我没有权限访问你的联系人”——虽然诚实但毫无用处——要么是一个自以为是、信誓旦旦地给出错误姓名和细节的答案。

有几种方法可以让LLM访问你的数据。我们来逐一看看。

方法A:把所有信息都塞进提示框

最直接的解决方法——把所有数据都塞进上下文。提示:“以下是我所有的联系人:- Sarah Chen,谷歌,企业级负责人……,- Mike Ross,Meta,潜在客户……,- …(还有498个联系人)我应该跟进谁?”

问题:

  • 上下文限制:设备端模型通常支持8K-32K个令牌。如果你有很多数据,根本装不下。
  • 速度慢且成本高:上下文中的每个令牌都会增加计算时间和内存占用。上下文越长,响应速度越慢,电池消耗也越快。
  • 容易被淹没在噪声中:查询某个联系人时,为什么要处理其他 499 个联系人的信息?更糟糕的是,LLM 往往会忽略隐藏在冗长上下文中的细节——你需要的那个联系人很可能被遗漏。

方法 B:微调模型

使用你的数据训练模型,使其“了解”这些数据。但是:

  • 容易过时:你的 CRM 数据每天都在变化。每天都要重新训练吗?
  • 成本高:微调需要消耗计算资源和时间。
  • 无法扩展:每个用户的数据都不同。理论上,你可以使用 LoRa 为每个用户进行微调,但这既复杂又难以维护。

对于静态知识,微调效果很好——我在使用 LoRa 进行 Gemma 设备端推理微调中详细介绍了这一点。但对于频繁变化的数据,微调就不是最佳方案了。

方法 C:RAG — 先检索后生成 (优胜方案)

与其将所有信息都提供给 LLM,不如针对每个查询仅检索相关信息

  • 查询联系人 → 仅检索该联系人的信息 → 发送给 LLM
  • 查询公司 → 仅检索该公司的联系人 → 发送给 LLM
  • 查询后续跟进 → 检索需要跟进的联系人 → 发送给 LLM

其优势:

  • 始终保持最新:数据在查询时检索,而非预先嵌入模型。
  • 可扩展至任何用户:相同的模型,不同的数据——每个设备都有自己的本地存储。
  • 聚焦上下文:LLM 只看到它需要的信息——没有干扰,没有“中间丢失”的信息。

这就是 RAG(检索增强生成)——它就是解决方案。

最棒的是什么? RAG 可以完全在设备端运行——检索和生成都在本地完成,无需云端。

但“检索相关信息”究竟意味着什么?我们如何在成千上万条记录中找到正确的数据?让我们深入了解一下其机制。

RAG 基础知识

RAG 指的是先检索相关数据,然后生成响应。但我们究竟该如何找到“相关”数据呢?

查找正确的记录

为了回答用户的问题,我们需要在数据库中找到正确的记录。检索技术有很多种,例如关键词搜索、混合搜索、基于图的检索等等。本文将重点介绍一种目前可以使用 Flutter Gemma 轻松实现的技术:语义搜索(也称为向量搜索)。

什么是语义搜索?

传统的关键词搜索存在缺陷。例如,搜索“大公司”时,你找不到“企业客户”——即使它们的意思相同。语义搜索通过理解文本含义而非仅仅匹配单词来解决这个问题。其核心思想是:将文本转换为能够捕捉其含义的数字(向量)。含义相似 → 向量相似。这样,查找相关记录就变成了一个数学问题——找到与查询最接近的向量。

嵌入:将文本转换为向量

为了实现语义搜索,我们需要将数据转换为向量。这就是嵌入模型的作用所在。嵌入模型接收文本并输出一个向量——通常包含 768 个或更多数字,用于表示文本的含义:

1"enterprise sales lead"    [0.12, -0.45, 0.78, 0.23, ...]  (768 numbers)  
2"big company executive"    [0.11, -0.44, 0.77, 0.25, ...]  (similar!)  
3"banana smoothie recipe"   [0.89, 0.12, -0.34, 0.56, ...]  (very different)
4

2025 年 9 月,谷歌发布了EmbeddingGemma——一个基于 Gemma 3 的 3.08 亿参数嵌入模型,专为设备端使用而设计。你可以在 HuggingFace 上找到该模型。我一看到公告就知道 Flutter Gemma 需要嵌入支持——这意味着要为三个平台实现嵌入生成(当时桌面平台还不支持)。

在 Android 平台上,Google 提供了一个官方的 AI Edge LocalAgents RAG SDK,因此集成非常简单。iOS 和 Web 平台的情况则不同——没有官方 SDK,所以我从头开始构建了整个流程:iOS 使用 TensorFlow Lite 解释器,Web 使用 LiteRT.js,两者都使用 SentencePiece 分词器,手动处理张量,并进行精细的内存管理。桌面版支持即将推出(希望如此)

rag_1.webp

趁着这个机会,我觉得只支持 EmbeddingGemma 还不够——所以我又添加了 Gecko,这是谷歌于 2024 年 3 月发布的一款较早的嵌入模型。Gecko 利用大型 LLM 的知识蒸馏技术,仅用 1.1 亿个参数就能实现强大的检索性能。它的速度是 EmbeddingGemma 的 2.6 倍,但准确率略低——这在资源受限的设备上需要实时搜索,并且可以牺牲一些质量来换取速度时非常有用。

以下是如何使用 Flutter Gemma 生成嵌入代码:

1final embedder = await FlutterGemma.getActiveEmbedder();  
2final vector = await embedder.generateEmbedding("enterprise sales lead");  
3// vector: [0.12, -0.45, 0.78, ...]  ready for search
4

存储向量:向量数据库

有了向量之后,你需要一个地方来存储它们并进行高效搜索。这就是向量数据库的作用。

像 Pinecone、Chroma 或 Qdrant 这样的云解决方案很受欢迎,但它们需要网络调用,这与设备端 AI 的初衷背道而驰。对于嵌入式应用,ObjectBox 是一个不错的选择——它是一个内置 HNSW 向量搜索的设备端数据库,专为移动设备和物联网设计。ObjectBox 拥有 Flutter SDK,因此你可以轻松地将其与 Flutter Gemma 结合使用进行嵌入式开发。但如果你不想在应用中添加额外的数据库依赖项,Flutter Gemma 提供了一种更简单的开箱即用方案:带有 HNSW 索引的纯 SQLite。无需额外依赖——SQLite 已集成到 Flutter 支持的每个平台上。

底层实现虽然跨平台保持一致,但使用了平台特定的 API:

  • Android 使用 SQLiteOpenHelper,嵌入数据以二进制 BLOB (float32) 格式存储。每个 768 维向量仅占用 3KB 空间——紧凑且读取速度快。
  • iOS 直接使用 SQLite3 C API,存储格式与 Android 相同。向量编码为小端 float32 数组,与 Android 相同,因此数据库文件与二进制文件兼容。
  • Web 的实现比较复杂——浏览器本身没有原生 SQLite。我使用了 wa-sqlite,它是 SQLite 的 WebAssembly 移植版本,并使用 OPFS (Origin Private File System) 进行持久化存储。需要注意的是:OPFS 需要 Web Worker,因此所有数据库操作都在专用的工作线程中运行,并通过消息传递将数据传递给主线程。

Fluter Gemma RAG

这三个平台都使用相同的模式、相同的 BLOB 编码和相同的余弦相似度计算方法——因此无论应用程序运行在何处,搜索结果都保持一致。

搜索工作原理

当用户提出问题时,我们会使用相同的嵌入模型将其转换为向量,然后在数据库中找到最接近的向量。

“最接近”是通过余弦相似度来衡量的——它计算高维空间中两个向量之间的角度。得分范围从 -1 到 1:1 表示向量指向同一方向(含义相同),0 表示它们垂直(不相关),-1 表示方向相反。实际上,任何高于 0.3-0.4 的值通常都具有相关性——具体的阈值取决于你的数据和用例。

rag_cosine.webp

最简单的方法是暴力搜索:将你的查询与数据库中的每个文档进行比较,计算每个文档的余弦相似度,然后返回匹配度最高的文档。这种方法对于小型数据集(数百甚至数千条记录)来说效果很好。但是,对于 10 万个联系人,每次搜索都需要进行 10 万次相似度计算。这速度太慢了。

HNSW(分层可导航小世界) 通过巧妙的多层图结构解决了这个问题。可以把它想象成地铁线路图:快线连接主要枢纽(顶层),而慢线连接每个站点(底层)。搜索从顶层开始,先进行大范围的跳跃以接近目标,然后再向下移动到局部层以提高精度。

HNSW

结果是:搜索复杂度从 O(n) 降低到 O(log n)。对于 10 万个联系人,只需要进行约 17 次比较,而不是 10 万次。Flutter Gemma 的 HNSW 实现采用了一种“过度获取并重新排序”的策略——HNSW 返回 2 倍的候选结果(快速、近似),然后使用精确的余弦相似度筛选出前 K 个结果。近似搜索速度快,精确搜索准确。

API 保持不变——Flutter Gemma 会在底层处理优化:

1final results = await FlutterGemmaPlugin.instance.searchSimilar(  
2  query: "enterprise contacts",  
3  topK: 5,  
4  threshold: 0.3,  
5);  
6// results: records semantically similar to "enterprise contacts"
7

分块:数据准备

在建立索引之前,你需要决定如何将数据拆分成可搜索的单元——这称为分块。对于 CRM 系统,自然的分块方式是:一个联系人对应一个文档。但对于较长的内容,例如会议记录或电子邮件,你需要一种策略:固定大小的分割(简单但可能会截断句子中间部分)、语义分割(尊重语义边界)或文档结构(段落、章节)。最佳实践是在分块之间保持 10-20% 的重叠,以防止在边界处丢失上下文。

使用 Flutter Gemma 实现 RAG

现在我们已经了解了理论,接下来让我们看看如何在实践中应用它。Flutter Gemma 示例应用 包含一个完整的 RAG 实现——你可以查看它以获取完整代码。

rag_result.webp

主要有两种工作流程:数据导入(将数据导入矢量存储)和检索(在查询时进行搜索)。

数据导入

在进行任何搜索之前,所有数据都需要矢量化并存储。此过程在应用程序首次加载时执行一次,之后会随着数据的变化而增量更新。

初始索引 — 首次启动时,处理所有现有记录:

1Future<void> indexAllContacts(List<Contact> contacts) async {  
2  final embedder = await FlutterGemma.getActiveEmbedder();  
3    
4  for (final contact in contacts) {  
5    final embedding = await embedder.generateEmbedding(  
6      contact.toSearchableText(),  
7    );  
8      
9    await FlutterGemmaPlugin.instance.addDocumentWithEmbedding(  
10      id: contact.id,  
11      content: contact.toSearchableText(),  
12      embedding: embedding,  
13      metadata: jsonEncode(contact.toJson()),  
14    );  
15  }  
16}
17

保持数据同步 — 当记录发生更改时,矢量存储必须随之更新。为你的数据源设置监听器:

1// Listen for changes and update vector store  
2contactsRepository.onContactChanged.listen((contact) async {  
3  final embedder = await FlutterGemma.getActiveEmbedder();  
4  final embedding = await embedder.generateEmbedding(  
5    contact.toSearchableText(),  
6  );  
7    
8  // addDocumentWithEmbedding uses INSERT OR REPLACE    
9  // same ID overwrites the old record  
10  await FlutterGemmaPlugin.instance.addDocumentWithEmbedding(  
11    id: contact.id,  
12    content: contact.toSearchableText(),  
13    embedding: embedding,  
14  );  
15});
16

关键在于:你的向量存储是主数据的派生缓存。如果它不同步,搜索结果就会过时或出错。

检索

当用户提出问题时,将其转换为向量并查找相似文档:

1Future<String> answerQuestion(String userQuery) async {  
2  // 1. Search for relevant context  
3  final results = await FlutterGemmaPlugin.instance.searchSimilar(  
4    query: userQuery,  
5    topK: 5,  
6    threshold: 0.3,  
7  );  
8    
9  // 2. Build context from results  
10  final context = results  
11    .map((r) => r.content)  
12    .join('\n\n');  
13    
14  // 3. Pass to LLM with context  
15  final prompt = '''  
16Based on the following information:  
17$context  
18  
19Answer the user\'s question: $userQuery  
20''';  
21    
22  final chat = await model.createChat();  
23  await chat.addQuery(Message(text: prompt, isUser: true));  
24  final response = await chat.generateChatResponse();  
25  return (response as TextResponse).token;  
26}
27

LLM 现在可以访问数据库中的相关数据,而无需将所有内容都塞进提示框中。

从搜索到理解

语义搜索打开了关键词匹配永远无法实现的大门。“查找我的企业联系人”会检索到标记为“财富 500 强”的记录、提及“大交易”的笔记以及“Alphabet”的联系人——即使这些记录中没有包含“企业”一词。嵌入模型理解的是含义,而不仅仅是文本。

但是,如果你尝试搜索“我昨天和谁谈过?”,这种神奇的功能就会失效。

“昨天”一词会生成一个表示“昨天”概念的嵌入向量,类似于“最近”、“过去”、“之前”。你的联系人记录包含日期:“2026-01-19”、“1月19日”。这些日期在语义上并不相似——不存在一个向量空间,使得“昨天”和“2026-01-19”指向同一方向。

而且,日期并非唯一的问题:

这些查询需要的是推理——字段过滤、排序、连接——而不是相似性匹配。我们需要语言学习模型(LLM)理解意图并将自然语言翻译成结构化查询。

基于 LLM 的增强型 RAG(基于函数调用)

我们发现,当查询需要推理时(例如日期解析、字段过滤和排序),语义搜索会失效。解决方案是什么?让 LLM 处理推理,然后调用函数来执行结构化查询。

这就是 Agentic RAG——2025-2026 年的主流模式,其中 LLM 动态地决定如何以及何时检索信息。

我们不会将用户的查询直接传递给向量搜索,而是先让 LLM 解析它。为了使这种智能体行为在设备上生效,你需要一个能够理解函数调用(而不仅仅是聊天)的模型。谷歌专门为此发布了 FunctionGemma:一个拥有 2.7 亿个参数的模型,经过优化,可以解析用户意图并使用正确的参数调用函数。我在使用 FunctionGemma 进行设备端函数调用中详细介绍了如何使用它。

LLM 负责推理(日期运算、意图提取),而函数负责数据检索。

混合搜索:RAG 与过滤器的结合

FunctionGemma 从用户查询中提取参数后,你的函数可以将语义搜索与结构化过滤相结合:

1Future<List<Contact>> searchContacts(Map<String, dynamic> params) async {  
2  List<Contact> contacts;  
3    
4  // If semantic query provided, start with RAG  
5  if (params['semantic_query'] != null) {  
6    final results = await FlutterGemmaPlugin.instance.searchSimilar(  
7      query: params['semantic_query'],  
8      topK: 50,  
9      threshold: 0.25,  
10    );  
11    contacts = results.map((r) => Contact.fromJson(jsonDecode(r.metadata!))).toList();  
12  } else {  
13    contacts = await contactsRepository.getAll();  
14  }  
15    
16  // Apply structured filters extracted by LLM  
17  if (params['company'] != null) {  
18    contacts = contacts.where((c) =>   
19      c.company.toLowerCase().contains(params['company'].toLowerCase())  
20    ).toList();  
21  }  
22    
23  if (params['last_contact_before'] != null) {  
24    final before = DateTime.parse(params['last_contact_before']);  
25    contacts = contacts.where((c) => c.lastContact.isBefore(before)).toList();  
26  }  
27    
28  return contacts.take(params['limit'] ?? 10).toList();  
29}
30

关键在于:RAG 处理语义部分(“对企业定价感兴趣”),而结构化过滤器处理逻辑部分(日期、状态、公司)。 FunctionGemma 会根据查询决定使用哪些参数。

现在一切正常

1//  Temporal queries  
2await query("Who did I talk to yesterday?");  
3// LLM extracts: last_contact_after="2026-01-18", last_contact_before="2026-01-19"  
4  
5//  Structured filters    
6await query("Leads assigned to me without email");  
7// LLM extracts: status="lead", owner="current_user", has_email=false  
8  
9//  Combined semantic + structured  
10await query("Google contacts interested in enterprise pricing");  
11// LLM extracts: company="Google", semantic_query="interested in enterprise pricing"  
12  
13//  Complex reasoning  
14await query("Prospects I should follow up with from Q4 last year");  
15// LLM calculates Q4 2025 dates, adds semantic_query="follow up needed"
16

LLM 发挥其优势(理解语言、推断日期),而结构化查询则发挥其优势(过滤、排序、连接)。

结论

RAG 弥合了通用 LLM 和你的私有数据之间的鸿沟。借助 Flutter Gemma,整个流程(嵌入、向量搜索和生成)都在设备端运行,无需云端。

首先,对于语义至关重要的查询,请使用语义搜索。当遇到日期、过滤器、排序等限制时,可以使用 FunctionGemma 添加函数调用,让 LLM 协调结构化检索。

Flutter Gemma 示例应用 包含完整的 RAG 实现。克隆它,用你的数据进行测试,看看当人工智能完全在用户口袋里运行时,会发生什么。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!


端侧RAG实战指南》 是转载文章,点击查看原文


相关推荐


Gemini 3.1 Pro 正式发布:一次低调更新,还是谷歌的关键反击?
IvanCodes2026/2/25

今天凌晨,谷歌发布了新一代模型——Gemini 3.1 Pro 没有大型发布会,没有提前预热,甚至连宣传节奏都显得克制。 很多人会把它看作 Gemini 3 的小版本升级,但从目前披露的测试数据和演示能力来看,这更像是一次结构性强化,而不是简单的参数迭代。 如果说 Gemini 3 是谷歌重新回到核心竞争区间的标志,那么 Gemini 3.1 Pro,则明显带着更强的实战优化意味。 它在几个关键方向上给出了非常明确的信号:谷歌不只是追赶者。 性能升级:从可用到强势竞争 这次升


React 性能优化:图片懒加载
NEXT062026/2/17

引言 在现代 Web 应用开发中,首屏加载速度(FCP)和最大内容绘制(LCP)是衡量用户体验的核心指标。随着富媒体内容的普及,图片资源往往占据了页面带宽的大部分。如果一次性加载页面上的所有图片,不仅会阻塞关键渲染路径,导致页面长时间处于“白屏”或不可交互状态,还会浪费用户的流量带宽。 图片懒加载(Lazy Loading)作为一种经典的性能优化策略,其核心思想是“按需加载”:即只有当图片出现在浏览器可视区域(Viewport)或即将进入可视区域时,才触发网络请求进行加载。这一策略能显著减少首屏


【Linux】进程信号(上半)
Lsir10110_2026/2/8

当我们想要强行终止掉前台进程的时候,只需要按下Ctrl+c即可,但是Ctrl+c是如何精准杀掉前台进程的? 一、信号概念 1.如何理解信号 假设点了一份外卖,外卖员到了楼下会给你发信息或者打电话,那么这通电话或者这个信息就是信号,也就是用来提醒你需要去做某事的一种通知手段。那么对照Linux系统,信号就是内核向进程发送的通知,提醒进程需要去完成某个任务。 2.信号的特点 对照外卖例子,当点完外卖,我们肯定不会一直在门口等着外卖员,而是先忙手中的事情,比如打游戏,那么这个过程就叫做异步


JSyncQueue——一个开箱即用的鸿蒙异步任务同步队列
江澎涌2026/1/31

零、JSyncQueue JSyncQueue 是一个开箱即用的鸿蒙异步任务同步队列。 项目地址:github.com/zincPower/J… 一、JSyncQueue 有什么作用 在鸿蒙应用开发中,有时需要让多个异步任务按顺序执行,例如状态的转换处理,如果不加控制,会因为执行顺序混乱而产生一些莫名其妙的问题。 所以 JSyncQueue 提供了一个简洁的解决方案: 保证顺序执行:所有任务严格按照入队顺序执行,即使任务内部有异步操作也能保证顺序 两种执行模式:支持 "立即执行" 和 "延时执


Python 线程局部存储:threading.local() 完全指南
哈里谢顿2026/1/21

一句话总结: threading.local() 是 Python 标准库提供的「线程局部存储(Thread Local Storage, TLS)」方案,让同一段代码在不同线程里拥有各自独立的变量空间,从而避免加锁,也避免了层层传参的狼狈。 1. 为什么需要线程局部存储? 在多线程环境下,如果多个线程共享同一个全局变量,就必须: 加锁 → 代码变复杂、性能下降; 或者层层传参 → 代码臃肿、可维护性差。 有些场景只想让线程各自持有一份副本,互不干扰: Web 服务:每个请求线程绑定自


绘制K线第二章:背景网格绘制
佛系打工仔2026/1/13

绘制K线第二章:背景网格绘制 在第一章的基础上,我们简单修饰一下,补充一个背景九宫格的绘制功能。这个功能可以让K线图更加清晰易读,帮助用户快速定位价格和时间。 二、网格配置 确定网格的行数和列数 在绘制网格之前,我们需要确定: 几行:将高度分成几等份(对应价格轴) 几列:将宽度分成几等份(对应时间轴) 例如:4列5行,表示宽度分成4等份,高度分成5等份。 在Config中配置 为了灵活配置网格,我们在 KLineConfig 中添加了两个字段: data class KLineConfig(


Linux系统安全及应用(账号权限管理、登录控制、弱口令、端口扫描)
晚风吹人醒.2026/1/5

目录 1. 账号管理与权限控制         1.1 基本安全措施:                 1.1.1 账号管理和文件权限                 1.1.2 密码安全控制                 1.1.3历史命令和自动注销         1.2 用户切换与提权: 2. 系统引导与登录控制         2.1 开关机安全控制:                 2.1.1 GRUB                 2.1.2 限制更改GRUB


算法竞赛中的数据结构:图
喜欢吃燃面2025/12/27

目录 一.图的基本概念1.图的定义2.图、树、线性表的联系与区别2.1 核心联系2.2 核心区别 二.图的分类1.按边的方向分类2.按边的权重分类3 .按顶点和边的数量分类4 .按连通性分类(针对无向图)5 .按强连通性分类(针对有向图)6 .其他特殊类型7.顶点的度(补充)8.路径及相关长度概念(补充)8.1 路径8.2 路径长度(无权图)8.3 带权路径长度(带权图)8.4 核心区别对比 三.邻接矩阵1.邻接矩阵【注意】 四.邻接表五.链式前向星


ZooKeeper+Kafka
吉良吉影1232025/12/18

目录 一、Zookeeper 1.1 Zookeeper 概述 1.2 Zookeeper 工作机制 1.3 ZooKeeper 特点 1.4 Zookeeper 数据结构 1.5 ZooKeeper 应用场景 1.6 Zookeeper 选举机制 1.6.1 第一次启动选举机制 1.6.2 非第一次启动选举机制 Leader 的作用 1. 处理所有写请求(核心职责) 2. 主导 Leader 选举 3. 管理集群数据同步 4. 维护集群状态 Follower


编程界 语言神 : 赶紧起来学 Rust 了!
Pomelo_刘金2025/12/10

大家对 Rust 的印象 没接触过的: 编程界语言神 整天重构这重构那 还要 要干掉 c++ ?! 稍微了解过的: 学习曲线: 但实际上是: 第一个高峰是 借用检查器,第二个是异步,第三个是unsafe,第四个是宏怎么玩? 开始接触之后 编译器不让我写代码,怎么写都报错 写 rust 代码像是在跟 rust 编译器谈对象 , 我只是传个参数,你跟我讲所有权、借用、生命周期?” 写的代码上线之后,还不错哦 “别的语言项目上线流程” 内容: 编译 ✔ 测试(偶尔挂一两条)✔ 上线后:半

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客