실무 Multi-Agent 오케스트레이션 9장 · 에이전트 루프 직접 구현 3 / 7 ← 이전목차다음 → TechLead Cro

9장. 에이전트 루프 직접 구현

contents — 루프의 기억은 리스트 하나다

한 줄 요약

모델은 호출과 호출 사이에 아무것도 기억하지 못합니다. 루프가 이어지는 것은 지금까지의 질문, 모델의 요청, 도구의 결과를 리스트 contents에 차례로 쌓아 매번 통째로 보내기 때문입니다. 이 리스트가 에이전트의 기억이고, 길어질수록 매 호출의 입력 토큰도 늘어납니다.


1. 모델은 앞의 호출을 모른다

generate_content를 두 번 부르면 두 번째 호출의 모델은 첫 번째 호출에서 무슨 일이 있었는지 모릅니다. 호출은 서로 독립입니다.

그러면 두 번째 바퀴의 모델은 어떻게 "주문 목록에 배송중이 세 건 있었다"는 것을 알까요. 우리가 다시 알려 주기 때문입니다. 5장에서 두 번째 호출에 "질문 + 모델의 결정 + 실행 결과" 세 덩어리를 담아 보냈던 것을 떠올립니다. 루프는 그 덩어리를 계속 늘려 가는 것입니다.


2. 한 바퀴마다 두 덩어리가 쌓인다

contents: list[types.Content] = [
    types.Content(role="user", parts=[types.Part.from_text(text=question)])
]

처음에는 고객의 질문 하나입니다. 도구를 쓰는 바퀴마다 두 덩어리가 붙습니다.

contents.append(response.candidates[0].content)                       # ① 모델의 요청
...
contents.append(types.Content(role="user", parts=observation_parts))  # ② 도구의 결과

이번 장의 질문을 실행했을 때 contents에 쌓인 것을 그대로 출력하면 이렇습니다.

contents 에 쌓인 것
  1. role=user  글
  2. role=model 도구 요청(get_my_orders)
  3. role=user  도구 결과(get_my_orders)
  4. role=model 도구 요청(get_order_status), 도구 요청(get_order_status), 도구 요청(get_order_status)
  5. role=user  도구 결과(get_order_status), 도구 결과(get_order_status), 도구 결과(get_order_status)
바퀴 모델에게 보낸 것 모델이 아는 것
1 1번 질문뿐
2 1~3번 질문 + 주문 목록
3 1~5번 질문 + 주문 목록 + 세 주문의 상세

바퀴가 돌수록 모델이 아는 것이 늘어납니다. 2바퀴의 모델이 주문번호 세 개를 인자로 쓸 수 있는 것은 3번 덩어리(주문 목록)를 읽었기 때문입니다.

루프가 똑똑해 보이는 이유는 모델이 달라져서가 아닙니다. 같은 모델에게 매번 더 많이 알려 주기 때문입니다.


3. 누가 한 말인가 — role

contents의 각 덩어리에는 누가 한 말인지를 나타내는 role이 붙습니다.

덩어리 role 만드는 것
고객의 질문 "user" 우리 코드
모델의 도구 요청 "model" 모델 (응답에서 그대로 가져온다)
도구의 실행 결과 "user" 우리 코드

도구 결과를 돌려줄 때의 role은 "user" 입니다.

# ── Observation: 실행 결과를 대화에 추가 ──
contents.append(types.Content(role="user", parts=observation_parts))

모델 쪽에서 보면 대화 상대는 둘뿐입니다. 자기(model)와 자기 바깥(user). 고객의 질문도, 도구의 결과도 모델 바깥에서 들어오는 것이므로 같은 "user" 쪽입니다. 둘을 구분하는 것은 role이 아니라 덩어리 안의 내용입니다. 질문은 글(text)이고, 도구 결과는 function_response입니다.

자료를 찾다 보면 이 자리에 role="tool"을 쓴 예제를 만날 수 있습니다. 이 과정에서는 Gemini API의 함수 결과 전달 형식에 맞춰 role="user"를 사용합니다. "user"를 씁니다.


4. 모델의 요청은 통째로 넣는다

①번 줄을 다시 봅니다.

contents.append(response.candidates[0].content)   # 모델의 결정 기록

모델의 응답에서 도구 이름만 뽑아 새로 적는 것이 아니라, 응답에 들어 있던 덩어리를 손대지 않고 그대로 넣습니다. 이유가 둘 있습니다.

  • 도구 결과만 넣고 요청을 빼면, 모델이 보기에 묻지도 않은 결과가 갑자기 나타납니다. 요청과 결과는 짝으로 있어야 합니다.
  • 응답 덩어리에는 눈에 보이는 도구 요청 말고도 모델이 다음 호출에서 이어 쓰는 부가 정보가 함께 들어 있습니다. 직접 다시 만들면 그것이 빠집니다.

①번 줄과 ②번 줄은 늘 짝입니다. 모델의 요청을 넣고, 그 결과를 넣습니다. 이 두 줄이 있어서 모델은 다음 바퀴에 "무엇을 불렀고 무엇을 받았는지"를 알고, 같은 도구를 다시 부르지 않고 다음 일로 넘어갑니다. 쌓인 결과가 실제로 어떻게 보이는지는 「실습문제와 해답」의 문제 3에서 로그로 찍어 봅니다.


5. 기억에는 값이 든다

contents는 매 바퀴 통째로 모델에게 갑니다. 길어지면 입력 토큰이 늘어납니다. 같은 질문에서 바퀴마다 입력 토큰을 쟀습니다.

바퀴 contents 덩어리 수 입력 토큰
1 1 694
2 3 1,427
3 5 2,003

세 번의 호출에 쓴 토큰은 출력까지 합쳐 4,467입니다. 1바퀴의 694토큰에는 질문 말고도 시스템 프롬프트와 도구 여섯 개의 선언이 들어 있습니다(7장). 그 위에 바퀴마다 도구 결과가 얹힙니다.

세 바퀴째의 입력에는 첫 바퀴에 보낸 것이 전부 다시 들어 있습니다. 바퀴가 늘면 비용은 바퀴 수에 비례하는 것보다 더 빨리 늘어납니다. 여덟 바퀴를 돈다면 여덟 번째 입력이 얼마나 클지 지금 코드는 세지 않습니다. 이 측정은 「실습문제와 해답」의 문제 1에서 직접 합니다.


핵심 정리

  • 모델은 앞의 호출을 기억하지 못합니다. 루프의 기억은 contents 리스트입니다.
  • 도구를 쓰는 바퀴마다 모델의 요청과 도구의 결과 두 덩어리가 쌓입니다.
  • 도구 결과는 role="user" 로 돌려줍니다. 기본 모델은 role="tool"을 오류로 거절합니다.
  • 모델의 요청은 응답의 덩어리를 통째로 넣습니다. 요청과 결과는 짝입니다.
  • contents는 매번 통째로 보내므로 바퀴가 돌수록 입력 토큰이 늘어납니다 (694 → 1,427 → 2,003).
← 이전 절ReAct — 생각, 행동, 관찰을 되풀이한다다음 절 →종료 조건과 max_steps — 루프는 언제 멈추는가
오명운 · macro@prag-ai.com