学习目标
完成本节学习后,学员将能够:
- 深刻理解 Function Call 为何是现代 AI Agent 的能力基石
- 掌握 OpenAI Function Call 协议的标准结构与调用流程
- 熟练设计符合最佳实践的工具描述 Schema(function schema)
- 清晰区分 Function Call 与传统 API 调用的本质差异
一、为什么 Function Call 是 Agent 能力的核心基础?
提问:如果没有 Function Call,Agent 还能"行动"吗?
答案是否定的。我们先从一个反例说起。
传统方式的问题:让 LLM “猜"参数
假设我们要调用一个 OCR 工具来识别图片文字。早期做法是这样写 prompt:
你是一个助手,请调用 ocr_service 处理这张图片。
图片路径是 /uploads/hw_001.jpg
请返回 JSON 格式:{"tool": "ocr_service", "image_path": "/uploads/hw_001.jpg"}
这种做法存在严重问题:
| 风险 | 说明 |
|---|---|
| 格式不可控 | LLM 可能返回非 JSON、字段名拼错、缺少引号等 |
| 参数易遗漏 | 图片路径可能被忽略或替换为占位符 |
| 安全性差 | 可能注入恶意命令,如 "; rm -rf /" |
| 难以自动化解析 | 需要正则匹配 + 错误重试,工程成本高 |
正确解法:使用 Function Call 协议
Function Call 是大模型平台(如 OpenAI)提供的一种结构化函数调用协议,它允许 LLM 在推理过程中"声明"要调用哪个外部函数,并输出标准化的参数。
其核心价值在于:
- 把"意图识别"和"参数提取"交给 LLM 做
- 把"调用执行"和"结果处理"交给程序代码做
- 实现自然语言理解与程序逻辑的精准衔接
技术定位:Function Call 是 Agent 的"神经系统”。
| 层级 | 功能 |
|---|---|
| 大脑(LLM) | 决策:“我现在需要识别这张图” |
| 神经信号(Function Call) | 传递指令:“调用 ocr_service,参数是 image_path=/xxx.jpg” |
| 肢体(Tool Implementation) | 执行动作:运行 OCR 算法并返回结果 |
没有 Function Call,Agent 就像瘫痪的人——有思想但无法行动。
二、OpenAI Function Call 协议标准与参数结构
虽然我们将以 OpenAI 为例讲解,但其设计理念已被 LangChain、Anthropic、阿里通义千问等广泛兼容。
2.1 请求阶段:向 LLM 注册可用函数
在调用 chat.completions.create 时,通过 functions 参数注册你的工具:
client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "请帮我识别这张作业图片"}],
functions=[
{
"name": "ocr_service",
"description": "对上传的图片进行光学字符识别,返回文本内容",
"parameters": {
"type": "object",
"properties": {
"image_path": {
"type": "string",
"description": "待识别图像的服务器路径"
},
"language": {
"type": "string",
"enum": ["chinese", "english"],
"description": "图像中文字的语言,默认为chinese"
}
},
"required": ["image_path"]
}
}
],
function_call="auto" # auto, none, 或指定函数名
)
关键字段说明:
| 字段 | 含义 | 是否必需 |
|---|---|---|
| name | 函数名称(必须与后端实现一致) | 是 |
| description | 功能描述(LLM 用它判断何时调用) | 是 |
| parameters.type | 固定为 "object" |
是 |
| properties | 参数定义对象 | 是 |
| required | 必填参数列表 | 推荐填写 |
注意:
description极其重要!它是 LLM 理解决策上下文的唯一依据。
2.2 响应阶段:LLM 返回结构化调用请求
如果 LLM 判断需要调用函数,它不会直接回答用户,而是返回如下格式:
{
"choices": [
{
"message": {
"role": "assistant",
"function_call": {
"name": "ocr_service",
"arguments": "{\"image_path\": \"/uploads/hw_001.jpg\", \"language\": \"chinese\"}"
}
}
}
]
}
解析要点:
function_call.name:表示要调用哪个函数arguments:是字符串化的 JSON,需用json.loads()解析- 若不需要调用函数,则无
function_call字段,正常输出回复
2.3 执行阶段:开发者调用真实函数并返回结果
你需要编写逻辑来执行该函数,并将结果以特定格式传回给 LLM:
import json
# 伪代码示例
if response.choices[0].message.get("function_call"):
func_name = response.choices[0].message.function_call.name
args = json.loads(response.choices[0].message.function_call.arguments)
if func_name == "ocr_service":
result = ocr_service(image_path=args["image_path"], language=args.get("language", "chinese"))
# 将结果作为新消息发回给 LLM
messages.append({
"role": "function",
"name": func_name,
"content": json.dumps(result) # 必须是字符串
})
此时,你可以继续让 LLM 基于 OCR 结果生成反馈:
“我已识别出题目:‘求 sin(α) 的值’。根据你的作答,你在第二步符号处理上出现了错误……”
三、工具描述 Schema 设计最佳实践
Schema 的质量直接决定 Agent 的决策准确性。以下是经过验证的最佳实践。
3.1 清晰的功能描述(description)
差的例子:
"description": "调用OCR"
好的例子:
"description": "对上传的学生手写作答图片进行高精度OCR识别,支持中英文数学公式和表格结构还原,适用于作业批改场景。输入为服务器本地路径,输出为纯文本内容。"
原则:让 LLM 明白这个工具"在什么场景下用"、“解决什么问题”。
3.2 精确的参数命名与类型定义
| 类型 | 使用建议 |
|---|---|
| string | 文件路径、ID、短文本 |
| number | 分数、难度等级、时间戳 |
| boolean | 开关类选项(是否启用某功能) |
| array | 多个题目 ID、多个知识点标签 |
示例:练习生成工具的 schema 片段
"parameters": {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "知识点主题,例如'三角函数诱导公式'"
},
"difficulty": {
"type": "number",
"minimum": 1,
"maximum": 5,
"description": "题目难度等级,1最简单,5最难"
},
"count": {
"type": "number",
"default": 5,
"description": "生成题目的数量"
},
"include_solution": {
"type": "boolean",
"default": true,
"description": "是否包含详细解答过程"
}
},
"required": ["topic"]
}
3.3 使用 enum 限制枚举值,提升可靠性
避免 LLM 输出无效参数:
"output_format": {
"type": "string",
"enum": ["plaintext", "latex", "markdown"],
"description": "期望的输出格式"
}
这样 LLM 只能在三个合法值中选择,大幅降低错误率。
3.4 添加默认值与可选性说明
- 非必填参数不要放入
required数组 - 对关键参数设置合理默认值(可在代码中补充)
教学提示:“Schema 不仅是接口契约,更是 LLM 的决策指南。”
四、参数提取、验证与错误处理机制
即使有了 Function Call,也不能完全信任 LLM 的输出。我们必须建立防御性编程机制。
4.1 参数提取的安全封装
def safe_parse_arguments(raw_args: str):
try:
return json.loads(raw_args)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in function call arguments: {raw_args}") from e
4.2 参数验证(Validation)
def validate_ocr_params(params):
if "image_path" not in params:
raise ValueError("Missing required parameter: image_path")
if not os.path.exists(params["image_path"]):
raise FileNotFoundError(f"Image not found: {params['image_path']}")
if "language" in params and params["language"] not in ["chinese", "english"]:
raise ValueError("Invalid language value")
建议:使用 Pydantic 模型进行自动校验(后续章节介绍)。
4.3 错误处理策略
当工具执行失败时,应构造清晰的错误反馈给 LLM:
try:
result = ocr_service(**args)
except Exception as e:
# 返回结构化错误信息,便于 LLM 理解
error_content = {
"error": "ocr_failed",
"message": str(e),
"suggestion": "请检查图片是否清晰,或尝试重新上传"
}
messages.append({
"role": "function",
"name": "ocr_service",
"content": json.dumps(error_content)
})
这样 LLM 可以做出智能响应:
“OCR识别失败:图片模糊。建议您重新拍摄一张更清晰的照片。”
五、Function Call 与传统 API 调用的区别
很多初学者容易混淆这两个概念。下面我们从多个维度进行对比:
| 维度 | Function Call | 传统 API 调用 |
|---|---|---|
| 发起者 | LLM(基于语义理解) | 开发者(硬编码逻辑) |
| 触发条件 | 动态决策结果 | 固定业务流程 |
| 参数来源 | 自然语言中提取 | 表单提交/数据库查询 |
| 调用时机 | 多轮对话中的任意步骤 | 预设的控制流节点 |
| 容错要求 | 高(需处理幻觉、格式错误) | 相对较低 |
| 典型场景 | Agent 任务执行 | Web 应用前后端交互 |
场景对比:学生说"看看这道题"
传统 API 方式(静态):
if user_input.contains("题") and has_image():
call_ocr_api()
else:
search_knowledge_base()
→ 缺乏灵活性,无法应对复杂表达
Function Call 方式(动态):
→ 基于语义理解自主决策
本质区别总结:
- 传统 API 调用是"程序驱动"
- Function Call 是"语义驱动"
只有后者才能支撑真正意义上的 AI Agent。
六、动手实践
构建一个基于 Function Call 的 Mini Agent:用户:“你能看懂我写的这道题吗?” → LLM → 决策:需要 OCR → 调用 ocr_service(image_path=...)
# mini_agent.py
import json
import openai
import os
from typing import Dict, Any
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
# ========================
# 1. 配置 OpenAI 客户端
# ========================
client = openai.OpenAI(
# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# ========================
# 2. 定义外部工具函数(Tool Implementation)
# ========================
def ocr_service(image_path: str) -> Dict[str, Any]:
"""
模拟OCR服务:识别图像中的文本内容
在真实项目中,这里会调用 PaddleOCR 或云OCR API
"""
print(f"[工具执行] 正在调用 OCR 识别图片: {image_path}")
# 模拟返回结果
if "math" in image_path:
return {
"success": True,
"text": "题目:已知 sin(α) = 3/5,且 α ∈ (π/2, π),求 cos(α) 的值。\n作答:cos(α) = 4/5"
}
else:
return {"success": False, "error": "无法识别图像内容"}
def explain_concept(concept: str) -> Dict[str, Any]:
"""
模拟知识讲解服务:返回某个知识点的解释
"""
print(f"[工具执行] 正在查询知识点: {concept}")
return {
"explanation": f"{concept} 是指在第二象限中,正弦为正,余弦为负。常用公式:sin²α + cos²α = 1。"
}
# ========================
# 3. 注册可用函数 Schema
# ========================
functions = [
{
"name": "ocr_service",
"description": "对上传的学生手写作答图片进行OCR识别,提取题目和答案文本,适用于作业批改场景。",
"parameters": {
"type": "object",
"properties": {
"image_path": {
"type": "string",
"description": "待识别图像的服务器路径,例如 /uploads/hw_001.jpg"
}
},
"required": ["image_path"]
}
},
{
"name": "explain_concept",
"description": "根据知识点名称,提供详细的中文讲解,包括定义、公式和常见错误。",
"parameters": {
"type": "object",
"properties": {
"concept": {
"type": "string",
"description": "要解释的知识点名称,如'三角函数诱导公式'"
}
},
"required": ["concept"]
}
}
]
# ========================
# 4. 主执行流程
# ========================
def run_agent(user_input: str):
messages = [{"role": "user", "content": user_input}]
while True:
response = client.chat.completions.create(
model="qwen-plus", # 使用通义千问等支持function call的模型
messages=messages,
functions=functions,
function_call="auto"
)
print(f'response: {response}')
message = response.choices[0].message
# 判断是否需要调用函数
if message.function_call:
func_name = message.function_call.name
try:
args = json.loads(message.function_call.arguments)
except json.JSONDecodeError as e:
print(f"[参数解析失败] {e}")
break
print(f"[决策] LLM 决定调用函数: {func_name},参数: {args}")
# 执行对应函数
if func_name == "ocr_service":
result = ocr_service(**args)
elif func_name == "explain_concept":
result = explain_concept(**args)
else:
result = {"error": f"未知函数: {func_name}"}
# 将结果以 function 角色回传给 LLM
print(f'message111: {message.model_dump()}')
messages.append(message.model_dump())
messages.append({
"role": "function",
"name": func_name,
"content": json.dumps(result)
})
# 继续循环,让 LLM 基于结果做下一步决策
continue
else:
# LLM 返回最终回复,结束循环
print(f"\n[最终回复]\n{message.content}")
break
# ========================
# 5. 运行测试
# ========================
if __name__ == "__main__":
test_input = "这是我写的数学题,请帮我看看做得对吗?图片路径是 /uploads/math_hw_001.jpg"
run_agent(test_input)
七、本节小结
| 主题 | 核心要点 |
|---|---|
| Function Call 的意义 | 是连接 LLM"想"与程序"做"的桥梁,实现感知→行动闭环 |
| 协议结构 | 包含 name/description/parameters 三要素,注册后由 LLM 触发 |
| Schema 设计原则 | 描述清晰、参数明确、枚举约束、默认值合理 |
| 安全处理流程 | 提取 → 验证 → 执行 → 错误反馈,缺一不可 |
| 与传统 API 区别 | 前者是语义驱动的动态调用,后者是逻辑驱动的静态调用 |