AI Agents — LangGraph / SpringAI / LangChain
Princípios para Agentes em Produção
- Determinismo onde possível: temperature baixa, seeds, mocks em testes.
- Tool calling seguro: validar inputs de tools; nunca executar código arbitrário do LLM.
- Observabilidade obrigatória: trace cada chamada LLM (tokens, latência, custo).
- Human-in-the-loop para ações irreversíveis (pagamento, delete, deploy).
- Fallback graceful: timeout, retry com backoff, resposta degradada.
LangGraph Python
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
next_action: str
def router(state: AgentState) -> str:
if state["next_action"] == "search":
return "search"
return "respond"
def search_node(state: AgentState) -> AgentState:
# RAG retrieval
return {"messages": [("assistant", "Resultado da busca...")]}
def respond_node(state: AgentState) -> AgentState:
return {"messages": [("assistant", "Resposta final")]}
graph = StateGraph(AgentState)
graph.add_node("search", search_node)
graph.add_node("respond", respond_node)
graph.add_conditional_edges("router", router, {"search": "search", "respond": "respond"})
graph.add_edge("search", "respond")
graph.add_edge("respond", END)
app = graph.compile()
# Human-in-the-loop com interrupt
app = graph.compile(interrupt_before=["respond"])
LangChain Python (LCEL + RAG)
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_community.vectorstores import Chroma
llm = ChatOpenAI(model="gpt-4o", temperature=0)
retriever = Chroma.from_documents(docs, OpenAIEmbeddings()).as_retriever()
prompt = ChatPromptTemplate.from_messages([
("system", "Responda com base no contexto. Se não souber, diga que não sabe."),
("human", "Contexto: {context}\n\nPergunta: {question}"),
])
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
SpringAI (Java)
@Configuration
public class AiConfig {
@Bean
ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) {
return builder
.defaultSystem("Você é um assistente. Use apenas o contexto fornecido.")
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
.build();
}
}
@Service
@RequiredArgsConstructor
public class RagService {
private final ChatClient chatClient;
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
// Tool Calling
@Bean
@Description("Busca pedido por ID")
Function<OrderQuery, OrderSummary> findOrder(OrderRepository repo) {
return query -> OrderSummary.from(repo.findById(query.orderId()).orElseThrow());
}
LangGraph4J (Java)
// Conceito equivalente ao LangGraph Python
StateGraph<AgentState> graph = new StateGraph<>(AgentState.class);
graph.addNode("retrieve", state -> retrieveDocuments(state));
graph.addNode("generate", state -> generateAnswer(state));
graph.addEdge(START, "retrieve");
graph.addEdge("retrieve", "generate");
graph.addEdge("generate", END);
CompiledGraph<AgentState> app = graph.compile();
// Checkpointing para conversas longas
var config = RunnableConfig.builder().threadId(sessionId).build();
app.invoke(initialState, config);
Padrões Agnósticos
| Padrão |
Descrição |
Quando usar |
| ReAct |
Reason + Act em loop |
Tarefas com tools externas |
| Reflection |
Crítica e refinamento da resposta |
Qualidade de texto/código |
| Planning |
Plano antes de executar |
Tarefas multi-step complexas |
| Multi-Agent |
Agentes especializados |
Domínios distintos (research + code) |
| RAG |
Retrieval + Generation |
Q&A sobre documentos internos |
Testes de Agentes
# Mock LLM para testes determinísticos
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
async def test_agent_returns_answer_from_context():
with patch("langchain_openai.ChatOpenAI.ainvoke") as mock_llm:
mock_llm.return_value = AIMessage(content="Resposta esperada")
result = await agent.ainvoke({"question": "Qual o prazo?"})
assert "Resposta esperada" in result["answer"]
# Avaliação com LangSmith (opcional)
# - dataset de perguntas/respostas gold
# - métricas: faithfulness, relevance, latency
@Test
void should_call_find_order_tool_when_asked_about_order() {
when(chatClient.prompt().user(anyString()).call().content())
.thenReturn("Pedido #123 está em trânsito");
var response = agentService.handle("Onde está meu pedido 123?");
assertThat(response).contains("trânsito");
verify(orderRepository).findById(OrderId.of("123"));
}
Observabilidade de LLM
# OpenTelemetry + LangSmith
from langsmith import traceable
@traceable(name="rag_pipeline")
async def rag_pipeline(question: str) -> str:
docs = await retriever.ainvoke(question)
return await chain.ainvoke({"context": docs, "question": question})
Métricas essenciais:
llm.tokens.input / llm.tokens.output
llm.latency.seconds
llm.errors.total
rag.retrieval.count
Anti-Patterns
| Anti-Pattern |
Risco |
| Prompt injection sem sanitização |
Execução indevida |
| Tool sem validação de input |
SQL/command injection |
| Sem timeout em chamadas LLM |
Request hang |
| Context window ilimitado |
Custo e latência |
| Sem eval em CI |
Regressão silenciosa de qualidade |
Referências
~/.cursor/skills/references/langgraph-patterns.md