Function Calling 能使 AI 识别用户说话中的特定意图,并调用您已实现的外部函数,以完成特定任务。例如:查询实时信息(如天气)、操作业务系统、控制 App 硬件功能(播放音乐、关灯、调整音量)等。
使用限制
模型要求:火山方舟模型或第三方大模型,且需具备 Function Calling 能力。
注意
- 支持的方舟模型,请参见模型列表。不建议使用 doubao-seed-1.6-thinking,该模型会强制开启思考模式且不可关闭,可能造成较高时延。
- Doubao 1.5 及之前系列的模型,不建议同时使用视觉理解和 Function Calling。
- 如需保留 Function Calling 历史,当前仅火山方舟模型、已开启视觉理解的 OpenAI-like 模型,以及支持 OpenAI-like tool_calls / tool_call_id 消息格式的第三方模型支持。不支持的模型即使配置 LLMConfig.EnableToolCallHistory,也不会保留工具调用历史。
工作流程
Function Calling 的工作流程涉及以下几个核心角色:
- 系统服务:作为技术底座,负责接收您的请求、调用 LLM,并将 LLM 的决策(如函数调用指令)通过回调传递给您的应用程序。
- 您的应用程序:负责实现和执行具体的本地函数(如查询天气)。
整个流程的核心是 “LLM 决策,您的代码执行”。具体实现过程如下:
- 在客户端应用程序中,定义好具体的本地函数。调用 StartVoiceChat 接口时,向模型声明您已定义的函数。
- 用户向 AI 提出需要外部能力才能完成的请求(例如:“帮我查询上海的天气”)。
- 大模型识别用户意图,决策需要调用哪个函数,并生成相应的调用指令。
- 先发送 “函数调用通知”:提前告知您有函数即将被调用。收到 “调用通知” 后,您可以立即请求播放一段安抚语(如 “正在查询,请稍候”),以优化用户等待体验。
- 再发送 “函数调用指令”:告知需要执行的函数与入参(如 location: "上海")。
- 您的应用程序根据 “调用指令” 执行本地函数,获取执行结果并回传给系统。
- 系统在收到上报的函数执行结果后如何向用户播报,会根据您选择的回传方式进行处理:
- 将函数执行结果直接通过 TTS 转化为语音播放。适用于结果无需润色、或回复内容较长的场景。
- 将函数执行结果返回给 LLM,由模型进行总结、润色后再通过 TTS 播放。适用于需要对回复进行二次处理的场景。
准备函数
您需要在业务应用程序中,实现需要 AI 调用的具体函数的逻辑。Function Calling 的作用是让模型 “决策” 调用哪个函数并传入什么参数,而函数的执行则完全由您的程序负责。
例如,如果您想实现天气查询,您需要自己实现一个能查询并返回天气信息的函数。
确定实现方式
客户端和服务端可以同时配置,两者回调相互独立。您可以根据实际需求,选择在任一端处理回调逻辑,并忽略另一端即可。
若为第三方大模型
如果您使用第三方 LLM 接入,无论流式或非流式,最终响应必须包含 tool_calls 字段,且 finish_reason 必须设为 "tool_calls"。
流式响应示例:
"id": "call_unique_id_123",
"name": "make_phone_call",
"arguments": "{\"phone_number\": \"13800138000\"}"
"finish_reason": "tool_calls"
通过服务端实现
步骤 1:声明函数并配置回调
在调用 StartVoiceChat 接口时,需提供以下配置:
- LLMConfig.Tools:声明您需要模型调用的所有函数。请确保此处声明的每一个函数,您都已在自己的业务代码中实现了对应的逻辑。其中,parameters 需严格遵守JSON Schema规范,详细可参见工具函数参数构造规范。
- LLMConfig.EnableToolCallHistory:可选。设为 true 后,若某轮触发 Function Calling,系统会将该轮的工具调用指令和工具返回结果随该轮历史一起保留,便于模型基于此前工具调用结果继续对话;默认值为 false。
- ServerMessageUrl:您的后端服务地址,用于接收函数调用指令。必须是公网可访问的 URL,且能正确处理无 Content-Type 的 HTTP POST 请求。
说明
对于嵌入式/IoT 等场景,如需在 FC 回调中识别对应的会话或设备,可将业务标识(如 TaskId、DeviceId)作为 query 参数附加在 ServerMessageUrl 中,例如:"ServerMessageUrl": "https://your-server.com/fc_callback?taskId=yourTaskId&deviceId=xxx",服务端直接从请求 URL 的 query 参数中读取即可。
- ServerMessageSignature: 自定义认证密钥。此字符串会在回调请求中原样返回,供您在服务端进行来源合法性校验。
// 声明您需要调用的所有函数(确保所有函数均已在业务代码中实现)
"name": "get_current_weather",
"description": "获取一个或多个指定城市的实时天气信息",
"description": "需要查询天气的城市名,例如:'上海市'"
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,枚举值必须为数组格式"
"required": ["location"]
"EnableParallelToolCalls": true, // 可选,设为 true 时支持模型在一轮对话中同时触发多个工具调用(并行工具调用);默认为 false,仅执行第一个被触发的工具
"EnableToolCallHistory": true // 可选,设为 true 时工具调用指令和工具返回结果会随触发工具调用的轮次一起写入后续请求;默认为 false
"FunctionCallingConfig": {
"ServerMessageUrl": "https://your-backend.com/api/fc_callback",
"ServerMessageSignature": "your_custom_signature"
步骤 2:处理回调消息
当 AI 识别到工具调用意图时,您配置服务器 URL 地址会先后收到 2 条跟 FC 相关的消息,您需要通过判断消息体的 Type 字段来区分并获取这两种消息。
- 函数执行需要一定时间。在收到函数调用通知后,AI 回复前,可插播安抚语以提升用户体验。
- 当 AI 识别到工具调用意图时,你会先收到函数调用通知消息(Type 为 information)。通知消息示例如下:
"Message": "{\"event_type\":\"function_calling\",\"function\":\"get_current_weather\",\"tool_call_id\":\"call_yx8****\",\"response_id\":\"021765**********e711d\"}",
"Signature": "test_fc_***", //即你在步骤1中配置的 ServerMessageSignature
"Type": "information", //固定值,表示这是一个函数调用通知
"RoomID": "ChatRoom01", //通话房间 ID
"TaskID": "73bc1a7e-73ef-470f-91****",
"TaskType": "voiceChat", //固定值,表示 AI 对话任务
"AppId": "661e****543cf" // 应用的 AppId
- Message 是一个转义 JSON 字符串,使用前需要解析。具体内容如下:
- 调用 UpdateVoiceChat 接口播放安抚语,以提升用户体验。
如果安抚语还未播放完,函数结果就已返回,系统会等待安抚语结束后再播报结果。
"AppId": "661e****543cf",
"Command": "ExternalTextToSpeech", // 固定为 ExternalTextToSpeech
"Message": "正在为您查询,请耐心等待。", // 内容结尾必须有标点
"InterruptMode": 3 // 固定为 3,安抚语播放优先级较低,会被后续的函数结果打断。
- 紧接着您会收到函数调用指令消息(Type 为 tool_calls), 您需要先解析外层 Message 字段,再解析内层 arguments 字段,来获取最终的函数名称和执行参数。
"Message": "[{\"id\":\"call_yx8****\",\"type\":\"function\",\"function\":{\"name\":\"get_current_weather\",\"arguments\":\"{\\\"location\\\":\\\"上海\\\",\\\"unit\\\":\\\"摄氏度\\\"}\"}}]",
"Signature": "cn_test_***", //即你在步骤1中配置的 ServerMessageSignature
"Type": "tool_calls", //固定值,表示此次消息为函数指令消息。
"RoomID": "ChatRoom01", //通话房间 ID
"TaskID": "73bc1a7e-73ef-470f-91****", // 仅用于标识单次 FC 任务,便于内部问题排查,不可用于 UpdateVoiceChat
"TaskType": "voiceChat", //固定值,表示 AI 对话任务
"AppId": "661e****543cf" // 应用的 AppId
- Message 字段是一个转义 JSON 字符串,包含内容如下:
步骤 3:执行外部函数
获取调用指令信息后,您需要在自己的应用程序中实现:
- 路由到函数:根据 function.name 的值(例如 "get_current_weather"),调用您在自己代码中已实现的相应函数。
- 传递参数:将上一步解析出的 function.arguments 中的参数作为入参传递给您的函数。
- 获取返回值:执行您的函数,并获取其返回结果,这个结果将在下一步中回传。
步骤 4:回传函数结果
调用 UpdateVoiceChat 接口将函数执行结果回传给系统,并由 AI 播放。
方式一:传给 LLM 处理后再播放
方式二:直接播放(不经 LLM 处理)
将函数返回的原始数据传回给 LLM 处理后再播放。适用于需要总结、润色等二次处理场景。
Message 中的 ToolCallID 的值必须与您在步骤 3:执行外部函数收到的 tool_calls 指令中的 id 字段值完全一致。 "AppId": "YOUR_AppId", // 与 StartVoiceChat 的一致
"RoomId": "YOUR_RoomId", // 与 StartVoiceChat 的一致
"TaskId": "YOUR_TaskId", // 与 StartVoiceChat 的一致
"Command": "function", // 必须为 function
"Message": "{\\\\"ToolCallID\\\\":\\\\"call_yx8****\\\\",\\\\"Content\\\\":\\\\"上海天气是晴天,25摄氏度\\\\"}" // 将函数结果作为 JSON 转义字符串传入
LLM 可能会生成回复:“上海今天天气不错,是个大晴天,温度是 25 摄氏度。”
将函数原始结果直接通过 TTS 播放。适用于返回结果无需处理或回复的句子较长需降低延迟等场景。
注意
为降低延迟,可将长回复切分为多个短句,分次调用UpdateVoiceChat 接口。
- 切句建议:首句按逗号(,)、分号(;)切分;后续分句按句号(。)、问号(?)等完整句子标点切分。避免切分数字中的小数点。
- 切句示例:今天的天气晴朗,、温度10-29摄氏度,北风三级。、未来七天,天气会保持晴朗。
"AppId": "YOUR_AppId", // 与 StartVoiceChat 的一致
"RoomId": "YOUR_RoomId", // 与 StartVoiceChat 的一致
"TaskId": "YOUR_TaskId", // 与 StartVoiceChat 的一致
"Command": "ExternalTextToSpeech", // 必须为 ExternalTextToSpeech
"Message": "今天上海的天气晴朗。", // 传入工具调用的结果,普通字符串
"InterruptMode": 2 //固定值为 2
通过客户端实现
通过客户端(如App、Web页面)处理 Function Calling 的通知和指令,并在本地执行函数后,将结果发送回系统。
步骤 1:声明函数
调用 StartVoiceChat 接口,配置 LLMConfig.Tools 字段来声明您要调用的函数(确保所有函数均已在业务代码中实现)。其中,parameters 需严格遵守JSON Schema规范,详细可参见工具函数参数构造规范。 如需让模型在后续轮次继续参考此前的工具调用结果,可同时配置 LLMConfig.EnableToolCallHistory: true。该参数默认值为 false。
// 声明您需要调用的所有函数(确保所有函数均已在业务代码中实现)
"name": "get_current_weather",
"description": "获取一个或多个指定城市的实时天气信息",
"description": "需要查询天气的城市名,例如:'上海市'"
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,枚举值必须为数组格式"
"required": ["location"]
"EnableParallelToolCalls": true, // 可选,设为 true 时支持模型在一轮对话中同时触发多个工具调用(并行工具调用);默认为 false,仅执行第一个被触发的工具
"EnableToolCallHistory": true // 可选,设为 true 时工具调用指令和工具返回结果会随触发工具调用的轮次一起写入后续请求;默认为 false
步骤 2:监听回调并处理
- 当 AI 识别到工具调用意图时,该回调会被先后触发两次,您会收到两条与 Function Calling 相关的二进制消息。您需要解析消息头部的 magic number 来识别并处理这两条消息。
- 函数执行需要一定时间。在收到函数调用通知后,AI 回复前,可插播安抚语以提升用户体验。
- 当 AI 识别到工具调用意图时,你先会收到函数调用通知消息 message(magic number 为 info),该消息为二进制格式,收到消息后需解析该字段。message 格式和解析示例如下:
-
std::string tool_call_id;
std::string response_id;
// 拆包函数,专门用于 "info" 消息:只做协议校验 + 提取 JSON 字符串
bool UnpackInfoMessage(const uint8_t *message, int size, std::string& json_payload) {
const int kHeaderSize = 8;
if (size < kHeaderSize) {
// 校验 magic number "info" (0x696E666F)
uint32_t magic = ntohl(*reinterpret_cast<const uint32_t*>(message));
if (magic != 0x696E666F) {
uint32_t length = ntohl(*reinterpret_cast<const uint32_t*>(message + 4));
if (size - kHeaderSize != static_cast<int>(length)) {
json_payload.assign(reinterpret_cast<const char*>(message) + kHeaderSize, length);
// 对外的解析接口:解析出结构体 FCInfoMessage
bool ParseInfoMessage(const uint8_t* message, int size, FCInfoMessage& out_info) {
if (!UnpackInfoMessage(message, size, json_str)) {
// 解析 JSON,容错一点,解析失败直接返回 false
nlohmann::json json_data = nlohmann::json::parse(json_str, /*callback=*/nullptr, /*allow_exceptions=*/false);
if (json_data.is_discarded()) {
out_info.event_type = json_data.value("event_type", "");
out_info.function = json_data.value("function", "");
out_info.tool_call_id = json_data.value("tool_call_id", "");
out_info.response_id = json_data.value("response_id", "");
// 在 onRoomBinaryMessageReceived 回调中
void onRoomBinaryMessageReceived(const char* uid, int size, const uint8_t* message) {
if (!ParseInfoMessage(message, size, info_msg)) {
// 不是合法的 info 消息,直接忽略,可以继续检查是否为 tool 等其他消息
// 解析成功!现在可以安全地使用 info_msg 里的字段了。
std::cout << "Function Call Notification for: " << info_msg.function << std::endl;
std::cout << "ToolCallID: " << info_msg.tool_call_id << std::endl;
// 例如收到函数调用通知后,调用 `sendUserBinaryMessage` 函数,让 AI 播放安抚语。
public String event_type;
public String tool_call_id;
public String response_id;
// 在 onRoomBinaryMessageReceived 回调中
public void onRoomBinaryMessageReceived(String uid, ByteBuffer message) {
message.order(ByteOrder.BIG_ENDIAN);
// 至少要有 4 字节 magic + 4 字节 length
if (message.remaining() < 8) {
byte[] magicBytes = new byte[4];
message.get(magicBytes);
String magic = new String(magicBytes, StandardCharsets.UTF_8);
if ("info".equals(magic)) {
int length = message.getInt();
// 简单的合法性检查:长度不能为负,剩余字节要够
if (length < 0 || message.remaining() < length) {
byte[] payload = new byte[length];
String infoStr = new String(payload, StandardCharsets.UTF_8);
// 使用 Gson 或 Jackson 等库解析 JSON
FCInfoMessage infoMsg = gson.fromJson(infoStr, FCInfoMessage.class);
// 现在可以根据 infoMsg.function 准备播放安抚语
System.out.println("Function Call Notification for: " + infoMsg.function);
// 立即调用 sendUserBinaryMessage 播放安抚语...
} else if ("tool".equals(magic)) {
// ... 此处处理 "tool" 消息(可选)
// 重置 buffer 位置,以便其他逻辑可以处理
interface FCInfoMessage {
// 在 onRoomBinaryMessageReceived 回调中
engine.on('onRoomBinaryMessageReceived', (event: { userId: string, message: ArrayBuffer }) => {
const { userId, message } = event;
if (message.byteLength < 8) return;
const dataView = new DataView(message);
const magic = new TextDecoder().decode(message.slice(0, 4));
const length = dataView.getUint32(4, false); // false for big-endian
const payload = new TextDecoder().decode(message.slice(8, 8 + length));
const infoMsg: FCInfoMessage = JSON.parse(payload);
// 现在可以根据 infoMsg.function 准备播放安抚语
console.log(`Function Call Notification for: \\\${infoMsg.function}`);
// 立即调用 engine.sendUserBinaryMessage 播放安抚语...
} else if (magic === 'tool') {
如果安抚语还未播放完,函数结果就已返回,系统会等待安抚语结束后再播报结果。
- userId:AI 的标识 ID(对应 AgentConfig.UserId)。
-
// 发送文本转语音(TTS)指令,让 AI 播报指定的安抚语内容。
void sendTTSMessage(const std::string &uid, const std::string& content) {
nlohmann::json json_data;
json_data["Command"] = "ExternalTextToSpeech";
json_data["Message"] = content;
json_data["InterruptMode"] = 3;
sendUserBinaryMessage(uid, json_data.dump());
void buildBinaryMessage(const std::string& magic_number, const std::string& message, size_t& binary_message_length, std::shared_ptr<uint8_t[]>& binary_message) {
auto magic_number_length = magic_number.size();
auto message_length = message.size();
binary_message_length = magic_number_length + 4 + message_length;
binary_message = std::shared_ptr<uint8_t[]>(new uint8_t[binary_message_length]);
std::memcpy(binary_message.get(), magic_number.data(), magic_number_length);
binary_message[magic_number_length] = static_cast<uint8_t>((message_length >> 24) & 0xFF);
binary_message[magic_number_length+1] = static_cast<uint8_t>((message_length >> 16) & 0xFF);
binary_message[magic_number_length+2] = static_cast<uint8_t>((message_length >> 8) & 0xFF);
binary_message[magic_number_length+3] = static_cast<uint8_t>(message_length & 0xFF);
std::memcpy(binary_message.get()+magic_number_length+4, message.data(), message_length);
int sendUserBinaryMessage(const std::string &uid, const std::string& message) {
if (rtcRoom_ != nullptr)
std::shared_ptr<uint8_t[]> binary_message = nullptr;
buildBinaryMessage("ctrl", message, length, binary_message);
return rtcRoom_->sendUserBinaryMessage(uid.c_str(), static_cast<int>(length), binary_message.get());
// 发送文本转语音(TTS)指令,让智能体播报指定的安抚语内容。
public void sendTTSMessage(String userId, String content) {
JSONObject json = new JSONObject();
json.put("Command", "ExternalTextToSpeech");
json.put("Message", content);
json.put("InterruptMode", 3);
} catch (JSONException e) {
throw new RuntimeException(e);
String jsonString = json.toString();
byte[] buildBinary = buildBinaryMessage("ctrl", jsonString);
sendUserBinaryMessage(userId, buildBinary);
private byte[] buildBinaryMessage(String magic_number, String content) {
byte[] prefixBytes = magic_number.getBytes(StandardCharsets.UTF_8);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
int contentLength = contentBytes.length;
ByteBuffer buffer = ByteBuffer.allocate(prefixBytes.length + 4 + contentLength);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put(prefixBytes);
buffer.putInt(contentLength);
buffer.put(contentBytes);
public void sendUserBinaryMessage(String userId, byte[] buffer) {
rtcRoom_.sendUserBinaryMessage(userId, buffer, MessageConfig.RELIABLE_ORDERED);
import VERTC from '@volcengine/rtc';
const BotName = 'RobotMan_'; // 自定义智能体名称
const CommandKey = 'ctrl';
const engine = VERTC.createEngine('Your AppID'); // 你的 RTC 应用 APPId
* @brief 发送文本转语音(TTS)指令,让智能体播报指定的安抚语内容。
EXTERNAL_TEXT_TO_SPEECH = 'ExternalTextToSpeech',
enum INTERRUPT_PRIORITY {
function stringToTLV(inputString: string, type = '') {
const typeBuffer = new Uint8Array(4);
for (let i = 0; i < type.length; i++) {
typeBuffer[i] = type.charCodeAt(i);
const lengthBuffer = new Uint32Array(1);
const valueBuffer = new TextEncoder().encode(inputString);
lengthBuffer[0] = valueBuffer.length;
const tlvBuffer = new Uint8Array(typeBuffer.length + 4 + valueBuffer.length);
tlvBuffer.set(typeBuffer, 0);
tlvBuffer[4] = (lengthBuffer[0] >> 24) & 0xff;
tlvBuffer[5] = (lengthBuffer[0] >> 16) & 0xff;
tlvBuffer[6] = (lengthBuffer[0] >> 8) & 0xff;
tlvBuffer[7] = lengthBuffer[0] & 0xff;
tlvBuffer.set(valueBuffer, 8);
return tlvBuffer.buffer;
* @brief 发送文本转语音(TTS)指令,让智能体播报指定的安抚语内容。
engine.sendUserBinaryMessage(
Command: COMMAND.EXTERNAL_TEXT_TO_SPEECH,
InterruptMode: INTERRUPT_PRIORITY.LOW,
- 收到调用通知后,您会收到函数调用指令消息(magic number 为 tool)。您需要解析 message,获取执行函数所需的具体参数。message 格式与解析示例如下:
-
std::string arguments; // 注意:这是一个字符串化的JSON
bool ParseToolMessage(const uint8_t* message, int size, std::vector<ToolCall>& out_calls) {
const int kHeaderSize = 8;
if (size < kHeaderSize) return false;
uint32_t magic = ntohl(*reinterpret_cast<const uint32_t*>(message));
if (magic != 0x746F6F6C) return false; // "tool"
uint32_t length = ntohl(*reinterpret_cast<const uint32_t*>(message + 4));
if (size - kHeaderSize != static_cast<int>(length)) return false;
json_str.assign(reinterpret_cast<const char*>(message) + kHeaderSize, length);
nlohmann::json json_data = nlohmann::json::parse(json_str, nullptr, false);
if (json_data.is_discarded() || !json_data.contains("tool_calls") || !json_data["tool_calls"].is_array()) return false;
for (const auto& item : json_data["tool_calls"]) {
tool_call.id = item.value("id", "");
tool_call.type = item.value("type", "");
if (item.contains("function") && item["function"].is_object()) {
const auto& func_json = item["function"];
tool_call.function.name = func_json.value("name", "");
tool_call.function.arguments = func_json.value("arguments", "{}");
out_calls.push_back(tool_call);
@SerializedName("arguments")
public String arguments; // 这是一个JSON字符串,可能需要二次解析
// 对应 tool_calls 数组中的每个元素
public String id; // 这是我们需要的 tool_call_id
@SerializedName("function")
public FunctionDetails function;
// 对应 payload 的顶层 JSON 对象
class ToolMessagePayload {
@SerializedName("subscriber_user_id")
public String subscriberUserId;
@SerializedName("tool_calls")
public ToolCall[] tool_calls;
// 文件 2: 在你的 RTC 客户端中实现回调
// 在 onRoomBinaryMessageReceived 回调中
public void onRoomBinaryMessageReceived(String uid, ByteBuffer message) {
// 确保在处理后恢复 ByteBuffer 的位置,以免影响其他处理器
message.order(ByteOrder.BIG_ENDIAN);
if (message.remaining() < 8) return;
byte[] magicBytes = new byte[4];
message.get(magicBytes);
String magic = new String(magicBytes, StandardCharsets.UTF_8);
if ("tool".equals(magic)) {
int length = message.getInt();
if (message.remaining() >= length) {
byte[] payloadBytes = new byte[length];
message.get(payloadBytes);
String payloadStr = new String(payloadBytes, StandardCharsets.UTF_8);
ToolMessagePayload toolPayload = gson.fromJson(payloadStr, ToolMessagePayload.class);
if (toolPayload != null && toolPayload.tool_calls != null && toolPayload.tool_calls.length > 0) {
ToolCall firstCall = toolPayload.tool_calls[0];
String toolCallId = firstCall.id; // 成功获取 tool_call_id!
String funcName = firstCall.function.name;
String argumentsStr = firstCall.function.arguments;
System.out.println("FC tool received:");
System.out.println(" - Tool Call ID: " + toolCallId);
System.out.println(" - Function Name: " + funcName);
System.out.println(" - Arguments: " + argumentsStr);
// argumentsStr 是一个JSON字符串,可以继续解析
// Map<String, Object> argsMap = gson.fromJson(argumentsStr, Map.class);
interface FunctionDetails {
arguments: string; // 这是一个JSON字符串,可能需要二次解析
// 对应 tool_calls 数组中的每个元素
id: string; // 这是我们需要的 tool_call_id
function: FunctionDetails;
// 对应 payload 的顶层 JSON 对象
interface ToolMessagePayload {
subscriber_user_id: string;
engine.on('onRoomBinaryMessageReceived', (event: { userId: string, message: ArrayBuffer }) => {
const { userId, message } = event;
if (message.byteLength < 8) return;
const dataView = new DataView(message);
const magic = new TextDecoder().decode(message.slice(0, 4));
const length = dataView.getUint32(4, false); // false for big-endian
if (message.byteLength < 8 + length) return;
const payloadStr = new TextDecoder().decode(message.slice(8, 8 + length));
const toolPayload: ToolMessagePayload = JSON.parse(payloadStr);
if (toolPayload && toolPayload.tool_calls && toolPayload.tool_calls.length > 0) {
const firstCall = toolPayload.tool_calls[0];
const toolCallId = firstCall.id; // 成功获取 tool_call_id!
const funcName = firstCall.function.name;
const argumentsStr = firstCall.function.arguments;
console.log('FC tool received:', {
tool_call_id: toolCallId,
function_name: funcName,
function_args_str: argumentsStr
// argumentsStr 是一个字符串化的JSON,可以继续解析
const args = JSON.parse(argumentsStr);
console.log('Parsed arguments:', args);
console.error('Failed to parse function arguments string:', e);
步骤 3:执行外部函数
在成功解析 tool 消息后,您需要在自己的业务逻辑中实现:
- 路由到函数:根据 function.name 的值(例如 "get_current_weather"),调用您在自己代码中已实现的相应函数。
- 传递参数:将上一步解析出的 function.arguments 中的参数作为入参传递给您的函数。
- 获取返回值:执行您的函数,并获取其返回结果,这个结果将在下一步中回传。
注意
function.arguments 字段本身是一个字符串,其内容是一个 JSON 对象。在调用您的本地函数之前,您必须对这个字符串再进行一次 JSON 解析,才能获得真正的参数。
步骤 4:回传函数结果
方式 1:经 LLM 处理后播放
将函数返回的原始数据传回给 LLM 处理后再播放。适用于需要总结、润色等二次处理场景。
- userId:AI 的标识 ID(对应 AgentConfig.UserId)。
- 大小限制:该接口默认单次发送的二进制消息内容包大小为 1 KB,如需更大的包传递(最大不可超过 64 KB),需在控制台启用 RTS 高级功能。
注意
若在 SDK 运行期间开启了该功能,则必须重新初始化引擎,配置才能生效。
-
- 你可参看以下示例代码,回传函数调用的结果信息并播放:
// 依赖 nlohmann/json 库进行 JSON 操作
// https://github.com/nlohmann/json
using json = nlohmann::json;
// 请将此处的 "RobotMan_" 替换为您为 AI 的实际 UserId
const std::string kAgentUserId = "RobotMan_";
void sendUserBinaryMessage(const std::string& userId, const std::vector<uint8_t>& message);
* @brief 将 JSON 字符串打包成 'func' 类型的 TLV 字节流
std::vector<uint8_t> stringToTLV(const std::string& content) {
const std::string type = "func";
size_t type_len = type.length();
size_t content_len = content.length();
size_t total_len = type_len + 4 + content_len;
std::vector<uint8_t> buffer(total_len);
std::memcpy(buffer.data(), type.data(), type_len);
buffer[type_len + 0] = (content_len >> 24) & 0xFF;
buffer[type_len + 1] = (content_len >> 16) & 0xFF;
buffer[type_len + 2] = (content_len >> 8) & 0xFF;
buffer[type_len + 3] = content_len & 0xFF;
// 3. 写入 Value (JSON 字符串)
std::memcpy(buffer.data() + type_len + 4, content.data(), content_len);
void SendFunctionResponse(const std::string& toolCallID, const std::string& content) {
payload["ToolCallID"] = toolCallID;
payload["Content"] = content;
std::string jsonString = payload.dump();
std::vector<uint8_t> binaryMessage = stringToTLV(jsonString);
sendUserBinaryMessage(kAgentUserId, binaryMessage);
public void SendFunctionResponse(String ToolCallID, String Content) {
JSONObject json = new JSONObject();
json.put("ToolCallID", ToolCallID);
json.put("Content", Content);
} catch (JSONException e) {
throw new RuntimeException(e);
String jsonString = json.toString();
System.out.println(jsonString);
// 请将 "AgentUserId" 替换为您为 AI 智能体设置的实际 UserId
app.rtcSdkWrap.sendUserBinaryMessage("AgentUserId", stringToTLV(jsonString));
public byte[] stringToTLV(String content) {
String func_type = "func";
byte[] prefixBytes = func_type.getBytes(StandardCharsets.UTF_8);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
int contentLength = contentBytes.length;
ByteBuffer buffer = ByteBuffer.allocate(prefixBytes.length + 4 + contentLength);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put(prefixBytes);
buffer.putInt(contentLength);
buffer.put(contentBytes);
import VERTC from '@volcengine/rtc';
function stringToTLV(inputString: string) {
const typeBuffer = new Uint8Array(4);
for (let i = 0; i < type.length; i++) {
typeBuffer[i] = type.charCodeAt(i);
const lengthBuffer = new Uint32Array(1);
const valueBuffer = new TextEncoder().encode(inputString);
lengthBuffer[0] = valueBuffer.length;
const tlvBuffer = new Uint8Array(typeBuffer.length + 4 + valueBuffer.length);
tlvBuffer.set(typeBuffer, 0);
tlvBuffer[4] = (lengthBuffer[0] >> 24) & 0xff;
tlvBuffer[5] = (lengthBuffer[0] >> 16) & 0xff;
tlvBuffer[6] = (lengthBuffer[0] >> 8) & 0xff;
tlvBuffer[7] = lengthBuffer[0] & 0xff;
tlvBuffer.set(valueBuffer, 8);
return tlvBuffer.buffer;
* | magic number | length(big-endian) | value |
* @param {ArrayBufferLike} tlvBuffer
function tlv2String(tlvBuffer: ArrayBufferLike) {
const typeBuffer = new Uint8Array(tlvBuffer, 0, 4);
const lengthBuffer = new Uint8Array(tlvBuffer, 4, 4);
const valueBuffer = new Uint8Array(tlvBuffer, 8);
for (let i = 0; i < typeBuffer.length; i++) {
type += String.fromCharCode(typeBuffer[i]);
(lengthBuffer[0] << 24) | (lengthBuffer[1] << 16) | (lengthBuffer[2] << 8) | lengthBuffer[3];
const value = new TextDecoder().decode(valueBuffer.subarray(0, length));
* @brief 通过 onRoomBinaryMessageReceived 接收 toolcall
* 通过 sendUserBinaryMessage 发送 response
function handleRoomBinaryMessageReceived(
const { message } = event;
const { type, value } = tlv2String(message);
const data = JSON.parse(value);
const tool_calls = data?.tool_calls;
if (tool_calls?.length) {
const toolCall = tool_calls[0];
const functionName = toolCall.function.name;
// 关键:'arguments' 是一个 JSON 字符串,必须先解析成对象才能使用
const functionArgs = JSON.parse(toolCall.function.arguments);
// 下方代码仅为通用示例,展示了如何根据函数名和参数动态生成结果。
// 在真实场景中,您需要根据 functionName 调用您自己的真实函数。
const functionResult = `函数 \\\${functionName} 已执行,收到的参数为: \\\${JSON.stringify(functionArgs)}`;
this.engine.sendUserBinaryMessage(
'Your AI Bot Name', // 请将 'Your AI Bot Name' 替换为您为 AI 智能体设置的实际 UserId
ToolCallID: toolCall.id,
Content: functionResult, // 将函数执行结果回传
this.engine.on(VERTC.events.onRoomBinaryMessageReceived, handleRoomBinaryMessageReceived.bind(this));
方式 2:直接播放(不经 LLM 处理)
将函数原始结果直接通过 TTS 播放。适用于返回结果无需处理或回复的句子较长需降低延迟等场景。
注意
若回复的句子较长,建议先自行将长句切分为多个短句,再分次调用 sendUserBinaryMessage 接口,降低延时。
- 切句建议:首个分句按逗号(,)、分号(;)切分;后续分句按句号(。)、问号(?)等完整句子标点切分。避免切分数字中的小数点。
- 切句示例:今天的天气晴朗,、温度10-29摄氏度,北风三级。、未来七天,天气会保持晴朗。
- userId:AI 的标识 ID(对应 AgentConfig.UserId)。
- 大小限制:该接口默认单次发送的二进制消息内容包大小为 1 KB,如需更大的包传递(最大不可超过 64 KB),需在控制台启用 RTS 高级功能。
注意
若在 SDK 运行期间开启了该功能,则必须重新初始化引擎,配置才能生效。
-
传入示例:
// 发送文本转语音(TTS)指令,让 AI 播报函数调用的结果信息
void sendTTSMessage(const std::string &uid, const std::string& content) {
nlohmann::json json_data;
json_data["Command"] = "ExternalTextToSpeech";
json_data["Message"] = content;
json_data["InterruptMode"] = 2;
sendUserBinaryMessage(uid, json_data.dump());
void buildBinaryMessage(const std::string& magic_number, const std::string& message, size_t& binary_message_length, std::shared_ptr<uint8_t[]>& binary_message) {
auto magic_number_length = magic_number.size();
auto message_length = message.size();
binary_message_length = magic_number_length + 4 + message_length;
binary_message = std::shared_ptr<uint8_t[]>(new uint8_t[binary_message_length]);
std::memcpy(binary_message.get(), magic_number.data(), magic_number_length);
binary_message[magic_number_length] = static_cast<uint8_t>((message_length >> 24) & 0xFF);
binary_message[magic_number_length+1] = static_cast<uint8_t>((message_length >> 16) & 0xFF);
binary_message[magic_number_length+2] = static_cast<uint8_t>((message_length >> 8) & 0xFF);
binary_message[magic_number_length+3] = static_cast<uint8_t>(message_length & 0xFF);
std::memcpy(binary_message.get()+magic_number_length+4, message.data(), message_length);
int sendUserBinaryMessage(const std::string &uid, const std::string& message) {
if (rtcRoom_ != nullptr)
std::shared_ptr<uint8_t[]> binary_message = nullptr;
buildBinaryMessage("ctrl", message, length, binary_message);
return rtcRoom_->sendUserBinaryMessage(uid.c_str(), static_cast<int>(length), binary_message.get());
// 发送文本转语音(TTS)指令,让智能体播报工具调用的结果信息
public void sendTTSMessage(String userId, String content) {
JSONObject json = new JSONObject();
json.put("Command", "ExternalTextToSpeech");
json.put("Message", content);
json.put("InterruptMode", 2);
} catch (JSONException e) {
throw new RuntimeException(e);
String jsonString = json.toString();
byte[] buildBinary = buildBinaryMessage("ctrl", jsonString);
sendUserBinaryMessage(userId, buildBinary);
private byte[] buildBinaryMessage(String magic_number, String content) {
byte[] prefixBytes = magic_number.getBytes(StandardCharsets.UTF_8);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
int contentLength = contentBytes.length;
ByteBuffer buffer = ByteBuffer.allocate(prefixBytes.length + 4 + contentLength);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put(prefixBytes);
buffer.putInt(contentLength);
buffer.put(contentBytes);
public void sendUserBinaryMessage(String userId, byte[] buffer) {
rtcRoom_.sendUserBinaryMessage(userId, buffer, MessageConfig.RELIABLE_ORDERED);
import VERTC from '@volcengine/rtc';
const BotName = 'RobotMan_'; // 自定义智能体名称
const CommandKey = 'ctrl';
const engine = VERTC.createEngine('Your AppID'); // 你的 RTC 应用 AppId
* @brief 发送文本转语音(TTS)指令,让智能体播报工具调用的结果信息
EXTERNAL_TEXT_TO_SPEECH = 'ExternalTextToSpeech',
enum INTERRUPT_PRIORITY {
* @brief 智能体播报调用指令消息内容模式。
function stringToTLV(inputString: string, type = '') {
const typeBuffer = new Uint8Array(4);
for (let i = 0; i < type.length; i++) {
typeBuffer[i] = type.charCodeAt(i);
const lengthBuffer = new Uint32Array(1);
const valueBuffer = new TextEncoder().encode(inputString);
lengthBuffer[0] = valueBuffer.length;
const tlvBuffer = new Uint8Array(typeBuffer.length + 4 + valueBuffer.length);
tlvBuffer.set(typeBuffer, 0);
tlvBuffer[4] = (lengthBuffer[0] >> 24) & 0xff;
tlvBuffer[5] = (lengthBuffer[0] >> 16) & 0xff;
tlvBuffer[6] = (lengthBuffer[0] >> 8) & 0xff;
tlvBuffer[7] = lengthBuffer[0] & 0xff;
tlvBuffer.set(valueBuffer, 8);
return tlvBuffer.buffer;
* @brief 发送文本转语音(TTS)指令,让智能体播报函数调用的结果信息。
engine.sendUserBinaryMessage(
Command: COMMAND.EXTERNAL_TEXT_TO_SPEECH,
Message: '上海天气是晴天,25摄氏度',
InterruptMode: INTERRUPT_PRIORITY.MEDIUM,
进阶使用与实践
多轮函数调用
当用户的一句话包含多个有依赖关系的任务时(如“先查会议室,再导航”),大模型会与您的服务进行多轮、串行的交互来逐步完成任务。
- 用户提问:“帮我查一下我下一场会议的会议室地点,然后导航到那里。”
- 模型:生成第一个指令,调用 get_next_meeting_info()。
- 您的服务:执行后,必须立即将结果 {"room": "A 座 301 会议室"} 回传给模型。
- 3. 模型:收到第一轮的结果“A 座 301 会议室”后,基于此信息发出第二个指令,调用 start_navigation(destination="A 座 301 会议室")。
- 4. 您的服务:执行后,再次立即将结果 {"status": "导航已开启"} 回传给模型。
- 模型:确认所有步骤完成,生成最终回复:“到 A 座 301 会议室的导航已经为您开启。”
- 启动会话:调用 StartVoiceChat,并在 Tools 参数中提供所有可能用到的函数。
- 接收并执行指令:您的后端服务会收到一个 tool_calls 指令。
- 回填结果:将函数的执行结果通过 UpdateVoiceChat 接口回填给大模型服务。
- 循环等待:大模型会根据您回填的结果,决定是生成下一个函数调用指令,还是生成最终的文本回复。您只需循环重复第 2-4 步,直到不再收到 tool_calls 指令为止。
并行函数调用
当用户的一句话包含多个互相独立的意图时(如“查天气和查股价”),您可以使用该功能,使大模型能够一次性返回多个函数调用指令,供您并发执行以提升效率。
注意
请确保你使用的模型支持该能力。对于方舟模型,仅 doubao-seed-1.6 系列模型支持该功能。
- 用户提问:“今天天气怎么样?顺便帮我查一下 A 的股价。”
- 模型:识别出两个独立任务,一次性生成两个函数的调用指令:
{"name": "get_current_weather", "arguments": {}},
{"name": "get_stock_price", "arguments": {"ticker": "A"}}
- 您的服务:接收到指令列表后,可以并发执行 get_current_weather 和 get_stock_price 这两个函数。
- 启用并行调用:调用 StartVoiceChat 时,在 LLMConfig 中设置:
- EnableParallelToolCalls:设置为 true。
- 接收并执行:您的后端服务会收到一个包含多个 tool_calls 对象的指令。遍历该指令列表,并发执行所有函数。
- 交由 LLM 总结后统一回复:将所有函数的执行结果收集起来,通过一次 UpdateVoiceChat 调用(Command: "function")将结果数组一并回传给大模型。
- 直接播报单个结果:任何一个函数执行完毕,就立即调用 UpdateVoiceChat(Command: "ExternalTextToSpeech"),将该函数的结果直接通过 TTS 播放给用户。
保留工具调用历史
如果您的场景要求模型在后续轮次继续引用此前的工具执行结果,例如“继续基于刚才查到的天气安排行程”或“根据上一轮函数返回的订单信息继续退款”,可在 StartVoiceChat 请求中将 LLMConfig.EnableToolCallHistory 设置为 true。
开启后,系统会把工具调用指令和工具返回结果作为所属对话轮次的一部分写入后续上下文,减少模型遗漏已执行结果的问题。若某轮未触发 Function Calling,该轮仍只包含用户提问和最终回复,不会额外产生工具调用历史。
注意
- 该参数仅在已配置 LLMConfig.Tools 并实际触发 Function Calling 时生效。
- HistoryLength 仍按对话轮次计数。一次完成的用户提问及其最终回复计为一轮;若中间触发工具调用,工具调用指令和工具返回结果归入该轮,不额外增加 HistoryLength 轮数,并随该轮一起保留和逐出。
提高 FC 触发准确性
为避免大模型在不相关或意图模糊的场景下(尤其是受历史对话干扰时)误触发函数调用(Function Calling),您可以通过以下方式来提升其触发准确性。
1. 高质量地声明工具
通过 StartVoiceChat 接口的 LLMConfig.Tools 声明工具时,提供清晰、准确的工具声明:
- name :应使用清晰、符合语义的英文名(如 get_current_weather),而不是 tool_1 或 my_func。
- description:用一句话清晰地描述函数的核心用途。
- (不推荐) "description": "一个天气工具。"
- (推荐) "description": "获取一个指定城市的实时天气信息。"
- parameters:清晰说明参数的用途、格式要求,并提供示例。
- (不推荐) "description": "地点"
- (推荐) "description": "需要查询天气的城市名,例如:'上海市'。请不要包含省份信息。"
2. 设计系统提示词
通过系统提示词(LLMConfig.SystemMessages)来为模型指定函数调用条件,从而精准控制模型的工具调用行为。
参考原则
- 意图匹配:仅当用户意图与工具功能明确匹配时才调用。
- 主动澄清:当用户意图模糊或缺少必要参数时,应通过提问来澄清,而不是猜测。
- 禁止幻觉:严禁调用未声明的工具或为参数捏造数值。
提示词示例
您可以参考以下模板,并根据您的业务场景进行调整:
提示词示例
对应 SystemMessages 示例
你拥有以下工具,仅在准确识别到用户的意图与下方某个工具功能匹配时,才调用该工具:
1. `adjust_volume`(调整音量):仅当用户表达了任何想改变设备音量时调用。例如,“我听不清了”、“声音太大了”、“帮我静音”、“调大音量”等。
2. `get_current_weather`(查询天气):仅当用户表达了想了解气象信息时调用。例如,“明天杭州会下雨吗?”、“帮我查询上海的天气”、“外面冷不冷?”等。
如果用户意图不匹配上述任何一个工具的触发条件,严禁调用任何工具请直接对话;如果用户意图模糊,请通过提问来澄清意图。
// ... 其他 LLM 参数,如 Mode, Tools ...
// ... 此处可包含其他角色设定或行为准则 ...
"## 工具调用规则\\n你拥有以下工具,仅在准确识别到用户的意图与下方某个工具功能匹配时,才调用该工具:\\n1. `adjust_volume`(调整音量):仅当用户表达了任何想改变设备音量时调用。例如,“我听不清了”、“声音太大了”、“帮我静音”、“调大音量”等。\\n2. `get_current_weather`(查询天气):仅当用户表达了想了解气象信息时调用。例如,“明天杭州会下雨吗?”、“帮我查询上海的天气”、“外面冷不冷?”等。\\n如果用户意图不匹配上述任何一个工具的触发条件,严禁调用任何工具请直接对话;如果用户意图模糊,请通过提问来澄清意图。"
3. 避免历史内容干扰
如果您的业务场景无需参考历史对话,也可将 HistoryLength 设置为 0,以避免历史内容干扰。
- Q1:配置了 Function Calling 后,通过 updateVoiceChat/sendUserBinaryMessage 返回给 AI 的函数调用结果信息可以不读出来吗?
- Q2:配置了 Function Calling 后,相关配置如何更新?
- 需要先调用 StopVoiceChat 接口停止 AI 对话任务,然后再调用 StartVoiceChat 接口重新配置 Function Calling 功能。
- **Q3:配置了 Tools,但客户端收不到 **** tool ** 消息?
- 检查 StartVoiceChat 是否开启了 AgentConfig.EnableConversationStateCallback: true。
- 确保自定义 LLM 返回的 JSON 包含 finish_reason: "tool_calls"。
- Q4:客户端执行函数,服务端也想收到通知可以吗?
- 可以。在 StartVoiceChat 中同时配置 LLMConfig.Tools 和 FunctionCallingConfig.ServerMessageUrl。RTC 会同时向客户端下发二进制消息和向服务端发送 HTTP POST 回调。
- 在函数执行结果回传后,通过 Prompt 明确告知 AI 该任务已完成。
- Q6:在多轮对话中 FC 被持续错误触发该如何处理?
- 若在长周期多轮对话中,遇到 FC 被持续错误触发的情况,可采取以下应对措施:
- 在 tools 的 description 中明确工具触发的强边界(如:“仅在用户询问天气时调用 get_weather 函数”)。
- 在 SystemMessages 中增加前置逻辑条件(如:“只有当用户明确提出 XXX 需求时,才调用工具”)。
- 通过重置上下文(将 LLMConfig.HistoryLength 设为 0)切断历史干扰。
- 过滤 UserPrompts,仅保留当前一轮的核心问询内容,防止历史信息影响判断。
- Q7:FC 中 TaskID、TaskId 和 ToolCallID 的区别是什么?
- 在 FC 完整流程中,存在三种不同的 ID,请勿混用:
- TaskID(UUID 格式,如 73bc1a7e-...):FC 回调请求体中的 TaskID 字段,仅用于标识单次 FC 任务,不可用于 UpdateVoiceChat。
- 会话侧 TaskId(用户自定义,如 task_001 / ChatTask01):您在 StartVoiceChat 中设置的任务 ID,UpdateVoiceChat 的 TaskId 参数必须使用此值。请从返回结果或对话状态变化回调中获取并缓存。
- ToolCallID(call_* 格式):FC 指令消息中的工具调用唯一 ID,回传函数消息结果时需使用此 ID。