- Research Agent מלא -- סוכן שמקבל שאלת מחקר ומייצר דוח מקיף עם מקורות, ניתוח, ומסקנות
- 4+ כלי חיפוש עובדים -- web search, academic search, news search, social media search
- Parallel search pipeline -- LangGraph fan-out שמריץ חיפושים מקביליים ומאחד תוצאות
- Analysis agent -- סוכן שמחלץ claims, מזהה סתירות, ונותן confidence scores
- Report generator -- מייצר דוחות בפורמט מובנה עם citations ו-executive summary
- Fact-checking layer -- שכבת אימות שתופסת 80%+ מ-hallucinated claims
- Iterative research -- מנגנון deep-dive שמזהה gaps ומבצע חיפושים ממוקדים נוספים
- 3 תבניות מחקר -- market research, competitor analysis, technology assessment
- REST API + scheduling -- פריסה עם דוחות אוטומטיים יומיים/שבועיים
- תוכלו לתכנן ארכיטקטורת research agent עם orchestrator, search agents, analysis agent, ו-writing agent
- תוכלו לבנות כלי חיפוש ל-4+ מקורות (web, academic, news, social) ולשלב אותם ב-pipeline אחד
- תוכלו לממש parallel search pipeline ב-LangGraph עם fan-out, result normalization, ו-de-duplication
- תוכלו לנתח תוצאות מחקר -- claim extraction, cross-referencing, contradiction detection, confidence scoring
- תוכלו לייצר דוחות מקצועיים עם executive summary, findings, analysis, recommendations, ו-citations
- פרקים קודמים: פרק 2 (Architecture), פרק 8 (LangGraph), פרק 11 (Tool Use Mastery), פרק 13 (Multi-Agent), פרק 14 (Safety & Guardrails), פרק 15 (Build: Support Agent)
- מה תצטרכו: Python 3.11+ ו/או Node.js 18+, מפתח API (Anthropic / OpenAI), עורך קוד,
pip install langgraph langchain-anthropic langchain-community tavily-python - מפתחות API: Tavily API key (free tier: 1,000 queries/month), Brave Search API key (optional), NewsAPI key (optional)
- ידע נדרש: LangGraph states ו-graphs, tool calling, multi-agent patterns, structured output
- זמן משוער: 5-7 שעות (כולל תרגילים)
- עלות API משוערת: $15-30 (LLM calls + search API calls)
בפרק 15 בניתם Customer Support Agent -- סוכן עם router, specialists, RAG, escalation, ו-monitoring. עכשיו אתם עוברים לפרויקט השני: Research & Analysis Agent. הפעם הדגש הוא על parallel execution (חיפושים מקביליים ממקורות שונים), data synthesis (איחוד והצלבת מידע), ו-structured output (דוחות מקצועיים). הטכניקות שתלמדו כאן -- fan-out patterns, result normalization, iterative deepening -- ישמשו אתכם בכל סוכן שמטפל בנתונים ממקורות מרובים. בפרק 17 תשתמשו בחלק מהכלים האלה כדי לבנות Marketing Automation Agent שמנטר מתחרים ומייצר תוכן.
| מונח (English) | עברית | הסבר |
|---|---|---|
| Fan-Out Pattern | תבנית פיזור | שליחת אותה משימה למספר סוכנים/כלים במקביל ואיסוף כל התוצאות. מאיץ חיפוש פי 4-5 |
| Fan-In | איסוף/איחוד | השלב שבו כל התוצאות מהחיפושים המקבילים מתאחדות למבנה נתונים אחד |
| Result Normalization | נורמליזציית תוצאות | המרת תוצאות ממקורות שונים לפורמט אחיד (title, url, snippet, date, source) |
| De-duplication | הסרת כפילויות | זיהוי והסרה של תוצאות זהות שהגיעו ממקורות שונים -- URL matching + semantic similarity |
| Claim Extraction | חילוץ טענות | זיהוי טענות ועובדות ספציפיות מתוך טקסט -- "המכירות עלו ב-30%" ולא "היה שיפור" |
| Cross-Referencing | הצלבת מקורות | בדיקה האם טענה מסוימת מופיעה ביותר ממקור אחד -- אישוש או סתירה |
| Confidence Score | ציון ביטחון | מדד 0-1 שמבטא כמה אפשר לסמוך על ממצא. מבוסס על מספר מקורות, עדכניות, ואמינות |
| Citation | ציטוט/הפניה | קישור מהטקסט למקור המקורי -- [1], [2] -- שמאפשר לקורא לאמת את המידע |
| Executive Summary | תקציר מנהלים | פסקה קצרה בתחילת הדוח שמסכמת את הממצאים העיקריים -- מיועדת למי שאין לו זמן לקרוא הכל |
| Iterative Deepening | העמקה איטרטיבית | תבנית שבה מחקר ראשוני חושף gaps --> נוצרות שאלות המשך --> חיפוש נוסף --> ניתוח מעודכן |
| Tavily | -- | Search API שתוכנן ספציפית לסוכני AI. מחזיר תוכן מנוקה, snippets רלוונטיים, ו-scores |
| Brave Search API | -- | API חיפוש אינטרנט עצמאי שלא תלוי ב-Google. מחזיר web, news, ו-video results |
| Semantic Scholar | -- | מנוע חיפוש אקדמי חינמי של AI2. גישה ל-200M+ מאמרים אקדמיים עם metadata עשיר |
| Research Template | תבנית מחקר | הגדרה מראש של סוג המחקר, המקורות, מבנה הדוח, והדגשים -- מותאם ל-use case ספציפי |
| Firecrawl | -- | שירות web scraping שמותאם ל-LLMs. הופך דפי אינטרנט ל-Markdown נקי שקל לעבד |
| Hallucination Detection | זיהוי הזיות | בדיקה האם הסוכן המציא מידע שלא מופיע במקורות -- dual-model verification |
סקירת הפרויקט וארכיטקטורה
מה אנחנו בונים
בפרק הזה תבנו Research & Analysis Agent -- סוכן שמקבל שאלת מחקר ומחזיר דוח מקיף. לא דוח שטחי שמבוסס על מקור אחד. דוח אמיתי -- עם מקורות מרובים, הצלבת מידע, זיהוי סתירות, fact-checking, ו-citations.
הנה מה שהסוכן עושה:
| שלב | מה קורה | סוכן אחראי | זמן טיפוסי |
|---|---|---|---|
| 1. קליטת שאלה | המשתמש שולח שאלת מחקר בטקסט חופשי | Orchestrator | 1-2 שניות |
| 2. ניתוח ופירוק | פירוק השאלה לתת-שאלות וזיהוי מקורות רלוונטיים | Orchestrator | 3-5 שניות |
| 3. חיפוש מקבילי | 4+ חיפושים במקביל -- web, academic, news, social | Search Agents (parallel) | 5-15 שניות |
| 4. ניתוח וסינתזה | חילוץ claims, הצלבה, זיהוי סתירות, confidence scoring | Analysis Agent | 10-20 שניות |
| 5. Fact-checking | אימות טענות מרכזיות מול מקורות | Fact-Checker Agent | 5-15 שניות |
| 6. כתיבת דוח | המרת הניתוח לדוח מובנה עם executive summary ו-citations | Writing Agent | 15-30 שניות |
| 7. Deep dive (אופציונלי) | זיהוי gaps --> חיפוש נוסף --> עדכון הדוח | Orchestrator + Search | 30-60 שניות |
סך הכל: 1-3 דקות לדוח מקיף. אנליסט אנושי לוקח שעות עד ימים לאותו תוצר.
הארכיטקטורה
הארכיטקטורה משתמשת ב-multi-agent pattern עם orchestrator מרכזי:
┌─────────────────┐
│ Orchestrator │
│ (LangGraph) │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────────▼──┐ ┌──────▼───┐ ┌──────▼──────┐
│ Search │ │ Search │ │ Search │
│ Agent 1 │ │ Agent 2 │ │ Agent 3 │
│ (Web) │ │ (News) │ │ (Academic) │
└────────┬──┘ └──────┬───┘ └──────┬──────┘
│ │ │
└──────────────┼──────────────┘
│
┌────────▼────────┐
│ Analysis Agent │
│ (Synthesis) │
└────────┬────────┘
│
┌────────▼────────┐
│ Fact-Checker │
│ Agent │
└────────┬────────┘
│
┌────────▼────────┐
│ Writing Agent │
│ (Report Gen) │
└─────────────────┘
Tech Stack
| רכיב | טכנולוגיה | למה |
|---|---|---|
| Orchestration | LangGraph | Fan-out/fan-in pattern, conditional routing, state management |
| LLM (Analysis) | Claude Sonnet 4.6 | איזון מצוין בין יכולת ועלות לניתוח |
| LLM (Writing) | Claude Sonnet 4.6 / Opus 4.6 | כתיבה איכותית לדוחות, Opus לדוחות מעמיקים |
| LLM (Routing) | Claude Haiku | מהיר וזול לסיווג וparsing |
| Web Search | Tavily API | תוכנן ל-AI agents, מחזיר תוכן נקי |
| Academic Search | Semantic Scholar API | 200M+ מאמרים, חינמי, API פשוט |
| News Search | Brave Search API / NewsAPI | חדשות עדכניות ממקורות מגוונים |
| Web Scraping | Firecrawl / Playwright | חילוץ תוכן מלא מדפים שה-search API לא מחזיר |
| TS Alternative | Vercel AI SDK + LangGraph.js | אותו pattern ב-TypeScript |
לפני שמתחילים לבנות, הכינו את הסביבה:
- צרו תיקיית פרויקט:
mkdir research-agent && cd research-agent - התקינו dependencies:
pip install langgraph langchain-anthropic langchain-community tavily-python httpx pydantic - צרו קובץ
.envעם:ANTHROPIC_API_KEY,TAVILY_API_KEY - אמתו שהכל עובד:
python -c "from tavily import TavilyClient; print('OK')"
Search Tools -- כלי חיפוש ואיסוף מידע
הבסיס של כל research agent הוא כלי חיפוש איכותיים. אנחנו צריכים כלים שמדברים עם APIs שונים ומחזירים תוצאות בפורמט אחיד. ההבדל בין סוכן מחקר טוב לבינוני הוא לא ב-LLM -- הוא באיכות ובמגוון של כלי החיפוש.
פורמט תוצאות אחיד -- The Normalized Result
לפני שבונים את הכלים, נגדיר את הפורמט שכולם צריכים להחזיר:
# Python -- models.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum
class SourceType(str, Enum):
WEB = "web"
ACADEMIC = "academic"
NEWS = "news"
SOCIAL = "social"
DOCUMENT = "document"
class SearchResult(BaseModel):
"""Normalized search result -- same format from every source."""
title: str
url: str
snippet: str = Field(description="Relevant excerpt, 100-300 chars")
full_content: Optional[str] = Field(default=None, description="Full text if available")
source_type: SourceType
source_name: str = Field(description="e.g., 'Tavily', 'Semantic Scholar'")
published_date: Optional[datetime] = None
relevance_score: float = Field(ge=0.0, le=1.0, description="0-1 relevance")
author: Optional[str] = None
metadata: dict = Field(default_factory=dict)
class SearchResults(BaseModel):
"""Collection of results from a single search source."""
query: str
source_type: SourceType
results: list[SearchResult]
total_found: int
search_time_seconds: float
כלי 1: Web Search עם Tavily
Tavily הוא ה-search API המוביל לסוכני AI. הוא לא רק מחפש -- הוא מחזיר תוכן נקי שמתאים לעיבוד על ידי LLM. ההבדל מ-Google Search API הוא משמעותי: Tavily מסיר פרסומות, navigation, ו-boilerplate ומחזיר רק את התוכן הרלוונטי.
# Python -- tools/web_search.py
import time
from tavily import TavilyClient
from models import SearchResult, SearchResults, SourceType
class WebSearchTool:
"""Web search using Tavily API -- optimized for AI agents."""
def __init__(self, api_key: str, max_results: int = 10):
self.client = TavilyClient(api_key=api_key)
self.max_results = max_results
def search(self, query: str, search_depth: str = "advanced") -> SearchResults:
"""
Search the web for a query.
search_depth: "basic" (fast, cheaper) or "advanced" (thorough, better for research)
"""
start = time.time()
response = self.client.search(
query=query,
search_depth=search_depth,
max_results=self.max_results,
include_raw_content=True, # Get full page content
include_answer=False, # We do our own analysis
)
results = []
for r in response.get("results", []):
results.append(SearchResult(
title=r.get("title", ""),
url=r.get("url", ""),
snippet=r.get("content", "")[:300],
full_content=r.get("raw_content"),
source_type=SourceType.WEB,
source_name="Tavily",
relevance_score=r.get("score", 0.5),
))
return SearchResults(
query=query,
source_type=SourceType.WEB,
results=results,
total_found=len(results),
search_time_seconds=round(time.time() - start, 2),
)
כלי 2: Academic Search עם Semantic Scholar
Semantic Scholar (של AI2) נותן גישה חינמית ל-200M+ מאמרים אקדמיים. ה-API מחזיר metadata עשיר: citation count, abstract, authors, year -- מצוין למחקר שדורש מקורות אקדמיים.
# Python -- tools/academic_search.py
import httpx
import time
from models import SearchResult, SearchResults, SourceType
class AcademicSearchTool:
"""Academic paper search using Semantic Scholar API (free, no key needed)."""
BASE_URL = "https://api.semanticscholar.org/graph/v1"
def __init__(self, max_results: int = 10):
self.max_results = max_results
def search(self, query: str) -> SearchResults:
start = time.time()
response = httpx.get(
f"{self.BASE_URL}/paper/search",
params={
"query": query,
"limit": self.max_results,
"fields": "title,abstract,url,year,authors,citationCount,externalIds",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
results = []
for paper in data.get("data", []):
citation_count = paper.get("citationCount", 0)
# Higher citations = higher relevance (simple heuristic)
relevance = min(1.0, citation_count / 100) if citation_count else 0.3
authors = ", ".join(
a.get("name", "") for a in (paper.get("authors") or [])[:3]
)
results.append(SearchResult(
title=paper.get("title", ""),
url=paper.get("url") or f"https://api.semanticscholar.org/paper/{paper.get('paperId', '')}",
snippet=paper.get("abstract", "")[:300] if paper.get("abstract") else "",
source_type=SourceType.ACADEMIC,
source_name="Semantic Scholar",
relevance_score=relevance,
author=authors,
metadata={
"citation_count": citation_count,
"year": paper.get("year"),
"paper_id": paper.get("paperId"),
},
))
return SearchResults(
query=query,
source_type=SourceType.ACADEMIC,
results=results,
total_found=data.get("total", len(results)),
search_time_seconds=round(time.time() - start, 2),
)
כלי 3: News Search
# Python -- tools/news_search.py
import httpx
import time
from datetime import datetime
from models import SearchResult, SearchResults, SourceType
class NewsSearchTool:
"""News search using Brave Search API."""
def __init__(self, api_key: str, max_results: int = 10):
self.api_key = api_key
self.max_results = max_results
def search(self, query: str, freshness: str = "pw") -> SearchResults:
"""
freshness: pd (past day), pw (past week), pm (past month), py (past year)
"""
start = time.time()
response = httpx.get(
"https://api.search.brave.com/res/v1/news/search",
headers={"X-Subscription-Token": self.api_key},
params={
"q": query,
"count": self.max_results,
"freshness": freshness,
},
timeout=30,
)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("results", []):
pub_date = None
if item.get("age"):
# Brave returns relative age, we approximate
pub_date = datetime.now()
results.append(SearchResult(
title=item.get("title", ""),
url=item.get("url", ""),
snippet=item.get("description", "")[:300],
source_type=SourceType.NEWS,
source_name=f"Brave News ({item.get('meta_url', {}).get('hostname', 'unknown')})",
relevance_score=0.7, # News is generally relevant if returned
published_date=pub_date,
metadata={"age": item.get("age", "unknown")},
))
return SearchResults(
query=query,
source_type=SourceType.NEWS,
results=results,
total_found=len(results),
search_time_seconds=round(time.time() - start, 2),
)
כלי 4: Social/Community Search
# Python -- tools/social_search.py
import httpx
import time
from models import SearchResult, SearchResults, SourceType
class SocialSearchTool:
"""Search Reddit and Hacker News for community discussions."""
def search(self, query: str, max_results: int = 10) -> SearchResults:
"""Search Reddit via its public JSON API."""
start = time.time()
results = []
# Reddit search (no API key needed for public search)
try:
response = httpx.get(
"https://www.reddit.com/search.json",
params={"q": query, "limit": max_results, "sort": "relevance", "t": "year"},
headers={"User-Agent": "ResearchAgent/1.0"},
timeout=15,
)
response.raise_for_status()
data = response.json()
for post in data.get("data", {}).get("children", []):
d = post.get("data", {})
results.append(SearchResult(
title=d.get("title", ""),
url=f"https://reddit.com{d.get('permalink', '')}",
snippet=d.get("selftext", "")[:300] or d.get("title", ""),
source_type=SourceType.SOCIAL,
source_name=f"Reddit r/{d.get('subreddit', 'unknown')}",
relevance_score=min(1.0, d.get("score", 0) / 500),
author=d.get("author"),
metadata={
"subreddit": d.get("subreddit"),
"score": d.get("score", 0),
"num_comments": d.get("num_comments", 0),
},
))
except httpx.HTTPError:
pass # Reddit can be flaky, continue without
return SearchResults(
query=query,
source_type=SourceType.SOCIAL,
results=results,
total_found=len(results),
search_time_seconds=round(time.time() - start, 2),
)
הריצו כל אחד מ-4 הכלים עם שאילתה אחת ובדקו שאתם מקבלים תוצאות:
# Test all 4 search tools
import os
from tools.web_search import WebSearchTool
from tools.academic_search import AcademicSearchTool
from tools.news_search import NewsSearchTool
from tools.social_search import SocialSearchTool
query = "AI agents in production 2026"
web = WebSearchTool(os.environ["TAVILY_API_KEY"])
print(f"Web: {web.search(query).total_found} results")
academic = AcademicSearchTool()
print(f"Academic: {academic.search(query).total_found} results")
# Brave requires API key -- skip if not available
if os.environ.get("BRAVE_API_KEY"):
news = NewsSearchTool(os.environ["BRAVE_API_KEY"])
print(f"News: {news.search(query).total_found} results")
social = SocialSearchTool()
print(f"Social: {social.search(query).total_found} results")
מצופה: תוצאות מכל מקור. אם כלי אחד נכשל -- תבדקו API key ו-rate limits.
Tavily: Free tier = 1,000 queries/month. Advanced search = ~$0.01/query. בדיקת API limit לפני שמריצים 50 חיפושים בלולאה.
Semantic Scholar: חינמי אבל rate limited ל-~100 requests/5 minutes. הוסיפו time.sleep(0.5) בין קריאות.
Brave Search: Free tier = 2,000 queries/month. מספיק לפיתוח ובדיקות.
Reddit: ה-JSON API הציבורי מוגבל ב-rate. לפרודקשן -- השתמשו ב-Reddit API הרשמי עם OAuth.
Parallel Research Pipeline -- חיפוש מקבילי ב-LangGraph
הנה הבעיה: אם מריצים 4 חיפושים אחד אחרי השני, כל אחד לוקח 3-5 שניות, מקבלים 12-20 שניות. עם fan-out pattern ב-LangGraph, כולם רצים במקביל ומסיימים ב-5 שניות. זה 4x speedup עם שורות קוד בודדות.
The Fan-Out / Fan-In Pattern
כל research pipeline מורכב מ-5 שלבים קבועים. אפשר להשתמש בתבנית הזו לכל סוג מחקר:
| שלב | Pattern | מה קורה | כלל אצבע |
|---|---|---|---|
| 1. Query Planning | Single agent | פירוק שאלה לתת-שאלות, בחירת מקורות | LLM זול ומהיר (Haiku/Flash) |
| 2. Parallel Search | Fan-out | כל מקור מחפש במקביל | timeout 15 שניות, fail gracefully |
| 3. Normalize & Dedupe | Fan-in | איחוד תוצאות, הסרת כפילויות | URL matching + semantic similarity |
| 4. Analyze | Single agent | חילוץ claims, הצלבה, confidence | LLM חזק (Sonnet/GPT-5) |
| 5. Report | Single agent | כתיבת דוח מובנה עם citations | LLM חזק, structured output |
כלל 80/20: 80% מזמן הריצה הוא בשלב 2 (חיפוש). לכן fan-out הוא הoptimization הכי חשוב.
State Definition
# Python -- graph/state.py
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from models import SearchResults
def merge_search_results(existing: list, new: list) -> list:
"""Merge search results, avoiding duplicates by URL."""
seen_urls = {r.url for result_set in existing for r in result_set.results}
merged = list(existing)
for result_set in new:
filtered_results = [r for r in result_set.results if r.url not in seen_urls]
for r in filtered_results:
seen_urls.add(r.url)
merged.append(SearchResults(
query=result_set.query,
source_type=result_set.source_type,
results=filtered_results,
total_found=len(filtered_results),
search_time_seconds=result_set.search_time_seconds,
))
return merged
class ResearchState(TypedDict):
"""State for the research pipeline."""
# Input
research_question: str
research_depth: int # 1-3, how many iterations
# Query planning
sub_queries: list[str]
selected_sources: list[str]
# Search results (fan-in with dedup)
search_results: Annotated[list[SearchResults], merge_search_results]
# Analysis
findings: list[dict] # extracted claims with confidence
contradictions: list[dict] # conflicting claims
gaps: list[str] # identified knowledge gaps
# Fact-checking
verified_findings: list[dict]
flagged_claims: list[dict] # claims that failed verification
# Report
report_markdown: str
report_metadata: dict
# Control
current_iteration: int
messages: Annotated[list, add_messages]
Building the Graph
# Python -- graph/research_graph.py
import os
from langgraph.graph import StateGraph, START, END
from langchain_anthropic import ChatAnthropic
from graph.state import ResearchState
from tools.web_search import WebSearchTool
from tools.academic_search import AcademicSearchTool
from tools.news_search import NewsSearchTool
from tools.social_search import SocialSearchTool
# Initialize LLMs
planner_llm = ChatAnthropic(model="claude-haiku-3.5", temperature=0)
analysis_llm = ChatAnthropic(model="claude-sonnet-4-6-20260325", temperature=0)
writer_llm = ChatAnthropic(model="claude-sonnet-4-6-20260325", temperature=0.3)
# Initialize search tools
web_search = WebSearchTool(os.environ["TAVILY_API_KEY"])
academic_search = AcademicSearchTool()
news_search = NewsSearchTool(os.environ.get("BRAVE_API_KEY", ""))
social_search = SocialSearchTool()
# ---- Node Functions ----
def plan_research(state: ResearchState) -> dict:
"""Break down the research question into sub-queries and select sources."""
response = planner_llm.invoke([
{"role": "system", "content": """You are a research planner.
Given a research question, output:
1. 3-5 sub-queries that together cover the topic comprehensively
2. Which sources to search (web, academic, news, social)
Respond in JSON: {"sub_queries": [...], "sources": [...]}"""},
{"role": "user", "content": state["research_question"]},
])
import json
plan = json.loads(response.content)
return {
"sub_queries": plan["sub_queries"],
"selected_sources": plan["sources"],
}
def search_web(state: ResearchState) -> dict:
"""Search the web for all sub-queries."""
all_results = []
for query in state["sub_queries"]:
results = web_search.search(query)
all_results.append(results)
return {"search_results": all_results}
def search_academic(state: ResearchState) -> dict:
"""Search academic papers for all sub-queries."""
all_results = []
for query in state["sub_queries"]:
results = academic_search.search(query)
all_results.append(results)
return {"search_results": all_results}
def search_news(state: ResearchState) -> dict:
"""Search news for all sub-queries."""
all_results = []
for query in state["sub_queries"]:
results = news_search.search(query)
all_results.append(results)
return {"search_results": all_results}
def search_social(state: ResearchState) -> dict:
"""Search social media for all sub-queries."""
all_results = []
for query in state["sub_queries"]:
results = social_search.search(query)
all_results.append(results)
return {"search_results": all_results}
def should_search_source(source: str):
"""Create a conditional function for source selection."""
def check(state: ResearchState) -> bool:
return source in state.get("selected_sources", [])
return check
# ---- Build the Graph ----
def build_research_graph():
graph = StateGraph(ResearchState)
# Add nodes
graph.add_node("plan", plan_research)
graph.add_node("search_web", search_web)
graph.add_node("search_academic", search_academic)
graph.add_node("search_news", search_news)
graph.add_node("search_social", search_social)
graph.add_node("analyze", analyze_results) # defined in next section
graph.add_node("fact_check", fact_check_findings) # defined later
graph.add_node("write_report", generate_report) # defined later
# Edges: START --> plan
graph.add_edge(START, "plan")
# Fan-out: plan --> all search nodes (parallel)
graph.add_conditional_edges(
"plan",
# Route to selected sources
lambda state: state.get("selected_sources", ["web"]),
{
"web": "search_web",
"academic": "search_academic",
"news": "search_news",
"social": "search_social",
},
)
# Fan-in: all searches --> analyze
graph.add_edge("search_web", "analyze")
graph.add_edge("search_academic", "analyze")
graph.add_edge("search_news", "analyze")
graph.add_edge("search_social", "analyze")
# analyze --> fact_check --> write_report --> END
graph.add_edge("analyze", "fact_check")
graph.add_edge("fact_check", "write_report")
graph.add_edge("write_report", END)
return graph.compile()
TypeScript Alternative -- Vercel AI SDK
// TypeScript -- research-agent.ts
import { generateText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
// Define search tools for the Vercel AI SDK
const webSearchTool = tool({
description: "Search the web using Tavily API",
parameters: z.object({
query: z.string().describe("Search query"),
depth: z.enum(["basic", "advanced"]).default("advanced"),
}),
execute: async ({ query, depth }) => {
const response = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.TAVILY_API_KEY,
query,
search_depth: depth,
max_results: 10,
include_raw_content: true,
}),
});
const data = await response.json();
return data.results.map((r: any) => ({
title: r.title,
url: r.url,
snippet: r.content?.slice(0, 300),
source: "tavily",
score: r.score,
}));
},
});
const academicSearchTool = tool({
description: "Search academic papers via Semantic Scholar",
parameters: z.object({
query: z.string().describe("Academic search query"),
}),
execute: async ({ query }) => {
const url = new URL("https://api.semanticscholar.org/graph/v1/paper/search");
url.searchParams.set("query", query);
url.searchParams.set("limit", "10");
url.searchParams.set("fields", "title,abstract,url,year,citationCount,authors");
const response = await fetch(url.toString());
const data = await response.json();
return (data.data || []).map((p: any) => ({
title: p.title,
url: p.url || `https://api.semanticscholar.org/paper/${p.paperId}`,
snippet: p.abstract?.slice(0, 300) || "",
source: "semantic_scholar",
citations: p.citationCount,
year: p.year,
}));
},
});
// Run the research agent
async function runResearch(question: string) {
// Step 1: Plan the research
const planResult = await generateText({
model: anthropic("claude-haiku-3.5"),
system: `You are a research planner. Break the question into 3-5 sub-queries.
Return JSON: {"sub_queries": [...], "sources": ["web", "academic"]}`,
prompt: question,
});
const plan = JSON.parse(planResult.text);
// Step 2: Run searches in parallel
const searchPromises = plan.sub_queries.flatMap((q: string) => [
webSearchTool.execute({ query: q, depth: "advanced" }, {} as any),
academicSearchTool.execute({ query: q }, {} as any),
]);
const allResults = await Promise.all(searchPromises);
const flatResults = allResults.flat();
// Step 3: Analyze and write report
const { text: report } = await generateText({
model: anthropic("claude-sonnet-4-6-20260325"),
system: `You are a research analyst. Analyze the search results and write
a comprehensive report with: Executive Summary, Key Findings, Analysis,
Recommendations, and Sources. Use inline citations [1], [2], etc.`,
prompt: `Research question: ${question}
Search results:
${JSON.stringify(flatResults, null, 2)}
Write a comprehensive research report in Markdown.`,
});
return report;
}
הריצו את ה-graph עם שאלת מחקר אמיתית ובדקו שאתם מקבלים תוצאות מכל המקורות:
graph = build_research_graph()
result = graph.invoke({
"research_question": "What are the most effective AI agent architectures in production in 2026?",
"research_depth": 1,
"current_iteration": 0,
})
# Check results
for sr in result["search_results"]:
print(f"{sr.source_type}: {sr.total_found} results ({sr.search_time_seconds}s)")
שימו לב: כל החיפושים רצו במקביל. הזמן הכולל צריך להיות קרוב לחיפוש הארוך ביותר, לא סכום כולם.
De-duplication -- הסרת כפילויות
כשמחפשים ב-4 מקורות שונים, אותו מאמר או אתר יכול להופיע פעמיים ושלוש. de-duplication מבוסס על שתי שכבות:
# Python -- utils/dedup.py
from urllib.parse import urlparse
from models import SearchResult
def deduplicate_results(results: list[SearchResult]) -> list[SearchResult]:
"""Remove duplicate results using URL matching + title similarity."""
seen_urls = set()
seen_titles = set()
unique = []
for result in sorted(results, key=lambda r: r.relevance_score, reverse=True):
# Normalize URL (remove query params, trailing slashes)
parsed = urlparse(result.url)
normalized_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}".rstrip("/")
# Normalize title (lowercase, strip whitespace)
normalized_title = result.title.lower().strip()
# Skip if URL or title already seen
if normalized_url in seen_urls:
continue
if normalized_title in seen_titles and len(normalized_title) > 20:
continue
seen_urls.add(normalized_url)
seen_titles.add(normalized_title)
unique.append(result)
return unique
Analysis & Synthesis -- ניתוח וסינתזה
חיפוש הוא 30% מהעבודה. ה-70% הקשים הם ניתוח: מה המידע הזה אומר? מה עקבי ומה סותר? מה אמין ומה מפוקפק? כאן הסוכן הופך מ"מחפש" ל-"אנליסט".
Claim Extraction
הצעד הראשון הוא חילוץ טענות ספציפיות מהתוצאות. לא "המצב השתפר" -- אלא "מכירות SaaS עלו ב-23% ברבעון השלישי של 2025" עם ציון המקור.
# Python -- agents/analysis_agent.py
from pydantic import BaseModel, Field
from langchain_anthropic import ChatAnthropic
class Claim(BaseModel):
"""A specific, verifiable claim extracted from search results."""
statement: str = Field(description="The specific claim or fact")
source_urls: list[str] = Field(description="URLs where this claim was found")
source_count: int = Field(description="Number of independent sources")
confidence: float = Field(ge=0.0, le=1.0, description="Confidence in this claim")
category: str = Field(description="e.g., 'statistic', 'trend', 'opinion', 'fact'")
date_relevance: str = Field(description="How current: 'current', 'recent', 'dated'")
class Contradiction(BaseModel):
"""Two claims that contradict each other."""
claim_a: str
claim_b: str
source_a: str
source_b: str
explanation: str = Field(description="Why these claims contradict")
class AnalysisResult(BaseModel):
"""Structured output from the analysis agent."""
key_findings: list[Claim]
contradictions: list[Contradiction]
gaps: list[str] = Field(description="Topics that need more research")
overall_confidence: float = Field(description="0-1 confidence in the analysis")
summary: str = Field(description="2-3 sentence summary of findings")
def analyze_results(state: dict) -> dict:
"""Analyze all search results -- extract claims, find contradictions, identify gaps."""
analysis_llm = ChatAnthropic(
model="claude-sonnet-4-6-20260325",
temperature=0,
)
# Flatten all results into a single context
all_snippets = []
for result_set in state["search_results"]:
for r in result_set.results:
all_snippets.append(
f"[{r.source_name}] {r.title}\n{r.snippet}\nURL: {r.url}"
)
context = "\n\n---\n\n".join(all_snippets[:30]) # Limit to top 30 results
structured_llm = analysis_llm.with_structured_output(AnalysisResult)
analysis = structured_llm.invoke([
{"role": "system", "content": """You are a research analyst.
Your job is to analyze search results and extract:
1. KEY FINDINGS: Specific, verifiable claims with source URLs and confidence scores
- confidence 0.9+: claim appears in 3+ reliable sources
- confidence 0.7-0.9: claim appears in 2 sources or 1 very reliable source
- confidence 0.5-0.7: claim appears in 1 source, seems plausible
- confidence <0.5: unverified, uncertain, or from unreliable source
2. CONTRADICTIONS: Claims that conflict with each other
3. GAPS: Topics that need more research (not enough data)
Be specific. "Revenue grew" is bad. "Revenue grew 23% YoY to $4.2B" is good.
Always cite source URLs."""},
{"role": "user", "content": f"""Research question: {state['research_question']}
Search results:
{context}
Analyze these results thoroughly."""},
])
return {
"findings": [f.model_dump() for f in analysis.key_findings],
"contradictions": [c.model_dump() for c in analysis.contradictions],
"gaps": analysis.gaps,
}
הריצו את ה-analysis agent על תוצאות החיפוש שלכם ובדקו:
- כמה claims חולצו? (מצופה: 8-15 לחיפוש טוב)
- האם ה-claims ספציפיים? (מספרים, תאריכים, שמות -- לא כלליות)
- האם ה-confidence scores הגיוניים? (claims עם 3 מקורות צריכים להיות 0.9+)
- האם זוהו contradictions? (אם יש -- זה סימן שהניתוח עובד טוב)
Confidence Scoring Matrix
ציון הביטחון (confidence) מבוסס על 4 גורמים. כל אחד תורם לציון הסופי:
| גורם | משקל | ציון גבוה (0.8+) | ציון נמוך (<0.5) |
|---|---|---|---|
| מספר מקורות | 35% | 3+ מקורות עצמאיים מאשרים | מקור יחיד, לא מאומת |
| אמינות מקור | 25% | אקדמי, ממשלתי, מוביל בתעשייה | בלוג אנונימי, פורום, מקור לא ידוע |
| עדכניות | 25% | 6 חודשים אחרונים | מעל שנתיים, מידע מיושן |
| ספציפיות | 15% | מספרים, תאריכים, שמות ספציפיים | טענות כלליות בלי evidence |
כלל אצבע: confidence מתחת ל-0.5 = לא לכלול בדוח כ"ממצא" אלא כ"טענה לא מאומתת". confidence מעל 0.8 = ניתן להציג כעובדה מאומתת.
כש-LLM נותן confidence score של 0.85, זה לא באמת אומר 85% סיכוי שזה נכון. LLMs ידועים כ-overconfident -- הם נוטים לתת ציונים גבוהים מדי. השתמשו ב-scores כ-heuristic יחסי (מה יותר אמין ממה), לא כ-probability מדויק. fact-checking בשלב הבא הוא חובה.
Report Generation -- הפקת דוחות
ניתוח טוב בלי דוח קריא הוא כמו נתונים בלי dashboard. ה-Writing Agent לוקח את הממצאים, הסתירות, וה-gaps ומייצר דוח מקצועי שמנהל יכול לקרוא ולקבל ממנו החלטות.
מבנה הדוח
כל דוח מכיל את הסעיפים הבאים:
| סעיף | אורך מומלץ | מטרה |
|---|---|---|
| Executive Summary | 2-3 פסקאות | TL;DR -- מה גילינו, מה ההמלצה |
| Methodology | 1 פסקה | אילו מקורות חיפשנו, כמה תוצאות |
| Key Findings | 5-10 bullets | ממצאים עיקריים עם citations |
| Detailed Analysis | 3-5 פסקאות | ניתוח מעמיק, trends, patterns |
| Contradictions & Uncertainties | 1-2 פסקאות | מה סותר, מה לא ברור |
| Recommendations | 3-5 bullets | מה לעשות עם המידע הזה |
| Sources | רשימה | כל המקורות עם קישורים |
The Report Generator
# Python -- agents/report_writer.py
from langchain_anthropic import ChatAnthropic
def generate_report(state: dict) -> dict:
"""Generate a structured research report from analysis results."""
writer_llm = ChatAnthropic(
model="claude-sonnet-4-6-20260325",
temperature=0.3, # Slight creativity for better prose
max_tokens=4096,
)
# Build source list for citations
all_sources = []
source_idx = 1
source_map = {} # url -> citation number
for result_set in state["search_results"]:
for r in result_set.results:
if r.url not in source_map:
source_map[r.url] = source_idx
all_sources.append(f"[{source_idx}] {r.title} - {r.url}")
source_idx += 1
findings_text = "\n".join(
f"- {f['statement']} (confidence: {f['confidence']}, sources: {f['source_count']})"
for f in state.get("verified_findings", state.get("findings", []))
)
contradictions_text = "\n".join(
f"- {c['claim_a']} vs {c['claim_b']}: {c['explanation']}"
for c in state.get("contradictions", [])
)
gaps_text = "\n".join(f"- {g}" for g in state.get("gaps", []))
flagged_text = "\n".join(
f"- {c['statement']}: {c.get('flag_reason', 'unverified')}"
for c in state.get("flagged_claims", [])
)
response = writer_llm.invoke([
{"role": "system", "content": """You are a professional research report writer.
Write a comprehensive, well-structured report in Markdown format.
Report structure:
# Research Report: [Topic]
## Executive Summary
(2-3 paragraphs: what was researched, key findings, top recommendation)
## Methodology
(What sources were searched, how many results, what analysis was performed)
## Key Findings
(Bulleted list of main findings with inline citations [1], [2])
## Detailed Analysis
(3-5 paragraphs of deeper analysis, trends, patterns)
## Contradictions & Uncertainties
(What conflicting info was found, what remains unclear)
## Recommendations
(3-5 actionable recommendations based on findings)
## Sources
(Numbered list of all sources)
RULES:
- Use inline citations [1], [2] etc. linking to the sources list
- Be specific: include numbers, dates, names when available
- Clearly mark unverified claims as such
- Write in professional but accessible English
- Flag low-confidence findings explicitly"""},
{"role": "user", "content": f"""Research question: {state['research_question']}
VERIFIED FINDINGS:
{findings_text}
CONTRADICTIONS:
{contradictions_text}
KNOWLEDGE GAPS:
{gaps_text}
FLAGGED/UNVERIFIED CLAIMS:
{flagged_text}
SOURCES:
{chr(10).join(all_sources)}
Write the complete research report."""},
])
return {
"report_markdown": response.content,
"report_metadata": {
"total_sources": len(all_sources),
"total_findings": len(state.get("verified_findings", state.get("findings", []))),
"contradictions_found": len(state.get("contradictions", [])),
"flagged_claims": len(state.get("flagged_claims", [])),
"gaps_identified": len(state.get("gaps", [])),
},
}
TypeScript Report Generator
// TypeScript -- report-writer.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
interface Finding {
statement: string;
confidence: number;
source_count: number;
source_urls: string[];
}
interface ReportInput {
question: string;
findings: Finding[];
contradictions: Array<{ claim_a: string; claim_b: string; explanation: string }>;
gaps: string[];
sources: Array<{ title: string; url: string }>;
}
async function generateReport(input: ReportInput): Promise {
const sourcesText = input.sources
.map((s, i) => `[${i + 1}] ${s.title} - ${s.url}`)
.join("\n");
const findingsText = input.findings
.map((f) => `- ${f.statement} (confidence: ${f.confidence})`)
.join("\n");
const { text } = await generateText({
model: anthropic("claude-sonnet-4-6-20260325"),
temperature: 0.3,
system: `You are a professional research report writer.
Write a comprehensive Markdown report with: Executive Summary, Methodology,
Key Findings (with [1] [2] citations), Detailed Analysis, Contradictions,
Recommendations, and Sources. Be specific and cite everything.`,
prompt: `Question: ${input.question}
Findings:
${findingsText}
Sources:
${sourcesText}
Write the full report.`,
});
return text;
}
הריצו את ה-pipeline המלא -- מחיפוש עד דוח -- ובדקו:
- האם הדוח מכיל את כל הסעיפים? (Executive Summary, Findings, Analysis, Sources)
- האם יש citations [1], [2] שמפנות למקורות אמיתיים?
- האם ה-Executive Summary מסכם נכון את הממצאים העיקריים?
- שמרו את הדוח כקובץ Markdown ופתחו אותו -- האם הוא קריא?
Fact-Checking Layer -- שכבת אימות עובדות
הנה הבעיה הכי גדולה עם research agents: הם ממציאים דברים. LLM יכול לקחת snippet מאתר אחד, "לשדרג" אותו עם מספרים שהמציא, ולהציג את זה כעובדה מאומתת. Fact-checking layer הוא ההבדל בין דוח שאפשר לסמוך עליו לדוח שעלול להזיק.
שלושה סוגי בדיקות
| בדיקה | מה בודקים | איך | דוגמה |
|---|---|---|---|
| Source Verification | האם הטענה באמת מופיעה במקור המצוטט? | השוואת הטענה לתוכן המקור | הדוח כותב "על פי Forbes, המכירות עלו 30%" -- האם Forbes באמת כתב את זה? |
| Cross-Reference Check | האם מקורות אחרים תומכים בטענה? | חיפוש ממוקד לאימות | הטענה שOKR adoption is 70% -- האם יש מקור נוסף? |
| Plausibility Check | האם הטענה הגיונית? | LLM בודק עקביות לוגית | "שוק ה-AI שווה $2T ב-2025" -- גבוה מדי, כנראה שגוי |
The Fact-Checker Agent
# Python -- agents/fact_checker.py
from pydantic import BaseModel, Field
from langchain_anthropic import ChatAnthropic
class FactCheckResult(BaseModel):
"""Result of fact-checking a single claim."""
original_claim: str
verified: bool
confidence_after_check: float = Field(ge=0.0, le=1.0)
flag_reason: str = Field(default="", description="Why this was flagged, if flagged")
corrected_claim: str = Field(default="", description="Corrected version if needed")
verification_method: str = Field(description="How this was verified")
class FactCheckResults(BaseModel):
verified_claims: list[FactCheckResult]
flagged_claims: list[FactCheckResult]
overall_reliability: float = Field(ge=0.0, le=1.0)
def fact_check_findings(state: dict) -> dict:
"""Fact-check all findings using dual-model verification."""
checker_llm = ChatAnthropic(
model="claude-sonnet-4-6-20260325",
temperature=0,
)
findings = state.get("findings", [])
# Build the context from search results for verification
source_context = {}
for result_set in state["search_results"]:
for r in result_set.results:
content = r.full_content or r.snippet
source_context[r.url] = {
"title": r.title,
"content": content[:2000], # Limit per source
}
# Create source context string for the checker
sources_for_checker = "\n\n".join(
f"SOURCE [{url}]:\n{info['title']}\n{info['content']}"
for url, info in list(source_context.items())[:20] # Limit to 20 sources
)
structured_checker = checker_llm.with_structured_output(FactCheckResults)
findings_text = "\n".join(
f"CLAIM {i+1}: {f['statement']} (cited sources: {', '.join(f.get('source_urls', [])[:3])})"
for i, f in enumerate(findings)
)
result = structured_checker.invoke([
{"role": "system", "content": """You are a skeptical fact-checker.
For each claim, verify it against the source material provided.
Verification rules:
1. SOURCE VERIFICATION: Does the cited source actually say this? Look for the specific text.
2. PLAUSIBILITY: Are the numbers/dates/facts plausible? Flag obviously wrong stats.
3. SPECIFICITY: Vague claims without evidence get lower confidence.
4. RECENCY: Old data presented as current gets flagged.
Mark a claim as "verified: true" only if you can confirm it from the source material.
Mark as "verified: false" and explain why if:
- The source doesn't actually say this (hallucination)
- The numbers are implausible
- The claim contradicts other verified information
- The source is unreliable for this type of claim"""},
{"role": "user", "content": f"""CLAIMS TO VERIFY:
{findings_text}
SOURCE MATERIAL:
{sources_for_checker}
Fact-check each claim against the source material."""},
])
return {
"verified_findings": [c.model_dump() for c in result.verified_claims],
"flagged_claims": [c.model_dump() for c in result.flagged_claims],
}
הריצו fact-checking על הממצאים שלכם ובדקו:
- כמה claims אומתו (verified=true) וכמה סומנו (flagged)?
- לclaims שסומנו -- האם ה-flag_reason הגיוני?
- נסו בכוונה להזין claim מומצא (למשל: "According to Gartner, 99% of companies use AI agents") -- האם ה-fact-checker תופס?
Target: ה-fact-checker צריך לתפוס לפחות 80% מ-claims מומצאים. אם הוא תופס פחות -- שפרו את ה-prompt.
LLM fact-checker לא יכול באמת "לקרוא" את המקור המקורי ולאמת ש-Forbes באמת פרסמה את המספר הזה. מה שהוא עושה הוא השוואת הטענה לcontent שה-search tool החזיר. אם ה-search snippet מתאים לטענה -- verified. אם לא -- flagged. זה טוב אבל לא מושלם. לדוחות קריטיים (החלטות השקעה, דוחות משפטיים), תמיד נדרש אימות אנושי.
Iterative Research -- מחקר איטרטיבי
מחקר טוב הוא לעולם לא one-shot. אנליסט אנושי קורא את התוצאות הראשונות, מזהה gaps, ושואל שאלות המשך. Iterative research נותן לסוכן את אותה יכולת: חפש --> נתח --> זהה gaps --> חפש שוב --> עדכן.
The Deep Dive Pattern
# Python -- graph/iterative.py
def identify_gaps(state: dict) -> dict:
"""Identify knowledge gaps and generate follow-up queries."""
gap_llm = ChatAnthropic(model="claude-haiku-3.5", temperature=0)
findings_summary = "\n".join(
f"- {f['statement']} (confidence: {f['confidence']})"
for f in state.get("findings", [])
)
response = gap_llm.invoke([
{"role": "system", "content": """You are a research quality analyst.
Given the current findings for a research question, identify:
1. What important aspects haven't been covered?
2. What claims need more evidence?
3. What follow-up questions would deepen the research?
Return JSON: {
"gaps": ["gap1", "gap2"],
"follow_up_queries": ["query1", "query2", "query3"],
"low_confidence_topics": ["topic that needs more sources"]
}"""},
{"role": "user", "content": f"""Research question: {state['research_question']}
Current findings:
{findings_summary}
Contradictions found: {len(state.get('contradictions', []))}
Sources searched: {sum(len(rs.results) for rs in state['search_results'])}
What gaps exist? What follow-up research is needed?"""},
])
import json
gaps_analysis = json.loads(response.content)
return {
"gaps": gaps_analysis.get("gaps", []),
"sub_queries": gaps_analysis.get("follow_up_queries", []),
"current_iteration": state.get("current_iteration", 0) + 1,
}
def should_continue_research(state: dict) -> str:
"""Decide whether to do another research iteration."""
current = state.get("current_iteration", 0)
max_depth = state.get("research_depth", 1)
if current >= max_depth:
return "write_report"
# Also stop if we have enough high-confidence findings
high_confidence = sum(
1 for f in state.get("findings", [])
if f.get("confidence", 0) >= 0.8
)
if high_confidence >= 10:
return "write_report"
return "search_web" # Do another round of searches
להוספת iterative research ל-graph:
# Add to graph builder
graph.add_node("identify_gaps", identify_gaps)
graph.add_edge("fact_check", "identify_gaps")
graph.add_conditional_edges(
"identify_gaps",
should_continue_research,
{
"search_web": "search_web", # Another iteration
"write_report": "write_report", # Done, generate report
},
)
הריצו את אותה שאלת מחקר פעמיים -- פעם עם research_depth=1 ופעם עם research_depth=2:
- כמה findings נוספים מצא depth=2? (מצופה: 30-50% יותר)
- האם ה-gaps מ-iteration 1 מכוסים ב-iteration 2?
- מה ההבדל בעלות? (depth=2 צריך לעלות פי ~2)
- האם ה-executive summary של depth=2 עשיר יותר?
כלל אצבע: depth=2 מספיק ל-90% מהמקרים. depth=3 שווה רק למחקר מאוד מעמיק. מעבר לזה -- diminishing returns.
Templates -- תבניות מחקר מותאמות
research agent גנרי הוא טוב, אבל research agent עם תבניות מותאמות הוא מצוין. כל סוג מחקר דורש מקורות שונים, שאלות שונות, ומבנה דוח שונה.
שלוש תבניות עיקריות
# Python -- templates/research_templates.py
from dataclasses import dataclass, field
@dataclass
class ResearchTemplate:
"""Template that configures the research agent for specific use cases."""
name: str
description: str
preferred_sources: list[str]
search_depth: str # "basic" or "advanced"
research_depth: int # 1-3 iterations
report_sections: list[str]
analysis_focus: str # What to prioritize in analysis
custom_instructions: str # Domain-specific guidance
# Template 1: Market Research
MARKET_RESEARCH = ResearchTemplate(
name="Market Research",
description="Analyze a market: size, trends, players, opportunities",
preferred_sources=["web", "news", "social"],
search_depth="advanced",
research_depth=2,
report_sections=[
"Executive Summary",
"Market Size & Growth",
"Key Players & Market Share",
"Trends & Drivers",
"Challenges & Risks",
"Opportunities",
"Competitive Landscape",
"Recommendations",
"Sources",
],
analysis_focus="market data, growth rates, competitive dynamics",
custom_instructions="""Focus on:
- Specific market size numbers (TAM, SAM, SOM if available)
- Growth rates (YoY, CAGR)
- Key players and their market share
- Recent funding rounds and M&A activity
- Pricing trends
Always distinguish between global and regional (Israeli) data.""",
)
# Template 2: Competitor Analysis
COMPETITOR_ANALYSIS = ResearchTemplate(
name="Competitor Analysis",
description="Deep dive on a specific competitor or set of competitors",
preferred_sources=["web", "news", "social"],
search_depth="advanced",
research_depth=2,
report_sections=[
"Executive Summary",
"Company Overview",
"Products & Services",
"Pricing & Business Model",
"Strengths & Weaknesses",
"Recent Activity",
"Customer Sentiment",
"Strategic Positioning",
"Recommendations",
"Sources",
],
analysis_focus="competitive positioning, product features, pricing, customer feedback",
custom_instructions="""Focus on:
- Product features and recent launches
- Pricing (tiers, changes)
- Customer reviews and sentiment (G2, Capterra, Reddit)
- Team size and hiring (LinkedIn, job boards)
- Funding and financial health
- Marketing strategy (content, ads, social presence)
Search in Hebrew too for Israeli competitors.""",
)
# Template 3: Technology Assessment
TECHNOLOGY_ASSESSMENT = ResearchTemplate(
name="Technology Assessment",
description="Evaluate a technology: maturity, adoption, pros/cons",
preferred_sources=["web", "academic", "social"],
search_depth="advanced",
research_depth=2,
report_sections=[
"Executive Summary",
"Technology Overview",
"Current State & Maturity",
"Adoption & Use Cases",
"Technical Architecture",
"Pros & Cons",
"Alternatives Comparison",
"Community & Ecosystem",
"Recommendations",
"Sources",
],
analysis_focus="technical capabilities, maturity, adoption rate, community strength",
custom_instructions="""Focus on:
- Technical capabilities and limitations
- Maturity level (experimental, production-ready, mainstream)
- Adoption statistics (GitHub stars, npm downloads, companies using it)
- Performance benchmarks
- Developer experience and documentation quality
- Community activity (GitHub issues, Stack Overflow, Discord)
Prioritize 2025-2026 data. Older benchmarks may be outdated.""",
)
TEMPLATES = {
"market_research": MARKET_RESEARCH,
"competitor_analysis": COMPETITOR_ANALYSIS,
"tech_assessment": TECHNOLOGY_ASSESSMENT,
}
שימוש בתבניות -- Israeli Context
# Python -- example: Israeli market research
result = graph.invoke({
"research_question": "מה גודל שוק ה-SaaS בישראל ב-2026? מי השחקנים המובילים?",
"research_depth": 2,
"current_iteration": 0,
"template": "market_research",
# Template overrides:
"custom_instructions": """
Focus on Israeli SaaS market specifically:
- Search in both Hebrew and English
- Include Israeli sources: Calcalist, Geektime, CTech, Globes
- Look for IVC Research Center data
- Include Israeli-founded companies (headquartered anywhere)
- Note companies that are Israeli but marketed as US companies
""",
})
צרו תבנית מחקר מותאמת לתחום שלכם. לדוגמה:
- SEO analysis -- מקורות: web, social. פוקוס: keywords, rankings, backlinks
- Investment research -- מקורות: news, web. פוקוס: financials, team, product-market fit
- Academic literature review -- מקורות: academic, web. פוקוס: papers, citations, methodology
הגדירו את ResearchTemplate, הריצו חיפוש, ובדקו שהדוח מתאים לצרכים.
Deployment & Automation -- פריסה ואוטומציה
research agent שרץ רק מה-terminal שלכם הוא כלי שימושי אבל מוגבל. בחלק הזה נהפוך אותו לשירות -- REST API שאפשר לקרוא לו מכל מקום, עם scheduling לדוחות אוטומטיים.
REST API עם FastAPI
# Python -- api/main.py
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Optional
import uuid
import asyncio
app = FastAPI(title="Research Agent API")
# In-memory storage (use Redis/DB in production)
research_jobs: dict = {}
class ResearchRequest(BaseModel):
question: str = Field(description="The research question")
template: str = Field(default="market_research", description="Research template")
depth: int = Field(default=1, ge=1, le=3, description="Research depth 1-3")
custom_instructions: Optional[str] = None
class ResearchResponse(BaseModel):
job_id: str
status: str
report: Optional[str] = None
metadata: Optional[dict] = None
async def run_research_background(job_id: str, request: ResearchRequest):
"""Run research in the background."""
research_jobs[job_id]["status"] = "running"
try:
graph = build_research_graph()
result = await asyncio.to_thread(
graph.invoke,
{
"research_question": request.question,
"research_depth": request.depth,
"current_iteration": 0,
},
)
research_jobs[job_id]["status"] = "completed"
research_jobs[job_id]["report"] = result["report_markdown"]
research_jobs[job_id]["metadata"] = result["report_metadata"]
except Exception as e:
research_jobs[job_id]["status"] = "failed"
research_jobs[job_id]["error"] = str(e)
@app.post("/research", response_model=ResearchResponse)
async def start_research(request: ResearchRequest, bg: BackgroundTasks):
"""Start a new research job (runs in background)."""
job_id = str(uuid.uuid4())
research_jobs[job_id] = {"status": "queued"}
bg.add_task(run_research_background, job_id, request)
return ResearchResponse(job_id=job_id, status="queued")
@app.get("/research/{job_id}", response_model=ResearchResponse)
async def get_research(job_id: str):
"""Check status of a research job."""
if job_id not in research_jobs:
return ResearchResponse(job_id=job_id, status="not_found")
job = research_jobs[job_id]
return ResearchResponse(
job_id=job_id,
status=job["status"],
report=job.get("report"),
metadata=job.get("metadata"),
)
Scheduled Research -- דוחות אוטומטיים
# Python -- scheduling/scheduler.py
import schedule
import time
from datetime import datetime
class ResearchScheduler:
"""Schedule recurring research reports."""
def __init__(self, graph, delivery_fn):
self.graph = graph
self.deliver = delivery_fn # Function to send the report (email, Slack, etc.)
def add_daily_digest(self, topic: str, time_str: str = "08:00"):
"""Run research daily and deliver results."""
def job():
print(f"[{datetime.now()}] Running daily research: {topic}")
result = self.graph.invoke({
"research_question": f"{topic} -- latest news and developments today",
"research_depth": 1,
"current_iteration": 0,
})
self.deliver(
subject=f"Daily Research Digest: {topic}",
body=result["report_markdown"],
)
schedule.every().day.at(time_str).do(job)
print(f"Scheduled daily research for '{topic}' at {time_str}")
def add_weekly_report(self, topic: str, day: str = "monday", time_str: str = "09:00"):
"""Run in-depth research weekly."""
def job():
print(f"[{datetime.now()}] Running weekly research: {topic}")
result = self.graph.invoke({
"research_question": f"{topic} -- comprehensive weekly analysis",
"research_depth": 2, # Deeper for weekly
"current_iteration": 0,
})
self.deliver(
subject=f"Weekly Research Report: {topic}",
body=result["report_markdown"],
)
getattr(schedule.every(), day).at(time_str).do(job)
print(f"Scheduled weekly research for '{topic}' on {day} at {time_str}")
def run(self):
"""Start the scheduler loop."""
print("Research scheduler started. Press Ctrl+C to stop.")
while True:
schedule.run_pending()
time.sleep(60)
# Example usage:
# scheduler = ResearchScheduler(graph, send_email)
# scheduler.add_daily_digest("AI agents market", "07:30")
# scheduler.add_weekly_report("Israeli SaaS competitive landscape", "sunday", "09:00")
# scheduler.run()
Slack/Email Delivery
# Python -- delivery/slack_delivery.py
import httpx
def send_to_slack(webhook_url: str, subject: str, body: str):
"""Send research report to Slack channel."""
# Slack has a 40,000 char limit, truncate if needed
if len(body) > 35000:
body = body[:35000] + "\n\n... (report truncated, full version attached)"
httpx.post(webhook_url, json={
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": subject},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": body[:3000]},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "_Full report in thread._"},
},
],
})
Cost Estimation
| סוג דוח | Depth | Search API Calls | LLM Cost | עלות כוללת |
|---|---|---|---|---|
| Quick summary | 1 | 4-8 | $0.20-0.50 | $0.30-0.70 |
| Standard report | 1 | 8-16 | $0.50-1.50 | $0.80-2.00 |
| Deep dive | 2 | 16-32 | $1.50-3.00 | $2.00-5.00 |
| Comprehensive analysis | 3 | 32-48 | $3.00-5.00 | $4.00-8.00 |
לשם השוואה: אנליסט אנושי שעושה את אותו מחקר עולה $50-200 לשעה ולוקח 4-8 שעות. הסוכן עושה את זה ב-$2-5 ו-2-3 דקות. גם אם הסוכן מפספס דברים ודורש review אנושי של 30 דקות -- עדיין חסכתם 80% מהזמן.
- הריצו:
uvicorn api.main:app --reload - שלחו request:
curl -X POST http://localhost:8000/research -H "Content-Type: application/json" -d '{"question": "AI agent frameworks comparison 2026", "depth": 1}' - בדקו סטטוס:
curl http://localhost:8000/research/{job_id} - חכו שהסטטוס יהיה "completed" וקראו את הדוח
כשמריצים מחקר על השוק הישראלי, חשוב לכלול מקורות מקומיים:
- Tech press: Geektime (geektime.co.il), CTech (calcalistech.com), Globes Tech
- מאגרי נתונים: IVC Research Center, Start-Up Nation Central (finder.startupnationcentral.org)
- חדשות כלליות: Calcalist, TheMarker, Walla Tech
- חיפוש בעברית: הוסיפו queries בעברית בנוסף לאנגלית -- הרבה מידע ישראלי לא מתורגם
טיפ: הוסיפו "Israel" OR "Israeli" OR "Tel Aviv" ל-queries כדי לקבל תוצאות רלוונטיות לשוק המקומי.
טעויות נפוצות -- ואיך להימנע מהן
הבעיה: שימוש ב-Tavily בלבד לכל חיפוש. Tavily מצוין אבל הוא רואה רק מה ש-search engine מחזיר -- לא מאמרים אקדמיים, לא דיונים בקהילות, לא חדשות בזמן אמת.
הפתרון: מינימום 3 מקורות שונים. Web + Academic + News / Social. כל מקור רואה "חלק אחר" של המציאות. research agent שמסתמך על מקור אחד הוא בעצם "Google search עם wrapper" -- לא research agent.
הבעיה: הסוכן מייצר דוח שנראה מקצועי אבל מכיל מספרים שהומצאו. LLMs נוטים "לשדרג" נתונים -- לקחת "grew significantly" ולכתוב "grew 47%" בלי שום מקור.
הפתרון: תמיד להריץ fact-checking layer לפני הדוח הסופי. אפילו fact-checker פשוט (השוואת claim ל-source content) תופס 60-80% מ-hallucinations. בלי fact-checking, הדוח שלכם עלול לגרום לנזק.
הבעיה: הגדרת depth=5 כי "יותר מעמיק = יותר טוב." בפועל, אחרי iteration 2-3 אתם מקבלים diminishing returns -- אותם מקורות חוזרים, שאלות ההמשך נהיות דלות, והעלות גדלה ליניארית.
הפתרון: depth=1 לדוחות מהירים, depth=2 לרוב המקרים, depth=3 למחקר מעמיק. הוסיפו early stopping: אם ה-iteration החדש לא מצא findings חדשים עם confidence > 0.7, עצרו.
הבעיה: ייצור דוחות של 20 עמודים כשהמשתמש צריך תשובה מהירה. מנהלים לא קוראים דוחות ארוכים -- הם קוראים executive summary וdive deeper רק אם צריך.
הפתרון: תמיד להתחיל עם Executive Summary של 2-3 פסקאות שנותן את כל מה שצריך. הפירוט אחרי -- למי שרוצה. הוסיפו length parameter: "short" (1 עמוד), "standard" (3-5 עמודים), "detailed" (10+ עמודים).
תרגילים
בנו את ה-research agent המלא מאפס:
- צרו את 4 כלי החיפוש (web, academic, news, social)
- בנו את ה-LangGraph עם fan-out/fan-in
- הוסיפו analysis agent עם claim extraction
- הוסיפו fact-checking layer
- הוסיפו report generator
- הריצו על 3 שאלות מחקר שונות
קריטריונים להצלחה:
- דוח מכיל findings עם citations שמפנות למקורות אמיתיים
- Fact-checker תפס לפחות claim אחד בעייתי
- זמן ריצה < 3 דקות ל-depth=1
- עלות < $3 לדוח
השתמשו ב-research agent לייצר דוח שוק ישראלי:
- בחרו נושא: "AI startups in Israel 2026" / "Israeli SaaS market" / "Cybersecurity in Israel"
- הוסיפו queries בעברית בנוסף לאנגלית
- הגדירו custom_instructions עם מקורות ישראליים (Geektime, Calcalist, IVC)
- הריצו עם depth=2
- בדקו: כמה מקורות ישראליים בדוח? כמה מקורות בעברית?
Target: לפחות 30% מהמקורות צריכים להיות ישראליים/עבריים.
בנו competitor analysis אוטומטי:
- בחרו 3 מתחרים בתחום שלכם
- השתמשו ב-competitor_analysis template
- הריצו analysis לכל אחד
- צרו comparison table -- features, pricing, strengths, weaknesses
- הוסיפו scheduling: weekly competitor update
Bonus: הוסיפו change detection -- ה-scheduler שומר את הדוח הקודם ומדגיש שינויים.
בנו evaluation framework ל-research agent:
- הכינו 10 שאלות מחקר שאתם יודעים את התשובות (ground truth)
- הריצו את הסוכן על כולן
- בדקו לכל דוח: accuracy (האם הממצאים נכונים?), coverage (האם כיסה את כל ההיבטים?), citation quality (האם ה-citations אמיתיות?)
- חשבו ציון ממוצע ל-3 הממדים
- זהו את ה-pattern: איפה הסוכן הכי חלש? שפרו את ה-prompt/tools בהתאם
ברגע שה-research agent עובד, כך תשתמשו בו ביום-יום:
| תדירות | משימה | Template | עלות משוערת |
|---|---|---|---|
| יומי | Daily news digest -- מה חדש בתחום שלי? | Quick summary (depth=1) | $0.30-0.50/יום |
| שבועי | Competitor update -- מה המתחרים עשו השבוע? | Competitor analysis (depth=1) | $1-2/שבוע |
| חודשי | Market deep dive -- trends, opportunities, threats | Market research (depth=2) | $3-5/חודש |
| Ad-hoc | שאלה ספציפית שצריכה מחקר | לפי סוג | $0.50-3 לשאלה |
עלות חודשית כוללת: $20-50. לעומת ציפייה ל-$200-800/חודש עבור אנליסט חיצוני שעושה את אותו עבודה.
אם אתם זוכרים דבר אחד מהפרק הזה: Research agent טוב הוא לא זה שמחפש הכי הרבה -- אלא זה שמאמת הכי טוב. חיפוש לוקח שניות. ניתוח לוקח עוד כמה שניות. אבל fact-checking הוא ההבדל בין דוח שאפשר לסמוך עליו לדוח שיכול לגרום לכם לקבל החלטה שגויה. תמיד תפעילו fact-checking layer -- גם אם זה מוסיף 10 שניות ו-$0.30 לעלות.
- מהו fan-out / fan-in pattern ולמה הוא קריטי ל-research agent? תארו איך הוא עובד ב-LangGraph. (רמז: parallel search, merge results)
- מהם 4 הגורמים ב-Confidence Scoring Matrix? למה ציוני ביטחון של LLM צריכים להילקח כ-heuristic ולא כ-probability? (רמז: source count, reliability, recency, specificity)
- תארו את 3 סוגי הבדיקות ב-fact-checking layer. למה source verification לא מספיק לבד? (רמז: source verification, cross-reference, plausibility)
- מה ההבדל בין depth=1 ל-depth=2 ב-iterative research? מתי כדאי להשתמש ב-depth=3 ומתי זה בזבוז? (רמז: diminishing returns, cost, gap coverage)
- למה כשעושים מחקר שוק ישראלי, חשוב לחפש גם בעברית וגם באנגלית? תנו דוגמה ספציפית. (רמז: local sources, Hebrew-only content)
עברתם 4 מתוך 5? מצוין -- אתם מוכנים לפרק 17.
בפרק הזה בניתם Research & Analysis Agent מלא -- מקלט שאלת מחקר ועד דוח מקצועי עם citations. התחלתם עם ארכיטקטורת multi-agent: orchestrator שמתאם בין search agents, analysis agent, fact-checker, ו-writing agent. בניתם 4 כלי חיפוש (Tavily לweb, Semantic Scholar לacademic, Brave Search לnews, Reddit API לsocial) עם פורמט תוצאות אחיד (SearchResult) שמאפשר למערכת לטפל בכל המקורות באותו אופן. מימשתם fan-out / fan-in pattern ב-LangGraph -- כל החיפושים רצים במקביל ומתאחדים עם de-duplication. בניתם analysis agent שמחלץ claims ספציפיים, מזהה contradictions, ונותן confidence scores מבוססים על 4 גורמים: מספר מקורות, אמינות מקור, עדכניות, וספציפיות. הוספתם fact-checking layer עם source verification, cross-referencing, ו-plausibility checks שתופס 80%+ מclaims מומצאים. מימשתם iterative research -- gap identification ו-follow-up queries שמעמיקים את המחקר אוטומטית. יצרתם 3 תבניות מחקר (market research, competitor analysis, technology assessment) עם התאמה ל-Israeli context ומקורות בעברית. לבסוף, פרסתם את הסוכן כ-REST API עם background jobs ו-scheduling לדוחות אוטומטיים.
הנקודה המרכזית: Research agent טוב לא רק מחפש -- הוא מנתח, מאמת, ומייצר insight. החיפוש הוא 30% מהערך, הניתוח 40%, וה-fact-checking 30%. בלי אימות, יש לכם "Google search עם wrapper." עם אימות, יש לכם אנליסט AI שעובד 24/7 ב-$20-50 לחודש.
בפרק הבא (פרק 17) תשתמשו בחלק מהכלים האלה כדי לבנות Marketing Automation Agent -- סוכן שמנטר מתחרים, מייצר תוכן, מנתח analytics, ומנהל קמפיינים.
צ'קליסט -- סיכום פרק 16
- מבין/ה את ארכיטקטורת ה-research agent -- orchestrator, search agents, analysis, fact-checker, writer
- בנית 4 כלי חיפוש עם פורמט תוצאות אחיד (SearchResult)
- מימשת fan-out / fan-in pattern ב-LangGraph -- חיפושים מקביליים ממקורות שונים
- מימשת de-duplication -- URL matching ו-title similarity
- בנית analysis agent שמחלץ claims עם confidence scores
- מבין/ה את Confidence Scoring Matrix -- 4 גורמים ומשקלים
- מימשת fact-checking layer עם source verification, cross-reference, ו-plausibility
- בנית report generator שמייצר דוחות עם Executive Summary ו-citations
- מימשת iterative research עם gap identification ו-configurable depth
- יצרת 3 תבניות מחקר -- market research, competitor analysis, tech assessment
- יודע/ת להתאים את הסוכן לIsraeli context -- מקורות בעברית, אתרים ישראליים
- פרסת את הסוכן כ-REST API עם FastAPI
- הגדרת scheduling -- דוחות אוטומטיים יומיים/שבועיים
- מבין/ה את מבנה העלויות -- $0.50-5 לדוח, $20-50 לחודש
- בנית Research & Analysis Agent מלא שמייצר דוחות מקיפים, מאומתים, ועם citations