Orquestração de agentes inteligentes em aplicações
Agentes
[editar | editar código]Agentes, de forma geral, são entidades de inteligência artificial capazes de ações orientadas em razão de um objetivo.[1] Para os nossos propósitos agentes serão definidos mais especificamente como uma abstração que existe da combinação de código, modelo de linguagem e memória capaz de cumprir um objetivo. O uso de agentes permite que uma aplicação demonstre comportamentos inteligentes de forma que seria impraticável de se definir de forma determinista apenas com uma linguagem de programação.
Tipos de agentes
[editar | editar código]Podemos dividir agentes de acordo com o nível de autonomia e autoridade que demonstram em suas aplicações.
| Níveis de
agência |
Descrição | Nome | Exemplo |
|---|---|---|---|
| 0 | Saída da LLM não tem impacto no programa. | Not an agent | process_llm_output(llm_response) |
| 1 | Saída da LLM controla flow básico do programa. | Router | if llm_decision(): path_a() else: path_b() |
| 2 | Saída da LLM decide a chamada de funções. | Tool caller | run_function(llm_chosen_tool, llm_chosen_args) |
| 3 | Saída da LLM controla iterações e continuação do programa. | Multistep Agent | while llm_should_continue(): execute_next_step() |
| 4 | Saída da LLM controla a chamada de outras LLMs | Multi-agent | if llm_trigger(): execute_agent() |
| 5 | Saída da LLM cria suas próprias funções e decide quando executá-las. | Code Agent | def custom_tool(args): ... |
No nível 0 temos apenas chamadas para o modelo de texto (LLM) sem que isso impacte o programa de alguma forma, enquanto no nível 5 o modelo de linguagem tem a autoridade de escrever suas próprias ferramentas, decidir quando executá-las e pedir ajuda para outros agentes conforme é conveniente.
Exemplos básicos
[editar | editar código]Abaixo temos um exemplo básico de agente que seria clássico no nível 3 (tool caller) da Tabela de Agência.
memory = [prompt]
def simple_agent(memory):
answer = ask_llm(memory)
action = parse(answer)
result = tool_call(action)
memory += [action, result]
if result[0] == “final_answer”:
return result[1]
else
return simple_agent(memory)
O agente começa com uma memória (string com o prompt) que estabelece a tarefa definida pelo usuário. A função simple_agent recebe essa memória e entrega para ask_llm, que por sua vez faz uma API call para algum modelo (alternativamente, o modelo poderia estar sendo executado localmente).
A answer entregue por ask_llm então é passada por um parser que tenta identificar chamadas de função que a LLM pode ter feito em sua resposta, e também se a LLM sinalizou que já terminou sua tarefa.
A função tool_call retorna um array bidimensional cuja primeira entrada informa se a LLM já terminou e a segunda entrada tem as respostas das ferramentas invocadas pela LLM. Finalmente, caso a LLM tenha decidido que ainda não terminou sua tarefa, a simple_agent é chamada de novo com sua memória atualizada pelas respostas das ferramentas que invocou.
Prompt
[editar | editar código]Não existe consenso na melhor forma de fazer um prompt para agentes, mas existem estruturas básicas que demonstram desempenho melhor do que ingenuamente descrever de forma direta o que você precisa para a LLM. Entre elas, vamos ressaltar a ReACT (Reason and ACTion) em que a LLM é incentivada a raciocinar sobre o problema que está tentando resolver antes de tomar qualquer ação em relação ao programa
Prompt = ["Você é um assistente bonito e inteligência em uma clínica médica e está ajudando o doutor a fazer um diagnóstico. Você deve cumprir sua tarefa seguindo os três passados: PENSAMENTO, AÇÃO E MEMÓRIA.
Durante o passo de PENSAMENTO você deve planejar como cumprir sua tarefa.
Durante o passo de AÇÃO tem a sua disposição as ferramentas checar_medicação() e checar_doença() que buscam respectivamente informações precisas sobre medicações e doenças em bancos de dados.
Para usar fazer a chamada correta da ferramente você precisa seguir o formato dos exemplos abaixo:
{”tool”: “buscar_doença”, “parâmetro”: “Dengue”}
{”tool”: “buscar_medicamento”, “parâmetro”: “Paracetamol”}
{”tool”: “responder_médico”, “parâmetro”: “sua_resposta”}
Lembre-se de apenas responder o médico apenas se tiver certeza da sua resposta.
Por último, no passo de MEMÓRIA, você deve fazer anotações das coisas importantes que
você quer lembrar no futuro para continuar resolvendo sua tarefa. “]
Memória
[editar | editar código]Note que acima pedimos para o modelo executar um passo de memória. Esse pedido tem a função de deixar que o modelo faça o gerenciamento de sua própria memória, isto é, decida quais informações são importantes o suficiente para serem levadas para o próximo passo. Evitamos assim que o histórico inteiro de saídas do agente seja repassado para o agente na próxima iteração, o que rapidamente levaria a uma memória sobrecarregada causando dois problemas principais:
- Custos. Usando APIs como Gemini, OpenAI, Claude, entre outros, pagamos tanto pelos tokens de input quanto de output. Se temos uma situação em que cada usuário pode ativar agentes que acumulam todo o histórico de suas interações, as cobranças da API podem nos levar a um falência rápida e sorrateira. E mesmo nos casos raros em que o modelo está sendo instanciado sobre o nosso cuidado, ainda é desejável evitar custos computacionais desnecessários.
- Desempenho. Modelos de linguagem notoriamente sofrem uma queda de performance quando precisam processar contextos muito longos. É possível que ao mantermos o preciosismo de não deixar o agente esquecer, façamos com que o agente se torne como paciente S, incapaz de se focar nas coisas que realmente importam.
A forma de gerenciamento de memória explicada acima é frequentemente chamada de Compress-then-Recall, além dela existem algumas outras técnicas que vale a pena mencionar:
- Vector Snapshotting - Cada mensagem é embutida em um vetor e enviada para um índice de similaridade (FAISS, Milvus, etc.). Na próxima iteração, em vez de repassar todo o histórico, o agente consulta o índice apenas com a query atual e recupera os trechos mais semelhantes.
- Hierarquia Curto, Longo Prazo - Divide-se a memória em dois anéis: Scratchpad que guarda as poucas últimas mensagens mantidas integralmente para raciocínio de curto prazo, em contraste da Base Persistente que mantém fatos, decisões finais ou key-value pairs (e.g., “ID do cliente → plano contratado”) salvos em um banco de dados (SQL, Redis, KV). Periodicamente, um critério (
if importância > τ) promove elementos do scratchpad para a base persistente; o restante é descartado.
Além da simplicidade
[editar | editar código]Acima mostramos uma versão simplificada da forma básica que um agente poderia ter, por isso, é importante ressaltar que aplicações reais possuem estruturas bem mais complicadas. Frequentemente precisamos instanciar diversos agentes diferentes com custos diferentes, além de garantir uma atualização dinâmica do prompt, principalmente para os níveis mais altos de agência.
Recursos recomendados
[editar | editar código]Escrito por Adrian Valentim.