감사 가능한 에이전트: 답변을 검증할 수 있는 클레임으로 전환하기
요약
본 글은 AI 에이전트의 답변을 단순한 메시지 스트림이 아닌 검증 가능한 '타이핑된 아티팩트'로 만드는 방법을 제시합니다. 이를 통해 에이전트가 어떤 근거(출처)를 바탕으로 결론에 도달했는지, 그리고 그 과정이 재현 가능한지를 구조적으로 증명할 수 있습니다.
핵심 포인트
- 답변을 메시지 로그 대신 버전 관리되는 아티팩트로 만드세요.
- 도출 과정을 타이핑된 엣지로 연결하여 출처를 추적합니다.
- 컨텍스트는 Git 히스토리처럼 버전을 관리하고 재현성을 보장합니다.
모델이 사용자에게 _"2분기 클라우드 지출은 45,000달러였으며 예산 대비 12.5%의 편차입니다."_라고 알려줍니다.
이제 두 가지 불편한 질문이 생깁니다:
- 왜? 어떤 스프레드시트 셀, 어떤 정책 조항, 어떤 계산을 근거로 했나요?
- 다시? 만약 내일 다시 실행한다면 같은 답변을 얻을 수 있나요 — 그리고 그것이 같다는 것을 증명할 수 있나요?
대부분의 에이전트 프레임워크는 이 두 질문에 깔끔하게 답하지 못합니다. 실행 과정은 메시지 스트림이며, '추론'은 로그 속의 산문이고, 숫자는 모델이 마음먹은 곳에서 왔습니다. 프로덕션 환경에서 문제가 발생했을 때, 사용자의 감사 추적(audit trail)은 눈으로 읽는 녹취록에 불과합니다.
이 글은 답변을 검증 가능하게 만드는 에이전트 구축에 관한 짧은 시리즈의 두 번째 게시물입니다. 첫 번째 글에서는 부작용(사이드 이펙트, side effects)에 관한 것이었습니다 (재실행 시 재전송을 막기 위한 아웃박스, outbox). 이번 글은 다른 절반, 즉 답변 뒤의 _상태(state)_를 검사하고 재생산할 수 있도록 만드는 것에 관한 것입니다. 이는 reactifact의 기반이 되는 접근 방식이지만, 타이핑된 아티팩트(typed artifacts), 타이핑된 엣지(typed edges), 상태에 대한 콘텐츠 해시(content hash over state)와 같은 아이디어는 이식성이 높습니다.
요약 — 답변을 메시지 더미 속의 내용이 아닌, 버전 관리되는 컨텍스트 내의 _타이핑된 아티팩트_로 만드세요. 각 도출 과정을 타이핑된 엣지로 그 입력값에 연결하세요. 그러면 "왜 그렇게 말했나요?"는 그래프 탐색(graph walk)이 되고 "이것이 같은 실행인가요?"는 눈으로 읽는 녹취록이 아닌 해시가 됩니다.
API 키 없이 약 1분 만에 오프라인에서 실행할 수 있습니다 (수치는 Python에서 계산되며, 모델을 호출하는 것은 없습니다):
pip install reactifact
python -m examples.fintech_audit.main # 감사 보고서를 출력하고 재해시하여 재생산 가능함을 증명합니다
코드: github.com/bzdvdn/reactifact
| 일반적인 에이전트 프레임워크 | reactifact | |
|---|---|---|
| 답변은 | 메시지 로그의 문자열 | 컨텍스트 내의 타이핑된 아티팩트 |
| ... |
핵심적인 변화는 지루하지만 모든 것을 바꿉니다. 에이전트가 생성하는 의미 있는 모든 결과물은 메시지 묶음(bag) 속의 메시지가 아니라, 진화하는 컨텍스트 내의 타입 지정된 아티팩트입니다.
Context v1 Question
Context v2 + Document, Document
Context v3 + Evidence, Evidence
...
아티팩트는 pydantic 모델입니다. 예: Evidence(text=..., source=..., locator="budget.csv"), Variance(pct=0.125). 아티팩트에는 ID, 버전, 콘텐츠 해시(content hash), 그리고 누가 생성했는지에 대한 정보가 포함됩니다. 컨텍스트는 git 히스토리처럼 버전을 관리합니다. 모든 단계는 diff를 하거나, 롤백하거나, 체크아웃할 수 있는 커밋입니다.
이것만 해도 단순한 기록(transcript)을 넘어섭니다. 하지만 흥미로운 부분은 그 경계(edges)에 있습니다.
출처 추적(Provenance)은 로그 라인이 아닌 구조이다
생산 과정에서 무언가를 도출해낼 때, 시스템은 단순히 "budget.csv를 사용했다"라고 산문으로 작성하지 않습니다. 대신 타입 지정된 관계(typed relation)—아티팩트 그래프 내의 일급 객체 엣지(first-class edge)—를 기록합니다.
source = self.effects.create(SourceRef(locator="transactions.csv"), id="ref:tx")
table = self.effects.create(Table(rows=...), id="doc:transactions.csv")
spend = self.effects.create(Spend(total=45000.0), id="spend:q2")
...
이러한 관계들은 쿼리 가능합니다(context.related(answer.id, "supported_by")). 따라서 "왜 그렇게 말했어?"라는 질문은 grep 검색이 아니라 그래프 순회(graph walk)가 됩니다. 그리고 이 그래프는 구축되기 때문에 시각화할 수 있습니다. CLI/대시보드에서 Mermaid를 사용하거나 구조화된 보고서를 만들 수 있습니다:
from reactifact.audit import build_report, report_to_markdown
report = build_report(context, answer) # 전체 체인을 너비 우선 탐색(breadth-first)으로 순회
...
build_report는 답변에 기여한 모든 아티팩트를 반환합니다. 각 아티팩트는 콘텐츠 해시, 버전, 그리고 생성 주체를 가지며, 답변이 의존하는 소스 로케이터(source locators)도 함께 제공됩니다. 이것이 바로 "왜?"에 대한 답입니다: 믿어야 하는 단락이 아니라, 기계가 검증 가능한 출처 추적 체인(machine-checkable provenance chain)입니다.
# 감사 보고서 (Audit report)
- context version: 7
- context sha256: `f38c6a42…`
...
재현성(Reproducibility): 느낌이 아닌 상태를 해시하라
출처 추적(Provenance)은 "왜?"에 답합니다. **재현성(Reproducibility)**은 "이것이 동일한 실행인가?"에 답합니다.
컨텍스트가 표준적(canonical)이기 때문에 이를 지문자식별(fingerprint)할 수 있습니다. context_hash는 실행 과정의 상태에 대한 sha256 해시 값입니다. 이에는 각 아티팩트의 ID, 유형, 버전 및 콘텐츠 해시, 그리고 모든 관계 엣지(relation edge)가 포함됩니다. 타임스탬프는 의도적으로 제외되므로, 동일한 상태 해시에 도달하는 두 번의 실행은 완전히 동일하게 처리됩니다:
from reactifact.audit import context_hash
first = context_hash(await run_pipeline())
...
이 단일 문자열 자체가 감사 원시값(audit primitive)입니다. 이 값을 답변 옆에 저장해두면, 나중에 검토자가 파이프라인을 재실행하거나 저장된 세션을 재생하여 비교할 수 있습니다:
reactifact replay sessions.sqlite3 --session q2 --verify f38c6a42…
# 불일치하는 경우 0이 아닌 값으로 종료됨
더 이상 "숫자가 대략 맞는 것 같다"는 식의 주장은 통하지 않습니다. 답변 뒤에 숨겨진 상태가 기록된 지문자식별과 해시 값이 일치하거나, 그렇지 않다는 것입니다.
결정론(Determinism)은 속성이 아니라 규율입니다
해시는 입력값이 제어될 때만 의미를 가집니다. 에이전트 실행 과정에는 세 가지 일반적인 비결정성(non-determinism)의 출처가 있으며, 각각에 대한 해결책이 있습니다:
- 모델. 재실행 제공자(replaying provider)를 사용하여 호출 기록을 한 번 저장한 다음, 네트워크 없이 오프라인으로 다시 실행합니다:
from reactifact.replay import ReplayLLM
# pass 1 — 실제 실행 기록
...
-
자동 생성된 ID (
uuid4) 및 벽시계 시간. 결정론적 ID 팩토리(deterministic id factory)를 전달하고,time.time()/uuid4()로부터 아티팩트 데이터를 시딩하는 것을 중단합니다. 기록된 모델과counter_ids()가 종종 전체 해결책이 됩니다. -
순서 및 집합 반복. 생성물에는 안정적이고 콘텐츠 기반의 ID와 명시적인 정렬을 선호하십시오.
누수의 원인을 파악하려면, 기록된 모델과 엄격한 ID를 사용하여 파이프라인을 몇 번 실행하고 지문자식별을 비교해 보십시오:
from reactifact.replay import verify_run
report = await verify_run(build, recording="calls.jsonl") # 두 번 실행함
...
이것이 "보통 같은 것을 반환한다"와 "두 번째 실행의 해시 값이 동일한 문자열과 일치한다" 사이의 차이점입니다.
재생(Replay)은 재실행(re-execution)이 아니라 재구축(reconstruction)입니다
여기서 미묘한 부분이 나오는데, 이것이 첫 번째 게시물의 outbox와 연결됩니다.
단순한 '재생(replay)'은 에이전트를 다시 실행합니다. 이는 네트워크에 다시 접근하고, 도구를 다시 호출하며, 드리프트할 수 있음을 의미합니다. 하지만 reactifact의 replay는 어떤 에이전트도 실행하지 않고 커밋 체인으로부터 상태를 다시 구축합니다:
from reactifact.replay import replay_context, replay_summary
context = await replay_context(store, session_id, version=7) # commit 7 시점의 상태
...
커밋 체인은 결정론적(deterministic)이므로, 어느 시점에서든 정확한 컨텍스트를 재구성할 수 있습니다. 출처(provenance)를 따라가고, 그래프를 렌더링하고, '왜'라는 질문에 답할 수 있으며, 에이전트가 실행되지 않기 때문에 외부적으로 아무것도 발생하지 않습니다. 이를 outbox와 결합하면 기록된 부작용(side effect)은 재전송되는 것이 아니라 상태로 읽어옵니다.
동일한 버전 관리는 대안적인 상태를 저렴하게 만듭니다: context.branch()를 사용하여 두 가지 가설을 탐색하고, 명시적인 충돌이 있는 세 방향 병합(three-way merge)을 수행하며 (사일런트 last-write-wins 방식 없음), context.diff(v4, v9)를 사용하여 두 턴 사이에 정확히 무엇이 변경되었는지 확인하고, context.checkout(v7)를 사용하여 헤드를 되돌려 잘못된 단계를 취소할 수 있습니다. 메시지 목록의 체크포인트가 아닌 하나의 아티팩트 그래프 위에서 시간 여행을 할 수 있는 것입니다.
문자열뿐만 아니라 상태 자체를 평가하기
실행이 구조화된 상태(structured state)가 되면, 평가는 더 이상 answer == expected_answer가 아닙니다. 레이어별로 점수를 매길 수 있습니다:
Evidence quality · Claim correctness · Provenance grounding ·
Calculation correctness · Confidence calibration · Answer quality · Source coverage
reactifact.eval은 최종 Context에 걸쳐 다단계 메트릭을 실행합니다. 여기에는 출처 기반 근거(provenance grounding), 즉
보고서는 **최종 상태(final state)**에서 '왜'라는 질문에 답합니다. 동일한 설계 덕분에 **런타임 추적(runtime trace)**도 보관할 가치가 있습니다. 실행 기록은 눈으로 읽는 로그 라인의 벽이 아니라, 실제로 무슨 일이 일어났는지 지시하는 기록입니다. 각 에이전트 스팬(agent span)에는 해당 에이전트를 트리거한 아티팩트 유형과 그 이벤트를 위해 어떤 Produce가 실행되었는지—각 Produce가 몇 개의 효과 연산(effect operations)을 작성했는지, 그리고 얼마나 오래 걸렸는지—가 담겨 있습니다. 따라서 '모델이 X라고 말했다'는 '이 이벤트가 이 에이전트를 깨웠고, 이 produce가 작업을 수행했다'로 분해되며, 블랙박스가 아닙니다.
이러한 뷰는 추가된 것이 아니라 내장되어 있습니다. 로컬 SQLite 대시보드(create_trace_router)에서는 각 스팬에 대해 Consume → Produce 흐름을 보여주며, 동일한 스팬은 Langfuse(Produce당 하나의 자식 관찰 기록으로, 폭포수 모양이 각 단계를 보여줌) 또는 모든 OTLP 컬렉터로 하나의 Tracer를 통해 전송됩니다. 감사(Audit)는 한 가지 것—해시할 수 있는 최종 출처 그래프(provenance graph), 그리고 그것을 구축한 인과적 추적(causal trace)—에 대한 두 가지 뷰가 됩니다.
정직한 한계점 (The honest limits)
여기서의 감사 가능성(Auditability)은 사용자의 파이프라인 속성이며, 사용자가 유지하는 범위 내에서만 유효합니다:
- 결정론성은 얻어내는 것입니다.
context_hash는 타임스탬프를 제외하지만, 만약 Produce가uuid4()를 호출하거나 클럭을 아티팩트 데이터로 읽으면 두 번의 실행이 달라질 수 있습니다.verify_run은 알려줄 뿐, 고쳐주지는 않습니다. - 단일 프로세스입니다. 버전 관리된 상태와 재실행(replay)은 컨텍스트별로 이루어지며, 이는 분산 원장(distributed ledger)이 아닙니다.
- 출처는 연결고리만큼만 좋습니다. 프레임워크는 유형화된 엣지(typed edges)와 보고서를 제공합니다. 만약 Produce가 자신의 증거를 연결하지 않으면, 감사는 주장을 지어내기보다는 공백을 보여줍니다—정직하게 말하자면.
즉, 주장은 답변이 검증 가능한 주장(claim)이어야 하며, 이를 검증하는 메커니즘—타입화된 아티팩트(typed artifacts), 타입화된 엣지(typed edges), 상태에 대한 콘텐츠 해시(content hash over state), 재현(replay that reconstructs)—은 로그에 나중에 붙이는 것보다 프레임워크 자체에 구축할 가치가 있다는 것입니다.
오프라인으로 실행하기
fintech_audit 예제는 바로 위 시나리오와 같습니다. 두 개의 CSV 파일과 정책 문서(policy doc)를 기반으로 분산된 감사(audit) 사례이며, API 키가 필요 없습니다 (어떤 것도 모델을 호출하지 않고, 수치들은 Python에서 계산됩니다):
.venv/bin/python -m examples.fintech_audit.main
이 코드는 계산된 수치를 출력하고, 아티팩트별 콘텐츠 해시가 포함된 감사 보고서를 생성한 다음, 파이프라인을 다시 실행하여 두 해시 값이 일치하는지 검증합니다—그 종료 코드 자체가 결정론적 스모크 테스트(determinism smoke test)입니다.
- 코드: https://github.com/bzdvdn/reactifact
- 릴리스: https://github.com/bzdvdn/reactifact/releases/latest
- 문서:
docs/en/replay.md,docs/en/durability.md,docs/en/observability.md,examples/fintech_audit
만약 여러분이 새벽 2시에 디버깅해야 했던 에이전트를 배포한 경험이 있다면: 여러분의 감사 추적(audit trail)은 무엇인가요—눈으로 읽는 로그, 아니면 조회하고 재현할 수 있는 상태인가요? 첫 번째 접근 방식이 어떤 지점에서 여러분에게 실패했는지 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기