Recentemente construí um agente de IA para fitness no meu app usando LangChain.js. Este post cobre a implementação real — não um exemplo brinquedo: decisões arquiteturais concretas, o design das ferramentas que tornam o agente útil e o que aprendi no caminho.
O Desafio: Construindo um Agente de Fitness Contextual
O domínio do fitness traz dificuldades reais para agentes de IA. Os usuários descrevem atividades das formas mais variadas ("comi frango" vs "comi 200g de peito de frango grelhado"). O agente precisa lembrar o histórico do usuário. Dados nutricionais e de exercícios exigem precisão para rastreamento de saúde. E as respostas precisam soar imediatas e personalizadas.
Esses não são problemas que se resolvem com um chatbot. Eles demandam um sistema que compreenda intenção, gerencie contexto e execute tarefas específicas. É aí que o LangChain.js se encaixa.
Por Que LangChain.js?
LangChain.js é um framework open-source para construir agentes de IA capazes de raciocinar, tomar decisões e chamar ferramentas externas a partir de entrada em linguagem natural. Ele oferece uma forma modular de orquestrar LLMs como o GPT com fluxos de trabalho estruturados, memória, ferramentas e prompts.
É a escolha certa quando sua aplicação precisa interagir com APIs ou bancos de dados externos com base em consultas do usuário, manter o histórico de conversa para respostas personalizadas, selecionar dinamicamente qual função ou ferramenta usar, interpretar e retornar saídas estruturadas como JSON, e escalar com segurança por meio de abstração de ferramentas e tratamento de erros.
Visão Geral da Arquitetura
A implementação usa uma estrutura limpa e modular construída em torno dos conceitos centrais do LangChain.js:
// Core service structure
export default class AiCoreService {
protected llm = new ChatOpenAI({
model: 'gpt-4.1-mini',
temperature: 0
});
protected prompt = ChatPromptTemplate.fromMessages([
['system', fs.readFileSync(path.join(__dirname, 'prompt.md'), 'utf8')],
new MessagesPlaceholder('chat_history'),
['human', '{input}'],
new MessagesPlaceholder('agent_scratchpad')
]);
async run(rawInput: string): Promise<AiCoreServiceResponse> {
const agent = await createOpenAIToolsAgent({
llm: this.llm,
prompt: this.prompt,
tools
});
const agentExecutor = new AgentExecutor({
agent,
verbose: false,
tools
});
const result = await agentExecutor.invoke(
{ input: rawInput, chat_history: this.chatHistory },
{ metadata: { userId: this.userId } }
);
return this.parseStructuredOutput(result.output);
}
}
Design Inteligente de Ferramentas
O verdadeiro poder vem de ferramentas bem projetadas que lidam com tarefas específicas relacionadas ao fitness.
1. Ferramenta de Busca no Contexto
Essa ferramenta permite ao agente pesquisar o histórico do usuário usando busca semântica:
export default new DynamicStructuredTool({
name: 'search_on_context',
description: 'Search user\'s personal context and history to answer questions about past activities, progress, or feelings.',
schema: z.object({
questionOrMessage: z.string().describe('User question or message'),
questionOrMessageLanguage: z.number().describe('Original language code'),
questionOrMessageInEnglish: z.string().describe('Question translated to English')
}),
func: async ({ questionOrMessageInEnglish }, runManager?, config?) => {
const userId = config?.metadata?.userId;
const graphiti = new GraphitiService('user', userId);
const searchResults = await graphiti.search({
query: questionOrMessageInEnglish
});
return searchResults.results
.filter(result => result.invalid_at === null)
.map(result => result.fact)
.join('\n');
}
});
Leia mais sobre Graphiti em: Long-Term Memory for AI: How Graphiti Works
2. Ferramenta de Registro de Alimentação
A ferramenta de alimentação mostra quanta lógica pertence a uma ferramenta bem projetada:
export default new DynamicStructuredTool({
name: 'register_food_eating',
description: 'Register food consumption when user reports eating something.',
schema: z.object({
date: z.string().optional().describe('Date in ISO format'),
foods: z.array(z.object({
name: z.string().describe('Food name in Portuguese'),
englishName: z.string().describe('Food name in English'),
amount: z.number().describe('Amount consumed'),
unit: z.string().describe('Measurement unit (g, ml, etc.)'),
amountInGrams: z.number().describe('Amount in grams'),
calories: z.number().describe('Calories in portion'),
proteinsInGrams: z.number().describe('Protein content'),
carbsInGrams: z.number().describe('Carbohydrate content'),
fatsInGrams: z.number().describe('Fat content'),
confidence: z.number().min(0).max(100).describe('Accuracy confidence 0-100'),
source: z.string().optional().describe('Data source (TACO, USDA, etc.)')
}))
}),
func: async ({ foods, date }, runManager?, config?) => {
// Process and validate food data
const processedFoods = await Promise.all(
foods.map(food => validateAndEnrichFoodData(food))
);
// Save to user's daily log
const dailyMealLog = await DailyMealLogService.findOrCreate(userId, date);
const result = await dailyMealLog.addFoods(processedFoods);
return formatFoodRegistrationResponse(result);
}
});
Estratégia Avançada de Prompting
O prompt de sistema é essencial para o comportamento do agente. Veja a estrutura real utilizada:
You are the AI agent for the app www.peso-certo.com, an app focused on health, fitness, weight loss, and overall well-being.
Your mission is to help users achieve their physical goals by providing **clear**, **useful**, and **personalized** answers, without offering medical diagnoses or treatments.
### Available Tools and Functions:
• When the user asks about their history, use `search_on_context`.
• When the user reports having eaten something with quantity, use `register_food_eating`.
• When the user reports exercising, use `register_exercise`.
• When the user expresses emotions or reflections, use `register_diary_entry`.
### Important Rules:
- For exercises, always recommend **demonstration videos and photos**.
- For food substitutions, match them **by calorie content**.
- Respond in the user's language.
- Use headings and emojis moderately.
- **Never provide medical diagnoses.**
## Response Format:
If you're not calling a function, respond in JSON:
{
"answer": "Markdown-formatted answer",
"sources": [{ "title": "Title", "url": "https://example.com" }]
}
Gerenciamento de Contexto e Memória
O serviço de chat gerencia o fluxo de conversa e o contexto:
export default class ChatService {
constructor(readonly chat: DocumentType<GPTConversation>) {
const userId = this.chat.userId.toString();
this.aiCoreService = new AiCoreService(userId);
this.messageProcessingService = new MessageProcessingService(this);
}
async addUserMessage(params: {
message?: string | null
imageData?: string | null
audioData?: string | null
}) {
const content = await this.messageProcessingService.processMessage(params);
this.chat.messages.push({ role: 'user', content });
this.aiCoreService.setChatHistory(this.chat.messages);
return this;
}
async invokeAiCore() {
const response = await this.aiCoreService.run(
await this.getLastUserMessage()
);
this.addAssistantMessage(response);
return this;
}
}
Análise da Saída Estruturada
Um recurso central é lidar tanto com chamadas de ferramentas quanto com respostas estruturadas:
parseStructuredOutput(output: string): AiCoreServiceResponse {
let content = output;
let sources: Array<{ title: string, url: string }> | undefined;
try {
// Try to parse JSON response
const match = output.match(/```json\s*([\s\S]*?)```/i);
if (match) {
const json = JSON.parse(match[1]);
content = json.answer;
sources = json.sources;
} else {
const json = JSON.parse(output);
content = json.answer;
sources = json.sources;
}
} catch (e) {
// Fallback to raw output if parsing fails
}
return { content, sources: sources || this.DEFAULT_SOURCES };
}
Principais Aprendizados de Implementação
1. Temperatura Zero para Consistência
temperature: 0 produz chamadas de ferramentas consistentes e reduz alucinações em saídas estruturadas.
2. Processamento de Alimentos Baseado em Confiança
A ferramenta de alimentação valida dados em APIs externas quando a confiança é baixa:
const process = async (food: Food): Promise<Food> => {
if (food.confidence >= 85) {
return food;
}
// Search external food databases
const searchResults = await FoodSearchService.searchFoods(
food.name,
food.englishName,
food.amount,
food.unit
);
const bestMatch = await FoodSearchService.selectBestFoodMatch(
food.name,
searchResults,
{ macros: extractMacros(food) }
);
return enrichFoodWithExternalData(food, bestMatch);
};
3. Suporte Multilíngue
O agente processa entrada em português enquanto utiliza inglês para chamadas de API e processamento de dados.
4. Injeção Contextual do ID do Usuário
As ferramentas recebem o contexto do usuário por meio do parâmetro metadata, possibilitando respostas personalizadas sem expor dados sensíveis ao modelo.
Lições Aprendidas
- O design das ferramentas é a parte mais difícil. Ferramentas atômicas, bem definidas e com descrições claras fazem a diferença entre um agente que adivinha e um que age corretamente.
- O prompt de sistema precisa de regras explícitas sobre quando usar cada ferramenta — não apenas o que cada ferramenta faz.
- Combinar chamadas de ferramentas com respostas JSON estruturadas oferece flexibilidade sem perder consistência.
- A busca semântica sobre o histórico do usuário é o que faz as respostas parecerem pessoais, e não genéricas.
- Fallbacks elegantes e tratamento de erros são inegociáveis em produção.
Conclusão
Um agente de IA pronto para produção exige atenção cuidadosa ao design das ferramentas, à estratégia de prompting e à arquitetura do sistema. O domínio do fitness é um bom teste de estresse porque mistura dados estruturados (macros, volumes de treino) com entrada não estruturada (como os usuários realmente falam sobre o dia deles).
O insight que mudou minha forma de pensar sobre isso: o agente não é um chatbot. É um coordenador. Ele compreende a intenção, recupera dados relevantes e executa a ação correta. Acerte as ferramentas, seja específico no prompt, e o restante segue naturalmente.