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

  1. 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.
  2. O prompt de sistema precisa de regras explícitas sobre quando usar cada ferramenta — não apenas o que cada ferramenta faz.
  3. Combinar chamadas de ferramentas com respostas JSON estruturadas oferece flexibilidade sem perder consistência.
  4. A busca semântica sobre o histórico do usuário é o que faz as respostas parecerem pessoais, e não genéricas.
  5. 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.