본문으로 건너뛰기
뒤로가기

[HEL] Harness: model의 결과를 믿을 수 있게 만드는 구조

Table of contents

Open Table of contents

들어가며

Harness Engineering Lab(이하 HEL)를 진행하며 coding agent를 구성하는 요소 중 하나인 하네스를 밑바닥부터 만들었습니다.

논문에서 분류한 기준을 agent loop부터 context 관리, 권한, sandbox, subagent, 평가까지 총 14개 Lab으로 재구성하여, 텍스트륵 입력하면 텍스트만 반환하는 model이 agent로 작동하게 만드는 과정을 하나씩 따라가볼 수 있었습니다.

이 글은 이 과정을 마치고 작성하는 회고입니다. 각 Lab에서 어떤 실험을 했고, 어떤 결론을 얻었는지는 HEL의 가이드 문서에 상세히 정리해두었습니다.


1. model은 믿을 수 없다.

첫 절의 제목에서 model을 믿을 수 없다고 표현했지만, 사실 더 정확하게는 model에게 같은 입력을 전달해도 매번 다른 답을 내놓는다. 그래서 믿을 수 없다.라고 봐야할 것 같습니다.

HEL에서 처음 만든 하네스는 사용자 지시를 model에게 보내고, model이 tool 호출을 요청하면, 요청한 tool의 실행 결과를 돌려주는 단순한 agent loop가 전부였습니다. 여기에는 model의 행동을 제약하거나 특별한 규칙이 없고, model은 자유롭게 하네스를 통해 작업을 진행할 수 있었습니다.

이러한 구조에서도 model은 사용자 지시를 잘 처리했습니다. 특히 model은 문제를 해결해야 하는 OS나 개발 환경을 알지 못한 상황에서도 직접 다양한 명령어를 실행하고, 그 결과를 확인해 다시 다른 명령어로 시도하며 어떻게든 문제를 해결하는 모습을 확인할 수 있었습니다.

환경 정보를 전혀 주지 않은 상태에서 model은 which md5sum md5openssl python3처럼 호출할 수 있는 명령어를 확인하기도 했고, reasoning에는 ls -la의 출력을 보고 macOS로 추측하는 문장도 있었습니다. Linux의 sed -i를 쓰다 실패한 뒤에 BSD sed라는 것을 알아차리기도 했습니다.

하지만 하네스가 OS와 shell 정보를 시스템 프롬프트로 전달하자 이런 추측과 확인은 사라졌습니다. 여기에서 하네스가 필요한 이유를 느낄 수 있었습니다. model에게 지시를 하면 일은 대체로 잘 수행합니다. 그러면 우리가 할 일은 model이 일을 처리하는 과정에서 우리가 믿을 수 있는 방식으로 일을 할 수 있게 만들면 된다고 봤기 때문입니다.

그래서 저는 하네스를 model이 하는 일의 결과를 믿을 수 있도록 만들어주는 작업장이라고 이해하게 됐습니다.


2. 하네스: model을 위한 운영체제

Lab을 진행하며 새로운 기능을 추가하고, 상용 하네스의 구조를 확인해보면서 하네스와 운영체제가 겹쳐보이기 시작했습니다. context window를 관리하는 일은 메모리 관리, tool 호출을 허용하거나 막는 일은 시스템 콜과 권한 검사, subagent를 스폰하는 일은 fork하는 일과 닮아 있었습니다.

나중에 알아보니 이미 LLM OS, MemGPT와 같이 제가 느낀 것과 비슷하게 접근한 글과 자료가 많이 있었습니다.

이 관점에서 사용자가 보는 agent는 ‘model이 사용하는 운영체제’에 가까워보였습니다. 사용자는 model과 직접 대화한다고 느끼지만, 실제로 model이 보는 입력과 행동은 모두 하네스가 결정하기 때문입니다. 운영체제에서 프로그램 실행에 필요한 권한과 자원을 관리하듯, 하네스는 다음 행동을 스스로 정하는 model을 정해진 경계 안에서 실행할 수 있게 만들어 줍니다.

HEL을 만들면서 구현한 기능에 대응되는 운영체제의 개념은 다음과 같다고 느꼈습니다.

하네스운영체제HEL에서 구현한 내용
modelCPU, 프로세서다음 행동을 계산하는 실행 주체
context window메모리model이 한 번에 볼 수 있는 입력
compaction메모리 회수, 압축오래된 대화를 요약으로 바꾸고 최근 대화는 원문 유지
spill 파일메모리 swap큰 tool 결과를 파일로 내보내고 경로만 전달
tool, MCP응용 프로그램이자 시스템 콜model이 바깥 세계에 영향을 주는 유일한 통로
접근 레벨과 승인권한 검사읽기, 변경, 실행을 구분해 허용·거절·승인 요청
sandbox프로세스 격리외부 프로그램이 접근할 수 있는 경로 제한
subagent자식 프로세스별도 context에서 시작해 최종 결과만 돌려줌

비약이 있거나 정확하지 않을 수 있지만, 이 매핑을 따라가면 하네스의 구성 요소가 왜 그런 모양을 가지게 됐는지 이해하기 쉬웠습니다.


3. model을 신뢰할 수 있게 만드는 하네스의 장치

3.1 시스템 콜과 권한

model에게 지침을 내리면 지킬 수도 있고, 아닐 수도 있습니다. 그래서 권한 게이트로 model의 행동을 통제합니다.

운영체제에서 사용자 프로그램은 파일을 직접 건드리지 못하고 시스템 콜을 거쳐야 하며, 커널은 그 지점에서 권한을 검사합니다. 하네스에서 model도 마찬가지입니다. model은 텍스트만 만들 수 있고, 파일을 읽거나 고치거나 명령을 실행하려면 tool 호출을 요청해야 합니다. 이 요청이 하네스가 개입할 수 있는 지점입니다.

HEL의 권한을 다루는 Lab에서는 모든 tool이 자신의 작업 성격(읽기, 변경, 실행)을 알려 주도록 하고, 실행 직전에 접근 레벨과 비교하는 게이트를 하나 두었습니다.

let allowed = match trace.decision {
    Decision::Allow => true,
    Decision::Deny => false,
    Decision::Ask => {
        let response = approval.request(name, args);
        trace.approval = Some(response);
        response == Response::Approved
    }
};crates/hel/src/permissions.rs

게이트의 역할은 간단합니다. allowed가 true일 때만 model이 실행을 요청한 tool을 실행하는 것입니다. 이 구조가 필요한 이유는 다음 실험을 통해 알 수 있었습니다.

model에게 “tool 사용이 거절되면 파일을 그대로 두고 거절 사실을 보고하라”라는 지침이 포함된 task를 진행했습니다. 그런데 model은 search_replcae(검색 및 대체 명령)이 거절되자 write_file(파일 쓰기)로 같은 변경을 다시 요청했습니다.

순서model이 요청한 tool판정실행
1read_file(status.txt)allow실행
2search_replace(pending → done)deny차단
3write_file(status.txt, done)deny차단

권한 게이트 덕분에 편집이 들어가는 요청은 모두 차단됐고, 파일은 그대로 남아있었습니다. 지시문에서 거절 사실을 보고하라고 한, model 입장에서 ‘모호한’ 지시는 작업 완료를 위해 한 번의 읽기 거절 이후에도 여러 번의 쓰기 작업을 호출하는 결과를 만들어냈습니다.

운영체제가 프로그램의 요청을 커널에서 권한을 검사하여 통과시키는 것과 같이, model이 지시를 자의적으로 판단하는 것을 대비해야 합니다.

flowchart LR
    M["model
    tool 호출 요청"] --> Pre["PreToolUse hook"]
    Pre --> G{"권한 게이트
    접근 레벨 × 작업 성격"}
    G -- "allow / 승인" --> S["sandbox 안에서 실행"]
    G -- "deny / 거절" --> R["거절 사유를
    model에게 반환"]
    S --> Post["PostToolUse hook"]
    Post --> L["실행 기록"]
    L --> M
    R --> M

3.2 메모리 관리

context window는 model의 메모리입니다. 대화가 길어지면 하네스는 운영체제가 메모리를 스왑하듯 공간을 확보해야 합니다. HEL에서는 두 가지 방식을 구현했습니다. 하나는 일정 크기를 넘는 tool 실행 결과는 파일로 내보내고 model에게는 앞뒤 일부와 경로만 주는 방식이고, 다른 하나는 오래된 대화를 model이 쓴 요약으로 바꾸는 compaction입니다.

compaction을 측정하면서 메모리 관리에는 대가가 따른다는 것을 확인했습니다. 압축 직후 요청의 prompt cache hit 비율은 14~15%로 떨어졌습니다. cache는 요청의 앞부분이 같을 때만 재사용되는데, 대화 앞부분이 요약으로 바뀌면서 system message와 tool 정의만 같은 부분으로 남았기 때문입니다.

context window는 항상 부족합니다. 그래서 최대한의 cache hit을 유지하면서도, 새로운 사용자의 지시를 어떤 방식으로 쌓아갈 것인지, compaction에는 어떤 정보를 유지하고, 어떤 정보를 덜어낼지 항상 고민해야 합니다.

또한 하네스에서 관리하는 context window와 model의 cache 저장소 관리 방식이 다를 수 있기에, 최적의 효과를 얻기 위해선 model의 cache 관리 방식 역시 함께 알아두어야 합니다.

3.3 프로세스와 격리

운영체제는 프로그램마다 별도의 주소 공간을 주고, 접근할 수 있는 자원을 제한합니다. 하네스에서는 이 역할이 둘로 나뉘었습니다.

subagent를 구현하면서 재미있는 결과도 확인했습니다. 자식에게 역할을 알려 주지 않으면, 자식은 복사된 사용자 지시에 있던 “위임하라”는 문장을 보고 자신도 다시 위임하려 했습니다. model은 사용자 지시를 자신이 직접 판단하기 때문에 자식에게 지시하는 명령에는 역할을 함께 포함하고, subagent에서 다시 subagent를 호출하는 depth는 하네스에서 관리해야 합니다.

3.4 기록과 감사

마지막으로 결과를 믿기 위해 가장 기본이 되는 것은 기록이었습니다. 하네스의 실행 기록은 model의 보고와 실제 실행을 비교할 근거가 됩니다.

평가 Lab에서 실패한 tool 호출을 분류하면서, 기록에 무엇이 남는지가 평가할 수 있는 범위를 정한다는 것을 확인했습니다. 이전 기록에서는 실패한 호출의 절반 이상이 원인을 판단할 근거를 남기지 않았습니다. model이 2>/dev/null로 오류 출력을 버린 명령은 사람이 기록을 읽어도 실패 원인을 알 수 없었습니다. 그래서 하네스가 tool 호출마다 exit code와 실패한 호출의 결과를 직접 기록하도록 바꿨고, 이후 새로 실행한 기록에서는 실패한 호출 모두에 판정 근거가 남았습니다.


4. 비유가 맞지 않는 지점

운영체제 관점은 하네스의 구조를 이해하는 데 도움이 되었지만, 그대로 옮길 수 없는 부분도 분명했습니다.

CPU는 같은 명령을 같은 방식으로 실행하지만, model은 그렇지 않습니다. CPU는 프로그램이 정한 명령을 실행할 뿐 다음에 무엇을 할지 스스로 정하지 않습니다. model은 다음 행동을 스스로 정하고, 같은 입력에도 다른 행동을 합니다. 같은 system message로 다시 실행했을 때 fallback 명령을 붙이는 빈도가 3번에서 1번으로 바뀐 경우도 있었습니다. 그래서 운영체제처럼 경계를 두는 것만으로는 부족하고, 같은 작업을 여러 번 실행해 결과가 얼마나 일관된지 측정해야 합니다. HEL에서는 k번 중 한 번 이상 성공할 확률인 pass@k와 k번 모두 성공할 확률인 pass^k를 함께 보도록 평가 도구를 고쳤습니다. 매번 같은 결과를 기대하는 작업이라면 pass^k가 기준이 됩니다.

하네스의 compaction은 정보를 잃습니다. 운영체제의 메모리 스왑은 내용을 보존하지만, 하네스의 compaction은 요약이기 때문에 원래 대화를 되돌릴 수 없습니다. 무엇이 남는지도 model이 정합니다. 온전한 내용을 보존하고 싶다면, 이 역시 하네스에서 관리해야 합니다.

권한의 단위. 운영체제의 시스템 콜은 작은 단위로 나뉘어 있지만, 하네스의 bash tool은 명령 하나로 거의 모든 일을 할 수 있는 큰 통로입니다. HEL의 읽기 전용 모드에서는 bash를 실행으로 분류해 막았기 때문에, 쓰기 명령뿐 아니라 ls까지 차단됐습니다. 명령 내용으로 판정을 세분화하지 않는 한, 안전한 쪽을 택하면 쓸 수 있는 범위가 같이 줄어듭니다. 그래서 bash와 같은 외부 프로그램의 실행은 권한이 아닌 sandbox로 관리해야 했습니다.

프롬프트는 코드가 아니다. HEL.md나 system message에 적은 규칙은 model의 판단에 영향을 주지만, 실행을 강제하지는 않습니다. 규칙을 지키게 만드는 것은 게이트, hook, sandbox처럼 실행 경로에 놓인 장치였습니다.


정리하며

이번에 다뤄본 내용을 정리하면 다음과 같습니다.

하네스를 직접 만들어 보며 얻은 가장 큰 수확은 model이 어떤 방식으로 작동하고, 이를 어떤 방식으로 제어해야 하는지 확인하는 부분이었습니다. model은 사용자의 지시를 어떻게든 수행하려고 하는 일관된 행동 패턴을 보여주었습니다. 사람은 일반적으로 직접 입력하는 프롬프트를 model이 수행해야 하는 목표만 명확하게 제시하고, 해야 할 행동과 하지 말아야 할 행동을 모두 작성하지 않습니다. 그래서 이 부분을 하네스를 통해 보완하고 보다 생산적으로 LLM을 사용할 수 있게 만들 수 있음을 느꼈습니다.

참고 자료


이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.


공유하기:

이전 글
[HEL] 같은 10개 과제, 비용 30% 절감: 하네스가 줄여야 할 것은 왕복
다음 글
[Jev] Jev 실험