--- 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 的完整流程?模型在其中做了什么、没做什么? - 模型返回的参数不合法怎么办? - 有副作用的工具怎么保证安全?