Novelty Gate Sentinel: 신뢰도가 곧 참신함은 아니다
요약
자율 코드 에이전트 Sentinel의 작동 방식을 분석하며, 단순히 반복되는 'SKIP' 답변을 높은 신뢰도로 오해하는 문제점을 지적합니다. 이 글은 AI 에이전트가 판단의 근거를 명확히 하기 위해 '신뢰도(Confidence)'와 '참신성(Novelty)'이라는 두 개념을 분리하여 관리할 필요성을 강조합니다.
핵심 포인트
- 반복되는 답변 = 높은 신뢰도가 아님. (같은 증거 반복)
- AI 에이전트는 판단의 근거를 명확히 해야 함.
- 신뢰도(Confidence)와 참신성(Novelty)을 분리하여 관리해야 함.
Novelty Gate — 자신감은 새로움이 아니다
자율 코드 에이전트가 "이 파일은 이전에 본 적이 있고, 아무것도 움직이지 않았으니 내 답변은 여전히 아니오"라고 말하는 법을 배운 과정과 이것이 프로덕션 환경에서 3개월 동안 어떻게 작동했는지에 대한 내용.
본문 속 실시간 수치는 Render의 프로덕션 Sentinel 인스턴스에서 가져온 것이며, 읽기 전용
GET /analytics엔드포인트에서 2026-10-11 08:24 UTC에 읽은 데이터(인스턴스4cd95249)를 기반으로 합니다. 이후 08:26:55 UTC에 다시 읽었을 때도 동일한 결과가 나왔습니다. 포착된 원본 출력물은 본문 끝의 _Captured output_에서 재현됩니다. 여기에 제시된 어떤 수치도 예측이나 합성 벤치마크가 아닙니다. 숫자가 _추정치(estimate)_인 경우, 그렇게 명시하고 공식을 보여줍니다.
파트 1 — 인간을 위한 설명
문제점: 같은 생각을 계속 되풀이하는 에이전트
Sentinel은 JavaScript 코드베이스를 감시하는 자율 에이전트입니다. 몇 분마다 깨어나 파일 배치(batch)를 살펴보고, 각 파일에 대해 다음 세 가지 중 하나를 결정합니다.
- SKIP — 그대로 둔다.
- EVOLVE — 변경 사항을 제안하는데, 이는 전체 안전 게이트 체계를 통과해야 한다.
- REJECT — 제안된 변경 사항이 거부되었다.
대부분의 경우 답변은 SKIP입니다. 이것은 좋은 일입니다. 매번 코드를 변경하는 보안 중심 에이전트는 에이전트가 아니라 사고(incident)에 가깝습니다.
문제는 그 에이전트가 어떻게 SKIP에 도달하느냐였습니다. 매번 파일의 내용을 다시 읽고, 파일의 위험도와 역할에 대한 그림을 재구성하며, 의사 결정 정책을 재실행했고 — 놀랍게도 — 2분 전과 똑같은 SKIP 답변에 도달했습니다. 같은 파일, 같은 위험도, 같은 역할, 같은 규칙, 같은 답변. 반복적으로요.
30초 전에 뭔가 새로운 것이 나타나기를 바라며 냉장고 문을 다시 열어본 경험이 있다면, 이미 이 버그를 이해하고 계실 겁니다.
"하지만 그건 _자신감에 차 있다_는 것만으로 충분하지 않나요?"
이것이 기사의 핵심 아이디어이며, 한 문장으로 요약하면 다음과 같습니다:
신뢰도(Confidence)는 “내 판단이 얼마나 믿을 만한가?”에 답합니다. 참신성(Novelty)은 “지난번 판단 이후로 무언가 변했는가?”에 답합니다. 이들은 서로 다른 질문입니다.
반복되는 동의를 증가하는 확신으로 간주하기 쉽습니다. “내가 SKIP이라고 10번 연속 말했다니, 정말 확실해야 해.” 이것은 함정입니다. 동일한 입력이 10번 주어지면, 증거가 10개인 것이 아니라 같은 증거가 10번 주어진 것일 뿐입니다. 만약 판단이 반복되었다는 이유만으로 신뢰도가 상승한다면, 에이전트는 자신이 아무것도 새롭게 배우지 못한 모듈을 믿도록 스스로를 설득하게 될 것입니다.
따라서 Sentinel은 이 둘을 분리하여 관리합니다:
- **신뢰도(Confidence)**는 실제 증거(학습, 롤백, 결과)에 연결되어 유지됩니다.
- **참신성(Novelty)**은 추론(reasoning) 전에 실행되는 별도의 저비용 검사입니다: “이 상황이 지난번과 같은가?”
만약 상황이 동일하다면, 그리고 지난 답변이 단순한 SKIP이었다면, Sentinel은 그 답변을 그대로 재사용합니다. 재추론도 없고, 새로운 메모리도 없고, LLM 호출도 없습니다. 이 검사가 바로 Novelty Gate입니다.
“동일한 상황”이란 무엇을 의미하는가?
단순히 “파일의 바이트가 동일하다”는 것을 넘어섭니다. 게이트는 새롭게 살펴봐야 할 모든 것의 지문(fingerprint)을 생성합니다:
- 파일 내용 (코드 자체가 아닌 해시 값),
- 위험 수준 및 역할 (예: “보안 모듈”),
- Sentinel이 현재 가지고 있는 해당 파일에 대한 신뢰도,
- 라이프사이클 상태 (관찰 중, 인증됨, 잠김 등…),
- 최근 변경 사항 중 롤백해야 했던 빈도와 학습된 페널티,
- 파일 주변 세계를 위한 증거 버전(evidence version) — 의존성 매니페스트 및 거버넌스 규칙. 규칙이 바뀌었다면 바이트가 동일하더라도 “변화 없음”은 아닙니다.
이 항목들 중 어느 하나라도 변경되면 지문이 바뀌고, 파일은 완전하고 새로운 평가를 받게 됩니다.
게이트가 하지 않을 것들
이 게이트는 의도적으로 신중합니다:
- 오직 SKIP만 재사용합니다. EVOLVE(파일을 변경하므로 지문(fingerprint)이 어쨌든 변경됨)는 절대 재생하지 않으며, REJECT나 롤백은 절대 재생하지 않습니다(이는 매번 다른 생성된 패치에 의존함). 또한 "예산 연기(budget deferred)" 결정도 재생하지 않습니다(이것은 파일에 관한 것이 아니라 시간에 관한 문제입니다).
- 파일 외부에 있는 것은 보지 못합니다. 브랜치, 머지 요청, 사람이 "이거 좀 봐주세요"라고 말하는 것, 프로덕션 런타임 오류 등—이러한 것들은 아직 지문에 포함되어 있지 않습니다. 이들은 Sentinel 자체 운영 컨텍스트(
SENTINEL_CONTEXT.md, 104행)에 미래 확장 기능으로 명시되어 있습니다. 따라서 "변경되지 않았다"는 것은 "파일별 신호가 알려줄 수 있는 범위 내에서 변경되지 않았다"는 의미입니다. - 무효화될 수 있습니다. 동결된 모듈에 대한 예약 검토, 또는 Sentinel이 SKIP를 의도적으로 재고하도록 허용하는 자율성 토큰 모두 캐시를 우회합니다.
그렇다면... 무언가를 했는가? 실제 운영 수치
실제 인스턴스가 반환한 novelty 블록은 다음과 같습니다:
{
"trackedFiles": 5,
"freshEvaluations": 42,
...
쉽게 말해 설명하면 다음과 같습니다:
| 필드 | 값 | 의미 |
|---|---|---|
trackedFiles | 5 | 게이트가 현재 결정을 기억하고 있는 파일 수. Sentinel의 스캐너는 하나의 디렉터리(libs/shared, 7개 파일)를 보고 최대 5개를 한 번에 처리하기 때문에 적은 수입니다. |
| ... |
이는 신규 평가당 약 4,476회의 재사용을 의미합니다. 대부분 변경되지 않는 5개 파일의 작업 세트라면 예상할 수 있는 수치입니다. 안정적인 파일이 한번 판단되면, 무언가 움직일 때까지 게이트는 "이전과 동일하다"고 계속 말하는 것입니다.
슬라이드에 '3억 1,500만 토큰 절약'이라고 적지 마세요
그 숫자는 엔드포인트가 반환하기 때문에 실재하는 것입니다. 이것은 우리가 달리 지불했을 OpenAI 토큰 3억 1,500만 개를 의미하는 것이 아닙니다. 세 가지 이유가 있습니다:
- 이는 반사실적 추정치입니다. 재사용할 때마다
ceil(chars / 4) + 400개의 '토큰'이 추가되는데, 이는 LLM 프롬프트와 고정 오버헤드를 합친 파일의 대략적인 크기입니다. 아무도 실제 토큰 수를 계산하지 않았습니다. - SKIP 중 대부분은 어차피 LLM을 호출하지 않았을 것입니다. 안정적이고 인증된 파일에 대한 새로운 SKIP 결정은 보통 로컬 결정입니다. 이 게이트가 실제로 방지한 것은 재파싱(re-parsing), 중간 표현(intermediate representation) 재구축, 정책 재실행(re-running policy), 메모리 재작성(re-writing memories) 등이었으며, 이는 API 지출이 아닌 CPU 및 메모리 사용량 증가였습니다. 규모로 보면: 동일 인스턴스의 결정 감사 추적 기록에는 LLM 호출에 실제로 추정된 토큰 수가 204,059개(제한된 감사 창, 141개의 결정)가 있습니다.
- 대부분은 더 큰 게이트가 존재하기 전의 일입니다. 지난 9월 초부터 Wake Gate이 어떤 파일도 보기 전에 작동합니다. 이 인스턴스에서는 **14,613틱(ticks)**을 보고했으며, 그중 13,509개(92.45 %)는 무작위 작업(no-ops)으로, 루프가 단 하나의 파일을 열지 않았음을 의미합니다. 참신한 재사용은 이제 주로 나머지 약 1,100번의 웨이크 동안 발생합니다.
솔직히 요약하자면: Novelty Gate은 이전에는 매 패스마다(설계상 2분마다, 그리고 7월 한 기간 동안에는 약 5초마다, Part 2 참조) 같은 다섯 개의 파일을 재고하던 에이전트를, 관련성이 있는 무언가가 변경될 때만 재고하는 에이전트로 바꿨습니다. 토큰 추정치는 청구서가 아니라 회피된 작업량의 크기 지표입니다.
30초 버전
- 신뢰도 ≠ 참신함. 반복은 증거가 아닙니다.
- Sentinel은 결정의 입력을 지문처럼 기록합니다. 아무것도 움직이지 않았고 마지막 답변이 SKIP이었다면, 그것을 재사용합니다.
- 실제 운영 환경: 추적된 파일 5개, 새로운 평가 42회, 재사용 188,000회, 재사용률 ≈ 0.9998.
- '절약된 토큰' 수치는 돈을 절약했다는 것이 아니라 다시 읽는 작업을 회피했다는 추정치입니다.
만약 아이디어만 보고 오셨다면 여기서 멈추셔도 됩니다. 나머지는 코드입니다.
Part 2 — 개발자를 위한 내용
게이트의 위치
게이트는 libs/core/novelty-gate.js에 위치하며, 결정 엔벨로프(decision envelope)가 구축된 직후 그리고 컴팩트 IR(compact IR), 참조 평가(reference assessment), 결정 정책(decision policy), 또는 모든 LLM 호출 이전에 Executor.processFile(libs/core/executor.js)에서 호출됩니다.
flowchart TD
A["mainLoop tick"] --> B{"Wake Gate: 실행 가능한 이벤트인가?"}
B -- "아니다" --> Z["no-op, 아무것도 스캔되지 않음"]
...
재사용 전에 두 가지 탈출구(escape hatches)가 있다는 점에 주목하세요. 하나는 예정된 검토(due scheduled review)이며 (캐시된 SKIP이 검토를 삼킨 실제 버그 이후 추가됨, tests/unit/executor-scheduled-review.test.js에 고정되어 있음), 다른 하나는 자율성 토큰(reconsider("novelty-reuse"))입니다.
지문(The fingerprint)
libs/core/novelty-gate.js, 35–69행:
function fingerprint(input = {}) {
const {
content,
...
몇 가지 의도적인 선택 사항들입니다:
- 소스가 아닌 해시(Hash, not source). SHA-256의 처음 16개 헥스 문자만 유지됩니다. 이 모듈은 코드를 저장하지 않으며 — "소스 코드 없음"이 헤더 주석에 명시되어 있고 실제로 그렇습니다.
- 소수점 둘째 자리까지 반올림된 신뢰도(Confidence rounded to 2 dp). 따라서
0.701과0.704는 동일한 지문을 생성합니다 (이를 위한 테스트가 존재함). 부동 소수점 변동은 참신함이 아닙니다.0.70에서0.71로의 변화는 참신함입니다. evidenceVersion은 파일 외부 세계를 나타냅니다. 실행기(executor)는OpportunityEngine.evidenceVersion(memory, { dependencies: readDependencyManifest() })을 전달합니다. 이것이 없으면, 고정된 모듈은 거버넌스 규칙이나 의존성이 이동한 후에도 영원히 이전의 SKIP을 재사용할 것이며 — Opportunity Engine은 존재해야 할 정확한 모듈에 도달하여 재검토할 수 없을 것입니다. 작은 부가 설명:SENTINEL_CONTEXT.md의 지문 설명(104행)은 이 필드보다 앞서 작성되었으며 7가지 구성 요소를 나열했지만, 코드는 8가지였고; 이는 본 기사와 함께 수정되었습니다.- 일반 문자열 결합(Plain string join).
console.log로 디버깅할 수 있습니다. 캐시 키가 사람이 읽을 수 있으면, "왜 이것이 누락되었지?"라는 질문에 오후 대신 몇 초 만에 답을 얻을 수 있습니다.
현재 설계상 포함되지 않은 것은 다음과 같습니다: branch/MR 상태, 인간의 확인(human confirmation), 프로덕션 실행 시간 신호(production runtime signals). 이들은 외부 관찰자(external observers)이며, 추가하는 것은 SENTINEL_CONTEXT.md:104에 향후 확장 기능으로 명시되어 있습니다.
evaluate → reuse / remember
// 재사용 가능한 경우는 이전 SKIP이 결정적일 때만 가능합니다.
function reusable(decision) {
return !!decision &&
...
remember()는 실행기(executor) 내의 세 가지 SKIP 반환 경로—라이프사이클 스킵(lifecycle skip), 로컬 "LLM 불필요" 스킵("LLM not needed" skip), 그리고 의도 게이트 스킵(intent-gate skip)—에서만 호출됩니다. 파일별로 기록되는 내용은 다음과 같습니다:
s[filePath] = {
fingerprint: fp,
decision: { action: decision.action, reason: decision.reason ?? null },
...
마지막 두 줄을 두 번 읽어보십시오. 이 부분이 프로덕션 수치를 해석하는 데 중요합니다: 지문(fingerprint)이 변경되면, reuses와 tokensSaved는 0으로 재설정되지만, evaluations는 계속 카운트됩니다. 따라서 실시간 reuses: 188000은 "각 파일의 현재 지문이 설정된 이후의 재사용 횟수"이며, 전체 누적 합계가 아닙니다. 오히려 평생 재사용 횟수를 과소 계산하고 있습니다.
핫 패스(hot path)에서 실행기는 지문이 변경되지 않았을 때 다음과 같이 작동합니다 (libs/core/executor.js, ~308–329행):
const saved = DecisionAnalytics.estimateTokens(
typeof content === "string" ? content.length : 0
);
...
결정 스트림 항목(decision-stream entry)도, 경험 요약(experience summary)도, 분석 행(analytics row)도 없습니다. 이것이 핵심입니다: 재사용된 결정은 새로운 메모리가 아닙니다.
토큰 추정기 (그리고 저 자신의 약어에 대한 수정)
저는 이 추정치를 "문자 수 / 4"로 설명해 왔습니다. 실제 함수는 libs/knowledge/decision-analytics.js의 18–21행에 있습니다:
function estimateTokens(contentLength = 0) {
const body = Math.ceil(Math.max(0, Number(contentLength) || 0) / 4);
return body + 400;
...
따라서 **ceil(chars / 4) + 400**이 됩니다. 이는 본문 크기에 고정된 프롬프트 오버헤드가 더해진 것입니다. 이 추정치는 DecisionAnalytics가 로컬에서 처리하는 모든 결정에 사용하는 것과 동일하므로, 참신도 수치와 분석 수치는 적어도 같은 (대략적인) 통화 단위에 있습니다.
코드와 실제 수치를 비교하기
estimatedTokensSaved = 314,893,830이라는 값이 reuses = 188,000 하에서 실제로 이 공식으로 나올 수 있을까요?
- 재사용당 평균: 314,893,830 / 188,000 = 1,674.97 토큰.
- 오버헤드 400을 빼면 → 본문 약 1,275 토큰 → 재사용된 파일당 평균 ~5,100 문자.
- 하한선 (모든 파일이 비어 있고, 재사용당 400): 75,200,000. ✔ 그보다 높음.
- 상한선 (모든 재사용이 가장 크게 스캔된 파일인
crypto-vault.js의 18,743 문자 → 5,086): 956,168,000. ✔ 그보다 낮음. - 기간 동안
master에서 스캔된 파일 크기는 1,370자부터 18,743자까지 다양했습니다 (crypto-vault.js는 8월에 5,645자에서 18,743자로 증가했고,translator-gateway.js는 5,235자에서 7,241자로 증가). 이 혼합된 파일들을 기준으로 5개 파일 배치당 평균 ~5,100자는 그럴듯합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기