Disclaimer: This article documents a personal educational experiment using fictional source packets. It is not legal, compliance, academic, financial, security, or professional advice. It does not validate sources, certify accuracy, or guarantee research quality. Real research workflows require independent validation and qualified professional review. NosisTech LLC accepts no liability for outcomes arising from use of this content.
Most people think of AI research as one big action: ask a question, get a report. The more useful architecture is smaller and more disciplined. A research agent needs to plan what it is looking for, gather evidence, and then write only from the evidence it actually selected.
I rebuilt the core idea behind GPT Researcher as a small educational model. I removed live web search, scraping, document ingestion, vector databases, frontend code, backend servers, and multi-agent orchestration. What remained was the part worth studying: the research loop.
What This Agent Does
This agent takes a fictional research question and breaks it into smaller research questions. Then it looks through a small set of fictional source packets and selects the packets that appear related to the plan. Finally, it asks the model to produce a cautious summary using only those selected packets.
The important point is not that this is a complete research tool. It is not. The important point is that the architecture separates three jobs that are often blurred together: planning, evidence selection, and publishing. That separation gives the builder more places to inspect, stop, and review the output before anyone mistakes a polished report for a verified result.
The Evidence Desk
Think of this build like a small research desk inside a company.
The planner is the person who turns a vague question into a few focused research questions. The gather step is the clerk who pulls source folders from a shelf. The publish step is the writer who drafts a short memo from those folders. The review gate is the manager who checks whether the memo actually cites the folders that were selected.
That is the core lesson. A research agent is not just a chatbot with a longer answer. It is a workflow. Each stage creates a record that can be inspected.
This matters because a report can look professional even when the evidence underneath it is weak. In this build, the unsupported broad topic was stopped locally with NEEDS_REVIEW. That is the behavior I wanted to see: the agent does not pretend a tiny fictional evidence set can answer every possible technology risk.
Vocabulary You Need
Research plan means the small list of questions the agent creates before it writes. In this build, the model creates up to three research questions so the system has something more specific to match against the source packets.
Source packet means a small fictional evidence record. Each packet has an ID, title, tags, and text. I used packets instead of live websites so the build stays safe, inspectable, and easy to explain.
Citation validation means checking whether the model cited only the source IDs that were actually selected. A citation is not proof that a claim is true, but it creates a useful review trail.
Fallback summary means a local backup response when the model does not return the requested JSON format. In this build, both Gemini and DeepSeek produced useful plans, but the publish step did not return usable JSON, so the code produced a cautious local fallback.
LiteLLM routing means the model provider is controlled through environment variables instead of hardcoded into the code. I tested the same script with Gemini and DeepSeek by changing the model setting, not the Python file.
What This Is Not
This is not the full GPT Researcher project. It does not search the live internet, scrape websites, open a browser, process PDFs, ingest local files, run a backend server, render a frontend, use LangChain, use LangGraph, or create production research reports.
It is also not a source verification system. The source packets are fictional. The output is only a learning artifact that shows how the plan, gather, publish pattern can be rebuilt in a small LiteLLM-native script.
Why I Built This
I wanted to isolate the part of research agents that most people skip over. The flashy part is the final report. The valuable part is the chain of decisions that produced it.
What I found is that a small research agent needs more than a writing prompt. It needs gates. It needs selected evidence. It needs a way to say the evidence is partial or the topic is too broad.
That is the standard I am setting with this rebuild. I am not treating research automation as magic. I am treating it as a sequence of reviewable decisions.
What Actually Happened.
What actually happened:
Both Gemini and DeepSeek generated useful research plans. Both selected relevant fictional source packets. Both failed the publish JSON format often enough that I added a local fallback.
What broke:
The publish step returned output that did not match the requested JSON structure. The first version turned every report into NEEDS_REVIEW, which made the demonstration less useful.
What surprised me:
The planning step worked better than the writing contract. That is useful because it shows where provider behavior can diverge even when the same architecture runs.
What I would change next:
I would add a source trust registry before the publish step. The agent would check whether a source packet is approved, current, and appropriate before using it.
Why this matters:
Research agents need evidence gates before writing. Citations alone are not enough if the system never checked whether the source belonged in the report.
The Architecture: How It Works
The agent runs in five layers.
First, it loads environment variables. These tell the script where the LiteLLM proxy is, which model to use, and which API key is available. The code prints the active model but never prints the key.
Second, it validates the question. Empty questions, sensitive topics, and very broad questions are stopped before the model gets involved.
Third, it asks the model to plan. The model creates a few research questions from the original topic.
Fourth, the script gathers evidence from local fictional source packets. This replaces web search, scraping, and document ingestion with something the reader can inspect in a few seconds.
Fifth, the model tries to publish a short cited report. If the model output is not usable JSON, the local fallback records the selected evidence and marks the result as partial.
The architecture is simple, but the lesson is serious: every research pipeline needs a place where weak evidence can stop the workflow.
What to Watch When You Run It
The first thing to watch is the active model line. In my tests, the same script ran with gemini-flash and deepseek-v4-pro.
The second thing to watch is the plan. Gemini sometimes kept the plan closer to the original topic. DeepSeek often expanded the plan into more detailed questions.
The third thing to watch is the verdict. The first two scenarios returned PARTIAL because related fictional evidence was selected, but the model report fell back to local formatting. The third scenario returned NEEDS_REVIEW because the question was too broad for the evidence set.
That is exactly the kind of behavior I want in an educational research agent. It does not hide uncertainty. It shows where the evidence was enough to proceed and where the question needed review.
The Build: Step by Step
The first important piece is the local evidence shelf.
PACKETS = [
{"id": "SRC-001", "title": "Aether Works AI Policy Notes", "tags": "ai policy governance approval automation", "text": "Internal AI workflow pilots receive documented review before wider use. Human review is expected when outputs may affect budgets or staffing."},
{"id": "SRC-002", "title": "Meridian Operations Automation Pilot", "tags": "workflow automation efficiency training pilot", "text": "Three reporting workflows used automation. Processing time decreased, while staff training created a temporary first-phase cost."},
{"id": "SRC-003", "title": "Orion Review Group Governance Survey", "tags": "governance audit oversight risk policy", "text": "Many teams lacked version tracking, review cycles, and incident notes for AI-assisted internal tools."},
{"id": "SRC-004", "title": "Callisto Responsible Automation Brief", "tags": "automation review transparency limits ethics", "text": "The brief recommends labeling automated outputs, documenting review steps, and avoiding unsupported claims when evidence is incomplete."},
]
What This Code Is Actually Doing
This is the agent’s tiny research library. Each packet is a fictional source with an ID, title, tags, and a short text body.
The ID is important because the report can cite SRC-001 or SRC-002 instead of pretending it found real websites. The tags are simple labels the gather step can match against. This keeps the build safe and inspectable.
The next piece is the local question gate.
def safe_question(question):
"""Reject empty or sensitive demo questions before model calls."""
if not question.strip():
return False, "Question is empty."
if "every possible" in question.lower() or "all possible" in question.lower():
return False, "Question is too broad for this small fictional evidence set."
if any(term in question.lower() for term in BLOCKED):
return False, "Question contains sensitive wording and needs review."
return True, ""
What This Code Is Actually Doing
This function is a front desk. Before the AI model receives anything, the script checks whether the question is empty, too broad, or sensitive.
That matters because some requests are not good candidates for a tiny educational research model. A question like “every possible technology risk” is too wide for four fictional source packets. The safer behavior is to return NEEDS_REVIEW.
The gather step selects fictional evidence.
def gather(questions):
"""Select fictional source packets using keyword overlap."""
selected = {}
for question in questions:
question_words = words(question)
for packet in PACKETS:
packet_words = words(packet["title"] + " " + packet["tags"] + " " + packet["text"])
if question_words & packet_words:
selected[packet["id"]] = packet
if len(selected) == 3:
return list(selected.values())
return list(selected.values())
What This Code Is Actually Doing
This is the clerk pulling folders from the shelf. The function compares words from the model’s research questions against words in each fictional source packet.
There is no search engine, no scraper, no browser, and no vector database here. It is intentionally plain. The goal is to show how evidence gets selected before the report is written.
The publish step asks the model to write from selected packets only.
def publish(client, topic, packets):
"""Ask the model to summarize selected source packets with citations."""
source_text = "\n\n".join("[" + item["id"] + "] " + item["title"] + "\n" + item["text"] for item in packets)
example_id = packets[0]["id"]
prompt = (
"Write a cautious educational research summary using only these fictional source packets.\n"
"Do not add outside facts. Do not give professional advice.\n"
"Return only valid JSON. The citations value must be a JSON array of selected source IDs.\n"
"Example shape: "
+ '{"verdict":"PARTIAL","summary":"Short cited summary using [' + example_id + '].","citations":["' + example_id + '"],"limits":"Short limits note."}'
+ "\nTopic: "
+ topic
+ "\n\nSources:\n"
+ source_text
)
return extract_json(call_model(client, prompt))
What This Code Is Actually Doing
This is the writing desk. The selected packets are placed inside the prompt, and the model is told to write only from those packets.
The prompt asks for JSON, which is a structured format software can parse. In the real test, both providers still failed this contract in the publish step. That failure became part of the lesson: model output needs validation, not trust.
The review step checks the report.
def review_report(report, packets):
"""Validate report verdict and source citations."""
allowed_ids = [item["id"] for item in packets]
if not isinstance(report, dict):
cited = ", ".join(allowed_ids)
titles = ", ".join(item["title"] for item in packets)
return {"verdict": "PARTIAL", "summary": "The selected fictional packets provide related evidence from " + titles + ". The model response did not match the requested JSON shape, so this local fallback summary records only that evidence was selected.", "citations": allowed_ids, "limits": "Fallback summary used because provider JSON formatting was not usable. Cited fictional packets: " + cited + "."}
What This Code Is Actually Doing
This is the review manager. If the model does not return a usable report, the script does not crash and it does not pretend the report was clean.
Instead, it creates a cautious fallback. It says evidence was selected, names the fictional packets, and marks the verdict as PARTIAL. That gives the reader a useful record without overstating what happened.
The final loop runs every scenario.
def run():
"""Run all fictional scenarios through the micro research pipeline."""
load_env()
check_env()
client = make_client()
for name, question in SCENARIOS:
allowed, reason = safe_question(question)
if not allowed:
show(name, [], [], {"verdict": "NEEDS_REVIEW", "summary": reason, "limits": reason})
continue
questions = plan(client, question)
packets = gather(questions)
if not packets:
show(name, questions, [], {"verdict": "NEEDS_REVIEW", "summary": "No matching fictional source packets were found.", "limits": "The topic needs better scoped evidence before summary."})
continue
report = review_report(publish(client, question, packets), packets)
show(name, questions, packets, report)
What This Code Is Actually Doing
This is the assembly line. Each scenario moves through the same sequence: load settings, check the question, plan, gather, publish, review, and print.
The continue lines act like exit ramps. If the question is not suitable or the evidence is missing, the workflow stops that scenario and moves on. That is how the script avoids forcing every question into a report.
Errors I Hit During This Build
The first issue was dependency creep. The first draft added python-dotenv, but this series keeps requirements minimal and uses LiteLLM’s dependency stack. I removed the extra direct dependency and kept only litellm==1.83.7.
The second issue was syntax checking on Windows. A normal Python compile command tried to create a __pycache__ folder and hit an access error. I switched to a no-write syntax parse using Python’s ast module, which checked the file without creating cache files.
The third issue was JSON formatting. Gemini and DeepSeek both produced useful plans, but the publish report did not match the requested JSON shape. I added a local fallback summary so the output still records the selected fictional evidence instead of collapsing into an empty failure.
The fourth issue was the broad-topic scenario. The first version allowed the broad question to reach the evidence selection step. I moved that into local validation so the question returns NEEDS_REVIEW before the model tries to publish from weak evidence.
Why This Matters
Research agents are going to become normal business infrastructure. They will summarize markets, policies, vendors, regulations, customer feedback, internal documents, and competitive landscapes.
The danger is that a confident report can hide a weak evidence trail. This build makes the evidence trail visible. It shows the plan, the selected packets, the verdict, and the limits.
That is the architecture lesson. The future of research agents is not just longer reports. It is reviewable research workflows.
Tools Used
- GPT Researcher (Apache 2.0): https://github.com/assafelovic/gpt-researcher
- LiteLLM (MIT): https://github.com/BerriAI/litellm
- OpenAI Python SDK (Apache 2.0): https://github.com/openai/openai-python
- NosisTech Agent Engineering (MIT): https://github.com/nosistech/agent-engineering
(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.