ES ▾

API de LLM sin censura compatible con OpenAIuncensoredapis.com

InicioAgentes

Agentes en una API de LLM sin censura: llamadas a funciones, frameworks y guardias

Un agente es un bucle: el modelo solicita una función, tu código la ejecuta y el resultado vuelve a la conversación. Como la API de LLM sin censura admite llamadas a funciones en el formato de chat estándar, puedes construir ese bucle en cuarenta líneas y luego conectar el mismo endpoint a LangChain o LlamaIndex. Esta guía cubre ambos enfoques, además de las comprobaciones para mantener segura la ejecución de herramientas.

Actualizado

Puntos clave

  1. Pasa las herramientas en la petición, vigila que finish_reason sea igual a tool_calls, ejecuta las funciones, añade mensajes tool y repite hasta que el modelo responda en texto.
  2. LangChain necesita ChatOpenAI con base_url; LlamaIndex necesita OpenAILike con api_base y la bandera de llamadas a funciones activada.
  3. El modelo propone llamadas; tu código decide si se ejecutan. Usa una lista permitida, valida, limita el tiempo y acota el bucle.
  4. Los agentes multiplican el número de peticiones, así que presupuesta el límite de 300 peticiones por minuto y la ventana de contexto de 100.000 tokens.

Anatomía del bucle de llamadas a funciones

Elimina los frameworks y todos los agentes hacen lo mismo en cuatro pasos. Envías la conversación más una lista tools. El modelo responde ya sea con texto, lo que indica que ha terminado, o con tool_calls, lo que indica que quiere que se ejecute algo. Ejecutas cada llamada y añades un mensaje tool por cada llamada, citando el id de la llamada. Luego preguntas al modelo de nuevo con la conversación más larga.

La señal es finish_reason. Cuando vale tool_calls, el mensaje del asistente tiene content en null y un array tool_calls, con id, nombre de función y un argumento en formato JSON. La página de referencia muestra la estructura exacta. Dos detalles confunden: los argumentos son un string que debe analizarse, y el mensaje del asistente con las llamadas debe mantenerse en el historial antes de los resultados de tus herramientas, en orden.

Un bucle completo desde cero

El script de abajo define una función, la describe en un esquema JSON y buclea hasta que el modelo produce texto plano. Usa el SDK oficial de Python apuntando a la API. Cada pasada añade el mensaje del asistente, luego un mensaje tool por cada llamada, y un contador de pasos detiene el comportamiento descontrolado.

import json, os
from openai import OpenAI

client = OpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])

TICKETS = {"T-101": "open", "T-102": "closed"}

def ticket_status(ticket_id: str) -> str:
    return TICKETS.get(ticket_id, "unknown ticket")

TOOLS = [{
    "type": "function",
    "function": {
        "name": "ticket_status",
        "description": "Look up the status of a support ticket by id.",
        "parameters": {
            "type": "object",
            "properties": {"ticket_id": {"type": "string"}},
            "required": ["ticket_id"],
        },
    },
}]
REGISTRY = {"ticket_status": ticket_status}

messages = [{"role": "user", "content": "Are tickets T-101 and T-102 still open?"}]

for step in range(6):                      # hard cap on loop turns
    r = client.chat.completions.create(
        model="uncensored", messages=messages, tools=TOOLS, max_tokens=400)
    msg = r.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))
    if r.choices[0].finish_reason != "tool_calls":
        print(msg.content)
        break
    for call in msg.tool_calls:
        fn = REGISTRY.get(call.function.name)
        try:
            args = json.loads(call.function.arguments)
            result = fn(**args) if fn else f"error: unknown tool {call.function.name}"
        except Exception as exc:
            result = f"error: {exc}"
        messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
else:
    print("stopped: step limit reached")

Observa qué hace el bucle en caso de error: los errores se convierten en resultados de herramienta como cadenas. Esto permite que el modelo lea el problema e intente de nuevo, por ejemplo corrigiendo un id de ticket incorrecto, en lugar de que tu programa falle. La cláusula for ... else se ejecuta solo si se alcanza el límite sin una respuesta final, lo cual es cómo detectas un modelo atrapado en círculos.

Mantén los prompts enfocados. Un mensaje de sistema breve que indique cuándo llamar a una herramienta y cuándo responder directamente reduce llamadas innecesarias, y cada llamada omitida ahorra un viaje de ida y vuelta, tokens y una ranura de tu límite de peticiones.

Protección de la ejecución de herramientas

La salida del modelo es una entrada no fiable. Un nombre de herramienta o argumento elegido por el modelo puede ser incorrecto, extraño o estar influenciado por texto que el modelo leyó de una página web o de un usuario. Trata cada llamada como una petición que llega desde internet público.

  • Lista blanca por nombre. Busca la función en un registro que hayas creado. Nunca uses la cadena para importar o evaluar código.
  • Separa lectura de escritura. Las herramientas solo de lectura se ejecutan libremente. Cualquier cosa que envíe correos, gaste dinero, elimine datos o toque archivos necesita aprobación humana explícita.
  • Valida los argumentos contra el esquema que anunciaste: claves requeridas, sin claves extra, tipos y límites de longitud.
  • Limita tiempo y salida. Aplica tiempos de espera y recorta los resultados antes de devolverlos para que una salida enorme de herramienta no consuma tu ventana de contexto de 100.000 tokens.
  • Acota iteraciones y coste. Limita pasos por tarea y tokens por sesión, usando el bloque usage.
  • Registra cada llamada con nombre, argumentos y resultado para que puedas auditar el comportamiento.
import json
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutTimeout

ALLOWED = {"ticket_status", "lookup_invoice"}      # read-only tools only
NEEDS_APPROVAL = {"refund_order"}                  # side effects wait for a human
POOL = ThreadPoolExecutor(max_workers=4)

def validate(args: dict, schema: dict) -> None:
    props = schema["properties"]
    for key in schema.get("required", []):
        if key not in args:
            raise ValueError(f"missing argument: {key}")
    for key, val in args.items():
        if key not in props:
            raise ValueError(f"unexpected argument: {key}")
        if props[key]["type"] == "string" and not isinstance(val, str):
            raise ValueError(f"{key} must be a string")
        if isinstance(val, str) and len(val) > 200:
            raise ValueError(f"{key} too long")

def run_tool(name, raw_args, registry, schemas, approve=lambda n, a: False):
    if name in NEEDS_APPROVAL:
        args = json.loads(raw_args)
        if not approve(name, args):
            return "denied: this action needs human approval"
    elif name not in ALLOWED:
        return f"error: tool {name} is not permitted"
    args = json.loads(raw_args)
    validate(args, schemas[name]["parameters"])
    future = POOL.submit(registry[name], **args)
    try:
        return str(future.result(timeout=5))[:2000]    # time and size limits
    except FutTimeout:
        return "error: tool timed out"

Trata también los resultados de las herramientas como no confiables. Si una herramienta devuelve texto extraído que contiene instrucciones, el modelo podría seguirlas. Envuelve ese contenido en delimitadores claros en el mensaje de la herramienta e indica en tu prompt del sistema que la salida de la herramienta es datos, no comandos.

Redacción de esquemas de herramientas que el modelo pueda usar

El esquema es la única documentación que ve el modelo, así que merece el mismo cuidado que una API pública. Nombra funciones con verbos y sustantivos que coincidan con la tarea, como ticket_status o lookup_invoice, en lugar de etiquetas genéricas como run o helper. Escribe la description como una instrucción: qué hace la función, qué devuelve y cuándo no usarla.

  • Mantén pocos parámetros y planos. Dos o tres campos simples se llaman correctamente mucho más a menudo que un objeto anidado con diez.
  • Usa enums para conjuntos cerrados, por ejemplo "enum": ["open", "closed"], para que el modelo no pueda inventar un valor.
  • Describe cada propiedad con una unidad o ejemplo: "ID de ticket como T-101" es mejor que una cadena sin adornos.
  • Devuelve resultados compactos. Las cadenas o objetos JSON pequeños funcionan mejor; los volcados largos consumen contexto con poco beneficio.
  • Preferir varias herramientas estrechas sobre una única herramienta omnibus con una bandera de modo.

Cuando una llamada falla la validación, devuelve el mensaje de validación como resultado de la herramienta en lugar de lanzar una excepción. El modelo suele reparar la llamada en el siguiente turno, ya que puede leer qué estaba mal. Si falla de la misma manera dos veces, reescribe la descripción, no el bucle.

Configuración de LangChain

La clase ChatOpenAI de LangChain acepta un base_url, así que no se necesita un paquete de proveedor personalizado. Establece model a uncensored, pasa la clave desde tu entorno y vincula herramientas con bind_tools. Instala langchain-openai y langchain-core.

import os
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = ChatOpenAI(
    model="uncensored",
    base_url="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    temperature=0.3,
    max_tokens=400,
)
llm_with_tools = llm.bind_tools([ticket_status])

ai = llm_with_tools.invoke("What is the status of ticket T-101?")
for call in ai.tool_calls:
    print(call["name"], call["args"], "->", ticket_status.invoke(call["args"]))

El resultado de invoke es un AIMessage cuya lista tool_calls contiene nombres y argumentos analizados, así que te saltas el paso manual de JSON. Para obtener un bucle autónomo completo, pasa el mismo objeto llm a los constructores de agentes de LangChain; el endpoint solo tiene que honrar las llamadas a funciones, lo cual hace. Si un agente parece ignorar sus herramientas, verifica que estén presentes el docstring y las anotaciones de tipo, ya que se convierten en el esquema que ve el modelo.

Configuración de LlamaIndex

LlamaIndex incluye OpenAILike para servidores que siguen el formato de finalizaciones de chat. Tres ajustes importan. api_base apunta a la API, is_chat_model=True selecciona la ruta de chat, y is_function_calling_model=True indica al framework que puede enviar herramientas. Establece context_window a 100000 para que sus divisores de texto y buffers de memoria se ajusten correctamente. Instala llama-index-llms-openai-like.

import os
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.tools import FunctionTool

def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = OpenAILike(
    model="uncensored",
    api_base="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    is_chat_model=True,
    is_function_calling_model=True,
    context_window=100000,
    max_tokens=400,
)

tool = FunctionTool.from_defaults(fn=ticket_status)
response = llm.predict_and_call([tool], "Check ticket T-102 for me.")
print(response)

Desde aquí puedes pasar el mismo llm a un trabajador de agente o un motor de consultas. Recuerda que los pipelines de recuperación pueden llenar un prompt con muchos fragmentos; con una ventana de 100.000 tokens compartida entre prompt y finalización, acota el número de fragmentos recuperados y mantén max_tokens moderado.

Gestión de agentes con presupuesto limitado

Un agente convierte un mensaje en varias peticiones, así que planifica capacidad. Una tarea con cinco rondas de herramientas cuesta seis llamadas al modelo. Diez tareas simultáneas generan sesenta peticiones en poco tiempo, y el límite de 300 peticiones por minuto se alcanza antes que en un chat. Usa el control de flujo con cubo de tokens del juego de errores y límites de velocidad en tu cliente.

El contexto crece con cada ronda porque cada resultado de herramienta se añade. Resume o elimina las salidas de herramientas antiguas una vez que se hayan utilizado, y vigila usage.prompt_tokens; cuando se acerque a 50 000, comprime el historial. Para el coste, asume una tarea ilustrativa de 6 llamadas con un promedio de 3 000 tokens de prompt y 200 tokens de finalización: eso son 18 000 tokens de entrada (aproximadamente $0,0045 a $0,25 por millón) y 1 200 tokens de salida (aproximadamente $0,0012 a $1,00 por millón). Esas cantidades de tokens son suposiciones para las sumas, no mediciones. Si eres nuevo en el endpoint, empieza con la documentación y el crédito de prueba gratis.

Pruebas y depuración de un agente

Los agentes fallan de formas que las pruebas unitarias de una sola función pasan por alto, así que prueba la conversación. Graba la lista completa de mensajes para cada ejecución, incluyendo argumentos y resultados de herramientas, y reproduce los fallos desde el registro. Tres hábitos pagan rápidamente.

  1. Simula el modelo. Stub el cliente para que devuelva mensajes tool_calls secuenciados. Esto prueba tu registro, validación y terminación del bucle sin red ni gasto.
  2. Muestreo de pin. Usa un temperature bajo, como 0.2, para tareas impulsadas por herramientas. Menor aleatoriedad genera un JSON de argumentos más repetible, y puedes subirlo después en pasos más orientados a la escritura.
  3. Verifica las trayectorias. Para un puñado de prompts fijos, comprueba qué herramientas se llamaron y en qué orden, no solo el texto final.

Observa también finish_reason. Un valor de length en medio de una llamada a herramienta significa que los argumentos se cortaron; aumenta max_tokens (hasta 32.000) y el JSON se completará. El streaming también funciona con herramientas, pero acumula los fragmentos de argumentos por índice de llamada y analiza solo después de que el stream se cierre. Para repasar esos fragmentos, consulta la sección de streaming de la referencia.

Preguntas y respuestas

¿La API admite llamadas a herramientas simultáneas en una sola respuesta?

El mensaje lleva un array tool_calls, así que procesa cada entrada y envía un mensaje de herramienta por cada tool id.

¿Por qué mi bucle de agente se repite indefinidamente?

Normalmente los errores de las herramientas no son informativos o el system prompt nunca indica cuándo detenerse. Añade un límite de pasos y devuelve cadenas de error claras.

¿Necesito LangChain o LlamaIndex?

No. Un bucle simple con el SDK de OpenAI es suficiente, y los frameworks solo añaden orquestación sobre el mismo endpoint.

¿Cómo evito que el modelo ejecute herramientas destructivas?

Nunca las registres sin un paso de aprobación. Solo las funciones de tu registro pueden ejecutarse, así que mantén las acciones de escritura detrás de una confirmación humana.

Tu clave está a un formulario de distancia

Crea una cuenta, copia la clave, cambia la base URL. Esa es toda la configuración.