
2026 년 AI 에이전트 구축 방법 (전체 과정)
AI features
- Views
- 696K
- Likes
- 368
- Reposts
- 68
- Comments
- 22
- Bookmarks
- 1.2K
TL;DR
agentic-harness 런타임에 대한 기술적 심층 분석을 다룹니다. 3계층 아키텍처, 원격 샌드박스, 컨텍스트 압축 기술을 활용하여 탄력적이고 실무에 즉시 투입 가능한 AI 에이전트를 구축하는 방법을 설명합니다.
Reading the 한국어 translation
여기 AI 빌더들에게 아무도 말해주지 않는 진실이 있다.
그들 대부분은 데모를 만들고 있다
당신이 실제로 만들어야 할 것은
프로덕션 수준의 AI 에이전트
TLDR; 읽기 싫다면, 이 링크를 에이전트에게 주고 질문하세요: ➡️https://github.com/codejunkie99/agentic-harness
이 모든 것을 시작한 트윗은 여기
문제는 대부분의 AI 엔지니어들이 에이전트를 진지하게 만들기로 마음먹었을 때, 실제로 무엇을 만들어야 할지 명확한 아이디어가 없다는 것이다.
어떤 이들은 LangChain을 선택한다. YouTube에서 멀티 에이전트 데모가 깔끔해 보이기 때문이다. 그리고 다음 2주를 Python 인터옵과 비동기 런타임 불일치와 싸우다가 결국 전체를 폐기한다.
어떤 이들은 처음부터 커스텀 오케스트레이션 레이어를 만들려고 한다: 루프, 세션 저장소, 컨텍스트 어셈블러. 하지만 인프라가 일정을 잡아먹어서 실제 에이전트는 끝내 완성하지 못한다.
또 다른 이들은 hello-world 웹훅 예제를 복사하고, JSON 응답을 받고, 시스템을 이해했다고 가정한 뒤, 세션이 10분을 넘기거나, 원격 샌드박스가 작업 중에 다운되거나, 컨텍스트 윈도우가 컴팩션 설정 없이 가득 차는 순간 처음으로 망가지는 무언가를 배포한다.
결과는 대개 같다: 많은 배관 작업, 프로덕션 에이전트는 없고, 프로덕션 에이전트 런타임이 실제로 어떻게 생겼는지에 대한 멘탈 모델도 없다.
2026년에 실제 에이전트를 구축하고 배포하는 것이 목표라면, 여섯 개의 프레임워크를 배울 필요가 없다.
하나의 런타임을 깊이 이해하여 핸들러부터 배포까지 프로덕션 에이전트를 완전히 소유할 수 있으면 된다.
즉, 다음을 배워야 한다:
- 3계층 아키텍처를 연결하여 핸들러 로직이 에이전트 코드를 건드리지 않고도 제공자 교체와 대상 변경에서 살아남도록 하는 방법
- 세션과 태스크를 올바르게 사용하여 긴 작업이 자신의 컨텍스트를 오염시키지 않도록 하는 방법
- 아무것도 재컴파일하지 않고 모델 동작을 형성하는 역할과 스킬을 작성하는 방법
- 2시간 동안 실행되는 세션이 1시간째부터 환각을 보지 않도록 컴팩션을 구성하는 방법
- HttpSessionEnv를 원격 샌드박스에 연결하여 바이너리는 로컬에서 실행되고 실행은 Linux에서 이루어지도록 하는 방법
- 올바른 빌드 타겟(네이티브, node, Cloudflare)을 선택하고, 그 사이에서 에이전트 로직을 다시 작성하지 않는 방법
- 어댑터를 직접 작성하는 대신 커넥터를 생성하고, 실제 부하에서 그 차이가 중요한 이유를 이해하는 방법
이 가이드는 실제 agentic-harness 코드베이스, 그 코드베이스로 실제 에이전트를 구축하고 부수는 6주, 그리고 디버깅에 가장 많은 시간을 소모한 실패 모드들로부터 만든 전체 기술 워크스루이다.
이 글은 4,000단어 이상이며, 리포지토리와 문서에서 직접 가져온 내용이다. 2차 요약이나 데모 수준의 예제가 아니다.
하지만 진정한 가치는 모든 섹션에 작동하는 코드 스니펫, 결정이 내려진 이유에 대한 명확한 설명, 그리고 건너뛰면 맞닥뜨리게 될 정확한 실패 모드가 있다는 점이다.
그렇게 함으로써, 읽기를 마칠 때쯤이면 첫 번째 핸들러부터 샌드박스, 그리고 무인으로 실행하는 CI 작업까지 프로덕션 에이전트를 처음부터 끝까지 소유할 수 있게 된다.
이 이해를 구축하는 데는 코드베이스와의 6주 이상의 매일 작업이 필요했으며, 대부분은 실제 조건에서 부서지기 전에는 올바르게 보였던 것들을 디버깅하는 데 소모되었다.
이제 시작해보자. ⬇️
프로젝트의 형태
두 개의 크레이트. 하나의 바이너리. 모든 실행 타겟은 설정 선택일 뿐, 재작성이 아니다.
- SDK는 모든 Rust 프로젝트에 가져올 수 있는 라이브러리다. CLI가 이를 감싼다. 에이전트는
use agentic_harness::prelude::*;로 시작하는 Rust 바이너리다. cargo build가 전체 파이프라인이다. 번들러도, 트랜스파일 단계도, 대상 머신의 언어 런타임도 없다. 하나의 자체 포함 실행 파일과 manifest.json만 있다.
모든 것을 결정한 설계 제약: 동일한 에이전트 바이너리가 노트북에서 대화형 모드로, GitHub Actions 작업에서 새 리포지토리를 클론하고, 원격 E2B 샌드박스에 대해 HTTP로, Cloudflare Worker 경계에서도 에이전트 로직을 한 줄도 변경하지 않고 실행되어야 한다.
이 코드베이스의 모든 결정은 그 제약을 존중하기 위해 존재한다.
3계층과 각 계층이 존재하는 이유
멘탈 모델은 세 개의 동심원이다. 각 경계가 어디에 있는지 아는 것은 이 가이드의 다른 어떤 것보다 더 많은 디버깅 시간을 절약해줄 것이다.
Rust 코드는 바깥쪽 원이다.
- 핸들러를 작성한다. 핸들러는 AgentContext를 받는다. 세션을 호출한다. 세션은 모델을 호출하고, 파일을 읽고, 파일을 쓰고, 셸 명령을 실행하고, 태스크를 생성하고, MCP 서버에 연결한다.
- HTTP 클라이언트를 직접 다루지 않는다. 모델 응답을 직접 파싱하지 않는다. SDK가 둘 다 처리한다.
Harness는 중간 원이다.
- 에이전트 레지스트리를 관리하고, URL 경로로 ID를 라우팅하고, 호출 간 세션 지속성을 처리하고, 세션이 커질 때 컨텍스트 컴팩션을 수행하고, 역할과 스킬 발견, 모델 선택 우선순위, 그리고 제공자 중립적인 ModelClient 트레이트를 처리한다.
- 이를 통해 Anthropic을 OpenAI로, 로컬 Ollama 인스턴스로 바꾸더라도 핸들러 코드를 건드릴 필요가 없다.
- Harness는 에이전트 로직을 제공자와 타겟 간에 재사용 가능하게 만드는 것이다.
- 또한 프로덕션에서 망가지는 모든 것(세션 상태, 컨텍스트 오버플로우, 제공자 실패, 동시 요청 순서)이 처리되는 곳이기도 하다.
실행 타겟은 안쪽 원이다.
- 로컬 파일시스템. CI 체크아웃. Daytona 또는 E2B를 가리키는 HttpSessionEnv. Cloudflare Worker 경계.
- Harness는 어떤 것을 사용하는지 신경 쓰지 않는다. 핸들러도 마찬가지다.
session.shell()과session.write()를 호출하면 Harness가 이를 기본 타겟에 필요한 것으로 변환한다. - 이 분리가 핵심이다. E2B가 새 API 버전을 출시하면 커넥터를 업데이트할 뿐, 에이전트 로직은 건드리지 않는다.
- Anthropic이 claude-opus-4-7을 출시하면 runtime.json을 업데이트할 뿐, 핸들러는 건드리지 않는다. 바깥쪽 원은 깨끗하게 유지되는데, 중간 원이 모든 변화를 흡수하기 때문이다.
런타임 설정: 모델 계층을 제어하는 파일
핸들러를 하나도 작성하기 전에, 워크스페이스에 runtime.json이 필요하다.
이것을 .agentic-harness/config.json 또는 워크스페이스 루트에 agentic-harness.json으로 넣어라. load_workspace_context()가 자동으로 읽어들인다.
런타임에서의 모델 선택 우선순위는 다음과 같다:
PromptOptions::model(...): 호출별 재정의- 선택된 역할의 모델 메타데이터: 역할별 기본값
- 런타임 설정의
defaultModel: 워크스페이스 기본값
이해해야 할 점: 모델 ID는 사용하기 전에 등록되어 있어야 한다. openaiCompatibleModels는 Harness가 내장된 chat-completions 클라이언트를 연결하는 데 사용하는 목록이다. 모델이 그 목록에 없으면, 세션 중간에 혼란스러운 실패 대신 시작 시점에 깔끔한 오류가 발생한다.
OpenAI 호환 게이트웨이의 경우 설정은 동일하게 보인다. baseUrl을 게이트웨이로 지정하라:
- runtime.json에 API 키를 리터럴로 절대 작성하지 마라.
apiKeyEnv를 사용하고 실제 키는 환경 변수에 보관하라. - Harness는 요청 시점에 환경 변수를 읽는다. 시작 시점이 아니다. 즉, 서버를 재시작하지 않고도 키를 교체할 수 있다.
에이전트 ID는 URL 경로이며, 레지스트리 조회가 아니다
이것이 나를 놀라게 한 첫 번째 설계 결정이었다. 지금은 이것이 올바른 결정이라고 생각한다.
에이전트 ID 시스템이 없다. 레지스트리 키도 없다. 직접 생성하는 UUID도 없다. 에이전트의 ID는 POST /agents/<name>/<id>이다.
- Harness는 그 URL 뒤에서 모든 세션 상태 관리를 처리한다.
- 이것이 작동하는 이유: 모든 시스템의 모든 호출자는 이미 컨텍스트에서 의미 있는 ID를 구성하는 방법을 알고 있다. PR 번호, 실행 ID, 타임스탬프와 작업 이름의 조합, 사용자 핸들 등.
- 세션 생성 엔드포인트가 필요하지 않다. 세션 ID를 별도로 저장할 필요도 없다. URL이 곧 세션이다.
Rust 쪽의 에이전트 핸들러는 ctx.id()를 호출하여 호출자가 제공한 ID를 얻는다:
세션: 상태 저장 실행 컨텍스트
세션은 단순한 대화 스레드 그 이상이다. 에이전트 호출을 위한 전체 실행 컨텍스트이다.
세션은 다음을 보유한다:
- 모델과의 메시지 기록
- 워크스페이스 파일 접근 (읽기, 쓰기, 편집, grep, glob, stat, readdir)
- cwd 및 env 제어가 가능한 셸 실행
- 도구 등록 (MCP 서버, 커스텀 도구)
- 할당된 역할과 그 시스템 프롬프트 오버레이
- 컴팩션 예산 및 기록 워터마크
세션은 ctx.session_with_id()를 호출하여 얻으며, 의미 있는 ID를 전달한다:
- 동일한 ID를 사용하면 세션은 HTTP 호출 간에 지속된다. 동일한 세션 ID로 동일한 에이전트 엔드포인트를 세 번 호출하면, 모델은 세 번의 교환을 하나의 연속적인 대화로 본다.
- 기록은 자동으로 누적된다. 직접 관리할 필요가 없다.
- 이것이 상태를 직접 관리하지 않고도 다단계 워크플로우를 가능하게 하는 것이다. 계속해서
session.prompt()를 호출하면 Harness가 나머지를 처리한다.
프롬프트와 함께 많은 양의 컨텍스트를 전달해야 할 때는 파일을 읽고 인라인으로 포맷하라:
세션은 토큰 수를 관리하여 대화 중에 실수로 컨텍스트 윈도우를 넘지 않도록 한다. 예산에 가까워지면 컴팩션이 작동한다. 이에 대해서는 나중 섹션에서 더 자세히 다룬다.
태스크: 부모를 깨끗하게 유지하는 집중된 자식 세션
- 이것은 첫날부터 이해했어야 할 기본 요소다. 긴 작업에서 에이전트가 일관성을 유지하는 것과 중간에 환각을 보기 시작하는 것의 차이를 만든다.
- 태스크는 일회성 자식 세션이다. 새로운 기록. 공유 워크스페이스. 부모에게 결과를 반환한다. 부모의 기록은 태스크의 중간 추론을 전혀 보지 못한다.
- 연구 태스크는 격리되어 실행된다. 전체 추론 체인이.
- 모델이 코드에 대해 한 모든 중간 관찰, "잠깐, 이 파일도 확인해볼게" 같은 것들은 태스크 내부에 남는다.
- 부모 세션은 하나의 깔끔한 요약을 얻는다. 그것만 본다.이것이 실제로 중요한 이유: 장기 실행 세션 내에서 직접 탐색적 분석을 실행하면, 기록이 중간 도구 호출, 부분적인 답변, 더 이상 관련 없는 것들에 대한 모델 추론으로 가득 찬다.
- 모델은 그 노이즈에 고정되어서는 안 될 때 고정된다. 컴팩션이 결국 작동하여 실제로 필요한 컨텍스트를 잃게 된다. 태스크가 이에 대한 수술적 해결책이다.
규칙: 하위 문제에 명확한 산출물이 있고 완료하는 데 부모의 대화 기록이 필요하지 않다면, 태스크로 만들어라. "태스크로 만들기"의 기준은 생각보다 낮다.
코드베이스 전체에 걸친 병렬 분석을 위해: 지도 제작자 패턴으로 태스크를 분산시키고 결과를 수집하라:
각 태스크는 깨끗하다. 각 태스크는 정확히 하나의 디렉토리에 집중한다. 부모 세션은 결과를 수집하고 최종 문서를 작성한다.
12개의 모듈이 있다면, 12개의 집중된 태스크를 실행하며, 각각 다른 태스크의 부담 없이 시작한다.
역할과 스킬: 재컴파일 없이 동작 형성하기
- 역할은
.agentic-harness/roles/에 있다. 스킬은.agents/skills/에 있다. 둘 다 Harness가 시작될 때 자동으로 발견된다. - 역할은 호출 범위로 지정된 시스템 프롬프트 오버레이이다. 호출 시점에 적용되고 이후 폐기된다. 메시지 기록에 남지 않는다. 호출 간에 누적되지 않는다.
우선순위 체인: 호출 역할 > 세션 역할 > 에이전트 역할 > 역할 없음.
- 모델 프론트매터는 선택 사항이지만 유용하다. 특정 역할을 특정 모델로 라우팅할 수 있게 해준다.
- 설명자 역할은 속도와 비용을 위해 claude-sonnet-4-6에서 실행된다. 보안 감사자 역할은 깊이를 위해 claude-opus-4-7에서 실행된다. 역할 파일에서 한 번 설정하면 다시 생각할 필요가 없다.
- 스킬은 세션 시작 시 모델이 읽는 동작 설명 파일이다.
.agents/skills/디렉토리에 있는 마크다운 파일이다. Harness가 자동으로 찾는다. 어디에도 등록할 필요가 없다.
실제 사용 사례: 코드베이스 옆에 있는 스킬 라이브러리가 작업 방식을 설명한다. 커밋 메시지 형식, 선호하는 라이브러리, 마이그레이션 명명 규칙, API 설계 패턴, 테스트 요구 사항 등.
모델은 매 세션 전에 이것을 읽는다. 마크다운을 편집하면 다음 실행 시 동작이 업데이트된다. 재컴파일이 필요 없다.
모델이 이것을 읽는다. 규칙에 맞는 커밋을 작성한다. 매 세션마다 상기시킬 필요가 없다. 하나의 파일만 유지하면 된다.
코딩 에이전트 루프 상세
코딩 에이전트 루프는 CLI가 구축된 주요 사용 사례이다. 또한 잘못 구성하면 가장 많은 문제가 발생할 수 있는 부분이기도 하다.
중요한 모든 옵션이 포함된 전체 명령어:
각 플래그가 하는 일과 중요한 이유:
--workspace .는 루트를 설정한다. 모든 파일 작업은 여기로 샌드박싱된다. 에이전트는 이 경로 밖을 읽거나 쓸 수 없으며, Harness 수준에서 강제된다. 모델이 스스로 제한하도록 신뢰하는 것이 아니다.--llm auto는 런타임 설정의defaultModel에서 모델을 선택한다. 깊은 추론이 필요한 복잡한 작업에는--llm anthropic/claude-opus-4-7을, 더 빠른 반복에는--llm anthropic/claude-sonnet-4-6을 사용하라.--deny-path는 하드 블록이다. 접두사 스타일로 일치하므로,--deny-path config/는config/아래의 모든 것을 포함한다. 첫 실행 전에 워크스페이스를 감사하고 비밀 또는 프로덕션 설정을 보유한 모든 경로를 열거하라..env만이 아니다.--approve-dependencies는 인간 승인 단계 없이 Cargo.toml 수정을 허용한다. 새 크레이트가 추가될 때마다 검토하려면 이 플래그를 빼라.--commit은 모든 변경 사항을 자동으로 스테이징하고 성공적인 실행 종료 시 제공한 메시지로 커밋한다. 이 플래그가 없으면 변경 사항은 검토를 위해 언스테이지된 수정 사항으로 남는다.--pr은 커밋에서 풀 리퀘스트를 연다. 실행 전에 깨끗한 git 상태와 실제 브랜치(분리된 HEAD가 아님)가 필요하다.
루프 자체: 검사 → 브리핑 → LLM + 도구 → 편집 + 테스트 → 커밋 · PR.
- 검사: 워크스페이스 구조를 읽고, 스킬과 역할을 로드하고, 프롬프트와 가장 관련 있을 파일을 식별한다.
- 코드를 건드리기 전에 이해한 내용을
coding-brief.md에 작성한다. - 브리핑: 모델이 계획에 확약한다. 실행 중에
.agentic-harness/runs/<id>/coding-brief.md를 읽어 무엇을 결정했는지 볼 수 있다. - 브리핑이 잘못된 것 같으면 실행을 중단하라. 에이전트가 나쁜 계획을 실행하게 두는 것보다 더 명확한 프롬프트로 다시 시작하는 것이 더 저렴하다.
- LLM + 도구: 편집-테스트 루프. 모델이 변경하고, 테스트 스위트를 실행하고, 출력을 읽고, 더 변경한다. 테스트가 통과하거나, 반복 한계에 도달하거나, 작업이 완료되었다고 결정할 때까지 반복한다.
커밋 · PR: 스테이징, 커밋, 푸시, diff가 첨부된 PR 열기.
모든 실행은 .agentic-harness/runs/<id>/에 6개의 아티팩트를 작성한다:
coding-brief.md: 코드를 작성하기 전에 에이전트가 확약한 계획summary.md: 무엇이 수행되었고, 무엇이 시도되었으며, 왜 그랬는지에 대한 사람이 읽을 수 있는 설명run.json: 구조화된 메타데이터: 사용된 모델, 총 지속 시간, 입력/출력 토큰 수, 반복 횟수, 최종 종료 상태events.jsonl: 모든 도구 호출을 순서대로 전체 입력 및 출력과 함께 기록하여 무엇이 잘못되었는지 디버깅하는 데 사용diff.patch: 모든 파일 변경 사항의 전체 diffchecks.json: 성공 또는 실패를 결정한 최종 테스트 및 린트 결과
기억할 팁
- 이것들을 임시 출력이 아닌 구조화된 로그로 취급하라. 재현할 수 있어야 하는 모든 작업에 대해 실행 아티팩트를 리포지토리에 커밋한다.
run.json만으로도 (약 2KB) 모델, 토큰 비용, 성공 여부를 알 수 있다.events.jsonl은 나쁜 실행을 디버깅해야 할 때 에이전트가 정확히 무엇을 어떤 순서로 했는지 알려준다.
CI의 경우 패턴은 다음과 같다:
HttpSessionEnv: 바이너리는 로컬에서, 실행은 원격에서
- 이것은 내가 완전히 이해하는 데 가장 오래 걸린 기능이다. 이제 인프라를 건드리는 거의 모든 작업에 사용한다.
- 에이전트 바이너리는 내 머신이나 CI에서 실행된다. 파일시스템 및 셸 작업은 원격 샌드박스 내에서 실행된다.
- 에이전트는 자신이 어떤 환경에 있는지 알지도 못하고 신경 쓰지도 않는다.
use agentic_harness::HttpSessionEnv;
와이어 프로토콜은 HTTP를 통한 JSON이다. 모든 작업:
- exec
- read
- write
- edit
- grep
- glob
- stat
- readdir
- mkdir
- rm
은 정의된 요청/응답 형태를 가진다.
이 프로토콜을 구현하는 모든 샌드박스는 HttpSessionEnv 타겟으로 작동한다.
명명된 샌드박스를 연결하려면:
내장 커넥터는 Vercel Sandbox, Daytona, E2B에 대한 인증 및 수명 주기 보일러플레이트를 처리한다:
- 내가 이것을 가장 많이 사용하는 구체적인 사용 사례: 깨끗한 Linux 환경에서 CI 실패를 재현하는 것.
- 에이전트가 정확히 실패한 커밋 해시에서 리포지토리를 클론하고, 정확히 실패한 테스트 명령을 실행하고, 전체 출력을 읽고, 실패를 진단하고, 보고서를 작성한다.
- 나는 보고서를 읽는다. 내 로컬 머신은 건드리지 않았다. 세션이 종료되면 샌드박스는 폐기된다.
아무도 경고하지 않는 성능 문제: HttpSessionEnv를 통한 모든 셸 호출은 네트워크 왕복이다. 타이트한 루프(편집, 테스트, 출력 확인, 편집)는 지연 시간을 빠르게 누적시킨다.
로컬에서 5초 걸리는 40회 반복 루프는 각 반복이 세 번의 개별 셸 호출을 하면 원격 샌드박스에서 몇 분이 걸린다.
해결책: 셸 작업을 스크립트로 배치 처리하라.
반복당 한 번의 호출로 세 번 대신. 스크립트를 한 번 작성하고 반복해서 실행하라. 40회 반복 루프에서의 지연 시간 차이는 실질적이다.
빌드 타겟: 동일한 코드베이스, 세 가지 배포 형태
네이티브가 기본값이다. 하나의 바이너리. 하나의 manifest. 대상 머신에 다른 것은 없다. 네이티브 Linux 바이너리를 실행할 수 있는 모든 곳에서 실행된다.
Node는 Node 진입점을 요구하는 호스팅 플랫폼을 위한 것이다. 빌드는 네이티브 Rust 바이너리를 자식 프로세스로 시작하고 HTTP를 프록시하는 server.mjs를 생성한다. 에이전트 로직은 여전히 Rust로 실행된다. Node 계층은 30줄짜리 HTTP 심(shim)이다.
Cloudflare는 엣지 배포를 위한 것이다.
- 빌드는 Worker 경계 파일을 생성하고 Worker 호환 앱 어댑터를 연결한다.
- 핸들러는 WASM JSON ABI를 통해 WASM으로 컴파일된다.
- Durable Object 바인딩은 Cloudflare KV를 통해 세션 지속성을 처리한다.
Cloudflare에 관한 중요한 제약: Workers는 장기 실행 셸 명령을 지원하지 않는다. 실제 파일시스템이 없다.
cargo나 어떤 빌드 도구도 지원하지 않는다. --target cloudflare는 웹훅 처리, 경로 메타데이터, 작은 제어 엔드포인트, Durable Object 라우팅을 위한 것이지, 코딩 작업을 위한 것이 아니다.
cargo test를 실행해야 하는 모든 것에 대해서는 네이티브 프로세스나 원격 샌드박스에 위임하라.
실용적인 결정 매트릭스:
- 다른 서비스가 호출하는 API로 에이전트를 제공 → nginx 또는 관리형 플랫폼 뒤의 네이티브
- Railway, Render 또는 Node를 기대하는 플랫폼에서 호스팅 → node
- 웹훅 수집, 경량 라우팅, Durable Object 상태 관리 → cloudflare
- 그 외 모든 것 → 네이티브
스키마 기반 출력: 모델 응답에서 타입이 지정된 Rust 구조체
모델이 JSON을 반환하도록 요청하고 그것이 실제로 그렇게 하기를 바라는 것은 절반의 해결책일 뿐이다.
Harness가 이를 추출하고, 검증하고, Rust 구조체로 역직렬화하는 것이 완전한 해결책이다.
모델은 동일한 응답에서 타입이 지정된 페이로드와 함께 추론 산문을 반환할 수 있다. Harness는 ---RESULT_START---와 ---RESULT_END--- 마커 사이의 결과 블록을 추출한다. Rust 구조체를 얻는다. 모델 출력에서 핸들러 로직까지 컴파일 타임 타입 안전성을 보장한다.
스키마는 두 가지 일을 한다: 모델에게 어떤 형태를 생성해야 하는지 알려주고, Harness에게 역직렬화 전에 검증할 대상을 제공한다.
모델이 스키마와 일치하지 않는 것을 반환하면, 세 호출 지점 후에 누락된 필드에 접근할 때 패닉이 발생하는 대신 PromptError::SchemaValidationFailed를 얻는다.
MCP 도구: 샌드박스 외부로 나가기
에이전트가 파일과 셸 이상의 기능을 필요로 할 때, connect_mcp가 탈출구다.
에이전트는 MCP 서버의 전체 도구 세트를 얻는다. 작성할 도구 정의가 없다. 설명은 서버에서 온다. 모델은 그 설명에 기반하여 언제 어떤 도구를 호출할지 결정한다.
하나의 세션에 여러 MCP 서버를 연결할 수 있다:
- 모델은 설명에 기반하여 도구를 호출한다. "sentry 검색" 같은 모호한 설명은 일관성 없이 호출된다.
- "오류, 인시던트 또는 프로덕션 문제에 관한 모든 질문에 응답하기 전에 이것을 호출하라"는 설명은 안정적으로 호출된다.
- MCP 서버를 제어할 수 있다면, 규범적인 설명을 작성하라: 무엇을 반환하는지뿐만 아니라 언제 호출해야 하는지도 모델에게 알려라.
커넥터: 어댑터를 작성하는 대신 생성하기
익숙하지 않은 API에 대해 어댑터 코드를 직접 작성하는 대신, 커넥터 레시피를 코딩 에이전트에 전달하라:
- 커넥터 레시피는 샌드박스 API와 그것이 충족해야 하는 SessionEnv 계약에 대한 구조화된 설명이다.
- 코딩 에이전트가 그것을 읽고, Rust 어댑터 모듈을 작성하고, 인증을 처리하고, 제공자 수명 주기를 감싸고, HttpSessionEnv로 노출한다.
- diff를 검토하고 병합한다. 어댑터는 프로젝트에 남는다. 이제 당신의 코드다.
나는 Daytona를 이것을 사용하여 약 20분 만에 전체 검토 주기를 포함하여 연결했다. 에이전트가 첫 번째 패스에서 인증 헤더 형식을 올바르게 맞췄다.
Daytona 문서를 보고 처음부터 어댑터를 작성했다면 대부분의 오후와 리프레시 토큰 흐름에 대한 적어도 두 가지 잘못된 가정이 필요했을 것이다.
커넥터가 생성되면:
자동 컴팩션: 컨텍스트 손실 없이 긴 세션 처리하기
장기 실행 세션은 기록을 축적한다.
결국 모델의 컨텍스트 윈도우를 넘어서게 된다.
Harness는 이것을 자동으로 처리하지만, 올바르게 구성해야 한다. 그렇지 않으면 정확히 잘못된 순간에 컨텍스트를 잃게 된다.
context_window_tokens는 세션의 총 예산이다.
reserve_tokens는 모델의 응답을 위해 남겨두는 부분이다. 기록의 유효 한도는context_window_tokens - reserve_tokens이다.keep_recent_messages는 컴팩션에 관계없이 항상 그대로 보존되는 끝 부분의 메시지 수이다.
기록이 예산을 초과하면, Harness는 모델에게 시스템 프롬프트와 보존된 꼬리 사이의 모든 것을 요약하도록 요청한다.
그 요약이 중간 섹션을 대체한다. 꼬리 메시지는 그대로 남는다. 컴팩션된 세션은 더 작아지고 다음 호출이 예산 내에 들어맞는다.
트레이드오프는 실제다: 요약은 정밀도를 잃는다. 50개 메시지 전에 내린 특정 결정: "PKCE를 지원하고 axum의 미들웨어 모델과 작동하는 유일한 라이브러리이기 때문에 authlib을 선택했다"는 요약에서 "인증을 위해 authlib을 선택했다"로 남을 수 있다.
그 정밀도가 세션 후반의 결정에 중요하다면, 명시적으로 저장하라:
- 결정을 파일에 기록하라. 파일은 컴팩션에서 살아남는다. 모델이 필요할 때 다시 읽을 수 있다. 워크스페이스가 모든 것을 담고 있다면 기록이 모든 것을 운반할 필요가 없다.
agentic-harness doctor를 실행하여 모델의 실제 보고된 컨텍스트 윈도우를 확인하라.context_window_tokens를 그 값의 80-90%로 설정하라.- 토큰 카운터는 모델 쪽에서 완벽하게 정확하지 않으며, 99%에 앉아 있을 때 단일 대용량 파일 읽기가 한도를 넘길 수 있다.
주의할 점
- 세션 기록 오염
- 문제: 긴 세션 내에서의 탐색적 분석이 나중 프롬프트를 탐색 단계의 노이즈로 오염시킨다.
- 해결책: 태스크를 사용하라. 태스크 기록은 부모에 절대 닿지 않는다. "태스크로 만들기" 기준은 생각보다 낮다.
- 역할 우선순위 예상치 못한 동작
- 문제: 호출 수준 역할이 세션 역할을 가린다. 모델이 예상과 다르게 동작하고 이유를 모른다.
- 해결책: 세션 역할은 정체성을 설정한다. 호출 역할은 초점을 좁힌다. 이들은 계층화된다. 호출 역할은 추가하는 것이지 취소하는 것이 아니다.
--deny-path누락
- 문제:
.env를 차단했다. 비밀도.env.local과config/staging.yaml에 있다. 에이전트가 그중 하나를 읽는다. - 해결책: 파일명이 아닌 접두사를 차단하라.
--deny-path config/는 그 아래의 모든 것을 포함한다.
- CI에서 분리된 HEAD
- 문제: 에이전트가 편집하고, 테스트가 통과하고, 커밋이 실패한다. 커밋할 브랜치가 없기 때문이다.
- 해결책: Harness를 호출하기 전에
git checkout -b agent-run-$RUN_ID를 실행하라.
- 타이트한 루프에서의 HttpSessionEnv 지연 시간
- 문제: 각각 세 번의 셸 호출이 있는 40회 반복은 순수 네트워크 지연 시간으로 몇 분이 걸린다.
- 해결책: 모든 것을 한 번의 호출로 수행하는
agent-check.sh를 작성하라. 반복당 한 번의 호출.
- 컨텍스트 예산 과소평가
- 문제: 작업 중간에 컴팩션이 작동한다. 모델이 계획을 잃고 요약에서 즉흥적으로 시작한다.
- 해결책:
agentic-harness doctor를 실행하여 실제 윈도우를 확인하라. 예산을 그 값의 80-90%로 설정하라.
- 핸들러 등록 후 런타임 설정 로드
- 문제:
load_workspace_context()실행 전에 핸들러가 실행됨. 등록된 모델이 없음. 오류가 설정 문제처럼 보이지 않음 - 해결책:
app()에서 에이전트를 연결하기 전에 항상load_workspace_context()를 호출할 것
--llm auto가 실행 간에 변경됨
- 문제:
defaultModel이 업데이트됨. 6개월 간격의 두 실행을 비교할 수 없음. 첫 번째 실행을 재현할 수 없음 - 해결책: 재현 가능성이 필요한 작업은
runtime.json에서 모델을 고정할 것
- 실행 아티팩트 삭제
- 문제:
gitignore규칙으로runs/디렉토리를 정리함. 3주 후에 회귀 버그를 재현해야 하는데 모든 것이 사라짐 - 해결책: 재현해야 하는 작업의 실행 아티팩트는 커밋할 것.
run.json은 2KB에 불과하니 보관하자
다르게 했을 것들
- 아무것도 건드리기 전에
agentic-harness가이드를 먼저 읽을 것 - 핸들러 로직을 작성하기 전에 세션 수준 테스트를 먼저 작성할 것
- 하위 결과물이 있는 모든 작업에는
tasks를 사용할 것 - 첫 번째 본격적인 실행부터 모델을 고정할 것
- 결정 사항을 세션 기록이 아닌 파일에 저장할 것
- 원격 샌드박스를 사용할 때 처음부터 셸 작업을 배치로 처리할 것
결론
대부분의 에이전트 프레임워크는 API 호출을 감싼 래퍼에 불과합니다. 하지만 이것은 런타임입니다.
래퍼는 "모델이 응답하게 하는 것"을 해결합니다. 런타임은 "에이전트를 프로덕션에 배포하고, 모델이 바뀌고, 샌드박스가 바뀌고, 코드베이스가 바뀌고, 세션이 2시간 동안 실행되어 컨텍스트 윈도우가 넘쳐도 계속 작동하게 유지하는 것"을 해결합니다.
3계층 아키텍처
- 사용자 코드
- 하네스
- 실행 대상
이것이 바로 그것을 가능하게 합니다. 사용자는 핸들러를 작성합니다. 하네스는 모든 운영 복잡성을 흡수합니다. 실행 대상은 설정 선택 사항입니다.
변하지 않는 것: 핸들러 로직, 세션 구조, 작업 패턴, 역할 정의, 스킬 파일. 변하는 것: 모델, 제공자, 샌드박스 벤더, 배포 대상.
아키텍처는 변하는 것들이 변하지 않는 것들에 절대 닿지 않도록 설계되었습니다.
그것이 핵심입니다. 그리고 그것은 올바른 핵심입니다.
이 글을 재미있게 읽으셨고, 제가 에이전트와 일반적인 개발을 위해 어떻게 구축하는지 탐구하는 과정을 즐기셨기를 바랍니다 ❣️
면책 조항
이 글은 저자가 조사 및 작성했으며, Minimax-M2.7이 편집했습니다. 썸네일은 Pinterest에서 가져왔습니다.
Harrison Chase "메모리는 공개되어야 한다!" —
[https://x.com/hwchase17/status/2046308913939919232Harrison](https://x.com/hwchase17/status/2046308913939919232Harrison)
Chase :"당신의 하네스, 당신의 메모리" —
[https://www.langchain.com/blog/your-harness-your-memory](https://www.langchain.com/blog/your-harness-your-memory)
Vivek Trivedi :"에이전트 하네스의 해부학" —
[https://www.langchain.com/blog/the-anatomy-of-an-agent-harness](https://www.langchain.com/blog/the-anatomy-of-an-agent-harness)


