# AI 에이전트

도구를 40개 붙이면 LLM은 맞는 도구를 고를까?

@Hong2026-10-04

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
LLM에 도구를 40개 붙이면 맞는 도구를 고르는 정확도와 프롬프트 토큰은 어떻게 변하나?
답
도구 40개를 실으면 요청마다 3,297토큰이 붙고 정답이 19/20 근처에서 15/20으로 떨어지며, 질문과 가까운 도구 5개만 실으면 정답이 18개로 늘어난다.
조건
CPU에서 돌린 qwen2.5:1.5b, Ollama 0.34.4, Docker Compose, 한국어 질문 20개, 가짜 도구 40개

환경

모델
qwen2.5:1.5b(Q4_K_M), GPU 없이 CPU 추론
Ollama
0.34.4, temperature: 0, seed: 42, num_ctx: 8192, num_thread: 2
컨테이너
Docker Compose, Ollama 메모리 2,700MiB·CPU 2.25개, 클라이언트 300MiB·CPU 0.25개
임베딩
qwen3-embedding:0.6b(비교 대상 nomic-embed-text:latest)
데이터
한국어 질문 20개(정답 도구마다 하나), 가짜 도구 40개
시간 예산
3시간
목차
  1. 실습 준비
  2. 실습 1: 도구 수별 프롬프트 토큰
  3. 실습 2: 도구를 늘리면
  4. 실습 3: 질문과 가까운 도구만 골라 싣기
  5. 정리

이번 시간에는 로컬 LLM에 함께 싣는 도구 수를 1개에서 40개까지 늘려 가며, 프롬프트 토큰과 도구 선택 정확도가 어떻게 바뀌는지 Ollama로 측정하며 배워가는 시간입니다.

과제 20개로 프롬프트 토큰, 처리 시간, 도구 선택과 인자의 정확도를 세고, 질문과 가까운 도구 5개만 임베딩 검색으로 골라 실으면 어떻게 되는지도 볼게요.

Ollama /api/chat 요청의 tools 배열은 모델에 그대로 전달되지 않아요. Ollama가 모델 템플릿에 맞춰 도구 정의를 프롬프트 글자로 바꿔 system 메시지 안에 넣습니다. 그리고 모델을 부를 때마다 다시 들어가요. 그러니 에이전트가 한 작업에서 모델을 열 번 부르면 도구 정의도 열 번 읽히는 거죠.

실습 준비

이 글의 명령은 모두 실습 코드의 agent-tool-count/ 디렉터리에서 실행합니다.

  • compose.yaml: ollama·client 서비스 2개와 메모리·CPU 상한
  • requirements.txt: client 컨테이너에 설치할 패키지 3개
  • agentlab/: Ollama 호출(llm.py)과 결과 기록(runlog.py) 패키지(파일 3개)
  • common.py: 도구 정의, 과제, 시스템 프롬프트, 호출 옵션을 모은 공용 모듈
  • tools40.json: 도구 40개 정의
  • tasks.jsonl: 질문 40개
  • pre_tasks.jsonl: 사전 실습 질문 5개(grade.py가 함께 읽음)
  • emb/: 도구·질문 임베딩(파일 4개)
  • toolsets.jsonl: 과제·조건별 도구 목록 440개
  • ids20.txt: 본 실행에 쓴 과제 id 20개
  • run.py: 조건에 맞는 도구 목록을 실어 과제를 보내고 첫 응답을 results/<조건>.jsonl에 붙이는 스크립트
  • render.py: 도구 목록을 실어 프롬프트 토큰 수(tokens)를 보는 스크립트
  • grade.py: 호출·도구·스키마·인자 값 네 단계로 채점하는 grade_rec
  • summary.py: 결과 파일을 다시 채점해 하위 명령마다 다른 요약을 출력하는 스크립트

CPU 상한에 맞춰 모든 요청에 num_thread: 2를 넣었고, 프롬프트 캐시가 메모리 상한을 넘지 않도록 LLAMA_ARG_CACHE_RAM을 256으로 줄였어요. 서비스는 compose.yaml의 ollama와 client 두 개입니다.

블록의 ol은 Ollama 컨테이너에서 ollama 명령을, lab은 클라이언트 컨테이너에서 Python 스크립트를 실행하는 함수예요. IDS에는 ids20.txt의 과제 id 20개를 담아 둡니다. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

export COMPOSE_PROJECT_NAME=atcnt
docker compose up -d

ol() { docker compose exec ollama ollama "$@"; }

lab() { docker compose exec -T client python "$@"; }

IDS=$(cat ids20.txt)

ol pull qwen2.5:1.5b

도구는 이 사이트 운영을 흉내 낸 가짜 도구 40개예요. 실행하지 않기 때문에 정의만 있습니다. 설명은 한두 문장, 인자는 한두 개로 실제 MCP 서버의 도구와 비슷한 길이에 맞췄고, 설명이 서로 비슷한 무리(글, 코스, 저자와 회원, 알림, 결제, 운영, 기타)를 일부러 만들었습니다.

예를 들어 get_like_count(좋아요 수)와 get_view_count(조회수)는 인자가 똑같이 slug 하나이고, 설명도 한 단어만 달라요. 정의 하나는 이렇게 생겼습니다.

{
  "name": "get_view_count",
  "description": "글의 누적 조회수를 돌려준다.",
  "parameters": {
    "type": "object",
    "properties": {
      "slug": {"type": "string", "pattern": "^[a-z0-9-]+$", "description": "글의 slug(영문 소문자, 숫자, 하이픈)"}
    },
    "required": ["slug"],
    "additionalProperties": false
  }
}

질문은 정답 도구 20개를 골라 도구마다 두 개씩 40개를 만들었고, 본 실행에서는 도구마다 첫 질문 하나씩, 20개만 썼어요. 도구 40개 호출 하나가 1분을 넘겨 3시간 예산에 맞추려고 줄인 거예요. slug, 주문 번호, 회원 ID 같은 인자 값은 모두 질문 안에 있어서 값까지 비교할 수 있어요.

과제마다 크기별 도구 목록을 실행 전에 만들어 toolsets.jsonl에 고정해 두었어요.

  • 정답만(N1): 정답 도구 하나
  • 비슷한 순서(S5, S10, S20): 정답 도구와 임베딩 유사도가 높은 도구부터 채워요. 정답 도구와 헷갈리기 쉬운 도구가 먼저 들어갑니다.
  • 전체(N40): 40개 전부
  • 상위 5개(K5): 질문과 가장 가까운 도구 5개. 정답 도구가 빠질 수 있어요.

크기 5의 목록은 크기 10의 목록 안에 들어가고, 10은 20 안에 들어가게 만들었어요. 왜 그럴까요? 그래야 크기 사이의 차이를 도구를 더 넣은 효과로 읽을 수 있기 때문이에요.

요청은 모두 /api/chat에 stream: false, system 1개와 user 1개로 보내고 첫 응답만 채점합니다. num_ctx: 8192는 도구 40개를 실어도 잘리지 않을 길이예요.

실습 1: 도구 수별 프롬프트 토큰

render.py tokens는 질문 하나(T05, 좋아요 수)에 도구 0, 1, 5, 10, 20, 40개를 실어 num_predict: 1로 보내고 prompt_eval_count를 비교해요. 도구를 뺀 요청과의 차이가 바로 도구 정의가 쓴 토큰이에요.

lab render.py tokens
N=0   prompt_eval_count    79  tools     0
N=1   prompt_eval_count   220  tools   141  141.0 tokens per tool
N=5   prompt_eval_count   487  tools   408   81.6 tokens per tool
N=10  prompt_eval_count   894  tools   815   81.5 tokens per tool
N=20  prompt_eval_count  1699  tools  1620   81.0 tokens per tool
N=40  prompt_eval_count  3376  tools  3297   82.4 tokens per tool

출력을 보면 도구 하나에 약 81토큰이 들었어요. 도구가 하나일 때 141토큰으로 큰 건, "# Tools"부터 호출 형식 안내까지 붙는 머리글이 60토큰쯤 되기 때문이에요.

도구 수를 두 배로 늘리면 도구 정의 토큰도 거의 정확히 두 배가 돼요. 도구 40개면 3,297토큰이고, system 프롬프트와 질문을 합친 79토큰의 40배가 넘습니다.

여러분 도구 목록의 비용도 같은 방법으로 재 보실 수 있어요. 도구를 실은 요청과 뺀 요청을 한 번씩 보내 prompt_eval_count 차이를 보시면 됩니다.

실습 2: 도구를 늘리면

과제 20개를 정답 도구 하나만 준 N1부터 비슷한 순서로 5, 10, 20, 40개까지 늘려 보낼게요. 도구가 하나뿐인데도 틀리는 과제는 도구 수와 상관없이 못 푸는 과제라서, N1이 비교의 기준선이에요.

채점은 grade.py의 grade_rec가 해요. 도구를 불렀는지, 정답 도구 하나만 불렀는지, 인자가 도구의 전체 JSON Schema를 통과하는지, 정답 인자 값이 맞는지를 차례로 보고, 앞 단계를 통과한 응답만 다음 단계로 넘겨요. 틀린 응답은 다음처럼 나눴습니다.

  • no call: 도구를 부르지 않음
  • same family: 같은 무리의 다른 도구를 고름(조회수 질문에 좋아요 수 도구)
  • schema: 정답 도구지만 인자가 스키마를 통과하지 못함
  • value: 스키마는 통과했지만 인자 값이 틀림

조건별 요약은 summary.py sizes로 내요. 네 단계 통과 수와 오답 분류, 프롬프트 토큰 범위를 찍고, 시간은 캐시 재사용이 110토큰 이하인 호출만으로 범위와 중앙값을 냅니다. Ollama는 앞 요청과 프롬프트 앞부분이 같으면 그 부분을 캐시에서 쓰기 때문이에요. 아래 출력은 summary.py 부분만 옮겼어요(발췌).

for c in N1 S5 S10 S20 N40; do lab run.py --cond $c --ids $IDS; done
lab summary.py sizes N1 S5 S10 S20 N40
N1   20 tasks: called 20 -> right tool 20 -> schema ok 19 -> values ok 19
     misses: schema 1
     prompt tokens 219~305, cached 3~100 (over 110: 0)
     prompt time 1.5~3.3s (median 1.8), total 2.2~5.9s (median 3.0)
S5   20 tasks: called 19 -> right tool 19 -> schema ok 17 -> values ok 17
     misses: schema 2, no call 1
     prompt tokens 487~622, cached 98~186 (over 110: 5)
     prompt time 5.0~10.9s (median 6.1), total 6.1~14.5s (median 7.2)
S10  20 tasks: called 19 -> right tool 19 -> schema ok 19 -> values ok 19
     misses: no call 1
     prompt tokens 887~1035, cached 3~100 (over 110: 0)
     prompt time 12.0~20.5s (median 17.5), total 13.4~24.9s (median 19.8)
S20  20 tasks: called 19 -> right tool 19 -> schema ok 19 -> values ok 19
     misses: no call 1
     prompt tokens 1686~1851, cached 3~175 (over 110: 1)
     prompt time 24.3~49.0s (median 40.2), total 25.2~53.2s (median 42.8)
N40  20 tasks: called 19 -> right tool 18 -> schema ok 16 -> values ok 15
     misses: schema 2, same family 1, no call 1, value 1
     prompt tokens 3374~3402, cached 3~177 (over 110: 1)
     prompt time 61.2~108.7s (median 92.9), total 62.9~111.6s (median 96.2)

출력을 보면 프롬프트 토큰은 도구 수에 비례해 늘었고, 프롬프트 처리 시간 중앙값은 1.8초(도구 1개)에서 6.1초, 17.5초, 40.2초, 92.9초로 늘었어요. 실행하는 동안 같은 머신에서 다른 LLM 실습 컨테이너가 함께 돌아서, 시간은 크기 사이의 대략적인 차이로만 읽어 주세요.

정확도는 도구 수에 따라 고르게 떨어지지 않았어요. 20개까지는 기준선과 같은 19개 안팎이었고, 40개에서 15개로 떨어졌습니다. 과제 단위로 보면 이래요.

lab summary.py flips N1 S5 S10 S20 N40
N1: right 19/20
S5   right 17/20; of the 19 right at N1: still right 17, lost 2 T33 T35; newly right 0 
S10  right 19/20; of the 19 right at N1: still right 18, lost 1 T33; newly right 1 T29
S20  right 19/20; of the 19 right at N1: still right 18, lost 1 T33; newly right 1 T29
N40  right 15/20; of the 19 right at N1: still right 14, lost 5 T13 T25 T33 T35 T37; newly right 1 T29

기준선에서 맞힌 19개를 분모로 보면, 도구 5~20개에서는 1~2개를 잃었고 40개에서는 5개를 잃었어요.

도구 5개 이상에서 계속 틀린 T33은 환불 요청이에요. 정답 도구 하나만 줬을 때는 refund_payment를 불렀지만, 다른 도구가 함께 있으면 부르지 않고 "환불 사유를 알려 달라"고 되물었습니다.

도구 40개에서 새로 잃은 과제 중 도구를 잘못 고른 것은 T13(get_course 대신 get_course_reviews) 하나뿐이에요. 나머지 셋은 도구는 맞게 고르고 인자를 틀렸습니다.

  • T25: 구독 조회에 unread_only를 붙였어요. 이 인자는 다른 도구 get_user_notifications의 것이에요. 스키마의 additionalProperties: false에 걸립니다.
  • T37: 서버 상태 조회에서 필수 인자 service를 빠뜨렸어요.
  • T35: 질문에 "api 서비스"라고 적었는데 service를 web으로 보냈어요.

도구를 고르는 단계만 채점했다면, 40개에서도 18개를 맞혔다고 봤을 거예요.

실습 3: 질문과 가까운 도구만 골라 싣기

이번에는 도구를 모두 싣는 대신, 질문을 임베딩해서 코사인 유사도가 가장 높은 도구 5개만 tools에 실어 볼게요. 임베딩은 문장을 숫자 벡터로 바꾼 것으로, 뜻이 가까운 문장일수록 벡터 방향이 가깝게 나오도록 학습돼 있어요.

K5 목록은 qwen3-embedding:0.6b로 도구마다 이름: 설명 문장과 질문 문장을 임베딩해 미리 골라 두었고, 그 벡터가 emb/에 있어요. summary.py topk는 이 벡터로 정답 도구가 상위 k개 안에 든 과제 수(적중@k)를 영어 위주로 학습한 nomic-embed-text:latest와 비교하고, K5 결과를 채점합니다. 아래 출력은 summary.py 부분만 옮겼어요(발췌).

lab run.py --cond K5 --ids $IDS
lab summary.py topk
retrieval nomic        hit@3 6/20, hit@5 8/20
retrieval qwen3-plain  hit@3 18/20, hit@5 19/20
K5: gold in the list 19, missed by retrieval 1 T01
K5: values ok 18/20; among retrieval hits 18/19; among retrieval misses 0/1
  T01 gold get_article, list ['get_article_revisions', 'get_comment_count', 'list_articles_by_tag', 'get_tag_list', 'list_author_articles'], got []
N40 values ok 15/20; K5 right & N40 wrong: 5, K5 wrong & N40 right: 2
K5   20 tasks: called 19 -> right tool 19 -> schema ok 18 -> values ok 18
     misses: no call 1, schema 1
     prompt tokens 495~620, cached 3~181 (over 110: 1)
     prompt time 5.4~13.0s (median 10.0), total 7.0~17.3s (median 12.6)

qwen3-embedding:0.6b는 과제 20개 중 19개에서 정답 도구를 상위 5개 안에 넣었어요. 같은 20개에서 nomic-embed-text는 8개였습니다.

결과를 검색과 선택 두 단계로 나눠 보면 이렇습니다.

  • 검색이 놓친 과제는 1개(T01)예요. "redis-ttl-notes 글의 제목이랑 발행일 알려줘"에 대해 get_article 대신 수정 이력, 댓글 수 같은 도구 5개가 실렸고, 모델은 어떤 도구도 부르지 않았어요. 정답 도구가 목록에 없으면 모델이 맞힐 방법이 없죠.
  • 검색이 정답 도구를 넣어 준 19개 중에서는 18개를 맞혔어요. 틀린 하나는 기준선에서도 틀린 슬랙 채널(T29)입니다.
  • 그래서 최종 정확도는 검색 적중률(19/20)과 적중했을 때의 선택 정확도(18/19)를 곱한 값이에요.

도구 40개를 모두 실었을 때(15개)보다 3개 더 맞혔어요. 40개에서 인자를 틀렸던 T13, T25, T35, T37은 상위 5개에서 모두 맞혔고, 프롬프트는 500~600토큰으로 줄었어요.

정리

범위: CPU에서 돌린 qwen2.5:1.5b, Ollama 0.34.4, 한국어 질문 20개, 가짜 도구 40개.

  • 도구 하나가 약 81토큰이어서 40개를 실으면 매 요청에 3,297토큰이 붙었고, 프롬프트 처리 시간 중앙값은 도구 1개 1.8초에서 40개 92.9초로 늘었어요. 여러분 도구 목록도 도구를 실은 요청과 뺀 요청의 prompt_eval_count 차이로 재 보세요.
  • 정답 수는 도구 20개까지 기준선 19/20 근처였다가 40개에서 15/20으로 떨어졌고, 새로 잃은 과제 중 셋은 도구는 맞게 고르고 인자를 틀렸어요. 도구 이름만 채점하지 말고 스키마와 인자 값까지 채점하세요.
  • 같은 무리의 다른 도구를 고른 오답은 도구 20개까지 없었고 40개에서 1개였어요. 이 글에 싣지 않은 추가 측정에서는 설명이 비슷한 도구부터 채운 목록과 무작위 목록이 한 과제만 달랐어요. 비슷한 도구를 걸러 내기보다 싣는 도구 수를 줄여 보세요.
  • 질문과 가까운 도구 5개만 실으니 프롬프트가 500~600토큰으로 줄고 정답은 15개에서 18개로 늘었으며, 최종 정답은 검색 적중 19개 안에서만 나왔어요. 한국어 질문 20개에서 적중@5는 nomic-embed-text 8개, qwen3-embedding:0.6b 19개였으니, 한국어 질문에는 영어 위주로 학습한 임베딩 모델을 쓰지 마세요.
내 링크 만들기