Syyskuussa 2026 TypeSafe esitteli mallityypin, jota se kutsuu System One -malleiksi, ja niistä ensimmäisen, Jevin. Lupaus on tarkoituksella kapea: jäsentymätön tila sisään, tyypitetty todennäköisyyspohjainen päätös ulos. Ei proosaa. Toimittajan mukaan vastaus tulee 70–500 millisekunnissa ja hinta on 0,042 dollaria miljoonalta syötetokenilta. Tuloste on ilmainen.
Samalla viikolla rakensimme juuri sen muotoista kerrosta Willitiin, suomalaiseen lähiruoan markkinapaikkaan, jonka Viher IT rakentaa ja ylläpitää. Syy ei ollut julkaisu. Syy oli lasku.
Lasku, josta kaikki alkoi
Willitissä on taustatyö, joka normalisoi kysyntäsignaaleja: vapaata tekstiä siitä, mitä ihmiset haluavat ostaa, sovitettuna tuoteluokitukseen. Se kutsui pientä kielimallia viiden minuutin välein. Se kävi 17. syyskuuta läpi 1,58 miljoonan rivin jonoa uudelleenyrityksin, kun rajapinnan saldo loppui. Mitään dramaattista ei hajonnut. Työ vain epäonnistui ja raportoi samaa virhettä viiden minuutin välein kuuden tunnin ajan.
Kävimme siksi läpi sovelluksen jokaisen kielimallikutsun. Niitä oli 26. Suuri osa oli muodoltaan samanlaisia: suljettu kysymys, jolla on kiinteä vastausjoukko. Millainen viesti tämä on. Mikä kuudesta yritystyypistä tämä yritys on. Onko tämä sivu tuotesivu. Kaksi kutsuista käytti keskikokoista mallia valitakseen yhden kuudesta vaihtoehdosta. Yhdellätoista ei ollut kustannuskattoa lainkaan.
Mikään tuosta ei ole päättelyä. Se on luokittelua, ja maksoimme siitä tekstin tuottamisen hintaa.
Mikä System One -malli on
Jev luopuu tekstin tuottamisesta kokonaan. Sille lähetetään tila, eli pieni tietorakenne jossa on tekstiä, ja joukko kysymyksiä. Kysymystyyppejä on kolme:
- Choice eli valinta. Valitse yksi vaihtoehto listasta. Palauttaa valinnan, jokaisen vaihtoehdon todennäköisyyden ja varmuuden.
- Score eli asteikko. Sijoita tila asteikolle. Palauttaa tason, todennäköisyydet ja varmuuden.
- Noul eli kyllä tai ei. Pitääkö väite paikkansa. Palauttaa luvun väliltä 0–1.
Yhden kutsun kaikki kysymykset arvioidaan rinnakkain ja toisistaan erillään, joten kysymyksen lisääminen ei juuri muuta vasteaikaa. Se muuttaa suunnittelua. Yhden nokkelan kehotteen sijaan kysytään viisi yksinkertaista kysymystä kerralla.
Tältä tukiviesti näyttää TypeSafen dokumentoidulla Python-kirjastolla:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
state = {"message": "Hei, tilasin perjantaina kalaa. Milloin sen voi noutaa?"}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions={
"is_automated": Noul(
instructions="Is `message` an automated reply or a notification?",
),
"intent": Choice(
instructions="What is `message` mainly about?",
criteria={
"order_status": "Asks where an order is or when to collect it.",
"refund": "Wants money returned.",
"listing_help": "A seller asks how to list or edit a product.",
"other": "None of the above.",
},
),
"urgency": Score(
instructions="How soon does `message` need a person?",
criteria=["Can wait.", "Today.", "Now."],
),
},
)
intent = response.answers["intent"]
print(intent.choice, intent.confidence)Hyödyllinen ajatus ei ole toimittaja vaan muoto: pyynnössä kiinnitetty suljettu vastausjoukko, todennäköisyydet takaisin ja kynnys, jota hallitset itse. Kun oma koodi puhuu tätä muotoa, sen takana oleva malli on vaihdettava osa. Niin me sen rakensimme.
Kerros: halvin porras ensin
Jokainen saapuva viesti kulkee kolmen portaan kaskadin läpi, ennen kuin yksikään kallis malli saa käynnistyä.
- Porras 0, säännöt. Ilmainen, mikrosekunteja. Lähettäjäosoite, joka alkaa
no-reply, on automaatti. Lomake kertoo liidin tyypin, koska tiedät miltä lomakkeelta se tuli. Tuotenimien sanakirja tunnistaa suurimman osan kysyntätekstistä ilman mallia. Sääntö on joko varma tai hiljaa. - Porras 1, pieni luokittelumalli. System One -mallin paikka. Alle sekunti ja muutama sentin tuhannesosa päätökseltä.
- Porras 2, pieni kielimalli. Sille esitetään sama tyypitetty kysymys, ja se pakotetaan samaan suljettuun vastausjoukkoon.
Sääntö, joka tekee tästä kannattavaa: jokainen porras saa vain ne kysymykset, joita edellinen ei ratkaissut. Kun säännöt ja pieni malli ratkaisevat kaiken, kielimallia ei kutsuta lainkaan. Siinä on koko säästö.
Tyypit ovat pieniä. Tämä on TypeScript-versio siitä, mikä Willitissä pyörii Rubylla:
type Question =
| { key: string; kind: "choice"; ask: string; options: string[]; actAbove?: number }
| { key: string; kind: "score"; ask: string; levels: string[]; actAbove?: number }
| { key: string; kind: "yesno"; ask: string; actAbove?: number };
type Answer = {
key: string;
value: string; // always one of the listed options
probabilities: Record<string, number>;
confidence: number; // 0..1
};
interface Tier {
name: string;
answer(state: Record<string, string>, open: Question[]): Promise<Answer[]>;
}
const FLOOR = 0.6;
const ACT_ABOVE = 0.85;
const allowed = (q: Question): string[] =>
q.kind === "choice"
? q.options
: q.kind === "score"
? q.levels.map((_, i) => String(i))
: ["true", "false"];
export async function decide(
state: Record<string, string>,
questions: Question[],
tiers: Tier[], // cheapest first: rules, small model, small LLM
) {
const settled = new Map<string, Answer & { tier: string }>();
const suggested = new Map<string, Answer & { tier: string }>();
let open = questions;
for (const tier of tiers) {
if (open.length === 0) break; // nothing left: no model is called
// A tier never throws upward. Failure means "no answers from here".
const answers = await tier.answer(state, open).catch(() => []);
for (const a of answers) {
const q = open.find((x) => x.key === a.key);
if (!q || !allowed(q).includes(a.value)) continue; // closed set, enforced
const found = { ...a, tier: tier.name };
if (a.confidence >= (q.actAbove ?? ACT_ABOVE)) settled.set(a.key, found);
else if (a.confidence >= FLOOR) suggested.set(a.key, found);
}
open = open.filter((q) => !settled.has(q.key));
}
// Whatever is still open goes to a person, best suggestion attached.
return { settled, suggested, open };
}Kolme yksityiskohtaa tuossa silmukassa on tärkeämpiä kuin miltä ne näyttävät.
- Suljettu joukko pakotetaan ulostulossa, siihen ei luoteta. Vastaus, joka nimeää vaihtoehdon jota kysymys ei listannut, hylätään. Vapaata tekstiä ei lähde tästä kerroksesta yhdeltäkään portaalta.
- Porras ei koskaan heitä virhettä ylöspäin. Epäonnistunut porras ei palauta mitään, ja sen kysymykset putoavat seuraavalle portaalle tai ihmiselle, aivan kuten ennen kerroksen olemassaoloa.
- Porttikysymys voi pysäyttää kaskadin. Jos säännöt ratkaisevat, että viesti on poissaolovastaus, kukaan ei tarvitse sen aihetta. Tämän löysimme rakentamalla simulaattorin ja katsomalla, kuinka kaskadi kysyi silti.
Varmuus hoitaa reitityksen
Jokaisella kysymyksellä on kaksi kynnystä. Lattian alla vastaus jätetään huomiotta. Lattian ja toimintakynnyksen välissä se on ehdotus, jonka seuraava porras tai ihminen vahvistaa. Sen yläpuolella koodi saa käyttää sitä. Oletuksemme ovat 0,6 ja 0,85. Kahdessa kysymyksessä, joissa väärä vastaus on kallis, kynnys on 0,9: kiireellisyys ja se, onko viesti automaattinen.
Varmuudella täytyy olla sama määritelmä joka portaalla, tai kynnykset eivät tarkoita mitään. Portailla, jotka palauttavat vain todennäköisyydet, käytämme parhaan vaihtoehdon etäisyyttä sattumasta:
// 0 when the top option is no better than chance, 1 when it is certain.
function confidence(probabilities: Record<string, number>): number {
const p = Object.values(probabilities);
const chance = 1 / p.length;
return Math.max(0, (Math.max(...p) - chance) / (1 - chance));
}On syytä olla rehellinen siitä, mikä tuo luku on. Kielimallin itse ilmoittamat todennäköisyydet eivät ole kalibroituja. TypeSafe kouluttaa Jevin varmuuden erikseen, mutta ei ole julkaissut luotettavuuskäyrää, joten pidämme sitäkin todentamattomana. Kynnykset ovat lähtöarvoja, jotka viritetään lokia vasten. Ne eivät ole tosiasioita.
Mikä pidetään poissa mallilta
TypeSafen dokumentaatio kertoo heikot kohdat suoraan, ja suunnittelu seuraa niistä.
- Laskeminen, päivämäärät ja aritmetiikka pysyvät koodissa. Malli ei laske, eikä se osaa arvioida, osuuko päivämäärä aikaikkunaan. Määrät ja määräajat jäsennetään tavallisella koodilla.
- Tila on lyhyt ja olennainen. Osumatarkkuus laskee, kun tilassa on päätökseen liittymätöntä aineistoa. Lähetä viesti, älä koko ketjua.
- Tila on vihamielinen. Pieni malli ei puolustaudu syötteeseen piilotettuja ohjeita vastaan. Suljettu vastausjoukko rajaa vahingon: pahinta, mitä ujutettu viesti voi tehdä, on luokitella itsensä väärin.
- Luokittelu ei ole lupa toimia. Tieto siitä, mikä viesti on, 0,99:n varmuudella ei muuta mitään siinä, mitä järjestelmä saa sille tehdä. Willitissä sen päättävät erilliset autonomian tikapuut, ja varmasti luokiteltu viesti tuottaa yhä luonnoksen, kunnes toiminto itse on ansainnut luottamuksen.
Halvan portaan on ansaittava paikkansa
Halvempi malli on säästö vain, jos se on oikeassa. Siksi mitään ei vaihdeta uskon varassa.
- Varjoajo. Porras vastaa, eikä vastausta käytetä mihinkään. Vanha polku päättää yhä, ja sen tulos tallennetaan vertailukohdaksi.
- Ylennys kysymys kerrallaan, ei joukko kerrallaan. Kysymys otetaan käyttöön, kun vähintään 200 verrattua päätöstä osoittaa 97 %:n osuvuuden niissä vastauksissa, joiden perusteella olisi toimittu, ja toimittu olisi vähintään puolessa. Kysymyksen sanamuodon muuttaminen nollaa laskurin.
- Käytössä, otoksen kanssa. Halpaa vastausta käytetään, kun se on varma. Kielimalli ajaa yhä 5 %:n otosta jatkuvana vertailukohtana.
- Pudotus automaattisesti. Jos otoksen osuvuus laskee alle 93 %:n viimeisestä sadasta päätöksestä, kysymys palaa varjoajoon ilman että kukaan päättää siitä.
Kaikki tämä nojaa yhteen tauluun: päätöslokiin, jossa ovat vastaukset, ajetut portaat, vertailukohta, ihmisen antama oikea vastaus silloin kun sellainen on, viive ja kustannus. Loki tallentaa viestistä tiivisteen, ei tekstiä. Ihmisen korjaamista riveistä tulee kultainen testijoukko ja myöhemmin oman luokittelijan opetusaineisto, jos sellaisen joskus haluat.
Ennen ylennystä merkitsee yksi mittari: kuinka moni niistä vastauksista, joiden perusteella porras olisi toiminut, oli oikein?
Virhekin tarvitsee portaansa
Viimeisen osan opetti saldon loppuminen. Mallia kutsuvan putken täytyy erottaa pysyvä virhe ohimenevästä.
- Katkaisija. Tunnistautumis- tai laskutusvirhe avaa katkaisijan heti, tunniksi, yhdellä hälytyksellä. Ohimenevät virheet avaavat sen kolmen peräkkäisen jälkeen viideksi minuutiksi. Vanhassa työssä ei ollut kumpaakaan, ja se nosti saman hälytyksen 72 kertaa.
- Kustannusvahti. Päiväbudjetti putkea kohti. Halpa porras rajataan kutsumäärällä eikä rahalla, koska päätös, joka maksaa kaksi sentin tuhannesosaa, pyöristyy käyttölokissa nollaan.
- Hätäkytkin. Koko kerros on yhden kytkimen takana ja toimittajan malliin nojaava porras toisen.
Mitä se maksaa
| Porras | Hinta per päätös | Viive | Vastaa kun |
|---|---|---|---|
| Säännöt | Ilmainen | Mikrosekunteja | Vastaus on tosiasia: lähettäjä, lomakkeen lähde, sanakirjaosuma |
| Pieni luokittelumalli | Noin 0,00002 € | 70–500 ms (toimittajan luku) | Vapaata tekstiä, suljettu kysymys |
| Pieni kielimalli | Noin 0,001 € | Sekunteja | Halvemmat portaat eivät olleet varmoja |
| Iso malli | Senttejä | Sekunneista minuutteihin | On jotain kirjoitettavaa tai suunniteltavaa |
Päätöskohtaiset luvut ovat omia arvioitamme lyhyille tukiviesteille listahinnoin. System One -mallin hinta on 0,042 dollaria miljoonalta syötetokenilta, kun pieni kielimalli maksaa noin dollarin, eikä tulosteesta veloiteta. TypeSafen julkaisukirjoitus raportoi työnkulkuja, jotka ovat 193 kertaa nopeampia ja 444 kertaa halvempia, ja toteaa itse, että luvut ovat todellisten hyötyjen yläpäästä.
Kaksi asiaa on arvokkaampia kuin toimittajan kerroin. Ensinnäkin kartoituksemme suurin yksittäinen säästö oli sanakirja: suurin osa kysyntätekstistä nimeää tuotteen, jonka hakutaulu jo tuntee, eikä mitään mallia silloin ajeta. Toiseksi kerros maksaa itsensä takaisin jo säännöillä ja pienellä kielimallilla. System One -porras on parannus rakenteeseen, joka toimii ilman sitäkin, ja siksi meillä on varaa odottaa näyttöä.
Ajatuksena tässä ei ole mitään uutta. Mallikaskadit kuvattiin FrugalGPT-artikkelissa vuonna 2023. Uutta on halvalle portaalle rakennettu malli, jossa tyypitetty tuloste on tae eikä toive.
Mihin emme vielä luota
- "0 % hallusinaatioita" tarkoittaa tyyppiturvallista, ei totta. Toimittaja sanoo itse, ettei luku ole kokeellinen: skeema täsmää aina, joten kaaviossa lukee nolla. Väärä vaihtoehto suljetusta joukosta on silti väärä.
- Suomea ei ole mitannut kukaan. Viestimme ovat suomeksi. Ajamme halpojen portaiden läpi joukon oikeita tukitapauksia, joille on merkitty oikeat vastaukset. Se luku ratkaisee, ei julkaisukirjoitus.
- Käsittelijä on Yhdysvalloissa. Henkilötietojen käsittelysopimus on olemassa, mutta emme ole allekirjoittaneet sitä. Siihen asti toimittajan porras pysyy kiinni, ja tilasta poistetaan sähköpostiosoitteet, puhelinnumerot, henkilötunnukset, tilinumerot ja linkit ennen kuin se voisi lähteä minnekään. Tunnisteiden poisto on minimointia. Se ei ole anonymisointia eikä korvaa sopimusta.
- Pääsy on jonotuslistan takana. TypeSafe avaa pääsyä vähitellen.
Rehellinen tilanne on siis tämä. Kerros on rakennettu ja toimitetaan pimeänä: oletuksena pois päältä, ja päällä ollessaan vain varjoajona. Osumatarkkuuslukuja meillä ei vielä ole, eikä tämä kirjoitus väitä muuta. Jos toimittajan porras ei koskaan läpäise noita tarkistuksia, sen paikan ottaa itse ajettava pieni enkooderimalli, eikä mikään muu rakenteessa liiku.
Mistä aloittaa
- Listaa sovelluksesi jokainen kielimallikutsu. Merkitse ne, jotka ovat suljettuja kysymyksiä.
- Kysy jokaisen kohdalla, minkä sääntö ratkaisisi ilmaiseksi: lähettäjä, lähde, hakutaulu, säännöllinen lauseke.
- Kirjoita loput tyypitetyiksi kysymyksiksi: avain, kiinteä vastausjoukko, kynnys.
- Kirjaa jokainen päätös lokiin sen rinnalle, mitä nykyinen polku päätti.
- Aja varjossa. Ylennä kysymys kerrallaan näytön perusteella. Pudota automaattisesti.
- Lisää kustannusvahti ja katkaisija jokaiseen kutsupaikkaan, myös niihin joita et ole korvaamassa.
Iso malli pitää työn, jossa se on hyvä: luonnostelun, suunnittelun ja tapauksen, jota kukaan ei osannut ennakoida. Se vain lakkaa olemasta oletusvastaus jokaiseen kysymykseen.
