This article is published in English.
Specialist Agents, a Keyword Router and interrupt(): A LangGraph Coach
Design a two-specialist LangGraph assistant with a deterministic router, shared state that survives handoffs, and a human checkpoint built on interrupt and Command.
A single assistant prompt that must cover two unrelated domains usually covers both badly. This article builds a fitness and nutrition coach as a small LangGraph system instead: two specialist agents, a deterministic router that decides who answers, one shared state that carries constraints across handoffs, and a human checkpoint that pauses the graph after every reply. By the end you will know how each piece works, why the graph is shaped the way it is, and how to reuse the same shape for any pair of specialists.
The scenario and the test conversation
The target product is an assistant inside a fitness app that helps with both training and diet. The design is validated against a three-turn conversation: a request for a workout plan, a follow-up about food, and a dietary constraint that must change the meal advice:
Scenario: A fitness app wants an assistant that helps with workouts and nutrition.
Test conversation flow (3 turns):
"Build muscle, lose fat — suggest a workout plan"
"What should I eat to support this workout plan?"
"I'm vegetarian, adjust the meal suggestion"
Every design decision below exists to make these three turns work correctly and predictably.
Why one generic chatbot falls short
Ask a single general-purpose chatbot for a workout plan and then for a matching diet, and the answers tend to be shallow. The model is juggling every persona inside one system prompt, so training advice loses specificity and dietary nuance gets flattened. That weakness comes from three distinct requirements hitting the same prompt at once:
- Separating domains. Workout programming and nutrition planning are different areas of expertise, and advice in one should not leak into or contradict the other.
- Retaining context across turns. A constraint raised earlier, say a knee injury mentioned three messages ago, must still shape a meal or training plan produced later.
- Keeping a human in control. The system should not run fully autonomously; it needs a deliberate point where a person can sign off, add detail or change direction before anything else is generated.
Rather than asking one prompt to do three jobs, the architecture gives each requirement its own mechanism: specialists for domain separation, shared state for context, and a human node for oversight.
The five building blocks
The system consists of five parts, each with a single responsibility:
- Workout Advisor: an agent responsible for exercise routines, training splits and modifications for injuries.
- Nutrition Advisor: an agent responsible for meal plans, macronutrients and dietary restrictions such as vegetarian, vegan or allergy-driven diets.
- Router: a lightweight decision step that inspects the latest user message and picks which advisor answers next.
- Human node: an intentional pause where the graph waits for the person instead of generating more output.
- Shared state: one authoritative record of the dialogue so far, plus which advisor replied last.
This is a hierarchical, or orchestration, pattern. A coordinator delegates to specialized sub-agents instead of one flat agent trying to hold every role simultaneously.
The rule that keeps handoffs predictable
One structural rule makes the whole design trustworthy: an advisor never hands control directly to the other advisor. Every agent output goes through the human node and then the router. Two agents can therefore never pass control back and forth silently, and every transition is both visible and interruptible.
How a message moves through the graph
Follow a single user message. It lands on the human node first and is passed on to the router, which reads the message text and, if the text alone is inconclusive, consults the last_active_agent field in the shared state to choose between the Workout Advisor and the Nutrition Advisor. The chosen advisor may call its domain tool, then returns control to the human node, which pauses again and waits for the next message. The loop is therefore always human node, router, advisor, optional tool, human node.
The model: an open-weight LLM on Groq
Both advisors use the open-weight openai/gpt-oss-120b model served by Groq, configured with temperature=0:
- Why this model: it has solid native tool calling, which the tool-driven workflow depends on.
- Why temperature 0: it makes outputs as repeatable as possible, so the graph follows its intended rules instead of varying from run to run. That matters when someone tests or grades the same conversation repeatedly. In practice, even temperature 0 is not a strict determinism guarantee with hosted models, so tests should assert on routing and structure rather than exact wording.
- Why Groq: its LPU inference hardware keeps latency low, which counts when one user turn triggers several hops (human node, router, advisor, tool, human node) before a reply appears.
Nothing in the graph depends on Groq specifically. Any chat model with tool calling works, for example Google Gemini or Anthropic Claude, by providing the corresponding API key (such as GEMINI_API_KEY or ANTHROPIC_API_KEY) and pointing the model wrapper at that provider. Model availability on hosted platforms changes over time, so confirm the model identifier against the provider's current catalog.
Shared state: where the memory actually lives
Every node reads from and writes to one typed state object, MultiAgentState, with two fields:
messages: the complete running conversation, inherited from LangGraph'sMessagesState, which also supplies the reducer that appends new messages instead of overwriting the list.last_active_agent: set to"workout_advisor"or"nutrition_advisor"; the router falls back on it whenever it cannot tell which domain a follow-up such as "tell me more" belongs to.
This state is why a constraint stated in one turn still applies in a later turn, even when a different node generates the reply. Nothing is summarized or re-explained to the model. Each advisor simply receives the same accumulated messages list whenever it runs, and a checkpointer persists the state between turns of the same conversation thread.
The key design choice is that memory is centralized, not scoped per agent. If each advisor kept its own private history, the Nutrition Advisor would never learn about a goal the user stated to the Workout Advisor.
One narrow tool per advisor
Each agent may call exactly one tool, and that tool belongs to its domain. In this build the tools are rule-based and match keywords, which keeps behavior predictable and easy to demonstrate. In production you might replace them with a real fitness or nutrition API, or let the model generate the answer fully. Because the rest of the graph only depends on the tool's interface, swapping the implementation requires no other changes.
Keeping each advisor in its lane
A dedicated tool is not enough to keep an agent on topic; the model also has to know what it must not answer. Each system prompt therefore sets an explicit boundary:
- The Workout Advisor is instructed not to give meal or nutrition advice, because the graph will send those requests to the Nutrition Advisor.
- The Nutrition Advisor is instructed not to design workout routines, because that belongs to the Workout Advisor.
The principle behind this is that the model does not route itself. Boundary prompting makes each agent defer instead of improvising outside its expertise, and the graph, not the LLM, owns every routing decision explicitly.
A router built on keywords, not another LLM call
With the advisors constrained, the router only has to pick the next speaker, and it does so with plain keyword matching:
- Words such as gym, reps, cardio or muscle route to the Workout Advisor.
- Words such as diet, food, eat, protein or vegetarian route to the Nutrition Advisor.
- Anything that matches neither list goes to whichever advisor is stored in
last_active_agent.
Deterministic routing is predictable and trivially testable: the same message always produces the same hop, and you can unit-test the router without calling a model. The trade-off is brittleness. Keyword lists miss synonyms, and a message that mentions both domains (a question about eating before the gym) needs an explicit tie-breaking rule. When phrasing becomes too varied for keywords, a small classification call is a reasonable upgrade, but it gives up some of that predictability.
Human-in-the-loop with interrupt and Command
The most important design decision is not the router but the human node. After every advisor turn the graph stops on purpose. It does not guess the user's next question or keep generating. Two LangGraph primitives implement the pause:
interrupt(...)stops the run right where it is called and hands control back to the caller, along with whatever value you pass to it.Command(resume=...)continues the paused graph, delivering the human's input as the return value of theinterruptcall inside the same node.
Interrupts rely on checkpointing: the graph has to save its state at the pause so it can resume later, which is why the compiled graph needs a checkpointer and a thread ID.
For a fitness and nutrition assistant this pause is more than a UI convenience. It is the moment when the user can add a safety-relevant constraint, such as an injury, an allergy or a dietary restriction, before the conversation proceeds, rather than after the system has already produced advice that ignores it.
Walking through the three turns
Turn 1: starting the thread
The user states two goals, gaining muscle and losing some fat, and asks for a workout plan. A new thread always starts at the Workout Advisor. It picks up "muscle" and "fat", calls its tool, and returns a structured routine, for example a four-day upper/lower split at 8 to 12 reps for 3 to 4 sets, combining three strength days with two cardio sessions.
Turn 2: handing off to nutrition
Next, the user wants to know which foods would support that plan. The human node forwards the message, the router matches "eat", and execution passes to the Nutrition Advisor. It returns a daily plan of roughly 2,500 kcal, focused on lean protein and complex carbohydrates, with meals every three to four hours.
Turn 3: applying a constraint from shared state
Finally, the user mentions being vegetarian and asks for the meal suggestion to be adjusted, with a snack added. The message goes to the Nutrition Advisor again, and two mechanisms agree here: "vegetarian" is itself a nutrition keyword, and last_active_agent still points to nutrition, so even a vaguer follow-up such as "adjust that, plus a snack" would land in the same place. The advisor rereads the complete history, still honors the goal set in the first turn, layers the vegetarian restriction on top, and produces a meat-free plan for gaining muscle built around tofu, paneer and lentils, with a snack such as edamame and hummus cups. Nobody had to restate the goal, because the state already contained it.
This turn is the payoff of centralized state. A different node than the one that heard the original goal is producing the answer, yet it has full context.
Assembling the state machine
Structurally, the result is a small state graph with three nodes (the two advisors and the human node) plus the router as the conditional logic between them, and one fixed entry point:
- Every new thread begins at the Workout Advisor. That default mirrors the product: people who open a fitness app usually ask about training before they ask about food.
- After that first reply, the human node and the router decide every subsequent hop.
- The graph is compiled with a
MemorySavercheckpointer and each conversation is identified by athread_id. Whenever the caller reuses the samethread_id, the graph restores the messages andlast_active_agentautomatically; the caller never passes history back in.
MemorySaver holds checkpoints in process memory, which is ideal for a notebook but loses everything on restart. A deployed version should use a persistent checkpointer backed by a database.
The full working implementation is available as a companion notebook. For other ways to structure routing and approval steps, see five LangGraph patterns covering routing, fan-out, critique and approval.
Reusing the pattern beyond fitness
Nothing here is specific to workouts or meals. The same three ingredients, specialist agents, a router driven by shared state, and a human checkpoint using interrupt and resume, fit any system that has:
- more than one distinct area of expertise a conversation may need,
- a need to stop partway through for a person to weigh in or sign off, whether for compliance, safety or personalization,
- context established in one part of the conversation that must correctly influence a different specialist later.
Replace the two advisors with another pair of domain experts and the graph shape stays identical. Only the tools, prompts and router keywords change.
Design principles
- Orchestration over monoliths. Use the graph to enforce separate lanes of expertise instead of relying on one agent to do everything.
- Centralize the state. One
MultiAgentStatepersisted by a checkpointer is what makes handoffs seamless and constraints persistent. - Control is crucial. Avoid fully autonomous loops in production; use
interrupt()to keep a human involved at the points that matter.
Key takeaways
- Divide the work by domain and give the routing decision to deterministic graph logic instead of the model.
- Keep one shared
messageshistory pluslast_active_agentso a constraint raised early shapes answers produced much later by a different agent. - Treat
interrupt()andCommand(resume=...)as the mechanism that lets a person add safety constraints ahead of the next reply, and remember they need a checkpointer and a thread ID. - Keep tools narrow behind a stable interface so rule-based logic can later become a real API or full model reasoning without touching the graph.
- The quickest way to internalize the pattern is to pick two well-defined specialists of your own and wire up a complete working graph; the topology carries over unchanged, while prompts and tools are the parts you tailor.