96%의 토큰 절약에도 불구하고 이름이 지정된 MCP 도구를 유지한 이유
요약
토큰 효율성을 극대화하는 단일 코드 실행 방식(execute_code)과 개별 이름 지정 도구 방식 사이의 설계 결정 과정을 다룹니다. 음성 우선(voice-first) 소형 로컬 모델 에이전트라는 특수한 목적에 맞춰 왜 토큰 절약보다 기존의 도구 방식을 유지했는지 분석합니다.
핵심 포인트
- 단일 코드 실행 방식은 이름 지정 도구 대비 최대 96%의 토큰 절약 가능
- 토큰 효율성이 항상 최선의 선택은 아니며 대상(Target) 모델의 특성이 중요함
- 음성 우선 소형 모델 에이전트에서는 기존 도구 방식이 더 적합할 수 있음
- MCP 서버 채택 시 설계 철학과 사용 환경을 고려한 결정 프레임워크 필요
이곳의 boat-agent 스택은 하나의 기본 원칙에 따라 작동합니다. 사용할 수 있는 것이 있다면 개선하고, 직접 만드는 것은 최후의 수단으로 삼는 것입니다. 따라서 SignalK MCP 서버가 필요했을 때, 정직한 첫 번째 단계는 직접 작성하는 것이 아니라 이미 존재하는 것을 평가하는 것이었습니다.
VesselSense/signalk-mcp-server (TypeScript, MIT)는 잘 만들어진 작업물입니다. 이 서버는 단일 execute_code 도구를 통해 에이전트에게 SignalK를 노출합니다. 모델이 JavaScript를 작성하면, 서버는 이를 샌드박스화된 V8 isolate (isolated-vm)에서 실행하고 결과만을 반환합니다. 해당 README는 기존의 이름이 지정된 (named) MCP 도구와 비교했을 때 90~96%의 토큰 감소를 주장합니다. 선박 상태 쿼리의 경우 2,000개 토큰에서 120개로, 다중 호출 워크플로우의 경우 13,000개에서 300개로 줄어듭니다. 이러한 수치는 타당하며, 복잡한 다단계 작업에서 코드 실행이 도구 호출 (tool-calling)보다 토큰 효율성 면에서 우수하다는 업계의 전반적인 결과와도 일치합니다.
우리는 이를 읽고, 우리 자신의 에이전트를 대상으로 수치를 계산해 보았지만, 결국 별도로 이름이 지정된 도구인 signalk-mcp를 그대로 유지했습니다. 대신 VesselSense의 아이디어 중 세 가지를 가져와 우리의 로드맵에 반영했습니다. 이 포스트는 그 평가에 관한 내용입니다. 두 가지 철학, 왜 겉보기에 명백해 보이는 승리가 음성 우선 (voice-first) 에이전트에게는 적용되지 않는지, 그리고 여러분이 직접 MCP 서버를 채택하거나 구축하기 전에 재사용할 수 있는 결정 프레임워크를 다룹니다.
이 글은 디버깅 연대기가 아닌 설계 추론(design-reasoning) 포스트이지만, 동일한 흐름을 따릅니다. 질문, 명백한 '예'처럼 보이는 막다른 길, 그리고 실제로 유효했던 결정에 대한 이야기입니다.
질문
두 개의 SignalK MCP 서버, 진정으로 다른 두 가지 설계:
VesselSense/signalk-mcp-server sailingnaturali/signalk-mcp
───────────────────────────── ───────────────────────────
단일 도구: execute_code 개별적으로 이름이 지정된 도구들:
...
채택할 것인가 유지할 것인가의 문제: 토큰 효율성 측면의 이점이 우리 에이전트에게 구속력을 갖는가? 만약 그렇다면, 채택하는 것이 두 번째 서버를 유지하는 것보다 낫습니다. 그렇지 않다면, 지침은 채택을 강제하는 것이 아니라 대상(target)에 맞는 올바른 것을 만들 것을 강제합니다.
여기서는 대상이 그 무엇보다 중요합니다. 우리의 설계 기준점은 소형 로컬 모델 기반의 음성 우선 에이전트 (voice-first agent) — 즉, 보트 위에서 텍스트 음성 변환 (TTS) 프런트엔드를 구동하는 Hermes 3 8B입니다. 채팅창 속의 프런티어 모델 (frontier model)이 아닙니다. 이 단 하나의 사실이 전체 평가를 결정합니다.
두 가지 유효한 철학, 서로 다른 대상
execute_code는 영리하며, 토큰 계산은 실제적입니다. 에이전트가 모든 AIS 대상(target)을 가져오고, 가까운 것들로 필터링하고, CPA(최근 접근 경로) 순으로 정렬한 뒤 요약 형식을 갖춰야 할 때, 이름이 지정된 도구 (named-tool) 패턴은 모든 중간 호출에 대해 전체 입력+출력 토큰 비용을 지불합니다. 모델이 구조화된 호출 (structured call)을 생성하고, 전체 결과가 컨텍스트로 다시 흐르는 과정을 반복하는 식입니다. 코드 실행 (code execution)은 이를 하나의 스크립트와 하나의 집계된 결과로 압축합니다. 복잡하고 다단계적인 해양 쿼리를 수행하는 프런티어 모델의 경우, 90~96%의 절감 주장은 믿을 만합니다.
하지만 그 절감 비용은 한 가지 화폐로 지불됩니다: 에이전트가 반드시 정확한 코드를 안정적으로 작성해야 한다는 점입니다. 이는 프런티어 모델에게는 저렴한 비용이지만, 8B 모델에게는 비싼 비용입니다. 여기서의 역량 격차는 미미한 수준이 아닙니다. 현장의 목소리는 다음과 같습니다:
llama3.2:3b 및 llama3.1:8b와 같은 소형 모델들은 도구 호출 (tool calling) 사양을 지원하지만, 실제로는 특히 순차적 명령이나 다중 엔티티 (multi-entity) 명령에서 일관성 없이 실패합니다… 도구 호출은 로컬 모델과 클라우드 모델 사이의 가장 큰 역량 격차입니다. 배관(plumbing)은 존재하지만 모델의 신뢰성(reliability)은 아직 갖춰지지 않았습니다.
소형 모델이 _구조화된 도구 호출 (structured tool call)_을 생성하는 것조차 불안정하다면, _정확한 JavaScript_를 생성하도록 요구하는 것은 엄격하게 더 어려운 일입니다. execute_code는 우리 에이전트의 모델 부담을 줄여주는 것이 아니라, 오히려 높입니다. 토큰 예산은 결코 우리의 구속 조건이 아니었습니다. 신뢰성이 구속 조건입니다.
따라서 비교 대상은 "어떤 설계가 더 나은가"가 아닙니다. 그것은 다음과 같습니다:
- Frontier 모델, 복잡한 쿼리, 토큰 예산이 제약 조건인 경우 →
execute_code가 승리합니다. VesselSense를 채택하세요. - 작은 로컬 모델, 음성 프론트 엔드, 신뢰성이 제약 조건인 경우 → 명시된 이름의 도구(discrete named tools)가 승리합니다. 인자 하나만을 가진 명명된 도구 — `battery_state(
execute_code는 에이전트의 스크립트가 반환하는 결과물인 가공되지 않은(raw) SignalK를 그대로 반환합니다. 이는 모든 포맷팅(formatting)의 부담을 에이전트에게 떠넘기는데, 이는 소형 모델(small model)이 실패하는 바로 그 계층입니다. 우리는 이전에 포맷팅은 프롬프트가 아니라 도구 계층에 속해야 하는 이유에 대해 작성한 바 있습니다. 프롬프트 규칙은 권고 사항일 뿐이며 정보가 유출(leak)될 수 있지만, 도구 응답(tool response)은 결정론적(deterministic)입니다. execute_code는 포맷팅을 모델에게 떠넘기는 방식의 극단적인 버전입니다. 이는 단순히 계약(contract)을 유출하는 것을 넘어, 계약을 둘러둘 공간조차 마련하지 않습니다. 음성 우선(voice-first) 에이전트에게 이는 결격 사유이며, 그 어떤 토큰 절약도 이를 되돌릴 수 없습니다.
토큰 이득이 구속력을 갖지 못하는 두 번째 이유: 404-as-null과 서킷 브레이커(circuit breaker)
두 번째 구조적 이유는 에러 핸들링(error handling)입니다. 우리의 클라이언트에서 SignalK 404는 에러가 아닙니다. 이는 선박이 해당 경로를 게시하지 않음(해당 센서가 없거나 추측한 경로임)을 의미합니다. 클라이언트는 예외(exception)를 발생시키는 대신 깔끔한 null을 반환합니다:
# 선박이 게시하지 않는 경로에 대한 read_sensor
{ "path": "navigation.headingTrue", "value": None, "display": None,
"unit": None, "timestamp": None } # 예외가 아님
이는 의도된 설계이며, 소형 모델에 대한 교훈이 담겨 있습니다. 에이전트 런타임(runtime)은 흔히 **도구별 서킷 브레이커(per-tool circuit breaker)**를 실행합니다. 우리의 런타임(Hermes)은 동일한 도구에서 3회 연속 실패가 발생하면 작동합니다. 나침반이 없는 배에서 소형 모델이 생소한 선박의 추측된 경로들 — headingTrue, headingMagnetic, courseOverGroundTrue —를 탐색하며 퍼져나간다면, 각각을 도구 실패로 간주하는 404 에러가 폭발적으로 발생할 것입니다. 이는 서킷 브레이커를 작동시켜 그 뒤에 대기 중인 유효한 읽기 작업들까지 차단해 버립니다. 깔끔한 null을 반환함으로써 누락된 경로를 성공적인 호출로 유지하면, 부재(absence)로 인해 서킷 브레이커가 작동하는 일을 방지할 수 있습니다.
execute_code 하에서 누락된 경로는 SignalK 클라이언트가 아이솔레이트(isolate) 내부에서 던지는 무엇이든 의미하며, 에이전트는 이미 올바른 코드를 작성하는 데 한계에 다다른 모델 상태에서 자신이 직접 작성한 코드로 이를 포착하고 해석해야 합니다. 이름이 지정된 도구(named-tool) 설계는 "이 센서는 존재하지 않습니다"를 구조적으로 정상적이고 치명적이지 않은 결과로 만듭니다. 이는 소형 모델을 위해 구축하는 모든 MCP 작성자에게 재사용 가능한 교훈입니다: 부재(absence) 시 도구가 무엇을 할지 결정하고, 부재를 결함이 아닌 성공으로 만드세요.
커버리지 감사: 느낌이 아닌 증거
"채택 선호" 지침이라 할지라도 여전히 커버리지 감사(coverage audit)를 요구합니다. 즉, 기존 도구가 실제로 작업을 수행하는지, 아니면 작업을 에이전트에게 떠넘기는지를 확인해야 합니다. 우리는 기능별로 차이점(diff)을 비교했습니다.
활성 알람. VesselSense의 getActiveAlarms()는 상태와 함께 알람을 반환하고 필터링 및 정렬 작업을 아이솔레이트 내의 클라이언트 측 코드에 맡깁니다. 해당 사례의 예시에서는 에이전트가 작성한 JS 내에서 a.state === "alarm" || a.state === "emergency"와 같이 필터링을 수행합니다. 일반 상태(normal-state) 알림이 결과에 그대로 남아 있으며, 심각도 순서 정렬도 없습니다. 반면 우리의 도구는 해당 작업을 도구 내부에서 수행하므로, 에이전트가 필터를 작성할 필요가 없습니다:
# signalk-mcp: get_active_alarms가 도구 내부에서 필터링 + 정렬을 수행함
_ALARM_SEVERITY = {"emergency": 0, "alarm": 1, "warn": 2, "alert": 3}
_INACTIVE_STATES = {"normal", "nominal"}
...
또한 각 경로에서 notifications. 접두사를 제거하여 결과가 우리의 후속 알람 설명 도구로 즉시 전달되도록 합니다. 최악의 상태 우선, 일반 상태 필터링, 경로 정리 — JavaScript가 전혀 필요하지 않습니다.
누락된 도구. VesselSense의 getVesselState는 SignalK 트리를 쏟아내고(dump) 에이전트가 직접 파헤치도록 합니다. 우리는 그와 대등한 덤프 도구는 없지만, 대신 덤프를 통해 에이전트가 조립해야 했을 목적 기반의 도구들을 제공합니다: battery_state, depth_state (흘수 계산 없이 에이전트가 "좌초될 위험이 얼마나 되나요?"라는 질문에 답할 수 있도록 선저 여유 수심(under-keel clearance)을 우선 제공), get_route, get_local_time (GPS 기반 시간대 인식). 각 도구는 말하기 적합하고 계약을 준수하는(contract-compliant) 답변을 직접 반환합니다.
감사(audit) 전반에 걸친 패턴은 다음과 같습니다: VesselSense는 마지막 단계의 작업 — 필터링(filter), 정렬(sort), 포맷팅(format), 해석(interpret) — 을 에이전트가 작성한 코드로 떠넘기는데, 이는 소형 모델(small model)이 가장 못하는 작업입니다. 이것은 VesselSense의 결함이 아닙니다. 이는 _프론티어 모델(frontier model)_을 위한 올바른 분업입니다. 하지만 우리 모델에게는 잘못된 분업입니다.
성숙도 감사 ( "채택 선호" 상황에서도 여전히 중요함)
코드를 채택한다는 것은 그 유지보수 책임을 물려받는 것을 의미합니다. 평가 시점에서 업스트림 리포지토리(upstream repo)의 공개된 신호들은 다음과 같습니다:
최근 푸시(last push) 2025-11-26 (6개월 이상 휴면 상태)
스타(stars) 8
MCP SDK @modelcontextprotocol/sdk pinned ^0.5.0 (메이저 버전 2단계 뒤처짐)
...
이 중 어느 것도 단독으로는 치명적이지 않습니다. 하지만 이 정보들을 종합하면 다음과 같은 결론이 나옵니다: 채택한다는 것은 휴면 상태의 코드베이스를, 두 번째 언어 런타임(language runtime) 환경에서, 네이티브 샌드박스 의존성(isolated-vm)과 함께, 메이저 버전이 두 단계 뒤처진 MCP SDK에 고정된 상태로 떠안는 것을 의미합니다. 이 모든 것을 감수하면서 얻는 것은 우리 에이전트가 애초에 소비하지도 않는 토큰 효율성뿐입니다. 유지보수 비용은 실재하며, 이점은 실현되지 않습니다. 이것이 바로 실질적인 측면에서 채택을 권장하지 않는 지침입니다.
의사결정 프레임워크 (이를 재사용하세요)
해양 관련 특수성을 제거하면, 이는 MCP 서버에 대한 일반적인 채택 대 자체 구축(adopt-vs-build) 체크리스트가 됩니다:
- 대상 에이전트를 먼저 지정하세요. 프론티어 모델 (Frontier model)인가요, 아니면 소형/로컬 모델인가요? 채팅인가요, 아니면 음성/TTS인가요? 대상이 무엇인지에 따라 당신이 최적화하려는 통화(currency)가 결정됩니다. 즉, 토큰 (tokens)인가요, 아니면 신뢰성 (reliability)인가요? MCP 설계에 관한 대부분의 의견 충돌은 사실 대상에 대한 의견 충돌입니다.
- 결정적인 제약 조건 (binding constraint)을 식별하세요. 토큰 예산이 벽인가요, 아니면 모델의 신뢰성이 벽인가요?
execute_code는 신뢰성을 토큰과 맞바꿉니다. 토큰이 벽인 경우에만 이를 채택하세요. - 라스트 마일 (last mile)을 누가 수행하는지 확인하세요. 기존 도구가 필터링/정렬/포맷팅/해석을 수행하나요, 아니면 그 작업을 에이전트가 작성한 코드로 넘기나요? 소형 모델의 경우, 에이전트에게 넘기는 라스트 마일 코드의 모든 한 줄은 곧 실패 모드 (failure mode)가 됩니다.
- 출력 계약 (output contract)을 확인하세요. 출력이 포맷팅 요구 사항이 있는 무언가(TTS, 엄격한 다운스트림 파서 (downstream parser))에 의해 소비된다면, 로우 데이터 (raw-data) 도구는 그 계약을 모델에게 외주로 주는 셈입니다. 이름이 지정된 도구 (Named tools)는 이를 내장할 수 있습니다.
- 부재(absence)가 무엇을 의미하는지 결정하세요. "해당 항목이 없음"을 예외가 아닌 성공적인 결과로 만드세요. 특히 서킷 브레이커 (circuit breaker) 뒤에 있거나, 경로를 추측하는 모델의 경우에는 더욱 그렇습니다.
- 어쨌든 성숙도 감사 (maturity audit)를 수행하세요. 휴면 상태, 고정된 (pinned-back) SDK, 두 번째 런타임 (runtime), 네이티브 의존성 (native deps) 등을 확인하세요. 채택한다는 것은 상속받는다는 것을 의미합니다.
- 스틸맨 (Steelman) 기법을 사용한 후, 수확하세요. 직접 구축하더라도 대안적인 방식에서 아이디어를 캐내세요. 아키텍처에 대한 "아니오"가 그 안에 담긴 모든 아이디어에 대한 "아니오"는 아닙니다.
솔직한 스틸맨, 그리고 우리가 수확한 것들
우리는 VesselSense를 거부한 것이 아니라, 거기서 채굴했습니다. 그들의 서버를 읽고 세 가지 아이디어를 즉시 우리의 로드맵 (roadmap)에 반영했습니다:
get_active_alarms— 출시됨 (v0.5.0). 활성 알림, 심각도가 높은 순서 우선, 일반 알림은 필터링, 다운스트림 도구를 위해 경로의 접두사 제거.list_paths— 출시됨 (v0.3.0). 에이전트가 추측 없이, 그리고 404 서킷 브레이커를 작동시키지 않고도 생소한 SignalK 트리를 탐색할 수 있도록 하는 경로 발견 기능.- AIS 타겟 (AIS targets) — 로드맵에 열려 있음, 동일한 음성 계약 (
"cargo vessel, 1.2 nautical miles, bearing North-East") 적용 예정.
그리고 execute_code는 계속해서 관찰 대상(watch-list)에 남아 있습니다. 우리가 클라우드 추론 레이어 (cloud-reasoning layer) — 토큰 예산이 진정으로 한계에 부딪히는, 복잡한 다단계 해양 분석을 수행하는 프런티어 모델(frontier model) — 를 추가하는 날, 코드 실행(code execution)은 적절한 도구가 될 것이며 우리는 그것을 사용할 것입니다. 이 평가의 핵심은 "이름이 지정된 도구(named tools)가 더 낫다"가 아닙니다. "이름이 지정된 도구가 이 에이전트(agent)에게 더 낫다, 그리고 그 상황이 정확히 언제 반전되는가"에 대한 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기