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

레슨 2: 코어 루프: 한 번의 왕복에서 지속적인 운전으로

학습 목표:

  • stop_reason이 구동하는 멀티턴 루프의 네 단계를 외우고, 한 번의 도구 호출 왕복을 계속 도는 while 루프로 잇기
  • stop_reason의 값(tool_use / end_turn)을 사용해 루프가 계속될지 멈출지 판단하고, 왜 그 필드가 루프의 while 조건인지 설명하기
  • 이 뼈대 루프에 빠져 있는 경계를 짚어 내고, 왜 매 턴 히스토리이 늘어나는지, 왜 모델이 "끝났습니다"라고 말하는 것에만 기댈 수 없는지 설명하기

전제: 레슨 1을 읽고 하네스가 모델을 감싸는 제어 코드의 층임을 알고 있으며, 한 번의 도구 호출 왕복의 tool_use / tool_result를 읽을 수 있어야 합니다 | 이전: 레슨 1 << | 다음: 레슨 3 >>

한 번의 왕복으로는 부족해집니다

레슨 1에서 완전한 도구 호출 왕복을 이미 분해했습니다. 모델이 stop_reason: "tool_use" 와 함께 tool_use 블록을 돌려주고, 여러분의 호스트 코드가 nameinput 을 읽어 내고, 실제로 그것을 실행하고, 출력을 tool_result 에 담아 돌려보내고, 그제서야 비로소 모델이 최종 답을 내놓습니다. JSON 세 덩어리, 왕복 한 번, 끝입니다.

실제 작업이 그렇게 예의 바른 경우는 드뭅니다. 시나리오를 바꿔 봅시다. 여러분은 온콜 대응 봇을 쓰고 있고, 사용자가 이렇게 말합니다. "api 서비스를 재시작해 주고, 그다음 로그에 아직 에러가 남아 있는지 확인해서, 있으면 붙여 줘." 이 한 문장에는 두 가지 일이 담겨 있고, 두 번째는 첫 번째에 의존합니다 — 재시작이 끝나기 전에는 로그를 확인해도 의미가 없습니다. 모델은 첫 턴에 둘 다 해낼 수 없습니다. 할 수 있는 것은 이것뿐입니다.

  1. 첫 턴은 tool_use 를 돌려주며 restart_service 를 호출합니다. 여러분은 그것을 실행하고 "재시작 성공"을 돌려보냅니다.
  2. 두 번째 턴은 다시 tool_use 를 돌려주며, 이번에는 read_logs 를 호출합니다. 여러분은 그것을 실행하고 로그 내용을 돌려보냅니다.
  3. 세 번째 턴에 비로소 stop_reason: "end_turn" 이 돌아오며, "재시작이 완료되었습니다. 로그에 타임아웃 에러가 두 건 있어, 아래에 붙입니다" 같은 한 줄이 따라옵니다.

사용자의 요청은 하나, 왕복은 세 번. 각 단계에서 모델이 볼 수 있는 것, 그리고 다음에 하는 것은, 앞의 tool_result 로 무엇이 돌아왔는지에 의존합니다 — 이것이 바로 Anthropic의 에이전트 정의 그 자체입니다. 루프 안에서 환경 피드백에 기반해 도구를 사용하는 LLM.1 레슨 1의 한 번의 왕복은, 그 루프가 마침 한 바퀴만 돈 특수한 경우일 뿐입니다. 이 레슨이 하는 일은 "한 번의 왕복"을 "왕복이 계속 이어지는 것"으로 잇는 것이며, 그 한가운데의 루프가 무엇이고, 무엇이 구동하며, 어디에 브레이크가 필요한지를 또렷이 보는 것입니다.

루프의 네 단계

한 번의 왕복을 루프로 바꾸는 데 새로운 것을 발명할 필요는 없습니다. 이미 알고 있는 동작을 되풀이할 뿐입니다. Claude API 문서는 이 멀티턴 프로세스를 정해진 절차로 적어 둡니다.2

  1. messages 와 도구 목록 tools 를 실은 요청을 보냅니다(tools 는 매 턴 반드시 동행시켜야 합니다).
  2. 모델이 응답을 돌려줍니다. 아직 도구가 필요하면 stop_reason"tool_use" 가 되고, content 에는 하나 이상의 tool_use 블록이 실려 있습니다.
  3. 모든 tool_use 블록을 실행하고, 각 출력을 tool_result 블록으로 바꿉니다. 이 단계에서 핵심은 모든이라는 말입니다. 한 응답에서 tool_use 블록이 몇 개 돌아왔든, 다음 user 메시지에는 그것과 같은 수의 대응하는 tool_result 블록이 필요하며, 각각 tool_use_id 로 묶이고, 모두가 바로 뒤따르는 그 하나의 user 메시지에 담깁니다. 문서는 이 규칙을 이렇게 말합니다. "Whichever strategy you use, return one tool_result for each tool_use block, all together in the next user message. Match each result to its call with tool_use_id, and put every tool_result block before any text content in that message."(어떤 전략을 쓰든, tool_use 블록 하나당 tool_result 하나를, 다음 user 메시지에 함께 모아 돌려주라. 각 결과는 tool_use_id로 그 호출에 묶고, 그 메시지 안에서는 모든 tool_result 블록을 어떤 텍스트 콘텐츠보다 앞에 두라)3
  4. 그 턴의 모델의 완전한 응답(assistant 롤)과, 여러분이 조립한 tool_result 일습(user 롤) 양쪽을 messages 에 덧붙이고, 다음 요청을 보냅니다.

그리고 가장 중요한 한 문장이 옵니다. stop_reason"tool_use" 인 동안, 단계 2부터 되풀이하라.2 이 "인 동안 되풀이하라"가 한 번의 왕복을 루프로 늘리는 굴대입니다. 레슨 1의 예가 첫 end_turn 에서 멈춘 것은, 그 작업에 왕복이 한 번만 필요했기 때문입니다. 온콜 봇은 단계 2부터 4까지를 세 번 돌리며, 세 번째 턴이 end_turn 으로 돌아올 때까지 계속합니다.

기억해 둘 가치가 있습니다. tool_use 블록과 tool_result 블록의 필드는 전혀 바뀌지 않았습니다. tool_use 블록은 id / name / input 을 가지고, tool_result 블록은 tool_use_id / content 를 가지며, 호출이 실패했을 때는 선택적으로 is_error 가 붙습니다.3 루프는 이 필드들의 의미를 하나도 다시 쓰지 않습니다. 같은 필드의 묶음이 몇 번이고 되풀이해 채워져 돌려보내지게 될 뿐입니다.

stop_reason이 루프의 while 조건입니다

앞의 한 줄 — "stop_reason 이 아직 tool_use 인 동안, 되풀이하라" — 은 코드로 하면 while 루프의 조건식이 됩니다. 그리고 이 레슨에서 챙겨 가야 할 한 문장은 이것입니다. 루프가 계속될지 멈출지의 판단은 stop_reason 이라는 단 하나의 필드로 귀착됩니다. 취할 수 있는 값은 여럿이지만, 루프 제어를 위해서는 우선 둘을 가려낼 수 있으면 충분합니다.

  • "tool_use": 모델은 아직 도구를 쓰고 싶어 합니다. 요청을 여러분에게 넘기고, 여러분이 실행해 결과를 돌려보내기를 기다린 뒤에 앞으로 나아갑니다. 루프는 한 바퀴 더 돕니다.
  • "end_turn": 모델은 더 이상 도구를 쓰고 싶어 하지 않습니다. 할 말은 했다고 여깁니다. 루프는 자연히 끝나고, 여러분은 최종 텍스트를 사용자에게 넘깁니다.

하네스 엔지니어링에 관한 어느 오픈소스 로드맵은 이 제어 층을 딱 잘라 말합니다. 하네스를 구동하는 것은 모델→도구→모델을 돌리는 while 루프다.4 그리고 그 while 루프의 조건식에 앉아 있는 것이 stop_reason 입니다. 같은 모델, 같은 도구 세트라도, 몇 바퀴를 돌고 언제 멈추는지는 호스트가 그 필드를 어떻게 읽고 그 조건을 어떻게 쓰는지로 완전히 정해집니다. 그래서 레슨 1이 "Same model, different harness, completely different result."(같은 모델, 다른 하네스, 완전히 다른 결과)라고 말한 것입니다4.

신호를 거꾸로 읽지 않도록, 먼저 또렷이 해 둘 것이 있습니다. stop_reason: "tool_use" 는 모델이 도구를 쓰고 싶어 한다는 뜻이지, 도구가 이미 쓰였다는 뜻이 아닙니다. 모델은 스스로 아무것도 실행하지 않습니다. 모델은 구조화된 요청을 발행할 뿐이고, 실제로 도구를 돌리는 것은 여러분의 호스트 코드(또는 Anthropic의 서버)이며, 결과는 그 뒤에야 비로소 대화로 흘러들어옵니다.2 그러니 루프 안에서 tool_use 가 나타난 그 순간에는, 아직 아무 일도 일어나지 않았습니다. 액션이 일어나는 것은 nameinput 을 읽어 내고 실제로 일을 하러 가는, 여러분 코드의 몇 줄 안에서입니다. "tool_use를 받았다"를 "도구의 실행이 끝났다"로 다뤄 버리는 것은, 한 번의 왕복에서 루프로 넘어갈 때 가장 빠지기 쉬운 걸림돌입니다 — 지금 이 루프가 어느 단계에 있는지를 잘못 보게 만듭니다.

써 보면, 겨우 몇 줄입니다

이 네 단계와 stop_reason 의 while 조건을 JavaScript로 옮기면, 뼈대는 놀랍도록 짧습니다.

네 단계를 다시 한번 코드와 맞춰 봅시다. while 줄이 "아직 tool_use 인 동안, 되풀이하라"입니다. 본체 안에서는 assistant 응답과 tool_result 블록으로 이루어진 user 메시지 양쪽이 messagespush 되고, 그다음 response 가 재대입됩니다. 마지막 대입이야말로 애초에 정지를 가능하게 하는 것입니다 — 이것을 빠뜨리면, response.stop_reason 은 옛 값을 영원히 붙들고, while 루프는 결코 빠져나오지 못합니다(그런 종류의 데드 루프는 레슨 4의 주역입니다).

이 코드는 돌아가지만, 어디까지나 뼈대입니다 — 루프 자체가 보일 만큼 단순하고, 프로덕션에 넘길 만큼 안전하지는 전혀 않습니다. 이 코드는 모델이 언젠가의 턴에 반드시 end_turn 을 돌려주리라 가정하고, 모든 도구가 문제없이 실행되리라 가정하고, 히스토리이 얼마나 길어지든 상관없다고 가정합니다. 이 세 가정이야말로 다음 몇 개의 레슨이 하나씩 해체해 갈 것입니다.

한 턴 나아갈 때마다 히스토리은 길어집니다

messages.push 줄을 다시 보십시오. 루프가 한 바퀴 돌 때마다 messages 에는 메시지가 둘 늘어납니다 — 모델의 assistant 응답과, 여러분이 돌려보낸 tool_result 일습입니다. 그리고 다음 callModelmessages 전체를 그대로 다시 보내야 합니다. 그러니 이 루프가 오래 돌수록, 각 요청이 나르는 히스토리은 늘어나고, 그것은 늘어나기만 합니다.

이것은 구현의 빠뜨림이 아닙니다. 루프라는 구조에 본래 갖춰진 성질입니다. 루프 안에서 도는 에이전트는, 다음 추론 턴에 관련될 수 있는 데이터를 점점 더 많이 생성합니다.5 온콜 봇의 세 턴이라면, 재시작 결과와 로그 덩어리가 쌓일 뿐입니다. 하지만 수십 턴이 필요한 작업은, 히스토리을 거대한 것으로 굴려 갑니다.

여기에는 "에이전트 메모리와 상태" 과정에서 비로소 본격적으로 열리는 문제가 잠겨 있지만, 지금 씨를 뿌려 둘 필요가 있습니다. 모델에는 '어텐션 예산'이 있고, 새로 도입된 토큰은 저마다 그 예산을 어느 정도 소모합니다.5 히스토리이 길수록 토큰은 늘고, 컨텍스트 윈도우 안의 토큰 수가 늘면, 그 컨텍스트에서 정보를 정확히 떠올리는 모델의 능력은 낮아집니다.5 이것은 길이와 함께 완만하게 내려가는 성능의 기울기이지, 어떤 임계값을 넘으면 떨어지는 절벽이 아님에 주의하십시오5 — "상한을 넘으면 쓸모없다"고 읽지 마십시오. 다만 방향은 분명합니다. 컨텍스트는 한계 효용이 체감하는 유한한 자원으로 다뤄야 합니다.5 뼈대 루프에 있는 그 무심한 messages.push 는, 이 어느 것에도 대처하지 않습니다. 히스토리은 영원히 늘릴 수 있다고 가정합니다 — 그리고 그 청구서를 정산하러 돌아오는 것이 "에이전트 메모리와 상태" 과정입니다.

루프만으로는 부족하다: 경계를 더해야 합니다

이제 도는 루프가 손에 들어왔습니다. 하지만 "돌 수 있다"와 "안전하게 돈다"는 별개입니다. 뼈대 루프는 멈출지 계속할지의 판단을 완전히 모델에 맡깁니다. 모델이 end_turn 을 돌려준 턴이, 루프가 멈추는 턴입니다. 그러나 에이전트란, 모델이 자신의 프로세스와 도구 사용을 동적으로 지휘하는 시스템입니다.1 그 자율성이야말로 유용함의 원천이며, 동시에 위험의 거처이기도 합니다. 자율성은 더 높은 비용과, 루프의 턴을 거듭할수록 쌓여 가는 에러의 가능성을 의미합니다.1 모델은 다수의 턴에 걸쳐 동작할 가능성이 있으며, 돌리기 전에 그 의사결정에 어느 정도의 신뢰를 두어야 합니다.1

성가신 것은, 신뢰가 무감독과 같지 않다는 점입니다. 모델이 어느 단계에서 막히거나, 도구가 돌려준 것에 이끌려 탈선하고, 그대로 end_turn 을 돌려주지 않게 되면, stop_reason 만 보는 루프는 언제까지고 함께 돌아갑니다. 그러니 모델 자신의 종료 신호에 더해, 대개는 명시적인 정지 조건도 더합니다 — 예를 들어 최대 반복 횟수의 상한을 두어, 제어를 자기 쪽에 지켜 두는 것입니다.1 뼈대에 있는 벌거벗은 while (response.stop_reason === "tool_use") 에는 그런 퓨즈가 없습니다. 자신의 도망길을 남기지 않은 채 모델을 신뢰합니다.

이로써 이 레슨의 두 복선이 놓였습니다. 이 루프에는 경계가 필요하다(모델이 end_turn 이라고 말하는 것에 기댈 수 없으므로, 명시적인 정지 조건이 필요합니다 — 레슨 3), 그리고 이 루프가 낳는 히스토리에는 관리가 필요하다(토큰은 유한한 자원이므로, 그저 던져 넣기만 할 수는 없습니다 — "에이전트 메모리와 상태" 과정에 맡깁니다). 루프가 실제로 탈선하면 어떻게 보이는지, 그리고 그것을 어떻게 잡는지는 레슨 4의 주제입니다. 이 레슨에서는 굴대를 앉히는 데까지면 충분합니다. 루프가 어떻게 돌고, 그것을 구동하는 것이 stop_reason 이라는 것입니다.

Recap

  • 한 번의 도구 호출 왕복을 루프로 잇는 데 새 장치는 필요 없고, 네 단계를 되풀이할 뿐이다: 요청을 보낸다 → stop_reasontool_use 블록을 읽는다 → 도구를 실행해 tool_result 블록에 담는다 → 히스토리에 덧붙여 다시 보낸다. 그리고 stop_reason 이 아직 tool_use 인 동안, 되풀이한다2
  • stop_reason 이 이 루프의 while 조건이다: tool_use 는 모델이 아직 도구를 쓰고 싶다는 뜻이라 루프가 계속되고, end_turn 은 모델이 마무리한다는 뜻이라 루프가 자연히 끝난다 — 모델→도구→모델이라는 굴대는, 그 필드가 구동한다4
  • tool_use 는 모델이 도구를 쓰고 싶다는 신호이지, 실행되었다는 영수증이 아니다. 모델은 스스로 아무것도 실행하지 않고, 액션은 호스트가 nameinput 을 읽어 내 일을 하러 가는 곳에서 일어난다2
  • 루프가 한 바퀴 돌 때마다 히스토리은 길어지고, 짧아지는 일은 없다. 루프 안의 에이전트는 관련될 수 있는 데이터를 계속 생성하기 때문이다5. 그리고 어텐션 예산은 유한하고, 컨텍스트가 늘수록 상기는 나빠지므로, 토큰은 한계 효용이 체감하는 유한한 자원으로 다뤄야 한다5
  • 루프만으로는 부족하다: 자율성은 더 높은 비용과 쌓여 가는 에러를 가져오고, 모델은 다수의 턴에 걸쳐 동작할 수 있으므로1, 모델 자신의 end_turn 에 더해, 대개는 명시적인 정지 조건(예를 들어 최대 반복 횟수)을 더해 제어를 자기 쪽에 지켜 둔다1 — 그 설정 방법은 레슨 3의 주제다

>> 레슨 3: 정지 조건: 에이전트는 언제 손을 떼야 하는가

Footnotes

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

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

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

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

  5. Effective context engineering for AI agents — Anthropic Engineering — https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents 2 3 4 5 6 7

연습

01

어느 일정 보조 에이전트에, search_calendar(캘린더를 본다)와 create_event(예정을 넣는다)라는 두 도구가 설정되어 있습니다. 사용자가 "목요일 오후가 비어 있는지 봐 주고, 비어 있으면 30분짜리 리뷰를 넣어 줘"라고 말했습니다. 호스트는 이 레슨의 뼈대 루프로 그것을 구동하고, 실제로 일어난 stop_reason 의 나열은 다음과 같습니다.

레벨 1: 이 루프가 몇 바퀴 도는지 세기
  • 첫 번째 callModel 이 돌려줌 → stop_reason: "tool_use"(tool_use 블록 하나, search_calendar 를 호출)
  • 두 번째 callModel 이 돌려줌 → stop_reason: "tool_use"(tool_use 블록 하나, create_event 를 호출)
  • 세 번째 callModel 이 돌려줌 → stop_reason: "end_turn"(텍스트: "목요일 14:00에 리뷰를 넣었습니다")

다음에 답하십시오. (1) callModel 은 모두 몇 번 불렸습니까? (2) executeTool 은 모두 몇 번 불렸습니까? (3) while 루프의 본체는 몇 번 실행되었습니까? (4) 어느 stop_reason 을 읽은 시점에 루프를 빠져나왔습니까?

완료 기준 · 로컬에서 확인
02

동료가 한 번의 왕복을 멀티턴 루프로 바꾸려고 아래 코드를 썼습니다. 도구 호출이 한 번으로 끝나는 작업에서는 "돌아가는 것처럼 보이"지만, 모델이 두 번 잇달아 도구를 호출해야 하는 작업이 되는 순간, 프로세스가 멈춰 걸리고, 리소스를 먹어 치우며, 로그에는 같은 도구가 몇 번이고 불리는 기록이 남습니다. 근본 원인을 찾아 고치십시오.

레벨 2: 왜 이 루프는 멈추지 않는가
완료 기준 · 로컬에서 확인