Inicio / Artículos / Notas prácticas: Ejecuta un LLM local útil en 30 minutos (programación, RAG, voz)

Notas prácticas: Ejecuta un LLM local útil en 30 minutos (programación, RAG, voz)

Guía paso a paso para implementar las notas prácticas: Ejecuta un LLM local útil en 30 minutos (programación, RAG, voz): contratos, verificaciones y espacios para código listo para usar destinados a los equipos que implementan este patrón.

2857 palabras

Las notas siguientes reconstruyen un camino práctico para “Ejecutar un LLM local útil en 30 minutos (programación, RAG, voz)”. Se da énfasis a los contratos, las verificaciones y los marcadores de posición para el código, en lugar de a un enfoque motivacional. Al trabajar en la sección de Resumen, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de un fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Documente tanto el camino óptimo como el de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no de mejoras posteriores.

$ ollama run qwen3:8b
>>> rewrite this function to use async/await

Configuración compartida en cinco minutos

La configuración compartida de cinco minutos funciona mejor cuando se trata como una superficie medible. Capture un transcripte ejemplar, un caso de fallo y la nota de reversión antes de ampliar el alcance. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado. Asigne un presupuesto de tokens por turno y por sesión; las herramientas agenciales amplían el contexto de forma excesiva; los límites estrictos evitan que las demostraciones se conviertan en facturas inesperadas.

brew install ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen3:8b
ollama run qwen3:8b "Write a python script to reverse a string."

Ruta A: El asistente de programación local

Ruta A: El Asistente de Codificación Local funciona mejor cuando se trata como una superficie medible. Capture una transcripción ejemplar, un caso de fallo y la nota de reversión antes de ampliar el alcance. Considere esta etapa como un contrato entre las entradas y las salidas validadas. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace completaciones parciales silenciosas. Asigne un presupuesto de tokens por turno y por sesión. Las herramientas agentes amplían el contexto de forma excesiva; los límites máximos evitan que las demostraciones se conviertan en facturas inesperadas.

# For 24GB+ VRAM or 32GB+ Mac Unified Memory
ollama pull qwen3-coder:30b

# For 16GB RAM laptops
ollama pull qwen2.5-coder:7b

Un modelo Ollama ajustado por Cline

Un modelo Ollama ajustado con Cline funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Registre los tiempos y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos. Fije el intérprete y el archivo de bloqueo de dependencias antes de enseñar el bucle. La diferencia entre la computadora portátil y los entornos de integración continua es la causa más común de fallos silenciosos en las demostraciones de API. Un modelo Ollama ajustado con Cline funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores.

# Modelfile — cline-tuned qwen3-coder
# Save as: ./Modelfile

FROM qwen3-coder:30b

# Cline's system prompt is ~25-30K tokens before your code is added.
# Ollama's default num_ctx for Qwen 3 is 40K, but Cline ships an
# expanded system prompt in v3+ that comfortably exceeds it on any
# real task. 65536 is the safe floor for serious use; 131072 if
# you have RAM/VRAM to spare.
PARAMETER num_ctx 65536

# Code edits want low-variance output. The default 0.7 is too loose.
PARAMETER temperature 0.2

# Stop after the model's natural turn. Cline parses this.
PARAMETER stop "<|im_end|>"
# Build the Cline-tuned model from the Modelfile in the current dir
ollama create qwen3-coder-cline -f ./Modelfile

# Verify the context window actually took
ollama show qwen3-coder-cline --modelfile | grep num_ctx
# Expected output:  PARAMETER num_ctx 65536

# Sanity-check it answers
ollama run qwen3-coder-cline "write a python one-liner to read /etc/hostname"
API Provider:      Ollama
Base URL:          http://localhost:11434
Model:             qwen3-coder-cline
Context Window:    65536

Camino B: RAG con sus propios documentos

Para el camino B: RAG sobre tus propios documentos, define las entradas, el responsable de la etapa y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la etapa desde un punto de control conocido sin tener que adivinar el estado oculto. Prefiere unidades pequeñas y verificables en lugar de scripts extensos. Cuando una etapa falla, el error debe indicar una única responsabilidad y no un proceso complicado. Prefiere salidas estructuradas con validación de esquema en lugar de texto libre cuando el siguiente paso sea código o una llamada a una herramienta.

llama pull nomic-embed-text
pip install ollama numpy
"""Local RAG over a folder of notes — Ollama embeddings + numpy cosine.

No vector DB. For a few hundred docs, an in-memory list + numpy is
faster to set up and fast enough to run. Swap in sqlite-vec when
the corpus outgrows it (typically past ~5000 chunks).

Setup:
    ollama pull qwen3:8b
    ollama pull nomic-embed-text
    pip install ollama numpy

Run:
    python rag.py ./notes "what did the team decide about pricing?"
"""
from __future__ import annotations
import glob
import os
import sys

import numpy as np
import ollama

EMBED_MODEL = "nomic-embed-text"
CHAT_MODEL = "qwen3:8b"
CHUNK_WORDS = 400          # ~500 tokens; fits 4 chunks in an 8K context
TOP_K = 3


def load_chunks(folder: str) -> list[str]:
    """Read every .md / .txt under folder, split into ~CHUNK_WORDS chunks."""
    chunks: list[str] = []
    for path in glob.glob(os.path.join(folder, "**/*"), recursive=True):
        if not path.endswith((".md", ".txt")):
            continue
        with open(path, encoding="utf-8") as f:
            words = f.read().split()
        for i in range(0, len(words), CHUNK_WORDS):
            chunk = " ".join(words[i : i + CHUNK_WORDS])
            if chunk.strip():
                chunks.append(chunk)
    return chunks


def embed(texts: list[str]) -> np.ndarray:
    """Embed a batch of texts via Ollama. Returns an (N, D) matrix."""
    resp = ollama.embed(model=EMBED_MODEL, input=texts)
    return np.array(resp["embeddings"], dtype=np.float32)


def top_k_indices(
    query_vec: np.ndarray, doc_mat: np.ndarray, k: int
) -> list[int]:
    """Return indices of the k most cosine-similar rows in doc_mat.

    The trick: if both vectors are unit-length (norm == 1), their
    dot product equals their cosine similarity. So we normalize
    once, then one matrix multiply gives a similarity score for
    every chunk against the query — no Python loop needed.
    """
    # Normalize query and every chunk to unit length.
    # The 1e-8 prevents division by zero on a zero vector.
    q = query_vec / (np.linalg.norm(query_vec) + 1e-8)
    d = doc_mat / (np.linalg.norm(doc_mat, axis=1, keepdims=True) + 1e-8)

    # One matmul across all chunks: scores[i] = cos(query, chunk_i).
    scores = d @ q

    # Sort descending, take the first k indices.
    return np.argsort(scores)[::-1][:k].tolist()


def answer(query: str, chunks: list[str], doc_mat: np.ndarray) -> str:
    """Retrieve top-K chunks, stuff them into a prompt, generate."""
    q_vec = embed([query])[0]
    idx = top_k_indices(q_vec, doc_mat, k=TOP_K)
    context = "\n\n---\n\n".join(chunks[i] for i in idx)

    prompt = (
        "Answer the question using ONLY the context below. "
        "If the context does not contain the answer, say so plainly.\n\n"
        f"CONTEXT:\n{context}\n\nQUESTION: {query}"
    )
    resp = ollama.chat(
        model=CHAT_MODEL,
        messages=[{"role": "user", "content": prompt}],
    )
    return resp["message"]["content"]


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit('usage: python rag.py <folder> "<question>"')

    folder, query = sys.argv[1], sys.argv[2]

    chunks = load_chunks(folder)
    if not chunks:
        sys.exit(f"no .md or .txt files found under {folder}")

    print(f"indexing {len(chunks)} chunks...")
    doc_mat = embed(chunks)

    print(answer(query, chunks, doc_mat))
python rag.py ~/Documents/meeting_notes "what did the team decide about pricing?"

Camino C: El bucle de voz

Para la ruta C: El bucle de voz, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Trate esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas. Prefiera resultados estructurados con validación de esquema sobre textos en formato libre cuando el siguiente paso sea código o una llamada a una herramienta.

brew install whisper-cpp ffmpeg
pip install -U sounddevice ollama kokoro-onnx

La tubería de voz

Para The Voice Pipeline, defina las entradas, el responsable de la etapa y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la etapa a partir de un punto de control conocido sin tener que adivinar el estado oculto. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana de los costos evita facturas inesperadas cuando el flujo pasa de entornos de demostración a entornos compartidos. Prefiera salidas estructuradas con validación de esquema sobre texto en formato libre cuando el siguiente paso sea escribir código o realizar una llamada a una herramienta. Para The Voice Pipeline, defina las entradas, el responsable de la etapa y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la etapa a partir de un punto de control conocido sin tener que adivinar el estado oculto. Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras realizadas posteriormente.

"""Local voice loop — mic → whisper.cpp → Ollama → Kokoro → speaker.

Everything runs offline. Nothing leaves the machine.

Setup:
    brew install whisper-cpp ffmpeg                  # macOS
    # (Linux: apt install ffmpeg; build whisper.cpp from source)

    pip install -U sounddevice ollama kokoro-onnx

    # Whisper GGML model — pick one:
    #   ggml-base.en.bin   (~150MB, fast, English-only)
    #   ggml-large-v3.bin  (~3GB, accurate, multilingual)
    # Download from https://huggingface.co/ggerganov/whisper.cpp

    # Kokoro model files auto-download to ~/.cache/kokoro-onnx/
    # on first run, or grab them manually from
    # https://github.com/thewh1teagle/kokoro-onnx/releases

Run:
    python voice.py
"""
from __future__ import annotations
import subprocess
import tempfile
import wave
from pathlib import Path

import sounddevice as sd
import ollama
from kokoro_onnx import Kokoro

WHISPER_MODEL = Path.home() / "models" / "ggml-base.en.bin"
CHAT_MODEL = "qwen3:8b"
KOKORO_VOICE = "af_sarah"      # see kokoro voices.json for all options
SAMPLE_RATE = 16000            # whisper.cpp requires 16kHz mono
RECORD_SECONDS = 5

# Kokoro auto-downloads the model files on first instantiation.
kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")


def record() -> Path:
    """Record from the default mic, write a 16kHz mono WAV, return its path."""
    print(f"listening for {RECORD_SECONDS}s...")
    audio = sd.rec(
        int(RECORD_SECONDS * SAMPLE_RATE),
        samplerate=SAMPLE_RATE,
        channels=1,                    # mono
        dtype="int16",                 # 16-bit PCM is what wave.open expects
    )
    sd.wait()                          # block until recording finishes

    # Fixed path in the system tempdir — overwritten each invocation.
    wav_path = Path(tempfile.gettempdir()) / "voice_in.wav"
    with wave.open(str(wav_path), "wb") as w:
        w.setnchannels(1)              # mono
        w.setsampwidth(2)              # 2 bytes per sample == int16
        w.setframerate(SAMPLE_RATE)    # 16 kHz — whisper.cpp's required rate
        w.writeframes(audio.tobytes())
    return wav_path


def transcribe(wav_path: Path) -> str:
    """Run whisper.cpp on a WAV file, return the transcript text."""
    result = subprocess.run(
        [
            "whisper-cli",
            "-m", str(WHISPER_MODEL),
            "-f", str(wav_path),
            "-otxt",                   # write the transcript to <input>.txt
            "-np",                     # suppress progress prints on stdout
        ],
        capture_output=True,
        text=True,
        timeout=60,
    )
    if result.returncode != 0:
        raise RuntimeError(f"whisper-cli failed: {result.stderr}")

    # -otxt writes the transcript next to the input file, with .txt
    # tacked onto the existing name — so /tmp/voice_in.wav becomes
    # /tmp/voice_in.wav.txt (same directory, same stem, extra suffix).
    txt_path = wav_path.with_suffix(wav_path.suffix + ".txt")
    return txt_path.read_text(encoding="utf-8").strip()


def respond(text: str) -> str:
    """Send the transcript to the local LLM, return the reply."""
    resp = ollama.chat(
        model=CHAT_MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a voice assistant. Keep replies to one or "
                    "two short sentences. No markdown, no lists."
                ),
            },
            {"role": "user", "content": text},
        ],
    )
    return resp["message"]["content"]


def speak(text: str) -> None:
    """Synthesize the reply with Kokoro and play it back."""
    samples, sample_rate = kokoro.create(
        text, voice=KOKORO_VOICE, speed=1.0, lang="en-us"
    )
    sd.play(samples, sample_rate)
    sd.wait()


if __name__ == "__main__":
    wav = record()
    heard = transcribe(wav)

    if not heard:
        print("nothing heard. exiting.")
        raise SystemExit(0)

    print(f"you said: {heard}")
    reply = respond(heard)
    print(f"model:    {reply}")
    speak(reply)

La realidad del hardware

Al trabajar en The Hardware Reality, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado. Almacene en caché las instrucciones del sistema estables y los esquemas de las herramientas. Reenviar un preámbulo idéntico es una causa común de problemas.

Continuar leyendo

Al trabajar en “Continuar leyendo”, anote primero el contrato: los datos requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Considere esta etapa como un contrato entre los datos de entrada y los resultados validados. Asigne nombres a los elementos generados, defina las comprobaciones de éxito y evite completaciones parciales silenciosas. Almacene en caché las instrucciones del sistema estables y los esquemas de las herramientas. Reenviar un preámbulo idéntico es una causa común de problemas.

Lista de verificación operativa

Para la lista de verificación operativa, defina los datos de entrada, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto.

Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin necesidad de leer todo el sistema.

Preferir outputs estructurados con validación de esquema sobre prosa libre cuando el siguiente paso sea escribir código o hacer una llamada a una herramienta.

Medir la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Fijar las versiones de dependencias y registrar el resumen de la imagen que se utilizó en la demostración. La reproducibilidad es mejor que el conocimiento basado en prácticas internas.

Tratar esta etapa como un contrato entre las entradas y los outputs validados. Nombrar los artefactos, definir las verificaciones de éxito y rechazar completaciones parciales silenciosas.

Antes de promocionar la solución, congelar las versiones, capturar una transcripción de referencia para el camino crítico y confirmar los pasos de reversión. Los entornos compartidos necesitan límites de uso, verificaciones de asignación y un responsable claro para la rotación de credenciales secretas. Preferir una fiabilidad aburrida a demostraciones ingeniosas pero puntuales.

Nota de lote para 9f628082e0d0: mantener las claves del proveedor fuera del repositorio, establecer un límite para los tokens por sesión y almacenar las transcripciones junto a los archivos de evaluación para que los cambios posteriores en el modelo sigan siendo comparables.

La nota de refuerzo 0 funciona mejor cuando se trata como una superficie medible. Capture una transcripción de referencia, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un único lugar que los operadores puedan auditar sin tener que leer todo el sistema.

Detalle de refuerzo 0/770: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Para la nota de fortalecimiento 1, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Es preferible utilizar unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el error debe indicar una única responsabilidad y no un proceso complicado.

Detalle de fortalecimiento 1/770: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de criterios en lugar de en observaciones anecdóticas.

Al trabajar en la nota de reforzamiento 2, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de la fase de demostración a entornos compartidos.

Detalle de reforzamiento 2/770: mida el tiempo empleado, la clase del error y el gasto en tokens para esta nota, y luego decida si mantener la modificación basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

La nota de reforzamiento 3 funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación. Los intentos repetidos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son algo que se añade posteriormente.

Detalle de reforzamiento 3/770: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Para la nota de reforzamiento 4, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Trate esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas.

Detalle de reforzamiento 4/770: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Al trabajar en la nota de fortalecimiento número 5, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema.

Detalle de fortalecimiento 5/770: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Lecturas relacionadas

  • Notas prácticas: Reclasificación para RAG: Cross-Encoders, LLM Rerankers y latencia — Guía paso a paso de las Notas prácticas: Reclasificación para RAG: Cross-Encoders, LLM Rerankers y latencia: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.
  • Notas prácticas: Mi sistema RAG perdió el 80% de mis datos. Este cambio lo solucionó. — Guía paso a paso de las Notas prácticas: Mi sistema RAG perdió el 80% de mis datos. Este cambio lo solucionó.: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.
  • Notas prácticas: Graphify, OKF o ambos. Más allá de RAG para bases de código — Guía paso a paso de las Notas prácticas: Graphify, OKF o ambos. Más allá de RAG para bases de código: contratos, verificaciones y espacios de código listos para usar para los equipos que implementan este patrón.