
아무 곳에도 연결되지 않았던 승인 버튼
요약
AI 에이전트의 프로덕션 단계에서 필수적인 '인간 승인(Human-in-the-loop)' 프로세스 구축 시 발생할 수 있는 설계 오류를 다룹니다. 버튼이 시각적으로는 정상이나 실제로는 연결되지 않은 두 가지 실패 사례를 통해 인터페이스와 백엔드 레코드 간의 결합 중요성을 강조합니다.
핵심 포인트
- AI 에이전트의 자율성을 제어하기 위한 승인 게이트 설계의 중요성
- 메신저 플랫폼의 라우팅 특성으로 인한 이벤트 유실 가능성
- 승인 UI 요소와 실제 데이터 레코드 간의 결합(Binding) 필수성
- 에러 없이 작동하는 것처럼 보이는 '조용한 실패'에 대한 경계
AI 에이전트가 프로덕션(production) 단계로 넘어갈 때 흔히 따르는 안전 수칙이 있습니다. 바로 사람이 모든 것을 내보내기 전에 승인하는 것입니다.
저 또한 그 패턴을 사용합니다. AI 에이전트는 제 개인 브랜드를 위한 댓글, 답글, 짧은 게시물 등 대중 참여 콘텐츠를 초안으로 작성합니다. 제가 개인 채널에서 승인하기 전까지는 아무것도 발행되지 않습니다. 저는 제 이름을 언어 모델(language model)에 위임하지 않기 때문에 의도적으로 그렇게 구축했습니다.
지난주에 저는 저의 승인 게이트(approval gate)를 점검했습니다. 버튼은 완벽해 보였습니다. AI가 작성한 모든 제안서 아래에 초록색의 자신감 넘치고 안심을 주는 모습으로 아름답게 렌더링(render)되어 있었습니다.
그런데 그 카드들 중 일부는, 아무 곳에도 연결되어 있지 않았습니다.
버튼이 아무 일도 하지 않았던 밤
체인을 통해 처음으로 실제로 전송되는 과정은 형식적인 절차여야 했습니다. 에이전트들이 댓글 초안을 작성했고, 승인 카드가 제 채널에 놓여 있었으며, 저는 '승인(Approve)'을 눌렀습니다. 아무 일도 일어나지 않았습니다. 확인도, 오류도 없었습니다. 채팅 클라이언트(chat client)에 아주 작은 경고 아이콘이 나타난 것 외에는 아무것도 없었습니다. 그 아이콘은 무시하기 쉽고 진단하기는 불가능한 종류의 글리프(glyph)였습니다.
알고 보니 버튼은 완전히 다른 두 가지 방식으로 실패할 수 있었고, 저는 두 가지 상황을 모두 겪었습니다.
첫 번째 실패: 카드가 잘못된 메신저에 의해 게시되었습니다. 두 개의 앱이 해당 채널에 게시물을 올리는데, 그중 하나의 앱은 버튼 탭(tap)을 감지할 수 있는 능력이 없었습니다. 플랫폼은 탭을 메시지를 게시한 앱으로 라우팅(routing)하기 때문에, 저의 결정은 채팅 클라이언트 내부에서 조용히 사라졌습니다. 제가 제어하는 어떤 서버도 이를 본 적이 없었습니다.
제 쪽에서는 디버깅(debug)할 것이 아무것도 없었습니다. 왜냐하면 제 쪽에서는 아무 일도 일어나지 않았기 때문입니다. 분명히 말씀드리자면, 그 라우팅 동작은 문서화되어 있으며 정상적인 것이었습니다. 배선(wiring) 실수는 제 잘못이었습니다. 하지만 그 모호한 아이콘 외에, 인터페이스는 무엇이 잘못되었는지 또는 어디가 잘못되었는지에 대한 어떤 힌트도 주지 않았습니다. 버튼은 작동하는 버튼과 똑같이 클릭할 수 있는 것처럼 보였습니다.
두 번째 실패는 더 심각했습니다. 일부 카드들은 백킹 레코드 (backing record) 없이 저에게 도달했습니다. 탭 (tap)이 결정을 기록해야 하는 시스템의 기록 저장소 (system of record)인 승인 저장소 (approval store)는 해당 제안이 존재한다는 사실조차 전달받지 못한 상태였습니다. 그래서 제 탭이 제 서버에 도달하더라도, 그 결정을 연결할 대상이 아무것도 없었습니다. 대상이 없는 승인이었습니다. 그날 밤, 저는 제 클릭이 의미를 가질 수 있도록 수동으로 레코드를 생성했습니다.
두 실패 모두 에러를 발생시키지 않았습니다. 두 카드 모두 정상 작동하는 카드와 픽셀 단위로 동일했습니다. 바로 이 지점이 깊이 고민해 볼 만한 세부 사항입니다. 승인이 결합된 레코드 (bound record)가 아니라 사용자 인터페이스 (user-interface) 요소일 때, 시스템은 "사람이 이것을 승인했다"와 "버튼이 존재했다" 사이의 차이를 구분할 수 없습니다.
잡을 것이 없는 녹색 상태
배선 (wiring) 문제를 수정한 후, 저는 시스템을 더 신뢰하기 전에 적대적 감사 (adversarial audit)를 수행했습니다. 6가지 실패 모드 (failure-mode) 관점에 걸쳐 88번의 결합된 검토 (bounded review)를 실시했으며, 각 후보 발견 사항을 반박하기 위해 세 번의 독립적인 시도를 거쳤습니다. 그 결과 27개의 발견 사항이 살아남았고, 그중 3개는 차단 요소 (blocking)였습니다.
모든 것을 재정의하게 만든 발견 사항은 전송 로직 (send logic)의 결함이 아니었습니다. 자동화된 전송 분기 (automated send branch)는 실제 항목에 대해 단 한 번도 실행된 적이 없었습니다. 모든 예정된 실행은 빈 큐 (empty queue)를 발견하고 아무것도 하지 않았습니다. 지금까지 발생한 유일한 실제 전송은 제가 수동으로 테스트한 것이었습니다.
전송 경로는 잡을 것이 없는 녹색 상태였습니다.
건강한 시스템과 한 번도 실행된 적 없는 시스템은 동일한 대시보드를 만들어낼 수 있습니다.
세 가지 차단 요소 중 두 가지는 상세히 설명할 가치가 있습니다.
첫째, 승인이 만료되지 않았습니다. 승인은 저장소에 영구적인 사실로서 남아 있었기에, 일주일 뒤에 다시 온라인 상태가 된 일시 중단된 전송기 (paused sender)는 이미 지나가 버린 세상에 일주일 전의 "예"라는 결정을 기쁘게 던질 것이었습니다. 승인은 특정 순간에 대한 주장입니다. 제 승인에는 시계가 달려 있지 않았습니다.
둘째로, 그리고 이것은 여전히 저를 괴롭히는 부분인데, 승인은 단어가 아니라 레이블 (label)에 묶여 있었습니다. 발신자는 콘텐츠 자체가 아니라 제안과 함께 전달되는 참조 (reference)를 확인했습니다. 만약 제가 '승인 (Approve)'을 누른 후, 편집이나 재생성 (regeneration)을 통해 텍스트가 변경된다면, 시스템은 저의 이전 '예'라는 답변 아래 새로운 단어들을 전송하고, 작업이 완료된 것으로 표시하며, 실제로 배송된 것이 제가 본 것과 다르다는 어떠한 흔적도 남기지 않을 것입니다. 저의 승인은 포인터 (pointer)를 커버하고 있었고, 그 포인터의 대상은 표류할 수 있었습니다.
세 번째 차단된 발견 사항은 추가적인 계층적 보호 (layered protection)이며, 이는 진행 중인 사안이기에 공개적으로 상세히 설명하지 않겠습니다. 처음 두 가지에 대한 투명성은 그것들이 종결되었기에 지금은 비용이 들지 않습니다. 세 번째에 대한 신중함은 반대 방향을 향한 동일한 규율입니다.
거짓말을 하는 행 (The row that lies)
감사 추적 (audit trail) 속에 숨겨진 또 다른 교훈이 하나 더 있었는데, 이는 빠지기 너무나 쉽기 때문에 제가 가장 좋아하는 교훈입니다.
빌드 초기에는 저장소에 '승인됨 (approved)'과 '발송됨 (fired)'이라고 읽히는 행 (row)들이 있었습니다. 그것들은 마치 엔드 투 엔드 (end-to-end) 증거처럼 보였습니다. 즉, 결정이 내려졌고, 전송이 이루어졌으며, 파이프라인 (pipeline)이 작동한다는 것이었습니다. 하지만 그것들은 전혀 그렇지 않았습니다. 그 행들은 합성된 테스트 항목 (synthetic test entries)이었으며, 그것들이 담고 있던 '발송됨 (fired)' 플래그는 제 시스템이 스스로에게 수행한 쓰기 (write) 작업이었습니다. 실제 발송 단계는 단 한 번도 실행된 적이 없었습니다.
자신의 데이터베이스에 있는 '발송됨 (Fired)'은 시스템이 스스로에 대해 말하는 문장입니다. 어떤 테스트 경로라도 그렇게 말할 수 있습니다. 내부의 '발송됨 (fired)' 플래그는 외부 세계가 무언가를 수락했다는 것을 증명하지 못합니다. 외부적 증명은 감사 추적 (audit trail)에 보존되어 있고 검사가 가능한 아티팩트 (artifact)와 연결된, 플랫폼 자체의 객체 식별자 (object identifier)로부터 시작됩니다.
그래서 우리는 세 가지 다리를 가진 증명 표준을 채택했습니다. 실제 인간의 상호작용에 의해 생성된 결정 행 (decision row). 생성된 대상에 대해 플랫폼 자체의 식별자를 담고 있는 영수증 (receipt). 그리고 플랫폼에서 확인할 수 있는 대상 그 자체. 이보다 못한 것은 여러분의 데이터베이스가 스스로에게 축하를 건네는 것에 불과합니다.
그 기준을 충족하는 첫 번째 전송(send)은 가장 위험 부담이 적은 대상으로 의도적으로 선택한 제 자신의 게시물에 달린 댓글이었습니다. 타인의 게시물에 이루어진 첫 번째 완전 통합된 전송(send)은 하루 뒤에 이루어졌으며, 경로 어디에도 수동적인 연결(manual bridging)은 없었습니다.
실제 승인 경로(approval path)가 보장해야 하는 것
감사(audit) 결과에 따라 조치하는 과정에서 4개의 코드베이스(codebases)에 걸쳐 11번의 별도 수정이 이루어졌습니다. 에이전트(agents)들이 모든 수정을 수행했고, 저는 그것이 반영되기 전에 모든 내용을 직접 검토하고 승인했습니다. 첫 빌드(build)부터 강화된 체인(hardened chain)이 완성되기까지 약 60시간이 소요되었으며, 감사는 제가 시스템이 완료되었다고 처음 선언한 지 약 하루 뒤에 도착했습니다. 시스템은 완료되지 않았었습니다. 시스템을 선언하는 사람이 곧 그것을 만든 사람일 때, 시스템이 완료되는 경우는 드뭅니다.
이제 승인 경로(approval path)가 보장하는 내용을 구현 방식이 아닌 속성(properties)으로서 기술하겠습니다. 이들은 세 가지 계층에서 작동합니다: 결정의 무결성(integrity of the decision), 실행의 증거(evidence of execution), 그리고 이 두 가지를 둘러싼 운영의 무결성(integrity of the operation)입니다.
결정의 무결성(The integrity of the decision):
연결됨 (Wired). 탭(tap) 한 번으로 기록 시스템(system of record)에 결정 사항이 기록됩니다. 기록이 없다면 승인은 없는 것이며, 전송(send)은 거부됩니다.
결합됨 (Bound). 승인 시점에 시스템은 승인된 정확한 콘텐츠에 대해 변조 방지 지문(tamper-evident fingerprint)을 봉인합니다. 전송(send) 시점에 시스템은 해당 지문을 재계산하며, 단 한 글자라도 어긋난(drifted) 것이 있다면 전송을 거부합니다.
신선함 (Fresh). 승인은 48시간 후에 만료됩니다. 오래된 '예(yes)'는 '아니오(no)'로 취급됩니다. 실패의 방향은 언제나 거부입니다.
실행의 증거(The evidence of execution):
영수증 발행 (Receipted). 전송(send)은 플랫폼 자체의 식별자(identifier)가 원장(ledger)에 기록될 때만 전송된 것으로 간주됩니다. 내부 플래그(internal flags)는 장부 정리(bookkeeping)일 뿐, 증거가 아닙니다.
운영의 무결성(The integrity of the operation):
내구성 (Durable). 감사 원장(audit ledger)은 한 번만 기록(write-once)할 수 있으며, 이를 생성하는 기기에서 복사되어 외부로 옮겨집니다. 누군가 조용히 다시 쓸 수 있는 역사는 역사가 아닙니다.
소유됨 (Owned). 전송 경로에는 명시된 소유자(owners)가 있으며, 경로에 변경이 생기면 자동으로 그들의 검토를 요청합니다. 가장 안전이 중요한(safety-critical) 코드는 무심코 변경하기 가장 어려워야 합니다.
살아있음 (Alive). 전송을 수행하는 시스템은 매일 자신이 살아있음을 증명해야 합니다. 그렇지 않으면 죽은 송신자와 건강한 유휴(idle) 송신자는 동일하게 보이기 때문입니다. 침묵이 건강함을 가장하도록 방치해서는 안 됩니다.
이 모든 과정 속에서 한 가지 사실은 유지되었습니다: 승인되지 않은 콘텐츠는 결코 외부로 나가지 않았다는 점입니다. 모든 실제 전송은 사람이 직접 승인(human-tapped)했으며, 공백은 악용되기보다는 잠재적인 상태로 머물렀습니다. 이것은 안심할 이유가 아닙니다. 그것은 운의 정의일 뿐이며, 사고가 발생하기 전에 감사(auditing)를 수행하는 전체 목적은 확률로부터 안전이라는 이야기를 빌려 쓰는 것을 멈추는 데 있습니다.
당신의 승인 버튼을 위한 다섯 가지 질문
만약 당신 팀의 AI 안전(AI safety) 이야기가 "사람이 모든 것을 승인한다"라면, 다음 다섯 가지 질문은 그 이야기가 하나의 시스템인지 아니면 단순한 느낌인지 알려줄 것입니다.
- 누군가 승인을 클릭했을 때, 그 결정은 정확히 어디에 기록됩니까? 만약 대답이 "채팅 스레드에"라면, 당신은 기록이 아닌 느낌(vibe)을 가지고 있는 것입니다.
- 승인이 무엇을 커버했습니까: 실제로 발송된 정확한 콘텐츠입니까, 아니면 클릭 이후에도 변경될 수 있는 콘텐츠에 대한 참조(reference)입니까?
- 오래된 승인이 실행될 수 있습니까? 만약 만료되는 것이 없다면, 당신이 지금까지 부여한 모든 승인은 여전히 살아있는 탄약과 같습니다.
- 전송이 실제로 발생했다는 것을 무엇이 증명합니까: 당신의 시스템이 설정한 플래그(flag)입니까, 아니면 콘텐츠가 발송된 플랫폼으로부터 받은 영수증(receipt)입니까?
- 만약 전송을 수행하는 시스템이 오늘 밤 멈춘다면, 내일이 조용한 하루와 다르게 보일 것이 있습니까?
지난 에디션에서 저는 지시사항(instructions)이 제어 흐름(control flow)이 아니라고 주장했습니다. 에이전트의 프롬프트(prompt)에 작성하는 것은 모델이 가중치를 두는 제안일 뿐, 시스템이 강제하는 규칙이 아닙니다. 이번 주의 주제도 한 단계 위에서 동일한 법칙을 따릅니다. 인간 참여(human in the loop) 역시 루프가 하나의 구조—연결되고, 구속되며, 신선하고, 영수증이 발행되며, 내구성이 있고, 소유되며, 살아있는 구조—가 되기 전까지는 제어 흐름이 아닙니다. 버튼을 만드는 것은 쉬운 부분이었습니다. "승인됨(approved)"을 느낌이 아닌 사실로 만드는 데에는 적대적 감사(adversarial audit)와 11번의 검토된 수정 작업이 필요했으며, 그 감사는 제가 제 자신에게 거짓말을 하고 있다고 가정했기에 비로소 이루어질 수 있었습니다.
관문은 그것이 무엇에 부착되어 있느냐만큼만 실재합니다.
원문은 danmercede.com에 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
