에이전트


**에이전트**란 루프 안에서 실행되며, 작업이 끝날 때까지 다음에 호출할 도구를 계속 선택해 나가는 모델을 말합니다. 이 장의 나머지 부분은 그것이 정확히 무엇을 뜻하는지, 어디에서 제 값을 하는지, 그리고 어디에서 조용히 대가를 치르게 되는지를 다룹니다.
에이전트 루프
도구 사용에서는 모델이 함수를 하나 호출하고 그 결과를 다시 돌려보냈습니다. 하지만 어떤 작업은 여러 단계가 필요하고, 그 단계를 미리 알 수 없는 경우도 있습니다. "가장 싼 항공권을 찾아서 예약해줘"라는 요청은 검색하고, 비교하고, 일정을 확인한 다음 예약하는 순서를 의미할 수 있는데, 모델은 그 순서를 진행하면서 스스로 파악해 나가야 합니다.
그래서 도구 호출 하나만 처리하는 대신, 전체 과정을 루프 안에 넣습니다. 이렇게 반복되는 순환 구조가 바로 **에이전트 루프**입니다. 작업이 끝날 때까지 한 턴이 계속 반복됩니다.
- 대화 내용과 사용 가능한 도구 목록을 모델에 전달합니다.
- 모델은 도구를 호출하거나 최종 답변을 내놓습니다.
- 도구를 호출했다면 그것을 실행하고, 결과를 대화에 추가한 다음 1단계로 돌아갑니다.
- 최종 답변을 내놓았다면 멈춥니다.
매 턴마다 모델은 이전 도구 결과를 포함해 지금까지 일어난 모든 일을 보고, 다음에 할 일을 결정합니다. 도구 사용과의 차이는 이것 하나입니다. 모델이 작업을 마쳤다고 판단할 때까지 계속 반복한다는 점입니다.
진짜 달라지는 것은 누가 방향을 정하느냐입니다. 지금까지 살펴본 모든 장에서는 코드가 제어 흐름을 결정했습니다. 언제 모델을 호출할지, 언제 도구를 실행할지, 다음에 무엇을 할지를 여러분이 정했습니다. 에이전트는 그 결정권을 모델에게 넘깁니다. 루프는 "다음에 무엇을 해야 하는가?"를 계속 묻고, 모델의 예측이 매번 그 답이 됩니다. 여러분은 제어권을 유연성과 교환하는 셈이고, 그 교환이 바로 도달 범위와 위험의 근원입니다.
그 이면에서는 새로운 일이 일어나지 않습니다. 점점 커지는 컨텍스트에 대한 동일한 예측일 뿐이며, 도구 사용에서의 왕복이 반복되는 것입니다. 모델은 계획을 세워놓고 그것을 따르는 게 아닙니다. 매 턴마다 지금 볼 수 있는 것을 기준으로 다음 한 걸음으로 가장 그럴듯한 것을 예측할 뿐이고, 그런 다음 루프가 다시 돌아갑니다.
이것이 잘 맞아떨어지면 목표를 향해 추론하는 것처럼 보입니다. 그렇지 않으면 에이전트는 스스로 맴돌거나 막다른 길로 빠져버립니다. 시스템의 어느 부분도 전체 계획을 붙잡고 있지 않기 때문입니다.
최소한의 에이전트
이 루프를 코드로 옮기면 다음과 같습니다. 도구 사용에서 만든 도구 정의와 구현을 그대로 재사용하고, 모델이 더 이상 도구를 요청하지 않을 때까지 실행되는 루프로 감쌉니다.
import json
def run_agent(user_text, tools, tool_impls, max_steps=6):
messages = [{"role": "user", "content": user_text}]
step = 0
while step < max_steps:
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message)
calls = message.tool_calls
if not calls:
return message.content # 도구를 원하지 않음: 이것이 최종 답변
# 요청된 도구를 모두 실행하고 결과를 다시 넣어준다
for call in calls:
args = json.loads(call.function.arguments)
result = tool_impls[call.function.name](args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
step += 1
return "Stopped: the agent reached its step limit." # 안전장치위에서 아래로 읽어보면 이렇습니다. 모델을 호출하고, 그 메시지를 대화에 쌓고, 도구를 요청했다면 모두 실행해서 결과를 추가한 다음 다시 반복합니다. 모델이 도구 호출 대신 텍스트를 반환하면 그것이 최종 답변이고, 여러분은 그것을 반환합니다.
max_steps 값은 **단계 제한**을 설정하는데, 이는 루프가 폭주하지 않도록 막아주는 안전장치이며, 다음 절에서 다룰 내용입니다. 여기 나온 형태는 특정 제공사의 SDK를 따른 것입니다. 필드 이름은 제공사마다 다르지만, 루프 구조는 어디서나 동일합니다.
멈추기와 안전장치
모델이 언제 멈출지 결정하게 두는 루프는 멈추지 못할 수도 있습니다. 도구를 계속 호출하거나, 같은 일을 반복하거나, 막다른 길로 빠질 수 있습니다. 매 단계가 토큰으로 비용이 매겨지는 모델 호출이기 때문에, 폭주하는 에이전트는 돈과 시간을 빠르게 태워버립니다.
그래서 루프 주변에 두는 한계선, 즉 **안전장치**는 나중에 덧붙이는 다듬기 작업이 아니라 설계의 일부입니다.
- 단계 제한.
max_steps처럼 루프 횟수를 제한합니다. 제한에 도달하면 멈추고 반환합니다. - 도구 입력값 검증. 모델이 인자를 선택하므로, 실제 영향을 미치는 작업을 실행하기 전에 검증하세요.
- 도구가 할 수 있는 일을 제한. 에이전트에게 필요한 최소한의 권한만 주세요. 읽기만 하는 도구가 삭제하는 도구보다 안전합니다.
- 비용을 지켜보기. 작업당 여러 번의 모델 호출은 빠르게 늘어나므로, 토큰을 추적하고 예산을 정하세요.
이 모든 것을 하나로 묶는 흐름은 이렇습니다. 모델이 즉흥적으로 경로를 찾아가므로, 모델이 하지 않을 제한을 여러분이 대신 걸어두는 것입니다. 에이전트가 적어도 한 번은 잘못된 행동을 할 것이라고 가정하고, 그 상황을 견딜 수 있게 만드세요.
워크플로우가 에이전트보다 나은 경우
에이전트는 흥미로운 도구라서, 필요하지 않을 때도 손이 가기 쉽습니다. 간단한 규칙은 이것입니다. 이미 단계를 알고 있다면 에이전트를 쓰지 마세요. 각 단계가 고정된 순서로 실행되는 평범한 코드, 즉 **워크플로우**로 작성하세요.
워크플로우는 더 안정적이고, 더 저렴하고, 디버깅하기도 훨씬 쉽습니다. 모델이 경로를 찾아내기를 바라는 대신 여러분이 그 경로를 직접 통제하기 때문입니다. 에이전트는 그 단계들이 진행 과정에서 알게 되는 것에 따라 달라지는, 열린 형태의 작업을 위해 아껴두세요.
에이전트가 필요할 것처럼 보이는 많은 기능이 실제로는 구조화된 출력 호출이나 손수 연결한 RAG 조회로 더 잘 처리됩니다. 단계를 미리 적어둘 수 없을 때만 에이전트를 쓰세요.
실전에서
위에서 본 run_agent 함수가 전부입니다. 질문 하나, 도구 모음, 그리고 그 구현을 넘겨주면, 끝날 때까지 또는 단계 제한에 걸릴 때까지 반복됩니다. 작은 작업으로 이 루프가 제 값을 하는 모습을 볼 수 있습니다.
answer = run_agent(
"What's the weather in Oslo and should I bring an umbrella?",
tools, # 도구 사용 장에 나온 get_weather 포함
tool_impls, # {"get_weather": ...}
)
# 에이전트는 get_weather를 호출하고, "cloudy"를 읽은 다음, 마지막 한 턴에 답변한다:
# "오슬로는 흐리고 18도이니, 우산을 챙기는 게 좋겠습니다."도구 하나, 루프 하나, 답변 하나입니다. 같은 구조가 형태를 바꾸지 않고 여러 도구와 여러 단계로 확장됩니다. 이만큼 능력 있는 것을 만들 수 있게 되면, 다음 질문은 그것이 실제로 잘 작동하는지를 어떻게 아느냐입니다. 이것이 바로 평가에서 다룰 내용입니다.
run_agent 루프가 전체 패턴입니다. 질문, 도구, 그리고 그 구현을 넘겨주면, 모델이 더 이상 도구를 요청하지 않거나 단계 제한에 걸릴 때까지 반복됩니다. 도구 하나와 루프 하나는 형태를 바꾸지 않고 여러 개로 확장됩니다. 다음 질문은 그것이 실제로 잘 작동하는지 어떻게 아느냐이고, 이것이 평가입니다. 
