Inicio / Artículos

Un agente de IA local con Ollama: herramientas especializadas, validación y límites estrictos de pasos

Un agente de IA local en Ollama que se comporta con listas de herramientas restringidas, un bucle con límite, salida estructurada con Pydantic, estado explícito y un conjunto de pruebas para casos de fallo.

Editor: REACTAPP.TOP9 min de lectura2021 palabras

Fuentes: I Built a Local AI Agent With Ollama — and the Hard Part Wasn’t the Model

La primera versión de un agente para facturas locales parece impresionante durante unos cinco minutos. La tarea es sencilla: ayudar al dueño de una pequeña empresa a cobrar las facturas vencidas. El agente puede buscar a los clientes, encontrar las facturas impagadas, calcular cuánto tiempo están atrasadas y redactar un recordatorio. Una solicitud de prueba le pide que encuentre las facturas vencidas de Acme Plumbing y prepare un recordatorio amable. Responde de inmediato con una factura, el saldo y un correo electrónico bien redactado.

El problema es que parte de esa información es inventada. El cliente existe, pero el número de la factura no. La cantidad parece plausible, pero está incorrecta. El agente ha creado la apariencia de un resultado exitoso sin realizar el trabajo real.

Ese episodio enseña la lección más importante sobre la creación de agentes: sonar inteligente no equivale a ser fiable. Un agente es fiable cuando cada afirmación significativa se basa en una acción controlada o en una fuente confiable. A continuación, se reconstruye el agente para generar facturas utilizando Ollama y Python, añadiendo herramientas explícitas, validación y un bucle de agente compacto. La inferencia se realiza en la máquina local, pero las lecciones son aplicables a casi cualquier framework de agentes.

Por qué Ollama

Una configuración local evita enviar los registros de los clientes a un proveedor de modelos externo durante el desarrollo, y permite cambiar entre modelos abiertos sin tener que reconstruir la aplicación. Ollama proporciona modelos localmente a través de una API de chat sencilla. También se admite la llamada a herramientas, de modo que un modelo puede solicitar una función en lugar de inventar una respuesta que no posee. La configuración es sencilla:

ollama pull qwen3
pip install ollama

Una solicitud de chat normal desde Python se ve así:

from ollama import chat
response = chat(
    model="qwen3",
    messages=[
        {"role": "user", "content": "Explain what an overdue invoice is."}
    ],
)print(response.message.content)

Eso da como resultado un chatbot, no aún un agente.

Un chatbot responde, un agente actúa paso a paso

La palabra agente se utiliza de manera amplia. Una definición práctica es un modelo que funciona dentro de un bucle, con acceso a herramientas, estado y reglas que determinan qué puede hacer a continuación. Para la tarea de la factura, el bucle se desarrolla de la siguiente manera:

  1. El usuario realiza una solicitud.
  2. El modelo decide si se necesita una herramienta.
  3. La aplicación valida la solicitud de herramienta.
  4. La aplicación ejecuta la herramienta.
  5. El resultado se agrega a la conversación.
  6. El modelo solicita otra herramienta o da una respuesta final.

El modelo nunca interactúa con la base de datos. Solo puede solicitar a la aplicación que ejecute una herramienta definida por el desarrollador. Este límite es importante porque los modelos de lenguaje son buenos para interpretar la intención y generar texto, mientras que el código de las aplicaciones es más eficaz en cuestiones de permisos, consultas, cálculos y reglas de negocio.

Herramientas específicas primero

Un error común al principio es utilizar una sola herramienta poderosa, como manage_invoices, capaz de buscar, actualizar, redactar y enviar información. Esto dificulta la depuración y facilita peligrosamente su uso indebido. Funcionan mejor las herramientas pequeñas con una función cada una:

  • find_customer convierte el nombre de un cliente en su ID real.
  • get_overdue_invoices devuelve las facturas vencidas correspondientes a ese ID.
  • draft_reminder genera el texto pero no envía nada.
  • send_reminder realiza la acción externa y requiere aprobación.

Una capa de datos local simplificada proporciona los datos necesarios:

CUSTOMERS = {
    "acme plumbing": {
        "customer_id": "cus_1042",
        "name": "Acme Plumbing",
        "email": "billing@example.com",
    }
}
INVOICES = [
    {
        "invoice_id": "INV-2048",
        "customer_id": "cus_1042",
        "amount": 1850.00,
        "days_overdue": 18,
    }
]
def find_customer(name: str) -> dict:
    customer = CUSTOMERS.get(name.strip().lower())
    return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
    matches = [
        invoice
        for invoice in INVOICES
        if invoice["customer_id"] == customer_id
        and invoice["days_overdue"] > 0
    ]
    return {"invoices": matches, "count": len(matches)}

Mire lo que estas funciones se abstienen de hacer. Nunca adivinan. La ausencia de un cliente genera un error explícito, y la falta de facturas correspondientes produce una lista vacía.

Herramientas reales, no acceso imaginario

Ollama acepta funciones de Python como herramientas, y los nombres, parámetros y strings de documentación le proporcionan al modelo lo necesario para elegir la adecuada.

import json
from ollama import chat
def find_customer(name: str) -> dict:
    """Find a customer by business name and return its verified record."""
    customer = CUSTOMERS.get(name.strip().lower())
    return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
    """Return overdue invoices for a verified customer ID."""
    matches = [
        invoice
        for invoice in INVOICES
        if invoice["customer_id"] == customer_id
        and invoice["days_overdue"] > 0
    ]
    return {"invoices": matches, "count": len(matches)}
TOOLS = {
    "find_customer": find_customer,
    "get_overdue_invoices": get_overdue_invoices,
}

El diccionario del registro es más que una comodidad, ya que funciona como lista de permisos. Incluso si el modelo solicita una función llamada delete_customer, la aplicación la rechaza porque no existe tal herramienta en TOOLS.

El bucle del agente

El bucle es lo que convierte la llamada a herramientas en un agente. Comienza con una instrucción del sistema:

SYSTEM_PROMPT = """
You are an invoice assistant.
Rules:
- Never invent a customer, invoice, email address, balance, or date.
- Use find_customer before requesting invoices.
- Only use customer IDs returned by tools.
- If a tool returns an error or no records, explain that clearly.
- You may draft communication, but you cannot send it.
"""
def run_agent(user_request: str) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_request},
    ]    for _ in range(6):
        response = chat(
            model="qwen3",
            messages=messages,
            tools=list(TOOLS.values()),
        )        messages.append(response.message)        if not response.message.tool_calls:
            return response.message.content        for call in response.message.tool_calls:
            name = call.function.name
            arguments = call.function.arguments            if name not in TOOLS:
                result = {"error": "tool_not_allowed"}
            else:
                try:
                    result = TOOLS[name](**arguments)
                except (TypeError, ValueError) as error:
                    result = {
                        "error": "invalid_tool_arguments",
                        "detail": str(error),
                    }            messages.append(
                {
                    "role": "tool",
                    "tool_name": name,
                    "content": json.dumps(result),
                }
            )    return "I stopped because the task exceeded the maximum number of steps."

El límite de seis iteraciones es intencional, ya que un agente nunca debe seguir ejecutándose indefinidamente mientras reconsidera el mismo problema. El bucle también trata la salida de las herramientas como evidencia reciente. El modelo resuelve primero el nombre “Acme Plumbing”, recupera cus_1042 y solo después solicita facturas para ese ID verificado.

Un prompt mejor no fue la solución

Las alucinaciones llevan a los desarrolladores a añadir una oración más al prompt del sistema, como una petición de precisión y de no inventar nada. Esa instrucción ayuda, pero no constituye un control efectivo. La solución fiable provino de una reestructuración:

  • La base de datos sigue siendo la fuente de verdad.
  • El modelo puede solicitar datos y no puede inventar registros que parezcan válidos.
  • Los nombres de las herramientas están en una lista permitida y los argumentos se validan.
  • El bucle tiene un límite estricto de pasos.
  • La lectura y la escritura se mantienen separadas.
  • El envío de un correo electrónico requiere aprobación humana.

Los mensajes de confirmación influyen en el comportamiento, y el código lo hace cumplir.

Salida estructurada en los límites

El texto en formato libre es adecuado para correos amigables, pero es arriesgado cuando otra parte de la aplicación procesa la respuesta. Si el agente debe devolver una pantalla de revisión con el cliente, los IDs de las facturas, el saldo total y un borrador, un modelo Pydantic puede definir ese contrato:

from pydantic import BaseModel, Field
class ReminderReview(BaseModel):
    customer_name: str
    invoice_ids: list[str]
    total_due: float = Field(ge=0)
    draft_subject: str
    draft_body: str
    requires_approval: bool = True

Ollama acepta un esquema JSON a través de su parámetro format:

review_response = chat(
    model="qwen3",
    messages=messages,
    format=ReminderReview.model_json_schema(),
)
review = ReminderReview.model_validate_json(
    review_response.message.content
)

Eso garantiza que la respuesta tenga la forma esperada. No demuestra que los valores sean verdaderos, por lo que invoice_ids y total_due siguen siendo comparados con los resultados reales de la herramienta antes de que aparezca el botón de aprobación. La validación de esquema verifica si algo tiene la forma correcta. La validación empresarial verifica si algo está permitido y es verdadero. Un agente de producción necesita ambas cosas.

El estado no es memoria

Las conversaciones largas revelan otro problema: el modelo olvida qué cliente fue seleccionado o mezcla una factura antigua con una nueva solicitud. La causa es tratar toda la transcripción de la conversación como estado de la aplicación. La conversación es un contexto útil, pero el estado crítico debe encontrarse en una estructura explícita:

task_state = {
    "customer_id": "cus_1042",
    "verified_invoice_ids": ["INV-2048"],
    "approved_actions": [],
}

El modelo puede resumir eventos, pero la aplicación es la que posee los IDs verificados y las aprobaciones. Si la conversación se acorta para ajustarse a la ventana de contexto, esos datos se mantienen intactos.

La longitud del contexto también afecta el rendimiento, ya que más contexto consume más memoria y lleva más tiempo procesarse. En lugar de enviar todos los mensajes y filas de la base de datos indefinidamente, proporcione solo el conjunto mínimo de información necesario para la decisión actual.

Local no significa seguro

Ejecutar un modelo a través de Ollama mantiene la inferencia bajo control local, pero no protege el resto de la aplicación. Un agente aún puede filtrar datos en los registros, llamar a un endpoint interno inseguro, seguir instrucciones maliciosas ocultas en un documento o actuar con permisos excesivos. Se aplican los principios habituales de seguridad del backend:

  • Proporcione a cada herramienta solo los permisos que necesite.
  • Mantenga los secretos fuera de los prompts y del código fuente.
  • Trate el texto recuperado como datos no confiables, nunca como instrucciones del sistema.
  • Registre cada llamada a la herramienta junto con su resultado, latencia y la decisión final.
  • Elimine los campos sensibles de los resultados antes de que ingresen al contexto del modelo.
  • Solicite aprobación para acciones externas o irreversibles.
  • Haga que las escrituras importantes sean idempotentes, para que las reintentos no las dupliquen.

El modelo nunca debe ser el límite de seguridad.

Pruebas

Las demostraciones de flujo óptimo ocultan la mayoría de los fallos del agente, por lo que vale la pena crear un conjunto de evaluación pequeño. Los casos incluyen a un cliente conocido con una factura vencida y otro sin ninguna, un nombre que no coincide con nadie, dos nombres de empresa similares, un intento de envío que omite la aprobación, una instrucción insertada para ignorar las reglas, un argumento de herramienta mal formado y una herramienta que se queda sin tiempo. Para cada caso, se deben registrar las herramientas esperadas, las acciones permitidas y el resultado final, y se debe volver a ejecutar el conjunto cada vez que cambien el modelo, la instrucción del sistema o las descripciones de las herramientas.

La medida más útil no es si la respuesta final suena bien, sino si el agente siguió la secuencia correcta de pasos verificados.

La versión funcional

Después del rediseño, la solicitud original deja una huella como esta:

User request
  → find_customer(name="Acme Plumbing")
  → verified customer_id: cus_1042
  → get_overdue_invoices(customer_id="cus_1042")
  → verified invoice: INV-2048, $1,850, 18 days overdue
  → generate draft
  → wait for human approval

Es menos mágico que la primera demostración y mucho más útil. Un cliente inexistente hace que el agente se detenga y pida aclaraciones. No se reportan facturas vencidas como tales. Una solicitud de herramienta desconocida es rechazada por la aplicación, y un bucle que funciona durante demasiado tiempo se detiene de forma segura. Nada de eso es espectacular, pero todo ello fomenta la confianza.

La lección final

Ollama facilita la ejecución de modelos locales capaces, pero el modelo es solo una parte. El verdadero agente es el sistema que lo rodea: el bucle, los contratos de herramientas, los datos de fuente de verdad, la capa de validación, la máquina de estados, los límites de permisos y el paso de aprobación.

Para un agente principiante, omite las diez herramientas y el acceso completo a la aplicación. Comienza con un problema real y dos herramientas de solo lectura limitadas, registra cada decisión, agrega casos de fallo e introduce el acceso de escritura solo después de que el proceso de lectura sea extremadamente fiable. El cambio importante es reemplazar la pregunta de cómo hacer que el modelo sea más autónomo por la pregunta de cómo hacer que cada paso sea observable, verificable y seguro. La autonomía sirve para una buena demostración, mientras que la autonomía controlada genera un producto útil.

Leer más