Skip to content

Function Call / Tool Call:让模型会"调工具" — Function Call

教学目标

理解 Function Call / Tool Call 的本质:模型本身只会"生成文本",要让它"采取行动"(查天气、算数、查数据库),就要让模型输出结构化的工具调用,执行后把结果回填到上下文,继续生成最终答案。本步实现一个 ReAct 风格的工具调用循环。

问题:模型只会"说",不会"做"

主系列 step20 实现了 OpenAI 兼容的 HTTP 服务,但只做了 messages + stream 基础功能,README 里明确标注"不支持 function calling、tools"。

为什么不直接让模型"自由生成"工具调用?因为模型自由生成常输出格式错误的 JSON:

用户:"北京天气如何?"
模型自由生成:
  "北京今天天气不错,温度大概25度..."   ← 这是一段话,下游程序没法解析执行
  或 "{name: get_weather, args: {city: 北京}}"  ← JSON 格式错误(key 没引号)

下游解析脆弱,一个标点错误就崩。Function Call 的解法:用 adv15 Guided Decoder 把模型输出约束成永远合法的 JSON 工具调用格式,然后执行、回填、循环。

原理:ReAct 循环 model ↔ tool

用户提问:"北京天气如何?人口多少?"


┌───────────────────────────────────────────────┐
│  循环(最多 max_iters 轮)                      │
│                                                 │
│  1. 模型(经 guided decoder 约束)输出工具调用    │
│     → {"name":"get_weather","args":{"city":"北京"}}│
│                                                 │
│  2. parse_tool_call 解析出 {name, args}           │
│                                                 │
│  3. execute_tool 执行(查天气/计算器/数据库)     │
│     → "北京: 晴 25°C"                            │
│                                                 │
│  4. 把结果拼回 prompt:[tool:get_weather]->...    │
│                                                 │
│  5. 模型看到结果,决定:继续调工具 or 给最终答案   │
└───────────────────────────────────────────────┘


  最终答案:"北京晴 25°C,人口约 2189 万。"

关键:每轮模型输出都是合法 JSON 工具调用(由 adv15 的 regex/grammar 约束保证),parse_tool_call 一定能解析,不会崩。

实现细节

TOOL_SCHEMA:工具注册表(JSON Schema 格式)

本教学版采用与 OpenAI function calling 相同的 JSON Schema 规范来描述工具:

python
TOOL_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如 '北京'、'上海'",
                    },
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算数学表达式,返回计算结果",
            "parameters": {
                "type": "object",
                "properties": {
                    "expr": {
                        "type": "string",
                        "description": "合法的数学表达式,如 '2+3*4'",
                    },
                },
                "required": ["expr"],
            },
        },
    },
]

为什么用 JSON Schema? 这正是 guided decoder(adv15)能约束模型输出的关键:

真实框架中 JSON Schema → Guided Decoder 的工作流程:

  1. 从 TOOL_SCHEMA 提取 parameters 字段的 JSON Schema
     → {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}

  2. Guided Decoder 把 JSON Schema 编译为有限状态机 (FSM)
     → 状态: START → { → "city" → : → " → [任意字符]* → " → } → END

  3. 推理时,每一步只允许模型从"当前状态的合法后继 token"中采样
     → 保证输出 100% 是合法的、符合 schema 的 JSON

  结果:模型输出永远是 {"city": "北京"} 这样的合法参数对象
       下游 parse_tool_call 一定能解析,不会崩

这与 build_system_prompt() 配合使用:schema 既写入 system prompt(让模型"知道"工具存在),又传给 guided decoder(在解码层面"强制"输出格式合法)。

parse_tool_call:从文本里抠出工具调用

python
def parse_tool_call(text):
    m = re.search(r'\{.*\}', text, re.S)   # 抠出第一个 {...}
    obj = json.loads(m.group(0))
    if 'name' in obj and 'args' in obj: return obj

教学版用正则抠 JSON;真实版由 guided decoder 保证整段输出就是合法 JSON,无需正则。

execute_tool:模拟执行

python
def execute_tool(call):
    name, args = call['name'], call['args']
    if name == "get_weather":  return f"{args.get('city','?')}: 晴 25°C"
    if name == "calculator":   return str(eval(args.get('expr','0')))  # 教学用 eval

教学版用本地函数模拟;真实版调外部 API(HTTP/数据库)。

tool_loop:ReAct 主循环

python
def tool_loop(user_query, max_iters=3):
    prompt = user_query
    for _ in range(max_iters):
        out = scripted_model(prompt)          # 教学版:脚本驱动多轮不同工具
        call = parse_tool_call(out)
        if call is None:
            return prompt + " → 最终答案"      # 不再调工具,收敛
        result = execute_tool(call)
        prompt += f"\n[tool:{call['name']}]->{result}"
    return prompt

教学版用 scripted_model 按轮次返回不同工具调用(先 calculator 再 get_weather),最后一轮返回非 JSON 给出最终答案,体现"多轮工具 → 收敛"的完整轨迹。真实版由模型自己决定下一步。

❓ Q1:模型怎么"知道"有哪些工具可以调?

问题:模型不是只会生成文本吗,它怎么知道 get_weathercalculator 的存在?

答案:工具描述被写进 system prompt

System: 你有以下工具可用:
  - get_weather(city): 查询指定城市的天气
  - calculator(expr): 计算数学表达式

请根据需要调用工具。如果不需要,直接回答用户问题。

模型在训练时见过大量"工具调用"的对话样本,学会了根据上下文选择合适的工具。它不是真的"调用"函数,而是生成一个看起来像工具调用的 JSON 字符串。下游解析器拿到这个 JSON 后,才真正执行函数。

❓ Q2:eval() 在教学版里用,生产环境有什么安全问题?

问题eval(args.get('expr','0')) 可以执行任意 Python 代码!

答案绝对不要在生产环境用 eval 处理用户输入! 教学版只是为了演示。生产系统的做法:

python
# 安全替代方案:
import ast
# 方案 1:ast.literal_eval(只允许字面量表达式)
result = ast.literal_eval(safe_expr)  # 不能调用函数/导入模块

# 方案 2:专用计算器库
import numexpr
result = numexpr.evaluate(expr)  # 只支持数学运算

# 方案 3:沙箱执行
# 在隔离容器(Docker / gVisor)中执行,即使被注入也影响有限

OpenAI 的 calculator tool 内部也是沙箱执行,不是直接 eval。

❓ Q3:多工具并行(parallel tool calls)是怎么实现的?

问题:OpenAI 支持一次调多个工具(parallel tool calls),教学版是串行的。

答案:Parallel tool calls 的核心是模型一次性输出多个工具调用

json
{
  "tool_calls": [
    {"name": "get_weather", "args": {"city": "北京"}},
    {"name": "get_weather", "args": {"city": "上海"}}
  ]
}

执行时并行调用:

python
results = asyncio.gather(
    execute_tool(call) for call in tool_calls
)

把结果一起回填到 prompt。收益:两个独立工具调用可以并发,延迟取 max 而非 sum。教学版是串行的,没有展示这个优化。


教学版 vs 真实框架

维度教学版真实框架
模型fake_model_output 脚本驱动真 LLM 生成工具调用
工具描述JSON Schema(与 OpenAI 规范一致)JSON Schema + guided decoder 编译为 FSM
输出约束假设已是合法 JSONadv15 guided decoder 从 schema 编译 FSM 强约束
工具执行本地函数外部 HTTP API / 数据库 / 代码沙箱
循环固定 max_iters模型自主决定何时收敛(生成 final_answer
多工具并行串行OpenAI parallel tool calls

真实框架参考:

  • OpenAI function callingtools + tool_choice 参数,模型输出 tool_calls 数组
  • vLLM guided tool calling:用 Outlines/lm-format-enforcer 把输出约束成工具 schema
  • LangChain / LlamaIndex agent:ReAct / Tool Calling agent 框架,封装上述循环

运行

bash
python run.py

预期输出(循环轨迹):

用户提问: 北京天气如何?人口多少?
[轮 1] 模型输出: {"name":"calculator","args":{"expr":"1899+29000000"}}
       执行 → 29001899
[轮 2] 模型输出: {"name":"get_weather","args":{"city":"北京"}}
       执行 → 北京: 晴 25°C
[轮 3] 模型输出: (无工具调用,给最终答案)
       最终答案: 北京晴 25°C,人口约 2900 万。

✅ adv16_function_call 通过

下一步

adv01–adv16 至此全部完成。回到 推理优化总结 查看完整优化手段速查表,或复习主系列任意一步对比进阶版与教学版差异。

本进阶系列覆盖的 16 项与主系列 15 步合起来,构成了一个相对完整的 LLM 推理优化知识地图:精度控制 → 解码加速 → 缓存结构 → 大规模并行 → 分离式架构 → 模型架构变体 → 服务工程。