Chapter 27 · Capstone 2: Multi-Source Research Assistant Team
Learning objectives: Build a "multi-source retrieval → collaborative writing → line-by-line verification" research pipeline — Researcher A handles public web search, Researcher B handles local document library retrieval, the Writer synthesizes a report, the Fact-checker verifies each key claim and annotates confidence levels, and the final output is a
research-report.mdwith cited sources.
27.1 Requirements and Team Design
The hard part of writing a credible research report is not "writing" but breadth of source material and credibility of claims. We split these two concerns across different roles:
Research topic: "The 2025 landscape of edge AI inference frameworks"
│
▼
┌──────────────────────────────────────────────┐
│ Research Team (coordinate mode) │
│ │
│ Leader: decompose sub-tasks, dispatch, synthesize │
│ ├── Researcher A: DuckDuckGo web search │
│ │ (news, blogs, official announcements) │
│ └── Researcher B: local Knowledge retrieval │
│ (arXiv paper abstract library, LanceDb)│
└──────────────────────────────────────────────┘
│ Material pack (with source annotations)
▼
Writer Agent ──▶ Report draft
│
▼
Fact-checker Agent ──▶ Annotate confidence for each key claim
│
▼
research-report.md (with cited-source list)Why use Team (coordinate) rather than a linear Workflow?
- The two researchers' retrievals are independent; in coordinate mode the leader delegates concurrently, so total time ≈ the slowest member; a linear Workflow can only wait serially;
- Retrieval quality is unpredictable — the leader needs to decide dynamically whether to dispatch more (e.g., "public results are too sparse; have B search the paper library again") — this dynamism is exactly what coordinate mode excels at;
- The latter half "write → verify" has strong sequential dependency — putting it inside a Team would be overkill, so we split it into two independent Agents wired in series.
Install dependencies:
pip install agno duckduckgo-search "agno[lancedb]" pypdf python-dotenv27.2 Preparing the Model and the Local Paper Knowledge Base
Use DeepSeek uniformly as the underlying model; Researcher B检索的是预先灌入 LanceDb 的 arXiv 论文摘要库 (retrieves from an arXiv paper abstract library pre-loaded into LanceDb):
# research_common.py —— Shared infrastructure for the team
import os
from agno.knowledge.knowledge import Knowledge
from agno.knowledge.embedder.openai import OpenAIEmbedder
from agno.models.openai import OpenAIChat
from agno.vectordb.lancedb import LanceDb, SearchType
def make_model() -> OpenAIChat:
"""All roles share the same third-party model config for easy model swaps."""
return OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
temperature=0.3, # Research scenarios demand restraint; lower divergence
)
# Researcher B's ammunition: arXiv paper abstract vector library
paper_kb = Knowledge(
vector_db=LanceDb(
table_name="arxiv_abstracts",
uri="tmp/lancedb",
search_type=SearchType.vector,
embedder=OpenAIEmbedder(
id="Qwen/Qwen3-Embedding-8B",
api_key=os.getenv("EMBEDDING_API_KEY"),
base_url="https://api.siliconflow.cn/v1",
),
)
)
# Insert local paper PDF directory on first run (skip_if_exists ensures idempotency)
# paper_kb.insert(path="docs/papers/", skip_if_exists=True)Where do materials come from?
insert(path="docs/papers/") auto-detects PDFs in the directory, chunks them, and indexes them. No papers handy? Drop any domain materials in there — this chapter demonstrates retrieval capability, not the data itself.
27.3 Two Researchers: Complementary Tools
Researcher A uses DuckDuckGo to search the public web; Researcher B mounts Knowledge and lets the model decide when to retrieve. Both unify their output format to 「发现 + 来源」(finding + source), making downstream referencing straightforward:
# researchers.py —— Two non-overlapping retrieval channels
from agno.agent import Agent
from agno.tools.duckduckgo import DuckDuckGoTools
from research_common import make_model, paper_kb
researcher_web = Agent(
id="researcher-web",
name="Researcher A - Web",
role="Collect latest updates, product launches, and industry news via search engines.",
model=make_model(),
tools=[DuckDuckGoTools(news=True, fixed_max_results=8)],
instructions=[
"Run 2–3 searches with different keywords around the topic; breadth over depth.",
"Every finding must include a source URL and approximate date.",
"Distinguish 'official announcements' from 'opinion pieces' and label accordingly.",
"Output a numbered list at the end; each line formatted as: [Finding] —— [Source URL] —— [Date]",
],
markdown=True,
)
researcher_local = Agent(
id="researcher-local",
name="Researcher B - Literature",
role="Retrieve academic perspectives and technical comparisons from the local paper knowledge base.",
model=make_model(),
knowledge=paper_kb,
search_knowledge=True, # Model decides when to trigger retrieval on its own
instructions=[
"Execute at least 2 knowledge-base retrievals from different angles (e.g., by tech stack, by benchmark).",
"When citing a paper, note the paper title or filename as the source.",
"If the knowledge base has nothing relevant, say so explicitly — do not fabricate claims.",
"Output a numbered list at the end; each line formatted as: [Finding] —— [Paper/Source Name]",
],
markdown=True,
)Note that both members' instructions enforce structured output — this is the prerequisite for the downstream verification step to locate claims line-by-line.
27.4 Research Team: coordinate Mode Assembly
# research_team.py
from agno.team import Team, TeamMode
from researchers import researcher_web, researcher_local
from research_common import make_model
research_team = Team(
id="research-team",
name="Research Team",
mode=TeamMode.coordinate,
model=make_model(),
members=[researcher_web, researcher_local],
instructions=[
"On receiving the topic, decompose it into 'web dynamics' and 'literature perspectives' sub-tasks.",
"Delegate sub-tasks in parallel to the corresponding researchers; wait for both returns.",
"If either side produces fewer than 3 items, dispatch an additional round with more specific keywords.",
"When synthesizing, deduplicate, merge similar viewpoints, and form a structured material pack.",
"Preserve original source annotations for each item in the material pack.",
],
markdown=True,
)
if __name__ == "__main__":
research_team.print_response(
"The 2025 landscape of edge AI inference frameworks: mainstream solutions, performance comparisons, and trends",
stream_intermediate_steps=True, # Stream-print each member's intermediate output for observation
)stream_intermediate_steps=True streams the leader's dispatch decisions and each member's output to stdout — highly recommended to enable on the first run; you'll see the coordinate delegation process in real time.
27.5 Writer and Fact-checker
The Writer only consumes the material pack to write the report; the checker receives the draft report and verifies every key claim line-by-line before producing the revised version:
# writer.py —— Writer: consumes material pack only
from agno.agent import Agent
from research_common import make_model
writer = Agent(
id="report-writer",
name="Writer",
role="Write a structured research report based on the material pack.",
model=make_model(),
instructions=[
"Report structure: Executive Summary / Landscape Overview / Key Comparisons / Trend Assessment.",
"After each key claim, annotate its supporting sources with [S1][S2]… numbers.",
"Present opposing viewpoints faithfully; do not flatten disagreements for narrative flow.",
"Append a 'References' section at the end listing all numbered URLs or paper names.",
],
markdown=True,
)The checker receives the draft report and verifies every key claim line-by-line:
# checker.py —— Fact-checker: line-by-line review with confidence annotation
from agno.agent import Agent
from research_common import make_model
fact_checker = Agent(
id="fact-checker",
name="Fact-checker",
role="Verify every key claim in the report line-by-line and annotate confidence.",
model=make_model(),
instructions=[
"Extract all claims marked with [Sn] numbers; verify each one:",
"Does the claim exceed what the source material supports? Do numbers/dates/names match?",
"For each claim output: ✅ Supported / ⚠️ Partially supported / ❌ Unsubstantiated, with a one-sentence reason.",
"Give an overall high/medium/low confidence verdict.",
"Output the revised complete report: delete or rewrite unsubstantiated claims as 'pending verification',",
"and append a 'Verification Log' section at the end.",
],
markdown=True,
)The checker's three-state output spec (✅/⚠️/❌) is far more actionable than a blanket "verified" — low-confidence claims can be handled separately before delivery.
27.6 Wiring the Full Pipeline and Writing to Disk
# run_research.py —— Entry script
import datetime
from pathlib import Path
from research_team import research_team
from writer_and_checker import writer, fact_checker
TOPIC = "The 2025 landscape of edge AI inference frameworks"
# Leg 1: Team multi-source retrieval → material pack
material_pack = research_team.run(f"Research topic: {TOPIC}").content
# Leg 2: Draft the report
draft = writer.run(
f"Research topic: {TOPIC}\n\nBelow is the research material pack:\n{material_pack}"
).content
# Leg 3: Verify line-by-line and revise
final = fact_checker.run(
f"Please verify and revise the following research report draft:\n\n{draft}"
).content
# Write to disk: date-stamped archive for traceability
out_dir = Path("reports"); out_dir.mkdir(exist_ok=True)
today = datetime.date.today().isoformat()
report_path = out_dir / f"research-report-{today}.md"
report_path.write_text(final, encoding="utf-8")
print(f"Report generated: {report_path}")Open reports/research-report-<date>.md after the run: [S1][S2] numbers in the body map to entries in the References section; the Verification Log appendix tells you which claims were downgraded to "pending verification."
Cost note
This pipeline runs entirely on deepseek-chat; the dominant token cost comes from the Team's multi-turn coordination and the long material injection. If budget-sensitive, swap the checker for a smaller model — verification tasks demand less reasoning power than writing.
Chapter Summary
- Team selection: independent parallel retrievals →
TeamMode.coordinate(concurrent delegation, dynamic re-dispatch); strongly sequential tail → independent Agents in series; - Structured intermediates: researchers output a unified 「finding + source」format, the key enabler for downstream writer/checker to wire automatically;
- Separation of concerns: Writer focuses on expression; checker focuses on verification — two perspectives on the same draft are far more reliable than one Agent self-checking;
- Confidence tiering: the ✅/⚠️/❌ three-state check log makes delivery quality quantitatively manageable.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Why are the two researchers' retrieval tasks better suited to Team (coordinate) than a linear Workflow?
2. The primary purpose of enforcing a "[Finding] —— [Source]" output format in researchers' instructions is?
3. Compared to a flat "verified" reply, what is the advantage of the fact-checker outputting ✅/⚠️/❌ three states per claim?
4. Regarding cost optimization for this pipeline, which approach is most reasonable?
🛠️ Hands-On Exercises
- Give Researcher A an RSS subscription reader tool (e.g.,
agno.tools.rss.RssTools); compare the report's freshness with and without it. - Hook the checker's output into a new Agent "revision executor" that only rewrites the report per the checker's意见, fully separating "verify" from "revise."
- Add cost instrumentation to the pipeline: print model usage before and after each of the three stages, identify the token-heavy环节, and attempt optimizations.
Next chapter pivots from "research" back to "service": put the customer-service knowledge-base Agent onto the production line — Chapter 28: Customer-Support Knowledge Base Agent on AgentOS.