ArtikelTestning

En seed låser inte AI-svaret – bygg ett reproducerbarhetstest

Två till synes identiska AI-anrop ger olika svar. Innan du skyller på modellen behöver du bevisa att requesten verkligen var identisk, att din kod inte ändrade resultatet och hur stor variationen faktiskt är.

Senast granskad: 12 augusti 2026

En mörk simiansk neuralsilhuett betraktar tre cirkulära avtryck ovanför en acid-grön mätlinje.
Tre likadana avtryck går att jämföra först när de läggs mot samma referens.

Kortfattat

Spara requesten som kanonisk JSON och beräkna en request-hash. Kör samma anrop flera gånger. Spara både exakt och försiktigt normaliserad output-hash. Om request-hashen ändras ligger variationen före modellen. Om råsvaret är stabilt men appresultatet ändras ligger felet efter modellen. Först när båda är stabila indata och appkod, men modellsvaret varierar, har du mätt modellvariation.

Seed är en försöksparameter, inte ett lås

En seed kan göra ett experiment mer kontrollerat när den valda modellen och ändpunkten stöder parametern. Den gör däremot inte ett externt AI-API till en ren funktion där samma indata alltid ger samma byte. Stödet skiljer sig dessutom mellan leverantörer och modeller.

Google dokumenterar seed i Gemini API:s GenerationConfig. OpenAI:s aktuella Chat-referens beskriver kopplingen mellan seed och ett fingeravtryck för backendkonfiguration, men markerar fingeravtrycksfältet som föråldrat. Anthropic anger att temperatur noll inte ger full determinism; deras aktuella dokumentation säger också att Claude 4.7 och senare inte stöder temperature, top_p eller top_k. Slutsatsen är praktisk: kontrollera stöd för just den modell och API-version du kör, men bygg inte testets sanningsvillkor på att ett visst reglage finns.

Undvik ett falskt grönt test

Ett test som kräver identisk prosa kan falla trots att svaret är lika användbart. Ett test som accepterar vilken välformad text som helst kan gå igenom när sakuppgifter saknas. Hasharna mäter variation; separata assertions måste mäta om verksamhetskravet fortfarande uppfylls.


Frys den request som faktiskt skickas

Hasha inte det promptutkast som ligger i testfilen. Hasha det fullständiga objektet direkt före serialisering: modellidentifierare, meddelanden, systeminstruktion, verktygsdefinitioner om sådana ingår i just ditt prov, outputformat och alla uttryckliga genereringsinställningar. Sätt även sådant som annars får ett leverantörsstandardvärde. Då blir ett ändrat standardvärde inte en osynlig förändring.

Vanlig JSON.stringify räcker inte som portabel identitet om objekt kan byggas med olika nyckelordning. RFC 8785 beskriver JSON Canonicalization Scheme, JCS: en invariant JSON-representation med bland annat deterministisk sortering, bestämd serialisering och UTF-8. Använd en granskad JCS-implementation i produktion. Den förenklade funktionen nedan visar själva testkontraktet, men utelämnar RFC:ens viktiga kantfall för tal, Unicode och ogiltig I-JSON.

import { createHash } from 'node:crypto';

function demoCanonicalJson(value) {
  if (Array.isArray(value)) {
    return `[${value.map(demoCanonicalJson).join(',')}]`;
  }
  if (value && typeof value === 'object') {
    return `{${Object.keys(value).sort().map(key =>
      `${JSON.stringify(key)}:${demoCanonicalJson(value[key])}`
    ).join(',')}}`;
  }
  return JSON.stringify(value);
}

function sha256(text) {
  return createHash('sha256').update(text, 'utf8').digest('hex');
}

const canonicalRequest = demoCanonicalJson(request);
const requestHash = sha256(canonicalRequest);

Prova kanoniseringen innan modellen anropas: bygg två objekt med omvänd nyckelordning och kräv samma hash. Ändra sedan ett enda prompttecken eller modellfält och kräv olika hash. Om båda proven inte går igenom kan experimentet inte skilja stabila indata från ett fel i mätningen.


Spara två output-hashar, inte en

Den exakta output-hashen svarar på frågan ”fick vi samma byte?”. En normaliserad hash kan svara på en snävare, praktisk fråga, exempelvis om enda skillnaden är radslut eller avslutande blanksteg. Spara båda. Annars går det inte att se om normaliseringen har dolt en verklig skillnad.

function normalizeText(text) {
  return text
    .replace(/\r\n/g, '\n')
    .split('\n')
    .map(line => line.replace(/[\t ]+$/g, ''))
    .join('\n')
    .trimEnd();
}

function fingerprintOutput(text) {
  return {
    exact: sha256(text),
    normalized: sha256(normalizeText(text))
  };
}

Normalisera bara skillnader som kontraktet uttryckligen säger saknar betydelse. Sortera inte listor om ordningen är relevant. Gör inte text till gemener om versaler kan bära information. För strukturerad JSON bör svaret först valideras mot schemat och därefter kanoniseras som JSON; att hasha den råa JSON-strängen blandar ihop nyckelordning med innehåll.

Behåll läsbara provdata


Kör en liten matris som isolerar variationskällan

Ett enstaka lyckat dubbelanrop säger lite. Kör exempelvis tio anrop i samma process med exakt samma request-hash. Starta därefter om processen och kör tio till utan att ändra kod eller konfiguration. Upprepa slutligen mot nästa avsedda modellversion eller efter en kontrollerad klientuppgradering. Tio är inget statistiskt bevis, men tillräckligt för att avslöja grov variation och brister i selen; öka antalet när risken kräver snävare intervall.

async function runSample({ request, count, invokeModel }) {
  const requestHash = sha256(demoCanonicalJson(request));
  const runs = [];

  for (let index = 0; index < count; index += 1) {
    const text = await invokeModel(request);
    runs.push({
      index,
      requestHash,
      ...fingerprintOutput(text),
      contractOk: validateBusinessContract(text)
    });
  }
  return runs;
}

Räkna antalet unika exakta hashvärden, unika normaliserade hashvärden och underkända verksamhetsassertions. Redovisa exempelvis ”10 körningar, 4 normaliserade varianter, 10 av 10 godkända kontrakt” i stället för ”reproducerbart”. Det första går att jämföra över tid; det andra döljer vilken tolerans du faktiskt accepterade.


Lokalisera skillnaden i fyra steg

Request-hashen skiljer sig. Felet ligger före anropet. Jämför den kanoniska requesten fält för fält: dynamiskt datum, osorterad kontext, ändrad mall eller ett utelämnat standardvärde är vanliga orsaker.
Requesten är samma, råsvaret skiljer sig. Du har mätt variation i genereringen eller dess körmiljö. Kontrollera först dokumenterat parameterstöd och modellidentifieraren, och kvantifiera sedan hur ofta kontraktet påverkas.
Råsvaret är samma, appresultatet skiljer sig. Felet ligger efter anropet: parser, samtidighet, lokal sortering, tidsberoende eller annan efterbehandling. Lägg en separat hash efter varje deterministiskt transformationssteg.
Hasharna skiljer sig men kontraktet håller. Bestäm om ordalydelsen verkligen behöver låsas. För sammanfattning kan semantiska assertions vara rätt; för maskinläsbar data bör schema, typer och domänregler styra.

Den här indelningen är avsiktligt smal. Behöver du felsöka varför en AI-byggd applikation ger olika resultat från en miljö till en annan, kombinera den med kontraktstest av AI-integrationen. Där ägs frågan om vad som får ändras när modellen uppdateras.


Testa beteendet ovanpå hashmätningen

Anta att uppgiften är att returnera tre produktkoder i JSON. Hashen avslöjar att ordningen skiftar, men verksamhetstestet bör kontrollera separat att svaret är giltig JSON, att exakt tre unika koder finns, att varje kod finns i det tillåtna underlaget och att ingen förklarande prosa ligger utanför objektet. Om ordningen saknar betydelse kan testet jämföra mängder. Om första koden är den högst rankade får ordningen inte normaliseras bort.

För fri text kan du dela kontraktet i hårda och mjuka villkor. Hårda villkor kan vara obligatoriska fält, förbjudna påståenden och citerade käll-ID:n. Mjuka villkor kan mätas med ett separat, versionsbundet bedömningsprov och stickprov av människor. Låt aldrig en likhetspoäng ensam avgöra om en säkerhetskritisk uppgift är korrekt.

Kör också ett negativt kontrollprov. Ta bort en nödvändig faktarad ur underlaget eller ändra en tillåten produktkod. Minst en relevant assertion ska falla. Om bara output-hashen ändras medan testet fortfarande är grönt har selen bevisat variation, inte kvalitet.


En releasegrind som tål variation

Indata har identitet. Samma försök ger samma JCS-baserade request-hash; ett ändrat fält ger en ny.
Normaliseringen är versionssatt. Det framgår exakt vilka skillnader den får dölja, och råhashen sparas bredvid.
Variationen är kvantifierad. Rapporten visar antal körningar, unika svar och andel godkända kontrakt.
Kontrollen kan misslyckas. Ett avsiktligt förstört underlag eller svar fäller rätt assertion.
Modellbyte jämförs med baslinje. Samma frysta korpus körs före och efter bytet; grinden bygger på verksamhetskraven, inte identisk formulering.

Om du vill kontrollera att själva assertions verkligen hittar fel, fortsätt med mutationstestning av AI-genererade tester. Byt ett förväntat fält, kasta en post eller gör en förbjuden formulering tillåten och verifiera att rätt del av testet reagerar.


Källor

JSON-kanoniseringen följer principerna i RFC 8785: JSON Canonicalization Scheme. Leverantörsuppgifterna är kontrollerade mot OpenAI Chat API Reference, Gemini API: GenerationConfig och Anthropics Using the Messages API samt Messages API-referens. Källor kontrollerade 12 augusti 2026.