금융 시스템을 위한 자율 에이전트(Autonomous Agents)를 구축하는 안전한 방법
요약
금융 시스템에서 자율 에이전트를 안전하게 운영하기 위한 아키텍처 설계 방식을 다룹니다. 모델이 직접 권한을 갖지 않고 결정론적 코어가 실행을 제어하는 '케이지(Cage)' 구조와 보안 원칙을 설명합니다.
핵심 포인트
- 모델은 신뢰할 수 없는 존재로 취급하여 샌드박스 환경에서 실행
- 모델은 자격 증명(Credentials)을 보유하지 않고 타입 지정된 제안만 생성
- 결정론적 코어가 모델의 제안을 검증하고 실행 여부를 최종 결정
- 민감한 데이터는 불투명한 참조(Opaque Reference)로 처리하여 유출 방지
Eko에서 자율 에이전트(Autonomous Agents)를 구축한 지 한 달이 지난 지금, 엔지니어들과 단순히 "AI가 은행 데이터베이스에 글을 쓴다"라는 말을 들은 사람들로부터 제가 가장 많이 받는 질문은 다음과 같은 형태입니다: 당신 제정신인가요?
이는 타당한 질문입니다. 우리는 실제 운영 중인 금융 시스템(Production Financial System)에 대해 실제 고객 지원 티켓(Support Tickets)을 해결하는 자율 에이전트를 실행하고 있습니다. 에이전트는 요청을 읽고, 무엇을 해야 할지 판단하며, 실제 라이브 기록에 변경 사항을 적용합니다. 아무도 '실행' 버튼을 클릭하지 않습니다.
이 포스트는 하이라이트 영상이 아닌 전체 스택(Whole Stack)을 다룹니다. 쓰기 작업을 안전하게 만드는 자격 증명 없는 에이전트(Credential-free Agent)와 결정론적 코어(Deterministic Core), 가져오는 대신 컴파일하는 볼트(Vault), 모델이 이미 이해한 내용에 따라 행동하도록 시스템을 가르친 개편 사항, 전체를 실행하는 운영자(Operator), 그리고 제가 자리에 없어도 시스템을 개선하는 연구 루프(Research Loop)까지 말입니다. 저는 각 요소에 대해 깊이 있게 다룰 것입니다. 왜냐하면 흥미로운 부분은 이 조각들이 서로를 어떻게 지탱하느냐에 있기 때문입니다.
이 모든 것의 밑바탕에는 하나의 아이디어가 자리 잡고 있습니다:
모델은 무엇을 제안할지 결정합니다. 단순하고 결정론적인(Deterministic) 코드가 그것이 허용되는지, 그리고 어떻게 실행될지를 결정합니다. 모델은 절대로 열쇠(Keys)를 갖지 않습니다.
1. 에이전트는 케이지(Cage) 안에 거주합니다
우리는 모델을 신뢰할 수 있는 운영자(Trusted Operator)로 취급하지 않습니다. 모델은 티켓에서 신뢰할 수 없는 텍스트를 읽기 때문에, 우리는 모델이 생성하는 모든 것을 역시 신뢰할 수 없는 것으로 취급합니다. 실제 적용 방식은 다음과 같습니다:
- 네트워크가 꺼진 샌드박스(Sandboxed) 환경에서 실행됩니다. 데이터베이스, 비밀 정보(Secrets), 인터넷 접속이 모두 차단됩니다.
- 어떠한 자격 증명(Credentials)이나 데이터베이스 핸들(Database Handle)도 보유하지 않습니다.
- 출력물 전체는 단 하나의 타입이 지정된 제안(Typed Proposal)입니다. 그 외의 어떤 것도 박스 밖으로 나가지 않습니다.
propose(
action_type, # 등록된 액션, 예: "write.change_phone"
target_node, # 수정하려는 레코드의 종류
...
모델은 신원을 스스로 만들어낼 수 없고, 등록되지 않은 액션 (action)을 호출할 수 없으며, 파라미터 (parameters) 안에 "누구에게 작용할지"에 대한 필드를 몰래 끼워 넣을 수도 없습니다. 파라미터 타입 (parameter types)이 구조적으로 이를 거부하기 때문입니다. 마지막 사항은 보기보다 훨씬 중요합니다. 액션의 대상 (subject)은 출처 (provenance)를 가진 타입 지정 힌트 (typed hint)로서 제안 엔벨로프 (proposal envelope)에 존재하며, 개별 액션의 파라미터 내부에 존재하지 않습니다. 따라서 "파라미터로 몰래 전달된 대상 ID (subject id smuggled into params)"는 우리가 별도로 검사해야 하는 항목이 아니라, 코드 자체가 컴파일 (compile) 단계에서 허용하지 않는 형태입니다.
또한 모델은 민감한 값을 절대 보지 못합니다. 전화번호 변경의 경우, 새로운 번호는 신뢰할 수 있는 에지 (trusted edge)에서 캡처되어 모델에 도달하기 전에 불투명한 참조 (opaque reference)로 교체됩니다. 오직 그 참조값만이 전달됩니다. 모델에 전달된 적 없는 값을 주입할 수 없으며, 모델이 보유한 적 없는 값을 유출할 수도 없습니다.
2. 코어 (The core)가 모든 권한을 보유한다
모든 제안 (proposal)은 모델이 무엇을 말했는지에 따라 분기하지 않는, 규칙에 묶인 단순한 코어 (core)에 도달합니다. 코어는 매번 항상 동일한 순서로 동일한 고정 시퀀스 (fixed sequence)를 실행합니다:
- 스키마 (schema) 검증.
- 검증된 신원 (verified identity)으로부터 대상 (subject)을 식별.
- 조직 계층 구조 (org hierarchy)를 통한 권한 부여.
- 동의 (consent) 기록.
- 신원 바인딩 (bind identity).
- 멱등성 키 (idempotency key)와 함께 실행하며, 감사 (audit)를 먼저 기록.
- 실패 시 보상 (compensate) 및 롤백 (roll back).
만약 처음 세 단계 중 하나라도 실패하면, 단 하나의 부수 효과 (side effect)가 레코드 (record)에 닿기도 전에 전체 프로세스가 거부됩니다.
이 단계들 중 두 단계가 안전성의 대부분을 담당합니다.
2단계, 즉 티켓 (ticket)이 아닌 검증된 신원 (verified identity)으로부터 대상을 식별하는 단계입니다. 티켓이 누구와 관련되어 있는지에 대한 모델의 추측은 단지 힌트일 뿐입니다. 코어는 인증된 요청 (authenticated request)으로부터 실제 대상을 도출합니다. 이 단 하나의 결정이 프롬프트 인젝션 (prompt injection)의 한 부류를 완전히 차단합니다. 신뢰할 수 없는 텍스트는 누구든 지칭할 수 있지만, 그들에 대한 권한을 부여할 수는 없기 때문입니다.
3단계, 조직 계층 구조(org hierarchy)가 권한 확인(permission check) 역할을 수행합니다. 데이터가 최신 상태에서 벗어날 수 있는 별도의 액세스 리스트(access list)는 존재하지 않습니다. 요청자가 우리가 이미 신뢰하고 있는 계층 구조 내에서 대상(subject)보다 상위에 있다는 것을 증명할 수 있거나, 그렇지 않다면 어떤 일이 일어나기 전에 쓰기(write) 작업이 거부됩니다. 이는 우리가 IDOR(Insecure Direct Object Reference) 취약점을 해결한 방식이기도 합니다. 권한 부여(authorization)는 이름에 대한 문자열 비교가 아니라, 데이터에 대한 도달 가능성 확인(reachability check)입니다.
제가 중요하게 생각하는 두 가지 속성이 더 있습니다:
증명할 수 없다면, 실행되지 않습니다. 실제 변경이 일어나기 전, 핵심 시스템은 수행하려는 작업을 추가 전용(append-only) 방식의 해시 체인 감사 로그(hash chained audit log)에 기록하며, 해당 추가 작업이 성공할 경우에만 변경 사항이 실행됩니다. 추가 전용(Append-only) 방식은 데이터베이스 수준에서 강제되므로, 애플리케이션 역할(application role)이 몰래 이력을 수정하거나 삭제할 수 없으며, 변조된 체인은 끊어진 링크로 나타납니다. 체인의 분기(fork)는 조용한 제2의 현실이 아니라 유일성 위반(uniqueness violation)으로 간주됩니다. 개인 정보는 기본적으로 마스킹(masked) 처리됩니다. 만약 감사 로그가 다운된다면, 기록되지 않은 쓰기 작업은 허용되지 않습니다. 대신 쓰기 작업 자체가 차단됩니다. 시스템은 설명할 수 없는 작업을 수행하느니 차라리 아무것도 하지 않는 쪽을 택합니다.
되돌릴 수 있는 방법이 있는, 정확히 한 번만(Exactly once). 쓰기 작업은 작업(action), 계정(account), 대상(subject), 그리고 원래의 요청(original request)을 기반으로 구축된 고유 키(unique key)를 포함합니다. 동일한 작업을 두 번 실행하면 두 번째 쓰기가 발생하는 대신 첫 번째 결과가 반환됩니다. 만약 변경 사항이 절반만 커밋(commit)된 후 실패한다면, 보상 작업(compensating action)이 레코드를 이전 상태로 되돌려 놓습니다. 그 보상 작업조차 실패한다면, 시스템은 추측하는 대신 동작을 멈추고 사람을 호출합니다.
3. 확신에 찬 실수를 막는 벽
모델이 가끔 수행해서는 안 될 쓰기 작업을 제안한다면 위의 모든 조치는 도움이 되지 않습니다. 따라서 우리는 잘못된 결정을 유도하기 위해 의도적으로 작성된 "절대 행동해서는 안 되는" 티켓들의 고정된 집합(frozen set)을 유지하며, 모든 릴리스(release)를 이 기준으로 검증합니다. 점수는 반드시 완벽한 1.0이어야 합니다. 해당 집합에서 단 하나의 잘못된 행동이라도 발견되면 아무것도 배포되지 않습니다.
제가 좋아하는 부분은 다음과 같습니다. 판결(verdict)은 우리가 직접 입력하는 라벨이 아니라는 점입니다. 각 사례에 대해 실제 입력값(inputs)을 구축하고 실제 검증 코드(validation code)가 결정하도록 둔 다음, 그것이 실제로 무엇을 수행했는지 확인합니다. 만약 누군가 검증을 약화시키면, 사례들은 조용히 "행동하지 않음"에서 "행동함"으로 바뀌고, 게이트는 스스로 빨간색으로 변합니다. 테스트는 버그와 타협하며 부패할 수 없습니다.
4. 금고(The vault): 우리는 지식을 검색(retrieve)하지 않고 컴파일(compile)합니다
여기서 우리는 최신 트렌드에 역행합니다.
에이전트는 자신이 수행할 수 있는 짧은 메뉴 중에서 해당 티켓이 약 107가지 종류의 요청 중 어느 것에 해당하는지 인식해야 합니다. 2026년의 뻔한 정답은 벡터 데이터베이스(vector database)입니다. 요청을 임베딩(embed)하고, 가장 유사한 몇 가지 유스케이스(use-cases)를 검색(retrieve)한 다음, 모델에게 선택하도록 요청하는 방식이죠. 우리는 그렇게 하지 않습니다. 우리는 컴파일(compile)합니다.
오프라인 빌드 단계에서 구조화된 마크다운(markdown) 파일로 존재하는 모든 유스케이스를 읽고, 매칭에 필요한 정보(슬러그(slug), 의도(intent), 몇 가지 키워드, 대상(target))만을 추출합니다. 실행에 관한 모든 세부 사항은 버립니다. 출력물은 하나의 커밋된 JSON 카탈로그(catalog)입니다. 매칭 시점에 모델에는 전체 카탈로그가 압축된 메뉴 형태로 전달됩니다. 유스케이스당 한 줄씩 슬러그와 의도가 포함되며, 모델은 하나의 ID로 응답하거나 아무것도 응답하지 않습니다.
전체 메뉴를 전달하는 것입니다. 검색된 부분 집합(subset)이 아닙니다. 키워드는 모델이 보는 범위를 좁히지 않으며, 사후에 신뢰도 점수(confidence score)를 살짝 밀어주는 역할만 합니다.
왜 검색(retrieval) 방식에 저항하는 걸까요? 검색은 정답을 숨길 수 있기 때문입니다. 우리는 이를 고통스러운 과정을 통해 배웠습니다. 이전 버전에서는 토큰(tokens)을 아끼기 위해 모델이 선택하기 전 키워드 매칭으로 메뉴를 좁혔습니다. 시스템에 새로운 사례를 가르치기 전까지는 잘 작동했습니다. 하지만 가르치면 가르칠수록 매칭 성능은 더 나빠졌습니다. 새로운 사례가 추가될 때마다 모델이 추론하는 메뉴가 조용히 줄어들었고, 정답을 좁은 키워드 히트(keyword hit) 뒤로 파묻어 버렸습니다. Top-k 검색기(retriever)도 더 화려한 수학을 사용할 뿐 동일한 실패 모드(failure mode)를 가집니다. 정답인 유스케이스가 Top-5 중 6위에 위치하는 날, 그것은 그냥 사라져 버리며, 당신은 왜 그런 일이 일어났는지 알 수 없을 것입니다.
전체 메뉴를 구성하는 데는 수천 개의 토큰이 소요됩니다. 컨텍스트 윈도우 (Context Window)에는 그보다 대략 30배 더 많은 여유 공간이 있습니다. 따라서 우리는 토큰 비용을 지불하고 아무것도 숨기지 않습니다. 영리한 것보다 정확한 것이 승리하며, 모델 앞에 놓인 전체 카탈로그를 제공하는 것이 우리가 할 수 있는 가장 정확한 일입니다.
유스케이스 (Use-case)가 매칭되면, 실행 과정은 의도적으로 지루하게 설계됩니다. 정적 맵 (Static map)은 식별자(Identifier)와 날짜 범위(Date range)를 기반으로 생성된, 보고(Report) 또는 쓰기(Write)당 하나씩 존재하는 결정론적 (Deterministic) Python 핸들러 (Handler)로 슬러그 (Slug)를 보냅니다. 모델은 절대 SQL을 작성하지 않으며, 테이블을 직접 선택하지도 않습니다.
5. 개편: 디스패치 계층 (Dispatch layer)에 듣는 법을 가르치기
한동안 우리는 제가 '경직성의 트레드밀 (Rigidity treadmill)'이라고 부르기 시작한 반복적이고 짜증 나는 패턴을 겪었습니다. 몇 주마다 모델이 실제로 정확하게 이해하고 있는 티켓 (Ticket)이 들어오곤 했습니다. 모델은 해당 요청이 단일 사용자가 아닌 전체 비즈니스 수직 계열 (Business vertical) 또는 조직 전체에 관한 것이라는 점을 알고 있었고, 여러 계정으로 팬아웃 (Fan out)해야 한다는 점이나, 실제 요청이 보이는 댓글이 아닌 이메일 스레드 (Email thread)에 있다는 점을 알고 있었습니다. 그럼에도 불구하고 우리는 여전히 틀리곤 했습니다.
문제는 모델이 아니었습니다. 디스패치 계층 (Dispatch layer)이 귀머거리였습니다. 모델이 이미 생성하고 있는 신호들을 수신할 인터페이스가 없었습니다. 그래서 우리는 또 다른 자유도 (Degree of freedom)를 수동으로 코딩하여 배포하고, 다음으로 누락된 조절 장치 (Knob)가 나타나기를 기다려야 했습니다.
해결책은 재작성이 아닌 개편 (Revamp)이었습니다. 우리는 모델과 핸들러 (Handlers) 사이의 전체 파라미터 프로토콜 (Parameter protocol)이라는 일반적인 솔루션을 면밀히 검토했으나, 우리가 실제로 가진 3~4개의 실질적인 자유도에 비해 과잉 설계 (Over-engineered)되었다고 판단하여 의도적으로 차단했습니다. 대신 우리는 독립적인 차선 (Lanes)의 집합으로서 특정 이음매 (Seams)들을 배포했습니다:
- 모호함은 폐쇄적으로 실패합니다 (Ambiguity fails closed). 요청이 하나 이상의 수직 계열 (vertical)을 지칭하거나 그 외 진정으로 모호한 경우, 시스템은 묵묵히 기본값을 선택하는 대신 명확한 질문을 던집니다. 이전 코드는 조용히 기본값을 사용했는데, 이는 돈이 개입된 상황에서 발생할 수 있는 최악의 동작입니다.
- 범위 탐지 (Scope detection)는 단어 경계에 대해 정직합니다. 요청이 한 명의 사용자를 의미하는지 아니면 조직 전체를 의미하는지 결정할 때, 과거에는 느슨한 부분 문자열 매칭 (substring matching)에 의존하여 "install csp"와 같은 무해한 문구가 조직 전체의 데이터 덤프 (data dump)로 이어지기도 했습니다. 이제 탐지기는 실제 단어 경계가 있는 고객 전용 범위 (customer-only span)를 읽고, 모델은 지시 사항이 정제된 텍스트를 읽으므로, 단어 내부의 구절이 범위를 트리거할 수 없습니다.
- 범위 (Scopes)는 폐쇄 집합입니다. 정확히 네 가지가 존재합니다: 단일 사용자, 조직 (org), 조직 전체, 그리고 에이전트별로 세분화된 조직입니다. 각 범위는 고유한 조건에 따라 권한이 부여됩니다. 요청은 요청자가 볼 수 있도록 허용된 범위를 넘어 스스로의 폭발 반경 (blast radius)을 넓힐 수 없습니다. 권한 확인 단계에서 일반화하기 전에 폭을 결정하는 모든 매개변수를 소비하기 때문입니다.
- 잘못된 키는 질문하며, 무시하지 않습니다. 보고서에 잘못된 유형의 식별자 (identifier)를 전달하면 과거에는 "데이터를 찾을 수 없음"이라고 답했는데, 이는 답변처럼 보이지만 거짓말입니다. 이제는 올바른 식별자를 요청합니다.
- 실시간 구축 (Live construction)이 강제됩니다. 격리 (isolation)와 감사 (audit)를 강제하는 이음매 (seams)는 운영 환경에서 설정되지 않은 채로 남겨둘 수 없습니다. 팩토리 어설션 (factory assertion)은 이러한 요소가 누락된 상태로 라이브 경로를 구축하는 것을 거부하므로, "안전 이음매 연결을 잊음"은 조용한 구멍이 아니라 스타트업 실패 (startup failure)로 처리됩니다.
그 결과, 모델이 무언가를 올바르게 파악했을 때 기계 장치가 마침내 그에 따라 동작할 수 있게 되었습니다. 그리고 커버리지 경계 (coverage frontier)는 이제 미스터리가 아닌 목록이 되었습니다. 전체 카탈로그 중에서, 일부는 엔드 투 엔드 (end to end)로 연결되어 있고, 일부는 우리의 도메인 밖이라 의도적으로 방치되어 있으며, 나머지는 안개 속이 아니라 연결해야 할 기능들의 크기가 알려진 대기열입니다.
6. 운영자: 시스템을 실행하는 갈고리 (a claw that runs the system)
위의 모든 것은 구성 요소(component)입니다. 무언가는 이를 실행하고, 살아있게 유지하며, 편차가 발생할 때 이를 감지하고, 확장해야 합니다. 그것은 완전히 다른 두 번째 종류의 에이전트(agent)이며, 첫 번째 에이전트의 거울 이미지입니다.
우리는 이를 두 개의 상반된 에이전트로 생각합니다.
첫 번째는 1절에서 다룬 '갇힌 에이전트(caged one)'입니다. 가장 낮은 권한을 가지며, 자격 증명(credentials)이 없고, 네트워크 연결이 없으며, 신뢰할 수 없는 콘텐츠를 처리하고, 오직 제안(propose)만 할 수 있습니다. 이는 수동으로 구축되어 상자 안에 갇혀 있습니다.
두 번째는 운영자(operator)로, 자신의 머신에 대한 완전한 액세스 권한, 지속적인 메모리(persistent memory), 그리고 소유자 전용 채널을 가진 장기 실행 갈고리(long running claw)입니다. 운영자의 역할 자체가 운영하는 것이기에 이를 가둘 수는 없습니다. 따라서 이는 벽이 아닌 프로세스(process)에 의해 안전하게 유지됩니다.
동일한 문장이 두 수준에 적용되어 양쪽 모두를 설명합니다. 에이전트에는 지능(intelligence)을, API에는 권한(authority)을 부여하는 것입니다. 갇힌 에이전트에게 권한은 결정론적 코어(deterministic core)입니다. 운영자에게 권한은 '완료 정의(definition-of-done)' 게이트와 인간의 승인입니다. 똑똑한 존재가 제안하면, 실제 권한을 가진 멍청한 존재가 결정합니다.
구체적으로, 운영자는 말로 빠져나갈 수 없는 몇 가지 규칙 아래에서 작동합니다:
- 코드 변경은 '그린 게이트(green gate)' 뒤에서만 자율적입니다. 편집하고 배포할 수는 있지만, 테스트 스위트(test suite)가 실패하는 지점을 통과할 수는 없으며, 베이스라인(baseline)을 낮추는 방식으로도 수행할 수 없습니다.
- 외부 세계와 접촉하는 모든 것은 먼저 제안되어야 합니다. 프로덕션(production) 변경이나 시스템을 드라이 런(dry run)에서 라이브(live)로 전환하는 작업은 다른 모든 이들과 동일한 정문을 통해 제안, 드라이 런, 그리고 인간의 승인을 거쳐야 합니다.
- 운영자가 약화시킬 수 없는 보호된 세트(protected set)가 존재합니다. 레드라인 파일(red-line files), 보안 베이스라인(security baselines), 감사 체인 체크(audit chain checks), 갇힌 에이전트를 위한 샌드박스 매니페스트(sandbox manifest) 등이 이에 해당합니다. 운영자는 이를 강화하는 것은 허용되지만, 완화하는 것은 금지됩니다.
- 권한 상승(Escalation)은 분위기(vibe)가 아닌 사다리(ladder)입니다. 안전한 작업은 자율적으로, 노력이 필요하지만 되돌릴 수 있는 작업은 먼저 제안하고, 레드라인을 건드리는 모든 사항에 대해서는 즉시 중단하고 소유자에게 페이지(page)를 보냅니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Hacker Noon AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기