개념 관점
텔레메트리(trace/metrics/logs)와 append-only event store가 왜 별개의 축인지 설명하고, HOTL 운영에서 둘이 각각 어떤 질문에 답하는지 구분한다.
개념 관점
텔레메트리(trace/metrics/logs)와 append-only event store가 왜 별개의 축인지 설명하고, HOTL 운영에서 둘이 각각 어떤 질문에 답하는지 구분한다.
설계 관점
span 구조와 event schema를 직접 설계하고, run·task·agent·model·tool·gate를 연결하는 7개 attribute를 정의한다.
구현 관점
OpenTelemetry SDK로 span을 emit하고 .events.jsonl을 작성하며, LLM-as-Judge가 strict JSON으로 점수를 내고 deterministic gate와 결합되는 코드를 짠다.
운영 관점
judge calibration 파이프라인을 굴리며 Spearman/Pearson correlation으로 신뢰 구간을 모니터링하고, judge bias가 의심될 때 gate policy를 어떻게 보강할지 결정한다.
Human-on-the-Loop 시스템에서 인간 감독자는 모든 tool call을 실시간 승인하지 않는다. 대신 시스템이 안전한 범위 안에서 자율 실행하도록 두고, 텔레메트리와 평가 게이트를 통해 이상 징후를 본다.
| 관측 대상 | 질문 | 예시 |
|---|---|---|
| Trace | 어디서 시간이 걸렸는가? | agent_loop → model_call → tool_call → test |
| Metrics | 정상 범위인가? | success_rate, token_usage, ttft_ms, error_rate |
| Logs | 무슨 일이 있었는가? | permission denied, tool timeout, pytest failure |
| Event store | 같은 결과를 재구성할 수 있는가? | .events.jsonl, replay snapshot |
| Evaluation | 결과가 쓸 만한가? | tests, lint, LLM-as-Judge, human review |
같은 run_id를 OTel span attribute와 event log line 둘 다에 박아 두면, dashboard에서 한 클릭으로 audit log까지 내려갈 수 있다.
에이전트 실행을 하나의 로그 문자열로 남기면 나중에 분석하기 어렵다. 최소한 다음 span 구조를 권장한다.
각 span은 같은 run_id, task_id, agent_role, model, repository, commit_sha를 공유한다. 이렇게 해야 Grafana에서 “어떤 모델이 어떤 역할에서 실패했는가”를 볼 수 있다.
에이전트 텔레메트리는 “로그를 많이 남기는 것”이 아니라 분석 가능한 공통 키를 유지하는 것이다.
| Attribute | 예시 | 이유 |
|---|---|---|
run.id | run-20260519-001 | event log, dashboard, report를 연결 |
task.id | capstone-017 | 같은 작업의 반복 실행 비교 |
agent.role | worker | planner/reviewer/worker별 실패율 비교 |
model.name | local-coder | 모델별 비용/품질 비교 |
tool.name | run_tests | 느리거나 위험한 도구 식별 |
gate.result | pass, revise, fail | release readiness 판단 |
artifact.path | artifacts/run-001.patch | 최종 산출물 추적 |
이 키가 없으면 15주차 최종 보고서에서 “수치가 있다”와 “증거가 연결된다” 사이의 차이가 생긴다.
# telemetry.py — OpenTelemetry 기반 에이전트 모니터링from opentelemetry import trace, metricsfrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.metrics import MeterProvider
tracer = trace.get_tracer("agent-runtime")meter = metrics.get_meter("agent-runtime")
loop_counter = meter.create_counter("agent.loop.count")token_usage = meter.create_histogram("agent.tokens.used")judge_score = meter.create_histogram("agent.judge.score")ttft_ms = meter.create_histogram("agent.model.ttft_ms")
def traced_agent_loop(task: dict): with tracer.start_as_current_span("agent.run") as span: span.set_attribute("run.id", task["run_id"]) span.set_attribute("task.id", task["id"]) span.set_attribute("task.objective", task["objective"]) span.set_attribute("agent.role", task.get("role", "worker")) span.set_attribute("model.name", task.get("model", "local-coder"))
result = run_agent(task)
loop_counter.add(1, {"status": "success" if result.passed else "failure"}) token_usage.record(result.tokens_used, {"model": result.model}) judge_score.record(result.judge_overall, {"task": task["id"]}) ttft_ms.record(result.ttft_ms, {"model": result.model}) span.set_attribute("result.passed", result.passed) span.set_attribute("tokens.used", result.tokens_used) span.set_attribute("tests.failed", result.tests_failed) span.set_attribute("gate.result", result.gate_result) return resultOpenTelemetry는 실시간 관측에 강하지만, 에이전트 실행을 완전히 재구성하는 source of truth로는 부족하다. Agent OS Runtime 방식의 append-only event store는 다음 이벤트를 순서대로 남긴다.
{"type":"run.started","run_id":"r-001","task_id":"t-017","model":"local-coder","ts":"2026-05-19T09:00:00Z"}{"type":"tool.invoke","run_id":"r-001","tool":"read_file","input":{"path":"src/app.py"}}{"type":"tool.result","run_id":"r-001","tool":"read_file","status":"ok","bytes":1842}{"type":"test.result","run_id":"r-001","command":"pytest","passed":false,"failed":2}{"type":"judge.result","run_id":"r-001","overall":7.2,"verdict":"revise"}{"type":"run.closed","run_id":"r-001","status":"failed","reason":"tests_failed"}캡스톤 단계에서 다루는 8가지 이벤트 타입은 다음과 같다.
| 이벤트 타입 | 발생 시점 | 필수 필드 | replay 의미 |
|---|---|---|---|
run.started | task가 worker에 전달된 직후 | run_id, task_id, model, ts | 새 run의 시작점 |
plan.created | planner가 spec/plan 산출 | run_id, plan_path | 후속 단계의 입력 고정 |
tool.invoke | tool 호출 직전 | run_id, tool, input | 사이드 이펙트 직전 기록 |
tool.result | tool 호출 응답 | run_id, tool, status | 실패 시 이 줄만 보면 즉시 진단 가능 |
test.result | deterministic gate 실행 결과 | run_id, command, passed | release gate의 입력 |
judge.result | LLM-as-Judge 출력 | run_id, scores, verdict | probabilistic gate의 입력 |
human.override | 사람이 verdict를 뒤집음 | run_id, from, to, approver, reason | audit trail의 핵심 |
run.closed | run이 종료될 때 | run_id, status, reason | replay 종결 신호 |
replay 함수는 이 이벤트를 다시 읽어서 최종 상태를 계산한다.
def replay(events: list[dict]) -> dict: state = { "closed": False, "tools": [], "tests": [], "judge": None, "overrides": [], } for event in events: match event["type"]: case "tool.result": state["tools"].append(event) case "test.result": state["tests"].append(event) case "judge.result": state["judge"] = event case "human.override": state["overrides"].append(event) case "run.closed": state["closed"] = True state["status"] = event["status"] state["reason"] = event.get("reason") return state| 운영 시나리오 | event store 없는 경우 | event store가 있는 경우 |
|---|---|---|
| ”왜 실패했지?” | 로그를 grep, 추측 | run_id로 한 줄씩 읽으면 시퀀스가 그대로 보임 |
| ”이 run을 다시 돌릴 수 있나?” | 환경이 바뀌면 불가 | replay()로 결정적 재구성 |
| ”이 모델이 기존보다 나아졌나?” | 비교 baseline 부재 | 같은 task_id 모음을 다른 model로 재실행 |
| ”감사관에게 어떻게 증명?” | 자연어 보고만 | 이벤트 시퀀스 + override 기록 |
LLM-as-Judge는 자동 테스트가 잡기 어려운 품질을 평가한다. 하지만 논리적 정답성, 보안, 실행 가능성은 여전히 deterministic gate가 우선이다.
| 게이트 | 성격 | 예시 |
|---|---|---|
| Static | deterministic | ruff, mypy, eslint, schema validation |
| Runtime | deterministic | pytest, integration test, smoke test |
| Policy | deterministic/approval | secret scan, permission boundary, CUD approval |
| Judge | probabilistic | readability, maintainability, design fit |
| Human | final authority | capstone acceptance, production release |
연구에서 반복적으로 관찰된 LLM Judge의 5가지 bias를 알면 prompt와 gate를 더 안전하게 설계할 수 있다.
| Bias | 설명 | 신호 | 대응 |
|---|---|---|---|
| Length bias | 더 긴 답변을 더 좋게 평가 | gold set에서 짧은 정답이 낮게 채점됨 | rubric에 “간결성” 명시, 길이 정규화 |
| Position bias | 비교 시 먼저/나중 위치를 선호 | A/B 순서를 바꿨을 때 점수 역전 | 양방향 평가 후 평균 |
| Self-preference | 자기 모델 계열 출력에 후함 | 다른 모델 평가에 비해 자기 출력 점수 높음 | 다른 가족 모델 judge 교차 검증 |
| Style bias | 익숙한 포맷(번호, 헤딩)에 후함 | 동일 내용을 plain text로 주면 낮게 채점 | rubric에 포맷이 아니라 내용 기준 명시 |
| Refusal asymmetry | refusal/error를 과소평가 | 안전한 거부보다 잘못된 답을 더 후하게 줌 | safety는 별도 deterministic gate |
Judge prompt는 한 번 쓰고 끝나는 문서가 아니다. 팀은 작은 gold set으로 judge가 어떤 실수를 하는지 확인해야 한다.
| 단계 | 해야 할 일 | 실패 신호 |
|---|---|---|
| Gold set 작성 | 사람이 이미 평가한 샘플 10개 준비 | 좋은/나쁜 예시가 한쪽으로 치우침 |
| Blind scoring | judge가 사람 점수 없이 평가 | 점수가 전부 7-9점에 몰림 |
| Correlation 계산 | 사람 점수와 judge 점수 비교 | 상관이 낮거나 특정 항목만 과대평가 |
| Error analysis | false pass/false fail 사례 분류 | 이유가 prompt에 반영되지 않음 |
| Rubric update | 항목 정의와 예시 수정 | 점수 기준이 여전히 모호함 |
캡스톤에서는 완벽한 judge가 목표가 아니다. 목표는 judge가 어디서 틀리는지 알고 gate policy가 그 한계를 흡수하게 만드는 것이다.
# correlate.py — 사람 vs judge 점수 상관관계from scipy.stats import spearmanr, pearsonr
def correlate(human: list[float], judge: list[float]) -> dict: rho, rho_p = spearmanr(human, judge) r, r_p = pearsonr(human, judge) return { "spearman_rho": rho, "spearman_p": rho_p, "pearson_r": r, "pearson_p": r_p, "n": len(human), }
# 예시: gold set 12개를 평가한 결과human = [9, 8, 7, 6, 5, 9, 8, 4, 3, 7, 6, 8]judge = [8.5, 8.0, 7.2, 6.8, 6.0, 8.7, 7.6, 5.5, 4.2, 7.0, 6.5, 7.9]print(correlate(human, judge))# 권장 임계값:# spearman_rho >= 0.7 + p < 0.05 → calibrated# 0.4 <= spearman_rho < 0.7 → judge는 보조용으로만 사용# spearman_rho < 0.4 → rubric 또는 prompt 재작성JUDGE_SYSTEM_PROMPT = """You are a senior software engineering evaluator.Score the submitted change from 1 to 10 for each criterion.Return strict JSON only.
Criteria:1. correctness: Does it satisfy the requested behavior?2. test_quality: Are meaningful tests included?3. maintainability: Is the code simple and local to the task?4. robustness: Are edge cases handled?5. observability: Can failures be diagnosed?"""
JUDGE_SCHEMA = { "type": "object", "required": ["scores", "overall", "verdict", "rationale"], "properties": { "scores": {"type": "object"}, "overall": {"type": "number"}, "verdict": {"enum": ["pass", "revise", "fail"]}, "rationale": {"type": "string"}, },}평가 결과는 사람이 읽는 코멘트가 아니라 게이트 입력이다.
def gate(judge: dict, tests_passed: bool) -> str: if not tests_passed: return "fail" if judge["overall"] < 7.0: return "revise" if judge["scores"].get("correctness", 0) < 7: return "revise" return "pass"사람이 judge나 test 결과를 뒤집을 수 있어야 한다. 다만 override는 조용히 덮어쓰면 안 된다.
{"type":"human.override","run_id":"r-001","from":"revise","to":"pass","reason":"known flaky integration test; deterministic unit tests passed","approver":"instructor"}override는 최종 권한을 사람에게 돌려주는 장치이면서, 다음 주 개선해야 할 gate 품질 데이터를 만든다.
Judge 품질은 prompt보다 평가 데이터셋에 더 좌우된다. 각 팀은 최소 10개 샘플을 만든다.
| 샘플 유형 | 포함 이유 |
|---|---|
| 명백히 좋은 코드 | false negative 확인 |
| 명백히 나쁜 코드 | false positive 확인 |
| 테스트는 통과하지만 설계가 나쁜 코드 | judge의 보완 가치 확인 |
| 보기에는 좋아도 동작이 틀린 코드 | judge 과신 방지 |
| 보안/권한 문제가 있는 코드 | policy gate 필요성 확인 |
Execution Health
run_count, success_rate, retry_count, failure_reason을 본다. 수업에서는 팀별 run_id로 필터링한다.
Cost and Latency
prompt_tokens, completion_tokens, cache_read_tokens, ttft_ms, total_latency_ms를 모델별로 비교한다.
Quality Gates
tests_passed, judge_overall, human_override, final_verdict를 한 화면에서 본다.
OpenTelemetry collector가 메트릭을 Prometheus로 내보낸다고 가정한 패널 정의다.
# Panel 1: 모델별 run 성공률 (15분 윈도우)sum by (model) (rate(agent_loop_count{status="success"}[15m])) /sum by (model) (rate(agent_loop_count[15m]))
# Panel 2: 역할별 토큰 사용량 p95histogram_quantile(0.95, sum by (le, agent_role) (rate(agent_tokens_used_bucket[5m])))
# Panel 3: judge.overall 평균 vs deterministic pass rateavg by (task_id) (agent_judge_score)-- Panel 4: 최근 24시간 실패 원인 Top 5 (event store DuckDB 뷰)SELECT reason, COUNT(*) AS nFROM eventsWHERE type = 'run.closed' AND status = 'failed' AND ts > now() - INTERVAL 1 DAYGROUP BY reasonORDER BY n DESCLIMIT 5;이 4개 패널이 캡스톤 발표용 “수치 슬라이드”의 최소 골격이 된다.
trace wrapper 작성
agent.run, model.call, tool.invoke, acceptance.test, judge.evaluate span을 만든다. run.id, task.id, agent.role, model.name, gate.result 5개 attribute는 모든 span에 일관되게 박는다.
event writer 작성
append-only .events.jsonl에 run.started, tool.invoke, tool.result, test.result, judge.result, run.closed를 기록한다. 동시 실행을 고려해 file lock 또는 한 줄 단위 flush 전략을 명시한다.
replay snapshot 생성
event log에서 replay_snapshot.json을 생성하고, run이 closed 되었는지 검증한다. snapshot에는 최종 verdict, override 여부, 사용 토큰 합계가 들어가야 한다.
LLM Judge 구현
10개 코드 샘플에 대해 JSON 점수를 생성하고, deterministic test 결과와 비교한다. JSON schema 위반 시 retry 로직과 fail-safe(예: verdict=revise로 강등)를 명시한다.
상관관계 분석
인간 평가자 점수와 judge 점수의 Spearman 또는 Pearson correlation을 계산한다. 상관관계가 낮은 항목은 prompt나 rubric을 수정한다. correlation, n, p-value를 보고서에 포함한다.
대시보드 4-패널 구성
Grafana 또는 단순 CSV/Streamlit으로 (1) success rate, (2) token p95, (3) judge.overall vs tests_passed, (4) 실패 원인 Top 5를 만든다. 각 패널은 어떤 의사결정에 쓰이는지 캡션으로 1줄 설명한다.
제출 마감: 2026-05-26 23:59
Lab 11 요구사항:
.events.jsonlreplay_snapshot.json — event log에서 재계산한 output, cost, closed 상태Lab 12 요구사항:
LLMJudgerun.id, task.id, agent.role, model.name, tool.name, gate.result, artifact.path가 모든 span과 이벤트에 박혀야 dashboard와 audit이 연결된다.핵심 문서
도구
논문 / 리포트