Kotlin
Önce coroutine. Etkin span coroutine bağlamında durur, iptal bir başarısızlık olarak değil iptal olarak kaydedilir ve buradaki hiçbir şey ajanınıza hata fırlatmaz.
Kurulum
Kotlin 2.4 ile derlenir, JVM 17 veya üstü gerekir. Tek bağımlılıklar kotlinx coroutines ve serialization. HTTP işini JDK'nın kendi istemcisi yapar, yani HTTP konusunda zaten bir tercihi olan bir servise hiçbir şey eklenmez.
build.gradle.kts
dependencies {
implementation("dev.mcpd:mcpd-kotlin:0.1.0")
} İstemciyi oluşturma
Süreç başına bir örnek. McpdConfig alanlarının her biri
varsayılanını bir ortam değişkeninden okur ve kodda da verilebilir, böylece
aynı kod tek bir düzenleme olmadan hem geliştirmede hem üretimde çalışır.
Argümansız Mcpd(), yerel bir mcpd ile çalışır.
bağlanma
// Every field also reads an environment variable (MCPD_ENDPOINT,
// MCPD_PROJECT, MCPD_API_KEY, ...), so `Mcpd()` on its own works too.
val mcpd = Mcpd(
McpdConfig(
endpoint = "http://localhost:4317",
project = "support-desk",
environment = "development",
serviceName = "support-agent",
serviceVersion = "1.4.0",
)
) Kod ortam değişkenine, ortam değişkeni de varsayılana üstün gelir.
| Alan | Ortam değişkeni | Varsayılan |
|---|---|---|
endpoint | MCPD_ENDPOINT | http://localhost:4317 |
apiKey | MCPD_API_KEY | yok |
project | MCPD_PROJECT | default |
environment | MCPD_ENVIRONMENT | yok |
serviceName | MCPD_SERVICE_NAME | yok |
serviceVersion | MCPD_SERVICE_VERSION | yok |
sample | MCPD_SAMPLE | 1.0 |
enabled | MCPD_ENABLED | true |
capturePayloads | MCPD_CAPTURE_PAYLOADS | true |
logLevel | MCPD_LOG_LEVEL | debug |
batchSize | MCPD_BATCH_SIZE | 128 |
flushInterval | MCPD_FLUSH_INTERVAL_MS | 2s |
maxQueue | MCPD_MAX_QUEUE | 8192 |
timeout | MCPD_TIMEOUT_MS | 10s |
enabled = false, her kayıt çağrısını yine de geçerli span'ler
döndüren boş bir işleme çevirir; böylece testlerde sınanan koda dokunmadan
mcpd kapatılabilir. Okumalar yine sunucuya gider: bir sorgu, çağıranın
yanıtını beklediği bir şeydir ve boş bir yanıt yalan olurdu.
Bir çalıştırmayı kaydetme
span alışılmış biçimdir. Span'i kapatır, bloğun çağırdığı her
şey için onu etkin span yapar ve blok ne döndürdüyse onu döndürür. Hata
fırlatan bir blok, istisnayı taşıyan bir error span'i bırakır ve
istisnayı olduğu gibi yeniden fırlatır.
kapsamlı biçim
// Bloğun değeri span'in değeridir, yani ölçümleme sizi kodu onun çevresinde
// yeniden kurmaya zorlamaz.
val answer = mcpd.span("retrieve_context", SpanKind.RETRIEVAL) { span ->
span.setInput(query)
val docs = index.search(query)
span.setOutput(docs.map { it.id })
span.setAttribute("retrieval.hits", docs.size)
docs
}
İptal edilen bir blok farklıdır ve farklı kaydedilir. Çağıranı ya da bir
zaman aşımı tarafından terk edilen bir coroutine, span'i error
yerine cancelled olarak kapatır; çünkü bilerek yapılan bir
terk, işlemin başarısızlığı değildir ve trace listesinde öyle okunmamalıdır.
bir çalıştırmayı kaydetme
val question = "where is my order 41?"
// `span` starts the operation, ends it, and records anything it throws.
// Inside the block the span is the active one, so the helpers below find
// their parent through the coroutine context rather than being handed it.
val traceId = mcpd.span(
"support_agent",
SpanKind.AGENT_RUN,
input = question,
sessionId = "session-8f21",
userId = "customer-4477",
tags = listOf("support", "email"),
) { run ->
mcpd.log(LogLevel.INFO, "handling a question", mapOf("channel" to "email"))
val plan = planAnswer(mcpd, question)
val order = lookUpOrder(mcpd, 41)
val estimate = estimateDelivery(mcpd, 41)
val answer = buildString {
append(plan).append(' ').append(order)
if (estimate != null) append(" It should arrive ").append(estimate).append('.')
}
run.setOutput(answer)
mcpd.metric("support.answers", 1.0, MetricKind.COUNTER, unit = "{answer}")
// The block's value is the value of `span`.
run.traceId
} Blok yapısı size uymuyorsa span'i kendiniz başlatıp kendiniz kapatın.
bloksuz
val run = mcpd.trace("nightly_reindex", SpanKind.WORKFLOW_RUN, tags = listOf("batch"))
try {
reindex()
run.end()
} catch (e: Exception) {
run.setError(e)
run.end()
} Bağlam
Etkin span, coroutine bağlamının bir ögesidir; yani thread'i değil
coroutine'i izler. Birkaç katman aşağıdaki bir suspend fonksiyon, hiçbir
çağıran ona bir üst vermeden doğru üst span'in altında bir alt span açar ve
aynı dispatcher üzerindeki iki çalıştırma birbirinin span'lerini hiç görmez.
Etkin span'i mcpd.currentSpan() ile okuyun, başlığını ise
mcpd.traceparent() ile alın.
iç içe geçme
mcpd.span("support_agent", SpanKind.AGENT_RUN) { run ->
// draft'a hiçbir şey geçilmez. Kaç suspend çağrı aşağıda olursa olsun,
// çalıştırmayı coroutine bağlamından bulur.
run.setOutput(draft(question))
}
suspend fun draft(question: String): String =
mcpd.span("draft_answer", SpanKind.MODEL_GENERATION) { generation ->
generation.setInput(question)
"..."
}
Servisler arasında bağlam, örnekleme kararını da birlikte taşıyan W3C
traceparent başlığında gider; böylece bir trace hiçbir zaman
yarım kaydedilmez. Bozuk bir başlık, onu taşıyan isteği başarısız kılmak
yerine yeni bir trace başlatır.
servis sınırının ötesinde
// Giden: başlığı bir sonraki servise verin.
request.header("traceparent", span.traceparent())
// Gelen: yeni bir trace başlatmak yerine o trace'i sürdürün.
mcpd.span("checkout", traceparent = headers["traceparent"]) { span ->
charge(span)
} Model, kullanım ve hatalar
mcpd alım sırasında token sayılarını maliyete çevirir, dolayısıyla trace'te
gördüğünüz rakam size faturalanan rakamdır. Bir hatayı kaydetmek aynı
hamlede span'in durumunu error yapar ve her zaman bir mesaj
bulunur: "bir şey başarısız oldu" ile kimse bir şey yapamaz.
bir span neler taşır
generation.setModel("gpt-4o", provider = "openai")
generation.setUsage(inputTokens = 1_180, outputTokens = 96)
// setError elinizdeki throwable'ı alır; mesaj, sınıf ve yığın izinin hepsi
// ondan gelir.
tool.setError(e, retryable = true)
// Ya da doğrudan bir mesaj; iki durumda da mesaj zorunludur.
tool.setError("the shipping service returned 503")
Geri kalanlar setInput, setOutput,
setMetadata, setAttribute,
setAttributes, setTags,
setCorrelation ve addLink; ayrıca açık bir alt span
için child, bir zaman noktası için event. Hepsi her
thread'den güvenle çağrılabilir ve hiçbiri hata fırlatmaz. Başka bir izleme
sisteminin ağacına ait bir span, onu externalParentSpanId ile
adlandırır; bu değer ilişkilendirme için taşınır ve mcpd'nin kendi ağacında
hiçbir zaman üst olarak kullanılmaz.
Payload'lar, sink'ler ve bitmiş span'ler
capturePayloads = false, input ve
output alanlarını sürecin içinde tutar: kayıt kuyruğa varmadan
atılırlar, böylece bir flush sırasında hassas hiçbir şey dışarı çıkmaz; tek
bir span bunu kendisi için geçersiz kılabilir. Exporter her yığını bir
Sink nesnesine verir: varsayılan olarak HTTP taşıyıcısına, bir
test için yığınları bellekte tutan bir RecordingSink nesnesine
ya da sinyal başına tek bir JSON satırı yazan bir ConsoleSink
nesnesine. Hata fırlatan bir sink, aralık başına bir kez bildirilen ve
çağırana asla iletilmeyen başarısız bir yığın demektir.
yakala, kaydet, doğrula
// Prompt'ları ve tamamlamaları sürecin içinde tutun: span yine zamanlamasını,
// durumunu, kullanımını, modelini, ilişkilendirmesini ve hatalarını taşır.
val quiet = Mcpd(McpdConfig(capturePayloads = false))
quiet.span("draft_answer", input = question, capturePayloads = true) { } // bu tutulur
// Başka bir izleyicinin bitirdiği bir span, verildiği gibi kuyruğa girer: üstü,
// zamanları ve örneklemesi değiştirilmez. Bozuk bir kimlik atılır ve sayılır,
// hiç fırlatılmaz.
mcpd.record(finishedSpan)
// Kaydedileni sunucu olmadan doğrulayın.
val recorder = RecordingSink()
val local = Mcpd(McpdConfig(sink = recorder))
local.span("support_agent") { run -> run.setUsage(inputTokens = 12, outputTokens = 3) }
local.flush()
check(recorder.spans.single().usage?.inputTokens == 12L)
// queued, dropped, capacity, exported, failed, rejected.
val stats = local.stats() Kalıcı çalıştırmalar
Daemon'ın kalıcı çalıştırmalarını da aynı istemci karşılar.
Workflow tanımı kurar ve adım gövdelerini suspend lambda olarak
saklar; bir Worker bu tanımı kaydeder, lease'ler için uzun
yoklama yapar ve her birini bir coroutine üzerinde karşılar;
startRun, watchRun, publishEvent ve
diğerleri ise istemci üzerinde çalıştırma yüzeyini bire bir yansıtan
uzantılardır. Bir gövde, adımın bütçesi içinde yeniden denenmek için
RetryableException fırlatır ve StepContext
üzerinden kendi adımının altına span kaydeder. Bağlam üzerinden açılan her
span, bittikçe kendi kullanımını adımınkine ekler; böylece model çağrılarını
kaydeden bir gövde token'ları hiç elle toplamaz. setUsage ise
span'lerin topladığını geçersiz kılar. Bir Worker,
onError yanında dört isteğe bağlı hook alır
(onLease, onStepStart, onStepEnd,
onConflict) ve stats() çağrısını yanıtlar:
talepler, yolda olan gövdeler, tamamlamalar, sınıfına göre başarısızlıklar,
nedenine göre çakışmalar ve karşılanamayan lease'ler.
bir iş akışı
val support = Workflow("support_ticket")
.step("classify") { ctx ->
val ticket = ctx.input<JsonObject>()["ticket"]!!.jsonPrimitive.content
mapOf("kind" to if (ticket.startsWith("Q")) "question" else "bug")
}
.agent("draft", StepOptions(input = mapOf("kind" to "\${steps.classify.output.kind}"))) {
ctx ->
// Your own model call, recorded under this step in the run's trace.
// Its usage is summed into the step, so the daemon prices the run.
ctx.span("model_call", SpanKind.MODEL_GENERATION) { span ->
span.setModel("claude-sonnet-4", "anthropic")
span.setUsage(inputTokens = 812, outputTokens = 37)
}
"an answer to a " + ctx.input<JsonObject>()["kind"]!!.jsonPrimitive.content
}
.waitFor(
"approval", "approved", "\${input.ticket}", 7.days,
StepOptions(guard = predicate("steps.classify.output.kind", "eq", "bug")),
)
.step(
"send",
StepOptions(
input = mapOf("text" to "\${steps.draft.output}"),
effect = Effect.AT_MOST_ONCE,
retry = RetryPolicy(
maxAttempts = 3, backoff = Backoff.EXPONENTIAL, initial = 500.milliseconds,
),
),
) { ctx ->
val text = ctx.input<JsonObject>()["text"]!!.jsonPrimitive.content
if (!deliver(text, ctx.idempotencyKey)) {
throw RetryableException("mail relay busy", retryAfter = 2.seconds)
}
mapOf("sent" to true)
}
.output(mapOf("sent" to "\${steps.send.output.sent}")) Her yapının ne yaptığı, lease protokolü ve çalıştırma API'si şu sayfalarda: İş akışları, Worker'lar ve Çalıştırmalar.
Puanlar ve geri bildirim
min ve max zorunlu parametrelerdir. Yedi, on
üzerinden çok iyidir ama yüz üzerinden kötüdür; yani ölçeği bildirilmemiş
bir sayı hiçbir şeyle karşılaştırılamaz. Geri bildirim bir insanın
yargısıdır ve puandan ayrı durur; çünkü ikisi farklı sorulara yanıt verir ve
asla birlikte ortalanmamalıdır.
puan ve geri bildirim
// A machine evaluation. `min` and `max` are required: 7 is excellent out of
// 10 and poor out of 100, and a score with no scale cannot be compared.
mcpd.score(
traceId = traceId,
scorerId = "answer_relevance",
value = 0.86,
min = 0.0,
max = 1.0,
reason = "answered the question asked, without the delivery estimate",
)
// A person's judgement, from the thumbs-up control in your own product.
mcpd.feedback(
traceId = traceId,
kind = FeedbackKind.THUMBS,
value = "up",
comment = "fast and clear",
authorId = "customer-4477",
)
// Nothing above touched the network. This is where it goes.
mcpd.flush() Loglar ve metrikler etkin span'e bağlanır, böylece bir grafikteki sıçrama onu doğuran çalıştırmayı açar.
Flush ve kapatma
Kayıt, bellekteki sınırlı bir kuyruğa ekler ve döner; yığınlamayı,
sıkıştırmayı ve göndermeyi bir worker coroutine'i yapar.
flush() kuyrukta bekleyeni gönderir ve bekler.
shutdown() worker'ı durdurur, kalanı gönderir ve iki kez
çağrılması güvenlidir. Son dışa aktarım iptal edilemez biçimde çalışır;
çünkü kapatmaya genellikle iptal sırasında bir finally
bloğundan gelinir ve sürecin neden gittiğini açıklayan yığın da o son
yığındır.
Mcpd bir AutoCloseable olduğu için hem
use hem de bir kapatma hook'u işe yarar. İkisini de çağırmayan
serverless bir fonksiyon ya da kısa ömürlü bir worker son yığınını kaybeder,
çünkü runtime handler döner dönmez zamanlamayı durdurur.
kapatma
// Bir JVM servisi: çıkarken kapatın.
Runtime.getRuntime().addShutdownHook(Thread { mcpd.close() })
// Kısa ömürlü bir worker: use ile kullanın, kapanış süslü parantezinde kapanır.
Mcpd(McpdConfig(project = "batch")).use { mcpd ->
mcpd.span("reindex") { run -> reindex(run) }
} Geri aldıklarınız
Okuma metotları HTTP API'yi bire bir yansıtır ve üretilmiş tipleri döndürür.
Hata fırlatan tek metotlar bunlardır ve McpdException
fırlatırlar: yanıtını beklediğiniz bir sorgu, yutulacak bir şey değildir.
$ gradle run> Task :runsupport_agent: 4 spans, ok agent_run support_agent 79ms model_generation plan_answer 41ms tool_call lookup_order 16ms tool_call estimate_delivery 11ms shipping service returned 503 for order 41 arama ve analiz
val detail = mcpd.getTrace(traceId, include = listOf("scores", "feedback"))
println("${detail.trace.name}: ${detail.spans.size} spans, ${detail.trace.status}")
for (span in detail.spans) {
val ms = (span.durationNs ?: 0) / 1_000_000
println(" ${span.kind} ${span.name} ${ms}ms ${span.error?.message ?: ""}")
}
val summary = mcpd.analyticsSummary(
from = Clock.System.now() - 1.hours,
to = Clock.System.now(),
)
println(
"last hour: ${summary.traceCount} runs, " +
"${summary.errorCount} failed, ${summary.totalCost} USD",
) Güvenebilecekleriniz
-
Ölçümleme hiçbir zaman hata fırlatmaz. Başarısız bir dışa aktarım,
erişilemeyen bir sunucu ya da serileştirilemeyen bir değer,
java.util.loggingüzerinden 30 saniyede en çok bir kez bildirilir, geri kalanında yutulur. Gözlemlenebilirlik, gözlediği şeyi asla devirmemelidir. - Kayıt bloklamaz, yani servisinizin gecikmesi mcpd'nin hızlı ya da ayakta olmasına hiç bağlı değildir.
-
Bellek sınırlıdır. Kuyruk
maxQueuekadar sinyal tutar; bunun ötesinde en eskiler atılır ve sayılır; bu sayıdroppedSignalsalanından vestats()içinden okunur. Sınırsız bir tampon, bir ağ kesintisini bellek yetersizliğinden ölüme çevirir. - Örnekleme kararı bir kez, kökte verilir ve aşağıya kalıtılır.
- Bir span hem başladığında hem bittiğinde dışa aktarılır, yani yolun ortasında çökmüş bir çalıştırma yine de görünür olur. Bir span, başlangıç kaydı gönderilmeden önce biterse kayıt yerinde güncellenir ve yalnızca bir tanesi gönderilir.
- Yığınlar 1 KiB üzerinde gzip ile sıkıştırılır ve yeniden denemeyi atlatan bir idempotency anahtarı taşır; böylece sunucu kaydettikten sonra zaman aşımına uğrayan bir istek iki kez sayılamaz.
- Süreler monotonik bir saatten ölçülür, yani bir NTP sıçraması negatif bir süre üretemez.
Çalıştırma bulma filtreleri eksiksiz anlatır. Veri modeli bu tiplerin taşıdığı her alanı listeler.