Det tredje af de kiilske principper for AI er igen et spørgsmål.
Kan I bagefter vise mig, hvorfor systemet traf en bestemt beslutning?
Læg mærke til ordet systemet. Jeg spørger ikke, hvad modellen svarede. Modellen er én del af beslutningen. Reglerne, dataene og det menneske, der sagde god for det, er resten.
I det forrige indlæg byggede jeg en lille screening af jobopslag, hvor regler og model svarer på det samme spørgsmål, og et menneske kigger, når de er uenige. Den virker fint. Men spørg den om en uge, hvorfor et bestemt opslag blev sprunget over, og den har ikke noget svar.
En log er ikke et spor #
De fleste AI-systemer, jeg ser, logger prompts og svar. Det er godt, men det er en udskrift af en samtale. Den fortæller, hvad der blev sagt, og ikke hvad der skete bagefter.
Et spor følger beslutningen. For hver sag vil jeg kunne se:
- hvilken sag det var
- hvilke data der gik ind
- hvilke regler der slog til
- hvad modellen fik at vide, og hvad den svarede
- hvad udfaldet blev
- hvem der besluttede det, og hvorfor
Det lyder som bureaukrati, indtil en kunde klager eller revisoren ringer. Så er det det eneste, der tæller.
Hos mig var det sporet, der afslørede, at en automatisk påmindelse var på vej til at rykke mig for noget, jeg allerede havde gjort. Modellen havde ikke gjort noget forkert. Uden sporet havde jeg kun kunnet se, at påmindelsen var sendt. Med sporet kunne jeg se, hvad den byggede på.
Tre regler for et spor #
Jeg holder mig til tre regler.
Skriv, før I handler. Beslutningen skrives i sporet, før der sker noget med den. Hvis den ikke kan skrives, sker der ingenting. Et system, der handler uden at kunne huske det, er værre end et system, der står stille.
Tilføj, ret aldrig. Sporet er en liste af hændelser. Ændrer nogen mening, er det en ny hændelse. Det gamle bliver stående.
Mennesker er også hændelser. Når et menneske godkender eller afviser, skrives det ind på samme måde som maskinens beslutninger, med navn og begrundelse. Uden begrundelse ingen beslutning.
Et eksempel: screeningen får et spor #
Lad os bygge videre på screen.nu fra det forrige indlæg. Hver sag får et kort id, og hver beslutning bliver én linje JSON i filen decisions.jsonl.
use screen.nu [assert-not-empty parse-verdict route]
# Rules: return the keywords that matched, so the trail can say which rule fired
export def rule-hits [keywords: list<string>]: record -> list<string> {
let text = $"($in.title) ($in.description)" | str lowercase
$keywords | where {|k| $text | str contains ($k | str lowercase) }
}
# Judgement: keep the prompt and the raw answer, not just the verdict
export def model-call [profile: string]: record -> record {
let ad = $in
let prompt = [
"Is this job ad relevant for the profile below?"
'Answer only with JSON: {"relevant": true or false, "reason": "one short sentence"}'
""
$"Profile: ($profile)"
$"Title: ($ad.title)"
$"Ad: ($ad.description)"
] | str join (char newline)
let answer = ^aichat $prompt
{prompt: $prompt, answer: $answer, verdict: ($answer | parse-verdict)}
}
# One event per line. If the line cannot be written, the run stops before anything happens.
export def log-event [trail: path]: record -> record {
let fields = $in
let event = {at: (date now | format date "%+")} | merge $fields
$"($event | to json --raw)\n" | save --append $trail
$event
}
export def screen-ads [
profile: string
keywords: list<string>
--trail: path = "decisions.jsonl"
]: list<record> -> table {
assert-not-empty "The job feed"
| each {|ad|
let case = $ad | to json --raw | hash sha256 | str substring 0..7
let hits = $ad | rule-hits $keywords
let call = $ad | model-call $profile
let route = {rule: ($hits | is-not-empty), model: $call.verdict.relevant} | route
{
case: $case
event: "screened"
input: $ad
rules_fired: $hits
prompt: $call.prompt
answer: $call.answer
outcome: $route
by: "system"
why: $call.verdict.reason
} | log-event $trail | ignore
{case: $case, title: $ad.title, route: $route}
}
}
# A human decision is an event too: who decided, what and why
export def decide [
case: string
outcome: string
why: string
--trail: path = "decisions.jsonl"
] {
if ($why | str trim | is-empty) {
error make {msg: "A decision needs a reason", help: "Write one sentence your future self will understand."}
}
{case: $case, event: "decided", outcome: $outcome, by: $env.USER, why: $why}
| log-event $trail
| ignore
}
# Everything that happened to one case, in order
export def why [case: string, --trail: path = "decisions.jsonl"]: nothing -> table {
open --raw $trail | from json --objects | where case == $case
}Gem den som trail.nu ved siden af screen.nu. To ting er værd at bemærke.
rule-hits returnerer de søgeord, der ramte, og ikke bare sandt eller falsk. Så kan sporet sige, hvilken regel der slog til.
model-call gemmer hele prompten og det rå svar. Hvis jeg senere retter prompten, kan jeg stadig se, hvad modellen fik at vide dengang.
Vi kører de samme tre opslag som sidst:
use trail.nu [screen-ads decide why]
[[title description];
["Nushell developer" "Automate data pipelines"]
["Sommelier" "Pair wine with food"]
["Prompt trainer" "Teach staff to use LLMs"]
]
| screen-ads "AI and automation" [nushell automation]╭───┬──────────┬───────────────────┬───────╮
│ # │ case │ title │ route │
├───┼──────────┼───────────────────┼───────┤
│ 0 │ 0adbd15a │ Nushell developer │ apply │
│ 1 │ 9981e976 │ Sommelier │ skip │
│ 2 │ 58572c95 │ Prompt trainer │ human │
╰───┴──────────┴───────────────────┴───────╯Prompt trainer-opslaget ryger igen til mig. Jeg kigger på det og beslutter mig:
decide 58572c95 apply "Close enough to what I do."Og en uge senere, når jeg har glemt alt om det:
why 58572c95 | select event outcome by why╭───┬──────────┬─────────┬────────┬───────────────────────────────────╮
│ # │ event │ outcome │ by │ why │
├───┼──────────┼─────────┼────────┼───────────────────────────────────┤
│ 0 │ screened │ human │ system │ LLM training is close to AI work. │
│ 1 │ decided │ apply │ lk │ Close enough to what I do. │
╰───┴──────────┴─────────┴────────┴───────────────────────────────────╯Der står hele historien. Systemet var i tvivl, fordi modellen syntes, opslaget var relevant, og mine søgeord ikke ramte. Så besluttede jeg, at det skulle med, og skrev hvorfor. Bag hver linje ligger også opslagets tekst, prompten og modellens rå svar, hvis jeg vil grave dybere.
Se det fejle, igen #
Det forrige indlæg sluttede med, at et tjek, I kun har set bestå, er et tjek, I ikke ved, om virker. Det gælder også sporet. Så jeg tester, at det stopper, når det skal:
use std/assert
use trail.nu [screen-ads log-event decide why]
# A trail that cannot be written stops the run
assert error { {case: "x"} | log-event /no/such/dir/decisions.jsonl }
# A decision without a reason is refused
assert error { decide "x" "apply" " " --trail test.jsonl }
# Every screened ad leaves an event that can be found again
let screened = [{title: "Sommelier", description: "Pair wine with food"}]
| screen-ads "AI" [nushell] --trail test.jsonl
assert equal (why $screened.0.case --trail test.jsonl | get event) ["screened"]Den første test er den vigtigste. Hvis sporet ikke kan skrives, får I en fejl, og intet opslag bliver sendt videre. Den anden sørger for, at »fordi« aldrig er tomt.
Loven og ledelsen #
EU’s AI-forordning kræver logning for højrisikosystemer. Men lov eller ej, så kan en ledelse ikke tage ansvar for noget, den ikke kan se. Og det er lettere at bygge sporet fra starten end at skrue det på bagefter, når nogen spørger.
For at opsummere #
Når I bygger noget med en model i, så sørg for, at I bagefter kan svare på, hvorfor systemet gjorde, som det gjorde:
- Følg beslutningen. Gem sagen, input, reglerne, prompten, svaret, udfaldet og hvem der besluttede.
- Skriv, før I handler. Kan beslutningen ikke skrives, sker der ingenting.
- Tilføj, ret aldrig. Nye meninger er nye hændelser.
- Kræv en begrundelse. Også når det er et menneske, der beslutter.
Næste indlæg handler om det fjerde og sidste princip: Hvilke handlinger kræver et menneske, og hvorfor lige dem?
Hvis I vil have hjælp til at give jeres egne systemer et spor, så skriv eller ring.