2
respostas

Human in loop

1 - Fundamentos de Human in the Loop (HITL) e importância na orquestração de agentes: HITL é o padrão em que o grafo/agent pausa em pontos críticos, persiste o estado, expõe o que será feito e aguarda aprovação/edição humana antes de continuar. Em LangGraph isso é viabilizado por interrupts (pontos de pausa) + checkpointer/persistence (salvar checkpoints/snapshots para retomar depois), permitindo revisão humana, correções e retomada do fluxo sem perder contexto.

2 - Configurar ambiente com bibliotecas/padrões das aulas anteriores (código): nas suas aulas/repo aparecem pacotes como langgraph, langchain, langchain-openai, python-dotenv etc., além do padrão de instalar via pip/requirements.

# (opção A) instalar exatamente o que já estava no seu requirements.txt
pip install -r requirements.txt

# (opção B) instalar apenas o essencial do HITL (caso você queira mínimo)
pip install -U langgraph langchain-core langchain-openai python-dotenv

3 - Implementar a função reduceMessages para atualizar o estado substituindo ou anexando mensagens (código): o comportamento desejado é “append-only, exceto quando o ID coincide (substitui)”, que é justamente a ideia do reducer de mensagens (como add_messages).

from typing import List, Dict, Any

Message = Dict[str, Any]

def reduceMessages(left: List[Message], right: List[Message]) -> List[Message]:
    """
    Merge de mensagens:
    - se 'id' de uma mensagem nova já existir, substitui a antiga
    - senão, anexa ao final
    """
    if left is None:
        left = []
    if right is None:
        right = []

    index = {m.get("id"): i for i, m in enumerate(left) if m.get("id") is not None}
    merged = list(left)

    for m in right:
        mid = m.get("id")
        if mid is not None and mid in index:
            merged[index[mid]] = m
        else:
            merged.append(m)
            if mid is not None:
                index[mid] = len(merged) - 1
    return merged

4 - Usar UUID para garantir unicidade das mensagens (código):

from uuid import uuid4

def make_message(role: str, content: str) -> dict:
    return {
        "id": str(uuid4()),     # UUID garante unicidade
        "role": role,
        "content": content,
    }

5 - Adicionar “interrupt before action” para aprovação humana antes da execução (código): LangGraph suporta interrupções estáticas configuradas na compilação para pausar antes de um nó específico, viabilizando aprovação humana.

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()

# ... (builder = StateGraph(...); add_node etc.)

app = builder.compile(
    checkpointer=checkpointer,
    interrupt_before_nodes=["action"],  # pausa antes do nó "action"
)

6 - Estruturar o grafo com nós de ação, pausa e integração com ferramentas externas (código):

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()

# ... (builder = StateGraph(...); add_node etc.)

app = builder.compile(
    checkpointer=checkpointer,
    interrupt_before_nodes=["action"],  # pausa antes do nó "action"
)

7 - Capturar thread_id dinâmico para evitar conflito com históricos anteriores (código): em LangGraph, o thread_id é a “chave” da persistência: reutilizar retoma o histórico; usar novo começa “thread” limpa.

from uuid import uuid4

thread_id = str(uuid4())  # thread novo para não conflitar com históricos antigos

config = {
    "configurable": {
        "thread_id": thread_id
    }
}

8 - Configurar pausa e requerer intervenção humana em momentos críticos (código): o mecanismo base é interrupt + checkpointer; ao interromper, o estado é salvo e o fluxo aguarda retomada.

# (A) pausa estática: já configurada no compile com interrupt_before_nodes=["action"]
# Resultado: a execução para antes de rodar o nó "action".

# (B) pausa dinâmica: dentro de um nó, usando interrupt()
from langgraph.types import interrupt

def approval_node(state: AgentState):
    decision = interrupt({
        "question": "Aprova executar a ação X?",
        "draft_id": state.get("draft_id")
    })
    # decision será o payload passado no Command(resume=...) na retomada
    return {"approved": bool(decision.get("approved", False))}
    
2 respostas

9 - Injetar manualmente uma resposta modificada no estado do agente (código): a forma mais “segura” (e compatível com o reducer) é editar/substituir a mensagem pelo mesmo id durante a retomada (human edit), para que reduceMessages faça a substituição.

from langgraph.types import Command

# suponha que o grafo interrompeu e você exibiu ao humano o rascunho (draft_id)
edited_text = "Texto revisado pelo humano (mais empático e claro)."
draft_id = "COLE_AQUI_O_DRAFT_ID_EXIBIDO"

# cria mensagem substituta com MESMO id => reducer substitui
replacement = {"id": draft_id, "role": "assistant", "content": edited_text}

# retomar a execução injetando a edição como 'resume'
resume_payload = {"approved": True, "replacement": replacement}

# ao chamar invoke com Command(resume=...), o nó que chamou interrupt() recebe isso
result = app.invoke(Command(resume=resume_payload), config=config)
``

10 - Usar snapshots para registrar e restaurar o estado durante a intervenção (código): com persistence/checkpointer, LangGraph salva checkpoints (snapshots) a cada passo, organizados por thread_id. Você pode inspecionar o estado atual e retomar a partir de um checkpoint específico usando checkpoint_id (time travel / restore)

# inspeção do estado atual (snapshot) — útil para UI de revisão humana
snapshot = app.get_state(config)   # StateSnapshot (conceito descrito na doc)
print(snapshot)

# restaurar/rodar a partir de um checkpoint específico:
checkpoint_id = "COLE_AQUI_UM_CHECKPOINT_ID_EXISTENTE"

config_restore = {
    "configurable": {
        "thread_id": thread_id,
        "checkpoint_id": checkpoint_id
    }
}

# invocar a partir daquele ponto (o grafo carrega aquele snapshot)
out = app.invoke(None, config=config_restore)
print(out)

11 - Testar o fluxo integrado e validar que a intervenção humana altera a resposta (código): (roteiro mínimo: 1) roda até interromper antes de action, 2) recupera draft_id do estado, 3) injeta edição com mesmo id, 4) retoma e confere histórico)

# 1) Primeira execução: deve pausar antes de "action" (por interrupt_before_nodes=["action"])
initial = app.invoke(
    {"messages": [make_message("user", "Quero executar a ação X")]},
    config=config
)

# 2) Inspeciona snapshot e pega draft_id
snap = app.get_state(config)
draft_id = snap.values.get("draft_id")

# 3) Humano edita o rascunho (substituição por id)
replacement = {"id": draft_id, "role": "assistant", "content": "Rascunho revisado pelo humano ✅"}

# 4) Retoma o fluxo (e deixa seguir para action)
resume = Command(resume={"approved": True, "replacement": replacement})
final = app.invoke(resume, config=config)

# 5) Verifica se a mensagem revisada está no histórico (foi substituída, não duplicada)
snap2 = app.get_state(config)
msgs = snap2.values.get("messages", [])
print([m for m in msgs if m.get("id") == draft_id])  # deve conter o texto revisado

Oi, Ricardo.

Que excelente exercício de consolidação. Você estruturou os pilares do Human in the Loop (HITL) com o LangGraph de forma muito organizada, cobrindo desde a persistência até a manipulação direta do estado.

Parabéns por praticar.

Alura Conte com o apoio da comunidade Alura na sua jornada. Abraços e bons estudos!