函数调用¶
演示如何为 LLM 定义工具、解析模型返回的工具调用、本地执行工具并把结果回传给模型, 完成一次完整的 ReAct 式闭环。所有逻辑通过依赖注入可在测试中用伪造客户端验证。 对应教程:第3卷《OpenAI API》第3章 函数调用。
文件路径: examples/volume-03-openai-api\function_calling.py
源代码¶
"""函数调用(Function Calling / Tool Calling)示例。
演示如何为 LLM 定义工具、解析模型返回的工具调用、本地执行工具并把结果回传给模型,
完成一次完整的 ReAct 式闭环。所有逻辑通过依赖注入可在测试中用伪造客户端验证。
对应教程:第3卷《OpenAI API》第3章 函数调用。
"""
from __future__ import annotations
import json
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, Protocol
class ChatClient(Protocol):
"""支持工具调用的聊天补全客户端协议。"""
@property
def chat(self) -> Any: ...
@dataclass
class ToolCall:
"""一次工具调用请求。
Attributes:
id: 调用 ID(用于回传结果时对应)。
name: 工具名称。
arguments: 工具参数(已解析的字典)。
"""
id: str
name: str
arguments: dict[str, Any]
@classmethod
def from_raw(cls, raw: Any) -> ToolCall:
"""从模型返回的原始对象构造 ``ToolCall``。
Args:
raw: 形如 ``{"id": ..., "function": {"name": ..., "arguments": "..."}}``
的字典或对象。
Returns:
``ToolCall`` 实例。
Raises:
ValueError: 参数 JSON 非法时。
"""
if isinstance(raw, dict):
call_id = raw.get("id", "")
function = raw.get("function", {})
name = function.get("name", "")
raw_args = function.get("arguments", "{}")
else:
call_id = getattr(raw, "id", "")
function = getattr(raw, "function", None)
name = getattr(function, "name", "")
raw_args = getattr(function, "arguments", "{}")
try:
arguments = json.loads(raw_args) if isinstance(raw_args, str) else dict(raw_args)
except json.JSONDecodeError as exc:
raise ValueError(f"工具参数 JSON 非法: {raw_args!r}") from exc
return cls(id=call_id, name=name, arguments=arguments)
@dataclass
class ToolDef:
"""工具定义。
Attributes:
name: 工具名称。
description: 工具描述。
parameters: JSON Schema 参数定义。
func: 实际执行的 Python 函数。
"""
name: str
description: str
parameters: dict[str, Any]
func: Callable[..., Any]
def to_openai_schema(self) -> dict[str, Any]:
"""转换为 OpenAI 工具调用 schema。
Returns:
OpenAI ``tools`` 字段所需的字典。
"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
},
}
@dataclass
class ToolRegistry:
"""工具注册表。"""
_tools: dict[str, ToolDef] = field(default_factory=dict)
def register(self, tool: ToolDef) -> None:
"""注册工具。
Args:
tool: 工具定义。
"""
self._tools[tool.name] = tool
def schemas(self) -> list[dict[str, Any]]:
"""返回所有工具的 OpenAI schema。
Returns:
schema 列表。
"""
return [t.to_openai_schema() for t in self._tools.values()]
def dispatch(self, call: ToolCall) -> str:
"""执行一次工具调用。
Args:
call: 工具调用请求。
Returns:
工具执行结果(字符串)。
Raises:
KeyError: 工具未注册时。
"""
tool = self._tools.get(call.name)
if tool is None:
raise KeyError(f"未注册的工具: {call.name}")
result = tool.func(**call.arguments)
return str(result)
def extract_tool_calls(response: Any) -> list[ToolCall]:
"""从模型响应中提取工具调用列表。
Args:
response: 聊天补全响应对象。
Returns:
``ToolCall`` 列表;若无工具调用返回空列表。
"""
choices = getattr(response, "choices", None)
if not choices:
return []
message = getattr(choices[0], "message", None)
if message is None:
return []
raw_calls = getattr(message, "tool_calls", None)
if not raw_calls:
return []
return [ToolCall.from_raw(c) for c in raw_calls]
def get_response_text(response: Any) -> str:
"""提取模型回复文本。"""
choices = getattr(response, "choices", None)
if not choices:
return ""
message = getattr(choices[0], "message", None)
return getattr(message, "content", "") or ""
def run_function_call_loop(
client: ChatClient,
user_query: str,
registry: ToolRegistry,
model: str = "gpt-4o-mini",
max_rounds: int = 4,
) -> str:
"""执行完整的函数调用闭环。
流程:发起请求 → 若返回工具调用则执行并把结果回传 → 直到模型给出最终文本。
Args:
client: 符合 ``ChatClient`` 协议的客户端。
user_query: 用户问题。
registry: 工具注册表。
model: 模型名称。
max_rounds: 最大往返轮数,防止死循环。
Returns:
模型最终回复文本。
Raises:
RuntimeError: 超过最大轮数仍未结束。
"""
messages: list[dict[str, Any]] = [{"role": "user", "content": user_query}]
tools = registry.schemas()
for _ in range(max_rounds):
response = client.chat.completions.create( # type: ignore[union-attr]
model=model,
messages=messages,
tools=tools,
)
tool_calls = extract_tool_calls(response)
if not tool_calls:
return get_response_text(response)
# 把助手发起的工具调用加入历史
messages.append(
{
"role": "assistant",
"content": get_response_text(response),
"tool_calls": [
{
"id": c.id,
"type": "function",
"function": {"name": c.name, "arguments": json.dumps(c.arguments)},
}
for c in tool_calls
],
}
)
# 执行每个工具并把结果作为 tool 消息回传
for call in tool_calls:
result = registry.dispatch(call)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
raise RuntimeError("函数调用循环超过最大轮数仍未结束")
# ===== 内置示例工具 =====
def get_weather(city: str, unit: str = "celsius") -> str:
"""示例工具:获取城市天气(教学用,返回固定值)。
Args:
city: 城市名。
unit: 温度单位。
Returns:
天气描述字符串。
"""
fake_temp = {"北京": 18, "上海": 22, "广州": 28}.get(city, 20)
if unit == "fahrenheit":
fake_temp = fake_temp * 9 // 5 + 32
return f"{city}:{fake_temp}°F,晴。"
return f"{city}:{fake_temp}°C,晴。"
def calculate(expression: str) -> str:
"""示例工具:计算算术表达式。
Args:
expression: 算术表达式字符串。
Returns:
计算结果字符串。
"""
allowed = set("0123456789+-*/(). ")
if not expression or not set(expression) <= allowed:
return "错误:表达式非法。"
try:
value = eval(expression, {"__builtins__": {}}, {}) # noqa: S307
except ZeroDivisionError:
return "错误:除零。"
except Exception: # noqa: BLE001
return "错误:无法计算。"
return f"{expression} = {value}"
def build_default_registry() -> ToolRegistry:
"""构造包含示例工具的注册表。
Returns:
注册了天气与计算工具的 ``ToolRegistry``。
"""
registry = ToolRegistry()
registry.register(
ToolDef(
name="get_weather",
description="获取指定城市的当前天气",
parameters={
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
func=get_weather,
)
)
registry.register(
ToolDef(
name="calculate",
description="计算一个只含数字与四则运算符的算术表达式",
parameters={
"type": "object",
"properties": {
"expression": {"type": "string", "description": "算术表达式"},
},
"required": ["expression"],
},
func=calculate,
)
)
return registry
def main() -> None:
"""演示工具调用闭环(使用伪造客户端,不调用真实 API)。"""
# 构造一个按脚本返回的伪造客户端
import types
class _FakeMessage:
def __init__(self, content: str, tool_calls: list | None = None) -> None:
self.content = content
self.tool_calls = tool_calls
class _FakeChoice:
def __init__(self, message: _FakeMessage) -> None:
self.message = message
class _FakeResponse:
def __init__(self, message: _FakeMessage) -> None:
self.choices = [_FakeChoice(message)]
class _FakeCompletions:
def __init__(self, script: list[_FakeMessage]) -> None:
self._script = script
self._i = 0
def create(self, **kwargs: object) -> _FakeResponse:
msg = self._script[self._i]
self._i += 1
return _FakeResponse(msg)
class _FakeChat:
def __init__(self, script: list[_FakeMessage]) -> None:
self.completions = _FakeCompletions(script)
class _FakeClient:
def __init__(self, script: list[_FakeMessage]) -> None:
self.chat = _FakeChat(script)
script = [
_FakeMessage(
content="",
tool_calls=[
types.SimpleNamespace(
id="call-1",
function=types.SimpleNamespace(
name="get_weather",
arguments=json.dumps({"city": "北京"}),
),
)
],
),
_FakeMessage(content="北京目前 18°C,晴天,适合出行。"),
]
client = _FakeClient(script)
registry = build_default_registry()
answer = run_function_call_loop(client, "北京天气怎么样?", registry)
print(f"最终回复: {answer}")
if __name__ == "__main__":
main()