hyunsu blog

홈강좌LAYA 결정 모델 24편 중 12편째

내 LAYA 를 API 로 — laya-serve 로 로컬 서버 띄우기

목차 · 3
  1. 01서버를 띄우는 스크립트
  2. 02실행
  3. 03실습 — 모델 이름을 잘못 쓰면

laya-serve 는 LAYA 에 들어 있는 HTTP 서버다. 환경 변수 셋으로 내가 학습한 v2 를 tickets 라는 이름으로 올리고, API 키를 걸고, curl 로 /v1/systemone 에 묻는다. 모델 이름을 잘못 쓰면 오류 없이 기본 모델로 가는 함정도 실제로 본다.

01 서버를 띄우는 스크립트

bash
#!/bin/sh
# 내 체크포인트(v2)를 laya-serve 로 127.0.0.1:8000 에 띄우고, 요청을 몇 개 보내 본 뒤 끈다.
export LAYA_HOST=127.0.0.1
export LAYA_PORT=8000
: "${LAYA_API_KEY:=$(python -c 'import secrets; print(secrets.token_hex(16))')}"  # 밖에서 주지 않으면 이번 실행용 키를 만든다
export LAYA_API_KEY
export LAYA_EXTRA_MODELS='{"tickets": "checkpoints/v2"}'

laya-serve > server.log 2>&1 &
SERVER=$!
for i in $(seq 1 90); do
  curl -s -o /dev/null localhost:8000/health && break
  sleep 1
done

echo "== health (no key)"
curl -s localhost:8000/health; echo
echo "== no key -> 401"
curl -s -o /dev/null -w "%{http_code}\n" localhost:8000/v1/systemone -H 'content-type: application/json' -d @request.json
echo "== with key, model tickets"
curl -s localhost:8000/v1/systemone \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $LAYA_API_KEY" \
  -d @request.json | python -m json.tool
echo "== typo in model name (tikets)"
sed 's/"tickets"/"tikets"/' request.json | curl -s localhost:8000/v1/systemone \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $LAYA_API_KEY" \
  -d @- | python -c 'import json,sys; r=json.load(sys.stdin); print("routed to:", r["routing"]["model"], "-", r["routing"]["reason"]); print("team:", r["answers"]["team"]["choice"])'

kill $SERVER
wait $SERVER 2>/dev/null || true
  • LAYA_HOST=127.0.0.1 — 이 컴퓨터 안에서만 받는다. 기본값은 0.0.0.0(모든 네트워크)이라, 노트북에서 시험할 때는 꼭 바꾼다.
  • LAYA_API_KEY — 값을 주면 모든 요청에 Authorization: Bearer <키> 가 있어야 한다. 주지 않으면 인증 없이 열린다(공식 문서). 스크립트는 밖에서 키를 주지 않으면 실행할 때마다 무작위 키를 만든다(secrets.token_hex). 파일에 키 값을 적어 두지 않기 위해서다. 실제 서비스의 키는 파일·비밀 저장소로 넘긴다(15편).
  • LAYA_EXTRA_MODELS — {이름: 체크포인트} 로 내 모델을 등록한다. 10편에서 만들고 11편에서 승격한 v2 를 tickets 라는 이름으로 올렸다.

나머지는 서버가 뜰 때까지 /health 를 기다렸다가 요청 셋을 보내고 끈다. 요청 본문은 이렇다.

json
{
  "model": "tickets",
  "state": "결제창이 계속 로딩만 되고 넘어가지 않아요. 빨리 확인 부탁드려요.",
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "이 문의를 맡을 팀은?",
      "criteria": {
        "billing": "결제·환불·쿠폰·영수증",
        "delivery": "배송·교환·반품",
        "account": "로그인·비밀번호·회원·포인트",
        "technical": "앱·웹사이트 오류, 결제창 오류 포함"
      }
    },
    "urgent": {"type": "noul", "instructions": "고객이 서두르거나 불편을 드러내는가?"}
  }
}

model 칸에 등록한 이름(tickets)을 쓴다. 질문은 학습 때와 똑같은 문장과 선택지다(6편).

02 실행

bash
sh serve.sh
text
== health (no key)
{"status":"ok"}
== no key -> 401
401
== with key, model tickets
{
    "model": "laya-rl-agent",
    "answers": {
        "team": {
            "type": "choice",
            "choice": "technical",
            "probabilities": {
                "billing": 0.0,
                "delivery": 0.0,
                "account": 0.0,
                "technical": 1.0
            },
            "confidence": 1.0,
            "answer_confidence": 1.0,
            "action": {
                "act_probability": 1.0
            },
            "x_jev_confidence": 1.0
        },
        "urgent": {
            "type": "noul",
            "noul": 1.0,
            "confidence": 1.0,
            "answer_confidence": 1.0,
            "action": {
                "act_probability": 1.0
            }
        }
    },
    "usage": {
        "input_tokens": 180,
        "output_tokens": 0,
        "state_tokens": 30,
        "state_tokens_dropped": 0,
        "truncated": false,
        "truncated_questions": []
    },
    "routing": {
        "model": "tickets",
        "repo": "checkpoints/v2",
        "reason": "explicit model='tickets'",
        "detection": null,
        "workflow": null
    }
}

열려 있는 것과 닫힌 것

  • /health 는 키 없이도 {"status":"ok"} 만 돌려준다. 로드 밸런서·컨테이너가 살아 있는지 확인하는 용도다. 키를 보내면 어떤 판이 올라가 있는지 같은 정보가 더 붙는다.
  • 키 없이 /v1/systemone 을 부르면 401 이다.

응답

3편의 predict 결과와 같은 모양이 HTTP 로 왔다.

  • answers.team.choice 가 technical 이다. "결제창이 계속 로딩만 되고" 는 결제 낱말이 들어 있지만 회사 규칙으로는 기술팀이다. 학습 전 기본 모델이 가장 많이 틀린 유형인데, v2 는 확신 1.0 으로 맞혔다.
  • x_jev_confidence 는 JEV 클라이언트와 맞추려고 붙는 칸이다(1편). JEV 를 쓰던 코드가 주소만 바꿔 옮겨 올 수 있는 이유다.
  • usage 의 state_tokens_dropped: 0·truncated: false 는 문장이 잘리지 않았다는 뜻이다. 긴 문서를 보내면 여기서 잘린 양을 알려 준다.
  • routing.reason: explicit model='tickets' — 등록한 v2(checkpoints/v2)가 답했다.

03 실습 — 모델 이름을 잘못 쓰면

마지막 요청은 tickets 를 tikets 로 잘못 쓴 것이다.

text
== typo in model name (tikets)
routed to: multilingual - non-Latin script (hangul, 100% of letters); the English checkpoint cannot read it
team: technical

오류가 나지 않았다. 서버는 모르는 이름을 "모델을 지정하지 않음" 으로 보고, 언어를 보고 공개 다국어 체크포인트로 보냈다. JEV 클라이언트가 보내는 jev-1 같은 이름을 받아 주려는 동작이다(서버 소스에 이유가 적혀 있다). 답도 technical 로 같아 보이니 알아채기 어렵다. 하지만 이 문의는 학습하지 않은 모델이 판단한 것이다.

그래서 클라이언트는 응답의 routing.model 이 기대한 이름(tickets)인지 확인한다. 다르면 로그를 남기고 알린다. 판을 바꾸는 15편에서도 같은 확인으로 "지금 어느 판이 답하는지" 를 본다.

다음 편에서 이 서버를 어디에 올릴지 고르고, 14편에서 컨테이너 이미지로 만든다.