Function Call——函数调用
以我们的RAG系统为例,整个RAG流程大概是这样的:用户提出问题→(问题拆分)→检索分块→生成答案→(比标注来源)
在基础场景中已经很完善了,能够给出符盖对应知识点的回答,但是用户的问题有时候并不只是查询文档并生成回答这么简单。
EX:用户“帮我查询12345中订单”,“帮我点杯奶茶”(千问点单、小美等)。RAG或许能够通过预训练知识/知识库文档查询到对应的知识/相关指南,但是无法真正查询到用户相关的真实订单信息、无法真实执行下单操作。——需要查询/点单相关API。
通过Function Call 我们能够拓宽RAG系统边界、让Agent执行能力落地。
查询知识库到其他功能:传统应用能力边界
即使是RAG这种应用,所有的数据来源都是写死的预训练知识/知识库分块的再组装。
企业场景下/特定实际应用场景下,用户需求实际上不止查询文档:
查询实时业务数据
查询业务/服务状态
执行操作:申报/发送信息等
传统方案&局限性
最LOW的方案:Prompt当中写死兜底回复:
最烂的方案,用户体验是最差的,同时也是最死板的。
通过规则进行用户意图匹配,调用接口:
例如用户询问的问题出现“年假”关键字我们就查询用户的年假
if (userQuestion.contains("年假")) { int days = hrSystem.getAnnualLeave(userId); return "您还剩 " + days + " 天年假"; } if (userQuestion.contains("订单") && userQuestion.contains("物流")) { String status = logisticsSystem.getOrderStatus(orderId); return "订单物流状态:" + status; }方案问题:规则是写不完的,维护成本很高。这和我们为什么需要LLM的原因是一样的——每种不同的表达背后的意图是一样的,难以靠规则进行匹配。
每次新增一个工具,我们就要修改代码,增加规则(当然你可以用责任链模式进行解耦,但是规则匹配问题依旧存在)
需要更加灵活的方案:模型判断使用什么工具,什么时候需要使用,传入什么参数。
Function Call
本质:模型输出调用意图
模型并不具备直接调用程序当中方法函数的功能,而是输出我们规定好的格式的文本(例如JSON),告诉程序“当前我认为需要调用什么函数,对应的参数有什么”。真正执行函数/对应服务的还是我们的代码。
流程
定义好工具函数,写好对应的参数描述(参数名称、类型、描述)、函数含义、描述
将所有工具列表、用户问题拼接好之后发送给模型
模型判断需要调用工具,输出JSON(统一称为tool_calls),包含函数名、参数
解析JSON,组装为函数参数对象,调用对应的函数,得到结果
执行结果、记忆等返回给大模型
大模型生成最终答案
EX:
第一轮:发送工具列表、用户问题
第一轮响应:模型输出调用意图:
模型分析用户问题,发现需要调用getUserAnnualLeave函数,并给出对应参数
{ "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "getUserAnnualLeave", "arguments": "{\"userId\": \"12345\"}" } } ] }代码执行函数:
解析tool_calls,执行函数,拿到结果:
{ "remainingDays": 5, "totalDays": 10, "usedDays": 5 }第二轮:返回结果、记忆等给模型
请求包含:
第一轮的用户问题
第一轮的模型响应(带有tool_calls)
函数返回执行结果
您还剩 5 天年假(总共 10 天,已使用 5 天)。通过Function Call:
将工具执行需求判断交给大模型,函数具体执行留给项目代码
LLM工具调用决策:
OpenAI Function Call协议详解
工具格式
将工具列表以JSON数组格式发给模型
每个工具定义格式:
{ "type": "function", "function": { "name": "getUserAnnualLeave", "description": "查询用户的年假余额,包括总天数、已使用天数、剩余天数", "parameters": { "type": "object", "properties": { "userId": { "type": "string", "description": "用户 ID" } }, "required": ["userId"] } } }字段说明:
类型type:固定为function
function.name:函数名称,后续function calls当中返回这个名字
function.description:函数功能性描述,模型判断是否调用工具的依据
function.parameters:函数当中参数定义
function.parameters的JSON Schema格式
parameters使用JSON Schema格式定义参数,常用字段:
type:参数类型,常用“object”,标识参数是一个对象
properties:对象属性,每个属性有对应的type(类型)和description(描述)
required:必填参数列表
EX:
{ "type": "object", "properties": { "userId": { "type": "string", "description": "用户 ID" }, "year": { "type": "integer", "description": "查询的年份,默认为当前年份" } }, "required": ["userId"] }含义:这个函数当中有两个参数userId和year,userId是必填的(类型为字符串),year是可选的(类型为整数)
请求格式:工具列表发给模型
完整请求JSON实例:
{ "model": "Qwen/Qwen2.5-7B-Instruct", "messages": [ { "role": "user", "content": "我还剩几天年假" } ], "tools": [ { "type": "function", "function": { "name": "getUserAnnualLeave", "description": "查询用户的年假余额", "parameters": { "type": "object", "properties": { "userId": { "type": "string", "description": "用户 ID" } }, "required": ["userId"] } } } ], "tool_choice": "auto" }字段说明:
model:模型名称
messages:对话历史,格式和普通Chat API一样
tools:工具列表
tool_choice:控制模型是否调用工具,可选值:
auto:模型自己判断是否需要调用工具(默认值,大部分场景够用,让模型自己判断)
none:不调用工具,只生成文本回答
required:必须调用,不能只生成文本回答
{"type": "function", "function": {"name": "getUserAnnualLeave"}}:指定调用某个工具
响应格式:模型输出tool_calls
模型判断需要使用工具,响应格式如下:
{ "choices": [ { "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "getUserAnnualLeave", "arguments": "{\"userId\": \"12345\"}" } } ] }, "finish_reason": "tool_calls" } ] }关键字段:
message.tool_calls:模型调用数组,可能包含多个工具调用(可能一次调用多个工具)
tool_calls[0].id:调用ID,第二轮请求时需要带上对应id,用于确定这次返回结果属于哪次调用,填补之前特定请求的空白(虽然LLM的api本身并不存储我们的上下文记忆,但是这是协议的强制要求,并且实际上我们通过id用于区分多个历史记录)
tool_calls[0].function.name:需要调用的函数名
tool_calls[0].functionl.arguments:函数参数名,JSON字符串(请注意),需要自己进行解析
finish_reason:值为tool_calls,因为模型输出的是工具调用而不是正常结束(对应reason为stop)
注意:message.content为null,因为模型没有生成文本回答,而是输出工具调用。
再次请求:函数执行结果返回给模型
执行完函数之后需要将整个函数执行结果以及完整对话历史返回给api用于生成最终答案:
{ "model": "Qwen/Qwen2.5-7B-Instruct", "messages": [//第一次调用内容 { "role": "user", "content": "我还剩几天年假" }, { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123",//可以看到这里和下面的tool_call_id是对应的 "type": "function", "function": { "name": "getUserAnnualLeave", "arguments": "{\"userId\": \"12345\"}" } } ] }, { //函数对应返回结果 "role": "tool", "tool_call_id": "call_abc123", "content": "{\"remainingDays\": 5, \"totalDays\": 10, \"usedDays\": 5}" } ] }关键:
第一条消息:用户原始问题
第二条消息:第一轮模型响应(带tool_calls),role是assistant
第三条消息:函数执行结果、role是tool,tool_call_id需要和第二条消息当中的对应,content是函数对应的返回值
模型基于上面信息返回答案:
{ "choices": [ { "message": { "role": "assistant", "content": "您还剩 5 天年假(总共 10 天,已使用 5 天)。" }, "finish_reason": "stop" } ] }注:这次的final_reason是stop,标识模型生成最终答案。
存在问题
Function Call解决了让模型调用工具的问题,但是带来了新的问题:
工具定义维护成本高
跨语言、跨系统集成复杂
权限和安全控制需要自己实现
可观测性不足(开发者无法清晰看到、理解或者追踪系统内部发生了什么、中间的工具调用过程,需要完备的日志记录等手段)