A generator-critic pattern for multi-agent AI pipelines: one agent proposes a draft, a structurally separate agent is instructed to actively try to break it, and a third pass reconciles both before anything ships.
Most LLM pipelines generate an answer in a single pass and ship it. The adversarial critic pattern splits generation and evaluation into two separate agents with opposing incentives: a Draft Strategist agent proposes a solution, and a Critic agent — with no tools, no delegation, and an explicit adversarial goal — is asked to find every reason the draft is wrong rather than confirm it's right. A third, final pass is then forced to consume both the original draft and the critique and produce a revised, defensible output.
ProspectAI's CriticAgent is a minimal CrewAI Agent — no tools attached, pure textual reasoning against the draft portfolio strategy already produced by InvestorStrategicAgent:
from .base_agent import BaseAgent
from crewai import Agent
class CriticAgent(BaseAgent):
"""Adversarial reviewer that challenges the Draft Strategist's portfolio."""
def __init__(self, config_path: str = None):
super().__init__(agent_key="critic", config_path=config_path)
def create_agent(self) -> Agent:
return Agent(
role=self.role,
goal=self.goal,
backstory=self.backstory,
verbose=self.verbose,
allow_delegation=self.allow_delegation,
llm=self._get_llm(),
max_iter=self.max_iter,
)
The orchestration layer (a CrewAI Flow) is what actually enforces the two-pass structure: critique_review runs after the draft strategy and validates the critic's output against a strict CriticOutput schema (per_ticker_critiques, revision_directives); final_strategy is then required to build its prompt context from both the original draft and the critique — it cannot skip straight from draft to final without the adversarial pass in between:
@listen(draft_strategy)
async def critique_review(self):
...
self.state.critique_output = self._extract_pydantic(
result, CriticOutput, "critique_review"
)
return result.raw or ""
@listen(critique_review)
async def final_strategy(self):
...
ctx = "\n\n".join([
self._fmt_ctx("Draft Strategy Output", self._slim_draft()),
self._fmt_ctx("Critic Review Output", self._slim_critique()),
])
task = self._factory.build_task(
"final_strategy", self.state.sector, self.state.today,
ctx, risk_profile=self.state.risk_profile,
)
Decoupling "propose" and "attack" into separate LLM calls with separate prompts (and in ProspectAI's case, the same higher-reasoning model tier for both — see model tiering) measurably improves output quality versus a single-pass generation, because the critic isn't anchored by having just written the answer itself. The cost is real: an extra LLM call's worth of latency and tokens per run. For a system producing investment recommendations from an LLM — output that can't be validated against ground truth in real time — that trade-off favors correctness over speed.