
Claude Code와 같은 에이전트를 처음부터 만들기
요약
Claude Code와 같은 에이전트 하네스를 밑바닥부터 구축하는 과정을 다룬 기술 가이드입니다. 에이전트 루프의 기본 원리부터 도구 사용(Tool Use), 권한 관리(Permission) 등 핵심 메커니즘을 단계별로 설명합니다.
핵심 포인트
- 에이전트의 핵심은 모델이 도구를 호출할 때까지 반복되는 'Agent Loop' 메커니즘임
- 도구(Tool) 추가는 정의와 핸들러 등록이라는 단순한 규칙으로 확장 가능함
- 모델의 판단에만 의존하지 않는 안전한 권한(Permission) 제어가 필수적임
- 지능은 모델이 담당하고, 실행 환경은 하네스가 담당하는 구조적 분리가 중요함
서론
GitHub에서 70k 이상의 스타를 기록하고 있는 shareAI-lab/learn-claude-code를 전 20장까지 모두 읽었습니다.
이 리포지토리(Repository)는 Claude Code와 같은 "에이전트 하네스" (agent harness, 모델이 동작하기 위한 토대)를 처음부터 만드는 교재입니다. 각 장은 이전 장의 코드에 메커니즘을 하나씩 추가해 나가는 방식입니다. 이 기사에서는 제1장부터 제20장까지 순서대로 각각 무엇을 해결하고 있는지 살펴보겠습니다. 읽으면서 특히 흥미로웠던 장은 자세히, 그렇지 않은 장은 사고방식 위주로 작성하겠습니다.
제1장 The Agent Loop
Claude Code와 같은 에이전트가 등장하기 전, 모델과 어떻게 프로그래밍을 했는지 떠올려 보세요. "X를 하는 함수를 작성해줘"라고 요청하면 모델은 코드를 만들어 줍니다. 하지만 작성이 끝나면 거기서 멈춥니다. 실행은 해주지 않습니다. 직접 실행해보고 에러가 발생하면, 그 에러를 복사해서 채팅창에 붙여넣고, 수정된 코드를 받아 다시 실행합니다. 이 왔다 갔다 하는 과정을 매번 인간이 연결해 주어야 했습니다.
이 "연결하는 작업"을 자동화하는 것이 제1장입니다. 우선 이 코드를 보세요. 제1장의 code.py에 있는 에이전트의 전체 모습이며, 이것이 20장 전체의 토대가 됩니다.
def agent_loop(messages):
while True:
response = client.messages.create(
...
이것뿐입니다. while True로 계속 회전하며, 모델이 도구(Tool)를 호출하는 동안에는 계속되고, 호출하지 않게 되면 멈춥니다. 전체 흐름은 단 두 가지 사인(Sign)으로 결정됩니다.
| 사인 | 의미 | 루프의 움직임 |
|---|---|---|
stop_reason == "tool_use" | 모델이 손을 드는 "도구가 필요합니다" | 실행 → 결과를 반환 → 계속 |
stop_reason != "tool_use" | 모델이 말하는 "끝났습니다" | 루프를 탈출 |
이 코드가 동작하는 최소한의 하네스입니다. 지능 그 자체가 아니라, 모델이 계속 움직이기 위한 최소한의 메커니즘입니다. 결정하는 것은 모델(도구를 호출할지, 어떤 것을 호출할지)이고, 움직이게 하는 것은 하네스(호출되면 실행하여 결과를 반환)입니다. 이후의 장들은 이 루프 주변에 메커니즘을 추가할 뿐, 루프 자체는 마지막까지 변하지 않습니다.
제2장 Tool Use
제1장의 에이전트가 가지고 있던 도구는 bash 하나뿐이었습니다.
# 제1장의 상태
TOOLS = [{"name": "bash", ...}]
def run_bash(command): ...
bash만 있어도 무엇이든 할 수 있습니다. 읽으려면 cat, 쓰려면 echo "..." > file.py, 수정하려면 sed를 사용하면 됩니다. 하지만 "이 파일을 읽고 싶다"를 셸 명령어(Shell command)로 다시 작성해야 하는 번거로움이 있고, 잘못 작성할 수도 있습니다.
그래서 하고 싶은 일을 그대로 호출할 수 있는 도구를 준비합니다. read, write, edit, glob 4가지를 추가하는데, 루프 측의 변경은 이 두 줄뿐입니다.
# 제1장에서는 bash 1개뿐이었다
TOOLS = [
{"name": "bash", "description": "Run a shell command.", ...},
...
도구를 추가할 때 하는 일은 두 가지뿐입니다. TOOLS에 정의를 한 건 추가하고, TOOL_HANDLERS에 처리를 한 줄 추가하는 것, 그것뿐입니다. 눈에 띄지는 않지만, 이 규칙이 이후의 모든 장을 지탱합니다.
제3장 Permission
제2장의 에이전트는 도구를 5개 가지고 있지만, bash에는 제한이 없습니다. "프로젝트를 정리해줘"라고 부탁하면 rm -rf /를 실행해 버릴지도 모릅니다. 안전을 모델의 판단에 맡길 수는 없으므로, 코드로 막아야 합니다.
그래서 루프의 형태는 바꾸지 않고, 도구를 실행하기 직전에 체크를 한 줄 넣습니다.
# agent_loop 내부, 제2장의 루프에 한 줄 추가할 뿐
for block in response.content:
if block.type == "tool_use":
...
루프 구조에는 손을 대지 않습니다. 도구를 실행하기 직전에 check_permission
를 통해, 허용되지 않는다면 실행하지 않고 「거부했습니다」라는 결과만 반환하고 다음으로 넘어갑니다. 안전에 대한 판단을 하네스 (Harness) 측의 한 곳으로 모았다는 뜻입니다.
제4장 Hooks
권한 체크는 추가할 수 있었습니다. 하지만 "bash를 전부 로그에 남기고 싶다", "쓰기 작업 후에 git add를 하고 싶다"라는 요청이 있을 때마다, agent_loop 안에 if 문을 한 줄씩 추가해야 합니다. 늘리고 싶은 것은 에이전트의 동작인데, 정작 수정하고 있는 것은 루프 그 자체입니다.
그래서 모든 체크를 후크 (Hook)로 옮기고, 루프 내에서는 이벤트를 호출하기만 하도록 만듭니다.
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event, callback):
HOOKS[event].append(callback)
...
루프 측은 trigger_hooks("PreToolUse", block)를 호출하기만 할 뿐, 내부에서 무엇이 동작하는지는 알지 못합니다. 권한도, 로그도, git 연동도 나중에 register_hook으로 외장(add-on)할 수 있습니다. 제3장에서 추가했던 한 줄도 여기서 후크 측으로 옮깁니다.
제5장 TodoWrite
"Python 파일을 전부 snake_case로 바꾸고, 테스트를 돌려서, 실패한 부분을 수정해줘"라고 요청한다고 가정해 봅시다. 에이전트는 도중에 테스트 실패에 집중하게 되어, 원래의 목적을 잊어버립니다. 대화가 길어질수록 도구 실행 결과가 컨텍스트 (Context)를 채우게 되어, 최초 지시의 영향력이 약해지기 때문입니다.
그래서 todo_write라는 도구를 추가합니다. 내용은 이것뿐입니다.
CURRENT_TODOS: list[dict] = []
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
...
리스트를 받아서 표시할 뿐, 파일이나 커맨드에는 손을 대지 않습니다. 그럼에도 효과가 있는 이유는, 모델이 todo_write를 호출하는 순간 그 인자(태스크 내용과 status)가 tool_use 블록으로서 messages[]에 남기 때문입니다. 업데이트할 때마다 마지막에 추가되므로, 계획은 항상 모델의 눈에 보이는 위치에 있게 됩니다.
따라서 반환값은 "Updated 3 tasks"만으로도 충분합니다. 상태는 모델이 작성한 tool_use 측에 있으므로, tool_result는 수신 확인만으로 충분합니다.
참고로, 3라운드 연속으로 todo_write가 호출되지 않으면 하네스가 자동으로 리마인더 (Reminder)를 넣는 메커니즘도 있습니다.
제6장 Subagent
버그를 고치기 위해 호출 흐름을 추적하다 보면 messages[]가 점점 늘어납니다. 그중 상당수는 조사 과정의 중간 결과물로, "버그를 고친다"라는 목적과는 관계없는 쓰레기 데이터입니다. 인간이라면 다른 터미널에서 조사하고, 결과만 메모해서 돌아옵니다. 에이전트에게도 똑같은 일을 시킵니다.
그래서 task 도구를 추가합니다. 호출되면 독립된 messages[]를 가진 자식 에이전트가 기동됩니다.
# 자식은 부모와 다른 시스템 프롬프트 (System Prompt)를 가짐
SUB_SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
...
자식은 부모의 복사본이 아니라, messages[]와 시스템 프롬프트, 도구 목록 모두 전용의 것을 가집니다. 마지막 텍스트만 부모에게 전달되므로 "짧게 요약해서 반환할 것"이라고 지시하며, SUB_TOOLS에서 task를 제외하여 자식이 또 다른 자식을 만들지 못하도록 제한합니다.
제7장 Skills
프로젝트에 React 규약, SQL 스타일 가이드, API 설계서가 있다고 가정해 봅시다. 가장 간단한 방법은 전부 시스템 프롬프트에 넣는 것입니다.
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read()
...
하지만 그렇게 하면 CSS 색상을 하나 바꾸는 요청만 하더라도 매번 이 정도 분량을 보내야 합니다. 대부분은 현재 태스크와 관계가 없으며, 토큰 (Token)을 낭비할 뿐입니다.
그래서 2단계로 읽어오도록 합니다. 기동 시에는 카탈로그(이름과 설명만)를 시스템 프롬프트에 넣어두고, 본문은 모델이 load_skill
를 호출했을 때 tool_result로 전달합니다.
제8장 Context Compact (컨텍스트 압축)
에이전트가 순조롭게 작동하고 있더라도, 컨텍스트 윈도우 (Context Window)가 가득 차면 갑자기 정지할 수 있습니다. 파일을 읽은 내용도, 명령의 출력 결과도 모두 messages[]에 쌓이게 되며, 상한선을 넘는 순간 API가 prompt_too_long이라는 에러를 반환하기 때문입니다. 압축이 없다면 큰 프로젝트에서의 작업은 불가능합니다.
그래서 LLM을 호출하기 전에 4단계의 압축 과정을 넣습니다. 핵심 아이디어는 "저렴한 것부터 먼저, 비싼 것은 마지막에"입니다.
def agent_loop(messages):
while True:
# 전처리 3단계 (API 호출 없음). 이 순서는 변경해서는 안 됨
...
세 함수 모두 "긴 내용을 짧은 메모로 교체하는" 역할만 합니다. 교체한 후 messages[]에 남는 것은 오른쪽 열의 항목들입니다.
| 함수 | 압축 대상 | context에 남는 것 | 내용을 복구하는 방법 |
|---|---|---|---|
tool_result_budget | 너무 큰 도구 결과 (tool result) | 저장된 파일 경로와 앞부분 프리뷰 | 경로의 파일을 읽음 |
snip_compact | 대화 중간의 메시지 | "여기서 몇 건을 생략함"이라는 한 줄 메모 | 복구 불가 |
micro_compact | 오래된 도구 결과의 내용 | "압축했으므로 필요하면 다시 실행하세요"라는 한 줄 메모 | 도구를 다시 호출 |
가장 오른쪽 열이 포인트입니다. budget과 micro는 내용을 복구할 수 있지만, snip만은 복구할 수 없습니다. 따라서 먼저 복구 가능한 방법부터 사용하고, 그래도 부족할 때 snip이 작동하는 순서로 구성되어 있습니다.
제9장 Memory (메모리)
LLM에는 영구적인 상태가 없으며, 모든 정보는 컨텍스트 윈도우 안에만 존재합니다. 그리고 컨텍스트가 가득 차면 압축되며, 압축하면 반드시 무언가가 손실됩니다. 필요한 것은 압축 대상이 되지 않고, 세션(Session)을 넘어서 유지되는 저장 계층입니다. 그래서 메모리를 Markdown 파일로서 디스크에 둡니다.
파일이 늘어나면 목차가 필요합니다. MEMORY.md에 한 줄당 하나의 링크로 나열하고, 이를 시스템 프롬프트 (System Prompt)에 넣습니다. 이것이 설계의 핵심인데, 목차만 시스템 프롬프트에 두면 프롬프트 캐시 (Prompt Cache)를 사용할 수 있고, 본문은 필요할 때만 대화에 포함하므로 캐시를 깨뜨리지 않습니다.
제10장 System Prompt (시스템 프롬프트)
제1장에서 제9장까지 시스템 프롬프트는 하드코딩된 한 줄이었습니다. 하지만 기능이 늘어날 때마다 설명을 추가하다 보면 점차 다음과 같이 변합니다.
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain. "
...
전체가 하나의 문자열이기 때문에 어디를 수정해야 할지 알기 어렵고, 현재 대화와 관계없는 설명까지 매번 전송하게 됩니다.
그래서 토픽별로 나누어, 매 턴(Turn)마다 필요한 것만 조립합니다.
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
...
중요한 것은, 넣을지 말지를 대화의 키워드로부터 추측하지 않는 것입니다. 메모리를 넣는 조건은 "사용자가 메모리에 대해 이야기했기 때문"이 아니라 "메모리 파일이 실제로 존재하기 때문"입니다. 추측이 아닌 사실로 결정합니다.
제11장 Error Recovery (에러 복구)
에이전트가 작동하는 도중에 Error: 529 overloaded가 발생하여 멈춘 경우, 재시도(Retry)도 하지 않고, 모델을 전환하지도 않으며, 그냥 종료됩니다. 실무에서는 API 에러가 매일같이 발생하므로, 복구 메커니즘이 없는 에이전트는 살짝만 건드려도 멈춰버리는 자동차와 같습니다.
그래서 LLM 호출을 try/except로 감싸고, 에러 종류에 따라 대응 방식을 나눕니다.
def agent_loop(messages):
while True:
try:
...
교재에서 다루는 것은 출력 끊김, 컨텍스트 초과, 일시적인 장애 세 가지이지만, 실제 프로덕트에서는 십수 종류의 케이스를 개별적으로 처리해야 합니다. 모두 대응이 끝나면 continue로 루프의 처음으로 돌아갈 뿐이므로, 루프 본체는 변하지 않습니다.
제12장 Task System
"DB를 준비하고, API를 작성하고, 테스트를 추가해줘"라고 요청하면, 에이전트는 API부터 작성하기 시작했다가 테이블이 없다는 사실을 깨닫고 되돌아오는 등의 일이 발생합니다. 토대가 없으면 지붕을 만들 수 없습니다. 태스크(Task)에는 순서가 있습니다.
그래서 각 태스크를 .tasks/{id}.json으로 디스크에 저장하고, 의존 관계(dependency)를 작성할 수 있도록 합니다.
@dataclass
class Task:
id: str
...
이 Task를 조작하는 도구(tool)가 5개 늘어납니다.
| 도구 | 하는 일 | 필요한 이유 |
|---|---|---|
create_task | 태스크를 생성한다. blockedBy로 의존성도 지정할 수 있다 | 큰 목표를 분해하고 순서를 선언해 둔다 |
list_tasks | 모든 태스크를 status, owner, 의존성을 포함하여 목록화한다 | 현재 어디까지 진행되었는지 매번 확인한다 |
get_task | 1건의 상세 내용을 본다 | 이제 시작할 태스크의 내용을 확인한다 |
claim_task | 담당을 선언한다. owner를 기록하고 status를 in_progress로 변경한다 | 두 명이 같은 태스크를 시작하는 것을 방지한다 |
complete_task | 완료 상태로 만든다. 이를 통해 시작할 수 있게 된 다음 태스크도 알려준다 | 다음에 무엇을 할 수 있게 되었는지 알 수 있다 |
claim_task만은 언제나 성공하는 것은 아닙니다. blockedBy가 모두 completed 상태가 아니면 거절되므로, 에이전트가 의존성을 무시하고 먼저 시작할 수는 없습니다.
제5장의 Todo가 세션 내부의 체크리스트였던 것에 반해, 이것은 디스크에 남습니다. 그리고 owner와 claim_task가 있기 때문에, 이후 장에서 팀메이트가 "이 태스크는 제가 하겠습니다"라고 손을 들 수 있게 됩니다.
제13장 Background Tasks
세탁기를 돌리는 동안 사람은 옆에서 기다리지 않습니다. 하지만 pip install처럼 몇 분씩 걸리는 명령을 실행하면, 에이전트는 bash가 반환될 때까지 기다리기만 할 뿐 아무것도 할 수 없습니다.
그래서 도구의 실행을 두 가지 경로로 나눕니다. 느린 처리는 데몬 스레드(daemon thread)로 넘기고, 그 자리에서는 임시 결과만 반환합니다.
for block in response.content:
if block.type != "tool_use":
continue
...
느린 처리에는 bg_id가 붙은 임시 tool_result를 반환합니다. 이를 통해 LLM은 "이 명령은 아직 실행 중이다"라는 것을 알 수 있으므로, 먼저 다른 일을 진행할 수 있습니다. 처리가 끝나면 그 통지는 독립된 text 블록으로서, 해당 턴의 tool_result와 함께 하나의 user 메시지에 담겨 전달됩니다.
제14장 Cron Scheduler
알람 시계는 지켜보고 있지 않아도 울립니다. 하지만 제13장까지의 에이전트는 사람이 말을 걸지 않으면 움직이지 않습니다. "매일 아침 9시에 테스트", "30분마다 CI 확인"을 매번 인간이 지시하는 것은 이상합니다.
그래서 시간을 확인하는 역할과 에이전트를 구동하는 역할을 나눕니다. 시계를 보는 것은 스케줄러(scheduler)뿐이며, 나머지 모두는 "큐(queue)에 작업이 들어와 있는가"만을 알 뿐입니다.
# ① 스케줄러: 독립 스레드에서 1초마다 시간을 확인하고 큐에 넣기만 함
def cron_scheduler_loop():
while True:
...
배달 담당이 agent_lock을 보고 있기 때문에, 사용자와 대화하는 도중에 끼어들지 않습니다. 전달되는 형태는 단순한 [Scheduled] ... 메시지이므로, 에이전트 입장에서는 사람이 말을 건 것과 다를 바 없습니다.
참고로 스케줄러는 에이전트 프로세스 안에서 동작합니다. 앱을 닫으면 스케줄러도 멈추므로, "닫혀 있어도 동작하게" 만들고 싶다면 OS의 crontab에 맡겨야 합니다.
제15장 Agent Teams
「백엔드 전체를 리팩터링해줘」와 같은 요청은 인증, DB 계층, API 라우트, 테스트에 걸쳐 있습니다. 컨텍스트 윈도우 (Context Window)에는 한계가 있으므로, 에이전트 한 대로는 전부를 커버할 수 없습니다. 제6장의 서브 에이전트 (Sub-agent)는 일회성 조력자이므로, 상호작용을 지속할 수도 없습니다.
그래서 팀메이트를 계속 동작시켜 두고, 파일 수신함(Inbox)을 통해 상호작용합니다. 구현은 이것뿐입니다.
class MessageBus:
def send(self, from_agent, to_agent, content, msg_type="message"):
msg = {"from": from_agent, "to": to_agent,
...
송신은 한 줄의 JSON을 파일에 추가하고, 수신은 읽은 뒤 삭제합니다. 팀메이트는 각각 자신의 시스템 프롬프트 (System Prompt)를 가진 데몬 스레드 (Daemon Thread)로 동작하며, 결과를 리더의 수신함에 기록합니다. 리더는 루프 사이사이에 수신함을 확인하여 도착한 메시지를 이력에 넣습니다. "에이전트 팀"이라고 하면 분산 시스템을 상상하기 쉽지만, 실체는 파일을 사용한 비동기 상호작용입니다.
제16장 팀 프로토콜 (Team Protocols)
제15장의 팀메이트는 일할 수는 있지만, 상호작용에 대한 규칙이 없습니다. 리더가 팀메이트를 멈추고 싶어서 스레드를 강제 종료하면, 쓰다 만 파일이 디스크에 남게 됩니다. 팀메이트가 큰 변경을 시작할 때, 리더는 먼저 계획을 확인하고 싶을 것입니다.
두 경우 모두 "한쪽이 요청을 보내고, 다른 한쪽이 응답하며, 동일한 ID로 연결된다"는 동일한 형태를 띠고 있습니다. 그래서 상호작용 상태를 하나의 타입 (Type)으로 정의합니다.
@dataclass
class ProtocolState:
request_id: str
...
리더는 요청을 보낼 때 request_id를 발행하여 pending_requests에 넣고, 응답이 도착하면 동일한 ID를 찾아 status를 변경합니다. ID를 찾을 수 없는 응답이나 종류가 일치하지 않는 응답은 무시합니다. 분위기로 협력하는 것이 아니라, 메시지의 형식을 결정한다는 뜻입니다.
제17장 자율 에이전트 (Autonomous Agents)
팀메이트가 모두 리더의 지시만 기다리고 있다면, 보드에 처리되지 않은 태스크 (Task)가 10개 있을 때 리더가 10번을 할당해야 하므로 규모가 커지면 돌아가지 않습니다.
그래서 업무가 끝난 팀메이트를 즉시 종료시키지 않고, WORK와 IDLE 상태를 오가게 합니다. IDLE 상태 동안에는 5초마다 수신함과 태스크 보드를 확인하여, 비어 있는 업무가 있으면 스스로 가져옵니다.
while True:
# WORK 페이즈: 수신함을 읽고, LLM을 호출하고, 도구를 실행한다
...
...
수신함을 먼저 확인하는 이유는 중지 요청이 왔을 수도 있기 때문입니다. 태스크 보드의 경우, status가 pending이고, owner가 비어 있으며, 제12장의 can_start를 통과하는 태스크만 가져옵니다. 가져오는 데 성공했는지는 claim_task의 반환값으로 확인하므로, 다른 팀메이트에게 선점당했다면 다음 것을 찾습니다.
리더는 아무것도 할당하지 않습니다. 그럼에도 업무는 진행됩니다. 제12장에서 준비한 owner와 claim_task가 여기서 유용하게 쓰입니다.
제18장 워크트리 격리 (Worktree Isolation)
여러 팀메이트가 동일한 디렉토리에서 작업하면, 두 에이전트가 동시에 같은 파일을 작성하여 서로 덮어쓰게 됩니다. 제15장에서 제17장까지 "누가 무엇을"과 "어떻게 상호작용할지"는 해결했지만, "어디서 작업할지"는 아직 해결하지 못했습니다.
그래서 git worktree를 사용합니다. 하나의 리포지토리 (Repository)로부터 독립된 작업 디렉토리를 여러 개 만들 수 있는 기능입니다. 태스크마다 하나씩 준비하고, 제12장의 Task에 위치를 기억시켜 두면, 팀메이트가 태스크를 가져오는 순간 작업 장소가 전환됩니다.
@dataclass
class Task:
# ... 제12장의 필드는 그대로 유지 ...
...
팀메이트 자신은 자신이 어디에 있는지 의식하지 않습니다. 동일한 config.py를 건드리더라도, 실제로는 서로 다른 디렉토리의 서로 다른 파일이므로 충돌하지 않습니다.
제19장 MCP 도구 (MCP Tools)
제1장에서 제18장까지 도구는 모두 수작업으로 작성했습니다. 하지만 Jira API, 사내 배포 시스템, Notion 지식 베이스를 연결하고 싶을 때, 서비스마다 도구 코드를 새로 작성하는 것은 매우 힘든 일입니다.
해결 방법은 MCP(Model Context Protocol, 외부 도구를 연결하기 위한 표준 규격)입니다. 이 규격에 대응하고 있다면, 어떤 언어로 작성되었더라도 동일한 도구 목록에 포함됩니다.
# 제2장의 TOOLS / TOOL_HANDLERS는 여기서 BUILTIN_*으로 이름이 바뀝니다.
# 수동으로 작성한 도구와 MCP 도구를 섞은 결과가 새로운 목록이 되기 때문입니다.
def assemble_tool_pool() -> tuple[list[dict], dict]:
...
이름은 바뀌더라도, 제2장에서 만든 '정의 목록'과 '이름으로부터 함수를 찾는 딕셔너리(Dictionary)'라는 형태는 그대로 유지됩니다. 외부 도구도 동일하게 이 두 가지에 나열될 뿐이므로, 루프(Loop) 관점에서는 수동으로 작성한 도구와 구분이 되지 않습니다.
제20장 Comprehensive Agent
제1장에서 제19장까지는 한 장당 하나씩 메커니즘을 추가해 왔습니다. 배우는 순서로서는 이것이 맞지만, 실제 에이전트는 하나의 메커니즘만으로 작동하는 것이 아닙니다. 어려운 점은 기능을 늘리는 것이 아니라, 각각을 루프의 어느 위치에 둘지를 결정하는 것입니다.
최종장에서는 새로운 메커니즘을 추가하지 않고, 19장 분량의 부품을 하나의 하네스(Harness)로 되돌립니다.
user input
→ UserPromptSubmit hooks (제4장)
→ cron / 백그라운드 알림 주입 (제13~14장)
...
도구는 크게 늘어났고, 태스크 보드도 팀도 워크 트리(Work Tree)도 움직이고 있습니다. 그럼에도 제1장의 while True는 단 한 줄도 변하지 않았습니다. 늘어난 것은 루프 전후에 매달린 부품들뿐입니다.
요약
전 20장을 읽으며 얻은 가장 큰 배움은, "에이전트의 지능은 모델이 가지고 있으며, 하네스의 역할은 모델에게 컨텍스트(Context)와 도구를 전달하는 것이다"라는 점이었습니다. 20장 분량의 메커니즘은 모두 이 루프 주변에 "모델에게 무엇을 보여주고, 무엇을 시킬 것인가"를 추가했을 뿐이며, 루프 본체는 마지막까지 변하지 않습니다.
앞으로 읽으실 분들은 꼭 제1장부터 순서대로 읽어보시기 바랍니다. 각 장은 이전 장과의 차이점만을 설명하므로, 중간부터 읽으면 전제 조건을 알 수 없어 막히게 됩니다. 각 장의 code.py는 단독으로 실행 가능하므로, 실제로 구동하면서 읽는 것을 추천합니다.
참고 및 라이선스
본 기사의 코드는 shareAI-lab/learn-claude-code에서 인용했습니다. 각 장의 해설은 해당 리포지토리(Repository)의 README와 learn.shareai.run을 참고했습니다.
해당 리포지토리는 MIT License로 공개되어 있습니다.
MIT License
Copyright (c) 2024 shareAI Lab
기사에 실린 코드는 설명을 위해 일부를 생략하거나 간략화했습니다. 실제로 작동하는 코드는 각 장의 code.py를 참조하십시오.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기