LLM WikiAccess-protected knowledge portal
← 스터디 홈
8편 · 약 22분

MLflow 3.14: 에이전트 트레이스를 WAL로 지키고 pytest로 품질을 게이트하는 방법

에이전트 관측성에서 가장 위험한 순간은 세션이 끝날 때다

코딩 에이전트는 한 세션에서 수백 개의 이벤트를 만든다. 사용자 프롬프트, 모델 응답, 파일 읽기와 수정, 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가 만들어진다.

  1. 실행 중 일어난 일을 trace로 남긴다.
  2. 실패한 trace를 사람이 같은 질문으로 검토한다.
  3. 확인된 실패를 재현 가능한 regression test로 고정한다.
  4. pull request마다 같은 실패가 돌아오지 않는지 검사한다.

이 장은 2026년 7월 15일 기준으로 작성했다. MLflow 3.14.0은 공개 후 28일 지난 정식 릴리스로 90일 recency 기준 안에 있다. WAL 세부 동작은 v3.14.0 tag의 TypeScript source와 merged pull request를 기준으로 확인했다. Review Queues와 @mlflow.test는 3.14.0에서 experimental로 표시되므로 API 고정보다 검증 가능한 운영 계약에 초점을 맞춘다.


3.14에서 바뀐 세 가지 경계

경계3.14의 변화운영자가 얻는 것새로 관리할 것
trace 생성 process와 원격 serverhook이 trace를 로컬 WAL에 fsync한 뒤 반환하고 daemon이 업로드종료 지연과 network 장애를 분리WAL disk, daemon, retry, dead letter
평가 결과와 CI@mlflow.testEvaluationResult.passed/reasonagent 행동을 일반 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에서 떼어낸다

세션 종료와 원격 업로드 사이에 로컬 durability 경계를 둔다 Coding agent session prompt · response · tool call subagent · token · latency root span 종료 MlflowWalSpanExporter full trace를 WalRecord로 직렬화 traceInfo · traceData · OTLP protobuf trackingUri · attempt metadata child span은 별도 record로 만들지 않음 Local IPC POSIX Unix socket / Windows Named Pipe newline JSON request · 최대 16 MiB daemon 없으면 spawn 후 재접속 ACK는 queue.log fsync 뒤에만 반환 Stop hook 반환 원격 HTTP를 기다리지 않음 local durability 확보 server 반영은 아직 미완료 OS user별 singleton trace-uploader daemon BatchingWriter writev + 한 번의 fsync로 group commit Append-only queue.log append record + tombstone · compact 15초 batch loop due record 조회 trackingUri별 병렬 trace info + data 업로드 retry / dead letter 지수 backoff · 시간 budget MLflow tracking server trace · span · feedback 저장 evaluation run과 연결 성공 후 WAL tombstone Review Queue 같은 질문으로 trace 검토 feedback / expectation 기록 3.14에서는 experimental @mlflow.test failure input + scorer 고정 passed / reason assertion pytest session을 MLflow run으로 기록 PR CI gate known failure 재발 차단 실패 trace 재검토 dataset과 test suite 확장 새 실패를 다시 review와 regression case로 환류
MLflow 3.14의 WAL 기반 trace export와 regression feedback loop

핵심은 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에는 다음 정보가 있다.

필드의미운영상 주의점
idWAL row를 식별하는 UUIDretry row는 새 ID를 받음
trackingUri업로드 대상 serverdaemon은 URI별 group을 병렬 처리
experimentIdtrace가 속할 experiment잘못된 설정도 disk에 durable하게 남을 수 있음
traceInfotrace metadataprompt와 session metadata의 민감도 확인
traceDataspan과 입출력 데이터tool output이나 source snippet이 포함될 수 있음
attempts실패 후 retry 횟수alert와 dead-letter 분석에 사용
nextAttemptAt다음 전송 가능 시각batch loop가 due record만 선택
firstAttemptAtretry budget 기준 시각재시도 row가 바뀌어도 유지
otlpSpansbase64 OTLP protobufbase64는 암호화가 아님

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_LOGGINGunset/false1 또는 true일 때 WAL path 활성화
MLFLOW_WAL_DIR~/.mlflow/walspool root override
MLFLOW_ASYNC_TRACE_LOGGING_MAX_INTERVAL_MILLIS15000daemon batch loop 간격, ms
MLFLOW_ASYNC_TRACE_LOGGING_RETRY_TIMEOUT500record retry wall-clock budget, 초
MLFLOW_TRACE_DAEMON_IDLE_MS120000WAL이 비어 있을 때 daemon idle 종료, ms
MLFLOW_TRACE_LOG_RETENTION_DAYS14rotated 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.logdaemon.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.tstraceLocation이 있으면 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.reason

EvaluationResult.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 ID
  • mlflow.test.case_id: parametrized case ID
  • mlflow.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 fieldschema 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는 두 갈래로 나눈다.

  1. tracing, WAL, Review Queue, pytest contract test
  2. 기존 sklearn, LightGBM, PyTorch model save/load와 serving compatibility test

기존 형식을 유지해야 한다면 migration 기간 동안 serialization format을 명시한다. "agent 기능만 추가한 minor upgrade"라고 보고 model artifact smoke test를 생략하면 안 된다.


도입 검증: 정상 경로보다 실패 경계를 먼저 시험한다

WAL contract test

  1. MLFLOW_ENABLE_ASYNC_TRACE_LOGGING=true와 전용 MLFLOW_WAL_DIR을 test host에 설정한다.
  2. reachable tracking server로 session 하나를 끝내고 hook 종료 latency와 UI 반영 시간을 따로 측정한다.
  3. tracking server를 차단한 상태에서 session을 끝내고 queue.log에 pending record가 durable하게 남는지 확인한다.
  4. network를 복구하고 record upload와 tombstone 반영을 확인한다.
  5. fsync 뒤 ACK 전에 연결을 끊는 fault를 주어 duplicate submit 가능성을 확인한다.
  6. credential을 만료시켜 401/403 client refresh와 일반 retry 전환을 확인한다.
  7. retry budget을 짧게 둔 test에서 failed.log.YYYY-MM-DD 생성과 alert를 확인한다.
  8. WAL disk를 quota limit 근처로 만들어 append/fsync 실패가 어떻게 노출되는지 확인한다.
  9. daemon을 강제 종료한 뒤 stale socket recovery와 pending replay를 확인한다.
  10. trace input/output의 민감정보가 local spool과 remote server에서 같은 redaction policy를 따르는지 확인한다.

Regression CI contract test

  1. mlflow==3.14.x와 pytest plugin 활성화를 lockfile에 고정한다.
  2. known failure 하나를 작은 fixture와 deterministic scorer로 먼저 옮긴다.
  3. pass와 fail case에서 pytest exit code, result.reason, MLflow run status를 비교한다.
  4. persistent MLFLOW_TRACKING_URI를 사용해 CI 종료 뒤에도 trace를 볼 수 있게 한다.
  5. LLM judge model, timeout, token/cost budget을 고정한다.
  6. pytest-xdist를 쓴다면 worker 결과가 하나의 의도한 run으로 모이는지 확인한다.
  7. skip, xfail, infrastructure error가 품질 실패와 어떻게 구분되는지 runbook에 적는다.
  8. model/prompt/tool revision을 run tag 또는 artifact로 남긴다.

Human review contract test

  1. authentication이 켜진 server에서 reviewer identity가 정확히 기록되는지 확인한다.
  2. 질문 schema의 이름, instruction, label과 version을 정한다.
  3. 같은 trace 표본을 둘 이상의 reviewer가 평가해 불일치율을 본다.
  4. expectation을 dataset으로 옮길 때 개인정보와 내부 source를 제거한다.
  5. review 답을 바꿨을 때 이전 판단의 audit 요구사항을 충족하는지 확인한다.
  6. experimental API 변경에 대비해 queue와 label schema를 재생성할 수 있는 script를 보관한다.

운영 dashboard에서 분리해서 볼 지표

계층핵심 지표질문
Hooksubmit latency, submit failurelocal durability 경계까지 얼마나 걸리는가
WALpending count, oldest age, bytesserver 반영을 기다리는 trace가 쌓이는가
Daemonalive/restart, batch duration, auth refreshuploader가 실제로 drain하는가
Retryattempt count, next attempt, dead-letter 증가일시 장애인가, 자동 복구를 포기했는가
Serveringest success, trace freshnessUI와 query에서 언제 확인 가능한가
Evaluationtest pass/fail, scorer error, judge latency/costagent 실패와 평가 인프라 실패를 구분했는가
Reviewpending age, completion, reviewer agreementhuman feedback이 일관되고 제때 처리되는가

단일 "trace upload success" metric으로는 부족하다. hook success와 server ingest 사이에 의도적으로 queue가 생겼기 때문이다. oldest pending age가 커지는 동안 hook success가 100%일 수 있다.


채택 판단표

먼저 채택해도 좋은 조건
coding agent Stop hook이 backend latency 때문에 느리거나 trace 유실이 실제 운영 문제다.
known failure를 재현하는 fixture와 hard invariant scorer를 만들 수 있다.
local spool의 권한·용량·보존과 tracking server의 인증을 운영할 수 있다.
canary에서 확인할 경계
hook ACK는 local fsync이며 remote ingest 완료가 아님을 dashboard와 runbook에 반영한다.
ACK loss duplicate, network 단절, credential 만료, disk full, daemon crash를 fault test한다.
judge model·비용·병렬 실행을 고정하고 test failure와 evaluator failure를 분리한다.
도입을 미뤄야 하는 조건
WAL에 남는 prompt·tool output을 보호하거나 삭제할 정책이 없다.
dead letter와 oldest pending을 감시하지 않고 hook 성공만으로 delivery를 판단한다.
experimental Review Queue API를 장기 호환성이 보장된 workflow engine처럼 사용하려 한다.
rollback 기준
WAL backlog가 정한 freshness SLO 안에서 drain되지 않으면 synchronous exporter 또는 이전 plugin으로 복귀한다.
3.14 serialization 기본값 때문에 기존 model artifact load가 깨지면 형식을 명시하고 SDK rollout을 중단한다.
CI flakiness나 judge 비용이 budget을 넘으면 hard invariant gate만 유지하고 semantic judge는 비차단 평가로 낮춘다.
MLflow 3.14 agent quality loop adoption gate

정리

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