All articles
Build Guides

Build a Multi-Agent Research Assistant with Gemini Flash Free Tier

FDE Coach EditorialAugust 9, 202610 min read

What We're Building

We're assembling a multi-agent research assistant that takes a complex question, breaks it into sub-questions, fetches live web results for each, and synthesizes everything into a clean structured brief. No paid APIs, no enterprise contracts—just Google Gemini 1.5 Flash free tier, SerpAPI's free plan, and LangChain's open-source orchestration.

Feature checklist:

  • Planner agent decomposes any research question into 3–5 targeted sub-queries
  • Searcher agent executes each sub-query against Google Search via SerpAPI
  • Writer agent consumes raw search snippets and produces a structured brief with executive summary, key findings, sources, and follow-up questions
  • LangGraph state machine orchestrates agent handoffs with retry logic
  • Fully configurable via environment variables—swap models or search backends without touching agent logic

If you've built single-agent LLM pipelines before, this is the natural next step. If you haven't, the FDE portfolio project guide lays out why multi-agent demos signal serious shipping velocity in technical interviews.

Architecture Overview

The system uses a directed graph where each node is an agent invocation and edges carry structured state. The planner runs once, the searcher fans out across sub-queries, and the writer aggregates results into a final output.

State flows top-to-bottom. The Planner produces a list of search-optimized strings. The Searcher iterates that list, calling SerpAPI for each and collecting organic result snippets. The Writer receives the original question plus all search context and generates the brief. LangGraph handles the fan-out as a parallel map step internally, so you get concurrency for free even on the free tier.

Prerequisites and Free-Tier Setup

Before writing a line of code, grab these free-tier credentials:

ServiceFree Tier LimitSign-Up Link
Google Gemini 1.5 Flash15 RPM, 1M tokens/dayaistudio.google.com
SerpAPI100 searches/monthserpapi.com

Gemini API key: Go to Google AI Studio, create an API key under "Get API Key," and copy it. No billing setup required for the free tier.

SerpAPI key: Sign up, verify email, and grab the key from the dashboard. The free plan gives you exactly 100 searches—plenty for testing and light production use.

Store both in a .env file at the project root:

GEMINI_API_KEY=your-gemini-key-here
SERPAPI_API_KEY=your-serpapi-key-here

You'll need Python 3.10+ and a virtual environment. That's it. No Docker, no cloud infra, no GPU.

Step 1: Project Scaffold and Dependencies

Create the project and install exactly what we need:

mkdir multi-agent-research && cd multi-agent-research
python -m venv .venv && source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install langchain langchain-google-genai google-search-results python-dotenv langgraph

Package breakdown:

  • langchain: core abstractions for prompts, chains, and tools
  • langchain-google-genai: Gemini chat model integration
  • google-search-results: SerpAPI Python client
  • python-dotenv: loads .env into os.environ
  • langgraph: stateful multi-agent graph orchestration

Create main.py and agents.py files. We'll keep agent logic in agents.py and the orchestration graph plus CLI entry point in main.py.

Step 2: Defining Agent State and Tools

LangGraph operates on a typed state dictionary that every node reads and writes. Define it once and reuse it across all agents.

In agents.py:

from typing import TypedDict, Annotated, List, Optional
from langgraph.graph.message import add_messages

class ResearchState(TypedDict):
    question: str
    sub_queries: Optional[List[str]]
    search_results: Annotated[list, add_messages]
    brief: Optional[str]
    error: Optional[str]

Annotated[list, add_messages] tells LangGraph to concatenate lists across parallel branches rather than overwriting—critical when the Searcher fans out.

Now define the SerpAPI tool wrapper:

from serpapi import GoogleSearch
import os

def search_web(query: str, num_results: int = 5) -> str:
    """Execute a Google search via SerpAPI and return formatted organic results."""
    params = {
        "q": query,
        "api_key": os.getenv("SERPAPI_API_KEY"),
        "num": num_results,
        "engine": "google"
    }
    search = GoogleSearch(params)
    results = search.get_dict()
    organic = results.get("organic_results", [])
    
    formatted = []
    for r in organic:
        snippet = r.get("snippet", "")
        link = r.get("link", "")
        title = r.get("title", "")
        formatted.append(f"Title: {title}\nSnippet: {snippet}\nURL: {link}")
    
    return "\n\n".join(formatted) if formatted else "No results found."

This returns plain text that the Writer can consume directly. No JSON parsing gymnastics needed.

Step 3: The Planner Agent

The Planner's job: take a high-level question and output a JSON list of 3–5 search-optimized sub-queries. We use Gemini Flash with a constrained system prompt and structured output parsing.

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.prompts import ChatPromptTemplate
import json

def planner_node(state: ResearchState) -> ResearchState:
    llm = ChatGoogleGenerativeAI(
        model="gemini-1.5-flash",
        temperature=0.2,
        google_api_key=os.getenv("GEMINI_API_KEY")
    )
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are a research planner. Given a complex question, decompose it into 3-5 specific, 
search-engine-optimized sub-queries. Each sub-query should target a distinct angle of the main question.
Return ONLY a valid JSON object with a single key 'sub_queries' mapping to an array of strings.
No markdown, no explanation."""),
        ("human", "Research question: {question}")
    ])
    
    chain = prompt | llm
    response = chain.invoke({"question": state["question"]})
    
    try:
        parsed = json.loads(response.content)
        state["sub_queries"] = parsed["sub_queries"]
    except (json.JSONDecodeError, KeyError):
        # Fallback: treat the whole response as a single query
        state["sub_queries"] = [state["question"]]
    
    return state

Temperature is set low (0.2) because we want deterministic decomposition, not creative flair. The fallback ensures the pipeline doesn't crash if Gemini occasionally ignores the JSON instruction—a real-world edge case you'll hit within your first 10 runs.

Step 4: The Searcher Agent

The Searcher iterates sub_queries, calls search_web for each, and appends results to state. LangGraph's Send API handles parallel fan-out automatically.

from langgraph.graph import Send

def searcher_node(state: ResearchState, query: str) -> dict:
    results = search_web(query)
    return {"search_results": [f"### Query: {query}\n{results}"]}

def fan_out_searches(state: ResearchState):
    """Return a list of Send objects, one per sub-query."""
    return [Send("searcher", {"query": q}) for q in state.get("sub_queries", [])]

The Send pattern is LangGraph's canonical way to map a node over a list. Each Send executes searcher_node with an isolated query argument, and results merge back into state["search_results"] via the add_messages reducer.

Step 5: The Writer Agent

The Writer receives the original question plus all aggregated search results and produces a structured brief. We prompt-engineer a consistent output format with clear sections.

def writer_node(state: ResearchState) -> ResearchState:
    llm = ChatGoogleGenerativeAI(
        model="gemini-1.5-flash",
        temperature=0.4,
        google_api_key=os.getenv("GEMINI_API_KEY")
    )
    
    search_context = "\n\n".join(state.get("search_results", []))
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are a research analyst. Synthesize the provided search results into a structured brief.
Use the following format exactly:

## Executive Summary
(2-3 sentence overview answering the research question)

## Key Findings
- Finding 1 with supporting detail
- Finding 2 with supporting detail
(3-5 bullet points)

## Sources
- Source title and URL for each key claim

## Follow-Up Questions
- Question 1
- Question 2

Be concise. Cite specific sources inline where possible."""),
        ("human", "Research question: {question}\n\nSearch results:\n{search_context}")
    ])
    
    chain = prompt | llm
    response = chain.invoke({
        "question": state["question"],
        "search_context": search_context
    })
    
    state["brief"] = response.content
    return state

Temperature bumps to 0.4 here—we want coherent prose, not rigid output. The markdown structure makes the brief immediately readable in terminal output or a rendered frontend.

Step 6: Orchestrating the Multi-Agent Graph

With all nodes defined, we wire them into a StateGraph and compile it. This goes in main.py:

from dotenv import load_dotenv
load_dotenv()

from langgraph.graph import StateGraph, START, END
from agents import ResearchState, planner_node, fan_out_searches, searcher_node, writer_node

def build_graph():
    builder = StateGraph(ResearchState)
    
    builder.add_node("planner", planner_node)
    builder.add_node("searcher", searcher_node)
    builder.add_node("writer", writer_node)
    
    builder.add_edge(START, "planner")
    builder.add_conditional_edges("planner", fan_out_searches, ["searcher"])
    builder.add_edge("searcher", "writer")
    builder.add_edge("writer", END)
    
    return builder.compile()

def run_research(question: str) -> str:
    graph = build_graph()
    initial_state = ResearchState(
        question=question,
        sub_queries=None,
        search_results=[],
        brief=None,
        error=None
    )
    final_state = graph.invoke(initial_state)
    return final_state.get("brief", "Error: No brief generated.")

if __name__ == "__main__":
    question = input("Enter your research question: ")
    print("\n--- Research Brief ---\n")
    print(run_research(question))

The edge from searcher to writer fires once all parallel Send calls resolve—LangGraph handles the synchronization barrier automatically.

Running the Assistant

From your project root with the virtual environment active:

python main.py

Example session:

Enter your research question: How is AI being used to improve battery technology in 2025?

--- Research Brief ---

## Executive Summary
AI is accelerating battery R&D across materials discovery, manufacturing optimization, and...

First run will be slow—Gemini cold-start latency plus SerpAPI round trips typically total 8–15 seconds. Subsequent runs within the same session are faster as connections stay warm.

Rate limit watch: With 3–5 sub-queries per question, you get roughly 20–30 full research cycles per month on SerpAPI's free tier. Gemini's 15 RPM limit is rarely the bottleneck; SerpAPI is the constraining resource here.

Sensible Extensions

Once the baseline works, these upgrades turn it into a genuinely useful daily driver:

1. Add a Critic Agent — Insert a node between Writer and END that reads the brief and checks for hallucinations against search results. If confidence is low, loop back to Writer with feedback. This is the single highest-impact improvement for output quality.

2. Switch to Tavily for search — Tavily's free tier offers 1,000 searches/month (10x SerpAPI) and returns LLM-optimized results. Drop-in replacement: swap search_web to call Tavily's API instead.

3. Persistent conversation memory — Store briefs in SQLite and let users ask follow-up questions that reference prior research. LangGraph's MemorySaver checkpointer makes this a 3-line change.

4. Streaming output — Wrap the graph invocation in LangGraph's stream() method to show intermediate results (sub-queries, search snippets) as they arrive, rather than blocking until the final brief.

If you're building this for a portfolio piece, the FDE portfolio projects guide covers how to present multi-agent systems in a way that signals architectural thinking, not just API-wrapping.

Common Pitfalls and Debugging

Gemini returns non-JSON despite explicit instructions. The fallback in planner_node catches this, but if you see consistently malformed output, drop temperature to 0 and add a response_mime_type="application/json" parameter to the Gemini model constructor (available in newer langchain-google-genai versions).

SerpAPI exhausts free credits silently. SerpAPI returns a error key in the response dict when credits run out, but search_web currently ignores it. Add a check: if "error" in results: return f"Search error: {results['error']}".

LangGraph state mutation surprises. Nodes receive state by reference and mutations persist. If you accidentally modify a list in place rather than returning a new list via the reducer, you'll get duplicate entries. Always return new objects or rely on add_messages semantics.

Gemini rate limit 429 errors. The free tier enforces 15 RPM strictly. If you're firing multiple research runs back-to-back, add a time.sleep(4) between runs. For production, implement exponential backoff with tenacity.

FAQ

Why not use OpenAI's free tier? Gemini 1.5 Flash free tier offers 1M tokens/day with no credit card requirement. OpenAI's free tier is credit-limited and expires. For a system that makes multiple LLM calls per research run, Gemini's throughput is hard to beat at zero cost.

Can I replace SerpAPI with Bing or DuckDuckGo? Yes, but free tiers vary. DuckDuckGo's Instant Answer API is free but limited in result depth. Bing's free tier requires Azure signup. SerpAPI is chosen here for the simplest setup path.

How do I deploy this for a team? Wrap run_research in a FastAPI endpoint, add API key auth, and deploy on Render or Railway free tiers. The FDE interview prep guide discusses why live-deployed prototypes carry more weight in technical evaluations than local-only scripts.

What if the Writer hallucinates facts not in the search results? This is the fundamental challenge of RAG systems. Adding the Critic agent (see Extensions) catches most hallucinations. For production, consider grounding the Writer with Google's built-in grounding feature available in the Gemini API.

Is this production-ready? As-is, it's a solid prototype. For production: add the Critic agent, implement proper error handling with retries, add logging, and swap SerpAPI for a higher-volume search backend. The architecture (LangGraph state machine) is production-grade and scales to dozens of agents without restructuring.

#multi-agent#research#gemini#automation#planning

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