随着大型语言模型(LLMs,大规模语言模型)的不断进步和发展,将它们集成到移动应用中变得越来越可行,并且带来了许多好处。在设备上处理LLMs可以减少延迟,增强隐私保护,并提供离线功能等优势。
通过在设备上直接运行这些大型语言模型 (LLMs),应用程序可以提供实时响应,无需依赖持续的互联网连接,也不用担心将敏感数据暴露给外部服务器。
这篇博客探讨了在Android上使用LLM处理的概念,展示了如何用Kotlin实现这一功能。我们将逐步介绍一个利用LLM的Android应用的关键部分,该应用利用LLM进行实时文本生成和处理,提供了一种在设备上高效且安全地处理语言模型的方式,使信息更加连贯。
开始我们正在用Gemma 2B,Gemma 是一系列轻量级的开源模型,基于 Google 创建 Gemini 模型的研究和技术。你可以从提供的链接下载并解压模型,之后就可以用了。
要开始,请创建一个新的Android项目,我们将使用Compose框架。我们将使用Google的Mediapipe来与模型进行互动。MediaPipe解决方案提供了一系列库和工具,帮助您快速地将AI和ML功能集成到您的应用中。
将模型复制到设备- 在电脑的下载文件夹里解压下载的模型,然后连接你的移动设备。
- 在终端中运行以下adb命令,
使用adb shell命令删除/data/local/tmp/llm/目录及其内容
使用adb shell命令创建/data/local/tmp/llm/目录
使用adb命令将gemma2b.bin文件推送到/data/local/tmp/llm/目录
这些命令会将gemma2b.bin模型文件复制到临时文件夹中。现在模型已经放到正确的位置了,我们就可以开始写代码了。
咱们来编程吧!在 AndroidManifest 文件中添加此内容以支持本地库:
<uses-native-library
android:name="libOpenCL.so"
android:required="false" />
<!-- 注意:所有库都可选 -->
<uses-native-library
android:name="libOpenCL-car.so"
android:required="false" />
<uses-native-library
android:name="libOpenCL-pixel.so"
android:required="false" />
在你的 Android 应用的 build.gradle 文件中加入这个依赖项。
依赖项 {
实现 'com.google.mediapipe:tasks-genai:0.10.14'
}
LLMTask 类
我们的实现的核心部分是 LLMTask 类。这个类处理LLM推理的初始化和执行,确保模型在设备上高效运行起来。我们来看看这个类的关键要素:
class LLMTask(context: Context) {
private val _partialResults = MutableSharedFlow<Pair<String, Boolean>>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val partialResults: SharedFlow<Pair<String, Boolean>> = _partialResults.asSharedFlow()
private var llmInference: LlmInference
init {
val options = LlmInference.LlmInferenceOptions.builder()
.setModelPath(MODEL_PATH)
.setMaxTokens(2048)
.setTopK(50)
.setTemperature(0.7f)
.setRandomSeed(1)
.setResultListener { partialResult, done ->
_partialResults.tryEmit(partialResult to done)
}
.build()
llmInference = LlmInference.createFromOptions(
context,
options
)
}
fun generateResponse(prompt: String) {
llmInference.generateResponseAsync(prompt)
}
companion object {
private const val MODEL_PATH = "/data/local/tmp/llm/gemma2b.bin"
private var instance: LLMTask? = null
fun getInstance(context: Context): LLMTask {
return if (instance != null) {
instance!!
} else {
LLMTask(context).also { instance = it }
}
}
}
}
主要组件
- MutableSharedFlow 和 SharedFlow: 这些用于管理从 LLM 推理得到的部分结果的流动。MutableSharedFlow 让我们可以发布新的结果,而 SharedFlow 还可以将这些结果传递给应用程序的其他部分。
- 初始化 LlmInference: LlmInference 实例通过选项进行初始化,包括模型路径、最大标记数以及处理部分推断结果的结果监听器。
我们可以使用以下配置选项来初始化LlmInference。
- modelPath: 模型在项目目录中的路径。
- maxTokens: 模型处理的最大令牌数(输入令牌加输出令牌),默认值为512。
- topK: 模型在生成过程中考虑的令牌数量。将预测限制在最有可能的前k个令牌上。
- temperature: 较高的温度使生成的文本更具创意,而较低的温度则让生成的文本更可预测。默认值为0.8。
- randomSeed: 生成文本时使用的随机种子,默认值为0。
- loraPath: 设备上LoRA模型的绝对路径。注意:这仅适用于GPU模型。
- resultListener: 设置结果监听器以异步接收结果,仅适用于异步生成方法。
- errorListener: 设置可选的错误监听器。
密封类 LLMState {
数据对象 LLMModelLoading : LLMState() // 表示大型语言模型正在加载
数据对象 LLMModelLoaded : LLMState() // 表示大型语言模型已经加载完成
数据对象 LLMResponseLoading : LLMState() // 表示大型语言模型正在加载响应
数据对象 LLMResponseLoaded : LLMState() // 表示大型语言模型已经加载响应完成
// 判断当前状态是否为大型语言模型正在加载
val isLLMModelLoading get() = this 类型为 LLMModelLoading
// 判断当前状态是否为大型语言模型正在加载响应
val isLLMResponseLoading get() = this 类型为 LLMResponseLoading
}
这个封闭的类帮助管理和应对不同的状态,例如模型正在加载时、加载完毕后和生成响应时。
聊天状态 (ChatState)ChatState 类负责维护聊天状态,包括用户消息和模型的回应。
class ChatState(
messages: List<ChatDataModel> = emptyList()
) {
private val _chatMessages: MutableList<ChatDataModel> = messages.toMutableStateList()
val chatMessages: List<ChatDataModel>
get() = _chatMessages.map { model ->
val isUser = model.isUser
val prefixToRemove =
if (isUser) USER_PREFIX else MODEL_PREFIX
model.copy(
chatMessage = model.chatMessage
.replace(
START_TURN + prefixToRemove + "\n",
""
)
.replace(
END_TURN,
""
)
)
}.reversed()
val fullPrompt
get() =
_chatMessages.takeLast(5).joinToString("\n") { it.chatMessage }
fun createLLMLoadingMessage(): String {
val chatMessage = ChatDataModel(
chatMessage = "",
isUser = false
)
_chatMessages.add(chatMessage)
return chatMessage.id
}
fun appendFirstLLMResponse(
id: String,
message: String,
) {
appendLLMResponse(
id,
"$START_TURN$MODEL_PREFIX\n$message",
false
)
}
fun appendLLMResponse(
id: String,
message: String,
done: Boolean
) {
val index = _chatMessages.indexOfFirst { it.id == id }
if (index != -1) {
val newText = if (done) {
_chatMessages[index].chatMessage + message + END_TURN
} else {
_chatMessages[index].chatMessage + message
}
_chatMessages[index] = _chatMessages[index].copy(chatMessage = newText)
}
}
fun appendUserMessage(
message: String,
) {
val chatMessage = ChatDataModel(
chatMessage = "$START_TURN$USER_PREFIX\n$message$END_TURN",
isUser = true
)
_chatMessages.add(chatMessage)
}
fun addErrorLLMResponse(e: Exception) {
_chatMessages.add(
ChatDataModel(
chatMessage = e.localizedMessage ?: "生成消息时出错",
isUser = false
)
)
}
companion object {
private const val MODEL_PREFIX = "模型前缀"
private const val USER_PREFIX = "用户前缀"
private const val START_TURN = "开始回合"
private const val END_TURN = "结束回合"
}
}
关键方法:
- createLLMLoadingMessage : 向聊天状态添加新的加载消息,并返回该消息的ID。
- appendFirstLLMResponse 和 appendLLMResponse : 这些方法用于向聊天消息中追加部分和完整的LLM响应。
- appendUserMessage : 向聊天状态添加用户消息。
- addErrorLLMResponse : 如果LLM处理过程中出现问题,则添加错误消息。
- fullPrompt : 将最后5条消息连接起来,以提供更好的上下文给LLM。
ChatViewModel
这个类管理 UI 和 LLM 处理逻辑间的交互。它使用 Kotlin 协程来管理异步任务的管理,并根据需要更新 UI 状态。
@HiltViewModel
class ChatViewModel @Inject constructor(@ApplicationContext private val context: Context) :
ViewModel() {
private val _llmState = MutableStateFlow<大模型状态>(大模型状态.LLMModelLoading)
val llmState = _llmState.asStateFlow()
private val _chatState: MutableStateFlow<对话状态> = MutableStateFlow(对话状态())
val chatState: StateFlow<对话状态> = _chatState.asStateFlow()
fun 初始化大模型() {
viewModelScope.launch(Dispatchers.IO) {
_llmState.emit(大模型状态.LLMModelLoading)
LLMTask.getInstance(context)
}.invokeOnCompletion {
_llmState.value = 大模型状态.LLMModelLoaded
}
}
fun 发送消息(message: String) {
viewModelScope.launch(Dispatchers.IO) {
_chatState.value.appendUserMessage(message)
try {
_llmState.emit(大模型状态.LLMResponseLoading)
var currentLLMResponseId: String? = _chatState.value.createLLMLoadingMessage()
LLMTask.getInstance(context).generateResponse(_chatState.value.fullPrompt)
LLMTask.getInstance(context).partialResults
.collectIndexed { index, (partialResult, done) ->
currentLLMResponseId?.let { id ->
if (index == 0) {
_chatState.value.appendFirstLLMResponse(id, partialResult)
} else {
_chatState.value.appendLLMResponse(id, partialResult, done)
}
if (done) {
_llmState.emit(大模型状态.LLMResponseLoaded)
currentLLMResponseId = null
}
}
}
} catch (e: Exception) {
_chatState.value.addErrorLLMResponse(e)
}
}
}
}
主要功能:
- initLLMModel :初始化LLM模型并相应地更新状态。
- sendMessage :处理用户消息,生成LLM的回答,并用部分和最终的结果来更新聊天状态。
在聊天窗口里汇总一下
優點隐私: 通过在设备上处理数据,大语言模型减少了向互联网发送敏感信息的需求,从而更好地保护了用户隐私。
离线功能: 设备上的LLM可以离线运行,让用户在没有网络连接的情况下也能使用语言处理功能。
低延迟: 本地处理数据可以减少将数据发送到远程服务器处理的延迟,从而实现更快的响应。
降低数据成本: 用户可以避免因将数据发送到远程服务器处理而产生的额外数据费用。
自定义: 在设备上的LLM可以针对特定应用场景或设备进行定制和优化,从而达到更大的灵活性和性能提升。
成本效益: 从长远来看,设备处理更具成本效益,因为它减少了昂贵的服务器基础设施和数据传输成本。
不足模型大小和复杂度: 在设备上运行模型需要将大型语言模型存储在设备本地,这可能对现代大型语言模型来说是个挑战。更大的模型需要更多的存储空间和计算资源,这对低端设备来说是个负担。这可能让低端设备吃不消。
资源密集型: 在设备上运行LLM可能会非常消耗资源,尤其是对于复杂模型或长序列。这将导致电池消耗增加和性能下降,尤其是在较老或性能较差的设备上。
模型更新提示: 保持 LLM 模型与最新的进展和改进同步可能会很有挑战性。更新模型需要更新应用程序,这可能在某些情况下并不实际或可行。
请看,重要注意
Mediapipe LLM 推理 API 并不支持所有设备(不支持 32 位的 armeabi-v7a 设备),并且曾观察到仅使用 CPU 会生成格式错误的文本。
看看这些 GitHub 上的问题:
2. https://github.com/google-ai-edge/mediapipe-samples/issues/414 (GitHub问题链接)
🚀 喜欢我最新一篇 Medium 文章中的见解吗?要是你觉得有帮助,请拍手支持 (👏) 并分享给你的朋友们。
别忘了点那个“关注”按钮哦。🚀📌 让我们一起探索更多吧!🚀📌
共同学习,写下你的评论
评论加载中...
作者其他优质文章