This commit is contained in:
kennethcheng 2026-04-16 01:02:24 +08:00
commit 35f41e10f3
11 changed files with 3217 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.env

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

183
README.md Normal file
View File

@ -0,0 +1,183 @@
# LangChain Learning Project
> 基于 LangChain 1.0.3 的 AI Agent 学习项目,展示如何使用 MCP (Model Context Protocol) 构建智能代理
## 项目概述
本项目是一个 LangChain 学习与实践项目,演示了如何构建具有外部工具调用能力的 AI Agent。通过集成 MCP 服务器,实现了对外部服务的调用,如天气查询、数学运算、学术搜索等功能。
## 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| **LangChain** | 1.0.3 | 核心框架 |
| **LangChain-Community** | 0.4.1 | 社区组件 |
| **LangChain-MCP-Adapters** | ≥0.1.11 | MCP 客户端 |
| **FastMCP** | ≥2.13.0 | MCP 协议实现 |
| **LangChain-Ollama** | ≥1.0.0 | Ollama 本地模型 |
| **LangGraph** | - | Agent 记忆组件 |
## 项目结构
```
langchain-learning/
├── agent/
│ ├── langchain_agent.py # MCP Agent (Ollama + weather + math)
│ └── langchain_agent_tool_mcp.py # 扩展 Agent (+ arxiv + wikipedia + 自定义工具)
├── mcp/
│ ├── math_server.py # 数学运算 MCP 服务器 (stdio)
│ └── get_weather_server.py # 天气查询 MCP 服务器 (HTTP)
├── ollama/
│ └── ollama_demo.py # Ollama 本地模型调用示例
├── main.py # 程序入口
├── pyproject.toml # 项目配置
└── .env # 环境变量 (API Keys)
```
## 功能特性
### MCP 服务器
| 服务器 | 功能 | 传输方式 | 端口 |
|--------|------|----------|------|
| `math_server` | 加法、乘法运算 | stdio | - |
| `weather_server` | 城市天气查询 | HTTP/SSE | 9000 |
### 支持的 LLM
| 模型 | 类型 | 调用方式 |
|------|------|----------|
| `gemma4:e2b` / `gemma4:26b` | Ollama 本地 | `langchain_ollama` |
| `Qwen/Qwen3-14B` | SiliconFlow 云端 | OpenAI 兼容接口 |
### 工具集成
| 来源 | 工具 |
|------|------|
| **MCP Servers** | `get_weather`, `add`, `multiply` |
| **内置 (load_tools)** | `arxiv`, `wikipedia` |
| **自定义** | `greet(name)` |
## 快速开始
### 1. 安装依赖
```bash
uv sync
```
### 2. 配置环境变量
编辑 `.env` 文件:
```env
# Ollama (本地)
OLLAMA_API_KEY=ollama
OLLAMA_BASE_URL=http://localhost:11434/v1
# SiliconFlow (可选云端)
SILICONFLOW_API_KEY=your_siliconflow_key
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1
# 天气API
WEATHER_API_KEY=your_weatherapi_key
```
### 3. 启动 MCP 服务器
```bash
# 终端 1: 启动天气服务器 (HTTP)
python mcp/get_weather_server.py
# 终端 2: 启动数学服务器 (stdio)
python mcp/math_server.py
```
### 4. 运行 Agent
```bash
# Agent 1: Ollama + MCP (weather + math)
python agent/langchain_agent.py
# Agent 2: Ollama + MCP + 内置工具 + 自定义工具
python agent/langchain_agent_tool_mcp.py
```
### 5. Ollama 直接调用示例
```bash
python ollama/ollama_demo.py
```
## 使用示例
```
请输入你的问题输入exit则退出 > 北京天气怎么样
计算过程:
- 调用工具: get_weather({'city': '北京'})
- get_weather 的结果是: {"location":{"name":"北京",...},"current":{"temp_c":22.0,...}}
最终答案: 北京的天气晴朗温度约22°C。
```
## Agent 对比
| Agent | 模型 | MCP | 内置工具 | 自定义工具 |
|-------|------|-----|----------|------------|
| `langchain_agent.py` | Ollama | ✅ weather, math | ❌ | ❌ |
| `langchain_agent_tool_mcp.py` | Ollama | ✅ weather, math | ✅ arxiv, wikipedia | ✅ greet |
## 核心代码说明
### MCP 客户端配置
```python
client = MultiServerMCPClient({
"weather": {
"url": "http://localhost:9000/mcp",
"transport": "streamable_http",
},
"math": {
"command": sys.executable, # 使用当前解释器
"args": ["./mcp/math_server.py"],
"transport": "stdio"
}
})
```
### Agent 创建
```python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model=llm,
tools=tools,
checkpointer=InMemorySaver() # 会话记忆
)
```
## 依赖列表
核心依赖见 `pyproject.toml`
- `langchain >= 1.0.3`
- `langchain-community >= 0.4.1`
- `langchain-mcp-adapters >= 0.1.11`
- `langchain-ollama >= 1.0.0`
- `fastmcp >= 2.13.0.2`
- `wikipedia >= 1.4.0`
- `arxiv >= 2.2.0`
## 学习资源
- [LangChain 官方文档](https://python.langchain.com/)
- [MCP 协议规范](https://modelcontextprotocol.io/)
- [LangChain GitHub](https://github.com/langchain-ai/langchain)
- [FastMCP 文档](https://github.com/jlowin/fastmcp)
## License
MIT License

113
agent/langchain_agent.py Normal file
View File

@ -0,0 +1,113 @@
import asyncio
import logging
import sys
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
import os
import dotenv
from langgraph.checkpoint.memory import InMemorySaver
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
dotenv.load_dotenv()
## 设置环境变量
os.environ['OPENAI_API_KEY'] = os.getenv("OLLAMA_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OLLAMA_BASE_URL")
# 默认的 'model_name': 'deepseek-ai/DeepSeek-V3.1',
llm = ChatOpenAI(model="gemma4:e2b")
def print_optimized_result(agent_response):
"""
解析代理响应并输出优化后的结果
:param agent_response: 代理返回的完整响应
"""
messages = agent_response.get("messages", [])
steps = [] # 用于记录计算步骤
final_answer = None # 最终答案
for message in messages:
if hasattr(message, "additional_kwargs") and "tool_calls" in message.additional_kwargs:
# 提取工具调用信息
tool_calls = message.additional_kwargs["tool_calls"]
for tool_call in tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = tool_call["function"]["arguments"]
steps.append(f"调用工具: {tool_name}({tool_args})")
elif message.type == "tool":
# 提取工具执行结果
tool_name = message.name
tool_result = message.content
steps.append(f"{tool_name} 的结果是: {tool_result}")
elif message.type == "ai":
# 提取最终答案
final_answer = message.content
# 打印优化后的结果
print("\n计算过程:")
for step in steps:
print(f"- {step}")
if final_answer:
print(f"\n最终答案: {final_answer}")
async def execute():
# 1. 创建langchain中的mcp客户端 —— uv add langchain_mcp_adapters
client = MultiServerMCPClient(
# mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
{
# 这里是定义服务端信息的,可以有多个服务端
"weather": {
"url": "http://localhost:9000/mcp",
"transport": "streamable_http",
},
"math": {
# "command": "python", # npx uvx
"command": sys.executable,
"args": ["./mcp/math_server.py"],
"transport": "stdio"
}
}
)
try:
# 2. 通过客户端获取工具列表
## 这里是通过服务端获取,所以可能会有异常(比如服务端没有启动,或者网络连接有问题
tools = await client.get_tools()
# 3. 创建一个智能代理,能够完成 思考--> 行动 --> 观察 --> 思考 --> 行动 --> ... --> 最终答案
from langchain.agents import create_agent
## 创建一个带记忆和工具的agent
agent = create_agent(
model=llm,
tools=tools,
# 配置记忆后必须增加thread_id, checkpoint_ns, checkpoint_id参数之一用于区分会话
checkpointer=InMemorySaver()
)
while True:
user_input = input("请输入你的问题输入exit则退出 > ")
if user_input == "exit":
print("感谢使用,再见👋🏻")
break
agent_response = await agent.ainvoke(
{"messages": [{"role": "user", "content": user_input}]},
{"configurable": {"thread_id": "1"}},
)
## agent会自己去调用 tools ,不需要我们去进行调用
print_optimized_result(agent_response)
finally:
# 资源回收
if hasattr(client, 'close'):
client.close()
if __name__ == '__main__':
asyncio.run(execute())

View File

@ -0,0 +1,136 @@
import asyncio
import logging
import sys
from docutils.nodes import description
from langchain_community.agent_toolkits.load_tools import load_tools
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
import os
import dotenv
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, Field
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
dotenv.load_dotenv()
## 设置环境变量
os.environ['OPENAI_API_KEY'] = os.getenv("OLLAMA_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OLLAMA_BASE_URL")
# 默认的 'model_name': 'deepseek-ai/DeepSeek-V3.1',
llm = ChatOpenAI(model="gemma4:e2b")
def print_optimized_result(agent_response):
"""
解析代理响应并输出优化后的结果
:param agent_response: 代理返回的完整响应
"""
messages = agent_response.get("messages", [])
steps = [] # 用于记录计算步骤
final_answer = None # 最终答案
for message in messages:
if hasattr(message, "additional_kwargs") and "tool_calls" in message.additional_kwargs:
# 提取工具调用信息
tool_calls = message.additional_kwargs["tool_calls"]
for tool_call in tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = tool_call["function"]["arguments"]
steps.append(f"调用工具: {tool_name}({tool_args})")
elif message.type == "tool":
# 提取工具执行结果
tool_name = message.name
tool_result = message.content
steps.append(f"{tool_name} 的结果是: {tool_result}")
elif message.type == "ai":
# 提取最终答案
final_answer = message.content
# 打印优化后的结果
print("\n计算过程:")
for step in steps:
print(f"- {step}")
if final_answer:
print(f"\n最终答案: {final_answer}")
def greet(name: str) -> str:
"""用于打招呼的函数"""
return f"你好呀,{name}"
class greetInput(BaseModel):
name: str = Field(description="打招呼的对象")
async def execute():
# 1. 创建langchain中的mcp客户端 —— uv add langchain_mcp_adapters
client = MultiServerMCPClient(
# mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
{
# 这里是定义服务端信息的,可以有多个服务端
# 这里是定义服务端信息的,可以有多个服务端
"weather": {
"url": "http://localhost:9000/mcp",
"transport": "streamable_http",
},
"math": {
# "command": "python", # npx uvx
"command": sys.executable,
"args": ["./mcp/math_server.py"],
"transport": "stdio"
}
}
)
try:
# 2. 通过客户端获取工具列表
## 这里是通过服务端获取,所以可能会有异常(比如服务端没有启动,或者网络连接有问题
tools_mcp = await client.get_tools()
tools = tools_mcp + tools_internal + [tools_custom]
# 3. 创建一个智能代理,能够完成 思考--> 行动 --> 观察 --> 思考 --> 行动 --> ... --> 最终答案
from langchain.agents import create_agent
## 创建一个带记忆和工具的agent
agent = create_agent(
model=llm,
tools=tools,
# 配置记忆后必须增加thread_id, checkpoint_ns, checkpoint_id参数之一用于区分会话
checkpointer=InMemorySaver()
)
while True:
user_input = input("请输入你的问题输入exit则退出 > ")
if user_input == "exit":
print("感谢使用,再见👋🏻")
break
agent_response = await agent.ainvoke(
{"messages": [{"role": "user", "content": user_input}]},
{"configurable": {"thread_id": "1"}},
)
## agent会自己去调用 tools ,不需要我们去进行调用
print_optimized_result(agent_response)
finally:
# 资源回收
if hasattr(client, 'close'):
client.close()
if __name__ == '__main__':
tools_internal = load_tools(["arxiv", "wikipedia"], llm=llm, allow_dangerous_tools=True)
tools_custom = tool(
name_or_callable="greet",
runnable=greet,
description="用于打招呼的函数",
args_schema=greetInput
)
asyncio.run(execute())

6
main.py Normal file
View File

@ -0,0 +1,6 @@
def main():
print("Hello from langchain-learning-new!")
if __name__ == "__main__":
main()

32
mcp/get_weather_server.py Normal file
View File

@ -0,0 +1,32 @@
# uv add fastmcp
import os
import logging
import requests
from fastmcp import FastMCP
mcp = FastMCP("mcp demo")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
# @mcp.tool()
# async def get_weather(city: str) -> str:
# """获取传入的城市的天气信息"""
# logging.info(f"调用了查询天气服务,传入的参数为{city}")
# return f"{city}的天气很好,阳光明媚,晴空万里"
@mcp.tool()
async def get_weather(city: str) -> str:
"""获取传入的城市的天气信息"""
logging.info("The get_weather method is called: city=%s", city)
api_key = os.getenv("WEATHER_API_KEY")
# url = f"https://api.weatherapi.com/v1/current.json?key=6056d3b7245449069d0152825252910&q={city}"
url = f"https://api.weatherapi.com/v1/current.json?key={WEATHER_API_KEY}&q={city}"
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.text
if __name__ == "__main__":
logging.info("启动一个可以通过MCP调用获取天气的服务")
mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
# mcp.run(transport="stdio", host="127.0.0.1", port=9000)

29
mcp/math_server.py Normal file
View File

@ -0,0 +1,29 @@
from fastmcp import FastMCP
import logging
# 配置日志记录器
logging.basicConfig(
level=logging.INFO, # 设置日志级别为 INFO
format="%(asctime)s - %(levelname)s - %(message)s" # 日志格式
)
logger = logging.getLogger(__name__)
# 创建 FastMCP 实例
mcp = FastMCP("Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
logger.info("The add method is called: a=%d, b=%d", a, b) # 记录加法调用日志
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
logger.info("The multiply method is called: a=%d, b=%d", a, b) # 记录乘法调用日志
return a * b
if __name__ == "__main__":
logger.info("Start math server through MCP") # 记录服务启动日志
# mcp.run(transport="streamable-http",port=8081,path='/mcp') # 启动服务并使用标准输入输出通信
mcp.run(transport="stdio") # 启动服务并使用标准输入输出通信(子进程)

26
ollama/ollama_demo.py Normal file
View File

@ -0,0 +1,26 @@
# 配置一下解释器
# uv add langchain_ollama
from langchain_core.messages import HumanMessage
from langchain_ollama import ChatOllama
#此时调用的是本地的大模型。省略base_url、api-key
llm = ChatOllama(
model = "gemma4:26b",
)
# llm.invoke("你好,请介绍一下你自己!")
messages = [
HumanMessage(content="你好,请介绍一下你自己!")
]
# response = llm.invoke(messages)
# print(response.content)
chunks = []
for chunk in llm.stream(messages):
# chunks.append(chunk)
print(chunk.content, end="", flush=True)

21
pyproject.toml Normal file
View File

@ -0,0 +1,21 @@
[project]
name = "langchain-new"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
"langchain>=1.1.3",
"langchain-community>=0.4.1",
"langchain-mcp-adapters>=0.1.11",
"langchain-openai>=1.0.1",
"langchain-text-splitters>=1.0.0",
"numexpr>=2.10.2",
"wikipedia>=1.4.0",
"arxiv>=2.2.0",
"httpx>=0.28.1",
"socksio>=1.0.0",
"fastmcp>=2.13.0.2",
"langchain-ollama>=1.0.0",
"langgraph-prebuilt>=1.0.9",
"uvicorn[standard]>=0.30.0",
]

2659
uv.lock Normal file

File diff suppressed because it is too large Load Diff