一、Agent

在上一篇文章AI 大模型agent开发入门(一)- 理解API接口-CSDN博客中,我们沿着一次 API 请求的完整链路,理解了 API Key、Base URL、Messages、Token、上下文窗口和 Streaming。

接下来我们来看一下下面这个场景,用户问:

北京今天适合穿什么衣服?

模型可能会根据已有知识给出一些建议。但这里存在一个明显的问题:

模型并不知道北京今天的真实天气。

它只能根据训练阶段学到的知识进行推测,无法主动查询实时天气,也无法读取数据库、操作文件或者调用其他业务接口。普通大模型应用解决的是“如何回答问题”,而 Agent 要解决的是:

如何根据用户目标判断下一步,并调用外部工具真正完成任务。

 因此,Agent 并不是一种完全不同的大模型。更准确地说,Agent 是一套围绕大模型建立的执行系统:

大模型
+ 结构化数据
+ 外部工具
+ 执行循环

本文将从这三个最核心的问题开始:

  • 如何让模型输出程序能够稳定读取的数据;
  • 如何让模型调用外部工具;
  • 如何把多次模型调用和工具调用组织成一个 Agent。

假设我们正在开发一个订单处理AI客服,希望AI客服能解决用户的问题,并且希望模型帮助用户的售后请求。

用户输入:

我昨天买的耳机只有左边有声音,想换一个新的。

模型可能向系统反馈:

用户购买的耳机存在质量问题,建议为用户办理换货。

这段回答对人来说非常清楚,但程序很难直接使用。
程序真正需要的可能是:

{
  "issue_type": "quality_problem",
  "requested_action": "replacement",
  "urgency": "normal"
}

有了这种结构,程序才能继续执行:

读取 issue_type
→ 判断售后类型
→ 创建换货工单
→ 通知客服

这就产生了 Agent 开发中的第一个重要问题:

如何把模型的自然语言输出,转换成稳定的数据接口?

答案是 Structured Output,也就是结构化输出


二、Structured Output:让模型输出可用的数据

1、定义一个Structured Output

Structured Output 的核心目标不是让回答“看起来整齐”,而是让模型输出的数据满足程序预先定义的结构。

例如,我们可以规定模型必须返回以下字段:

字段 类型 含义
issue_type 字符串 问题类型
requested_action 字符串 用户希望采取的处理方式
urgency 字符串 紧急程度
summary 字符串 问题摘要

最终输出应当类似:

{
  "issue_type": "quality_problem",
  "requested_action": "replacement",
  "urgency": "normal",
  "summary": "用户购买的耳机左侧无声音,希望更换新品。"
}

程序可以直接读取:

requested_action = replacement

然后进入换货流程。仅仅要求“输出 JSON”并不可靠,初学者经常在提示词中这样写:

请使用 JSON 格式回答,不要输出其他内容。

模型有时能够遵守,但也可能返回:

根据用户的描述,建议办理换货。

{
  "issue_type": "quality_problem",
  "requested_action": "replacement"
}

也可能返回:

{
  "issue_type": "耳机坏了",
  "requested_action": "给用户换一个",
  "urgency": "比较着急"
}

这些内容虽然能够被人理解,却可能出现以下问题:

  • JSON 前后包含额外文字;
  • 缺少必要字段;
  • 字段类型错误;
  • 字段值不统一;
  • 返回了程序无法识别的内容。

例如,程序规定 urgency 只能取:

low
normal
high

模型却返回:

比较着急

程序就不知道该如何处理。

2、Schema 

更稳定的做法,是提前定义数据结构。

例如:

issue_type:
  必须是字符串

requested_action:
  只能是 refund、replacement 或 repair

urgency:
  只能是 low、normal 或 high

summary:
  必须是字符串

这套规则通常被称为 Schema。可以把 Schema 理解为模型和程序之间的一份接口合同:

模型负责按照规定格式生成数据
程序负责按照规定格式读取数据

这和普通后端接口非常相似。

假设一个接口规定返回:

{
  "code": 200,
  "message": "success",
  "data": {}
}

调用方就会按照这个结构读取结果。

Structured Output 做的事情,本质上也是为模型输出建立接口规范。

3、Structured Output 适合哪些场景

只要模型输出还需要进入程序的下一步,通常都应该考虑结构化输出。

常见场景包括:

  • 信息抽取;
  • 文本分类;
  • 意图识别;
  • 表单填写;
  • 工单生成;
  • 内容审核;
  • 数据入库;
  • 工作流分发;
  • Agent 状态更新。

例如,用户输入:

帮我安排下周二下午和产品经理开会。

模型可以输出:

{
  "intent": "create_meeting",
  "date": "2026-07-28",
  "time_period": "afternoon",
  "participants": ["产品经理"],
  "missing_information": ["具体时间", "会议时长"]
}

程序读取后就知道:

  • 用户想创建会议;
  • 当前信息还不完整;
  • 下一步应继续询问时间和会议时长。

这已经开始具备 Agent 的行为特征。


三、Function Calling:让模型选择工具

1、Function Calling

Structured Output 解决了“模型应该返回什么数据”,但它仍然没有让模型真正执行操作。

例如,用户说:

帮我查一下订单 A1024 什么时候送到。

模型本身通常无法访问公司的订单系统。

要查询订单,程序必须提供一个工具:

query_order(order_id)

Function Calling 的作用,就是让模型根据用户问题判断:

  • 是否需要调用工具;
  • 应该调用哪个工具;
  • 调用工具时需要哪些参数。

这是理解 Function Calling 最关键的一点。假设程序提供了这样一个工具:

query_order(order_id)

用户输入:

帮我查一下订单 A1024。

模型可能返回:

{
  "tool_name": "query_order",
  "arguments": {
    "order_id": "A1024"
  }
}

这只是一个调用请求,模型并没有真的查询数据库。真正执行的是程序:

模型生成工具调用请求
→ 程序读取工具名称
→ 程序校验参数
→ 程序执行 query_order
→ 获得订单结果

订单系统返回:

{
  "order_id": "A1024",
  "status": "运输中",
  "estimated_delivery": "2026-07-26"
}

程序随后把这个结果再次交给模型。模型才能生成最终回答:

订单 A1024 当前正在运输中,预计 2026 年 7 月 26 日送达。

完整过程是:

用户
  ↓
模型选择工具
  ↓
程序执行工具
  ↓
工具返回真实数据
  ↓
模型组织最终回答

2、为什么不让程序自己判断调用哪个接口

看到这里,可能会产生一个疑问:

既然工具最终还是程序执行,为什么不直接用普通代码判断?

对于简单且固定的规则,当然应该直接使用普通代码。例如:

用户点击“查询订单”按钮
→ 程序直接调用订单接口

这个过程不需要 Agent。

Function Calling 的价值主要体现在用户使用自然语言表达复杂目标时。

例如:

我上周买的那台显示器还没到,帮我看看物流。
如果今天还送不到,就帮我申请催单。

系统需要理解:

  1. 用户要查询哪个订单;
  2. 哪个订单对应显示器;
  3. 当前物流状态是什么;
  4. 今天能否送达;
  5. 是否需要创建催单。

模型可以根据上下文动态决定工具调用顺序:

查询用户订单→ 找到显示器订单→ 查询物流→ 判断预计送达时间→ 必要时创建催单

这种流程很难只依靠几个简单的 if 完成。


四、工具应该怎样设计

Function Calling 是否稳定,很大程度上取决于工具设计。

一个好的工具通常具备三个特点:

1. 工具职责单一

不推荐设计:

handle_order_request(user_request)

这个工具的职责过于模糊。

模型不知道它究竟能查询订单、修改订单,还是申请退款。

更合理的设计是:

list_user_orders()
query_order_status(order_id)
create_delivery_reminder(order_id)
request_refund(order_id, reason)

每个工具只负责一件事。

2. 工具名称和描述清晰

模型选择工具时,会参考工具名称和描述。

例如:

query_order_status
查询指定订单的当前状态和预计送达时间

比下面这种命名更容易理解:

order_tool
处理订单相关内容

工具越模糊,模型越容易选错。

3. 参数要尽量明确

不推荐:

{
  "data": "A1024,帮我查一下"
}

更推荐:

{
  "order_id": "A1024"
}

如果工具有多个参数,也应说明每个参数的类型、含义和是否必填。

例如:

{
  "order_id": "A1024",
  "reason": "预计时间已超过承诺送达时间"
}

需要注意的是:

即使模型返回的参数看起来正确,程序仍然必须再次校验。

模型生成的工具参数不能直接视为可信输入。

程序至少需要检查:

  • 工具是否真实存在;
  • 必填参数是否齐全;
  • 参数类型是否正确;
  • 参数值是否越权;
  • 当前用户是否有执行权限;
  • 操作是否需要二次确认。

尤其是删除文件、发送消息、支付、退款、修改数据库等操作,不能只因为模型请求调用,就直接执行。


五、Agent Loop:把模型和工具连接起来

1、Agent Loop

Structured Output 让输出可控,Function Calling 让模型能够请求工具。

但真正让系统成为 Agent 的,是把这些步骤组织成循环。

这个循环通常被称为 Agent Loop

一个最小 Agent Loop 包含五个步骤:

1. 接收用户目标
2. 调用模型判断下一步
3. 如果需要工具,程序执行工具
4. 将工具结果返回模型
5. 模型继续判断,直到输出最终答案

关键在于:

Agent 不是一次模型调用,而是多次模型调用和工具调用组成的循环。

2、一次订单查询的完整过程

用户输入:

查询订单 A1024。如果明天还送不到,就帮我创建一个提醒。

第一次调用模型时,模型发现需要查询订单:

{
  "tool_name": "query_order_status",
  "arguments": {
    "order_id": "A1024"
  }
}

程序执行工具,得到:

{
  "status": "运输中",
  "estimated_delivery": "2026-07-28"
}

程序将结果返回给模型。

模型判断预计送达时间晚于明天,于是继续请求:

{
  "tool_name": "create_delivery_reminder",
  "arguments": {
    "order_id": "A1024",
    "remind_at": "2026-07-27 18:00"
  }
}

程序创建提醒后,将结果返回:

{
  "success": true,
  "reminder_id": "R8891"
}

模型最终回答:

订单 A1024 当前正在运输中,预计 7 月 28 日送达。
我已经为你创建了 7 月 27 日下午 6 点的物流提醒。

这一次用户任务中,模型进行了两次工具调用。

程序并没有提前写死必须按照这个顺序执行,而是由模型根据每一步的结果继续判断。

这就是 Agent 的核心。

3、一个最小 Agent 的完整代码结构

前面先讲清楚概念,下面再集中观察一个完整代码结构。

为了突出 Agent Loop,下面使用简化的 OpenAI 兼容形式作为示例。不同模型平台的具体字段可能略有差异,但整体流程基本一致。

import json
from typing import Any


def query_order_status(order_id: str) -> dict[str, Any]:
    """模拟查询订单系统。"""

    orders = {
        "A1024": {
            "status": "运输中",
            "estimated_delivery": "2026-07-28"
        }
    }

    return orders.get(
        order_id,
        {
            "error": "订单不存在"
        }
    )


def create_delivery_reminder(
    order_id: str,
    remind_at: str
) -> dict[str, Any]:
    """模拟创建物流提醒。"""

    return {
        "success": True,
        "order_id": order_id,
        "remind_at": remind_at
    }


TOOL_FUNCTIONS = {
    "query_order_status": query_order_status,
    "create_delivery_reminder": create_delivery_reminder
}


TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "query_order_status",
            "description": "查询订单状态和预计送达时间",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号"
                    }
                },
                "required": ["order_id"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_delivery_reminder",
            "description": "为指定订单创建物流提醒",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号"
                    },
                    "remind_at": {
                        "type": "string",
                        "description": "提醒时间"
                    }
                },
                "required": [
                    "order_id",
                    "remind_at"
                ],
                "additionalProperties": False
            }
        }
    }
]


def execute_tool(
    tool_name: str,
    arguments: dict[str, Any]
) -> dict[str, Any]:
    """统一执行工具,并处理基础异常。"""

    tool = TOOL_FUNCTIONS.get(tool_name)

    if tool is None:
        return {
            "success": False,
            "error": f"不存在的工具:{tool_name}"
        }

    try:
        result = tool(**arguments)

        return {
            "success": True,
            "data": result
        }

    except TypeError as exc:
        return {
            "success": False,
            "error": f"工具参数错误:{exc}"
        }

    except Exception as exc:
        return {
            "success": False,
            "error": f"工具执行失败:{exc}"
        }


def run_agent(
    client,
    model: str,
    user_input: str,
    max_steps: int = 6
) -> str:
    """运行一个最小 Agent Loop。"""

    messages = [
        {
            "role": "system",
            "content": (
                "你是订单服务助手。"
                "需要查询订单或创建提醒时,"
                "请调用相应工具。"
                "不要编造订单状态。"
            )
        },
        {
            "role": "user",
            "content": user_input
        }
    ]

    for _ in range(max_steps):
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto"
        )

        message = response.choices[0].message
        messages.append(message)

        # 没有工具调用,说明模型已经准备好最终回答
        if not message.tool_calls:
            return message.content or ""

        for tool_call in message.tool_calls:
            tool_name = tool_call.function.name

            try:
                arguments = json.loads(
                    tool_call.function.arguments
                )
            except json.JSONDecodeError:
                tool_result = {
                    "success": False,
                    "error": "工具参数不是合法 JSON"
                }
            else:
                tool_result = execute_tool(
                    tool_name,
                    arguments
                )

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(
                    tool_result,
                    ensure_ascii=False
                )
            })

    raise RuntimeError(
        "Agent 超过最大执行步数,任务已终止"
    )

这段代码看起来较长,但核心逻辑只有下面几行:

调用模型
→ 检查是否存在工具调用
→ 执行工具
→ 把工具结果加入 messages
→ 再次调用模型

这就是最基础的 Agent Loop。

为什么要设置最大执行步数

代码中设置了:

max_steps = 6

这是为了防止模型不断调用工具,进入无限循环。

例如,模型可能出现:

查询订单
→ 再次查询订单
→ 再次查询订单
→ 仍然继续查询

如果没有最大步数限制,程序可能持续消耗 Token 和接口额度。

因此,一个 Agent 至少应该设置:

  • 最大模型调用次数;
  • 最大工具调用次数;
  • 最大运行时间;
  • 必要时设置最大成本。

为什么要保留模型的工具调用消息

工具执行完成后,不能只把工具结果发给模型。

程序还需要保留模型之前发出的工具调用请求:

assistant:我要调用 query_order_status
tool:这是 query_order_status 的执行结果

模型只有看到完整过程,才能知道:

  • 自己调用了哪个工具;
  • 工具返回的数据对应哪个请求;
  • 下一步应该继续做什么。

这和普通聊天需要携带历史消息的原理相同。


六、Agent 并不是所有问题的答案

学习完 Function Calling 后,很容易产生一种冲动:

把所有业务流程都交给模型判断。

这通常不是一个好设计。

如果业务流程完全固定:

用户点击查询
→ 查询数据库
→ 返回结果

直接使用普通代码会更稳定、更快,也更便宜。

Agent 更适合下面这些场景:

  • 用户通过自然语言提出目标;
  • 完成任务可能需要多个工具;
  • 工具调用顺序不固定;
  • 下一步取决于上一步执行结果;
  • 需要在多个方案中动态判断。

可以用一句话区分:

步骤确定,用 Workflow(即我们自己规定接下来的执行流程)。
步骤需要动态判断,用 Agent(让模型来决定接下来的流程)。

例如,普通订单查询适合固定 Workflow:

获取订单编号
→ 查询订单
→ 返回结果

而下面这个任务更适合 Agent:

找到我最近买的键盘,
查一下为什么还没发货,
如果缺货就帮我换成同价位的其他型号。

因为系统需要动态完成:

查询订单
→ 识别键盘订单
→ 查询发货状态
→ 判断是否缺货
→ 搜索替代商品
→ 比较价格
→ 请求用户确认


结语

从普通大模型应用到 Agent,真正发生的变化并不是模型突然拥有了自主意识。

模型仍然只负责生成内容。

不同之处在于,程序开始允许模型:

  • 使用结构化格式表达判断;
  • 从预先提供的工具中进行选择;
  • 根据工具结果继续完成任务。

因此,一个最小 Agent 可以表示为:

模型
+ 工具
+ 消息上下文
+ 执行循环

掌握本文内容后,你应该能够回答三个问题:

  1. 为什么自然语言回答不能直接作为稳定的程序接口;
  2. 模型调用工具时,模型和程序分别负责什么;
  3. 为什么 Agent 需要不断重复“模型判断—工具执行—结果反馈”。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐