All articles
Build Guides

Build a Multi-Agent Research Assistant with LangGraph & Groq Free Tier

FDE Coach EditorialAugust 24, 20269 min read

What We’re Building

A multi-agent research assistant that takes a messy prompt like “compare retrieval-augmented generation vs long-context windows for legal doc review” and hands you back a structured, cited research brief—without touching a credit card.

Core feature list:

  • Planner agent decomposes the topic into 3-5 targeted search queries
  • Researcher agent executes each query against Tavily’s web search API, extracts snippets and URLs
  • Synthesizer agent merges findings, deduplicates, and writes a Markdown brief with inline citations
  • Router (LangGraph) orchestrates state transitions, handles rate limits, and retries on failure
  • Everything runs on free tiers: Groq’s Llama 3.1 8B (1500 requests/day), Tavily (1000 searches/month), LangGraph OSS

By the end, you’ll have a single python run.py command that produces a brief.md file ready to paste into Notion or Slack.

Architecture & Flow

The swarm uses a directed graph with conditional edges. The Planner emits a list of queries; the Researcher fans out (sequentially, to respect Tavily’s free-tier rate limit); the Synthesizer collapses results into a final document.

State shape flows through every node as a typed dictionary: topic, search_queries, raw_results, synthesis_attempts, final_brief, error. LangGraph’s StateGraph guarantees each node sees the same schema.

Prerequisites (All Free Tier)

Before writing a line of code, grab three API keys. Each takes under two minutes.

ServiceFree Tier LimitSign-Up Link
Groq Cloud1,500 requests/day (Llama 3.1 8B)console.groq.com
Tavily Search1,000 searches/monthapp.tavily.com
LangGraphOSS, zero costpip install langgraph

Python environment: 3.10+ with pip available. Create a virtual environment—don’t pollute your system Python.

python -m venv .venv && source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install langgraph langchain-groq tavily-python python-dotenv

Drop keys into .env:

GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxx
TAVILY_API_KEY=tvly-xxxxxxxxxxxxxxxxxxxx

Load them at the top of every script with from dotenv import load_dotenv; load_dotenv().

If you’ve shipped agents before, this is the same pattern we used in the Gmail triage agent build—free-tier LLMs are surprisingly capable when you constrain the task.

Step 1: Scaffold the Project

Three files, clean separation of concerns:

research_swarm/
├── .env
├── agents.py      # Planner, Researcher, Synthesizer logic
├── graph.py       # LangGraph state machine definition
└── run.py         # CLI entry point

agents.py holds pure functions—no graph wiring. graph.py imports them and builds the StateGraph. run.py invokes the graph and writes output. This separation lets you unit-test each agent independently.

Step 2: Define State & Memory

In graph.py, define the ResearchState as a TypedDict. LangGraph uses this for type-checking and serialization between nodes.

from typing import TypedDict, List, Optional

class ResearchState(TypedDict):
    topic: str
    search_queries: List[str]
    raw_results: List[dict]          # list of {query, title, url, snippet}
    current_query_index: int
    synthesis_attempts: int
    final_brief: str
    error: Optional[str]

Initialize with sensible defaults:

def initial_state(topic: str) -> ResearchState:
    return {
        "topic": topic,
        "search_queries": [],
        "raw_results": [],
        "current_query_index": 0,
        "synthesis_attempts": 0,
        "final_brief": "",
        "error": None,
    }

Step 3: Build the Planner Agent

This agent receives the raw topic and returns 3-5 search-engine-optimized queries. We use Groq’s Llama 3.1 8B via LangChain’s ChatGroq wrapper. Temperature stays low (0.2) for deterministic planning.

In agents.py:

from langchain_groq import ChatGroq
import os

def plan_queries(state: ResearchState) -> ResearchState:
    llm = ChatGroq(
        model="llama-3.1-8b-instant",
        temperature=0.2,
        api_key=os.getenv("GROQ_API_KEY")
    )
    prompt = f"""You are a research strategist. Given the topic below, generate 3-5 distinct, high-signal search queries that cover different angles. Return ONLY a JSON array of strings, no commentary.

Topic: {state["topic"]}"""

    response = llm.invoke(prompt)
    # Parse JSON array from response.content
    import json
    try:
        queries = json.loads(response.content)
        state["search_queries"] = queries[:5]
    except json.JSONDecodeError:
        # Fallback: extract lines that look like queries
        state["search_queries"] = [
            line.strip("- ").strip()
            for line in response.content.split("\n")
            if len(line.strip()) > 10
        ][:5]
    return state

Why JSON mode instead of structured output? Groq’s free tier doesn’t reliably support tool calling on the 8B model. Forcing JSON in the prompt with a parse fallback costs zero extra latency and works every time.

Step 4: Build the Researcher Agent

Tavily’s free tier returns title, URL, content snippet, and a relevance score. We process one query per invocation to stay under the 10 requests/minute soft cap.

from tavily import TavilyClient

def search_one_query(state: ResearchState) -> ResearchState:
    client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
    idx = state["current_query_index"]
    if idx >= len(state["search_queries"]):
        return state  # all done

    query = state["search_queries"][idx]
    try:
        response = client.search(query=query, max_results=3, include_raw_content=False)
        for result in response.get("results", []):
            state["raw_results"].append({
                "query": query,
                "title": result.get("title", ""),
                "url": result.get("url", ""),
                "snippet": result.get("content", "")[:500],
            })
    except Exception as e:
        state["error"] = f"Tavily error on query '{query}': {str(e)}"

    state["current_query_index"] += 1
    return state

Step 5: Build the Synthesizer Agent

The Synthesizer takes all raw results and produces a Markdown brief with inline citations. We use a structured prompt that enforces sections: Overview, Key Findings, Contrasting Views, and References.

def synthesize_brief(state: ResearchState) -> ResearchState:
    llm = ChatGroq(
        model="llama-3.1-8b-instant",
        temperature=0.4,
        api_key=os.getenv("GROQ_API_KEY")
    )
    # Build context string from results
    context_blocks = []
    for i, r in enumerate(state["raw_results"]):
        context_blocks.append(f"[{i+1}] {r['title']}\n{r['snippet']}\nURL: {r['url']}")
    context = "\n\n".join(context_blocks)

    prompt = f"""You are a research analyst. Synthesize the following search results into a concise, well-structured Markdown research brief. Use inline citations like [1], [2] referencing the source numbers below.

Sections: ## Overview, ## Key Findings, ## Contrasting Views, ## References.

Topic: {state["topic"]}

Search Results:
{context}"""

    response = llm.invoke(prompt)
    state["final_brief"] = response.content
    state["synthesis_attempts"] += 1
    return state

If you want to dive deeper into LLM-driven research workflows, the screenshot-to-code agent article shows how vision models handle a similar parse-and-synthesize pipeline.

Step 6: Wire the LangGraph Router

Now the orchestration. graph.py imports the agent functions and builds a StateGraph with conditional routing.

from langgraph.graph import StateGraph, END

def should_continue_search(state: ResearchState) -> str:
    if state.get("error"):
        return "synthesize"  # proceed with partial results
    if state["current_query_index"] < len(state["search_queries"]):
        return "search"
    return "synthesize"

def build_graph():
    workflow = StateGraph(ResearchState)

    workflow.add_node("plan", plan_queries)
    workflow.add_node("search", search_one_query)
    workflow.add_node("synthesize", synthesize_brief)

    workflow.set_entry_point("plan")
    workflow.add_edge("plan", "search")
    workflow.add_conditional_edges(
        "search",
        should_continue_search,
        {"search": "search", "synthesize": "synthesize"}
    )
    workflow.add_edge("synthesize", END)

    return workflow.compile()

Why a loop on search? Each Tavily call is a separate node invocation. The conditional edge loops back until the queue is drained. This gives you natural pause points for rate limiting and error recovery without threading.

Step 7: Run the Swarm

run.py ties it together with a CLI argument and writes the brief to disk.

import sys
from dotenv import load_dotenv
from graph import build_graph, initial_state

load_dotenv()

def main():
    if len(sys.argv) < 2:
        print("Usage: python run.py \"your research topic\"")
        sys.exit(1)

    topic = sys.argv[1]
    graph = build_graph()
    state = initial_state(topic)

    # Invoke the graph—LangGraph streams state through each node
    final_state = graph.invoke(state)

    if final_state["error"]:
        print(f"Warning: completed with errors - {final_state['error']}")

    output_path = "brief.md"
    with open(output_path, "w") as f:
        f.write(final_state["final_brief"])

    print(f"Brief written to {output_path}")
    print(f"Sources consulted: {len(final_state['raw_results'])}")

if __name__ == "__main__":
    main()

Run it:

python run.py "compare RAG vs long-context windows for legal document review"

Expect 15-30 seconds wall time. The bottleneck is Tavily’s 1-second rate limit, not the LLM.

Extensions That Actually Matter

Once the baseline works, these upgrades deliver real value without breaking the free tier:

  1. Parallel search with asyncio: Use asyncio.gather to fire all Tavily queries concurrently, then await them. Stays under 10 req/min if you batch 3-5 queries. Cuts latency from 15s to 5s.
  2. Human-in-the-loop review: Add an interrupt node before synthesis. LangGraph’s interrupt() pauses execution—you inspect raw results, prune irrelevant ones, then resume. Critical for high-stakes briefs.
  3. Persistent memory with SQLite: Dump raw_results and final_brief into a local SQLite DB keyed by topic hash. Next time someone asks a similar question, skip the search phase entirely.
  4. Multi-model fallback: If Groq returns gibberish (rare but happens), catch it and retry with a different model from the free tier list (e.g., Mixtral 8x7B).

For engineers looking to prove they can ship in chaotic environments, this kind of multi-agent project is exactly what we cover in the FDE portfolio guide—real pipelines, real constraints, no toy demos.

Common Pitfalls & Fixes

PitfallSymptomFix
Groq JSON parse failurejson.JSONDecodeErrorThe fallback in plan_queries handles this. If it still fails, reduce temperature to 0 and explicitly say "ONLY the JSON array"
Tavily 429 rate limitHTTP 429 in search_one_queryAdd time.sleep(1.5) between calls. Free tier allows ~10 req/min—sequential execution already paces this
Empty search resultsraw_results is empty, brief is genericTavily sometimes returns zero results for obscure queries. Add a retry with broader query terms
LangGraph state mutationNodes overwrite each other’s fieldsStateGraph copies state between nodes by default. If you need true shared memory, use a Checkpointer
Brief too long for context windowTruncated outputLimit raw_results to the 10 most relevant before synthesis. Llama 3.1 8B handles ~8K tokens comfortably

FAQ

Q: Why LangGraph instead of CrewAI or AutoGen? LangGraph gives you explicit control over state transitions. For a linear plan→search→synthesize pipeline, that’s exactly what you want. CrewAI adds opinionated role abstractions that work against you when debugging a stuck agent loop. LangGraph’s conditional edges make the flow visible, not magical.

Q: Can I use this for commercial work? The free tiers have rate limits that cap you at ~50 briefs/day (Tavily’s 1000 searches/month is the bottleneck). For production, upgrade to Tavily’s $30/month plan and swap Groq for a paid endpoint. The architecture doesn’t change.

Q: What if Tavily returns paywalled content? Tavily’s free tier only surfaces publicly indexed snippets. You won’t get full-text from paywalled journals. For deep research, pair this with a GitHub PR review bot-style pipeline that ingests PDFs directly.

Q: How do I debug a stuck graph? Add print(state.keys()) at the top of each node function. LangGraph also supports stream() mode—call graph.stream(state) instead of invoke() to see intermediate states.

Q: Is the 8B model smart enough for real synthesis? For summarization and citation formatting, absolutely. Llama 3.1 8B outperforms GPT-3.5 on most benchmarks. Where it struggles is nuanced argumentation—if your brief requires legal reasoning, consider the patterns we use in the Claude Mythos 5 cybersecurity workflows.

Q: What’s the next skill to learn after shipping this? Speed. The difference between a hobby project and a Forward Deployed asset is how fast you can adapt it to a customer’s weird data format. We break down the highest-leverage FDE skills—speed, taste, data wrangling—in this deep dive.

#multi-agent#research#langgraph#groq

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now