端侧RAG实战指南

本文译自「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 个或更多数字,用于表示文本的含义:

bash 复制代码
"enterprise sales lead"  →  [0.12, -0.45, 0.78, 0.23, ...]  (768 numbers)  
"big company executive"  →  [0.11, -0.44, 0.77, 0.25, ...]  (similar!)  
"banana smoothie recipe" →  [0.89, 0.12, -0.34, 0.56, ...]  (very different)

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 分词器,手动处理张量,并进行精细的内存管理。桌面版支持即将推出(希望如此)

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

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

Dart 复制代码
final embedder = await FlutterGemma.getActiveEmbedder();  
final vector = await embedder.generateEmbedding("enterprise sales lead");  
// vector: [0.12, -0.45, 0.78, ...] --- ready for search

存储向量:向量数据库

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

像 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,因此所有数据库操作都在专用的工作线程中运行,并通过消息传递将数据传递给主线程。

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

搜索工作原理

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

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

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

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

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

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

Dart 复制代码
final results = await FlutterGemmaPlugin.instance.searchSimilar(  
  query: "enterprise contacts",  
  topK: 5,  
  threshold: 0.3,  
);  
// results: records semantically similar to "enterprise contacts"

分块:数据准备

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

使用 Flutter Gemma 实现 RAG

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

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

数据导入

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

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

Dart 复制代码
Future<void> indexAllContacts(List<Contact> contacts) async {  
  final embedder = await FlutterGemma.getActiveEmbedder();  
    
  for (final contact in contacts) {  
    final embedding = await embedder.generateEmbedding(  
      contact.toSearchableText(),  
    );  
      
    await FlutterGemmaPlugin.instance.addDocumentWithEmbedding(  
      id: contact.id,  
      content: contact.toSearchableText(),  
      embedding: embedding,  
      metadata: jsonEncode(contact.toJson()),  
    );  
  }  
}

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

Dart 复制代码
// Listen for changes and update vector store  
contactsRepository.onContactChanged.listen((contact) async {  
  final embedder = await FlutterGemma.getActiveEmbedder();  
  final embedding = await embedder.generateEmbedding(  
    contact.toSearchableText(),  
  );  
    
  // addDocumentWithEmbedding uses INSERT OR REPLACE ---   
  // same ID overwrites the old record  
  await FlutterGemmaPlugin.instance.addDocumentWithEmbedding(  
    id: contact.id,  
    content: contact.toSearchableText(),  
    embedding: embedding,  
  );  
});

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

检索

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

Dart 复制代码
Future<String> answerQuestion(String userQuery) async {  
  // 1. Search for relevant context  
  final results = await FlutterGemmaPlugin.instance.searchSimilar(  
    query: userQuery,  
    topK: 5,  
    threshold: 0.3,  
  );  
    
  // 2. Build context from results  
  final context = results  
    .map((r) => r.content)  
    .join('\n\n');  
    
  // 3. Pass to LLM with context  
  final prompt = '''  
Based on the following information:  
$context  
  
Answer the user\'s question: $userQuery  
''';  
    
  final chat = await model.createChat();  
  await chat.addQuery(Message(text: prompt, isUser: true));  
  final response = await chat.generateChatResponse();  
  return (response as TextResponse).token;  
}

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 从用户查询中提取参数后,你的函数可以将语义搜索与结构化过滤相结合:

Dart 复制代码
Future<List<Contact>> searchContacts(Map<String, dynamic> params) async {  
  List<Contact> contacts;  
    
  // If semantic query provided, start with RAG  
  if (params['semantic_query'] != null) {  
    final results = await FlutterGemmaPlugin.instance.searchSimilar(  
      query: params['semantic_query'],  
      topK: 50,  
      threshold: 0.25,  
    );  
    contacts = results.map((r) => Contact.fromJson(jsonDecode(r.metadata!))).toList();  
  } else {  
    contacts = await contactsRepository.getAll();  
  }  
    
  // Apply structured filters extracted by LLM  
  if (params['company'] != null) {  
    contacts = contacts.where((c) =>   
      c.company.toLowerCase().contains(params['company'].toLowerCase())  
    ).toList();  
  }  
    
  if (params['last_contact_before'] != null) {  
    final before = DateTime.parse(params['last_contact_before']);  
    contacts = contacts.where((c) => c.lastContact.isBefore(before)).toList();  
  }  
    
  return contacts.take(params['limit'] ?? 10).toList();  
}

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

现在一切正常

Dart 复制代码
// ✅ Temporal queries  
await query("Who did I talk to yesterday?");  
// LLM extracts: last_contact_after="2026-01-18", last_contact_before="2026-01-19"  
  
// ✅ Structured filters    
await query("Leads assigned to me without email");  
// LLM extracts: status="lead", owner="current_user", has_email=false  
  
// ✅ Combined semantic + structured  
await query("Google contacts interested in enterprise pricing");  
// LLM extracts: company="Google", semantic_query="interested in enterprise pricing"  
  
// ✅ Complex reasoning  
await query("Prospects I should follow up with from Q4 last year");  
// LLM calculates Q4 2025 dates, adds semantic_query="follow up needed"

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

结论

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

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

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

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

保护原创,请勿转载!

相关推荐
二流小码农15 小时前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
xq952717 小时前
Android 手游SDK组件化开发实战指南
android
煤球王子19 小时前
学习记录:Android14中的WiFi-wpa_supplicant(1)
android
张小潇20 小时前
AOSP15 Input专题InputDispatcher源码分析
android
TT_Close20 小时前
【Flutter×鸿蒙】debug 包也要签名,这点和 Android 差远了
android·flutter·harmonyos
Kapaseker21 小时前
2026年,我们还该不该学编程?
android·kotlin
雨白2 天前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk2 天前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING2 天前
RN容器启动优化实践
android·react native