Appearance
🔎 Web Research Agent:让 Agent 帮你做深度调研,输出带引用的报告
📌 这篇适合谁
- 每天花大量时间搜索、阅读、整理信息的知识工作者
- 想做一个"真正有用"的 Agent 项目但不想做 ChatBot 的开发者
- 学完 ReAct + Tool Use 想挑战"多工具协作"场景的 Lv2 毕业生
- 对 Deep Research / Perplexity 类产品好奇、想自己实现一个的人
这是案例库「个人项目」系列的第四篇。
我是🍋AI小柠檬。这个 Agent 是我日常写文章时的"研究助手"——给它一个主题,它自动搜索、阅读、交叉验证、输出带引用来源的报告。每周帮我省 3-5 小时的调研时间。
写在前面:为什么"搜索+阅读+总结"是 Agent 的杀手级场景 🎯
⚠️ 我之前的调研流程
写一篇关于"LangGraph vs CrewAI"的对比文章,我之前的流程:
- Google 搜索 "LangGraph vs CrewAI 2026"(5 分钟)
- 打开前 10 个链接,逐个阅读(40 分钟)
- 发现有些文章过时了,再搜一轮(10 分钟)
- 把关键信息复制到笔记里(20 分钟)
- 整理成结构化对比(30 分钟)
- 回头找每个观点的出处链接(15 分钟)
总计 2 小时。 其中真正需要"我的判断力"的只有第 5 步。其余全是机械操作。
现在:给 Agent 一句话"对比 LangGraph 和 CrewAI 的优劣势,面向生产环境",10 分钟出报告,我花 15 分钟审核修改。从 2 小时压缩到 25 分钟。
1️⃣ 这个 Agent 的能力边界 📦
✅ 能做的
| 能力 | 说明 |
|---|---|
| 多源搜索 | 同时搜 Google、学术论文、GitHub |
| 深度阅读 | 读取网页全文,不是只看摘要 |
| 交叉验证 | 多个来源说同一件事才采信 |
| 带引用输出 | 每个观点标注来源 URL |
| 追问深挖 | 第一轮搜索不够,自动生成新查询继续搜 |
❌ 不能做的
- 不能访问付费墙内容(WSJ、知网等)
- 不能保证 100% 准确(仍需人工审核)
- 不适合实时性极强的信息(股价、突发新闻)
2️⃣ 技术架构:ReAct + 四个工具 🏗
💡 核心设计
一个 ReAct Agent + 四个工具(搜索、阅读、笔记、输出)。Agent 自主决定搜几轮、读哪些页面、什么时候信息够了可以写报告。
架构图
用户输入: "对比 LangGraph 和 CrewAI"
↓
┌─────────────────────────────────────┐
│ ReAct Agent │
│ (System Prompt: 研究分析师) │
│ │
│ Tool 1: web_search() │ ← 搜索引擎查询
│ Tool 2: read_page() │ ← 读取网页全文
│ Tool 3: take_note() │ ← 记录关键信息+来源
│ Tool 4: write_report() │ ← 生成最终报告
└─────────────────────────────────────┘
↓
输出: Markdown 报告(带 [来源] 引用)为什么需要 take_note 工具
这是很多人做 Research Agent 时忽略的关键设计。如果 Agent 搜索了 10 个网页,所有内容都堆在 Context 里,会:
- Token 爆炸:10 个网页 × 3000 字 = 30000 字,一轮对话就烧完预算
- 信息淹没:LLM 在超长 Context 里找关键信息的能力会下降(Lost in the Middle 问题)
take_note 的作用:Agent 每读完一个网页,提取关键信息存到"笔记本"里,然后原文可以丢掉。最终写报告时只看笔记本,不看原文。
3️⃣ 完整代码 💻
工具定义
python
# tools.py
import json
import httpx
from langchain_core.tools import tool
# === 搜索工具 ===
@tool
def web_search(query: str, num_results: int = 5) -> str:
"""搜索互联网,返回相关网页的标题、URL 和摘要。
用于获取某个主题的初始信息来源。"""
# 方案 A:Tavily(专为 Agent 设计的搜索 API,有免费额度)
# 方案 B:SerpAPI(Google 搜索代理)
# 方案 C:Bing Search API
# 使用 Tavily 示例
from tavily import TavilyClient
client = TavilyClient(api_key="YOUR_TAVILY_KEY")
results = client.search(
query=query,
max_results=num_results,
include_raw_content=False, # 只要摘要,不要全文
)
formatted = []
for r in results["results"]:
formatted.append({
"title": r["title"],
"url": r["url"],
"snippet": r["content"][:200],
})
return json.dumps(formatted, ensure_ascii=False)
# === 网页阅读工具 ===
@tool
def read_page(url: str) -> str:
"""读取指定 URL 的网页全文内容。
返回清洗后的纯文本(去除导航、广告等干扰内容)。
内容会被截断到 4000 字以控制 Token 消耗。"""
# 方案 A:Jina Reader API(免费,效果最好)
# 方案 B:自己用 BeautifulSoup 解析
# 方案 C:Firecrawl(付费,支持 JS 渲染)
# 使用 Jina Reader(免费,无需 API Key)
reader_url = f"https://r.jina.ai/{url}"
headers = {"Accept": "text/plain"}
try:
resp = httpx.get(reader_url, headers=headers, timeout=15, follow_redirects=True)
if resp.status_code != 200:
return f"无法读取该页面(HTTP {resp.status_code})"
content = resp.text
# 截断到 4000 字(约 2000 Token)
if len(content) > 4000:
content = content[:4000] + "\n\n[... 内容已截断 ...]"
return content
except Exception as e:
return f"读取失败:{str(e)}"
# === 笔记工具 ===
_notebook: list[dict] = [] # 全局笔记本
@tool
def take_note(content: str, source_url: str, relevance: str = "high") -> str:
"""将一条研究发现记录到笔记本。
每条笔记包含:内容摘要、来源 URL、相关度。
最终写报告时会基于笔记本内容生成。"""
note = {
"id": len(_notebook) + 1,
"content": content,
"source": source_url,
"relevance": relevance,
}
_notebook.append(note)
return f"✅ 已记录笔记 #{note['id']}(当前共 {len(_notebook)} 条笔记)"
@tool
def write_report(topic: str, style: str = "对比分析") -> str:
"""基于笔记本中的所有笔记,生成最终的研究报告。
报告格式为 Markdown,每个观点带 [来源] 引用。
在收集了足够信息后调用此工具。"""
if not _notebook:
return "❌ 笔记本为空,请先搜索和阅读网页,用 take_note 记录关键信息"
# 整理笔记供 LLM 生成报告
notes_text = "\n".join([
f"[{n['id']}] {n['content']} (来源: {n['source']})"
for n in _notebook
])
return f"请基于以下 {len(_notebook)} 条笔记生成{style}报告:\n\n{notes_text}"Agent 组装
python
# agent.py
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from tools import web_search, read_page, take_note, write_report
SYSTEM_PROMPT = """你是一个专业的研究分析师。你的任务是针对用户给定的主题,进行深度网络调研,输出带引用的研究报告。
## 工作流程
1. **搜索阶段**:用 web_search 搜索 2-3 个不同角度的查询词
2. **阅读阶段**:从搜索结果中选择最相关的 3-5 个页面,用 read_page 阅读
3. **记录阶段**:每读完一个页面,用 take_note 记录关键发现和来源
4. **验证阶段**:如果某个观点只有一个来源,再搜一轮验证
5. **输出阶段**:笔记够了之后,用 write_report 生成最终报告
## 搜索策略
- 第一轮:搜索主题的核心关键词
- 第二轮:搜索主题的对比/评价/缺点
- 第三轮(可选):搜索最新动态/更新
## 质量标准
- 每个核心观点至少有 2 个独立来源支撑
- 不要只看一篇文章就下结论
- 区分"事实"和"观点"——事实需要引用,观点需要标注是谁的观点
- 如果信息有冲突,两边都记录,在报告里说明分歧
## 输出格式
最终报告用 Markdown 格式,结构:
- 一句话结论
- 详细分析(分点)
- 每个观点后标注 [来源N]
- 末尾附完整引用列表
"""
tools = [web_search, read_page, take_note, write_report]
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
agent = create_react_agent(llm, tools, prompt=SYSTEM_PROMPT)
def research(topic: str) -> str:
"""执行一次完整的研究任务"""
global _notebook
_notebook = [] # 清空笔记本
result = agent.invoke({
"messages": [("user", f"请深度调研以下主题并输出报告:{topic}")]
})
return result["messages"][-1].content运行示例
python
report = research("LangGraph vs CrewAI:2026 年生产环境该选哪个")
print(report)输出:
markdown
# LangGraph vs CrewAI:2026 年生产环境选型报告
## 一句话结论
LangGraph 适合需要精细控制流程的生产系统,CrewAI 适合快速搭建角色协作的原型。
生产环境首选 LangGraph。
## 详细对比
### 1. 架构理念
- **LangGraph**:声明式状态图,开发者定义节点和边,完全控制执行流程 [来源1][来源3]
- **CrewAI**:角色扮演范式,定义 Agent 角色和任务,框架自动编排 [来源2][来源4]
### 2. 生产就绪度
- LangGraph 内置 Checkpointer(持久化)、Interrupt(人工介入)、流式输出 [来源1]
- CrewAI 的生产特性较少,缺乏原生的状态持久化和断点恢复 [来源3][来源5]
### 3. 学习曲线
- LangGraph 概念较多(State/Node/Edge/Conditional),上手需要 2-3 天 [来源1]
- CrewAI 上手极快,定义角色和任务即可运行,30 分钟出 Demo [来源2]
...
## 引用
- [来源1] LangGraph 官方文档 - https://langchain-ai.github.io/langgraph/
- [来源2] CrewAI 官方文档 - https://docs.crewai.com/
- [来源3] "LangGraph in Production" - https://blog.langchain.dev/...
- [来源4] "CrewAI vs LangGraph" - https://medium.com/...
- [来源5] Reddit r/LangChain 讨论 - https://reddit.com/...4️⃣ 搜索 API 选型 📡
📊 三大搜索 API 对比
| API | 价格 | 特点 | 适合场景 |
|---|---|---|---|
| Tavily | 免费 1000 次/月,$20/月起 | 专为 AI Agent 设计,返回结构化结果 | ⭐ Agent 首选 |
| SerpAPI | $50/月 5000 次 | Google 搜索代理,结果最全 | 需要 Google 级搜索质量 |
| Bing Search API | 免费 1000 次/月 | 微软出品,Azure 生态 | 已有 Azure 账号 |
我的选择:Tavily。原因:
- 免费额度够个人用(1000 次/月)
- 返回的结果已经做了相关性排序,比原始 Google 结果更适合 Agent
- 有
include_raw_content选项,搜索和阅读一步完成
5️⃣ 关键设计决策 🧠
决策 1:搜几轮?
python
# 不要写死"搜 3 轮"——让 Agent 自己判断
# 在 System Prompt 里写:
# "如果前两轮搜索已经覆盖了主题的主要方面,不需要第三轮"
# "如果发现信息有冲突或空白,追加搜索"经验值:大多数主题 2 轮搜索就够。第一轮搜核心关键词,第二轮搜对立观点/最新动态。超过 3 轮通常是 Agent 在"打转"。
决策 2:读几个页面?
Token 成本计算:
每个页面 ≈ 4000 字 ≈ 2000 Token(输入)
GPT-4o 输入价格:$2.5 / 1M Token
读 5 个页面:5 × 2000 = 10000 Token = $0.025
加上 Agent 推理的输出 Token:约 $0.05/次调研结论:读 3-5 个页面是性价比最高的。超过 5 个页面边际收益递减。
决策 3:怎么处理信息冲突?
python
# 在 take_note 里加 confidence 字段
@tool
def take_note(content: str, source_url: str, confidence: str = "high") -> str:
"""
confidence 取值:
- high: 多个来源一致,或来自权威来源(官方文档、论文)
- medium: 单一来源,但来源可信
- low: 单一来源,或来源可信度不明
"""
...Agent 写报告时,low confidence 的信息会被标注"待验证"。
6️⃣ 生产化补丁 🔧
| 补丁 | 问题 | 解决方案 |
|---|---|---|
| 缓存 | 同一个 URL 重复读取浪费钱 | 用 URL 做 key 缓存 read_page 结果,TTL 24 小时 |
| 并发读取 | 串行读 5 个页面太慢 | 用 asyncio.gather 并发读取 |
| Token 预算 | 单次调研不能无限烧钱 | 设 max_steps=15,超过就强制输出当前笔记 |
| 域名白名单 | 搜索结果可能包含垃圾站 | 优先读取 github.com、官方文档、arxiv.org |
| 结果持久化 | 报告生成后想回看 | 自动保存到本地 Markdown 文件 |
缓存实现
python
import hashlib
import json
from pathlib import Path
from datetime import datetime, timedelta
CACHE_DIR = Path(".cache/pages")
CACHE_DIR.mkdir(parents=True, exist_ok=True)
CACHE_TTL = timedelta(hours=24)
def get_cached_page(url: str) -> str | None:
"""检查缓存"""
key = hashlib.md5(url.encode()).hexdigest()
cache_file = CACHE_DIR / f"{key}.json"
if cache_file.exists():
data = json.loads(cache_file.read_text())
cached_time = datetime.fromisoformat(data["timestamp"])
if datetime.now() - cached_time < CACHE_TTL:
return data["content"]
return None
def cache_page(url: str, content: str):
"""写入缓存"""
key = hashlib.md5(url.encode()).hexdigest()
cache_file = CACHE_DIR / f"{key}.json"
cache_file.write_text(json.dumps({
"url": url,
"content": content,
"timestamp": datetime.now().isoformat(),
}, ensure_ascii=False))并发读取
python
import asyncio
import httpx
async def read_pages_concurrent(urls: list[str]) -> list[str]:
"""并发读取多个网页"""
async with httpx.AsyncClient(timeout=15) as client:
tasks = [
client.get(f"https://r.jina.ai/{url}", headers={"Accept": "text/plain"})
for url in urls
]
responses = await asyncio.gather(*tasks, return_exceptions=True)
results = []
for resp in responses:
if isinstance(resp, Exception):
results.append(f"读取失败:{str(resp)}")
elif resp.status_code == 200:
results.append(resp.text[:4000])
else:
results.append(f"HTTP {resp.status_code}")
return results7️⃣ 和 Perplexity / Deep Research 的区别 🤔
| 维度 | Perplexity | 你的 Research Agent |
|---|---|---|
| 搜索深度 | 1 轮搜索 | 2-3 轮,可追问 |
| 阅读深度 | 摘要级 | 全文阅读 |
| 可定制性 | 不可定制 | 完全可控(Prompt/工具/策略) |
| 成本 | $20/月 | ~$0.05/次(按需付费) |
| 输出格式 | 固定 | 自定义(对比表/报告/PPT 大纲) |
| 数据隐私 | 数据经过第三方 | 本地运行,数据不出境 |
你的 Agent 的独特价值:可以针对你的领域定制搜索策略。比如我做 AI Agent 调研时,会在 Prompt 里写"优先搜索 GitHub、LangChain 博客、Anthropic 文档"——这是 Perplexity 做不到的。
8️⃣ 常见坑 🚨
| 坑 | 症状 | 修复 |
|---|---|---|
| Agent 搜索打转 | 反复搜同一个关键词 | Prompt 里加"不要重复搜索已搜过的关键词" + 传入搜索历史 |
| 网页内容太长 | Token 爆炸 / 超时 | read_page 截断到 4000 字 |
| 搜索结果全是广告 | 报告质量差 | 加域名白名单过滤 |
| 引用链接失效 | 报告里的链接打不开 | 缓存原文 + 在报告里标注"访问日期" |
| Agent 过早写报告 | 只搜了一轮就输出 | Prompt 里加"至少搜索 2 轮、记录 5 条笔记后才能写报告" |
| 信息过时 | 引用了 2023 年的文章 | 搜索时加年份限制"2025 OR 2026" |
🎓 总结
🎯 一句话总结
Web Research Agent = ReAct + 搜索/阅读/笔记/报告四个工具。核心设计是"笔记本模式"——读完就记、记完就丢原文,避免 Token 爆炸。搜索用 Tavily(免费额度够用),阅读用 Jina Reader(免费无限制)。每次调研成本约 $0.05,比 Perplexity 便宜且可定制。
💡 自测题
- 为什么需要 take_note 工具?不用它直接让 Agent 记住所有网页内容会怎样?
- 搜索策略为什么要分多轮?每轮搜什么?
- 怎么处理多个来源信息冲突的情况?
- 和 Perplexity 相比,自建 Research Agent 的核心优势是什么?
- 单次调研的 Token 成本大约是多少?怎么控制?
答不上来的,回头再读一遍对应章节 ✍️
📌 转载声明
本文为🍋AI小柠檬原创。 转载请注明来源,付费产品包内禁止转载。