最近折腾了一个挺有意思的项目,用MCP协议搭建了一个本地智能舆情分析智能体。整个过程踩了不少坑,但最终效果很不错:输入一句话,智能体自动搜索新闻、分析情感、生成报告,还能发邮件推送结果。
总算是从流程到架构,再到代码实现把这一系列的文章写完了,今天把完整的搭建过程分享给大家,代码和配置都贴出来,跟着做就能跑起来。
一、项目到底做了什么?
简单说,这是一个CS架构的舆情分析智能体:
-
客户端:接收用户输入,调用大模型规划任务,协调各种工具
-
服务端:提供搜索、分析、发邮件等具体功能
工作流程是这样的:
-
用户输入:"分析小米汽车的舆情"
-
智能体自动搜索相关新闻
-
调用大模型分析情感倾向
-
生成Markdown报告
-
发邮件给指定收件人
听起来复杂,但实际上就是把"查资料→分析→写报告→发送"这套流程自动化了。具体流程如下图:
二、环境准备
首先安装uv(Python包管理工具):
pip install uv
创建项目目录(要在哪个目录上面创建的话,先到这个目录):
uv init mcp-project1
cd mcp-project1
在项目根目录创建三个文件:
-
client_local.py
(客户端) -
server_local.py
(服务端) -
.env
(环境配置)
三、配置文件设置
先把.env
文件配好,这里需要几个API密钥:
BASE_URL="https://api.siliconflow.cn/v1"
MODEL="Pro/moonshotai/Kimi-K2-Instruct"#试了一下,比千问的模型效果要好
API_KEY="你的API密钥"
# Serper搜索API(用于搜索Google新闻)
SERPER_API_KEY="你的Serper密钥"
# 邮箱SMTP配置(以163邮箱为例)
SMTP_SERVER="smtp.163.com"
SMTP_PORT=465
EMAIL_USER="your_email@163.com"
EMAIL_PASS="你的授权码"
API获取说明:
-
硅基流动的服务,https://cloud.siliconflow.cn/i/nRDJFg4z
-
Serper:访问https://serper.dev/ 注册获取免费额度
-
163邮箱:开启SMTP服务,获取授权码(不是登录密码!)
这份完整版的大模型 AI 学习和面试资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】
四、服务端代码实现
server_local.py
提供三个核心工具,我们一个个来实现:
import os
import json
import smtplib
from datetime import datetime
from email.message import EmailMessage
import httpx
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
from openai import OpenAI
# 加载环境变量
load_dotenv()
# 初始化 MCP 服务器
mcp = FastMCP("NewsServer")
# @mcp.tool() 是 MCP 框架的装饰器,表明这是一个 MCP 工具。之后是对这个工具功能的描述
@mcp.tool()
async def search_google_news(keyword: str) -> str:
"""
使用 Serper API(Google Search 封装)根据关键词搜索新闻内容,返回前5条标题、描述和链接。
参数:
keyword (str): 关键词,如 "小米汽车"
返回:
str: JSON 字符串,包含新闻标题、描述、链接
"""
# 环境变量检查:获取并验证 SERPER_API_KEY 是否存在
api_key = os.getenv("SERPER_API_KEY")
if not api_key:
return "❌ 未配置 SERPER_API_KEY,请在 .env 文件中设置"
# 请求配置:设置 API 请求的 URL、头部和负载
url = "https://google.serper.dev/news"
headers = {
"X-API-KEY": api_key,
"Content-Type": "application/json"
}
payload = {"q": keyword}
# 异步请求处理:发送 POST 请求并获取响应数据
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=payload)
data = response.json()
# 响应验证:检查返回数据是否包含有效新闻结果
if "news" not in data:
return "❌ 未获取到搜索结果"
# 数据提取:从响应中解析前5条新闻的标题、描述和链接
articles = [
{
"title": item.get("title"),
"desc": item.get("snippet"),
"url": item.get("link")
} for item in data["news"][:5]
]
# 文件存储:创建输出目录并生成带时间戳的 JSON 文件名
output_dir = "./google_news"
os.makedirs(output_dir, exist_ok=True)
filename = f"google_news_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
file_path = os.path.join(output_dir, filename)
# 数据持久化:将新闻数据写入 JSON 文件
with open(file_path, "w", encoding="utf-8") as f:
json.dump(articles, f, ensure_ascii=False, indent=2)
# 结果返回:生成包含新闻摘要和文件路径的成功消息
return (
f"✅ 已获取与 [{keyword}] 相关的前5条 Google 新闻:\n"
f"{json.dumps(articles, ensure_ascii=False, indent=2)}\n"
f"📄 已保存到:{file_path}"
)
# @mcp.tool() 是 MCP 框架的装饰器,标记该函数为一个可调用的工具
@mcp.tool()
async def analyze_sentiment(text: str, filename: str) -> str:
"""
对传入的一段文本内容进行情感分析,并保存为指定名称的 Markdown 文件。
参数:
text (str): 新闻描述或文本内容
filename (str): 保存的 Markdown 文件名(不含路径)
返回:
str: 完整文件路径(用于邮件发送)
"""
# 初始化 OpenAI 客户端
# 从环境变量获取 API 密钥、模型名称和基础 URL
openai_key = os.getenv("API_KEY")
model = os.getenv("MODEL")
client = OpenAI(api_key=openai_key, base_url=os.getenv("BASE_URL"))
# 构建情感分析提示词
# 将用户输入的文本嵌入到预定义的提示模板中
prompt = f"请对以下新闻内容进行情绪倾向分析,并说明原因:\n\n{text}"
# 调用大语言模型进行情感分析
# 发送单轮对话请求并获取模型生成的响应内容
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}]
)
result = response.choices[0].message.content.strip()
# 生成舆情分析报告模板
# 包含时间戳、原始文本和分析结果的结构化 Markdown 内容
markdown = f"""# 舆情分析报告
**分析时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
---
## 📥 原始文本
{text}
---
## 📊 分析结果
{result}
"""
# 创建报告输出目录
# 确保目标目录存在(如不存在则自动创建)
output_dir = "./sentiment_reports"
os.makedirs(output_dir, exist_ok=True)
# 处理默认文件名逻辑
# 当未提供文件名时,使用时间戳生成默认文件名
if not filename:
filename = f"sentiment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
# 保存分析报告到文件系统
# 将生成的 Markdown 内容写入指定路径的 UTF-8 编码文件
file_path = os.path.join(output_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(markdown)
return file_path
@mcp.tool()
async def send_email_with_attachment(to: str, subject: str, body: str, filename: str) -> str:
"""
发送带附件的邮件。
参数:
to: 收件人邮箱地址
subject: 邮件标题
body: 邮件正文
filename: 保存的 Markdown 文件名(不含路径)
返回:
邮件发送状态说明
"""
# 从环境变量获取SMTP配置信息
smtp_server = os.getenv("SMTP_SERVER") # 例如 smtp.qq.com
smtp_port = int(os.getenv("SMTP_PORT", 465))
sender_email = os.getenv("EMAIL_USER")
sender_pass = os.getenv("EMAIL_PASS")
# 检查附件文件是否存在
full_path = os.path.abspath(os.path.join("./sentiment_reports", filename))
if not os.path.exists(full_path):
return f"❌ 附件路径无效,未找到文件: {full_path}"
# 创建邮件对象并设置基本信息
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to
msg.set_content(body)
# 读取并添加附件
try:
with open(full_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(full_path)
msg.add_attachment(file_data, maintype="application", subtype="octet-stream", filename=file_name)
except Exception as e:
return f"❌ 附件读取失败: {str(e)}"
# 通过SMTP服务器发送邮件
try:
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
server.login(sender_email, sender_pass)
server.send_message(msg)
return f"✅ 邮件已成功发送给 {to},附件路径: {full_path}"
except Exception as e:
return f"❌ 邮件发送失败: {str(e)}"
if __name__ == "__main__":
mcp.run(transport='stdio')
五、客户端代码实现
client_local.py
是整个智能体的大脑,负责协调各种工具:
import asyncio
import os
import json
from typing import Optional, List
from contextlib import AsyncExitStack
from datetime import datetime
import re
from openai import OpenAI
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
load_dotenv()
class MCPClient:
def __init__(self):
"""初始化类实例并配置API连接
该构造函数执行以下关键操作:
1. 初始化异步上下文管理器栈
2. 从环境变量加载API配置参数
3. 验证必要的API密钥是否存在
4. 创建OpenAI客户端实例
5. 初始化会话状态
属性说明:
self.exit_stack: 管理异步上下文资源的退出栈
self.openai_api_key: 从环境变量加载的API密钥
self.base_url: API服务的基础URL地址
self.model: 使用的AI模型名称
self.client: 配置好的OpenAI客户端实例
self.session: 存储异步HTTP会话的容器(初始为空)
"""
# 初始化异步资源管理栈
self.exit_stack = AsyncExitStack()
# 从环境变量加载API配置
self.openai_api_key = os.getenv("API_KEY")
self.base_url = os.getenv("BASE_URL")
self.model = os.getenv("MODEL")
# 关键参数验证:API密钥缺失时抛出异常
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 API_KEY")
# 创建配置好的API客户端
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
# 初始化会话容器(后续建立连接时填充)
self.session: Optional[ClientSession] = None
async def connect_to_server(self, server_script_path: str):
"""
连接到指定脚本路径的服务器进程
该方法会启动一个子进程运行指定的服务器脚本,并通过标准输入输出与服务器建立通信连接,
初始化客户端会话,并获取服务器支持的工具列表。
参数:
server_script_path (str): 服务器脚本文件路径,必须是.py或.js扩展名
返回:
None: 此方法没有直接返回值,但会初始化以下成员属性:
self.stdio: 服务器进程的标准输入输出流
self.write: 向服务器写入数据的函数
self.session: 与服务器通信的客户端会话对象
异常:
ValueError: 当脚本扩展名不是.py或.js时抛出
"""
# 验证服务器脚本文件扩展名
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
if not (is_python or is_js):
raise ValueError("服务器脚本必须是 .py 或 .js 文件")
# 根据文件类型确定启动命令
# 根据您的环境修改这个路径
python_executable = "d:/tanjp/miniconda3/envs/guiji/python.exe"
command = python_executable
# 配置服务器进程参数
server_params = StdioServerParameters(command=command, args=[server_script_path], env=None)
# 启动服务器进程并建立标准IO通信
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
# 解构通信通道对象
self.stdio, self.write = stdio_transport
# 创建并初始化客户端会话
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# 获取并打印服务器支持的工具列表
response = await self.session.list_tools()
tools = response.tools
print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""
处理用户查询的主函数,执行完整的情感分析流程。
该函数执行以下关键步骤:
1. 准备工具列表并生成报告文件名
2. 规划工具调用链
3. 执行工具链并收集结果
4. 生成最终回复并保存对话记录
参数:
query: 用户输入的查询字符串
返回值:
str: 模型生成的最终回复文本
"""
# 准备初始消息和获取工具列表
messages = [{"role": "user", "content": query}]
response = await self.session.list_tools()
# 构建可用工具列表,提取工具元数据
available_tools = [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
} for tool in response.tools
]
# 提取查询关键词并生成安全的报告文件名
keyword_match = re.search(r'(关于|分析|查询|搜索|查看)([^的\s,。、?\n]+)', query)
keyword = keyword_match.group(2) if keyword_match else "分析对象"
safe_keyword = re.sub(r'[\\/:*?"<>|]', '', keyword)[:20]
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
md_filename = f"sentiment_{safe_keyword}_{timestamp}.md"
md_path = os.path.join("./sentiment_reports", md_filename)
# 更新查询内容,注入文件名参数供后续工具使用
query = query.strip() + f" [md_filename={md_filename}] [md_path={md_path}]"
messages = [{"role": "user", "content": query}]
# 获取工具调用计划
tool_plan = await self.plan_tool_usage(query, available_tools)
tool_outputs = {}
messages = [{"role": "user", "content": query}]
# 按计划执行工具链
for step in tool_plan:
tool_name = step["name"]
tool_args = step["arguments"]
# 解析工具参数中的变量引用
for key, val in tool_args.items():
if isinstance(val, str) and val.startswith("{{") and val.endswith("}}"):
ref_key = val.strip("{} ")
resolved_val = tool_outputs.get(ref_key, val)
tool_args[key] = resolved_val
# 为特定工具注入默认文件参数
if tool_name == "analyze_sentiment" and "filename" not in tool_args:
tool_args["filename"] = md_filename
if tool_name == "send_email_with_attachment" and "attachment_path" not in tool_args:
tool_args["attachment_path"] = md_path
# 调用工具并存储输出
result = await self.session.call_tool(tool_name, tool_args)
tool_outputs[tool_name] = result.content[0].text
messages.append({
"role": "tool",
"tool_call_id": tool_name,
"content": result.content[0].text
})
# 使用完整对话上下文生成最终回复
final_response = self.client.chat.completions.create(
model=self.model,
messages=messages
)
final_output = final_response.choices[0].message.content
# 定义文件名清理函数
def clean_filename(text: str) -> str:
text = text.strip()
text = re.sub(r'[\\/:*?\"<>|]', '', text)
return text[:50]
# 准备对话记录存储路径
safe_filename = clean_filename(query)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"{safe_filename}_{timestamp}.txt"
output_dir = "./llm_outputs"
os.makedirs(output_dir, exist_ok=True)
file_path = os.path.join(output_dir, filename)
# 持久化存储对话记录
with open(file_path, "w", encoding="utf-8") as f:
f.write(f"🗣 用户提问:{query}\n\n")
f.write(f"🤖 模型回复:\n{final_output}\n")
print(f"📄 对话记录已保存为:{file_path}")
return final_output
async def chat_loop(self):
"""
主聊天循环函数,用于处理用户交互。
此函数实现了一个持续运行的异步聊天循环,主要功能包括:
1. 显示初始提示信息
2. 循环接收用户输入
3. 处理退出指令
4. 异步处理用户查询并显示AI响应
5. 捕获并显示运行过程中的异常
循环将持续运行,直到用户输入'quit'退出指令。
"""
# 初始化提示信息
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
# 进入主循环中等待用户输入
while True:
try:
# 获取用户输入并移除首尾空格
query = input("\n你: ").strip()
# 检测退出指令
if query.lower() == 'quit':
break
# 处理用户查询并获取AI响应
response = await self.process_query(query)
# 打印AI回复
print(f"\n🤖 AI: {response}")
# 异常处理模块
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def plan_tool_usage(self, query: str, tools: List[dict]) -> List[dict]:
"""
根据用户查询规划工具调用链
此函数通过大模型分析用户请求,生成结构化工具调用计划。将可用工具列表转换为提示词,
要求模型返回JSON格式的调用计划,支持多步骤调用(通过{{上一步工具名}}占位符实现)。
Args:
query: 用户自然语言请求文本
tools: 可用工具定义列表,每个工具需包含'function'字典(含'name'和'description'字段)
Returns:
工具调用计划列表,每个元素包含:
name: 工具名称字符串
arguments: 工具参数字典
解析失败时返回空列表
"""
# 打印工具定义用于调试
print("\n📤 提交给大模型的工具定义:")
print(json.dumps(tools, ensure_ascii=False, indent=2))
# 构造工具描述文本列表
tool_list_text = "\n".join([
f"- {tool['function']['name']}: {tool['function']['description']}"
for tool in tools
])
# 构建系统提示词:包含工具列表和输出格式要求
system_prompt = {
"role": "system",
"content": (
"你是一个智能任务规划助手,用户会给出一句自然语言请求。\n"
"你只能从以下工具中选择(严格使用工具名称):\n"
f"{tool_list_text}\n"
"如果多个工具需要串联,后续步骤中可以使用 {{上一步工具名}} 占位。\n"
"返回格式:JSON 数组,每个对象包含 name 和 arguments 字段。\n"
"不要返回自然语言,不要使用未列出的工具名。"
)
}
# 构造对话上下文(系统提示+用户查询)
planning_messages = [
system_prompt,
{"role": "user", "content": query}
]
# 调用大模型获取规划结果(禁止模型直接调用工具)
response = self.client.chat.completions.create(
model=self.model,
messages=planning_messages,
tools=tools,
tool_choice="none"
)
# 提取模型返回内容并清理JSON格式
content = response.choices[0].message.content.strip()
match = re.search(r"```(?:json)?\s*([\s\S]+?)\s*```", content)
if match:
json_text = match.group(1)
else:
json_text = content
# 解析JSON并返回调用计划
try:
plan = json.loads(json_text)
return plan if isinstance(plan, list) else []
except Exception as e:
print(f"❌ 工具调用链规划失败: {e}\n原始返回: {content}")
return []
async def cleanup(self):
"""
异步执行资源清理操作。
该方法使用异步上下文管理栈(exit_stack)来统一管理所有需要清理的资源。
调用栈的异步关闭方法(aclose)会按照后进先出顺序关闭栈中所有资源。
参数:
无(除实例自身self外)
返回值:
无
"""
await self.exit_stack.aclose()
async def main():
server_script_path = "E:\\llm\\mcp\\mcp-project\\server_local.py"
client = MCPClient()
try:
await client.connect_to_server(server_script_path)
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
六、运行测试
安装依赖:
pip install mcp openai httpx python-dotenv
启动程序:
python client.py
测试示例:
你: 分析小米汽车的舆情
智能体会自动:
-
搜索小米汽车相关新闻
-
分析情感倾向
-
生成Markdown报告
-
发送邮件(如果配置了邮件工具)
七、核心原理解析
整个智能体的精妙之处在于工具链的自动规划。当你输入一个需求时,大模型会分析需要哪些工具,按什么顺序执行。
比如"分析小米汽车舆情"这个需求,模型会规划出:
[
{
"name": "search_google_news",
"arguments": {"keyword": "小米汽车"}
},
{
"name": "analyze_sentiment",
"arguments": {"text": "{{search_google_news}}", "filename": "..."}
}
]
这种设计让智能体非常灵活,你可以轻松添加新工具,比如微博搜索、股价查询等。
八、实际应用场景
我已经用这套智能体做了几个有意思的事情:
-
竞品监控:定时分析竞争对手的舆情变化
-
品牌监测:跟踪自家产品的网络口碑
-
热点分析:快速了解某个事件的网络反应
-
投资研究:分析上市公司的新闻情感倾向
九、扩展思路
这个框架还有很多扩展可能:
-
数据源扩展:接入微博、知乎、抖音等平台
-
分析维度扩展:加入关键词云、情感时间线分析
-
通知方式扩展:支持钉钉、企业微信推送
-
可视化扩展:生成图表和可视化报告
十、踩坑总结
分享几个我踩过的坑:
-
API配额限制:Serper免费版有调用次数限制,建议升级付费版
-
邮箱授权码:163邮箱要用授权码,不是登录密码
-
文件路径问题:Windows下路径分隔符要注意
-
异常处理:网络请求要做好超时和重试机制
这个项目让我深刻体会到了MCP协议的强大之处,并且实现MCP+LLM+Agent架构。它让AI不再是一个孤立的对话工具,而是能够调动各种资源的智能助手。
之前商界有位名人说过:“站在风口,猪都能吹上天”。这几年,AI大模型领域百家争鸣,百舸争流,明显是这个时代下一个风口!
那如何学习大模型&AI产品经理?
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
只要你是真心想学AI大模型,我这份资料就可以无偿共享给你学习。大模型行业确实也需要更多的有志之士加入进来,我也真心希望帮助大家学好这门技术,如果日后有什么学习上的问题,欢迎找我交流,有技术上面的问题,我是很愿意去帮助大家的!
如果你也想通过学大模型技术去帮助就业和转行,可以点扫描下方链接👇👇
大模型重磅福利:入门进阶全套104G学习资源包免费分享!
01.从入门到精通的全套视频教程
包含提示词工程、RAG、Agent等技术点
02.AI大模型学习路线图(还有视频解说)
全过程AI大模型学习路线
03.学习电子书籍和技术文档
市面上的大模型书籍确实太多了,这些是我精选出来的
04.大模型面试题目详解
05.这些资料真的有用吗?
这份资料由我和鲁为民博士共同整理,鲁为民博士先后获得了北京清华大学学士和美国加州理工学院博士学位,在包括IEEE Transactions等学术期刊和诸多国际会议上发表了超过50篇学术论文、取得了多项美国和中国发明专利,同时还斩获了吴文俊人工智能科学技术奖。目前我正在和鲁博士共同进行人工智能的研究。
所有的视频由智泊AI老师录制,且资料与智泊AI共享,相互补充。这份学习大礼包应该算是现在最全面的大模型学习资料了。
资料内容涵盖了从入门到进阶的各类视频教程和实战项目,无论你是小白还是有些技术基础的,这份资料都绝对能帮助你提升薪资待遇,转行大模型岗位。
智泊AI始终秉持着“让每个人平等享受到优质教育资源”的育人理念,通过动态追踪大模型开发、数据标注伦理等前沿技术趋势,构建起"前沿课程+智能实训+精准就业"的高效培养体系。
课堂上不光教理论,还带着学员做了十多个真实项目。学员要亲自上手搞数据清洗、模型调优这些硬核操作,把课本知识变成真本事!
如果说你是以下人群中的其中一类,都可以来智泊AI学习人工智能,找到高薪工作,一次小小的“投资”换来的是终身受益!
应届毕业生:无工作经验但想要系统学习AI大模型技术,期待通过实战项目掌握核心技术。
零基础转型:非技术背景但关注AI应用场景,计划通过低代码工具实现“AI+行业”跨界。
业务赋能 突破瓶颈:传统开发者(Java/前端等)学习Transformer架构与LangChain框架,向AI全栈工程师转型。
👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓