에이전트를 만들 때 Agent SDK를 쓸지 루프를 직접 짤지, 실행 환경을 직접 운영할지 관리형에 맡길지 정해야 합니다. 이 수업에서는 그 선택지들과, 직접 짤 때 최소한으로 갖춰야 할 하네스 구성 요소를 다룹니다.
직접 짠 루프에 최소로 필요한 것
에이전트 루프를 직접 만든다면 세 가지가 반드시 있어야 합니다. 하나라도 빠지면 돌아가는 것처럼 보이다가 특정 상황에서 무너집니다.
- 대화 누적 — 모델 응답과 도구 결과를 순서대로 쌓습니다. 이력이 곧 작업 기억입니다.
- 도구 실행과 결과 반환 — 요청된 도구를 실행하고 결과를 정해진 형식으로 돌려줍니다.
- 종료 조건 — 자연 종료와 상한 둘 다 필요합니다.
def run_agent(goal: str, tools: list, max_turns: int = 20) -> str:
messages = [{"role": "user", "content": goal}]
for turn in range(max_turns):
resp = client.messages.create(
model=MODEL, max_tokens=4096, tools=tools, messages=messages
)
if resp.stop_reason != "tool_use":
return text_of(resp) # 자연 종료
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": run_tools(resp.content)})
raise BudgetExceeded(f"{max_turns}턴 안에 끝나지 않았습니다") # 상한
형식에서 자주 틀리는 지점이 있습니다. 모델의 응답은 assistant 역할로, 도구 결과는 user 역할의 tool_result 블록으로 넣습니다. 도구 결과를 assistant로 넣으면 모델은 자기가 그 결과를 만들어 냈다고 보게 됩니다. 그리고 tool_result에는 어느 호출에 대한 답인지 알리는 tool_use_id가 반드시 들어가야 합니다.
Agent SDK를 쓸 때
위 루프에 필요한 것들 — 대화 누적, 도구 배선, 권한 확인, 세션 저장 — 을 이미 갖춰 둔 것이 Claude Agent SDK입니다. 직접 짜면 며칠 걸리는 배선을 설정으로 대신합니다.
SDK가 유리한 경우는 분명합니다. 파일 시스템이나 셸처럼 흔한 도구를 쓰고, 권한 승인 흐름이 필요하고, 세션을 이어서 재개해야 할 때입니다. 이런 것들은 직접 만들면 잔버그가 오래 남습니다.
반대로 직접 짜는 편이 나은 경우도 있습니다.
- 이미 있는 서비스의 요청 처리 흐름 안에 얇게 끼워 넣어야 할 때
- 도구가 우리 내부 API 몇 개뿐이라 배선이 단순할 때
- 루프의 각 단계에 우리만의 로깅·과금·차단 로직을 촘촘히 넣어야 할 때
판단 기준은 "얼마나 많이 SDK의 기본 동작을 끄고 우리 것으로 바꿔야 하는가"입니다. 대부분을 바꿔야 한다면 얻는 것보다 싸우는 시간이 깁니다. 시험에서는 "에이전트를 만들려면 반드시 SDK를 써야 한다"는 선택지가 오답으로 나옵니다. API만으로도 만들 수 있고, 실제로 위의 짧은 루프가 그 증거입니다.
훅으로 결정적 동작 끼워 넣기
모델의 판단에 맡기면 안 되는 일이 있습니다. 위험한 명령 차단, 감사 로그 남기기, 형식 강제 같은 것들입니다. 이런 동작은 훅(hook) 으로 정해진 시점에 반드시 실행되게 합니다.
훅의 핵심 성질은 결정적이라는 점입니다. 프롬프트에 "위험한 명령은 실행하지 마세요"라고 쓰면 대체로 지켜지지만 항상은 아닙니다. 훅은 코드라서 조건이 맞으면 언제나 실행됩니다.
# 도구 실행 직전에 걸리는 검사 — 프롬프트가 아니라 코드가 막는다
def pre_tool_use(tool_name: str, tool_input: dict):
if tool_name == "run_shell" and "rm -rf" in tool_input.get("command", ""):
return {"deny": True, "reason": "파괴적 명령은 차단됩니다"}
audit_log.write(tool_name, tool_input)
return {"deny": False}
Claude Code에서는 PreToolUse·PostToolUse·SessionStart·Stop 같은 시점에 훅을 걸 수 있습니다. 시점 이름과 각 시점이 무엇을 할 수 있는지가 시험에 나옵니다. 차단은 실행 전 시점에서만 의미가 있습니다. 실행 후 시점에서는 이미 벌어진 일을 기록하거나 결과를 다듬을 수 있을 뿐입니다.
훅으로 해결되지 않는 것도 알아야 합니다. "답변 품질을 높여라" 같은 요구는 조건으로 적을 수 없어 훅의 대상이 아닙니다. 훅은 조건과 동작을 코드로 적을 수 있는 일에만 씁니다.
어디서 돌릴 것인가
에이전트는 도구를 실행하므로 실행 환경이 필요합니다. 크게 두 갈래입니다.
자체 호스팅 — 우리 인프라에서 루프와 도구를 돌립니다. 사내망 자원에 직접 붙을 수 있고 로그도 우리 것입니다. 대신 격리, 자원 상한, 정리 작업을 우리가 만들어야 합니다. 특히 셸이나 코드 실행을 도구로 주면 샌드박스가 필수입니다.
관리형 실행 — 제공되는 실행 환경에 맡깁니다. 격리와 확장을 신경 쓰지 않아도 되고 시작이 빠릅니다. 대신 사내망 접근이나 세밀한 환경 제어에는 제약이 생깁니다.
선택 기준은 대개 데이터가 어디에 있는가입니다. 도구가 사내 데이터베이스와 내부 API를 써야 하면 자체 호스팅 쪽으로 기울고, 공개 API와 업로드된 파일만 다루면 관리형이 편합니다.
어느 쪽이든 공통으로 필요한 것이 있습니다. 실행 시간 상한, 동시 실행 수 제한, 실패한 실행의 정리입니다. 에이전트는 사람이 지켜보지 않는 동안 도는 경우가 많아서, 버려진 실행이 자원을 붙잡고 있는 상황이 실제로 자주 생깁니다.
시험 함정
- 도구 결과를 assistant 역할로 넣는 선택지 — user 역할의 tool_result 블록이 맞고 tool_use_id가 함께 들어가야 합니다.
- 에이전트를 만들려면 SDK가 반드시 필요하다는 서술 — Messages API와 루프만으로도 만들 수 있습니다.
- 훅과 프롬프트 지시를 같은 수준의 통제로 보는 선택지 — 훅은 코드라 결정적이고 프롬프트는 보장이 없습니다.
- 실행 후 시점의 훅으로 위험한 명령을 막는다는 선택지 — 차단은 실행 전 시점에서만 가능합니다.
- "답변을 더 정확하게" 같은 요구를 훅으로 처리하려는 선택지 — 조건과 동작을 코드로 적을 수 있는 일만 훅의 대상입니다.
- 자체 호스팅과 관리형을 비용만으로 가르는 선택지 — 실제 기준은 도구가 접근해야 하는 데이터의 위치입니다.
실습 시나리오
실제 시험과 같은 형식의 시나리오 문제입니다.
직접 짠 에이전트 루프가 대부분 잘 돌지만, 가끔 도구 실행 결과를 받은 뒤 모델이 같은 도구를 똑같은 인자로 다시 호출합니다. 대화 기록을 보니 도구 결과가 assistant 역할로 들어가 있습니다. 원인으로 가장 적절한 것은 무엇입니까?
빌드 연습 · 최소 하네스를 만들고 훅으로 위험한 호출 막기
약 45분1.루프 뼈대
대화 누적, 도구 실행, 종료 조건 세 요소를 갖춘 루프를 작성합니다.
기대 결과 · stop_reason으로 자연 종료가 판정되고 턴 상한이 코드에 있습니다.
2.역할 배치 확인
도구 결과를 user 역할의 tool_result로 넣고 tool_use_id를 붙입니다.
기대 결과 · 대화 기록에서 assistant와 user가 번갈아 나오고 id가 짝을 이룹니다.
3.차단 훅 추가
도구 실행 직전에 파괴적 명령을 검사해 거부하는 함수를 넣습니다.
기대 결과 · 거부된 호출에도 오류 표시가 붙은 결과가 모델에게 돌아갑니다.
4.감사 로그
실행된 도구 이름과 인자를 파일로 남깁니다.
기대 결과 · 거부된 호출도 기록에 남아 사후 확인이 가능합니다.
5.SDK와 비교
같은 기능을 SDK로 구성했다면 어느 부분이 줄었을지 목록으로 적습니다.
기대 결과 · 직접 만든 배선 중 대체 가능한 항목이 구체적으로 나열됩니다.