<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ProspectAI - multi agentic pipeline for investment portfolio]]></title><description><![CDATA[ProspectAI - multi agentic pipeline for investment portfolio]]></description><link>https://prospectai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 28 Oct 2026 12:04:56 GMT</lastBuildDate><atom:link href="https://prospectai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a 6-Agent Investment Research Pipeline with CrewAI]]></title><description><![CDATA[A 6-agent CrewAI pipeline for stock research: sequential agents, typed contracts, deterministic tools, and an adversarial critic. The architecture.]]></description><link>https://prospectai.hashnode.dev/building-a-6-agent-investment-research-pipeline-with-crewai</link><guid isPermaLink="true">https://prospectai.hashnode.dev/building-a-6-agent-investment-research-pipeline-with-crewai</guid><category><![CDATA[CrewAI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[Python]]></category><category><![CDATA[agent-architecture]]></category><dc:creator><![CDATA[Moises Prat]]></dc:creator><pubDate>Tue, 21 Jul 2026 21:54:39 GMT</pubDate><content:encoded><![CDATA[<p><em>How I split stock research across six specialized agents, and the two design rules that kept it from hallucinating its way into a portfolio.</em></p>
<p>I build <a href="https://prospect-ai.moisesprat.dev">ProspectAI</a>, a multi-agent system that researches stock portfolios. It's a technical demo, not an investment tool, and the interesting part is the architecture, not the picks. This post is about how the pipeline is put together and the two rules that made it work.</p>
<h2>The problem with one big agent</h2>
<p>The obvious way to build this is a single agent: "here is a stock, analyze it and tell me if it's a buy." One prompt, one call, one answer.</p>
<p>It works in a demo and falls apart in practice. That single prompt has to hold macro context, technical signals, fundamentals, and a final decision all at once. When the output is wrong, you can't tell which part failed. When it hallucinates a number, nothing catches it. And the model tends to talk itself into a conclusion, because there is nobody in the loop to disagree.</p>
<p>So I split the work. Not because more agents is better, but because clear boundaries are.</p>
<h2>The pipeline, top to bottom</h2>
<p>Six agents run as a pipeline. Each one has a narrow job and passes a structured result to the next.</p>
<pre><code>Market Analyst  -&gt;  macro &amp; sector context
                        │
             ┌──────────┴──────────┐
             v                     v
   Technical Analyst      Fundamental Analyst
   price, momentum        valuation, financials
             │                     │
             └──────────┬──────────┘
                        v
                Draft Strategist   -&gt;   builds a candidate portfolio
                        │
                        v
              Adversarial Critic   -&gt;   attacks the draft
                        │                (risks, contradictions, weak setups)
                        v
                Final Strategist   -&gt;   revises and commits
</code></pre>
<p>The Market Analyst runs first and sets the macro and sector context. That context feeds the Technical and Fundamental Analysts, which produce their views. The Draft Strategist turns everything into a candidate portfolio with entries, stops, and targets. The Critic tries to break it. The Final Strategist rewrites it based on the critique.</p>
<p>That's the shape. The two rules below are what make the shape actually hold up.</p>
<h2>Rule 1: LLMs reason, tools calculate</h2>
<p>This is the rule I would keep if I could only keep one.</p>
<p>No number that ends up in the portfolio is produced by a language model. Not the composite score, not the position size, not the stop-loss, not the risk/reward ratio. Every figure is computed by a deterministic Python tool. The LLM decides <em>which</em> signals matter and <em>why</em>. The math is not its job.</p>
<p>In pseudocode, the split looks like this:</p>
<pre><code># WRONG: the model invents the number
score = llm("rate this stock from 0 to 100 based on the analysis")

# RIGHT: the model reasons, the tool computes
weights   = llm.decide_signal_weights(analysis)   # qualitative judgment
score     = composite_score_tool(metrics, weights)  # deterministic
size      = allocator_tool(score, risk_profile)     # deterministic
stop, tgt = risk_tool(entry, atr, rr_floor=1.5)     # deterministic
</code></pre>
<p>The reason is simple. A model that can hallucinate a stock thesis can also hallucinate a performance metric, and the second one is far more dangerous, because it's the number you trust to decide whether the first one was any good. Keep the model away from arithmetic and a whole category of silent errors disappears.</p>
<h2>Rule 2: typed contracts between agents</h2>
<p>The second rule is about what crosses the boundary between agents.</p>
<p>Early on, agents passed prose to each other. The Technical Analyst wrote a paragraph, the Draft Strategist read it and interpreted it. This breaks in a quiet, annoying way: the downstream agent re-reads the narrative and subtly reinvents what the upstream agent meant.</p>
<p>The fix is to pass typed objects, not text. Each agent returns a schema, validated before it moves on.</p>
<pre><code>class TechnicalView(BaseModel):
    trend: Literal["up", "down", "sideways"]
    momentum_score: float
    key_levels: list[float]

class DraftPosition(BaseModel):
    ticker: str
    action: Literal["LONG-BUY", "WAIT", "AVOID"]
    entry_zone: tuple[float, float]
    stop_loss: float
    take_profit: float
</code></pre>
<p>If it isn't in the schema, it doesn't cross. The downstream agent can't act on a vibe it inferred from prose, because it only receives fields. This one change removed most of the hand-off bugs I had.</p>
<h2>The agent whose only job is to attack</h2>
<p>The Critic is my favorite part, and it does zero analysis.</p>
<p>It gets a fresh context and one instruction: attack the draft. Find the missing catalyst, the contradictory signals, the setup where the risk doesn't justify the reward. It doesn't rebuild the portfolio. It produces structured revision directives, and the Final Strategist applies them.</p>
<pre><code>draft     = draft_strategist(analyst_views)
critique  = critic(draft)          # fresh context, adversarial role
final     = final_strategist(draft, critique)
</code></pre>
<p>Why a separate agent instead of asking one agent to "double-check its work"? Because a model reviewing its own output in the same context tends to agree with itself. A separate role, with a fresh context and an adversarial instruction, actually pushes back. On one run the Critic bounced a draft several times and moved it from a 55 to a 77 on my own evaluator.</p>
<h2>The part I got wrong first</h2>
<p>Two honest ones, because that's the useful part of a build post.</p>
<p>The Critic started out too powerful. I let it recalculate values, and it began overriding the deterministic tools with its own numbers, which broke Rule 1. The fix was to narrow its role: the Critic challenges reasoning, it never computes. Recalculation is not its job.</p>
<p>And I almost shipped a stats card reading "100% win rate" on two signals. A hundred percent on n=2 is a coin landing heads twice. The evaluation layer now refuses to report a metric below a real sample size. A dashboard that shows confident numbers on tiny samples isn't a feature, it's a bug.</p>
<h2>The stack</h2>
<p>Nothing exotic. CrewAI for the agent orchestration, a different model per agent (cheaper models on the analysts, stronger ones on the strategist and critic), deterministic Python tools for every calculation, Modal for the backend, Cloudflare Pages for the frontend. Results stream to the browser over SSE.</p>
<h2>The pattern is not about finance</h2>
<p>Strip out the tickers and this generalizes to almost any LLM pipeline.</p>
<p>Let the model reason and let deterministic code do anything that has one correct answer. Pass typed objects between steps, not free text. Give one component the explicit job of disagreeing with the others. And refuse to report results you don't have the sample to support.</p>
<p>None of these are finance ideas. They're what keeps a multi-agent system honest, whatever it's doing.</p>
<p>If you want to see it run, the <a href="https://prospect-ai.moisesprat.dev">live pipeline is here</a> and the Python package is on <a href="https://pypi.org/project/prospectai">PyPI</a>. I'd genuinely like feedback on the critic pattern, so if you've built something similar, tell me what worked for you.</p>
<p><em>ProspectAI is a technical demonstration of multi-agent AI systems, paper-traded on public data, and explicitly not investment advice.</em></p>
]]></content:encoded></item></channel></rss>