9장. 도구를 쓰는 LLM 과 Agentic Workflow
실습 2 — 도구 결과를 돌려주고 답 받기
실습 1의 도구 호출을 실제로 실행하고, 결과를 tool 메시지로 돌려줘 최종 답을 받습니다. 도구 없이 물었을 때의 답과 견줍니다.
1단계. 실습 1과 같은 준비
import ast, json, operator
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
MODEL = "qwen2.5:3b"
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"))
TOOLS = [{"type": "function", "function": {
"name": "calculate", "description": "사칙연산 식을 계산한다. 숫자 계산이 필요하면 반드시 이 도구를 쓴다.",
"parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "계산할 식. 예: 1234*5678"}},
"required": ["expression"]}}}]
question = "한 상자에 48개씩 든 사과 1,375상자가 있어요. 사과는 모두 몇 개예요?"
print(question, "· 정답:", 48 * 1375)한 상자에 48개씩 든 사과 1,375상자가 있어요. 사과는 모두 몇 개예요? · 정답: 660002단계. 도구 호출 받고 실행하기
messages = [{"role": "user", "content": question}]
msg = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS, temperature=0, seed=42).choices[0].message
# 모델의 응답(도구 호출 요청)을 그대로 대화에 덧붙인다
messages.append({"role": "assistant", "content": msg.content or "", "tool_calls": [tc.model_dump() for tc in msg.tool_calls]})
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments) # 인자는 JSON 글자로 온다
result = safe_calc(args["expression"]) # 우리 코드가 실행한다
print("실행:", tc.function.name, args, "→", result)
messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps({"결과": result})})
print("쌓인 메시지:", [m["role"] for m in messages])실행: calculate {'expression': '48*1375'} → 66000
쌓인 메시지: ['user', 'assistant', 'tool']3단계. 최종 답 받기와 도구 없이 견주기
final = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS, temperature=0, seed=42).choices[0].message
print("도구를 쓴 답:", final.content)
# 같은 질문을 도구 없이: 모델이 머릿속으로 계산한다
plain = client.chat.completions.create(model=MODEL, messages=[{"role": "user", "content": question}], temperature=0, seed=42)
print("도구 없는 답:", plain.choices[0].message.content.replace("\n", " ")[:120])도구를 쓴 답: 한 상자에 48개씩 들어있는 사과가 1,375개의 상자라면, 총 사과는 66,000개입니다.
도구 없는 답: 한 상자에 48개의 사과가 들어있는데, 1,375 상자가 있습니다. 이 사과의 총 개수는 다음과 같이 계산할 수 있습니다: 1 상자당 사과 개수 x 상자 수 = 총 사과 개수 48개 x 1,375상자 = 65,00이 실습이 뜻하는 것
이 실습은 비서가 내민 쪽지를 받아 우리가 계산기를 두드리고, 결과를 다시 건네는 일과 같습니다. 문제와 정답을 미리 적어 둡니다(1단계). 비서가 "48*1375 를 계산해 달라"는 쪽지를 주면 계산기로 66000 을 얻고, 대화 기록에 질문, 쪽지, 결과를 차례로 붙입니다(2단계). 그 기록을 다시 보여 주면 비서가 결과를 문장으로 다듬어 답합니다. 같은 문제를 계산기 없이 암산하게도 해 봅니다(3단계). 여기서 대화 기록이 messages, 결과를 건네는 쪽지가 tool 메시지, 어느 쪽지에 대한 답인지 알려 주는 번호표가 tool_call_id 입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 정답 | 66000 | 48 × 1375 를 파이썬이 계산한 값입니다. 모델의 답을 견줄 기준입니다 |
| 2. 실행 | calculate {'expression': '48*1375'} → 66000 | 모델이 식을 바르게 세웠고, 계산은 우리 코드의 safe_calc 가 했습니다 |
| 2. 쌓인 메시지 | ['user', 'assistant', 'tool'] | 질문, 모델의 도구 요청, 도구 결과가 차례로 쌓였습니다. 이 셋이 있어야 모델이 앞뒤 사정을 압니다 |
| 3. 도구를 쓴 답 | "총 사과는 66,000개입니다" | 정답과 같습니다. 계산은 코드가, 문장은 모델이 맡았습니다 |
| 3. 도구 없는 답 | "48개 x 1,375상자 = 65,00…" | 식은 맞게 세웠지만 암산 결과가 정답과 다릅니다. 출력이 120글자에서 잘려 끝자리는 보이지 않습니다 |
한 줄로 줄이면, 자릿수가 많은 계산은 모델이 틀리기 쉽고, 계산을 도구에 맡기면 바로잡힌다는 것을 한 문제로 보인 실습입니다. 한 문제의 결과라 "도구 없이는 늘 틀린다"는 뜻은 아닙니다. 다음 실습 3에서는 도구를 여러 개 주고 모델이 고르게 합니다.
코드 설명
- 모델은
48*1375를 요청했고,safe_calc가 66000 을 계산했습니다. 대화에는 user, assistant(도구 호출), tool(결과) 세 메시지가 쌓였습니다. - 모델의 응답(
msg)을tool_calls와 함께 대화에 그대로 덧붙여야, 다음 요청에서 모델이 "내가 계산기를 불렀고 이것이 결과다"를 압니다.tc.model_dump()는 응답 객체를 사전으로 바꿉니다. - 결과를 받은 모델은 "총 사과는 66,000개입니다"라고 답했습니다. 계산은 코드가, 문장은 모델이 맡았습니다.
- 도구 없이 물으면 모델이 식은 바르게 세우지만 "48개 x 1,375상자 = 65,00…"처럼 머릿속 계산에서 정답(66,000)과 다른 값을 냅니다. 언어 모델은 다음 토큰을 고르는 방식이라 자릿수가 많은 계산에 약합니다. 계산을 도구에 맡기는 까닭입니다.