Irbisa · Шпаргалка
AI-функции в мобильных приложениях
Инференс на устройстве или на сервере, стриминг токенов, структурированный вывод, задержки, приватность
Как выбрать между моделью на устройстве (Gemini Nano, LiteRT) и серверной моделью для фичи вроде «Суммаризировать переписку»?
interface ThreadSummarizer { suspend fun summarize(text: String): String } class OnDevice(context: Context) : ThreadSummarizer { private val client = Summarization.getClient( SummarizerOptions.builder(context) .setInputType(InputType.ARTICLE)…В этом чате на каждом ходе отправляется вся история. Что станет со стоимостью и задержкой к 50-му ходу и как это исправить?
class ChatViewModel(private val api: ChatApi) : ViewModel() { private val history = mutableListOf<Message>() fun send(text: String) = viewModelScope.launch { history += Message("user", text) val reply = api.complete(history) // the whole conversation, every turn history += Message("assistant", reply) }}Этот адаптер превращает SSE-стрим токенов во
Flow. Что с ним не так?fun streamAnswer(client: OkHttpClient, request: Request): Flow<String> = callbackFlow { val listener = object : EventSourceListener() { override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) { trySend(data) } override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) { close(t) } } EventSources.createFactory(client).newEventSource(request, listener)}Найдите баг: английские ответы стримятся нормально, а в русских посреди слова иногда появляется
�.// The endpoint streams the answer as plain text over chunked HTTP.fun streamAnswer(body: ResponseBody): Flow<String> = flow { val buf = ByteArray(1024) body.byteStream().use { input -> while (true) { val n = input.read(buf) if (n == -1) break emit(String(buf, 0, n, Charsets.UTF_8)) } }}.flowOn(Dispatchers.IO)Ответ в стриме дёргается по мере прихода токенов, а список постоянно прыгает, пока текст растёт. Что вы поменяете?
// Deltas arrive far faster than a frame. Coalesce once, at the boundary.val answer: Flow<String> = flow { val buf = StringBuilder() api.streamAnswer(prompt).collect { delta -> buf.append(delta); emit(buf.toString()) }} .flowOn(Dispatchers.Default) // string building off the main thread…Пользователь жмёт «назад» на середине генерации. Что на самом деле должно произойти на клиенте и что серверу всё ещё нужно от вас?
class AnswerViewModel(private val repo: AnswerRepo) : ViewModel() { private var job: Job? = null fun ask(turnId: String, prompt: String) { job?.cancel() // a new question kills the old stream, and its cost job = viewModelScope.launch { // dies with the screen, not with the process…Какого времени до первого токена стоит добиваться на экране со стримингом ответа и какие способы скрыть ожидание честные?
// TTFT is a different metric from total duration. Time the first chunk, not the last.suspend fun ask(prompt: String, sink: (String) -> Unit) { val started = SystemClock.elapsedRealtime() var firstAt: Long? = null var tokens = 0 try {…Как запустить свою модель LiteRT (TensorFlow Lite) на Android и что меняет GPU-делегат?
class Classifier(context: Context) : Closeable { private val compat = CompatibilityList() private val gpu: GpuDelegate? = if (compat.isDelegateSupportedOnThisDevice) GpuDelegate(compat.bestOptionsForThisDevice) else null private val interpreter = Interpreter(…Что Google Play и App Store требуют от приложения с фичей генеративного ИИ?
Как устроен function calling (tool use) в мобильном ассистенте и какие инструменты должны выполняться на устройстве?
@Serializable data class CreateEvent(val title: String, val startEpochMs: Long, val minutes: Int) sealed interface ToolOutcome { data class Result(val json: String) : ToolOutcome data class NeedsConfirmation(val prompt: String, val run: suspend () -> String) : ToolOutcome}…Почему вызовы LLM должны идти через ваш бэкенд, а не с ключом вендора в приложении, и чем прокси должен владеть помимо скрытия ключа?
// WRONG: the client builds the prompt and carries the vendor key.// Anyone with the APK has the key; anyone with the key has your bill.val body = ChatBody( model = "vendor-large", messages = listOf(Message("system", SYSTEM_PROMPT), Message("user", userText)),)…Ваш ассистент суммаризирует письма и умеет вызывать инструменты. Как защитить его от prompt injection?
Вы просите у модели JSON и хотите показывать поля по мере их прихода. Что делать с наполовину написанным объектом и как выглядит на экране ещё не сгенерированное поле?
@Serializable data class Recipe(val title: String, val minutes: Int, val steps: List<String>) // The streaming mirror: every field nullable, because every field may not exist yet.@Serializable data class RecipeDraft( val title: String? = null, val minutes: Int? = null, val steps: List<String>? = null,)…Найдите баг: пользователь переключается на другое приложение посреди саммари, возвращается, и саммари начинается заново с пустого экрана, а в счёте две генерации.
class SummaryViewModel(api: AssistantApi, threadId: String) : ViewModel() { val summary: StateFlow<String> = api.stream(SummarizeRequest(threadId)) .runningFold("") { text, delta -> text + delta } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "")} @Composablefun SummaryScreen(vm: SummaryViewModel) { val summary by vm.summary.collectAsStateWithLifecycle() MarkdownText(summary)}Приложение убили посреди ответа, и пользователь вернулся через час. Что вы восстанавливаете, что отправляете заново и что нельзя отправлять дважды?
@Entitydata class TurnEntity( @PrimaryKey val id: String, // minted on the client, reused by every retry val conversationId: String, val role: String, // "user" | "assistant" val text: String,…Модель на устройстве весит 1.2 ГБ, а скачивание приложения столько весить не может. Как эти байты попадают на телефон и когда вы их удаляете?
// Play Asset Delivery: the model is an on-demand asset pack, not part of the APK.class ModelStore(private val packs: AssetPackManager) { fun localPath(): String? = packs.getPackLocation(PACK)?.assetsPath() // null = not here yet fun ensure(activity: Activity, onReady: (String) -> Unit) { localPath()?.let { return onReady(it) }…Модель отвечает по-разному при каждом запуске. Как протестировать ИИ-фичу перед релизом и понять, что она всё ещё работает после?