사람은 숫자 하나씩 본다
solve.md에 적은 사람의 풀이는 이렇습니다. 판을 읽고 숫자 하나를 고릅니다. 그 숫자가 이미 있는 행·열·박스를 지우면 자리가 하나만 남는 박스나 줄이 생깁니다. 그 칸은 100% 답입니다. 채우고 다시 판을 읽습니다.
그래서 VLM에게 시키는 일을 둘로 줄였습니다.
- 판 읽기: 81칸의 숫자를 그대로 읽습니다.
- 숫자 하나를 정해 그 숫자를 둘 수 없는 칸을 모두 적습니다.
어느 칸이 정답인지는 묻지 않습니다. 둘 수 없는 칸을 지우고 남은 자리가 하나뿐인지는 코드가 셉니다.

학습 전의 8B
모델은 Qwen3-VL-8B-Instruct로 바꿨습니다. LoRA 없이 20문제를 시켜 보니 좌표는 100% 맞혔지만 81칸을 모두 맞게 읽은 판은 20%(칸 단위 83%)였습니다. 숫자 1을 둘 수 없는 칸은 칸 단위로 91%를 맞혔지만 빠짐없이 맞힌 판은 하나도 없었습니다.
단계별 학습
두 일을 따로 학습했습니다. 사이클마다 시험을 보고 틀린 문제를 더해 다시 학습합니다.
- 판 읽기: 2B는 17.5%에서 4사이클 만에 100%, 8B는 25.2%에서 2사이클 만에 100%가 됐습니다.
- 숫자 1을 둘 수 없는 칸: 2B는 9사이클 동안 69.5%까지 올랐고, 8B는 15사이클에 97.6%까지 올랐습니다. 칸 단위로는 99.98%입니다.
2편과 달리 학습할수록 올랐습니다. 정답의 근거가 판 안에 있는 일이기 때문입니다.

이 단계의 실제 코드
VLM에 주는 프롬프트입니다. 숫자 하나를 정해 둘 수 없는 칸만 적게 합니다.
vlm_prompt.py
def build_digit_illegal_prompt(
board: list[list[int]],
given_mask: list[list[bool]],
digit: int,
*,
retry_hints: list[str] | None = None,
) -> str:
"""digit 의 비합법수(둘 수 없는 모든 칸) 나열."""
board_txt = board_to_text(board, given_mask)
placed = sum(1 for r in range(9) for c in range(9) if board[r][c] == digit)
prompt = (
"You are solving Sudoku like a human (solve.md digit tracking).\n"
f"{_COORD_HEADER}"
f"{_HUMAN_RULES}"
"The board has been read. Now track ONE digit on the number pad.\n"
f"TARGET DIGIT: {digit} (already placed {placed} times on the board).\n\n"
f"Task: list EVERY cell where digit {digit} CANNOT be placed.\n"
"A cell is ILLEGAL for this digit if:\n"
f"- it is filled with a digit other than {digit}, OR\n"
f"- it is EMPTY but its row, column, or 3×3 box already contains {digit}.\n"
f"Do NOT list cells that already contain {digit} — those are legal placements.\n"
"You MUST include ALL illegal cells. Do not omit any.\n"
"Use compact JSON (no spaces) to fit all cells.\n\n"
f"Text grid:\n{board_txt}\n\n"
f'Return ONLY JSON: {{"digit":{digit},"illegal":[[row,col],...]}}\n'
f'Example: {{"digit":{digit},"illegal":[[0,3],[1,5],[2,8]]}}\n'
)
return _append_retry_hints(prompt, retry_hints)학습 정답을 만드는 함수입니다. 행·열·박스에 그 숫자가 이미 있는지만으로 정해집니다.
vlm_board.py
def illegal_cells_for_digit(
board: list[list[int]], digit: int
) -> list[tuple[int, int]]:
"""digit 을 둘 수 없는 모든 칸 (이미 digit 인 칸은 제외)."""
from sudoku_human_solver import cell_candidates
out: list[tuple[int, int]] = []
for row in range(9):
for col in range(9):
v = board[row][col]
if v == digit:
continue
if v != 0:
out.append((row, col))
continue
if digit not in cell_candidates(board, row, col):
out.append((row, col))
return out다음 글은 ‘4. 학습 전과 후의 풀이 비교’입니다.
COMMENTS · CHAPTER 03
사람의 풀이를 규칙으로 옮기다 댓글 0
아직 댓글이 없습니다. 읽다가 궁금한 점이나 보탤 내용을 남겨 주세요.
댓글을 등록하려면 구글로 시작하기를 눌러 닉네임과 메일 주소를 등록해 주세요.
구글로 시작하기