一个项目带你入门AI应用开发04
第 4 课:让 Agent 调用工具
4.1 你的目标
用户说"查订单 ORD-001",Agent 调用函数获取数据,再把结果组织成自然语言回复:
You: 查一下订单 ORD-001
[调用工具: query_order({"order_id": "ORD-001"})]
AI: 您的订单 ORD-001(智能蓝牙耳机 Pro,299元)已发货,正在配送中。
4.2 反例:硬编码分支
def handle_order(user_input):
if "ORD-001" in user_input:
return "您的订单 ORD-001 已发货"
elif "ORD-002" in user_input:
return "您的订单 ORD-002 正在处理中"
else:
return "请提供订单号"
这样为什么不行?
- 用户说"帮我查一下 001 那个订单"——没有 "ORD-",匹配不到
- 新增订单要改代码
- 订单号多了 if-else 会变得非常长
本质问题是什么?
工具调用(查订单)的触发条件应该是 LLM 的判断,而不是写死的字符串匹配。LLM 能理解"查一下 001 那个订单"是在查什么,代码不行。
4.3 Function Calling 的原理
Function Calling 让 LLM 能看到你定义的工具列表,并且自主决定要不要调用、调用哪个、传什么参数。
工具定义
tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单状态和物流信息",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单编号"}
},
"required": ["order_id"],
},
},
},
]
每个工具需要三样东西:
- name: 函数名,执行时用这个名字找到对应的 Python 函数
- description: LLM 靠这个描述来判断"当前场景要不要调用这个工具"
- parameters: JSON Schema 格式,告诉 LLM 需要什么参数
LLM 的响应
如果不调工具,LLM 返回:
{"content": "直接回复用户", "tool_calls": None}
如果要调工具,LLM 返回:
{
"content": None,
"tool_calls": [
{
"id": "call_xxx",
"function": {
"name": "query_order",
"arguments": '{"order_id": "ORD-001"}'
}
}
]
}
content 是 None,因为 LLM 没有直接回复——它决定由工具来提供信息。
4.4 为什么需要两轮 LLM 调用?
初学者最容易犯的错误:工具返回了 JSON 就直接拼接字符串返回给用户。
工具返回: {"found": true, "status": "shipped", "product": "智能蓝牙耳机 Pro"}
直接返回: "您的订单查询结果:{"found": true, "status": "shipped"}"
← 用户看到 JSON,体验很差
正确的流程
第一轮:LLM 决定调用 query_order
→ LLM 说"调 query_order,参数 order_id=ORD-001"
→ 我们执行 get_order("ORD-001")
→ 返回 {"found": true, "status": "shipped", "product": "智能蓝牙耳机 Pro"}
第二轮:把工具结果以 "tool" role 发回给 LLM
→ messages 变为:
system: "你是电商客服系统的工具调用助手"
user: "查一下订单 ORD-001"
assistant: (tool_calls=...)
tool: '{"found": true, "status": "shipped", "product": "智能蓝牙耳机 Pro"}'
→ LLM 看到结果后生成:
"您的订单 ORD-001(智能蓝牙耳机 Pro)已发货,正在配送中。"
为什么不能直接拼接字符串?
工具返回的是 JSON 原始数据,LLM 需要:
- 理解 JSON 的含义("shipped" 对用户应该说"已发货")
- 翻译 成自然语言("智能蓝牙耳机 Pro, 299元" 比原始 JSON 更容易阅读)
- 添加上下文("正在配送中"补充了用户关心的信息)
这叫"推理"(reasoning),不是简单的字符串拼接。
什么时候可以省略第二轮?
如果工具返回的已经是可以直接展示的内容(比如查百科返回了一段文字),那可以省略。但多数情况下,工具返回的是结构化数据,建议走两轮。
4.5 执行工具
def _execute_tool(name, args):
if name == "query_order":
order = get_order(args["order_id"])
if order:
return json.dumps({"found": True, "order": asdict(order)})
return json.dumps({"found": False, "message": "未找到"})
# ... 其他工具
注意结果统一用 json.dumps 序列化成字符串。因为 tool role 的 content 字段是字符串类型。
4.6 完整调用流程
def handle_with_tools(user_input):
messages = [
{"role": "system", "content": "根据用户问题调用合适的工具获取信息。"},
{"role": "user", "content": user_input},
]
# 第一轮:LLM 决定是否调用工具
msg = call_llm(messages, tools=tools, tool_choice="auto")
tool_calls = msg.get("tool_calls")
if not tool_calls:
return msg.get("content", "")
# 第二轮:执行工具 + 结果回传
messages.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
for tc in tool_calls:
name = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
result = _execute_tool(name, args)
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
final_msg = call_llm(messages, temperature=0.3)
return final_msg.get("content", "")
tool_choice 参数
"auto": LLM 自主决定要不要调工具(默认)"required": 强制 LLM 必须调一个工具(用于"先查再说"的场景){"type": "function", "function": {"name": "query_order"}}: 强制调指定工具
第 4 课用 "auto" 就够了。
本课知识点
| 概念 | 你做了什么 | 为什么 |
|---|---|---|
| Function Calling | 定义 tools schema + tool_choice | LLM 自主决定调用,不靠关键词匹配 |
| 工具描述 | name + description + parameters | LLM 靠 description 判断何时调用 |
| 两轮调用 | 决策 → 执行 → 推理 | 工具返回 JSON,需要 LLM 翻译成自然语言 |
| tool role | 把工具结果按 role 传回 | 符合 OpenAI 标准,LLM 能理解这是"工具说的" |
课后作业
- 加一个
get_discount工具,根据用户等级(vip/normal)返回折扣信息 - 把
tool_choice改成"required",观察 LLM 的行为变化
面试可能会问
"Function Calling 的 tool_choice=auto 和 tool_choice=required 有什么区别?分别用于什么场景?"
auto: LLM 自主决定(多数场景)。required: 强制调用工具(如"必须先查数据库再回答"的场景)。"工具执行后为什么要把结果以 tool role 发回给 LLM?直接拼接字符串不行吗?"
工具返回的是原始数据,LLM 需要推理后生成自然语言。而且 tool role 的消息在后续对话中可以作为上下文,方便用户追问。

浙公网安备 33010602011771号