hyunsu blog

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

LAYA 서비스 운영 — 그림자 비교, 판 교체, 되돌리기, 재학습 주기

목차 · 4
  1. 011. 그림자 비교 — 바꾸기 전에 실제 문의로
  2. 022. 교체와 되돌리기 — 이미지 태그로
  3. 033. 재학습 주기 — 1~15편을 한 장으로
  4. 04실습 — 운영 확인 목록

지속 학습한 판을 운영에 넣는 마지막 단계다. 새 판을 지금 판 옆에 그림자로 띄워 실제 문의에 함께 묻고, 이미지 태그를 바꿔 교체하고, 문제가 생기면 이전 태그로 되돌린다. 끝으로 1~15편의 고리를 재학습 주기 하나로 묶는다.

01 1. 그림자 비교 — 바꾸기 전에 실제 문의로

11편의 판정은 고정된 평가 데이터로 한 것이다. 실제 문의는 다를 수 있으니, 바꾸기 전에 새 판을 그림자로 돌린다. 고객에게는 지금 판(v1)의 답만 쓰고, 새 판(v2)의 답은 기록만 한다. 새 판이 틀려도 손해가 없다.

12편의 서버는 모델을 여럿 올릴 수 있다. v1 을 tickets, v2 를 tickets-next 로 함께 올린다.

bash
#!/bin/sh
# 지금 판(v1)과 새 판(v2)을 한 서버에 같이 올리고, 다음 주 문의 150건을 둘 다에 물어 비교한다(그림자 비교).
: "${LAYA_API_KEY:=$(python -c 'import secrets; print(secrets.token_hex(16))')}"  # 밖에서 주지 않으면 이번 실행용 키를 만든다
export LAYA_API_KEY
export LAYA_HOST=127.0.0.1 LAYA_PORT=8000
export LAYA_EXTRA_MODELS='{"tickets": "checkpoints/v1", "tickets-next": "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
python shadow.py
kill $SERVER
wait $SERVER 2>/dev/null || true
python
"""같은 문의를 서비스 중인 판(tickets)과 새 판(tickets-next)에 보내 답을 비교한다.

고객에게는 tickets 의 답만 쓰고, tickets-next 의 답은 기록만 한다 — 그래서 새 판이 틀려도 손해가 없다.
"""
import json
import os
import urllib.request

URL = "http://127.0.0.1:8000/v1/systemone"
HEADERS = {"content-type": "application/json", "authorization": f"Bearer {os.environ['LAYA_API_KEY']}"}


def ask(model, case):
    body = json.dumps({"model": model, "state": case["state"], "questions": case["questions"]}).encode()
    req = urllib.request.Request(URL, data=body, headers=HEADERS)
    with urllib.request.urlopen(req) as res:
        return json.load(res)["answers"]["team"]["choice"]


cases = [json.loads(line) for line in open("next-week-150.jsonl", encoding="utf-8")]
served_ok = next_ok = differ = 0
examples = []
for case in cases:
    served, nxt = ask("tickets", case), ask("tickets-next", case)
    gold = case["expected"]["team"]
    served_ok += served == gold
    next_ok += nxt == gold
    if served != nxt:
        differ += 1
        if len(examples) < 4:
            examples.append(f"  {case['state']}  served={served} next={nxt} gold={gold}")

n = len(cases)
print(f"cases: {n}, answers differ: {differ} ({differ / n:.0%})")
print(f"served (v1) correct: {served_ok / n:.3f}")
print(f"next   (v2) correct: {next_ok / n:.3f}")
print("where they differ:")
print("\n".join(examples))

그다음 주에 들어온 문의 150건(6편에서 만든 next-week-150.jsonl)에 둘 다 물었다.

bash
sh shadow.sh
text
cases: 150, answers differ: 4 (3%)
served (v1) correct: 0.973
next   (v2) correct: 1.000
where they differ:
  패딩 결제 취소했는데 카드 승인이 그대로예요 확인 좀 해 주세요.  served=account next=billing gold=billing
  등급 혜택 쿠폰이 이번 달엔 안 나왔어요 빨리 확인 부탁드려요.  served=billing next=account gold=account
  같은 주문이 결제 완료로 두 개 떠요  served=technical next=billing gold=billing
  청바지 결제 취소했는데 카드 승인이 그대로예요 빨리 확인 부탁드려요.  served=account next=billing gold=billing
그림 1.그림자 비교 — 다음 주 문의 150건
0.900.920.940.960.981.00v1 (서비스)v2 (그림자)v1 (서비스): 0.973v2 (그림자): 1.0000.9731.0000.900.920.940.960.981.00v1 (서비스)v2 (그림자)v1 (서비스): 0.973v2 (그림자): 1.0000.9731.000

답이 다른 것은 4건(3%) — 모두 v2 가 맞혔다

숫자로 보기
항목정답 비율
v1 (서비스)0.973
v2 (그림자)1.000
  • 150건 가운데 4건(3%)만 답이 달랐다. 나머지 146건은 둘이 같은 답을 냈다. 판을 바꿔도 대부분의 고객에게는 아무 차이가 없다는 뜻이다.
  • 다른 4건은 모두 v2 가 맞혔다. "결제 취소했는데 카드 승인이 그대로예요" 는 결제팀인데 v1 은 계정팀으로 보냈다. 9편에서 사람이 고친 바로 그 유형이다.
  • 실제 운영에서는 정답이 바로 없다. 답이 다른 건만 사람이 보면 된다. 150건 가운데 4건이면 검토할 양도 적다.

이 다음 주 문의는 9편의 운영 문의와 같은 표현을 많이 공유해서, v2 가 학습한 표현이 다시 나왔다. 그래서 v2 가 1.000 이다. 실제 서비스에서도 운영 문의는 비슷한 표현이 되풀이되니, 운영에서 배운 판이 운영에서 강한 것은 자연스럽다. 다만 평가 데이터(11편)의 0.815 가 처음 보는 표현에 대한 더 보수적인 숫자다.

02 2. 교체와 되돌리기 — 이미지 태그로

14편과 같은 방법으로 v1 도 이미지로 만들어 두었다. 판마다 이미지가 하나씩 있다.

text
image laya-tickets:v1 size 1.87GB

교체는 지금 컨테이너를 멈추고 새 태그로 띄우는 것이다. 되돌리기는 같은 일을 이전 태그로 하는 것이다.

bash
#!/bin/sh
: "${LAYA_API_KEY:=$(python -c 'import secrets; print(secrets.token_hex(16))')}"  # 밖에서 주지 않으면 이번 실행용 키를 만든다
export LAYA_API_KEY
# 판 교체와 되돌리기 — 같은 포트에 v2 이미지를 띄웠다가, 문제가 생긴 셈 치고 v1 이미지로 되돌린다.
answer() {
  curl -s localhost:8001/v1/systemone -H 'content-type: application/json' \
    -H "authorization: Bearer $LAYA_API_KEY" -d @request.json |
    python -c 'import json,sys; t=json.load(sys.stdin)["answers"]["team"]; print("  team", t["choice"], t["probabilities"])'
}
start() {
  docker run -d --rm --name laya-tickets -p 127.0.0.1:8001:8000 -e LAYA_API_KEY laya-tickets:$1 > /dev/null
  for i in $(seq 1 120); do curl -s -o /dev/null localhost:8001/health && break; sleep 1; done
  echo "serving laya-tickets:$1"
}
stop() { docker stop laya-tickets > /dev/null; }

docker image ls laya-tickets --format '{{.Repository}}:{{.Tag}} {{.Size}}' | sort
start v2; answer; stop
echo "rollback"
start v1; answer; stop

요청은 그림자 비교에서 둘이 다르게 답한 문의 "패딩 결제 취소했는데 카드 승인이 그대로예요" 다.

bash
sh swap.sh
text
laya-tickets:v1 1.87GB
laya-tickets:v2 1.87GB
serving laya-tickets:v2
  team billing {'billing': 1.0, 'delivery': 0.0, 'account': 0.0, 'technical': 0.0}
rollback
serving laya-tickets:v1
  team account {'billing': 0.004, 'delivery': 0.0, 'account': 0.996, 'technical': 0.0}

v2 로 바꾸자 결제팀(billing 1.0)으로 맞게 갔고, v1 로 되돌리자 다시 계정팀(account 0.996)으로 갔다. 어느 판이 돌고 있는지가 태그 하나로 정해지고, 되돌리기가 명령 한 줄이다. 실제 서버에서는 멈췄다 띄우는 사이 요청이 끊기지 않게, 새 컨테이너를 먼저 띄워 /health 를 확인한 뒤 프록시(또는 로드 밸런서)의 대상을 바꾸고 옛 컨테이너를 멈춘다.

03 3. 재학습 주기 — 1~15편을 한 장으로

bash
# 1) 운영: 서비스 판이 판단하고, 확신이 낮은 것과 표본을 사람이 본다 (9편)
python collect.py 0.99 0.30 data/feedback.jsonl
# 2) 데이터 쌓기: 이전 학습 데이터 + 새 정답 (10편)
cat data/train-v2.jsonl data/feedback.jsonl > data/train-v3.jsonl
# 3) 학습: 바탕 모델에서 누적 데이터로 처음부터 (10편)
laya-train --data data/train-v3.jsonl --base multilingual --out checkpoints/v3
# 4) 판정: 고정 평가 데이터로 지금 판과 비교, 기준을 넘을 때만 (11편)
python promote.py checkpoints/v2 checkpoints/v3
# 5) 이미지: 통과한 판만 (14편)
sh build.sh v3
# 6) 그림자 비교 → 교체 → 문제 있으면 v2 로 되돌리기 (이 편)

주기는 데이터가 쌓이는 속도로 정한다. 새 정답이 수십 건 모였을 때마다, 처음에는 1~2주에 한 번이 알맞다. 학습은 맥북에서 몇 분이니 사람이 결과를 보며 손으로 돌려도 된다. 판이 안정되면 1)~4) 를 정기 작업으로 묶고, 5)~6) 은 사람이 승인한다.

04 실습 — 운영 확인 목록

확인왜
응답의 routing.model 이 기대한 이름인가이름이 틀리면 오류 없이 기본 모델로 간다(12편)
표본 검사의 실제 오류율이 판마다 줄고 있나지속 학습이 되고 있다는 첫 지표(9편)
판정 기록(registry.jsonl)과 이미지 태그가 맞나지금 도는 판이 승인된 판인지(11편)
API 키를 이미지가 아니라 실행 환경에서 넘기나이미지가 새도 키는 안전하다(14편)
모델 파일을 확인하고 올리나체크포인트 해시를 기록해 두고 expected_sha256 으로 확인할 수 있다(공식 문서의 Checkpoint integrity)
평가 데이터를 가끔 새 운영 문의로 보태나평가가 오래되면 새 표현을 못 잰다(10편)

여기까지가 첫 사례의 한 바퀴다. 설치·테스트(1~5편), 내 모델 만들기(6~8편), 지속 학습(9~11편), 배포(12~15편)였다. 16편에서 이 사례의 결과를 한자리에 모아 지속 학습이 정답률을 올렸는지 정리하고, 17~23편에서 다른 사례(쇼핑 검색어를 필터로)로 같은 과정을 한 번 더 하고, 24편에서 LAYA 를 LAYA 답게 쓰는 법을 정리한다. 이 강좌의 숫자는 합성 데이터로 잰 것이라 실제 서비스에서는 달라진다. 방법은 그대로 쓰되, 숫자는 늘 자기 데이터로 다시 잰다.