Building an AI Coach: From Experiment to LLM-Powered Application

A few months ago, I wrote about using AI as my coach for a cross-country triathlon.
The experiment started with a simple question: could AI actually help me prepare for a difficult endurance race when I had only a couple of months to train?
The answer turned out to be yes, but not because AI could generate a training plan. The interesting part was the conversation around the plan: giving the model my training data, asking questions about recovery and nutrition, adjusting workouts around real-life constraints, and continuously adding new information.
As I used AI more, I started thinking about the engineering problem behind it.
What would it take to turn this experience into a real application?
That became Andre-AI.
From AI experiment to application
The first version of my AI coaching setup was essentially a collection of conversations with ChatGPT and Gemini. I gradually added more context: Garmin data, sleep, weight, nutrition, training history, goals, and personal observations.
The pattern became clear: the more relevant context the AI had, the more useful its answers became.
That led to the core idea behind Andre-AI: instead of building another fitness dashboard and adding an AI chatbot on top, build the application around the LLM.
The LLM should be able to understand the user's context, access relevant data, use tools when necessary, maintain conversations, and generate plans.
The application becomes the system around the model.
What is Andre-AI?
Andre-AI is an AI-powered fitness and health coaching application.
From a user's perspective, the workflow is straightforward. You select a coaching persona, start a conversation, and interact with the coach. Behind the scenes, however, the application has to decide what information the model should receive and what capabilities it should have.
The architecture is roughly:
User
↓
Conversation
↓
Persona
↓
Context
↓
Tools
↓
LLM
↓
Response
This is the part I find most interesting from an engineering perspective.
The LLM is responsible for generating the response, but the application is responsible for providing the model with the right information and capabilities.
The LLM is not just a chatbot
A basic AI integration can be very simple:
User → API → LLM → Response
That works for a general-purpose chatbot, but it doesn't work particularly well for a personalized coach.
A coach needs to know things about the person it is coaching. It needs access to historical information, current data, previous conversations, and potentially external information.
Andre-AI therefore treats the LLM as one component inside a larger application.
┌──────────────┐
│ User │
└──────┬───────┘
│
▼
┌──────────────┐
│ Application │
└──────┬───────┘
│
┌──────────┴──────────┐
│ │
▼ ▼
Context Tools
│ │
└──────────┬──────────┘
▼
┌──────────────┐
│ LLM │
└──────┬───────┘
│
▼
Response
This separation turned out to be one of the most important design decisions in the project.
Personas are more than system prompts
One of the first things I wanted was the ability to have different types of coaches.
Instead of hard-coding a single system prompt, personas are stored as data.
A persona defines the coaching style and references the information that should be available to the model. For example, a persona can be associated with an athlete profile, weight history, nutrition information, training templates, or previous plans.
When a session starts, these references are resolved and included in the model's context.
So instead of simply telling the model:
You are a triathlon coach.
the application can provide something closer to:
You are this type of coach.
Here is the athlete profile.
Here is the recent training history.
Here is the current goal.
Here is the relevant nutrition information.
Here are the athlete's preferences.
This makes the persona a structured part of the application rather than just a prompt string.
It also means that changing the coaching behavior doesn't necessarily require changing application code.
Context is where the real AI engineering starts
One of the biggest lessons from the project is that integrating an LLM API is relatively easy.
The harder problem is deciding what the model should know.
Some information is relatively static:
- athlete profile
- goals
- training preferences
- nutrition preferences
- coaching persona
Other information changes constantly:
- weight
- sleep
- recent activities
- recovery
- training load
- weather
- current training plan
I didn't want to put everything into every prompt.
Instead, Andre-AI has a context layer that resolves the relevant information and prepares it for the LLM.
This is important because context isn't just a technical implementation detail. It directly affects the quality of the AI response.
The previous article demonstrated this from the user's perspective. Once I started giving the AI my actual training data instead of asking generic questions, the quality of the conversation improved significantly.
Andre-AI essentially turns that observation into an application architecture.
Giving the model tools
Context is useful when the information is already available.
Sometimes the model needs to retrieve something.
That's where tools come in.
Personas can be given access to model-callable tools for things such as:
- sleep data
- activity history
- weather
- athlete profile
- training plans
The model can request the information it needs while processing a conversation.
For example:
User:
Should I do a hard workout today?
↓
LLM:
I need to check recent recovery.
↓
Sleep tool
Activity tool
↓
Tool results
↓
LLM
↓
Response
This is a very different approach from putting an entire database export into the prompt.
The model can access capabilities when they are relevant, while the application controls what those capabilities actually do.
Garmin becomes an AI data source
The original coaching experiment relied heavily on Garmin data.
Andre-AI brings that data into the application through synchronization and imports.
The application can work with information such as:
- sleep
- weight
- steps
- VO₂ max
- activities
The interesting part isn't simply storing the data.
The goal is to make the data useful to the AI.
Garmin data can become part of the model's context or be exposed through tools, allowing the coach to reason about the athlete's actual training history rather than a hypothetical one.
This is where an LLM application starts becoming significantly more useful than a generic chat interface.
Keeping the model replaceable
I also didn't want the application to become tightly coupled to one LLM provider.
Andre-AI currently supports Gemini, OpenAI, and Ollama.
The provider is selected through configuration, while the rest of the application works through an abstraction:
┌─────────────┐
│ AI Coach │
└──────┬──────┘
│
┌──────▼──────┐
│ LLM Adapter │
└──────┬──────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Gemini OpenAI Ollama
The persona system, context handling, tools, and session management don't need to know which model is generating the response.
This makes it much easier to experiment with different models without changing the rest of the application.
It also makes local models possible through Ollama.
The application architecture
The application consists of two main parts.
The frontend is a React/Vite application. The backend is a Node.js/TypeScript API built with Fastify.
┌──────────────────────┐
│ ai-coach-web │
│ React + Vite │
└──────────┬───────────┘
│
HTTPS / SSE
│
▼
┌──────────────────────┐
│ ai-coach-server │
│ Node.js / TypeScript│
│ Fastify │
└──────────┬───────────┘
│
┌─────┼───────────────┐
│ │ │
▼ ▼ ▼
Postgres LLM Garmin
Provider
The frontend stays relatively thin.
The server owns the important application logic: conversations, personas, context, tools, LLM integration, Garmin data, and plans.
A typical AI request looks roughly like this:
User message
↓
Load conversation
↓
Load persona
↓
Resolve context
↓
Expose available tools
↓
Call LLM
↓
Execute tools if required
↓
Continue LLM interaction
↓
Stream response
↓
Persist conversation
Responses are streamed back to the frontend using Server-Sent Events, so the user sees the response as it is generated.
Keeping the AI layer separate
Inside the server, I deliberately separated the AI-related responsibilities.
interfaces/server
│
├── core
│
├── domain
│ ├── personas
│ ├── context
│ └── tools
│
└── integrations
├── llm
├── garmin
└── mail
The context layer is responsible for turning application data into useful model context.
The tools layer exposes capabilities to the model.
The LLM integration handles communication with the selected provider.
The session layer manages conversations and their history.
This separation makes it possible to add another data source or another AI capability without rewriting the whole application.
What I learned building it
The biggest lesson is that building an AI application isn't really about calling an LLM API.
That is probably the easiest part.
The interesting engineering questions are:
- What does the model need to know?
- When should it receive that information?
- Which information belongs in context?
- Which capabilities should be exposed as tools?
- How much conversation history should be retained?
- How do you keep the model provider replaceable?
- How do you keep the system maintainable as the amount of context grows?
These are software architecture problems.
The LLM is simply a new type of dependency with some unusual characteristics.
It is probabilistic, context-dependent, expensive to call, and capable of using tools. Designing around those characteristics is what makes LLM application development interesting.
The pattern I'd reuse
The architecture I ended up with can be applied to much more than fitness.
The general pattern is:
┌──────────────┐
│ User │
└──────┬───────┘
↓
┌──────────────┐
│ Application │
└──────┬───────┘
↓
┌─────────────────────┐
│ Context + Tools │
└──────────┬──────────┘
↓
┌──────────────┐
│ LLM │
└──────────────┘
The model is replaceable.
The application owns the domain logic.
Context gives the model knowledge.
Tools give it capabilities.
Together, they turn a general-purpose LLM into something that can actually perform a useful job inside a product.
That is the part of Andre-AI I find most interesting.
The project started with a very practical question during my triathlon training: can AI actually act like a useful coach?
The next question was much more interesting from an engineering perspective:
What software architecture do you need to make that experience reliable, contextual, and extensible?
Andre-AI is my attempt at answering that question.
The beta version is at this link