대화 을 직접 만들어 전체 흐름을 완성합니다. xchat.py 로 질문을 보내 내 와 직접 한 이 답하게 해 보세요.
9
XChat 만들기
은 대화 화면, 는 실행 서비스, 은 한 입니다. xchat.py는 질문을 읽어 학습 형식을 붙이고 요청을 보낸 뒤 결과를 보여 줍니다. 모델을 직접 불러오거나 답을 스스로 만들어 내지 않습니다.
첫 XChat은 안에서 실행합니다. 지금은 을 보내지 않아 질문마다 독립적입니다. 다음 도전은 작은 길이에 맞게 대화 기록을 붙이는 것입니다. 유창함보다 내가 만든 모델까지 요청이 실제로 왕복하는지가 완성 기준입니다.
요청이 지나가는 길 — XChat 에서 가중치까지
XChat 에 hi 를 치고 Enter 를 누르면 이런 일이 차례로 일어납니다.
xchat.py 가 질문을 학습 형식으로 감쌉니다. "Q: hi" 다음 줄에 "A: ".
{"": …, "": 24} 를 만들어 http://127.0.0.1:8765/generate 로 합니다.
Runner 의 do_POST 가 본문을 읽고 검사한 뒤 generate 를 부릅니다.
generate 가 로 번호를 만들고, 의 로 토큰을 하나씩 뽑고, 에서 멈춥니다.
Runner 가 {"text": …} 로 응답하고, xchat.py 가 XSLM> 뒤에 보여 줍니다.
이 길의 어느 한 곳이 끊기면 대화가 안 됩니다. 그래서 문제가 생기면 길을 따라 하나씩 확인합니다. Runner 가 켜져 있나( ), 생성이 되나(curl /generate), XChat 의 주소와 가 맞나.
XChat 클라이언트 — 입력 루프와 HTTP 요청
xchat.py 는 두 부분입니다.
ask(question): 요청 하나를 보내고 답을 돌려줍니다. Request 로 주소·본문·헤더를 담은 POST 요청을 만들고, 으로 보낸 뒤 json.load 로 응답 본문을 읽어 text 를 꺼냅니다. timeout=120 은 2분 안에 답이 없으면 포기한다는 뜻입니다. 느린 에서 긴 생성을 기다릴 수 있게 넉넉히 잡았습니다.
입력 루프: while True 로 계속 You> 를 보여 주며 한 줄씩 읽습니다. /exit 이면 끝내고, 빈 줄은 건너뛰고, 나머지는 ask 에 넘겨 XSLM> 뒤에 답을 찍습니다.
질문마다 settings.json 을 다시 읽습니다. 프롬프트 형식("Q: " … "A: ")은 가 붙입니다. 가 형식을 붙이도록 만들면 클라이언트는 형식을 몰라도 됩니다. 어느 쪽이든 형식은 한 곳에서만 정해야 어긋나지 않습니다.
오류 다루기 — 가짜 답을 만들지 않는다
xchat.py 는 오류를 이렇게 나눠 다룹니다.
EOFError·KeyboardInterrupt: (입력 끝)나 를 누른 것입니다. 루프를 끝내고 로 돌아갑니다.
URLError: Runner 에 연결하지 못했습니다( 등). Runner 가 꺼져 있거나 포트가 다릅니다.
HTTPError: 연결은 됐지만 Runner 가 400 같은 오류 코드로 답했습니다. URLError 의 한 종류라 같은 줄에서 잡힙니다.
TimeoutError: 정한 시간 안에 답이 오지 않았습니다.
어느 경우든 "Runner: 오류 내용" 을 보여 주고 다음 질문을 기다립니다. 연결이 안 될 때 미리 정해 둔 문장을 대신 보여 주는 식으로 감추지 않습니다. 그래야 무엇이 고장 났는지 바로 압니다. Runner 를 끄고 질문해 보고(오류가 나야 정상), 다시 켜서 대화가 돌아오는지 확인하세요.
대화 기록 붙이기 — 문맥 길이의 한계
지금 XChat 은 질문마다 독립적입니다. 앞에서 한 말을 모델이 모릅니다. 실제 이 앞 대화를 기억하는 것처럼 보이는 까닭은, 매번 지난 대화 전체를 프롬프트 앞에 붙여 보내기 때문입니다. 모델 자체에는 기억이 없습니다.
XSLM 으로 해 보면 곧 벽에 부딪힙니다. 아래 실험은 대화 한 번(Q: hi / A: hi friend.) 뒤에 새 질문 cat 을 붙입니다. 프롬프트는 21토큰인데 모델은 마지막 12토큰만 보므로, 실제로 보는 글은 "iend." 부터입니다. 앞 대화는 잘려 나갑니다.
그래서 대화 기록을 붙이려면 이런 일을 함께 합니다.
오래된 대화부터 버려 문맥 길이 안에 맞춥니다.
context 를 키운 모델을 새로 학습합니다(위치 표가 커지고 계산이 느려집니다).
에도 여러 차례 이어진 대화를 넣어야 모델이 그 모양을 배웁니다.
다음 도전: xchat.py 에 history 목록을 두고 지난 질문·답을 붙여 보내 보세요. 토큰 수를 세어 12를 넘으면 앞부분을 버리게 만듭니다.
직접 확인하기 lab/context.py
import json
from pathlib import Path
from tokenizer import BPE
tok = BPE(**json.loads(Path('data/tokenizer.json').read_text()))
size = json.loads(Path('settings.json').read_text())['context']
history = 'Q: hi\nA: hi friend.\nQ: cat\nA: '
ids = tok.encode(history)
print(len(ids), 'tokens; the model sees the last', size)
print(repr(tok.decode(ids[-size:])))
cd ~/xslm
mkdir -p lab
micro lab/context.py
python3 -m lab.context
실행 결과
21 tokens; the model sees the last 12
'iend.\nQ: cat\nA: '
실제 대화 모델은 어떻게 만드나 — 사전 학습 다음 단계
XSLM 은 Q/A 형식의 글로 처음부터 학습했습니다. · 같은 대화 모델은 보통 여러 단계를 거칩니다.
(pre-training): 아주 많은 글로 다음 토큰 맞히기를 합니다. XSLM 의 train.py 가 하는 일과 같고 규모만 다릅니다.
(SFT, instruction tuning): 질문과 좋은 답의 쌍으로 더 학습해 지시를 따르는 습관을 들입니다. 사용자·도우미 차례를 나누는 대화 형식()도 이때 익힙니다.
선호 학습( 등): 사람이 더 낫다고 고른 답 쪽으로 기울도록 다듬습니다.
최신 정보가 필요하면 다시 학습하는 대신 를 씁니다. 질문으로 문서를 검색해 찾아 프롬프트에 함께 넣고, 모델은 그 문서를 보며 답합니다. 자료 형식에 [DOC] 같은 문서 칸을 두고 문서를 참고해 답하는 모양을 미리 학습해 두면, 나중에 검색 결과를 그 칸에 넣어 쓸 수 있습니다.
다음 도전 — XSLM 을 키우는 방법
완성한 XSLM 으로 해 볼 수 있는 실험입니다. 하나씩 바꾸고 과 답을 기록하세요.
자료 늘리기: 질문을 여러 표현으로, 답을 같은 형식으로 수십~수백 줄 씁니다.
학습 늘리기: ·학습 횟수를 바꾸며 학습·을 비교합니다.
모델 키우기: width 를 16으로, context 를 24로 바꿉니다. 수와 걸음마다의 시간이 어떻게 변하는지 잽니다.
쌓기: model.py 의 + 를 로 묶고 를 블록마다 따로 만들어 두 번 지나가게 합니다.
나누기: 폭 8을 4칸씩 두 헤드로 나눠 따로 어텐션한 뒤 이어 붙입니다.
더하기: generate 에 로 후보를 자르는 옵션을 만듭니다.
·대화 기록: Runner 와 XChat 에 붙여 봅니다.
여기까지 할 수 있게 되면 에서 로 같은 구조를 다시 써 보세요. 로 계산하면 수천 배 빠르게, 수천만 파라미터를 학습할 수 있습니다. 의 ·build-nanogpt 도 좋은 다음 교재입니다.
직접 타이핑하며 실습하기
Runner를 켜 둔 채 xchat.py를 작성하고 실행하세요. You>에서 hi를 치고 Enter를 누릅니다. /exit로 bash로 나온 다음에만 마지막 으로 Runner를 끝냅니다. 완성한 프로젝트를 내보내세요.
cd ~/xslm
micro xchat.py
python3 xchat.py
hi
/exit
kill "$(cat runner.pid)"
질문마다 settings.json 을 읽어 토큰 수와 포트를 얻습니다. 질문을 "Q: " + 질문 + 줄바꿈 + "A: " 로 감싸 학습 형식을 맞춥니다. Request 에 를 주면 POST 가 되고, 본문은 JSON , 헤더는 입니다. with urlopen(…) 은 응답을 다 읽은 뒤 연결을 닫아 줍니다. json.load 가 응답을 사전으로 읽고, ask 는 text 만 돌려줍니다.
xchat.py 대화 루프
if __name__ == '__main__': print('XChat -> Runner -> XSLM | /exit') while True: try: question = input('You> ').strip() if question == '/exit': break if question: print('XSLM>', ask(question)) except (EOFError, KeyboardInterrupt): break except (URLError, TimeoutError) as error: print('Runner:', error)
시작 안내를 찍고 while True 로 질문을 받습니다. input 은 You> 를 보여 주고 Enter 까지 한 줄을 읽으며, strip 으로 앞뒤 공백을 지웁니다. /exit 면 break 로 끝내고, 빈 질문은 건너뜁니다. Ctrl+D 는 EOFError, Ctrl+C 는 KeyboardInterrupt 로 들어와 역시 끝냅니다. Runner 연결 오류와 시간 초과는 알려 주고 다음 질문을 기다립니다.
여기까지 이해했나요?
XChat → HTTP Runner → 저장한 가중치 → 생성한 글의 흐름을 보여 주세요. Runner를 끄고 대화하면 연결 오류가 나야 합니다. 다시 켜면 대화가 돌아오나요? 각 파일이 맡은 일을 다른 사람에게 설명해 보세요.
막혔을 때 확인하기
XChat 안에서 kill을 입력하면 명령이 아니라 질문으로 보내집니다. 먼저 /exit로 나가세요. 내용이 어설퍼도 실제 모델의 응답이 돌아오면 연결은 완성입니다. 다음에는 자료와 학습을 개선해 보세요.
자주 묻는 질문
XChat 에서 kill 을 쳤는데 Runner 가 안 꺼져요.
You> 에 친 글은 명령이 아니라 질문으로 Runner 에 보내집니다. 먼저 /exit 나 Ctrl+D 로 XChat 을 끝내 bash 프롬프트($)로 돌아온 뒤 kill 명령을 칩니다.
Runner: <urlopen error [Errno 111] Connection refused> 가 나와요.
Runner 가 꺼져 있거나 아직 켜지는 중입니다. cat runner.log 로 시작 문구(XSLM Runner: …)를 확인하고, curl -s http://127.0.0.1:8765/health 가 답하는지 본 뒤 다시 질문하세요. 새로 고침을 했다면 Runner 를 다시 켜야 합니다.
답이 질문과 상관없어요. 연결이 잘못된 건가요?
XSLM> 뒤에 무엇이든 생성된 글이 나왔다면 연결은 맞습니다. 내용은 모델의 실력입니다. 같은 질문을 generate.py --prompt 로 직접 실행해 보세요. 설정과 가 같으면 Runner 를 거친 답과 똑같이 나옵니다.