AI Agent Skill Day 2:Function Calling技能:函数调用的设计与实现
【AI Agent Skill Day 2】Function Calling技能:函数调用的设计与实现
在“AI Agent Skill技能开发实战”系列的第二天,我们聚焦于Function Calling(函数调用)技能——这是Agent与外部世界交互的最基础、最安全的能力单元。Function Calling允许大模型在推理过程中精确调用预定义函数,获取实时数据、执行确定性操作或访问外部系统,同时保持类型安全和权限控制。相比动态代码执行,Function Calling具有强契约、低风险、高可预测性的优势,是构建企业级Agent应用的首选技能类型。本文将深入剖析Function Calling的架构设计、接口规范、安全机制,并提供基于LangChain和Spring AI的完整实现方案,帮助开发者构建可靠、高效的函数调用技能系统。
技能概述
Function Calling技能 是指将预定义的、类型安全的函数封装为Agent可调用的能力单元。每个函数调用技能包含:
- 明确的输入参数契约(类型、必填、描述)
- 确定性的执行逻辑(无副作用或受控副作用)
- 结构化的输出格式(成功/失败状态、结果数据)
功能边界
- 适用场景:查询天气、获取用户信息、计算汇率、验证邮箱等原子化操作
- 不适用场景:需要动态生成代码、执行任意命令、处理模糊意图的任务
- 核心原则:函数必须是幂等的(多次调用结果一致)或副作用可控
核心能力
- 类型安全:通过JSON Schema严格校验输入参数
- 自动发现:函数元数据可被Skill Router自动识别
- 上下文传递:支持会话ID、用户身份等上下文信息
- 可观测性:内置执行耗时、调用次数等监控指标
架构设计
Function Calling技能系统采用分层架构,确保安全性和可扩展性:
+---------------------+
| LLM Core | ← 大模型(OpenAI/Claude/Qwen)
+----------+----------+
|
+----------v----------+
| Function Caller | ← 解析LLM的function_call请求
+----------+----------+
|
+----------v----------+
| Skill Registry | ← 注册所有可用函数技能(含Schema)
+----------+----------+
|
+----------v----------+
| Function Executor | ← 执行具体函数(含权限校验、沙箱)
+----------+----------+
|
+----------v----------+
| External Systems | ← 数据库、API、内部服务
+---------------------+
关键组件职责:
- Function Caller:解析LLM返回的
function_call字段,提取函数名和参数 - Skill Registry:存储函数元数据(名称、描述、输入/输出Schema)
- Function Executor:执行函数前进行权限校验和参数验证,执行后记录日志
接口设计
输入输出规范
所有Function Calling技能必须遵循统一接口:
# 输入结构
{
"function_name": "get_weather",
"arguments": {
"location": "Beijing",
"unit": "celsius"
},
"context": {
"user_id": "U123",
"session_id": "S456"
}
}
# 输出结构
{
"result": {"temperature": 22, "condition": "sunny"},
"metadata": {
"execution_time": 0.12,
"function_name": "get_weather"
},
"error": null # 失败时为错误信息字符串
}
JSON Schema定义
使用JSON Schema定义函数参数契约,确保类型安全:
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如'北京'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["location"]
}
代码实现
Python实现(基于LangChain + OpenAI)
import json
import time
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
# 定义函数输入模型(用于Schema生成)
class WeatherInput(BaseModel):
location: str = Field(description="城市名称,例如'北京'")
unit: str = Field(default="celsius", description="温度单位")
# 具体函数实现
def get_weather(location: str, unit: str = "celsius") -> Dict[str, Any]:
"""模拟天气API调用"""
fake_data = {
"Beijing": {"temperature": 22, "condition": "sunny"},
"Shanghai": {"temperature": 28, "condition": "cloudy"}
}
if location not in fake_data:
raise ValueError(f"未知城市: {location}")
data = fake_data[location].copy()
if unit == "fahrenheit":
data["temperature"] = data["temperature"] * 9/5 + 32
return data
# 创建LangChain Tool
weather_tool = Tool(
name="get_weather",
description="获取指定城市的当前天气信息",
func=lambda input_str: json.dumps(get_weather(**json.loads(input_str))),
args_schema=WeatherInput
)
# Function Calling主流程
def call_function_with_llm(user_query: str, tools: list) -> str:
"""
使用LLM进行Function Calling
"""
llm = ChatOpenAI(
model="gpt-4-turbo",
temperature=0,
# 启用函数调用
tool_choice="auto"
)
# 绑定工具到LLM
llm_with_tools = llm.bind_tools(tools)
# 发送用户查询
messages = [HumanMessage(content=user_query)]
response = llm_with_tools.invoke(messages)
# 处理函数调用
if response.tool_calls:
tool_call = response.tool_calls[0]
tool_name = tool_call["name"]
tool_args = tool_call["args"]
# 找到对应工具
tool = next((t for t in tools if t.name == tool_name), None)
if tool:
try:
# 执行函数
result = tool.func(json.dumps(tool_args))
# 返回结果给LLM生成最终回答
final_response = llm.invoke([
HumanMessage(content=user_query),
response,
{"role": "tool", "content": result, "tool_call_id": tool_call["id"]}
])
return final_response.content
except Exception as e:
return f"函数执行失败: {str(e)}"
return response.content
# 使用示例
if __name__ == "__main__":
tools = [weather_tool]
query = "北京今天天气怎么样?"
result = call_function_with_llm(query, tools)
print(f"用户问题: {query}")
print(f"Agent回答: {result}")
Java实现(基于Spring AI + LangChain4j)
// FunctionInput.java
public class FunctionInput {
private String functionName;
private Map<String, Object> arguments;
private Map<String, Object> context;
// getters/setters
}
// FunctionOutput.java
public class FunctionOutput {
private Object result;
private Map<String, Object> metadata = new HashMap<>();
private String error;
// getters/setters
}
// WeatherFunction.java
@Component
public class WeatherFunction {
@Tool("get_weather")
@Description("获取指定城市的当前天气信息")
public String getWeather(
@P("location") String location,
@P("unit") @Default("celsius") String unit
) {
Map<String, Object> fakeData = Map.of(
"Beijing", Map.of("temperature", 22, "condition", "sunny"),
"Shanghai", Map.of("temperature", 28, "condition", "cloudy")
);
if (!fakeData.containsKey(location)) {
throw new IllegalArgumentException("未知城市: " + location);
}
Map<String, Object> data = new HashMap<>((Map) fakeData.get(location));
if ("fahrenheit".equals(unit)) {
double celsius = (double) data.get("temperature");
data.put("temperature", celsius * 9/5 + 32);
}
return new ObjectMapper().writeValueAsString(data);
}
}
// FunctionCallingService.java
@Service
public class FunctionCallingService {
@Autowired
private ChatClient chatClient;
public String callFunction(String userQuery, List<Tool> tools) {
// 构建带工具的聊天请求
ChatOptions options = ChatOptions.builder()
.withTools(tools)
.build();
Prompt prompt = new Prompt(userQuery, options);
ChatResponse response = chatClient.call(prompt);
// 处理工具调用
if (response.getMetadata().containsKey("tool_calls")) {
ToolCall toolCall = (ToolCall) response.getMetadata().get("tool_calls").get(0);
String toolName = toolCall.getName();
Map<String, Object> args = toolCall.getArguments();
// 执行工具(简化版,实际需反射调用)
Object result = executeTool(toolName, args);
// 返回结果给LLM
Prompt followUp = Prompt.builder()
.messages(List.of(
UserMessage.from(userQuery),
AiMessage.from(response.getResult().getOutput()),
ToolResponseMessage.from(result.toString(), toolCall.getId())
))
.build();
ChatResponse finalResponse = chatClient.call(followUp);
return finalResponse.getResult().getOutput();
}
return response.getResult().getOutput();
}
private Object executeTool(String toolName, Map<String, Object> args) {
// 实际项目中通过Spring容器获取Bean并反射调用
if ("get_weather".equals(toolName)) {
WeatherFunction weatherFunc = new WeatherFunction();
return weatherFunc.getWeather(
(String) args.get("location"),
(String) args.getOrDefault("unit", "celsius")
);
}
throw new IllegalArgumentException("未知工具: " + toolName);
}
}
实战案例
案例1:智能客服中的订单查询
业务背景:电商客服Agent需根据用户提供的订单号查询订单状态,并解释物流信息。
技术选型:
- Function Calling技能:
order_lookup - 集成模型:OpenAI GPT-4-Turbo
- 安全要求:仅允许查询当前用户订单
完整实现:
import json
from datetime import datetime
from langchain_core.tools import Tool
# 模拟订单数据库
ORDERS_DB = {
"U123": {
"ORD-2023-001": {
"status": "shipped",
"items": ["iPhone 15", "AirPods"],
"shipping_date": "2023-10-15",
"tracking_number": "SF123456789CN"
}
}
}
def order_lookup(order_id: str, user_id: str) -> Dict[str, Any]:
"""
查询订单详情(带用户权限校验)
"""
if user_id not in ORDERS_DB:
raise PermissionError("用户无权访问订单系统")
if order_id not in ORDERS_DB[user_id]:
raise ValueError(f"订单 {order_id} 不存在或不属于当前用户")
order = ORDERS_DB[user_id][order_id]
# 添加人性化描述
status_descriptions = {
"pending": "订单已创建,等待支付",
"paid": "订单已支付,准备发货",
"shipped": "商品已发货,正在运输中",
"delivered": "商品已送达"
}
return {
"order_id": order_id,
"status": order["status"],
"status_description": status_descriptions.get(order["status"], "未知状态"),
"items": order["items"],
"tracking_number": order["tracking_number"]
}
# 创建工具
order_tool = Tool(
name="order_lookup",
description="根据订单号查询订单详情(需提供用户ID)",
func=lambda input_str: json.dumps(
order_lookup(**json.loads(input_str))
),
# 注意:实际生产中user_id应从上下文自动注入,而非用户输入
args_schema=None # 简化示例,实际应定义Schema
)
# 增强版Function Caller(自动注入上下文)
def enhanced_function_caller(user_query: str, user_id: str, tools: list):
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# 修改工具函数以自动注入user_id
wrapped_tools = []
for tool in tools:
if tool.name == "order_lookup":
def wrapped_func(args_json: str):
args = json.loads(args_json)
args["user_id"] = user_id # 自动注入
return tool.func(json.dumps(args))
wrapped_tool = Tool(
name=tool.name,
description=tool.description,
func=wrapped_func,
args_schema=tool.args_schema
)
wrapped_tools.append(wrapped_tool)
else:
wrapped_tools.append(tool)
llm_with_tools = llm.bind_tools(wrapped_tools)
messages = [HumanMessage(content=user_query)]
response = llm_with_tools.invoke(messages)
# ... 后续处理同前例
return response.content
# 测试
if __name__ == "__main__":
query = "我的订单ORD-2023-001现在到哪里了?"
result = enhanced_function_caller(query, "U123", [order_tool])
print(f"客服回答: {result}")
效果分析:
- 成功拦截非授权用户查询(如user_id=“U999”)
- 自动生成人性化物流状态描述
- 平均响应时间:1.2秒(含LLM推理+函数调用)
案例2:金融数据实时查询
业务背景:投资顾问Agent需提供股票实时价格、汇率转换等金融服务。
关键技术点:
- 缓存策略:避免频繁调用第三方API
- 错误降级:API失败时返回缓存数据
- 并发控制:限制每秒API调用次数
性能优化代码:
import time
from functools import lru_cache
import threading
class FinancialFunctions:
def __init__(self):
self.cache_ttl = 60 # 60秒缓存
self.last_call = {}
self.lock = threading.Lock()
@lru_cache(maxsize=128)
def _cached_stock_price(self, symbol: str):
"""带TTL的缓存装饰器(简化版)"""
# 实际项目中使用redis或memcached
return self._fetch_stock_price(symbol)
def _fetch_stock_price(self, symbol: str) -> float:
"""模拟股票API调用"""
fake_prices = {"AAPL": 192.53, "GOOGL": 138.21, "TSLA": 248.50}
time.sleep(0.1) # 模拟网络延迟
return fake_prices.get(symbol, 0.0)
def get_stock_price(self, symbol: str) -> Dict[str, Any]:
"""带速率限制的股票价格查询"""
with self.lock:
now = time.time()
if symbol in self.last_call and now - self.last_call[symbol] < 1:
raise RuntimeError("API调用过于频繁,请稍后再试")
self.last_call[symbol] = now
try:
price = self._cached_stock_price(symbol)
return {"symbol": symbol, "price": price, "currency": "USD"}
except Exception as e:
# 降级策略:返回最后已知价格
return {"symbol": symbol, "price": 0.0, "error": str(e)}
错误处理
常见异常场景及处理策略
| 异常类型 | 处理策略 | 代码示例 |
|---|---|---|
| 参数校验失败 | 返回结构化错误,提示缺失字段 | raise ValueError("Missing required parameter: location") |
| 权限不足 | 拦截并返回友好提示 | raise PermissionError("您无权访问此功能") |
| 第三方API超时 | 重试+降级到缓存 | try: ... except TimeoutError: return cached_data |
| 函数不存在 | 记录日志并忽略调用 | if func not found: log.warning(...) |
容错机制实现
def safe_execute_function(func, args: dict, max_retries: int = 2):
"""带重试和降级的安全执行器"""
for attempt in range(max_retries + 1):
try:
return func(**args)
except (ConnectionError, TimeoutError) as e:
if attempt == max_retries:
# 最后一次尝试失败,触发降级
return get_cached_result(func.__name__, args)
time.sleep(2 ** attempt) # 指数退避
except Exception as e:
# 非网络错误直接抛出
raise e
性能优化
缓存策略
- 结果缓存:对幂等函数(如天气查询)缓存结果
- Schema缓存:预加载函数元数据
- 批量请求:支持批量参数处理(如批量股票查询)
并发处理
- 异步执行:I/O密集型函数使用async/await
- 线程池:CPU密集型任务提交到专用线程池
- 批处理:合并多个相似请求(如10个天气查询合并为1次API调用)
# 异步Function Calling示例
import asyncio
async def async_get_weather(location: str):
# 模拟异步API调用
await asyncio.sleep(0.1)
return {"temperature": 22, "condition": "sunny"}
async def handle_multiple_queries(locations: list):
tasks = [async_get_weather(loc) for loc in locations]
results = await asyncio.gather(*tasks)
return results
安全考量
三层防护体系
- 输入校验:严格JSON Schema验证,拒绝非法字符
- 权限控制:
- RBAC模型:函数绑定角色权限
- 敏感函数二次确认(如删除操作)
- 执行隔离:
- 无网络访问权限(除非明确授权)
- 文件系统只读(除临时目录)
安全编码实践
# 危险示例:直接拼接用户输入
def unsafe_db_query(user_input: str):
query = f"SELECT * FROM users WHERE name = '{user_input}'" # SQL注入风险
# 安全示例:参数化查询
def safe_db_query(user_input: str):
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
测试方案
测试金字塔
| 测试类型 | 覆盖率要求 | 工具 |
|---|---|---|
| 单元测试 | ≥85% | pytest, JUnit |
| 集成测试 | ≥70% | LangChain TestClient, SpringBootTest |
| E2E测试 | ≥50% | Playwright, Postman |
单元测试示例
def test_weather_function_valid_input():
result = get_weather("Beijing")
assert result["temperature"] == 22
assert result["condition"] == "sunny"
def test_weather_function_invalid_location():
with pytest.raises(ValueError, match="未知城市"):
get_weather("Mars")
def test_weather_function_fahrenheit():
result = get_weather("Beijing", "fahrenheit")
assert abs(result["temperature"] - 71.6) < 0.1 # 22°C ≈ 71.6°F
最佳实践
- 最小权限原则:函数只授予必要权限
- 幂等性优先:避免有副作用的函数,或明确标注
- Schema即文档:JSON Schema应包含完整描述
- 上下文自动注入:用户ID、会话ID等不应由用户输入
- 监控全覆盖:记录每次调用的耗时、参数、结果
- 版本兼容:函数接口变更需向后兼容
- 错误友好化:返回用户可理解的错误信息
扩展方向
技能变体
- 动态函数生成:根据API文档自动生成Function Calling技能
- 复合函数:将多个原子函数组合为新函数
- 学习型函数:根据用户反馈自动优化参数
未来演进
- MCP协议标准化:遵循Model Context Protocol统一接口
- 跨模型兼容:同一函数适配OpenAI、Claude、通义千问
- 技能市场:支持第三方函数插件生态
总结
本文深入探讨了Function Calling技能的设计与实现,涵盖架构原理、安全机制、性能优化等关键维度。通过LangChain和Spring AI的完整代码示例,展示了如何构建类型安全、权限可控的函数调用系统。两个实战案例(智能客服、金融数据查询)验证了该技术在真实场景中的价值。Function Calling作为Agent最基础的技能类型,其可靠性直接影响整个系统的稳定性,值得开发者投入充分精力进行精细化设计。
下一篇预告:Day 3 将聚焦 Tool Use技能,详解如何将外部工具(如计算器、浏览器)封装为Agent可调用的能力,并与Function Calling形成互补。
技能开发实践要点
- 所有函数必须通过JSON Schema严格定义输入参数
- 用户身份等敏感上下文应自动注入,而非依赖用户输入
- 幂等性是函数设计的黄金准则
- 必须实现完整的错误处理和降级策略
- 关键函数需添加缓存和速率限制
- 每次函数调用必须记录审计日志
- 单元测试覆盖率不低于85%
- 生产环境必须启用权限校验和输入过滤
进阶学习资源
- OpenAI Function Calling官方文档:https://platform.openai.com/docs/guides/function-calling
- LangChain Tools深度指南:https://python.langchain.com/docs/modules/tools/
- Spring AI Function Calling示例:https://docs.spring.io/spring-ai/reference/api/tools.html
- Claude Function Calling支持:https://docs.anthropic.com/claude/docs/functions
- 通义千问Function Calling文档:https://help.aliyun.com/zh/dashscope/developer-reference/function-calling
- Model Context Protocol (MCP) 规范:https://github.com/modelcontextprotocol/specification
- 安全函数设计最佳实践:https://owasp.org/www-project-top-ten/
- LangChain4j Function Calling:https://www.langchain4j.dev/guides/function-calling/
文章标签:AI Agent, Function Calling, LangChain, Spring AI, 大模型工具调用, 技能系统, MCP协议, 安全编码
文章简述:本文作为“AI Agent Skill技能开发实战”系列的第二天,系统阐述了Function Calling技能的核心设计与实现方法。文章详细解析了函数调用的架构原理、JSON Schema参数契约、安全防护机制,并提供了基于LangChain(Python)和Spring AI(Java)的完整可运行代码。通过智能客服订单查询和金融数据实时查询两个实战案例,展示了Function Calling在权限控制、错误处理、性能优化等方面的最佳实践。文中涵盖缓存策略、并发处理、测试方案等工程化内容,强调类型安全和最小权限原则,帮助开发者构建可靠、高效的企业级Agent函数调用系统。所有代码示例均可直接执行,表格严格遵循Markdown规范,适合AI工程师和架构师深入学习。
更多推荐



所有评论(0)