2026-10-02

비자기회귀 판단을 MCP로 연결하기: Jev·Laya·Kev와 nar-server

Jev·Laya·Kev의 typed decision을 하나의 MCP 서버로 연결한 구조와, confidence 정규화·입력 적격성·failover·로컬 모델 운영에서 확인한 사항을 정리합니다.

웹 인터페이스를 만들어야 한다. 환경을 보니 Python도 있고 Node.js도 있다. 그렇다면 어느 쪽을 선택해야 할까?

환경에 무엇이 설치되어 있는지는 명령과 파일로 확인할 수 있다. 하지만 기존 코드, UI 요구, 배포 조건을 종합해 구현 방향을 고르는 일에는 판단이 필요하다. 이처럼 작은 판단을 별도의 모델에 맡기고, 에이전트가 그 결과를 사용하도록 만든 것이 nar-server의 출발점이다.

Jev와 Laya의 공통 primitive를 비교하면서 시작한 프로젝트는 현재 Kev까지 연결하는 Python 서버로 확장됐다. WebUI와 MCP가 같은 backend를 사용하며, 요청에 따라 로컬 모델과 원격 API를 선택한다.

이 글은 2026년 10월 2일 확인한 코드와 공식 문서, 그리고 9월 22–23일에 남긴 구현 검증 기록을 바탕으로 한다. 기존 실행 기록과 아직 검증하지 않은 업무 품질을 구분한다.

어떤 판단을 맡길 것인가

여기서 비자기회귀 판단은 답변 문장을 토큰 하나씩 생성하는 대신, 주어진 후보나 단계에 대한 판단값을 반환하는 사용 방식을 가리킨다. 모델 계열 전체가 같은 구조라는 뜻은 아니다.

Laya는 encoder와 decision head를 사용한다. Kev는 causal LM backbone에 pointer readout을 결합한다. 생성 모델 계열의 backbone을 사용하더라도, 결정 API가 자유로운 문장을 생성하는 방식으로 동작하는 것은 아니다. Jev는 TypeSafe의 호스팅된 System One API로 연결한다. Laya 소스, Kev 소스, TypeSafe System One

이 방식에 맡기기 좋은 질문은 범위가 좁고 답의 형태가 명확하다.

Primitive 질문 예 반환값의 의미
choice Python, Node.js, 추가 확인 중 무엇이 적합한가? 후보별 확률과 선택
score UI 상호작용 복잡도가 어느 단계인가? 순서 있는 단계에 대한 분포와 기대값
noul 사용자가 특정 언어를 명시했는가? 참이라는 판단의 확률

Score의 단계가 0, 1, 2라면 결과는 그 범위의 기대값이다. 예를 들어 확률이 0.1, 0.3, 0.6이면 score는 1.5다. Noul의 0.5는 “중간 수준”이 아니라 참과 거짓 사이의 불확실성을 뜻한다. TypeSafe Primitives, Score

“웹 애플리케이션을 완성하라”는 질문은 이 API에 너무 크다. 환경 관찰, 요구 해석, 후보 선택, 코드 작성과 검증을 나누고 그중 후보 선택 같은 하위 판단을 위임하는 편이 적절하다.

Jev·Laya·Kev를 하나의 인터페이스로

Backend 이 프로젝트의 연결 방식 주요 확인 사항
Jev TypeSafe 원격 API 원격 전송 허용, 인증, rate limit, 모델 버전
Laya 로컬 Python 모델과 선택적 DirectML 실행 언어별 checkpoint, 짧은 입력 예산, 실제 device
Kev 4B 로컬 모델과 TypeSafe 호환 endpoint base·adapter revision, 입력 길이, CPU/GPU 구성

프로젝트는 Jev jev-1.13.0과 고정한 Laya·Kev 소스 및 모델 revision을 사용한다. 가변 alias가 바뀌어도 같은 결과를 기대하는 배포 방식은 피했다.

Jev의 공식 제한은 요청 전체 64k 토큰, state와 가장 긴 질문의 합 32k 토큰이다. 이 프로젝트에서 고정한 Laya 설정은 English 512, Multilingual·Typed decisions 1024 토큰의 질문 시퀀스를 사용한다. 숫자만 비교해 길이를 변환할 수는 없다. tokenizer와 질문 구성 방식이 다르기 때문이다. TypeSafe Models, Laya 고정 설정

Kev는 같은 primitive를 pointer 기반 판단에 대응시키며 TypeSafe 호환 API를 제공한다. 프로젝트에는 이를 공통 계약에 연결하는 어댑터를 추가했다. API 형태가 맞는다는 사실이 세 모델의 판단 품질까지 같다는 뜻은 아니다. Kev 프로젝트

MCP가 맡는 역할

호스트에 노출하는 도구는 두 개다.

  • nar_capabilities: 실제 모델, revision, 준비 상태, backend 우선순위와 실행 정책을 확인한다.
  • nar_evaluate: 관찰된 state와 명시적인 질문들을 평가한다.

구조는 다음과 같다.

호스트 에이전트
  └─ 관찰한 환경·사용자 요구
       ↓
nar_evaluate
  ├─ 입력 계약·원격 전송 정책 검사
  ├─ backend별 언어·길이·준비 상태 검사
  ├─ pinned / failover / round-robin
  │    ├─ Laya 로컬 worker
  │    ├─ Kev 로컬 worker
  │    └─ Jev 원격 API
  └─ 결과 검증·정규화·출처 기록
       ↓
호스트의 최종 계획과 구현

MCP는 판단을 호출하고 결과를 받는 통로다. 에이전트 내부의 모든 추론 단계가 자동으로 이 서버를 통과하는 구조는 아니다.

Skill에는 어떤 상황에서 이 도구를 호출할지 적을 수 있다. 예를 들어 “관찰한 요구를 바탕으로 후보 선택이 필요하면 nar_evaluate를 사용한다”는 규칙이다. 반드시 거쳐야 하는 절차라면 skill 문장에만 의존하지 않고 애플리케이션의 실행 흐름으로 통제해야 한다.

응답은 structuredContent에 담고 같은 JSON을 text content로도 제공한다. 출력 schema 검증은 파싱 가능한 응답을 보장하는 데 쓰며, 답의 정답 여부를 보장하지는 않는다. MCP Tools 사양

Python과 Node가 모두 있는 경우

아래는 공통 요청 형식을 보여주는 합성 예제다. 실제 환경 탐지나 모델 실행 결과가 아니다.

{
  "schema_version": "1.0",
  "request_id": "web-stack-example",
  "state": {
    "task": "대화형 웹 인터페이스 구현",
    "environment": {
      "python": true,
      "node": true,
      "npm": true
    },
    "requirements": {
      "existing_stack": null,
      "interactive_ui": true,
      "user_preference": null
    }
  },
  "questions": {
    "stack": {
      "type": "choice",
      "instructions": "요구에 적합한 구현 생태계를 고르세요. 근거가 부족하면 needs_more_info를 선택하세요.",
      "criteria": {
        "python": "Python 중심 UI",
        "node": "Node.js 기반 웹 프런트엔드",
        "needs_more_info": "추가 요구 확인"
      }
    },
    "preference": {
      "type": "noul",
      "instructions": "사용자가 특정 구현 생태계를 명시했나요?"
    }
  },
  "routing": {
    "strategy": "failover",
    "allowed_backends": [
      "laya",
      "kev",
      "jev"
    ],
    "allow_remote": false,
    "timeout_ms": 30000,
    "laya_model": "auto"
  }
}

이 요청은 primary를 생략했으므로 서버 기본 우선순위를 따른다. 코드의 기본 순서는 Laya → Kev → Jev이지만, allow_remote=false이므로 Jev는 제외된다. 서버 설정에서도 원격을 금지했다면 요청이 true여도 그 제한을 풀 수 없다.

질문 두 개는 같은 state에서 독립적으로 판단할 수 있으므로 묶었다. 어느 답을 받아야만 다음 요청의 후보나 새로운 evidence를 만들 수 있다면 그때 호출을 나눈다. TypeSafe의 질문 의존성 설명

후보에는 needs_more_info를 넣었다. Python과 Node가 모두 설치되어 있다는 사실만으로 올바른 기술 선택이 결정되지는 않기 때문이다. 사용자가 이미 Python을 지정했다면 그 요구를 후보 투표로 뒤집지 않고 호스트가 제약으로 적용해야 한다.

현재 서버의 preflight는 언어·길이·준비 상태 같은 실행 적격성을 확인한다. 업무별 품질 인증까지 수행하는 것은 아니다. schema를 통과한 요청도 Laya의 짧은 head 예산에 들어가지 않으면 해당 backend에서 거절될 수 있다.

같은 confidence를 그대로 비교하면 안 되는 이유

응답 필드의 이름이 같아도 계산은 다를 수 있다.

2026년 10월 2일 확인한 TypeSafe 공식 문서는 Choice confidence를 다음과 같이 정의한다.

choice_confidence = (p_max - 1/n) / (1 - 1/n)

여기서 n은 후보 수다. 이진 분포가 [0.8, 0.2]라면 값은 0.6이다. TypeSafe의 Score confidence는 단계 간 거리를 반영하며, Noul에는 별도 confidence가 없다. TypeSafe Confidence

반면 프로젝트에서 고정한 Laya의 Choice·Score confidence는 1 - H(p)/ln(n)이다. 같은 [0.8, 0.2] 분포에서 약 0.2781이 된다. Laya Noul은 또 다른 계산인 max(p, 1-p)를 쓴다. Laya confidence 구현, Laya 반환 코드

그래서 공통 응답은 원래의 provider_confidence와 계산 종류를 보존하고, p_max, 1·2위 차이인 margin, entropy_confidence를 별도로 제공한다.

공통 지표를 계산했다고 calibration까지 같아지는 것은 아니다. 분포가 날카롭다는 것과 업무에서 정답을 잘 맞힌다는 것을 구분해야 한다. 한국어 기술 선택과 영어 고객 문의에 같은 threshold를 적용하려면 각각의 평가 근거가 필요하다.

Round-robin보다 먼저 필요한 적격성 검사

두 모델이 같은 primitive를 지원하면 교대로 호출할 수는 있다. 하지만 긴 Jev 요청을 Laya에 맞춰 자르면 같은 질문을 처리한 것이 아니다.

nar-server의 Laya preflight는 state만 검사하지 않는다. instructions와 후보 설명을 포함해 실제 tokenizer와 rendering 규칙으로 시퀀스를 비교하고, 내용이 잘리는 요청을 거절한다. 한국어·혼합 입력은 적절한 모델 경로인지 함께 검사한다.

세 전략의 동작도 구분했다.

전략 동작
pinned 선택한 backend 하나만 사용한다. 실패하면 그대로 반환한다.
failover 허용된 우선순위에서 다음 backend를 시도한다.
round_robin 사전 검사를 통과한 후보 사이에서 순환 선택한다. 선택 후 실패를 자동 failover와 결합하지 않는다.

failover는 요청 전체를 옮긴다. 한 요청의 일부 질문은 Laya, 나머지는 Jev의 답으로 조용히 섞지 않는다. 응답에는 실제 backend, 모델 revision, device, 각 시도와 오류를 남긴다.

현재 구현은 backend별 최대 한 번, 세 backend를 허용하면 최대 세 번 시도한다. 각 시도는 하나의 전체 deadline을 공유한다. 인증·입력 오류와 일시적인 연결 실패를 구분하고, SDK 내부 retry가 서버의 시도 횟수와 곱해지지 않도록 관리한다.

낮은 confidence는 정상적인 모델 응답이다. 이를 네트워크 장애처럼 취급해 더 자신만만한 모델이 나올 때까지 재시도하지 않는다. 품질 때문에 두 모델을 비교하는 기능은 가용성 failover와 별도로 평가해야 한다.

로컬 모델을 서버로 운영하면서 달라지는 것

모델을 호출할 수 있다는 것과 반복 요청을 안정적으로 처리하는 것은 다르다.

첫째, 모델 worker를 공유한다. stdio MCP 프로세스는 기존 HTTP backend를 사용한다. 클라이언트를 추가할 때마다 가중치를 다시 적재하지 않기 위한 구조다. 전역 라우팅 상태가 필요하면 HTTP gateway를 사용하고, stdio별 counter가 독립적이라는 점을 고려한다.

둘째, timeout과 작업 종료를 구분한다. 클라이언트가 기다리기를 멈춰도 GPU나 CPU 작업은 계속 실행될 수 있다. 실제 연산이 끝날 때까지 실행 슬롯을 유지하고 늦은 결과를 버려야, timeout이 동시 추론 폭증으로 이어지지 않는다.

셋째, stdout을 지킨다. stdio의 stdout은 MCP 메시지 전용이다. 라이브러리의 경고나 진행 상황을 그대로 흘리면 JSON-RPC가 깨질 수 있다. 추론 worker의 진단 출력과 MCP 응답 채널을 분리한다.

넷째, 가중치 용량과 peak VRAM을 구분한다. Laya 세 체크포인트의 FP32 파라미터 합은 약 4.34 GiB지만 activation, workspace, 드라이버와 allocator 메모리는 별도다. 파일이 FP16으로 저장됐다는 이유만으로 실제 적재도 FP16이라고 계산하지 않는다.

이 프로젝트의 AMD GPU 경로도 모델마다 다르다. Laya는 검증된 FP32 ONNX를 DirectML로 실행하고, Kev는 일부 MLP를 DirectML로 옮기며 나머지를 CPU에서 처리한다. 따라서 Kev의 실제 device는 directml+cpu로 표시한다. 이를 전체 모델의 GPU 실행이라고 설명하지 않는다.

확인한 범위와 다음 평가

9월 22–23일 개발 기록에는 다음 검증이 남아 있다.

  • 세 primitive와 공통 요청·응답 계약, 로컬 전용 정책, backend 우선순위와 장애 전환.
  • 실제 Laya·Kev 추론, Jev API 호출, MCP HTTP·stdio의 initialize/list/call.
  • 제한된 합성 샘플에서 CPU와 DirectML의 출력 비교, 실제 GPU kernel 실행 확인.
  • WebUI에서 backend 선택과 실제 응답 출처 확인.

이는 해당 날짜의 개발 기록이며, 이 글을 작성하면서 모든 live·GPU 검사를 다시 실행한 결과는 아니다. 코드와 문서를 확인해 현재 구조를 설명했고, 글의 요청 예제는 schema 검증 대상으로 다뤘다.

아직 필요한 평가는 별도로 남아 있다. 한국어 업무의 정답률, calibration, backend 간 paired 비교, 장시간 부하와 peak VRAM이다. 공개 benchmark나 API 호환성만으로 이 항목을 통과했다고 볼 수 없다.

다음 단계는 실제 사용할 판단 업무부터 좁히는 것이다. 정답과 보류 기준을 정하고 같은 state·질문·후보를 각 backend에 전달해 비교한다. 어떤 입력에서 모델을 바꿔도 되는지, 어떤 입력은 한 모델에 고정해야 하는지 확인한 뒤 라우팅 범위를 넓힐 수 있다.

nar-server에서 얻고자 하는 것은 관찰, 판단, 실행의 경계가 드러나는 에이전트 구조다. 호스트는 사실을 확인하고 요구를 책임지며, 판단 모델은 명시된 질문에 분포를 돌려주고, 서버는 그 판단의 입력과 출처를 보존한다.