Viher IT  /  Blogi  /  System One käytännössä

System One -mallit käytännössä: päätöskerros ennen kielimallia.

TypeSafe julkaisi Jevin, mallin joka ei kirjoita tekstiä. Se vastaa suljettuihin kysymyksiin tyypitetyillä arvoilla ja todennäköisyyksillä alle puolessa sekunnissa, hintaan joka pyöristyy nollaan. Tässä kerrotaan koodin kanssa, miten sellainen malli asettuu kalliiden kielimallikutsujen eteen. Esimerkkinä on markkinapaikka, johon kerroksen rakensimme.

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.
Kaavio System One -mallin kolmesta kysymystyypistä. Valinta palauttaa yhden vaihtoehdon kiinteästä listasta sekä todennäköisyydet ja varmuuden. Asteikko palauttaa tason. Kyllä tai ei -kysymys palauttaa luvun nollan ja yhden väliltä.
Kolme kysymystyyppiä, kolme tyypitettyä vastausta. Vastausjoukko kiinnitetään pyynnössä, joten tuloste jäsentyy aina. Arvot ovat havainnollistavia.

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)
Muoto on TypeSafen SDK-dokumentaatiosta. Emme ole ajaneet Jeviä tuotannossa: pääsy on jonotuslistan takana.

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ä.

Kaavio päätöskaskadista. Saapuva viesti menee säännöille, sitten pienelle luokittelumallille ja sitten pienelle kielimallille. Varmat vastaukset lähtevät jokaiselta portaalta tyypitettynä päätöksenä. Vain ratkaisemattomat kysymykset putoavat seuraavalle portaalle, ja yhä epävarmat menevät ihmiselle. Iso malli herää vasta, kun on jotain kirjoitettavaa.
Kaskadi. Jokainen porras näkee vain ne kysymykset, joita edellinen ei ratkaissut. Kustannukset ovat omia arvioitamme päätöstä kohti lyhyillä viesteillä.
  • 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.

Kaavio siitä, miten halpa porras ansaitsee luottamuksen. Vaihe yksi, varjoajo: porras vastaa, mutta vastausta ei käytetä. Vaihe kaksi, näyttö kysymys kerrallaan: vähintään 200 verrattua päätöstä ja 97 prosentin osuvuus silloin kun se toimisi. Vaihe kolme, käytössä, ja kielimalli ajaa yhä 5 prosentin otosta. Katkoviiva näyttää automaattisen pudotuksen, kun osuvuus laskee alle 93 prosentin viimeisestä sadasta päätöksestä.
Hitaasti ylös, heti alas. Ylennys tehdään kysymys kerrallaan ja vaatii näyttöä. Pudotus on automaattinen.
  1. Varjoajo. Porras vastaa, eikä vastausta käytetä mihinkään. Vanha polku päättää yhä, ja sen tulos tallennetaan vertailukohdaksi.
  2. 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.
  3. Käytössä, otoksen kanssa. Halpaa vastausta käytetään, kun se on varma. Kielimalli ajaa yhä 5 %:n otosta jatkuvana vertailukohtana.
  4. 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

PorrasHinta per päätösViiveVastaa kun
SäännötIlmainenMikrosekuntejaVastaus on tosiasia: lähettäjä, lomakkeen lähde, sanakirjaosuma
Pieni luokittelumalliNoin 0,00002 €70–500 ms (toimittajan luku)Vapaata tekstiä, suljettu kysymys
Pieni kielimalliNoin 0,001 €SekuntejaHalvemmat portaat eivät olleet varmoja
Iso malliSenttejäSekunneista minuutteihinOn 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

  1. Listaa sovelluksesi jokainen kielimallikutsu. Merkitse ne, jotka ovat suljettuja kysymyksiä.
  2. Kysy jokaisen kohdalla, minkä sääntö ratkaisisi ilmaiseksi: lähettäjä, lähde, hakutaulu, säännöllinen lauseke.
  3. Kirjoita loput tyypitetyiksi kysymyksiksi: avain, kiinteä vastausjoukko, kynnys.
  4. Kirjaa jokainen päätös lokiin sen rinnalle, mitä nykyinen polku päätti.
  5. Aja varjossa. Ylennä kysymys kerrallaan näytön perusteella. Pudota automaattisesti.
  6. 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.

Lähteet

Risto Holappa
Kirjoittaja
Founder & Technical Lead, Viher IT Oy

Risto Holappa on Viher IT Oy:n ja Willit Oy:n perustaja. Willit on lähiruoan markkinapaikka, jonka kokemuksiin tämä kirjoitus perustuu. Hän rakentaa Oulusta käsin agenttisia ohjelmistoja, joiden alla on luokittelupalvelut.

Agentti, joka on liian hidas tai liian kallis ajaa?

Yhteys
Sijainti
Oulu, Finland