Agent Mentor Learn
에이전트 하네스 기초: 루프와 통제 · 6 / 6강

레슨 6: 실습: 제어가 달린 에이전트 하네스를 손으로 쓰기

학습 목표:

  • 앞선 레슨들의 stop_reason 루프를 @anthropic-ai/sdk 위에서 돌아가는 while 루프로 바꾸고, 도구를 계속 호출할지 텍스트를 반환하고 마무리할지 스스로 판단하기
  • tool_usetool_result 콘텐츠 블록을 명세 그대로 만들고, 한 턴의 여러 결과를 하나의 user 메시지 안에 담아 돌려보내기
  • 그 루프에 제어 밸브 넷 — 최대 턴 수, 예산 상한, 무진행 감지, 영향이 큰 행동에 대한 승인 — 을 달고, 각각이 루프의 어느 단계에 있어야 하는지 정확히 말하기

전제: 레슨 2부터 5까지 읽었고, stop_reason으로 구동되는 루프, 정지 조건, 폭주 백스톱, 휴먼 인 더 루프 개입을 이해한다 | 이전: 레슨 5 <<

먼저, 돌아가는 모습부터

앞의 다섯 레슨은 기계를 조각조각 뜯어냈습니다. 루프가 어떻게 도는지, 언제 멈춰야 하는지, 폭주는 어떻게 생겼는지, 사람은 어떻게 끼어드는지. 이 레슨은 그 조각들을 가장 작은 돌아가는 하네스로 용접합니다. 코드에 앞서, 터미널에서 무엇을 하는지 보세요 — 장난감 도구 두 개(get_time은 시간을 알려 주고, read_file은 프로젝트 안의 파일을 읽습니다)를 단 에이전트에, 한 문장을 건넵니다. "README.md의 첫 줄을 읽고, 그다음 지금 몇 시인지 알려 줘."

text
$ node agent.js "README.md의 첫 줄을 읽고, 그다음 지금 몇 시인지 알려 줘"
[turn 1] model requests tool: read_file({"path":"README.md"})[turn 1] tool returned: "# 에이전트 하네스 기초\n..."[turn 2] model requests tool: get_time({})[turn 2] tool returned: "2026-08-26T10:42:07+08:00"[turn 3] model wraps up (end_turn)
README.md의 첫 줄은 "# 에이전트 하네스 기초"이고, 지금은 2026년 8월 26일 10시 42분입니다.도구 호출 2턴, 모델 요청 3회로 끝났습니다.

무슨 일이 일어났는지 자세히 보세요. 사용자는 한 문장을 말했고, 도구가 몇 개 호출됐는지, 어느 것이 먼저였는지, 언제 멈출지는 모두 루프 안의 모델이 결정했습니다. 그것이 에이전트와 워크플로의 경계선입니다 — 워크플로의 경로는 코드에 고정돼 있지만, 에이전트는 모델이 자신의 과정을 동적으로 지휘하며 어떤 도구를 쓸지 결정합니다1. 호스트 코드(이 레슨에서 우리가 쓰는 하네스)는 "먼저 파일을 읽고, 그다음 시간을 확인하라"고 지정한 적이 없습니다. 그저 충실히 루프를 돌리고, 모델이 지목한 도구를 실행하고, 결과를 먹여 줬을 뿐입니다. 여기 두 도구는 다 무해해서 실행을 멈추게 한 것은 없습니다 — 하지만 이 하네스에는 승인 밸브도 용접돼 있어서, 모델이 파일 삭제나 요청 쏘기 같은 영향이 큰 것에 손을 뻗으면, 멈추고 행동하기 전에 사람의 끄덕임을 기다립니다(뒤에서 씁니다). 이 레슨의 나머지는 저 터미널 출력 뒤의 코드를 한 줄씩 짓습니다.

핵심 루프: 골격을 그대로 옮기고, 진짜 SDK로 갈아 끼운다

레슨 2 "핵심 루프: 한 번의 왕복에서 지속 동작으로"의 callModel은 의사코드였습니다. 이제 그것이 실제 @anthropic-ai/sdk가 됩니다. 루프의 골격은 동일합니다. messages를 실은 요청을 보내고, response.stop_reason을 봅니다 — "tool_use"이면 도구를 돌리고, 결과를 도로 꿰고, 다시 보냅니다. 아니면(가령 end_turn) 텍스트를 반환하고 루프를 빠져나옵니다2.

밸브가 하나도 없는 최소 버전이 여기 있습니다. 루프 자체가 잘 보이도록.

이것을 레슨 2 골격 옆에 놓으면 구조는 움직이지 않았습니다. while 줄은 여전히 "stop_reasontool_use인 동안 반복"이라 말하고, 본문은 여전히 같은 네 단계입니다 — assistant push, 도구 실행, tool_result push, response 재대입. 실질적 변화는 callModelclient.messages.create(...)가 된 것과, 본문 끝의 그 재대입뿐입니다. 그 재대입이야말로 멈춤을 가능하게 하는 것입니다. 그것을 빼면 stop_reason은 옛 값에 영영 머물고, 그것이 바로 레슨 4 "폭주와 폴백: 데드 루프, 공회전, 예산 소진"의 데드 루프입니다.

tool_use / tool_result 필드, 하나도 빠뜨리지 않기

runToolUses는 모델이 지목한 도구가 실제로 돌아가는 곳입니다. 여기서 가장 틀리기 쉬운 것이 콘텐츠 블록 필드이므로, 명세를 따르세요. tool_use 블록은 id / name / input을 싣고, tool_result 블록은 tool_use_id(어느 호출에 답하는지 밝힘)와 content를 싣고, 도구 실행이 실패하면 is_error: true를 더합니다3. 굳은 규칙이 하나 더 있습니다. 응답에 tool_use 블록이 몇 개 담겼든, 그만큼의 tool_result 블록이 돌아와야 하고, 전부 바로 뒤따르는 하나의 user 메시지에 담겨야 합니다3 — 위 루프 본문의 messages.push({ role: "user", content: toolResults }) 줄이 그 규칙을 지킵니다.

try/catch를 눈여겨보세요. 도구 하나가 터진다고 하네스 전체가 함께 무너져서는 안 됩니다. 에러를 is_error: true로 표시한 tool_result에 싸서 돌려보내면, 모델은 다른 인자로 재시도하거나 다른 경로를 택할 기회를 얻습니다. 그것이 예외를 던져 프로세스를 죽이는 것보다 훨씬 안정적입니다.

제어 밸브 네 개 볼트로 죄기

이제 루프는 돌지만, 레슨 2의 헐벗은 루프 — 모델을 믿고 스스로에게 빠져나갈 길을 남기지 않은 루프 — 입니다. 모델이 end_turn을 반환하는 턴에서 멈추고, 그 사이 어디에도 경계가 없습니다. 그리고 에이전트의 자율성은 더 높은 비용에 더해, 루프를 한 바퀴 두 바퀴 돌며 에러가 누적 증폭할 가능성을 뜻하고, 모델은 여러 턴을 돌 수 있습니다1 — 헐벗은 루프는 멈출지 계속할지의 결정 전체를 모델에 거는데, 그것은 너무 위험합니다. 이제 앞선 레슨들의 밸브 넷을 하나씩 용접합니다.

각 밸브는 하나를 지키고, 그 위치는 어느 것도 임의가 아닙니다.

  • 밸브 1, 최대 턴 수(레슨 3 "정지 조건: 에이전트는 언제 그만둬야 하는가"): turns >= MAX_TURNSturns++ 앞, 본문 맨 위에 앉습니다. "이번 바퀴 전에, 한 바퀴 더 도는 것이 아직 허용되는지 확인하라"는 뜻입니다. 이 명시적 정지 조건은 모델 자신의 end_turn과 나란히, 제어를 여러분 손안에 두기 위해 존재합니다1.
  • 밸브 2, 예산 상한(레슨 4 "폭주와 폴백: 데드 루프, 공회전, 예산 소진"): 응답이 돌아올 때마다 response.usage의 토큰을 더하고 천장에서 멈춥니다. 턴은 적은데 매 턴 컨텍스트가 거대할 때, 턴 수만으로는 지출을 붙잡지 못하므로, 토큰을 별개의 독립 게이트로 둬야 합니다.
  • 밸브 3, 무진행 감지(레슨 4): 이번 턴 도구 호출을 시그니처로 납작하게 만들어 직전과 비교합니다. 같으면 헛도는 것입니다. 이것은 턴이 상한을 넘지 않았고 예산도 안 터졌는데, 모델이 제자리걸음을 하며 같은 도구를 같은 인자로 몇 번이고 호출하는 정체 상황을 잡습니다.
  • 밸브 4, 승인 밸브(레슨 5 "개입과 조향: 중단, 방향 전환, 휴먼 인 더 루프"): runToolUses 안, 도구를 실제로 실행하기 전에, 영향이 큰 행동은 사람의 확인을 먼저 받습니다. 영향이 큰 행동에 대한 휴먼 인 더 루프 승인이야말로 과도한 에이전시 위험을 누르는 권장 방법입니다4.

밸브 3의 시그니처 함수는 지루할 만큼 단순합니다 — 이번 턴 모든 tool_use 블록의 이름과 인자를 하나의 문자열로 잇습니다. "무엇이 어떤 인자로 호출됐나"를 구별하는 것만 하면 됩니다.

승인 밸브: 실행 직전의 순간에 끼워 넣는다

네 밸브 중 승인 밸브의 위치가 가장 중요하고 가장 틀리기 쉽습니다. 모델이 도구를 지목했지만 도구가 아직 실제로 돌지 않은 순간에 끼워 넣어야 합니다 — 벌어지려는 행동을 출력하고, 사람을 기다리고, 확인 뒤에만 실행합니다. 한 단계라도 늦으면 파일은 이미 쓰였고, 요청은 이미 나갔고, "확인?"이라 묻는 것은 무의미합니다. 그래서 runToolUses 안, impl(...) 줄 앞에 갑니다.

approve는 바깥에서 넘겨받는 함수입니다. 터미널에서는 "행동을 출력하고, 한 줄의 입력을 읽는다"를 뜻합니다.

중요한 디테일 하나. 사용자가 거부하더라도, 아무것도 반환하지 않는 대신 is_error: true로 표시한 tool_result를 여전히 반환합니다. 명세는 모든 tool_use에 대응하는 tool_result가 돌아오기를 요구합니다3. 그것을 빼먹으면 도구 호출 하나에 결과가 없어 다음 요청이 에러가 납니다. 거부는 무시와 같지 않습니다 — 거부 자체가 모델이 들을 자격이 있는 결과이고, 거부됐음을 배운 모델은 흔히 영향이 큰 행동이 아예 필요 없는 경로로 갈아탑니다.

장난감 도구 두 개, 루프가 실제로 돌도록

밸브는 달렸습니다. 빠진 것은 모델이 호출할 도구입니다. 이 레슨은 절대적으로 안전한 장난감 두 개만 쓰고 위험한 조작은 문밖에 둡니다. get_time은 현재 시간을 알려 주고, read_file은 파일을 읽되 — path.resolve로 프로젝트 디렉터리 안에 단단히 못 박아, 모델(또는 도구 출력에 궤도를 벗어난 모델)이 /etc/passwd 같은 범위 밖 경로를 읽지 못하게 합니다.

두 도구 다 HIGH_IMPACT 집합에 없으므로 어느 것도 승인을 촉발하지 않습니다 — 만듦새부터 무해합니다. 승인 밸브를 시연하려면 toolImplsHIGH_IMPACTwrite_file을 더하세요. 이 레슨은 예제를 돌려도 여러분 파일을 망가뜨릴 수 없도록, 실제 쓰기 조작을 일부러 들이지 않습니다.

조립하기: node agent.js로 돌릴 수 있는 진입점

마지막으로, runAgent, runToolUses, 도구 정의, 승인 함수를 직접 돌릴 수 있는 진입점으로 모읍니다 — 이 레슨 맨 위 터미널 출력 뒤의 그것입니다.

앞의 조각들(import, client, MODEL, runAgent, runToolUses, signatureOf, approveInTerminal, toolImpls, tools, main)을 하나의 agent.js에 떨어뜨리고, ANTHROPIC_API_KEY를 세팅하고, npm i @anthropic-ai/sdk를 돌리면, node agent.js "여러분의 작업"으로 돌아갑니다.

이 백여 줄을 되돌아보면 새 개념이 하나도 없다는 것을 알아챌 것입니다. while 루프와 stop_reason은 레슨 2에서, MAX_TURNS는 레슨 3에서, 예산과 헛돎 감지는 레슨 4에서, 승인 밸브는 레슨 5에서 왔습니다. 하네스는 무슨 깊은 프레임워크가 아닙니다. 여러분이 직접 쓰고 제어하는 이 루프-더하기-밸브의 층입니다. 같은 모델, 같은 두 도구 — 그런데도 이 네 밸브가 달린 하네스와 레슨 2의 헐벗은 루프는 같은 작업을 얼마나 안정적으로 돌리느냐에서 엄청나게 갈릴 수 있습니다. 에이전트가 믿을 만한지를 결정하는 것은 안쪽 모델만이 아니라 대체로 이 바깥 층의 제어 코드이기 때문입니다5.

복잡성에 대한 균형 감각도 지키세요. 모든 에이전트가 네 밸브를 다 필요로 하지는 않으며, 기억할 만한 한 줄은 복잡성은 그것이 결과를 눈에 띄게 개선함이 증명될 때에만 더하는 것을 고려해야 한다는 것입니다1. 통제된 환경에서 서너 턴 도는 작은 도구는 MAX_TURNS 하나로 족할 수 있습니다. 네 밸브는 여러 턴을 연달아 돌고 영향이 큰 행동에 손을 뻗을 수 있는 경우를 위한 것입니다.

정리

  • 돌아가는 하네스의 핵심은 여전히 레슨 2의 루프다: messages를 실은 요청을 보낸다 → stop_reason을 확인하고, tool_use이면 도구를 돌리고 tool_result를 도로 꿰어 다시 보낸다; 아니면 텍스트를 반환하고 마무리한다2. 진짜 SDK로 갈아 끼우는 것은 callModelclient.messages.create(...)로 바꿀 뿐이다
  • 콘텐츠 블록 필드는 하나도 빠뜨리지 않고 명세를 따른다: tool_useid / name / input을 싣고, tool_resulttool_use_id / content에 실패 시 is_error를 더한다; 한 턴에 tool_use 블록이 몇 개든 그만큼의 tool_result가 돌아오며, 전부 바로 뒤따르는 하나의 user 메시지에 담긴다3
  • 네 제어 밸브는 각각 한 자리를 지키고 위치를 뒤섞을 수 없다: 최대 턴 수(레슨 3)와 예산 상한(레슨 4)은 루프가 반드시 멈추게 하는 하드 경계이고, 무진행 감지(레슨 4)는 제자리걸음을 잡고, 승인 밸브(레슨 5)는 도구 실행 앞에 끼워 넣어야 한다 — 에이전트의 자율성은 더 높은 비용과 에러의 누적 증폭을 데려오고 모델이 여러 턴을 돌 수 있어1, 모델 자신의 end_turn으로는 붙들 수 없기 때문이다
  • 영향이 큰 행동에 사람의 확인을 요구하는 승인 밸브는 과도한 에이전시 위험을 누르는 권장 방법이다4. 거부되더라도 is_error tool_result를 반환하고 호출을 매달아 두지 않는다3
  • 하네스는 깊은 프레임워크가 아니다. 여러분이 직접 쓰고 제어하는 이 루프-더하기-밸브의 층이다 — 같은 모델, 다른 제어 코드, 그리고 신뢰성은 엄청나게 갈릴 수 있다5. 다만 밸브를 밸브 자체를 위해 쌓지도 마라. 복잡성은 결과를 눈에 띄게 개선함이 증명될 때에만 더하라1

이 코스를 마쳤습니다. "하네스란 무엇인가"에서 네 제어 밸브가 달린 루프를 손으로 쓰기까지, 지금 여러분 손에 든 것은 개념 한 묶음만이 아닙니다 — 실제로 돌아가고, 고칠 수 있고, 제어를 계속 더할 수 있는 진짜 코드입니다. 여러분 자신의 도구에 물려, 여러분을 위해 일을 좀 시켜 보세요.

Footnotes

  1. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4 5 6

  2. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2

  3. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2 3 4 5

  4. LLM06:2025 Excessive Agency — OWASP Gen AI Security Project — https://genai.owasp.org/llmrisk/llm062025-excessive-agency/ 2

  5. The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 2

연습

01

지금 승인 밸브는 설정이 둘입니다. 영향이 크면 묻고, 나머지는 전부 허용. 제품 담당 동료가 더 세밀한 요구를 냅니다 — 도구 이름별로 세 계층으로 제어하기: allow(그대로 통과, get_time처럼), ask(실행 전에 사람 확인 필요, write_file처럼), deny(항상 거부, 아예 호출 불가, 은퇴한 send_email처럼). 이 정책 밸브를 하네스에 더하세요. 데이터 구조를 설계하고, 루프의 어느 단계에 있어야 하며 기존 승인 밸브와 어떤 관계인지 말하고, deny에 걸렸을 때 모델에게 무엇이 돌아가야 하는지 써 보세요.

레벨 1: 하네스에 도구별 계층 제어 밸브 더하기
완료 기준 · 로컬에서 확인
02

한 동료가 아래 하네스 루프가 "돌아간다"고 말하지만, 모델이 스스로 end_turn을 반환하기를 멈추거나 제자리걸음에 빠지는 순간 무너집니다. 짚어 주세요. (1) 어떤 제어가 없고 각 부재가 어떤 폭주 행동을 낳는지, (2) 최소한의 수정 — 루프가 반드시 멈추도록 보장하는 하드 경계 적어도 하나 — 을, 그것이 어느 단계에 가는지 분명히 밝혀서.

레벨 2: 이 루프에는 어떤 게이트가 빠졌고, 어떻게 폭주하는가
완료 기준 · 로컬에서 확인