Las personas miran un número cada vez

La forma de resolver de las personas descrita en solve.md es así. Se lee el tablero y se elige un número. Si se tachan las filas, columnas y cajas donde ese número ya está, aparece una caja o una línea en la que solo queda un sitio. Esa casilla es la respuesta al 100%. Se rellena y se vuelve a leer el tablero.

Así que reduje a dos las tareas que se le encargan al VLM.

  1. Leer el tablero: leer tal cual los números de las 81 casillas.
  2. Elegir un número y escribir todas las casillas donde ese número no puede ir.

Nunca se le pregunta qué casilla es la respuesta. Es el código el que cuenta si, tras tachar las casillas donde no puede ir el número, queda un solo sitio.

El sitio que queda al tachar las casillas donde no puede ir el número 1
El sitio que queda al tachar las casillas donde no puede ir el número 1

El 8B antes de entrenar

Cambié el modelo a Qwen3-VL-8B-Instruct. Al ponerle 20 problemas sin LoRA, acertó el 100% de las coordenadas, pero solo leyó correctamente las 81 casillas en el 20% de los tableros (83% por casilla). En las casillas donde no puede ir el número 1, acertó un 91% por casilla, pero no hubo ni un tablero acertado sin fallos.

Entrenamiento por etapas

Entrené las dos tareas por separado. En cada ciclo se hace una prueba, se añaden los problemas fallados y se vuelve a entrenar.

  • Leer el tablero: el 2B pasó del 17,5% al 100% en 4 ciclos, y el 8B del 25,2% al 100% en 2 ciclos.
  • Casillas donde no puede ir el número 1: el 2B subió hasta el 69,5% en 9 ciclos, y el 8B hasta el 97,6% en 15 ciclos. Por casilla, eso es un 99,98%.

A diferencia de la entrada 2, cuanto más entrenaba, más mejoraba. Es porque el fundamento de la respuesta está dentro del tablero.

Proporción de tableros acertados sin fallos en la lectura del tablero y en las casillas donde no puede ir el número 1
Proporción de tableros acertados sin fallos en la lectura del tablero y en las casillas donde no puede ir el número 1

El código real de este paso

Este es el prompt que se le da al VLM. Hace que elija un número y escriba solo las casillas donde no puede ir.

vlm_prompt.pypython · 28 líneas
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)

Esta es la función que genera las respuestas correctas para el entrenamiento. Se deciden solo según si ese número ya está en la fila, la columna o la caja.

vlm_board.pypython · 18 líneas
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

La siguiente entrada es "4. Comparación de las resoluciones antes y después del entrenamiento".