9장. 도구를 쓰는 LLM 과 Agentic Workflow
자율 루프
레시피를 보며 요리할 때는 한 단계를 하고, 맛을 보고, 다음 단계를 정합니다. 간이 맞으면 멈춥니다. 자율 루프도 모델이 도구를 부르면 실행해 결과를 보여 주고, 모델이 더 이상 도구를 부르지 않으면 멈춥니다.
1번 메시지와 도구 목록을 모델에 보냅니다. 2번 응답에 도구 호출이 있으면 3번에서 실행해 결과를 메시지에 덧붙이고 1번으로 돌아갑니다. 도구 호출이 없으면 4번에서 그 글을 최종 답으로 돌려주고 끝냅니다. 한 바퀴가 한 걸음이고, 걸음 수에 한도를 둡니다. 여기서 맛보기가 도구 결과를 보는 일입니다.
루프의 뼈대
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_impl | implementation(구현)의 줄임말이다. 도구 이름과 실제 함수를 짝지은 사전이다 |
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 으로 둡니다.
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 메시지를 붙입니다.