9장. 도구를 쓰는 LLM 과 Agentic Workflow
실습 1 — 도구 하나 붙이기
Ollama 의 qwen2.5:3b 에 계산기 도구 하나를 주고, 모델이 언제 도구를 부르는지 봅니다. Ollama 와 qwen2.5:3b(6장에서 받은 것)가 있어야 합니다. 이 실습은 모델의 요청만 보고, 실행은 실습 2에서 합니다.
1단계. 계산기와 도구 설명 만들기
import ast, json, operator
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
MODEL = "qwen2.5:3b"
# 본문 "에이전트의 안전장치"의 계산기: eval 을 쓰지 않고 사칙연산만 계산한다
OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}
def safe_calc(expression):
def walk(node):
if isinstance(node, ast.Expression):
return walk(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -walk(node.operand)
raise ValueError("숫자와 + - * / 괄호만 쓸 수 있습니다")
return walk(ast.parse(expression, mode="eval"))
# 모델에게 보여 줄 도구 설명(JSON 스키마)
TOOLS = [{"type": "function", "function": {
"name": "calculate",
"description": "사칙연산 식을 계산한다. 숫자 계산이 필요하면 반드시 이 도구를 쓴다.",
"parameters": {"type": "object",
"properties": {"expression": {"type": "string", "description": "계산할 식. 예: 1234*5678"}},
"required": ["expression"]}}}]
print(safe_calc("1234*5678"))70066522단계. 도구가 필요한 질문
resp = client.chat.completions.create(model=MODEL, messages=[{"role": "user", "content": "1234 곱하기 5678 은 얼마예요?"}],
tools=TOOLS, temperature=0, seed=42)
msg = resp.choices[0].message
print("글(content):", repr(msg.content))
for tc in msg.tool_calls or []:
# 도구 이름과 인자(JSON 글자), 그리고 결과를 돌려줄 때 쓸 id
print("도구:", tc.function.name, "· 인자:", tc.function.arguments, "· id:", tc.id[:12])
print("멈춘 까닭:", resp.choices[0].finish_reason)글(content): ''
도구: calculate · 인자: {"expression":"1234*5678"} · id: call_c5er4tc
멈춘 까닭: tool_calls3단계. 도구가 필요 없는 질문
for q in ["안녕하세요, 오늘 기분 어때요?", "세탁기와 건조기의 차이를 한 문장으로 알려 주세요.", "387 더하기 1,250 에서 99 를 빼면?"]:
msg = client.chat.completions.create(model=MODEL, messages=[{"role": "user", "content": q}],
tools=TOOLS, temperature=0, seed=42).choices[0].message
calls = [(tc.function.name, tc.function.arguments) for tc in msg.tool_calls or []]
print(q, "→", calls if calls else "도구 없이 답: " + (msg.content or "")[:40].replace("\n", " "))안녕하세요, 오늘 기분 어때요? → 도구 없이 답: 안녕하세요! 저는 인공지능으로, 기분을 느끼는 것은 아니지만, 도와드릴
세탁기와 건조기의 차이를 한 문장으로 알려 주세요. → 도구 없이 답: 세탁기는衣物를 청결하게 하며, 건조기는 청결한衣物를 건조시키는 기계입니다
387 더하기 1,250 에서 99 를 빼면? → [('calculate', '{"expression":"387 + 1250 - 99"}')]이 실습이 뜻하는 것
이 실습은 계산기를 옆에 둔 비서에게 일을 맡기는 장면과 같습니다. 먼저 계산기를 만들고, "사칙연산 식을 넣으면 답을 준다"는 사용 설명서를 써 둡니다(1단계). 비서에게 곱셈을 물으면 직접 답하지 않고 "계산기에 1234*5678 을 넣어 주세요"라는 쪽지를 내밉니다(2단계). 인사나 상식 질문에는 계산기 없이 바로 답하고, 덧셈과 뺄셈이 섞인 질문에는 다시 쪽지를 씁니다(3단계). 여기서 비서가 qwen2.5:3b, 계산기가 safe_calc, 사용 설명서가 TOOLS 의 도구 설명(JSON 스키마, JSON 은 JavaScript Object Notation), 쪽지가 tool_calls 입니다. 비유와 다른 점은, 이 실습의 비서는 쪽지만 내밀 뿐 계산기를 직접 누르지 못한다는 것입니다. 계산기를 눌러 결과를 돌려주는 일은 실습 2에서 우리 코드가 합니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 계산기 확인 | 7006652 | safe_calc("1234*5678") 가 바르게 계산합니다. 모델을 부르기 전에 도구부터 제대로 도는지 본 것입니다 |
| 2. 글(content) | '' | 곱셈 질문에 모델은 글로 된 답을 한 글자도 쓰지 않았습니다 |
| 2. 도구 | calculate, 인자 {"expression":"1234*5678"} | 대신 "계산기에 이 식을 넣어 달라"는 요청을 보냈습니다. 인자는 JSON 꼴의 글자입니다 |
| 2. 멈춘 까닭 | tool_calls | 답을 다 써서가 아니라, 도구를 불러 달라고 해서 멈췄다는 표시입니다 |
| 3. 인사 | 도구 없이 답 | 계산이 필요 없는 말에는 도구를 부르지 않았습니다 |
| 3. 세탁기와 건조기 | 도구 없이 답, "衣物" 섞임 | 이것도 도구 없이 답했습니다. 작은 Qwen 모델은 한국어 답에 중국어를 섞기도 합니다 |
| 3. 덧셈과 뺄셈 | calculate, 387 + 1250 - 99 | 말로 된 계산을 식으로 옮기고, "1,250"의 쉼표도 지워 적었습니다 |
한 줄로 줄이면, 도구 설명만 주면 모델이 "계산이 필요한 때"를 스스로 가려 도구를 부른다는 것을 보인 실습입니다. 아직 계산기를 실제로 돌리지 않았으므로 최종 답은 없습니다. 결과를 돌려주고 답을 받는 일은 실습 2에서 합니다. 질문 네 개로 본 것이라, 모델이 늘 알맞게 고른다는 뜻은 아닙니다.
코드 설명
tools=TOOLS를 주면 모델은 글로 답하거나, 답 대신tool_calls를 돌려줍니다. 본문 "도구 호출의 흐름"의 2번입니다.- 곱셈 질문에는 글(content)이 빈 글자이고,
tool_calls에calculate와 인자{"expression":"1234*5678"}이 담겼습니다.finish_reason이tool_calls인 것은 "답을 다 써서"가 아니라 "도구를 불러 달라고 해서" 멈췄다는 뜻입니다. - 인사와 세탁기 질문에는 도구 없이 글로 답하고, "387 더하기 1,250 에서 99 를 빼면?"에는
387 + 1250 - 99로 식을 바꿔 계산기를 불렀습니다. 쉼표가 든 숫자도 식으로 옮겨 적었습니다. 세탁기 답에 "衣物" 같은 한자가 섞인 것처럼, 작은 Qwen 모델은 한국어 답에 중국어를 섞기도 합니다. - 계산기는
eval대신ast로 만든safe_calc를 씁니다. 모델이 넘긴 식을 그대로 실행하지 않기 위해서입니다.