10. 고급 기능 활용: 비전, 임베딩, 구조화 출력, 툴 호출
10. 고급 기능 활용: 비전, 임베딩, 구조화 출력, 툴 호출
앞선 장들에서 우리는 Ollama를 로컬에서 실행하고, 모델을 고르고, CLI와 API와 코드에서 호출하는 방법까지 익혔다. 여기까지 왔다면 이제 질문이 바뀐다. "대답은 잘하네. 그런데 일도 할 수 있나?"
이 장에서 다루는 네 가지 기능이 바로 그 답이다. 비전은 이미지를 읽게 해 주고, 임베딩은 텍스트를 검색 가능한 숫자 벡터로 바꿔 주며, 구조화 출력은 모델의 답을 프로그램이 먹기 좋은 JSON으로 고정해 준다. 그리고 툴 호출은 모델이 외부 함수나 API를 불러 실제 행동을 하게 만든다.1345
이 네 가지가 합쳐지면 Ollama는 단순한 채팅 상자를 넘어선다. 문서를 읽고, 검색하고, 필요한 도구를 부르고, 결과를 정리하는 로컬 AI 엔진이 된다. 쉽게 말해, 말만 잘하는 친구에서 일을 맡길 수 있는 동료로 진화하는 셈이다.

왜 이 기능들이 중요한가
일반 채팅만으로도 꽤 많은 일을 할 수 있다. 하지만 실제 제품이나 자동화에서는 곧 한계가 드러난다. 예를 들어 스크린샷에서 글자를 읽고 싶을 때, 사내 문서를 의미 기반으로 검색하고 싶을 때, 응답을 JSON으로 받아 DB에 넣고 싶을 때, 현재 날씨나 사내 API 데이터를 끌어와 답하게 하고 싶을 때는 그냥 텍스트 생성만으로는 부족하다.
고급 기능은 이 부족한 부분을 메운다.
비전은 이미지, 스캔 문서, 차트, 스크린샷을 다룬다.
임베딩은 검색, 추천, RAG의 기초가 된다.
구조화 출력은 자동화 파이프라인을 안정적으로 만든다.
툴 호출은 에이전트형 작업의 시작점이 된다.
즉, 이 장은 "모델을 더 똑똑하게 쓰는 법"이 아니라 "모델을 시스템 안에서 일하게 만드는 법"에 가깝다.
비전: 텍스트만 읽던 모델에 눈을 달아주기
Ollama의 비전 모델은 텍스트와 함께 이미지를 입력받을 수 있다. 덕분에 이미지 설명, 분류, 차트 해석, 스크린샷 OCR 비슷한 작업, 영수증이나 문서 추출 같은 일이 가능해진다.1
가장 빠른 예시는 이런 식이다.
ollama run gemma3 ./image.png "이 이미지에 무엇이 보이는지 짧게 설명해줘."공식 API에서는 messages 안에 images 배열을 넣는다. REST API에서는 이미지를 base64로 보내고, Python·JavaScript SDK에서는 파일 경로나 바이트를 직접 넘길 수 있다.1
IMG=$(base64 < receipt.jpg | tr -d 'n')
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "gemma3",
"messages": [
{
"role": "user",
"content": "이 영수증에서 품목과 총액을 추출해줘.",
"images": ["'"$IMG"'"]
}
],
"stream": false
}'Python에서는 훨씬 자연스럽다.
from ollama import chat
response = chat(
model='gemma3',
messages=[
{
'role': 'user',
'content': '이 스크린샷에 보이는 오류 메시지를 읽고 원인을 추정해줘.',
'images': ['./error-shot.png'],
}
],
stream=False,
)
print(response.message.content)여기서 중요한 감각은 하나다. 비전 모델에게는 "이 사진 어때?"라고 막연히 묻기보다, 어떤 관점으로 보길 원하는지 구체적으로 지시하는 편이 훨씬 낫다는 점이다. 예를 들어 아래처럼 목적을 분명히 주면 결과가 좋아진다.
"보이는 텍스트를 그대로 옮겨 적어줘."
"차트의 핵심 추세를 세 줄로 요약해줘."
"송장 번호, 날짜, 총액만 찾아 JSON으로 정리해줘."
어떤 비전 모델을 고르면 좋을까
실무 감각으로 보면 모델 선택도 꽤 중요하다. 일반적인 이미지 설명은 llava 계열이나 gemma3가 무난하고, 문서·차트·스크린샷처럼 구조가 복잡한 이미지에는 qwen2.5vl 계열이 더 강한 편으로 알려져 있다. CPU 환경에서 빠르게 돌리고 싶다면 moondream2처럼 작은 모델이 현실적인 선택이 된다.2
이 차이는 꽤 실용적이다. 여행 사진 설명은 작은 모델도 그럭저럭 한다. 하지만 회의 자료 캡처 화면에서 작은 글씨를 읽고 표 구조를 파악하는 일은 훨씬 어렵다. 이미지가 복잡할수록 모델의 시력도 좋아야 한다고 생각하면 된다.
비전에서 자주 만나는 한계
비전 모델은 만능 OCR 엔진이 아니다. 작은 글씨가 빽빽한 표, 복잡한 회로도, 행이 많은 스프레드시트에서는 누락이나 오독이 생길 수 있다. 특히 작은 7B급 모델은 복잡한 구조 이미지에서 일부를 놓치기 쉽다.2
그래서 실전에서는 두 가지 습관이 중요하다. 첫째, 가능한 선명한 원본 이미지를 쓰는 것. 둘째, 한 번에 너무 많은 일을 시키지 않는 것이다. "이 이미지 전체를 다 분석해줘"보다 "제목, 본문 요약, 표의 결론만 알려줘"가 더 안정적이다.
임베딩: 검색을 가능하게 만드는 숫자 표현
임베딩은 텍스트를 숫자 벡터로 바꾸는 기능이다. 얼핏 보면 갑자기 수학 냄새가 나서 멀게 느껴지지만, 실제 역할은 단순하다. 문장의 뜻을 숫자로 바꿔 비슷한 것끼리 가깝게 놓는 것이다.3
예를 들어 "환불 정책이 어떻게 되나요?"와 "반품 규정 알려주세요"는 글자 모양은 다르지만 뜻은 비슷하다. 임베딩은 이런 의미상의 유사성을 잡아낸다. 그래서 키워드 검색이 놓치는 문장도 찾을 수 있다.
Ollama는 최신 임베딩 API로 /api/embed를 제공하고, 하나의 문자열뿐 아니라 문자열 배열도 한 번에 처리할 수 있다.3
curl -X POST http://localhost:11434/api/embed \
-H "Content-Type: application/json" \
-d '{
"model": "embeddinggemma",
"input": "환불 규정은 구매일로부터 7일 이내 적용됩니다."
}'Python에서는 이렇게 쓴다.
import ollama
result = ollama.embed(
model='embeddinggemma',
input=[
'환불은 구매 후 7일 이내 가능합니다.',
'배송 지연 시 고객센터로 문의해 주세요.',
'비밀번호 재설정 링크는 이메일로 발송됩니다.',
],
)
vectors = result['embeddings']
print(len(vectors), len(vectors[0]))공식 문서는 embeddinggemma, qwen3-embedding, all-minilm 같은 모델을 추천하고, 벡터 길이는 모델마다 다르지만 대개 384~1024 차원 정도라고 설명한다.3
임베딩은 어디에 쓰나
임베딩의 대표 무대는 세 곳이다.
문서 검색
RAG
유사도 기반 분류나 추천
가장 흔한 흐름은 이렇다. 먼저 문서를 잘게 나눈다. 그 조각들을 임베딩으로 바꿔 벡터 DB나 배열에 저장한다. 사용자의 질문도 같은 임베딩 모델로 벡터화한다. 그다음 코사인 유사도 같은 방식으로 가장 가까운 문서 조각을 찾는다. 공식 문서도 대부분의 의미 검색에는 코사인 유사도를 권장하며, 색인과 질의에 같은 임베딩 모델을 써야 한다고 안내한다.3
즉, 검색 품질을 높이는 가장 쉬운 규칙은 이것이다.
문서를 넣을 때와 질문할 때, 같은 임베딩 모델을 써라.
아주 작은 로컬 검색 예제
벡터 DB 없이도 개념은 금방 이해할 수 있다.
import math
import ollama
texts = [
'환불은 구매일로부터 7일 이내 가능합니다.',
'배송은 결제 후 평균 2영업일이 소요됩니다.',
'이 서비스는 비밀번호 재설정 기능을 제공합니다.',
]
embed_result = ollama.embed(model='embeddinggemma', input=texts)
doc_vectors = embed_result['embeddings']
query = '반품 가능한 기간이 궁금해요.'
query_vector = ollama.embed(model='embeddinggemma', input=query)['embeddings'][0]
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
return dot / (norm_a * norm_b)
scores = [cosine_similarity(query_vector, v) for v in doc_vectors]
best_index = max(range(len(scores)), key=lambda i: scores[i])
print(texts[best_index])이 코드는 장난감 수준이지만, RAG의 핵심 아이디어를 거의 그대로 보여준다. 결국 RAG는 "질문과 가장 비슷한 문서 조각을 찾고, 그것을 모델에게 함께 보여 주는 일"이다.
구조화 출력: 말 잘하는 모델을 일 잘하는 모델로 바꾸기
모델이 아무리 똑똑해도 응답 형식이 들쭉날쭉하면 자동화는 금방 삐끗한다. 어떤 날은 JSON을 주고, 어떤 날은 설명문을 길게 붙이고, 어떤 날은 키 이름을 살짝 바꾸면 프로그램은 바로 토라진다.
이럴 때 쓰는 것이 구조화 출력이다. Ollama는 format: "json"으로 유효한 JSON 응답을 강제할 수 있고, 더 나아가 JSON Schema 자체를 format에 넣어 응답 구조를 더 엄격하게 고정할 수 있다.4
먼저 JSON 모드다.
from ollama import chat
response = chat(
model='gpt-oss',
messages=[
{'role': 'user', 'content': '이 회의 메모를 JSON으로 요약해줘. 필드: topic, decisions, action_items'}
],
format='json',
)
print(response.message.content)이 방식은 "무조건 JSON으로 내놔"에 가깝다. 빠르고 편하지만, 필드 구조를 완전히 못 박지는 못한다. 더 안정적인 방식은 스키마를 넘기는 것이다.
from ollama import chat
from pydantic import BaseModel
class MeetingSummary(BaseModel):
topic: str
decisions: list[str]
action_items: list[str]
response = chat(
model='gpt-oss',
messages=[
{
'role': 'user',
'content': '다음 메모를 읽고 회의 주제, 결정 사항, 후속 작업을 추출해줘.'
}
],
format=MeetingSummary.model_json_schema(),
)
summary = MeetingSummary.model_validate_json(response.message.content)
print(summary)이 패턴의 장점은 아주 크다. 같은 Pydantic 모델을 요청용 스키마와 응답 검증기로 동시에 쓸 수 있기 때문이다.4 다시 말해, "이런 구조로 답해"라고 말할 때도 쓰고, "정말 그렇게 왔는지 확인"할 때도 쓴다. 한 사람이 주문도 받고 검수도 하는 셈이다.
구조화 출력이 특히 빛나는 순간
구조화 출력은 다음 같은 작업에서 진가를 발휘한다.
영수증, 이메일, 문의글에서 필드 추출
분류 결과를 DB에 바로 저장
프런트엔드 컴포넌트가 바로 그릴 수 있는 JSON 생성
비전 모델의 이미지 분석 결과를 일정한 형태로 맞춤
특히 재미있는 점은 비전 모델에도 같은 format을 쓸 수 있다는 것이다. 즉, 이미지를 보고도 자유 서술문 대신 스키마에 맞는 JSON을 돌려받을 수 있다.4
from typing import Literal
from pydantic import BaseModel
from ollama import chat
class ReceiptInfo(BaseModel):
merchant: str
total: float
currency: str
category: Literal['food', 'transport', 'shopping', 'other']
response = chat(
model='gemma3',
messages=[
{
'role': 'user',
'content': '이 영수증에서 상호명, 총액, 통화, 지출 카테고리를 추출해줘.',
'images': ['./receipt.jpg'],
}
],
format=ReceiptInfo.model_json_schema(),
options={'temperature': 0},
)
info = ReceiptInfo.model_validate_json(response.message.content)
print(info)공식 문서는 구조화 출력의 신뢰도를 높이려면 스키마를 쓰고, 온도를 낮추며, 프롬프트에도 원하는 구조를 분명히 적는 편이 좋다고 안내한다.4
툴 호출: 모델이 직접 행동하지는 않지만, 행동을 요청하게 만들기
툴 호출은 이름만 들으면 모델이 혼자 함수를 실행하는 것처럼 느껴질 수 있다. 실제로는 조금 다르다. 모델은 도구를 직접 실행하지 않는다. 대신 어떤 도구를 어떤 인자로 호출해야 하는지 제안한다. 실제 실행은 당신의 코드가 맡는다.5
이 차이를 이해하면 툴 호출이 갑자기 쉬워진다. 모델은 두뇌이고, 당신의 함수는 손발이다.
기본 흐름은 두 번 왕복한다.
사용자 질문과 도구 목록을 모델에 보낸다.
모델이
tool_calls로 "이 함수를 이렇게 호출해 주세요"라고 답한다.애플리케이션이 그 함수를 실제로 실행한다.
실행 결과를
role: tool메시지로 다시 모델에 보낸다.모델이 그 결과를 바탕으로 최종 답을 만든다.5
공식 Python SDK는 함수 정의에서 타입 힌트와 독스트링을 읽어 도구 스키마로 바꿔 주는 방식도 지원한다.5
from ollama import chat
def get_temperature(city: str) -> str:
"""도시의 현재 온도를 반환한다."""
data = {
'Seoul': '18°C',
'Busan': '20°C',
'Tokyo': '17°C',
}
return data.get(city, 'Unknown')
messages = [
{'role': 'user', 'content': '서울의 현재 기온이 얼마야?'}
]
first = chat(
model='qwen3',
messages=messages,
tools=[get_temperature],
stream=False,
)
messages.append(first.message)
if first.message.tool_calls:
call = first.message.tool_calls[0]
result = get_temperature(**call.function.arguments)
messages.append({
'role': 'tool',
'tool_name': call.function.name,
'content': result,
})
final = chat(
model='qwen3',
messages=messages,
tools=[get_temperature],
stream=False,
)
print(final.message.content)이 흐름을 처음 보면 약간 번거로워 보일 수 있다. 하지만 실제로는 매우 중요하다. 왜냐하면 이 구조 덕분에 모델이 외부 세계와 안전하게 연결되기 때문이다. 모델이 마음대로 파일을 삭제하거나 결제를 눌러 버리는 것이 아니라, 당신이 허용한 함수만 요청할 수 있기 때문이다.
툴 호출에서 설계가 반이다
툴 호출의 품질은 모델보다 도구 설계에 크게 좌우된다. 함수 이름, 설명, 파라미터 설명이 모호하면 모델도 엉뚱하게 고른다. 반대로 설명이 분명하면 작은 모델도 놀랄 만큼 정확하게 부른다.5
좋은 도구 설명은 이런 느낌이다.
함수 이름이 구체적이다.
무엇을 하는지 한 줄로 명확하다.
파라미터 의미가 애매하지 않다.
enum이나 필수 필드를 적극적으로 쓴다.
예를 들어 do_task(data)보다 convert_currency(amount, from_curr, to_curr)가 훨씬 낫다. 전자는 추측 게임이고, 후자는 지도다.
여러 도구를 동시에 부를 수도 있다
공식 문서는 병렬 툴 호출 예시도 제공한다. 예를 들어 두 도시의 날씨와 기온을 함께 묻는 질문에는 get_temperature, get_conditions 같은 함수가 여러 번 호출될 수 있다.5
이 기능은 에이전트형 앱에서 특히 유용하다. "오늘 서울 날씨 확인하고, 회의실 예약 가능 여부도 보고, 그 결과를 요약해줘" 같은 복합 지시가 가능해지기 때문이다. 물론 그만큼 함수 설계와 권한 통제가 중요해진다.
네 기능을 합치면 어떤 일이 가능할까
이제 가장 중요한 그림을 보자. 이 기능들은 따로따로도 쓸 수 있지만, 진짜 재미는 연결될 때 나온다.
예를 들어 사내 문서 도우미를 만든다고 해보자. 사용자가 PDF 캡처 이미지를 올린다. 비전 모델이 화면 내용을 읽는다. 구조화 출력으로 제목, 날짜, 핵심 키워드, 표 요약을 JSON으로 뽑는다. 그 요약과 본문 조각을 임베딩해 벡터 저장소에 넣는다. 이후 사용자가 질문하면 관련 문서를 검색한다. 마지막으로 최신 정보가 더 필요하면 툴 호출로 사내 API를 불러 상태를 확인한다.
한 줄로 정리하면 이런 파이프라인이다.
비전 → 구조화 출력 → 임베딩 검색 → 툴 호출 → 최종 답변
바로 이 조합이 Ollama를 검색, 자동화, 에이전트형 작업의 기반으로 만들어 준다.
실전에서 자주 쓰는 패턴
고급 기능을 처음 붙일 때는 거대한 에이전트부터 만들 필요가 없다. 오히려 작은 패턴부터 쌓는 편이 훨씬 낫다.
첫 번째 패턴은 문서 추출기다. 스캔 문서나 영수증 이미지를 넣고, 구조화된 JSON으로 필드를 뽑는다. 이건 비전과 구조화 출력만으로도 꽤 쓸 만한 결과가 나온다.
두 번째 패턴은 로컬 검색기다. 문서를 임베딩해 두고, 질문과 비슷한 조각을 찾아 다시 모델에게 넘긴다. 이것만으로도 내부 문서 검색 품질이 크게 올라간다.
세 번째 패턴은 도구형 비서다. 모델이 답을 지어내지 말고, 필요한 경우 계산기 함수, 날씨 API, 파일 검색 함수, 사내 DB 조회 함수를 부르게 만든다. 이때 툴 호출이 핵심 역할을 한다.
네 번째 패턴은 멀티모달 검수기다. 이미지나 스크린샷을 비전으로 읽고, 결과를 스키마에 맞게 정리한 뒤, 특정 조건이면 도구를 호출해 후속 조치를 취한다. 예를 들어 "오류 스크린샷에서 에러 코드를 읽고, 지식 베이스 검색 도구를 호출한 뒤 해결 절차를 요약"하는 식이다.
자주 걸리는 함정
고급 기능은 강력하지만, 그만큼 헷갈리는 지점도 있다.
비전 모델인데 텍스트 전용 모델을 골라 놓고 왜 이미지가 안 되지 하고 당황한다.
임베딩은 해놓고 검색 쿼리에는 다른 임베딩 모델을 써서 결과가 이상해진다.
JSON 모드만 믿고 프롬프트를 대충 써서 필드가 흔들린다.
툴 호출에서 도구 결과를 다시 모델에게 돌려주지 않아 최종 답이 비어 보인다.
함수 권한 통제를 하지 않고 위험한 도구를 너무 넓게 열어 둔다.
특히 마지막은 중요하다. 툴 호출은 편리하지만, 결국 애플리케이션이 실행한다. 따라서 파일 삭제, 결제, 외부 전송, 데이터 수정 같은 함수는 승인 절차나 화이트리스트 검증을 꼭 넣는 편이 좋다. 모델은 똑똑하지만, 권한 정책까지 대신 짜 주지는 않는다.
마무리
이번 장의 핵심은 간단하다. Ollama의 고급 기능은 각각 따로 놀지 않는다. 비전은 입력 범위를 넓히고, 임베딩은 기억과 검색을 만들고, 구조화 출력은 자동화를 안전하게 만들고, 툴 호출은 외부 세계와 연결한다.
이 조합이 갖춰지는 순간, 로컬 LLM은 단순한 채팅 도구가 아니라 작고 유능한 작업 엔진이 된다. 이미지를 읽고, 문서를 찾고, 결과를 JSON으로 정리하고, 필요한 API를 호출해 답을 완성하는 흐름이 가능해진다. 바로 여기서부터 검색 도우미, 사내 자동화, 에이전트형 앱, 멀티모달 RAG 같은 다음 단계가 열린다.
다음 장에서는 이런 기능들이 실제 도구와 제품 안에서 어떻게 쓰이는지, 즉 Open WebUI, 코딩 도구, RAG, 데스크톱 앱 같은 실전 활용 패턴으로 이어서 살펴보자.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.