Local LLM 구축하기 · 21/30 · 에이전트와 연결
도구 하나를 직접 연결해 보면 에이전트가 어디까지 결정하고 어디부터 프로그램이 맡는지 보입니다. 여기서는 가상의 장비 재고를 조회하게 해 볼게요. 파일 수정이나 셸 실행 없이 요청, 인수 검사, 결과 반환의 흐름을 확인합니다.
13편의 공용 함수와 도구 호출을 지원하는 모델을 준비합니다. 조회할 데이터는 코드 안의 가상 장비 목록으로 제한합니다.
요청·검사·실행의 흐름
모델은 사용할 도구의 이름과 인수를 요청합니다. 이를 실제로 실행할지는 애플리케이션이 판단합니다. 결과를 tool 메시지로 돌려주면 모델이 읽고 답변을 이어 가요. 이 요청 → 검사 → 실행 → 결과 → 답변이 반복되는 구조입니다.
스키마에 enum을 넣었더라도 프로그램의 인수 검사는 필요합니다. 모델 출력도 외부 입력으로 취급합니다. 샘플은 허용된 딕셔너리 조회만 수행하고 eval, 임의 import, 셸 명령은 실행하지 않습니다.
허용된 재고 조회 함수
공식 도구 호출 예제에서 사용하는 Qwen3 계열의 8B를 받습니다. 메모리가 부족하면 더 작은 도구 지원 모델을 별도로 시험합니다. 모델이 도구를 부르지 않고 재고를 추측해 답하는지도 함께 살펴보세요.
ollama pull qwen3:8b
import json
from llm_http import post
inventory = {"lab-gpu": 2, "lab-cpu": 5}
tools = [{"type": "function", "function": {
"name": "get_stock", "description": "가상 실습 장비의 재고 수량 조회",
"parameters": {"type": "object", "properties": {
"product": {"type": "string", "enum": list(inventory)}},
"required": ["product"], "additionalProperties": False}
}}]
messages = [{"role": "user", "content": "lab-gpu의 재고를 도구로 조회해서 알려 줘."}]
used = 0
for turn in range(3):
response = post("/api/chat", {
"model": "qwen3:8b", "messages": messages, "tools": tools,
"think": False, "stream": False,
"options": {"num_ctx": 4096, "num_predict": 256, "temperature": 0}
})
msg = response["message"]
messages.append(msg)
calls = msg.get("tool_calls") or []
if not calls:
print(msg.get("content", ""))
break
for call in calls:
used += 1
if used > 4:
raise RuntimeError("Tool call limit exceeded")
fn = call["function"]
args = fn.get("arguments", {})
if isinstance(args, str):
args = json.loads(args)
if fn["name"] != "get_stock" or not isinstance(args, dict):
raise ValueError("Unsupported tool request")
if set(args) != {"product"} or not isinstance(args["product"], str):
raise ValueError("Unexpected arguments")
if args["product"] not in inventory:
raise ValueError("Unknown product")
result = {"stock": inventory[args["product"]]}
print("도구 실행:", args["product"], result)
messages.append({"role": "tool", "tool_name": "get_stock",
"content": json.dumps(result)})
else:
raise RuntimeError("No final answer within 3 turns")
.\.venv\Scripts\python.exe .\tool_loop.py
실행 로그와 최종 답변
도구 실행 로그와 stock=2 반환값을 확인합니다. 마지막 문장에 2가 나와도 조회 로그가 없다면 실제 데이터를 읽었는지 알 수 없습니다. 반대로 조회 결과는 2인데 답변에서 다른 수를 쓰면 생성 단계의 문제입니다. 두 결과를 나눠 기록합니다.
잘못된 이름, 빠진 인수, 허용하지 않은 제품명은 예외로 멈춥니다. 운영 서비스는 이 오류를 사용자에게 이해할 수 있는 상태로 바꾸고 원문 전체를 로그에 남기지 않는 방식으로 관찰성을 추가합니다.
쓰기 도구와 승인 단계
조회가 됐다고 바로 파일 수정이나 메일 전송을 연결하지 않습니다. 외부에 영향을 주는 도구는 대상과 변경 내용을 먼저 보여 주고 승인을 받도록 설계합니다. 재시도 시 중복 실행을 막을 식별자와 멱등성도 필요합니다.
도구가 늘면 정의가 차지하는 문맥과 선택할 범위도 커집니다. 작업에 필요한 도구만 보여 주고 턴 수, 실행 횟수, 전체 시간을 제한해 두세요. 프레임워크를 사용하더라도 이 경계를 확인하는 과정은 같습니다.
참고 자료
문서·모델 태그 확인 기준: 2026년 10월 18일. 기본 실습 환경: Windows 11 x64.




답글 남기기