자율 엔지니어: AI가 실제 은행 티켓을 엔드 투 엔드로 해결하는 방법과 이를 정확하게 유지하는 머신
요약
Eko에서 구축한 자율 에이전트가 은행 업무와 같은 민감한 작업을 엔드 투 엔드로 안전하게 처리하는 아키텍처를 설명합니다. 모델의 권한 남용을 방지하기 위해 컴파일 단계의 타입 검증, 불투명 참조 활용, 그리고 엄격한 코어 로직을 통한 권한 부여 방식을 제안합니다.
핵심 포인트
- 타입 힌트와 스키마 검증을 통해 모델의 파라미터 조작 및 프롬프트 인젝션 차단
- 민감 정보 보호를 위해 모델에 직접적인 값 대신 불투명 참조(opaque reference) 전달
- 조직 계층 구조를 활용한 권한 확인으로 IDOR 취약점 근본적 해결
- 모든 작업은 실행 전 해시 체인 감사 로그에 기록되어 증명 가능해야 함
Eko에서 자율 에이전트 (autonomous agents)를 구축한 지 한 달이 지난 지금, 엔지니어들과
모델은 정체성을 임의로 만들어낼 수 없고, 등록되지 않은 액션 (action)을 호출할 수 없으며, 파라미터 (parameters)에 "대상자 (who to act on)" 필드를 몰래 끼워 넣을 수도 없습니다. 파라미터 타입 (parameter types)이 구조적으로 이를 거부하기 때문입니다. 마지막 사항은 보기보다 훨씬 중요합니다. 액션의 대상 (subject)은 제안 봉투 (proposal envelope) 상에 출처 (provenance)가 포함된 타입 힌트 (typed hint)로 존재하며, 개별 액션의 파라미터 내부에 존재하지 않습니다. 따라서 "파라미터에 대상 ID를 밀반입하는 것"은 우리가 별도로 수행하는 체크 항목이 아니라, 코드가 컴파일 (compile) 단계에서부터 허용하지 않는 형태입니다.
또한 모델은 민감한 값을 절대 볼 수 없습니다. 전화번호 변경의 경우, 새로운 번호는 신뢰할 수 있는 에지 (trusted edge)에서 캡처되어 모델에 도달하기 전에 불투명한 참조 (opaque reference)로 교체됩니다. 오직 그 참조 값만이 전달됩니다. 모델이 전달받지 않은 값을 주입할 수 없으며, 모델이 보유하지 않은 값을 유출할 수도 없습니다.
2. 코어 (The core)가 모든 권한을 보유한다
모든 제안은 모델이 무엇을 말했는지에 따라 분기하지 않는, 단순하고 규칙에 묶인 코어 (core)에 도달합니다. 코어는 매번 동일한 순서로 동일한 고정 시퀀스 (fixed sequence)를 실행합니다:
- 스키마 (schema) 검증.
- 검증된 정체성 (verified identity)으로부터 대상 (subject)을 식별.
- 조직 계층 구조 (org hierarchy)를 통한 권한 부여.
- 동의 (consent) 기록.
- 정체성 바인딩 (bind identity).
- 멱등성 키 (idempotency key)와 함께 실행하며, 감사 (audit)를 먼저 기록.
- 실패 시 보상 (compensate) 및 롤백 (roll back) 수행.
처음 세 단계 중 하나라도 실패하면, 단 하나의 부수 효과 (side effect)가 레코드 (record)에 닿기도 전에 전체 프로세스가 거부됩니다.
이 단계들 중 두 단계가 안전성의 대부분을 담당합니다.
2단계, 즉 티켓이 아닌 검증된 정체성으로부터 대상을 식별하는 단계입니다. 티켓이 누구에 관한 것인지에 대한 모델의 추측은 단지 힌트일 뿐입니다. 코어는 인증된 요청 (authenticated request)으로부터 실제 대상을 도출합니다. 이 단 하나의 결정이 프롬프트 인젝션 (prompt injection)의 한 부류를 완전히 차단합니다. 신뢰할 수 없는 텍스트는 누구든 지목할 수 있지만, 그들에 대한 권한을 부여할 수는 없기 때문입니다.
3단계, 조직 계층 구조(org hierarchy)가 권한 확인(permission check) 역할을 수행합니다. 시간이 지나면서 데이터가 어긋날 수 있는 별도의 액세스 리스트(access list)는 존재하지 않습니다. 요청자가 우리가 이미 신뢰하고 있는 계층 구조 내에서 대상(subject)보다 상위에 있다는 것이 증명되거나, 그렇지 않으면 아무런 작업이 일어나기 전에 쓰기(write)가 거부됩니다. 이는 우리가 IDOR(Insecure Direct Object Reference) 취약점을 해결한 방식이기도 합니다. 권한 부여(authorization)는 이름에 대한 문자열 비교가 아니라, 데이터에 대한 도달 가능성 확인(reachability check)입니다.
제가 중요하게 생각하는 두 가지 속성이 더 있습니다:
증명할 수 없다면, 실행되지 않습니다. 실제 변경이 일어나기 전, 코어(core)는 수행하려는 작업을 추가 전용(append-only) 방식의 해시 체인 감사 로그(hash chained audit log)에 기록하며, 해당 추가 작업이 성공해야만 변경이 실행됩니다. 추가 전용 방식은 데이터베이스 수준에서 강제되므로, 애플리케이션 역할(application role)이 조용히 이력을 수정하거나 삭제할 수 없으며, 변조된 체인은 끊어진 링크로 나타납니다. 체인의 포크(fork)는 고유성 위반(uniqueness violation)으로 간주되며, 조용한 두 번째 현실(silent second reality)로 인정되지 않습니다. 개인 정보는 기본적으로 마스킹(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)합니다.
오프라인 빌드(offline build) 단계에서 구조화된 마크다운(markdown) 파일로 존재하는 모든 유스케이스를 읽고, 매칭에 필요한 정보만을 추출합니다: 슬러그(slug), 의도(intent), 몇 가지 키워드(keywords), 그리고 대상(target)입니다. 실행에 관한 모든 세부 사항은 버립니다. 출력물은 하나의 커밋된 JSON 카탈로그(catalog)입니다. 매칭 시점에 모델에는 전체 카탈로그가 압축된 메뉴 형태로 전달됩니다. 유스케이스당 한 줄씩 슬러그와 의도가 포함되며, 모델은 하나의 ID로 응답하거나 아무것도 응답하지 않습니다.
전체 메뉴를 전달하는 것입니다. 검색된 부분 집합(retrieved 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)를 기반으로 생성된, 보고 (Read) 또는 쓰기 (Write) 작업당 하나씩 존재하는 결정론적 (Deterministic) Python 핸들러 (Handler)로 슬러그 (Slug)를 보냅니다. 모델은 결코 SQL을 직접 작성하지 않으며, 테이블을 직접 선택하지도 않습니다.
5. 개편: 디스패치 레이어 (Dispatch layer)에 듣는 법 가르치기
한동안 우리는 제가 '경직성의 트레드밀 (Rigidity treadmill)'이라고 부르기 시작한, 반복적이고 짜증 나는 패턴을 겪었습니다. 몇 주마다 모델이 실제로 정확하게 이해한 티켓 (Ticket)이 들어오곤 했습니다. 모델은 요청이 특정 사용자가 아닌 전체 비즈니스 버티컬 (Business vertical)이나 조직 전체에 관한 것이라는 점을 알고 있었고, 여러 계정에 걸쳐 팬아웃 (Fan out)이 필요하다는 점이나, 실제 요청이 보이는 댓글이 아닌 이메일 스레드 (Email thread)에 있다는 점을 파악했습니다. 하지만 우리는 여전히 이를 틀리게 처리했습니다.
문제는 모델이 아니었습니다. 디스패치 레이어 (Dispatch layer)가 귀를 닫고 있었던 것입니다. 모델이 이미 생성하고 있는 신호들을 수신할 인터페이스가 없었습니다. 그래서 우리는 또 다른 자유도 (Degree of freedom)를 수동으로 코딩하여 배포하고, 다음으로 누락된 조절 장치가 무엇인지 기다려야만 했습니다.
해결책은 재작성이 아닌 개편이었습니다. 우리는 모델과 핸들러 (Handler) 사이의 완전한 파라미터 프로토콜 (Parameter protocol)이라는 일반적인 솔루션을 면밀히 검토했으나, 우리가 실제로 가진 3~4개의 실질적인 자유도에 비해 과잉 엔지니어링 (Over-engineered)된 것으로 판단하여 의도적으로 차단했습니다. 대신 우리는 독립적인 차선 (Lanes)의 집합으로서 특정 이음매 (Seams)들을 배포했습니다:
- 모호함은 차단(fails closed)됩니다. 요청이 하나 이상의 버티컬 (Vertical)을 지칭하거나 진정으로 모호한 경우, 시스템은 묵묵히 기본값을 선택하는 대신 명확히 하기 위한 질문을 던집니다. 이전 코드는 조용히 기본값을 사용했는데, 이는 돈이 개입된 상황에서 발생할 수 있는 최악의 동작입니다.
- 범위 탐지 (Scope detection)는 단어 경계에 대해 정직합니다. 요청이 한 명의 사용자를 의미하는지 아니면 조직 전체를 의미하는지 결정하는 작업은 과거에 느슨한 부분 문자열 매칭 (Substring matching)에 의존했으나, 이로 인해 "install csp"와 같은 무해한 문구가 조직 전체의 데이터 덤프 (Data dump)로 변질되기도 했습니다. 이제 탐지기는 실제 단어 경계가 있는 고객 전용 스팬 (Span)을 읽고, 모델은 지시 사항이 정제된 텍스트를 읽으므로 단어 내부의 구절이 범위를 트리거할 수 없습니다.
- 범위 (Scopes)는 폐쇄 집합 (Closed set)입니다. 정확히 네 가지가 존재합니다: 단일 사용자, 조직 (Org), 조직 전체, 그리고 에이전트별로 분류된 조직입니다. 각 범위는 고유한 조건에 따라 권한이 부여됩니다. 요청은 요청자가 볼 수 있도록 허용된 범위를 넘어 스스로의 폭발 반경 (Blast radius)을 넓힐 수 없습니다. 권한 확인 단계에서 일반화하기 전에 폭을 결정하는 모든 매개변수를 소비하기 때문입니다.
- 잘못된 키 (Keys)에 대해서는 질문을 던지며, 무시하지 않습니다. 보고서에 잘못된 유형의 식별자 (Identifier)를 전달하면 과거에는 "데이터를 찾을 수 없음"이라고 답변했는데, 이는 답변처럼 보이지만 거짓말입니다. 이제는 올바른 식별자를 요청합니다.
- 실시간 구축 (Live construction)이 강제됩니다. 격리 (Isolation)와 감사 (Audit)를 강제하는 이음매 (Seams)들은 운영 환경에서 설정되지 않은 채로 남겨질 수 없습니다. 팩토리 어설션 (Factory assertion)은 이러한 이음매가 누락된 상태로 라이브 경로를 구축하는 것을 거부하므로, "안전 이음매 연결을 잊음"은 조용한 구멍이 아니라 스타트업 실패 (Startup failure)로 나타납니다.
그 결과, 모델이 무언가를 올바르게 수행하면 기계 장치가 마침내 그에 따라 동작할 수 있게 되었습니다. 그리고 커버리지 프런티어 (Coverage frontier)는 이제 미스터리가 아닌 목록이 되었습니다. 전체 카탈로그 중 일부는 엔드 투 엔드 (End to end)로 연결되어 있고, 일부는 우리의 도메인 밖이라 의도적으로 방치되어 있으며, 나머지는 안개 속이 아니라 연결해야 할 역량들의 크기가 파악된 알려진 대기열 (Queue)입니다.
6. 운영자: 시스템을 실행하는 갈고리 (A claw that runs the system)
위의 모든 것은 컴포넌트(Component)입니다. 무언가는 이를 실행하고, 살아있게 유지하며, 드리프트(Drift)가 발생했을 때 이를 감지하고, 확장해야 합니다. 그것은 완전히 다른 두 번째 종류의 에이전트(Agent)이며, 첫 번째 에이전트의 거울 이미지입니다.
우리는 이를 두 개의 상반된 에이전트로 생각합니다.
첫 번째는 1절에서 다룬 '갇힌 에이전트(Caged agent)'입니다. 가장 낮은 권한을 가지며, 자격 증명(Credentials)도 없고, 네트워크 연결도 없으며, 신뢰할 수 없는 콘텐츠를 처리하고, 오직 제안(Propose)만 할 수 있습니다. 이는 수동으로 구축되어 상자 안에 갇혀 있습니다.
두 번째는 '운영자(Operator)'로, 자신의 머신(Machine)에 대한 전체 액세스 권한, 지속적인 메모리(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 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기