Responses API hay Chat Completions: nên xây dựng trên nền nào
Cẩm nang thực chiến về Responses API và Chat Completions của OpenAI: cấu trúc request, item đầu ra, trạng thái, suy luận, công cụ, và khi nào nên chọn từng endpoint.
Nếu hôm nay bạn bắt đầu một tích hợp mới, hãy xây dựng trên Responses API (POST /v1/responses) và dành Chat Completions (POST /v1/chat/completions) cho phần mã đã tồn tại và đang chạy tốt. Hai endpoint này giải quyết cùng một bài toán cốt lõi — gửi một prompt, nhận về câu trả lời của mô hình — nhưng chúng mô hình hóa cuộc hội thoại theo những cách khác nhau. Chat Completions coi một lượt hội thoại là một danh sách messages phẳng và trả về một mảng choices. Responses coi một lượt hội thoại là một danh sách các item input có kiểu và trả về một mảng output có kiểu, có thể chứa đồng thời tin nhắn, suy luận và lời gọi công cụ. Chính sự khác biệt về cấu trúc đó là toàn bộ câu chuyện, và nó quyết định endpoint nào phù hợp với khối lượng công việc của bạn.
Bài viết này so sánh hai endpoint trên các khía cạnh: cấu trúc request, cấu trúc đầu ra, trạng thái, suy luận và công cụ, rồi đưa ra cho bạn một quy tắc quyết định có thể áp dụng mà không cần đoán mò. Bài viết không tuyên bố bất cứ thứ gì đã lỗi thời: Chat Completions vẫn được hỗ trợ và vẫn là hình thái dễ chuyển đổi nhất giữa các nhà cung cấp.
Cấu trúc request: messages hay input
Chat Completions yêu cầu một mảng messages, trong đó mỗi phần tử có một role (system, user, assistant, tool) và content. Mọi thứ trong một lượt — chỉ dẫn, lịch sử, kết quả công cụ — đều được gộp vào cùng một danh sách đó.
from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")
completion = client.chat.completions.create(
model="gpt-5.6",
messages=[
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "Name three uses for an API gateway."},
],
)
print(completion.choices[0].message.content)
Responses tách các mối quan tâm đó ra riêng biệt. Chỉ dẫn ở cấp cao nhất chuyển sang instructions, còn nội dung của lượt hội thoại chuyển sang input, vốn nhận vào hoặc một chuỗi thuần túy, hoặc một mảng các item có kiểu.
response = client.responses.create(
model="gpt-5.6",
instructions="You are a terse assistant.",
input="Name three uses for an API gateway.",
)
print(response.output_text)
Có hai điều đáng chú ý ngay lập tức. Thứ nhất, response.output_text là một bộ truy cập tiện lợi, tự nối phần đầu ra dạng văn bản cho bạn, thay thế cho lối đi completion.choices[0].message.content. Thứ hai, instructions không "dính" qua các lượt được nối chuỗi — nếu bạn tiếp tục một cuộc hội thoại bằng previous_response_id, instructions của phản hồi trước đó sẽ không được mang theo, vì vậy bạn phải gửi lại chúng ở mỗi lượt.
Cấu trúc đầu ra: choices hay các item output
Chat Completions trả về một mảng choices. Với tham số n, bạn có thể yêu cầu vài completion ứng viên; mỗi cái là một choice riêng biệt. Responses loại bỏ n và trả về một lần sinh duy nhất, nhưng lần sinh đó là một mảng output gồm các item, mỗi item có type riêng. Các item mà mô hình trả về là:
message— văn bản của trợ lý (và mọi lời từ chối)reasoning— bản tóm tắt suy luận hoặc token suy luận đã mã hóafunction_call— một lời gọi công cụ mà mô hình muốn bạn thực thi
Hãy chú ý điều còn thiếu: function_call_output không phải là một item do mô hình trả về. Đó là một item input mà bạn tự tạo ra và gửi lại để trao kết quả của công cụ cho mô hình, được định danh qua call_id. Việc giữ hai danh sách này tách biệt — item output bạn đọc, item input bạn viết — giúp loại bỏ một nguồn nhầm lẫn phổ biến, vì lời gọi và kết quả của nó nằm ở hai phía đối diện của cuộc trao đổi.
Coi mọi mục trong output là một tin nhắn là lỗi di trú thường gặp nhất. Một mô hình suy luận có thể phát ra một item reasoning trước một message, và một lượt dùng công cụ có thể phát ra một function_call mà hoàn toàn không có văn bản nào người dùng nhìn thấy được. Hãy duyệt output theo type thay vì mặc định rằng chỉ số 0 là câu trả lời của bạn:
for item in response.output:
if item.type == "message":
print(item.content[0].text)
elif item.type == "function_call":
print("tool wanted:", item.name, item.arguments)
Cách hạch toán mức sử dụng cũng đổi từ vựng. Chat Completions báo cáo prompt_tokens và completion_tokens; Responses báo cáo input_tokens và output_tokens trên đối tượng usage. Bất kỳ bảng điều khiển nào phân tích số lượng token cũng cần xử lý được cả hai hình thái, vì vậy một gateway chuẩn hóa chúng thành một khung nhìn kế toán duy nhất sẽ giúp mọi client khỏi phải mang theo hai luồng mã.
Trạng thái: lịch sử do client quản lý hay luồng lưu trữ trên máy chủ
Với Chat Completions, bạn sở hữu cuộc hội thoại: bạn gửi lại toàn bộ lịch sử messages ở mỗi lần gọi, và request mang theo mọi thứ mô hình cần. Hình thái do client quản lý đó dễ suy luận và dễ định tuyến qua bất kỳ proxy nào, đó là phần lớn lý do khiến nó trở thành ngôn ngữ chung của ngành. Việc OpenAI có lưu giữ một bản sao của cuộc trao đổi hay không là một câu hỏi tách biệt với endpoint — nó được chi phối bởi cài đặt store và các mặc định tài khoản của bạn (vốn có thể bật lưu trữ để phục vụ ghi log), chứ không phải vì Chat Completions vốn dĩ "không trạng thái". Nếu bạn cần đảm bảo tuyệt đối không có gì được giữ lại phía thượng nguồn, hãy gửi store: false một cách tường minh thay vì mặc định rằng endpoint không giữ gì cả.
Responses được lưu trữ theo mặc định. Mỗi lần gọi trả về một id, và bạn có thể tiếp tục luồng bằng cách truyền previous_response_id ở request kế tiếp thay vì gửi lại lịch sử. Bạn có ba cách để quản lý trạng thái:
previous_response_idđể nối chuỗi tới một phản hồi đã lưu,- thủ công đưa các item
outputtrước đó trở lại vàoinput, - Conversations API cho các luồng tồn tại lâu hơn.
Lưu trữ là một tiện ích, không phải một điều bắt buộc. Đặt store: false để từ chối, đó là điều bạn muốn cho các quy trình không lưu giữ dữ liệu (zero-data-retention). Có một chi tiết tính phí quan trọng cần lưu ý khi lập kế hoạch: ngay cả khi bạn nối chuỗi bằng previous_response_id, mọi token đầu vào trước đó trong chuỗi vẫn được tính là token đầu vào. Nối chuỗi giúp bạn khỏi phải gửi lại các byte qua đường truyền; nó không làm cho ngữ cảnh trước đó trở nên miễn phí.
Suy luận: nơi Responses bứt lên trước
Lý do rõ ràng nhất để ưu tiên Responses là các mô hình suy luận. Vì suy luận là một kiểu item output hạng nhất, API có thể giữ lại trạng thái suy luận qua các lượt thay vì loại bỏ nó giữa các lần gọi. Với các thiết lập không trạng thái, bạn có thể yêu cầu suy luận đã mã hóa bằng cách gửi store: false cùng với include: ["reasoning.encrypted_content"]; API trả về các token suy luận đã mã hóa mà bạn truyền lại ở lượt kế tiếp, và nội dung được giải mã trong bộ nhớ rồi bị loại bỏ. Chat Completions không có khe có kiểu tương đương cho suy luận, nên một mô hình suy luận sẽ đánh mất tính liên tục đó giữa các lượt.
Nếu sản phẩm của bạn phụ thuộc vào suy luận nhiều bước, các vòng lặp công cụ do một mô hình suy luận điều khiển, hoặc các lượt chạy agent kéo dài, thì Responses là endpoint được thiết kế cho việc đó. Để tìm hiểu sâu hơn về các tham số liên quan, xem các tham số API của mô hình suy luận.
Công cụ và đầu ra có cấu trúc
Cả hai endpoint đều hỗ trợ gọi hàm và đầu ra có cấu trúc, nhưng tên trường khác nhau:
| Khía cạnh | Chat Completions | Responses |
|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses |
| Nội dung lượt | messages[] | input (chuỗi hoặc item) |
| Prompt hệ thống | tin nhắn system | instructions |
| Văn bản kết quả | choices[0].message.content | output_text |
| Đầu ra có cấu trúc | response_format | text.format |
| Liên kết kết quả công cụ | tool_call_id trên tin nhắn tool | function_call_output với call_id |
| Nhiều ứng viên | n | đã loại bỏ (một lần sinh duy nhất) |
| Công cụ gốc | đấu nối thủ công | tools: [{ "type": "web_search" }] |
Việc đổi text.format thay cho response_format rất dễ bỏ sót và tạo ra những lỗi khó hiểu khi bạn gửi sai trường. Với Structured Outputs, hãy đặt strict: true một cách tường minh trên schema để mô hình bị ràng buộc theo nó. (Một cơ chế dự phòng riêng biệt — bỏ qua strict để thử áp đặt nghiêm ngặt rồi lùi về chế độ không nghiêm ngặt khi schema không tương thích — áp dụng cho các định nghĩa function/tool của Responses, chứ không áp dụng cho đầu ra có cấu trúc text.format; đừng nhầm lẫn hai thứ này.) Nếu bạn dựa vào đầu ra theo JSON Schema, hãy đọc đầu ra có cấu trúc với JSON Schema trước khi chuyển đổi mã kiểm định.
Một vòng lặp công cụ khác nhau ra sao trên thực tế
Vòng đi-về khi gọi công cụ là nơi hai hình thái cảm thấy khác nhau nhất, nên đáng để đặt cạnh nhau mà xem. Trong Chat Completions, mô hình trả về một tin nhắn trợ lý có tool_calls mà bạn thực thi, rồi bạn thêm vào một tin nhắn với role: "tool" và một tool_call_id khớp:
# Chat Completions: append a tool result as a message
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
Trong Responses, mô hình phát ra một item function_call, và bạn đưa kết quả trở lại dưới dạng một item function_call_output liên kết qua call_id — chứ không phải dưới dạng một tin nhắn:
# Responses: append a typed function_call_output item
client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": call.call_id,
"output": result,
}],
)
Khóa liên kết có tên là call_id trong Responses và tool_call_id trong Chat Completions, và việc gửi kết quả mà thiếu id khớp là một nguồn lỗi thường gặp. Vì Responses giữ lời gọi và kết quả của nó thành các item có kiểu riêng biệt, một vòng lặp agent đọc gọn gàng hơn: bạn không bao giờ phải bóc tách ý định gọi công cụ từ bên trong một tin nhắn trợ lý. Các định nghĩa công cụ chuyển đổi được giữa các nhà cung cấp vẫn cần lớp tương thích riêng của chúng.
Một quy tắc quyết định bạn có thể áp dụng
Bạn không cần phải day dứt về chuyện này. Hãy dùng bảng sau:
| Tình huống | Endpoint |
|---|---|
| Tích hợp mới, nhất là agent hoặc mô hình suy luận | Responses |
| Mã Chat Completions hiện có đang chạy tốt, không có yêu cầu mới | Giữ Chat Completions |
| Bạn cần tính chuyển đổi tối đa giữa nhiều nhà cung cấp ngay hôm nay | Chat Completions (hình thái được nhân bản rộng rãi) |
| Bạn muốn trạng thái hội thoại do máy chủ quản lý hoặc công cụ gốc | Responses |
| Bạn muốn tính liên tục của suy luận đã mã hóa với việc không lưu giữ dữ liệu | Responses với store: false + suy luận mã hóa |
Một gateway làm cho lựa chọn này trở nên rẻ hơn ở cả hai phía. Đằng sau một base URL duy nhất, bạn có thể chạy lưu lượng Chat Completions và lưu lượng Responses tới cùng các nhà cung cấp thượng nguồn, đăng ký nhà cung cấp một lần, và công bố những gì mỗi mô hình hỗ trợ cũng như chi phí trên một trang giá công khai. Nếu về sau bạn di trú một endpoint, thay đổi phía client rất nhỏ và việc định tuyến vẫn giữ nguyên. Lộ trình từng bước được trình bày trong di trú Chat Completions sang Responses API.
Streaming hoạt động trên cả hai, với các sự kiện khác nhau
Cả hai endpoint đều truyền phát qua Server-Sent Events, nhưng từ vựng sự kiện khác nhau. Chat Completions truyền phát các đối tượng chat.completion.chunk với choices[].delta và kết thúc bằng một data: [DONE] theo nghĩa đen. Responses truyền phát các sự kiện ngữ nghĩa như response.created, response.output_text.delta, và response.completed, vốn dễ định tuyến hơn vì mỗi sự kiện tự nêu tên nó là gì. Nếu bạn đang xây một giao diện hiển thị từng token, truyền phát phản hồi LLM bằng SSE sẽ hướng dẫn bạn cách phân tích cả hai định dạng.
Câu hỏi thường gặp
Chat Completions có bị coi là lỗi thời khi đã có Responses không?
Không. Chat Completions vẫn là một endpoint được hỗ trợ và vẫn là hình thái request được nhân bản rộng rãi nhất trên các nhà cung cấp không phải OpenAI. Assistants API mới là phần mà OpenAI đã đánh dấu sẽ ngừng hoạt động, không phải Chat Completions. Hãy xây dựng các công việc agent mới trên Responses vì các item đầu ra có cấu trúc và khả năng hỗ trợ suy luận của nó, chứ không phải vì Chat Completions sắp biến mất.
Tôi có thể lấy nhiều câu trả lời ứng viên từ Responses API như n trong Chat Completions không?
Không thể chỉ bằng một tham số n đơn lẻ — Responses trả về một lần sinh duy nhất mỗi lần gọi. Nếu cần nhiều ứng viên, hãy gửi nhiều request. Đây là một thay đổi hành vi thực sự cần tính đến nếu mã Chat Completions của bạn dựa vào n > 1.
previous_response_id có giảm hóa đơn token của tôi không?
Nó giảm số byte truyền trên đường dây vì bạn ngừng gửi lại lịch sử, nhưng nó không giảm số token đầu vào bị tính phí: mọi token đầu vào trước đó trong một chuỗi đều được tính là token đầu vào. Hãy xem việc nối chuỗi là một tính năng về trải nghiệm và độ trễ, không phải một cách tối ưu chi phí. Để thực sự cắt giảm chi phí ngữ cảnh, hãy xem bộ nhớ đệm prompt trên OpenAI, Claude và Gemini.
Một thiết lập đa nhà cung cấp nên chuẩn hóa trên endpoint nào?
Nếu bạn định tuyến tới nhiều nhà cung cấp, Chat Completions hiện vẫn là mẫu số chung dễ chuyển đổi nhất, trong khi Responses là bề mặt gốc của OpenAI phong phú hơn. Một gateway cho phép bạn giữ cả hai: phơi bày hình thái mà mỗi client đã sẵn nói, và dịch theo từng kênh.
Chạy cả hai trên cùng một quy trình
Hãy chọn một quy trình ít rủi ro và cho nó chạy hai lần qua gateway của bạn — một lần dưới dạng lời gọi Chat Completions và một lần dưới dạng lời gọi Responses — rồi so sánh hình thái output/choices, cách hạch toán token, và độ trễ trong nhật ký sử dụng của gateway. Chỉ một lần đặt cạnh nhau đó nói cho bạn nhiều hơn bất kỳ bản tóm tắt đặc tả kỹ thuật nào về việc endpoint nào phù hợp với mã của bạn.