“A practical guide to structuring multi-agent architectures where planner agents generate strict JSON schemas and curator agents ground responses with search tools.”
Monolithic prompts that ask a single LLM to simultaneously act as a strategic architect, web researcher, fact-checker, and formatted output generator frequently fail in production. They hallucinate links, violate output schemas, and degrade under complex input constraints.
The solution is a decoupled multi-agent pipeline where specialized agents operate with strict boundaries, validated communication schemas, and targeted tool grounding.
1. The Separation of Concerns Pattern#
When building agentic platforms like curriculum builders or automated job scanners, splitting the responsibility into dedicated stages creates predictable, testable, and maintainable systems.
┌────────────────────────────────────────────────────────┐
│ Architect Agent │
│ - Focus: Goal Decomposition & Schema Structuring │
│ - Input: User Goal & Constraints │
│ - Output: Strictly typed JSON Roadmap (No search) │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Resource Curator Agent │
│ - Focus: Grounding with Live Search & Verification │
│ - Input: Specific task items from Architect │
│ - Output: Verified URLs, tutorials, and documentation│
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Reviewer Agent │
│ - Focus: Deduplication & Quality Assessment │
│ - Output: Assembled Final Curriculum │
└────────────────────────────────────────────────────────┘2. Defining Strict Schemas with Zod#
By enforcing JSON Schemas at the boundary of each agent using Zod, we guarantee that subsequent agents receive clean, well-structured payloads:
import { z } from "zod";
export const TaskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(5),
objective: z.string(),
estimatedHours: z.number().positive(),
prerequisites: z.array(z.string()),
searchKeywords: z.array(z.string()).min(2),
});
export const MilestoneSchema = z.object({
milestoneNumber: z.number().int().positive(),
title: z.string(),
summary: z.string(),
tasks: z.array(TaskSchema),
});
export const CurriculumPlanSchema = z.object({
learnerGoal: z.string(),
difficultyLevel: z.enum(["beginner", "intermediate", "advanced"]),
milestones: z.array(MilestoneSchema),
});
export type CurriculumPlan = z.infer<typeof CurriculumPlanSchema>;3. Grounding Curator Agents with Search Tools#
Once the plan structure is locked, the Curator Agent takes the generated searchKeywords and queries live search engines (e.g. Google Search Grounding with Gemini):
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI();
export async function curateResourcesForTask(task: z.infer<typeof TaskSchema>) {
const prompt = `
Task: ${task.title}
Objective: ${task.objective}
Keywords: ${task.searchKeywords.join(", ")}
Find 2 high-quality, reputable learning resources (official documentation, top GitHub repos, or verified tutorials).
Ensure each resource has a working, valid HTTPS URL.
`;
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: prompt,
config: {
tools: [{ googleSearch: {} }],
},
});
return {
taskId: task.id,
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
text: response.text,
};
}4. Why This Outperforms Single-Prompt Systems#
- 01Deterministic Structure: If the search fails or returns unexpected data, the roadmap structure remains 100% intact.
- 02Independent Observability: Latency, token usage, and error metrics can be measured per agent.
- 03Model Specialization: High-reasoning models can be reserved for planning, while ultra-fast models handle tool execution.
5. Summary & Best Practices#
- Always validate inputs and outputs with strict schemas (like Zod).
- Never allow a planning agent to make external web calls in the same turn it designs the overarching architecture.
- Keep agent state minimal and communicate via serializable JSON events.
