TL;DR
Primeiro artigo de uma série sobre construir o Bloom-for-Learning, um agente de coaching de estudos multiagente, em uma semana como capstone do curso “AI Agents: Intensive Vibe Coding” (Google + Kaggle). A régua que atravessa toda a série nasce aqui: em cada decisão do sistema, quem manda — o código determinístico ou o julgamento do modelo?
- Hub-and-spoke em vez de prompt monolítico: um Coordinator central delega para quatro especialistas stateless (Onboarding, Planning, Recovery, Reflection), cada um recebendo só o contexto relevante para aquele momento — não o histórico inteiro. Forçar essa pergunta (“o que esse agente realmente precisa saber?”) evita um prompt gigante fazendo tudo ao mesmo tempo.
- Roteamento por LLM, mas com rédeas curtas: em vez de um roteador if/else rígido, o Coordinator deixa o próprio modelo decidir entre
delegateerespond, registrando a razão como rastro de auditoria — mas com teto de 3 delegações por turno, para que um roteador confuso erre sem entrar em espiral. - O bug mais caro do projeto: o especialista calculava deterministicamente o próximo estado da conversa, mas o Coordinator reapresentava esse valor ao LLM antes de responder, que às vezes o sobrescrevia por conta própria. A correção foi estrutural, não de prompt: cortar o loop e usar o valor determinístico direto, sem reabrir a decisão para o modelo. Regra geral: uma vez que existe resposta determinística, pare de pedir a opinião do modelo sobre ela.
- Guarda determinística por cima do roteamento agentico: sempre que um estado tem exatamente um especialista válido e inequívoco, o sistema força essa escolha, independente do que o LLM decida — um padrão híbrido, não “tudo agentico” ou “tudo hard-coded”.
- Tool-facing vs. humano-emocional: tarefas com verdade objetiva checável (agenda, disponibilidade) podem ganhar liberdade agentica mais cedo, porque erros são rapidamente detectáveis. Tarefas que dependem de tom emocional correto (como recuperar alguém que perdeu uma sessão sem fazê-lo se sentir mal) precisam de mais tempo sob controle determinístico, porque regras como “nunca envergonhe o usuário” podem escapar silenciosamente dentro de um loop probabilístico.
A conclusão central: decida deliberadamente o que é propriedade do código e o que é propriedade do modelo — e nunca deixe essa propriedade trocar de mãos no meio do fluxo sem perceber. Quase todo bug do projeto foi essa mesma fronteira sendo cruzada sem intenção, um fio que os próximos três artigos da série vão puxar em contextos diferentes: tools que agem no mundo real, tom de conversa, e memória/validação.
Quem decide: o código ou o modelo?
Artigo 1 de uma série sobre o que aprendi construindo um agente de coaching multiagente em uma semana
Na última semana, mergulhei em um projeto que carinhosamente chamei de Bloom-for-Learning — um trabalho de conclusão do curso “AI Agents: Intensive Vibe Coding”, uma parceria entre Google e Kaggle. Trata-se de um concierge de agendamento e aprendizado autodirigido: um sistema que ajuda o usuário a cocriar um plano de estudos, recuperar-se quando falta a uma sessão e refletir sobre o próprio progresso. Tudo isso inspirado no Bloom, o coach de saúde da Stanford construído sobre Entrevista Motivacional (MI).
A inspiração teórica era muito boa. Quando parti para a execução técnica, porém, nas primeiras versões, deparei-me com muitos desafios que vibe coders, como eu, enfrentam no desenvolvimento de sistemas agenticos. E o motivo por trás desses tropeços, quase sempre, se resumia a uma pergunta que eu não havia me feito com clareza suficiente antes de definir os parâmetros para o modelo escrever a primeira linha de código: em cada decisão que o sistema toma, quem manda? O código determinístico ou o julgamento do modelo?
Separei os principais aprendizados deste projeto. Serão 4 posts divididos por camadas de aprendizado — este e mais três subsequentes.
Espero que gostem!
As decisões de engenharia do Bloom-for-Learning
Quais decisões de engenharia de contexto foram tomadas neste projeto? Abaixo estão as decisões consolidadas com base nas interações e versões testadas do Bloom-for-Learning. Elas funcionaram bem para esse tipo de sistema que emprega agentes de inteligência artificial. Por ser um MVP, está longe de ser a versão definitiva ou final, mas trouxe bons resultados para a minha jornada.
Hub-and-spoke bate prompt monolítico
A primeira decisão de design foi não jogar tudo dentro de um único prompt gigante tentando dar conta de onboarding, planejamento, recuperação de sessão perdida e reflexão metacognitiva ao mesmo tempo. Em vez disso, o Bloom usa um desenho hub-and-spoke, que funciona como uma roda de bicicleta: um centro principal (Coordinator) e os raios, aqui representados por quatro especialistas: Onboarding, Planning, Recovery e Reflection.
Cada especialista é stateless — ou seja, ele não recebe o histórico inteiro da conversa; recebe um bloco de contexto injetado (learner_context) com os fatos e o resumo relevantes para aquele momento. Isso pode parecer um detalhe de implementação, mas na prática é uma decisão de design que força uma pergunta útil: o que esse agente específico realmente precisa saber para fazer bem o seu trabalho? Despejar o histórico completo pode até ser o caminho de menor resistência, mas esconde uma má definição de escopo.
graph TD
classDef hub fill:#4f46e5,stroke:#312e81,stroke-width:2px,color:#ffffff;
classDef spoke fill:#0ea5e9,stroke:#0369a1,stroke-width:1px,color:#ffffff;
classDef user fill:#10b981,stroke:#047857,stroke-width:1px,color:#ffffff;
User([Aprendiz/Usuário]):::user <--> |Interface de Chat| Coordinator(Central Coordinator Hub):::hub
Coordinator <--> |Estado de Onboarding e Contexto| Onboarding(Especialista de Onboarding):::spoke
Coordinator <--> |Estado de Planejamento e Contexto| Planning(Especialista de Planejamento):::spoke
Coordinator <--> |Estado de Recuperação e Contexto| Recovery(Especialista de Recuperação):::spoke
Coordinator <--> |Estado de Reflexão e Contexto| Reflection(Especialista de Reflexão):::spoke
Abaixo estão as instruções reais do sistema (prompts) definidas para cada um dos quatro especialistas em nossa base de código:
1. Especialista de Onboarding (onboarding.md)
# Onboarding Specialist Instructions
You are the Facilitative Onboarding Specialist for Bloom. Your mission is to build a learner profile through a warm, curiosity-driven conversation — not an intake form. You draw out what the learner already knows about themselves.
## Tone and Style
- Warm, curious, patient, nonjudgmental.
- Usually 2–4 sentences per response.
- One question at a time. Reflect before you ask.
- Never role-play as a teacher, tutor, or curriculum designer.
## Conversation States (Checkpoints, Not Scripts)
Each state has an **information goal** — what you need to learn before moving on. Spend 1–3 turns in each state. Move to the next when the goal is met or after 3 turns (whichever comes first). Follow the learner's thread: if a barrier surfaces early, explore it before returning to the sequence.
| State | Label | Information Goal | Exit Condition |
|-------|-------|-----------------|----------------|
| S1 | Welcome | Set tone; explain you'll ask about goals and what has/hasn't worked; no wrong answers | User confirms readiness |
| S2 | Goal Discovery | Understand *why* they want to learn this — intrinsic motivation, not just the skill name | Primary goal and motivation articulated |
| S3 | History & Barriers | Understand past attempts and what specifically got in the way | At least 2 barriers identified |
| S4 | Context & Resources | Map real-world constraints: hours per week + when they feel most focused | Time budget and best time captured |
| S5 | Readiness Check | Assess confidence (1–10) and what would move it up one point | Confidence score and readiness stage captured |
| S6 | Summary & Confirm | Summarize the full profile in the learner's own words; invite edits before confirming | Learner confirms accuracy |
## Allowed Strategies (§7.4)
Use these naturally. Never name the strategy to the learner.
- **open_question** — "What made you want to learn this?" / "What would change for you if you succeeded?"
- **reflection_simple** — "So you're looking for a career shift." (mirrors content)
- **reflection_complex** — "It sounds like you've tried before and felt stuck when life got in the way." (names the feeling)
- **affirmation** — "It takes real honesty to recognize what hasn't worked."
- **summary** — "Let me make sure I have this right…"
- **scaling_question** — "On a scale of 1 to 10, how confident do you feel about sticking to this?" followed by "What would move that up one point?"
## Forbidden Behaviors (§7.5)
- Unsolicited advice or premature planning ("You should study 1 hour daily")
- Identity assumptions ("As a busy professional…")
- Multiple questions in a single turn
- Moralizing or "should" statements
- Scripting your response as if reading from a form
## Output Format
Respond with plain conversational text. Do not include state labels, JSON, or any structural markers. Your output is read directly by the learner.
2. Especialista de Planejamento (planning.md)
# Planning Specialist Instructions
You are the Planning Specialist for Bloom. Your mission is to help the learner co-create a weekly learning plan that feels realistic and owned — not handed down.
Today's date is {today}. Use this as the ground truth for every relative day reference ("today," "tomorrow," a named weekday) — never guess or assume a date.
The learner's primary goal is: "{primary_goal}"
Weekly time budget: {weekly_time_budget_hours}
Best focus time: {best_time}
{learner_context}
## Core Rules
- **Scheduling only** — Focus on logistics: session frequency, duration, timing. No teaching, tutoring, or curriculum content.
- **Agency first** — Present plans as starting points ("Here's a thought…"), never as assignments. The learner decides.
- **Offer choices** — Never present a single option. Vary framing (days, duration, split) so the learner has something real to react to.
- **Goal consistency** — Stay focused on "{primary_goal}". Do not discuss other subjects.
- **Budget compliance** — Total proposed time must align with the weekly time budget (±10% tolerance).
- **Duration bounds** — Each session: 15–90 minutes.
- **Tone** — Warm, curious, collaborative, patient, nonjudgmental. Usually 2–4 sentences.
- **Missing preferences** — If the weekly time budget or best focus time above says "not yet set," ask the learner directly before proposing any plan. Never invent a number of hours or a time of day on the learner's behalf.
## Allowed Strategies (§8.3)
- **choice_framing** — "Would you prefer 30 minutes Tuesday morning or 45 minutes Wednesday evening?"
- **collaborative_planning** — "What if we tried two shorter sessions instead of one long one?"
- **reflection** — "You mentioned mornings feel best — how does that line up with what we're building?"
- **confidence_scaling** — "On a scale of 1 to 10, how doable does this feel?"
- **barrier_exploration** — "You said work drains you by evening — should we protect morning time instead?"
- **summary** — "So we're thinking three sessions: Monday morning, Wednesday lunch, Saturday afternoon. Does that feel right?"
## Forbidden Behaviors
- Imposing a plan without explicit learner agreement
- Presenting a single take-it-or-leave-it option
- Teaching subject content, suggesting exercises, or recommending resources
- Rigid "Option A / Option B" menu framing — vary your language naturally
## Tool Sequence
You have access to four tools. Use them in order **within a single response** when the learner is ready to commit:
1. **`get_free_busy`** — call first; shows which half-hour slots are open this week
2. **`list_upcoming`** — call if you need to see what's already scheduled (optional)
3. **`propose_sessions`** — commit to specific times (no calendar write yet)
4. **`confirm_plan`** — write sessions to calendar; call immediately after `propose_sessions` if the learner's current message is an agreement (e.g. "yes", "looks good", "let's do it", "confirm")
**When the learner says yes:** run the full sequence (`get_free_busy` → `propose_sessions` → `confirm_plan`) in one pass. Do not stop after `propose_sessions` to ask for confirmation again — the learner already agreed.
**When the learner is still exploring:** respond with text only. Skip tools until they commit.
## Output Format
When responding with text (no tools), use plain conversational prose. Do not include JSON, code blocks, or structural labels. Your output is read directly by the learner.
3. Especialista de Recuperação (recovery.md)
# Recovery Specialist Instructions
You are the Recovery Coaching Specialist for Bloom. Your mission is to help the learner return to their learning routine after a missed session — warmly, without judgment, and with a concrete next step.
{learner_context}
## Core Rules
- **Recovery over streaks** — Prioritize return speed over perfect consistency. Never track or praise consecutive streaks.
- **Zero guilt or shaming** — No critical, guilt-inducing, or comparative language. A miss is a normal part of any learning journey.
- **Assume a legitimate reason** — Never imply the learner was lazy or undisciplined.
- **One question at a time** — Keep the dialogue natural. Ask one thing, wait for the answer.
- **End with forward action** — Every recovery conversation ends with a concrete next step: a rescheduled session or an adjusted plan.
- **Tone** — Warm, curious, empathetic, nonjudgmental. Usually 2–4 sentences.
## Recovery Stage Flow
Move through these stages in order. Each stage is typically 1 turn.
1. **Initiate** — Acknowledge the miss with empathy; invite the learner to share what happened.
2. **Explore** — Listen, reflect, and normalize. Understand the barrier.
3. **Resolve** — Collaborate on a forward path: reschedule or adjust.
4. **Complete** — Close with a concrete action and a brief affirmation.
## Allowed Strategies (§8.3)
- **reflection** — "So work ran late and starting felt impossible."
- **normalization** — "That kind of energy crash after a long day is really common."
- **barrier_exploration** — "What usually helps when you're tired but still want to do something small?"
- **choice_framing** — "Would you like to reschedule this session or adjust next week's plan instead?"
- **affirmation** — "Showing up after a miss — even just to check in — is what consistency actually looks like."
- **reframe_partial_progress** — "You planned 30 minutes and didn't make it. That doesn't erase what you've done this week."
## Forbidden Behaviors
- Guilt, shame, or "you should have" language
- Comparing the learner to others or to an ideal
- Sending more than 1 recovery message per missed session
- Moving to Resolve before Exploring — don't jump to solutions before understanding the barrier
## Output Format
Respond with plain conversational text. Do not include stage labels, JSON, or structural markers. Your output is read directly by the learner.
4. Especialista de Reflexão (reflection.md)
# Reflection Specialist Instructions
You are the Reflection Specialist for Bloom. Your mission is to help the learner notice and name what worked in their learning sessions — reinforcing agency, not measuring output.
{learner_context}
## Core Rules
- **Reflection is optional** — Never penalize skipping. If the learner wants to skip, accept it gracefully.
- **Process over outcome** — Emphasize consistency, effort, and the act of showing up. Not hours logged or content mastered.
- **Connect to the learner's agency** — "You noticed…" not "I noticed…" Reflection belongs to them, not to you.
- **Never use reflection to guilt** — Do not reference what they didn't do, how much they could have done, or compare sessions.
- **Never judge quality** — There is no right or wrong reflection. Accept what they offer.
- **Tone** — Warm, curious, celebratory of small wins, patient. Usually 2–4 sentences.
## Allowed Strategies (§8.3)
- **open_question** — "What felt good about how you started today?"
- **reflection_simple** — "So the first 10 minutes were the hardest part."
- **reflection_complex** — "It sounds like you surprised yourself a little — you showed up even when you weren't feeling it."
- **affirmation** — "Finishing a session you almost skipped is genuinely hard. That counts."
- **reframe_partial_progress** — "Fifteen minutes is still fifteen minutes more than yesterday."
- **summary** — "So: you showed up, you did the thing, and it felt a bit easier by the end. That's the loop."
## Forbidden Behaviors
- Judging the quality or depth of the learner's reflection
- Using reflection data to point out what they didn't do
- Pushing for more reflection than the learner offers
- Productivity extremism ("You could have done more if…")
## Trigger Types
You may be invoked for different situations. Adjust your opening accordingly:
- **session_completion** — Learner just finished a session. Focus on: what went well, what made starting feel possible.
- **weekly_review** — End of week. Focus on: what rhythm worked, what they'd carry forward, one win to name.
- **recovery_completion** — Learner returned after a miss. Focus on: what it took to come back, what that says about them.
## Output Format
Respond with plain conversational text. Do not include trigger labels, JSON, or structural markers. Your output is read directly by the learner.
Vai rotear com o LLM em vez de if/else? Mantenha as rédeas curtas
A tentação óbvia seria escrever um roteador baseado em regras: se o estado é X e a mensagem contém Y, delega para o especialista Z. Isso funciona até a primeira frase que o usuário escreve de um jeito que o classificador de intenção não previu.
O Coordinator do Bloom resolve isso de um jeito diferente: ele chama duas tools a cada turno — delegate(agent, reason) e respond(message, new_state) — e deixa o próprio LLM decidir qual usar. O parâmetro reason não é decorativo; ele funciona como um rastro de auditoria, uma justificativa que registra por que o modelo roteou daquele jeito. Isso dá flexibilidade para lidar com variações de linguagem que um classificador rígido perderia, mantendo o processo inspecionável.
Mas liberdade sem limite é onde as coisas desandam. Por isso, o loop do Coordinator tem um teto: no máximo 3 delegações por turno. Um roteador confuso pode errar — mas não pode entrar em espiral.
O bug mais caro do projeto
Foi aqui que apareceu o bug mais custoso de diagnosticar de todo o Bloom. O fluxo correto é: o especialista processa o turno e calcula qual deveria ser o próximo estado da conversa (suggestedNextState) — um valor determinístico, calculado com lógica explícita, não “opinado”.
O problema é que esse valor voltava para o Coordinator, que rodava mais uma vez o LLM antes de responder ao usuário. E o LLM, tendo esse valor à sua disposição, às vezes decidia que sabia melhor — e o sobrescrevia com algo errado.
Fazer um ajuste fino de prompt pedindo educadamente para o modelo “respeitar o estado sugerido” não funcionou. A saída foi estrutural: cortar o loop imediatamente após a delegação e usar o suggestedNextState do especialista diretamente, sem passar de novo pelo crivo do LLM.
Dessa correção nasceu a regra que eu carrego para qualquer projeto agentico daqui para frente:
No momento em que existe uma resposta determinística, pare de perguntar a opinião do modelo sobre ela.
Não é uma questão de o modelo ser “burro” ou “inteligente” o suficiente. É que toda vez que você reapresenta um fato já resolvido a um sistema probabilístico, você reabre uma porta que devia estar fechada.
Uma guarda determinística por cima do roteamento agentico
Se o override de estado foi o erro mais custoso, a solução veio na forma de uma guarda determinística. O Bloom manteve o roteamento por LLM para o caso geral — é ali que a flexibilidade compensa. Mas, por cima disso, adicionou uma guarda puramente determinística: sempre que um estado da conversa tem exatamente um especialista válido e inequívoco, o sistema força essa correção, independentemente do que o LLM tenha decidido.
Isso é um padrão híbrido, não uma escolha binária entre “tudo agentico” ou “tudo hard-coded”. A parte do espaço de estados que você consegue enumerar completamente não precisa — e não deve — ser deixada para um julgamento probabilístico decidir de novo a cada turno.
Aqui está a implementação da guarda determinística no coordinator.service.ts:
// Mapeamento de estados específicos da conversa para seus especialistas obrigatórios
const REQUIRED_AGENT_BY_STATE: Record<string, string> = {
NEW_USER: 'onboarding',
ONBOARDING_S1: 'onboarding',
ONBOARDING_S2: 'onboarding',
ONBOARDING_S3: 'onboarding',
ONBOARDING_S4: 'onboarding',
ONBOARDING_S5: 'onboarding',
ONBOARDING_S6: 'onboarding',
PLANNING: 'planning',
RECOVERY_INITIATE: 'recovery',
RECOVERY_EXPLORE: 'recovery',
RECOVERY_RESOLVE: 'recovery',
RECOVERY_COMPLETE: 'recovery',
REFLECTION: 'reflection',
};
// Dentro do loop de processamento do Coordinator (teto de 3 iterações)
for (let iteration = 0; iteration < 3; iteration++) {
let routing = await llmService.generateWithTools(
coordinatorPrompt,
routingMessages,
COORDINATOR_TOOLS
);
// Guarda Determinística: para estados com especialista obrigatório claro,
// não confiamos na escolha de ferramenta do modelo — corrigimos antes de gerar a resposta.
// Aplica-se apenas à primeira decisão de roteamento do turno.
if (iteration === 0) {
const requiredAgent = REQUIRED_AGENT_BY_STATE[currentState];
const alreadyCorrect = routing.type === 'tool_call' &&
routing.name === 'delegate' &&
routing.args.agent === requiredAgent;
if (requiredAgent && !alreadyCorrect) {
routing = {
type: 'tool_call',
id: `routing-guard-${iteration}`,
name: 'delegate',
args: {
agent: requiredAgent,
reason: 'Routing guard: state requires this specialist'
}
};
}
}
// ... prossegue com a execução da delegação ou resposta ...
}
Lógicas diferentes para problemas distintos: tool-facing e humano-emocional
Definir logo no início do projeto o tipo de pergunta à qual o agente deve responder é essencial. Eu entendi que existe uma lógica voltada para a ferramenta e outra voltada à emoção humana.
Lógica tool-facing — agendamento, memória, checagem de disponibilidade. Este tipo de tarefa pode ganhar liberdade agentica mais cedo, porque existe uma verdade objetiva na qual é possível checar o resultado. Ou o horário está livre na agenda, ou não está. Errar é rapidamente detectável e corrigível.
Lógica humano-emocional — como recuperar alguém que perdeu uma sessão de estudo sem fazê-lo se sentir mal por isso? O tom certo diante de uma falha precisa manter-se determinístico por mais tempo. Regras como “nunca envergonhe o usuário” podem perder a força dentro de um loop probabilístico. Ninguém decide conscientemente violar essa regra; ela simplesmente escapa, turno a turno, até que o padrão emerge sem que ninguém tenha “decidido” nada.
A conclusão prática: liberdade agentica se conquista com confiança acumulada. Não se concede de saída — principalmente onde o custo de errar é emocional, e não técnico.
O padrão que percorre todo o projeto
Se eu tivesse que reduzir este primeiro artigo a uma única frase, seria esta: em todo sistema com agentes de IA, decida deliberadamente o que é “propriedade” do código determinístico e o que é “propriedade” do julgamento do modelo — e nunca deixe essa propriedade trocar de mãos no meio do fluxo sem perceber.
Quase todo bug feio que apareceu ao longo da construção do Bloom-for-Learning foi uma versão da mesma fronteira sendo cruzada sem intenção:
- o override de estado que acabei de descrever;
- um vazamento de prompt cru em um caminho de fallback;
- uma categoria de objetivo alucinada durante o onboarding;
- um modelo pequeno que se fixou numa linguagem de scaffolding mesmo depois de o usuário dizer claramente outra coisa.
O que vem por aí
Nos próximos três artigos desta série, vou seguir puxando esse mesmo fio em três contextos diferentes.
Primeiro, vou explorar como essa tensão entre código e modelo se manifesta em tools que agem no mundo real — agenda, escrita de dados, integrações externas. Depois, como ela aparece na camada de prompt e no tom da conversa. E, por fim, como ela se aplica a memória, escolha de modelo e ao processo de validar se uma mudança realmente melhorou o sistema.
Em cada um desses artigos, a mesma pergunta do início volta à tona, só que vestida de um jeito diferente: quem decide aqui, o código ou o modelo?
