FastAPI+AI Agent工具调用实战
FastAPI + AI Agent工具调用实战
文章信息:标题《FastAPI + AI Agent工具调用实战》| 字数:约4000字 | 预估阅读时间:15分钟
1. AI Agent架构概述
AI Agent(智能体)是能够自主完成复杂任务的AI系统。与普通LLM对话不同,Agent有:
- 规划能力:分解任务、制定执行计划
- 工具调用:调用外部API、搜索、计算等
- 记忆:保存对话历史、上下文
- 反思:检查结果,决定是否重试
一个典型的Agent执行流程:
用户请求 → 规划(Plan) → 执行(Act) → 观察(Observe) → 决策(Decide) → 输出
2. MCP协议详解
MCP(Model Context Protocol)是Anthropic提出的标准化协议,让AI模型与外部工具通信。
为什么需要MCP?
- 统一工具定义格式
- 支持多工具协同
- 安全性(工具权限控制)
- 可扩展(新增工具无需改代码)
MCP核心概念:
| 概念 | 说明 |
|---|---|
| Host | 调用工具的AI应用(如Claude Desktop) |
| Server | 提供工具的服务(如文件系统、数据库) |
| Tool | 具体操作(read_file、search、query_db) |
| Resource | AI可读取的数据(文档、配置) |
3. 用FastAPI实现MCP Server
3.1 项目结构
fastapi-mcp-server/
├── main.py # FastAPI主入口
├── mcp_server.py # MCP Server核心
├── tools/ # 工具定义
│ ├── __init__.py
│ ├── calculator.py
│ ├── weather.py
│ └── search.py
├── models.py # Pydantic模型
├── config.py # 配置
└── requirements.txt
3.2 安装依赖
pip install fastapi uvicorn pydantic httpx python-dotenv
3.3 工具定义
# tools/__init__.py
from .calculator import calculator_tool
from .weather import weather_tool
from .search import search_tool
__all__ = ["calculator_tool", "weather_tool", "search_tool"]
# tools/calculator.py
from typing import Annotated, Any
from pydantic import BaseModel, Field
class CalculatorInput(BaseModel):
"""计算器输入"""
expression: str = Field(..., description="数学表达式,如 '2+3*5'")
def calculate(a: float, b: float, operator: str) -> float:
"""执行数学计算"""
if operator == "+":
return a + b
elif operator == "-":
return a - b
elif operator == "*":
return a * b
elif operator == "/":
if b == 0:
raise ValueError("除数不能为0")
return a / b
else:
raise ValueError(f"未知运算符: {operator}")
calculator_tool = {
"name": "calculate",
"description": "执行数学计算,支持加减乘除",
"input_schema": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "第一个数"},
"b": {"type": "number", "description": "第二个数"},
"operator": {"type": "string", "enum": ["+", "-", "*", "/"], "description": "运算符"}
},
"required": ["a", "b", "operator"]
},
"handler": calculate
}
# tools/weather.py
import httpx
from typing import Any
async def get_weather(city: str) -> dict:
"""获取城市天气(模拟)"""
# 实际项目中使用真实天气API
weather_data = {
"北京": {"temp": 22, "condition": "晴", "humidity": 45},
"上海": {"temp": 25, "condition": "多云", "humidity": 60},
"深圳": {"temp": 28, "condition": "雨", "humidity": 80},
}
result = weather_data.get(city, {"temp": 20, "condition": "未知", "humidity": 50})
return {
"city": city,
"temperature": result["temp"],
"condition": result["condition"],
"humidity": result["humidity"]
}
weather_tool = {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如'北京'"}
},
"required": ["city"]
},
"handler": get_weather
}
# tools/search.py
from typing import Any
class SearchInput(BaseModel):
query: str = Field(..., description="搜索关键词")
limit: int = Field(default=5, description="返回结果数量")
async def web_search(query: str, limit: int = 5) -> dict:
"""模拟网络搜索"""
# 实际项目中使用真实搜索API
results = [
{"title": f"关于'{query}'的结果1", "url": "https://example.com/1", "snippet": "这是搜索结果摘要..."},
{"title": f"关于'{query}'的结果2", "url": "https://example.com/2", "snippet": "这是搜索结果摘要..."},
{"title": f"关于'{query}'的结果3", "url": "https://example.com/3", "snippet": "这是搜索结果摘要..."},
]
return {"query": query, "results": results[:limit], "total": len(results)}
search_tool = {
"name": "web_search",
"description": "搜索网络信息",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"limit": {"type": "integer", "description": "返回结果数量", "default": 5}
},
"required": ["query"]
},
"handler": web_search
}
3.4 MCP Server核心
创建mcp_server.py。先注册工具并定义请求/响应模型:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
import asyncio
from tools import calculator_tool, weather_tool, search_tool
app = FastAPI(title="MCP Server", version="1.0.0")
# 注册所有工具到字典,按名称索引
TOOLS = {
calculator_tool["name"]: calculator_tool,
weather_tool["name"]: weather_tool,
search_tool["name"]: search_tool,
}
class ToolCallRequest(BaseModel):
tool: str = Field(..., description="工具名称")
arguments: Dict[str, Any] = Field(default_factory=dict, description="工具参数")
class ToolCallResponse(BaseModel):
success: bool
result: Optional[Any] = None
error: Optional[str] = None
class AgentRequest(BaseModel):
user_message: str = Field(..., description="用户消息")
class AgentResponse(BaseModel):
response: str
tool_calls: List[Dict[str, Any]] = Field(default_factory=list)
然后是工具列表和工具调用接口。调用时需要判断 handler 是同步还是异步函数:
@app.get("/tools")
async def list_tools():
return {
"tools": [
{"name": t["name"], "description": t["description"], "input_schema": t["input_schema"]}
for t in TOOLS.values()
]
}
@app.post("/tools/call", response_model=ToolCallResponse)
async def call_tool(request: ToolCallRequest):
if request.tool not in TOOLS:
raise HTTPException(status_code=404, detail=f"工具'{request.tool}'不存在")
tool = TOOLS[request.tool]
try:
handler = tool["handler"]
if asyncio.iscoroutinefunction(handler):
result = await handler(**request.arguments)
else:
result = handler(**request.arguments)
return ToolCallResponse(success=True, result=result)
except Exception as e:
return ToolCallResponse(success=False, error=str(e))
最后是 Agent 接口——通过关键词匹配用户意图,分发到对应的工具:
import re
@app.post("/agent", response_model=AgentResponse)
async def agent_invoke(request: AgentRequest):
user_msg = request.user_message.lower()
tool_calls = []
if "天气" in user_msg or "weather" in user_msg:
city = user_msg.replace("天气", "").replace("weather", "").strip() or "北京"
result = await weather_tool["handler"](city=city)
tool_calls.append({"tool": "get_weather", "args": {"city": city}, "result": result})
final = f"{city}今天天气:{result['condition']},气温{result['temperature']}°C"
elif "搜索" in user_msg or "search" in user_msg:
query = user_msg.replace("搜索", "").replace("search", "").strip()
result = await search_tool["handler"](query=query, limit=5)
tool_calls.append({"tool": "web_search", "args": {"query": query}, "result": result})
lines = [f"{i}. {r['title']}" for i, r in enumerate(result["results"], 1)]
final = f"搜索'{query}'找到{len(result['results'])}条结果:\n" + "\n".join(lines)
elif any(op in user_msg for op in ["+", "-", "*", "/", "计算"]):
numbers = re.findall(r'-?\d+\.?\d*', user_msg)
operators = [op for op in ["+", "-", "*", "/"] if op in user_msg]
if len(numbers) >= 2 and operators:
result = await calculator_tool["handler"](a=float(numbers[0]), b=float(numbers[1]), operator=operators[0])
tool_calls.append({"tool": "calculate", "result": result})
final = f"{numbers[0]} {operators[0]} {numbers[1]} = {result}"
else:
return AgentResponse(response="请提供完整的计算表达式,如:'计算 5 + 3'")
else:
return AgentResponse(response="支持:查天气、搜索信息、数学计算。请告诉我你想做什么?")
return AgentResponse(response=final or "处理完成", tool_calls=tool_calls)
这个 Agent 是简化版:用关键词匹配意图。生产环境中应使用 DeepSeek 等 LLM 来理解用户意图并决定调用哪个工具(见第 6 节)。
4. 主入口文件
# main.py
from mcp_server import app
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
5. 测试MCP Server
5.1 启动服务
cd fastapi-mcp-server
uvicorn main:app --reload --host 0.0.0.0 --port 8000
5.2 测试端点
# 1. 查看所有工具
curl http://localhost:8000/tools
# 2. 直接调用天气工具
curl -X POST http://localhost:8000/tools/call \
-H "Content-Type: application/json" \
-d '{"tool": "get_weather", "arguments": {"city": "上海"}}'
# 3. Agent调用(查天气)
curl -X POST http://localhost:8000/agent \
-H "Content-Type: application/json" \
-d '{"user_message": "上海天气怎么样"}'
# 4. Agent调用(搜索)
curl -X POST http://localhost:8000/agent \
-H "Content-Type: application/json" \
-d '{"user_message": "搜索Python FastAPI教程"}'
# 5. Agent调用(计算)
curl -X POST http://localhost:8000/agent \
-H "Content-Type: application/json" \
-d '{"user_message": "计算 128 + 456"}'
5.3 Swagger文档
打开 http://localhost:8000/docs 查看交互式API文档。
6. 与DeepSeek/Qwen集成
上面的 Agent 用关键词匹配意图,能力有限。生产环境中应使用 LLM 的 Tool Calling 能力——让 DeepSeek 自己决定调用哪个工具。
先定义 API 调用函数和工具描述(符合 OpenAI Tool Calling 格式):
import httpx
import json
import os
from typing import List, Dict
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
SYSTEM_PROMPT = """你是一个智能助手,可以调用以下工具:
- get_weather(city): 获取城市天气
- web_search(query, limit): 搜索网络信息
- calculate(a, operator, b): 执行数学计算
当需要使用工具时,返回 tool_calls 字段。"""
async def deepseek_chat(messages: List[Dict], tools: List[Dict]) -> Dict:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{DEEPSEEK_BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"},
json={"model": "deepseek-chat", "messages": messages, "tools": tools, "temperature": 0.7},
timeout=30.0,
)
return response.json()["choices"][0]["message"]
# 工具定义(OpenAI tool calling 格式)
openai_tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名称"}},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "web_search",
"description": "搜索网络信息",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"limit": {"type": "integer", "description": "返回数量", "default": 5}
},
"required": ["query"]
}
}
}
]
Agent 主循环的核心逻辑:发送消息给 DeepSeek → 如果返回 tool_calls 就执行工具 → 把结果追加到消息列表 → 再次调用,直到 LLM 返回普通文本:
from mcp_server import TOOLS
import asyncio
async def agent_loop(user_message: str):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]
MAX_ITERATIONS = 10
for _ in range(MAX_ITERATIONS):
response = await deepseek_chat(messages, openai_tools)
if not response.get("tool_calls"):
return response["content"] # LLM 返回最终回答
# 执行所有工具调用
for tool_call in response["tool_calls"]:
tool_name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"]) # JSON 字符串需要解析
tool = TOOLS.get(tool_name)
if tool:
handler = tool["handler"]
if asyncio.iscoroutinefunction(handler):
result = await handler(**args)
else:
result = handler(**args)
messages.append(response)
messages.append({"role": "tool", "tool_call_id": tool_call["id"], "content": str(result)})
return "达到最大迭代次数,未能生成最终回答"
为什么需要
MAX_ITERATIONS? 防止 LLM 陷入无限工具调用循环。10 次对于大多数场景足够,复杂任务可以适当增大。
7. 多工具协同示例
一个更复杂的场景:用户问"北京今天热吗?顺便搜一下有什么好玩的"。
async def multi_tool_example():
"""多工具协同示例"""
# 1. 先查天气
weather = await weather_tool["handler"](city="北京")
# 2. 根据天气决定搜索关键词
if weather["temp"] > 25:
search_query = "北京室内活动推荐"
else:
search_query = "北京户外景点推荐"
# 3. 执行搜索
search_results = await search_tool["handler"](query=search_query, limit=3)
# 4. 整合结果
return {
"weather": weather,
"suggestions": search_results["results"],
"summary": f"北京今天{weather['condition']},气温{weather['temperature']}°C。推荐:{search_results['results'][0]['title']}"
}
8. 踩坑记录
坑1:异步工具与同步工具混用
问题:定义工具时没注意handler可能是异步也可能是同步,调用时会出错。
解决:统一用asyncio.iscoroutinefunction()检查,如果是同步函数就直接调用,如果是异步函数就用await调用。
import asyncio
if asyncio.iscoroutinefunction(handler):
result = await handler(**args)
else:
result = handler(**args)
坑2:工具参数验证失败
问题:传入的参数格式不对(如少了必需字段),但只返回了通用错误。
解决:在调用前用Pydantic验证参数:
from pydantic import ValidationError
try:
validated = tool["input_schema"].parse_obj(arguments)
result = await handler(**validated.dict())
except ValidationError as e:
return {"error": f"参数错误: {e}"}
坑3:工具调用死循环
问题:Agent在某些情况下会反复调用同一工具(如工具返回结果不完整)。
解决:添加最大调用次数限制和结果验证:
MAX_TOOL_CALLS = 5
call_count = 0
while call_count < MAX_TOOL_CALLS:
call_count += 1
response = await agent_step()
if response["done"]:
break
# 验证工具返回是否有效
if not is_valid_result(response["result"]):
break
坑4:DeepSeek API超时
问题:工具调用涉及网络请求,可能超时。
解决:设置合理的超时时间,并提供重试:
async def call_with_retry(func, max_retries=3, timeout=10):
for i in range(max_retries):
try:
return await asyncio.wait_for(func(), timeout=timeout)
except asyncio.TimeoutError:
if i == max_retries - 1:
raise
continue
坑5:工具权限控制
问题:生产环境中,Agent可能会执行危险操作(如删除文件)。
解决:添加权限白名单和操作日志:
ALLOWED_TOOLS = ["get_weather", "web_search", "calculate"]
DENIED_TOOLS = ["delete_file", "exec_command"]
def check_tool_permission(tool_name: str) -> bool:
if tool_name in DENIED_TOOLS:
return False
if tool_name not in ALLOWED_TOOLS:
return False
return True
9. 总结
本文介绍了如何用FastAPI实现一个MCP Server,并构建简单的AI Agent系统:
- MCP协议:标准化的工具调用协议,让AI与外部服务解耦
- 工具定义:统一的工具格式,支持同步/异步handler
- Agent循环:Plan → Act → Observe → Decide
- 多工具协同:根据任务自动选择合适的工具组合
- 与DeepSeek集成:用大模型作为Agent的大脑
进阶方向:
- 添加记忆系统(保存对话历史)
- 实现更复杂的规划算法(ReAct、CoT)
- 添加自我反思能力(检查结果是否正确)
- 支持更多工具(数据库查询、文件操作等)
- 添加安全审计和权限控制
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)