Function Call原理:LLM如何识别函数调用意图与JSON Schema生成
Function Call原理:LLM如何识别函数调用意图与JSON Schema生成
适读人群:Java后端工程师,正在或计划将LLM集成进业务系统的开发者 | 阅读时长:约18分钟
开篇故事
两年前,我第一次接触Function Call是通过OpenAI的API文档。当时看到那个tools参数,我以为是LLM真的能调用我写的Java方法,兴奋了一阵子。后来实际跑起来才发现:LLM根本不会直接调用我的方法,它只是在生成文本时"决定要调用哪个函数,以及传什么参数",真正的执行还是我的代码来做。
这个认知纠正之后,我才真正理解了Function Call的本质:它是一种结构化的输出机制,让LLM输出的不是自然语言,而是符合JSON Schema规范的函数调用描述。
理解了这个,才能设计出可靠的工具调用系统,而不是把LLM当成一个黑盒魔法。
一、Function Call的本质
Function Call不是真正的函数调用,它是:
- 你告诉LLM:我有这些工具,每个工具能做什么,接受什么参数(JSON Schema描述)
- LLM理解用户意图后:决定是否需要调用工具,需要的话调用哪个,参数是什么
- LLM返回的是:一段结构化的JSON,描述"我想调用
get_weather函数,传入{location:'北京'}" - 你的代码接收后:解析这个JSON,真正调用
get_weather("北京"),把结果返回给LLM - LLM基于结果:继续生成最终的自然语言回复
二、JSON Schema:工具描述的语言
2.1 OpenAI的tools格式
{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气信息。当用户询问天气相关问题时调用此函数。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如:北京、上海、广州"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认celsius(摄氏度)"
}
},
"required": ["location"]
}
}
}
]
}2.2 LLM的tool_call响应
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"北京\", \"unit\": \"celsius\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}注意:arguments是一个JSON字符串(不是JSON对象),需要再次解析。
2.3 LLM是如何"学会"Function Call的
LLM在预训练和微调阶段见过大量的"函数调用格式"示例,学会了:
- 理解JSON Schema中每个字段的含义
- 匹配用户意图与工具的description
- 从用户输入中提取参数值,填入JSON
- 在
finish_reason为tool_calls时生成结构化输出而非自然语言
关键:description的质量直接决定LLM能否正确识别调用意图。写好description比写好代码更重要。
三、Java中构建Function Call的完整代码
3.1 手工构建JSON Schema发送请求
// 使用OpenAI Java SDK + 手工构建Schema
public class WeatherFunctionCallDemo {
private final OpenAIClient openAIClient;
private final WeatherService weatherService;
// 构建工具定义
private List<ChatCompletionTool> buildTools() {
// 参数Schema
ObjectNode parameters = objectMapper.createObjectNode();
parameters.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
// location参数
ObjectNode locationProp = objectMapper.createObjectNode();
locationProp.put("type", "string");
locationProp.put("description", "城市名称,支持中文城市名,如:北京、上海、深圳");
properties.set("location", locationProp);
// unit参数(可选)
ObjectNode unitProp = objectMapper.createObjectNode();
unitProp.put("type", "string");
ArrayNode unitEnum = unitProp.putArray("enum");
unitEnum.add("celsius");
unitEnum.add("fahrenheit");
unitProp.put("description", "温度单位,celsius=摄氏度,fahrenheit=华氏度,默认celsius");
properties.set("unit", unitProp);
parameters.set("properties", properties);
ArrayNode required = parameters.putArray("required");
required.add("location");
// 构建FunctionTool
FunctionDefinition function = FunctionDefinition.builder()
.name("get_weather")
.description("获取指定城市的实时天气信息。" +
"当用户询问某城市的天气、温度、天气预报时使用此工具。" +
"不要用于查询历史天气。")
.parameters(parameters)
.build();
return List.of(ChatCompletionTool.builder()
.type(ChatCompletionToolType.FUNCTION)
.function(function)
.build());
}
public String chat(String userMessage) {
List<ChatCompletionMessageParam> messages = new ArrayList<>();
messages.add(ChatCompletionUserMessageParam.builder()
.role(ChatCompletionUserMessageParam.Role.USER)
.content(userMessage)
.build());
// 第一次调用:让LLM决定是否需要调用工具
ChatCompletion response = openAIClient.chat().completions().create(
ChatCompletionCreateParams.builder()
.model("gpt-4o-mini")
.messages(messages)
.tools(buildTools())
.toolChoice(ChatCompletionToolChoiceOptionParam.AUTO)
.build()
);
ChatCompletionMessage assistantMessage = response.choices().get(0).message();
// 检查是否有工具调用
if (response.choices().get(0).finishReason() == FinishReason.TOOL_CALLS) {
// 处理工具调用
List<ChatCompletionMessageToolCall> toolCalls =
assistantMessage.toolCalls().orElse(Collections.emptyList());
// 把assistant的消息加入历史
messages.add(assistantMessage);
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
String functionName = toolCall.function().name();
String arguments = toolCall.function().arguments();
// 执行工具
String result = executeFunction(functionName, arguments);
// 把工具结果加入消息历史
messages.add(ChatCompletionToolMessageParam.builder()
.role(ChatCompletionToolMessageParam.Role.TOOL)
.toolCallId(toolCall.id())
.content(result)
.build());
}
// 第二次调用:让LLM基于工具结果生成最终回答
ChatCompletion finalResponse = openAIClient.chat().completions().create(
ChatCompletionCreateParams.builder()
.model("gpt-4o-mini")
.messages(messages)
.build()
);
return finalResponse.choices().get(0).message().content().orElse("无法获取回答");
}
// 不需要工具调用,直接返回
return assistantMessage.content().orElse("无法获取回答");
}
private String executeFunction(String functionName, String argumentsJson) {
try {
JsonNode args = objectMapper.readTree(argumentsJson);
if ("get_weather".equals(functionName)) {
String location = args.get("location").asText();
String unit = args.has("unit") ? args.get("unit").asText() : "celsius";
WeatherInfo weather = weatherService.getWeather(location, unit);
return objectMapper.writeValueAsString(weather);
}
return "{\"error\": \"Unknown function: " + functionName + "\"}";
} catch (Exception e) {
return "{\"error\": \"" + e.getMessage() + "\"}";
}
}
}3.2 annotation驱动的Schema生成(Spring AI风格)
// 用Java注解描述函数,自动生成JSON Schema
public class AnnotationDrivenFunctionCall {
// 函数参数类(用Jackson注解描述Schema)
@JsonClassDescription("获取天气的请求参数")
public record WeatherRequest(
@JsonProperty(required = true, value = "location")
@JsonPropertyDescription("城市名称,支持中文,如:北京、上海")
String location,
@JsonProperty(value = "unit")
@JsonPropertyDescription("温度单位:celsius(摄氏度)或 fahrenheit(华氏度),默认celsius")
@JsonSetter(nulls = Nulls.SKIP)
String unit
) {
public WeatherRequest {
if (unit == null) unit = "celsius";
}
}
// 函数返回类型
public record WeatherResponse(
String location,
double temperature,
String unit,
String description,
int humidity
) {}
// 从Record类生成JSON Schema
public static JsonNode generateSchema(Class<?> paramClass) {
ObjectMapper mapper = new ObjectMapper();
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
try {
JsonSchema schema = schemaGen.generateSchema(paramClass);
return mapper.valueToTree(schema);
} catch (Exception e) {
throw new RuntimeException("Failed to generate schema for " + paramClass, e);
}
}
}四、踩坑实录
坑1:LLM幻觉调用不存在的函数
现象:明明只定义了3个工具,LLM生成了一个get_stock_price的tool_call,这个函数根本不存在。
根因:LLM有时会"幻觉"出不在工具列表里的函数名。虽然比较少见,但要做防御:
private String executeFunction(String functionName, String argumentsJson) {
// 白名单校验
Set<String> allowedFunctions = Set.of("get_weather", "search_products", "create_order");
if (!allowedFunctions.contains(functionName)) {
log.warn("LLM attempted to call unknown function: {}", functionName);
return "{\"error\": \"Function not found: " + functionName + "\"}";
}
// ... 正常执行
}坑2:description写得太模糊导致错误调用
工具描述模糊会导致LLM在错误的时机调用工具。
# 差的description:
"获取天气"
# 好的description:
"获取指定城市的当前实时天气信息,包括温度、天气状况、湿度等。
适用场景:用户询问某城市当前天气、今天天气怎么样等问题。
不适用:历史天气查询、多日天气预报、气候统计。"坑3:arguments是字符串不是对象
很多新手犯这个错:把tool_calls.function.arguments当作JSON对象直接用,但它实际上是一个JSON字符串,需要二次解析:
// 错误:
JsonNode args = (JsonNode) toolCall.function().arguments(); // ClassCastException
// 正确:
String argumentsStr = toolCall.function().arguments();
JsonNode args = objectMapper.readTree(argumentsStr); // 二次解析!坑4:多个tool_call同时返回时只处理了第一个
LLM可能在一次响应中返回多个tool_calls(并行工具调用),要遍历处理所有:
List<ChatCompletionMessageToolCall> toolCalls = assistantMessage.toolCalls()
.orElse(Collections.emptyList());
// 并行执行所有工具调用
List<CompletableFuture<ToolResult>> futures = toolCalls.stream()
.map(tc -> CompletableFuture.supplyAsync(() ->
new ToolResult(tc.id(), executeFunction(tc.function().name(),
tc.function().arguments()))))
.collect(toList());
// 等待所有完成
List<ToolResult> results = futures.stream()
.map(CompletableFuture::join)
.collect(toList());五、总结与延伸
Function Call的本质是:让LLM输出结构化的调用意图,由开发者代码执行实际操作。
三个关键要素:
- 工具描述要精准:description决定LLM能否正确理解工具的使用场景
- 参数Schema要严格:用JSON Schema约束参数类型和枚举值,减少幻觉
- 执行层要防御:白名单校验、异常处理、结果格式化
下一篇我们看Spring AI的FunctionCallback源码,了解Spring AI是如何封装这套机制的。
