LLM 엔지니어링9장 · 도구를 쓰는 LLM 과 Agentic Workflow7 / 21← 이전목차다음 →MetaContents

9장. 도구를 쓰는 LLM 과 Agentic Workflow

자율 루프

레시피를 보며 요리할 때는 한 단계를 하고, 맛을 보고, 다음 단계를 정합니다. 간이 맞으면 멈춥니다. 자율 루프도 모델이 도구를 부르면 실행해 결과를 보여 주고, 모델이 더 이상 도구를 부르지 않으면 멈춥니다.

모델에 묻기, 도구 호출이 있나, 실행하고 결과 덧붙이기, 다시 묻기, 없으면 답하고 끝의 흐름도

1번 메시지와 도구 목록을 모델에 보냅니다. 2번 응답에 도구 호출이 있으면 3번에서 실행해 결과를 메시지에 덧붙이고 1번으로 돌아갑니다. 도구 호출이 없으면 4번에서 그 글을 최종 답으로 돌려주고 끝냅니다. 한 바퀴가 한 걸음이고, 걸음 수에 한도를 둡니다. 여기서 맛보기가 도구 결과를 보는 일입니다.

루프의 뼈대

Python
def run_agent(ask_model, tools_impl, messages, max_steps=5):
    """ask_model(messages) -> (글, 도구 호출 목록). 도구 호출이 없으면 끝낸다"""
    for step in range(1, max_steps + 1):
        text, calls = ask_model(messages)
        if not calls:
            return text, step                      # 최종 답과 걸음 수
        for name, args in calls:
            result = tools_impl[name](**args)      # 우리 코드가 실행
            messages.append({"role": "tool", "content": str(result)})
    return "[걸음 한도에 닿았습니다]", max_steps

# 설명용 가짜 모델: 처음엔 계산기를 부르고, 결과를 보면 답한다
def fake_model(messages):
    if messages[-1]["role"] == "tool":
        return f"답은 {messages[-1]['content']} 입니다.", []
    return "", [("calculate", {"a": 37, "b": 48})]

print(run_agent(fake_model, {"calculate": lambda a, b: a * b}, [{"role": "user", "content": "37×48?"}]))

('답은 1776 입니다.', 2) 가 나옵니다. 첫 걸음에서 계산기를 부르고, 둘째 걸음에서 결과를 보고 답했습니다. 실습 4에서 가짜 모델 자리에 Ollama 를 넣습니다.

걸음 한도를 두는 까닭

한도가 없으면생기는 일
같은 도구를 되풀이해 부른다끝나지 않고 시간과 비용이 계속 든다
오류를 받고 같은 인자로 다시 부른다같은 오류가 끝없이 되풀이된다
쓰는 도구를 여러 번 부른다예약이 여러 개 생긴다

코드에 나오는 함수

코드쉬운 뜻
ask_model(messages)모델에 메시지를 보내고 (글, 도구 호출 목록)을 돌려받는 함수다. 여기서는 가짜 모델을 넣는다
tools_implimplementation(구현)의 줄임말이다. 도구 이름과 실제 함수를 짝지은 사전이다
for step in range(1, max_steps + 1)1 걸음부터 max_steps 걸음까지만 돈다
if not calls:도구 호출 목록이 비었으면, 곧 모델이 답을 냈으면 끝낸다
tools_impl[name](**args)이름으로 함수를 찾아 부른다. **args 는 사전 {"a": 37, "b": 48} 을 a=37, b=48 로 풀어 넘긴다
messages.append({...})결과를 메시지 목록 끝에 덧붙인다. 모델의 기억이 늘어난다
lambda a, b: a * b이름 없는 한 줄 함수다. 두 수를 곱한다

이 코드가 뜻하는 것

요리사가 레시피대로 한 단계 하고 맛보고, 다음 단계를 정하는 일입니다. 맛이 맞으면 접시에 담고 끝내지만, 타이머가 울리면 다 안 됐어도 멈춥니다. 여기서 맛보기가 ask_model, 접시에 담기가 return text, step, 타이머가 max_steps 입니다.

줄하는 일쉽게 말하면
text, calls = ask_model(messages)모델에 묻는다맛을 보고 다음 할 일을 정한다
if not calls: return text, step도구 호출이 없으면 답과 걸음 수를 돌려준다맛이 맞아 접시에 담는다
result = tools_impl[name](**args)요청한 도구를 실행한다소금을 넣는다
messages.append({"role": "tool", ...})결과를 덧붙인다넣은 것을 레시피 옆에 적어 둔다
return "[걸음 한도에 닿았습니다]", max_steps한도까지 돌면 실패를 알린다타이머가 울려 멈춘다
fake_model마지막이 tool 메시지면 답하고, 아니면 계산기를 부른다연습용 가짜 요리사

('답은 1776 입니다.', 2) 는 두 걸음 만에 답을 냈다는 뜻입니다. 비유와 다른 점은, 요리사는 스스로 타이머를 보지만 모델은 한도를 모르므로 우리 코드가 세어 멈춰야 한다는 것입니다.

걸음 한도에 닿는 모습

결과를 받아도 계속 계산기만 부르는 고장 난 가짜 모델을 넣어 봅니다. 걸음 한도는 3 으로 둡니다.

Python
def run_agent(ask_model, tools_impl, messages, max_steps=5):
    for step in range(1, max_steps + 1):
        text, calls = ask_model(messages)
        if not calls:
            return text, step
        for name, args in calls:
            result = tools_impl[name](**args)
            messages.append({"role": "tool", "content": str(result)})
    return "[걸음 한도에 닿았습니다]", max_steps

# 설명용 고장 난 가짜 모델: 결과를 받아도 계산기를 또 부른다
def stuck_model(messages):
    return "", [("calculate", {"a": 37, "b": 48})]

messages = [{"role": "user", "content": "37×48?"}]
print(run_agent(stuck_model, {"calculate": lambda a, b: a * b}, messages, max_steps=3))
print("쌓인 tool 메시지:", sum(m["role"] == "tool" for m in messages))
출력
('[걸음 한도에 닿았습니다]', 3)
쌓인 tool 메시지: 3
결과읽는 법
('[걸음 한도에 닿았습니다]', 3)세 걸음을 다 돌아도 답이 없어 실패를 돌려줬다
쌓인 tool 메시지: 3같은 계산을 세 번 했다. 한도가 없으면 끝없이 쌓인다

계산기는 몇 번 불러도 해가 없지만, 예약 도구였다면 예약이 세 개 생겼을 것입니다. 걸음 한도는 이런 되풀이를 끊는 브레이크입니다.

주의할 점

  • 걸음 한도에 닿으면 실패로 보고 사용자에게 알립니다. 조용히 마지막 글을 답으로 내보내지 않습니다.
  • 한 번의 응답에 도구 호출이 여러 개 올 수 있습니다. 목록을 돌며 모두 실행하고, 결과마다 tool 메시지를 붙입니다.
← 이전 절9장 · 도구 설명 쓰기다음 절 →9장 · ReAct