Bygning af en Privacy-First AI Kundesupport Agent med LanceDB, Ollama og Node.js
Note
Dette dokument dækker vores rejse med at bygge en selvhostet AI support agent. Vi skrev om lignende udfordringer i vores Email Startup Graveyard blogindlæg. Vi overvejede ærligt at skrive en opfølger kaldet "AI Startup Graveyard", men måske må vi vente et år eller to, indtil AI-boblen potentielt brister(?). For nu er dette vores hjerneudladning af, hvad der virkede, hvad der ikke gjorde, og hvorfor vi gjorde det på denne måde.
Sådan byggede vi vores egen AI kundesupport agent. Vi gjorde det på den hårde måde: selvhostet, privacy-first og fuldstændig under vores kontrol. Hvorfor? Fordi vi ikke stoler på tredjepartstjenester med vores kunders data. Det er et GDPR- og DPA-krav, og det er det rigtige at gøre.
Dette var ikke et sjovt weekendprojekt. Det var en måneds lang rejse gennem ødelagte afhængigheder, vildledende dokumentation og den generelle kaos i open source AI-økosystemet i 2025. Dette dokument er en registrering af, hvad vi byggede, hvorfor vi byggede det, og de forhindringer vi stødte på undervejs.
Kunde Fordele: AI-Forstærket Menneskelig Support
Vores AI-system erstatter ikke vores supportteam – det gør dem bedre. Her er hvad det betyder for dig:
Hurtigere, Mere Præcise Svar
Menneske-i-Loop: Hvert AI-genereret udkast bliver gennemgået, redigeret og kurateret af vores menneskelige supportteam, før det sendes til dig. AI’en håndterer den indledende research og udkast, hvilket frigør vores team til at fokusere på kvalitetskontrol og personalisering.
Trænet på Menneskelig Ekspertise: AI’en lærer fra:
- Vores håndskrevne vidensbase og dokumentation
- Menneskeskrevne blogindlæg og tutorials
- Vores omfattende FAQ (skrevet af mennesker)
- Tidligere kundesamtaler (alle håndteret af rigtige mennesker)
Du får svar, der er baseret på års menneskelig ekspertise, blot leveret hurtigere.
Konsistens Uden Udbrændthed
Vores lille team håndterer hundredvis af supporthenvendelser dagligt, hver med forskellig teknisk viden og mental kontekstskift:
- Faktureringsspørgsmål kræver kendskab til finansielle systemer
- DNS-problemer kræver netværksekspertise
- API-integration kræver programmeringsviden
- Sikkerhedsrapporter kræver sårbarhedsvurdering
Uden AI-assistance fører denne konstante kontekstskift til:
- Langsommere svartider
- Menneskelige fejl på grund af træthed
- Inkonsekvent svar kvalitet
- Teamudbrændthed
Med AI-forstærkning:
- Svarer teamet hurtigere (AI laver udkast på sekunder)
- Laver færre fejl (AI fanger almindelige fejl)
- Opretholder ensartet kvalitet (AI refererer til samme vidensbase hver gang)
- Forbliver frisk og fokuseret (mindre tid på research, mere tid på at hjælpe)
Hvad Du Får
✅ Hastighed: AI laver udkast til svar på sekunder, mennesker gennemgår og sender inden for få minutter
✅ Præcision: Svar baseret på vores faktiske dokumentation og tidligere løsninger
✅ Konsistens: Samme høje kvalitet uanset om det er kl. 9 eller 21
✅ Menneskelig berøring: Hvert svar gennemgås og personaliseres af vores team
✅ Ingen hallucinationer: AI bruger kun vores verificerede vidensbase, ikke generiske internetdata
Note
Du taler altid med mennesker. AI’en er en forskningsassistent, der hjælper vores team med at finde det rigtige svar hurtigere. Tænk på det som en bibliotekar, der øjeblikkeligt finder den relevante bog – men et menneske læser den stadig og forklarer den for dig.
En Personlig Refleksion: To Årtiers Slid
Før vi dykker ned i de tekniske detaljer, en personlig note. Jeg har været i gang i næsten to årtier. De endeløse timer ved tastaturet, den utrættelige jagt på en løsning, det dybe, fokuserede slid – det er virkeligheden ved at bygge noget meningsfuldt. Det er en virkelighed, der ofte overses i hypen omkring ny teknologi.
Den nylige eksplosion af AI har været særligt frustrerende. Vi bliver solgt en drøm om automatisering, om AI-assistenter, der vil skrive vores kode og løse vores problemer. Virkeligheden? Outputtet er ofte skralde-kode, der kræver mere tid at rette, end det ville have taget at skrive fra bunden. Løftet om at gøre vores liv lettere er falsk. Det er en distraktion fra det hårde, nødvendige arbejde med at bygge.
Og så er der catch-22 ved at bidrage til open source. Du er allerede spredt tyndt, udmattet af sliddet. Du bruger en AI til at hjælpe dig med at skrive en detaljeret, velstruktureret fejlrapport, i håb om at gøre det lettere for vedligeholdere at forstå og rette problemet. Og hvad sker der? Du bliver irettesat. Dit bidrag afvises som "off-topic" eller lav indsats, som vi så i en nylig Node.js GitHub issue. Det er et slag i ansigtet på seniorudviklere, der bare prøver at hjælpe.
Dette er virkeligheden i det økosystem, vi arbejder i. Det handler ikke kun om ødelagte værktøjer; det handler om en kultur, der ofte ikke respekterer tiden og indsatsen fra sine bidragydere. Dette indlæg er en krønike over den virkelighed. Det er en historie om værktøjerne, ja, men det er også om den menneskelige pris ved at bygge i et ødelagt økosystem, der trods alle løfter er fundamentalt ødelagt.
Hvorfor Privatliv Er Vigtigt
Vores tekniske whitepaper dækker vores privatlivsfilosofi i dybden. Den korte version: vi sender aldrig kundedata til tredjepart. Aldrig. Det betyder ingen OpenAI, ingen Anthropic, ingen cloud-hostede vektordatabaser. Alt kører lokalt på vores infrastruktur. Dette er ikke til forhandling for GDPR-overholdelse og vores DPA-forpligtelser.
Omkostningsanalyse: Cloud AI vs Selvhostet
Før vi dykker ned i den tekniske implementering, lad os tale om, hvorfor selvhosting betyder noget fra et omkostningsperspektiv. Prisstrukturerne for cloud AI-tjenester gør dem urimeligt dyre for brugsscenarier med højt volumen som kundesupport.
Sammenligning af Cloud AI-tjenester
| Service | Udbyder | Embedding-omkostning | LLM-omkostning (Input) | LLM-omkostning (Output) | Privatlivspolitik | GDPR/DPA | Hosting | Data Deling |
|---|---|---|---|---|---|---|---|---|
| OpenAI | OpenAI (US) | $0.02-0.13/1M tokens | $0.15-20/1M tokens | $0.60-80/1M tokens | Link | Begrænset DPA | Azure (US) | Ja (træning) |
| Claude | Anthropic (US) | N/A | $3-20/1M tokens | $15-80/1M tokens | Link | Begrænset DPA | AWS/GCP (US) | Nej (påstået) |
| Gemini | Google (US) | $0.15/1M tokens | $0.30-1.00/1M tokens | $2.50/1M tokens | Link | Begrænset DPA | GCP (US) | Ja (forbedring) |
| DeepSeek | DeepSeek (Kina) | N/A | $0.028-0.28/1M tokens | $0.42/1M tokens | Link | Ukendt | Kina | Ukendt |
| Mistral | Mistral AI (Frankrig) | $0.10/1M tokens | $0.40/1M tokens | $2.00/1M tokens | Link | EU GDPR | EU | Ukendt |
| Selvhostet | Dig | $0 (eksisterende hardware) | $0 (eksisterende hardware) | $0 (eksisterende hardware) | Din politik | Fuld overholdelse | MacBook M5 + cron | Aldrig |
Warning
Bekymringer om datasuverænitet: Amerikanske udbydere (OpenAI, Claude, Gemini) er underlagt CLOUD Act, som giver den amerikanske regering adgang til data. DeepSeek (Kina) opererer under kinesiske datalove. Selvom Mistral (Frankrig) tilbyder EU-hosting og GDPR-overholdelse, er selvhosting stadig den eneste mulighed for fuldstændig datasuverænitet og kontrol.
Omkostningsopdeling: 5GB Vidensbase
Lad os beregne omkostningerne ved at behandle en 5GB vidensbase (typisk for en mellemstor virksomhed med dokumenter, e-mails og supporthistorik).
Antagelser:
- 5GB tekst ≈ 1,25 milliarder tokens (forudsat ~4 tegn/token)
- Initial embedding-generering
- Månedlig genuddannelse (fuld re-embedding)
- 10.000 supportforespørgsler pr. måned
- Gennemsnitlig forespørgsel: 500 tokens input, 300 tokens output Detaljeret omkostningsoversigt:
| Komponent | OpenAI | Claude | Gemini | Selv-hostet |
|---|---|---|---|---|
| Initial embedding (1,25 mia. tokens) | $25,000 | N/A | $187,500 | $0 |
| Månedlige forespørgsler (10K × 800 tokens) | $1,200-16,000 | $2,400-16,000 | $2,400-3,200 | $0 |
| Månedlig gen-træning (1,25 mia. tokens) | $25,000 | N/A | $187,500 | $0 |
| Første års total | $325,200-217,000 | $28,800-192,000 | $2,278,800-2,226,000 | ~$60 (elektricitet) |
| Privatlivsoverholdelse | ❌ Begrænset | ❌ Begrænset | ❌ Begrænset | ✅ Fuld |
| Data suverænitet | ❌ Nej | ❌ Nej | ❌ Nej | ✅ Ja |
Caution
Geminis embedding-omkostninger er katastrofale ved $0,15/1M tokens. En enkelt 5GB vidensbase embedding ville koste $187,500. Det er 37x dyrere end OpenAI og gør det fuldstændig ubrugeligt til produktion.
Selv-hostede hardwareomkostninger
Vores setup kører på eksisterende hardware, vi allerede ejer:
- Hardware: MacBook M5 (allerede ejet til udvikling)
- Yderligere omkostning: $0 (bruger eksisterende hardware)
- Elektricitet: ~$5/måned (estimeret)
- Første års total: ~$60
- Løbende: $60/år
ROI: Selv-hosting har reelt set nul marginalomkostninger, da vi bruger eksisterende udviklingshardware. Systemet kører via cron-jobs uden for spidsbelastningstider.
Dogfooding af vores egen API
En af de vigtigste arkitektoniske beslutninger, vi tog, var at lade alle AI-opgaver bruge Forward Email API direkte. Det er ikke bare god praksis — det er en drivkraft for performanceoptimering.
Hvorfor dogfooding er vigtigt
Når vores AI-opgaver bruger de samme API-endpoints som vores kunder:
- Performance-flaskehalse rammer os først – Vi mærker problemerne før kunderne
- Optimering gavner alle – Forbedringer for vores opgaver forbedrer automatisk kundeoplevelsen
- Test i virkeligheden – Vores opgaver behandler tusindvis af e-mails og leverer kontinuerlig belastningstest
- Kodegenbrug – Samme autentificering, rate limiting, fejlhåndtering og caching-logik
API-brugseksempler
Liste over beskeder (train-from-history.js):
// Bruger GET /v1/messages?folder=INBOX med BasicAuth
// Ekskluderer eml, raw, nodemailer for at reducere svarstørrelse (brug kun IDs)
const response = await axios.get(
`${this.apiBase}/v1/messages`,
{
params: {
folder: 'INBOX',
limit: 100,
eml: false,
raw: false,
nodemailer: false
},
auth: {
username: process.env.FORWARD_EMAIL_ALIAS_USERNAME,
password: process.env.FORWARD_EMAIL_ALIAS_PASSWORD
}
}
);
const messages = response.data;
// Returnerer: [{ id, subject, date, ... }, ...]
// Fuld beskedindhold hentes senere via GET /v1/messages/:id
Hentning af fulde beskeder (forward-email-client.js):
// Bruger GET /v1/messages/:id for at hente fuld besked med råt indhold
const response = await axios.get(
`${this.apiBase}/v1/messages/${messageId}`,
{
auth: {
username: this.aliasUsername,
password: this.aliasPassword
}
}
);
const message = response.data;
// Returnerer: { id, subject, raw, eml, nodemailer: { ... }, ... }
Oprettelse af kladde-svar (process-inbox.js):
// Bruger POST /v1/messages til at oprette kladde-svar
const response = await axios.post(
`${this.apiBase}/v1/messages`,
{
folder: 'Drafts',
subject: `Re: ${originalSubject}`,
to: senderEmail,
text: generatedResponse,
inReplyTo: originalMessageId
},
{
auth: {
username: process.env.FORWARD_EMAIL_ALIAS_USERNAME,
password: process.env.FORWARD_EMAIL_ALIAS_PASSWORD
}
}
);
Performancefordele
Fordi vores AI-job kører på den samme API-infrastruktur:
- Caching-optimeringer gavner både job og kunder
- Ratebegrænsning testes under reel belastning
- Fejlhåndtering er gennemprøvet i praksis
- API-responstider overvåges konstant
- Databaseforespørgsler er optimerede til begge brugsscenarier
- Båndbreddeoptimering – Udelukkelse af
eml,raw,nodemailerved listing reducerer responsstørrelse med ~90%
Når train-from-history.js behandler 1.000 e-mails, foretages der 1.000+ API-kald. Enhver ineffektivitet i API’en bliver straks tydelig. Det tvinger os til at optimere IMAP-adgang, databaseforespørgsler og responsserialisering – forbedringer, der direkte gavner vores kunder.
Eksempel på optimering: Listing af 100 beskeder med fuldt indhold = ~10MB respons. Listing med eml: false, raw: false, nodemailer: false = ~100KB respons (100x mindre).
Krypteringsarkitektur
Vores e-mail-lagring bruger flere lag af kryptering, som AI-jobbene skal dekryptere i realtid til træning.
Lag 1: Mailboks-kryptering (chacha20-poly1305)
Alle IMAP-mailbokse gemmes som SQLite-databaser krypteret med chacha20-poly1305, en kvantesikker krypteringsalgoritme. Dette er beskrevet i vores blogindlæg om kvantesikker krypteret e-mail-service.
Nøgleegenskaber:
- Algoritme: ChaCha20-Poly1305 (AEAD-krypteringsalgoritme)
- Kvantesikker: Modstandsdygtig over for kvantecomputing-angreb
- Lagring: SQLite-databasefiler på disk
- Adgang: Dekrypteres i hukommelsen ved adgang via IMAP/API
Lag 2: Beskedniveau PGP-kryptering
Mange support-e-mails er yderligere krypteret med PGP (OpenPGP-standarden). AI-jobbene skal dekryptere disse for at udtrække indhold til træning.
Dekrypteringsflow:
// 1. API returnerer besked med krypteret råindhold
const message = await forwardEmailClient.getMessage(id);
// 2. Tjek om råindholdet er PGP-krypteret
if (isMessageEncrypted(message.raw)) {
// 3. Dekrypter med vores private nøgle
const decryptedRaw = await pgpDecrypt(message.raw);
// 4. Parse dekrypteret MIME-besked
const parsed = await simpleParser(decryptedRaw);
// 5. Udfyld nodemailer med dekrypteret indhold
message.nodemailer = {
text: parsed.text,
html: parsed.html,
from: parsed.from,
to: parsed.to,
subject: parsed.subject,
date: parsed.date
};
}
PGP-konfiguration:
# Privat nøgle til dekryptering (sti til ASCII-armoreret nøglefil)
GPG_SECURITY_KEY="/path/to/private-key.asc"
# Adgangskode til privat nøgle (hvis krypteret)
GPG_SECURITY_PASSPHRASE="your-passphrase"
pgp-decrypt.js hjælperen:
- Læser den private nøgle fra disk én gang (cachet i hukommelsen)
- Dekrypterer nøglen med adgangskoden
- Bruger den dekrypterede nøgle til al beskeddekryptering
- Understøtter rekursiv dekryptering for indlejrede krypterede beskeder
Hvorfor det betyder noget for træning
Uden korrekt dekryptering ville AI’en træne på krypteret vrøvl:
-----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.10
wcBMA8Z3lHJnFnNUAQgAqK7F8...
-----END PGP MESSAGE-----
Med dekryptering træner AI’en på faktisk indhold:
Subject: Re: Bug Report
Hi John,
Thanks for reporting this issue. I've confirmed the bug
and created a fix in PR #1234...
Lagringssikkerhed
Dekrypteringen sker i hukommelsen under jobudførelse, og det dekrypterede indhold konverteres til embeddings, som derefter gemmes i LanceDB-vektordatabasen på disk.
Hvor dataene befinder sig:
- Vektordatabasen: Gemt på krypterede MacBook M5-arbejdsstationer
- Fysisk sikkerhed: Arbejdsstationerne forbliver hos os hele tiden (ikke i datacentre)
- Diskkryptering: Fuld diskkryptering på alle arbejdsstationer
- Netværkssikkerhed: Firewall og isoleret fra offentlige netværk
Fremtidig datacenter-udrulning: Hvis vi nogensinde flytter til datacenter-hosting, vil serverne have:
- LUKS fuld diskkryptering
- USB-adgang deaktiveret
- Fysiske sikkerhedsforanstaltninger
- Netværksisolation For komplette detaljer om vores sikkerhedspraksis, se vores Sikkerhedsside.
Note
Vektordatabasen indeholder embeddings (matematiske repræsentationer), ikke den oprindelige almindelige tekst. Dog kan embeddings potentielt blive reverse-engineered, hvilket er grunden til, at vi opbevarer dem på krypterede, fysisk sikrede arbejdsstationer.
Lokal lagring er standardpraksis
At gemme embeddings på vores teams arbejdsstationer er ikke anderledes end, hvordan vi allerede håndterer e-mail:
- Thunderbird: Downloader og gemmer fuldt e-mailindhold lokalt i mbox/maildir-filer
- Webmail-klienter: Cache e-maildata i browserlagring og lokale databaser
- IMAP-klienter: Opretholder lokale kopier af beskeder til offline adgang
- Vores AI-system: Gemmer matematiske embeddings (ikke almindelig tekst) i LanceDB
Den væsentlige forskel: embeddings er mere sikre end almindelig tekst e-mail, fordi de er:
- Matematiske repræsentationer, ikke læsbar tekst
- Sværere at reverse-engineere end almindelig tekst
- Stadig underlagt samme fysiske sikkerhed som vores e-mail-klienter
Hvis det er acceptabelt for vores team at bruge Thunderbird eller webmail på krypterede arbejdsstationer, er det lige så acceptabelt (og måske mere sikkert) at gemme embeddings på samme måde.
Arkitekturen
Her er det grundlæggende flow. Det ser simpelt ud. Det var det ikke.
Note
Alle jobs bruger Forward Email API direkte, hvilket sikrer, at performanceoptimeringer gavner både vores AI-system og vores kunder.
Overordnet flow
Detaljeret scraper-flow
scraper.js er kernen i dataindsamlingen. Det er en samling af parsere til forskellige dataformater.
Hvordan det virker
Processen er opdelt i tre hoveddele: opbygning af vidensbasen, træning fra historiske e-mails og behandling af nye e-mails.
Opbygning af vidensbasen
update-knowledge-base.js: Dette er hovedjobbet. Det kører natligt, rydder den gamle vektorbutik og genopbygger den fra bunden. Det bruger scraper.js til at hente indhold fra alle kilder, processor.js til at opdele det i bidder, og ollama-client.js til at generere embeddings. Endelig gemmer vector-store.js alt i LanceDB.
Datakilder:
- Lokale Markdown-filer (
docs/*.md) - Teknisk whitepaper PDF (
assets/technical-whitepaper.pdf) - API-spec JSON (
assets/api-spec.json) - GitHub issues (via Octokit)
- GitHub diskussioner (via Octokit)
- GitHub pull requests (via Octokit)
- Sitemap URL-liste (
$LANCEDB_PATH/valid-urls.json)
Træning fra historiske e-mails
train-from-history.js: Dette job scanner historiske e-mails fra alle mapper, dekrypterer PGP-krypterede beskeder og tilføjer dem til en separat vektorbutik (customer_support_history). Dette giver kontekst fra tidligere supportinteraktioner.
Emailbehandlingsflow:
Nøglefunktioner:
- PGP-dekryptering: Bruger
pgp-decrypt.jshjælper medGPG_SECURITY_KEYmiljøvariablen - Trådgruppering: Grupperer relaterede e-mails i samtaletråde
- Bevarelse af metadata: Gemmer mappe, emne, dato, krypteringsstatus
- Svar-kontekst: Knytter beskeder med deres svar for bedre kontekst
Konfiguration:
# Miljøvariabler for train-from-history
HISTORY_SCAN_LIMIT=1000 # Maks antal beskeder der skal behandles
HISTORY_SCAN_SINCE="2024-01-01" # Behandl kun beskeder efter denne dato
HISTORY_DECRYPT_PGP=true # Forsøg PGP-dekryptering
GPG_SECURITY_KEY="/path/to/key.asc" # Sti til PGP privatnøgle
GPG_SECURITY_PASSPHRASE="passphrase" # Nøgleadgangskode (valgfri)
Hvad gemmes:
{
type: 'historical_email',
folder: 'INBOX',
subject: 'Re: Bug Report',
date: '2025-01-15T10:30:00Z',
messageId: '67e2f288893921...',
threadId: 'Bug Report',
hasReply: true,
encrypted: true,
decrypted: true,
replySubject: 'Bug Report',
replyText: 'First 500 chars of reply...',
chunkSize: 1000,
chunkOverlap: 200,
chunkIndex: 0
}
Tip
Kør train-from-history efter den indledende opsætning for at udfylde den historiske kontekst. Dette forbedrer svarenes kvalitet markant ved at lære af tidligere supportinteraktioner.
Behandling af indkommende e-mails
process-inbox.js: Denne job kører på e-mails i vores support@forwardemail.net, abuse@forwardemail.net og security@forwardemail.net postkasser (specifikt INBOX IMAP-mappestien). Den benytter vores API på https://forwardemail.net/email-api (f.eks. GET /v1/messages?folder=INBOX med BasicAuth adgang via vores IMAP-legitimationsoplysninger for hver postkasse). Den analyserer e-mailens indhold, forespørger både vidensbasen (forward_email_knowledge_base) og den historiske e-mail vektor-lager (customer_support_history), og sender derefter den samlede kontekst til response-generator.js. Generatoren bruger mxbai-embed-large via Ollama til at udforme et svar.
Automatiserede workflow-funktioner:
-
Inbox Zero Automation: Efter en vellykket oprettelse af et udkast flyttes den oprindelige besked automatisk til Arkiv-mappen. Dette holder din indbakke ren og hjælper med at opnå inbox zero uden manuel indgriben.
-
Spring AI-behandling over: Tilføj blot et
skip-ailabel (case-insensitive) til en hvilken som helst besked for at forhindre AI-behandling. Beskeden forbliver uberørt i din indbakke, så du kan håndtere den manuelt. Dette er nyttigt for følsomme beskeder eller komplekse sager, der kræver menneskelig vurdering. -
Korrekt e-mail-trådning: Alle udkastssvar inkluderer den oprindelige besked citeret nedenfor (med standard
>præfiks), efter e-mail-svarskonventioner med "På [dato], [afsender] skrev:" format. Dette sikrer korrekt samtalekontekst og trådning i e-mail-klienter. -
Svar-alle-adfærd: Systemet håndterer automatisk Reply-To headers og CC-modtagere:
- Hvis en Reply-To header findes, bliver den til To-adressen, og den oprindelige From tilføjes til CC
- Alle oprindelige To- og CC-modtagere inkluderes i svar-CC (undtagen din egen adresse)
- Følger standard e-mail svar-alle konventioner for gruppe-samtaler Kilde Rangering: Systemet bruger vægtet rangering til at prioritere kilder:
- FAQ: 100% (højeste prioritet)
- Teknisk whitepaper: 95%
- API-specifikation: 90%
- Officielle dokumenter: 85%
- GitHub issues: 70%
- Historiske e-mails: 50%
Vector Store Management
VectorStore-klassen i helpers/customer-support-ai/vector-store.js er vores interface til LanceDB.
Tilføjelse af dokumenter:
// vector-store.js
async addDocument(text, metadata) {
const embedding = await this.ollama.generateEmbedding(text);
await this.table.add([{
vector: embedding,
text,
...metadata
}]);
}
Rydning af lageret:
// Option 1: Brug clear() metoden
await vectorStore.clear();
// Option 2: Slet den lokale database-mappe
await fs.rm(process.env.LANCEDB_PATH, { recursive: true, force: true });
LANCEDB_PATH miljøvariablen peger på den lokale indlejrede databasedirektorium. LanceDB er serverløs og indlejret, så der er ingen separat proces at administrere.
The Vector Database Graveyard
Dette var den første store forhindring. Vi prøvede flere vektordatabaser, før vi valgte LanceDB. Her er hvad der gik galt med hver enkelt.
| Database | GitHub | Hvad gik galt | Specifikke problemer | Sikkerhedsbekymringer |
|---|---|---|---|---|
| ChromaDB | chroma-core/chroma | pip3 install chromadb giver dig en version fra stenalderen med PydanticImportError. Den eneste måde at få en fungerende version er at kompilere fra kilden. Ikke udviklervenligt. |
Python afhængighedskaos. Flere brugere rapporterer ødelagte pip-installationer (#774, #163). Dokumentationen siger "brug bare Docker", hvilket er et ikke-svar for lokal udvikling. Crasher på Windows med >99 poster (#3058). | CVE-2024-45848: Vilkårlig kodeudførelse via ChromaDB-integration i MindsDB. Kritiske OS-sårbarheder i Docker-image (#3170). |
| Qdrant | qdrant/qdrant | Homebrew tap (qdrant/qdrant/qdrant) nævnt i deres gamle dokumentation er væk. Forsvundet. Ingen forklaring. De officielle dokumenter siger nu bare "brug Docker." |
Manglende Homebrew tap. Ingen native macOS binær. Kun Docker er en barriere for hurtig lokal test. | CVE-2024-2221: Vilkårlig filupload-sårbarhed, der tillader fjernkodeudførelse (fikset i v1.9.0). Svag sikkerhedsmodenhedsscore fra IronCore Labs. |
| Weaviate | weaviate/weaviate | Homebrew-versionen havde en kritisk clustering-fejl (leader not found). De dokumenterede flag til at rette det (RAFT_JOIN, CLUSTER_HOSTNAME) virkede ikke. Fundamentalt ødelagt for enkelt-node opsætninger. |
Clustering-fejl selv i enkelt-node tilstand. Over-engineered til simple brugsscenarier. | Ingen større CVE’er fundet, men kompleksitet øger angrebsoverfladen. |
| LanceDB | lancedb/lancedb | Denne virkede. Den er indlejret og serverløs. Ingen separat proces. Den eneste irritation er den forvirrende pakke-navngivning (vectordb er forældet, brug @lancedb/lancedb) og spredte dokumenter. Vi kan leve med det. |
Forvirring omkring pakkenavne (vectordb vs @lancedb/lancedb), men ellers solid. Indlejret arkitektur eliminerer hele klasser af sikkerhedsproblemer. |
Ingen kendte CVE’er. Indlejret design betyder ingen netværksangrebsoverflade. |
Warning
ChromaDB har kritiske sikkerhedssårbarheder. CVE-2024-45848 tillader vilkårlig kodeudførelse. Pip-installationen er fundamentalt brudt med Pydantic-afhængighedsproblemer. Undgå til produktionsbrug.
Warning
Qdrant havde en filupload RCE-sårbarhed (CVE-2024-2221), som kun blev rettet i v1.9.0. Hvis du skal bruge Qdrant, skal du sikre dig, at du har den nyeste version.
Caution
Det open source vektordatabasesystem er ustabilt. Stol ikke på dokumentationen. Antag at alt er brudt, indtil det modsatte er bevist. Test lokalt før du binder dig til en stack.
Systemkrav
- Node.js: v18.0.0+ (GitHub)
- Ollama: Seneste (GitHub)
- Model:
mxbai-embed-largevia Ollama - Vektordatabse: LanceDB (GitHub)
- GitHub-adgang:
@octokit/resttil scraping af issues (GitHub) - SQLite: Til primær database (via
mongoose-to-sqlite)
Cron Job Konfiguration
Alle AI-jobs kører via cron på en MacBook M5. Her er hvordan du opsætter cron jobs til at køre ved midnat på tværs af flere indbakker.
Miljøvariabler
Jobbene kræver disse miljøvariabler. De fleste kan sættes i .env-filen (indlæst via @ladjs/env), men HISTORY_SCAN_SINCE skal beregnes dynamisk i crontab.
I .env-fil:
# Forward Email API legitimationsoplysninger (ændres pr. indbakke)
FORWARD_EMAIL_ALIAS_USERNAME=support@forwardemail.net
FORWARD_EMAIL_ALIAS_PASSWORD=your-imap-password
# PGP dekryptering (deles på tværs af alle indbakker)
GPG_SECURITY_KEY=/path/to/private-key.asc
GPG_SECURITY_PASSPHRASE=your-passphrase
# Historisk scanningskonfiguration
HISTORY_SCAN_LIMIT=1000
# LanceDB sti
LANCEDB_PATH=/path/to/lancedb
I crontab (beregnet dynamisk):
# HISTORY_SCAN_SINCE skal sættes inline i crontab med shell-dato beregning
# Kan ikke være i .env fil da @ladjs/env ikke evaluerer shell-kommandoer
HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" # macOS
HISTORY_SCAN_SINCE="$(date -d 'yesterday' +%Y-%m-%d)" # Linux
Cron Jobs for Flere Indbakker
Rediger din crontab med crontab -e og tilføj:
# Opdater vidensbase (kører én gang, deles på tværs af alle indbakker)
0 0 * * * cd /path/to/forwardemail.net && LANCEDB_PATH="/path/to/lancedb" GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" node jobs/customer-support-ai/update-knowledge-base.js >> /var/log/update-knowledge-base.log 2>&1
# Træn fra historik - support@forwardemail.net
0 0 * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="support-password" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-support.log 2>&1
# Træn fra historik - abuse@forwardemail.net
0 0 * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="abuse@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="abuse-password" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-abuse.log 2>&1
# Træn fra historik - security@forwardemail.net
0 0 * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="security@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="security-password" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-security.log 2>&1
# Behandl indbakke - support@forwardemail.net
*/5 * * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="support-password" GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-support.log 2>&1
# Behandl indbakke - abuse@forwardemail.net
*/5 * * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="abuse@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="abuse-password" GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-abuse.log 2>&1
# Behandl indbakke - security@forwardemail.net
*/5 * * * * cd /path/to/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="security@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="security-password" GPG_SECURITY_KEY="/path/to/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/path/to/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-security.log 2>&1
Cron Tidsplan Oversigt
| Job | Tidsplan | Beskrivelse |
|---|---|---|
train-from-sitemap.js |
0 0 * * 0 |
Ugentligt (søndag midnat) - Henter alle URL'er fra sitemap og træner vidensbase |
train-from-history.js |
0 0 * * * |
Dagligt ved midnat - Scanner gårsdagens e-mails pr. indbakke |
process-inbox.js |
*/5 * * * * |
Hvert 5. minut - Behandler nye e-mails og genererer udkast |
Dynamisk Dato Beregning
Variablen HISTORY_SCAN_SINCE skal beregnes inline i crontab fordi:
.envfiler læses som bogstavelige strenge af@ladjs/env- Shell kommando substitution
$(...)virker ikke i.envfiler - Datoen skal beregnes frisk hver gang cron kører
Korrekt tilgang (i crontab):
# macOS (BSD date)
HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" node jobs/...
# Linux (GNU date)
HISTORY_SCAN_SINCE="$(date -d 'yesterday' +%Y-%m-%d)" node jobs/...
Forkert tilgang (virker ikke i .env):
# Dette vil blive læst som bogstavelig streng "$(date -v-1d +%Y-%m-%d)"
# IKKE evalueret som en shell kommando
HISTORY_SCAN_SINCE=$(date -v-1d +%Y-%m-%d)
Dette sikrer, at hver natlig kørsel dynamisk beregner gårsdagens dato og undgår unødvendigt arbejde.
Første Opsætning: Udtræk URL-liste fra Sitemap
Før du kører process-inbox jobbet første gang, skal du udtrække URL-listen fra sitemap. Dette skaber en ordbog af gyldige URL'er, som LLM kan referere til og forhindrer URL-hallucination.
# Første opsætning: Udtræk URL-liste fra sitemap
cd /path/to/forwardemail.net
node jobs/customer-support-ai/train-from-sitemap.js
Hvad dette gør:
- Henter alle URL'er fra https://forwardemail.net/sitemap.xml
- Filtrerer kun ikke-lokaliserede URL'er eller /en/ URL'er (undgår duplikeret indhold)
- Fjerner lokaliseringspræfikser (/en/faq → /faq)
- Gemmer en simpel JSON-fil med URL-listen til
$LANCEDB_PATH/valid-urls.json - Ingen crawling, ingen metadata scraping - bare en flad liste af gyldige URL'er
Hvorfor det er vigtigt:
- Forhindrer at LLM hallucinerer falske URL'er som
/dashboardeller/login - Giver en whitelist af gyldige URL'er som svar-generatoren kan referere til
- Simpelt, hurtigt og kræver ikke lagring i vektordatabase
- Svar-generatoren loader denne liste ved opstart og inkluderer den i prompten
Tilføj til crontab for ugentlige opdateringer:
# Udtræk URL-liste fra sitemap - ugentligt søndag midnat
0 0 * * 0 cd /path/to/forwardemail.net && node jobs/customer-support-ai/train-from-sitemap.js >> /var/log/train-sitemap.log 2>&1
Test Cron Jobs Manuel
For at teste et job før det tilføjes til cron:
# Test sitemap træning
cd /path/to/forwardemail.net
export LANCEDB_PATH="/path/to/lancedb"
node jobs/customer-support-ai/train-from-sitemap.js
# Test support indbakke træning
cd /path/to/forwardemail.net
export FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net"
export FORWARD_EMAIL_ALIAS_PASSWORD="support-password"
export HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)"
export HISTORY_SCAN_LIMIT=1000
export GPG_SECURITY_KEY="/path/to/key.asc"
export GPG_SECURITY_PASSPHRASE="pass"
export LANCEDB_PATH="/path/to/lancedb"
node jobs/customer-support-ai/train-from-history.js
Overvågning af Logs
Hvert job logger til en separat fil for nem fejlfinding:
# Overvåg support indbakke behandling i realtid
tail -f /var/log/process-support.log
# Tjek gårsdagens træningskørsel
cat /var/log/train-support.log | grep "$(date -v-1d +%Y-%m-%d)"
# Se alle fejl på tværs af jobs
grep -i error /var/log/train-*.log /var/log/process-*.log
Tip
Brug separate logfiler pr. indbakke for at isolere problemer. Hvis en indbakke har autentificeringsproblemer, forurener det ikke logs for andre indbakker.
Codeeksempler
Scraping og behandling
// jobs/customer-support-ai/update-knowledge-base.js
const scraper = new Scraper();
const processor = new Processor();
const ollamaClient = new OllamaClient();
const vectorStore = new VectorStore();
// Ryd op i gamle data
await vectorStore.clear();
// Scrape alle kilder
const documents = await scraper.scrapeAll();
console.log(`Scraped ${documents.length} dokumenter`);
// Behandl til chunks
const allChunks = [];
for (const doc of documents) {
const chunks = processor.processDocuments([doc]);
allChunks.push(...chunks);
}
console.log(`Genererede ${allChunks.length} chunks`);
// Generer embeddings og gem
const texts = allChunks.map(chunk => chunk.text);
const embeddings = await ollamaClient.generateEmbeddings(texts);
for (let i = 0; i < allChunks.length; i++) {
await vectorStore.addDocument(texts[i], {
...allChunks[i].metadata,
embedding: embeddings[i]
});
}
Træning fra historiske e-mails
// jobs/customer-support-ai/train-from-history.js
const scanner = new EmailScanner({
forwardEmailApiBase: config.forwardEmailApiBase,
forwardEmailAliasUsername: config.forwardEmailAliasUsername,
forwardEmailAliasPassword: config.forwardEmailAliasPassword
});
const vectorStore = new VectorStore({
collectionName: 'customer_support_history'
});
// Scan alle mapper (INBOX, Sendt post osv.)
const messages = await scanner.scanAllFolders({
limit: 1000,
since: new Date('2024-01-01'),
decryptPGP: true
});
// Gruppér i samtaletråde
const threads = scanner.groupIntoThreads(messages);
// Behandl hver tråd
for (const thread of threads) {
const context = scanner.extractConversationContext(thread);
for (const message of context.messages) {
// Spring krypterede beskeder over, som ikke kunne dekrypteres
if (message.encrypted && !message.decrypted) continue;
// Brug allerede-parset indhold fra nodemailer
const text = message.nodemailer?.text || '';
if (!text.trim()) continue;
// Chunk og gem
const chunks = processor.chunkText(`Emne: ${message.subject}\n\n${text}`, {
chunkSize: 1000,
chunkOverlap: 200
});
for (const chunk of chunks) {
await vectorStore.addDocument(chunk.text, {
type: 'historical_email',
folder: message.folder,
subject: message.subject,
date: message.nodemailer?.date || message.created_at,
messageId: message.id,
threadId: context.subject,
encrypted: message.encrypted || false,
decrypted: message.decrypted || false,
...chunk.metadata
});
}
}
}
Forespørgsel efter kontekst
// jobs/customer-support-ai/process-inbox.js
const vectorStore = new VectorStore();
const historyVectorStore = new VectorStore({
collectionName: 'customer_support_history'
});
// Forespørg begge lagre
const knowledgeContext = await vectorStore.query(emailEmbedding, { limit: 8 });
const historyContext = await historyVectorStore.query(emailEmbedding, { limit: 3 });
// Vægtet rangering og deduplikering sker her
const rankedContext = rankAndDeduplicateContext(knowledgeContext, historyContext);
// Generer svar
const response = await responseGenerator.generate(email, rankedContext);
Fremtiden: Spam Scanner F&U
Hele dette projekt var ikke kun for kundesupport. Det var F&U. Vi kan nu tage alt, hvad vi har lært om lokale embeddings, vector stores og kontekstgenfinding og anvende det på vores næste store projekt: LLM-laget til Spam Scanner. De samme principper om privatliv, selvhosting og semantisk forståelse vil være nøglen.
Fejlfinding
Fejl ved vektordimension mismatch
Fejl:
Error: Failed to execute query stream: GenericFailure, Invalid input, No vector column found to match with the query vector dimension: 1024
Årsag: Denne fejl opstår, når du skifter embeddingsmodel (f.eks. fra mistral-small til mxbai-embed-large), men den eksisterende LanceDB-database blev oprettet med en anden vektordimension.
Løsning: Du skal genoplære vidensbasen med den nye embedding-model:
# 1. Stop alle kørende customer support AI jobs
pkill -f customer-support-ai
# 2. Slet den eksisterende LanceDB database
rm -rf ~/.local/share/lancedb/forward_email_knowledge_base.lance
rm -rf ~/.local/share/lancedb/customer_support_history.lance
# 3. Bekræft at embedding-modellen er sat korrekt i .env
grep OLLAMA_EMBEDDING_MODEL .env
# Skal vise: OLLAMA_EMBEDDING_MODEL=mxbai-embed-large
# 4. Hent embedding-modellen i Ollama
ollama pull mxbai-embed-large
# 5. Genoplær vidensbasen
node jobs/customer-support-ai/train-from-history.js
# 6. Genstart process-inbox jobbet via Bree
# Jobbet kører automatisk hvert 5. minut
Hvorfor det sker: Forskellige embedding-modeller producerer vektorer med forskellige dimensioner:
mistral-small: 1024 dimensionermxbai-embed-large: 1024 dimensionernomic-embed-text: 768 dimensionerall-minilm: 384 dimensioner
LanceDB gemmer vektordimensionen i tabelskemaet. Når du forespørger med en anden dimension, fejler det. Den eneste løsning er at genskabe databasen med den nye model.
Tom Vidensbase Kontekst
Symptom:
debug Retrieved knowledge base context {
total: 0,
afterRanking: 0,
questionType: 'capability'
}
Årsag: Vidensbasen er ikke blevet trænet endnu, eller LanceDB-tabellen findes ikke.
Løsning: Kør træningsjobbet for at fylde vidensbasen:
# Træn fra historiske emails
node jobs/customer-support-ai/train-from-history.js
# Eller træn fra website/docs (hvis du har en scraper)
node jobs/customer-support-ai/train-from-website.js
PGP Dekrypteringsfejl
Symptom: Beskeder vises som krypterede, men indholdet er tomt.
Løsning:
- Bekræft at GPG-nøglestien er sat korrekt:
grep GPG_SECURITY_KEY .env
# Skal pege på din private nøglefil
- Test dekryptering manuelt:
node -e "const decrypt = require('./helpers/customer-support-ai/pgp-decrypt'); decrypt.testDecryption();"
- Tjek nøgle-tilladelser:
ls -la /path/to/your/gpg-key.asc
# Skal kunne læses af brugeren, der kører jobbet
Brugertips
Opnå Inbox Zero
Systemet er designet til automatisk at hjælpe dig med at opnå inbox zero:
-
Automatisk Arkivering: Når et udkast er oprettet succesfuldt, flyttes den oprindelige besked automatisk til Arkiv-mappen. Dette holder din indbakke ren uden manuel indgriben.
-
Gennemgå Udkast: Tjek Udkast-mappen regelmæssigt for at gennemgå AI-genererede svar. Rediger efter behov før afsendelse.
-
Manuel Overstyring: For beskeder, der kræver særlig opmærksomhed, tilføj blot
skip-aietiketten før jobbet kører.
Brug af skip-ai etiketten
For at forhindre AI-behandling af specifikke beskeder:
- Tilføj etiketten: I din email-klient, tilføj en
skip-aietiket/tag til enhver besked (case-insensitive) - Beskeden forbliver i indbakken: Beskeden bliver ikke behandlet eller arkiveret
- Håndter manuelt: Du kan selv svare uden AI-indblanding
Hvornår du skal bruge skip-ai:
- Følsomme eller fortrolige beskeder
- Komplekse sager der kræver menneskelig vurdering
- Beskeder fra VIP-kunder
- Juridiske eller compliance-relaterede forespørgsler
- Beskeder der kræver øjeblikkelig menneskelig opmærksomhed
Email-trådning og Svar-Alle
Systemet følger standard email-konventioner:
Citerede Originalbeskeder:
Hej,
[AI-genereret svar]
--
Tak,
Forward Email
https://forwardemail.net
On Mon, Jan 15, 2024, 3:45 PM John Doe <john@example.com> wrote:
> Dette er den oprindelige besked
> med hver linje citeret
> ved brug af standard "> " præfiks
Reply-To Håndtering:
- Hvis den oprindelige besked har en Reply-To header, svarer udkastet til den adresse
- Den oprindelige From-adresse tilføjes i CC
- Alle andre oprindelige To og CC modtagere bevares
Eksempel:
Original besked:
Fra: john@company.com
Reply-To: support@company.com
Til: support@forwardemail.net
CC: manager@company.com
Udkast svar:
Til: support@company.com (fra Reply-To)
CC: john@company.com, manager@company.com
Overvågning og Vedligeholdelse
Tjek udkastskvalitet regelmæssigt:
# Vis nylige udkast
tail -f /var/log/process-support.log | grep "Draft created"
Overvåg arkivering:
# Tjek for arkiveringsfejl
grep "archive message" /var/log/process-*.log
Gennemgå sprunget beskeder:
# Se hvilke beskeder der blev sprunget over
grep "skip-ai label" /var/log/process-*.log
Testning
Kundesupport AI-systemet inkluderer omfattende testdækning med 23 Ava-tests.
Kørsel af Tests
På grund af npm-pakkeoverstyringskonflikter med better-sqlite3, brug det medfølgende testscript:
# Kør alle kundesupport AI-tests
./scripts/test-customer-support-ai.sh
# Kør med detaljeret output
./scripts/test-customer-support-ai.sh --verbose
# Kør specifik testfil
./scripts/test-customer-support-ai.sh test/customer-support-ai/message-utils.js
Alternativt kan tests køres direkte:
NODE_ENV=test node node_modules/.pnpm/ava@5.3.1/node_modules/ava/entrypoints/cli.mjs test/customer-support-ai
Testdækning
Sitemap Fetcher (6 tests):
- Locale mønster regex-matching
- URL-sti-ekstraktion og locale-fjernelse
- URL-filtreringslogik for locales
- XML-parselogik
- Dedupliceringslogik
- Kombineret filtrering, fjernelse og deduplicering
Message Utils (9 tests):
- Ekstraher afsendertekst med navn og email
- Håndter kun email når navn matcher præfiks
- Brug from.text hvis tilgængelig
- Brug Reply-To hvis til stede
- Brug From hvis ingen Reply-To
- Inkluder originale CC-modtagere
- Ekskluder vores egen adresse fra CC
- Håndter Reply-To med From i CC
- Deduplicer CC-adresser
Response Generator (8 tests):
- URL-grupperingslogik til prompt
- Afsendernavn-detekteringslogik
- Promptstruktur inkluderer alle nødvendige sektioner
- URL-listeformatering uden vinkelparenteser
- Håndtering af tom URL-liste
- Forbudte URL-liste i prompt
- Inkludering af historisk kontekst
- Korrekte URL’er for konto-relaterede emner
Testmiljø
Tests bruger .env.test til konfiguration. Testmiljøet inkluderer:
- Mock PayPal og Stripe legitimationsoplysninger
- Test krypteringsnøgler
- Deaktiverede autentifikationsudbydere
- Sikker testdatastier
Alle tests er designet til at køre uden eksterne afhængigheder eller netværkskald.
Vigtige Pointer
- Privatliv først: Selvhosting er ufravigeligt for GDPR/DPA-overholdelse.
- Omkostninger betyder noget: Cloud AI-tjenester er 50-1000x dyrere end selvhosting til produktionsarbejdsmængder.
- Økosystemet er brudt: De fleste vektordatabaser er ikke udviklervenlige. Test alt lokalt.
- Sikkerhedssårbarheder er reelle: ChromaDB og Qdrant har haft kritiske RCE-sårbarheder.
- LanceDB virker: Det er indlejret, serverløst og kræver ikke en separat proces.
- Ollama er solid: Lokal LLM-inferens med
mxbai-embed-largefungerer godt til vores brug. - Typefejl dræber dig:
textvs.content, ObjectID vs. string. Disse fejl er stille og brutale. - Vægtet rangering betyder noget: Ikke al kontekst er lige. FAQ > GitHub issues > Historiske emails.
- Historisk kontekst er guld: Træning på tidligere supportemails forbedrer svarenes kvalitet dramatisk.
- PGP-dekryptering er essentielt: Mange supportemails er krypterede; korrekt dekryptering er kritisk for træning.
Lær mere om Forward Email og vores privatlivsfokuserede tilgang til email på forwardemail.net.