
로컬 LLM (Ollama + Qwen2.5)으로 AI 에이전트를 직접 만드는 핸즈온
요약
Ollama와 Qwen2.5를 활용하여 프레임워크 없이 로컬 환경에서 AI 에이전트를 직접 구축하는 핸즈온 가이드입니다. 도구 호출(tool calling)과 루프 구조를 직접 구현하며 에이전트의 내부 동작 원리를 심도 있게 이해할 수 있습니다.
핵심 포인트
- Ollama와 Qwen2.5를 이용한 로컬 AI 에이전트 구축 방법 제시
- LangGraph 등 프레임워크 없이 최소 단위의 에이전트 루프 직접 구현
- LLM의 도구 호출(tool calling) 메커니즘과 데이터 흐름 파악
- Python을 활용한 실전적인 에이전트 개발 핸즈온 제공
시작하며
클라우드 API의 비용이나 정보 유출 리스크를 걱정하지 않고, AI 에이전트의 메커니즘을 직접 손으로 움직이며 이해하고 싶다——는 동기에서, 로컬 LLM (Ollama + Qwen2.5)만으로 AI 에이전트를 구축하는 1~2시간 분량의 핸즈온을 만들었습니다.
프레임워크 (LangGraph, CrewAI 등)는 편리하지만, 처음에 블랙박스 상태로 사용해 버리면 "에이전트가 무엇을 하고 있는지" 파악하기 어려워집니다. 이 핸즈온에서는 의도적으로 프레임워크를 사용하지 않고, LLM + 도구 호출 (tool calling)의 for 루프라는 최소 구성을 직접 작성함으로써, 프레임워크가 뒤에서 무엇을 대신 처리해 주고 있는지를 체감할 수 있도록 구성했습니다.
코드와 핸즈온 자료 일체는 여기 있습니다 (README는 영어이지만, 일본어판은 README.ja.md입니다).
대상 독자
- Python 경험이 있는 엔지니어
- 클라우드 API를 사용하지 않고 로컬에서만 AI 에이전트를 테스트하고 싶은 사람
- "에이전트는 결국 내부에서 무엇을 하고 있는가?"를 구현 레벨에서 이해하고 싶은 사람
환경
- Ollama (네이티브 또는 Docker)
- 모델:
qwen2.5:14b-instruct-q4_K_M(도구 호출 (tool calling) 대응) - Python 3.10+ /
ollamaPython 라이브러리
Docker 버전 Ollama에서도, 네이티브 설치에서도 동일한 절차로 진행할 수 있습니다. 필자는 Docker (ollama/ollama 이미지) 상에서 구동하고 있습니다.
docker ps --filter "name=ollama"
docker exec -it <컨테이너명> ollama list
전체 구성
소요 시간은 1~2시간을 상정하여 5단계로 나누었습니다.
| 단계 | 내용 |
|---|---|
| 0 | 셋업 및 통신 확인 |
| ... |
이하, 각각의 포인트를 간략하게 소개합니다.
단계 1: 단발성 도구 호출
Ollama의 도구 호출 (tool calling) API의 장점은 Python 함수를 그대로 tools 리스트에 전달할 수 있다는 점입니다. 타입 힌트(type hint)와 docstring이 그대로 도구의 스키마 (schema)가 됩니다.
def get_temperature(city: str) -> str:
"""지정된 도시의 현재 기온을 가져옵니다.
Args:
...
모델이 "도구를 호출하고 싶다"고 판단하면 response.message.tool_calls에 호출 내용이 담겨 반환됩니다. 여기서 실제로 함수를 실행하고, 결과를 role: "tool" 메시지로 대화 이력에 추가한 뒤, 다시 한번 chat()을 호출하면 최종 답변이 반환되는 흐름입니다.
"LLM이 도구를 선택한다 → 애플리케이션 측에서 실행한다 → 결과를 LLM에 되돌려준다"라는 AI 에이전트의 최소 단위를 여기서 생코드로 체험할 수 있습니다.
단계 2: 멀티턴 에이전트 루프
실제 태스크는 한 번의 도구 호출로 끝나지 않습니다. 그래서 while True 루프를 사용하여, 모델이 도구 호출을 멈출 때까지 반복합니다.
while True:
response = chat(model=MODEL, messages=messages, tools=[add, multiply])
messages.append(response.message)
...
이것이 모든 에이전트 프레임워크 내부에 있는 최소 구성입니다. 이 부분을 이해하면 프레임워크가 자동으로 처리해 주는 프로세스 (상태 관리, 재시도, 병렬 실행 등)의 정체가 보이기 시작합니다.
실제로 발생한 문제: 인자 (argument)의 할루시네이션 (hallucination)
이 핸즈온을 실제로 qwen2.5:14b-instruct-q4_K_M에서 구동했을 때, 흥미로운 (그리고 교재로서 가치 있는) 실패가 발생했습니다.
(11434 + 12341) * 412라는 계산을 add → multiply의 2단계로 풀게 하려 했더니, 모델이 add와 multiply를 동일한 턴에 병렬로 호출하여, add의 결과가 아직 나오지 않은 단계에서 multiply의 인자에 "__SUM_OF_PREVIOUS_STEP__"라는 플레이스홀더 (placeholder) 문자열을 넣어버린 것입니다.
이를 그대로 실행하면 어떻게 될까요? Python은 "문자열" * 412...
이를 「문자열을 412번 반복하는」 처리로 실행해 버리기 때문에, 에러조차 발생하지 않고 의미를 알 수 없는 거대한 문자열이 도구 결과로 반환되었습니다. 조용히 망가지는, 가장 까다로운 유형의 버그입니다.
대책으로서, add / multiply에 인자(argument)의 타입 검증 (type validation)을 넣었습니다.
def _validate_numbers(a, b) -> None:
for name, value in (("a", a), ("b", b)):
if isinstance(value, bool) or not isinstance(value, (int, float)):
...
나아가, 도구 실행 부분을 try/except로 감싸서, 검증 에러가 발생하더라도 크래시(crash)를 일으키지 않고 에러 메시지를 그대로 모델에게 반환하도록 했습니다.
try:
result = available_functions[tc.function.name](**tc.function.arguments)
except ValueError as e:
...
이를 통해 모델은 다음 턴(turn)에서 에러 내용을 확인하고, 올바른 숫자(23775)를 사용하여 multiply를 다시 호출하여, 최종적으로 올바른 답(9,795,300)에 도달할 수 있었습니다. "도구 호출의 인자는 신뢰하지 말고, 반드시 검증한 뒤에 실행한다" —— 실무 운영에서도 통용되는, 구현을 통해 얻은 교훈입니다.
단계 3: 로컬 파일 조사 에이전트 직접 만들기
여기가 이번 핸즈온의 메인입니다. 참가자 스스로 다음 3가지 도구를 구현하게 됩니다.
list_files(directory): 디렉토리 내의 파일 목록을 반환read_file(path): 파일의 내용을 반환 (너무 길 경우 자름)search_files(directory, keyword): 파일군에서 키워드를 포함하는 행을 검색
이를 에이전트 루프(agent loop)에 전달하면, "sample_docs/ 폴더 안에 API 키 설정 방법에 대한 메모가 있나요? 있다면 요약해 주세요"라는 질문에 대해, 모델이 자율적으로 "목록 보기 → 의심스러운 파일 검색/특정 → 내용 읽기 → 요약하기"라는 다단계 행동을 취할 수 있게 됩니다.
==== 턴 1 ====
[도구 실행] list_files({'directory': './sample_docs'})
==== 턴 2 ====
...
3턴 만에 목적 정보에 도달하였고, 내용도 원문과 일치하는 요약이 반환되었습니다. 프레임워크 없이도 도구 설계와 루프만 제대로 작성하면 실용적인 다단계 에이전트를 만들 수 있음을 확인할 수 있습니다.
또 다른 사례: 경로의 할루시네이션 (Hallucination)
질문 문장을 모호하게 둔 채 실행하면, 모델이 실재하지 않는 경로(/filepath/sample_docs와 같은 가공의 절대 경로)를 추측하여 도구에 전달하는 경우가 있었습니다. 하지만 list_files / search_files 측에서 디렉토리 존재 여부 체크를 하고 있었기 때문에, 에이전트는 크래시되지 않고 거짓 요약을 지어내지도 않으며, "경로가 다른 것 같습니다. 알려주세요"라고 솔직하게 되묻는 동작을 보였습니다.
질문 문장에 "sample_docs (현재 디렉토리 바로 아래의 상대 경로)"라고 명시하는 것만으로도 이러한 추측 실수는 상당히 줄어듭니다. 도구의 견고성(존재 여부 체크)과 프롬프트의 구체성, 이 두 가지가 모두 효과를 발휘한 좋은 사례였습니다.
단계 4: 안전장치
실무 사용을 염두에 두고 다음과 같은 안전책을 마련했습니다.
- 최대 턴 수 제한 (
MAX_TURNS): 무한 루프 및 무한 도구 호출 방지 - 알 수 없는 도구 이름 핸들링: 존재하지 않는 도구 이름이 호출되어도 크래시되지 않도록 처리
- 출력 자르기:
read_file은 컨텍스트 길이 (context length)를 압박하지 않도록 앞부분 수천 자로 제한 - 인자 검증: 단계 2에서 시연한 것처럼, 숫자여야 하는 인자에 문자열이 들어오는 상황을 상정
- 파괴적 작업 확인:
write_file과 같은 작업을 도구화할 경우, 실행 전에 Human-in-the-loop 확인 과정을 거침
14B 클래스 모델에서도 도구 호출 (Tool Calling) 판단 실수나 동일한 도구의 불필요한 반복은 흔히 발생합니다. "에이전트는 똑똑하게 행동한다"가 아니라, "에이전트는 때때로 이상한 행동을 한다는 전제하에, 고장 나더라도 안전하게 멈추도록 설계한다"라는 관점이 로컬 LLM 에이전트를 실제로 다뤄보면 강하게 실감됩니다.
요약 및 다음 단계
이번에 작성한 내용은 "LLM + 도구 호출 (Tool Calling)의 for 루프"라는, 모든 에이전트 프레임워크의 내부에 있는 최소 구성입니다. 이 부분을 이해하고 있다면, 프레임워크가 무엇을 대신 처리해 주고 있는지 보이기 시작할 것입니다.
다음 단계로는 아래 사항들을 추천합니다.
- LangGraph: 상태 전이(State Transition)·재시도(Retry)·체크포인트(Checkpoint)를 다루는 본격적인 에이전트를 구축하고 싶은 경우
- CrewAI: 역할(Role)을 가진 다중 에이전트를 협업시키고 싶은 경우
- 모델 스케일 업 (Scale-up): 더 복잡한 판단을 시키고 싶은 경우, Qwen2.5 32B나 Llama 3.3 70B 등 더 큰 모델이 계획 능력(Planning Ability)이 높음
- 하이브리드 구성: 가벼운 태스크는 로컬에서, 중요한 판단만 클라우드의 프론티어 모델(Frontier Model)에 던지는 구성도 실무에서는 일반적
코드와 핸즈온 자료 전체는 아래 리포지토리에 올려두었습니다. 개인의 Ollama 환경이 있다면, 그대로 1~2시간 내에 시도해 볼 수 있는 구성입니다.
참고 링크
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기