
LLM 에이전트의 "완료했습니다"를 검증하기 — OpenTelemetry 트레이스를 완료 게이트의 증거로 만들기
요약
LLM 에이전트의 자기 신고(Self-reporting) 오류를 해결하기 위해 OpenTelemetry 트레이스를 '증거'로 활용하는 가관측성 전략을 제안합니다. 에이전트의 확신도나 요약에 의존하지 않고, 신뢰 경계와 독립 판정을 통해 작업 완료를 검증해야 함을 강조합니다.
핵심 포인트
- 에이전트의 '완료' 선언과 자기 확신은 실제 결과와 괴리가 있을 수 있음
- 관측 데이터를 에이전트 성과를 판정하는 독립적 '증거'로 격상해야 함
- OpenTelemetry 트레이스를 신뢰 경계 및 결과물과 결합하여 검증 필요
- 자기 신고의 확신도(Confidence)는 허위 완료를 잡아내는 신호로 부적합함
LLM의 가관측성(Observability)에 대해, 저 나름의 답을 가져왔습니다.
먼저 말씀드리겠습니다. 답은, 관측을 "증거"로 격상시키는 것입니다. 식으로 쓰면 다음과 같습니다.
telemetry + 신뢰 경계(Trust Boundary) + 결과물과의 결합 + 독립 판정 = 증거
트레이스(Trace)를 남기고, 메트릭(Metrics)을 보고, 대시보드를 바라보는 것 — 거기서 끝내지 말고, 관측된 데이터를 "에이전트의 성과를 판정하는 증거"의 자리에 앉히는 것입니다. 트레이스 단독으로는 증거가 될 수 없습니다. 신뢰할 수 있는 측정 지점에서 collector가 수신하고(신뢰 경계), 결과물과 대조할 수 있는 형태로 연결되며(결합), 선언한 본인이 아닌 주체가 판정해야 합니다(독립 판정). 이 네 가지가 갖춰졌을 때, telemetry는 비로소 "완료했습니다"를 완료로 수락하기 위한 증거가 됩니다. LLM 에이전트 시대의 가관측성은 거기까지 도달해야 비로소 업무가 완결된다는 것이 저의 생각입니다.
왜 그렇게 생각하는가 — "완료했습니다" 문제
AI 에이전트에게 일을 맡기면, 돌아오는 것은 "구현했습니다", "테스트를 통과했습니다"라는 **자기 신고(Self-reporting)**입니다. 그리고 자기 신고는 종종 틀리곤 합니다. 악의가 없어도 말이죠. 에이전트는 자신의 작업에 대한 **요약(Summary)**을 생성할 뿐이며, 요약은 현실과 괴리가 생기기 마련이기 때문입니다.
상상이 아닙니다 — 제 손에 남은 기록
이것은 일반론이 아니라, 저에게 실제로 일어난 일입니다. 기록이 남아 있습니다.
저는 연구 리포지토리를 AI 에이전트(Claude)와 함께 개발하고 있는데, 그 개발 이력 중에 에이전트가 "4/4 ALL PASS"라고 커밋 메시지에 적어 선언한 실험이 2건 있습니다. 선언 시점에는 보이는 모든 것이 초록색이었습니다 — 테스트는 통과했고, 결과 파일은 저장되었으며, 플롯(Plot)도 생성되어 있었습니다. 그런데 며칠 후, 다른 실험의 회귀 확인 중에 관측값의 계산이 hash()의 프로세스 솔트(Process Salt)에 의존하여 실행할 때마다 수치가 변한다는 사실이 밝혀졌습니다. 올바른 값으로 재계산하자, 두 실험 모두 합격 조건 중 하나가 근소한 차이로 FAIL로 뒤집혔습니다.
done 선언 ∧ 모니터링 전면 초록 ∧ 실제로는 망가져 있음. 악의 없이도 이런 일이 일어납니다.
흥미로운 점은 여기서부터입니다. 이 개발 이력 11개 에피소드에 "선언과, 훗날 그것이 뒤집혔는지 여부"의 라벨을 붙여 그라운드 트루스(Ground Truth)로 만들고, 재현성 프로브(Reproducibility Probe)(동일 코드를 독립된 2개 프로세스에서 실행하여 관측값을 대조함)를 적용했습니다. 결과 — 해시 기인 비재현 3건은 3건 모두 검출, 건전한 에피소드에서의 오경보(False Alarm)는 0건. 반면, 다른 종류의 의미적 결함(집계가 자기 자신에게 질량을 운반하고 있었음)은 2건 모두 그대로 통과되었습니다. 하나의 계기에는 반드시 사각지대가 있습니다. 그렇기에 독립된 다수의 관측이 필요합니다 — 이것 또한 가관측성의 교훈 그 자체입니다.
또 하나의 수치를 제시하겠습니다. 다른 실험에서 LLM 에이전트(gpt-5-mini)에게 문서 QA 태스크를 풀게 하고, 60건의 완료 선언에 자기 확신(Self-confidence)을 덧붙이게 했습니다. 허위 완료는 11건이었습니다. 그 11건 모두가 자기 확신 0.85 이상을 주장하고 있었습니다(실측치: 0.85 ×1, 0.90 ×7, 0.95 ×2, 0.98 ×1). "확신이 낮은 것만 검증하면 된다"라는 직관적인 정책(임계값 0.9)의 포착률은 0.081 — 허위 완료의 9할 이상을 놓칩니다. 자기 신고의 내용뿐만 아니라, 자기 신고에 덧붙여진 확신 또한 신호(Signal)가 되지 않습니다. 남는 것은 관측뿐입니다.
옵저버빌리티(Observability)의 세계에는 원래 좋은 신조가 있죠. "로그에 나오지 않는다면, 일어나지 않은 것이다." 저는 이것을 문(Gate)의 입장에서 다시 말하고 싶습니다 — "이 문에서는, 요청한 트레이스에 남아 있지 않다면, 완료로 인정하지 않는다." 우리는 이미 "무엇을 말했는가"가 아니라 "무엇을 했는가"를 기록하는 표준어(OpenTelemetry)를 가지고 있습니다. 부족한 것은 관측 이후의 한 걸음뿐입니다.
제가 이전부터 해오던 것
사실 조금 전부터 저 나름대로 이 문제에 매달려 왔습니다.
저는 혼자 사업을 하고 있어서, 저와 AI 에이전트의 업무를 기록하는 **banto(番頭)**라는 도구를 Rust로 만들고 있습니다(최근 OSS로 공개했습니다). 추가 전용·해시 체인 방식의 이벤트 장부에 두 가지 규율을 담은 것입니다:
- claim ≠ verified — 「했습니다」라는 선언(claim)은 결과물 그 자체를 증거로 결합하지 않으면 문을 통과할 수 없다. 요약도 자기 평가도 증거가 될 수 없다.
- 선언한 본인은 자신을 검증할 수 없다 — verify 할 수 있는 것은 별도의 actor뿐이다. 인간이든 에이전트든 마찬가지다.
이 도구를 사용하여 나의 특허 출원 3건을 실제로 완수했습니다. 마지막 「출원할 수 있었습니다」라는 성과 주장도, 수령증을 증거로 삼아 선언자와는 다른 작성자의 검증을 통해 verified 되었습니다.
즉, 내 손에는 「증거를 요구하는 문」이 먼저 있었습니다. 이번 실험은 그 문에 OpenTelemetry 트레이스(trace)를 증거로 제시하면 어떻게 되는가입니다.
구체적인 구현은 다음과 같습니다
데모 작업은 「리포지토리의 Rust 행 수를 세어 보고서를 작성하기」입니다. 전체 30분 구성의 최소 단위입니다.
미리 고백하자면, 이 데모의 작업 주체는 LLM이 아니라 결정론적인 Python 스크립트입니다. 여기서 보여주고 싶은 것은 문의 구조 — 관측 → 증거 → 다른 눈 — 이며, 그 구조는 작업 주체가 사람이든 스크립트든 LLM이든 변하지 않습니다. API 키 없이 누구나 몇 분 안에 재현할 수 있는 것을 우선시했습니다. LLM을 작업 주체로 세웠을 때 실제로 어떤 일이 일어나는지는 앞 절의 기록(「4/4 ALL PASS」와 확신도 0.85의 가짜 완료들)이 이미 답하고 있습니다.
그리고 한 가지, 미리 입장을 명확히 해두겠습니다.
OpenTelemetry 트레이스는 그것만으로 진실은 아닙니다. 하지만 신뢰할 수 있는 측정 지점에서 수집하고, 결과물과 결합하며, 선언자와는 다른 주체가 판정한다면, 트레이스는 「했습니다」를 완료로 수락하기 위한
증거가 될 수 있습니다. 이 데모가 우선 다루는 것은 악의적인 위조가 아니라, 자기 신고의 괴리와 우발적인 오류입니다.
1. 에이전트의 작업을 계측(Instrumentation)하기
with tracer.start_as_current_span("agent.task") as task:
task.set_attribute("task.description", "crates/ 의 Rust 행 수를 세어 report.md 작성")
with tracer.start_as_current_span("read_sources") as span:
...
실행하면 report.md [IMG:1] (결과물)과 trace.json [IMG:2] (트레이스)가 나옵니다. span에는 다음과 같이 남습니다:
"name": "read_sources",
"attributes": { "files.read": 13, "lines.total": 4106 }
에이전트가 실제로 읽은 것은 13개 파일, 4106행입니다. 이것은 자연어로 된 신고가 아니라, 계측 코드가 생성한 구조화된 측정값입니다.
2. 트레이스를 증거로 선언하기
$ banto gate declare "crates 행수 보고서 작성 완료" \
--evidence report.md --evidence trace.json --store --actor agent:claude
✓ 記録 seq=0 type=claim.declare id=b2f44885...
결과물과 트레이스는 SHA-256으로 장부(ledger)에 보존되며, 선언 후의 교체를 탐지할 수 있는 형태로 보존됩니다.
3. 에이전트는 스스로 사실로 만들 수 없다
선언한 에이전트 자신이 verify를 시도하면:
$ banto gate verify b2f44885... --verdict pass --actor agent:claude
banto: 선언과 동일한 actor 'agent:claude'는 검증할 수 없음(계약 불변량 3).
exit code 1
트레이스를 첨부하더라도 「했습니다」는 본인의 손으로는 사실이 되지 않습니다. 이 부분이 이 메커니즘의 핵심입니다.
4. 다른 눈이 트레이스로 대조한다 — 그리고 직접 다시 세어본다
검증자(나)가 수행한 것은 세 가지 대조였습니다.
| 대조 | 확인한 내용 | 결과 |
|---|---|---|
| 주장 ↔ 산출물 | 리포트 내역의 합계 | 717+112+…+35 = 4106 으로 일치 |
| 산출물 ↔ 트레이스 (Trace) | files.read: 13 / lines.total: 4106 | 리포트의 숫자와 일치 |
| 트레이스의 일관성 | 동일 trace_id · 부모-자식 span · 시간 순서 | 하나의 trace로서 구조적으로 정합 |
$ banto gate verify b2f44885... --verdict pass --actor keisuke \--reason "내역 합계・트레이스와 산출물의 일치・trace 일관성 세 가지 확인"
✓ 기록 seq=1 type=claim.verify id=171d357a...
여기서 솔직하게 한 단계 더 깊이 들어가 보겠습니다. 위의 세 가지 확인에는 도달할 수 있는 범위가 있습니다. report.md와 trace.json은 동일한 계측 (Instrumentation) 스크립트의 동일한 counts 변수로부터 생성되었기 때문에, 세 가지 확인이 보여주는 것은 "동일한 계산 결과가 일관되게 전기(transcribe)되었다"는 점까지입니다. 계수(Counting) 그 자체의 정확성은 아직 독립적으로 검증되지 않았습니다. 또한 trace_id는 W3C Trace Context의 상관관계용 식별자이지 서명이 아니므로, ID · 부모-자식 관계 · 시간으로부터 알 수 있는 것도 "하나의 trace로서 내부적으로 정합한가"까지입니다.
따라서 진정한 의미의 "다른 눈"은, 고정된 Git 트리(Tree)에 대해 검증자가 별도의 구현으로 다시 세어보는 것입니다. 이 데모의 대상은 공개 리포지토리의 고정된 커밋이므로, 누구나 한 줄의 명령어로 할 수 있습니다:
$ git checkout 4c437df # bantollla의 고정 커밋
$ find crates -name '*.rs' | sort | xargs wc -l | tail -1
4106 total
Python의 splitlines()와는 다른 구현인 wc -l을 통해, 4106행이 일치했습니다. 계수 그 자체가 선언자의 계산 경로로부터 독립적으로 재현되었습니다. 만약 리포트가 "5000행을 읽었습니다"라고 부풀렸다면, 이 재계산이 대조 과정에서 이를 잡아냈을 것입니다. 보고서는 거짓말을 할 수 있지만, 고정된 트리는 다시 셀 수 있습니다.
왜 트레이스(Trace)인가 — 보고와의 차이는 "만들어지는 방식"에 있습니다
보고서와 트레이스는 둘 다 텍스트입니다. 차이는 내용이 아니라 만들어지는 방식에 있습니다. 보고는 작업이 끝난 후에 작업과는 별개의 행위로서 작성되는 요약입니다. 트레이스는 작업 도중에 파일을 읽는 코드 경로(Code path) 그 자체가 내뱉는 부산물입니다. files.read: 13을 기록한 것은 실제로 파일을 읽은 바로 그 함수입니다. 동시에 · 과정 속에서 · 업무를 위해 만들어진 기록 — 회계 세계에서 장부가 증거로 취급되는 것과 같은 논리가 여기에 적용됩니다. 따라서 트레이스는 이번의 주 대상인 '악의 없는 어긋남'(요약이 현실에서 벗어나는 현상)에 대해 구조적으로 강합니다. 보고는 작업과 분리될 수 있지만, 계측(Instrumentation)은 작업 안에서만 움직일 수 있기 때문입니다.
또 하나, span 속성은 구조화된 측정값이므로 검증자가 산출물이나 입력값과 기계적으로 대조할 수 있습니다. "테스트는 대체로 통과했습니다"와 같은 자연어는 대조의 대상이 될 수 없습니다.
그럼에도 솔직히 말하자면 — 게이트(Gate) 안에서 트레이스의 역할은 증거의 주인공이라기보다 "어디를 보고, 무엇을 다시 세어야 하는가"를 가리키는 안내자입니다. 증거의 무게 중심은 고정된 트리에 대한 재계산과 독립적인 판정에 있습니다. 그렇다면 트레이스는 조연일까요? 그렇지 않습니다. 트레이스가 없다면 검증 비용이 너무 높아져 아무도 하지 않을 것입니다. 검증 비용을 사람이 지불할 수 있는 가격까지 낮추는 것 — 그것이 식의 첫 번째 항의 역할이며, OpenTelemetry가 이미 그 파이프라인을 갖추고 있기 때문에 이 게이트는 현실적으로 운영될 수 있습니다.
결과 — OpenTelemetry는 상당히 쓸만하다
직접 해본 솔직한 소감입니다. 이 용도에 OTel은 생각했던 것 이상으로 잘 맞아떨어졌습니다.
- 계측(Instrumentation)이 정직함. 증거가 될 관측 데이터(읽은 파일 수, 쓴 경로)를 span 속성(attribute)에 담기만 하면 됩니다. SDK에 역행하는 부분이 전혀 없었습니다.
- span이 그대로 "증거의 그릇"이 됨. attributes는 구조화된 관측 사실이며, 검증자가 결과물과 대조하기에 부족함이 없습니다.
- 일관성 검토가 처음부터 가능함. trace_id, parent_id, 타임스탬프(timestamp)라는 OTel의 기본 구조만으로도 "하나의 trace로서 구조적으로 정합성을 갖추었는가"를 검토할 수 있습니다. 다만 앞서 언급했듯이 trace_id는 서명이 아니며, W3C 사양도 위조를 상정하고 있습니다. 즉, 정합성 검토와 진위성 보증은 별개의 작업입니다.
- "권한이 분리된 수령·보존 경계"의 자리가 생태계에 이미 존재함. 이번 데모는 선언자 스스로가 트레이스(trace)를 생성합니다(원리상 트레이스 자체를 조작할 수도 있습니다). 이에 대한 정석적인 해답은 OTel 생태계가 이미 마련해 두었습니다. 바로 span을 에이전트의 손이 닿지 않는 독립된 collector로 보내는 것입니다. 이렇게 하면 수령 후 이력을 수정할 여지를 줄일 수 있습니다. 다만 collector도 만능은 아닙니다. 전송 전의 허위나 누락까지는 찾아낼 수 없으며, 인증·TLS·collector 설정 보호·액세스 제어(access control)까지 포함되어야 비로소 경계가 된다고 공식 문서에서도 설명하고 있습니다. 여러분이 매일 운영하고 있는 그 파이프라인이, 올바르게 보호되고 있다면 그대로 수령·보존 경계의 역할을 수행합니다.
장부 이벤트와 트레이스의 대응은 이번에는 단순하게 파일로 연결했습니다. 매핑(mapping)은 더 정교하게 정의할 수 있지만, 그것은 다음 과제로 남겨두기 위해 의도적으로 얇게 처리했습니다.
증거를 제출합니다 — 이 기사 자체도 관문을 통과합니다
"기록이 있습니다"라고 끝내는 것은 이 기사의 주장과 모순됩니다. 그래서 공개할 수 있는 것은 전부 올려두었습니다.
- 데모 세트 — 계측 스크립트
agent_task.py, 결과물report.md,trace.json, 그리고 데모 장부 그 자체(claim.declare → 자기 verify 거부 → 다른 actor의 claim.verify 실제 이벤트가 해시 체인(hash chain) 형태로 들어 있습니다). 재현은 banto v0.1.0과 기사 중의 세 가지 명령어로 가능합니다. - "4/4 ALL PASS" 허위 완료 기록 — 해당 커밋의 해시와 메시지, hash 문제 수정 커밋, 11개 에피소드의 라벨이 붙은 그라운드 트루스(ground truth,
real1_phase0_episodes.json). - 60건 QA 생데이터 — 허위 완료 11건의 자기 확신 실측값, CONF 정책(임계값 0.9)의 정의, 포착률 0.081의 계산 근거(frozen protocol과 results의 JSON).
그리고, 이 기사의 공개 주장 자체를 동일한 데모 장부에 claim.declare 해 두었습니다(선언 actor는 기사를 작성한 agent:claude). 규율에 따라 선언자는 스스로 verify할 수 없습니다. verified한 것은 다른 actor입니다. 그 verify 이벤트 또한 장부 안에서 확인할 수 있습니다.
맺음말 — 트레이스는 증거가 될 수 있습니다
가시성(Observability) 엔지니어는 자기 신고를 믿지 않도록 훈련받은 사람들입니다. "로그에 남지 않았다면 일어나지 않은 일이다"를 신조로 삼아온 여러분에게, 에이전트의 "완료했습니다"를 증거로 받아들이는 메커니즘은 새로운 무언가라기보다, 평소 업무의 한 단계 앞에 있습니다.
저의 답은 "OTel이 진실을 증명한다"가 아닙니다. 관측을 증거로서 수리하기 위한 계약을 만들었다는 것입니다. telemetry + 신뢰 경계 + 결과물과의 결합 + 독립 판정 = 증거. 이 식의 첫 번째 항에 여러분이 매일 만드는 트레이스가 그대로 들어갈 수 있습니다.
여러분의 트레이스는 단순히 훑어보는 자료로 끝나기에는 아깝습니다. 판정의 증거로 승격시켜 보지 않겠습니까?
*관련 기술은 특허 출원 중입니다. 본 리포지토리(bantollla)는 Apache-2.0으로 공개되어 있으며, 특허 라이선스의 범위는 해당 라이선스 §3의 규정에 따릅니다. 출원된 내용은 "자기 신고가 아닌, 관측에 기반하여 검증 및 권한 부여를 수행하는 기구"에 관한 것입니다. 숨기기 전에 미리 적어둡니다. bantollla의 이용을 방해할 의도는 없으며, 코드는 Apache-2.0이고, 해당 특허 허가 조항(§3)이 이용자에게 적용됩니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기