Assistant turn (tool_calls 포함) — 모델이 "이거 답하려면 get_weather(Tokyo) 호출 필요" 라고 함.
Tool turn — 네 코드가 함수를 실행하고 conversation에 {"role": "tool", "content": "..."}를 붙여.
Assistant turn (final) — 모델이 tool 결과 가지고 답.
Single round 아니라 loop
모델이 tool 결과 본 후 또 tool 호출할 수 있어. Loop은 이래. messages를 보내고 → 응답에 tool_calls가 있으면 실행해서 append하고 → 없으면 그 답을 반환해. 정해진 max는 없지만 실전에선 5–10 iteration이면 거의 다 덮어. 폭주를 막으려면 cap을 꼭 걸어두고.
Replace 아니라 Append
Conversation은 message history 전체야. 매 iteration마다 붙여 나가. assistant의 tool_calls turn, 그다음 tool 결과 turn 순서로. 다음 turn에서 모델이 제대로 reasoning하려면 history가 통째로 있어야 하거든. Assistant turn을 갈아치우면 모델이 세운 plan이 통째로 날아가.
Code
Production-grade tool loop·python
import httpx, json, glob
OLLAMA = "http://localhost:11434/api/chat"
def get_weather(city: str, unit: str = "celsius") -> str:
# Stub — 실제 API 호출로 교체
return json.dumps({"city": city, "temp": 22, "unit": unit, "condition": "sunny"})
def search_files(pattern: str, directory: str = ".") -> str:
matches = glob.glob(f"{directory}/{pattern}")
return json.dumps({"files": matches[:10], "count": len(matches)})
REGISTRY = {"get_weather": get_weather, "search_files": search_files}
def chat_with_tools(model: str, user_message: str, tools: list,
max_iters: int = 8) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(max_iters):
resp = httpx.post(OLLAMA, json={
"model": model, "messages": messages,
"tools": tools, "stream": False,
}, timeout=120.0).json()
msg = resp["message"]
messages.append(msg)
if not msg.get("tool_calls"):
return msg["content"]
for call in msg["tool_calls"]:
name = call["function"]["name"]
args = call["function"]["arguments"]
try:
result = REGISTRY[name](**args) if name in REGISTRY \
else json.dumps({"error": f"Unknown tool: {name}"})
except Exception as e:
result = json.dumps({"error": str(e)})
messages.append({"role": "tool", "content": result})
return f"<<max iterations ({max_iters}) reached>>"
# 사용 — 두 tool 순차 필요
print(chat_with_tools(
"qwen2.5:7b",
"Find all .py files under '.' and tell me the weather in Seoul.",
tools=[
{"type": "function", "function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}},
"required": ["city"]}}},
{"type": "function", "function": {
"name": "search_files",
"description": "Search files by glob pattern under a directory.",
"parameters": {"type": "object",
"properties": {"pattern": {"type": "string"},
"directory": {"type": "string"}},
"required": ["pattern"]}}},
],
))