Alle artikler

En AI-app uden server, uden konto, og intet forlader telefonen

· 10 min. læsning

En lørdag i familiekalenderen: „Piscine 10h avec Tom“. Samme aften får barnet et fransk diktat om svømmeturen med Tom, skrevet på dets niveau, som træner den bøjningsregel, det tog fejl af i sidste uge.

Intet fra den lørdag forlader telefonen. Ikke kalenderposten, ikke barnets fornavn, ikke dets fejl. Der er ingen konto at oprette, ingen server at ringe til, ingen statistik. Sæt enheden i flytilstand, og appen opfører sig præcis som et minut før.

Appen hedder Dictée Ludique. Jeg har skrevet den, den ligger i App Store, og den er det klareste svar, jeg har på et spørgsmål, kunder nu stiller mig næsten hver uge: kan vi lave AI-delen uden at sende vores brugeres data nogen steder hen?

På Apple-hardware er svaret ja, og det findes i dag. Resten her handler om, hvad det krævede, hvad det kostede mig, og hvad iOS 27 ændrer i september.

Hvorfor ikke bare spørge en chatbot

Det er det oplagte spørgsmål, og hele appen er født ud af det.

Jeg begyndte som alle andre. Jeg bad en chatbot skrive et diktat til et barn på ni, og den skrev et. Endda et fornuftigt et.

Så gjorde jeg det igen ugen efter og så, hvad jeg faktisk havde. En chatbot giver dig en tekst. Den giver dig ikke et system. Den ved ikke, at dette barn sad sikkert på kongruens i perfektum i november og havde mistet det i januar, at homofoner aldrig er blevet testet en eneste gang, eller at de sidste tre tekster alle handlede om den samme ferie. Det var mig, der bar alt det, og som tastede det ind i hver eneste prompt igen. Skrivningen var gratis. Bogholderiet var hele arbejdet.

Det, jeg ville have, var maskineriet omkring skrivningen. Femten stavefærdigheder fra skolens læreplan, fulgt barn for barn, hver placeret på behersket, i gang, skrøbelig eller ikke set endnu. Et rettetrin efter hvert diktat, der flytter de tilstande, og som tager forælderen omkring femten sekunder. Derefter en næste tekst, der først sigter mod det skrøbelige og bagefter mod det, der endnu ikke er dukket op, med længde og tæthed, der følger sidste resultat, og uden en eneste indstilling, nogen skal sætte op.

Det skulle også føles som et spil, hvilket er den anden halvdel af navnet. Ikke point og badges. Der er bevidst ingen samlet score nogen steder i appen, for et gennemsnit over femten færdigheder betyder ingenting og ville være det ene tal, et barn huskede. Spillet er, at barnet får tre kort og vælger et, og at teksten handler om dets egen lørdag i stedet for en fremmeds. Et barn skriver anderledes, når diktatet handler om dets fødselsdag frem for naboens kat.

Genereringen viste sig at være den lille del. Sløjfen omkring den er appen, og den har afgjort alt herunder.

Hvorfor skyen aldrig var på bordet

Der er en regulatorisk grund, og der er den, jeg faktisk havde.

Den regulatoriske er almindelig. Det er en børneapp, der sælges i Europa, så spørgsmålene om databeskyttelse kommer med den første forælder, der åbner privatlivsskærmen, ikke efter en investeringsrunde. At indsamle ingenting betyder ingenting at angive, ingenting at lække og ingenting at forklare.

Den, der reelt afgjorde det: appen læser en familiekalender. Det er den mest intime fil på en telefon. Der står hospitalsaftaler, overleveringer ved delt forældremyndighed, jobsamtaler, den uge hvor nogen flyttede ud. Jeg ville ikke bygge et rør derfra til et datacenter, jeg ikke ejer, og ingen privatlivspolitik, jeg kunne have skrevet, ville have gjort mig rolig ved det.

Begrænsningen kom altså først, og arkitekturen måtte leve med den.

Kalenderen filtreres, før modellen ser den

Det er den del, alle antager AI’en klarer. Det gør den ikke.

En rå begivenhedstitel når aldrig modellen. Det, der når den, er en kanonisk nominalfrase fra en lukket liste med treoghalvfjerds genkendte former. „Piscine 10h avec Tom“ kommer ud som „la sortie piscine“. „Anniv de Tom“ kommer ud som „l’anniversaire de Tom“, og det fornavn, fanget af et bevidst snævert mønster, er den eneste frie tekst i hele kæden.

Der sammenlignes ved ordgrænser, hvilket lyder pedantisk, indtil man ser, hvad det forhindrer. „ski“ må ikke slå til på „Kandinsky“. „colo“, den franske forkortelse for en feriekoloni, må ikke slå til på „coloscopie“. Den længste form vinder, så „sortie scolaire“ slår det nøgne „sortie“.

En anden liste på toogtres ord diskvalificerer hele begivenheden med det samme: hospital, begravelse, skilsmisse, arbejdsløshed, fængsel, og resten af de registre, en familiekalender er fuld af. „Anniversaire de mamie à l’hôpital“ er ikke en brugbar fødselsdag, selv om ordet fødselsdag står lige der.

Det, listen ikke genkender, beholder slet intet navn. Det lander på forælderskærmen som et forslag uden etiket, og det kan ikke bruges, før forælderen selv taster et navn til det. Afvist én gang bliver det aldrig foreslået igen.

Læsevinduet er asymmetrisk med vilje: to år tilbage for enkeltstående begivenheder, to uger frem, og kun én uge tilbage for alt, der gentager sig. Et barn husker en rejse i årevis. En judotime fra atten måneder siden er støj.

Alt dette er almindelig Swift. Det er deterministisk, det er dækket af tests, og det er den del, der udfører det egentlige privatlivsarbejde. Modellen er sidste skridt, ikke det første.

Modellen skriver teksten og intet andet

Genereringen kører på Apples model på enheden gennem Foundation Models-frameworket, som kom med iOS 26. Outputtet er styret: en @Generable-struktur med en kort titel og et array af sætninger, med loft på tolv.

Sætninger som array frem for én blok prosa, for modellen holder en mållængde langt bedre, når den selv tæller sætningerne.

Modellen bliver aldrig spurgt, hvor stavefælderne ligger. Det er arbejdet for en deterministisk analyse med sine egne tests. En model, der annoterer sit eget output, tager fejl en gang imellem uden at nogen opdager det, og det er værre end at tage fejl højlydt.

Den ser heller ingen skabelon. Det håndskrevne bibliotek skal tjene ethvert barn, så det bærer pladsholdere og et kongruensmaskineri. Her kender appen allerede fornavnet og dets grammatiske køn, så den beder om „Léa est partie“ i stedet for „{ENFANT} est parti{E_ENFANT}“. En hel klasse af fejl forsvinder.

Ingen genereret tekst får frit lejde

Hver genereret tekst går gennem den samme validering i syv trin som det håndskrevne bibliotek: ordforråd afstemt efter klassetrin, tilladte tider, sætningslængde, forbudte registre, de færdigheder barnet skal træne, dets eget fornavn, og det kalenderøjeblik, der faktisk nævnes.

En genereret tekst har ingen fortrinsret over en tekst, et menneske har læst igennem. Den har endda lidt mindre, fordi den krævede et loft, biblioteket aldrig havde brug for.

Når fem forsøg fejler, serverer appen en tekst fra biblioteket og siger ingenting. Barnet får aldrig at vide, at modellen havde et dårligt minut.

Hvad modellen på enheden faktisk kostede mig

Den del står ikke i nogen WWDC-session, så her er tallene fra logfilerne på min egen enhed.

  • En generering løb løbsk i 48,9 sekunder, før den døde på „Exceeded model context window size“. De fire andre forsøg, med identiske instruktioner, kom tilbage på 2 til 3,5 sekunder. Inputtet var aldrig problemet. Outputtet ville ikke stoppe. Derfra kommer loftet på tolv sætninger.
  • Første udkast rammer jævnligt en tredjedel af den bestilte længde. Målt på én session: 29 ord, så 184, så 34, ved en instruktion der bad om 55 til 75. At hæve temperaturen ryster kun tilfældet. At fortælle modellen på klart fransk, hvad den lige ramte forbi, virker. Gentagelsessløjfen sender derfor nu en sætning tilbage, der forklarer afvigelsen, i stedet for kun at skrue på temperaturen.
  • Modellen lavede tekster, der bestod hver eneste kontrol og alligevel var forkerte for læseren. Elleve sætninger til et barn på ni, ordtallet inden for rammen, hver sætning under maksimum, og barnet får et telegram. Et menneske havde skrevet hver skabelon i biblioteket, så den fejl fandtes ikke der. Modellen laver den med bemærkelsesværdig vedholdenhed, deraf loftet på antallet af sætninger.
  • Apples værn udløses uden at sige hvorfor. Fejlen lyder „Detected content likely to be unsafe“ og intet andet. Fem gentagelser ved stigende temperaturer med samme input fejler fem gange på samme måde. Det appen gør nu, er at fjerne ting i rækkefølge: først kalenderøjeblikket, da det er det eneste indhold, appen ikke selv har skrevet, derefter indholdsreglen i instruktionerne. Det trin, der låser op, er diagnosen, og det går i loggen som sådan.
  • EventKit kostede mig en eftermiddag af grunde, der intet havde med AI at gøre. En EKEventStore, der oprettes før tilladelsen, beholder en tom cache i hele processens levetid, så kalenderopslaget giver nul begivenheder, selv efter forælderen har trykket Tillad. Og authorizationStatus(for:) halter efter tildelingen: på iPad kom anmodningen tilbage som true, mens status, læst tretten millisekunder senere, stadig sagde notDetermined. Begge ligner præcis en rettighedsfejl, og ingen af dem er det.

En sidste begrænsning har formet appen mere end alle de andre. Fem forsøg er loftet, når et barn kigger på en skærm med „vi skriver dit diktat“. Appen genererer derfor på forhånd. At læse et diktat højt tager minutter, og generatoren arbejder i det vindue, med loft på ti runder pr. session, bundet til læseskærmen og afbrudt når den lukkes. Intet uvalideret kommer nogensinde i cachen. Det køber tid, ikke lempelse.

Hvad iOS 27 ændrer

Størstedelen af listen ovenfor bliver løst af den version, der udkommer i september, hvilket er mærkeligt at skrive om et framework, der er et år gammelt.

  • Modellen på enheden er bygget om, med bedre logik, bedre kald af værktøjer og værn, som ifølge Apple giver færre falske alarmer. Det rammer direkte min dyreste fejltilstand. Bedre efterlevelse af instruktioner burde også sænke antallet af gentagelser, og det er forskellen mellem at generere på forhånd og at generere på forlangende.
  • response.usage rapporterer nu tokens for input, output, cache og ræsonnement, model.contextSize viser vinduet, og tokenCount(for:) tæller en prompt før afsendelse. Mit udsving på 48,9 sekunder bliver dermed noget, jeg kan se komme, i stedet for noget jeg finder i en log bagefter.
  • Modellen på enheden tager imod billeder, og frameworket leverer et OCRTool på enheden, båret af Vision. Det er det, jeg bliver ved med at tænke på. Beskrivelsen i App Store lover i dag, at appen ikke fotograferer siden, fordi håndskriftgenkendelse opfinder fejl, barnet aldrig har lavet, og en staveapp, der hallucinerer en fejl, er værre end ingen app. Privatliv var aldrig den eneste grund til at sige nej der. Men at kunne prøve det helt på enheden, uden nogen upload, er nyt.
  • LanguageModel-protokollen lader enhver model bære en session. Appen havde allerede den form: én skranke afgør under kørslen, om frameworket findes på dette system, og derfor betjener én binær både macOS 15 uden Apple Intelligence og macOS 26 med. Den indirekte vej tilhører nu styresystemet i stedet for mig.
  • DynamicProfile lader én session skifte instruktioner og værktøjer midt i samtalen. At skrive et diktat og at rette et er to forskellige opgaver i denne app, og i dag to forskellige sessioner.

Apples gratis sky-model, slået fra som standard

Den samme version gør det meget let at forlade enheden.

PrivateCloudComputeLanguageModel giver et kontekstvindue på 32.000 tokens og en indstilling for ræsonnement, uden API-nøgle, uden konto og uden skyregning for små udviklere under en grænse på to millioner førstegangsdownloads. Anthropic og Google udgiver Swift-pakker, der kobler sig på den samme sessionstype. At skifte denne app over er cirka én linje, og teksten ville komme tilbage længere, bedre bygget og langt oftere gyldig allerede ved første forsøg.

Der er et reelt argument for at tage imod, og det er ikke det, jeg forventede fra start. Modellen på enheden skriver et godt diktat til et barn på ni. Den bliver tynd, når den, der spørger, er en voksen, som lærer fransk og vil have en længere tekst med bisætninger og et bredere ordforråd, eller en forælder, der vil noget mere ambitiøst end niveauskemaet foreslår. De læsere findes, de bruger allerede appen, og den mindste model på telefonen skriver ikke rigtig til dem.

I iOS 27 vil appen derfor tilbyde det som en indstilling, og den indstilling leveres slået fra. Installer appen, rør ikke ved noget, og løftet den giver i dag holder stadig: intet forlader enheden, og flytilstand beviser det på fire sekunder.

Det, der overhovedet gør den mulighed tilbudsegnet, er netop arbejdet længere oppe. Selv med kontakten tændt krydser en rå kalendertitel stadig aldrig netværket. Positivlisten har for længst skåret den ned til en kanonisk nominalfrase, så det, der rejser, er „la sortie piscine“ og et fornavn, aldrig „RDV Dr Meunier, résultats“. Filteret blev bygget for at beskytte et barn mod modellen. Det viser sig også at beskytte familien mod netværket.

Privatlivsskærmen vokser med én sætning, og det er den del, jeg vil have rigtigt. Én linje, der siger at intet forlader enheden, bliver til to: intet forlader enheden, medmindre du slår rigere tekster til, og her står præcis hvad der så går ud. Apples private sky er et godt løfte, og jeg tror på det. Det er stadig et løfte, og et løfte hører hjemme bag en kontakt, en forælder bevidst har slået til, ikke i en standardindstilling, de aldrig har læst.

Enheden bliver også standard af en mindre prangende grund. Et diktat i toget, i bilen, i et lejet hus med dårligt wifi, er præcis det øjeblik, hvor en familie griber ud efter denne app. Uanset hvad indstillingen siger, skal det blive ved med at virke helt uden netværk, så motoren på enheden går ingen steder.

Hvis du har brug for dette i din egen app

Hvis du kigger på en app, hvor data ikke må forlade enheden, og nogen har fortalt dig, at det udelukker AI, så er det råd et år gammelt. Sundhed, uddannelse, HR, jura, alt der involverer børn, alt hvor en kundekontrakt nævner det land, data skal ligge i: vejen over enheden findes, og den virker nu.

Forbeholdene, som du hellere skal høre fra mig end opdage i måned to:

  • Modellen på enheden er lille. Den skriver en god kort tekst. Den ræsonnerer sig ikke gennem et langt dokument. Tilpas funktionen til modellen frem for omvendt.
  • Du kommer til at skrive et valideringslag. Behandl modellen som en hurtig skribent uden dømmekraft, og læg deterministisk kode omkring den. Det er i den kode, produktet faktisk bor.
  • Du har brug for en reserve til den enhed, der slet ikke har Apple Intelligence, og den skal være usynlig. Her står et håndskrevet bibliotek bag modellen, og et barn kan ikke sige, hvilken af de to der skrev dagens tekst.
  • Enhed og sky er ikke et valg mellem to produkter. Læg filtreringen foran modellen frem for inde i den, så kan du tilbyde skyen som en indstilling senere uden at omskrive et eneste ord af din privatlivsskærm.
  • Intet af dette er webteknologi. Det er EventKit, et privatlivsmanifest, styret generering og platformadfærd, der kun viser sig på rigtig hardware, én grund mere til at native slår en cross-platform wrapper, når den svære del er styresystemet.

Det hele, kalenderfilteret inklusive, tog dage og ikke måneder. Størstedelen af den tid gik til valideringskæden og fejlvejene, ikke til prompten.

Brug for en app, der ikke kan sende noget ud?

Native iOS og Android, bygget omkring AI på enheden, EventKit og de platform-API’er, en webview ikke når. En fungerende app på din egen enhed ved udgangen af dag ét, fra 500 € ekskl. moms.

Tal om mit projekt