
레버에는 하네스가 필요하다
요약
다양한 자산군(주식, 외환, 암호화폐 등)을 지원하는 복잡한 트레이딩 시스템의 인프라를 재구축하는 과정을 다룹니다. AI를 단순 코드 생성기가 아닌, 복잡한 의존성을 분석하고 설계 계획을 수립하는 '설계자' 및 '감사자'로 활용하는 워크플로우를 제시합니다.
핵심 포인트
- 다양한 시장 시그널을 통합하기 위한 트레이딩 레이어의 근본적 재설계 필요성
- AI를 코드 작성이 아닌 시스템 설계 및 의존성 그래프 분석 도구로 활용
- 빌더(Builder)와 감사자(Auditor) 역할을 분리하여 설계의 무결성을 검증하는 워크플로우
저는 시스템이 다음과 같은 사항을 지원하기를 원합니다: 단일 백테스트 (backtest) 내에서 조정되는 최대 2만 개의 시그널 (signals). 어떤 것들은 미국 주식 (US equities)으로만 학습되었습니다. 어떤 것들은 외환 (forex)으로만, 어떤 것들은 암호화폐 (crypto)로, 어떤 것들은 홍콩 주식 (Hong Kong stocks)으로 학습되었습니다. 어떤 것들은 여러 시장을 의도적으로 혼합한 풀링된 교차 시장 데이터셋 (pooled cross-market dataset)에서 왔습니다. 각 시그널은 고유한 시장 범위 (market scope)를 가지며, 그 범위는 시그널마다 다릅니다. 백테스트 자체도 사용자가 무엇을 선택하느냐에 따라 단일 시장 (single-market)이 될 수도 있고 교차 시장 (cross-market)이 될 수도 있습니다. 따라서 한 번의 실행에서 시그널 A는 주식만 거래하고, 시그널 B는 외환만, 시그널 C는 둘 다 거래하며, 시그널 D는 해당 실행의 시장에 포함되지 않기 때문에 아무것도 거래하지 않을 수 있습니다. 포트폴리오 (portfolio)는 이들의 합집합이며, 각 시그널은 자신만의 영역에서 작동하고 그 외의 모든 곳에서는 침묵합니다. 이를 구축하는 데 AI의 도움을 받는 것은 모델 (model)의 문제가 아니었습니다. 그것은 하네스 (harness)의 문제였습니다.
이를 작동시키기 위해, 저는 트레이딩 레이어 (trading layer)를 밑바닥부터 다시 구축해야 했습니다. 단순히 패치 (patch)를 하거나 모서리를 다듬는 수준이 아니었습니다. 포지션 (position)이 무엇인지, 현금 (cash)이 무엇을 의미하는지, 가치가 어떻게 측정되는지, 그리고 언제 거래가 존재할 수 있는지 결정하는 시스템의 부분을 재구축했습니다. 포지션 구조체 (position struct)에는 악기 유형 (instrument type)이 없었습니다. 현금은 통화별 인덱스 맵 (currency-indexed map) 대신 단일 숫자였습니다. 포트폴리오 가치 평가 (Portfolio valuation)는 모든 것이 주식이라고 조용히 가정하고 있었습니다. 수수료 로직 (Commission logic)은 하드코딩 (hardcoded)되어 있었습니다. 증거금 체크 (Margin checks)는 실제 증거금 (actual margin) 대신 명목 가치 (notional value)를 사용했습니다. 다섯 가지 근본 원인이 너무 단단하게 얽혀 있어서, 하나를 당기면 다른 것들이 움직이지 않을 수 없었습니다. 서로 다른 시장에서 학습된 시그널들은 밑바탕이 되는 인프라스트럭처 (infrastructure)가 여전히 단일 시장인 것처럼 가장하고 있다면 함께 거래할 수 없습니다.
나는 AI에게 이 문제를 설명했습니다. 돌아온 결과물은 코드가 아니었습니다. 그것은 계획이었습니다. 6개의 웨이브 (waves)로 구성된 30개의 티켓 (tickets)이었습니다. 어떤 웨이브들은 병렬로 실행될 수 있었고, 어떤 것들은 순차적으로 진행되어야 했습니다. 한 웨이브는 원자적 단위 (atomic unit)였습니다. 즉, 세 개의 티켓이 단일 커밋 (single commit)으로 반영되거나, 아니면 아예 반영되지 않아야 했습니다. AI는 변경 사항들 사이의 의존성 그래프 (dependency graph)를 살펴보고, 어떤 것들이 동시에 안전하게 수행될 수 있는지, 그리고 어떤 것들이 너무 일찍 시작될 경우 서로를 조용히 방해할지를 계산해 냈습니다.
그 중 어떤 것도 실행되기 전에, 나는 두 번째 터미널을 열고 다른 세션에 이 설계를 감사 (audit)해 달라고 요청했습니다. 이것이 현재 내가 일하는 방식입니다. 빌더 (Builder)와 감사자 (auditor), 분리된 세션, 분리된 컨텍스트 (context). 하나가 설계하면, 다른 하나는 단 한 줄의 코드가 작성되기 전에 그 설계를 검토합니다. 나는 빌더가 완성된 작업물을 자신 있게 제시했지만 정작 언급하지 않은 빈틈이 발견되거나, 대화 초기에 내렸던 결정을 조용히 잊어버리거나, 완화하거나, 혹은 더 편리한 무언가로 다시 써버린 상황을 여러 차례 겪은 후에 이 방식을 시작했습니다. 테스트가 없는 코드를 신뢰하지 않는 법을 배우는 것과 마찬가지로, 단일 세션을 신뢰하는 것을 멈추는 법을 배우게 된 것입니다.
감사 결과가 돌아왔습니다. 22개의 티켓이 건전해 보였습니다. 모든 상태가 녹색 (green)이었습니다. 나는 CI 파이프라인 (CI pipeline)이 단 한 번에 통과했을 때 느끼는 종류의 만족감을 느꼈습니다. 즉, 나는 즉시 무언가 잘못되었다고 의심했습니다.
제 예상이 맞았습니다. 두 번째 단계(wave two), 즉 스키마 마이그레이션(schema migration) 단계였습니다. 하나의 원자적 단위(atomic unit)로 묶여 있어야 할 세 개의 티켓(ticket)이 있었습니다. 설계(design)에는 이들이 나열되어 있었지만, 명세(specs)는 텅 비어 있었습니다. 마이그레이션 단계(migration steps), 컬럼 정의(column definitions), 백필 로직(backfill logic)이 전혀 없었습니다. AI는 두 번째 단계의 티켓 제목과 요약을 작성한 뒤, 마치 스키마 작업이 아무도 보지 않을 때 예의 바르게 스스로 완료될 것처럼 세 번째 단계로 넘어가 버렸습니다. 이후의 모든 단계 설계는 두 번째 단계에서 도입되었어야 할 컬럼과 타입(types)을 참조하고 있었지만, 정작 두 번째 단계의 티켓들은 그저 자리 표시자(placeholders)에 불과했습니다. 마치 데이터베이스 스키마가 직업적 예의를 갖춰 스스로 처리라도 한 것처럼 말입니다.
대충 훑어본다면 설계는 내부적으로 일관성 있어 보였습니다. 두 번째 단계에는 티켓 제목, 요약, 의존성(dependencies)이 있었습니다. 구조를 갖춘 듯한 냄새가 났습니다. 다만 실제 내용이 없을 뿐이었습니다. 감사자(auditor)가 이를 잡아낼 수 있었던 이유는, 단순히 의존성 그래프(dependency graph)를 감상하는 것이 아니라 티켓 본문(ticket bodies)을 읽도록 설정되어 있었기 때문입니다.
저는 이전의 모든 컨텍스트(context)를 파헤쳤습니다. 설계 문서(design docs), 이전 티켓 명세(ticket specs), 미완성된 노트, 제가 썼던 것으로 기억나는 것들, 그리고 분명히 기억나지는 않지만 분명히 존재했던 것들을 뒤졌습니다. 저는 두 번째 단계가 무엇을 해야 하는지를 조각조각 맞춰냈고, AI에게 그 세 개의 티켓을 처음부터 다시 설계하도록 시켰습니다. 마이그레이션 단계, 컬럼 정의, 백필 로직 등 모든 것을 포함해서 말입니다. 그런 다음 감사자에게 새로운 두 번째 단계 설계가 세 번째부터 여섯 번째 단계와 비교했을 때 의존성 체인(dependency chain)이 실제로 온전한지 검증하도록 했습니다.
그 시점에서 저는 완전히 다른 AI로 교체하여 전체 설계를 처음부터 다시 감사하도록 했습니다. 30개의 모든 티켓, 6개의 모든 단계에 대해, 새로운 시각으로 말입니다.
결과는 14개의 심각도 높은(high-severity) 결함 발견으로 돌아왔습니다.
14개입니다. "고려해 보는 것이 좋겠습니다" 수준의 발견이 아닙니다. "이런 방식으로 구축하면 망가질 것입니다"라는 14개의 발견입니다. 한 티켓(ticket)의 명세(spec)가 참조한 컬럼 이름이, 다른 티켓의 마이그레이션(migration)에서 이미 이름을 변경해 버린 사례가 있었습니다. 두 개의 티켓은 2단계 재설계(wave two redesign) 이후 스키마(schema)와 더 이상 일치하지 않는 값에 대해 테스트 기대치(test expectations)를 정의했습니다. 한 티켓의 타입 정의(type definition)에서는 특정 필드를 필수(required)로 내보냈지만, 이를 사용하는 티켓에서는 해당 필드가 선택 사항(optional)일 것으로 예상했습니다. 오케스트레이터 계약(orchestrator contract)은 스테이지(stages)의 상수 튜플(const-tuple)을 지정했지만, 한 스테이지가 잘못된 타입 시그니처(type signature)로 등록되어 있었습니다. 이와 같은 사례들이 계속되었습니다. 이 모든 것은 실행 중인 코드가 아니라 설계 명세(design specs)에 있었습니다. 아직 코드는 작성되지 않은 상태였습니다. 이 정도 규모에서는 AI가 순수하게 기억력만으로 30개의 티켓에 걸친 구현 버그(implementation bugs)를 잡아낼 수는 없습니다. AI는 모든 코드를 한 번에 컨텍스트(context)에 담아 유지할 수 없습니다. AI가 할 수 있는 일은 30개의 티켓 명세를 서로 교차 참조(cross-reference)하여 설계가 스스로 모순되는 지점을 찾아내는 것입니다. 그것은 컨텍스트 윈도우(context window) 규모에 적합한 문제입니다. 6단계에 걸쳐 실행 중인 코드를 디버깅(debugging)하는 것은 그렇지 않습니다.
제가 이에 대해 어떤 우아한 반응을 보였다고 거짓말하지는 않겠습니다. 저는 그저 잠시 그 자리에 앉아 있었습니다. 저는 AI가 30개의 티켓을 설계하고, 빈약한 단계(hollow wave)를 수정하며, 감사를 통과하는 것을 지켜보았고, 제가 예정보다 앞서 나가고 있다고 느꼈습니다. 그러다 두 번째 AI가 해당 설계에 여전히 14개의 논리적 모순이 있다고 차분하게 설명하는 것을 보았습니다. 모델은 첫 번째 터미널과 두 번째 터미널 사이에 더 멍청해지지 않았습니다. 모델은 동일했습니다. 변한 것은 모델에게 무엇을 찾으라고 명령했는지, 얼마나 많은 컨텍스트가 주어졌는지, 그리고 자신의 작업물을 스스로 검토할 수 있는지 아니면 자신의 기억력을 믿어야만 하는지의 차이였습니다.
저는 계속 이 문제로 돌아오게 됩니다. 어려운 점은 모델과 제가 동일한 제약 사항을 동시에 보면서, 동일한 각도에서, 동일한 문제를 바라보고 있는지 확인하는 것입니다. 저는 더 나은 용어가 떠오르지 않아 제 노트에 이를 '설계 정렬 (design alignment)'이라고 불러왔습니다. 그리고 모델이 더 똑똑해졌다고 해서 이것이 거저 주어지는 것은 아닙니다.
이제 저에게는 두 개의 AI 세션과 14개의 설계 모순(design contradictions) 목록이 남았습니다. 명백한 다음 단계는 빌더(builder)가 감사자(auditor)가 찾아낸 문제들을 수정하도록 만드는 것이었습니다.
그 뒤로 두 모델이 서로 주고받는 수십 차례의 라운드가 이어졌습니다. 제가 예상했던 것보다 더 많은 라운드였고, 합리적이라고 느껴지는 수준보다도 더 많은 라운드였습니다. 감사자가 무언가를 플래그(flag)하면, 저는 그 발견 사항을 빌더에게 전달했습니다. 그러면 빌더는 사양(spec)을 수정하고 기존 설계가 왜 틀렸는지 설명했습니다. 저는 그 수정 사항을 다시 감사자에게 전달했습니다. 감사자는 그 수정이 새로운 불일치(inconsistency)를 유발했다고 말했습니다. 다시 빌더에게 돌아갔습니다. 빌더는 그 불일치를 패치(patch)한 뒤, 다른 두 개의 발견 사항이 실제로는 서로 연관되어 있어 함께 처리되어야 한다는 점을 알아차렸습니다. 다시 감사자에게 돌아갔습니다.
그들은 높은 심각도(severity)부터 낮은 심각도 순으로 목록을 처리해 나갔습니다. 빌드 시 파괴될 모순(Will-break-on-build contradictions)을 먼저 처리하고, 그다음 의존성 체인(dependency chain) 내의 논리적 공백(logic gaps), 그다음 티켓 경계(ticket boundaries) 간의 타입 불일치(type mismatches), 마지막으로 명명(naming) 및 컨벤션(convention) 문제를 처리했습니다. 20라운드 정도 지났을 때쯤, 감사자는 "이 필드 이름은 기술적으로는 정확하지만, 두 단계 상위 티켓을 고려하면 오해의 소지가 있다"와 같은 발견 사항을 반환하기 시작했습니다. 저는 그들이 진짜 문제들을 다 써버리고 서로의 숙제를 교정(copyediting)하는 단계로 넘어갔다는 것을 깨달았습니다.
그들이 설계가 완료되었다는 데 합의했을 때쯤, 정작 인내심을 잃은 쪽은 10분 전에 이미 한계에 다다랐던 저였습니다.
그들이 마침내 서로의 사양에서 수정할 사항을 더 이상 찾지 못하게 되었을 때, 저는 한 번 더 검토를 진행했습니다. 저는 감사자에게 빌더가 설계한 내용뿐만 아니라, 왜 그렇게 설계했는지까지 검토하도록 했습니다. 각 티켓 뒤에 숨겨진 동기(motivation), 그것이 선택한 접근 방식과 거부한 접근 방식, 그리고 문제를 올바른 각도에서 바라보고 있는지까지 말입니다. 그 과정에 몇 라운드가 더 소요되었습니다. 하지만 그 이후에야, 저는 해안선(shore)이 보이기 시작했습니다.
설계가 확고해지자, 저는 실행을 시작했습니다. 각 티켓(ticket)은 자신만의 세션(session)을 가졌습니다. 각 세션은 자신이 무엇을 건드릴 수 있는지, 그리고 무엇이 금지 구역인지를 알고 있었습니다. 첫 번째 웨이브(wave)가 끝나면, 두 번째 웨이브가 자동으로 이어졌습니다. 세 번째 웨이브의 두 티켓이 공유 의존성(shared dependency)을 가지고 있을 때는, 시스템이 한쪽이 커밋(commit)될 때까지 다른 한쪽을 대기시켰습니다. 저는 호루라기를 불며 교통정리를 하느라 중간에 서 있지 않았습니다. 저는 풀 리퀘스트(pull request)를 검토하고 있었습니다. 저는 직접 코드를 커밋하는 것을 좋아합니다. 그래야 실제로 무언가를 구축했다는 느낌이 들고, AI가 저를 대신해 git을 실행할 필요가 없으므로 토큰(token)도 절약됩니다. 작은 차이지만, 30개의 티켓을 처리하다 보면 그 차이는 쌓이게 됩니다.
이와 같은 의존성 그래프(dependency graph)를 마주했을 때:
이것의 구체적인 형태는 start_927.py라는 스크립트였습니다. 이 스크립트는 의존성 그래프를 읽고, 어떤 티켓들이 함께 실행될 수 있는지와 어떤 것들이 기다려야 하는지를 인코딩(encode)한 뒤 세션을 실행했습니다. 단 하나의 명령어로 말이죠. 하네스(harness)는 코드를 작성하는 것이 아니었습니다. 하네스는 모델이 코드를 작성하는 동안 30개의 세션이 서로 충돌하지 않도록 유지하는 역할을 했습니다.
실행 후, 마지막 단계는 테스트 정렬(test alignment)이었습니다. 단위 테스트(unit test), 그다음은 통합 테스트(integration test), 마지막으로 시스템 테스트(system test) 순이었습니다. 결국 테스트가 제약 조건(constraint)이 되었고, 구현(implementation)은 그 테스트에 맞게 형성되는 것이었습니다. 만약 무언가가 맞지 않는다면, 테스트가 틀린 것이 아니라 구현이 틀린 것이었습니다.
다시 한 걸음 물러나 생각해 봅시다. AI는 빠릅니다. 코드를 작성하는 데 있어 비합리적일 정도로 빠릅니다. AI는 당신의 뇌와 코드베이스(codebase) 사이에서 번역 계층(translation layer)처럼 자리 잡고 있으며, 그 번역은 어떤 인간도 따라갈 수 없는 속도로 일어납니다. 하지만 다음 모델이 더 좋아진다고 해서 사라지지 않는 약점들이 존재합니다.
그 응답들은 결정론적 (deterministic)이지 않습니다. 같은 질문을 두 번 던지면, 두 개의 서로 다른 답변을 얻게 됩니다. 때로는 둘 다 괜찮을 수도 있습니다. 때로는 하나가 운영 환경 (production)에 투입될 때까지는 알아차리지 못할 방식으로 틀릴 수도 있습니다. 그리고 컨텍스트 (context) 문제가 있습니다. 모델은 자신이 보지 못하는 것을 기억하지 못합니다. 만약 관련 제약 조건이 컨텍스트 창 (context window) 밖에 있거나, 압축되어 버린 이전 세션에 있었다면, 모델은 그 빈칸을 그럴듯한 무언가로 채워 넣을 것입니다. '그럴듯하다'는 것은 그저 예의 바른 방식으로 틀렸다는 뜻일 뿐입니다.
이 두 가지 문제는 모두 모델이 학습된 방식 때문에 발생합니다. 강화학습 (Reinforcement learning)은 모델에게 과업 (task)을 완수하는 법이 아니라, 대화를 완수하는 법을 가르쳤습니다. 모델은 반드시 정답을 내놓는 법이 아니라, 당신을 만족시키는 답변을 내놓는 법을 배웠습니다. 모델은 동조하고, 말을 흐리고, 사전 학습 (pretraining) 과정에서 절반만 배운 것들 사이를 보간 (interpolating)하고 있을 때조차 자신감 있게 들리도록 하는 법을 배웠습니다. 때때로 이는 모델이 더 둥글둥글한 응답을 제공하기 위해 기술적 정밀함을 희생한다는 것을 의미합니다. 업계에서는 이를 일컫는 용어가 있습니다. 바로 슬롭 (Slop)입니다. 그리고 이것은 실재합니다.
그렇다면 어떻게 그런 것을 통제 가능하게 만들 수 있을까요? 그것을 하네스 (harness)에 넣어야 합니다. 당신의 머릿속에 있는 설계와 AI의 컨텍스트 안에 있는 설계를 일치시키는 것부터 시작하십시오. 그리고 그 결과물을 그럴듯함, 자신감, 또는 어조 따위에는 신경 쓰지 않는 견고한 테스트 스크립트 (test scripts)에 맞춰 정렬하는 것으로 마무리하십시오. 그 과정 사이에서, 모든 티켓 (ticket), 모든 웨이브 (wave), 모든 감사 통과 (audit pass)는 모델이 교묘히 빠져나갈 수 있는 범위를 좁히는 또 다른 제약 조건이 됩니다.
저는 그것이 바로 하네스라고 생각합니다. 모델의 출력을 하네스에 통과시키면, 그 반대편으로 나오는 것은 모델이 당신이 아마 원할 것이라고 생각한 형태가 아니라, 당신이 실제로 원했던 형태를 갖추게 됩니다.
아무도 모델 그 자체를 구매하지 않습니다. 당신은 무언가의 내부에 포함된 모델을 구매하는 것입니다. 가공되지 않은 API (raw API)와 당신이 실제로 마주하게 되는 것 사이의 차이점은 바로 하네스 (harness)입니다. 가중치 (weights), 학습 데이터 (training data), 추론 능력 (reasoning capability)은 동일합니다. 하지만 하네스가 컨텍스트 (context)를 어떻게 관리하는지, 어떤 도구 (tools)를 노출하는지, 오류 (errors)를 어떻게 처리하는지, 그리고 모델이 스스로의 작업물을 검토하게 하는지 아니면 자신의 기억을 그대로 믿도록 강제하는지에 따라 완전히 다른 경험을 제공하게 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기