Files

101 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: ai.function-calling.tool-loop
title: 工具调用原理:工具定义、调用循环、参数校验、并行调用和错误回传
type: lesson
domain: ai
stack: [function-calling]
tags: [Function Calling, Tool Calling, JSON Schema, 调用循环, 参数校验]
level: intermediate
prerequisites: [python.python.typing-errors-testing]
est_hours: 3
related: [ai.mcp.index, ai.langgraph.state-graph, ai.function-calling.interview]
week: [10]
order: 1
---
# 工具调用原理:工具定义、调用循环、参数校验、并行调用和错误回传
> 各家模型的工具调用格式大同小异,本课以 OpenAI Chat Completions 的格式为例,字段以各家官方文档为准:https://platform.openai.com/docs/guides/function-calling
## 解释说明
**模型本身不会执行任何代码。** 工具调用(Function Calling / Tool Calling)的流程是:
1. 你把工具的名字、描述、参数的 JSON Schema 和对话一起发给模型;
2. 模型判断需要工具时,返回 `tool_calls`(工具名 + JSON 字符串参数),而不是文本答案;
3. **你的代码**校验参数、检查权限、执行工具;
4. 把结果作为 `role: "tool"` 的消息(带 `tool_call_id`)加回对话,再次请求模型;
5. 重复,直到模型返回普通文本答案,或者达到最大步数。
这就是所有 Agent 框架的核心循环(ReAct 的工程实现)。LangChain 的 `create_agent`、LangGraph 的工具节点、MCP 的工具,最后都落到这个循环。
### 工程上要处理的事
| 问题 | 做法 |
| --- | --- |
| 参数不可信(格式错、越权、注入) | pydantic 校验;权限在工具内部按**当前登录用户**校验,不能信任模型传的用户 ID |
| 一次返回多个调用(并行调用) | 逐个(或并发)执行,每个结果带对应的 `tool_call_id` 回传 |
| 工具执行失败 | 把错误信息作为工具结果回传,让模型修正;不要直接抛异常中断 |
| 有副作用的操作(退款、发邮件、删数据) | 用户确认 / 人工审批;幂等键;审计日志 |
| 无限循环 | 最大步数,超过后转人工 |
| 工具太多 | 工具描述占用上下文、模型选错;按场景动态选择工具子集(见 [Agent 设计](../agent-design/index.md)) |
### 工具描述怎么写
- 名字动词开头、含义明确:`get_order`、`create_refund`。
- 描述写清楚**什么时候用、什么时候不用**、返回什么。
- 参数加 `description`、枚举、格式约束(`pattern`、`minimum`)。模型会读这些。
## 案例
售后助手:用户说“订单 1001 我要退款”。模型第一次传了 `order_id: "1001"`,校验失败(格式应为 `A1001`),错误回传后模型改成 `A1001` 查询成功;随后调用退款工具,因为是有副作用的操作,先请求用户确认;最后回复用户。
## 代码示例
完整可运行版本(带 5 个测试):[examples/tool-loop](examples/tool-loop/)。核心循环:
```python
for step in range(max_steps):
msg = model.chat(messages, tool_schemas)
messages.append(msg)
if not msg.get("tool_calls"):
return msg["content"] # 模型给出最终答案
for c in msg["tool_calls"]:
tool = TOOLS.get(c["function"]["name"])
try:
args = tool.args_model.model_validate_json(c["function"]["arguments"])
if tool.needs_confirm and not confirm(tool.name, args.model_dump()):
result = {"error": "用户拒绝执行该操作"}
else:
result = tool.fn(args, ctx) # ctx 里是当前登录用户,不来自模型
except Exception as e:
result = {"error": f"参数错误:{e}"} # 错误回传给模型
messages.append({"role": "tool", "tool_call_id": c["id"],
"content": json.dumps(result, ensure_ascii=False)})
return "步骤过多,已转人工处理。"
```
运行输出:
```text
调用 get_order({"order_id": "1001"}) -> {'error': "参数错误:1 validation error for GetOrderArgs ..."}
调用 get_order({"order_id": "A1001"}) -> {'order_id': 'A1001', 'user': 'u1', 'paid': 199.0, 'status': 'delivered'}
调用 refund({"order_id": "A1001", "amount": 199}) -> {'ok': True, 'refunded': 199.0}
回答: 已为订单 A1001 退款 199 元。 步数: 4
```
## 易错点
- 以为模型会执行工具:执行、权限、错误处理全是你的代码。
- `arguments` 是 JSON **字符串**,要解析和校验,可能不是合法 JSON。
- 回传结果时漏了 `tool_call_id`,或者顺序和数量对不上,接口报错。
- 工具结果太长(整张表、整篇文档)塞满上下文:只返回需要的字段,必要时分页。
- 工具结果里包含的文本也可能是提示词注入(例如网页内容),要当作数据而不是指令。
- 让模型决定“是否需要确认”:确认规则应写在代码里。
## 面试常问
- Function Calling 的完整流程?模型在其中做了什么、没做什么?
- 模型返回的参数不合法怎么办?
- 有副作用的工具怎么保证安全?