---
url: https://docs.oakxgen.ai/zh-Hant-TW/guide/agents/api-conversation.md
description: >-
  自家網站或 App 要直接跟 Skill agent 對話、做出打字機式的逐字回覆，用對話 API。這篇說明認證方式、請求格式、串流事件，以及跟通用
  Webhook 怎麼選。
---

對話 API 是**一問一答的同步 API**：送一段對話過去，直接在回應裡拿到 AI 的回覆；
加上 `stream: true` 就會以 Server-Sent Events 一個字一個字吐回來。只給 **Skill agent** 用。

## 跟通用 Webhook 怎麼選

| | 對話 API | [通用 Webhook](./webhook-integration.md) |
|---|---|---|
| 回覆方式 | 同一個請求直接回（可逐字串流） | 先回 `200`，回覆稍後 POST 到你的 callback |
| 對話歷史 | 你每次帶完整對話 | 你每次帶完整對話 |
| 適合 | 網站聊天視窗、App 內即時對話 | 客服系統、訊息平台這類非同步的收發 |
| Agent 類型 | Skill | Skill、Channel（含表單、轉真人、主動追問） |

用 Channel agent 的金鑰打對話 API 會回 `400`：

```json
{"error": "客服 AI（Channel agent）不支援對話 API，請改用通用 Webhook 串接"}
```

Channel agent 的表單、情境、轉真人都要跨好幾則訊息保存狀態，一問一答的 API 撐不起來。

## 認證

到 Skill agent 的**串接**分頁，**對話 API** 卡片上有**API 金鑰**，每個 agent 一把。
放在 header：

```
Authorization: Bearer <API 金鑰>
```

**API 金鑰就是憑證**，拿到的人可以用這個 agent 對話、消耗你的用量，所以：

* **只放在你的後端**，不要寫進網頁或 App 的前端程式碼裡——任何人打開瀏覽器開發者工具都看得到。
  網站聊天視窗請由你的後端轉呼叫。
* 外流了按**重新產生金鑰**，舊的立刻失效。

沒帶或金鑰錯誤會回 `401`：

```json
{"detail": "UNAUTHORIZED", "status_code": 401}
```

## 請求

```http
POST https://api.oakxgen.ai/agent/api/v2/conversation/
Authorization: Bearer <API 金鑰>
Content-Type: application/json

{
  "prompt": [
    {"role": "user", "content": "你們有賣帆布包嗎？"},
    {"role": "assistant", "content": "有的，目前有經典款與大容量款。"},
    {"role": "user", "content": "大容量款多少錢？"}
  ],
  "stream": false
}
```

| 欄位 | 必填 | 說明 |
|---|---|---|
| `prompt` | ✅ | 完整對話，照時間順序。`role` 是 `user`（客人）或 `assistant`（AI），**最後一則必須是 `user`**，也就是這次要 AI 回答的那句。 |
| `stream` | | `true` 逐字串流，預設 `false`。 |
| `webhook_url` | | 給了就改成非同步：立刻回 `200`，回覆 POST 到這個網址。同時帶 `stream: true` 的話以串流為準，`webhook_url` 會被忽略。 |
| `webhook_secret` | | 搭配 `webhook_url`，我們用它簽回呼的內容，見下方「非同步回呼」。 |
| `json_schema` | | 給了就改成回結構化 JSON，見下方「結構化輸出」。 |
| `prompt_vars` | | 代換 agent 提示詞裡的 `{變數名}`，見下方「提示詞變數」。 |
| `allowed_tools` | | 限制 AI 能用的工具，`[]` 代表完全不用工具（不查知識庫、不打 API）。不給就是 agent 設定的全部工具。 |

OakXgen **不保存對話內容**，每次都要帶完整的 `prompt`。要控制成本的話，可以只帶最近幾輪。

最後一則不是 `user` 會回 `400`：

```json
{"error": "prompt 最後一則必須是 user 訊息"}
```

## 同步回應（`stream: false`）

```json
{
  "stream": false,
  "messages": [
    {"type": "text", "text": "大容量款是 NT$ 1,290。"},
    {"type": "card_carousel", "cards": [{"title": "大容量帆布包", "description": "NT$ 1,290", "image_url": "https://..."}]},
    {"type": "quick_replies", "replies": [{"label": "加入購物車", "data": "加入購物車"}]}
  ],
  "references": [],
  "usage": {"input_tokens": 1834, "output_tokens": 52}
}
```

`messages` 是一串區塊，照順序顯示：`text` 文字、`card_carousel` 卡片、`quick_replies` 快速回覆。
`references` 只有 agent 開了**顯示參考資料**時才會出現，列出 AI 引用了知識庫的哪些段落。
`usage` 是這次請求消耗的 token（含 AI 查工具時的多次呼叫），也會記進後台的 **AI 用量**頁面。

## 結構化輸出（`json_schema`）

要拿 AI 的判斷結果去跑自己的程式（意圖分類、欄位抽取、從清單裡挑項目），帶一個
[JSON Schema](https://json-schema.org/)，回應會多一個 `parsed`，保證符合這個格式：

```json
{
  "prompt": [{"role": "user", "content": "明天台北空氣好嗎"}],
  "allowed_tools": [],
  "json_schema": {
    "title": "intention",
    "type": "object",
    "properties": {"intention": {"type": "string"}},
    "required": ["intention"]
  }
}
```

```json
{
  "messages": [{"type": "text", "text": "{\"intention\": \"空氣品質查詢\"}"}],
  "parsed": {"intention": "空氣品質查詢"},
  "usage": {"input_tokens": 229, "output_tokens": 19}
}
```

* agent 的提示詞與模型照用，只問一次、**不會用工具**，通常搭配 `allowed_tools: []`。
* Python 用 pydantic 的話，直接傳 `Model.model_json_schema()`。
* 只支援 OpenAI、Anthropic 引擎的 agent；其他引擎，或同時帶 `stream: true`，會回 `400`。
* 模型回的不是合法 JSON 時回 `502`（極少見）。

## 提示詞變數（`prompt_vars`）

agent 的提示詞可以留 `{變數名}`，每次呼叫時再填：

```json
{
  "prompt": [{"role": "user", "content": "生小孩有補助嗎"}],
  "prompt_vars": {"chatbot_information": "[{\"id\": 1, \"template_name\": \"育兒津貼說明\"}]"}
}
```

只換 `prompt_vars` 裡有的名稱，其他花括號（例如提示詞裡寫的 JSON 範例）原樣保留。
值不是字串的話會先轉成 JSON 文字。`prompt_vars` 與 `json_schema` 不會出現在回應裡。

## 逐字串流（`stream: true`）

回應的 `Content-Type` 是 `text/event-stream`，每個事件是一行 `data: <JSON>`，事件之間空一行，
最後一行是 `data: [DONE]`：

```
data: {"type": "thinking"}

data: {"type": "tool-input-available", "toolCallId": "call_1", "toolName": "search_knowledge", "label": "查詢知識庫", "input": {...}}

data: {"type": "tool-output-available", "toolCallId": "call_1"}

data: {"type": "text-start", "id": "t1"}

data: {"type": "text-delta", "id": "t1", "delta": "大容量款"}

data: {"type": "text-delta", "id": "t1", "delta": "是 NT$ 1,290。"}

data: {"type": "text-end", "id": "t1"}

data: {"type": "complete", "session_id": null, "session_name": null, "references": []}

data: {"type": "usage", "input_tokens": 1834, "output_tokens": 52}

data: [DONE]
```

| `type` | 意思 |
|---|---|
| `thinking` | AI 開始處理，可以顯示「思考中」。 |
| `tool-input-available` / `tool-output-available` | AI 正在用工具（查知識庫、打 API…），`label` 是給人看的說明。 |
| `text-start` / `text-delta` / `text-end` | 文字回覆。把 `delta` 依序接起來就是完整回覆。 |
| `rich-content` | 卡片或快速回覆，欄位是 `cards` 與 `quick_replies`，格式同上面的同步回應。 |
| `complete` | 回覆結束，`references` 是引用的知識庫段落。 |
| `usage` | 這次請求消耗的 token，緊接在 `[DONE]` 之前。 |
| `error` | 處理失敗，`message` 是原因。**這之後連線直接結束，不會有 `[DONE]`**，讀取迴圈要能處理串流自然結束。 |

**瀏覽器內建的 `EventSource` 只能發 GET、也不能帶 `Authorization` header**，所以用不了。
請在後端用 `fetch` 或任何支援串流讀取的 HTTP 客戶端，逐行讀 `data:`。

Node.js：

```js
const res = await fetch('https://api.oakxgen.ai/agent/api/v2/conversation/', {
  method: 'POST',
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ prompt, stream: true })
})

const decoder = new TextDecoder()
let buffer = ''
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true })
  const events = buffer.split('\n\n')
  buffer = events.pop()
  for (const event of events) {
    const data = event.replace(/^data: /, '')
    if (data === '[DONE]') break
    const payload = JSON.parse(data)
    if (payload.type === 'text-delta') process.stdout.write(payload.delta)
  }
}
```

Python：

```python
import json, requests

with requests.post(
    "https://api.oakxgen.ai/agent/api/v2/conversation/",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"prompt": prompt, "stream": True},
    stream=True,
) as res:
    for line in res.iter_lines(decode_unicode=True):
        if not line.startswith("data: "):
            continue
        data = line[len("data: "):]
        if data == "[DONE]":
            break
        payload = json.loads(data)
        if payload["type"] == "text-delta":
            print(payload["delta"], end="", flush=True)
```

中間如果經過你自己的反向代理（nginx 等），記得關掉 response buffering，不然會整段收完才一次吐出來。

## 非同步回呼（`webhook_url`）

帶了 `webhook_url` 會立刻回 `{"status": "ok"}`，AI 回覆好之後 POST 同步回應那一包 JSON 到
`webhook_url`。**送一次、不重試**。

有帶 `webhook_secret` 的話，回呼會帶簽章，算法跟[通用 Webhook 的簽章](./webhook-integration.md#簽章)相同：

```
X-OakXgen-Timestamp: <unix 秒數>
X-OakXgen-Signature: base64(HMAC-SHA256(webhook_secret, "<timestamp>." + raw body))
```

請拿收到的 raw body 驗簽，並拒收時間差超過 5 分鐘的回呼。舊的 `X-Webhook-Secret` header
（直接放 `webhook_secret` 明文）目前還會帶，但只是為了相容；它擋不了內容被竄改或重送，新接的請改驗簽章。

`webhook_url` 必須是對外公開的 **`https://`** 網址，`http://`、內網 IP、`localhost`、雲端 metadata 位址會回 `400`，
錯誤訊息以「webhook\_url 不允許」開頭。這個檢查在 AI 開始回覆之前就做，網址不合格不會消耗額度。

需要真人接手、主動追問這些功能的話，請改用[通用 Webhook](./webhook-integration.md)。

## Agent 被關閉時

agent 被關閉時回 `200` 與空物件 `{}`，不會有回覆。
