0

Function Calling: So sánh Responses API và Chat Completions

Bài viết này được dịch và biên tập từ bài gốc tiếng Anh trên blog của Modelflare.

So sánh ở mức wire format giữa định nghĩa function, Call ID, message kết quả, streaming argument, phân quyền (authorization), tính idempotent và khả năng tương thích của route.

Cả Responses API lẫn Chat Completions đều có thể yêu cầu ứng dụng chạy một function, nhưng mỗi bên biểu diễn tool loop theo cách khác nhau. Chat Completions đặt định nghĩa tool trong tools[].function, trả về message.tool_calls và nhận kết quả dưới dạng message có role: "tool". Responses API dùng định nghĩa function dạng phẳng (flat), trả về các output item có kiểu function_call và nhận các item function_call_output được liên kết bằng call_id.

Model không tự chạy function của ứng dụng. Chính code của bạn phải validate các argument được đề xuất, phân quyền cho thao tác, thực thi đúng như ý định, trả kết quả về cho model và ngăn side effect bị lặp lại khi request được retry.

Tool loop gồm bốn bước

JSON của từng endpoint khác nhau, nhưng state machine của ứng dụng thì giống nhau:

1. Khai báo function được phép gọi và schema cho argument của nó
2. Nhận một hoặc nhiều lời gọi function do model đề xuất
3. Validate, phân quyền và thực thi từng lời gọi trong code ứng dụng
4. Trả về từng kết quả kèm correlation ID của lời gọi đó

Chỉ sau bước bốn, model mới có thể đưa ra câu trả lời dựa trên kết quả của tool. Nếu model yêu cầu thêm tool khác, vòng lặp lặp lại với một Call ID mới.

Hướng dẫn Function Calling của OpenAI mô tả đây là một quá trình trao đổi nhiều bước. Coi lời gọi tool đầu tiên là câu trả lời cuối cùng là một lỗi tích hợp rất phổ biến.

So sánh contract ở mức wire format

Khác biệt chính nằm ở cấu trúc chứ không phải ở khái niệm.

Khía cạnh Responses API Chat Completions
Định nghĩa function Item phẳng trong tools[] với name, description, parameters, strict Item trong tools[] với các field đó lồng bên trong function
Lời gọi do model đề xuất Output item có kiểu function_call Phần tử tool_calls[] trong message của assistant
Định danh để đối chiếu call_id id của tool call, được trả lại dưới dạng tool_call_id
Tên function function_call.name tool_calls[].function.name
Argument Chuỗi JSON trong function_call.arguments Chuỗi JSON trong tool_calls[].function.arguments
Kết quả tool Input item function_call_output Message có role: "tool"
Dạng streaming argument thường gặp Event delta argument có kiểu Các mảnh delta.tool_calls[] có đánh index
Text cuối cùng Output item có kiểu và helper lấy output text choices[0].message.content

Đừng ghép lời gọi với kết quả theo vị trí trong mảng. Các lời gọi song song và chunk streaming có thể đến theo thứ tự khác với thứ tự ứng dụng xử lý xong. Call ID tường minh mới là khóa ghép (join key) ổn định.

Định nghĩa một function schema strict

Các ví dụ dưới đây expose một function chỉ đọc là get_delivery_status. Model có thể hỏi trạng thái đơn hàng, nhưng không thể sửa đơn, hoàn tiền hay truy cập storage tùy ý.

Schema của argument được cố ý giới hạn chặt:

{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "pattern": "^ORDER-[0-9]{4}$"
    }
  },
  "required": [
    "order_id"
  ],
  "additionalProperties": false
}

strict: true yêu cầu model (nếu có hỗ trợ) tuân thủ schema argument của function. Dù vậy, ứng dụng vẫn phải parse và validate JSON trước khi thực thi. Mức hỗ trợ pattern và các tính năng JSON Schema khác có thể khác nhau giữa các model hoặc provider, nên bước kiểm tra tương thích dùng cho Structured Outputs cũng áp dụng cho argument của function.

Cài đặt vòng lặp với Responses API

Ví dụ JavaScript này giữ cho quá trình trao đổi là stateless bằng cách gửi lại các output item của model cùng với kết quả function. Ví dụ dùng một function cục bộ có tính tất định (deterministic) để luồng điều khiển hiện rõ mà không cần thêm một API bên ngoài nào khác.

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MODELFLARE_API_KEY,
  baseURL: "https://modelflare.dev/v1",
});

const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID is required");

const orders = new Map([
  ["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);

const tools = [
  {
    type: "function",
    name: "get_delivery_status",
    description: "Return the current delivery status for one order.",
    parameters: {
      type: "object",
      properties: {
        order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
      },
      required: ["order_id"],
      additionalProperties: false,
    },
    strict: true,
  },
];

function executeTool(name, rawArguments) {
  if (name !== "get_delivery_status") {
    throw new Error(`Tool is not allowed: ${name}`);
  }

  const args = JSON.parse(rawArguments);
  if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
    throw new Error("Invalid order_id");
  }

  return orders.get(args.order_id) ?? { status: "not_found" };
}

const first = await client.responses.create({
  model,
  input: "Where is ORDER-1001?",
  tools,
  parallel_tool_calls: false,
});

const calls = first.output.filter((item) => item.type === "function_call");
if (calls.length === 0) {
  console.log(first.output_text);
  process.exit(0);
}

const toolOutputs = calls.map((call) => ({
  type: "function_call_output",
  call_id: call.call_id,
  output: JSON.stringify(executeTool(call.name, call.arguments)),
}));

const final = await client.responses.create({
  model,
  input: [...first.output, ...toolOutputs],
  tools,
  parallel_tool_calls: false,
});

console.log(final.output_text);

Nếu cần tích hợp có trạng thái (stateful), Responses API cũng cho phép tiếp tục từ một response trước đó khi route và chính sách lưu trữ cho phép. Ví dụ stateless giúp toàn bộ lịch sử hội thoại hiện rõ và tránh giả định rằng mọi route tương thích OpenAI đều lưu trạng thái response.

Các field quan trọng nhất là call.call_id trong output của model và call_id tương ứng trong function_call_output. Trả kết quả với ID sai sẽ làm dữ liệu bị tách rời khỏi yêu cầu của model.

Cài đặt vòng lặp với Chat Completions

Cùng function đó của ứng dụng có thể dùng với Chat Completions. Định nghĩa tool và message kết quả dùng lớp bọc (wrapper) theo kiểu Chat.

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MODELFLARE_API_KEY,
  baseURL: "https://modelflare.dev/v1",
});

const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID is required");

const orders = new Map([
  ["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);

const tools = [
  {
    type: "function",
    function: {
      name: "get_delivery_status",
      description: "Return the current delivery status for one order.",
      parameters: {
        type: "object",
        properties: {
          order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
        },
        required: ["order_id"],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

function executeTool(name, rawArguments) {
  if (name !== "get_delivery_status") {
    throw new Error(`Tool is not allowed: ${name}`);
  }

  const args = JSON.parse(rawArguments);
  if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
    throw new Error("Invalid order_id");
  }

  return orders.get(args.order_id) ?? { status: "not_found" };
}

const messages = [
  { role: "user", content: "Where is ORDER-1001?" },
];

const first = await client.chat.completions.create({
  model,
  messages,
  tools,
  parallel_tool_calls: false,
});

const assistant = first.choices[0].message;
const calls = assistant.tool_calls ?? [];
if (calls.length === 0) {
  console.log(assistant.content);
  process.exit(0);
}

messages.push(assistant);
for (const call of calls) {
  const result = executeTool(call.function.name, call.function.arguments);
  messages.push({
    role: "tool",
    tool_call_id: call.id,
    content: JSON.stringify(result),
  });
}

const final = await client.chat.completions.create({
  model,
  messages,
  tools,
  parallel_tool_calls: false,
});

console.log(final.choices[0].message.content);

Ở đây, message đầu tiên của assistant phải được thêm vào messages trước các kết quả tool của nó. Mỗi kết quả dùng tool_call_id: call.id. Bỏ sót message chứa tool call của assistant hoặc trả về ID không khớp sẽ tạo ra lịch sử hội thoại không hợp lệ.

Argument được stream theo từng mảnh, không phải JSON hoàn chỉnh

Ở chế độ streaming, argument của function có thể đến theo từng mảnh. Một chunk chứa {"order không phải là JSON sai; nó chỉ là JSON chưa hoàn chỉnh.

Parser an toàn giữ một buffer cho mỗi định danh lời gọi:

  1. xác định lời gọi hoặc slot lời gọi có index từ event;
  2. nối từng delta argument vào buffer của lời gọi đó;
  3. chờ tín hiệu argument done hoặc completed của endpoint;
  4. parse chuỗi hoàn chỉnh đúng một lần;
  5. chỉ validate và thực thi sau khi đã hoàn tất.

Responses API dùng các event có kiểu cho argument của function call và cho output item. Chat Completions phát ra các mảnh trong choices[].delta.tool_calls[], trong đó index xác định phần tử đang được xây dựng và tool call cuối cùng mang ID ổn định của nó. Đừng thực thi function khi mảnh đầu tiên vừa đến.

AI API Streaming Guide giải thích vì sao kết nối, event đầu tiên, output hữu ích đầu tiên, idle timeout và hoàn tất cần được đo riêng.

Thực thi tool an toàn và idempotent

Argument strict cải thiện cấu trúc chứ không cải thiện phân quyền. Trước khi thực thi bất kỳ lời gọi nào:

  • chỉ cho phép các tên function đã đăng ký;
  • parse argument với giới hạn kích thước;
  • validate toàn bộ schema trong code ứng dụng;
  • phân quyền cho user hoặc workload hiện tại trên tài nguyên được yêu cầu;
  • tách tool chỉ đọc khỏi tool có side effect;
  • che (redact) secret và output nhạy cảm trước khi trả về cho model;
  • đặt deadline và giới hạn số lần retry ở downstream;
  • ghi lại một tham chiếu thao tác an toàn, không ghi secret hay nội dung riêng tư dạng thô.

Với tool có side effect, hãy dùng idempotency key được suy ra từ một request ổn định của ứng dụng và định danh của lời gọi. Lưu kết quả đã hoàn tất trước khi trả về cho model. Nếu một lần retry ở tầng mạng gửi lại đúng lời gọi đó, hãy trả về kết quả đã lưu thay vì hoàn tiền, gửi tin nhắn, deploy hay ghi database thêm một lần nữa.

Tên function và argument do model sinh ra là input không đáng tin cậy. Một schema hợp lệ không cấp quyền, không chứng minh quyền sở hữu và không thay thế được ranh giới transaction.

Quyết định cách xử lý nhiều lời gọi

Các ví dụ đặt parallel_tool_calls: false để state machine dễ quan sát. Nếu bật lời gọi song song:

  • ghép mọi kết quả theo Call ID, không bao giờ theo thứ tự hoàn tất;
  • đặt giới hạn số lời gọi tối đa trên mỗi response;
  • áp dụng giới hạn concurrency trong ứng dụng;
  • xác định khi một tool lỗi thì sẽ hủy, chặn hay để nguyên các tool đã thành công;
  • trả về đúng một kết quả cho mỗi lời gọi đã chấp nhận;
  • giữ các thao tác có side effect chạy tuần tự khi thứ tự là quan trọng.

Chạy song song có thể giảm latency cho các thao tác đọc độc lập, nhưng làm tăng độ phức tạp về phân quyền, thứ tự, retry và lỗi một phần. Hãy bật nó khi workload thực sự được lợi, chứ không phải chỉ vì tham số đó tồn tại.

Ranh giới tương thích của Modelflare

Lớp tương thích OpenAI của Modelflare giữ nguyên các định nghĩa function strict và chuyển đổi các dạng function call được hỗ trợ giữa Responses API và Chat Completions trên các route OpenAI/Codex đủ điều kiện. Phạm vi chuyển đổi này được cố ý giới hạn hẹp hơn toàn bộ tập tính năng tool của Responses API.

Function tool do ứng dụng định nghĩa có thể biểu diễn ở cả hai định dạng. Còn các hosted tool chỉ có ở Responses API — như search do provider chạy, thực thi code hay các loại tool built-in khác — không tương đương với function calling của ứng dụng, và không thể mặc định là chúng sẽ được chuyển sang Chat Completions.

Các họ model tương thích OpenAI khác được cung cấp qua cơ chế pass-through Chat Completions nguyên bản, trừ khi đã được kiểm chứng riêng. Với các route này, provider upstream quyết định việc có hỗ trợ tools, strict, lời gọi song song, mảnh argument khi streaming và các dạng tool_choice cụ thể hay không.

Trước khi bật một route, hãy chạy trọn vòng lặp thay vì chỉ kiểm tra request đầu tiên có trả về 200 hay không:

Bài test Bằng chứng cần có
Một lời gọi chỉ đọc Tên function, argument đã parse, Call ID, liên kết kết quả, text cuối cùng
Argument không hợp lệ Lỗi validate tường minh, tool không được thực thi
Function không xác định Bị allowlist của ứng dụng từ chối
Không cần tool Text cuối cùng bình thường, không có lời gọi bịa ra
Lời gọi dạng streaming Argument được ghép lại đầy đủ và Call ID chính xác
Request lặp lại Một side effect hoặc một kết quả từ cache, không thực thi trùng
Nhiều lời gọi Ghép đúng, không phụ thuộc thứ tự hoàn tất
Fallback route Giữ nguyên model, giao thức, tool schema và contract kết quả

Tham khảo Reliable AI API Routing để quản lý thay đổi route một cách tường minh, và AI API Key Security để tách biệt workload và quyền hạn.

Chọn định dạng theo contract của ứng dụng

Dùng Responses API khi ứng dụng được lợi từ output item có kiểu, mô hình event của Responses API, các tùy chọn tiếp tục trạng thái hoặc các tính năng Responses API khác đã được kiểm chứng. Dùng Chat Completions khi ứng dụng đã tự quản lý một lịch sử message ổn định và tool contract của provider được chọn đã được kiểm chứng. Bức tranh rộng hơn về việc chọn endpoint được trình bày trong Responses API vs Chat Completions.

Dù chọn bên nào, tiêu chuẩn cài đặt vẫn phải giống nhau: allowlist tool tường minh, schema argument strict, validate phía ứng dụng, phân quyền trước khi thực thi, ghép theo Call ID, idempotency cho side effect và trả kết quả đầy đủ về cho model. Chính những cơ chế kiểm soát này — chứ không phải tên endpoint — mới làm cho một workflow function calling trở nên đáng tin cậy.


Tác giả là đội ngũ phát triển Modelflare, một AI API gateway; nếu bạn muốn dùng nhiều provider qua một API tương thích OpenAI, có thể tham khảo thêm tại https://modelflare.dev.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí