터미널에 claude나 codex를 치고 "이 버그 좀 고쳐줘"라고 부탁하면, 잠시 뒤 파일 몇 개가 알아서 수정되고 테스트가 초록색으로 바뀌어 있다. 처음 보면 마술 같다. 저 안에서 대체 무슨 일이 벌어지는 걸까? 모델이 코드를 읽고, 고치고, 테스트를 돌려보고, 실패하면 다시 고친다. 마치 옆자리 동료가 잠깐 자리에 앉아 일을 처리해준 것 같다.
이런 물건을 코딩 에이전트라고 부른다. 2024년의 Devin 발표를 기점으로 갑자기 쏟아져 나왔고, 지금은 Claude Code, Codex CLI, OpenCode 같은 이름들이 터미널에서 코드를 짠다. 자동완성을 해주던 도구(Copilot)가 IDE 안으로 들어오더니(Cursor), 이제는 아예 터미널에 앉아 스스로 파일을 고치는 단계까지 온 것이다.
그런데 이 물건의 정체를 알고 나면 조금 김이 샌다. 코딩 에이전트의 핵심은 놀라울 만큼 단순하기 때문이다. Amp를 만든 Thorsten Ball은 How to Build an Agent라는 글에서 이렇게 정리했다. "그것은 LLM이고, 루프이고, 충분한 토큰이다.1 나머지, Amp를 그렇게 중독적이고 인상적으로 만드는 그 나머지는 그냥 노가다다."2 실제로 그의 예제는 도구 세 개로 Go 300줄이 채 안 된다.
이번 글에서는 그 300줄의 정체를 직접 확인해본다. 목표는 pi3 같은 미니멀 코딩 에이전트를 바닥부터 만들어보는 것이다. 에이전트 루프가 무엇인지, 모델에게 어떤 도구를 쥐여주는지, 컨텍스트는 어떻게 관리하는지, 그리고 만든 에이전트가 얼마나 쓸 만한지를 어떻게 측정하는지까지 다룬다. 미리 말해두자면 프로덕션에서 쓸 물건은 아니다. 실무라면 Claude Code를 쓰는 편이 낫다. 그런데도 직접 만들어볼 가치가 충분한 이유는, 이 작은 루프 하나를 이해하고 나면 "AI가 코드를 짠다"는 마법이 몇 개의 평범한 부품으로 분해되기 때문이다. 그러고 나면 에이전트가 왜 어떤 일은 잘하고 어떤 일은 어이없게 실패하는지, 같은 모델인데 왜 도구에 따라 성능이 갈리는지가 보이기 시작한다.
언어는 TypeScript를 쓰고, 만들 에이전트의 이름은 Beaver라고 하겠다. 부지런히 나무를 갉아 댐을 짓는 비버처럼, 도구를 하나씩 써가며 일을 처리하는 작은 에이전트다. 글에 나오는 코드와 수치는 전부 실제로 실행해 확인한 것이다.
에이전트란 무엇인가
코드부터 짜기 전에 용어를 정확히 해두자. 요즘 "에이전트"라는 말이 너무 헤프게 쓰여서, 챗봇에 버튼 하나 붙여놓고 에이전트라고 부르는 경우도 많다. Anthropic은 Building Effective Agents라는 글에서 이 경계를 꽤 명확하게 그었다.4
한쪽에는 워크플로(Workflow)가 있다. 미리 사람이 짜둔 코드 경로를 따라 LLM과 도구가 정해진 순서대로 호출되는 시스템이다. 예를 들어 "고객 문의가 들어오면 → 분류 모델로 카테고리를 나누고 → 카테고리별로 다른 답변 템플릿을 채운다"는 파이프라인은 워크플로다. 흐름이 코드에 박혀 있고, LLM은 그 흐름의 한 칸을 채울 뿐이다.
다른 한쪽에 에이전트(Agent)가 있다. 다음에 무엇을 할지를 사람이 아니라 LLM이 스스로 정한다. 파일을 읽을지, 테스트를 돌릴지, 코드를 고칠지, 이제 그만할지를 매 순간 모델이 판단한다. 흐름이 코드에 없고 모델의 머릿속에 있다.
이 차이가 왜 중요한가. 버그를 고치는 일을 워크플로로 짜려고 하면 금방 막힌다. "파일을 읽고 → 고치고 → 테스트한다"까지는 짤 수 있지만, 테스트가 실패하면? 어떤 파일을 읽어야 할지 모르면? 고쳐야 할 곳이 세 군데면? 현실의 코딩은 분기가 너무 많아서 모든 경우를 코드로 미리 짤 수가 없다. 그래서 판단을 모델에게 넘긴다. 이것이 코딩이 에이전트에게 잘 맞는 이유다.
Anthropic이 이 글에서 반복하는 조언이 하나 있다. 가능하면 단순하게 시작하라. 화려한 에이전트 프레임워크를 끌어오기 전에, 정말로 모델이 스스로 판단해야 하는 문제인지부터 따져보라는 것이다. 이 조언은 우리가 만들 Beaver에도 그대로 적용된다. Beaver에는 플래너도, 복잡한 상태 기계도, 그래프도 없다. 그냥 루프 하나다.
가장 작은 에이전트
에이전트의 심장은 루프다. 얼마나 단순한지 먼저 글로 적어보자.
- 사용자의 요청을 모델에게 보낸다.
- 모델의 응답을 본다.
- 모델이 도구를 쓰겠다고 하면, 그 도구를 실행하고 결과를 모델에게 돌려준 뒤 2번으로 돌아간다.
- 모델이 도구를 안 쓰고 그냥 답하면, 그것이 최종 답이다. 루프를 끝낸다.
이게 전부다. 정말로 이게 전부다. 코드로 옮기면 이렇게 된다.
async function runAgent(task: string) {
const messages = [{ role: "user", content: [{ type: "text", text: task }] }];
while (true) {
// 1. 지금까지의 대화 + 도구 목록을 모델에게 보낸다
const response = await callModel(messages, tools);
messages.push({ role: "assistant", content: response.content });
// 2. 모델이 도구를 호출했는가?
const toolUses = response.content.filter((b) => b.type === "tool_use");
if (toolUses.length === 0) {
return response; // 도구를 안 썼으니 최종 답변이다
}
// 3. 도구를 실행하고 결과를 다음 메시지로 붙인다
const results = toolUses.map((t) => ({
type: "tool_result",
tool_use_id: t.id,
content: runTool(t.name, t.input),
}));
messages.push({ role: "user", content: results });
// 4. 루프의 처음으로 돌아간다
}
}while (true)가 에이전트의 전부라는 게 실감이 안 날 수 있다. 하지만 이 루프에 담긴 발상은 생각보다 깊다. 모델이 도구를 쓸지 말지를 스스로 정하고, 도구 결과를 본 뒤 다음 행동을 다시 정한다. 파일을 읽어보고 "아, 여기가 문제구나" 싶으면 고치고, 테스트를 돌려보고 "아직 안 됐네" 싶으면 다시 고친다. 이 모든 판단이 저 루프를 한 바퀴 돌 때마다 한 번씩 일어난다.
한 가지 짚고 넘어갈 것이 있다. 저 코드에서 "도구를 조합해서 문제를 푸는 능력"은 우리가 짠 게 아니다. callModel이 부르는 모델(Claude, GPT 등)이 이미 그런 능력을 갖고 있다. 요즘 프런티어 모델(그 시점에 가장 앞선 최상위 모델을 이렇게 부른다)들은 학습 과정에서 "코딩 에이전트가 어떻게 행동하는가"를 이미 충분히 익혔기 때문에, 도구 몇 개만 쥐여주면 알아서 순서를 짜서 쓴다.5 우리가 하는 일은 그 능력이 발휘될 무대를 깔아주는 것에 가깝다. 무대란 곧 도구와 루프다.
이제 진짜로 만들어보자. 먼저 callModel이 무엇인지부터다.
모델과 대화하기
모델을 도구와 함께 부르는 방법은 프로바이더마다 조금씩 다르지만, 발상은 같다. Anthropic의 Messages API를 기준으로 설명하겠다.
API에 요청을 보낼 때, 우리는 대화 메시지와 함께 도구 목록을 넘긴다. 각 도구는 이름, 설명, 그리고 입력 형태(JSON 스키마)로 정의된다.
const tools = [
{
name: "read_file",
description: "파일 내용을 읽는다. 줄 번호가 붙은 텍스트를 돌려준다.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "읽을 파일의 상대 경로" },
},
required: ["path"],
},
},
// ... 다른 도구들
];여기서 description이 그냥 주석이 아니라는 점이 중요하다. 모델은 이 설명을 읽고 언제 이 도구를 쓸지를 판단한다. 설명이 부실하면 모델이 엉뚱한 순간에 도구를 쓰거나, 있는 도구를 안 쓰고 헤맨다. 도구 설명을 잘 쓰는 것이 에이전트 품질의 상당 부분을 차지한다. Anthropic은 이것을 사람을 위한 UI(User Interface)에 빗대어 ACI(Agent-Computer Interface)라고 부른다. 사람에게 좋은 버튼 레이블을 고민하듯, 모델에게 좋은 도구 설명을 고민해야 한다는 것이다.
이제 이 도구 목록을 넘기면, 모델의 응답이 두 갈래로 나뉜다. 첫째는 그냥 텍스트로 답하는 경우다. 둘째는 도구를 쓰겠다고 하는 경우인데, 이때 응답에 tool_use라는 블록이 들어온다.
{
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "먼저 파일을 읽어볼게." },
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "read_file",
"input": { "path": "src/cart.js" }
}
]
}stop_reason이 tool_use라는 것은 "나 도구 쓸 거니까 결과를 달라"는 신호다. 우리는 read_file을 실제로 실행한 뒤, 그 결과를 다음 사용자 메시지에 tool_result 블록으로 담아 돌려준다. 이때 tool_use_id를 그대로 맞춰줘야 모델이 "아까 요청한 그 도구의 결과구나" 하고 짝을 맞춘다.
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": " 1| function calculateTotal(items) { ..."
}
]
}이 왕복이 에이전트 루프의 한 바퀴다. 모델이 tool_use를 보내면 우리가 실행해서 tool_result를 돌려주고, 모델이 그것을 보고 다음 행동을 정한다. 이걸 모델이 tool_use 없이 그냥 답할 때까지, 즉 stop_reason이 end_turn이 될 때까지 반복하면 된다.
몇 가지 자잘한 규칙이 있는데 실무에서 놓치기 쉬우니 적어둔다. 도구를 한 턴에 여러 개 호출하면 결과도 모두 한 메시지에 담아야 한다. 그리고 도구 실행이 실패했을 때도 그 사실을 반드시 결과로 돌려줘야 한다. 이때 is_error 표시를 붙인다.
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "오류: 그런 파일이 없다",
"is_error": true
}실패했다고 결과를 아예 안 보내면, 모델은 요청과 응답의 짝이 안 맞는 대화를 받고 혼란에 빠진다. 실패도 결과의 일종이라고 생각하면 편하다.
도구를 쥐여주기
루프의 뼈대는 섰다. 이제 모델의 손발이 될 도구를 만들 차례다. 여기서 첫 번째 설계 결정을 내려야 한다. 도구를 몇 개나, 어떤 걸 줄 것인가?
이 지점에서 코딩 에이전트들의 철학이 갈린다. Claude Code는 파일 읽기·쓰기·편집·검색(glob, grep)·셸 실행·웹 검색·할 일 목록 관리까지 십수 개의 도구를 갖고 있다. 반대편 극단에 pi가 있다. pi를 만든 Mario Zechner는 의도적으로 미니멀한 에이전트를 만든 경험을 정리하면서, 도구를 딱 네 개로 줄였다. read, write, edit, bash. 그의 원칙은 "필요하지 않으면 만들지 않는다"였다.6
이 미니멀리즘에는 나름의 근거가 있다. 파일을 검색하고 싶으면 bash로 grep을 실행하면 된다. 굳이 전용 grep 도구를 만들지 않아도 모델은 셸을 쓸 줄 안다. Zechner의 표현을 빌리면, 프런티어 모델은 이미 셸에 통달해 있어서 셸 하나만 쥐여줘도 웬만한 건 다 한다. 도구를 늘리면 그만큼 도구 설명이 컨텍스트를 차지하고, 모델이 "어떤 도구를 쓸까"를 고민할 여지도 늘어난다.
Beaver도 이 미니멀리즘을 따르되, 초보자가 각 도구의 역할을 또렷이 보도록 다섯 개로 나눴다.
- read_file: 파일을 읽는다. 줄 번호를 붙여서 돌려준다.
- list_files: 디렉터리의 파일 목록을 본다.
- edit_file: 파일에서 특정 문자열을 찾아 다른 문자열로 바꾼다.
- write_file: 파일을 새로 만들거나 통째로 덮어쓴다.
- bash: 셸 명령을 실행한다. 테스트 실행, 검색 등에 쓴다.
이 중에서 가장 까다로운 게 edit_file이다. 왜 파일을 통째로 덮어쓰는 write_file이 있는데 굳이 edit_file을 따로 두는가? 모델에게 파일 전체를 다시 쓰게 하면, 긴 파일일수록 토큰을 많이 쓰고 그 과정에서 엉뚱한 줄을 건드릴 위험도 커진다. 한 줄만 고치면 되는데 500줄을 다시 뱉게 할 이유가 없다. 그래서 실무의 코딩 에이전트들은 거의 예외 없이 문자열 치환 방식의 편집 도구를 쓴다. "이 문자열을 찾아서 저 문자열로 바꿔라"고 시키는 것이다.
case "edit_file": {
const content = fs.readFileSync(abs, "utf-8");
const count = content.split(input.old_string).length - 1;
if (count === 0) {
return "오류: old_string을 파일에서 찾지 못했다. read_file로 다시 확인할 것.";
}
if (count > 1) {
return `오류: old_string이 ${count}번 등장한다. 주변 줄을 포함해 유일하게 만들 것.`;
}
fs.writeFileSync(abs, content.replace(input.old_string, input.new_string));
return `수정 완료: ${input.path}`;
}여기서 눈여겨볼 것은 오류 메시지다. 찾을 문자열이 없거나 여러 번 나오면, 그냥 실패시키는 게 아니라 "왜 실패했고 어떻게 하면 되는지"를 모델에게 알려준다. 이게 왜 중요한가. 모델은 이 tool_result를 읽고 다음 행동을 스스로 교정한다. "아, 그 문자열이 두 번 나오는구나. 그럼 앞뒤 줄을 더 붙여서 유일하게 만들어야지" 하고. 오류 메시지가 곧 모델에게 주는 힌트인 셈이다. 실제로 Beaver를 돌려보면 edit_file이 처음에 실패하고, 다음 턴에서 모델이 더 긴 문자열로 다시 시도해 성공하는 장면을 자주 본다.
도구를 만들 때 하나 더 신경 쓸 것은 안전장치다. 모델이 bash로 무슨 명령이든 실행할 수 있다는 건 무섭다. 그래서 Beaver는 파일 접근을 작업 디렉터리 안으로 가두고, 셸 명령에 타임아웃을 걸고, 도구 출력이 너무 길면 잘라낸다.7 사소해 보이지만, 출력을 안 자르면 cat으로 거대한 로그 파일을 읽었다가 컨텍스트가 순식간에 터진다.
시스템 프롬프트와 환경 정보
도구를 다 만들었으니 이제 모델에게 "너는 코딩 에이전트다"라고 알려줄 차례다. 이 역할을 하는 것이 시스템 프롬프트다. Beaver의 시스템 프롬프트는 이렇게 생겼다.
너는 Beaver, 터미널에서 동작하는 코딩 에이전트다.
사용자의 요청을 도구를 사용해 직접 해결한다.
규칙:
- 파일을 고치기 전에 반드시 읽는다.
- 코드를 고친 후에는 테스트나 실행으로 검증한다.
- 추측하지 말고 확인한다. 모르는 것은 도구로 알아낸다.
- 작업이 끝나면 무엇을 했는지 한두 문장으로 보고한다.
환경 정보:
- 작업 디렉터리: /Users/kciter/project/cart
- 플랫폼: darwin (arm64)
- 오늘 날짜: 2026-07-10짧다. pi의 시스템 프롬프트는 도구 정의까지 합쳐도 1,000토큰이 안 된다고 한다. Beaver도 비슷하다. 프런티어 모델은 이미 코딩 에이전트가 뭔지 아니까, 장황하게 설명할 필요가 없다.
그런데 이 짧은 프롬프트에서 두 부분이 실제로 큰 차이를 만든다. 첫째는 규칙이다. 특히 "고친 후에는 검증한다"는 한 줄이 그렇다. 이 줄이 없으면 모델은 코드를 고치고 나서 테스트를 안 돌려보고 "다 고쳤습니다"라고 선언해버리는 경우가 생긴다. 정말 고쳐졌는지 확인도 안 하고 말이다. 이 한 줄의 효과는 뒤에서 실측으로 확인하겠다.
둘째는 환경 정보다. 작업 디렉터리, 운영체제, 날짜 같은 것들을 프롬프트에 박아준다. 사소해 보이지만, 이게 없으면 모델이 ls부터 쳐서 자기가 어디 있는지 확인하느라 턴을 낭비하거나, macOS인데 리눅스 명령을 쓰는 실수를 한다. 실제 코딩 에이전트들은 여기에 git 상태, 현재 브랜치 같은 정보까지 채워 넣는다.
여기에 관례 하나가 더 붙는다. 프로젝트 루트에 AGENTS.md(또는 Claude Code의 CLAUDE.md)라는 파일을 두면, 에이전트가 시작할 때 그 내용을 시스템 프롬프트에 끼워 넣는 것이다.8 "이 프로젝트는 pnpm을 쓴다", "테스트는 npm test로 돌린다", "커밋 메시지는 이런 형식으로" 같은, 그 프로젝트에서만 통하는 규칙을 적어두는 곳이다. Beaver도 작업 디렉터리에 AGENTS.md가 있으면 읽어서 프롬프트에 붙인다.
const agentsPath = path.join(workdir, "AGENTS.md");
if (fs.existsSync(agentsPath)) {
agentsMd = `\n다음은 이 프로젝트의 AGENTS.md 내용이다. 반드시 따를 것:\n${fs.readFileSync(
agentsPath,
"utf-8",
)}\n`;
}컨텍스트라는 골칫거리
여기까지 오면 동작하는 에이전트가 생긴다. 그런데 조금만 어려운 일을 시키면 새로운 문제가 튀어나온다. 컨텍스트가 계속 불어난다.
여기서 컨텍스트란, 모델이 답을 만들 때 한 번에 읽어들이는 대화 전체를 말한다. 앞서 나온 토큰으로 그 크기를 잰다. 에이전트 루프를 한 바퀴 돌 때마다 대화 기록이 쌓인다. 모델의 응답, 도구 호출, 도구 결과가 전부 messages 배열에 붙는다. 그리고 매 턴 우리는 이 배열 전체를 모델에게 다시 보낸다. 모델에는 기억이 없기 때문이다. 세 번째 턴에서 모델이 첫 번째 턴에 읽은 파일 내용을 기억하려면, 그 내용이 여전히 messages 안에 들어 있어야 한다.
문제는 도구 결과가 컨텍스트를 게걸스럽게 먹는다는 것이다. 파일 하나를 읽으면 그 내용 전체가 컨텍스트에 들어온다. bash로 테스트를 돌리면 그 출력이 통째로 들어온다. 큰 저장소에서 몇십 턴을 돌면 모델의 컨텍스트 한계(예: 20만 토큰)에 부딪힌다. 한계를 넘으면 API가 요청을 거부한다.
이 문제를 다루는 방법이 몇 가지 있다.
가장 흔한 건 압축(Compaction)이다. 컨텍스트가 한계에 가까워지면, 지금까지의 대화를 모델에게 "요약해줘"라고 시켜서 긴 기록을 짧은 요약으로 갈아치우는 것이다. Claude Code의 자동 압축이 이 방식이다. 다만 부작용이 있다. 요약 과정에서 중요한 세부사항이 날아갈 수 있다. 실제로 압축 후에 CLAUDE.md의 규칙을 잊어버리는 사례가 보고된 적 있다.
두 번째는 애초에 도구 출력을 줄이는 것이다. 앞에서 Beaver가 도구 출력을 잘라낸다고 했는데, 이게 그 대비책이다. 파일을 읽을 때 필요한 부분만 읽고(offset/limit), 테스트 출력이 길면 뒷부분만 남긴다.
세 번째 선택지는 조금 뜻밖이다. 아무것도 안 하는 것. pi는 처음에 압축 기능을 일부러 넣지 않았다. Zechner의 말로는 "압축이 없어서 불편했던 적이 개인적으로 없다"는 것이다.9 한 번의 작업이 컨텍스트 한계에 닿을 만큼 길어지는 경우가 생각보다 드물고, 오히려 압축이 만드는 미묘한 정보 손실이 더 성가시다는 판단이다. Beaver도 압축을 넣지 않았다. 대신 도구 출력을 8,000자에서 잘라, 파일 하나가 컨텍스트를 통째로 삼키는 사고만 막았다.
컨텍스트 관리에 정답은 없다. 다만 한 가지 원칙은 분명하다. 모델의 컨텍스트에 무엇이 들어가는지를 정확히 통제할수록 결과가 좋아진다. 이건 Zechner가 미니멀리즘의 근거로 든 원칙이기도 하다. 도구를 줄이고, 프롬프트를 줄이고, 출력을 자르는 이 모든 노력이 결국은 "컨텍스트를 깨끗하게 유지하려는" 하나의 목표를 향한다.
실제로 돌려보기
말로만 하면 와닿지 않으니 진짜로 돌려보자. 커머스 서비스의 장바구니 모듈에 버그가 하나 있다고 하자. 합계를 계산하는 코드가 이렇게 생겼다.
function calculateTotal(items) {
let total = 0;
for (const item of items) {
total = item.price * item.quantity; // 버그: += 여야 하는데 =
}
return total;
}+=여야 할 자리에 =가 들어갔다. 그래서 합계가 누적되지 않고 마지막 상품의 금액만 남는다. 커피 두 잔(9,000원)에 케이크 하나(6,000원)를 담으면 15,000원이 나와야 하는데 6,000원이 나온다. 테스트가 이걸 잡아낸다.
Beaver에게 "npm test가 실패한다. 원인을 찾아서 고쳐줘"라고 시켜보자. 아래는 실제 실행 로그를 간추린 것이다.
[beaver] 먼저 현재 디렉터리의 파일 구조를 확인하겠습니다.
[tool] list_files({})
package.json, src/cart.js, test.js
[beaver] 테스트 파일을 확인하겠습니다.
[tool] read_file({"path":"test.js"})
... checkout(items) === 15000 ...
[beaver] 구현을 확인하겠습니다.
[tool] read_file({"path":"src/cart.js"})
... total = item.price * item.quantity ...
[beaver] 테스트를 실행해 오류를 확인하겠습니다.
[tool] bash({"command":"npm test"})
종료 코드 1 ... 6000 !== 15000 ...
[beaver] 문제를 찾았습니다. calculateTotal에서 total을
누적하지 않고 덮어쓰고 있습니다. += 로 고치겠습니다.
[tool] edit_file({"path":"src/cart.js", ...})
수정 완료: src/cart.js
[tool] bash({"command":"npm test"})
모든 테스트 통과
[beaver] calculateTotal의 total = 를 total += 로 고쳤습니다.
이제 모든 테스트가 통과합니다.일곱 턴 만에 끝났다. 이 로그를 뜯어보면 앞에서 만든 부품이 전부 제 역할을 하는 게 보인다. list_files로 지형을 파악하고, read_file로 코드를 읽고, bash로 실패를 눈으로 확인하고, edit_file로 고친 다음, 다시 bash로 검증한다. 그리고 도구를 더 안 쓰고 최종 보고를 하는 순간 루프가 끝난다.
특히 마지막에서 두 번째 줄, 고치고 나서 다시 테스트를 돌리는 저 행동이 시스템 프롬프트의 "검증한다" 규칙에서 나온 것이다. 저 검증이 없으면 모델은 edit_file 직후에 "고쳤습니다"라고 선언하고 끝냈을 것이다. 정말 고쳐졌는지 아무도 모르는 채로.
그런데 이런 로그 하나만으로는 "우리 에이전트가 쓸 만하다"고 말할 수 없다. 한 번 성공한 건 운일 수도 있으니까. 그래서 필요한 게 평가다.
얼마나 잘하는지 어떻게 재는가
에이전트를 만들었으면 반드시 따라오는 질문이 있다. 이거 진짜 쓸 만한가? 도구 설명을 이렇게 바꿨더니 나아졌나, 나빠졌나? 감으로 답할 수는 없다. 재야 한다.
코딩 에이전트 업계에는 이걸 재는 표준 잣대들이 있다. 가장 유명한 게 SWE-bench다.10 프린스턴 대학이 만든 것으로, 실제 GitHub 저장소의 진짜 이슈와 그걸 고친 진짜 PR을 2,294개 모아뒀다. 채점 방식이 핵심이다. 에이전트가 이슈를 읽고 패치를 만들면, 그 패치를 저장소에 적용한 뒤 원래 PR에 딸려 있던 테스트를 돌린다. 테스트가 통과하면 정답이다. 사람이 채점하는 게 아니라 테스트가 채점한다.
여기서 한 가지 교훈이 나온다. 좋은 벤치마크는 실제 작업 + 자동 채점의 조합이라는 것이다. 실제 작업이어야 현실을 반영하고, 자동 채점이어야 수천 개를 빠르고 공정하게 재릴 수 있다. Beaver의 작은 평가도 이 구조를 그대로 베낀다.
SWE-bench에는 뒷이야기가 있다. 원본을 자세히 뜯어보니 문제가 있었다. 정답 패치인데도 채점 테스트가 너무 까다로워서 오답 처리되거나, 이슈 설명이 애매해서 사람도 못 풀 문제가 섞여 있었다. 그래서 OpenAI가 사람 개발자 93명을 투입해 전수 검토를 거쳐 믿을 만한 500개만 추린 것이 SWE-bench Verified다. 벤치마크조차도 검증이 필요하다는 이야기다. 2026년 현재 최상위 모델들은 이 Verified에서 80% 후반대를 기록한다.11
SWE-bench 말고도 잣대는 여럿이다. 터미널에서 이것저것 시키는 Terminal-Bench, 여섯 개 언어로 문제를 푸는 Aider polyglot 등이 있다. 여기서 pass@k라는 지표를 알아두면 좋다. 같은 문제를 k번 풀게 해서 한 번이라도 맞으면 통과로 치는 방식이다. pass@1은 한 방에 맞히는 비율, pass@5는 다섯 번 안에 맞히는 비율이다. 에이전트는 같은 문제도 실행할 때마다 답이 달라지기 때문에(LLM은 확률적이다), 여러 번 돌려서 통과율을 보는 게 정직하다.
하네스는 언제 점수를 가르는가
평가 이야기에서 가장 중요한 대목이 남았다. 벤치마크 점수를 볼 때 흔히 "GPT가 몇 점, Claude가 몇 점" 하고 모델 이름으로 이야기한다. 그런데 이게 절반만 맞는 말이다.
같은 모델이라도 하네스(harness), 즉 그 모델을 감싼 에이전트 구조에 따라 점수가 달라진다. 하네스란 우리가 지금까지 만든 그 모든 것이다. 어떤 도구를 주는지, 시스템 프롬프트를 어떻게 쓰는지, 컨텍스트를 어떻게 관리하는지, 오류를 어떻게 돌려주는지. 문헌에서는 이 껍데기를 잘 만드느냐 못 만드느냐에 따라 같은 모델의 점수가 수십 퍼센트포인트까지 벌어진다고 보고된다.12 정말 그럴까? 이것도 직접 재보기로 했다.
이 말은 곧, 우리가 지금까지 한 노가다가 헛수고가 아닐 수도 있다는 뜻이다. 도구 설명을 다듬고, "검증하라"는 규칙 한 줄을 넣고, 오류 메시지를 친절하게 쓰는 이 모든 게 정말 점수로 나타날까? 말로만 믿을 게 아니라 직접 재보기로 했다.
Beaver로 작은 평가 세트를 만들었다. 실무에서 흔한 다섯 가지 상황을 과제로 냈다.
- cart-total: 장바구니 합계의 누적 버그(
+=→=) - feed-pagination: 커뮤니티 피드 페이지네이션의 off-by-one(경계에서 하나가 어긋나는 흔한 버그)
- admin-permission: 어드민 삭제 권한의 조건 로직 오류(AND/OR 혼동)
- order-refund: 주문 상태 기계(정해진 상태들 사이만 오갈 수 있게 짜둔 구조)에 '환불' 상태 추가(기능 구현)
- inventory-stock: 재고 차감의 경계 조건 버그(
>→>=)
각 과제는 SWE-bench처럼 버그가 심어진 코드와 그걸 잡아내는 테스트로 이루어진다. 에이전트가 고치면 테스트로 채점한다. LLM은 확률적이라 매번 결과가 다르므로 각 과제를 세 번씩, 조건마다 15회를 돌렸다. 모델은 저렴하고 빠른 Claude Haiku를 썼다.
진짜 실험은 하네스를 하나씩 망가뜨려 보는 것이다. 부품을 하나 빼고 같은 과제를 풀게 해서, 그 부품이 점수에 얼마나 기여하는지 본다. 네 가지 조건을 비교했다. 도구 전부에 검증 규칙까지 넣은 전체(Beaver의 기본형), 편집 도구를 빼 write_file로만 고치게 한 edit_file 없음, 셸을 빼 테스트를 못 돌리게 한 bash 없음, 그리고 시스템 프롬프트에서 "고친 후에는 검증한다" 한 줄을 지운 검증 규칙 없음.
그런데 결과가 예상과 달랐다.
| 조건 | 통과율 | 평균 턴 |
|---|---|---|
| 전체 | 93% (14/15) | 7.6 |
| edit_file 없음 | 87% (13/15) | 6.3 |
| bash 없음 | 93% (14/15) | 5.9 |
| 검증 규칙 없음 | 100% (15/15) | 6.6 |
부품을 빼도 통과율이 거의 안 떨어진다. 셸을 빼서 테스트를 못 돌리게 해도 93%, 검증 규칙을 지워도 오히려 100%다. "하네스가 성능의 절반"이라더니, 여기서는 하네스를 반쯤 부숴도 점수가 멀쩡하다. 뭔가 이상하다.
이유를 파고들면 부끄럽지만 분명하다. 과제가 너무 쉬웠다. 위 다섯 버그는 전부 코드를 한 번 읽으면 눈에 보인다. total = 가 total += 여야 한다는 건 읽는 순간 안다. 그러니 모델은 테스트를 돌려 확인할 필요조차 없다. 읽고, 고치고, 끝. bash가 있든 없든, "검증하라"는 규칙이 있든 없든 결과가 같은 이유다.
다만 통과율이 아니라 평균 턴 수를 보면 부품이 하는 일이 슬쩍 드러난다. bash를 뺐을 때 턴이 7.6에서 5.9로 줄었다. 셸이 없으니 고친 뒤 테스트를 돌려보는 그 왕복이 통째로 사라진 것이다. 즉 하네스는 일하는 방식을 바꾸긴 했다. 다만 과제가 쉬워서 그 차이가 성패로 이어지지 않았을 뿐이다.
이건 그냥 실험이 실패한 게 아니라, 앞에서 SWE-bench가 왜 Verified를 따로 만들었는지와 정확히 같은 이야기다. 벤치마크가 하네스를 측정하려면, 과제가 그 하네스를 실제로 필요로 할 만큼 어려워야 한다. 눈으로 풀리는 문제만 내면 좋은 하네스든 나쁜 하네스든 다 만점을 받는다.
그래서 과제를 다시 설계했다. 이번엔 코드만 읽어서는 정답을 알 수 없는 세 가지를 냈다. 할인·포인트·등급이 얽혀 어느 조합이 틀리는지 테스트를 돌려야 아는 것, 세금을 품목별로 반올림해야 하는데 전체에 반올림해서 특정 값에서만 1원씩 어긋나는 것, 그리고 예약 시간의 경계(끝시각=시작시각)에서만 틀려서 읽어서는 멀쩡해 보이는 것. 셋 다 테스트를 돌려봐야 진짜 실패 지점을 찾고, 고친 게 맞는지 확인할 수 있다.
같은 ablation을 이 어려운 세트에 다시 돌렸다. 이제 그림이 달라진다.
| 조건 | 쉬운 세트 | 어려운 세트 |
|---|---|---|
| 전체 | 93% | 89% (8/9) |
| bash 없음 | 93% | 78% (7/9) |
| 검증 규칙 없음 | 100% | 100% (9/9) |
bash 없음이 쉬운 세트에선 93%로 멀쩡했는데, 어려운 세트에선 78%로 내려앉는다. 셸이 없어 테스트를 못 돌리니, 그럴듯하지만 틀린 수정을 해놓고도 확인할 방법이 없어서 그대로 제출해버리는 것이다. 특히 세금 반올림처럼 "고쳤다고 착각하기 쉬운" 과제에서 실패가 몰렸다. 하네스가 성능을 가른다는 말은 과제가 하네스를 필요로 할 만큼 어려울 때 참이 된다.
정직하게 덧붙이면, 이 어려운 세트조차 규모가 작아서(조건당 9회) 숫자를 절대적으로 믿을 건 못 된다. 검증 규칙을 뺀 조건은 두 세트 모두 100%라, 적어도 Haiku와 이 과제들에서는 규칙 한 줄의 효과를 잡아내지 못했다. 문헌에서 보고되는 수십 퍼센트포인트짜리 하네스 효과는 실제 GitHub 이슈처럼 훨씬 더 어려운 과제에서 나온다.12 과제 여덟 개짜리 장난감 벤치로는 그 격차를 재현하기 어렵다. 그런데 바로 그 사실이, 이 실험에서 얻은 가장 값진 교훈이다. 에이전트를 제대로 평가하려면, 모델과 하네스를 가를 만큼 어려운 문제가 필요하다.
만들면서 부딪힌 것들
여기까지는 매끄러운 이야기였지만, 실제로 Beaver를 만드는 과정은 그렇지 않았다. 가장 애를 먹은 지점을 하나 짚어두자. 이 글을 쓰는 환경에는 API 키가 없어서, 모델을 부르는 부분을 로컬에 설치된 claude CLI로 대신했다. 즉 진짜 Messages API 대신, CLI를 원시 LLM처럼 써서 도구 호출을 텍스트 규약으로 주고받게 한 것이다.
그랬더니 예상 못 한 일이 벌어졌다. "도구를 쓰려면 TOOL_CALL {...} 형식으로 텍스트를 출력하라"고 지시했더니, 모델이 자꾸 자기가 학습한 네이티브 도구 형식(<function_calls> 같은 XML)으로 튀어나가는 것이다. 심지어 도구 결과까지 자기가 지어내서, 한 번의 응답 안에 "도구 호출 → (가짜)결과 → 또 호출 → 완료"를 통째로 연기해버렸다. 실제로는 아무 도구도 실행되지 않았는데 "다 고쳤습니다"라고 당당히 보고했다.
이게 바로 Zechner가 말한 "모델은 코딩 에이전트가 뭔지 이미 안다"의 어두운 이면이다. 모델이 코딩 에이전트 행동을 너무 잘 학습한 나머지, 여기서 정해준 규약을 무시하고 자기가 아는 방식으로 돌진한다. 해결책은 두 가지였다. 첫째, 프롬프트 첫머리에 "너는 실제로 아무것도 실행할 수 없는 텍스트 생성기다. 진짜 도구는 없다"고 못을 박아서, 모델이 네이티브 도구를 쓰려는 충동을 눌렀다. 둘째, 파서를 튼튼하게 만들어서 모델이 어떤 형식으로 튀든 첫 번째 도구 호출 하나만 뽑아내고 그 뒤에 지어낸 가짜 결과는 전부 버리게 했다.
두 번째 문제도 있었다. 모델이 가끔 "먼저 파일을 확인하겠습니다"라고 말만 하고는 정작 도구 호출을 빠뜨린 채 턴을 끝내버렸다. 이걸 최종 답변으로 오해하면 에이전트가 아무 일도 안 하고 멈춘다. 그래서 실행 의도가 뻔히 보이는데 도구 호출이 없으면, "말만 하지 말고 실제로 도구를 호출하라"고 한 번 재촉하는 장치를 넣었다.
이 삽질들은 CLI를 억지로 LLM처럼 쓰느라 생긴, 이 글만의 특수한 문제다. 진짜 API로 네이티브 도구를 쓰면 안 겪을 일이다. 그런데도 굳이 적는 이유는, 이게 "나머지는 노가다"의 정체를 아주 잘 보여주기 때문이다. 에이전트의 개념은 루프 하나로 끝나지만, 그 루프를 실제로 안정적으로 돌게 만드는 데는 이런 자잘한 방어 코드가 끝없이 붙는다. 코딩 에이전트를 만들어 팔던 사람들이 하나같이 "핵심은 단순한데 나머지가 힘들다"고 말하는 이유가 여기 있다.
장난감과 실무 사이
그 "나머지"가 대체 뭔지 조금 더 구체적으로 보자. Beaver로 버그를 고칠 수는 있지만, Claude Code처럼 매일 믿고 맡기기에는 멀었다. 그 격차는 대부분 새로운 개념이 아니라 견고함에 있다. 현실 세계는 지저분하다. 네트워크가 끊기고, 토큰이 돈으로 청구되고, 사용자가 이상한 입력을 넣고, 저장소가 수만 파일로 불어난다. 실무 에이전트가 하는 일의 절반은 이 지저분함을 견디는 것이다. 크게 세 갈래로 나눠 보자.
깨져도 계속 돌기
가장 먼저, 무언가 실패했을 때 에이전트가 통째로 죽으면 안 된다. API 호출이 네트워크 문제로 실패하면? 모델이 빈 응답을 내면? bash로 실행한 명령이 무한 루프에 빠지면? 실무 에이전트는 이 모든 경우에 살아남아야 한다.
Beaver도 최소한의 방어는 한다. 모델 호출은 실패하면 몇 번 다시 시도하고, 셸 명령에는 타임아웃을 걸어 영원히 매달리지 않게 한다.
// 모델 호출: 실패하거나 빈 응답이면 다시 시도한다
for (let attempt = 0; attempt < 3; attempt++) {
try {
raw = callModel(...);
if (raw) break;
} catch (e) {
if (attempt === 2) throw e; // 세 번 다 실패하면 포기
}
}
// 셸 명령: 30초 넘으면 강제 종료한다
execSync(command, { cwd: workdir, timeout: 30_000 });실무로 가면 여기에 더 붙는다. API가 "지금은 너무 바쁘니 잠시 뒤 다시 오라"(HTTP 429)고 하면, 무작정 재시도하지 말고 점점 더 긴 간격을 두고(지수 백오프) 기다려야 한다. 도구 하나가 실패했다고 전체 작업을 버리는 대신, 실패를 tool_result에 담아 돌려주고 모델이 다른 길을 찾게 둔다. 이 "실패를 결과로 돌려주기"는 앞에서 edit_file이 오류 메시지로 모델을 교정하던 것과 정확히 같은 원리다. 견고한 에이전트일수록 죽는 대신 모델에게 상황을 설명하고 다시 판단하게 만든다.
컨텍스트와 비용
앞에서 컨텍스트가 매 턴 불어나고, 도구 결과가 그 대부분을 차지한다고 했다. 이건 단순히 한계에 부딪히는 문제만이 아니다. 토큰은 곧 돈이고 지연이다. 매 턴 대화 전체를 다시 보내니, 긴 세션의 비용 대부분은 "똑같은 앞부분을 반복해서 다시 읽어들이는" 데서 나온다. 20턴짜리 작업이면 맨 처음 읽은 파일 내용을 스무 번 다시 전송하는 셈이다.
실무 에이전트는 이 낭비를 여러 방법으로 줄인다. 프롬프트 캐싱은 바뀌지 않는 앞부분(시스템 프롬프트, 이미 읽은 파일)을 서버가 기억하게 해서 재전송 비용을 크게 깎는다. 압축은 앞에서 봤듯 오래된 대화를 요약으로 갈아치운다. 실제 구현은 생각보다 단순하다. 컨텍스트가 한계의 몇 할에 다다르면, 앞쪽 메시지 뭉치를 모델에게 "지금까지 한 일을 요약하라"고 시켜서 그 요약 하나로 통째로 대체하는 것이다.
// 컨텍스트가 한계의 80%를 넘으면 오래된 메시지를 요약으로 압축한다
if (estimateTokens(messages) > limit * 0.8) {
const old = messages.slice(0, -6); // 최근 6개는 남기고
const summary = await callModel(
[...old, { role: "user", content: "지금까지의 작업을 요약하라." }],
[],
);
messages = [{ role: "user", content: summary }, ...messages.slice(-6)];
}그리고 스트리밍이 있다. Beaver는 모델의 응답이 다 만들어질 때까지 기다렸다가 한꺼번에 보여준다. 짧은 작업이면 괜찮지만, 모델이 30초 동안 긴 설명을 쓰는 중이라면 사용자는 빈 화면만 본다. 그래서 실무 에이전트는 토큰이 생성되는 대로 흘려보낸다. Claude Code에서 글자가 또르르 나타나는 게 이것이다. 원리는 API가 응답을 한 덩어리가 아니라 조각들의 연속으로 보내주고(이 방식을 SSE, Server-Sent Events라 부른다), 우리는 그 조각을 받는 족족 화면에 붙이는 것이다.
더 크게, 더 안전하게
마지막은 규모와 안전이다. 지금까지 Beaver는 한 번에 도구 하나씩만 썼지만, 모델은 사실 한 턴에 여러 도구를 동시에 요청할 수 있다. 파일 세 개를 읽어야 하면 read_file을 세 번 나눠 부를 게 아니라 한 번에 세 개를 요청하고, 우리가 그걸 병렬로 실행하면 훨씬 빠르다. 앞에서 "도구를 여러 개 호출하면 결과도 모두 한 메시지에 담으라"고 한 규칙이 바로 이 병렬 호출을 위한 것이었다.
한 걸음 더 나가면 서브에이전트가 있다. 큰 작업을 통째로 한 에이전트가 짊어지면 컨텍스트가 금세 지저분해진다. 그래서 "이 디렉터리에서 관련 파일을 찾아줘" 같은 하위 작업을 자식 에이전트에게 통째로 위임한다. 자식은 자기만의 깨끗한 컨텍스트에서 파일을 잔뜩 뒤진 뒤, 부모에게는 결과 요약만 돌려준다. 부모의 컨텍스트는 그 수많은 탐색 과정에 더럽혀지지 않고 요약 한 줄만 받는 셈이다. 컨텍스트를 깨끗하게 유지하려는 노력의 연장선이다.
그리고 격리다. 모델이 bash로 무슨 명령이든 실행할 수 있다는 건, 잘못하면 작업 폴더 밖의 파일을 지우거나 위험한 명령을 돌릴 수도 있다는 뜻이다. Beaver는 최소한의 울타리로 파일 접근을 작업 디렉터리 안에 가둔다.
// 작업 디렉터리 밖으로 나가는 경로를 막는다
function resolveSafe(workdir: string, p: string): string {
const abs = path.resolve(workdir, p);
if (!abs.startsWith(path.resolve(workdir))) {
throw new Error(`작업 디렉터리 밖의 경로는 접근할 수 없다: ${p}`);
}
return abs;
}이 정도는 얇은 방어막일 뿐이다. bash로 rm -rf ..를 실행하면 이 검사를 우회한다. 그래서 실무 에이전트는 위험한 명령 앞에서 사용자에게 확인을 받거나(Claude Code의 권한 프롬프트), 아예 격리된 컨테이너 안에서 에이전트를 돌린다. 앞에서 본 pi의 YOLO 모드가 "어설픈 확인 절차보다 차라리 격리된 환경이 낫다"고 한 것도 같은 고민의 다른 답이다. 에이전트에게 코드를 실행할 손을 쥐여주는 순간, 그 손이 어디까지 닿을 수 있는지를 반드시 정해줘야 한다.
이 세 갈래를 관통하는 건 하나다. 실무 에이전트의 복잡함은 대부분 똑똑함이 아니라 견고함에서 온다. 루프의 개념은 그대로인데, 그 위에 네트워크·비용·규모·안전에 대한 방어가 겹겹이 쌓인다. Claude Code 팀이 시간의 대부분을 쓰는 곳도, Thorsten Ball이 "나머지는 노가다"라고 부른 것도 바로 이 겹겹의 방어다.
마치며
버그를 대신 고쳐주는 마법 같던 물건이, 뜯어보니 루프 하나와 도구 다섯 개였다. 모델을 부르고, 도구 호출을 받아 실행하고, 결과를 돌려주고, 다시 부른다. 이 왕복을 모델이 그만두겠다고 할 때까지 반복하는 것. 그게 코딩 에이전트의 전부다. 침착하게 뜯어보면 생각보다 간단하다.
물론 "전부"라는 말에는 함정이 있다. 개념은 루프 하나지만, 그 루프를 쓸 만하게 만드는 데 많은 것이 걸려 있다. 도구 설명을 다듬고, 검증 규칙 한 줄을 넣고, 오류 메시지를 친절하게 쓰고, 컨텍스트를 깨끗하게 유지하는 그 노가다가 곧 하네스다. 그리고 앞의 실험에서 봤듯, 이 껍데기가 실제로 힘을 발휘하는 건 문제가 충분히 어려울 때다. 눈으로 풀리는 버그 앞에서는 하네스가 있으나 없으나 비슷하지만, 읽어서는 안 보이는 버그 앞에서 좋은 하네스와 나쁜 하네스가 갈린다.
이 구조를 한 번 이해하고 나면, 매일 쓰는 Claude Code나 Codex가 다르게 보이기 시작한다. 저것이 파일을 고치기 전에 굳이 한 번 더 읽고, 고친 뒤에는 꼭 테스트를 돌리고, grep을 전용 도구 대신 셸로 실행하고, 컨텍스트가 길어지면 슬그머니 요약을 하는 이유. 그게 우연이 아니라 누군가 하네스에 박아둔 설계 결정 하나하나라는 걸, 이제는 안다. 우리가 Beaver를 만들며 마주친 것과 똑같은 고민의 실물 크기 버전이다.
더 가보고 싶다면 좋은 길잡이들이 있다. Thorsten Ball의 How to Build an Agent는 이 글과 같은 루프를 Go로 300줄 안에 짜 보이는 최고의 출발점이고, Anthropic의 Building Effective Agents는 언제 에이전트를 쓰고 언제 쓰지 말아야 하는지를 정리한다. Mario Zechner의 pi 회고는 "무엇을 넣지 않을 것인가"라는, 이 글에서 미처 다 못 한 미니멀리즘의 깊은 쪽을 보여준다. 직접 만들어보고 싶다면, 앞에서 스케치만 하고 넘어간 것들을 진짜로 구현해보는 게 좋은 연습이다. 응답을 스트리밍으로 흘려보내거나, 앞의 압축 스케치를 실제로 붙여 긴 작업을 버티게 하거나, 서브에이전트를 넣어 큰 탐색을 자식에게 위임해보는 것. grep 전용 도구를 붙여 성능이 오르는지 재보는 것도 좋다.
eval 한 줄이나 API 호출 한 번으로 때울 뻔했던 문제 뒤에 이만큼의 설계가 서 있다는 걸 아는 것과 모르는 것은, 다음번에 "AI로 뭔가 자동화해주세요"라는 요청을 받았을 때 전혀 다른 결과를 만든다. 그 차이를 만드는 것이 이 시리즈가 바퀴를 계속 다시 발명하는 이유다.
Footnotes
-
토큰(token)은 모델이 글을 처리할 때 세는 단위다. 대략 단어나 단어 조각 하나에 해당하며, 모델이 한 번에 다룰 수 있는 토큰 수에는 한계가 있다. 뒤의 컨텍스트 이야기에서 다시 등장한다. ↩
-
Thorsten Ball, How to Build an Agent. Amp라는 코딩 에이전트를 만든 사람이 쓴 글로, 에이전트의 핵심이 얼마나 단순한지를 실제 코드로 보여준다. ↩
-
Mario Zechner(libGDX 게임 엔진을 만든 사람)가 공개한 미니멀 코딩 에이전트. "도구에 워크플로를 맞추지 말고, 도구를 워크플로에 맞춰라"를 표방한다. ↩
-
Anthropic, Building Effective Agents. 워크플로와 에이전트의 구분, 그리고 흔히 쓰이는 에이전트 패턴들을 정리한 글이다. ↩
-
프런티어 모델들은 학습 단계에서 도구를 쓰는 행동을 대량으로 훈련받는다. 그래서 도구 정의만 던져줘도 여러 단계에 걸쳐 도구를 조합하는 능력이 별도 지시 없이 발휘된다. ↩
-
원문은 "if I don't need it, it won't be built". Claude Code처럼 기능이 계속 쌓이는 것에 대한 반발에서 나온 원칙이다. 할 일 목록, plan 모드, MCP 등을 의도적으로 뺐다. ↩
-
반대로 pi는 기본적으로 권한 확인 없이 바로 실행하는 'YOLO 모드'다. Zechner의 논리는 "에이전트가 코드를 쓰고 실행할 수 있는 순간 이미 게임 오버"라, 어설픈 확인 절차보다 차라리 격리된 환경에서 돌리는 게 낫다는 것이다. 안전 설계에는 정답이 없다. ↩
-
CLAUDE.md는 Claude Code의 관례, AGENTS.md는 OpenAI가 주도해 여러 도구가 공유하는 형식이다. 2025년 말에는 이런 관례들을 표준화하려는 움직임도 생겼다. 둘 다 "이 프로젝트에서만 통하는 규칙"을 에이전트에게 알려주는 파일이라는 점은 같다. ↩
-
다만 이 회고 글 이후 pi.dev 문서에는 자동 압축 기능이 추가된 것으로 보인다. "지금은 필요 없다"가 "영원히 필요 없다"는 아니었던 셈이다. ↩
-
SWE-bench. 12개 인기 파이썬 저장소의 실제 이슈-PR 쌍 2,294개로 이루어진다. 코딩 에이전트 평가의 사실상 표준이 됐다. ↩
-
서드파티 리더보드 기준이며, 벤치마크가 유명해질수록 학습 데이터에 스며드는 오염(contamination) 문제와 점수 포화 논란도 함께 커진다. 그래서 최근에는 SWE-bench Pro 같은 더 어려운 후속 벤치마크가 계속 나온다. 특정 점수를 절대적 진실로 받아들이지 않는 편이 좋다. ↩
-
관련 연구와 블로그가 여럿 있는데, 최소한의 하네스와 잘 짠 하네스 사이에서 같은 모델의 점수가 수십 퍼센트포인트 벌어진 사례들이 보고된다. 정확한 수치는 실험 설정에 크게 좌우되므로, 여기서는 "하네스가 점수를 크게 가른다"는 방향성만 취한다. ↩ ↩2