AI 개발 · 기본
바이브 코딩 시대의 기본서 - 처음 AI 와 함께 만드는 프로그램
AI 는 코드를 어떻게 만드나 - 그럴듯한 다음 낱말과 그럴듯한 오답
언어 모델이 다음 낱말을 고르는 원리(확률·문맥)를 비유로, 왜 자신 있게 틀리는가(없는 함수·옛 문법), 같은 요청에 다른 답이 나오는 이유, 완성 코드는 작은 문장 묶음에서 다음 낱말 빈도표를 만들고 seed 고정 표본 추출로 문장을 이어 쓰는 장난감 모델
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 바이브 코딩을 말로 요청하고 눈으로 확인하는 작업으로 살펴보았다. 이제 그 말을 받은 AI가 어떻게 코드를 이어 쓰는지 알아본다. AI의 답을 읽다 보면 설명이 자연스럽고 코드 모양도 익숙해서, 이미 실행까지 마친 결과처럼 느껴질 수 있다. 그러나 자연스럽게 이어진 답과 실제로 작동하는 프로그램은 서로 다른 기준으로 확인해야 한다.
이 장에서는 문장 몇 개에서 다음 낱말의 빈도를 세고, 그 비율에 따라 낱말을 골라 문장을 잇는 작은 프로그램을 만든다. 실제 AI를 재현하려는 프로그램은 아니다. 문맥에 따라 후보가 달라지고, 후보 중 하나를 고르면 결과도 달라진다는 원리를 눈으로 확인하는 도구다. 실행 환경을 준비하는 구체적인 순서는 다음 장에서 다룬다. 여기서는 먼저 요청과 결과를 연결해서 읽어 본다.
- 언어 모델이 문맥을 바탕으로 다음 조각의 확률을 계산한다는 뜻을 설명한다.
- 그럴듯한 코드에 없는 함수나 현재 환경과 맞지 않는 사용법이 들어가는 이유를 구별한다.
- 같은 요청에서 서로 다른 답이 나올 수 있는 이유를 이해한다.
- 빈도표와 난수의 시작값을 이용한 작은 문장 생성 프로그램을 읽는다.
- AI의 설명과 별개로 실행 결과와 검사 결과를 확인하는 습관을 익힌다.
문제 상황
친구들과 나눠 쓸 단어 목록을 정리한다고 생각해 보자. 같은 단어가 여러 번 들어 있어서 AI에게 중복을 없애는 프로그램을 부탁했다. AI는 짧은 코드와 함께 “이 함수를 사용하면 중복을 제거할 수 있다”는 설명을 돌려준다. 그런데 코드를 실행하자 해당 함수가 없다는 오류가 나온다. 설명은 매끄러웠는데 프로그램은 첫 작업도 끝내지 못한 것이다.
다시 같은 요청을 보내면 이번에는 다른 코드가 나온다. 첫 답이 정답이었다면 왜 답이 바뀌는지, 두 번째 답이 정답이라면 왜 처음부터 그렇게 쓰지 않았는지 궁금해진다. 이때 AI를 변덕스러운 사람처럼 생각하기보다, 답을 만드는 방식을 알아두면 상황을 이해하기 쉽다.
요청: 단어 목록에서 중복을 없애는 Python 프로그램을 만들어 줘. 입력은 프로그램 안의 목록으로 두고, 중복을 없앤 결과를 출력해 줘.
AI의 답에서 확인할 부분: 실제로 존재하는 기능을 썼는가. 단어의 원래 순서를 유지했는가. 제시한 결과가 실행 결과와 같은가.
이 요청에는 아직 “원래 순서를 유지한다”는 조건이 없다. 따라서 서로 다른 코드가 나왔다고 해서 모두 오류는 아니다. 반면 존재하지 않는 함수를 호출했다면 요구사항 해석과 별개로 고쳐야 한다. 답이 달라지는 현상과 답이 틀리는 현상을 구분하는 것이 이 장의 출발점이다.
다음 낱말을 고른다는 뜻
휴대전화에서 “오늘 점심은”까지 입력했을 때 뒤에 붙일 만한 말을 제안받는 장면을 떠올려 보자. 앞에 무엇을 썼는지에 따라 어울리는 후보가 달라진다. “오늘 점심은” 뒤와 “파일 이름은” 뒤에는 서로 다른 말이 자연스럽다. 이렇게 앞뒤 관계를 판단하는 데 쓰이는 정보를 문맥이라고 한다.
언어 모델(language model)은 주어진 문맥 뒤에 어떤 글 조각이 이어질지를 계산하는 모델이다. 실제 모델이 다루는 단위는 항상 띄어쓰기 기준의 낱말은 아니다. 글자 일부, 낱말, 문장부호 등이 하나의 처리 단위가 될 수 있으며 이를 토큰(token)이라고 한다. 이 장에서는 원리를 보기 쉽도록 띄어쓰기로 나눈 낱말을 사용한다.
모델은 다음 후보마다 확률을 계산한다. 확률은 어떤 후보를 고를 가능성을 수로 나타낸 것이다. 한 후보의 확률이 크다는 것은 현재 문맥에서 그 후보가 더 잘 이어질 것으로 계산했다는 뜻이다. 그 내용이 사실이라는 보증은 아니다. 예를 들어 함수 이름처럼 보이는 글 조각이 자연스럽게 이어져도, 실제 Python에 그 함수가 있다는 결론으로 바로 넘어갈 수는 없다.
다음 표는 이 장의 프로그램에서 사용할 문장이다. 사람이 직접 만든 짧은 자료이므로, 어떤 관계를 배우는지 모두 들여다볼 수 있다. “나는” 다음에는 “차를”과 “책을”이 한 번씩 나온다. 이 자료만 놓고 보면 두 후보의 선택 비율은 같다.
| 문장 자료 | 살펴볼 낱말 | 바로 다음 낱말 |
|---|---|---|
| 나는 차를 마신다 | 나는 | 차를 |
| 나는 책을 읽는다 | 나는 | 책을 |
| 너는 차를 마신다 | 너는 | 차를 |
| 너는 책을 읽는다 | 너는 | 책을 |
여기서 빈도는 자료에서 나온 횟수다. “차를” 뒤의 낱말을 세면 “마신다”가 두 번 나온다. “책을” 뒤에서는 “읽는다”가 두 번 나온다. 프로그램은 이 관계를 표처럼 저장하고, 현재 낱말에 해당하는 후보 중 하나를 고른다. 고른 낱말을 문장에 붙이고 같은 일을 반복하면 문장이 조금씩 길어진다.
다만 실제 언어 모델을 거대한 낱말 빈도표라고 이해해서는 안 된다. 실제 모델은 학습 과정에서 조정된 많은 수를 이용해 더 긴 문맥의 관계를 계산한다. 이 장의 프로그램은 바로 앞 낱말 하나만 본다. 따라서 “나는”에 이르기 전까지 어떤 대화를 나눴는지 기억하거나 요청의 목적을 판단하지 못한다. 다음 후보를 계산하고 이어 붙인다는 공통점만 떼어 낸 장난감이다.
자연스러운 설명이 정답을 보장하지 않는 이유
코드에는 반복해서 나타나는 모양이 많다. 자료를 읽고, 이름을 붙이고, 함수를 호출하고, 결과를 출력하는 모양이 여러 프로그램에 등장한다. 모델은 이런 관계를 이용해 새 코드를 만들 수 있다. 하지만 그럴듯한 이름을 만들 수 있다는 능력과, 지금 설치된 환경에서 그 이름을 사용할 수 있다는 사실은 다르다.
가령 중복 제거를 부탁했을 때 “중복을 지우다”라는 뜻이 드러나는 함수 이름을 제안할 수 있다. 이름이 의도와 잘 맞더라도 실제 기능이 아닐 수 있다. 다른 도구의 사용법과 섞였거나, 특정 버전에만 있던 기능을 현재도 쓸 수 있는 것처럼 설명할 수도 있다. 예전 문법이나 예전 호출 방식을 현재 환경에 적용하는 답도 같은 관점에서 살펴봐야 한다.
모델이 자신 있게 말하는 듯 보이는 이유도 여기와 연결된다. 설명문에서 흔히 쓰는 단정적인 문장 역시 생성할 수 있기 때문이다. “이 코드는 정상적으로 작동한다”는 문장은 실행 기록이 아니다. 코드를 실행하는 기능이 연결된 도구라면 실제 실행을 거칠 수도 있지만, 그 경우에도 무엇을 어떤 입력으로 확인했는지 기록을 따로 봐야 한다.
물론 다음 조각을 예측한다는 설명만으로 AI가 코드를 다루는 모든 능력을 설명할 수는 없다. 학습한 관계를 바탕으로 조건을 연결하거나 여러 단계의 문제를 해결하는 답도 만든다. 여기서 기억할 것은 능력이 없다는 주장이 아니라, 답의 말투를 검증의 증거로 삼을 수 없다는 점이다.
| 답에서 보이는 것 | 남아 있는 의문 | 확인할 증거 |
|---|---|---|
| 뜻이 잘 드러나는 함수 이름 | 실제로 제공되는 기능인가 | 현재 환경에서의 실행과 해당 기능의 설명 |
| 자세한 코드 해설 | 해설과 코드의 동작이 같은가 | 작은 입력을 넣었을 때의 실제 출력 |
| 정상 작동한다는 문장 | 실행을 했는가, 무엇을 확인했는가 | 실행 명령, 사용한 입력, 검사 결과 |
| 수정한 코드 전체 | 요청한 부분만 바뀌었는가 | 이전 코드와 수정 코드의 차이 |
이 장에서도 같은 태도를 적용한다. 먼저 자료가 어떻게 집계되어야 하는지 정하고, 코드 안에서 그 값을 검사한다. 그다음 출력과 예상 결과를 비교한다. AI에게 수정을 부탁했다면 파일의 바뀐 부분을 보여 주는 차이 비교(diff)도 살펴본다. 실행 성공만으로 모든 동작이 맞다고 판단하지 않고, 설명만으로 실행을 대신하지도 않는다.
같은 요청에 다른 답이 나오는 이유
후보 중 확률이 가장 큰 것만 계속 고를 수도 있고, 계산된 확률에 따라 여러 후보 중 하나를 뽑을 수도 있다. 뒤의 방식을 표본 추출(sampling)이라고 한다. 상자에 후보 이름이 적힌 종이를 넣는 모습을 생각하면 쉽다. 같은 이름의 종이가 많이 들어 있을수록 그 이름이 뽑힐 가능성이 커진다.
가능성이 크다는 말은 매번 뽑힌다는 뜻이 아니다. 두 후보의 비율이 같더라도 세 번 뽑았을 때 한 후보가 두 번 나올 수 있다. 또한 낱말 하나를 다르게 고르면 그다음 선택의 문맥도 달라진다. 처음의 작은 차이가 뒤 문장을 다른 방향으로 이을 수 있는 것이다.
실제 AI의 답에는 표본 추출 외에도 함께 전달된 대화 내용과 지침, 도구가 가져온 자료, 시스템의 설정 등이 영향을 줄 수 있다. 화면에서 같은 문장을 보냈다고 생각해도 모델에 주어진 전체 문맥까지 같다고 단정하기는 어렵다. 답이 바뀐 이유를 언제나 하나로 설명할 수는 없다.
실습에서는 결과를 비교하기 위해 난수의 시작값을 고정한다. 난수는 무작위 선택에 사용하는 수이고, 시드(seed)는 이 프로그램에서 그 수의 흐름을 시작하는 값이다. 같은 자료와 같은 실행 조건에서 같은 시작값을 쓰면 이번 프로그램의 선택을 재현할 수 있다. 이는 모든 AI 서비스에 같은 시작값을 주면 답이 같아진다는 뜻은 아니다.
시작값은 실행이 시작될 때 한 번 설정한다. 그 뒤에는 선택할 때마다 이어지는 수를 사용한다. 따라서 한 번의 실행 안에서 세 문장이 모두 같을 필요는 없다. 프로그램을 처음부터 다시 실행했을 때 세 문장의 순서가 그대로 나오는 것이 이번 실습에서 말하는 재현이다.
완성 코드
AI에게 이 프로그램을 부탁한다면 다음처럼 결과를 확인할 조건까지 함께 적을 수 있다. 상세한 요청문 구성은 뒤에서 다루므로, 여기서는 자료와 검사 기준이 드러나는지에 집중한다.
Python 표준 라이브러리만 사용하는 main.py를 만들어 줘. “나는 차를 마신다”, “나는 책을 읽는다”, “너는 차를 마신다”, “너는 책을 읽는다”에서 바로 다음 낱말의 빈도표를 만들어 줘.
“나는”에서 시작해 빈도에 비례해서 다음 낱말을 고르고 문장 세 개를 출력해 줘. 난수 시작값은 7로 고정하고, 문장 끝 표시와 반복 횟수 제한을 넣어 줘. “나는” 뒤의 후보 횟수와 만들어진 문장도 검사해 줘.
다음은 main.py의 전체 내용이다. 총 67줄이며 빈 줄도 포함한다. 외부 파일이나 네트워크 연결 없이 실행을 마친다. 문장 끝을 나타내는 표시는 자료를 집계할 때만 붙이며, 완성 문장에는 출력하지 않는다.
import random
SENTENCES = [
"나는 차를 마신다",
"나는 책을 읽는다",
"너는 차를 마신다",
"너는 책을 읽는다",
]
END = "<끝>"
def build_table(sentences):
table = {}
for sentence in sentences:
words = sentence.split() + [END]
for current, following in zip(words, words[1:]):
if current not in table:
table[current] = {}
counts = table[current]
counts[following] = counts.get(following, 0) + 1
return table
def choose_next(counts, rng):
if len(counts) == 1:
return next(iter(counts))
total = sum(counts.values())
point = rng.random() * total
cumulative = 0
for word, count in counts.items():
cumulative += count
if point < cumulative:
return word
raise RuntimeError("다음 낱말을 고르지 못했다")
def generate(table, start, rng):
words = [start]
for _ in range(10):
following = choose_next(table[words[-1]], rng)
if following == END:
return " ".join(words)
words.append(following)
raise RuntimeError("문장 길이 제한을 넘었다")
def main():
table = build_table(SENTENCES)
expected_counts = {"차를": 1, "책을": 1}
if table["나는"] != expected_counts:
raise RuntimeError("빈도표 검사 실패")
rng = random.Random(7)
results = [generate(table, "나는", rng) for _ in range(3)]
expected = [
"나는 차를 마신다",
"나는 차를 마신다",
"나는 책을 읽는다",
]
if results != expected:
raise RuntimeError("문장 검사 실패")
print("나는 다음:", table["나는"])
for number, sentence in enumerate(results, start=1):
print(f"{number}: {sentence}")
print("검사: 통과")
main()
줄별 해설
아래 줄 번호는 완성 코드의 첫 줄부터 빈 줄을 포함해 센 번호다. 문법을 한꺼번에 외우기보다 자료가 들어가고, 선택이 일어나고, 결과가 확인되는 흐름을 따라가면 된다. 괄호 뒤에 값을 넣어 어떤 작업을 시키는 것을 호출이라고 읽는다.
- 1줄: Python에 포함된 random 도구를 가져온다. 무작위 선택에 사용할 수를 만드는 역할이다.
- 2줄: 가져온 도구와 문장 자료를 빈 줄로 구분한다. 빈 줄은 읽기 위한 여백이다.
- 3~8줄: 문장 네 개를 SENTENCES라는 이름으로 묶는다. 따옴표 안은 글자 자료이며, 대괄호는 여러 값을 순서대로 담는 목록이다. 쉼표는 각 값을 구분한다.
- 9줄: END에 문장 끝 표시를 넣는다. 이 표시가 없으면 “마신다” 다음에도 낱말을 찾아야 하는지 판단하기 어렵다.
- 10~11줄: 자료와 다음 작업 정의 사이를 띄운다.
- 12줄: 빈도표를 만드는 build_table 함수를 정의한다. 함수는 이름을 붙여 둔 작업 묶음이다. sentences는 작업에 전달받을 자료의 이름이다.
- 13줄: 빈 사전을 만든다. 사전은 이름과 값을 짝지어 저장하는 자료다. 여기서는 낱말을 찾으면 그다음 후보의 횟수표를 얻는다.
- 14줄: 전달받은 문장을 하나씩 살펴본다. for는 같은 작업을 여러 값에 반복한다. 안쪽으로 들여쓴 줄들이 반복할 작업이다.
- 15줄: split으로 문장을 공백 기준의 낱말로 나누고, 목록 뒤에 끝 표시를 붙인다.
- 16줄: 낱말 목록과 첫 항목을 뺀 목록을 나란히 짝짓는다. zip이 만드는 첫 짝은 현재 낱말과 바로 다음 낱말이다.
- 17~18줄: 현재 낱말을 표에서 처음 만났다면 빈 후보 사전을 만든다. if는 조건이 맞을 때만 안쪽 작업을 실행한다.
- 19줄: 현재 낱말의 후보 사전을 counts라는 짧은 이름으로 가리킨다. 이 사전을 바꾸면 원래 표에도 반영된다.
- 20줄: 다음 낱말의 기존 횟수를 가져와 1을 더한다. get에 준 0은 아직 그 낱말이 없을 때 쓸 값이다.
- 21줄: 완성한 표를 함수를 부른 곳으로 돌려준다. return을 만나면 해당 함수의 작업이 끝난다.
- 22~23줄: 빈도표를 만드는 작업과 후보를 고르는 작업을 구분한다.
- 24줄: 후보 횟수표와 난수 도구를 전달받는 choose_next 함수를 정의한다.
- 25~26줄: 후보가 하나라면 그 이름을 바로 돌려준다. len은 후보 수를 센다. iter와 next는 사전의 첫 이름을 꺼내는 데 쓴다. 선택지가 하나이므로 난수를 사용할 필요가 없다.
- 27줄: 모든 후보의 횟수를 합친다. values는 횟수들만 꺼내고 sum은 그 합을 구한다.
- 28줄: 0 이상 1 미만인 난수에 전체 횟수를 곱한다. 그 결과를 후보별 구간 중 어디를 가리키는지 판단하는 점으로 사용한다.
- 29줄: 지금까지 더한 횟수를 0에서 시작한다.
- 30~33줄: 후보 이름과 횟수를 하나씩 꺼내 횟수를 누적한다. 점이 누적값보다 작아지는 순간 그 후보를 고른다. 횟수가 큰 후보는 더 넓은 구간을 차지한다.
- 34줄: 후보를 고르지 못한 상태를 오류로 알린다. 이번 자료에서 정상적으로 도달할 줄은 아니며, 잘못된 상태를 조용히 넘기지 않도록 둔다.
- 35~36줄: 후보 선택과 문장 생성 사이를 띄운다.
- 37~38줄: 문장을 만드는 generate 함수를 정의하고 시작 낱말을 목록에 담는다.
- 39줄: 다음 낱말을 찾는 작업을 최대 열 번 반복한다. 밑줄 이름은 반복 순서의 숫자를 작업에서 사용하지 않는다는 표시다.
- 40줄: words[-1]로 마지막 낱말을 찾고, 그 낱말의 후보표를 선택 함수에 전달한다.
- 41~42줄: 끝 표시를 골랐다면 낱말 사이에 공백을 넣어 문장으로 돌려준다. 끝 표시 자체는 문장에 넣지 않는다.
- 43줄: 끝 표시가 아니라면 새 낱말을 목록 뒤에 붙인다.
- 44줄: 열 번 안에 끝나지 않으면 오류로 알린다. 예상과 다른 자료가 들어와도 끝없이 이어 쓰지 않게 하는 제한이다.
- 45~46줄: 문장 생성 작업과 전체 실행 순서를 구분한다.
- 47~48줄: 전체 실행을 맡는 main 함수를 정의하고 자료로부터 빈도표를 만든다.
- 49~51줄: “나는” 뒤의 두 후보가 각각 한 번인지 검사한다. !=는 두 값이 다르다는 뜻이다. 예상과 다르면 실행을 멈춘다.
- 52줄: 시작값 7을 사용하는 난수 도구를 한 번 만든다. 이 도구를 이어서 사용해야 선택의 흐름이 유지된다.
- 53줄: 문장 생성을 세 번 수행해 결과 목록을 만든다. 대괄호 안의 for는 반복한 결과를 목록에 모으는 표현이다.
- 54~58줄: 이번 자료와 시작값에서 기대하는 문장 세 개를 순서대로 적는다.
- 59~60줄: 실제 문장 목록이 기대 목록과 같은지 검사한다. 문장 하나의 내용이나 순서가 달라도 실패로 처리한다.
- 61줄: “나는” 다음의 후보와 횟수를 출력한다. print는 화면에 값을 보여 준다.
- 62~63줄: 결과에 1부터 번호를 붙여 출력한다. f가 붙은 문자열에서는 중괄호 안의 값을 글자 사이에 넣을 수 있다.
- 64줄: 앞선 두 검사를 지나왔다는 사실을 출력한다. 이 문구가 프로그램의 모든 가능한 입력을 확인했다는 뜻은 아니다.
- 65~66줄: 작업 정의와 실행 지점을 구분한다.
- 67줄: main을 호출한다. 위에서 정의한 작업들이 이 줄에서 정한 순서에 따라 실행된다.
선택 과정을 숫자로 한 번 더 보자. “나는” 뒤에는 두 후보가 각각 한 번 있으므로 전체 횟수는 2다. 첫 후보 “차를”은 0 이상 1 미만의 구간, 둘째 후보 “책을”은 1 이상 2 미만의 구간을 차지한다. 28줄에서 만든 점이 어느 구간에 들어가는지에 따라 다음 낱말이 정해진다. 자료의 횟수가 바뀌면 구간 너비도 바뀐다.
실행 결과
전체 코드를 main.py라는 이름으로 저장한 뒤, 그 파일이 있는 폴더에서 다음 명령을 실행한다. 명령을 입력하는 터미널과 폴더 이동은 다음 장에서 직접 준비한다.
python3 main.py
예상 출력은 다음과 같다. 코드는 후보 하나인 경우에 난수를 사용하지 않으므로, 세 문장을 만들 때 갈림길에서만 난수를 차례로 사용한다.
나는 다음: {'차를': 1, '책을': 1}
1: 나는 차를 마신다
2: 나는 차를 마신다
3: 나는 책을 읽는다
검사: 통과
같은 파일을 다시 실행하면 같은 순서로 출력된다. 처음 두 문장이 같다고 해서 무작위 선택이 실패한 것은 아니다. 같은 후보를 연속해서 고르는 일도 가능하다. 세 번 중 각 후보가 정확히 절반씩 나와야 하는 것도 아니다.
이 출력은 언어를 이해했다는 증거가 아니다. 프로그램은 “차를”과 “마신다”의 뜻을 검사하지 않았다. 자료에서 두 낱말이 붙어 있었다는 관계를 이용했다. 문장이 자연스럽게 보이는 이유를 확인하려면 결과만 보는 대신 어떤 자료에서 어떤 후보를 골랐는지도 함께 살펴봐야 한다.
또한 “검사: 통과”는 정해 둔 빈도와 문장 세 개가 맞았다는 뜻이다. 다른 시작 낱말이나 다른 자료까지 검사한 결과는 아니다. AI가 검사를 추가했다고 말하면 검사 대상과 기대값을 읽어 보는 이유가 여기에 있다.
실무에서 자주 틀리는 것
후보의 개수만 보고 횟수를 무시한다
틀린 예는 후보마다 같은 너비를 준다. 아래 코드는 실행되지만 “차를”이 세 번, “책을”이 한 번 나온 자료의 비율을 반영하지 않는다. 첫 출력은 후보 수인 2이고, 고친 출력은 전체 횟수인 4다.
틀린 코드:
counts = {"차를": 3, "책을": 1}
total = len(counts)
print(total)
고친 코드:
counts = {"차를": 3, "책을": 1}
total = sum(counts.values())
print(total)
완성 자료는 두 후보의 횟수가 같아서 이런 실수를 놓치기 쉽다. 한 후보를 더 자주 넣은 자료로도 확인하면 차이가 드러난다. 자연스럽게 보이는 문장이 나왔다는 사실만으로 선택 비율까지 맞다고 판단하지 않는다.
고를 때마다 시작값을 다시 설정한다
틀린 예는 매번 같은 시작점으로 돌아간다. 따라서 첫 번째 난수만 세 번 사용한다. 고친 예는 도구 하나를 계속 사용한다. 두 코드는 각각 독립해서 실행할 수 있다.
틀린 코드:
import random
for _ in range(3):
rng = random.Random(7)
print(round(rng.random(), 3))
고친 코드:
import random
rng = random.Random(7)
for _ in range(3):
print(round(rng.random(), 3))
틀린 예는 0.324를 세 번 출력한다. 고친 예는 0.324, 0.151, 0.651을 차례로 출력한다. round는 여기서 소수점 아래 세 자리까지 읽기 쉽게 나타내려고 썼다. 완성 코드에서는 반올림하지 않은 수로 후보를 고른다.
변경한 자료와 예전 기대값을 함께 둔다
자료를 바꾼 뒤 검사에서 실패했다고 해서 언제나 구현 오류인 것은 아니다. 아래 틀린 예는 자료를 두 문장으로 바꾸고도 예전 문장 수를 기대한다. 고친 예는 의도한 변경에 맞춰 기대값도 바꾼다.
틀린 코드:
sentences = ["나는 차를 마신다", "나는 책을 읽는다"]
expected_count = 4
print(len(sentences) == expected_count)
고친 코드:
sentences = ["나는 차를 마신다", "나는 책을 읽는다"]
expected_count = 2
print(len(sentences) == expected_count)
첫 코드는 False, 고친 코드는 True를 출력한다. 각각 조건이 맞지 않음과 맞음을 나타내는 값이다. 다만 검사를 통과시키려고 기대값부터 실제 결과에 맞추면 안 된다. 먼저 의도한 자료가 무엇인지 확인한다. 그런 다음 자료와 기대값의 변경 부분을 함께 비교한다. 완성 코드의 문장 자료를 바꿀 때도 같은 순서로 판단한다.
한눈에 보기
| 개념 | 이 장에서의 뜻 | 확인할 일 |
|---|---|---|
| 문맥 | 다음 후보를 계산하는 데 쓰는 앞선 정보 | 요청에 필요한 조건이 들어 있는지 본다. |
| 확률 | 후보가 선택될 가능성 | 높은 확률을 사실 확인과 혼동하지 않는다. |
| 표본 추출 | 후보의 비율에 따라 하나를 고르는 과정 | 같은 요청의 답이 달라도 요구를 충족하는지 살핀다. |
| 시작값 고정 | 이번 프로그램의 난수 흐름을 재현하는 방법 | 같은 조건에서 결과 순서가 반복되는지 본다. |
| 그럴듯한 오답 | 표현은 자연스럽지만 기능이나 결과가 틀린 답 | 실행, 기대값 검사, 변경 부분 비교로 확인한다. |
| 장난감 모델의 한계 | 바로 앞 낱말의 빈도만 이용함 | 실제 언어 모델 전체를 설명한다고 확대하지 않는다. |
AI에게 코드를 맡길 때 필요한 태도는 답을 무조건 의심하는 것도, 자연스러운 설명을 그대로 믿는 것도 아니다. 요청한 동작을 작은 확인 기준으로 바꾸고 실제 결과와 대조하는 것이다. 이번 프로그램에서는 “후보가 각각 한 번인가”와 “고정한 조건에서 문장 세 개가 예상대로 나오는가”가 그 기준이었다.
연습 문제
- 완성 코드의 자료에서 “차를” 다음에 올 후보와 그 횟수를 적어 보라. “나는” 뒤의 횟수와 다른 이유도 설명하라.
- 자료에 “나는 차를 마신다”를 한 문장 더 추가하면 “나는” 뒤의 두 후보는 어떤 비율로 선택되는가. 다음 한 번의 선택에서 반드시 “차를”이 나오는지도 설명하라.
- AI가 “실행을 마쳤고 모두 정상이다”라고 답했지만 실행 기록은 보여 주지 않았다. 이 말과 별개로 확인할 항목 세 가지를 적어 보라.
- 완성 코드에서 선택한 후보가 하나일 때도 난수를 사용하도록 바꿨다고 하자. 문장 자료와 시작값이 같으니 기존 기대 문장 세 개를 그대로 믿어도 되는가. 판단 이유와 확인 순서를 적어 보라.
정답과 해설
“마신다”가 두 번이다. “나는 차를 마신다”와 “너는 차를 마신다”에 각각 한 번씩 등장한다. 이 프로그램은 문장 전체가 아니라 바로 붙어 있는 낱말 짝을 센다. 따라서 앞의 주어가 달라도 “차를 → 마신다”라는 같은 관계에 횟수를 더한다. “나는” 뒤에는 서로 다른 두 후보가 한 번씩 있지만, “차를” 뒤에는 같은 후보가 두 번 있다.
“차를”이 두 번, “책을”이 한 번이므로 선택 비율은 2 대 1이다. 전체 횟수 3 중 “차를”이 차지하는 몫은 3분의 2다. 그렇다고 다음 한 번에 반드시 “차를”을 고르는 것은 아니다. “책을”도 선택 구간을 가지고 있다. 실제로 자료를 수정한다면 빈도 검사와 문장 검사도 새 조건에 맞는지 다시 확인해야 한다.
첫째, 지금 사용할 Python 환경에서 코드가 실행되는지 확인한다. 둘째, 예시 입력의 실제 출력이 미리 정한 기대 출력과 같은지 확인한다. 셋째, 수정본이라면 이전 파일과 비교해 무엇이 바뀌었는지 확인한다. 실행 기록이 추가로 제공되면 어떤 코드와 어떤 입력을 사용한 기록인지도 연결해서 읽는다. “정상”이라는 표현만으로 확인 범위를 추측하지 않는다.
기존 기대값을 그대로 믿을 수는 없다. 후보가 하나인 단계에서도 난수를 사용하면 다음 갈림길에 도달했을 때 사용할 수의 순서가 달라질 수 있다. 시작값만 같다고 모든 선택이 같아지는 것은 아니다. 먼저 변경 부분을 비교해 난수를 사용하는 위치가 달라졌는지 보고, 변경 목적이 맞는지 판단한다. 그다음 실행 결과를 확인하고, 새 동작이 의도에 맞다는 근거를 세운 뒤 기대값을 조정한다. 달라진 출력을 그대로 복사해 검사만 통과시키는 것으로 끝내지 않는다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.