본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

멀티 에이전트

이 페이지에 묻기

멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.

생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

MULTI · AGENT

멀티 에이전트

생성 수정

도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.

26개 문서 · 2일차 · 10 / 26

9차시 · 에이전트 루프 직접 구현

지도 위치 : 2일차 · README 4장의 ② 행동 층 — 도구를 한 번 부르던 앱이 스스로 반복 하기 시작하는 자리 | 이전: 8차시 보안 — 안전한 도구 실행 | 다음: 10차시 루프 제어와 가드레일 키워드 : Agent Loop, ReAct 패턴

이 차시를 다 읽으면

  1. 에이전트 루프를 유와 종차로 정의하고, 앞에서 배운 "도구 한 번 호출"(5차시)과 누가 흐름을 정하는가 축으로 가를 수 있다.
  2. ReAct(생각 → 행동 → 관찰의 반복)를 그림으로 그리고, 오피스봇 질문 하나에 대입할 수 있다.
  3. 수동 루프 코드에서 멈추는 조건 두 가지 (stop_reason, MAX_STEPS)를 찾아 짚을 수 있다.
  4. API는 무상태인데 루프가 앞 단계 결과를 "기억"하는 이유를 축 ⑦로 설명할 수 있다.
  5. 루프 로그를 읽고 "몇 번 돌았나, 무엇을 불렀나, 왜 멈췄나"를 말할 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1⭐ 누가 흐름을 정하는가코드 — 미리 짠 순서대로 도구를 부름모델 — 결과를 보고 다음 행동을 정함워크플로우 ↔ 에이전트 루프중심 축 (README 5.2), 부모 ⑦
2무상태 ↔ 상태API 호출 한 번은 이전을 모름messages 리스트에 관찰이 쌓임루프가 앞 단계를 "기억"하는 방식⑦ 무상태 ↔ 상태
3한 번 ↔ 반복도구 호출 1회 → 답목표에 닿을 때까지 여러 바퀴도구를 쓰는 앱 ↔ 에이전트이 차시의 축
4누가 멈추는가모델 — 답을 냄(end_turn)코드 — 상한에 닿으면 끊음(MAX_STEPS)정상 종료 ↔ 강제 종료부탁 ↔ 강제 (10차시에서 확장)

이 차시의 질문 — 직원이 "다음 주 월~수 휴가 쓸 수 있어?"라고 물으면 잔여 연차 조회 와 휴가 규정 확인 이 둘 다 필요합니다. 앞에서 배운 도구 호출(5차시)은 "요청 → 실행 → 결과 → 답"의 한 바퀴였고, 다중 도구 라우팅(7차시)은 여러 도구 중 하나를 고르는 일이었습니다. 이번 차시는 한 바퀴를 여러 번 돌리고, 몇 바퀴를 돌지 모델이 정하게 합니다. 1일차가 대부분 중심 축의 왼쪽(코드가 흐름을 정함)이었다면, 이 차시에서 처음으로 오른쪽 으로 건너갑니다.


1. ⭐ 누가 흐름을 정하는가 — 한 번에서 반복으로

1.1 앞에서 배운 한 바퀴 (5차시 회수)

짧게 다시 짚습니다. 앱이 tools=[...](도구 선언: 이름 · 설명 · 인자 형식)를 붙여 모델을 부르면, 모델은 답 대신 "이 도구를 이 인자로 불러 달라"는 tool_use 블록 을 낼 수 있습니다. 이때 stop_reason은 "tool_use"입니다. ⭐ 모델은 실행하지 않고 요청만 합니다. 앱이 함수를 실행해 그 결과를 tool_result로 돌려주고 다시 부르면, 모델이 결과를 읽고 답합니다.

질문필요한 도구 호출한 바퀴로 끝나는가
"올해 연차 며칠 남았어?"잔여 조회 1번⭕
"다음 주 월~수 휴가 쓸 수 있어?"잔여 조회 → (결과를 보고) 규정 확인 → 답❌ 두 번째 호출이 첫 결과에 따라 달라짐
"IT 티켓 상태 보고, 아직 미처리면 새로 접수해 줘"조회 → (미처리면) 접수❌ 다음 행동이 관찰에 달려 있음

⭐ 아래 두 줄이 핵심입니다. 다음에 무엇을 할지가 앞 결과를 봐야 정해지면 , 순서를 미리 코드로 짤 수 없습니다. 그 판단을 모델에게 넘기는 구조가 에이전트 루프입니다.

1.2 정의와 경계

에이전트 루프 (Agent Loop) — [LLM 응용의 실행 구조]인데, [ 모델의 응답이 도구 요청인 동안 "호출 → 실행 → 결과 돌려주기"를 반복하고, 모델이 답을 내거나 코드가 정한 상한에 닿으면 멈춘다 ]는 점에서 코드가 정한 순서대로 도구를 부르는 워크플로우와 구별된다.

워크플로우 (코드가 정함)에이전트 루프 (모델이 정함)
다음 행동개발자가 짠 if · 순서모델이 직전 관찰을 보고 결정
도구 호출 횟수미리 정해짐실행해 봐야 앎 (0번일 수도, 6번일 수도)
예측 · 시험쉬움어려움 — 같은 질문에 경로가 달라질 수 있음
비용 · 맞는 일고정에 가까움 · 순서가 늘 같은 일바퀴 수만큼 늘어남 (10차시) · 순서를 미리 정할 수 없는 일

1.3 반례 — 이건 아직 에이전트 루프가 아닙니다

대상왜 아닌가
도구 목록을 준 한 번의 호출반복이 없음. 5차시의 한 바퀴
코드가 도구 셋을 정해진 순서로 부르고 마지막에 LLM이 요약반복은 있지만 순서를 코드가 정함 . 워크플로우
응답이 깨졌을 때 같은 요청을 다시 보내는 재시도반복이지만 관찰을 보고 다음 행동을 고르지 않음
한 응답 안에 tool_use가 셋 (병렬 도구 호출)한 바퀴 안에서 여럿을 동시에 요청한 것. 바퀴 수는 1

💡 비유(기억 보조용): 체크리스트를 위에서부터 그대로 따르는 신입 ↔ 서류를 하나 확인할 때마다 다음에 어디를 볼지 스스로 정하는 담당자. 다만 이 담당자에게는 "최대 여덟 군데까지만 확인"이라는 규칙을 회사(코드)가 걸어 둡니다(5절).


2. ReAct 패턴 — 생각 · 행동 · 관찰

2.1 정의

ReAct (Reason + Act) — [에이전트 루프의 설계 패턴]인데, [ 생각(Reason) → 행동(Act, 도구 호출) → 관찰(Observation, 도구 결과)을 한 바퀴로 삼고, 관찰을 다음 생각의 재료로 쓴다 ]는 점에서 생각만 길게 하고 한 번에 답하는 방식이나, 생각 없이 도구만 연달아 부르는 방식과 구별된다.

  ┌─▶ 생각 "잔여부터 봐야겠다" ─▶ 행동 tool_use: 잔여 조회 ─▶ 관찰 tool_result: "5일 남음" ─┐
  └──────────────────────────────── 관찰을 들고 다시 생각 ◀────────────────────────────────┘
        │ 더 할 일이 없다고 판단하면
        ▼
      답 (stop_reason = end_turn)

2.2 Claude API에서는 어디에 해당하는가

ReAct가 처음 나왔을 때는 프롬프트에 "Thought: … Action: … Observation: …" 형식을 글로 쓰게 했습니다. 지금은 API의 도구 호출 구조가 그 자리를 대신합니다.

ReAct의 칸Claude API에서의 모습누가 만드나
생각모델 내부의 생각(thinking) · 설명 text 블록모델
행동tool_use 블록 (name, input)모델이 요청
관찰tool_result 블록⭐ 앱이 실행해서 돌려줌
답stop_reason == "end_turn"인 응답의 text 블록모델

💡 최신 Claude 모델은 적응형 생각(adaptive thinking)이 기본으로 켜져 있어 "단계적으로 생각해"라고 따로 부탁할 필요가 줄었습니다. 그래서 응답 content에는 text · tool_use 외에 생각 블록이 섞여 올 수 있습니다. ⚠️ 이 때문에 루프는 응답을 골라 담지 않고 response.content를 통째로 다음 요청에 넣습니다(3.1).


3. 직접 구현 — 수동 루프

3.1 루프 골격

import anthropic
client = anthropic.Anthropic()            # 키는 환경변수 ANTHROPIC_API_KEY에서 자동으로 읽음

MAX_STEPS = 8
messages = [{"role": "user", "content": "다음 주 월~수 휴가 쓸 수 있어?"}]

for step in range(MAX_STEPS):
    response = client.messages.create(
        model="claude-opus-5-5", max_tokens=16000,
        system=SYSTEM_PROMPT, tools=tools, messages=messages,
    )
    if response.stop_reason != "tool_use":
        break                                              # 모델이 더 부를 도구가 없다 → 루프 끝
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            content, is_error = run_tool(block.name, block.input)
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": content, "is_error": is_error})
    messages.append({"role": "user", "content": results})
else:
    print("MAX_STEPS 도달 — 답을 내지 못하고 코드가 멈춤")   # 강제 종료

🔍 읽는 포인트

  • for step in range(MAX_STEPS) — ⭐ 반복 상한 . 모델이 멈추지 않아도 코드가 여덟 바퀴에서 끊습니다. while True로 쓰지 않습니다. 맨 아래 else:는 break 없이 끝까지 돌았을 때, 즉 "상한에 닿았다"는 뜻입니다.
  • if response.stop_reason != "tool_use": break — 모델이 도구를 더 요청하지 않으면 나갑니다. 다만 이 줄은 정상 답(end_turn)과 잘림(max_tokens) · 거절(refusal)을 구별하지 않습니다 → 5절에서 나눕니다.
  • messages.append(... response.content) — 모델의 요청을 먼저 , 그 뒤에 tool_result를 넣습니다. 둘은 tool_use_id로 묶인 짝 입니다.
  • 안쪽 for block — 한 응답에 tool_use가 여럿이면 결과를 모두 한 user 메시지에 담습니다.

3.2 도구 실행부 — 1일차의 안전 장치가 그대로 들어가는 자리

TOOL_FUNCS = {"get_leave_balance": get_leave_balance,
              "get_leave_policy": get_leave_policy}

def run_tool(name, args):
    func = TOOL_FUNCS.get(name)
    if func is None:
        return f"오류: 알 수 없는 도구 {name}", True
    try:
        return func(employee_id=session.employee_id, **args), False   # 신원은 로그인 세션에서
    except Exception as e:
        return f"오류: {e}", True                                      # 실패도 결과로 돌려줌

🔍 읽는 포인트

  • TOOL_FUNCS.get(name) — 모델이 없는 도구 이름을 지어내도 실행되지 않습니다. 허용 도구 목록 입니다(10차시에서 가드레일로 다시 봄).
  • employee_id=session.employee_id — 앞에서 배운 권한 경계 (8차시): 누구의 데이터인지는 모델이 준 인자가 아니라 로그인 세션 에서 가져옵니다.
  • return ..., True — 실패를 예외로 터뜨리지 않고 is_error가 붙은 결과로 돌려줍니다. 모델이 읽고 다른 방법을 고를 기회 가 되며, 이것도 관찰의 하나입니다.
  • ⭐ 루프는 새 안전 장치를 만들지 않습니다. 1일차의 권한 검사 · 마스킹 · 예외 처리가 run_tool 안에서 매 바퀴 똑같이 돌 뿐입니다.

4. 축 ⑦ — messages가 쌓이며 상태가 된다

4.1 API는 기억하지 않는다

앞에서 배운 대로(2차시) LLM API는 무상태 입니다. 두 번째 호출은 첫 번째 호출을 모릅니다. 그런데 루프의 3바퀴째에 모델은 1바퀴째 결과("5일 남음")를 압니다. 이유는 하나입니다 — 앱이 매번 지금까지의 messages 전체를 다시 보내기 때문입니다.

1바퀴 보냄: [user 질문]
     받음:  assistant [생각, tool_use 잔여조회]
2바퀴 보냄: [user 질문, assistant tool_use, user tool_result "5일"]
     받음:  assistant [생각, tool_use 규정확인]
3바퀴 보냄: [user 질문, assistant tool_use, user tool_result "5일",
             assistant tool_use, user tool_result "3일 전까지 신청"]
     받음:  assistant [text 답]  → stop_reason = end_turn
무엇이 상태를 들고 있나어디에 있나언제 사라지나
messages 리스트앱의 메모리 (축 ①)루프 · 요청이 끝나면
대화 이력 · 세션 저장소메모리 또는 DB16차시에서 설계

⚠️ 두 가지가 따라옵니다.

  • 매 바퀴 입력 토큰이 늘어납니다. 3바퀴째는 1 · 2바퀴의 내용을 다시 보냅니다. 바퀴가 늘수록 비용은 더하기가 아니라 누적의 누적 으로 늘어납니다 → 10차시의 비용 상한.
  • tool_use와 tool_result의 짝이 깨지면 400 에러 가 납니다. 이력을 잘라 줄일 때 이 짝을 같이 다뤄야 합니다 → 16차시.

앞으로 다시 만납니다 — 16차시(멀티턴 대화)에서 이 messages를 요청을 넘어 세션 단위로 저장하고, 18차시(그래프 워크플로우)에서는 같은 생각을 그래프의 State 라는 이름으로 다시 만납니다.


5. 멈춤 조건 — 누가 멈추는가

5.1 모델이 멈추는 경우 — stop_reason 읽기

stop_reason뜻루프가 할 일
tool_use도구를 요청함실행하고 한 바퀴 더
end_turn답을 다 씀⭕ 정상 종료. text 블록을 답으로
max_tokens출력이 max_tokens에서 잘림⚠️ 답이 중간에 끊김. 그대로 보여 주지 말 것
refusal안전상 거절⚠️ content를 읽기 전에 확인하고 거절 안내로
pause_turn · stop_sequence긴 작업의 일시 멈춤 · 지정한 멈춤 문자열용도에 따라 처리 (세부는 공식 문서 확인)

5.2 코드가 멈추는 경우 — 상한

모델이 "이제 답하겠다"고 판단해 주기를 기대 하는 것은 부탁입니다. 같은 도구를 계속 부르거나, 결과가 계속 실패해 헛도는 경우는 언제든 생깁니다. 그래서 MAX_STEPS로 강제 합니다. README 6장의 문장 2(모델은 판단하고, 코드는 지킨다)가 루프에서 처음 나타나는 자리입니다.

💡 MAX_STEPS 값은 "평소 가장 긴 정상 경로 + 여유"로 잡습니다. 오피스봇의 휴가 질문은 보통 2~3바퀴이므로 8이면 넉넉합니다. 상한에 닿았을 때 사용자에게 보여 줄 문장("확인이 길어져 담당자에게 넘깁니다" 등)도 미리 정해 둡니다.

앞으로 다시 만납니다 — 10차시에서 멈춤 조건이 넷으로 늘어납니다: 최대 반복, 토큰 · 비용 누적 상한, 같은 호출 반복 감지, 시간 제한.


6. 오피스봇 적용 — "다음 주 휴가 가능해?"

6.1 한 질문의 세 바퀴

도구는 둘입니다. get_leave_balance(year) — 잔여 연차 조회(6차시에서 만든 것), get_leave_policy(topic) — 휴가 규정 조회(지금은 고정된 규정 문구를 돌려주는 가짜 함수, 12~14차시에서 문서 검색으로 바뀜).

바퀴생각 (모델)행동 (tool_use)관찰 (tool_result)
13일을 쓰려면 잔여부터 봐야 한다get_leave_balance({"year": 2026})"남은 연차: 5일"
2일수는 충분. 신청 기한 규정이 있을 것이다get_leave_policy({"topic": "연차 신청 기한"})"연차는 사용 3일 전까지 신청"
3둘 다 확인됨. 답할 수 있다(없음) → end_turn—

최종 답(예시): "남은 연차가 5일이라 월~수 3일은 쓸 수 있습니다. 다만 규정상 사용 3일 전까지 신청해야 하니, 이번 주 목요일까지 신청하세요."

⚠️ 같은 질문이라도 모델은 순서를 바꾸거나(규정 먼저), 2바퀴에 두 도구를 한꺼번에 요청할 수도 있습니다. 경로가 고정되지 않는 것이 에이전트 루프의 본성입니다. 그래서 "어떤 경로든 답이 맞고 상한 안에서 끝나는가 "를 검사합니다.

6.2 루프 로그를 사람이 읽는 법

[REQ-7c21] step=1 stop=tool_use tool=get_leave_balance input={"year":2026} → "남은 연차: 5일"   in=1,240 out=88
[REQ-7c21] step=2 stop=tool_use tool=get_leave_policy  input={"topic":"연차 신청 기한"} → "3일 전까지 신청"  in=1,410 out=95
[REQ-7c21] step=3 stop=end_turn 답=212자                                                     in=1,690 out=240
[REQ-7c21] 종료=정상(end_turn) 바퀴=3 누적 in=4,340 out=423

(예시 — 실측 아님)

🔍 읽는 포인트 — 코드를 몰라도 이 네 줄은 읽을 수 있어야 합니다.

볼 것정상 신호이상 신호
종료= · 바퀴 수정상(end_turn), 질문 난이도에 맞음 (2~3)MAX_STEPS · max_tokens · refusal, 단순 질문에 6바퀴 이상
tool= · input= · → 결과질문과 관련된 도구, 질문에 있는 값, 결과에 값이 있음같은 도구 · 같은 인자가 연속, 오류:인데 같은 호출 반복 → 헛돎
in=바퀴마다 조금씩 증가한 바퀴에 크게 뜀 (긴 도구 결과가 들어옴)

⭐ 요청 ID(REQ-7c21)로 한 질문의 모든 바퀴를 묶어 둡니다. 로그에 직원 정보가 찍히므로 앞에서 배운 마스킹 (8차시: 외부로 나가거나 기록되기 전에 주민번호 · 전화번호 등을 가리는 것)을 로그에도 적용합니다.


7. 이런 도우미도 있다 — Tool Runner

SDK에는 위 루프를 대신 돌려 주는 Tool Runner (베타)가 있습니다. 함수에 데코레이터를 붙이면 도구 선언을 만들어 주고, 호출 → 실행 → 결과 돌려주기를 알아서 반복합니다.

from anthropic import beta_tool

@beta_tool
def get_leave_balance(year: int) -> str:
    """직원의 남은 연차 일수를 조회한다. 휴가 잔여를 물을 때 사용."""
runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5", max_tokens=16000,
    tools=[get_leave_balance], messages=messages,
)
for message in runner:          # 바퀴마다 모델의 응답이 하나씩 나옴
    ...

🔍 읽는 포인트 : @beta_tool이 함수의 이름 · 설명(docstring) · 인자 타입으로 도구 선언을 만들고, for message in runner가 3.1의 루프를 대신합니다. 베타 기능이라 이름 · 옵션이 바뀔 수 있으니 공식 문서 확인 .

⭐ 도우미는 코드가 짧은 대신 루프 안을 감춥니다. 직접 구현을 먼저 하는 이유는, 감춰진 루프 안에서 무엇을 어디에 끼워야 하는지 (10차시의 비용 상한 · 반복 감지 같은 가드레일)를 알기 위해서입니다.

앞으로 다시 만납니다 — 11차시에서 같은 루프를 LangChain 프레임워크로 옮기며 "직접 구현 ↔ 프레임워크: 무엇을 얻고 잃나"를 다시 비교합니다.


8. 실습 — 맡기고 검수하기

지시

오피스봇에 에이전트 루프를 직접 구현해줘. (Tool Runner·프레임워크 쓰지 말고 messages.create로 직접)
- 참고: Claude API 공식 문서의 tool use 페이지 (최신 SDK 기준으로 확인하고, 확인한 문서 주소를 보고에 적을 것)
- 도구 2개: get_leave_balance(year) — 가짜 DB(직원 3명), get_leave_policy(topic) — 고정 규정 문구 3개
- 직원 신원은 함수 인자가 아니라 session 객체에서 가져올 것 (모델이 준 사번은 무시)
- MAX_STEPS = 8. stop_reason을 end_turn / max_tokens / refusal / 상한 도달로 나눠 처리
- 도구 실패는 is_error=True 인 tool_result로. 바퀴마다 [요청ID, step, stop_reason, 도구, 입력, 결과 앞 50자, input/output 토큰] 한 줄 로그
- API 키는 환경변수로만 (코드·로그에 키 금지)
테스트 질문: ① "안녕" ② "올해 연차 며칠 남았어?" ③ "다음 주 월~수 휴가 쓸 수 있어?"
 ④ get_leave_policy가 항상 오류를 내게 바꾼 뒤 ③  ⑤ MAX_STEPS=1로 바꾼 뒤 ③
추측하지 말고 실행 출력 원문을 붙여.
보고 형식: [질문 | 바퀴 수 | 부른 도구 순서 | 종료 이유 | 누적 토큰 | 최종 답 요약] 표

받을 증거

  • 질문 ①~⑤의 루프 로그 원문 과 위 형식의 표, ⑤에서 상한 도달로 코드가 멈췄다는 로그 줄, 참고한 공식 문서 주소

내가 직접 확인할 것

볼 것이걸로 아는 것
①의 바퀴 수가 1이고 도구 호출이 0번인가루프가 필요 없을 때 도구를 남발하지 않는가 (1.2)
③의 로그에 도구 두 개가 나뉘어 또는 함께 나타나는가모델이 관찰을 보고 다음 행동을 정했는가 (2절, 6.1)
③의 in= 숫자가 바퀴마다 늘어나는가messages 누적 = 상태 (4.1)
④에서 모델이 오류를 숨기지 않고 "규정을 확인하지 못했다"고 말하는가is_error 관찰이 전달됐는가 (3.2)
⑤에서 사용자에게 보이는 문장이 정해 둔 안내인가강제 종료 처리 (5.2)

부록 A. 스스로 확인하기

#질문답이 있는 곳
1에이전트 루프를 유와 종차로 정의하면? 워크플로우와 무엇으로 갈리나?1.2
2ReAct의 생각 · 행동 · 관찰은 Claude API의 어떤 블록에 해당하나?2.2
3응답을 골라 담지 않고 response.content를 통째로 넣는 이유는?2.2, 3.1
4수동 루프의 멈춤 조건 두 가지는?3.1, 5절
5무상태 API에서 루프가 앞 결과를 아는 이유는?4.1
6루프 로그에서 헛돎을 알아보는 신호는?6.2
7Tool Runner가 있는데도 직접 구현을 먼저 하는 이유는?7절

부록 B. 자주 하는 실수

실수바로잡기
while True로 루프를 돌림❌ 모델이 멈춰 주길 기대하는 것은 부탁. ⭕ MAX_STEPS로 강제
stop_reason != "tool_use"면 무조건 정상 답으로 처리⚠️ max_tokens(잘림) · refusal(거절)을 나눠 처리
응답에서 text 블록만 골라 assistant 메시지에 넣음❌ tool_use · 생각 블록이 빠져 짝이 깨짐. ⭕ response.content 통째로
도구 실패를 예외로 터뜨려 루프가 죽음⭕ is_error: True 결과로 돌려줘 모델이 대응하게
"루프를 쓰면 더 똑똑해진다"⚠️ 한 바퀴로 되는 질문에 루프는 비용만 늘림 (README 문장 1)

부록 C. NotebookLM 복습 프롬프트

이 9차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"누가 흐름을 정하는가(워크플로우 ↔ 에이전트 루프)", "ReAct의 세 칸", "messages 누적이 곧 상태", "멈춤 조건 두 가지"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.