Início/Agentes
Agentes na API LLM sem Censura: Chamada de Funções, Frameworks e Guardas
Um agente é um loop: o modelo solicita uma função, seu código a executa e o resultado volta para a conversa. Como a API LLM sem censura suporta chamada de funções no formato de chat padrão, você pode construir esse loop em quarenta linhas e conectar o mesmo endpoint ao LangChain ou LlamaIndex. Este guia cobre ambos, além das verificações para manter a execução de ferramentas segura.
Atualizado
Pontos-chave
- Passe ferramentas na requisição, observe finish_reason igual a tool_calls, execute as funções, anexe mensagens tool e repita até que o modelo responda em texto.
- LangChain precisa de ChatOpenAI com base_url; LlamaIndex precisa de OpenAILike com api_base e a flag de chamada de funções ativada.
- O modelo propõe chamadas; seu código decide se elas são executadas. Permita apenas o que está na lista de permitidos, valide, limite o tempo e limite o loop.
- Agentes multiplicam o número de requisições, então planeje-se para o limite de 300 requisições por minuto e a janela de contexto de 100.000 tokens.
Anatomia do loop de chamada de funções
Abstraia frameworks e todo agente faz as mesmas quatro coisas. Você envia a conversa mais uma lista tools. O modelo responde com texto, o que significa que terminou, ou com tool_calls, o que significa que quer algo executado. Você executa cada chamada e anexa uma mensagem tool por chamada, citando o id da chamada. Então você pergunta ao modelo novamente com a conversa mais longa.
O sinal para o branch é finish_reason. Quando é igual a tool_calls, a mensagem do assistente tem content definido como null e um array tool_calls, onde cada entrada contém um id, um nome de função e uma string de argumentos em JSON. A página de referência mostra a forma exata. Dois detalhes confundem as pessoas: os argumentos são uma string que deve ser analisada, e a mensagem do assistente contendo as chamadas deve permanecer no histórico antes dos resultados das ferramentas, na ordem correta.
Um loop completo do zero
O script abaixo define uma função, a descreve em esquema JSON e faz um loop até que o modelo produza texto simples. Usa o SDK Python oficial apontado para a API. Cada iteração anexa a mensagem do assistente, depois uma mensagem tool por chamada, e um contador de etapas interrompe comportamentos descontrolados.
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")Observe o que o loop faz em falhas: erros tornam-se resultados de ferramentas como strings. Isso permite que o modelo leia o problema e tente novamente, por exemplo, corrigindo um ID de ticket inválido, em vez do seu programa falhar. A cláusula for ... else dispara apenas se o limite for atingido sem uma resposta final, que é como você detecta um modelo preso em círculos.
Mantenha os prompts focados. Uma mensagem de sistema curta que define quando chamar uma ferramenta e quando responder diretamente reduz chamadas desnecessárias, e cada chamada pulada economiza uma ida e volta, tokens e um slot do seu limite de requisições.
Protegendo a execução de ferramentas
A saída do modelo é uma entrada não confiável. Um nome de ferramenta ou argumento escolhido pelo modelo pode estar errado, estranho ou influenciado pelo texto que o modelo leu de uma página da web ou de um usuário. Trate cada chamada como uma requisição chegando da internet pública.
- Lista de permissões por nome. Consulte a função em um registro que você construiu. Nunca use a string para importar ou avaliar código.
- Separe leitura de gravação. Ferramentas apenas de leitura são executadas livremente. Qualquer coisa que envie e-mail, gaste dinheiro, exclua dados ou toque em arquivos precisa de aprovação humana explícita.
- Valide os argumentos contra o esquema que você anunciou: chaves obrigatórias, sem chaves extras, tipos e limites de comprimento.
- Limite tempo e saída. Aplique timeouts e trunque os resultados antes de alimentá-los de volta para que uma saída de ferramenta enorme não consuma sua janela de contexto de 100.000 tokens.
- Limite iterações e custo. Limite etapas por tarefa e tokens por sessão, usando o bloco
usage. - Registre cada chamada com nome, argumentos e resultado para que você possa auditar o comportamento.
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"Trate os resultados das ferramentas como não confiáveis também. Se uma ferramenta retornar texto extraído contendo instruções, o modelo pode segui-las. Envolva esse conteúdo em delimitadores claros na mensagem da ferramenta e declare no seu prompt do sistema que a saída da ferramenta é dados, não comandos.
Escrevendo esquemas de ferramentas que o modelo pode usar
O esquema é a única documentação que o modelo vê, então merece o mesmo cuidado de uma API pública. Nomeie funções com verbos e substantivos que correspondam à tarefa, como ticket_status ou lookup_invoice, em vez de rótulos genéricos como run ou helper. Escreva a description como uma instrução: o que a função faz, o que ela retorna e quando não usá-la.
- Mantenha poucos parâmetros e planos. Dois ou três campos simples são chamados corretamente muito mais frequentemente do que um objeto aninhado com dez.
- Use enums para conjuntos fechados, por exemplo
"enum": ["open", "closed"], para que o modelo não invente um valor. - Descreva cada propriedade com uma unidade ou exemplo: "ID do ticket como T-101" supera uma string crua.
- Retorne resultados compactos. Strings ou pequenos objetos JSON funcionam melhor; dumps longos queimam contexto por pouco ganho.
- Prefira várias ferramentas estreitas a uma única ferramenta omnibus com uma flag de modo.
Quando uma chamada falha na validação, retorne a mensagem de validação como o resultado da ferramenta em vez de lançar. O modelo geralmente corrige a chamada no próximo turno, já que ele pode ler o que estava errado. Se falhar da mesma maneira duas vezes, reescreva a descrição, não o loop.
Configuração do LangChain
A classe ChatOpenAI do LangChain aceita um base_url, então nenhum pacote de provedor personalizado é necessário. Defina model para uncensored, passe a chave do seu ambiente e vincule ferramentas com bind_tools. Instale langchain-openai e 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"]))O resultado de invoke é um AIMessage cuja lista tool_calls contém nomes e argumentos analisados, então você pula a etapa manual do JSON. Para obter um loop autônomo completo, passe o mesmo objeto llm para os construtores de agente do LangChain; o endpoint só precisa honrar a chamada de ferramentas, o que ele faz. Se um agente parece ignorar suas ferramentas, verifique se o docstring e as anotações de tipo estão presentes, já que eles se tornam o esquema que o modelo vê.
Configuração do LlamaIndex
O LlamaIndex entrega OpenAILike para servidores que seguem o formato de conclusões de chat. Três configurações importam. api_base aponta para a API, is_chat_model=True seleciona a rota de chat, e is_function_calling_model=True diz ao framework que ele pode enviar ferramentas. Defina context_window para 100000 para que seus divisores de texto e buffers de memória dimensionem corretamente. Instale 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)A partir daqui, você pode passar o mesmo llm para um trabalhador de agente ou um mecanismo de consulta. Lembre-se de que pipelines de recuperação podem empurrar muitos chunks para um prompt; com uma janela de 100.000 tokens compartilhada entre prompt e conclusão, limite o número de chunks recuperados e mantenha max_tokens modesto.
Operando agentes com orçamento limitado
Um agente transforma uma mensagem de usuário em várias requisições, então planeje capacidade. Uma tarefa com cinco rodadas de ferramentas custa seis chamadas de modelo. Dez tarefas simultâneas portanto produzem sessenta requisições em um curto espaço de tempo, e o teto de 300 requisições por minuto chega mais rápido do que um aplicativo de chat alcançaria. Coloque o throttle de balde de tokens do playbook de erros e limites de requisições na frente do seu cliente.
O contexto cresce a cada rodada porque cada resultado de ferramenta é anexado. Resuma ou remova saídas de ferramentas antigas uma vez que tenham sido usadas, e monitore usage.prompt_tokens; quando se aproximar de 50.000, comprima o histórico. Para custo, assuma uma tarefa ilustrativa de 6 chamadas com média de 3.000 tokens de prompt e 200 tokens de conclusão: isso são 18.000 tokens de entrada (cerca de $0,0045 a $0,25 por milhão) e 1.200 tokens de saída (cerca de $0,0012 a $1,00 por milhão). Essas contagens de tokens são suposições para as somas, não medições. Se você é novo no endpoint, comece com a documentação e o crédito de teste grátis.
Testando e depurando um agente
Agentes falham de maneiras que testes unitários em uma única função perdem, então teste a conversa. Registre a lista completa de mensagens para cada execução, incluindo argumentos e resultados de ferramentas, e reproduza falhas a partir do log. Três hábitos pagam rapidamente.
- Simule o modelo. Mock o cliente para que ele retorne mensagens
tool_callsroteirizadas. Isso testa seu registro, validação e término de loop sem rede e sem gasto. - Amostragem fixa. Use um
temperaturebaixo, como 0,2, para tarefas orientadas a ferramentas. Menor aleatoriedade dá JSON de argumento mais repetível, e você pode aumentar depois para etapas mais focadas em escrita. - Afirme sobre trajetórias. Para um conjunto pequeno de prompts fixos, verifique quais ferramentas foram chamadas e em que ordem, não apenas o texto final.
Observe também finish_reason. Um valor de length no meio de uma chamada de função significa que os argumentos foram cortados; aumente max_tokens (até 32.000) e o JSON será completado. O streaming funciona com funções também, mas acumule fragmentos de argumento por índice de chamada e analise apenas após o stream fechar. Para uma atualização sobre esses chunks, consulte a seção de streaming da referência.
Perguntas e respostas
A API suporta chamadas de ferramentas em paralelo em uma única resposta?
A mensagem contém uma matriz tool_calls, então processe cada entrada dela e envie uma mensagem de ferramenta por id de chamada.
Por que meu loop de agente fica infinito?
Geralmente os erros da ferramenta não são informativos ou o prompt do sistema nunca diz quando parar. Adicione um limite de etapas e retorne strings de erro claras.
Preciso do LangChain ou LlamaIndex?
Não. Um loop simples com o SDK da OpenAI é suficiente, e os frameworks apenas adicionam orquestração por cima do mesmo endpoint.
Como impedir que o modelo execute ferramentas destrutivas?
Nunca registre-as sem uma etapa de aprovação. Apenas as funções no seu registro podem ser executadas, então mantenha as ações de gravação sob confirmação humana.
Sua chave está a um formulário de distância
Crie uma conta, copie a chave, altere a URL base. Essa é toda a configuração.