올라마(Ollama) - 개념 및 실습

2026. 6. 6. 14:57AI/LLM

1. 로컬 LLM (Local LLM)

 

현재 상용화된 LLM인 ChatGPT, Claude, Gemini 등은 우리가 돈을 지불하고 사용을 해야 하는데

 

우리는 허깅 페이스 플랫폼에서 오픈 소스로 제공해주는 무료 모델을 사용했었다.

 

이제 이 무료 모델들을 로컬에서 쓰고 싶다는 생각에서 유래했다.

 

앞에서 이야기 했던 LLM 아키텍처를 살펴보자.

 

1) 트랜스포머 핵심 아키텍쳐

 

2) LLM 추론 파이프라인

 

 대부분의 현업의 상용화된 LLM은 이런 과정을 거친다.

  • 입력 텍스트는 컴퓨터가 이해할 수 있게 토큰 단위로 나누는 토크나이저(토큰화) 단계를 거친다.
  • 토큰화된 리스트를 차원을 늘려 임베딩 과정을 거친다 (벡터화)
  • 임베딩 과정까지는 글자를 나누는 것이 의미있게 가져오는 과정은 아니다.
  • 앞 뒤 문맥(Context) 참고하여 의미 파악한다. -> 셀프 어텐션(Self Attention)
  • 문맥 인코딩을 여러번 반복한다.
  • 이제 의미를 알아냈으니까 어떤 의미를 가지고 있는지 서로 확률 분포 확인
  • 최종적으로 우리가 원하는 텍스트를 출력

3) 올라마(Ollama) 아키텍쳐

  • 올라마(Ollama)는 llama.cpp를 기반으로 하는 로컬 LLM 서버
  • GPU와 CPU 기반 엔진 제공
  • RestAPI 서버 제공, OpenAI 호환 API 제공

4) 올라마(Ollama) 설치 방법

 

https://ollama.com/

 

Ollama

Ollama is the easiest way to automate your work using open models, while keeping your data safe.

ollama.com

여기서 Downloads 버튼을 통해 설치하면 된다.

 

설치되었으면 아래와 같은 화면이 뜰 것이다.

 

CLI 명령어로 다음과 같이 모델을 다운로드 받을 수 있다.

 

5) 양자화(Quantization)

 

모델 정보를 32비트로 표현하면 공간도 많이 사용하고,

 

정밀하기 때문에 느리기도 하는데, 로컬 컴퓨터의 환경 조차도 좋지 않을 수 있다.

 

그래서 정밀도 부분에서 손실을 보더라도 가중치를 조금 낮추어 추론 속도를 높이는 핵심 기술

 

6) 정밀도별 특성 비교

 

7) GGUF(GPT-Generated Unified Format)

  • 모델을 어떤 포맷으로 저장할 지에 관한 것 (허깅 페이스에서도 다운로드 가능)
  • Apple Slicon , CUDA 가속 가능
  • CPU Only 실행 파일 지원 (GPU 없어도 동작)

8) 오픈 소스 모델 비교

2. 올라마(Ollama) 샘플 코드

 

chat 이라는 공통 인터페이스를 사용하여 질의한다.

 

1) 간단한 자기소개

from ollama import chat

response = chat(
    model='qwen2.5',
    messages = [
        {
            'role': 'user',
            'content': '안녕하세요! 자기소개 해줘!'
        }
    ]
)

print(response.message.content)

 

 

2) 어디까지 학습되었는지 샘플 코드

 

from ollama import chat

response = chat(
    model='qwen2.5',
    messages = [
        {
            'role': 'user',
            'content': '대한민국 대통령은 누구야?'
        }
    ]
)

print(response.message.content)

 

현재 답변하는 시점까지 학습이 되었으므로,

 

이재명 대통령이 아닌 윤석열 대통령으로 답변해줌을 확인할 수 있다.

 

모델이 어느 시점까지 학습이 되었는지도 중요하다고 보인다.

 

3) DeepSeek R1 모델 : 추론에 강하다

 

CoT(Chain Of Thought)으로써 단계별 사고적 흐름으로 추론하는 것에 특화되어 있는 모델이다.

from ollama import chat

response = chat(
    model='deepseek-r1:1.5b',
    messages = [
        {
            'role': 'user',
            'content': '파이썬으로 피보나치 수열 구현해줘.'
        }
    ]
)

print(response.message.content)

 

4) 산수문제 테스트 : 어떤 모델이 더 특화되어 있는지

 

버젼에 따라 답변이 다르긴 하지만, R1 모델이 수학적으로 추론해서 접근하는 것에 더 특화되어 있다.

models = [
    'qwen2.5',
    'deepseek-r1:1.5b'
]

for model in models:
    response = chat(
        model = model,
        messages = [
            {
                'role': 'user',
                'content': '사과 3개가 있다. 2개를 친구에게 주고 1개를 다시 받았다. 현재 몇 개인가?'
            }
        ]
    )
    print(model)
    print(response.message.content)
    print("\n")

 

5) 퓨샷 메시지 예제

messages = [
    # 시스템 프롬프트
    {
        "role": "system",
        "content": "너는 문장의 감정을 긍정, 부정, 중립 중 하나로 분리하는 AI야"
    },
    # 퓨샷
    {
        "role": "user",
        "content": "정말 만족스러운 서비스였어요"
    },
    {
        "role": "assistant",
        "content": "긍정"
    },
    {
        "role": "user",
        "content": "다시는 이용 안할 것 같아요."
    },
    {
        "role": "assistant",
        "content": "부정"
    },
    # 사용자 프롬프트
    {
        "role": "user",
        "content": "배송이 너무 늦었어요"
    }
]

response = chat(
    model='qwen2.5',
    messages=messages
)

print(response.message.content)

 

6) 스트림 효과 예제

from ollama import chat

response = chat(
    model='qwen2.5',
    messages = [
        {
            'role': 'user',
            'content': '안녕하세요! 자기소개 해줘!'
        }
    ],
    stream=True
)

for chunk in response:
    content = chunk['message']['content']
    print(content, end="", flush=True)

 

3. 구조화된 출력

 

주로 파이썬에서는 딕셔너리 구조로, 자바스크립트에서는 오브젝트 구조로 받기 위해

 

JSON으로 받아주는 것이 편한데, 그 방법으로는 여러가지가 있다.

 

1) 사용자 프롬프트에 지정

prompt = """
    다음 문장의 감정을 분석하라.
    JSON으로만 반환하시오.

    {
        "sentiment" : "",
        "score" : 0
    }

    문장: 오늘 정말 행복한 하루였다.
"""

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)

print(response.message.content)

prompt = """
    다음 뉴스 기사를 분석하시오.
    JSON으로만 반환하시오.

    {
        'summary': '',
        'sentiment': '',
        'keywords': []
    }

    기사 내용 ----------
    스위스 "19일 뷔르겐슈토크서 종전 MOU 서명식"(종합)

    (베를린=연합뉴스) 김계연 특파원 = 미국과 이란의 종전 양해각서(MOU) 서명식이 오는 19일(현지시간) 스위스 휴양지 뷔르겐슈토크에서 열린다고 스위스 정부가 16일 밝혔다.

    서명식은 당초 유엔 사무국 등 국제기구가 모여 있는 스위스 제네바에서 열린다고 알려졌었다. 스위스 외무부는 미국과 이란, 중재국 파키스탄과 카타르가 뷔르겐슈토크를 서명식 장소로 제안했다면서 이들 4개국과 소통하며 외교적 여건을 조성하고 있다고 말했다.

    스위스 중부 니드발덴주에 있는 뷔르겐슈토크는 루체른 호수가 내려다보이는 알프스 산악지대 휴양지다. 100여개국 대표단이 참석한 2024년 우크라이나 평화회의 등 국제행사가 종종 열린다.

    스위스 외무부는 현재로서는 서명식 절차와 세부 사항을 알릴 수 없다고 덧붙였다. 미국과 이란은 이미 지난 14일 종전 MOU에 전자 서명했다. 양측은 협상 대표인 JD 밴스 미국 부통령과 모하마드 바게르 갈리바프 이란의회 의장 등이 참석하는 MOU 공식 서명식에 이어 후속 실무협상을 할 예정이다.

    서명식 장소를 바꾼 데는 제네바와 달리 일반인 접근이 어려워 보안·경호에 유리한 점도 작용한 것으로 보인다. 제네바는 인근 프랑스 에비앙레뱅에서 17일 끝나는 주요 7개국(G7) 정상회의 반대 시위대가 세계 곳곳에서 몰려들어 몸살을 앓고 있다.

    서명식이 열리는 뷔르겐슈토크 리조트는 중재국 카타르 국부펀드인 카타르투자청(QIA) 자회사 카타라호스피탤러티가 소유하고 있다. 스위스 일간 노이에취르허차이퉁(NZZ)은 카타르가 서명식 주최 역할을 할 것으로 내다봤다.


"""

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)

print(response.message.content)

 

2) JSON Schema를 넘기는 방식

schema = {
    "type": "object",
    "properties" : {
        "sentiment": {
            "type": "string",
            "description": "sentiment"
        },
        "score": {
            "type": "integer",
            "description": "score"
        }
    }
}

prompt = """
    다음 문장의 감정을 분석하라.
    
    단, format으로 주어진 구조로 출력하시오.

    문장: 오늘 정말 행복한 하루였다.
"""

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ],
    format=schema
)

print(response.message.content)

 

schema = {
    "type": "object",
    "properties": {
        "summary": {
            "type": "string",
            "description": "news writer summary"
        },
        "sentiment": {
            "type": "string",
            "enum": ["positive","negative","neutral"]
        },
        "keywords": {
            "type": "array"
        }
    },
    "required": [
        "summary",
        "sentiment",
        "keywords"
    ]
}

prompt = """
    다음 뉴스 기사를 분석하시오.
    단, format으로 주어진 구조로 반환하시오.

    기사 내용 ----------
    스위스 "19일 뷔르겐슈토크서 종전 MOU 서명식"(종합)

    (베를린=연합뉴스) 김계연 특파원 = 미국과 이란의 종전 양해각서(MOU) 서명식이 오는 19일(현지시간) 스위스 휴양지 뷔르겐슈토크에서 열린다고 스위스 정부가 16일 밝혔다.

    서명식은 당초 유엔 사무국 등 국제기구가 모여 있는 스위스 제네바에서 열린다고 알려졌었다. 스위스 외무부는 미국과 이란, 중재국 파키스탄과 카타르가 뷔르겐슈토크를 서명식 장소로 제안했다면서 이들 4개국과 소통하며 외교적 여건을 조성하고 있다고 말했다.

    스위스 중부 니드발덴주에 있는 뷔르겐슈토크는 루체른 호수가 내려다보이는 알프스 산악지대 휴양지다. 100여개국 대표단이 참석한 2024년 우크라이나 평화회의 등 국제행사가 종종 열린다.

    스위스 외무부는 현재로서는 서명식 절차와 세부 사항을 알릴 수 없다고 덧붙였다. 미국과 이란은 이미 지난 14일 종전 MOU에 전자 서명했다. 양측은 협상 대표인 JD 밴스 미국 부통령과 모하마드 바게르 갈리바프 이란의회 의장 등이 참석하는 MOU 공식 서명식에 이어 후속 실무협상을 할 예정이다.

    서명식 장소를 바꾼 데는 제네바와 달리 일반인 접근이 어려워 보안·경호에 유리한 점도 작용한 것으로 보인다. 제네바는 인근 프랑스 에비앙레뱅에서 17일 끝나는 주요 7개국(G7) 정상회의 반대 시위대가 세계 곳곳에서 몰려들어 몸살을 앓고 있다.

    서명식이 열리는 뷔르겐슈토크 리조트는 중재국 카타르 국부펀드인 카타르투자청(QIA) 자회사 카타라호스피탤러티가 소유하고 있다. 스위스 일간 노이에취르허차이퉁(NZZ)은 카타르가 서명식 주최 역할을 할 것으로 내다봤다.


"""

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ],
    format=schema
)

print(response.message.content)

 

3) pydantic

from pydantic import BaseModel, Field

class SentimentResult(BaseModel):
    sentiment: str
    score: float = Field(ge=0.0, le=1.0) #범위지정
    keywords: list[str]

prompt = '''
다음 문장의 감정을 분석하라.

문장 : 오늘 정말 행복한 하루였다.
'''

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ],
    format=SentimentResult.model_json_schema()
)

result = SentimentResult.model_validate_json(response.message.content)

print(result.sentiment)
print(result.score)
print(result.keywords)

 

from typing import Literal

class NewsWriteResult(BaseModel):
    summary: str
    sentiment: Literal['positive','neutral','negative']
    keywords: list[str]

prompt = """
    다음 뉴스 기사를 분석하시오.

    기사 내용 ----------
    스위스 "19일 뷔르겐슈토크서 종전 MOU 서명식"(종합)

    (베를린=연합뉴스) 김계연 특파원 = 미국과 이란의 종전 양해각서(MOU) 서명식이 오는 19일(현지시간) 스위스 휴양지 뷔르겐슈토크에서 열린다고 스위스 정부가 16일 밝혔다.

    서명식은 당초 유엔 사무국 등 국제기구가 모여 있는 스위스 제네바에서 열린다고 알려졌었다. 스위스 외무부는 미국과 이란, 중재국 파키스탄과 카타르가 뷔르겐슈토크를 서명식 장소로 제안했다면서 이들 4개국과 소통하며 외교적 여건을 조성하고 있다고 말했다.

    스위스 중부 니드발덴주에 있는 뷔르겐슈토크는 루체른 호수가 내려다보이는 알프스 산악지대 휴양지다. 100여개국 대표단이 참석한 2024년 우크라이나 평화회의 등 국제행사가 종종 열린다.

    스위스 외무부는 현재로서는 서명식 절차와 세부 사항을 알릴 수 없다고 덧붙였다. 미국과 이란은 이미 지난 14일 종전 MOU에 전자 서명했다. 양측은 협상 대표인 JD 밴스 미국 부통령과 모하마드 바게르 갈리바프 이란의회 의장 등이 참석하는 MOU 공식 서명식에 이어 후속 실무협상을 할 예정이다.

    서명식 장소를 바꾼 데는 제네바와 달리 일반인 접근이 어려워 보안·경호에 유리한 점도 작용한 것으로 보인다. 제네바는 인근 프랑스 에비앙레뱅에서 17일 끝나는 주요 7개국(G7) 정상회의 반대 시위대가 세계 곳곳에서 몰려들어 몸살을 앓고 있다.

    서명식이 열리는 뷔르겐슈토크 리조트는 중재국 카타르 국부펀드인 카타르투자청(QIA) 자회사 카타라호스피탤러티가 소유하고 있다. 스위스 일간 노이에취르허차이퉁(NZZ)은 카타르가 서명식 주최 역할을 할 것으로 내다봤다.


"""

response = chat(
    model='qwen2.5',
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ],
    format=NewsWriteResult.model_json_schema()
)

result = NewsWriteResult.model_validate_json(response.message.content)

print(result.summary)
print(result.sentiment)
print(result.keywords)

 

 

4. 툴 콜링 (Tool Calling)

 

오픈 소스 모델을 가지고 날씨에 대해 물어보면 모델은 대답을 해주지 못한다.

 

그래서 날씨를 알려주는 API에게 가서 받은 날씨를 가져오라고 해야 한다.

 

1) 1단계 : 함수 정의

def calculator(a, b):
    return a + b

 

2) 2단계 : 함수를 적용할 Tools 지정

tools = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "두 숫자를 더한다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "a" : {
                        "type": "integer",
                        "description": "첫번째 숫자"
                    },
                    "b" : {
                        "type": "integer",
                        "description": "두번째 숫자"
                    }
                },
                "required": ["a","b"]
            }
        }
    }
]

 

3) 3단계 : 사용자의 메시지와 메세지 파악한 LLM이 필요하다면 호출할 Tools를 넘겨줌.

 

아직 계산은 안들어가고 요청을 받아주는 상태

response = chat(
    model='qwen2.5',
    messages=[{"role": "user", "content": "25와 36을 더해줘."}],
    tools=tools
)

print(response.message)

 

4) 4단계: Tool을 실행해달라고 요청

tool_call = response.message.tool_calls[0]
args = tool_call.function.arguments

result = calculator(args["a"], args["b"])
print(result)

messages = [
    {
        "role": "user",
        "content": "25와 6을 더해줘."
    },
    response.message,
    {
        "role": "tool",
        "content": str(result)
    }
]

final_response = chat(
    model='qwen2.5',
    messages=messages
)

print(final_response.message.content)

 

5) 날씨 함수 예시

import json

# 함수 정의
def get_weather(city:str) -> dict:
    data = {
        "서울" : { "temp": 22, "condition": "맑음" },
        "부산" : { "temp": 25, "condition": "흐림" }
    }
    return data.get(city, { "temp": 0, "condition": "알 수 없음"})

# 툴 스키마
tools = [
    {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "특정 도시의 날씨를 조회한다.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "날씨를 조회할 도시 이름 (예 : 서울, 부산)"
                }
            },
            "required": ["city"]
        }
    }
}
]

messages = [{ "role": "user", "content": "서울 날씨 알려줘"}]

# LLM 호출 - Tool 목록 전달
response = chat(
    model='qwen2.5',
    messages=messages,
    tools=tools
)

print(response.message)
if response.message.tool_calls:
    for tool_call in response.message.tool_calls:
        name = tool_call.function.name
        args = tool_call.function.arguments

        tool_map = {"get_weather": get_weather}
        result = tool_map[name](**args)
        print(f"Tool 실행 : {name}({args}) -> {result}")

        messages.append(response.message)
        messages.append(
            {
                "role": "tool",
                "content": json.dumps(result, ensure_ascii=False)
            }
        )

final = chat(model='qwen2.5', messages=messages)
print(final.message.content)

 

6) 여러 툴 등록 및 자동 루프 처리

def get_weather(city):
    return {"temp": 12, "condition": "비" }

def need_umbrella(condition):
    return { "result" : condition in ["비","눈"] }

def calculate(expression):
    return { "result": eval(expression) }
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "도시 날씨 조회",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": { "type" : "string" }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "need_umbrella",
            "description" : "날씨 상태로 우산 필요 여부 판단",
            "parameters": {
                "type": "object",
                "properties": {
                    "condition": {
                        "type": "string"
                    }
                },
                "required": ["condition"]
            }
        }
    }
]

tool_map = {
    "get_weather": get_weather,
    "need_umbrella": need_umbrella
}

messages = [ { "role": "user", "content": "서울 날씨 보고서 우산이 필요한지 알려줘."}]

while True:
    response = chat(model='qwen2.5',messages=messages,tools=tools)

    if not response.message.tool_calls:
        print(response.message.content)
        break

    messages.append(response.message)

    for tc in response.message.tool_calls:
        name = tc.function.name
        args = tc.function.arguments
        result = tool_map[name](**args)
        print(f"[Tool] {name}({args}) -> {result}")

        messages.append(
            {
                "role": "tool",
                "content": json.dumps(result, ensure_ascii=False)
            }
        )

f = chat(model='qwen2.5', messages=messages)
print(f.message.content)

 

7) LLM 요청 시 REST 방식으로 요청

# post

import requests

response = requests.post("http://localhost:11434/api/chat", json= {
    "model":'qwen2.5',
    "messages": [
        {"role": "system", "content": "당신은 친절한 AI 어시스턴트입니다."},
        {"role": "user", "content": "파이썬의 장점을 3가지 알려주세요"}
    ],
    "stream": False
})

data = response.json()
print(data['message']['content'])

 

사용되는 API 엔드포인트들은 아래와 같다.

 

5. 임베딩(Embedding)

 

토큰 리스트를 고차원 벡터 변환 해주는 과정이다.

 

먼저 Ollama 사이트에서 임베딩 모델을 설치한다.

다음 코드를 샘플 코드로 입력한다. (몇 차원인지도 확인한다)

import ollama

response = ollama.embed(
    model='nomic-embed-text-v2-moe',
    input='The sky is blue because of Rayleigh scattering',
)
print(response.embeddings)
print(len(response.embeddings[0]))

 

임베딩 값들은 다음과 같은 경우에 이용할 수 있다.

  • RAG : 주어진 문서 안에서 질의 응답 가능한 형태
  • 분류, 군집 : 비슷한 의미의 문서끼리 그룹화한다.
  • 벡터 DB : 임베딩 값을 저장할 수 있는 데이터베이스

예를 들어, 문자들의 비슷한 의미를 따지는 코사인 유사도 코드로 알아볼 수 있다.

# 코사인 유사도

from numpy import dot
from numpy.linalg import norm

def cosine_similarity(a, b):
    return dot(a, b) / (norm(a) * norm(b))
# 두 개의 문장이 유사한가?

s1 = '파이썬은 프로그래밍 언어이다.'
s2 = 'Python은 개발에 사용되는 언어이다'

s1 = ollama.embed(model='nomic-embed-text-v2-moe', input=s1).embeddings[0]
s2 = ollama.embed(model='nomic-embed-text-v2-moe', input=s2).embeddings[0]

print(cosine_similarity(s1,s2))

 

나중에, pdf 문서를 다 쪼개고, 모두 벡터화(임베딩) 처리를 해서 질문을 했을 때,

 

질문도 벡터화 시켜, 기존의 벡터화 시킨 숫자와 유사도를 비교해서 답변을 해주는 방식으로 응용한다.

 

예시로 다음과 같이 구성해보자.

documents = [
    "파이썬은 프로그래밍 언어이다.",
    "축구는 세계적으로 인기있는 스포츠이다.",
    "인공지능은 머신러닝을 활용한다.",
    "서울은 대한민국의 수도이다."
]

doc_vectors = []

for doc in documents:
    vector = ollama.embed(model='nomic-embed-text-v2-moe', input=doc).embeddings[0]
    doc_vectors.append(vector)

query = "AI 기술이란?"
query_vector = ollama.embed(model='nomic-embed-text-v2-moe', input=query).embeddings[0]

scores = []
for doc, vec in zip(documents, doc_vectors):
    score = cosine_similarity(query_vector, vec)
    scores.append((doc, score))

scores.sort(key=lambda x:x[1], reverse=True)

for doc, score in scores:
    print(doc, score)

context = scores[0][0]

prompt = f'''
문서: {context}

질문: {query}
'''

response = chat(model='qwen2.5', messages=[{
    "role": "user",
    "content": prompt
}])

print(response.message.content)

 

6. 정해진 인터페이스

 

모델 플랫폼에 따라, 지금 인터페이스가 조금씩 다르고 있다.

 

이것을 정형화된 템플릿이 필요한데, 이것은 다음 시간에 배우게 될 LangChain 프레임워크로 이어진다.