LangGraph+BrightData+PaperSearch的研究助理
摘要:使用LangGraph的ReAct-Agent范式集成了BrightData和PaperSearch的MCP工具,通过搜索和爬取领英和学术网站,实现论文搜索和读取,学者信息提取,邮箱查找等功能。
🚀 前言:我的 AI 助理"精神分裂"了
大家好,我是你们的技术博主。最近在开发 AI Agent 的时候,我遇到了一个很头疼的问题:如何让一个 Agent 既能做"正经事",又能"好好聊天"?
- 当我让它分析论文时,我希望它给我一个结构清晰、字段分明的 JSON 报告。
- 当我让它抓取个人主页时,我也希望得到一份规整的结构化数据。
- 但当我只是想跟它说句"干得不错"时,我希望它能像个正常"人"一样回复我,而不是冷冰冰地甩给我一个空的 JSON 对象。
传统的 Agent 开发模式,比如用 response_format 强行规定输出格式,虽然能解决前两个问题,但却让 Agent 丧失了灵活性,变成了一个只会"填表"的机器人。这显然不是我们想要的"智能助理"。
经过一番探索,我找到了一种极其优雅的解决方案:把结构化输出本身,也变成一种"工具"! 让 Agent 自己来决定什么时候该"填表",什么时候该"聊天"。
今天,我就将手把手带大家,使用 LangGraph 和 Google 最新的 gemini-2.5-flash 模型,构建一个能够无缝切换 于学术研究 、信息抓取 和日常对话三种模式的"全能AI研究助理"。

🔥 核心思路:让"格式"成为一种可选工具
我们这次优化的核心思想,简单来说就是:不强迫,只引导。
我们不再粗暴地告诉 LLM:"你必须用这个格式回复我!" 而是换一种更聪明的方式:
- 我们定义好想要的报告格式,比如
PaperAnalysis(论文分析报告)和LinkedinProfile(领英主页报告)。 - 但我们不把它们作为强制的
response_format,而是把它们伪装成两个特殊的"工具"交给 Agent。 - 我们在 System Prompt 中明确告诉 Agent:"你的工具箱里有搜索、抓取等普通工具,还有两个特殊的'报告生成'工具。当你需要产出正式报告时,请在收集完所有信息后,调用这两个工具来格式化你的最终答案。"
这样做的好处是显而易见的:
- 高度灵活:Agent 掌握了主动权,可以根据对话上下文自主判断是否需要结构化输出。
- 任务解耦:将"信息收集"和"格式化输出"两个步骤分开,Agent 的思考过程(Chain of Thought)更加清晰,有助于完成复杂任务。
- 自然交互:对于普通聊天,Agent 可以直接回复,交互体验大大提升。
理论说完了,让我们 show the code!
🛠️ 实战演练:三步构建全能助理
步骤 1: 环境准备与依赖安装
首先,我们需要在 Colab 环境中安装所有必需的库。这里我们用到了 langgraph 核心框架,langchain-google-genai 用于驱动 Gemini 模型,以及 beautifulsoup4 等辅助库。
⚠️ 注意: 每次安装或升级库之后,为了让新版本生效,请务必在 Colab 菜单栏点击 "代码执行程序" -> "重启会话"。
python
# --- 步骤 1: 安装与重启 ---
!pip install --upgrade --quiet langchain langchain-core langchain-mcp-adapters langchain-google-genai langgraph beautifulsoup4
print("✅ 库已升级。请务必从菜单栏点击 '代码执行程序' -> '重启会话',然后再继续运行下面的代码!")
步骤 2: 编写"灵魂"代码
这是我们整个项目的核心代码。我会逐一拆解,让你看懂每一部分的功能。
2.1 导入与密钥配置
常规操作,导入所有需要的模块,并从 Colab 的 userdata 中加载我们的 API 密钥。这种方式比直接把密钥写在代码里更安全。
python
# 2.1: 导入
import asyncio
import os
from typing import List, Union
from dataclasses import dataclass
import nest_asyncio
nest_asyncio.apply() # 允许在Jupyter/Colab环境中嵌套运行asyncio事件循环
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, Field # 导入 Pydantic 用于定义我们的"伪工具"
# 2.2: 加载密钥
from google.colab import userdata
os.environ["GEMINI_API_KEY"] = userdata.get('GEMINI_API_KEY')
BrightData_API_KEY = userdata.get('BrightData_API_KEY')
Paper_Search_API_KEY = userdata.get('Paper_Search_API_KEY')
2.2 🔥 核心优化点:创建"伪工具"🔥
这里就是我们整个方案的"魔法"所在!我们使用 Pydantic 的 BaseModel 来定义两个类:PaperAnalysis 和 LinkedinProfile。
BaseModel: Pydantic 的基础类,能让我们像定义一个普通的 Python 类一样来定义数据结构。Field: 用于为类的属性添加额外的描述信息。这一点至关重要 ,因为 LLM 正是通过读取这些description来理解每个字段的含义以及整个"工具"的作用。- 类的文档字符串 (docstring) : 比如
"""当用户要求...调用此工具来格式化最终报告。""",这是对整个工具最直接的描述,Agent 会优先读取它来判断工具的用途。
python
# ================================================================= #
# 🔥🔥🔥 核心优化点 1: 创建专门用于结构化输出的"伪工具" 🔥🔥🔥
# ================================================================= #
# 我们不再将 dataclass 作为 response_format,而是将它们包装成 Pydantic 模型,
# 并作为"工具"提供给 Agent。这让 Agent 可以自己决定何时调用它们。
class PaperAnalysis(BaseModel):
"""当用户要求对一篇学术论文进行详细分析时,调用此工具来格式化最终报告。"""
title: str = Field(description="论文的完整标题")
authors: List[str] = Field(description="论文的核心作者列表")
research_field: str = Field(description="根据内容总结出的研究方向")
summary: str = Field(description="对论文核心贡献的详细总结")
author_contact: str = Field(description="从抓取内容中找到的作者邮箱或个人主页,如果找不到则为 '联系方式未找到'")
class LinkedinProfile(BaseModel):
"""当用户要求提取领英个人主页信息时,调用此工具来格式化最终报告。"""
full_name: str = Field(description="用户的全名")
headline: str = Field(description="用户的头衔或当前职位")
location: str = Field(description="用户所在的地理位置")
summary: str = Field(description="个人简介部分的总结")
experience: List[str] = Field(description="一个包含所有工作经历的列表")
contact: str = Field(description="从抓取内容中找到的邮箱或个人主页,如果找不到则为 '联系方式未找到'")
2.3 设计"行动指南":System Prompt
一个好的 System Prompt 是 Agent 的"灵魂"。在这里,我们明确地为 Agent 设定了角色、能力,以及最重要的------行动指南(ReAct 思考模式)。
请注意,我们特地强调了 PaperAnalysis 和 LinkedinProfile 是特殊的"报告生成"工具 ,并指导 Agent 在收集完信息后必须调用它们来生成报告。这种明确的指令对于引导 Agent 行为至关重要。
python
# --- 步骤 2.3: 设计一个更通用的 System Prompt ---
SYSTEM_PROMPT = """
你是一个全能的AI研究助理。你可以处理多种任务,包括分析学术论文和查询个人资料。
**你的能力 (工具箱):**
* 你拥有学术搜索、通用网页搜索和网页抓取等一系列工具。
* **特别注意:** 你还拥有两个特殊的"报告生成"工具:`PaperAnalysis` 和 `LinkedinProfile`。
**你的行动指南 (ReAct 思考模式):**
1. **分析与规划:** 理解用户的请求。如果用户的最终目的是生成一份结构化的报告(比如论文分析或个人资料总结),你的最终行动**必须**是调用 `PaperAnalysis` 或 `LinkedinProfile` 工具。
2. **信息收集:** 使用你的其他工具(如 `search_arxiv`, `scrape_as_markdown`)来收集填充报告所需的所有信息。
3. **生成报告:** 当你收集到足够的信息后,调用相应的报告生成工具 (`PaperAnalysis` 或 `LinkedinProfile`),将收集到的信息作为参数传入。
4. **普通对话:** 如果用户只是进行普通聊天或提出简单问题,直接用自然语言回答即可,无需调用报告工具。
"""
2.4 Agent 的组装与测试
现在,万事俱备,我们来组装 Agent。
- 初始化工具集 :我们通过
MultiServerMCPClient加载了来自 BrightData 和 Paper Search 的真实工具集。 - 注入"伪工具" :我们将刚才定义的
PaperAnalysis和LinkedinProfile类,像普通工具一样,追加到all_tools列表中。 - 创建 Agent :调用
create_agent函数。请注意,我们没有传递response_format参数!这就是我们赋予 Agent 自由的关键。 - 多任务测试:我们设计了三个连续的测试用例,覆盖了论文分析、个人资料查询和普通聊天这三种场景。
python
# --- 步骤 2.4: 定义主异步函数 ---
async def main():
print("🚀 开始配置通用 AI Agent...")
# --- 初始化工具集 ---
# MultiServerMCPClient 用于连接和管理多个外部工具服务
mcp_client = MultiServerMCPClient({
"bright_data": {
"url": f"https://mcp.brightdata.com/mcp?token={BrightData_API_KEY}&pro=1",
"transport": "streamable_http",
},
"Paper_Search": {
"url": f"https://server.smithery.ai/@adamamer20/paper-search-mcp-openai/mcp?api_key={Paper_Search_API_KEY}",
"transport": "streamable_http",
}
})
# 异步获取所有可用的真实工具
real_tools = await mcp_client.get_tools()
# 将我们的"伪工具"(Pydantic模型)加入到工具列表中
all_tools = real_tools + [PaperAnalysis, LinkedinProfile]
print(f"✅ 成功加载 {len(all_tools)} 个工具。")
# --- 配置 LLM ---
# 使用 Google 的 gemini-2.5-flash 模型,性价比很高
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=os.environ["GEMINI_API_KEY"])
print("✅ LLM 配置完成: gemini-2.5-flash")
# --- 配置内存 ---
# 使用内存存储会话历史,方便进行多轮对话
checkpointer = InMemorySaver()
print("💾 内存已配置: InMemorySaver")
# --- 步骤 2.5: 创建 Agent,注意这次不指定 response_format ---
print("🤖 正在创建通用 Agent...")
agent_executor = create_agent(
model=llm,
tools=all_tools, # 传入包含真实工具和"伪工具"的完整列表
system_prompt=SYSTEM_PROMPT,
checkpointer=checkpointer
# 我们移除了 response_format,给予 Agent 更大的自由度
)
print("✅ Agent 创建成功!")
# --- 步骤 2.6: 进行多任务测试 ---
print("\n" + "="*50)
print("🚀 开始多任务对话测试!")
print("="*50)
# 为本次对话创建一个唯一的线程ID,以保持上下文
conversation_config = {"configurable": {"thread_id": "multi-task-thread-1"}}
# --- 任务1: 论文分析 ---
user_input_1 = "请使用 'search_arxiv' 工具搜索 'https://arxiv.org/abs/2409.09046'里的这篇论文,并用 'read_arxiv_paper' 工具读取论文内容。帮我提取论文主要内容和提取作者信息,尤其是作者们的邮箱。"
print(f"👤 用户 (任务1): {user_input_1}\n")
response_1 = await agent_executor.ainvoke({"messages": [("user", user_input_1)]}, config=conversation_config)
final_answer_1 = response_1['messages'][-1].content
# ... (打印部分省略) ...
# --- 任务2: 个人资料查询 ---
user_input_2 = "干得好!现在,请帮我查找吴恩达的领英主页信息,并以结构化形式返回。"
print(f"👤 用户 (任务2): {user_input_2}\n")
response_2 = await agent_executor.ainvoke({"messages": [("user", user_input_2)]}, config=conversation_config)
final_answer_2 = response_2['messages'][-1].content
# ... (打印部分省略) ...
# --- 任务3: 普通聊天 ---
user_input_3 = "谢谢你,你真是个好帮手。"
print(f"👤 用户 (任务3): {user_input_3}\n")
response_3 = await agent_executor.ainvoke({"messages": [("user", user_input_3)]}, config=conversation_config)
final_answer_3 = response_3['messages'][-1].content
# ... (打印部分省略) ...
# --- 统一输出最终结果 ---
print("\n" + "---" * 20)
print("最终成果展示:")
print("---" * 20)
print("\n[任务一:论文分析结果]\n")
print(final_answer_1)
print("\n[任务二:个人资料查询结果]\n")
print(final_answer_2)
print("\n[任务三:普通聊天结果]\n")
print(final_answer_3)
# --- 步骤 2.7: 运行主函数 ---
if __name__ == "__main__":
asyncio.run(main())
步骤 3: 见证奇迹!分析运行结果
让我们看看这个聪明的 Agent 是如何处理我们抛给它的三个不同任务的。
任务 1: 论文分析 📖
用户输入 : 请使用 'search_arxiv' 工具搜索 'https://arxiv.org/abs/2409.09046'里的这篇论文...帮我提取论文主要内容和提取作者信息...
Agent 回答:
text
好的,我已经读取了这篇论文。
**论文主要内容摘要:**
大型语言模型(LLMs)在人工智能法律和政策应用中面临知识过时、幻觉和复杂语境中推理能力差的局限性。检索增强生成(RAG)系统通过整合外部知识来解决这些问题...为高风险法律和政策应用提供了一个强大且适应性强的解决方案。
**作者信息:**
* **作者:** Rishi Kalra, Zekun Wu, Ayesha Gulley, Airlie Hilliard, Xin Guan, Adriano Koshiyama, Philip Treleaven
* **所属机构:** Holistic AI 和 University College London
论文中并未直接提供作者的邮箱地址...为了获取作者的邮箱,我需要进行一次网页搜索。请问您是否希望我尝试搜索作者的邮箱?
结果分析 : Agent 准确地执行了 search_arxiv 和 read_arxiv_paper 工具,并用 Markdown 格式清晰地总结了论文内容和作者信息。有趣的是,它没有 调用我们预设的 PaperAnalysis 工具。这恰恰证明了它的智能和灵活性!它判断当前场景下,一个格式化的文本回复已经足够清晰,并且在发现信息不完整(找不到邮箱)时,它没有强行填表,而是主动与用户沟通,请求下一步指令。非常棒!
任务 2: 个人资料查询 🧑💼
用户输入 : 干得好!现在,请帮我查找吴恩达的领英主页信息,并以结构化形式返回。
Agent 回答:
text
好的,这是吴恩达(Andrew Ng)的领英主页信息:
**全名:** Andrew Ng
**头衔:** DeepLearning.AI 创始人;AI Fund 管理合伙人;LandingAI 执行主席
**地点:** 美国加利福尼亚州帕洛阿尔托
**个人简介:** Andrew Ng 是人工智能领域的杰出人物...他的个人主页是 http://andrewng.org。
**经历:**
* DeepLearning.AI 创始人 (2017 年 6 月 - 至今)
* AI Fund 管理合伙人 (2018 年 1 月 - 至今)
* ...
**联系方式:** https://www.linkedin.com/in/andrewyng
结果分析 : 完美! 这次 Agent 的表现堪称典范。
- 它理解了"吴恩达"和"领英主页"。
- 调用
search_engine工具找到了正确的 URL。 - 调用
web_data_linkedin_person_profile工具抓取了页面信息。 - 最关键的一步 :它调用了我们定义的
LinkedinProfile这个"伪工具",将抓取到的非结构化信息,自动填充到了我们预设的字段中。 - 最后,它将这个结构化的结果以用户友好的格式呈现出来。
这完美地展示了我们的核心策略是多么成功!
任务 3: 普通聊天 😊
用户输入 : 谢谢你,你真是个好帮手。
Agent 回答:
text
不客气!很高兴能帮到您。如果您还有其他问题或需要进一步的帮助,请随时告诉我。
结果分析 : 正如我们所期望的,Agent 在面对简单的感谢时,给出了一个自然、礼貌的回复。它没有调用任何工具,更没有去尝试匹配 PaperAnalysis 或 LinkedinProfile,因为它清楚地知道,这只是一个简单的对话。这证明了我们的 Agent 真正实现了"能屈能伸",在不同场景下游刃有余。
总结与展望
通过这次实践,我们成功使用LangGraph的ReAct-Agent范式集成了BrightData和PaperSearch的MCP工具,通过搜索和爬取领英和学术网站,实现了具备论文搜索和读取,学者信息提取,邮箱查找等功能的 AI 助理。其成功的关键,就在于我们转变了思路:
将"强制的输出格式"转变为"可选的格式化工具",把最终决策权交还给 Agent。
这种基于 Pydantic 模型和 ReAct 模式的"伪工具"方法,不仅让我们的 Agent 更加智能和灵活,也为我们未来构建更复杂的、多功能的 Agent 系统提供了一个极具价值的设计范式。
希望今天的分享能对你有所启发。如果你对这个项目有任何疑问或者更好的想法,欢迎在评论区留言讨论!
附:完整项目源码
(为了方便大家复制代码,这里再次贴出完整的、可直接运行的代码)
python
# --- 步骤 1: 安装与重启 (同前) ---
!pip install --upgrade --quiet langchain langchain-core langchain-mcp-adapters langchain-google-genai langgraph beautifulsoup4
print("✅ 库已升级。请务必从菜单栏点击 '代码执行程序' -> '重启会话',然后再继续运行下面的代码!")
# --- 步骤 2: 完整代码 ---
# 2.1: 导入
import asyncio
import os
from typing import List, Union
from dataclasses import dataclass
import nest_asyncio
nest_asyncio.apply()
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
# from langchain_core.pydantic_v1 import BaseModel, Field # 导入 Pydantic 用于动态工具
from pydantic import BaseModel, Field
# 2.2: 加载密钥
from google.colab import userdata
os.environ["GEMINI_API_KEY"] = userdata.get('GEMINI_API_KEY')
BrightData_API_KEY = userdata.get('BrightData_API_KEY')
Paper_Search_API_KEY = userdata.get('Paper_Search_API_KEY')
# ================================================================= #
# 🔥🔥🔥 核心优化点 1: 创建专门用于结构化输出的"伪工具" 🔥🔥🔥
# ================================================================= #
# 我们不再将 dataclass 作为 response_format,而是将它们包装成 Pydantic 模型,
# 并作为"工具"提供给 Agent。这让 Agent 可以自己决定何时调用它们。
class PaperAnalysis(BaseModel):
"""当用户要求对一篇学术论文进行详细分析时,调用此工具来格式化最终报告。"""
title: str = Field(description="论文的完整标题")
authors: List[str] = Field(description="论文的核心作者列表")
research_field: str = Field(description="根据内容总结出的研究方向")
summary: str = Field(description="对论文核心贡献的详细总结")
author_contact: str = Field(description="从抓取内容中找到的作者邮箱或个人主页,如果找不到则为 '联系方式未找到'")
class LinkedinProfile(BaseModel):
"""当用户要求提取领英个人主页信息时,调用此工具来格式化最终报告。"""
full_name: str = Field(description="用户的全名")
headline: str = Field(description="用户的头衔或当前职位")
location: str = Field(description="用户所在的地理位置")
summary: str = Field(description="个人简介部分的总结")
experience: List[str] = Field(description="一个包含所有工作经历的列表")
contact: str = Field(description="从抓取内容中找到的邮箱或个人主页,如果找不到则为 '联系方式未找到'")
# --- 步骤 2.3: 设计一个更通用的 System Prompt ---
SYSTEM_PROMPT = """
你是一个全能的AI研究助理。你可以处理多种任务,包括分析学术论文和查询个人资料。
**你的能力 (工具箱):**
* 你拥有学术搜索、通用网页搜索和网页抓取等一系列工具。
* **特别注意:** 你还拥有两个特殊的"报告生成"工具:`PaperAnalysis` 和 `LinkedinProfile`。
**你的行动指南 (ReAct 思考模式):**
1. **分析与规划:** 理解用户的请求。如果用户的最终目的是生成一份结构化的报告(比如论文分析或个人资料总结),你的最终行动**必须**是调用 `PaperAnalysis` 或 `LinkedinProfile` 工具。
2. **信息收集:** 使用你的其他工具(如 `search_arxiv`, `scrape_as_markdown`)来收集填充报告所需的所有信息。
3. **生成报告:** 当你收集到足够的信息后,调用相应的报告生成工具 (`PaperAnalysis` 或 `LinkedinProfile`),将收集到的信息作为参数传入。
4. **普通对话:** 如果用户只是进行普通聊天或提出简单问题,直接用自然语言回答即可,无需调用报告工具。
"""
# --- 步骤 2.4: 定义主异步函数 ---
async def main():
print("🚀 开始配置通用 AI Agent...")
# --- 初始化工具集 ---
mcp_client = MultiServerMCPClient({
"bright_data": {
"url": f"https://mcp.brightdata.com/mcp?token={BrightData_API_KEY}&pro=1",
"transport": "streamable_http",
},
"Paper_Search": {
"url": f"https://server.smithery.ai/@adamamer20/paper-search-mcp-openai/mcp?api_key={Paper_Search_API_KEY}",
"transport": "streamable_http",
}
})
real_tools = await mcp_client.get_tools()
# 将我们的"伪工具"加入工具列表
all_tools = real_tools + [PaperAnalysis, LinkedinProfile]
print(f"✅ 成功加载 {len(all_tools)} 个工具。")
# --- 配置 LLM ---
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=os.environ["GEMINI_API_KEY"])
print("✅ LLM 配置完成: gemini-2.5-flash")
# --- 配置内存 ---
checkpointer = InMemorySaver()
print("💾 内存已配置: InMemorySaver")
# --- 步骤 2.5: 创建 Agent,注意这次不指定 response_format ---
print("🤖 正在创建通用 Agent...")
agent_executor = create_agent(
model=llm,
tools=all_tools,
system_prompt=SYSTEM_PROMPT,
checkpointer=checkpointer
# 我们移除了 response_format,给予 Agent 更大的自由度
)
print("✅ Agent 创建成功!")
# --- 步骤 2.6: 进行多任务测试 ---
print("\n" + "="*50)
print("🚀 开始多任务对话测试!")
print("="*50)
conversation_config = {"configurable": {"thread_id": "multi-task-thread-1"}}
# --- 任务1: 论文分析 ---
user_input_1 = "请使用 'search_arxiv' 工具搜索 'https://arxiv.org/abs/2409.09046'里的这篇论文,并用 'read_arxiv_paper' 工具读取论文内容。帮我提取论文主要内容和提取作者信息,尤其是作者们的邮箱。"
print(f"👤 用户 (任务1): {user_input_1}\n")
response_1 = await agent_executor.ainvoke({"messages": [("user", user_input_1)]}, config=conversation_config)
final_answer_1 = response_1['messages'][-1].content
print("\n" + "🤖" * 25)
print("🤖 Agent 的回答 (任务1):")
print(final_answer_1)
print("🤖" * 25 + "\n")
# --- 任务2: 个人资料查询 ---
user_input_2 = "干得好!现在,请帮我查找吴恩达的领英主页信息,并以结构化形式返回。"
print(f"👤 用户 (任务2): {user_input_2}\n")
response_2 = await agent_executor.ainvoke({"messages": [("user", user_input_2)]}, config=conversation_config)
final_answer_2 = response_2['messages'][-1].content
print("\n" + "🤖" * 25)
print("🤖 Agent 的回答 (任务2):")
print(final_answer_2)
print("🤖" * 25 + "\n")
# --- 任务3: 普通聊天 ---
user_input_3 = "谢谢你,你真是个好帮手。"
print(f"👤 用户 (任务3): {user_input_3}\n")
response_3 = await agent_executor.ainvoke({"messages": [("user", user_input_3)]}, config=conversation_config)
final_answer_3 = response_3['messages'][-1].content
print("\n" + "🤖" * 25)
print("🤖 Agent 的回答 (任务3):")
print(final_answer_3)
print("🤖" * 25)
# --- 步骤 2.7: 运行主函数 ---
if __name__ == "__main__":
asyncio.run(main())