大白话构建一个最小可用 AI Agent

发布于:2025-07-03 ⋅ 阅读:(42) ⋅ 点赞:(0)

目标: 快速上手,一个完整可运行的最小 Agent 示例。本文将深入浅出地解释 Agent 的大脑和行动部分,带你从零开始,构建一个能听懂、会思考、更能行动的智能助理。


在人工智能的浪潮之巅,大型语言模型(LLM)的强大能力已经毋庸置疑。然而,一个只会聊天的模型,终究只是一个“数字鹦鹉”。要让它真正成为我们生活和工作中的得力助手,就需要赋予它行动的能力——这,就是 Agent 的核心使命。

本文将摒弃复杂晦涩的理论,用最直白的大白话,带你一步步构建一个最小可用(Minimal Viable)的 Agent。读完本文,你不仅能拥有一个可以运行的 Agent 代码,更能深刻理解其背后的工作原理,为你将来构建更复杂的智能体打下坚实的基础。

1. 核心理念:解构 Agent 的“大脑”与“行动”

在开始敲代码之前,我们必须先建立一个清晰的心智模型。一个 Agent 主要由两个核心部分组成:

  1. 大脑(Brain): 这是 Agent 的决策核心,通常由一个强大的大型语言模型(如 GPT-4)扮演。它的职责是“思考”和“决策”。具体来说,它需要:

    • 理解用户意图: 弄明白用户用自然语言提出的需求到底是什么。
    • 选择合适的工具: 根据用户的意图,从一堆可用工具(比如查天气、转账)中,判断应该使用哪一个。
    • 提取关键参数: 从用户的话语中,解析出执行工具所必需的信息(比如查天气需要“城市”,转账需要“收款人”和“金额”)。
  2. 行动(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 函数非常标准,它有明确的参数 locationunit,并且通过类型提示(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)

深度解析这段核心逻辑:

  1. 定义工具 (tools): 我们创建了一个 tools 列表。列表中的每一个对象都代表一个可供“大脑”使用的“行动”。"description"parameters 中的 "description" 字段,就是我们之前强调的、直接来自于函数 docstring 的关键信息。这是整个 Function Calling 的基石。

  2. 第一次调用 LLM(决策阶段):

    • 我们将用户的原始问题 user_prompt 和我们定义好的 tools 一起发送给 LLM。
    • tool_choice="auto" 是一个关键参数,它授权 LLM 自行判断:是直接回答用户问题,还是需要调用一个或多个工具来辅助回答。
    • 如果 LLM 判断需要调用工具,它的返回结果将不会是常规的文本回答,而是一个 tool_calls 对象。这个对象清晰地指明了它想调用哪个函数(function.name)以及从用户问题中提取出的参数(function.arguments)。这完美地体现了“大脑”的决策过程。
  3. 执行行动(本地代码):

    • 我们的代码接收到 tool_calls 后,就进入了“行动”阶段。
    • 我们通过 function_name 在一个预设的 available_tools 字典中找到并调用了真实的 Python 函数 get_current_weather
    • 我们将函数执行返回的真实天气信息 function_response 拿到。
  4. 第二次调用 LLM(总结阶段):

    • 我们不能直接把 {"location": "北京", "temperature": "10", "unit": "celsius"} 这样的原始 JSON 数据丢给用户。
    • 因此,我们将工具的执行结果(function_response)连同之前的对话历史,再次发送给 LLM。
    • 这次调用的目的是让“大脑”用它强大的自然语言能力,将这个结构化的、冷冰冰的数据,“翻译”成一句通顺、人性化的话语返回给用户。例如,它会说:“现在北京的温度是10摄氏度。”

这个 “决策 -> 行动 -> 总结” 的三步循环,就是最基础、但也是最核心的 Agent 工作流。

5. 第三步:扩展!轻松接入多个工具

让 Agent 只会查天气显然是不够的。一个强大的 Agent 应该能够根据用户需求,在多个工具中自如切换。扩展多个工具非常简单,我们只需要遵循两个原则:

  1. tools.py 中定义更多的函数。
  2. main.pytools 列表和 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,它能理解、决策并行动。但它依然非常初级,存在着一些明显的限制:

  1. 没有记忆 (No Memory): 我们的 run_conversation 函数每次都是从一个空的 messages 列表开始(除了用户的第一次提问)。它完全不记得之前的对话历史。你问完北京天气,再问“那里适合穿什么?”,它会一头雾水,因为它已经忘了“那里”指的是北京。真正的 Agent 需要有短期和长期记忆机制。

  2. 没有多轮对话规划 (No Multi-turn Conversation): 当前的架构是“一问一答,一轮解决”。对于需要多步骤、多轮交互才能完成的复杂任务(比如“帮我规划一个北京三日游,并预订机票和酒店”),这个简单的 Agent 就无能为力了。

  3. 没有规划器 (No Planner): 我们的 Agent 只能在预设的工具中做“单选题”。它无法自主地将一个复杂任务分解成多个子任务,并按顺序、有逻辑地调用一系列工具来完成。例如,要完成“查询北京和东京的天气,并告诉我哪个城市更冷”这个任务,一个高级的 Agent 需要规划出两步:第一步调用 get_current_weather(location="北京"),第二步调用 get_current_weather(location="东京"),最后再比较两个结果。

  4. 没有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 协议


网站公告

今日签到

点亮在社区的每一天
去签到