找回密码
立即注册
搜索
发回帖 发新帖

6460

积分

0

好友

781

主题
发表于 14 小时前 | 查看: 5| 回复: 0

当答案可以从模型已有的知识中直接获得时,调用一次大语言模型 [1] 就够了,比如解释政策、起草回复或总结文本。但一旦答案依赖训练数据之外的信息,比如实时的订单状态或数据库中的某条记录,这种单次调用就不够用了。这正是 Agent 需要弥合的差距。

当模型能够调用其他东西——一个函数、一次数据库查询、一个 API——并在回答前使用返回结果时,它就变成了一个 AI Agent [2]。为了实现这一点,使用 Agent 编排框架 [3] 是很自然的选择。

但如果用纯 Python 搭配原始 API 从零编写,底层结构会清晰很多。核心其实很简单:一个模型、一个管理交互的循环,以及一组定义清晰的函数,为模型提供所需能力。去掉框架和抽象之后,这些组件如何协同工作就一目了然了。

Python从零构建AI Agent架构图:LLM与工具调用数据流

引言

本文涵盖:

  • 为什么需要工具调用,以及尝试它需要安装什么
  • 如何描述一个工具,使模型知道何时以及如何调用它
  • 请求-执行-响应循环在代码中是什么样的
  • 如何添加持久化记忆,使 Agent 能够在多轮交互中保持上下文
  • 你可以在这里 [4] 中找到本文配套的代码

前置条件

你需要 Python 3.10 [5] 或更高版本、一个支持 OpenAI 兼容模式的 API 密钥(如阿里云百炼 DashScope),以及 OpenAI Python SDK:

pip install openai

将你的 API 密钥设置为环境变量,这样客户端就可以自动获取它,而无需将密钥硬编码在任何地方:

export OPENAI_API_KEY="your-key-here"

注意: 在 Windows CMD 中应使用 set OPENAI_API_KEY=your-key-here(不加引号),避免引号被当作值的一部分。

这些就是开始所需要的一切。你可以在 agent.py [6] 脚本中找到全部代码。

设置模型调用

对 API 做一个最小封装,就是编写一个发送 prompt 并返回文本的函数:

from openai import OpenAI

client = OpenAI()

def ask(prompt):
    response = client.chat.completions.create(
        model="qwen-plus",
        max_tokens=512,
        messages=[
            {"role": "system", "content": "你是一个乐于助人的客服助手。回答要直接、基于事实。"},
            {"role": "user", "content": prompt},
        ],
    )
    return response.choices[0].message.content

这可以很好地处理相当一部分问题——例如解释一项政策、起草一封回复、总结一段文字。

但只要答案依赖于模型从未见过的数据,它就会失败。调用 ask("订单 #4471 的当前状态是什么?") 时,模型没有任何机制可以进行查询:这些信息存在于你的数据库中,而不是它的训练数据中。

它要么表示自己不知道,要么给出一个看似合理的答案,而无论怎样进行 prompt 调整都无法改变这一点,因为没有任何 prompt 能够让模型访问那些从未提供给它的数据。工具调用为模型提供了一种明确的方式,让它可以请求这些数据,而不是自行推断。

如果想进一步了解,请阅读《掌握 AI Agent 工具调用的路线图》 [8]。

定义一个模型可以请求调用的工具

让模型能够访问一个函数,意味着两件事:编写这个函数,以及编写一份模型能够读取的函数描述。这份描述很重要,因为它告诉模型这个函数是做什么的,以及什么时候应该调用它。

orders_db = {
    "4471": {"status": "已发货", "carrier": "顺丰速运", "eta": "2 天后送达"},
    "4472": {"status": "处理中", "carrier": None, "eta": None},
}

import json
from openai import OpenAI

client = OpenAI()

def get_order_status(order_id):
    return orders_db.get(order_id, {"error": "未找到该 ID 的订单"})

get_order_status_schema = {
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": (
            "根据订单 ID 查询客户订单的当前状态。"
            "当问题依赖于实时订单数据而非通用政策信息时,请始终使用此工具。"
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "要查询的订单 ID,例如 '4471'"},
            },
            "required": ["order_id"],
        },
    },
}

这个 schema 本质上是一份结构化元数据,包含函数名称、函数描述,以及函数所需要的参数定义。

工具调用(Tool use) [7] 在大多数模型提供商中的工作方式都类似:你需要向模型传入一个这样的工具列表。

执行工具,并将结果反馈给模型

传入 schema 后,事情就不一样了。模型返回的将不再是普通的文本答案,而是一个 finish_reason 为 tool_calls 的响应,其中包含一个 tool_calls 列表,描述模型想调用哪个函数,以及调用时使用什么参数。

注意:模型此时并没有真正执行任何操作。它只是暂停下来,返回一个请求,等待你来执行。

messages = [
    {"role": "system", "content": "你是一个乐于助人的客服助手。回答要直接、基于事实。"},
    {"role": "user", "content": "订单 #4471 的当前状态是什么?"},
]

response = client.chat.completions.create(
    model="qwen-plus",
    max_tokens=512,
    tools=[get_order_status_schema],
    messages=messages,
)

if response.choices[0].finish_reason == "tool_calls":
    message = response.choices[0].message
    tool_call = message.tool_calls[0]
    args = json.loads(tool_call.function.arguments)
    result = get_order_status(args["order_id"])

    messages.append(message)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": str(result),
    })

    final = client.chat.completions.create(
        model="qwen-plus",
        max_tokens=512,
        tools=[get_order_status_schema],
        messages=messages,
    )
    print(final.choices[0].message.content)

这里实际上发生了两次往返。

第一次,是询问模型它想做什么。你自己执行函数——这里是查询 orders_db;在生产环境中,则可能是调用订单服务。然后,把函数执行结果作为 tool_result 添加回对话,并再次发送整个上下文。

第二次往返时,模型读取这个工具返回的结果,并基于这个真实结果给出答案,而不是靠猜测。

AI Agent运行效果:订单状态查询输出

循环执行,直到模型获得所需的信息

当你不再把流程写死为一个工具、一次往返时,这种模式就可以进一步扩展。

一个更复杂的问题可能需要依次调用两三个工具——例如,先查询订单,再调用物流公司的追踪 API,最后整理成一条回复。而且,你事先并不知道模型到底需要调用几次工具。

因此,与其把每一步都写死,不如让程序循环执行,直到模型不再请求调用工具为止。

AI Agent工具调用循环流程图

def run_agent(user_input, tools, tool_map, max_iterations=6):
    messages = [
        {"role": "system", "content": "你是一个乐于助人的客服助手。回答要直接、基于事实。"},
        {"role": "user", "content": user_input},
    ]

    for _ in range(max_iterations):
        response = client.chat.completions.create(
            model="qwen-plus",
            max_tokens=512,
            tools=tools,
            messages=messages,
        )
        message = response.choices[0].message
        messages.append(message)

        if response.choices[0].finish_reason != "tool_calls":
            return message.content or ""

        for tool_call in message.tool_calls:
            function = tool_map[tool_call.function.name]
            args = json.loads(tool_call.function.arguments)
            output = function(**args)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(output),
            })

    return "已达到最大迭代次数,未获得最终答复。"

max_iterations 上限可以防止一种特定的故障:如果没有这个限制,模型可能不断判断自己还需要“再调用一次工具”,最终一直循环下去,直到预算耗尽,或者你失去耐心。

给循环设置一个上限,并在超过上限时返回一条明确的兜底消息,只需要增加几行代码,却可以避免生产环境中出现难以排查的问题。

去掉周围那些辅助代码后,整个机制其实就这么简单——一个模型、一个循环,以及一组模型可以请求你执行的函数。

这里没有任何东西是订单查询所特有的。你可以把工具替换成搜索、内部 API,或者你自己的数据库查询,只要 schema 对工具的功能和参数描述得足够清楚,同样的循环机制就可以处理它们。

添加记忆

按目前的写法,run_agent 在返回时就会忘记所有内容。连续调用它两次——第一次询问订单 #4471,第二次询问“那它什么时候到?”——第二次调用并不知道“它”指的是什么,因为每次调用都会从头开始创建一个全新的 messages 列表。

这里的记忆,意味着在多次调用之间保留这个列表,而不是将其丢弃。

最简单的做法,就是不再每次都传入一个新的 messages,而是开始将它保存在一个对象上:

class Agent:
    def __init__(self, tools, tool_map, max_iterations=6):
        self.tools = tools
        self.tool_map = tool_map
        self.max_iterations = max_iterations
        self.messages = [
            {"role": "system", "content": "你是一个乐于助人的客服助手。回答要直接、基于事实。"},
        ]

    def run(self, user_input):
        self.messages.append({"role": "user", "content": user_input})

        for _ in range(self.max_iterations):
            response = client.chat.completions.create(
                model="qwen-plus",
                max_tokens=512,
                tools=self.tools,
                messages=self.messages,
            )
            message = response.choices[0].message
            self.messages.append(message)

            if response.choices[0].finish_reason != "tool_calls":
                return message.content or ""

            for tool_call in message.tool_calls:
                function = self.tool_map[tool_call.function.name]
                args = json.loads(tool_call.function.arguments)
                output = function(**args)
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": str(output),
                })

        return "已达到最大迭代次数,未获得最终答复。"

self.messages 会在多次 run() 调用之间持续存在,因此模型每次都能看到完整的对话,包括之前的工具调用及其结果。先询问订单 #4471,然后再问“那它什么时候会到?”,模型就可以根据已经保存在 self.messages 中的历史记录,判断“它”指的是什么。

这涵盖了单个运行中的进程内的记忆。但它没有解决以下情况:当这个列表增长到超过模型的上下文窗口之后会发生什么,或者当进程重启、self.messages 被重置为空之后会发生什么。

这些都是这种方式的实际限制,也正因为如此,生产环境中的 Agent 通常会增加一个步骤,用来裁剪或总结较早的对话轮次;同时,还需要一个能够将对话持久化到进程之外的位置——例如数据库中的一行记录、一个文件,或者与会话 ID 关联的缓存键。

总结和下一步

到目前为止,已经有了一个模型调用、一个描述函数的工具 schema、一个不断执行工具调用直到模型获得足够信息来回答的循环,以及一个能够在多轮对话之间保存对话内容的类。

这就是一个可以工作的 Agent,而且其中的每一部分都是你自己编写的代码,可以逐行读懂。

接下来有几个值得探索的方向:

  • 当有多个工具都可能用于回答同一个问题时,测试模型会选择哪个工具
  • 持久化并裁剪对话历史,避免超出上下文窗口
  • 处理工具调用失败的情况,例如超时或上游 API 返回错误,避免整个循环崩溃
  • 在每次工具调用周围添加日志,方便调试和后续复盘

如果你对这类实践感兴趣,也欢迎在云栈社区交流更多技术细节。

引用链接

[1] https://www.ibm.com/think/topics/large-language-models
[2] https://cloud.google.com/discover/what-are-ai-agents
[3] https://www.langchain.com/resources/ai-agent-frameworks
[4] https://pan.baidu.com/s/1y_NJG6wbyzqO2A6jEEl2DA?pwd=92sh
[5] https://www.python.org/downloads/release/python-3100/
[6] https://github.com/balapriyac/ai-agent-from-scratch/blob/main/agent.py
[7] https://platform.openai.com/docs/guides/function-calling
[8] https://machinelearningmastery.com/the-roadmap-to-mastering-tool-calling-in-ai-agents/
[9] https://machinelearningmastery.com/how-and-why-to-build-an-ai-agent-from-scratch-in-python/




上一篇:Linux 负载飙到 100,CPU 却很闲:先查 D 状态进程,别急着扩容
下一篇:实测Claude Opus 5.5:从游戏复刻到催泪写作,真六边形战士
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-10 19:11 , Processed in 0.075395 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表