본문으로 건너뛰기
mcpd
데몬 없음
소스

실행 찾기

조사는 대시보드가 아니라 불만에서 시작합니다. 이 페이지는 그 불만을 그것을 일으킨 실행 하나로 바꾸는 방법입니다.

라벨이 아니라 페이로드를 검색하십시오

대부분의 트레이싱 도구는 이름과 속성을 색인합니다. mcpd는 프롬프트와 컴플리션까지 색인합니다. 사람이 불만을 제기하는 대상은 모델이 한 말이고, 그 사람이 인용해 주는 낱말이 쥘 수 있는 유일한 손잡이이기 때문입니다. q 파라미터가 그 색인입니다.

Search
shipped on Tuesdayscore_below=0.5min_duration_ms=5000model=gpt-4o AND status=error
  • 3 hits the run whose completion said it
  • 12 hits every answer a scorer marked down
  • 7 hits runs slower than five seconds
  • 2 hits failures on one model

q가 정확히 무엇을 덮는가

색인은 스팬마다 세 필드를 담습니다. 이름, 입력, 출력입니다. 입력과 출력은 JSON이므로, 색인하기 전에 mcpd가 페이로드를 훑으며 문자열과 숫자만 남기고 중괄호와 따옴표와 키는 버립니다. 날것의 JSON을 색인하면 구두점 하나하나가 토큰이 되어, 정작 찾는 낱말이 묻혀 버립니다.

이것에 기대기 전에 알아 둘 만한 결과가 여섯 있습니다.

  • 실행은 그 안의 스팬 하나라도 일치하면 걸립니다. 돌아오는 것은 실행 전체이니, 어느 단계였는지는 하나를 열어 보십시오.
  • 낱말은 AND로 묶입니다. q=refund delayed는 색인된 텍스트 어디에서든 둘 다를 담은 스팬을 찾으며, 둘이 붙어 있어야 하는 것은 아닙니다.
  • 일치는 낱말 단위이고 대소문자를 가리지 않으며, 발음 구별 기호는 같은 것으로 취급합니다. 그래서 resume이 résumé를 찾습니다. 어간 추출은 없습니다. refund는 refunds를 찾지 못합니다.
  • 입력과 출력 각각의 앞 64 KiB가 색인에 들어갑니다. 그보다 긴 페이로드도 통째로 저장되고 통째로 표시됩니다. 아주 긴 프롬프트의 꼬리만 검색되지 않습니다.
  • 마스킹은 저장 전에 이루어지므로, 마스킹이 지운 것은 색인에도 애초에 들어가지 않았습니다. 마스킹과 보존 기간을 보십시오.
  • 오래 도는 스팬이 끝날 때처럼 스팬이 두 번 이상 쓰이면 자기 색인 행을 대체하므로, 검색은 언제나 현재 버전을 반영합니다.

따옴표 하나는 오류가 아니라 검색어입니다

그 아래의 색인에는 자기만의 질의 언어가 있습니다. 큰따옴표는 구절을 열고, NEAR와 OR와 *는 연산자입니다. 입력한 텍스트를 그대로 넘긴다면, 따옴표가 들어 있는 고객 메시지가 결과 대신 구문 오류로 돌아올 것입니다. 예외를 던질 수 있는 검색창은 사람들이 쓰지 않게 되는 검색창입니다.

그래서 입력한 텍스트는 그 파서에 닿지 않습니다. mcpd는 입력된 것을 공백으로 나누고, 각 낱말에서 큰따옴표를 떼어 내고, 각 낱말을 리터럴 항으로 감싼 뒤 AND로 잇습니다. q=say "no"는 say와 no라는 낱말을 찾습니다. q=NEAR는 NEAR라는 낱말을 찾습니다. 입력할 수 있는 어떤 것도 구문 오류가 아니고, 쓸 수 있는 연산자도 없습니다. 그것이 맞바꾼 것이고, 일부러 이 방향으로 정했습니다.

공백뿐인 q는 아무것도 일치시키지 않는 대신 버려지고, 비어 있는 q=는 없는 것으로 봅니다. 모든 파라미터가 그렇게 동작하므로, 빈 필터 상자를 함께 보내는 폼이 실수로 빈 문자열을 걸러 내는 일은 없습니다.

실행 목록 좁히기

GET /v1/api/traces는 다음을 받습니다. 빼놓은 것은 적용되지 않습니다.

파라미터고르는 것
q 각 스팬의 이름과 입력과 출력에 대한 전문 검색.
status unset, ok, error 또는 cancelled. 실행 안의 스팬이 하나라도 실패하면 그 실행은 error이며, 오류를 잡아낸 스팬이라도 마찬가지입니다.
kind 실행의 루트 스팬 종류. agent_run이나 workflow_run 같은 것입니다.
name 루트 스팬의 이름과 정확히 일치. 접두사도 패턴도 아닙니다.
tags 쉼표로 구분합니다. 실행은 나열된 태그를 전부 달고 있어야 하며, 그중 아무거나가 아닙니다.
session_id한 대화 안의 모든 실행.
thread_id한 스레드 안의 모든 실행.
user_id한 최종 사용자의 모든 실행.
environmentproduction 같은 환경 하나.
model 이 모델 id를 상대로 한 스팬이 하나 이상 있는 실행.
from, to 구간. 실행의 시작 시각을 기준으로 잽니다.
min_duration_ms 적어도 이만큼 느린 실행. 밀리초 단위입니다.
min_cost 적어도 이만큼 비용이 든 실행. 값이 매겨진 스팬이 없는 실행은 0으로 세므로, 0보다 큰 값을 주면 그런 실행은 빠집니다.
has_score 이 스코어러 id의 점수를 달고 있는 실행.
score_below has_score와 함께 쓰면, 그 스코어러가 이 값 아래로 매긴 실행.
sort start_time, duration, cost, span_count 또는 score. 기본값은 start_time입니다. score는 어느 스코어러든 매긴 것 중 가장 낮은 점수로 실행을 정렬하므로, 오름차순이면 가장 나쁜 실행이 맨 앞에 옵니다. 점수가 없는 실행은 0으로 순위가 매겨집니다.
order asc 또는 desc. 기본값은 desc입니다.
limit 페이지 크기. 1에서 500 사이로 조정됩니다. 기본값은 50입니다.
cursor 앞 페이지에서 받은 불투명한 커서.
project 어느 프로젝트를 읽을지. 기본값은 데몬 자신의 기본 프로젝트이므로, 프로젝트가 하나뿐인 설치에서는 넘길 일이 없습니다.

name과 상관 정보 id들과 environment는 동등 비교이고, 그래서 값이 쌉니다. 이름의 일부만 맞추고 싶다면 q를 쓰십시오. 스팬 이름도 텍스트 색인에 들어 있습니다.

목록 한 줄이 치르는 비용

더 가벼운 두 번째 실행 목록은 없습니다. 첫 번째가 이미 가볍기 때문입니다. 저장된 트레이스 요약본은 입력도, 출력도, 속성도 전혀 싣지 않습니다. 이름, 개수, 시각, 비용, 태그, 상관 정보뿐이고, 모델이 한 말은 하나도 없습니다. 페이로드를 가져오는 것은 실행을 열 때입니다.

현실적인 실행 하나로 재 보았습니다. 정책 문서 세 건을 검색하고, 도구를 두 번 호출하고, 실제 프롬프트와 컴플리션으로 모델을 두 번 호출하는 여덟 단계짜리 지원 에이전트입니다. 목록 한 줄은 577바이트, 실행 전체는 10,717바이트로 94.6퍼센트가 줄어듭니다. 이 데몬으로, 그 실행에 대해, 실제 HTTP 표면 위에서 잰 값입니다. 실행마다 값은 다를 것이고, 중요한 것은 방향입니다. 프롬프트가 커질수록 절약도 커집니다. 커지는 부분이 바로 목록이 싣지 않는 부분이기 때문입니다.

이 수치는 여기에 타이핑된 것이 아니라 테스트가 만들어 냅니다. 그래서 한쪽만 낡고 다른 쪽은 멀쩡한 일이 생기지 않습니다. cargo test -p mcpd-server --test payload_size -- --nocapture를 돌리면 두 바이트 수를 모두 출력합니다. 페이로드를 싣는 필드를 요약본에 추가하면 그 테스트가 실패합니다.

그 밖의 것은 이름을 밝혀 거절합니다

저 표가 어휘의 전부입니다. 표에 없는 파라미터는 받아들여진 뒤 버려지는 대신, 그 이름을 밝히는 400으로 거절됩니다. 이것은 바뀐 동작입니다. 예전의 mcpd는 모르는 파라미터를 무시했기 때문에, ?statuss=error가 프로젝트의 모든 실행을 담은 200으로 돌아왔고, 필터가 적용되지 않았다는 사실을 응답의 어디에서도 알 수 없었습니다. 그럴듯한 오답은 오류보다 나쁩니다. 알아차릴 거리가 없기 때문입니다.

여기서 맞바꾼 것도 짚어 둘 만합니다. 더 새로운 데몬이 이해하는 필터를 보내는 클라이언트는 이제 더 넓은 결과 대신 옛 데몬에서 400을 받습니다. 두 실패 중에서는 그쪽이 낫습니다. 400은 그 파라미터의 이름을 말하고 거기서 멈춥니다.

"알 수 없음"보다 더 많은 것을 말하는 거절도 있습니다. mcpd는 모든 트레이스에 service_name, service_version, resource_id, run_id, request_id, experiment_id, release를 기록하지만, 실행 목록은 아직 그것들로 거를 수 없습니다. 일곱 가운데 디스커버리 패싯은 service_name 하나뿐이므로, 디스커버리가 값을 나열해 주는 것도 그것 하나입니다. 그 일곱은 그렇게 말하는 메시지와 함께 거절됩니다. service_name=checkout-api는 오타가 아니고, 그것을 알 수 없는 파라미터라고 말하면 있지도 않은 철자 오류를 찾아 헤매게 만들기 때문입니다.

이 규칙은 실행 목록만의 것이 아닙니다. GET /v1/api/spans, /v1/api/logs, /v1/api/scores, /v1/api/metrics, /v1/api/feedback이 각각 자기 어휘를 같은 방식으로 강제합니다. 마주치기 전에 알아 둘 만한 것이 둘 있습니다. 점수 목록은 order를 받지 않습니다. 최신순으로 고정되어 있고 그 커서가 한 방향으로만 비교하기 때문입니다. 그리고 피드백 목록은 trace_id를 요구합니다. mcpd가 아직 프로젝트 전체의 피드백을 나열할 수 없고, 빈 목록은 피드백이 없다는 뜻으로 읽힐 것이기 때문입니다.

필터 상자 채우기

무엇을 입력해야 하는지 알아야 필터를 쓸 수 있는데, 그 값들은 고정된 목록이 아닙니다. 프로젝트에 보고하는 서비스들이 보내는 것이 곧 그 값입니다. GET /v1/api/discovery가 그 답을 줍니다. facet으로 한 번에 한 차원씩 물으면, 그 차원이 프로젝트 안에서 취해 온 값들을 값 순서로 돌려주고, 각각에 그 값을 실은 시그널이 마지막으로 언제 왔는지가 붙습니다.

패싯나열하는 것
entity_kind에이전트, 워크플로, 도구, 모델, 스코어러, 프로세서, 파이프라인 또는 서비스.
entity_name호출된 도구처럼, 그것들이 가진 이름.
service_name보고해 온 서비스들.
environmentproduction처럼, 관측된 환경들.
tag어느 실행이든 달고 있는 모든 태그.
metric_name기록된 메트릭 이름들. mcpd가 파생시키는 것까지 포함합니다.
metric_label_key프로젝트에서 관측된 모든 라벨 키.
metric_label_value라벨 하나의 값들. key가 필요합니다.

prefix는 자동완성을 위해 목록을 좁히며, 글자 그대로 대소문자를 가려 맞춥니다. 이 값들은 필터가 대조될 대상이고, 여기서 대소문자를 뭉개면 아무것도 맞지 않는 값을 제시하게 되기 때문입니다. limit은 답을 1000개로 자릅니다. 값은 최신순이 아니라 값 순서로 정렬되므로, 누군가 읽고 있는 목록이 폴링 사이에 발밑에서 뒤섞이지 않습니다.

이것을 상대로 무언가를 만들기 전에 알아 둘 한계가 하나 있습니다. metric_label_key는 이름 붙은 메트릭 하나의 키가 아니라, 프로젝트 어디에서든 관측된 모든 라벨 키를 나열합니다. 고유 값 목록의 키는 그 라벨이지, 그것을 실어 온 메트릭이 아니기 때문입니다.

다시 돌리지 않고 목록 새로 고치기

GET /v1/api/deltas는 커서 이후로 도착한 것을 오래된 것부터 돌려줍니다. traces, spans, logs에 대해 쓸 수 있습니다. 지난번 폴링에서 받은 커서를 들고 있다가 다시 보내고, 돌아온 행들을 이미 들고 있는 목록에 반영하십시오.

폴링은 페이징이 아니고, 두 커서는 일부러 서로 바꿔 쓸 수 없게 되어 있습니다. 페이징은 고른 정렬로 고정된 결과 집합을 거슬러 걷습니다. 폴링은 도착 순서로 앞으로 걸으며 끝나지 않습니다. 워터마크는 행의 도착 시각과 그 id를 함께 씁니다. 배치는 그 안의 모든 행을 하나의 타임스탬프로 쓰기 때문에, "이 나노초 이후"밖에 말하지 못하는 워터마크라면 배치 전체를 다시 재생하거나 그 나머지를 건너뛸 것입니다. 아무것도 없는 폴링은 커서를 그대로 돌려줍니다. 커서를 앞으로 옮기면, 잠시 뒤에 더 이른 도착 시각을 달고 들어오는 행을 놓치게 되기 때문입니다.

점수와 피드백은 폴링할 수 없습니다. 호출자가 보낸 타임스탬프가 찍히고 서버가 정하는 도착 시각이 없어서, 그 위의 워터마크는 늦게 온 행을 건너뛰거나 모든 행을 영원히 반복하게 됩니다. signal=scores는 빈 페이지로 답하는 대신 거절됩니다.

이 배포가 할 수 있는 것

mcpd에는 저장소 트레이트가 하나 있고 그 뒤에 백엔드가 셋 있는데, 셋은 동등하지 않습니다. ClickHouse에서는 삭제를 받아들인 뒤 호출이 돌아온 다음에 적용하므로, 응답의 개수는 디스크에서 사라진 것이 아니라 일치한 것을 말합니다. SQLite와 Postgres에서는 호출이 돌아왔을 때 행이 이미 사라져 있습니다. GET /v1/api/capabilities가 그것이 적혀 있는 곳입니다. 데몬 뒤에 어떤 백엔드가 있는지, q가 무엇을 맞추는지, ASCII 밖에서도 대소문자를 뭉개는지, 호출이 돌아왔을 때 삭제가 디스크에 반영되어 있는지, 그리고 디스커버리와 델타 폴링이 답하기는 하는지가 있습니다.

연결할 때 한 번 읽고, 그 배포가 답하는 것만 제공하십시오. 기능이 거짓인 엔드포인트는 501과 함께 거절하며, 그 본문의 capability 필드는 기능 목록이 발표하는 것과 같은 낱말을 씁니다. 그래서 둘이 어긋날 수 없고, 그 거절을 만난 클라이언트는 절대 바뀌지 않을 것을 다시 시도하는 대신 찾아볼 수 있습니다.

오늘 제공되는 백엔드 셋은 모두 디스커버리와 델타 폴링에 답하므로, 아래의 501은 SQLite나 Postgres나 ClickHouse에서 보게 될 응답이 아니라 나중에 추가되는 백엔드가 거절할 때의 모양입니다. 어느 쪽인지 말해 주는 것이 기능 목록이고, 지금도 읽어 둘 가치가 있습니다. 셋은 호출이 돌아왔을 때 삭제가 디스크에 반영되어 있는지에서, 그리고 보존을 어떻게 강제하는지에서 서로 다릅니다. ASCII 밖의 대소문자는 셋 모두 뭉갭니다.

무엇으로 거를 수 있는가?

# Every environment this project has reported, for a filter box.
curl -s --get http://localhost:4317/v1/api/discovery \
  --data-urlencode 'project=default' \
  --data-urlencode 'facet=environment'

# Typeahead: the service names starting with "check".
curl -s --get http://localhost:4317/v1/api/discovery \
  --data-urlencode 'facet=service_name' \
  --data-urlencode 'prefix=check'

# The values one metric label takes.
curl -s --get http://localhost:4317/v1/api/discovery \
  --data-urlencode 'facet=metric_label_value' \
  --data-urlencode 'key=model'

시간 구간

from과 to는 RFC 3339 타임스탬프를 받거나, 상대적인 축약형을 받습니다. 빼기 기호와 숫자, 그리고 s, m, h, d 중 하나입니다. -15m, -1h, -7d 모두 됩니다. 축약형이 있는 이유는, 지난 한 시간을 보려고 ISO 타임스탬프를 손으로 치는 것은 아무도 치르지 않아야 할 마찰이기 때문입니다.

from은 포함이고 to는 제외이며, 둘 다 실행이 시작한 시각과 비교합니다. 지정한 구간 이전에 시작해 그 안에서도 계속 돌고 있던 긴 실행은 돌아오지 않습니다. 09:00에 돌고 있던 무언가를 쫓고 있다면, from을 좁히지 말고 뒤로 넓히십시오.

타임스탬프도 기간도 아닌 값은 무시되는 대신 이름을 밝혀 거절됩니다. 모든 목록 엔드포인트의 모든 파라미터가 이렇게 실패합니다. 조용히 버려진 필터는 그럴듯한 오답을 돌려주고, 그럴듯한 오답은 오류보다 나쁘기 때문입니다.

from=yesterday

{
  "error": "bad_parameter",
  "field": "from",
  "message": "\"yesterday\" is not an RFC 3339 timestamp or a relative duration like -1h"
}

나쁜 답변을 찾아내는 쿼리

has_score=answer_relevance&score_below=0.5는 그 스코어러가 0.5 아래로 매긴 모든 실행을 돌려줍니다. 불만이 실패가 아니라 품질에 대한 것일 때 집어 들 것이 이것입니다. 나쁜 답변은 대개 성공한 실행이기 때문입니다.

has_score만 쓰면 그 스코어러가 본 모든 실행이 돌아옵니다. score_below만 쓰면 아무 일도 일어나지 않습니다. 스코어러가 붙지 않은 임계값은 뜻이 없고, mcpd가 어느 스코어러를 말한 것인지 짐작해야 하기 때문입니다. mcpd는 짐작하지 않습니다. 그 값들이 어디에서 오는지는 점수와 피드백이 다룹니다.

실행 대신 단계 검색하기

GET /v1/api/spans는 모든 실행을 가로질러 개별 단계를 한 번에 검색합니다. 세션이 아니라 단계에 대한 질문에 쓰십시오. 어느 도구 호출이 느린가, 어느 모델 호출이 오류를 냈는가, 검색 한 번에 얼마나 걸리는가 같은 것입니다.

q, kind, status, name, model, trace_id, from, to, min_duration_ms, sort, order, limit, cursor를 같은 뜻과 같은 기본값으로 받습니다. 여기에는 tags, session_id, user_id 필터가 없습니다. 그것들은 실행에 속하는 것이고, 셋 모두 받아들여진 뒤 버려지는 대신 이름을 밝혀 거절됩니다.

sort는 실행 목록에서보다 받는 값이 적습니다. start_time, duration, cost뿐이고 그 밖에는 없습니다. 스팬은 스팬 개수도 점수 롤업도 싣지 않으므로 span_count와 score는 정렬할 대상이 없고, 무시되는 대신 이름을 밝혀 거절됩니다.

실행에서 model은 어느 스팬이든 그 모델을 썼는지를 묻습니다. 스팬에서 model은 그 스팬 자신의 모델입니다. 같은 낱말, 다른 두 질문입니다.

찾아내기

# Failed runs from the last day that mention both words, anywhere in a
# span name, a prompt or a completion.
curl -s --get http://localhost:4317/v1/api/traces \
  --data-urlencode 'q=Leeds depot' \
  --data-urlencode 'status=error' \
  --data-urlencode 'from=-24h'

# Every step that called one model and failed, across every run at once.
curl -s --get http://localhost:4317/v1/api/spans \
  --data-urlencode 'model=gpt-4o-mini' \
  --data-urlencode 'status=error' \
  --data-urlencode 'min_duration_ms=2000'

페이징, 그리고 커서가 불투명한 이유

목록은 최대 limit개의 행과 page 객체를 돌려줍니다. 더 있으면 page.next_cursor가 있고, page.has_more가 같은 말을 불리언으로 합니다.

페이징은 오프셋이 아니라 키셋 방식입니다. 커서는 받은 마지막 행을 가리키고, 다음 페이지는 그 뒤에서부터 엄격하게 시작합니다. 오프셋 페이징은 밀립니다. 트레이스는 계속 도착하므로, 50번부터 99번 행을 달라고 할 때쯤이면 창이 발밑에서 옮겨 가 어떤 실행은 두 번 읽히고 어떤 실행은 아예 나타나지 않습니다. 조사에서는 놓친 그 실행이 바로 찾던 실행입니다.

커서는 정렬 값과 행 id를 16진수로 인코딩한 쌍이며, 뜻이 없는 것으로 다루어야 합니다. 불투명한 이유는 모든 호출자를 깨뜨리지 않고 인코딩을 바꿀 수 있어야 하기 때문이고, 그 안의 값이 정렬마다 다른 양이기 때문입니다. start_time과 duration에는 나노초, cost에는 배율을 곱한 정수, span_count에는 평범한 개수, score에는 그 실행의 가장 낮은 점수입니다.

거기에 규칙이 하나 붙습니다. 커서는 그것을 만들어 낸 것과 같은 필터, 같은 sort, 같은 order와 함께 돌려보내십시오. mcpd는 그렇게 했는지 확인하지 않으며, 다른 정렬에 대고 다시 쓴 커서는 엉뚱한 열과 비교하여 행을 건너뜁니다. 편집되었거나 잘린 커서는 invalid_query와 malformed cursor 메시지로 거절됩니다.

총계는 없습니다. page.total은 일치 개수를 싸게 셀 수 있는 백엔드를 위해 남겨 둔 자리이고, 기본 제공되는 SQLite 저장소는 채우지 않습니다. 그러니 어떤 숫자를 향해 세지 말고 has_more로 반복하십시오.

페이지 넘기기

# The first page hands back a cursor in page.next_cursor.
curl -s --get http://localhost:4317/v1/api/traces \
  --data-urlencode 'q=refund' \
  --data-urlencode 'sort=cost' \
  --data-urlencode 'limit=100'

# The next page: the same filters, the same sort, plus the cursor.
curl -s --get http://localhost:4317/v1/api/traces \
  --data-urlencode 'q=refund' \
  --data-urlencode 'sort=cost' \
  --data-urlencode 'limit=100' \
  --data-urlencode 'cursor=MzEzNzQyfDBhZjM...'

실행을 찾은 뒤에

이 쿼리들에서 얻은 트레이스 id는 GET /v1/api/traces/{trace_id}로 가져가면 됩니다. 그 실행과 그 안의 모든 스팬을 돌려주며, 두 번째 요청 없이 워터폴이 그려지도록 이미 순서가 잡혀 있습니다. include=logs,scores,feedback을 붙이면 그것에 딸린 모든 것을 같은 응답으로 받습니다.

거기서부터, 비용 산정이 각 단계의 수치를 설명하고, HTTP API 레퍼런스가 명세가 선언한 대로 모든 엔드포인트와 필드를 나열합니다.