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.
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
- request-hash och den kanoniska requesten, med hemligheter borttagna före lagring,
- exakt och normaliserad output-hash samt själva svaret i en skyddad testmiljö,
- modellidentifierare och de parametrar som faktiskt accepterades,
- testsvitens kodversion och normaliseringsversion,
- resultatet från verksamhetsassertions, inte bara textens hash.
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
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
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.
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.