An AI research agent built with LangGraph. Given a topic, it plans a set of
sub-questions, researches each one in parallel with web search, and merges the
results into a single markdown report (report.md).
It runs against a self-hosted LLM (gpt-oss:20b via Ollama) and uses Tavily
for web search. No hosted LLM API is required.
+----------+
| planner | breaks the topic into 3-5 sub-questions
+----------+ (structured output, json_schema)
|
Send() fan-out (one branch per sub-question)
/ | \
+------------+------------+------------+
| researcher | researcher | researcher | Tavily search + summarize,
+------------+------------+------------+ in parallel
\ | /
findings merged via operator.add
|
+-------------+
| synthesizer | writes the final markdown report
+-------------+
|
report.md
- Map-reduce with
Send(). The planner does not know ahead of time how many sub-questions there will be, so the graph cannot use a fixed number of edges. A conditional edge returns a list ofSend("researcher", ...)objects, and LangGraph spawns one researcher branch per sub-question at runtime. Each branch receives only its own private state ({"sub_question": ...}). - Reducer state.
findingsis declared asAnnotated[list[str], operator.add]. Without a reducer, parallel branches writing to the same key would conflict; withoperator.add, each branch's findings are appended, which is the reduce step. - Structured output. The planner uses
.with_structured_output(ResearchPlan, method="json_schema")— Ollama's native JSON schema mode — so the sub-question list comes back as a validated Pydantic object instead of free text that needs parsing. Thejson_schemamethod also avoids the tool-calling code path, which is not implemented for all Ollama models. - Local-first LLM. All three node types call the same Ollama-served
model. The server address comes from
OLLAMA_BASE_URL, so the code never hardcodes a machine-specific address.
Requires Python 3.12+, uv, a running
Ollama server with gpt-oss:20b pulled, and a
Tavily API key.
uv sync
cp .env.example .envThen edit .env:
TAVILY_API_KEY=your-tavily-key-here
OLLAMA_BASE_URL=http://localhost:11434
Point OLLAMA_BASE_URL at wherever your Ollama server runs. On the server,
pull the model:
ollama pull gpt-oss:20buv run main.py "how do solid state batteries work"Prints the sub-questions it researched and writes the final report to
report.md (gitignored).
The same pipeline is also available as a notebook, research_agent.ipynb,
with a graph visualization and inline report rendering.
- A single Ollama server may serialize the "parallel" researcher requests
depending on its
OLLAMA_NUM_PARALLELsetting and available memory, so the fan-out is concurrent from the graph's point of view but not necessarily on the GPU. - Requires a recent
langchain-ollamaformethod="json_schema"structured output support. - Report quality depends entirely on the top-3 Tavily results per sub-question; there is no citation verification or follow-up searching.
- No retries: if the model returns malformed structured output or a search fails, the run fails.