Om att bygga rixdagen.se och andra AI-bottar
Jag sitter på ett tåg till Sundsvall. Det är mars 2023, grått utanför fönstret, och jag försöker för Vetenskapsradions räkning förstå vad som sagts i Riksdagen om de ruttnande träfiberbankarna som pappersindustrin lämnat kvar på botten av bl.a. Östersjön. Trots att all data finns tillgänglig på riksdagen.se så är det i princip omöjligt att förstå. Jag börjar därför skriva ett script för att bygga en sökbar databas.
Två veckor senare, på ett tåg till Gräv i Karlstad, registrerar jag den bedrägeriosande domänen rixdagen.se, en Streamlit-baserad sidan där man kan söka efter anföranden.
Nu, inför riksdagsvalet i höst, har jag uppgraderat sidan, och tänkte berätta lite om det.
Grunden är Riksdagens öppna data:
- 428 000 anföranden, från 1993.
- 126 000 motioner och 453 000 yrkanden i dem.
- 2 086 ledamöter, 17 800 debatter.
Varje tal och varje motion har dessutom körts genom en AI som skrivit en kort sammanfattning, listat argumenten och satt ämnestaggar. Det är ett extra lager metadata som gör materialet sökbart, men chattbotten citerar aldrig sammanfattningen utan alltid originalet.
Tre sätt att leta
Att söka i en halv miljon tal innebär tre olika problem, och sajten löser dem på tre olika sätt.
Textsökning. Den vanliga varianten: du skriver "sjukvård" och får träffar där ordet står. Databasen (PostgreSQL) kan svensk ordböjning, så "sjukvård" hittar även "sjukvårdens". Det här är den transparenta metoden — du ser exakt vad som matchade och varför.
Semantisk sökning. Varje textavsnitt har omvandlats till en lång rad siffror som representerar vad texten betyder - embeddings. Två texter som handlar om samma sak hamnar nära varandra i den sifferrymden, även om de inte delar ett enda ord. Det är så en sökning på "nyanlända" också kan hitta tal som säger "invandrare". Det ligger ungefär 4,4 miljoner sådana textavsnitt i databasen.
Direkta databasfrågor. För "hur många gånger …", "vem har oftast oftast …", och andra mer statistikorienterade frågor behövs ingen textsökning alls, utan snarare matematik. AI:n skriver sådana frågor till databasen själv och tolkar svaren - ungefär på samma sätt som du kan skriva frågorna i fliken Sök och få viss statistik presenterad.
En AI-agent som får söka själv
Chatten är inte en chatt som fått en massa text inklistrad i sig. Det är en agent som får jobba: den läser din fråga, bestämmer sig för en sökning, tittar på resultatet, bestämmer sig för nästa sökning, och håller på i upp till tjugo varv innan den ger dig ett svar. Den har sex verktyg: de tre sökvägarna ovan, plus möjligheten att hämta en hel debatt, hämta enskilda tal och slå upp en källa den redan sett.
Medan agenten söker omkring så berättar den lite av vad som händer, ex. om den stött på något intressant. Det är en separat, mindre AI som liksom tittar över axeln på den första och delar sina observationer med dig. Den kan inte söka, inte svara, inte påverka. Bara berätta. På så sätt hoppas jag att väntetiden kan vara begriplig och värdefull istället för bara lång.
Den viktigaste utgångspunkten för en AI-chatt
När jag designat den här chatten finns det en helt grundläggande princip:
Lita aldrig på att modellen citerar rätt. Kontrollera det efteråt.
Varje sökträff som AI:n får se registreras med ett id. När svaret är skrivet läser servern igenom texten, plockar ut alla källhänvisningar och jämför dem mot registret. Hittar den ett id som inte kommer från en verklig träff stryks hänvisningen — och händelsen loggas, så jag kan se hur ofta modellen försöker. Listan med källor under svaret genereras sedan av servern, inte av AI:n, och innehåller bara de källor som faktiskt citerats i texten.
Effekten är att svarstext och listan på källor stämmer mot varandra och att en påhittad källhänvisning inte blir en fotnot (har du använt ChatGPT vet du att du ibland får URL:er som inte går till något, mycket irriterande). Det skyddar inte mot att modellen kopplar ett* riktig*t id till fel påstående, att den tolkat en källa på fel sätt eller helt enkelt hittat på fakta – så dubbelkolla ändå – men du slipper påhittade källor.
Allt körs hemma
Sajten ligger på min egen server. PostgreSQL i en Docker-container, ett Python-API (FastAPI), ett React-gränssnitt, nginx framför alltihop, och AI-modeller som körs lokalt på ett gäng grafikkort jag köpte av en bitcoin-minare när elpriserna var höga. Ingen data lämnar källaren om du inte själv väljer att använda en annan AI-modell. Chattar sparas i sju dagar och sedan är de borta.
Det finns en kostnad för anarkin: de lokala AI-modellen är relativt små. De gör bra ifrån sig på någorlunda avgränsade frågor, men med komplicerad flerstegslogik tappar de ibland bort sig. Därför går det också att koppla in sitt eget API-konto hos en extern leverantör. Du skaffar då en API-nyckeln och lägger in den. Den sparas då i din webbläsare om du inte är inloggad och når aldrig min server, medan den för en inloggad användare krypteras i webbläsaren innan den sparas, så att jag inte kan läsa den ens om jag ville. Vill du inte skicka runt dina frågor kors och tvärs över jorden kan jag tipsa om svenska tjänsten berget.ai.
Tre misstag jag gjort
Hur enkelt är det då att göra ett sånt här verktyg? Inte jätteenkelt visade det sig, och det har varit en del dikeskörningar.
Jag raderade 425 000 tal. Under flyttningen mellan två databaser körde skriptet enligt principen "kopiera, verifiera, radera källan". En pågående backup störde ut kopieringen så att bara 117 poster kom över — men verifieringen tyckte att det såg bra ut och raderade originalet. Räddningen var att rådatan låg kvar som filer på disken, och att de dyraste sifferberäkningarna låg i en annan tabell som klarade sig. Skriptet är nu omskrivet och vägrar radera tal om antalet ser orimligt lågt ut.
Jag tränade en egen modell i onödan. Ett tag hade jag en databas med ett lite obskyrt frågespråk (AQL, används för ArangoDB), och jag tränade därför en liten modell på att översätta vanlig SQL till det språket. Efter tre dagar och tre träningsrundor var modellen fortfarande värdelös på att skriva AQL, och jag gick istället över till PostgreSQL så att mina modeller kan använda sitt SQL, ett språk de älskar!
AI:n kunde inte se vad den själv nyss gjort. I flera månader sökte agenten ibland på exakt samma sak två gånger i rad. Orsaken var ett formatfel i hur samtalet byggdes upp: jag sparade resultaten av varje sökning, men inte modellens egna beslut om vad den skulle söka på. Den såg alltså en hög svar utan frågor och kunde inte resonera "det där testade jag redan, nu provar jag smalare". En enkel lösning och det som är roligast med att göra sånt här nu när själva programmeringen sköt av AI: att klura ut hur systemet ska se ut, inte koden.
Om att bygga det här med AI
För det är så det ser ut nu: allt färre skriver egen kod, inte heller jag. Jag saknar de sena kvällarna och gemenskapen på StackOverflow, men det går onekligen snabbare att göra bättre grejer nu.
Några lärdomar:
- Skriv planen först, i en fil. De större funktionerna började som ett dokument som beskriver vad som ska hända och varför. Rita gärna upp det, antingen på papper som du fotar och lägger in, eller digitalt i ex. Excalidraw. Det ger dig möjlighet att förstå vad du gör, och något för AI:n att förhålla sig till. När du tror att du är klar, gå igenom allting en gång till, gärna med en AI som medhjälpare – det är oändligt mycket billigare att upptäcka ett tankefel i en text än i tjugotusen rader kod.
- Ha en instruktionsfil i projektet. I mitt fall står det exempelvis att det viktigaste är att svaren är grundade i fakta och att det är transparent för användaren. AI:n läser den filen varje gång och är rätt så duktig på att förhålla sig till det som står.
- Logga och utvärdera. Sajten sparar alla chattbottens snedsteg i en tabell: påhittade id:n, upprepade sökningar, trasiga verktygsanrop. Det är inte fel som kraschar något, utan snarare mönster i beteendet som gör att jag bättre förstår modellen och vad den behöver.
Om du använder rixdagen.se så berätta gärna vad du tänker om den och vad som saknas, och vet du någon som skulle kunna få användning av den så dela gärna var du nu delar saker!