Function Call原理

Function Call 协议、Schema 设计最佳实践与 Mini Agent 实战

学习目标

完成本节学习后,学员将能够:

  • 深刻理解 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 区别 前者是语义驱动的动态调用,后者是逻辑驱动的静态调用