cognify.

Southern hemisphere · 31°32′15″ S · 68°32′11″ W

Qwen 3.5 0.8B + Deterministic Skills

← Back to logbook

This experiment stems from a simple question: what can a small model do if we don't ask it to calculate or verify everything on its own, but rather give it real software tools?

The agent runs locally from PowerShell.

powershell
py agent.py

The model is not connected to a web application or a publishing service. It runs via Ollama and acts as the linguistic layer of the system: it receives a question, decides if an external capability is relevant, and if so, formulates the call.

Components

ComponentRole
qwen3.5:0.8bInterprets the query, selects a tool, and drafts the response.
OllamaRuns the local model and exposes the chat function.
agent.pyOrchestrator: presents the tools to the model, executes the chosen call, and returns the result.
skills.pyImplementation of deterministic capabilities.
Hunspell + es_ARReal external engine to check spelling in Argentine Spanish.

Design Principle

The proposal separates two classes of work:

  • Probabilistic: understanding the intent of a phrase and deciding if there is an appropriate tool.
  • Deterministic: counting characters or consulting the spell checker via software.

The model does not need to solve the counting internally once it decides to use the tool. Python or Hunspell produce the result. The model only intervenes again to explain it to the person.

The flow is deliberately scoped:

text
User question
        ↓
Qwen decides: respond or call a skill
        ↓
Python executes the skill (if there was a call)
        ↓
Qwen interprets the result, without new calls

Each question starts with clean context. This prevents a previous response from introducing reused arguments or decisions and allows observing each tool selection in isolation.

Agent Program: agent.py

python
from ollama import chat
from skills import contar_letra, contar_letras, revisar_ortografia

MODELO = "qwen3.5:0.8b"

TOOLS = {
    "contar_letra": contar_letra,
    "contar_letras": contar_letras,
    "revisar_ortografia": revisar_ortografia
}

SYSTEM = """
Eres un asistente general con acceso a capacidades externas.

Tu objetivo principal es responder normalmente al usuario.
Las herramientas son capacidades adicionales, no tu personalidad.

Antes de responder determina si alguna herramienta es realmente
necesaria para realizar la operación solicitada.

CAPACIDADES:

- contar_letra:
  cuenta cuántas veces aparece una letra específica en un texto.

- contar_letras:
  calcula la cantidad total de letras de un texto.

- revisar_ortografia:
  utiliza Hunspell para comprobar si una palabra está correctamente
  escrita y obtener sugerencias ortográficas.

REGLAS:

- Usa una herramienta solamente cuando corresponda.
- No inventes argumentos.
- No reutilices argumentos de preguntas anteriores.
- Si ninguna herramienta corresponde, responde normalmente.
- Deletrear significa expresar una palabra letra por letra.
- Deletrear NO significa borrar o eliminar.
- Actualmente NO tienes una herramienta específica para deletrear.
"""


def mostrar_monologo(mensaje, titulo="MONÓLOGO"):
    thinking = getattr(mensaje, "thinking", None)
    if thinking:
        print("\n========================================")
        print("🧠", titulo)
        print("========================================")
        print(thinking)
        print("========================================")


print("Agente Qwen iniciado.")
print("Escribe 'salir' para terminar.\n")

while True:
    pregunta = input("Tú: ")
    if pregunta.lower().strip() == "salir":
        break

    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": pregunta}
    ]

    respuesta = chat(
        model=MODELO,
        messages=messages,
        tools=list(TOOLS.values()),
        options={"temperature": 0}
    )

    mostrar_monologo(respuesta.message, "MONÓLOGO PRE-TOOL")
    messages.append(respuesta.message)
    llamadas = respuesta.message.tool_calls

    if not llamadas:
        print("\n[Qwen respondió sin usar skills]")
        print("Qwen:", respuesta.message.content)
        print()
        continue

    for llamada in llamadas:
        nombre = llamada.function.name
        argumentos = llamada.function.arguments
        print("\n[Qwen decidió usar una skill]")
        print("Skill:", nombre)
        print("Argumentos:", argumentos)

        if nombre not in TOOLS:
            resultado = "ERROR: herramienta desconocida"
        else:
            try:
                resultado = TOOLS[nombre](**argumentos)
            except Exception as e:
                resultado = f"ERROR: {e}"

        print("Resultado:", resultado)
        messages.append({
            "role": "tool",
            "tool_name": nombre,
            "content": str(resultado)
        })

    respuesta_final = chat(
        model=MODELO,
        messages=messages,
        options={"temperature": 0}
    )

    mostrar_monologo(respuesta_final.message, "MONÓLOGO POST-TOOL")
    print("\nQwen:", respuesta_final.message.content)
    print()

Skills: skills.py

python
import subprocess
from pathlib import Path

CARPETA_PROYECTO = Path(__file__).parent
DICCIONARIO = CARPETA_PROYECTO / "diccionarios" / "es_AR"


def contar_letra(texto: str, letra: str) -> int:
    """Cuenta cuántas veces aparece una letra específica en un texto."""
    if len(letra) != 1:
        raise ValueError("Debes indicar una sola letra.")
    return texto.count(letra)


def contar_letras(texto: str) -> int:
    """Cuenta el número total de letras de un texto."""
    return sum(1 for caracter in texto if caracter.isalpha())


def revisar_ortografia(palabra: str) -> dict:
    """Invoca Hunspell y transforma su salida en un resultado estructurado."""
    proceso = subprocess.run(
        ["hunspell", "-a", "-d", str(DICCIONARIO)],
        input=palabra + "\n",
        text=True,
        capture_output=True,
        encoding="utf-8",
        errors="replace"
    )

    lineas = [linea.strip() for linea in proceso.stdout.splitlines() if linea.strip()]
    resultados = [linea for linea in lineas if not linea.startswith("@(#)")]

    if not resultados:
        return {"palabra": palabra, "estado": "error", "resultado_hunspell": proceso.stdout}

    resultado = resultados[0]
    if resultado.startswith("*"):
        return {"palabra": palabra, "correcta": True, "sugerencias": [], "resultado_hunspell": resultado}

    if resultado.startswith("&"):
        sugerencias = []
        if ":" in resultado:
            sugerencias = [s.strip() for s in resultado.split(":", 1)[1].split(",")]
        return {"palabra": palabra, "correcta": False, "sugerencias": sugerencias, "resultado_hunspell": resultado}

    if resultado.startswith("#"):
        return {"palabra": palabra, "correcta": False, "sugerencias": [], "resultado_hunspell": resultado}

    return {"palabra": palabra, "correcta": None, "sugerencias": [], "resultado_hunspell": resultado}

Observed Dialogue

The first part of the conversation serves to separate what the model answers via general knowledge from what it delegates:

text
Tú: hola wacho
Qwen: ¡Hola! ¿Cómo estás?

Tú: todo bien mi hermano
Qwen: ¡Todo bien, mi hermano! 😊

Tú: genial. sabes cuantos huesos tiene el hombre?
Qwen: El cuerpo humano tiene aproximadamente 206 huesos.

For queries without an applicable capability, Qwen responds without a tool. This area shows the limits of a small model: in the subsequent anatomical talk, it produced incorrect explanations about supposed bones inside the brain. It is not a failure of the skills: none had been called. It is a sign that general knowledge and verifiable computation are different problems.

Then the core behavior of the experiment appears:

text
Tú: cuantas letras tiene luna?

Qwen decidió usar una skill
Skill: contar_letras
Argumentos: {"texto": "luna"}
Resultado: 4

Qwen: La palabra "luna" tiene 4 letras.
text
Tú: cuantas r tiene perrro?

Qwen decidió usar una skill
Skill: contar_letra
Argumentos: {"texto": "perrro", "letra": "r"}
Resultado: 3

Qwen: La letra 'r' aparece 3 veces en el texto "perrro".
text
Tú: cuantas r tiene ferrocarrril?

Qwen decidió usar una skill
Skill: contar_letra
Argumentos: {"texto": "ferrocarrril", "letra": "r"}
Resultado: 5

Qwen: La letra 'r' aparece 5 veces en el texto "ferrocarrril".
text
Tú: y cuantas r hay en strawberry?

Qwen decidió usar una skill
Skill: contar_letra
Argumentos: {"texto": "strawberry", "letra": "r"}
Resultado: 3

Qwen: Hay 3 r en strawberry.

In all four cases, the model identified the intent, chose the correct function, extracted the relevant parameters, and let the software produce the value. The result for strawberry is indeed 3: s-t-r-a-w-b-e-r-r-y.

What this prototype explores

The prototype explores whether the 0.8 billion parameter model can operate as a natural language interface for scoped and verifiable capabilities.

The useful question is not just “did it answer correctly?”, but four independent questions:

  1. Did it detect that the query required a tool?
  2. Did it choose the appropriate tool?
  3. Did it extract the exact arguments?
  4. Did the final response respect the result obtained?

The three skills have predictable behaviors; the uncertainty lives in the model's decision. That division allows expanding the agent by adding reliable capabilities without attributing to the model the execution of operations that specialized software can already do.

Possible next steps

  • Create a small set of evaluation cases to measure tool selection, arguments, result, and final wording.
  • Add time limits and exit code checking to the Hunspell invocation.
  • Serialize tool results as JSON to make the contract between the executor and the model more explicit.
  • Incorporate new tools without losing the scoped cycle of decision, execution, and interpretation.
  • Compare the performance of Qwen 3.5 0.8B vs Qwen 3.5 0.8B + Skills.