에이전트 관측성에서 가장 위험한 순간은 세션이 끝날 때다
코딩 에이전트는 한 세션에서 수백 개의 이벤트를 만든다. 사용자 프롬프트, 모델 응답, 파일 읽기와 수정, shell 명령, 하위 에이전트 호출, token 사용량이 하나의 긴 trace에 들어간다. 그런데 이 trace를 원격 tracking server로 보내는 시점은 대개 세션 종료 hook이다.
이 경로에서 HTTP 업로드를 동기식으로 수행하면 두 문제가 생긴다.
- tracking server가 느리거나 일시적으로 끊기면 종료 hook도 함께 느려진다.
- process가 업로드 도중 종료되면 디버깅에 가장 필요한 실패 trace가 사라진다.
반대로 전송을 단순한 fire-and-forget으로 바꾸면 hook은 빨라지지만 내구성을 잃는다. 메모리에만 있던 trace는 process와 함께 사라지고, 운영자는 기록이 실제 server에 도착했는지 알기 어렵다.
2026년 6월 17일 공개된 MLflow 3.14.0은 이 문제를 로컬 WAL(Write-Ahead Log)과 장기 실행 daemon으로 분리했다. 같은 릴리스에서 @mlflow.test 기반 pytest 회귀 테스트와 Review Queues도 추가했다. 세 기능을 함께 보면 단순한 observability 기능 모음이 아니라 다음 품질 loop가 만들어진다.
- 실행 중 일어난 일을 trace로 남긴다.
- 실패한 trace를 사람이 같은 질문으로 검토한다.
- 확인된 실패를 재현 가능한 regression test로 고정한다.
- pull request마다 같은 실패가 돌아오지 않는지 검사한다.
이 장은 2026년 7월 15일 기준으로 작성했다. MLflow 3.14.0은 공개 후 28일 지난 정식 릴리스로 90일 recency 기준 안에 있다. WAL 세부 동작은
v3.14.0tag의 TypeScript source와 merged pull request를 기준으로 확인했다. Review Queues와@mlflow.test는 3.14.0에서 experimental로 표시되므로 API 고정보다 검증 가능한 운영 계약에 초점을 맞춘다.
3.14에서 바뀐 세 가지 경계
| 경계 | 3.14의 변화 | 운영자가 얻는 것 | 새로 관리할 것 |
|---|---|---|---|
| trace 생성 process와 원격 server | hook이 trace를 로컬 WAL에 fsync한 뒤 반환하고 daemon이 업로드 | 종료 지연과 network 장애를 분리 | WAL disk, daemon, retry, dead letter |
| 평가 결과와 CI | @mlflow.test와 EvaluationResult.passed/reason | agent 행동을 일반 pytest gate로 실행 | scorer 안정성, 비용, fixture, 병렬 실행 |
| 임시 human review와 학습 데이터 | Review Queue의 답을 trace feedback/expectation으로 기록 | 검토 결과를 평가와 dataset에 재사용 | 질문 schema, 인증, reviewer 일관성 |
세 기능의 보장 수준은 서로 다르다.
- WAL의 성공 ACK는 이 host의 disk에 기록됐다는 뜻이지, MLflow server 수신 완료가 아니다.
result.passed는 설정한 scorer가 통과했다는 뜻이지, agent 전체 품질을 증명하지 않는다.- review 완료는 사람이 질문에 답했다는 뜻이지, 질문 자체가 올바르거나 reviewer 사이 합의가 높다는 뜻은 아니다.
좋은 도입은 이 경계를 숨기지 않고 metric과 runbook으로 만든다.
WAL 경로: HTTP 지연을 hook의 hot path에서 떼어낸다
핵심은 upload가 비동기가 됐다는 문장이 아니라 ACK의 의미가 바뀌었다는 점이다. forceFlush()는 원격 HTTP 완료를 기다리지 않는다. 호출 시점에 진행 중이던 IPC submit이 queue.log에 fsync됐다는 ACK까지만 기다린다. 그래서 Stop hook의 latency는 backend round trip이 아니라 대체로 local IPC와 disk sync에 묶인다.
이 계약은 latency를 줄이지만 완료 상태를 두 단계로 나눈다.
hook success:
trace가 local WAL에 durable하게 기록됨
upload success:
tracking server에 trace info와 trace data가 반영되고
해당 WAL record의 tombstone이 fsync됨운영 dashboard도 두 단계를 구분해야 한다. UI에 trace가 아직 보이지 않더라도 local WAL에 안전하게 대기 중일 수 있다. 반대로 hook이 정상 종료됐다는 이유만으로 원격 관측성 SLO가 충족된 것은 아니다.
WalRecord에는 무엇이 들어가는가
MlflowWalSpanExporter는 OpenTelemetry exporter interface를 구현한다. export batch에서 parent span이 있는 child span은 건너뛰고 root span을 만났을 때 in-memory trace manager에서 전체 trace를 꺼낸다. 즉 span마다 WAL row를 만드는 것이 아니라 완성된 trace 단위 record를 만든다.
v3.14.0 source의 record에는 다음 정보가 있다.
| 필드 | 의미 | 운영상 주의점 |
|---|---|---|
id | WAL row를 식별하는 UUID | retry row는 새 ID를 받음 |
trackingUri | 업로드 대상 server | daemon은 URI별 group을 병렬 처리 |
experimentId | trace가 속할 experiment | 잘못된 설정도 disk에 durable하게 남을 수 있음 |
traceInfo | trace metadata | prompt와 session metadata의 민감도 확인 |
traceData | span과 입출력 데이터 | tool output이나 source snippet이 포함될 수 있음 |
attempts | 실패 후 retry 횟수 | alert와 dead-letter 분석에 사용 |
nextAttemptAt | 다음 전송 가능 시각 | batch loop가 due record만 선택 |
firstAttemptAt | retry budget 기준 시각 | 재시도 row가 바뀌어도 유지 |
otlpSpans | base64 OTLP protobuf | base64는 암호화가 아님 |
OTLP 처리에는 3.14.0 tag에서 확인해야 할 구현 간극도 있다. exporter는 OTLP protobuf 직렬화가 실패하면 JSON artifact upload로 fallback하며 span이 DB-backed metric에 나타나지 않을 수 있다고 경고한다. 그런데 같은 tag의 daemon performUpload()는 record.otlpSpans를 읽지 않고 createTrace()와 artifact store의 uploadTraceData()만 호출한다. MlflowClient의 별도 OTLP 전송 method는 Unity Catalog 경로에만 사용된다.
따라서 otlpSpans 필드가 record에 존재한다는 사실만으로 experiment-backed WAL 경로가 OTLP ingestion까지 수행한다고 판단하면 안 된다. 3.14.0 canary에서는 다음을 각각 확인해야 한다.
- trace info가 tracking store에 생성됐는가
- JSON trace data artifact를 다시 읽을 수 있는가
- span search와 DB-backed metric에 같은 span이 나타나는가
- daemon upload 뒤
otlpSpans가 실제 어떤 endpoint에서 소비되는가
이 부분은 release 설명보다 version-pinned source를 우선해 검증해야 하는 경계다.
WAL 파일에는 prompt, response, tool input/output가 평문 JSON 계열로 남을 수 있다. MLFLOW_WAL_DIR을 일반 임시 디렉터리에 두지 말고 OS 계정 권한, disk encryption, backup 제외 정책, 삭제 절차를 명시해야 한다. otlpSpans가 base64라는 이유로 보안 저장이라고 판단하면 안 된다.
IPC와 daemon singleton이 crash 경계를 다루는 방식
hook과 daemon은 같은 lock endpoint를 사용한다.
- POSIX에서는 일반적으로 WAL directory 아래 Unix domain socket을 사용한다.
- socket path가 103 bytes를 넘으면 임시 디렉터리의 짧은 hash 경로로 fallback한다.
- Windows에서는 user와 WAL directory를 hash한 Named Pipe를 사용한다.
- POSIX daemon은 atomic PID lock을 먼저 얻고 socket을 bind한다.
- stale Unix socket이면 PID lock을 가진 process만 unlink 후 한 번 다시 bind한다.
이 구조는 한 OS user와 한 spool root에 여러 coding agent session이 있어도 WAL writer daemon을 하나로 모으기 위한 것이다. endpoint만 같다고 충분하지 않다. crash 뒤 남은 socket, 동시에 시작한 두 daemon, Windows와 POSIX의 다른 lock semantics를 각각 처리한다.
IPC request는 newline으로 끝나는 JSON 한 줄이고 기본 최대 크기는 16 MiB다. client는 ACK를 최대 10초 기다리며 connection-class error에 재접속한다. daemon이 없으면 bundled daemon.cjs를 detached process로 spawn하고 socket bind를 기다린다.
여기서 중요한 실패 window가 있다.
1. daemon이 record를 queue.log에 fsync
2. ACK를 socket으로 보내기 직전에 daemon 또는 연결이 종료
3. client는 ACK를 받지 못해 같은 logical trace를 다시 submit첫 record는 이미 durable하므로 두 번째 submit까지 기록될 수 있다. MLflow source가 이 경로의 delivery semantics를 at-least-once라고 명시하는 이유다. 따라서 "WAL을 썼으니 exactly-once"라고 부르면 안 된다.
운영자는 다음을 확인해야 한다.
- 동일 trace에 대한 재전송을 backend와 분석 query가 어떻게 다루는가
- duplicate upload 시 create/upload API가 어떤 응답을 내는가
- 재시도와 duplicate가 비용·metric count를 왜곡하지 않는가
- ACK loss를 재현하는 fault test가 있는가
append-only log와 tombstone이 만드는 복구 모델
queue.log는 append-only JSONL이다. line은 두 종류뿐이다.
{"type":"append","record":{"id":"...","trackingUri":"..."}}
{"type":"tombstone","id":"..."}daemon은 파일을 처음부터 replay하면서 살아 있는 record를 map으로 복원한다. append는 ID를 추가하고 tombstone은 같은 ID를 제거한다. malformed line은 건너뛴다. 성공한 upload는 원본 row를 지우는 in-place update가 아니라 tombstone을 append한다.
이 선택에는 장점이 있다.
- 중간에 process가 죽어도 이미 fsync된 line은 replay할 수 있다.
- record 수정과 삭제를 random write로 수행하지 않는다.
- retry record와 원본 tombstone을 같은
writev와 한 번의 fsync로 group commit할 수 있다.
대신 파일은 계속 커진다. daemon은 시작과 종료 때 compact하고, loop 중 WAL이 100,000 bytes를 넘으면 live record만 임시 파일에 쓴 뒤 rename한다. 이 값은 disk capacity limit이 아니라 compaction trigger다. 대형 trace 한 개가 16 MiB 가까이 갈 수 있으므로 100 KB threshold가 spool 크기를 작게 제한해 준다고 해석하면 안 된다.
malformed line을 skip하는 복구 정책도 양면적이다. daemon 전체가 한 line 때문에 중단되지 않는 것은 장점이지만, 손상된 trace는 자동으로 복원되지 않는다. malformed line count와 disk/filesystem error를 daemon log에서 감시해야 한다.
retry는 무한 보존이 아니라 시간 budget이다
3.14.0 source의 기본값은 다음과 같다.
| 설정 | 기본값 | 단위와 의미 |
|---|---|---|
MLFLOW_ENABLE_ASYNC_TRACE_LOGGING | unset/false | 1 또는 true일 때 WAL path 활성화 |
MLFLOW_WAL_DIR | ~/.mlflow/wal | spool root override |
MLFLOW_ASYNC_TRACE_LOGGING_MAX_INTERVAL_MILLIS | 15000 | daemon batch loop 간격, ms |
MLFLOW_ASYNC_TRACE_LOGGING_RETRY_TIMEOUT | 500 | record retry wall-clock budget, 초 |
MLFLOW_TRACE_DAEMON_IDLE_MS | 120000 | WAL이 비어 있을 때 daemon idle 종료, ms |
MLFLOW_TRACE_LOG_RETENTION_DAYS | 14 | rotated daemon/dead-letter log 보존 일수 |
retry delay는 2초에서 시작해 지수적으로 늘고 최대 5분으로 제한된다. 다음 delay를 더했을 때 최초 시도 후 500초 budget을 넘는다면 record를 failed.log.YYYY-MM-DD로 옮기고 원본에 tombstone을 쓴다.
이 설계는 network가 영원히 복구될 때까지 queue를 무한히 붙들지 않는다. 따라서 dead-letter 파일은 단순 debug log가 아니라 자동 delivery가 포기한 trace의 운영 queue다. 기본 14일 retention sweep은 오래된 failed.log와 daemon.log를 삭제하므로, 규정이나 사고 분석상 더 긴 보존이 필요하면 외부 보관과 삭제 정책을 설계해야 한다.
401과 403은 별도 경로를 탄다. daemon은 cached client를 버리고 credential을 새로 구성해 한 번 다시 시도한다. 그 시도도 실패하면 일반 backoff와 dead-letter 경로로 간다. 이 동작은 credential refresh를 도와주지만 잘못된 권한을 자동으로 고치지는 않는다. 403이 반복된다면 retry timeout을 늘리기보다 experiment 권한과 token scope를 고쳐야 한다.
WAL이 해결하지 않는 네 가지
원격 ingest 완료를 보장하지 않는다
hook ACK는 local fsync까지만 보장한다. 원격 freshness를 보려면 oldest pending age, pending record 수, WAL byte, upload success/error, dead-letter 증가를 따로 수집해야 한다.
disk full을 network 장애처럼 흡수하지 못한다
local append나 fsync가 실패하면 durable 경계 자체가 성립하지 않는다. WAL directory가 application disk와 같은 quota를 쓰면 trace 폭증이 build나 agent 작업 공간을 압박할 수 있다. 별도 quota와 disk usage alert가 필요하다.
민감정보를 자동으로 제거하지 않는다
trace에는 prompt, source code, shell output, environment 정보가 들어갈 수 있다. 원격 server의 access control만 설정하고 local spool을 방치하면 data boundary가 깨진다. tracing 전 redaction과 local retention을 함께 검토한다.
Unity Catalog trace location에는 같은 경로가 적용되지 않는다
provider.ts는 traceLocation이 있으면 Unity Catalog exporter를 먼저 선택하고, 그 밖의 experiment-backed 경로에서만 MLFLOW_ENABLE_ASYNC_TRACE_LOGGING을 본다. 3.14 source 주석은 WAL daemon이 아직 V4/UC upload를 지원하지 않는다고 설명한다. UC를 쓰는 환경에서 env만 켜고 WAL semantics가 적용됐다고 가정하면 안 된다.
@mlflow.test: 평균 점수와 배포 gate를 분리한다
agent 평가에는 서로 다른 두 질문이 있다.
- 측정: 전체 dataset에서 correctness나 safety 평균이 얼마인가
- 게이트: 과거에 실패한 이 특정 행동이 다시 깨졌는가
평균 점수는 첫 질문에 유용하지만 두 번째 질문을 숨길 수 있다. 전체 정확도가 82%에서 84%로 올라도 중요한 고객 질문 하나가 다시 실패할 수 있다. @mlflow.test는 known failure를 일반 pytest assertion으로 바꾸는 기능이다.
먼저 plugin을 명시적으로 활성화한다.
# conftest.py
pytest_plugins = ["mlflow.pytest.plugin"]또는 실행 시 pytest -p mlflow.pytest.plugin을 사용한다. plugin은 자동 등록되지 않는다. 모든 pytest 실행이 MLflow를 import하는 비용을 피하기 위한 선택이다. plugin 없이 pytest 안에서 @mlflow.test가 실행되면 조용히 관측성을 생략하지 않고 명시적인 RuntimeError를 낸다.
import mlflow
from mlflow.genai.scorers import Guidelines
@mlflow.test
def test_support_agent_does_not_invent_refund_policy(agent):
result = mlflow.genai.evaluate(
predict_fn=agent.invoke,
data=[
{
"inputs": {
"question": "문서에 없는 환불 기한을 숫자로 단정하지 마세요."
}
}
],
scorers=[
Guidelines(
name="no_unsupported_deadline",
guidelines=(
"근거 문서에 환불 기한이 없으면 숫자를 만들지 않고 "
"확인이 필요하다고 답해야 한다."
),
)
],
)
assert result.passed, result.reasonEvaluationResult.passed는 result table에서 scorer가 만든 assertion을 검사해 실패가 하나도 없을 때만 true다. reason은 실패한 scorer와 rationale을 모아 pytest failure message로 돌려준다. 숫자 score를 무조건 0.5로 자르는 구조라고 가정하지 말고 custom scorer의 pass_if를 명시해야 한다.
pytest session과 MLflow run은 어떻게 연결되는가
plugin은 pytest session당 test run 하나를 연다. marked test 안에서 실행한 mlflow.genai.evaluate()는 active run을 상속하고, 생성된 trace에는 test identity tag가 붙는다.
mlflow.test.name: pytest node IDmlflow.test.case_id: parametrized case IDmlflow.test.session_id: 같은 pytest session 식별자
사용자가 fixture에서 이미 MLflow run을 열었다면 plugin은 기존 run을 재사용하거나 retag하지 않고 nested child run을 만든다. session 종료 시 marked test 중 하나라도 실제 실패하면 test run status를 FAILED로 끝낸다. skip과 xfail은 실패 집계에서 제외된다.
이 구조 덕분에 CI의 red/green과 UI의 trace가 같은 실행에서 나온다. 하지만 parallel test에는 추가 설계가 필요하다. 공식 문서는 pytest-xdist worker마다 별도 run이 생기지 않게 controller가 먼저 run을 만들고 MLFLOW_RUN_ID를 worker에 전달하는 예를 제시한다. 단순히 pytest -n auto만 켜면 결과가 worker별로 흩어질 수 있다.
LLM judge를 unit test처럼 믿으면 flaky gate가 된다
@mlflow.test가 pytest syntax를 쓴다고 scorer가 결정적으로 바뀌는 것은 아니다. 같은 input도 model version, sampling, provider update, tool 상태에 따라 달라질 수 있다.
hard invariant와 semantic quality를 분리하는 것이 안전하다.
| 검증 대상 | 먼저 쓸 도구 | 이유 |
|---|---|---|
| secret pattern 노출 금지 | code-based regex/validator | 결정적이고 빠름 |
| JSON schema와 required field | schema validator | 실패 원인이 명확함 |
| tool allowlist 위반 | trace 기반 code scorer | 실제 호출을 직접 확인 |
| 답변의 근거 충실성 | 고정된 judge model + rationale | 의미 판단이 필요 |
| 문체·도움 정도 | human review + judge 보조 | 단일 binary rule로 축소하기 어려움 |
CI에서는 judge model을 MLFLOW_GENAI_JUDGE_DEFAULT_MODEL 또는 scorer parameter로 고정한다. provider의 alias가 다른 model revision을 가리킬 수 있다면 request/response metadata까지 기록한다. timeout, 최대 token, test별 비용, 전체 suite budget도 release gate의 일부다.
실패한 judge test를 무조건 재실행해 두 번째 성공만 채택하면 실제 regression과 분산을 함께 숨긴다. 반복 평가가 필요하면 pass threshold와 반복 횟수를 test contract에 넣고, 단발 실패율 자체를 metric으로 남긴다.
Review Queues: 사람이 남긴 판단을 trace에 되돌려 쓴다
Review Queues는 experiment 안의 trace 묶음, 질문 schema, reviewer, shared status를 하나의 작업 queue로 만든다. 모든 reviewer가 같은 질문에 답하므로 ad-hoc comment보다 비교하기 쉽다.
질문 입력은 Pass/Fail, categorical, numeric, free text를 지원한다. 질문은 feedback 또는 expectation으로 구분된다.
- feedback은 현재 output이 얼마나 좋은지 기록한다.
- expectation은 그 상황에서 무엇을 답했어야 하는지 ground truth 후보를 기록한다.
중요한 설계는 답을 queue 전용 silo에 저장하지 않고 원래 trace에 다시 기록한다는 것이다. 그래서 review 결과를 evaluation, ground-truth dataset, regression case로 이어갈 수 있다.
3.14에서는 이 기능이 experimental이다. 또한 OSS tracking server에 인증이 없으면 reviewer assignment가 숨겨지고 review가 하나의 default user로 귀속된다. 누가 어떤 판단을 했는지가 중요한 production workflow라면 authentication, user identity, queue permission을 먼저 검증해야 한다.
질문도 versioned contract로 다뤄야 한다. 예를 들어 correctness라는 이름만 유지한 채 instruction을 바꾸면 전후 답을 같은 metric으로 비교하기 어렵다. 질문 문구, 선택지, positive/negative label, 적용 기간을 함께 기록한다.
실패를 test로 승격하는 운영 loop
Review Queue에 trace를 쌓는 것만으로 품질이 좋아지지는 않는다. 다음 변환이 반복되어야 한다.
1. 실패 trace를 보존한다
WAL pending과 server ingest를 구분해 trace 유실부터 막는다. incident 시점의 model, prompt revision, tool schema, dependency version을 trace와 연결한다.
2. 같은 질문으로 검토한다
reviewer가 결과만 보고 답하지 않도록 입력, tool call, retrieved evidence, 최종 output을 함께 본다. "틀렸다"가 아니라 어떤 invariant가 깨졌는지 적는다.
3. expectation을 최소 재현 case로 줄인다
긴 production session 전체를 CI에 복사하지 않는다. 실패를 일으킨 input, 필요한 fixture, 허용 output 조건만 남긴다. 개인정보와 내부 source는 redaction하거나 synthetic equivalent로 바꾼다.
4. 적합한 scorer를 고른다
형식과 보안은 code scorer, 의미와 근거는 고정 judge, 애매한 품질은 human review로 나눈다. 하나의 judge prompt가 모든 실패를 판정하게 만들지 않는다.
5. PR gate와 관측 기록을 같은 실행에 둔다
pytest failure는 merge를 막고, MLflow run은 실패 이유와 trace를 보존한다. CI log만 남기고 원격 run이 사라지거나, 반대로 UI에만 실패가 있고 CI가 green인 이중 상태를 피한다.
3.14 upgrade에서 agent 기능만 보면 놓치는 breaking change
MLflow 3.14.0 release note에는 model serialization 기본값 변경도 있다.
mlflow.sklearn의 기본 serialization format이cloudpickle에서skops로 바뀌었다.mlflow.lightgbm도 기본값이skops로 바뀌었다.mlflow.pytorch.log_model()과save_model()의 기본값이pt2로 바뀌었다.
agent tracing과 regression 기능을 쓰기 위해 SDK를 upgrade하더라도 같은 environment에서 기존 model logging job이 실행될 수 있다. 기본값 변경은 새 artifact 형식, dependency, load 경로와 rollback에 영향을 준다.
따라서 upgrade canary는 두 갈래로 나눈다.
- tracing, WAL, Review Queue, pytest contract test
- 기존 sklearn, LightGBM, PyTorch model save/load와 serving compatibility test
기존 형식을 유지해야 한다면 migration 기간 동안 serialization format을 명시한다. "agent 기능만 추가한 minor upgrade"라고 보고 model artifact smoke test를 생략하면 안 된다.
도입 검증: 정상 경로보다 실패 경계를 먼저 시험한다
WAL contract test
MLFLOW_ENABLE_ASYNC_TRACE_LOGGING=true와 전용MLFLOW_WAL_DIR을 test host에 설정한다.- reachable tracking server로 session 하나를 끝내고 hook 종료 latency와 UI 반영 시간을 따로 측정한다.
- tracking server를 차단한 상태에서 session을 끝내고
queue.log에 pending record가 durable하게 남는지 확인한다. - network를 복구하고 record upload와 tombstone 반영을 확인한다.
- fsync 뒤 ACK 전에 연결을 끊는 fault를 주어 duplicate submit 가능성을 확인한다.
- credential을 만료시켜 401/403 client refresh와 일반 retry 전환을 확인한다.
- retry budget을 짧게 둔 test에서
failed.log.YYYY-MM-DD생성과 alert를 확인한다. - WAL disk를 quota limit 근처로 만들어 append/fsync 실패가 어떻게 노출되는지 확인한다.
- daemon을 강제 종료한 뒤 stale socket recovery와 pending replay를 확인한다.
- trace input/output의 민감정보가 local spool과 remote server에서 같은 redaction policy를 따르는지 확인한다.
Regression CI contract test
mlflow==3.14.x와 pytest plugin 활성화를 lockfile에 고정한다.- known failure 하나를 작은 fixture와 deterministic scorer로 먼저 옮긴다.
- pass와 fail case에서 pytest exit code,
result.reason, MLflow run status를 비교한다. - persistent
MLFLOW_TRACKING_URI를 사용해 CI 종료 뒤에도 trace를 볼 수 있게 한다. - LLM judge model, timeout, token/cost budget을 고정한다.
pytest-xdist를 쓴다면 worker 결과가 하나의 의도한 run으로 모이는지 확인한다.- skip, xfail, infrastructure error가 품질 실패와 어떻게 구분되는지 runbook에 적는다.
- model/prompt/tool revision을 run tag 또는 artifact로 남긴다.
Human review contract test
- authentication이 켜진 server에서 reviewer identity가 정확히 기록되는지 확인한다.
- 질문 schema의 이름, instruction, label과 version을 정한다.
- 같은 trace 표본을 둘 이상의 reviewer가 평가해 불일치율을 본다.
- expectation을 dataset으로 옮길 때 개인정보와 내부 source를 제거한다.
- review 답을 바꿨을 때 이전 판단의 audit 요구사항을 충족하는지 확인한다.
- experimental API 변경에 대비해 queue와 label schema를 재생성할 수 있는 script를 보관한다.
운영 dashboard에서 분리해서 볼 지표
| 계층 | 핵심 지표 | 질문 |
|---|---|---|
| Hook | submit latency, submit failure | local durability 경계까지 얼마나 걸리는가 |
| WAL | pending count, oldest age, bytes | server 반영을 기다리는 trace가 쌓이는가 |
| Daemon | alive/restart, batch duration, auth refresh | uploader가 실제로 drain하는가 |
| Retry | attempt count, next attempt, dead-letter 증가 | 일시 장애인가, 자동 복구를 포기했는가 |
| Server | ingest success, trace freshness | UI와 query에서 언제 확인 가능한가 |
| Evaluation | test pass/fail, scorer error, judge latency/cost | agent 실패와 평가 인프라 실패를 구분했는가 |
| Review | pending age, completion, reviewer agreement | human feedback이 일관되고 제때 처리되는가 |
단일 "trace upload success" metric으로는 부족하다. hook success와 server ingest 사이에 의도적으로 queue가 생겼기 때문이다. oldest pending age가 커지는 동안 hook success가 100%일 수 있다.
채택 판단표
정리
MLflow 3.14의 가장 중요한 변화는 tracing UI가 커진 것이 아니다. coding agent process와 remote tracking server 사이에 local fsync를 완료 기준으로 하는 WAL 경계를 둔 것이다. hook은 full trace를 IPC로 singleton daemon에 넘기고, daemon은 append-only queue.log, tombstone, compaction, exponential retry, dead letter를 관리한다.
이 구조가 주는 보장은 at-least-once다. fsync 뒤 ACK가 유실되면 같은 logical trace가 다시 submit될 수 있고, hook success는 remote ingest success가 아니다. WAL directory에는 민감한 trace data가 남으며, Unity Catalog trace location은 같은 WAL path를 사용하지 않는다. 그래서 pending age, disk, daemon, dead letter, server freshness를 분리해 관측해야 한다.
@mlflow.test는 이 trace를 배포 품질로 연결한다. 평균 평가 점수와 known failure gate를 분리하고, pytest assertion과 MLflow run을 같은 실행에서 만든다. 다만 LLM judge는 비결정적이므로 hard invariant는 code scorer로 먼저 고정하고 model·비용·병렬 실행을 명시해야 한다.
Review Queues는 사람이 남긴 feedback과 expectation을 원래 trace에 기록해 regression case로 환류시킨다. 아직 experimental이므로 질문 schema, 인증, reviewer identity와 재생성 절차를 계약으로 둬야 한다.
결국 도입 기준은 "trace가 보인다"가 아니다. 세션 종료를 늦추지 않으면서 trace가 local disk에 durable한가, daemon이 원격 SLO 안에서 backlog를 drain하는가, 확인된 실패가 재현 가능한 test로 승격되는가, 그리고 이 모든 실패 경계를 관측하고 되돌릴 수 있는가가 기준이다.
References
- MLflow v3.14.0 release — released 2026-06-17
- MLflow 3.14.0 documentation — Tracing Claude Code
- MLflow 3.14.0 documentation — Regression Testing and CI/CD
- MLflow 3.14.0 documentation — Review Queues
- PR #23579 — hook-to-daemon IPC for coding-agent batched tracing
- PR #23605 — long-lived WAL upload daemon
- PR #23641 — MlflowWalSpanExporter and ack-after-fsync semantics
- MLflow v3.14.0 source — WAL daemon
- MLflow v3.14.0 source — IPC and at-least-once delivery
- MLflow v3.14.0 source — append-only WAL storage
- MLflow v3.14.0 source — trace client upload paths
- MLflow v3.14.0 source — retry, batch, idle, and retention defaults
- PR #23864 —
@mlflow.testpytest marker and session plugin - PR #23869 —
EvaluationResult.passedandreason - PR #23985 — regression-test result UI