一、引子:大模型缺的那条腿
你有没有遇到过这种情况------你问 ChatGPT "今天北京的天气怎么样",它说"我无法实时获取天气信息"?
这不是模型不够聪明,而是它天生缺了一条腿:无法和外部世界交互。大模型的知识截止于训练数据的那一刻,它不知道你的数据库里有什么,也没法直接调用外部 API。
2024 年 OpenAI 推出了 Function Calling(函数调用)能力,让模型不仅能"说话",还能"动手"------调用 API、查数据库、发邮件、搜网页。这就是 Agent 架构最核心的基石。
今天我们就从零手写一个能自动调用工具的 Agent,你不需要任何框架,只需要 Python 和一点耐心。
二、Agent 核心逻辑:三步循环
别管任何花哨的名词,Agent 的本质就是一个简单的循环:
| 步骤 | 做什么 | 谁来干 |
|---|---|---|
| ① 思考 | 理解用户问题,决定是否需要调用工具 | LLM |
| ② 调用 | 输出结构化参数(JSON),程序执行对应函数 | 你的代码 |
| ③ 反馈 | 把工具执行结果送回 LLM,让它生成最终回答 | LLM + 代码 |
这个三步循环跑一次叫"单轮工具调用",跑多次叫"多轮 Agent"。
很多人把 Agent 和 RAG 搞混,它们的区别其实很清晰:
| 能力维度 | RAG | Agent(Function Calling) |
|---|---|---|
| 检索知识库 | ✅ 向量搜索 | ✅ 可调用搜索 API |
| 操作数据库 | ❌ | ✅ 执行 SQL 或 CRUD |
| 调用外部 API | ❌ | ✅ GET/POST 任意接口 |
| 获取实时数据 | ❌ | ✅ 天气/股票/新闻 |
| 自主决策 | ❌ | ✅ 判断何时调、调哪个工具 |
| 多步推理 | ❌ | ✅ 链式思考与中间结果复用 |
一句话总结:RAG 让 AI"知道更多",Agent 让 AI"能做更多"。
三、Function Calling 到底怎么工作的?
OpenAI 的 Chat Completions API 提供了一个 tools 参数,你可以把"函数定义"以 JSON Schema 的格式传给模型:
python
import json
from openai import OpenAI
client = OpenAI()
# 定义工具(JSON Schema 格式)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海"
}
},
"required": ["city"]
}
}
}
]
# 第一次调用:模型决定是否要调工具
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools
)
msg = response.choices[0].message
if msg.tool_calls:
print(f"模型选择了工具:{msg.tool_calls[0].function.name}")
print(f"参数:{msg.tool_calls[0].function.arguments}")
# 输出:模型选择了工具:get_weather
# 输出:参数:{"city": "北京"}
模型返回的 tool_calls 里包含了工具名称 和参数,你只需要解析它、执行对应的函数、把结果送回模型------Agent 循环就完成了。
关键点在于:模型只负责"决定调哪个工具、传什么参数",不负责"执行"。执行工具拿到结果后,再送一次请求让模型生成人类可读的回答。
四、实战:80 行代码写一个能搜索和计算的 Agent
下面是一个完整的 Agent 实现,它有两个工具:一个模拟搜索引擎,一个做数学计算。
python
import json
from openai import OpenAI
client = OpenAI()
# --- 工具函数 ---
def search_web(query: str) -> str:
"""模拟搜索引擎,实际项目可替换为 Bing/SerpAPI 等"""
results = {
"北京天气": "北京今天晴,气温 25-32°C,湿度 40%",
"北京人口": "北京市常住人口约 2188 万(2024年)",
}
return results.get(query, f"未找到「{query}」的相关信息")
def calculator(expr: str) -> str:
"""安全计算器,只允许数字和四则运算"""
try:
allowed = set("0123456789+-*/.()")
if not all(c in allowed for c in expr):
return "表达式包含非法字符"
return str(eval(expr))
except Exception as e:
return f"计算错误:{str(e)}"
# --- 工具注册表(映射 name → 函数) ---
FUNCTIONS = {
"search_web": search_web,
"calculator": calculator,
}
TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "搜索实时信息,适合查天气、新闻、百科",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "执行数学计算,支持加减乘除",
"parameters": {
"type": "object",
"properties": {
"expr": {"type": "string", "description": "数学表达式"}
},
"required": ["expr"]
}
}
}
]
# --- Agent 主循环 ---
def agent_loop(user_input: str, max_turns: int = 5) -> str:
messages = [{"role": "user", "content": user_input}]
for turn in range(max_turns):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS
)
msg = response.choices[0].message
# 模型不再需要工具 → 返回最终回答
if not msg.tool_calls:
return msg.content
# 把模型的工具调用请求加入对话历史
messages.append(msg)
# 逐个执行工具,把结果送回
for tc in msg.tool_calls:
func_name = tc.function.name
args = json.loads(tc.function.arguments)
result = FUNCTIONS[func_name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
return "已超过最大推理轮数"
# --- 测几个例子 ---
print(agent_loop("北京人口是多少?"))
# 输出:北京市常住人口约 2188 万
print(agent_loop("计算 (35 + 28) * 2 的结果"))
# 输出:126
print(agent_loop("查一下北京天气,然后帮我算如果温度降低 5 度是多少"))
# Agent 先调 search_web 查天气 → 拿到 25-32°C → 再调 calculator 算 32-5 → 最终回答
核心逻辑不到 60 行,但已经具备了一个 Agent 的全部要素:
- ✅ 工具定义:JSON Schema 描述函数签名
- ✅ 模型判断:LLM 自主决定是否调用、调哪个
- ✅ 函数执行:代码安全执行并返回结果
- ✅ 结果反馈:工具结果送回模型,生成自然语言回答
- ✅ 多轮循环:上一轮结果可以做下一轮输入
五、从手写到框架:什么时候该用什么?
手写 Function Calling 灵活性高、零依赖,适合原型验证。如果要上生产,框架能帮你省很多事:
| 使用场景 | 推荐方案 | 优势 |
|---|---|---|
| 快速原型验证 | OpenAI SDK 直接手写 | 零依赖,完全可控 |
| 单 Agent 复杂工具链 | LangChain Agent | 丰富的工具生态和内置 memory |
| 多 Agent 协作编排 | LangGraph / CrewAI | 内置状态机 + 图编排 |
| 企业级 .NET/Java 集成 | Semantic Kernel(微软) | 强类型,企业级链路追踪 |
| 低代码 AI 应用 | Dify / Coze | 可视化编排,内置 RAG + Agent |
六、避坑指南
这几个坑我亲身体验过,帮你省时间:
-
工具描述要写得像说明书 。模型理解工具的唯一渠道就是
description字段。写"搜索信息"不如写"搜索最新的实时信息,适合查天气、新闻、百科类问题"。 -
参数名要有语义 。
query比q好,city_name比city更明确------模型能通过参数名推断含义。 -
tool_call_id必须一一对应。多工具并行调用时,每个工具结果必须匹配正确的 ID,否则 API 报错。 -
注意 Token 消耗 。
tools定义本身会占用 Token。工具多了(10+ 个),光工具定义就可能吃掉 2000+ Token。 -
工具失败不要抛异常,把错误信息返回给模型。这样模型可以自主决定换参数重试,而不是让你的程序 crash。
-
限制 max_turns。防止 Agent 陷入死循环------5 轮通常是合理的上限。
七、总结
Agent 架构听起来高大上,拆开来看核心就是三步循环:LLM 思考 → 调用工具 → 反馈结果。Function Calling 让它成为可能,框架让它变得可维护。
下一期预告:当 Agent 遇到 RAG------如何让 AI 助手既能查知识库,又能操作数据库,真正实现"能问能查能做"。
关注我,不错过后续内容。欢迎在评论区讨论你的 Agent 使用场景或踩过的坑。
扩展阅读:Function Calling 与 MCP 的关系
如果你读过我之前的 MCP 系列文章,可能会问:Function Calling 和 MCP 有什么区别?
简单说,Function Calling 是一种机制,MCP 是一种协议。Function Calling 定义了"模型怎么告诉程序我要调什么工具"的接口格式,而 MCP 定义了"工具服务器和 AI 客户端之间怎么通信"的完整协议。两者不是替代关系,而是互补关系------你完全可以在 MCP Server 内部用 Function Calling 来实现工具调度。
工具函数的设计原则
写工具函数时,有几个设计原则值得记住:
原则一:函数的输入输出都应该是字符串。 不管是搜索 API 返回的 JSON、数据库查询结果、还是计算器算出的数字------最终给模型看到的都应该是一个人类可读的字符串。模型理解自然语言远比理解结构化数据容易。
原则二:工具粒度要适中。 一个工具函数只做一件事。把"搜索+计算+发邮件"塞进一个函数比拆成三个函数难维护得多,而且模型更可能用错参数。
原则三:错误信息也要有信息量。 "查询失败"不如"查询失败:API 返回 500,可能是网络超时"------后者让模型有机会做出合理的后续决策(比如等一下再重试)。