Skip to main content
工具調用

Function Calling

大模型無法訪問即時資料和外部系統。Function Calling 允許模型調用外部工具(API、資料庫、自訂函數等),擷取資訊或執行操作,突破模型自身能力的限制。

工作原理

Function Calling 通過應用程式與大模型之間的多步驟互動實現:
  1. 發起第一次模型調用 應用程式向大模型發送使用者問題和可用工具清單。
  2. 接收模型的工具調用指令(工具名稱與入參) 若模型判斷需要調用外部工具,返回JSON格式的指令,指定函數名稱與入參。
    若模型判斷無需調用工具,會返回自然語言格式的回複。
  3. 在應用端運行工具 應用程式執行指定工具,擷取輸出結果。
  4. 發起第二次模型調用 將工具輸出結果添加到訊息數組(messages),再次調用模型。
  5. 接收來自模型的最終響應 模型綜合工具輸出與使用者問題,產生自然語言回複。
工作流程示意圖:

支援的模型

  • 千問
  • DeepSeek
  • GLM
  • Kimi
  • MiniMax
  • 文本產生模型
    • 千問Max:Qwen3.8-Max系列、Qwen3.7-Max系列、Qwen3.6-Max系列、Qwen3-Max系列、Qwen-Max系列
    • 千問Plus:Qwen3.7-Plus系列、Qwen3.6-Plus系列、Qwen3.5-Plus系列、Qwen-Plus系列
    • 千問Flash:Qwen3.7-Flash系列、Qwen3.6-Flash系列、Qwen3.5-Flash系列、Qwen-Flash系列
    • 千問Coder:Qwen3-Coder系列、Qwen2.5-Coder系列、Qwen-Coder系列
    • 千問Turbo:Qwen-Turbo系列
    • Qwen3.6開源系列
    • Qwen3.5開源系列
    • Qwen3開源系列
    • Qwen2.5開源系列
    • Qwen3.8開源系列
  • 多模態模型
    • 千問VL: Qwen3-VL-Plus系列、 Qwen3-VL-Flash系列
    • 千問Omni:Qwen3.5-Omni-Plus系列、Qwen3.5-Omni-Flash系列、Qwen3-Omni-Flash系列
    • 千問Omni-Realtime:Qwen3.5-Omni-Plus-Realtime系列、Qwen3.5-Omni-Flash-Realtime系列
    • Qwen3-VL 開源系列
  • 語音對話模型
    • 千問Audio-Realtime:Qwen-Audio-3.0-Realtime-Plus系列、Qwen-Audio-3.0-Realtime-Flash系列

快速開始

您需要已擷取與配置 API Key配置API Key到環境變數。如果通過 OpenAI SDK或 DashScope SDK調用,還需安裝SDK 以下樣本示範天氣查詢情境的完整 Function Calling 流程。
  • OpenAI 相容
  • DashScope
from openai import OpenAI
from datetime import datetime
import json
import os
import random

client = OpenAI(
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用百鍊API Key將下行替換為:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    # 以下為新加坡地區URL,調用時請將 {WorkspaceId} 替換為真實的業務空間ID,各地區的URL不同。
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)
# 類比使用者問題
USER_QUESTION = "新加坡天氣咋樣"
# 定義工具列表
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如新加坡、紐約等。",
                    }
                },
                "required": ["location"],
            },
        },
    },
]

# 類比天氣查詢工具
def get_current_weather(arguments):
    weather_conditions = ["晴天", "多雲", "雨天"]
    random_weather = random.choice(weather_conditions)
    location = arguments["location"]
    return f"{location}今天是{random_weather}。"

# 封裝模型響應函數
def get_response(messages):
    completion = client.chat.completions.create(
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools,
    )
    return completion

messages = [{"role": "user", "content": USER_QUESTION}]
response = get_response(messages)
assistant_output = response.choices[0].message
if assistant_output.content is None:
    assistant_output.content = ""
messages.append(assistant_output)
# 如果不需要調用工具,直接輸出內容
if assistant_output.tool_calls is None:
    print(f"無需調用天氣查詢工具,直接回複:{assistant_output.content}")
else:
    # 進入工具調用迴圈
    while assistant_output.tool_calls is not None:
        # 遍曆本輪返回的全部工具調用,避免只處理第一個而遺漏其餘工具的返回結果
        for tool_call in assistant_output.tool_calls:
            tool_call_id = tool_call.id
            func_name = tool_call.function.name
            arguments = json.loads(tool_call.function.arguments)
            print(f"正在調用工具 [{func_name}],參數:{arguments}")
            # 執行工具
            tool_result = get_current_weather(arguments)
            # 構造工具返回資訊
            tool_message = {
                "role": "tool",
                "tool_call_id": tool_call_id,
                "content": tool_result,  # 保持原始工具輸出
            }
            print(f"工具返回:{tool_message['content']}")
            messages.append(tool_message)
        # 再次調用模型,擷取總結後的自然語言回複
        response = get_response(messages)
        assistant_output = response.choices[0].message
        if assistant_output.content is None:
            assistant_output.content = ""
        messages.append(assistant_output)
    print(f"助手最終回複:{assistant_output.content}")
運行後得到如下輸出:
正在調用工具 [get_current_weather],參數:{'location': '新加坡'}
工具返回:新加坡今天是多雲。
助手最終回複:新加坡今天是多雲的天氣。

如何使用

Function Calling 支援兩種傳入工具資訊的方式:
  • 方式一:通過 tools 參數傳入(推薦) 參見如何使用,按照定義工具建立 messages 數組發起 Function Calling運行工具函數大模型總結工具函數輸出的步驟調用。
  • 方式二:通過 System Message 傳入 通過 tools 參數傳入效果最佳,服務端會自動適配最優 prompt 模板。如使用 Qwen 模型且不期望使用 tools 參數,參見通過 System Message 傳入工具資訊
以下以 OpenAI 相容介面為例,通過 tools 參數分步驟介紹 Function Calling 的詳細用法。 假設業務情境會收到天氣查詢與時間查詢兩類問題。

1. 定義工具

工具串連大模型與外部服務,首先需定義工具。

1.1. 建立工具函數

建立兩個工具函數:天氣查詢工具與時間查詢工具。
  • 天氣查詢工具 接收arguments參數,arguments格式為{"location": "查詢的地點"}。工具的輸出為字串,格式為:“{位置}今天是{天氣}”
    為了便於示範,此處定義的天氣查詢工具並不真正查詢天氣,會從晴天、多雲、雨天隨機播放。在實際業務中可使用如 高德天氣查詢 等工具進行替換。
  • 時間查詢工具 時間查詢工具不需要輸入參數。工具的輸出為字串,格式為:“目前時間:{查詢到的時間}。”
    如果使用 Node.js,請運行 npm install date-fns 安裝擷取時間的工具包 date-fns:
## 步驟1.1:定義工具函數

# 添加匯入random模組
import random
from datetime import datetime

# 類比天氣查詢工具。返回結果樣本:“北京今天是雨天。”
def get_current_weather(arguments):
    # 定義備選的天氣條件列表
    weather_conditions = ["晴天", "多雲", "雨天"]
    # 隨機播放一個天氣條件
    random_weather = random.choice(weather_conditions)
    # 從 JSON 中提取位置資訊
    location = arguments["location"]
    # 返回格式化的天氣資訊
    return f"{location}今天是{random_weather}。"

# 查詢目前時間的工具。返回結果樣本:“目前時間:2024-04-15 17:15:18。“
def get_current_time():
    # 擷取當前日期和時間
    current_datetime = datetime.now()
    # 格式化當前日期和時間
    formatted_time = current_datetime.strftime('%Y-%m-%d %H:%M:%S')
    # 返回格式化後的目前時間
    return f"目前時間:{formatted_time}。"

# 測試載入器函數並輸出結果,運行後續步驟時可以去掉以下四句測試代碼
print("測試載入器輸出:")
print(get_current_weather({"location": "上海"}))
print(get_current_time())
print("\n")
運行工具後,得到輸出:
測試載入器輸出:
上海今天是多雲。
目前時間:2025-01-08 20:21:45。

1.2 建立 tools 數組

人類選擇工具前需瞭解工具的功能、使用情境和輸入參數。大模型同理——模型依據這些資訊選擇合適的工具。按以下JSON格式提供工具資訊。
  • type欄位固定為"function"
  • function欄位為 Object 類型;
    • name欄位為自訂的工具函數名稱,建議使用與函數相同的名稱,如get_current_weatherget_current_time
    • description欄位是對工具函數功能的描述,大模型會參考該欄位來選擇是否使用該工具函數。
    • parameters欄位是對工具函數入參的描述,類型是 Object ,大模型會參考該欄位來進行入參的提取。如果工具函數不需要輸入參數,則無需指定parameters參數。
      • type欄位固定為"object"
      • properties欄位描述了入參的名稱、資料類型與描述,為 Object 類型,Key 值為入參的名稱,Value 值為入參的資料類型與描述;
      • required欄位指定哪些參數為必填項,為 Array 類型。
對於天氣查詢工具來說,工具描述資訊的格式如下:
{
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "當你想查詢指定城市的天氣時非常有用。",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市或縣區,比如北京市、杭州市、餘杭區等。"
                }
            },
            "required": ["location"]
        }
    }
}
發起 Function Calling 前,在代碼中定義工具資訊數組(tools),包含每個工具的函數名、描述和參數定義。該數組在後續請求時作為參數傳入。
# 請將以下代碼粘貼到步驟1.1代碼後

## 步驟1.2:建立 tools 數組

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "當你想知道現在的時間時非常有用。",
            "parameters": {}
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。",
                    }
                },
                "required": ["location"]
            }
        }
    }
]
tool_name = [tool["function"]["name"] for tool in tools]
print(f"建立了{len(tools)}個工具,為:{tool_name}\n")

2. 建立messages數組

Function Calling 通過 messages 數組向大模型傳入指令與上下文。發起調用前,messages 數組需包含 System Message 和 User Message。

System Message

儘管在建立 tools 數組時已描述了工具的功能和使用情境,但在 System Message 中進一步強調何時調用工具,通常能提高工具調用的準確率。當前情境可將 System Prompt 設定為:
你是一個很有協助的助手。如果使用者提問關於天氣的問題,請調用 ‘get_current_weather’ 函數;
如果使用者提問關於時間的問題,請調用‘get_current_time’函數。
請以友好的語氣回答問題。

User Message

User Message 用於傳入使用者提問的問題。假設使用者提問“上海天氣”,此時的 messages 數組為:
# 步驟2:建立messages數組
# 請將以下代碼粘貼到步驟1.2 代碼後
# 文本產生模型的 User Message樣本
messages = [
    {
        "role": "system",
        "content": """你是一個很有協助的助手。如果使用者提問關於天氣的問題,請調用 ‘get_current_weather’ 函數;
     如果使用者提問關於時間的問題,請調用‘get_current_time’函數。
     請以友好的語氣回答問題。""",
    },
    {
        "role": "user",
        "content": "上海天氣"
    }
]

# 多模態模型的 User Message樣本
# messages=[
#  {
#         "role": "system",
#         "content": """你是一個很有協助的助手。如果使用者提問關於天氣的問題,請調用 ‘get_current_weather’ 函數;
#      如果使用者提問關於時間的問題,請調用‘get_current_time’函數。
#      請以友好的語氣回答問題。""",
#     },
#     {"role": "user",
#      "content": [{"type": "image_url","image_url": {"url": "https://img.alicdn.com/imgextra/i2/O1CN01FbTJon1ErXVGMRdsN_!!6000000000405-0-tps-1024-683.jpg"}},
#                  {"type": "text", "text": "根據映像上的地點,查詢該地點當前天氣"}]},
# ]

print("messages 數組建立完成\n")
由於備選工具包含天氣查詢與時間查詢,也可提問關於目前時間的問題。

3. 發起 Function Calling

將建立好的 tools 與 messages 傳入大模型,即可發起一次 Function Calling。大模型會判斷是否調用工具。若調用,則返回該工具的函數名與參數。
支援的模型參見 支援的模型
# 步驟3:發起 function calling
# 請將以下代碼粘貼到步驟2 代碼後
from openai import OpenAI
import os

client = OpenAI(
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用百鍊API Key將下行替換為:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    # 以下為新加坡地區URL,調用時請將 {WorkspaceId} 替換為真實的業務空間ID,各地區的URL不同。
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)

def function_calling():
    completion = client.chat.completions.create(
        # 此處以qwen3.8-max為例,可按需更換模型名稱。模型列表:https://www.alibabacloud.com/help/zh/model-studio/getting-started/models
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools
    )
    print("返回對象:")
    print(completion.choices[0].message.model_dump_json())
    print("\n")
    return completion

print("正在發起function calling...")
completion = function_calling()
由於使用者提問為上海天氣,大模型指定需要使用的工具函數名稱為:"get_current_weather",函數的入參為:"{\"location\": \"上海\"}"
{
    "content": "",
    "refusal": null,
    "role": "assistant",
    "audio": null,
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_6596dafa2a6a46f7a217da",
            "function": {
                "arguments": "{\"location\": \"上海\"}",
                "name": "get_current_weather"
            },
            "type": "function",
            "index": 0
        }
    ]
}
需要注意,如果問題被大模型判斷為無需使用工具,會通過content參數直接回複。在輸入“你好”時,tool_calls參數為空白,返回對象格式為:
{
    "content": "你好!有什麼可以協助你的嗎?如果你有關於天氣或者時間的問題,我特別擅長回答。",
    "refusal": null,
    "role": "assistant",
    "audio": null,
    "function_call": null,
    "tool_calls": null
}
如果 tool_calls 參數為空白,可使程式直接返回 content ,無需運行以下步驟。
若希望每次發起 Function Calling 後大模型都可以選擇指定工具,請參見 強制工具調用

4. 運行工具函數

運行工具函數將模型決策轉化為實際操作。
運行工具函數的過程由您的計算環境而非大模型來完成。
大模型僅輸出字串格式,運行工具函數前需對工具函數名稱與入參分別解析。
  • 工具函數 建立一個工具函數名稱工具函數實體的映射function_mapper,將返回的工具函數字串映射到工具函數實體;
  • 入參 Function Calling 返回的入參為JSON字串,使用工具將其解析為JSON對象,提取入參資訊。
完成解析後,將參數傳入工具函數並執行,擷取輸出結果。
# 步驟4:運行工具函數
# 請將以下代碼粘貼到步驟3 代碼後
import json

print("正在執行工具函數...")
# 從返回的結果中擷取函數名稱和入參
function_name = completion.choices[0].message.tool_calls[0].function.name
arguments_string = completion.choices[0].message.tool_calls[0].function.arguments

# 使用json模組解析參數字串
arguments = json.loads(arguments_string)
# 建立一個函數映射表
function_mapper = {
    "get_current_weather": get_current_weather,
    "get_current_time": get_current_time
}
# 擷取函數實體
function = function_mapper[function_name]
# 如果入參為空白,則直接調用函數
if arguments == {}:
    function_output = function()
# 否則,傳入參數後調用函數
else:
    function_output = function(arguments)
# 列印工具的輸出
print(f"工具函數輸出:{function_output}\n")
運行後得到如下輸出:
上海今天是多雲。
實際業務中,許多工具執行具體操作(如郵件發送、檔案上傳),而非資料查詢,不會輸出字串。建議為此類工具添加狀態原因資訊(如“郵件發送完成”、“操作執行失敗”),協助大模型瞭解執行狀態。

5. 大模型總結工具函數輸出

工具函數的輸出格式較為固定,直接返回使用者可能語氣生硬。將工具輸出提交到模型上下文並再次調用模型,可產生自然語言風格的回複。
  1. 添加 Assistant Message 發起 Function Calling後,通過completion.choices[0].message得到 Assistant Message,首先將它添加到 messages 數組中;
  2. 添加 Tool Message 將工具的輸出通過{"role": "tool", "content": "工具的輸出","tool_call_id": completion.choices[0].message.tool_calls[0].id}形式添加到 messages 數組。
    • 請確保工具的輸出為字串格式。
    • tool_call_id 是系統為每一次的工具調用請求產生的唯一識別碼。模型可能一次性要求調用多個工具,將多個工具結果返回給模型時,tool_call_id可確保工具的輸出結果能夠與它的調用意圖對應。
# 步驟5.1:向大模型提交工具輸出
# 請將以下代碼粘貼到步驟4 代碼後

messages.append(completion.choices[0].message)
print("已添加assistant message")
messages.append({"role": "tool", "content": function_output, "tool_call_id": completion.choices[0].message.tool_calls[0].id})
print("已添加tool message\n")
此時的 messages 數組為:
[
  System Message -- 指引模型調用工具的策略
  User Message -- 使用者的問題
  Assistant Message -- 模型返回的工具調用資訊
  Tool Message -- 工具的輸出資訊(如果採用下文介紹的並行工具調用,可能有多個 Tool Message)
]
更新 messages 數組後,運行以下代碼。
# 步驟5.2:大模型總結工具輸出
# 請將以下代碼粘貼到步驟5.1 代碼後
print("正在總結工具輸出...")
completion = function_calling()
可從content得到回複內容:“上海今天的天氣是多雲。如果您有其他問題,歡迎繼續提問。”
{
    "content": "上海今天的天氣是多雲。如果您有其他問題,歡迎繼續提問。",
    "refusal": null,
    "role": "assistant",
    "audio": null,
    "function_call": null,
    "tool_calls": null
}
至此,您已完成了一次完整的 Function Calling 流程。

進階用法

指定工具調用方式

並行工具調用

單一城市天氣查詢只需一次工具調用。若問題需要多次調用工具,如“北京上海的天氣如何”或“杭州天氣,以及現在幾點了”,發起 Function Calling 後只會返回一個工具調用資訊,以提問“北京上海的天氣如何”為例:
{
    "content": "",
    "refusal": null,
    "role": "assistant",
    "audio": null,
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_61a2bbd82a8042289f1ff2",
            "function": {
                "arguments": "{\"location\": \"北京市\"}",
                "name": "get_current_weather"
            },
            "type": "function",
            "index": 0
        }
    ]
}
返回結果中只有北京市的入參資訊。為瞭解決這一問題,在發起 Function Calling時,可佈建要求參數parallel_tool_callstrue,這樣返回對象中將包含所有需要調用的工具函數與入參資訊。
並行工具調用適合任務之間無依賴的情況。若任務之間有依賴關係(工具A的輸入與工具B的輸出結果有關),請參見快速開始,通過while迴圈實現串列工具調用(一次調用一個工具)。
def function_calling():
    completion = client.chat.completions.create(
        model="qwen3.8-max",  # 此處以qwen3.8-max為例,可按需更換模型名稱
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools,
        # 新增參數
        parallel_tool_calls=True
    )
    print("返回對象:")
    print(completion.choices[0].message.model_dump_json())
    print("\n")
    return completion

print("正在發起function calling...")
completion = function_calling()
在返回對象的tool_calls數組中包含了北京上海的入參資訊:
{
    "content": "",
    "role": "assistant",
    "tool_calls": [
        {
            "function": {
                "name": "get_current_weather",
                "arguments": "{\"location\": \"北京市\"}"
            },
            "index": 0,
            "id": "call_c2d8a3a24c4d4929b26ae2",
            "type": "function"
        },
        {
            "function": {
                "name": "get_current_weather",
                "arguments": "{\"location\": \"上海市\"}"
            },
            "index": 1,
            "id": "call_dc7f2f678f1944da9194cd",
            "type": "function"
        }
    ]
}

強制工具調用

大模型產生內容具有不確定性,可能選擇錯誤的工具。如需對某類問題強制使用或禁用特定工具,可修改tool_choice參數。tool_choice參數的預設值為"auto",表示由大模型自主判斷如何進行工具調用。
大模型總結工具函數輸出時,請將 tool_choice 參數去除,否則API仍會返回工具調用資訊。
  • 強制使用某個工具 如果您希望對於某一類問題,Function Calling 能強制調用某個工具,可設定tool_choice參數為{"type": "function", "function": {"name": "the_function_to_call"}},大模型將不參與工具的選擇,只輸出入參資訊。 假設當前情境中只包含天氣查詢的問題,可修改 function_calling 代碼為:
def function_calling():
    completion = client.chat.completions.create(
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools,
        tool_choice={"type": "function", "function": {"name": "get_current_weather"}}
    )
    print(completion.model_dump_json())

function_calling()
async function functionCalling() {
    const response = await openai.chat.completions.create({
        model: "qwen3.8-max",
        enable_thinking: false,
        messages: messages,
        tools: tools,
        tool_choice: {"type": "function", "function": {"name": "get_current_weather"}}
    });
    console.log("返回對象:");
    console.log(JSON.stringify(response.choices[0].message));
    console.log("\n");
    return response;
}

const response = await functionCalling();
無論輸入什麼問題,返回對象的工具函數都會是get_current_weather
使用該策略前請確保問題與選擇的工具相關,否則可能返回不符合預期的結果。
強制使用至少一個工具 某些需要使用工具的問題,大模型可能判斷為無需調用。如需強制 Function Calling 始終進行工具調用(返回對象中tool_calls參數不為空白),可以設定tool_choice參數為"required",Function Calling 將始終返回工具與入參資訊。 假設當前情境中的問題均需要調用工具,您可以修改 function_calling 代碼為:
def function_calling():
    completion = client.chat.completions.create(
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools,
        tool_choice="required"
    )
    print(completion.model_dump_json())

function_calling()
無論輸入什麼問題,返回對象的tool_calls參數將始終不為空白。
使用該策略前請確保問題與工具相關,否則可能返回不符合預期的結果。
  • 強制不使用工具 如需 Function Calling 始終不進行工具調用(返回對象中包含回複內容contenttool_calls參數為空白),可設定tool_choice參數為"none",或不傳入tools參數,Function Calling 返回的tool_calls參數將始終為空白。 假設當前情境中的問題均無需調用工具,可修改 function_calling 代碼為:
def function_calling():
    completion = client.chat.completions.create(
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        messages=messages,
        tools=tools,
        tool_choice="none"
    )
    print(completion.model_dump_json())

function_calling()
async function functionCalling() {
    const completion = await openai.chat.completions.create({
        model: "qwen3.8-max",
        enable_thinking: false,
        messages: messages,
        tools: tools,
        tool_choice: "none"
    });
    console.log("返回對象:");
    console.log(JSON.stringify(completion.choices[0].message));
    console.log("\n");
    return completion;
}

const completion = await functionCalling();

多輪對話

使用者可能第一輪提問“北京天氣”,第二輪提問“上海的呢?”。若模型上下文缺少第一輪資訊,模型無法判斷調用哪個工具。多輪對話情境中,每輪結束後保持 messages 數組完整,在此基礎上添加 User Message 並發起 Function Calling以及後續步驟。messages 結構如下所示:
[
  System Message -- 指引模型調用工具的策略
  User Message -- 使用者的問題
  Assistant Message -- 模型返回的工具調用資訊
  Tool Message -- 工具的輸出資訊
  Assistant Message -- 模型總結的工具調用資訊
  User Message -- 使用者第二輪的問題
]

流式輸出

使用流式輸出可即時擷取工具函數名稱與入參資訊,提升使用者體驗。其中:
  • 工具調用的參數資訊:以資料流的形式分塊返回。
  • 工具函數名稱:在流式響應的第一個資料區塊中返回。
from openai import OpenAI
import os

client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    # 以下為新加坡地區URL,調用時請將 {WorkspaceId} 替換為真實的業務空間ID,各地區的URL不同。
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。",
                    }
                },
                "required": ["location"],
            },
        },
    },
]

stream = client.chat.completions.create(
    model="qwen3.8-max",
    extra_body={"enable_thinking": False},
    messages=[{"role": "user", "content": "杭州天氣?"}],
    tools=tools,
    stream=True
)

for chunk in stream:
    delta = chunk.choices[0].delta
    print(delta.tool_calls)
運行後得到如下輸出:
[ChoiceDeltaToolCall(index=0, id='call_8f08d2b0fc0c4d8fab7123', function=ChoiceDeltaToolCallFunction(arguments='{"location":', name='get_current_weather'), type='function')]
[ChoiceDeltaToolCall(index=0, id='', function=ChoiceDeltaToolCallFunction(arguments=' "杭州"}', name=None), type='function')]
None
運行以下代碼拼接入參資訊(arguments):
tool_calls = {}
for response_chunk in stream:
    delta_tool_calls = response_chunk.choices[0].delta.tool_calls
    if delta_tool_calls:
        for tool_call_chunk in delta_tool_calls:
            call_index = tool_call_chunk.index
            tool_call_chunk.function.arguments = tool_call_chunk.function.arguments or ""
            if call_index not in tool_calls:
                tool_calls[call_index] = tool_call_chunk
            else:
                tool_calls[call_index].function.arguments += tool_call_chunk.function.arguments
print(tool_calls[0].model_dump_json())
獲得如下輸出:
{"index":0,"id":"call_16c72bef988a4c6c8cc662","function":{"arguments":"{\"location\": \"杭州\"}","name":"get_current_weather"},"type":"function"}
在使用大模型總結工具函數輸出步驟,添加的 Assistant Message 需要符合下方格式。僅需將下方的tool_calls中的元素替換為以上內容即可。
{
    "content": "",
    "refusal": None,
    "role": "assistant",
    "audio": None,
    "function_call": None,
    "tool_calls": [
        {
            "id": "call_xxx",
            "function": {
                "arguments": '{"location": "xx"}',
                "name": "get_current_weather",
            },
            "type": "function",
            "index": 0,
        }
    ],
}

Responses API的工具調用

上述樣本基於 OpenAI Chat Completions 和 DashScope API。若使用 OpenAI Responses API,整體流程相同,但介面格式有以下區別:
維度Chat CompletionsResponses API
工具定義格式
{
    "type": "function",
    "function": {
        "name":...,
        "parameters":...
    }
}
{
    "type": "function",
    "name":...,
    "parameters":...
}
工具調用輸出response.choices[0].message.tool_callsresponse.output 中 type 為 function_call 的項
工具結果回傳
{
    "role": "tool",
    "tool_call_id":...,
    "content":...
}
{
    "type": "function_call_output",
    "call_id":...,
    "output":...
}
最終回複response.choices[0].message.contentresponse.output_text
from openai import OpenAI
import json
import os
import random

# 初始化用戶端
client = OpenAI(
    # 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:api_key="sk-xxx",
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)
# 類比使用者問題
USER_QUESTION = "新加坡天氣咋樣"
# 定義工具列表
tools = [
    {
        "type": "function",
        "name": "get_current_weather",
        "description": "當你想查詢指定城市的天氣時非常有用。",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市或縣區,比如新加坡、倫敦等。",
                }
            },
            "required": ["location"],
        },
    }
]

# 類比天氣查詢工具
def get_current_weather(arguments):
    weather_conditions = ["晴天", "多雲", "雨天"]
    random_weather = random.choice(weather_conditions)
    location = arguments["location"]
    return f"{location}今天是{random_weather}。"

# 封裝模型響應函數
def get_response(input_data):
    response = client.responses.create(
        model="qwen3.8-max",
        extra_body={"enable_thinking": False},
        input=input_data,
        tools=tools,
    )
    return response

# 維護對話上下文
conversation = [{"role": "user", "content": USER_QUESTION}]

response = get_response(conversation)
function_calls = [item for item in response.output if item.type == "function_call"]
# 如果不需要調用工具,直接輸出內容
if not function_calls:
    print(f"助手最終回複:{response.output_text}")
else:
    # 進入工具調用迴圈
    while function_calls:
        for fc in function_calls:
            func_name = fc.name
            arguments = json.loads(fc.arguments)
            print(f"正在調用工具 [{func_name}],參數:{arguments}")
            # 執行工具
            tool_result = get_current_weather(arguments)
            print(f"工具返回:{tool_result}")
            # 將工具調用和結果成對追加到上下文中
            conversation.append(
                {
                    "type": "function_call",
                    "name": fc.name,
                    "arguments": fc.arguments,
                    "call_id": fc.call_id,
                }
            )
            conversation.append(
                {
                    "type": "function_call_output",
                    "call_id": fc.call_id,
                    "output": tool_result,
                }
            )
        # 攜帶完整上下文再次調用模型
        response = get_response(conversation)
        function_calls = [
            item for item in response.output if item.type == "function_call"
        ]
    print(f"助手最終回複:{response.output_text}")

全模態模型的工具調用

全模態模型支援工具調用,Qwen-Omni 系列和 Qwen-Omni-Realtime 系列的調用方式不同。

Qwen-Omni 系列

Qwen3.5-Omni-Plus、Qwen3.5-Omni-Flash、Qwen3-Omni-Flash 系列支援工具調用,通過 OpenAI 相容介面調用。擷取工具資訊階段與其他模型有以下不同:
  • 必須使用流式輸出:千問Omni僅支援流式輸出,在擷取工具資訊時也必須設定 stream=True
  • 建議僅輸出文本:模型在擷取工具資訊(函數的名稱和參數)時僅需文本資訊,為避免產生不必要的音頻,建議設定 modalities=["text"]。當輸出包含文本和音頻兩種模態時,擷取工具資訊時需要跳過音頻資料區塊。
千問Omni詳情參見: 非即時(Qwen-Omni)
from openai import OpenAI
import os

client = OpenAI(
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    # 以下為新加坡地區URL,調用時請將 {WorkspaceId} 替換為真實的業務空間ID,各地區的URL不同。
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。",
                    }
                },
                "required": ["location"],
            },
        },
    },
]

completion = client.chat.completions.create(
    model="qwen3.5-omni-plus",
    messages=[{"role": "user", "content": "杭州天氣?"}],

    # 設定輸出資料的模態,可取值:["text"]、["text","audio"],建議設定為["text"]
    modalities=["text"],

    # stream 必須設定為 True,否則會報錯
    stream=True,
    tools=tools
)

for chunk in completion:
    # 如果輸出包含音頻模態,請將下列條件改為:if chunk.choices and not hasattr(chunk.choices[0].delta, "audio"):
    if chunk.choices:
        delta = chunk.choices[0].delta
        print(delta.tool_calls)
運行後得到如下輸出:
[ChoiceDeltaToolCall(index=0, id='call_391c8e5787bc4972a388aa', function=ChoiceDeltaToolCallFunction(arguments=None, name='get_current_weather'), type='function')]
[ChoiceDeltaToolCall(index=0, id='call_391c8e5787bc4972a388aa', function=ChoiceDeltaToolCallFunction(arguments=' {"location": "杭州市"}', name=None), type='function')]
None
拼接入參資訊(arguments)的代碼請參見流式輸出

Qwen-Omni-Realtime 系列

Qwen3.5-Omni-Plus-Realtime、Qwen3.5-Omni-Flash-Realtime 系列支援工具調用,適用於語音對話情境。可通過 DashScope SDK或 WebSocket 原生協議調用。 工作流程 建立 WebSocket 串連後,通過 session.update 傳入工具定義,即可進入以下互動流程: 階段一:語音輸入與工具調用
  1. 使用者發起語音提問,用戶端採集音頻並發送至服務端(對應 append_audio() 方法),服務端 VAD 檢測語音結束後進行模型推理,判斷需要調用工具。
  2. 服務端將工具調用資訊返回給用戶端(對應 response.function_call_arguments.done 事件),包含函數名(name)、函數入參(arguments)和調用標識(call_id),樣本如下:
{
    "type": "response.function_call_arguments.done",
    "response_id": "resp_JnTOsWXlFhKcFohZbtfz6",
    "item_id": "item_Rhcms7CauTNsQprV5S4Hr",
    "output_index": 0,
    "name": "get_current_weather",
    "call_id": "call_2be200f4cafe419b9530dd",
    "arguments": "{\"location\": \"杭州\"}"
}
  1. 用戶端根據函數名和入參,在本地執行對應的工具函數,獲得執行結果。
階段二:用戶端回傳工具結果並觸發最終響應
  1. 用戶端將工具執行結果發回服務端(對應 conversation.item.create 事件),包含調用標識(call_id)和執行結果(output),樣本如下:
{
    "type": "conversation.item.create",
    "item": {
        "type": "function_call_output",
        "call_id": "call_2be200f4cafe419b9530dd",
        "output": "杭州今天天氣為晴,氣溫25℃,微風"
    }
}
  1. 用戶端繼續發送 response.create 事件,觸發服務端基於工具執行結果產生最終語音回答。
  2. 用戶端接收服務端返回的語音和文本(對應 response.audio.deltaresponse.audio_transcript.delta 事件),播放語音回複給使用者。
Qwen-Omni-Realtime 系列不支援 tool_choiceparallel_tool_calls 參數。
千問Omni-Realtime詳情請參見: 即時(Qwen-Omni-Realtime)用戶端事件服務端事件
DashScope Python SDK
import os
import uuid
import threading
import traceback
import json
import base64
import signal
import sys
import time
from typing import Dict, Any, Optional, List
import pyaudio
import queue
import contextlib
import dashscope
from dashscope.audio.qwen_omni import *

# ==================== 常量定義 ====================
VOICE = 'Tina'
MODEL = "qwen3.5-omni-plus-realtime"
# 如果需要訪問北京地區,請WS_URL將替換為:wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime
WS_URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime"
# 配置 API Key,若沒有設定環境變數,請用 API Key 將下行替換為 dashscope.api_key = "sk-xxx"
dashscope.api_key = os.getenv('DASHSCOPE_API_KEY')
AUDIO_SAMPLE_RATE = 16000
AUDIO_CHUNK_SIZE = 3200
OUTPUT_AUDIO_SAMPLE_RATE = 24000

# ==================== 工具定義 ====================
def get_train_price(src: str, dst: str) -> str:
    """查詢火車票價格"""
    return f"{src}{dst}的火車票價格為100~200元。"

def get_flight_price(src: str, dst: str) -> str:
    """查詢飛機票價格"""
    return f"{src}{dst}的機票價格為200~300美元。"

def get_current_weather(location: str) -> str:
    """查詢指定城市天氣"""
    return f"{location}今天天氣為霾轉晴,氣溫4/-4℃,微風"

# 統一的 OpenAI 格式工具定義
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。",
                    }
                },
                "required": ["location"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_flight_price",
            "description": "當你想查詢飛機票價格時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "src": {
                        "type": "string",
                        "description": "飛機起飛的城市,比如北京市、杭州市等。",
                    },
                    "dst": {
                        "type": "string",
                        "description": "飛機降落的城市,比如北京市、杭州市區等。",
                    },
                },
                "required": ["src", "dst"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_train_price",
            "description": "當你想查詢火車票價格時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "src": {
                        "type": "string",
                        "description": "火車出發的城市,比如北京市、杭州市等。",
                    },
                    "dst": {
                        "type": "string",
                        "description": "火車到達的城市,比如北京市、杭州市區等。",
                    },
                },
                "required": ["src", "dst"],
            },
        },
    },
]

# 工具名稱到函數的映射
TOOL_FUNCTIONS = {
    "get_current_weather": get_current_weather,
    "get_flight_price": get_flight_price,
    "get_train_price": get_train_price,
}

# ==================== 工具調用處理 ====================
def handle_tool_call(tool_call_response: Dict[str, Any]) -> Dict[str, Any]:
    """
    處理工具調用請求

    Args:
        tool_call_response: 包含 name, arguments, call_id 的工具調用資訊

    Returns:
        更新後的工具調用響應,包含 output 欄位
    """
    try:
        function_name = tool_call_response['name']
        tool_call_arguments = json.loads(tool_call_response['arguments'])

        print(f'[Tool Call] 開始處理: name={function_name}, args={tool_call_arguments}')

        # 尋找對應的函數
        if function_name not in TOOL_FUNCTIONS:
            tool_call_response['output'] = f"用戶端未找到工具: {function_name}"
            print(f'[Tool Call] 錯誤: 未找到工具 {function_name}')
            return tool_call_response

        # 調用函數
        func = TOOL_FUNCTIONS[function_name]
        result = func(**tool_call_arguments)
        tool_call_response['output'] = result

        print(f'[Tool Call] 完成: {result}')
        return tool_call_response

    except Exception as e:
        error_msg = f"工具調用失敗: {str(e)}"
        tool_call_response['output'] = error_msg
        print(f'[Tool Call] 異常: {error_msg}')
        traceback.print_exc()
        return tool_call_response

def send_tool_call_response(conversation: OmniRealtimeConversation, response: Dict[str, Any]) -> None:
    """發送工具調用結果到服務端"""
    conversation.create_item({
        "id": 'item_' + uuid.uuid4().hex,
        "type": "function_call_output",
        "call_id": response['call_id'],
        "output": response["output"],
    })

# ==================== PCM 音頻播放器 ====================
class PCMPlayer:
    """
    PCM 音頻播放器

    使用雙線程架構實現即時音頻播放:
    - 解碼線程:將 base64 編碼的音頻資料解碼為原始 PCM 資料
    - 播放線程:將 PCM 資料寫入音訊輸出裝置

    支援動態添加音頻資料、取消播放、儲存音頻檔案等功能。
    """

    def __init__(self, pya: pyaudio.PyAudio, sample_rate=24000, chunk_size_ms=100, save_file=False):
        """
        初始化 PCM 播放器

        Args:
            pya: pyaudio.PyAudio 執行個體
            sample_rate: 音頻採樣率(Hz),預設 24000
            chunk_size_ms: 音頻塊大小(毫秒),影響取消播放的延遲,預設 100ms
            save_file: 是否儲存播放的音頻到檔案(result.pcm),預設 False
        """

        self.pya = pya
        self.sample_rate = sample_rate
        self.chunk_size_bytes = chunk_size_ms * sample_rate * 2 // 1000
        self.player_stream = pya.open(format=pyaudio.paInt16,
                                       channels=1,
                                       rate=sample_rate,
                                       output=True)

        self.raw_audio_buffer: queue.Queue = queue.Queue()
        self.b64_audio_buffer: queue.Queue = queue.Queue()
        self.status_lock = threading.Lock()
        self.status = 'playing'
        self.decoder_thread = threading.Thread(target=self.decoder_loop)
        self.player_thread = threading.Thread(target=self.player_loop)
        self.decoder_thread.start()
        self.player_thread.start()
        self.complete_event: threading.Event = None
        self.save_file = save_file
        if self.save_file:
            self.out_file = open('result.pcm', 'wb')

    def decoder_loop(self):
        """解碼線程:將 base64 音頻資料解碼為 PCM 未經處理資料"""
        while self.status != 'stop':
            recv_audio_b64 = None
            with contextlib.suppress(queue.Empty):
                recv_audio_b64 = self.b64_audio_buffer.get(timeout=0.1)
            if recv_audio_b64 is None:
                continue
            recv_audio_raw = base64.b64decode(recv_audio_b64)
            # push raw audio data into queue by chunk
            for i in range(0, len(recv_audio_raw), self.chunk_size_bytes):
                chunk = recv_audio_raw[i:i + self.chunk_size_bytes]
                self.raw_audio_buffer.put(chunk)
                if self.save_file:
                    self.out_file.write(chunk)

    def player_loop(self):
        """播放線程:將 PCM 資料寫入音訊輸出裝置"""
        while self.status != 'stop':
            recv_audio_raw = None
            with contextlib.suppress(queue.Empty):
                recv_audio_raw = self.raw_audio_buffer.get(timeout=0.1)
            if recv_audio_raw is None:
                if self.complete_event:
                    self.complete_event.set()
                continue
            # write chunk to pyaudio audio player, wait until finish playing this chunk.
            self.player_stream.write(recv_audio_raw)

    def cancel_playing(self):
        """取消播放:清空所有緩衝隊列"""
        self.b64_audio_buffer.queue.clear()
        self.raw_audio_buffer.queue.clear()

    def add_data(self, data):
        """添加 base64 編碼的音頻資料到播放隊列"""
        self.b64_audio_buffer.put(data)

    def wait_for_complete(self):
        """等待播放完成"""
        self.complete_event = threading.Event()
        self.complete_event.wait()
        self.complete_event = None

    def shutdown(self):
        """關閉播放器並釋放資源"""
        self.status = 'stop'
        self.decoder_thread.join()
        self.player_thread.join()
        self.player_stream.close()
        if self.save_file:
            self.out_file.close()

# ==================== 音頻管理器 ====================
class AudioManager:
    """管理音頻輸入輸出資源"""

    def __init__(self):
        self.pya: Optional[pyaudio.PyAudio] = None
        self.mic_stream: Optional[pyaudio.Stream] = None
        self.player: Optional[PCMPlayer] = None

    def initialize(self) -> None:
        """初始化音訊裝置"""
        print('初始化音訊裝置...')
        self.pya = pyaudio.PyAudio()
        self.mic_stream = self.pya.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=AUDIO_SAMPLE_RATE,
            input=True
        )
        self.player = PCMPlayer(self.pya, sample_rate=OUTPUT_AUDIO_SAMPLE_RATE)
        print('音訊裝置初始化完成')

    def read_audio_chunk(self) -> Optional[bytes]:
        """讀取音頻資料區塊"""
        if not self.mic_stream:
            return None
        try:
            return self.mic_stream.read(AUDIO_CHUNK_SIZE, exception_on_overflow=False)
        except Exception as e:
            print(f'[Error] 讀取音頻資料失敗: {e}')
            return None

    def cleanup(self) -> None:
        """清理音頻資源"""
        print('清理音頻資源...')
        if self.player:
            self.player.shutdown()
        if self.mic_stream:
            self.mic_stream.close()
        if self.pya:
            self.pya.terminate()
        print('音頻資源清理完成')

# ==================== 回調處理器 ====================
class OmniCallback(OmniRealtimeCallback):
    """Omni 即時對話回調處理器"""

    def __init__(self, audio_manager: AudioManager):
        self.audio_manager = audio_manager
        self.tool_calls: Dict[str, Dict[str, Any]] = {}
        self.all_response_text: str = ''
        self.last_package_time: float = 0
        self.is_first_text: bool = True
        self.is_first_audio: bool = True
        self.conversation: Optional[OmniRealtimeConversation] = None

    def set_conversation(self, conversation: OmniRealtimeConversation) -> None:
        """設定對話執行個體引用"""
        self.conversation = conversation

    def on_open(self) -> None:
        """串連建立時的回調"""
        print('串連已建立')
        self.audio_manager.initialize()
        self.last_package_time = time.time() * 1000
        self.is_first_text = True
        self.is_first_audio = True
        self.tool_calls = {}
        self.all_response_text = ''

    def on_close(self, close_status_code: int, close_msg: str) -> None:
        """串連關閉時的回調"""
        print(f'串連已關閉: code={close_status_code}, msg={close_msg}')
        self.audio_manager.cleanup()
        sys.exit(0)

    def on_event(self, response: Dict[str, Any]) -> None:
        """處理事件回調"""
        try:
            event_type = response.get('type', '')

            # 會話建立
            if event_type == 'session.created':
                print(f'會話已啟動: {response["session"]["id"]}')

            # 語音轉文本完成
            elif event_type == 'conversation.item.input_audio_transcription.completed':
                print(f'使用者問題: {response.get("transcript", "")}')

            # 文本增量響應
            elif event_type in ('response.audio_transcript.delta', 'response.text.delta'):
                if self.is_first_text:
                    self.is_first_text = False
                    latency = time.time() * 1000 - self.last_package_time
                    print(f'首字延遲 (VAD結束): {latency:.0f} ms')

                text = response.get('delta', '')
                self.all_response_text += text

            # 音頻增量響應
            elif event_type == 'response.audio.delta':
                if self.is_first_audio:
                    self.is_first_audio = False
                    latency = time.time() * 1000 - self.last_package_time
                    print(f'首音延遲 (VAD結束): {latency:.0f} ms')

                audio_interval = time.time() * 1000 - self.last_package_time
                print(f'音頻間隔: {audio_interval:.0f} ms')
                self.last_package_time = time.time() * 1000

                recv_audio_b64 = response.get('delta', '')
                if self.audio_manager.player:
                    self.audio_manager.player.add_data(recv_audio_b64)

            # VAD 檢測到語音開始
            elif event_type == 'input_audio_buffer.speech_started':
                print('====== VAD 檢測到語音開始 ======')
                if self.audio_manager.player:
                    self.audio_manager.player.cancel_playing()

            # VAD 檢測到語音結束
            elif event_type == 'input_audio_buffer.speech_stopped':
                print('====== VAD 檢測到語音結束 ======')
                self.last_package_time = time.time() * 1000
                self.is_first_text = True
                self.is_first_audio = True
                self.tool_calls = {}

            # 函數調用參數完成
            elif event_type == 'response.function_call_arguments.done':
                print('====== 收到工具調用請求 ======')
                call_id = response.get('call_id', '')
                self.tool_calls[call_id] = response.copy()
                self.tool_calls[call_id]['processed'] = False

            # 響應完成
            elif event_type == 'response.done':
                print('====== 響應完成 ======')
                print(f'完整回複: {self.all_response_text}')

                if self.conversation:
                    response_id = self.conversation.get_last_response_id()
                    text_delay = self.conversation.get_last_first_text_delay()
                    audio_delay = self.conversation.get_last_first_audio_delay()

                    # 只有當所有指標都可用時才列印詳細指標
                    if response_id is not None and text_delay is not None and audio_delay is not None:
                        print(f'[Metric] 響應ID: {response_id}, '
                              f'首字延遲: {text_delay:.0f}ms, '
                              f'首音延遲: {audio_delay:.0f}ms')
                    else:
                        print('[Metric] 指標資訊暫不可用(可能是工具調用後的響應)')

                self.all_response_text = ''

        except Exception as e:
            print(f'[Error] 處理事件異常: {e}')
            traceback.print_exc()

    def process_pending_tool_calls(self) -> bool:
        """
        處理待處理的工具調用

        Returns:
            是否有新的工具調用需要響應
        """
        has_pending = False

        for call_id, tool_call in self.tool_calls.items():
            if not tool_call.get('processed', False):
                has_pending = True
                tool_call['processed'] = True

                # 處理工具調用
                result = handle_tool_call(tool_call)

                # 發送結果到服務端
                if self.conversation:
                    send_tool_call_response(self.conversation, result)

        return has_pending

# ==================== 主程式 ====================
def main():
    """主函數"""
    print('正在初始化 Omni 即時對話...')

    # 建立音頻管理器
    audio_manager = AudioManager()

    # 建立回調處理器
    callback = OmniCallback(audio_manager)

    # 建立對話執行個體
    conversation = OmniRealtimeConversation(
        api_key=dashscope.api_key,
        url=WS_URL,
        model=MODEL,
        callback=callback,
    )

    # 設定回調中的對話引用
    callback.set_conversation(conversation)

    # 建立串連
    conversation.connect()

    # 配置會話參數
    omni_output_modalities = [MultiModality.AUDIO, MultiModality.TEXT]

    conversation.update_session(
        output_modalities=omni_output_modalities,
        voice=VOICE,
        input_audio_format=AudioFormat.PCM_16000HZ_MONO_16BIT,
        output_audio_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
        enable_input_audio_transcription=True,
        enable_turn_detection=True,
        turn_detection_type='server_vad',
        tools=TOOLS,
    )

    # 設定訊號處理
    def signal_handler(sig, frame):
        print('\n接收到 Ctrl+C,正在停止...')
        conversation.close()
        audio_manager.cleanup()
        print('Omni 即時對話已停止')
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)
    print("按 Ctrl+C 停止對話...\n")

    # 主迴圈:持續發送音頻並檢查工具調用
    try:
        while True:
            # 處理待處理的工具調用
            has_tool_calls = callback.process_pending_tool_calls()

            if has_tool_calls:
                print("*** 工具調用完成,建立新響應 ***")
                conversation.create_response(
                    instructions=None,
                    output_modalities=omni_output_modalities
                )
                print('====== 工具調用處理完成 ======\n')

            # 讀取並發送音頻資料
            audio_data = audio_manager.read_audio_chunk()
            if audio_data:
                audio_b64 = base64.b64encode(audio_data).decode('ascii')
                conversation.append_audio(audio_b64)
            else:
                break

    except KeyboardInterrupt:
        signal_handler(signal.SIGINT, None)
    except Exception as e:
        print(f'[Error] 主迴圈異常: {e}')
        traceback.print_exc()
    finally:
        conversation.close()
        audio_manager.cleanup()

if __name__ == '__main__':
    main()

深度思考模型的工具調用

深度思考模型在輸出工具調用資訊前先進行推理,提升決策的可解釋性與可靠性。
  1. 思考過程 模型逐步分析使用者意圖、識別所需工具、驗證參數合法性,並規劃調用策略;
  2. 工具調用 模型以結構化格式輸出一個或多個函數調用請求。
    支援並行工具調用。
以下展示流式調用深度思考模型的工具調用樣本。
文本產生思考模型請參見: 深度思考 ;多模態思考模型請參見: 映像與視頻理解非即時(Qwen-Omni)
tool_choice 參數只支援設定為 "auto" (預設值,表示由模型自主選擇工具)或 "none" (強制模型不選擇工具)。開啟思考模式(enable_thinking=True)時,tool_choice 不支援設定為 "required" 或 object 形式,同時設定會報錯(The tool_choice parameter does not support being set to required or object in thinking mode),兩者不相容。因此不能將 tool_choice="required" 作為思考模式下保障 tool_calls 不為空白的方案。如需在思考模式下穩定進行工具調用(如 MCP 調用),建議改用 Responses API 接入 MCP,參見 MCP
  • OpenAI相容
  • DashScope
  • Python
  • Node.js
  • HTTP

範例程式碼

import os
from openai import OpenAI

# 初始化OpenAI用戶端,配置阿里雲DashScope服務
client = OpenAI(
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),  # 從環境變數讀取API密鑰
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)

# 定義可用工具列表
tools = [
    # 工具1 擷取當前時刻的時間
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "當你想知道現在的時間時非常有用。",
            "parameters": {}  # 無需參數
        }
    },
    # 工具2 擷取指定城市的天氣
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。"
                    }
                },
                "required": ["location"]  # 必填參數
            }
        }
    }
]

messages = [{"role": "user", "content": input("請輸入問題:")}]

# 多模態模型的 message樣本
# messages = [{
#     "role": "user",
#     "content": [
#              {"type": "image_url","image_url": {"url": "https://img.alicdn.com/imgextra/i4/O1CN014CJhzi20NOzo7atOC_!!6000000006837-2-tps-2048-1365.png"}},
#              {"type": "text", "text": "根據映像上的地點,請問該地點當前的天氣"}]
#     }]

completion = client.chat.completions.create(
    # 此處以qwen3.8-max為例,可更換為其它深度思考模型
    model="qwen3.8-max",
    messages=messages,
    extra_body={
        # 開啟深度思考,該參數對qwen3-30b-a3b-thinking-2507、qwen3-235b-a22b-thinking-2507、QwQ 模型無效
        "enable_thinking": True
    },
    tools=tools,
    parallel_tool_calls=True,
    stream=True,
    # 解除注釋後,可以擷取到token消耗資訊
    # stream_options={
    #     "include_usage": True
    # }
)

reasoning_content = ""  # 定義完整思考過程
answer_content = ""     # 定義完整回複
tool_info = []          # 儲存工具調用資訊
is_answering = False   # 判斷是否結束思考過程並開始回複
print("="*20+"思考過程"+"="*20)
for chunk in completion:
    if not chunk.choices:
        # 處理用量統計資訊
        print("\n"+"="*20+"Usage"+"="*20)
        print(chunk.usage)
    else:
        delta = chunk.choices[0].delta
        # 處理AI的思考過程(鏈式推理)
        if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
            reasoning_content += delta.reasoning_content
            print(delta.reasoning_content,end="",flush=True)  # 即時輸出思考過程

        # 處理最終回複內容
        else:
            if not is_answering:  # 首次進入回複階段時列印標題
                is_answering = True
                print("\n"+"="*20+"回複內容"+"="*20)
            if delta.content is not None:
                answer_content += delta.content
                print(delta.content,end="",flush=True)  # 流式輸出回複內容

            # 處理工具調用資訊(支援並行工具調用)
            if delta.tool_calls is not None:
                for tool_call in delta.tool_calls:
                    index = tool_call.index  # 工具調用索引,用於並行調用

                    # 動態擴充工具資訊儲存列表
                    while len(tool_info) <= index:
                        tool_info.append({})

                    # 收集工具調用ID(用於後續函數調用)
                    if tool_call.id:
                        tool_info[index]['id'] = tool_info[index].get('id', '') + tool_call.id

                    # 收集合函式名稱(用於後續路由到具體函數)
                    if tool_call.function and tool_call.function.name:
                        tool_info[index]['name'] = tool_info[index].get('name', '') + tool_call.function.name

                    # 收集合函式參數(JSON字串格式,需要後續解析)
                    if tool_call.function and tool_call.function.arguments:
                        tool_info[index]['arguments'] = tool_info[index].get('arguments', '') + tool_call.function.arguments

print(f"\n"+"="*19+"工具調用資訊"+"="*19)
if not tool_info:
    print("沒有工具調用")
else:
    print(tool_info)

返回結果

輸入“四個直轄市的天氣”,得到以下返回結果:
====================思考過程====================
好的,使用者問的是“四個直轄市的天氣”。首先,我需要明確四個直轄市是哪幾個。根據中國的行政區劃,直轄市包括北京、上海、天津和重慶。所以使用者想知道這四個城市的天氣情況。

接下來,我需要檢查可用的工具。提供的工具中有get_current_weather函數,參數是location,類型字串。每個城市需要單獨查詢,因為函數一次只能查一個地點。因此,我需要為每個直轄市調用一次這個函數。

然後,我需要考慮如何產生正確的工具調用。每個調用應該包含城市名稱作為參數。比如,第一個調用是北京,第二個是上海,依此類推。確保參數名稱是location,值是正確的城市名。

另外,使用者可能希望得到每個城市的天氣資訊,所以需要確保每個函數調用都正確無誤。可能需要連續調用四次,每次對應一個城市。不過,根據工具的使用規則,可能需要分多次處理,或者一次產生多個調用。但根據樣本,可能每次只調用一個函數,所以可能需要逐步進行。

最後,確認是否有其他需要考慮的因素,比如參數是否正確,城市名稱是否準確,以及是否需要處理可能的錯誤情況,比如城市不存在或API不可用。但目前看來,四個直轄市都是明確的,應該沒問題。
====================回複內容====================

===================工具調用資訊===================
[{'id': 'call_767af2834c12488a8fe6e3', 'name': 'get_current_weather', 'arguments': '{"location": "北京市"}'}, {'id': 'call_2cb05a349c89437a947ada', 'name': 'get_current_weather', 'arguments': '{"location": "上海市"}'}, {'id': 'call_988dd180b2ca4b0a864ea7', 'name': 'get_current_weather', 'arguments': '{"location": "天津市"}'}, {'id': 'call_4e98c57ea96a40dba26d12', 'name': 'get_current_weather', 'arguments': '{"location": "重慶市"}'}]

應用於生產環境

測試載入器調用準確率

  • 建立評估體系 構建貼近真實業務的測試資料集,定義清晰的評估指標,如工具選擇準確率、參數提取準確率、端到端成功率等。
  • 最佳化提示詞 根據測試暴露的具體問題(選錯工具、參數錯誤),針對性最佳化系統提示詞、工具描述和參數描述。
  • 升級模型 提示詞調優無法提升效能時,升級到更強的模型版本(如 qwen3.6-plus)是最直接有效方法。

動態控制工具數量

應用整合的工具數量達到幾十甚至上百個時,將全部工具提供給模型會帶來以下問題:
  • 效能下降:模型在龐大的工具集中選擇正確工具的難度劇增;
  • 成本與延遲:大量的工具描述會消耗巨量的輸入 Token,導致費用上升和響應變慢;
解決方案:在調用模型前增加工具路由/檢索層,根據使用者查詢從工具庫中篩選出小而相關的工具子集,再提供給模型。 實現工具路由的幾種主流方法:
  • 語義檢索 將工具描述資訊(description)通過 Embedding 模型轉化為向量,並存入向量資料庫。當使用者查詢時,將查詢向量通過向量相似性搜尋,召回最相關的 Top-K 個工具。
  • 混合檢索 將語義檢索的“模糊比對”能力與傳統關鍵詞或中繼資料標籤的“精確匹配”能力相結合。為工具添加 tags 或 keywords 欄位,檢索時同時進行向量搜尋和關鍵詞過濾,可以大幅提升高頻或特定情境下的召回精準度。
  • 輕量級 LLM 路由器 對於更複雜的路由邏輯,可以使用一個更小、更快、更便宜的模型(如 Qwen-Flash)作為前置“路由模型”。它的任務是根據使用者問題輸出相關的工具名稱列表。
實踐建議
  • 保持候選集精簡:無論使用何種方法,最終提供給主模型的工具數量建議不超過 20 個。這是在模型認知負荷、成本、延遲和準確率之間的最佳平衡點。
  • 分層過濾策略:可以構建一個漏鬥式的路由策略。例如,先用成本極低的關鍵詞/規則匹配進行第一輪篩選,過濾掉明顯不相關的工具,再對剩餘的工具進行語義檢索,從而提高效率和品質。

工具安全性原則

向大模型開放工具執行能力時,安全是首要考量。核心原則:“最小許可權”和“人類確認”。
  • 最小許可權原則:為模型提供的工具集應嚴格遵守最小許可權原則。預設情況下,工具應是唯讀(如查詢天氣、搜尋文檔),避免直接提供任何涉及狀態變更或資源操作的“寫”許可權。
  • 危險工具隔離:請勿向大模型直接提供危險工具,例如執行任意代碼(code interpreter)、操作檔案系統(fs.delete)、執行資料庫刪除或更新操作(db.drop_table)或涉及資金流轉的工具(payment.transfer)。
  • 人類參與:對於所有高許可權或無法復原的操作,必須引入人工審核和確認環節。模型可以產生操作請求,但最終的執行“按鈕”必須由人類使用者點擊。例如,模型可以準備好一封郵件,但發送操作需要使用者確認。

使用者體驗最佳化

Function Calling 鏈路較長,任何環節出問題都可能影響使用者體驗。

處理工具運行失敗

工具運行失敗是常見情況,可採取以下策略:
  • 最大重試次數:設定合理的重試上限(例如 3 次),避免因連續失敗導致使用者長時間等待或系統資源浪費。
  • 提供兜底話術:當重試耗盡或遇到無法解決的錯誤時,應向使用者返回清晰、友好的提示資訊,例如:“抱歉,我暫時無法查詢到相關資訊,可能是服務有些繁忙,請您稍後再試。”

應對處理延遲

較高延遲會降低使用者滿意度,需從前端互動和後端兩方面最佳化。
  • 設定逾時時間:為 Function Calling 的每一步設定獨立且合理的逾時時間。一旦逾時,應立即中斷操作並給出反饋。
  • 提供即時反饋:開始執行 Function Calling 時,建議在介面上給出提示,如“正在為您查詢天氣...”、“正在搜尋相關資訊...”,向使用者即時反饋處理進度。

計費說明

除 messages 數組中的 Token 外,工具描述資訊也作為輸入 Token 計費。
推薦您參考如何使用部分,通過 tools 參數向大模型傳入工具資訊。如果您需要通過 System Message 傳入工具資訊,為了模型的最佳效果,請參考以下代碼中的提示詞模板:
  • OpenAI相容
  • DashScope
  • Python
  • Node.js

範例程式碼

import os
from openai import OpenAI
import json

client = OpenAI(
    # 各地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用百鍊API Key將下行替換為:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    # 以下為新加坡地區URL,調用時請將 {WorkspaceId} 替換為真實的業務空間ID,各地區的URL不同。
    base_url="https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
)

# 自訂 System prompt,可根據您的需求修改
custom_prompt = "你是一個智能助手,專門負責調用各種工具來協助使用者解決問題。你可以根據使用者的需求選擇合適的工具並正確調用它們。"

tools = [
    # 工具1 擷取當前時刻的時間
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "當你想知道現在的時間時非常有用。",
            "parameters": {}
        }
    },
    # 工具2 擷取指定城市的天氣
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "當你想查詢指定城市的天氣時非常有用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市或縣區,比如北京市、杭州市、餘杭區等。"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

# 遍曆tools列表,為每個工具構建描述
tools_descriptions = []
for tool in tools:
    tool_json = json.dumps(tool, ensure_ascii=False)
    tools_descriptions.append(tool_json)

# 將所有工具描述組合成一個字串
tools_content = "\n".join(tools_descriptions)

system_prompt = f"""{custom_prompt}

# Tools

You may call one or more functions to assist with the user query.

You are provided with function signatures within <tools></tools> XML tags:
<tools>
{tools_content}
</tools>

For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{{"name": <function-name>, "arguments": <args-json-object>}}
</tool_call>"""

messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": "幾點了"}
]

completion = client.chat.completions.create(
    model="qwen3.8-max",
    extra_body={"enable_thinking": False},
    messages=messages,
)
print(completion.model_dump_json())
運行以上代碼後,可以使用 XML 解析器提取 <tool_call></tool_call> 之間的工具調用資訊,包括函數名和入參。

錯誤碼

如果模型調用失敗並返回報錯資訊,請參見錯誤碼進行解決。
Token Plan
模型體驗
用量統計與效能監控
資產中心
服務支援