Brownie — l'IA embarquée sur Android
Problème
Tout assistant IA réellement capable sur un téléphone est un client léger. Le modèle réside dans le datacenter d'un tiers, la conversation fait l'aller-retour, et l'ensemble cesse de fonctionner dans un avion ou derrière le pare-feu d'une entreprise. La confidentialité y est une promesse inscrite dans une politique, non une propriété du système.
Je voulais savoir ce qu'il restait vraiment si l'on refusait tout cela — non pas une démonstration de chatbot, mais un véritable agent qui appelle des outils, lit et écrit des fichiers, se souvient d'une session à l'autre et pilote l'appareil, en tournant entièrement sur le matériel que l'on a dans la poche.
Contrainte
Un téléphone n'est pas un serveur. Il n'y a pas de swap digne de ce nom, le budget mémoire est partagé avec toutes les autres applications en cours d'exécution, et une inférence soutenue se heurte directement à la limitation thermique. Les poids du modèle, la couche d'outils, l'historique de conversation et l'interface doivent tenir dans ce qu'un appareil Android de milieu de gamme peut céder — et l'interface doit rester réactive pendant que le modèle travaille.
La seconde contrainte était volontaire, et plus difficile : aucun recours au cloud. Une conception hybride qui appellerait discrètement un modèle distant dès que l'appareil peine aurait rendu chaque démonstration plus fluide et détruit la raison même de ce projet.
Décisions
- Quantification choisie à l'exécution, et non à la compilation. L'application détecte la RAM disponible au premier lancement et choisit un palier : Q4 à environ 1,5 Go pour les appareils de 6 à 8 Go, Q4 plus un projecteur multimodal pour 8 à 12 Go, Q8 avec ce projecteur au-delà. En dessous de 6 Go, l'application annonce qu'elle ne fonctionnera pas, plutôt que d'échouer lentement en rejetant la faute sur l'utilisateur.
- Cinq modules Gradle plutôt qu'un seul.
:core:inferenceencapsule LiteRT-LM, le téléchargement du modèle et la détection de l'appareil ;:core:agentcontient la boucle ;:core:toolsle registre et ses douze implémentations ;:core:memoryla persistance, le profil et la recherche plein texte dans les sessions ;:appl'interface Compose. La frontière qui a justifié son coût est l'inférence — c'est la pièce dont le remplacement est le plus certain à l'arrivée du prochain modèle embarqué. - Compression du contexte plutôt que troncature. Quand la fenêtre se remplit, l'agent résume et poursuit. La troncature aurait représenté une journée de travail ; perdre la première moitié d'une conversation est précisément le type de défaillance que les utilisateurs ne savent pas diagnostiquer.
- Un client MCP. De nouveaux outils s'ajoutent en pointant l'application vers un serveur, sans publier d'APK.
Résultat
120 fichiers Kotlin répartis sur cinq modules, 200 commits. Appels d'outils multi-tours, jusqu'à dix itérations par message, réponses en streaming avec des étiquettes d'appel d'outil dans l'interface, outils de fichiers dans un espace de travail cloisonné, contacts, agenda, presse-papiers, tâches autonomes planifiées, lecture de l'écran et automatisation de l'appareil via les API d'accessibilité, extraction web via une WebView, et la parole dans les deux sens.
Sans cloud, sans clé d'API, sans aucune donnée qui quitte le téléphone. Après le téléchargement du modèle, effectué une seule fois, le moteur se charge en cinq à quinze secondes et l'application fonctionne réseau coupé.
Ce que je ferais autrement
La couche d'outils a fini par dépasser l'application. :core:tools est devenu plus gros que le module d'interface — quarante-quatre fichiers contre quarante et un — parce que chaque nouvelle capacité signifiait une implémentation de plus compilée dans le binaire. Le client MCP ajouté en v6 a la bonne forme, et c'est par là que je commencerais : un jeu réduit d'outils intégrés pour ce qui a réellement besoin d'un accès au processus, et tout le reste derrière un serveur d'outils modifiable sans publier d'APK. J'ai tracé cette frontière trop tard ; elle aurait dû être la première.