---
title: "函数工具"
description: "为您的 AI 智能体提供可在对话过程中调用外部 API 的函数工具——获取客户数据、预约、更新记录——并使用类型化参数。"
---

函数工具允许您的 AI 智能体在通话期间调用外部 API。您可以使用它们查询客户数据、检查可用性、预约服务，或执行后端支持的任何操作。

## 工作原理

1. 使用架构定义工具（工具接受哪些参数）
2. 提供 `endpoint` 配置（ThunderPhone 在何处调用您的 API）——或者省略该配置，以便通过组织 webhook 接收工具调用
3. 在通话期间，AI 会根据对话决定何时使用工具
4. ThunderPhone 使用工具参数调用您的端点
5. 您的 API 响应会返回给 AI，以继续对话

| 功能 | 运行位置 | 设置 |
| --- | --- | --- |
| [内置工具](/zh/guides/built-in-tools) | ThunderPhone | 提示词指令；部分工具还需要智能体设置 |
| [应用连接](/zh/guides/connect-apps) | ThunderPhone 和已连接的提供商 | 连接账户并附加已批准的操作 |
| [API 连接](/zh/guides/api-connections)和函数工具 | 您的 HTTP API | 定义端点和架构，或通过 webhook 接收函数调用 |
| [MCP 服务器](/zh/guides/mcp-servers) | 远程 MCP 服务器 | 添加服务器、发现其工具并将其附加到智能体 |

---

## 工具架构

每个工具都遵循以下结构：

```json
{
  "type": "function",
  "function": {
    "name": "search_appointments",
    "description": "Find available appointment slots for a given date",
    "parameters": {
      "type": "object",
      "properties": {
        "date": {
          "type": "string",
          "description": "Date in YYYY-MM-DD format"
        },
        "service": {
          "type": "string",
          "description": "Type of service (e.g., 'consultation', 'follow-up')"
        }
      },
      "required": ["date"]
    }
  },
  "endpoint": {
    "url": "https://api.example.com/appointments/search",
    "method": "POST",
    "headers": {
      "X-Api-Key": "your-api-key"
    }
  },
  "timeout": 120
}
```

### 工具配置

| 字段 | 类型 | 必填 | 描述 |
|-------|------|----------|-------------|
| `timeout` | number | 否 | 最大执行时间，单位为秒（默认值：`20`，最大值：`180`） |

### 函数定义

| 字段 | 类型 | 必填 | 描述 |
|-------|------|----------|-------------|
| `name` | string | 是 | 工具的唯一标识符 |
| `description` | string | 是 | 向 AI 说明何时使用此工具 |
| `parameters` | object | 是 | 工具参数的 JSON Schema |

### 端点配置

| 字段 | 类型 | 必填 | 描述 |
|-------|------|----------|-------------|
| `url` | string | 是 | 您的 API 端点 URL |
| `method` | string | 否 | HTTP 方法（默认值：`POST`） |
| `headers` | object | 否 | 要包含的自定义请求头 |

<Note>
  `endpoint` 配置**不会**发送给 AI 模型——它仅供 ThunderPhone 用于执行工具调用。
</Note>

---

## 两种调用路径

您的服务器接收哪种请求取决于工具是否具有
`endpoint`：

| | **包含** `endpoint` 的工具 | **不包含** `endpoint` 的工具 |
|---|---|---|
| 请求发送至 | 直接发送至 `endpoint.url` | 您的组织的[旧版 Webhook URL](/api-reference/organizations#legacy-single-url-webhook) |
| 请求正文 | 所选请求格式（默认仅包含参数） | `telephony.tool` / `web.tool` 封装 |
| 请求头 | 您的 `endpoint.headers` + `X-ThunderPhone-Call-ID` + `X-ThunderPhone-Signature-V2` + `X-ThunderPhone-Signature` | `Content-Type` + `X-ThunderPhone-Signature-V2` + `X-ThunderPhone-Signature` |
| 签名密钥 | 组织 Webhook 密钥 | 组织 Webhook 密钥 |

两种路径都是**阻塞式**的——AI 会在句子中途等待
结果。默认超时为 **20 秒**；您可以设置工具顶层的
`timeout` 以允许更长的执行时间，最长可达平台规定的 **180 秒**
上限。请保持处理程序快速响应。您可以混合使用：
对于组织具有 webhook URL 的通话，具有 `endpoint` 的工具会被
直接调用，其余工具则回退至 webhook。

## 直接端点调用

当 AI 调用具有 `endpoint` 的工具时，ThunderPhone 会向您的 URL 发送
请求：

### 请求头

```http
POST /appointments/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=5f2c...
X-ThunderPhone-Signature: abc123...
X-ThunderPhone-Call-ID: 987654321
X-Api-Key: your-api-key
```

您在 `endpoint.headers` 中配置的自定义请求头始终会原样包含，此外还会包含以下以 ThunderPhone 命名空间开头的请求头：

- `X-ThunderPhone-Signature-V2` —— 基于方法、路径、查询参数和请求体计算的带时间戳 HMAC-SHA256，使用您的 **组织 Webhook 密钥** 作为密钥。请验证此签名并拒绝超过 5 分钟的请求；请参阅[验证 Webhook 签名](/zh/guides/verify-webhook-signatures)
- `X-ThunderPhone-Signature` —— 对完整请求体字节进行的旧版 HMAC-SHA256，使用相同密钥，仍会发送以兼容现有验证器
- `X-ThunderPhone-Call-ID` —— 当前通话 ID

除非您的 `endpoint.headers` 对其进行了覆盖，否则会设置
`Content-Type: application/json` —— 自定义 `Content-Type` 优先。

<Warning>
  签名使用来自
  [`GET /v1/webhook`](/api-reference/organizations#legacy-single-url-webhook) 的组织级网络钩子密钥作为密钥。
  如果您的组织从未配置过旧版网络钩子，则不会有
  密钥，工具调用将**仅**携带 `X-ThunderPhone-Call-ID` —— 对缺少签名直接失败的
  处理程序会拒绝这些调用。
  您可以配置旧版网络钩子以获取密钥，或者将您自己的
  共享密钥放入 `endpoint.headers`。
</Warning>

### 请求格式

每个 API 连接都有 `request_format` 设置。当导入的工具需要继续调用为该平台构建的端点时，请选择 `vapi` 或 `retell`。新的和现有的连接默认使用 `standard`。

对于 `POST` / `PUT` / `PATCH`，ThunderPhone 会以规范方式序列化所选封装格式（键已排序、分隔符紧凑）。两种签名都覆盖这些精确字节。

#### 标准

`standard` 仅发送工具参数，不含任何包装。这是默认设置，并会逐字节保留 ThunderPhone 的原始请求正文：

```json
{"date":"2025-01-02","service":"consultation"}
```

#### Vapi

`vapi` 会按照 Vapi 文档中定义的格式发送 Vapi 的 [`tool-calls` 服务器消息](https://docs.vapi.ai/tools/custom-tools)。`call.id` 是 ThunderPhone 通话 ID。工具调用 `id` 对每次调用都是唯一的，并会重复用于匹配端点的结果。该调用同时出现在 `toolCallList` 和 `toolWithToolCallList` 中，其中 `function.arguments` 为 JSON 对象：

```json
{
  "message": {
    "type": "tool-calls",
    "timestamp": 1725000000123,
    "call": { "id": "987654321" },
    "toolCallList": [
      {
        "id": "call_00112233445566778899aabbccddeeff",
        "type": "function",
        "function": {
          "name": "search_appointments",
          "arguments": { "date": "2025-01-02", "service": "consultation" }
        }
      }
    ],
    "toolWithToolCallList": [
      {
        "type": "function",
        "function": { "name": "search_appointments" },
        "toolCall": {
          "id": "call_00112233445566778899aabbccddeeff",
          "type": "function",
          "function": {
            "name": "search_appointments",
            "arguments": { "date": "2025-01-02", "service": "consultation" }
          }
        }
      }
    ]
  }
}
```

不会发送 `assistant`、`artifact` 等仅适用于 Vapi 的字段，以及完整的 `call` 对象。

返回包含匹配工具调用 ID 的 Vapi 响应封装：

```json
{
  "results": [
    {
      "toolCallId": "call_00112233445566778899aabbccddeeff",
      "result": "{\"available_slots\":[\"9:00 AM\",\"2:00 PM\"]}"
    }
  ]
}
```

ThunderPhone 会将未封装的 `result` 字符串提供给 AI。为兼容已返回 JSON 值的端点，它也接受 JSON 值。如果以 `error` 替代 `result`，该错误会作为工具错误返回给 AI。如果端点返回格式错误或不匹配的 Vapi 封装，ThunderPhone 会记录警告并回退到其标准响应解析，而不会导致通话失败。

#### Retell

`retell` 会发送 [Retell 自定义函数](https://docs.retellai.com/build/single-multi-prompt/custom-function)文档中定义的请求：

```json
{
  "name": "search_appointments",
  "call": { "call_id": "987654321" },
  "args": { "date": "2025-01-02", "service": "consultation" }
}
```

`call` 对象仅包含 `call_id`。ThunderPhone 不会包含在分派时无法如实提供的 Retell 专有字段，例如 `agent_id`、`transcript`、`transcript_object`、`metadata` 或动态变量。Retell 接受任何响应正文；ThunderPhone 会使用标准响应处理将其透传。

对于 `GET` / `DELETE`，无论 `request_format` 为何，参数都会作为**查询参数**发送，且正文为空。V2 签名覆盖实际发送的查询字符串；旧版签名基于空字节字符串计算。请参阅[验证 Webhook 签名](/zh/guides/verify-webhook-signatures)。

### 响应

返回包含工具结果的 JSON 响应：

```json
{
  "available_slots": ["9:00 AM", "2:00 PM", "4:30 PM"],
  "timezone": "America/Los_Angeles"
}
```

响应会被格式化并提供给 AI 以继续
对话。非 JSON 响应会包装为 `{"data": "<text>"}`；
超时和连接失败会作为错误报告给 AI，因此
智能体可以致歉并继续处理，而不会停滞。

## 网络钩子模式分发

**不具有** `endpoint` 的工具会作为已签名的 `telephony.tool`（电话调用）或 `web.tool`
（网络调用）请求，分发到您组织的旧版
网络钩子 URL。与执行后发送到网络钩子端点的[审计通知](/zh/webhooks/events)
不同，此请求**就是**
执行本身 —— 您的 HTTP 响应即为工具结果。

```json
{
  "type": "telephony.tool",
  "data": {
    "call_id": 987654321,
    "tool_name": "search_appointments",
    "arguments": { "date": "2026-04-21" },
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  }
}
```

`web.tool` 使用 `origin_domain`，而非 `from_number` /
`to_number`。以 JSON 格式返回工具结果——与直接端点调用使用相同的响应
契约。该请求使用组织的 webhook 密钥签名，并像其他所有 webhook 一样包含两个签名标头。

<Note>
  已订阅的[网络钩子端点](/zh/webhooks/endpoints)还会在每个工具执行后
  收到一个非阻塞的 `telephony.tool` / `web.tool` **通知
  （无论通过哪种路径执行）**，其中包含工具的响应 —— 适用于
  审计追踪。请参阅
  [事件目录](/zh/webhooks/events)。
</Note>

---

## 签名验证

直接工具调用的签名方式与 Webhook 相同，使用您组织的 Webhook 密钥：

- `X-ThunderPhone-Signature-V2` 对发送时的时间戳、方法、路径和查询参数，以及精确的请求正文字节进行签名。拒绝时间戳与您的时钟相差超过 5 分钟的请求。
- `X-ThunderPhone-Signature`（旧版）仅对请求正文字节进行签名；对于 `GET` / `DELETE`，签名内容为空字节字符串。该标头仍会照常发送，未作更改。

SDK 辅助函数会校验 V2 标头，包括 5 分钟时间窗口：

<CodeGroup>
```python Python
import json

from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature

app = FastAPI()

@app.post("/appointments/search")
async def search_appointments(request: Request):
    body = await request.body()
    query = request.scope.get("query_string", b"").decode("latin-1")
    path = request.url.path + (f"?{query}" if query else "")

    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=WEBHOOK_SECRET,
        method=request.method,
        path=path,
        body=body,
    ):
        raise HTTPException(status_code=401)

    data = json.loads(body)
    date = data["date"]

    # Look up availability
    slots = await get_available_slots(date)

    return {"available_slots": slots}
```

```javascript Node.js
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

app.post('/appointments/search', express.raw({type: 'application/json'}), (req, res) => {
  const ok = verifySignature({
    header: req.header('X-ThunderPhone-Signature-V2'),
    secret: WEBHOOK_SECRET,
    method: req.method,
    path: req.originalUrl,
    body: req.body,
  });
  if (!ok) return res.status(401).send('Invalid signature');

  const { date, service } = JSON.parse(req.body);

  // Look up availability
  const slots = getAvailableSlots(date, service);

  res.json({ available_slots: slots });
});
```
</CodeGroup>

完整示例——签名字符串、完整示例、无需 SDK 的代码、`GET` / `DELETE` 情况以及未设置密钥时的注意事项——请参阅[验证 Webhook 签名](/zh/guides/verify-webhook-signatures)。

---

## 示例：完整预约流程

以下是一组用于完整预约系统的工具：

```json
{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_appointments",
        "description": "Find available appointment slots",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "service": { "type": "string" }
          },
          "required": ["date"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/search",
        "method": "POST",
        "headers": { "X-Api-Key": "key" }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "book_appointment",
        "description": "Book an appointment at a specific time",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "time": { "type": "string", "description": "HH:MM format" },
            "customer_name": { "type": "string" },
            "customer_phone": { "type": "string" }
          },
          "required": ["date", "time", "customer_name"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/book",
        "method": "POST",
        "headers": { "X-Api-Key": "key" }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "cancel_appointment",
        "description": "Cancel an existing appointment",
        "parameters": {
          "type": "object",
          "properties": {
            "confirmation_number": { "type": "string" }
          },
          "required": ["confirmation_number"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/cancel",
        "method": "POST",
        "headers": { "X-Api-Key": "key" }
      }
    }
  ]
}
```

---

## 最佳实践

<AccordionGroup>
  <Accordion title="编写清晰的描述">
    `description` 字段可帮助 AI 理解**何时**使用该工具。请明确说明它的作用及适用场景。
  </Accordion>

  <Accordion title="妥善处理错误">
    返回 AI 能够理解的错误消息：使用 `{"error": "No slots available for that date"}`，而非通用的 500 错误。
  </Accordion>

  <Accordion title="保持响应简洁">
    仅返回 AI 继续对话所需的信息。较大的负载会降低响应速度。
  </Accordion>

  <Accordion title="谨慎使用必填字段">
    仅在确有必要时将字段标记为 `required`。AI 会在调用工具前向用户询问必填信息。
  </Accordion>
</AccordionGroup>

---

## 相关内容

<CardGroup cols={2}>
  <Card title="内置工具" icon="wrench" href="/zh/guides/built-in-tools">
    无需定义端点，即可提示由平台管理的通话操作。
  </Card>
  <Card title="应用连接" icon="plug" href="/zh/guides/connect-apps">
    适用于 HubSpot、Salesforce、Slack、Google
    Calendar、Google Sheets 和 Cal.com 的平台管理工具——无需端点。
  </Card>
  <Card title="MCP 服务器" icon="server" href="/zh/guides/mcp-servers">
    连接 MCP 服务器，让智能体调用其工具。
  </Card>
  <Card title="API 连接" icon="code" href="/zh/guides/api-connections">
    可重复使用的 REST 集成，可附加到智能体。
  </Card>
  <Card title="验证 webhook 签名" icon="shield-check" href="/zh/guides/verify-webhook-signatures">
    用于 webhook 和工具调用的统一验证帮助程序。
  </Card>
</CardGroup>
