大白话构建一个最小可用 Agent
目标: 快速上手,一个完整可运行的最小 Agent 示例。本文将深入浅出地解释 Agent 的大脑和行动部分,带你从零开始,构建一个能听懂、会思考、更能行动的智能助理。
在人工智能的浪潮之巅,大型语言模型(LLM)的强大能力已经毋庸置疑。然而,一个只会聊天的模型,终究只是一个“数字鹦鹉”。要让它真正成为我们生活和工作中的得力助手,就需要赋予它行动的能力——这,就是 Agent 的核心使命。
本文将摒弃复杂晦涩的理论,用最直白的大白话,带你一步步构建一个最小可用(Minimal Viable)的 Agent。读完本文,你不仅能拥有一个可以运行的 Agent 代码,更能深刻理解其背后的工作原理,为你将来构建更复杂的智能体打下坚实的基础。
1. 核心理念:解构 Agent 的“大脑”与“行动”
在开始敲代码之前,我们必须先建立一个清晰的心智模型。一个 Agent 主要由两个核心部分组成:
大脑(Brain): 这是 Agent 的决策核心,通常由一个强大的大型语言模型(如 GPT-4)扮演。它的职责是“思考”和“决策”。具体来说,它需要:
- 理解用户意图: 弄明白用户用自然语言提出的需求到底是什么。
- 选择合适的工具: 根据用户的意图,从一堆可用工具(比如查天气、转账)中,判断应该使用哪一个。
- 提取关键参数: 从用户的话语中,解析出执行工具所必需的信息(比如查天气需要“城市”,转账需要“收款人”和“金额”)。
行动(Action): 这是 Agent 的执行部分,由一系列我们预先定义好的“工具”(Tools)组成。每个工具都是一个具体的函数,负责执行一项特定任务。它可以是:
- 调用一个外部 API(如天气查询 API)。
- 执行一段内部代码(如操作数据库、读写文件)。
- 与另一个系统进行交互。
工作流程的本质,就是一场“大脑”与“行动”之间的精妙协作:用户提出问题 -> 大脑(LLM) 进行理解和决策,决定调用哪个 行动(Tool) 并准备好参数 -> 行动(Tool) 被执行,并返回结果 -> 大脑(LLM) 再将这个执行结果,以更自然、更人性化的语言反馈给用户。
而连接“大脑”和“行动”的关键桥梁,就是 OpenAI 提供的 Function Calling 功能。
2. 准备环境与依赖
要开始构建,我们需要准备好两样东西:OpenAI 的 Python 库和一个用于管理环境变量的库。
首先,请确保你已经安装了 Python。然后,在你的项目目录下,打开终端并执行以下命令来安装必要的库:
pip install openai python-dotenv
openai
: 这是 OpenAI 官方提供的 Python 客户端,我们将用它来与 GPT 模型进行交互。python-dotenv
: 这是一个非常实用的小工具,它可以帮助我们从一个.env
文件中加载环境变量,避免将我们的 API 密钥硬编码在代码中,从而保证安全。
接下来,在你的项目根目录下创建一个名为 .env
的文件,并在其中写入你的 OpenAI API 密钥:
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
请将 sk-xxxxxxxx...
替换为你自己的真实密钥。
3. 第一步:定义你的第一个“行动” - 天气查询工具
让我们从一个最经典的例子开始:创建一个能查询天气的工具。在现实世界中,我们可能会调用一个真实的天气 API。但为了让这个最小示例足够简单,我们先“假装”有一个天气查询函数。
这个函数就是一个纯粹的 Python 函数,它接收必要的参数(地点和温度单位),然后返回一个模拟的天气信息。
代码示例 (tools.py
):
import json
def get_current_weather(location: str, unit: str = "celsius") -> str:
"""
获取指定地点的当前天气信息。
Args:
location (str): 需要查询天气的城市,例如:北京, 东京。
unit (str): 温度单位,可以是 'celsius' 或 'fahrenheit'。
Returns:
str: 一个描述天气状况的 JSON 字符串。
"""
if "北京" in location.lower():
return json.dumps({"location": "北京", "temperature": "10", "unit": unit})
elif "东京" in location.lower():
return json.dumps({"location": "东京", "temperature": "8", "unit": unit})
else:
return json.dumps({"location": location, "temperature": "22", "unit": unit})
深度解析:
- 清晰的函数定义: 这个
get_current_weather
函数非常标准,它有明确的参数location
和unit
,并且通过类型提示(type hints)标明了它们的类型。 - 详尽的 Docstring: 这是至关重要的一步!函数下方的文档字符串(docstring)不仅仅是给开发者看的注释。在 Function Calling 机制中,这段描述会直接被发送给 LLM。LLM 正是依靠这个描述来理解该工具的功能、何时应该使用它,以及每个参数的含义。一个清晰、准确的描述,是“大脑”能否成功指挥“行动”的关键。
- 标准化的返回: 我们使用
json.dumps
将返回结果格式化为 JSON 字符串。这是一种良好的实践,结构化的数据更便于后续处理。
4. 第二步:构造调用链 - 让“大脑”学会使用工具
现在我们有了一个“行动”工具,接下来就要看如何让“大脑”(LLM)理解并使用它了。这就是 Function Calling 发挥作用的地方。
我们需要将我们定义好的工具,按照 OpenAI 指定的格式,“注册”给 LLM。这个格式本质上是一个 JSON Schema,它精确地描述了函数的名称、功能、以及所有参数的类型和描述。
代码示例 (main.py
):
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
from tools import get_current_weather
# 加载 .env 文件中的环境变量
load_dotenv()
# 初始化 OpenAI 客户端
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def run_conversation(user_prompt: str):
"""
主对话流程,负责与 LLM 交互并调用工具。
"""
messages = [{"role": "user", "content": user_prompt}]
# 1. 定义工具:将 Python 函数“翻译”成 LLM 能理解的格式
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "获取指定地点的当前天气信息。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "需要查询天气的城市,例如:北京, 东京",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
# 2. 第一次调用 LLM:让大脑决策
print("--- 步骤 1: 大脑(LLM)进行决策 ---")
response = client.chat.completions.create(
model="gpt-4-turbo", # 推荐使用能力更强的模型
messages=messages,
tools=tools,
tool_choice="auto", # 让模型自动决定是否调用函数
)
response_message = response.choices[0].message
print(f"LLM 决策结果: {response_message}")
# 3. 检查 LLM 是否决定调用工具
tool_calls = response_message.tool_calls
if tool_calls:
print("\n--- 步骤 2: 执行行动(Tool) ---")
# LLM 决定调用工具,我们在这里执行它
available_tools = {
"get_current_weather": get_current_weather,
}
# 将 LLM 的决策追加到对话历史中
messages.append(response_message)
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_tools[function_name]
function_args = json.loads(tool_call.function.arguments)
print(f"执行工具: {function_name}({function_args})")
# 调用函数并获取结果
function_response = function_to_call(**function_args)
print(f"工具执行结果: {function_response}")
# 将工具的执行结果追加到对话历史中
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
# 4. 第二次调用 LLM:让大脑总结结果
print("\n--- 步骤 3: 大脑(LLM)进行总结 ---")
second_response = client.chat.completions.create(
model="gpt-4-turbo",
messages=messages,
)
print("Agent 最终回复:")
return second_response.choices[0].message.content
else:
# 如果 LLM 决定不调用任何工具,直接返回其回答
print("\n--- LLM 决定不调用工具,直接回复 ---")
return response_message.content
# --- 示例演示 ---
if __name__ == "__main__":
# 示例 1: 查询天气
final_answer = run_conversation("你好,请问现在北京的天气怎么样?")
print(final_answer)
深度解析这段核心逻辑:
定义工具 (tools): 我们创建了一个
tools
列表。列表中的每一个对象都代表一个可供“大脑”使用的“行动”。"description"
和parameters
中的"description"
字段,就是我们之前强调的、直接来自于函数 docstring 的关键信息。这是整个 Function Calling 的基石。第一次调用 LLM(决策阶段):
- 我们将用户的原始问题
user_prompt
和我们定义好的tools
一起发送给 LLM。 tool_choice="auto"
是一个关键参数,它授权 LLM 自行判断:是直接回答用户问题,还是需要调用一个或多个工具来辅助回答。- 如果 LLM 判断需要调用工具,它的返回结果将不会是常规的文本回答,而是一个
tool_calls
对象。这个对象清晰地指明了它想调用哪个函数(function.name
)以及从用户问题中提取出的参数(function.arguments
)。这完美地体现了“大脑”的决策过程。
- 我们将用户的原始问题
执行行动(本地代码):
- 我们的代码接收到
tool_calls
后,就进入了“行动”阶段。 - 我们通过
function_name
在一个预设的available_tools
字典中找到并调用了真实的 Python 函数get_current_weather
。 - 我们将函数执行返回的真实天气信息
function_response
拿到。
- 我们的代码接收到
第二次调用 LLM(总结阶段):
- 我们不能直接把
{"location": "北京", "temperature": "10", "unit": "celsius"}
这样的原始 JSON 数据丢给用户。 - 因此,我们将工具的执行结果(
function_response
)连同之前的对话历史,再次发送给 LLM。 - 这次调用的目的是让“大脑”用它强大的自然语言能力,将这个结构化的、冷冰冰的数据,“翻译”成一句通顺、人性化的话语返回给用户。例如,它会说:“现在北京的温度是10摄氏度。”
- 我们不能直接把
这个 “决策 -> 行动 -> 总结” 的三步循环,就是最基础、但也是最核心的 Agent 工作流。
5. 第三步:扩展!轻松接入多个工具
让 Agent 只会查天气显然是不够的。一个强大的 Agent 应该能够根据用户需求,在多个工具中自如切换。扩展多个工具非常简单,我们只需要遵循两个原则:
- 在
tools.py
中定义更多的函数。 - 在
main.py
的tools
列表和available_tools
字典中“注册”这些新函数。
让我们来增加两个新工具:转账和查字典。
代码更新 (tools.py
):
import json
def get_current_weather(location: str, unit: str = "celsius") -> str:
# ... (代码同上) ...
def transfer_money(to_account: str, amount: float, currency: str = "CNY") -> str:
"""
向指定账户转账。
Args:
to_account (str): 收款人的账户名。
amount (float): 转账金额。
currency (str): 货币单位,默认为 'CNY'。
Returns:
str: 一个描述转账结果的 JSON 字符串。
"""
return json.dumps({"status": "success", "to_account": to_account, "amount": amount, "currency": currency})
def lookup_dictionary(word: str) -> str:
"""
查询一个单词的定义。
Args:
word (str): 需要查询的单词。
Returns:
str: 一个包含单词定义的 JSON 字符串。
"""
definitions = {
"agent": "在人工智能领域,指能够感知环境、进行决策并采取行动的智能实体。",
"llm": "大型语言模型(Large Language Model)的缩写。"
}
definition = definitions.get(word.lower(), "抱歉,词典里没有找到这个词。")
return json.dumps({"word": word, "definition": definition})
代码更新 (main.py
):
# ... (imports and client initialization) ...
from tools import get_current_weather, transfer_money, lookup_dictionary
def run_conversation(user_prompt: str):
messages = [{"role": "user", "content": user_prompt}]
# 1. 定义所有可用的工具
tools = [
# ... (get_current_weather 的定义同上) ...
{
"type": "function",
"function": {
"name": "transfer_money",
"description": "向指定账户转账。",
"parameters": {
"type": "object",
"properties": {
"to_account": {"type": "string", "description": "收款人的账户名。"},
"amount": {"type": "number", "description": "转账金额。"},
"currency": {"type": "string", "description": "货币单位,默认为 'CNY'。"},
},
"required": ["to_account", "amount"],
},
},
},
{
"type": "function",
"function": {
"name": "lookup_dictionary",
"description": "查询一个单词的定义。",
"parameters": {
"type": "object",
"properties": {
"word": {"type": "string", "description": "需要查询的单词。"},
},
"required": ["word"],
},
},
}
]
# ... (第一次调用 LLM 的代码不变) ...
tool_calls = response_message.tool_calls
if tool_calls:
# 2. 将所有工具函数放入一个字典,方便查找
available_tools = {
"get_current_weather": get_current_weather,
"transfer_money": transfer_money,
"lookup_dictionary": lookup_dictionary,
}
# ... (后续的工具执行和第二次 LLM 调用代码完全不变) ...
神奇之处: 请注意,我们几乎没有修改 run_conversation
函数的核心逻辑。我们只是简单地将新工具的信息添加到了 tools
列表和 available_tools
字典中。
现在,你可以尝试向它提出新的问题了:
run_conversation("帮我给张三转100块钱")
-> 大脑(LLM) 会准确地选择transfer_money
工具,并解析出to_account="张三"
和amount=100
。run_conversation("Agent 是什么意思?")
-> 大脑(LLM) 会选择lookup_dictionary
工具,并解析出word="Agent"
。
这就是 Function Calling 模式的优雅之处:扩展性极强。你可以不断地为你的 Agent 增加新的“行动”能力,而“大脑”的决策逻辑却无需重写。它会根据你提供的工具描述,智能地进行调度。
6. 局限与思考:这只是一个开始
我们已经成功构建了一个最小可用的 Agent,它能理解、决策并行动。但它依然非常初级,存在着一些明显的限制:
没有记忆 (No Memory): 我们的
run_conversation
函数每次都是从一个空的messages
列表开始(除了用户的第一次提问)。它完全不记得之前的对话历史。你问完北京天气,再问“那里适合穿什么?”,它会一头雾水,因为它已经忘了“那里”指的是北京。真正的 Agent 需要有短期和长期记忆机制。没有多轮对话规划 (No Multi-turn Conversation): 当前的架构是“一问一答,一轮解决”。对于需要多步骤、多轮交互才能完成的复杂任务(比如“帮我规划一个北京三日游,并预订机票和酒店”),这个简单的 Agent 就无能为力了。
没有规划器 (No Planner): 我们的 Agent 只能在预设的工具中做“单选题”。它无法自主地将一个复杂任务分解成多个子任务,并按顺序、有逻辑地调用一系列工具来完成。例如,要完成“查询北京和东京的天气,并告诉我哪个城市更冷”这个任务,一个高级的 Agent 需要规划出两步:第一步调用
get_current_weather(location="北京")
,第二步调用get_current_weather(location="东京")
,最后再比较两个结果。没有function calling: 我们目前的案例适用于有 function calling 的LLM, 但是有时候我们需要使用没有function calling 功能的大模型作为 agent 的基座充当大脑,这个该怎么办呢?后期会单独分享
这些限制,恰恰是通往更高级 Agent 的阶梯。诸如 LangChain/LangGraph、AutoGen、Open-Agent-Python-SDK … 这样的框架,正是为了解决记忆、规划、多工具链式调用等复杂问题而生的。
但无论上层框架如何封装,其最底层的核心逻辑,都离不开我们今天所构建的这个 “大脑(LLM)+ 行动(Tools)+ Function Calling(连接桥梁)” 的黄金三角。
7. 工具只能自定义吗?MCP协议 / A2A协议又是什么?
后期会单独写一篇文章讲透function calling 和 MCP协议 和 A2A 协议