GEOZ

llamadart 0.11:让 Flutter 应用真正跑起离线大模型

2026/10/9
llamadart 0.11:让 Flutter 应用真正跑起离线大模型

AIAI Summary (BLUF)

llamadart 0.11 版本简化了在 Dart/Flutter 应用中集成离线大语言模型的流程。本文通过一个写作助手示例,演示如何下载模型、管理对话会话、切换模型以及处理不同运行时环境,帮助开发者快速实现本地推理功能。

核心洞察

llamadart 这个项目最有意思的地方,是它把"离线跑大模型"这件事从 demo 级别拉到了能真正塞进 App 的程度。0.11 版本改的那些 API,看起来只是少写了几行代码,但背后解决的是"用户下载到一半关掉屏幕怎么办""切换模型时旧模型要不要先卸载"这类真正会让人头疼的问题。如果你正在做 Flutter 或 Dart 的端侧 AI 功能,这篇文章里的生命周期管理思路值得花十分钟看看。


我的写作助手本来就能跟 Gemini 对话。缺的那块拼图,是让它断网也能用。

这就是我做 llamadart 的原因:飞机上、网络不稳的地方、或者压根不允许访问外部 API 的环境,都需要一个离线模式。在第一篇文章里,我聊的是怎么把这事跑通——原生二进制、Dart 构建钩子、模型的对话模板。

本地模型能吐出第一个 token 的时候,确实挺爽。但爽完之后你会发现,面前摆着一堆应用层的问题。

模型从哪来?用户下载到一半取消了怎么办?对话历史存在哪?能不能在不弄丢当前可用模型的前提下切换模型?

这些问题正好能说明 llamadart 这段时间的变化。项目现在做的事情更多了,但 0.11 真正有意思的地方,是它把大量普通的集成工作收进了一个更小、更清晰的 API 里。

我们拿一个简单的写作助手流程来走一遍:下载模型,让它改写一段话,再让它给个更短的版本。这只是个演示流程,不代表某个具体模型的写作质量。

核心结论

  1. llamadart 0.11 版本将模型加载 API 从两步式(创建 engine + 手动 loadModelSource)改为 LlamaEngine.load() 一步完成,加载失败时自动释放已创建的 engine 和 backend,调用方无需手动清理。

  2. 在原生平台上,setModel 方法可在替代模型文件解析或下载期间保持当前模型可用;若该阶段失败或被取消,旧模型保持加载状态,只有文件就绪后才开始替换。

  3. ChatSession 负责管理对话历史,会自动裁剪较早的轮次以适应上下文预算,同时保留系统提示;流式返回的文本读取方式从 chunk.choices.first.delta.content 简化为 chunk.text。

  4. 0.11 版本通过 engine.runtime 标识当前运行时,通过 await engine.capabilities 报告支持的操作,使 App 能统一查询当前可用的功能,并在请求不被支持时获得明确失败。

  5. llamadart 要求 Dart 3.10.7 或更新版本,Flutter 应用需要 Flutter 3.38.0 或更新版本;使用 GGUF Apple 伴生包的 iOS 和 macOS 应用需搭配 llamadart_llama_cpp_flutter: ^0.0.21。

第一个回答之前

想象一下,给应用加一个"离线模式"按钮。按下去之后,大模型不可能凭空出现在设备上。

App 得解释下载这件事,展示进度,还得在用户关掉屏幕之后能恢复。下次启动应该复用已经下好的文件,而不是重新下载。这些细节,用户在看到第一个生成的字之前就会碰到。

第一篇文章里做的原生打包工作在这里依然有用。常规配置下,App 开发者不需要本地 C++ 工具链,包的构建钩子会自己解析原生运行时资源。模型文件是另一回事。

0.11 里,模型加载的入口把这些文件收拢到了一起。一个 LlamaModel 描述模型和一个可选的多模态投影器。它的 ModelSource 可以是本地路径、URL,或者 Hugging Face 引用。

改动小到可以直接贴出来看。假设 source 和 params 已经定义好了,旧写法是这样:

// 0.10
final engine = LlamaEngine(LlamaBackend());
try {
  await engine.loadModelSource(source, modelParams: params);
  // 使用 engine
} finally {
  await engine.dispose();
}

新写法里,加载完直接返回一个就绪的 engine:

// 0.11
final engine = await LlamaEngine.load(
  LlamaModel(source),
  params: params,
);
try {
  // 使用 engine
} finally {
  await engine.dispose();
}

关键区别在于所有权从哪开始。如果新的加载抛异常了,它创建的 engine 和 backend 已经被释放掉了。如果成功,调用方拿到一个就绪的 engine。

进度和取消仍然是 App 需要展示的东西。加载器通过 onProgress 和下载选项暴露它们,这样它们就能和加载模型属于同一个操作。生命周期指南里有详细说明。

从一条提示到一个对话

模型就绪之后,写作助手可以请求改写了。接着是追问:"再短一点。"

这条简短的指令依赖前面的对话。App 需要把原段落和第一个回答都保留在对话里。

ChatSession 负责这段历史。它在 0.11 之前就有了,新的便捷方法只是让常见场景更好读。用 session.send 发消息,然后通过 reply.text 拿到完整回复。

下面是一个完整的原生 Dart 起点:

import 'package:llamadart/llamadart.dart';

Future<void> main() async {
  final engine = await LlamaEngine.load(
    LlamaModel(
      ModelSource.parse(
        'hf://unsloth/SmolLM2-135M-Instruct-GGUF/'
        'SmolLM2-135M-Instruct-Q2_K.gguf',
      ),
    ),
    params: const ModelParams(contextSize: 1024, gpuLayers: 0),
  );

  try {
    final session = ChatSession(
      engine,
      systemPrompt: 'You help rewrite text concisely.',
    );
    final reply = await session.send(
      'Rewrite this: We are writing to let you know '
      'that the meeting will start at nine.',
    );
    print(reply.text);

    final shorter = await session.send('Make it shorter.');
    print(shorter.text);
  } finally {
    await engine.dispose();
  }
}

用这个小模型是为了让例子好上手。选一个听话的模型是另一回事,你得拿自己的输入去评估。

做实时聊天 UI 的话,session.create 仍然会流式返回。读取文本的方式从 chunk.choices.first.delta.content 变成了 chunk.text。这个改动很小,但每次把流接到 widget 上的时候都能感觉到。

对话记忆是有上限的。ChatSession 会裁剪较早的轮次来适应上下文预算,同时保留系统提示。如果你的 App 已经自己管理并编辑对话记录,engine.create 允许你直接提供完整的消息列表。

这些示例对应的是打了标签的 0.10 和 0.11 文档。

模型选择器改变了问题

现在假设写作助手有一个模型选择器。有人想在当前模型还能用的时候试试另一个模型。

App 可以先卸载当前模型,再开始下载替代品。在网络慢的情况下,用户会陷入两个模型都用不了的等待。

在原生平台上,setModel 会在替代文件解析或下载期间保持当前模型可用。如果这个阶段失败或被取消,旧模型保持加载状态。只有文件就绪之后,替换才会开始。

这里有个边界值得理解:后续加载阶段失败的话,可能什么都不剩。Web 运行时也会在获取替代品之前先卸载。这个 API 给 App 的是一个确定的生命周期,而不是"每次切换都成功"的保证。

离开聊天界面的时候也会碰到同样的所有权问题。Flutter engine 应该放在 service、provider 或 state 对象里,而不是塞在 build() 里面。所有者结束的时候释放它。桌面应用退出需要自己的清理路径,文档里有说明。

这些选择没有新模型 demo 那么显眼,但它们决定了用户在用 App 其他部分的时候,这个功能表现是否合理。

更多运行时,但不假装它们一样

项目也早就超出了最初 llama.cpp 那条路。

0.7 版本加了 LiteRT-LM 支持。0.8 把 Apple 运行时伴生包从核心包里拆了出来,让纯 Dart 用户不再被 Flutter SDK 约束卡住。0.10 通过一个可选的 stable-diffusion.cpp 运行时加了预览版图像生成。

这带来了更多可能性,也带来了更多 App 必须尊重的差异。

对我们的写作助手来说,一个有用的问题是:要不要启用图片附件按钮。答案应该来自已加载的模型和运行时。在 0.11 里,engine.runtime 标识运行时,await engine.capabilities 报告支持的操作。

设备选择遵循同样的原则。ModelParams.device 提供统一的 CPU、GPU 或 NPU 选择。显式请求要么在那个设备上跑,要么以不支持失败;auto 保持运行时的默认行为。

平台限制还是有的。Android 上 llama.cpp 的 Vulkan 是实验性的,而且因设备而异。Web 支持是实验性的。自动工具循环会拒绝固定的 LiteRT-LM 运行时,因为它们无法可靠地报告 token 限制截断。部署之前,支持矩阵是你要查的地方。

对 App 代码来说,回报就是有一个统一的方式去问"现在有什么可用",以及在请求的操作不被支持时有一个明确的失败。

先试一个有用的离线功能

跑这个例子,加上 llamadart: ^0.11.0,然后执行 dart pub get 或 flutter pub get。包要求 Dart 3.10.7 或更新版本,Flutter 应用需要 Flutter 3.38.0 或更新版本。

使用 GGUF Apple 伴生包的 Flutter iOS 和 macOS 应用,需要搭配 llamadart_llama_cpp_flutter: ^0.0.21。其他运行时有自己的伴生包和要求,照着安装指南走。

第一次配置需要联网来获取缺失的运行时资源和模型文件。原生模型下载会缓存以便复用。所需资源到位之后,推理就可以在本地跑了;你自己加的远程工具或云服务仍然需要各自的连接。

如果你已经有现成的 App,读一下迁移指南。旧的模型加载方法会一直弃用到 1.0,但其他 API 和生命周期改动需要留意。

从一个功能开始:改写、摘要,或者对话。先让它跑起来,然后试试取消、切换模型、离开屏幕。这些交互比第一次成功提示更能告诉你集成做得怎么样。

Dart 和 Flutter 示例放在那里就是给你改的。如果你觉得这条路还有哪里可以更顺,欢迎提反馈。

常见问题(FAQ)

llamadart 0.11 如何简化模型加载和释放?

0.11 引入 LlamaEngine.load 方法,加载成功直接返回就绪的 engine,失败则自动释放资源。调用方只需在 finally 中 dispose,避免了手动管理 backend 和 engine 的繁琐。

在 Flutter 中如何管理离线模型的下载和恢复?

模型加载器通过 onProgress 和下载选项暴露进度与取消,App 可展示进度并在用户关屏后恢复。下次启动复用已下载文件,避免重复下载。

llamadart 切换模型时如何保证旧模型可用?

在原生平台上,setModel 会在替代文件解析或下载期间保持当前模型可用。若失败或取消,旧模型保持加载;只有文件就绪后才替换。Web 运行时则先卸载。

Roger深圳
本文由 Roger 审核,最后更新于 2026年10月9日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。