Build a Multi-Agent Research Assistant with LangGraph & Groq Free Tier
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.
| Service | Free Tier Limit | Sign-Up Link |
|---|---|---|
| Groq Cloud | 1,500 requests/day (Llama 3.1 8B) | console.groq.com |
| Tavily Search | 1,000 searches/month | app.tavily.com |
| LangGraph | OSS, zero cost | pip 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:
- Parallel search with asyncio: Use
asyncio.gatherto fire all Tavily queries concurrently, thenawaitthem. Stays under 10 req/min if you batch 3-5 queries. Cuts latency from 15s to 5s. - Human-in-the-loop review: Add an
interruptnode before synthesis. LangGraph’sinterrupt()pauses execution—you inspect raw results, prune irrelevant ones, then resume. Critical for high-stakes briefs. - Persistent memory with SQLite: Dump
raw_resultsandfinal_briefinto a local SQLite DB keyed by topic hash. Next time someone asks a similar question, skip the search phase entirely. - 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
| Pitfall | Symptom | Fix |
|---|---|---|
| Groq JSON parse failure | json.JSONDecodeError | The fallback in plan_queries handles this. If it still fails, reduce temperature to 0 and explicitly say "ONLY the JSON array" |
| Tavily 429 rate limit | HTTP 429 in search_one_query | Add time.sleep(1.5) between calls. Free tier allows ~10 req/min—sequential execution already paces this |
| Empty search results | raw_results is empty, brief is generic | Tavily sometimes returns zero results for obscure queries. Add a retry with broader query terms |
| LangGraph state mutation | Nodes overwrite each other’s fields | StateGraph copies state between nodes by default. If you need true shared memory, use a Checkpointer |
| Brief too long for context window | Truncated output | Limit 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.
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