신뢰는 스칼라가 아니다: 에이전트 체인을 위한 타입화된 출처 (Typed Provenance)
요약
에이전트 체인에서 단일 신뢰 점수(스칼라)를 사용하는 방식의 한계를 지적하고, 성능 저하의 다양한 축을 반영하는 '타입화된 출처(Typed Provenance)' 개념을 제안합니다.
핵심 포인트
- 단일 신뢰 점수는 신선도와 능력 등 서로 다른 저하 축을 압축하여 정보 손실을 초래함
- 스칼라 방식은 과잉 거부 또는 과소 거부 문제를 발생시켜 에이전트의 신뢰성을 저해함
- 성능 저하의 원인을 벡터 형태로 전달하는 타입화된 출처(Typed Provenance)가 필요함
- 각 다운스트림 단계가 자신의 요구사항에 맞는 정책을 직접 적용해야 함
두 포스트 전, 에이전트가 조용히 실패하는 것에 관한 글에서 저는 조용한 성능 저하 (silent degradation)에 대한 해결책을 제시했습니다. 저하된 출력에 trust="degraded" 태그를 달고, 그 오염 (taint)을 체인 아래로 전파하며, 해당 태그가 있는 경우 되돌릴 수 없는 동작을 차단하는 방식이었습니다. 깔끔하고 바로 배포 가능한 방식이었지만, Theo라는 이름의 댓글 작성자가 하루 만에 지적했듯이 — 중요한 측면에서 틀렸습니다.
그 태그는 불리언 (boolean) 값이었습니다. 하지만 신뢰는 불리언이 아닙니다. 심지어 스칼라 (scalar)도 아닙니다.
이 포스트는 제가 공개적으로 틀렸음을 인정하고 이를 수정하는 글입니다. 왜냐하면 수정된 모델이 진정으로 더 낫기 때문이며, 그 과정의 대부분은 해당 댓글 스레드의 사람들이 만들어냈기 때문입니다. 크레딧은 마지막에 기재하겠습니다. 그들은 그럴 자격이 있습니다.
요약 (TL;DR) — 단일 신뢰 점수 (
full/degraded, 또는0.0–1.0)는 실제 체인에서 붕괴됩니다. 왜냐하면 성능 저하는 _서로 다른 축 (different axes)_을 따라 발생하기 때문입니다. 오래된 캐시는 _신선도 (freshness)_를 낮추고, 더 약한 폴백 (fallback) 모델은 _능력 (capability)_을 낮춥니다. 그리고 서로 다른 다운스트림 (downstream) 단계들은 서로 다른 축을 중요하게 여깁니다. 이를 하나의 숫자로 압축하면, 과잉 거부(모든 저하를 치명적으로 간주)하거나 과소 거부(위험한 요소가 평균화되어 사라짐)하게 됩니다. 실제로 합성(compose)되어야 하는 것은 **타입화된 출처 (typed provenance)**입니다. 무엇이 어떻게 저하되었는지에 대한 벡터 (vector)를 결과와 함께 전달하고, 이를 체인 전체에 전파하며, 각 소비자(consumer)가 행동하기 직전의 시점에 자체적인 정책을 적용하도록 해야 합니다.
스칼라가 붕괴되는 이유
제 불리언 방식을 무너뜨린 사례를 Theo의 댓글에서 거의 그대로 가져왔습니다.
상위(upstream) 결과를 소비하는 두 개의 다운스트림 단계가 있다고 가정해 봅시다:
- 요약 (summarization) 단계. 이 단계는 더 약한 모델을 사용하는 것은 잘 견디지만, 오래된 데이터(stale data)로 실행되어서는 안 됩니다.
- 가격 계산 (price calculation) 단계. 이 단계는 반대입니다. 최신 데이터가 필요하지만, 산술 연산을 수행하는 약간 더 약한 모델은 괜찮습니다.
이제 상위 결과가 2시간 된 캐시를 읽는 폴백 (fallback) 모델로부터 생성되었습니다. 즉, 이 결과는 능력 (capability) 축(더 약한 모델)과 신선도 (freshness) 축(오래된 캐시) 모두에서 저하되었습니다. 이때 당신의 단일 신뢰 점수는 무엇입니까?
- 만약 이를 낮게 설정한다면 (어떠한 저하도 심각하게 취급할 경우), 요약 단계에서 과도하게 거부(over-reject)가 발생합니다. 더 약한 모델을 사용했어도 완전히 괜찮았을 상황임에도, 스칼라(scalar) 값이 "저하됨"이라고 말했기 때문에 불필요하게 중단하거나 에스컬레이션(escalate)하게 됩니다.
- 만약 이를 높게 설정한다면 ("대체로 괜찮음"으로 간주할 경우), 가격 계산 단계에서 과소 거부(under-reject)가 발생합니다. 스칼라 값이 신선도(freshness) 문제를 수용 가능한 수준의 숫자로 평균화해버렸기 때문에, 오래된 데이터(stale data)를 바탕으로 동작하게 됩니다.
두 소비자 모두에게 동시에 적합한 단일 임계값(threshold)은 존재하지 않습니다. 왜냐하면 그들은 서로 다른 것을 측정하고 있기 때문입니다. 스칼라는 모든 소비자에게 단 하나의 "신뢰할 수 있는" 정의를 공유하도록 강요하지만, 그들에게는 공통된 정의가 없습니다. Theo가 말했듯, 벡터(vector)를 하나의 숫자로 붕괴시키면 소비자가 스스로 결정을 내리는 데 필요한 바로 그 정보를 파괴하게 됩니다.
이는 단순히 제 댓글창에서만 나오는 이야기가 아닙니다. 이 분야가 수렴하고 있는 방향이기도 합니다. 최근의 한 프레임워크(TrustBench)도 명시적으로 동일한 움직임을 보여줍니다. 신뢰를 단일 스칼라로 축소하는 대신, 신뢰의 각 측면별로 차원별 점수(dimensional scores)를 유지하고 도메인별로 가중치를 부여합니다. 예를 들어 헬스케어는 인용의 유효성과 최신성(recency)을 우선시하고, 금융은 계산과 컴플라이언스(compliance)를 우선시합니다. 구조는 같지만, 독립적으로 도달한 결론입니다. 여러 사람이 서로 다른 방향에서 동일한 구조를 지향한다면, 그것은 대개 그 구조가 실재하기 때문입니다.
신뢰는 벡터입니다; 출처(provenance)는 당신이 전파하는 것입니다
이 문제를 해결하는 재정의(reframe)는 다음과 같으며, 먼저 여러분께 사과하며 용어 교정부터 시작하겠습니다. 저는 계속해서 이 대상을 "신뢰(trust)"라고 불러왔습니다. 그것은 코드뿐만 아니라 언어 자체에 있었던 버그였습니다.
신뢰(Trust)는 값의 속성이 아닙니다. 그것은 소비자가 값에 대해 내리는 판단입니다. 값이 실제로 전달하는 것은 **출처 (provenance)**입니다. 즉, 해당 값이 어떻게 생성되었는지에 대한 타입화된 기록입니다: 어떤 모델이 생성했는지, 입력값의 최신성(freshness)은 어떠했는지, 어떤 도구(tools)가 실행되었는지, 무엇이 어떤 축(axis)을 따라 저하(degraded)되었는지에 대한 기록 말입니다. 신뢰란 각 소비자가 자신의 정책(policy)에 따라 그 출처로부터 계산해내는 것입니다. 가격 계산기(price calc)와 요약기(summarizer)가 동일한 출처를 보고 서로 다른 판결을 내린다면, 그것은 모순이 아니라 올바른 동작입니다.
따라서 저하 플래그(degraded flag)를 전파하는 것이 아닙니다. 대신 **타입화된 벡터 (typed vector)**를 전파하며, 각 축은 독립적으로 저하됩니다:
from dataclasses import dataclass, field
from enum import Enum
...
여기서 min 함수가 실제로 중요한 역할을 수행합니다. 제가 처음에 시도했던 '불리언(boolean)으로서의 오염(taint)' 방식이 완전히 실패했던 이유는, 그것이 체인 전체에 걸쳐 단일 OR 연산을 수행하며 "무언가 저하되었는가?"라는 질문에만 답했기 때문입니다. 반면 벡터는 "이 출력값이 어떤 종류의 저하를 어느 정도의 수준으로, 축별로(per axis) 담고 있는가?"에 답합니다. 그리고 결정적으로, 평균(averaging)을 내는 대신 **축별 최솟값(minimum per axis)**을 취합니다. 평균을 내는 것은 세 개의 우수한 역량 점수 뒤로 심각한 최신성 문제를 숨겨버리는 수학적 연산이기 때문입니다.
게이트는 전역(global)이 아니라 소비자별(per-consumer)입니다
이제 지난 포스트에서 다룬 비가역성 게이트(irreversibility gate)는 하나의 전역 임계값(global threshold)이 아니라, 각 소비자에게 귀속되는 정책이 됩니다:
@dataclass
class Policy:
# 이 소비자가 재확인 없이 동작하기 위해 요구하는 축별 최솟값
...
이것이 바로 결실입니다. 동일한 상류(upstream) 출처 벡터가 두 소비자 모두에게 흐르고, 그들은 이를 바탕으로 서로 다르며, 개별적으로 올바른 결정을 내립니다. 요약기는 작업을 진행하고, 가격 계산기는 데이터를 다시 가져옵니다(refetch). 단일 전역 점수로는 결코 이를 수행할 수 없습니다. 또한 실패한 축(failed-axis)은 불리언(boolean) 방식으로는 절대 알 수 없었던, 어떻게 복구해야 하는지를 알려줍니다.
이것은 다른 댓글 작성자(Manuel)가 독립적으로 제기한 관점도 수용하고 있음을 주목하십시오. 그는 태그가 불리언(bool)이 아닌 열거형(enum)이어야 한다고 주장했습니다. 즉, skipped-tool(도구 건너뜀)과 stale-data(오래된 데이터)와 retry-budget-exhausted(재시도 예산 소진)는 서로 다른 경로를 가집니다. 그의 말이 맞았으며, 벡터(vector)는 이를 일반화한 것입니다. 열거형(enum)은 하나의 축만 활성화된 벡터이며, 전체 구조(vector)를 사용하면 여러 축이 동시에 저하되는 상황을 다룰 수 있는데, 이것이 실제 운영(production) 환경의 사례입니다.
"신뢰도가 아닌 리스크에 기반한 게이팅(Gate on risk, not confidence)" — 그리고 신뢰도는 단지 하나의 축일 뿐입니다
지난 포스트에서는 모델이 스스로 보고하는 신뢰도(confidence)가 아니라, *가역성(irreversibility)*을 기준으로 게이팅(gating)해야 한다고 주장했습니다. 벡터는 이를 모호하지 않고 정밀하게 만들어 줍니다. 신뢰도는 여러 축 중 하나일 뿐이며, 모델이 스스로를 평가하는 축입니다. 모델은 오래된 캐시(stale cache)를 바탕으로 추론했기 때문에 신선도(freshness) 점수가 0.2임에도 불구하고 95%의 신뢰도(신뢰도 축에서는 높음)를 가질 수 있습니다. 기술 조건부 신뢰(skill-conditional-trust) 문헌에서도 라우팅(routing) 측면에서 동일한 주장을 합니다. 단일 글로벌 점수는 "이것은 잘하지만, 저것은 쓸모없다"라는 표현을 할 수 없기 때문에 잘못된 객체입니다. 신뢰도를 유일한 축으로 삼는 것은 모든 이들이 겪는 비극적인 사례, 즉 "확신에 차 있었지만, 잘못된 것에 확신했던 에이전트"를 만드는 지름길입니다.
가치가 없어지기 전까지 축(axis)은 몇 개까지 허용되는가?
이것은 솔직한 미결 질문이며, 제가 Theo에게 되물었던 질문이기도 합니다. 40개의 축을 가진 벡터는 스칼라(scalar)의 반대되는 실패 사례일 뿐입니다. 다루기 힘들고, 조정(tunable)이 불가능하며, 엄격함을 가장한 연극에 불과합니다. 저의 현재 답변은 다음과 같으며, 이에 대해 진심으로 반론을 환영합니다. 실제 성능 저하 원인(degradation sources)과 매핑되는 축들로 시작하고, 그 이상은 만들지 마십시오. 만약 시스템의 성능 저하 방식이 정확히 두 가지(폴백 모델과 오래된 캐시)라면, 두 개의 축(역량, 신선도)을 가지면 됩니다. 결과값을 전달하고 싶은 재확인(re-check) 단계가 생기는 즉시 verification(검증) 축을 추가하십시오. 도구가 부분적으로 성공할 수 있을 때 tool(도구) 축을 추가하십시오. 축의 개수는 당신이 상상할 수 있는 잘못될 수 있는 일의 개수가 아니라, 독립적으로 잘못될 수 있는 별개의 요소들의 개수와 같아야 합니다. 만약 두 "축"이 항상 함께 움직인다면, 그것은 하나의 축입니다.
제 생각에 가장 적절한 지점(sweet spot)은 각 축이 서로 다른 *복구 작업 (recovery action)*에 매핑되는 가장 작은 집합을 찾는 것입니다. 신선도(Freshness) → 다시 가져오기(refetch). 역량(Capability) → 기본 모델에서 재실행(re-run on primary). 검증(Verification) → 에스컬레이션(escalate). 만약 두 축이 동일한 복구를 트리거한다면, 하나로 합치십시오. 벡터는 당신이 수행하는 행동을 변화시키는 지점에서만 그 복잡성을 가질 가치가 있습니다.
실무 계층 (대부분 댓글에서 가져온 내용)
벡터가 핵심 아이디어이지만, 이 스레드는 이를 둘러싼 완전한 툴킷을 드러냈습니다. 이 중 어떤 것도 제 것이라고 주장하는 것은 부정직한 일이 될 것입니다:
-
모든 것의 상류에 위치한 승인 제어 (Admission control, upstream of everything) (Dan): 에이전트가 작업을 확산(fan out)시키기 전에, 전체 작업을 실행할 여력이 있는지 결정하십시오. 그리고 429(Too Many Requests) 오류로 인해 서로 뒤섞여 버리는 네 가지 제한 사항을 분리하십시오 — 제공자 할당량 (물리적 한계, provider quota), 계정 할당량 (정책적 한계, account quota), 작업 예산 (이번 실행의 한계, task budget), 원장 (포렌식적 한계, ledger). 원장은 결국 출처(provenance)와 동일한 기록임이 드러납니다: "이번 실행에는 47회의 호출이 소요되었으며, 그 중 12회는 폴백 티어(fallback tier)에서 발생했다"는 문장은 당신의 청구서인 동시에 역량 축(capability-axis)의 점수가 됩니다.
-
생산 시점이 아닌 소비 시점의 검증 (Validation at consumption, not production) (James): 신선한 호출 경로(fresh-call path)에서 검증하고 캐시를 신뢰하지 마십시오. 값이 어디에서 왔든 상관없이, 값이 사용될 때 검증하십시오. 그렇게 하면 소비자 측에서의 세탁 루프홀(laundering loophole)을 차단할 수 있으며, 이는 이미 소비자별 게이트(per-consumer gate)가 존재하는 바로 그 지점입니다.
-
벽시계 시간이 아닌 인과관계에 의한 시간 제한 (Time-bound by causality, not wall-clock) (HARD IN SOFT OUT): 저는 "N초 후에 오염(taint)을 초기화한다"는 방식에 유혹되었습니다. 하지만 그렇게 하지 마십시오. 저하된 상태(degraded state)는 잠복해 있다가 나중에 나타날 수 있습니다. 타이머가 만료될 때가 아니라, 라이브 경로(live path) 상의 어떤 것도 더 이상 저하된 단계로부터 파생되지 않을 때 해당 축을 제거하십시오.
-
1인 개발자를 위한 저비용 버전 (The poor-man's version for solo builders) (TuanAnhNguyen): 관측성 스택(observability stack)이 없습니까? 오래된 읽기 가능 입력(stale-readable input)에 따라 동작하는 도구가 있다면, 로그에 한 줄을 추가하게 하고 되돌릴 수 없는 작업을 수행하기 전에
grep으로 확인하십시오. 이것은 출처 벡터의 5% 정도의 노력만 들인 버전 — 그래프 대신 빵부스러기(breadcrumb)를 남기는 방식 — 이며, 특정 규모 이하에서는 이것이 올바른 엔지니어링 양입니다. -
분산 교정 (The distributed correction) (Abdullah): 나의 원래 동시성 제한 (concurrency cap)은 프로세스 내부의 세마포어 (semaphore)였는데, 이는 단일 프로세스를 암묵적으로 가정합니다. 서버리스 팬아웃 (serverless fan-out) 환경에서는 N개의 컨테이너가 각각 8개로 제한되더라도 실제로는 8N의 동시성이 발생합니다. 제한기 (limiter)는 워커 (worker) 외부에 존재해야 합니다. (또한: 긴 컨텍스트 (long-context) 에이전트의 경우 RPM보다 TPM이 먼저 포화되며, 동일한 풀링 티어 (pooled tier)를 사용하는 경우 "더 저렴한 모델로의 폴백 (fallback)"은 허구에 불과합니다. 이 두 가지 모두 그렇지 않으면 놓치게 될 역량/최신성 (capability/freshness) 축의 소스입니다.)
내가 말한 것보다 더 잘 설명해 주는 우화
한 댓글 작성자 (HARD IN SOFT OUT)가 남긴 글인데, 이 시리즈 전체를 다섯 줄로 요약해 줍니다:
에이전트가 속도 제한 (rate limit)에 걸렸습니다. 에이전트는 지난 화요일의 캐시된 답변으로 폴백 (fallback)했습니다. 수요일에 세상이 변했습니다. 에이전트는 계속 작동했습니다. 로그에는 "cache hit, 200 OK"라고 찍혔습니다. 사용자는 "주문하신 상품이 발송되었습니다"라는 메시지를 받았습니다. 목요일에 창고의 API 키가 만료되었습니다.
모든 단계가 녹색(정상)입니다. 모든 로그는 200입니다. 하지만 실제 패키지는 절대 발송되지 않습니다. 마지막 "주문 발송됨" 출력값에 대한 스칼라 (scalar) 신뢰 점수는 마지막 호출이 성공했으므로 _정상_으로 나타날 것입니다. 반면 출처 벡터 (provenance vector)는 freshness: 0.1, tainted_by: {warehouse_check}라고 읽으며, 배송 게이트는 실행을 거부합니다. 이것이 가동 시간 (uptime)과 올바른 가동 시간 (correct uptime)의 차이이며, 불리언 (boolean)과 벡터 (vector)의 차이입니다.
이 시리즈가 도달한 지점
세 번째 포스트에 이르러, 실제 논지가 조립되었습니다: 에이전트의 신뢰성은 출처 (provenance)의 문제입니다. 가용성 (Availability, 포스트 1)은 쉬운 축입니다. 정확성 (Correctness, 포스트 2)은 우리를 괴롭히는 축입니다. 그리고 정확성을 다룰 수 있게 만드는 구조 (포스트 3)는 체인을 통해 전달되는 타입화된 출처 (typed provenance)와 가장자리(edge)에서의 정책 (policy)입니다. 이 중 어느 것도 생소한 것이 아닙니다. 이는 데이터 리니지 (data lineage), 오염 분석 (taint analysis), 그리고 사가 패턴 (saga patterns)이며, 수십 년 전에 문제를 해결한 분야에서 빌려온 것입니다. 추적할 수 없는 대상이 이제 직접 _행동_하기 때문에 새롭게 핵심적인 역할을 수행하게 된 것입니다.
만약 이것을 구축하고 있다면: 두 개의 축과 min 값으로 시작하고, 정책을 소비자 (consumer) 측에 두며, 복구 동작 (recovery action)을 변화시킬 때만 축을 추가하십시오. 그 외의 모든 것은 시기상조입니다.
이 포스트는 주로 지난 포스트의 댓글들을 바탕으로 작성되었습니다. 구체적인 공로자(Credit)는 다음과 같습니다: 👤 Theo Valmis (trust-is-a-vector, 요약 대 가격 계산 사례, "typed provenance"), Manuel Bru👦 (enum-not-bool), Dan (admission control, 4단계 제한 분할), James O'Connor (소비 시점의 검증), HARD IN SOFT OUT (인과관계 기반의 오염 (causality-bound taint), 우화), TuanAnhNguyen (단독 빌더용 grep 버전), Abdullah Shahin (분산 제한기 (distributed-limiter) 및 풀 폴백 (pooled-fallback) 수정), 그리고 Scarab Systems ("evidence gate" 프레임워크를 통해 출처 (provenance)를 메타데이터가 아닌 의무 (obligation)로 생각하게 된 계기 제공). 이 사이트에서 가장 훌륭한 댓글 섹션이었습니다. 스레드에 질문을 던집니다: 여러분의 시스템에는 실제로 몇 개의 축 (axes)이 필요합니까? 그리고 그중 어떤 것들이 단순히 엄격해 보이는 것을 넘어, 별도의 복구 동작 (recovery action)으로 매핑됩니까?
출처 및 추가 읽을거리
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기