AI 大模型 Agent 开发入门(二):从让AI从“会回答”到“会做事”
一、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 的价值主要体现在用户使用自然语言表达复杂目标时。
例如:
我上周买的那台显示器还没到,帮我看看物流。
如果今天还送不到,就帮我申请催单。
系统需要理解:
- 用户要查询哪个订单;
- 哪个订单对应显示器;
- 当前物流状态是什么;
- 今天能否送达;
- 是否需要创建催单。
模型可以根据上下文动态决定工具调用顺序:
查询用户订单→ 找到显示器订单→ 查询物流→ 判断预计送达时间→ 必要时创建催单
这种流程很难只依靠几个简单的 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 可以表示为:
模型
+ 工具
+ 消息上下文
+ 执行循环
掌握本文内容后,你应该能够回答三个问题:
- 为什么自然语言回答不能直接作为稳定的程序接口;
- 模型调用工具时,模型和程序分别负责什么;
- 为什么 Agent 需要不断重复“模型判断—工具执行—结果反馈”。
更多推荐




所有评论(0)