Γιατί τα agents σπάνε διαφορετικά
Σε ένα κλασικό API, η αποτυχία φαίνεται: status 500, timeout, exception. Σε έναν agent όχι — γιατί ο agent μπορεί να «πετύχει» τεχνικά και να αποτύχει πραγματικά.
Ένας agent διάλεξε λάθος tool, ή άρπαξε λάθος έγγραφο, ή έκανε loop σε ένα βήμα — και παρόλα αυτά παρήγαγε μια αξιοπιστία φαινομενική απάντηση. Το τελικό output δείχνει σωστό.
Πόσες κλήσεις μοντέλου θα κάνει, πόσα tools θα τρέξει, πόσα retries — το αποφασίζει ο agent, όχι εσύ. Άρα το κόστος δεν είναι γραμμικό και πρέπει να μετριέται ανά trace.
Plan-act-observe loops και subagents παράγουν traces με εκατοντάδες ή χιλιάδες observations. Το scroll σε span tree δεν είναι debugging strategy.
Συνεπώς, το observability για agents δεν είναι «να βλέπω logs». Είναι να μπορώ να απαντήσω: γιατί αυτό το trace κόστισε $1,40 όταν το μέσο είναι $0,05; και να το απαντήσω σε δευτερόλεπτα, όχι σε ώρες.
Monitoring, observability, evals — τι είναι τι
Τα μπερδεύουν όλοι. Η διαφορά:
| Επίπεδο | Τι απαντά | Παράδειγμα |
|---|---|---|
| Metrics | Είναι καλά σε συνολικά; | p95 latency, κόστος/ώρα, error rate |
| Observability | Γιατί αυτό το trace απέτυχε; | Span tree με κάθε tool call και token |
| Evals | Είναι η απάντηση καλή; | LLM-as-a-judge σε groundedness, human annotation |
Ο σωστός συνδυασμός: metrics για alerts, traces για debugging, evals για ποιότητα. Και οι τρεις τροφοδοτούνται από την ίδια πηγή — τα OpenTelemetry GenAI conventions.
Setup A — τοπικός έλεγχος σε 2 λεπτά (Aspire Dashboard)
Πριν αγοράσεις οτιδήποτε, έλεγξε ότι ο κώδικάς σου εκπέμπει σωστά. Το Aspire Dashboard είναι δωρεάν, ανοιχτό, δέχεται OTLP απευθείας και έχει ενσωματωμένο trace viewer και GenAI visualizer.
# δωρεάν OTLP backend με trace viewer + GenAI visualization
docker run --rm -p 18888:18888 -p 4317:18889 -p 4318:18890 -d --name aspire-dashboard \
-e ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true \
mcr.microsoft.com/dotnet/aspire-dashboard:latestΣτέλνεις εκεί OTLP στο http://localhost:4318 και ανοίγεις http://localhost:18888 για να δεις traces, metrics και logs. Ιδανικό για local ανάπτυξη και debugging — δεν θέλεις cloud λογαριασμό για να δεις αν δουλεύει.
invoke_agent span με παιδιά chat (ανά κλήση μοντέλου) και execute_tool (ανά tool call). Αν βλέπεις μόνο ένα απλό span, δεν έχεις instrumentation.Setup B — Langfuse με OTLP (cloud)
Το Langfuse δέχεται traces ως απλό OpenTelemetry backend στο endpoint /api/public/otel. Δεν χρειάζεται έτοιμο integration — αρκεί να στέλνεις OTLP.
# EU data region (υπάρχουν και US, Japan, HIPAA) OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel" # Basic Auth: base64 των "pk-lf-...:sk-lf-..." OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ${AUTH_STRING},x-langfuse-ingestion-version=4"
# φτιάξε το AUTH_STRING από τα API keys σου
echo -n "pk-lf-1234567890:sk-lf-1234567890" | base64Τρεις λεπτομέρειες που κολλάνε τους περισσότερους
- Το
x-langfuse-ingestion-version: 4header είναι υποχρεωτικό για να εμφανίζονται τα δεδομένα σε πραγματικό χρόνο. Χωρίς αυτό, τα spans μπορεί να αργήσουν έως και 10 λεπτά. - Μόνο OTLP over HTTP (JSON ή protobuf). Το gRPC δεν υποστηρίζεται ακόμα — αν ο exporter σου είναι gRPC, άλλαξέ τον σε http/protobuf.
- Signal-specific endpoints: αν ο collector σου χρειάζεται ξεχωριστό traces endpoint, είναι
/api/public/otel/v1/traces.
http://localhost:3000/api/public/otel) — το endpoint εμφανίστηκε στην έκδοση v3.22.0, οπότε αν παίρνεις 4xx κάνε upgrade.Setup C — OpenTelemetry Collector
Όταν οι εφαρμογές σου είναι παραπάνω από μία, βάζεις Collector στη μέση: δέχεται OTLP από όλες και προωθεί με ξεχωριστό auth.
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
memory_limiter:
limit_mib: 1500 # 80% του μέγιστου, έως 2G
spike_limit_mib: 512
check_interval: 5s
batch:
exporters:
otlphttp/langfuse:
endpoint: "https://cloud.langfuse.com/api/public/otel"
headers:
Authorization: "Basic ${AUTH_STRING}"
x-langfuse-ingestion-version: "4"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp/langfuse]filterprocessor μπορείς να στείλεις μόνο spans με gen_ai.system=\"openai\" — αλλά το Langfuse χρειάζεται το root span για να δημιουργήσει σωστά το trace. Πάντα να περνάει το root.Τα attributes που μετράς (gen_ai.*)
Τα OpenTelemetry GenAI Semantic Conventions ορίζουν τα gen_ai.* attributes σε spans, metrics και events — για LLM clients και MCP. Αυτά είναι τα ίδια conventions που καταναλώνουν Langfuse, Arize, Datadog και οποιοδήποτε OTLP backend, άρα δεν κλειδώνεστε σε εργαλείο.
| Attribute | Τι σημαίνει |
|---|---|
gen_ai.operation.name | chat, invoke_agent, execute_tool, invoke_workflow, plan |
gen_ai.provider.name | openai, anthropic, gcp.vertex_ai, aws.bedrock |
gen_ai.request.model | το μοντέλο που κλήθηκε |
gen_ai.usage.input_tokens / output_tokens | η βάση για όλο τον υπολογισμό κόστους |
gen_ai.response.finish_reasons | stop, tool_calls, length — γιατί κόπηκε |
gen_ai.agent.name / id / version | ποιος agent έτρεξε (σε multi-agent) |
gen_ai.tool.name / call.id | ποιο tool κλήθηκε και με ποιο id |
gen_ai.conversation.id | ομαδοποίηση πολλαπλών γύρων και cross-service |
gen_ai.input.messages / output.messages | content — opt-in, προσοχή με PII |
finish_reasons=length συχνά σε κάποιο prompt, κόβεις απαντήσεις — φθηνότερο fix απ’ όσο νομίζεις. Και αν ο provider είναι κενός, το instrumentation δεν μαζεύει σωστά τα tokens.Trace attributes σε όλα τα spans (Baggage)
Το Langfuse φιλτράρει και ομαδοποιεί όλο και περισσότερο σε επίπεδο observation, όχι μόνο trace. Αν θες να φιλτράρεις με userId, sessionId ή tags, πρέπει να υπάρχουν σε κάθε span του trace — όχι μόνο στο root. Το πρότυπο είναι το OpenTelemetry Baggage με BaggageSpanProcessor.
- Βάζεις τα attributes ως baggage entries στην αρχή του trace
- Τα βάζεις και στο ενεργό span
- Ο
BaggageSpanProcessorτα αντιγράφει αυτόματα σε όλα τα παιδιά
Στο Langfuse SDK υπάρχουν οι συναρτήσεις propagate_attributes() (Python) / propagateAttributes() (TS) που το κάνουν αυτό χωρίς χειροκίνητη δουλειά. Αν χρησιμοποιείς απλό OTel SDK, βάλε χειροκίνητα attributes με prefix langfuse.trace.metadata.* για να είναι filterable.
Το dashboard που χρειάζεσαι (5 charts, όχι 50)
gen_ai.client.operation.duration (histogram), χωρισμένο per model. Εδώ βλέπεις αν μια αλλαγή μοντέλου «ζάλισε» τους χρήστες.Tokens (input × output) × τιμή, αθροισμένα ανά trace. Η πιο χρήσιμη μετρική για budget control.
% traces που ολοκληρώθηκαν επιτυχώς vs failed/canceled. Η κύρια μετρική ποιότητας ροής.
Counter αποτυχημένων tool calls. Με το
gen_ai.tool.name βλέπεις αμέσως ποιο tool είναι το προβληματικό.Μετρητής επαναλαμβανόμενων tool calls ανά observation + counter
loop_detected. Το συχνότερο αδιέξοδο σε production.Debugging σε πράξη: 3 περιπτώσεις
Περίπτωση 1 — Loop 30 tool calls
Σύμπτωμα: κόστος ×8, χρόνος απόκρισης 90s. Τι ψάχνεις: στο trace, μετράς πόσα execute_tool spans έχει το ίδιο όνομα και ίδια arguments. Αν βρεις 30 φορές το web.search με το ίδιο query, το πρόβλημα είναι στο tool result — προφανώς επιστρέφει κάτι που δεν ικανοποιεί τον agent, οπότε προσπαθεί ξανά. Η λύση είναι καλύτερο tool result ή σαφέστερη οδηγία, όχι περισσότερα iterations.
# στο observations table: βρες looping agents observation.type = "tool" AND count(tool.name) >= 30
Περίπτωση 2 — Κόστος $1,40 ανά trace
Ταξινομείς τα generations ανά gen_ai.usage.* και βλέπεις το χτυπημένο: συνήθως ένα τεράστιο input_tokens σε ένα retrieval observation. Δηλαδή το RAG γύρισε 40 αποσπάσματα αντί για 4, ή το prompt ξαναστέλνει όλη τη συνομιλία. Είναι μίας γραμμής η διόρθωση — αλλά μόνο αν το είδες.
Περίπτωση 3 — Agent «κολλάει» σε input-required
Το task μένει input-required και κανείς δεν τον ξαναπαίρνει. Με rate metric (
Alerts που έχουν νόημα
| Alert | Όριο-παράδειγμα | Γιατί |
|---|---|---|
| Κόστος ανά trace p95 | > $0,50 όταν μέσο = $0,08 | προλαμβάνει λογαριασμό-έκπληξη |
| loop_detected | > 0 σε 5 λεπτά | ο agent καίει χρήμα και χρόνο χωρίς να προχωρά |
| tool_errors rate | > 5% ανά tool | συνήθως σπάσει ένα εξωτερικό API, όχι ο agent |
| task_success_rate | < 90% για 15 λεπτά | η μοναδική μετρική που νοιάζονται οι χρήστες |
| p95 latency | > 25s | ο χρήστης εγκαταλείπει |
Στέλνε τα σε Slack/Discord/webhook. Κανένα alert δεν πρέπει να σε ξυπνάει αν δεν υπάρχει συγκεκριμένη ενέργεια που πρέπει να κάνεις.
Από traces σε evals — και μετά σε CI gate
Το observability δεν έχει αξία αν σταματάει εκεί. Ο κύκλος είναι: βλέπω αποτυχία στη production → την κάνω dataset item → τρέχω experiment στο CI → μπλοκάρω release αν πέσει το score. Έτσι το ίδιο bug δεν επιστρέφει.
Για ντετερμινιστικά checks: καλέστηκε το σωστό tool, σεβάστηκε το budget βημάτων, το output περνάει το schema. Μια γραμμή κώδικα, μηδέν αστάθεια.
Για σημασιολογικά: ολοκληρώθηκε το task, είναι grounded η απάντηση. Χρειάζεται βαθμονόμηση με human labels για να έχει αξία.
Προτίμα δυαδικό pass/fail αντί για κλίμακα 1–5. Σε αναγκάζει να ορίσεις τι σημαίνει «αποδεκτό» — και τα scores γίνονται συγκρίσιμα.
Δυο μέρη τρέχουν παράλληλα: offline (datasets + experiments πάνω σε πραγματικά traces πριν το deploy) και online (evaluators και user feedback πάνω στη ζωντανή κίνηση). Όταν η production βγάλει νέο failure mode, μπαίνει στο dataset και ο κύκλος κλείνει.
PII: τι δεν πρέπει να στέλνεις
Από προεπιλογή, το GenAI telemetry περιέχει metadata — ονόματα μοντέλων, token counts, durations. Το content capture (prompts, completions, tool arguments) είναι ξεχωριστή, ρητή επιλογή, ακριβώς επειδή μπορεί να περιέχει ευαίσθητα δεδομένα.
- Ξεκίνα με content capture κλειστό και άνοιξέ το μόνο όταν χρειάζεται για debugging.
- Αν το ανοίξεις: redaction πριν την εξαγωγή (emails, κάρτες, tokens), όχι μετά.
- Μην στέλνεις
gen_ai.input.messagesσε backends που δεν ελέγχεις. - Remember: τα attributes μεγαλώνουν γρήγορα — πολλά backends τα αποθηκεύουν ως raw JSON και δεν τα διαβάζει κανείς.
Production checklist
OTel GenAI σε κάθε agent. Framework-native instrumentation όπου υπάρχει (Vercel AI SDK, LangGraph, Pydantic AI, Claude Agent SDK).
Trace context σε κάθε service boundary, και μέσα στο MCP
_meta για client/server traces.OTLP endpoint +
x-langfuse-ingestion-version: 4 (αν χρησιμοποιείς Langfuse). Έλεγξε ότι φτάνουν spans πριν κοιμηθείς.p95 latency, cost per trace, success rate, tool errors, loops. Πέντε charts, όχι πενήντα.
Cost p95, loop_detected, tool error rate, success rate — με σαφή ενέργεια παρακάτω.
Datasets από παραγωγικά failures, experiments στο CI, binary scores.
Content capture opt-in, redaction πριν την εξαγωγή, κανένα ευαίσθητο σε baggage.
Όρια κόστους ανά κλειδί και max iterations ανά task. Το observability δείχνει, όρια αποτρέπουν.
Συχνές ερωτήσεις
Χρειάζομαι Langfuse ή μπορώ με ανοιχτό stack;
Μπορείς και με ανοιχτό: OpenTelemetry Collector + Grafana Tempo/Mimir. Το Langfuse προσθέτει έτοιμο UI, agent graph, evals και κόστος ανά trace. Το ίδιο OTLP τροφοδοτεί και τα δύο, άρα η επιλογή δεν είναι μόνιμη.
Πρέπει να στέλνω prompts και completions στα traces;
Όχι από προεπιλογή. Τα αρχικά telemetry περιέχουν metadata (model, tokens, durations) — αρκετά για το 90% του monitoring. Το content capture είναι opt-in και θέλει redaction για PII.
Πώς εντοπίζω agent που μπαίνει σε loop;
Με counter σε tool calls ανά observation και alert όταν ξεπερνάει όριο, συν μετρική σε επαναλαμβανόμενα ονόματα tools. Το aggregated agent graph δείχνει τους κύκλους οπτικά.
Λειτουργεί με Vercel AI SDK;
Ναι. Το AI SDK έχει ενσωματωμένο telemetry που εξάγει OTel spans για generations και tool calls, και συνδέεται απευθείας με OTLP backends όπως το Langfuse. Δες το tutorial για το SDK 7.
Είναι τα GenAI conventions σταθερά;
Όχι ακόμα — βρίσκονται υπό ενεργή ανάπτυξη και ορισμένα attributes αλλάζουν. Γι' αυτό διατηρούμε τα δεδομένα σε OTLP backend: αλλάζεις consumer χωρίς να ξαναγράφεις instrumentation. Αν θες τις νεότερες συμβάσεις, συχνά χρειάζεται OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental.
Πηγές
- OpenTelemetry — GenAI Observability (επίσημο blog: ρύθμιση export, Aspire Dashboard, visualization)
- open-telemetry/semantic-conventions-genai (repo των conventions)
- Langfuse — OpenTelemetry (OTEL) (endpoints, headers, collector config, attribute mapping)
- Langfuse — AI Agent Observability (observation types, agent graphs, distributed tracing, evals)
- Langfuse — Integrations (frameworks, coding agents, voice)
Διαφάνεια: τα endpoints, headers και ονόματα attributes επαληθεύτηκαν στα επίσημα docs τον Οκτώβριο 2026. Τα GenAI conventions εξελίσσονται — έλεγξε την έκδοση πριν το κλειδώσεις σε production.