logo

에이전트 루프 만들기

함수 호출을 while 루프로 감싸 모델이 도구를 여러 번 호출하며 작업을 끝내는 고객 지원 에이전트를 만들고, 종료 조건과 최대 단계를 정한다.

  • 함수 호출에서는 도구 호출 요청 → 실행 → 결과 전달 → 최종 답변을 한 번씩 진행.
  • 실제 작업은 앞 도구의 결과를 보고 다음 도구를 골라야 하는 경우가 많음.
  • 에이전트 루프(agent loop): 모델이 "이제 답할 수 있다"고 판단할 때까지 모델 호출과 도구 실행을 반복하는 구조.
  • "An agent is a while loop": 대화 기록을 매 턴 다시 넣으며 도는 while 루프가 에이전트의 뼈대(Breunig, 2026).

출처: Drew Breunig(2026), The State of Context Engineering.

생각-행동-관찰 루프

Mermaid 다이어그램 원문

flowchart LR
    G[임무 수령] --> S[상황 파악<br/>대화·기억·도구 목록]
    S --> T[생각<br/>모델이 다음 행동 결정]
    T -->|도구 호출| A[행동<br/>애플리케이션이 도구 실행]
    A --> O[관찰<br/>결과를 대화에 추가]
    O --> T
    T -->|도구 호출 없음| F[최종 답변]

대화형 다이어그램을 불러오기 전에는 원문으로 표시됩니다.

  • 임무 수령 → 상황 파악 → 생각 → 행동 → 관찰 → 다시 생각(Google, 2025). 뒤의 세 단계 반복이 생각-행동-관찰(Think-Act-Observe) 주기.
  • 루프를 도는 동안 바뀌는 것은 모델이 아니라 messages. 매 턴 쌓인 대화 기록 전체가 다음 입력.

출처: Google(2025), Introduction to Agents.

실습 준비

!pip install -q openai
import json
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=Path("openrouter-key.txt").read_text().strip(),
)
model = "openai/gpt-4.1-mini"

고객 지원 도구 만들기

  • 예제: "내 주문 #12345는 어디 있나요?"에 답하는 고객 지원 에이전트.
  • 필요한 일: ① 주문 정보에서 운송장 번호 찾기 → ② 운송장 번호로 배송 상태 조회 → ③ 종합해 답변.
  • ②의 입력은 ①의 결과를 봐야 알 수 있음. 함수 호출 한 번으로는 첫 응답에서 find_order밖에 요청할 수 없음.
orders = {
    "12345": {"product": "무선 마우스", "status": "출고 완료", "tracking_number": "ZYX987"},
    "12346": {"product": "기계식 키보드", "status": "상품 준비 중", "tracking_number": None},
}
shipments = {"ZYX987": {"status": "배송 중", "location": "서울 강서 허브", "eta": "2026-11-03"}}

functions = {
    "find_order": lambda order_id: orders.get(order_id.lstrip("#"), {"error": "없는 주문"}),
    "get_shipping_status": lambda tracking_number: shipments.get(tracking_number, {"error": "없는 운송장"}),
}
tools = [{"type": "function", "function": {"name": name, "description": desc, "parameters": {
    "type": "object", "properties": {param: {"type": "string"}}, "required": [param]}}}
    for name, desc, param in [("find_order", "주문 조회. 출고 전이면 tracking_number가 null", "order_id"),
        ("get_shipping_status", "운송장 번호로 배송 상태 조회", "tracking_number")]]
  • 없는 번호는 예외 대신 error 항목으로 돌려줌. 모델이 결과를 읽고 다음 행동(번호 재확인 요청 등)을 정할 수 있게 하기 위함.
  • functions: 모델이 요청한 이름으로 실행할 함수를 찾는 표. tools: 같은 도구들의 JSON Schema 정의(함수 호출과 같은 형식).

에이전트 루프

  • 종료 조건(stop condition): 모델이 도구를 호출하지 않고 답변만 돌려줄 때.
  • 최대 단계(max steps): 종료 조건이 끝내 오지 않을 때를 대비한 안전장치.
def run_agent(messages, tools, max_steps=5):
    step = 0
    while step < max_steps:
        step += 1
        message = client.chat.completions.create(
            model=model, messages=messages, tools=tools, parallel_tool_calls=False).choices[0].message
        messages.append(message.model_dump(exclude_none=True))  # 생각
        if not message.tool_calls:  # 종료 조건
            return message.content
        for call in message.tool_calls:
            result = functions[call.function.name](**json.loads(call.function.arguments))  # 행동
            print(f"[{step}단계] {call.function.name} → {result}")
            messages.append({"role": "tool", "tool_call_id": call.id,  # 관찰
                             "content": json.dumps(result, ensure_ascii=False)})
    return "(최대 단계 도달)"
  • 함수 호출 실습과 같은 부품(model_dump, tool 역할 메시지, tool_call_id)을 while로 감쌌을 뿐. 실제로는 도구 실행 오류(없는 도구, 잘못된 인자)도 try로 잡아 결과로 돌려주는 것이 좋음.
  • 실행 후 messages에 전체 궤적(trajectory)이 남음. 12주 하네스 실습에서 이 run_agent에 승인·로그·예산 장치를 덧붙임.
system = "쇼핑몰 고객 지원 에이전트입니다. 주문과 배송 정보는 도구로 조회한 결과로만 답하세요."

def ask(question):
    return run_agent([{"role": "system", "content": system}, {"role": "user", "content": question}], tools)

print(ask("내 주문 #12345는 어디 있나요?"))
실행 결과
[1단계] find_order → {'product': '무선 마우스', 'status': '출고 완료', 'tracking_number': 'ZYX987'}
[2단계] get_shipping_status → {'status': '배송 중', 'location': '서울 강서 허브', 'eta': '2026-11-03'}
고객님 주문하신 무선 마우스(#12345)는 현재 서울 강서 허브에서 배송 중입니다. 예상 도착일은 2026년 11월 3일입니다. 추가로 궁금하신 점 있으시면 알려주세요.
  • 위 실행에서는 1단계에 find_order로 운송장 번호를 얻고, 2단계에 그 번호로 get_shipping_status를 호출. 3단계에서 도구 없이 답변해 루프 종료.
  • 도구 순서는 코드에 적지 않았음. 모델이 앞 결과를 보고 정함.
  • 대화 기록에는 모델 응답 3번(assistant)과 도구 결과 2번(tool)이 쌓이고, 다음 호출 때 전체가 입력이 됨.

상황에 따라 달라지는 단계 수

출고 전 주문: 운송장 번호가 없으므로 배송 조회가 필요 없음.

print(ask("주문 12346은 언제 와요?"))
실행 결과
[1단계] find_order → {'product': '기계식 키보드', 'status': '상품 준비 중', 'tracking_number': None}
주문 번호 12346번은 현재 상품 준비 중인 상태입니다. 아직 출고되지 않아 운송장 번호가 없습니다. 상품이 출고되는 대로 배송 상태를 안내해 드리겠습니다.
  • 위 실행에서는 tracking_number가 None이라 배송 조회를 건너뛰고 1단계 만에 답변. 같은 루프가 질문에 따라 1단계로도, 2단계로도 끝남. LLM 출력이므로 문장은 실행마다 달라짐.
  • 더 해 볼 것: 없는 주문 번호(99999)를 물으면 error를 관찰한 뒤 어떻게 답하는지, run_agent(..., max_steps=1)이면 2단계가 필요한 질문에서 답하기 전에 멈추는지 확인.
  • max_steps는 너무 작으면 일을 못 끝내고, 너무 크면 헛도는 루프의 비용을 키움. 멈췄을 때 무엇을 돌려줄지(중간 결과, 사람에게 넘기기)도 설계 대상. 예산·시간 한도, 사람의 중단 같은 다른 종료 방식은 12주 하네스에서 다룸.

워크플로와 에이전트

구분 워크플로(workflow) 에이전트(agent)
도구 순서 코드에 미리 정함 모델이 매 단계 결정
단계 수 고정 작업에 따라 달라짐
장점 예측 가능, 디버깅 쉬움 예상하지 못한 상황에 대응
단점 정해진 경로 밖의 요청에 약함 비용·지연이 들쭉날쭉, 엉뚱한 행동 가능
  • 단계가 늘 같다면 워크플로가 더 싸고 안정적. 다음 행동이 앞 결과에 따라 달라질 때 에이전트 루프가 필요.

출처: Anthropic(2024), Building effective agents.

퀴즈

문제 1 / 5맞음: 0힌트: 0틀림: 0채점중: 0남음: 5

주문 #12345의 배송 위치를 묻는 질문을 함수 호출 한 번으로 끝낼 수 없는 이유는?

  • ○배송 조회에 필요한 운송장 번호를 주문 조회 결과를 봐야 알 수 있기 때문에
  • ○모델은 한 대화에서 도구를 하나만 정의할 수 있기 때문에
  • ○도구 실행 결과는 모델에 다시 전달할 수 없기 때문에
  • ○주문 조회 도구가 모델 안에서 직접 실행되기 때문에

퀴즈를 풀려면 대화형 기능을 불러와야 합니다.