![LLM-logning bedste praksis: 9 regler vi følger i produktion [2026]](/_next/image?url=https%3A%2F%2Fmedia.techsy.io%2Ftechsy-io%2Fhero-510-1200x630.webp&w=3840&q=75)
LLM-logning bedste praksis: 9 regler vi følger i produktion [2026]
Disse ni bedste praksisser for LLM-logning er de regler, vores produktions-stack faktisk kører efter: Vi logger 1,2 mio. LLM-requests om måneden på tværs af fire tjenester, og hver eneste lander i Grafana Loki som én JSON-linje med model, tokens, latency, cost_usd og trace_id. structlog 25.4.0 skriver recorden, Presidio fjerner PII først, og hele pipeline er lognings-delen af vores observability-stack.
Nøglepointer
- Log hver LLM-request som struktureret JSON med 14+ navngivne felter, aldrig fri tekst.
- Maskér PII før log-skrivningen med Presidio eller tilsvarende, ikke bagefter.
- Knyt OpenTelemetry GenAI semantic convention-attributter til hver trace.
- Ved 1 mio. requests/dag koster de samme 60 GB $108/måned i Datadog, $30 i Loki og $1,20 i ClickHouse.
Hvad LLM-logning faktisk betyder (og hvorfor »bare log alting« fejler)
LLM-logning betyder at opsamle en struktureret record af hver model-request og hvert svar: prompten, completion, token-antal, latency, omkostning og den trace, der binder det hele til en brugersession. Det er ikke infrastruktur-logning. CPU, hukommelse og pod-genstart hører til i din metrics-stack; dette indlæg dækker kun request-niveau-recorden, som lader dig fejlsøge, opgøre omkostninger og auditere modeladfærd.
Instinktet om »bare at logge alting« er svært at slippe, og det er dyrt. Fuld prompts og completions ved 1 mio. requests om dagen producerer cirka 60 GB tekst om måneden, og en del af den tekst er kunde-PII, som du nu lagrer på ubestemt tid. GDPR's artikel 5-princip om dataminimering kræver, at persondata er »tilstrækkeligt, relevant og begrænset til det nødvendige«, og et råt prompt-dump dumper den test på dag ét. At logge alting er ikke en strategi; det er en forpligtelse med en månedlig faktura.
Hvad er de 9 LLM-logningsregler?
Ni regler, i den rækkefølge vi ville implementere dem: log fulde prompts og svar med hashede identifikatorer, udsend struktureret JSON, opsaml tokens og omkostning pr. request, knyt OpenTelemetry trace-kontekst på, maskér PII før skrivningen, sample ved høj volumen, sæt opbevaringstrin, adskil sikkerhedshændelser, og gør resultatet søgbart. Hver regel nedenfor kommer med den kode eller den tabel, der håndhæver den.
Regel 1: Log den fulde prompt og det fulde svar (med hash, ikke rå PII)
Log den komplette prompt og den komplette completion for hver request, for delvise logs er sådan, du ender med at stirre på en incident uden nogen record af, hvad modellen faktisk så. Den ene undtagelse er identitet: skriv aldrig rå bruger-ID'er, e-mails eller navne ind i recorden. Gem et SHA-256-hash af bruger-ID'et i stedet. Et hash lader dig stadig rekonstruere én brugers fulde sessionshistorik med et offline opslag, mens selve log-linjen forbliver ubrugelig for enhver, der ikke bør læse den. Samme logik for system-prompts: hash dem, log hashet, og gem klarteksten i dit prompt-registry, hvor den allerede er versionsstyret.
Regel 2: Brug struktureret JSON: Hvert felt navngivet, intet som fri tekst
For bedste praksis inden for LLM-logning i Python eller ethvert andet sprog er struktureret logning i JSON den ikke-forhandlingsbare regel: hvert felt navngivet, typebestemt og søgbart, intet smidt som en formateret streng. En fri-tekst-linje som INFO called gpt-4o, took 812ms kan kun greppes. En JSON-record kan aggregeres efter model, summeres efter omkostning og joines til en trace. OpenAI's egne production best practices skubber samme idé: opsaml strukturerede metadata i SDK-laget frem for med print-sætninger.
Her er det skema, hver Techsy-tjeneste udsender, fjorten felter:
{
"request_id": "req_01J9XK4M7Q",
"timestamp": "2026-07-18T09:24:31.482Z",
"environment": "production",
"model": "claude-sonnet-4-20250514",
"prompt_tokens": 1284,
"completion_tokens": 396,
"latency_ms": 812,
"cost_usd": 0.0098,
"system_prompt_hash": "sha256:9f2c1a4e...",
"user_id_hash": "sha256:b7e4d831...",
"rag_sources": ["kb/pricing-2026.md", "kb/refund-policy.md"],
"guardrail_result": "pass",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7"
}Tre felter fortjener en bemærkning. cost_usd beregnes på request-tidspunktet ud fra token-antallet og modellens publicerede pris, aldrig efterfyldt af et nightly job. De to hash-felter er regel 1-kompromiset: korrelerbare offline, uigennemskuelige i loggen. Og trace_id og span_id er W3C trace-context-værdier, hvilket er præcis, hvad regel 4 handler om.
Hvis fire tjenester, der kalder udbydere direkte, lyder som fire steder at instrumentere, så centraliserer en LiteLLM-proxy det: én lognings-hook foran hver udbyder.
Regel 3: Opsaml token-antal og omkostning pr. request
Token usage tracking hører til i selve log-linjen, ikke i et warehouse-job, der kører i morgen. Hver udbyder returnerer input- og output-token-antal i svaret; gang med modellens per-token-pris på præcis det tidspunkt, og skriv cost_usd ind i recorden. Priser ændrer sig og varierer for cachede versus friske input-tokens, så at beregne omkostningen senere med en statisk pristabel omskriver stille historikken. Med omkostning på hver linje bliver »hvilken feature er dyr?« en ét-linjes forespørgsel i stedet for et finansprojekt, og det flyder direkte ind i arbejdet med at reducere dine LLM-API-udgifter.
Regel 4: Knyt trace-kontekst på (OpenTelemetry GenAI Semconv)
En log-linje uden et trace-ID er en forældreløs: du kan læse den, men du kan ikke se, hvilket retry, hvilket RAG-trin eller hvilken brugertur der producerede den. Løsningen er OpenTelemetry's GenAI semantic conventions, standard-attributnavnene til instrumentering af modelkald. Udsend loggen inde i et aktivt span, så knytter trace_id og span_id sig selv på, så ét klik i Grafana bringer dig fra trace-vandfaldet direkte til den rå record.
Attributterne, der er værd at sætte på hvert gen_ai-span:
| Attribut | Type | Eksempel | Formål |
|---|---|---|---|
| gen_ai.system | string | "anthropic" | Udbydernavn |
| gen_ai.request.model | string | "claude-sonnet-4-20250514" | Modellen du bad om |
| gen_ai.response.model | string | "claude-sonnet-4-20250514" | Modellen, der faktisk svarede |
| gen_ai.usage.input_tokens | int | 1284 | Promptstørrelse |
| gen_ai.usage.output_tokens | int | 396 | Completion-størrelse |
| gen_ai.response.finish_reasons | string[] | ["stop"] | Hvorfor genereringen stoppede |
| gen_ai.response.id | string | "msg_01XK9..." | Udbyderens svar-ID |
from opentelemetry import trace
tracer = trace.get_tracer("ai-sdr")
with tracer.start_as_current_span("chat.completion") as span:
span.set_attribute("gen_ai.system", "anthropic")
span.set_attribute("gen_ai.request.model", "claude-sonnet-4-20250514")
response = client.messages.create(**payload)
span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens)
span.set_attribute("gen_ai.usage.output_tokens", response.usage.output_tokens)
span.set_attribute("gen_ai.response.finish_reasons", [response.stop_reason])
log.info("llm.request", cost_usd=cost) # Rule 2 record, trace IDs auto-attachedRegel 5: Maskér PII før log-skrivningen
PII-maskering skal ske, før recorden skrives, ikke renses bagefter. Når først en e-mailadresse er i Loki, er den også i dine objektlager-backups, og »vi slettede den senere« er ikke et GDPR-svar. I vores setup kører Microsoft Presidio som en structlog-processor og fanger 94 % af e-mails og telefonnumre, før de rammer Loki; de oversete er næsten alle sammen mærkelig formatering, som vi patcher ind i custom recognizers, efterhånden som vi finder dem.
Hele hooket er femten linjer:
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
PII_ENTITIES = ["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD", "IBAN_CODE"]
def redact_pii(text: str) -> str:
results = analyzer.analyze(text=text, language="en", entities=PII_ENTITIES)
return anonymizer.anonymize(text=text, analyzer_results=results).text
# In the structlog processor chain, before JSONRenderer:
# event_dict["prompt"] = redact_pii(event_dict["prompt"])Maskering ligger i samme pipeline-lag som dine input- og output-filtre, og den bør testes på samme måde. Vores guardrail-pipeline behandler en lækket e-mail i en log som en fejlet eval, ikke en drifts-fodnote.
Regel 6: Sample intelligent ved høj volumen
Under cirka 100.000 requests om dagen: log alting. Over det er fuld-volumen-logning en lagerafgift på data, du aldrig læser, og sampling er måden, du beholder de records, der betyder noget. Hagen: tilfældig sampling er den dårligste mulighed for LLM-trafik, fordi fejl, refusals og fem-dollar-requests per definition er sjældne, så en ensartet 10 %-rate smider præcis de hændelser væk, du fejlsøger. Sample efter udfald, ikke efter møntkast.
| Strategi | Hvornår | Kompleksitet |
|---|---|---|
| Tilfældig (fast 10 %) | Grundlæggende volumen-metrics ved stabil trafik | Lav |
| Regelbaseret | Behold altid bestemte modeller, tenants eller routes | Lav |
| Hale-baseret | Behold langsomme, dyre eller fejlede requests; smid normale væk | Middel |
| Udløser-baseret | Fuld kontekst kun når et guardrail udløses eller en eval fejler | Middel |
| Adaptiv | Samplingraten stiger og falder med trafikvolumen | Høj |
Et almindeligt setup er regelbaseret i kanterne (produktion og enterprise-tenants: log altid) plus hale-baseret i midten. Den log-specifikke vinkel på denne ramme: dine guardrail_result- og cost_usd-felter er sampling-signalerne, allerede til stede, hvis du fulgte regel 2 og 8.
Regel 7: Sæt en opbevaringspolitik, før du får brug for den
En log-opbevaringspolitik er en beslutning, du træffer, mens der er ro, for alternativet er at træffe den under en omkostningsgennemgang med dobbelt volumen. GDPR's artikel 5-princip om opbevaringsbegrænsning siger, at persondata bør opbevares »ikke længere end nødvendigt«, hvilket i praksis betyder trindelt opbevaring:
| Trin | Opbevaringstid | Lager | Anvendelse |
|---|---|---|---|
| Hot | 7 dage | Loki / ClickHouse lokal disk | Live fejlsøgning, on-call forespørgsler |
| Varm | 30 dage | Objektlager-baseret indeks (S3) | Sprint-omkostningsanalyse, incident-gennemgang |
| Kold | 1 år | Komprimeret S3/GCS-arkiv | Compliance-forespørgsler, årlige audits |
Hot besvarer »hvad skete der for ti minutter siden?« hurtigt og dyrt; kold besvarer »hvad fortalte vi denne kunde i marts?« langsomt og billigt. Slet efter planen, automatisk, ellers er trinnene bare et diagram.
Regel 8: Log guardrail- og sikkerhedshændelser separat
Sikkerhedshændelser (guardrail-blokeringer, refusals, politikovertrædelser) er ikke telemetry; de er audit-records, og de hører til i deres egen stream. Tre grunde. Alarmering: en stigning i blokerede prompt injections bør tilkalde nogen, og du kan ikke tune den alarm mod 1 mio. rutine-linjer. Opbevaring: compliance kan kræve, at sikkerheds-records overlever debug-logs med flere år. Adgang: auditorer får sikkerheds-streamen, ikke hele din firehose. Tag afgørelsen i hoved-recorden (guardrail_result: "block"), og rout den fulde record til den separate stream. Hvad der tæller som en sikkerhedshændelse, dækkes i vores guide til guardrail-hændelser.
Regel 9: Gør logs søgbare, ikke bare lagrede
En log, du ikke kan søge i på under et minut, er en backup, ikke et observability-signal. Søgbar betyder indekserede felter, et forespørgselssprog, din on-call faktisk kan, og dashboards bygget før incidenten. Vi kører Loki og forespørger i den 30+ gange om ugen efter omkostningsanomalier, latency-regressioner og »vis mig hver refusal for tenant X i går.« Grafana Loki-dokumentationen er referencen for syntaksen; mønsteret, der tjener sig hjem, er filtrering på parsrede JSON-felter direkte:
{service="ai-sdr"} |= "llm.request" | json
| model = "claude-sonnet-4-20250514"
| cost_usd > 0.05
| line_format "{{.timestamp}} {{.request_id}} ${{.cost_usd}} {{.latency_ms}}ms"Fem linjer, ingen eksport til en notebook. Hvis dit nuværende lager ikke kan det, er det problemet, der skal løses først.
Hvad vi faktisk logger i produktion
Nok teori. Her er den maskerede config fra vores AI SDR-pipeline, tjenesten bag 1,2-mio.-requests-om-måneden-tallet i introen. Den kører structlog 25.4.0, der renderer JSON, sendt til Grafana Cloud Loki via Promtail. Modellen på denne pipeline er claude-sonnet-4-20250514, og hvert kald går gennem præcis den processor-kæde fra regel 2 og 5:
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
redact_pii, # Rule 5: Presidio, before serialization
add_otel_trace_ids, # Rule 4: trace_id + span_id from the active span
structlog.processors.JSONRenderer(),
],
)
log = structlog.get_logger()To tal fra det første kvartal med dette setup. Månedlig ingest lagde sig på 47 GB på tværs af fire tjenester, og p95 log-skrive-latency er 3 ms, hvilket vil sige, at pipeline ikke tilføjer noget målbart til request-tiden.
Config-ændringen, der betalte sig selv: vi tilføjede cost_usd til hver log-post i marts 2026. Inden for en uge fandt vi én prompt-skabelon, der brændte $340/måned af i retry-loops. En forbigående API-fejl udløste tre retries, der hver gensendte den fulde 4.000-token kontekst. Loggene gjorde det til en ét-linjes forespørgsel; uden omkostning pr. request var det dukket op som en uforklarlig linjepost i den næste kvartalsvise budgetgennemgang.
Hvad koster LLM-log-lagring i stor skala?
Ved 1 mio. requests om dagen koster LLM-log-lagring mellem cirka $1,20 og $108 pr. måned for de samme data, afhængigt af lageret. Regnestykket: en fuld struktureret record er i gennemsnit cirka 2 KB, så 1 mio. requests om dagen er 2 GB om dagen, eller 60 GB om måneden. De udbyderpublicerede priser nedenfor (juli 2026) er, hvad de 60 GB koster i tre almindelige backends.
| Backend | Prismodel (udbyderpubliceret, juli 2026) | 60 GB/måned | Noter |
|---|---|---|---|
| Datadog LLM Observability | $0,10/GB ingestet + $1,70/GB indekseret | ~$108 | Indeksering er den dyre linje |
| Grafana Cloud Loki | ~$0,50/GB via objektlager | ~$30 | Endnu billigere ved self-hosting |
| ClickHouse (self-hosted, S3) | ~$0,02/GB komprimeret lager | ~$1,20 + compute | Compute er den reelle omkostning |
Kilder: Datadog-priser, Grafana Loki og ClickHouse observability-dokumentationen.
To forbehold, for dette er vores beregning ud fra udbyderpriser, ikke en benchmark, vi kørte. For det første antager Datadogs tal, at du indekserer alting; de fleste teams indekserer en delmængde og betaler langt mindre, mens Loki og ClickHouse primært opkræver for det, du lagrer. For det andet skjuler self-hosted ClickHouse's $1,20 en reel regning: compute til at køre klyngen og ingeniørtimerne til at drive den. Ved 60 GB om måneden er en managed service næsten altid det billigste samlede svar. Self-hosting begynder at give mening over cirka 1 TB om måneden, hvor per-GB-forskellen overvælder driftsoverheaden.
Spredningen er pointen. Ved 1 mio. requests pr. dag er forskellen mellem Datadog-indekseret og self-hosted ClickHouse cirka 90x: $108 mod $1,20 for de samme 60 GB. Vælg lageret på arkitektur-tidspunktet, ikke efter at fakturaen ankommer.
Hvilket logningsværktøj skal du vælge?
For de fleste teams koges valget ned til fire muligheder: en LLM-native platform (Langfuse eller LangSmith), et proxy-lags-værktøj (Helicone) eller en almindelig OpenTelemetry-pipeline ind i infrastruktur, du allerede kører. Tabellen dækker de beslutningspunkter, der faktisk adskiller sig; dashboards, playback og prompt-versionering er basis i alle fire.
| Langfuse | LangSmith | Helicone | OTel-native (Loki/ClickHouse) | |
|---|---|---|---|---|
| Self-hosting | Ja (open-source kerne) | Nej (SaaS) | Ja (open-source) | Fuldt ud |
| OTel-kompatibel | Ja (OTLP ingest) | Delvis (OTLP export) | Delvis | Native |
| Cost-tracking | Ja | Ja | Ja | Gør-det-selv (beregn cost_usd selv) |
| Indbygget PII-maskering | Nej (forbehandling) | Nej | Nej | Nej (Presidio, jf. regel 5) |
| Gratis tier | Ja (cloud + self-host) | Ja (begrænset) | Ja | Gratis software; du betaler infra |
Vores mening, kort sagt: vi kører OTel-native plus Loki, fordi vi allerede havde Grafana-stacken til alt andet, og at tilføje én datakilde mere slog at adoptere en fjerde udbyder. Hvis du starter fra nul uden nogen observability-stack overhovedet, er Langfuses tracing-model og dens gratis tier den hurtigste vej til nyttig, og self-host-muligheden holder udgangsdøren åben. Hvis du vælger mellem de to LLM-native ledere, kører vores Langfuse vs LangSmith-gennemgang den fulde sammenligning. Og hvis logning er én brik i en større overvågningsbeslutning, dækker den fulde platformssammenligning det bredere felt.
Hvad er de mest almindelige LLM-logningsfejl?
Seks fejl står for de fleste af de ødelagte LLM-lognings-setups, vi har kigget på. Hver enkelt er billig at undgå, hvis du fanger den, før log-volumenen gør:
- Logning af rå PII uden maskering. Den mest almindelige og den dyreste. Én support-eksport eller én kompromitteret bucket gør prompt-logs til en databeskyttelses-incident. Maskér før skrivningen (regel 5), ikke ved læsning.
- Ingen opbevaringspolitik. Ubegrænset lagring er standarden overalt, og den fordobler stille din regning hvert år. Hvis du aldrig sletter, har du ikke et logningssystem; du har et arkiv med storhedsvanvid.
- Ustrukturerede tekst-logs. Print-output, du kun kan greppe, virker i demo-skala og bryder sammen ved 100.000 requests om dagen, hvor »find hver fejlet request for model X« bliver en shell-script-eftermiddag i stedet for en forespørgsel.
- Kun at logge fejl. Succesfulde requests er den baseline, du registrerer drift mod, og de er råmaterialet i din evaluerings-pipeline. Log også sejrene, samplet hvis volumen tvinger det.
- At ignorere omkostningsfelter. Ingen cost_usd pr. request betyder ingen omkostningsalarmer, ingen per-feature-attributering, og $340/måned-retry-loopet fra produktionsafsnittet ovenfor forbliver usynligt indtil den kvartalsvise regning.
- Ingen trace-korrelation. Logs, der er afkoblet fra spans, gør multi-trins agent-fejlsøgning til gætværk. Hvis din log-linje mangler et trace_id, er regel 4 løsningen.
Om forfatteren
Mert Batur er medstifter af Techsy.io, hvor teamet leverer AI-agenter, automationssystemer og voice/SDR-pipelines til B2B-kunder. Han skriver om den LLM-værktøjs-stack, Techsy-teamet faktisk bruger i produktion. Forbind på LinkedIn.
Ofte stillede spørgsmål
Hvad skal du logge for hver LLM-request?
Som minimum: den fulde prompt og completion med PII maskeret, modelnavnet, input- og output-token-antal, latency, omkostning i USD, en hashet brugeridentifikator samt OpenTelemetry trace- og span-ID'er. Tilføj RAG-kilde-ID'er og guardrail-afgørelsen, hvis din pipeline har de trin. Fjorten navngivne felter, én JSON-linje pr. request.
Hvad er det bedste format til LLM-logs?
Struktureret JSON, ét objekt pr. request, med hvert felt eksplicit navngivet. Fri-tekst-logs kan kun greppes; JSON-records kan aggregeres efter model, summeres efter omkostning og joines til traces. Udsend recorden med en struktureret logger som structlog i Python eller pino i Node, og render den med en JSON-serializer, aldrig strengformatering.
Hvordan håndterer du PII i LLM-logs?
Maskér før log-skrivningen, ikke bagefter. Kør prompten og completion gennem en detektor som Microsoft Presidio inde i din lognings-pipeline, og erstat navne, e-mails og telefonnumre med tokens som <EMAIL_ADDRESS>. Når rå PII først når dit log-lager, er den også i dine backups, og efterfølgende sletning opfylder sjældent GDPR's minimeringstest.
Hvad koster LLM-log-lagring i stor skala?
For 1 mio. requests om dagen, cirka 60 GB om måneden ved 2 KB pr. record, kan du forvente cirka $108/måned på Datadogs indekserede LLM Observability-pris, $30/måned på Grafana Cloud Loki eller cirka $1,20/måned i komprimeret S3-lagring til self-hosted ClickHouse plus compute. Det er udbyderpublicerede priser pr. juli 2026; self-hosting lægger ingeniørtid oveni.
Hvad er OpenTelemetry GenAI semantic conventions?
Det er OpenTelemetry's standard-attributnavne til instrumentering af LLM-kald: gen_ai.system for udbyderen, gen_ai.request.model for modellen, gen_ai.usage.input_tokens og output_tokens for token-antal og gen_ai.response.finish_reasons for, hvorfor genereringen stoppede. At bruge dem betyder, at enhver OTel-kompatibel backend, fra Jaeger til Tempo til Langfuse, læser dine traces uden custom parsere.
Hvordan sampler du LLM-logs ved høj trafik?
Behold hver fejl, hver guardrail-blokering og hver request over en omkostningstærskel, og sample så resten. Denne hale-baserede tilgang bevarer de sjældne hændelser, du faktisk fejlsøger, mens ensartet tilfældig sampling smider dem væk i samme takt som kedelig trafik. Under 100.000 requests om dagen: spring sampling helt over og log alting.
Hvor længe skal du opbevare LLM-logs?
Trin-del det: 7 dage hot til live fejlsøgning, 30 dage varm til incident-gennemgang og omkostningsanalyse, og op til 1 år kold i komprimeret objektlager til compliance og audits. GDPR's opbevaringsbegrænsningsprincip forbyder at opbevare persondata længere end nødvendigt, så kobl hvert trin med automatisk sletning frem for manuel oprydning.
Hvad er forskellen mellem LLM-logning og LLM-tracing?
En log er en flad record af én hændelse: denne request skete, med disse felter. En trace er et kausalt træ af spans på tværs af en hel request-vej, f.eks. retrieval, så modelkaldet, så to tool-kald. Logs fortæller dig hvad; traces fortæller dig hvor og hvorfor. Produktions-setups udsender begge, forbundet af trace_id.
Konklusion
Opsummering: log hver request som JSON med navngivne felter, beregn omkostning på request-tidspunktet, knyt OTel trace-kontekst på, maskér PII før skrivningen, sample efter udfald, når du passerer 100.000 requests om dagen, og vælg et lager, du faktisk kan søge i. De ni regler er ordnet, så du kan adoptere én pr. sprint, og regel 2, 4 og 5 er de tre, der betaler sig hurtigst tilbage. Hvis du vælger den bredere overvågnings-stack omkring loggene, så start med vores oversigt over de bedste AI observability-platforme. Og hvis du har brug for hjælp til at koble struktureret logning på din LLM-stack, så få en gratis konsultation.