Python 开发 AI Agent 实战:从零搭建一个能调用工具的智能助手(2026 完整教程)
最近在做一个内部项目,需求很简单:用户问一句话,AI 自己判断要不要查天气、搜数据库、调计算器,然后把结果整合起来回复。听着简单对吧?但从"能跑"到"能用",中间踩了不少坑。
这篇文章把我的完整实现过程分享出来,从基础概念到可运行的代码,适合有 Python 基础、想上手 AI Agent 开发的同学。
什么是 AI Agent?跟普通聊天机器人有啥区别?
先别急着写代码,搞清楚概念很重要。
普通聊天机器人:你问什么,它答什么。输入 → 模型 → 输出,一条线走到底。
AI Agent:你给它一个目标,它自己规划步骤、调用工具、根据中间结果调整策略,最终完成任务。核心区别是——它会"思考"下一步该干什么。
举个例子:
- 聊天机器人:“帮我查一下北京明天的天气” → 回一段天气描述(可能是编的)
- AI Agent:“帮我查一下北京明天的天气” → 调用天气 API → 拿到真实数据 → 格式化返回
Agent 的关键能力有三个:
- 规划(Planning):拆解任务,决定执行顺序
- 工具调用(Tool Use):调 API、查数据库、执行代码
- 反思(Reflection):根据执行结果判断是否需要调整
技术选型
我选的方案:
- Python 3.11+
- OpenAI SDK(兼容各种模型的 Function Calling)
- Claude / GPT 系列模型(都支持原生 Tool Use)
- 不用 LangChain(对于这个规模的项目,直接调 SDK 更可控)
为什么不用 LangChain?不是说它不好,是对于 Agent 入门来说,直接用 SDK 能让你更清楚每一步在干什么。等你理解了底层原理,再上框架事半功倍。
第一步:定义工具(Tools)
Agent 的"手脚"就是工具。我们先定义三个常用的:
import json
import httpx
from datetime import datetime
# 工具 1:获取当前时间
def get_current_time(timezone: str = "Asia/Shanghai") -> str:
"""获取指定时区的当前时间"""
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo(timezone))
return now.strftime("%Y-%m-%d %H:%M:%S %Z")
# 工具 2:天气查询(用免费 API)
def get_weather(city: str) -> str:
"""查询城市天气"""
# 这里用 wttr.in 的免费 API
try:
resp = httpx.get(f"https://wttr.in/{city}?format=j1", timeout=10)
data = resp.json()
current = data["current_condition"][0]
return json.dumps({
"city": city,
"temperature": f"{current['temp_C']}°C",
"feels_like": f"{current['FeelsLikeC']}°C",
"description": current["weatherDesc"][0]["value"],
"humidity": f"{current['humidity']}%"
}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": f"查询失败: {str(e)}"})
# 工具 3:简单计算器
def calculate(expression: str) -> str:
"""计算数学表达式"""
try:
# 安全限制:只允许数学运算
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return json.dumps({"error": "不支持的字符"})
result = eval(expression) # 生产环境建议用 ast.literal_eval 或 sympy
return json.dumps({"expression": expression, "result": result})
except Exception as e:
return json.dumps({"error": f"计算失败: {str(e)}"})
几个注意点:
- 每个工具函数都返回 JSON 字符串,方便模型解析
- 异常处理要做好,Agent 调用失败不能直接崩
calculate里用了eval,生产环境一定要换成安全的实现
第二步:描述工具(让模型知道有哪些工具可用)
模型不会自动知道你有什么工具,你得用 JSON Schema 格式告诉它:
TOOLS = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "获取指定时区的当前时间。当用户询问时间相关问题时调用。",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "时区名称,如 Asia/Shanghai、America/New_York",
"default": "Asia/Shanghai"
}
},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气信息,包括温度、体感温度、天气描述和湿度。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,支持中文和英文,如 Beijing、上海"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "计算数学表达式,支持加减乘除和括号。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 (10 + 20) * 3"
}
},
"required": ["expression"]
}
}
}
]
关键经验:description 写得好不好,直接决定模型调工具准不准。写清楚"什么时候该调用"比"这个工具是什么"更重要。
第三步:实现 Agent 核心循环
Agent 的核心就是一个循环:发消息 → 模型决策 → 如果要调工具就调 → 把结果喂回模型 → 重复直到模型觉得可以直接回答了。
from openai import OpenAI
# 初始化客户端
client = OpenAI(
api_key="your-api-key",
base_url="https://api.ofox.ai/v1" # 国内直连,支持 Claude/GPT 等多模型
)
# 工具名称到函数的映射
TOOL_MAP = {
"get_current_time": get_current_time,
"get_weather": get_weather,
"calculate": calculate,
}
def run_agent(user_message: str, model: str = "claude-sonnet-4-20250514"):
"""运行 Agent,支持多轮工具调用"""
messages = [
{
"role": "system",
"content": (
"你是一个有用的助手,可以使用工具来帮助用户。"
"如果需要查询实时信息(时间、天气等),请调用相应工具。"
"如果不需要工具就能回答,直接回答即可。"
)
},
{"role": "user", "content": user_message}
]
max_rounds = 10 # 防止无限循环
for round_num in range(max_rounds):
print(f"\n--- Round {round_num + 1} ---")
response = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS,
tool_choice="auto" # 让模型自己决定要不要调工具
)
msg = response.choices[0].message
messages.append(msg) # 把模型回复加到对话历史
# 如果模型决定不调工具,直接返回
if not msg.tool_calls:
print(f"Agent 最终回复: {msg.content}")
return msg.content
# 执行所有工具调用
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"调用工具: {func_name}({func_args})")
# 执行工具
if func_name in TOOL_MAP:
result = TOOL_MAP[func_name](**func_args)
else:
result = json.dumps({"error": f"未知工具: {func_name}"})
print(f"工具返回: {result}")
# 把工具结果加到对话历史
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "Agent 达到最大轮次限制,请简化问题后重试。"
这段代码是整个 Agent 的核心,几个关键点:
tool_choice="auto":让模型自己判断要不要调工具,也可以设成"required"强制调用- 循环执行:模型可能需要多轮工具调用才能完成任务
max_rounds限制:防止模型陷入死循环,生产环境必备- 完整的消息历史:每一轮的工具调用和结果都要加到 messages 里,模型需要这些上下文
第四步:测试一下
if __name__ == "__main__":
# 测试 1:简单问题,不需要工具
print("=" * 50)
run_agent("Python 的 GIL 是什么?")
# 测试 2:需要调用一个工具
print("=" * 50)
run_agent("现在北京几点了?")
# 测试 3:需要调用多个工具
print("=" * 50)
run_agent("帮我查一下上海和东京的天气,然后算一下温差")
# 测试 4:复杂多步任务
print("=" * 50)
run_agent("现在几点了?如果是工作时间(9-18点),帮我查下北京天气")
运行测试 3 的输出大概是这样的:
--- Round 1 ---
调用工具: get_weather({"city": "上海"})
工具返回: {"city": "上海", "temperature": "18°C", ...}
调用工具: get_weather({"city": "东京"})
工具返回: {"city": "东京", "temperature": "14°C", ...}
--- Round 2 ---
调用工具: calculate({"expression": "18 - 14"})
工具返回: {"expression": "18 - 14", "result": 4}
--- Round 3 ---
Agent 最终回复: 查询结果如下:
- 上海当前气温 18°C
- 东京当前气温 14°C
- 温差为 4°C,上海比东京暖和一些。
可以看到模型自己规划了三步:先查两个城市的天气,再用计算器算温差,最后整合结果回复。这就是 Agent 的魅力。
进阶:添加错误重试和流式输出
基础版跑通之后,生产环境还需要几个改进:
import time
from openai import OpenAI, APITimeoutError, APIConnectionError
client = OpenAI(
api_key="your-api-key",
base_url="https://api.ofox.ai/v1"
)
def run_agent_v2(user_message: str, model: str = "claude-sonnet-4-20250514"):
"""带重试和流式输出的 Agent"""
messages = [
{"role": "system", "content": "你是一个有用的助手,可以使用工具来帮助用户。"},
{"role": "user", "content": user_message}
]
max_rounds = 10
max_retries = 3
for round_num in range(max_rounds):
# 带重试的 API 调用
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS,
tool_choice="auto",
timeout=30
)
break
except (APITimeoutError, APIConnectionError) as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # 指数退避
print(f"请求失败,{wait_time}秒后重试: {e}")
time.sleep(wait_time)
else:
raise
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content
# 并行执行工具调用(如果互不依赖)
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
try:
func_args = json.loads(tool_call.function.arguments)
result = TOOL_MAP.get(func_name, lambda **_: '{"error": "未知工具"}')(**func_args)
except Exception as e:
result = json.dumps({"error": f"工具执行失败: {str(e)}"})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "达到最大轮次限制"
改进点:
- 指数退避重试:网络不稳定时自动重试,不会直接挂掉
- 统一的异常处理:工具执行失败也不会中断 Agent 循环
- 超时控制:单次 API 调用 30 秒超时
踩坑记录
分享几个我实际开发中遇到的问题:
1. 工具描述写得太模糊,模型乱调
反面:"description": "获取天气" — 模型可能在用户问"天气怎么样?"(闲聊)时也去调 API。
正面:"description": "查询指定城市的实时天气。当用户明确询问某个城市的天气状况时调用。" — 加上"什么时候该调用"的说明。
2. 忘了处理模型并行调用工具
Claude 和 GPT 都支持一次返回多个 tool_calls。如果你的代码只处理第一个,就会丢数据。一定要遍历 msg.tool_calls 列表。
3. 消息历史越来越长,Token 爆了
Agent 跑多轮之后,messages 会很长。两个方案:
- 设置
max_rounds限制轮次 - 超过一定长度时,只保留 system message + 最近 N 轮
4. eval() 安全问题
计算器那个 eval() 在生产环境千万别直接用。可以用 ast.literal_eval 处理简单表达式,或者用 sympy 库做安全计算。
完整项目结构
ai-agent/
├── agent.py # Agent 核心逻辑
├── tools/
│ ├── __init__.py
│ ├── time_tool.py # 时间工具
│ ├── weather.py # 天气工具
│ └── calculator.py # 计算器
├── config.py # 配置(API Key、模型选择等)
├── requirements.txt
└── main.py # 入口
requirements.txt:
openai>=1.30.0
httpx>=0.27.0
下一步可以做什么
这篇文章搭了一个基础 Agent 框架,你可以在这个基础上扩展:
- 加更多工具:搜索引擎、数据库查询、文件读写、代码执行
- 加记忆:用 SQLite 或 Redis 存对话历史,让 Agent 有长期记忆
- 接入 MCP:用 Model Context Protocol 标准化工具接入,可以一次对接大量工具
- 多 Agent 协作:让多个专业 Agent 各司其职,通过消息传递协作
AI Agent 开发没有想象中那么难,核心就是工具定义 + 循环调用 + 结果整合。理解了这个模式,剩下的就是往里面填工具和优化体验。
希望这篇对你有帮助,有问题欢迎评论区讨论。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)