How to Build a Multi-Agent Pipeline That Doesn't Lose the Plot
The biggest problem with using LLMs for long-form content generation isn't the quality of the prose—it's the loss of coherence. You start with a brilliant premise, but by chapter five, your protagonist has forgotten their motivation, and the magic system has completely broken.
When you treat an LLM as a single, monolithic writer, you are asking it to perform three cognitively heavy tasks simultaneously: structural planning, character consistency, and atmospheric prose generation. Even with a massive context window, the "attention" drifts.
To solve this, I implemented a hierarchical, three-layer agentic architecture in NovelGenerator. Instead of one prompt, we use a pipeline of specialized agents where each layer's output becomes the "source of truth" for the next.
Here is how you can build a multi-agent pipeline that maintains narrative integrity.
The Architecture: The Hierarchy of Intent
The core principle is delegation. We move from high-level abstraction (the "what") to low-level implementation (the "how").
- Structure Agent (The Architect): Defines the skeleton.
- Character Agent (The Soul): Populates the skeleton with identity and memory.
- Scene Agent (The Painter): Renders the final, atmospheric text.
Step 1: Establishing the Narrative Skeleton (Structure Agent)
The first agent's job is purely structural. It doesn't care about adjectives or dialogue. Its only goal is to ensure the plot follows a logical progression (e.g., Hero's Journey or Three-Act Structure).
The output of this agent must be highly structured—preferably JSON—so that subsequent agents can parse it without ambiguity.
// The blueprint that the next agents will consume
interface PlotOutline {
title: string;
acts: {
actNumber: number;
summary: string;
chapters: Chapter[];
}[];
}
interface Chapter {
chapterNumber: number;
setting: string;
keyEvents: string[];
requiredCharacters: string[];
}
class StructureAgent {
async generateOutline(premise: string): Promise<PlotOutline> {
const prompt = `Analyze this premise: "${premise}".
Create a structured 3-act plot outline in JSON format.
Focus on logical causality and pacing.`;
// Implementation calls LLM with JSON mode enabled
const response = await this.llm.call(prompt);
return JSON.parse(response);
}
}
By forcing the StructureAgent to work with keyEvents and requiredCharacters as discrete arrays, we prevent the "drifting plot" syndrome. The next agent isn't guessing what happens; it is following a checklist.
Step 2: Injecting Personality and Memory (Character Agent)
Once we have the chapters, we need to ensure that the characters behave consistently. If a character is established as "stoic and traumatized" in Chapter 1, they cannot suddenly become a "jovial comedian" in Chapter 3.
The CharacterAgent takes the requiredCharacters from the StructureAgent and expands them into deep profiles. It also manages the "memory" of these characters.
interface CharacterProfile {
name: string;
traits: string[];
backstory: string;
internalConflict: string;
}
class CharacterAgent {
async enrichCharacters(chapters: Chapter[], characters: string[]): Promise<CharacterProfile[]> {
const profiles: CharacterProfile[] = [];
for (const charName of characters) {
const prompt = `Based on the plot outline, develop a deep profile for ${charName}.
Define their traits, backstory, and how their internal conflict
will drive the events in the provided chapters.`;
const profile = await this.llm.call(prompt);
profiles.push(JSON.parse(profile));
}
return profiles;
}
}
The magic happens here: the CharacterAgent acts as a bridge. It takes the "skeleton" and adds "muscle." When the pipeline moves to the final stage, the prompt will include not just the chapter summary, but the specific psychological profile of every character present in that scene.
Step 3: Atmospheric Rendering (Scene Agent)
The final layer, the SceneAgent, is the most computationally expensive. This agent is responsible for the actual prose. Because the structural and character constraints are already baked into its context, it can focus entirely on sensory details, dialogue, and pacing.
The prompt for the SceneAgent is a synthesis of all previous layers:
[Structure Context] + [Character Context] + [Atmospheric Instructions] = Final Prose
class SceneAgent {
async generateScene(
chapter: Chapter,
characters: CharacterProfile[],
settingContext: string
): Promise<string> {
const characterContext = characters
.filter(c => chapter.requiredCharacters.includes(c.name))
.map(c => `${c.name}: ${c.traits.join(', ')}. Conflict: ${c.internalConflict}`)
.join('\n');
const prompt = `
Write a detailed prose scene for Chapter ${chapter.chapterNumber}.
CONTEXT:
Setting: ${chapter.setting}
Characters Present:
${characterContext}
PLOT EVENTS TO COVER:
${chapter.keyEvents.map(e => `- ${e}`).join('\n')}
STYLE GUIDE:
Use sensory details (smell, sound, texture).
Maintain a ${settingContext} atmosphere.
Focus on the internal monologue of the characters.
`;
return await this.llm.call(prompt);
}
}
By the time the SceneAgent receives the request, the "hard work" of logic and consistency is already done. It is simply "painting" the scene within the boundaries defined by the previous agents.
The Pipeline Orchestration
The real complexity lies in the orchestration. You need a controller that manages the state and ensures that the output of Agent A is correctly formatted for Agent B.
class NovelGeneratorPipeline {
async run(premise: string) {
// 1. Architect Phase
const outline = await this.structureAgent.generateOutline(premise);
// 2. Soul Phase
const allCharacters = this.extractCharactersFromOutline(outline);
const characterProfiles = await this.characterAgent.enrichCharacters(outline.acts.chapters, allCharacters);
// 3. Painter Phase
const manuscript = [];
for (const chapter of outline.acts.chapters) {
const sceneProse = await this.sceneAgent.generateScene(
chapter,
characterProfiles,
"dark and cinematic"
);
manuscript.push(sceneProse);
}
return manuscript.join('\n\n');
}
}
Lessons Learned
- JSON is your best friend: Never let an agent return raw text if another agent needs to read it. Use structured outputs (JSON mode) to ensure the pipeline doesn't break.
-
Context Dilution: Don't pass the entire book to the
SceneAgent. Only pass the characters and plot points relevant to the specific chapter being written. This keeps the focus sharp and saves tokens. -
The "Identity" Problem: The
CharacterAgentis the most critical for long-term consistency. If you skip this step, your characters will become generic archetypes within three chapters.
Building multi-agent systems is not about making one agent "smarter"; it is about creating a specialized assembly line where each worker has a narrow, well-defined task.
If you want to see a full implementation of this architecture, including how I handle long-context memory and EPUB generation, check out the source code here:
https://github.com/KazKozDev/NovelGenerator
Top comments (0)