LLM函数调用与工具使用工程化深度实战:从Function Calling到并行调用与错误恢复的全解析
【摘要】 LLM函数调用与工具使用工程化深度实战:从Function Calling到并行调用与错误恢复的全解析 引言函数调用(Function Calling)是LLM从"文本生成器"进化为"行动执行者"的关键能力。模型根据用户意图决定调用哪个函数、传递什么参数,应用层执行后把结果返回模型继续推理。但生产级工具使用系统远非"定义几个函数"那么简单:并行调用的编排、参数校验与类型安全、执行错误的自恢...
LLM函数调用与工具使用工程化深度实战:从Function Calling到并行调用与错误恢复的全解析
引言
函数调用(Function Calling)是LLM从"文本生成器"进化为"行动执行者"的关键能力。模型根据用户意图决定调用哪个函数、传递什么参数,应用层执行后把结果返回模型继续推理。但生产级工具使用系统远非"定义几个函数"那么简单:并行调用的编排、参数校验与类型安全、执行错误的自恢复、上下文窗口管理(多轮工具调用的token爆炸)、工具权限与安全沙箱、工具调用可观测性。本文从函数调用协议讲起,覆盖工具定义与JSON Schema、并行调用与依赖排序、参数验证与类型安全、错误恢复与重试策略、上下文窗口压缩、工具权限矩阵、调用链追踪与审计、性能优化,构建企业级LLM工具使用系统。
一、工具定义与Schema
1.1 类型安全的工具定义
# tools/registry.py - 工具注册与Schema生成
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, get_type_hints
import inspect
import json
@dataclass
class ToolDefinition:
name: str
description: str
parameters: dict # JSON Schema
required: list[str]
function: Callable
timeout_ms: int = 30000
category: str = "general"
permissions: list[str] = field(default_factory=list)
idempotent: bool = False
destructive: bool = False
class ToolRegistry:
"""工具注册中心:Schema生成、权限管理、分类索引"""
def __init__(self):
self.tools: dict[str, ToolDefinition] = {}
self.categories: dict[str, list[str]] = {}
def register(self, name: str, description: str, category: str = "general",
timeout_ms: int = 30000, permissions: list[str] = None,
idempotent: bool = False, destructive: bool = False):
"""装饰器:注册函数为工具"""
def decorator(func: Callable):
# 从类型注解生成JSON Schema
hints = get_type_hints(func)
sig = inspect.signature(func)
properties = {}
required = []
for param_name, param in sig.parameters.items():
if param_name in ('self', 'ctx', 'context'):
continue
py_type = hints.get(param_name, str)
schema = self._py_type_to_schema(py_type, param)
properties[param_name] = schema
if param.default == inspect.Parameter.empty:
required.append(param_name)
tool = ToolDefinition(
name=name, description=description,
parameters={"type": "object", "properties": properties},
required=required, function=func,
timeout_ms=timeout_ms, category=category,
permissions=permissions or [],
idempotent=idempotent, destructive=destructive,
)
self.tools[name] = tool
self.categories.setdefault(category, []).append(name)
return func
return decorator
def get_openai_tools(self) -> list[dict]:
"""生成OpenAI函数调用格式的工具列表"""
return [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.parameters,
},
}
for t in self.tools.values()
]
def get_tool(self, name: str) -> ToolDefinition | None:
return self.tools.get(name)
def _py_type_to_schema(self, py_type, param) -> dict:
type_map = {
str: {"type": "string"},
int: {"type": "integer"},
float: {"type": "number"},
bool: {"type": "boolean"},
list: {"type": "array", "items": {"type": "string"}},
dict: {"type": "object"},
}
schema = type_map.get(py_type, {"type": "string"})
if param.default != inspect.Parameter.empty:
schema["default"] = param.default
return schema
# 全局注册器
registry = ToolRegistry()
# 注册工具
@registry.register(
"search_products",
"搜索商品库,返回匹配的商品列表。支持按名称、类别、价格范围筛选。",
category="commerce", idempotent=True,
)
async def search_products(
query: str,
category: str = "",
min_price: float = 0,
max_price: float = 999999,
limit: int = 20,
) -> list[dict]:
"""搜索商品"""
# 实际查询数据库
return [{"id": "p1", "name": "Widget", "price": 29.9}]
@registry.register(
"create_order",
"创建订单。需要用户ID、商品列表、配送地址。",
category="commerce", destructive=False, idempotent=True,
permissions=["order:create"],
)
async def create_order(
user_id: str,
items: list[dict],
shipping_address: str,
) -> dict:
return {"order_id": "ord-123", "status": "pending"}
@registry.register(
"cancel_order",
"取消订单。仅限待支付状态的订单。",
category="commerce", destructive=True,
permissions=["order:cancel"],
)
async def cancel_order(order_id: str, reason: str = "") -> dict:
return {"cancelled": True, "order_id": order_id}
二、并行调用与依赖排序
2.1 调用编排器
# tools/orchestrator.py
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import Any
@dataclass
class ToolCall:
id: str
name: str
arguments: dict
result: Any = None
error: str = None
duration_ms: float = 0
class ToolOrchestrator:
"""工具调用编排:并行执行无依赖调用"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
async def execute_calls(self, calls: list[ToolCall],
context: dict) -> list[ToolCall]:
"""执行一批工具调用,无依赖的并行"""
# 分析依赖:如果调用的参数引用了其他调用的结果,则串行
# 简化:全部并行(生产实现需解析参数中的引用)
tasks = [self._execute_one(call, context) for call in calls]
results = await asyncio.gather(*tasks, return_exceptions=True)
for call, result in zip(calls, results):
if isinstance(result, Exception):
call.error = str(result)
else:
call = result
return calls
async def _execute_one(self, call: ToolCall,
context: dict) -> ToolCall:
import time
tool = self.registry.get_tool(call.name)
if not tool:
call.error = f"unknown tool: {call.name}"
return call
# 权限检查
if tool.permissions:
user_perms = context.get("permissions", [])
if not all(p in user_perms for p in tool.permissions):
call.error = f"permission denied: requires {tool.permissions}"
return call
# 参数校验
validation = self._validate_args(tool, call.arguments)
if not validation["valid"]:
call.error = f"invalid args: {validation['errors']}"
return call
# 超时执行
start = time.monotonic()
try:
result = await asyncio.wait_for(
tool.function(**call.arguments),
timeout=tool.timeout_ms / 1000,
)
call.result = result
except asyncio.TimeoutError:
call.error = f"timeout after {tool.timeout_ms}ms"
except Exception as e:
call.error = str(e)
# 幂等工具自动重试
if tool.idempotent and not call.error.startswith("timeout"):
await asyncio.sleep(1)
try:
result = await asyncio.wait_for(
tool.function(**call.arguments),
timeout=tool.timeout_ms / 1000,
)
call.result = result
call.error = None
except Exception:
pass
call.duration_ms = (time.monotonic() - start) * 1000
return call
def _validate_args(self, tool: ToolDefinition,
args: dict) -> dict:
errors = []
for req in tool.required:
if req not in args or args[req] is None:
errors.append(f"missing required: {req}")
properties = tool.parameters.get("properties", {})
for key, value in args.items():
if key not in properties:
errors.append(f"unknown parameter: {key}")
continue
expected_type = properties[key].get("type")
if expected_type and not self._check_type(value, expected_type):
errors.append(f"{key}: expected {expected_type}, got {type(value).__name__}")
return {"valid": len(errors) == 0, "errors": errors}
def _check_type(self, value: Any, expected: str) -> bool:
checks = {
"string": lambda v: isinstance(v, str),
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
"number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
"boolean": lambda v: isinstance(v, bool),
"array": lambda v: isinstance(v, list),
"object": lambda v: isinstance(v, dict),
}
return checks.get(expected, lambda v: True)(value)
2.2 多轮对话循环
# tools/conversation_loop.py
import json
from typing import Any
class ToolConversationLoop:
"""多轮工具调用对话循环"""
def __init__(self, llm_client, orchestrator: ToolOrchestrator,
registry: ToolRegistry, max_rounds: int = 10):
self.llm = llm_client
self.orchestrator = orchestrator
self.registry = registry
self.max_rounds = max_rounds
async def run(self, user_message: str,
system: str = "", context: dict = None) -> str:
context = context or {}
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": user_message})
tools = self.registry.get_openai_tools()
for round_num in range(self.max_rounds):
# 1. 模型决策
response = await self.llm.chat.complete(
messages=messages, tools=tools, tool_choice="auto",
temperature=0.1,
)
assistant_msg = response.choices[0].message
messages.append(assistant_msg.model_dump())
# 2. 如果没有工具调用,返回最终回答
if not assistant_msg.tool_calls:
return assistant_msg.content
# 3. 执行工具调用
tool_calls = []
for tc in assistant_msg.tool_calls:
import uuid
args = json.loads(tc.function.arguments)
tool_calls.append(ToolCall(
id=tc.id, name=tc.function.name, arguments=args,
))
executed = await self.orchestrator.execute_calls(tool_calls, context)
# 4. 将结果返回给模型
for call in executed:
if call.error:
content = json.dumps({"error": call.error})
else:
content = json.dumps(call.result, ensure_ascii=False,
default=str)
# 截断过长的结果
if len(content) > 4000:
content = content[:4000] + "...(truncated)"
messages.append({
"role": "tool", "tool_call_id": call.id,
"content": content,
})
# 5. 上下文窗口管理
messages = self._compress_context(messages)
return "达到最大工具调用轮数限制"
def _compress_context(self, messages: list[dict]) -> list[dict]:
"""压缩上下文:保留system+最近N轮,中间用摘要替代"""
if len(messages) <= 20:
return messages
system_msgs = [m for m in messages if m["role"] == "system"]
recent = messages[-12:]
# 中间消息摘要(简化:直接删除中间的tool结果)
compressed = system_msgs + [{"role": "system",
"content": f"(之前{len(messages) - len(system_msgs) - len(recent)}条消息已压缩)"}]
+ recent
return compressed
三、工具权限与安全
3.1 权限矩阵
# tools/security.py
from dataclasses import dataclass, field
from typing import Any
@dataclass
class ToolPermission:
tool_name: str
required_roles: list[str] = field(default_factory=list)
required_scopes: list[str] = field(default_factory=list)
rate_limit_per_minute: int = 60
destructive_confirmation: bool = False
allowed_parameters: dict[str, Any] = field(default_factory=dict) # 参数值白名单
class ToolSecurityManager:
"""工具安全:权限、限流、确认门"""
PERMISSIONS = {
"create_order": ToolPermission(
tool_name="create_order",
required_roles=["customer", "admin"],
required_scopes=["order:create"],
rate_limit_per_minute= 10,
),
"cancel_order": ToolPermission(
tool_name="cancel_order",
required_roles=["customer", "admin"],
required_scopes=["order:cancel"],
rate_limit_per_minute=5,
destructive_confirmation=True,
),
"delete_user": ToolPermission(
tool_name="delete_user",
required_roles=["admin"],
required_scopes=["user:delete"],
rate_limit_per_minute=2,
destructive_confirmation=True,
),
"search_products": ToolPermission(
tool_name="search_products",
required_roles=[], # 公开
rate_limit_per_minute=100,
),
}
def __init__(self):
self.rate_counters: dict[str, list[float]] = {}
def check_permission(self, tool_name: str, context: dict) -> dict:
perm = self.PERMISSIONS.get(tool_name)
if not perm:
return {"allowed": False, "reason": "unknown tool"}
# 角色检查
user_roles = context.get("roles", [])
if perm.required_roles and not any(r in user_roles for r in perm.required_roles):
return {"allowed": False, "reason": f"requires roles: {perm.required_roles}"}
# Scope检查
user_scopes = context.get("scopes", [])
if perm.required_scopes and not all(s in user_scopes for s in perm.required_scopes):
return {"allowed": False, "reason": f"requires scopes: {perm.required_scopes}"}
# 限流
if not self._check_rate_limit(tool_name, context.get("user_id", "anon"),
perm.rate_limit_per_minute):
return {"allowed": False, "reason": "rate limit exceeded"}
# 破坏性操作需要确认
if perm.destructive_confirmation:
return {"allowed": True, "requires_confirmation": True,
"reason": "destructive operation"}
return {"allowed": True}
def _check_rate_limit(self, tool: str, user: str, limit: int) -> bool:
import time
key = f"{tool}:{user}"
now = time.time()
self.rate_counters.setdefault(key, [])
self.rate_counters[key] = [t for t in self.rate_counters[key] if now - t < 60]
if len(self.rate_counters[key]) >= limit:
return False
self.rate_counters[key].append(now)
return True
四、调用链追踪
# tools/trace.py
from dataclasses import dataclass, field
from datetime import datetime
import json
@dataclass
class TraceSpan:
span_id: str
tool_name: str
arguments: dict
result: Any = None
error: str = None
start_time: str = ""
end_time: str = ""
duration_ms: float = 0
permissions_checked: bool = False
retried: bool = False
@dataclass
class ToolTrace:
request_id: str
user_id: str
spans: list[TraceSpan] = field(default_factory=list)
total_tokens: int = 0
total_rounds: int = 0
def summary(self) -> dict:
return {
"request_id": self.request_id,
"user_id": self.user_id,
"rounds": self.total_rounds,
"tools_called": [s.tool_name for s in self.spans],
"total_duration_ms": sum(s.duration_ms for s in self.spans),
"errors": [s.error for s in self.spans if s.error],
"tokens": self.total_tokens,
}
class ToolTracer:
"""工具调用链追踪器"""
def __init__(self):
self.traces: dict[str, ToolTrace] = {}
def start_trace(self, request_id: str, user_id: str) -> ToolTrace:
trace = ToolTrace(request_id=request_id, user_id=user_id)
self.traces[request_id] = trace
return trace
def add_span(self, request_id: str, span: TraceSpan):
if request_id in self.traces:
self.traces[request_id].spans.append(span)
def get_trace(self, request_id: str) -> ToolTrace | None:
return self.traces.get(request_id)
def export(self, request_id: str) -> str:
trace = self.traces.get(request_id)
if not trace:
return "{}"
return json.dumps({
"request_id": trace.request_id,
"spans": [
{
"tool": s.tool_name,
"args": s.arguments,
"result": str(s.result)[:200] if s.result else None,
"error": s.error,
"duration_ms": s.duration_ms,
}
for s in trace.spans
],
}, ensure_ascii=False, indent=2)
五、OpenAI函数调用完整示例
# tools/openai_integration.py
from openai import AsyncOpenAI
class OpenAIToolAgent:
"""完整的OpenAI函数调用Agent"""
SYSTEM_PROMPT = """你是ACME商城的智能客服助手。
你可以使用工具帮助用户查询商品、创建订单、取消订单。
规则:
1. 创建订单前必须确认用户意图
2. 取消订单必须询问取消原因
3. 无法处理的请求转人工客服
4. 所有金额以人民币(CNY)显示
"""
def __init__(self, registry: ToolRegistry, api_key: str):
self.client = AsyncOpenAI(api_key=api_key)
self.registry = registry
self.orchestrator = ToolOrchestrator(registry)
self.security = ToolSecurityManager()
self.tracer = ToolTracer()
self.loop = ToolConversationLoop(
self.client, self.orchestrator, registry, max_rounds=8,
)
async def chat(self, user_id: str, message: str,
roles: list[str] = None) -> str:
import uuid
request_id = str(uuid.uuid4())
context = {
"user_id": user_id,
"roles": roles or ["customer"],
"scopes": ["order:create", "order:cancel"],
"permissions": ["order:create", "order:cancel"],
"request_id": request_id,
}
# 注入安全检查到orchestrator
self.orchestrator.security = self.security
self.orchestrator.context = context
# 启动追踪
self.tracer.start_trace(request_id, user_id)
# 运行对话循环
response = await self.loop.run(
user_message=message,
system=self.SYSTEM_PROMPT,
context=context,
)
# 记录追踪摘要
trace = self.tracer.get_trace(request_id)
if trace:
print(f"[TRACE] {trace.summary()}")
return response
总结
LLM函数调用的工程化体系以"安全、可靠、可观测"三柱展开:工具注册中心从Python类型注解自动生成JSON Schema保证类型安全,并行调用编排器将无依赖的工具调用并行执行最大化吞吐,权限矩阵以角色+scope+限流+破坏性确认门分层管控工具使用风险,多轮对话循环处理模型决策→工具执行→结果回传→继续推理的迭代,上下文压缩在多轮调用token爆炸时智能截断中间结果保留最近上下文,调用链追踪器以span粒度记录每次工具调用的参数、结果、耗时、错误用于可观测性与审计。当LLM从"回答问题"进化为"调用工具执行操作",这套工程化体系正是把"偶尔能用"推向"生产可靠"的安全网与加速器。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)