Construindo um agente de código com LangChain e ferramentas customizadas

1. Fundamentos do LangChain para agentes de código

O LangChain é um framework que facilita a construção de aplicações baseadas em LLMs (Large Language Models). Para agentes de código, sua arquitetura se baseia em quatro componentes principais: chain (cadeia de chamadas), tool (ferramenta que o agente pode usar), memory (memória de conversa) e executor (orquestrador que decide qual ação tomar).

A diferença fundamental entre chains e agents está na autonomia: chains seguem um fluxo pré-definido, enquanto agents decidem dinamicamente quais ferramentas usar e em qual ordem. O AgentExecutor gerencia esse loop de decisão-ação-observação.

Para configurar o ambiente:

pip install langchain langchain-community langchain-openai

Configuração básica com OpenAI:

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import Tool

llm = ChatOpenAI(model="gpt-4", temperature=0)

2. Definindo o escopo do agente de código

Nosso agente será especializado em tarefas de revisão e correção de código Python. Ele deve ser capaz de:

  • Ler arquivos do sistema
  • Executar código em ambiente controlado
  • Navegar em repositórios Git
  • Analisar diffs e commits
  • Sugerir correções automaticamente

Boas práticas importantes:
- Limitar o contexto máximo do LLM para evitar estouro de tokens
- Implementar timeouts em todas as ferramentas
- Sanitizar entradas do usuário para evitar injeção de comandos

3. Construindo ferramentas customizadas (Tools)

Cada Tool no LangChain precisa de: nome único, descrição clara (usada pelo LLM para decidir quando chamá-la), schema de argumentos opcional e função executora.

Ferramenta para ler arquivos:

import os
from pydantic import BaseModel, Field
from langchain.tools import tool

class ReadFileInput(BaseModel):
    file_path: str = Field(description="Caminho absoluto do arquivo a ser lido")

@tool(args_schema=ReadFileInput)
def read_file_tool(file_path: str) -> str:
    """Lê o conteúdo de um arquivo de código no sistema."""
    if not os.path.exists(file_path):
        return f"Erro: arquivo {file_path} não encontrado"
    with open(file_path, 'r', encoding='utf-8') as f:
        return f.read()

Ferramenta para executar código com sandbox:

import subprocess
import tempfile
import os

class RunCodeInput(BaseModel):
    code: str = Field(description="Código Python a ser executado")
    timeout: int = Field(default=10, description="Timeout em segundos")

@tool(args_schema=RunCodeInput)
def run_code_tool(code: str, timeout: int = 10) -> str:
    """Executa código Python em ambiente isolado e retorna a saída."""
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(code)
        temp_path = f.name

    try:
        result = subprocess.run(
            ['python', temp_path],
            capture_output=True,
            text=True,
            timeout=timeout
        )
        output = result.stdout if result.returncode == 0 else result.stderr
        return output[:2000]  # Limitar tamanho da resposta
    except subprocess.TimeoutExpired:
        return "Erro: execução excedeu o tempo limite"
    finally:
        os.unlink(temp_path)

4. Integração com repositórios Git e versionamento

Ferramenta para checkout de branches:

import git
from pydantic import BaseModel, Field

class GitCheckoutInput(BaseModel):
    repo_path: str = Field(description="Caminho do repositório local")
    branch: str = Field(description="Nome da branch para checkout")

@tool(args_schema=GitCheckoutInput)
def git_checkout_tool(repo_path: str, branch: str) -> str:
    """Faz checkout para uma branch específica em um repositório Git."""
    try:
        repo = git.Repo(repo_path)
        repo.git.checkout(branch)
        return f"Checkout realizado para branch {branch}"
    except Exception as e:
        return f"Erro no checkout: {str(e)}"

Ferramenta para buscar diffs:

class GitDiffInput(BaseModel):
    repo_path: str = Field(description="Caminho do repositório")
    commit_a: str = Field(description="Hash do commit inicial")
    commit_b: str = Field(description="Hash do commit final")

@tool(args_schema=GitDiffInput)
def git_diff_tool(repo_path: str, commit_a: str, commit_b: str) -> str:
    """Retorna o diff entre dois commits."""
    try:
        repo = git.Repo(repo_path)
        diff = repo.git.diff(commit_a, commit_b)
        return diff[:3000]  # Limitar tamanho
    except Exception as e:
        return f"Erro ao obter diff: {str(e)}"

5. Implementação do loop agente e memória

Configuramos o agente com memória de curto prazo para manter contexto entre interações:

from langchain.memory import ConversationBufferMemory
from langchain.agents import create_react_agent
from langchain.agents import AgentExecutor
from langchain.prompts import PromptTemplate

# Lista de ferramentas disponíveis
tools = [read_file_tool, run_code_tool, git_checkout_tool, git_diff_tool]

# Template do prompt para o agente ReAct
prompt = PromptTemplate.from_template(
    """Você é um assistente especializado em análise de código Python.
Use as ferramentas disponíveis para ler, executar e analisar código.

{chat_history}
Pergunta: {input}
{agent_scratchpad}"""
)

# Criar agente com memória
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    max_iterations=10,
    early_stopping_method="generate",
    handle_parsing_errors=True
)

6. Orquestração de múltiplas ferramentas em um fluxo real

Exemplo completo de uso: agente encontra um bug, sugere correção e gera PR.

# Exemplo de interação
resultado = agent_executor.invoke({
    "input": """Analise o arquivo /projeto/app.py. 
    Encontre possíveis bugs de sintaxe ou lógica.
    Execute o código para verificar se há erros em tempo de execução.
    Se encontrar problemas, sugira correções."""
})

print(resultado["output"])

Para agrupar ferramentas relacionadas, usamos Toolkits:

from langchain.agents import Tool

git_toolkit = [
    git_checkout_tool,
    git_diff_tool,
    Tool(name="git_log", func=lambda path: git.Repo(path).git.log("--oneline", "-10"),
         description="Mostra os últimos 10 commits")
]

7. Testes, validação e segurança do agente

Testes unitários para cada ferramenta:

import pytest
from tools import read_file_tool

def test_read_file_success():
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f:
        f.write("print('hello')")
        f.flush()
        result = read_file_tool(f.name)
        assert "print('hello')" in result

def test_read_file_not_found():
    result = read_file_tool("/caminho/inexistente.py")
    assert "não encontrado" in result

Medidas de segurança essenciais:

import re

def sanitize_command(command: str) -> str:
    """Remove comandos perigosos antes de executar."""
    dangerous = ['rm -rf', 'sudo', 'chmod 777', 'dd if=']
    for cmd in dangerous:
        if cmd in command.lower():
            raise ValueError(f"Comando bloqueado por segurança: {cmd}")
    return command

8. Deploy e próximos passos

Empacotamento como API FastAPI:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class QueryRequest(BaseModel):
    question: str

@app.post("/agent/query")
async def query_agent(request: QueryRequest):
    result = agent_executor.invoke({"input": request.question})
    return {"response": result["output"]}

Roadmap para evolução:
1. Suporte a múltiplos modelos (Claude, Gemini, modelos locais via Ollama)
2. Integração com ferramentas de qualidade como SonarQube e ESLint
3. Extensão para VS Code via LSP (Language Server Protocol)
4. Cache inteligente de resultados para reduzir custos com tokens

O agente construído pode ser estendido para suportar outras linguagens, integrar com CI/CD e servir como assistente de desenvolvimento em tempo real.


Referências