WARNING: NOT AN INVESTMENT TOOL. This is a technical demonstration of multi-agent AI systems. All output is AI-generated and must not be used as financial advice.
Architecture Pattern

Adversarial Critic Pattern: How AI Agents Challenge Their Own Output

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.

What it is

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.

How ProspectAI implements it

ProspectAI's CriticAgent is a minimal CrewAI Agent — no tools attached, pure textual reasoning against the draft portfolio strategy already produced by InvestorStrategicAgent:

ProspectAI / agents/critic_agent.py
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:

ProspectAI / prospect_ai_flow.py, lines 474-508
@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,
    )

Why it matters

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.

Related Patterns