# Gọi thẳng API Claude và OpenAI: tự viết vòng tool use, rồi đối chiếu với Gemini

> Trước khi giao mọi thứ cho framework, bạn nên tự đi hết một vòng request, streaming và tool call với Claude và OpenAI, rồi biết Gemini khác ở đâu. Đó là những thứ bạn sẽ phải gỡ lỗi khi ngồi ở văn phòng khách hàng.

Bản gốc: https://fdetimes.net/vi/bach-khoa/goi-truc-tiep-api-claude-openai-gemini/

Chỉ một tham số cũng đủ làm hỏng buổi demo. Trên các model Claude mới, nếu đặt `temperature`, `top_p` hay `top_k` khác giá trị mặc định, API sẽ trả lỗi 400. Từ dòng 4.6 trở lên, kỹ thuật prefill cũng bị từ chối, nên đoạn code chép từ một bài blog cũ có thể gãy ngay trước mặt khách hàng.

Framework che bớt những chi tiết này, nhưng không che mãi được. Khi bạn làm FDE, hệ thống của khách hàng có thể hỏng ở tầng thấp nhất: một header sai, một id không khớp, một lượt hội thoại bị mất. Cách nhanh nhất để đọc được những lỗi đó là tự tay gọi API thô, ít nhất một lần.

Bài này dẫn bạn dựng một script nhỏ có tool `get_order_status(order_id)`, tra trạng thái đơn hàng từ một hàm giả lập. Bạn sẽ đi trọn vòng request, streaming và tool use với Claude và OpenAI, rồi đối chiếu cách Gemini xử lý cùng bài toán. Bạn cần Python, API key đặt trong biến môi trường và hai SDK `anthropic`, `openai`.

Code dưới đây được rút gọn để dễ theo dõi, chưa có xử lý lỗi hay retry. Tên model nằm trong hai biến `MODEL` và `OA_MODEL`; bạn điền tên model hiện hành theo docs của từng hãng.

## Bước 1: vì sao nên bắt đầu bằng curl?

Bạn phải nhìn thấy request thô trước khi để SDK bọc nó lại. Claude Messages API nhận `POST /v1/messages`, xác thực bằng header `x-api-key` và cần thêm header `anthropic-version`. Trong ví dụ của Anthropic, `max_tokens` và mảng `messages` là hai trường bắt buộc.

```bash
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model": "'"$CLAUDE_MODEL"'", "max_tokens": 512,
"messages": [{"role": "user", "content": "Chào bạn"}]}'
```

**Kiểm tra:** bạn phải nhận về một JSON có nội dung trả lời. Giờ hãy gửi lượt thứ hai mà chỉ có câu hỏi mới. Model sẽ không nhớ gì về lượt trước, vì Messages API là stateless: lượt nào bạn cũng phải gửi lại toàn bộ lịch sử hội thoại.

Vì thế, khi khách hàng phàn nàn rằng model "quên" lượt trước, đây là chỗ đầu tiên nên kiểm tra.

## Bước 2: streaming phải hiện chữ dần dần

Người dùng ngồi nhìn màn hình trắng mười giây sẽ nghĩ hệ thống đã treo. Với Claude, bạn đặt `"stream": true` để nhận phản hồi từng phần qua server-sent events. SDK Python có sẵn helper `text_stream` nên bạn không phải tự parse event.

```python
import anthropic
client = anthropic.Anthropic()
history = [{"role": "user", "content": "Giải thích SSE trong 3 câu"}]

with client.messages.stream(model=MODEL, max_tokens=512,
messages=history) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
```

**Kiểm tra:** chữ phải hiện ra dần trên terminal. Nếu cả đoạn văn hiện ra cùng một lúc, hãy xem giữa máy bạn và API có proxy hay gateway nào gom response lại trước khi trả về hay không. Ở mạng nội bộ của khách hàng, đó là một câu hỏi đáng đặt ra sớm.

## Bước 3: model chỉ đề xuất, code của bạn mới chạy hàm

Đây là ý quan trọng nhất của cả bài. Khi Claude muốn dùng một client tool, nó trả về `stop_reason: "tool_use"` cùng một hoặc nhiều block `tool_use`. Code của bạn chạy hàm, rồi gửi kết quả về trong block `tool_result`; server tools thì khác, chúng chạy trên hạ tầng của Anthropic.

Trước hết, viết một hàm giả lập trả về chuỗi, khai báo tool và một hàm gọi API dùng chung:

```python
def get_order_status(order_id): return f"Đơn {order_id}: đang giao"  # dữ liệu giả

tools = [{"name": "get_order_status",
"description": "Tra trạng thái đơn hàng theo mã",
"input_schema": {"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]}}]

def ask():
return client.messages.create(model=MODEL, max_tokens=512,
tools=tools, messages=history)
```

Rồi đặt câu hỏi vào `history` và viết vòng lặp, có giới hạn số vòng để tránh lặp vô tận:

```python
history = [{"role": "user", "content": "Đơn A123 tới đâu rồi?"}]
resp = ask()
for _ in range(5):
if resp.stop_reason != "tool_use":
break
history.append({"role": "assistant", "content": resp.content})
results = [{"type": "tool_result", "tool_use_id": b.id,
"content": get_order_status(**b.input)}
for b in resp.content if b.type == "tool_use"]
history.append({"role": "user", "content": results})
resp = ask()
```

**Kiểm tra:** thêm một dòng log ngay trong `get_order_status`. Nếu dòng log hiện ra và câu trả lời cuối có chữ "đang giao" do hàm trả về, vòng lặp đã chạy đúng.

Cũng cần nhớ là tool không miễn phí: API tự chèn một system prompt ẩn để bật tool use, và tham số `tools` cùng các block `tool_use`, `tool_result` đều tính token.

## Bước 4: OpenAI Responses đổi tên, giữ nguyên logic

Sang Responses API, vòng lặp vẫn như cũ, chỉ có tên gọi thay đổi. Lời gọi hàm nằm trong mảng `output`, dưới dạng một item có `type` bằng `function_call` và kèm theo `call_id`. Bạn trả kết quả về bằng một item `function_call_output` mang đúng `call_id` đó.

Khai báo tool theo định dạng của OpenAI, dùng lại schema và hàm giả lập ở bước 3:

```python
import json
from openai import OpenAI
oa = OpenAI()
oa_tools = [{"type": "function", "name": "get_order_status",
"description": "Tra trạng thái đơn hàng theo mã",
"parameters": tools[0]["input_schema"]}]
inputs = [{"role": "user", "content": "Đơn A123 tới đâu rồi?"}]
```

Rồi chạy một vòng gọi hàm và gửi kết quả về:

```python
resp = oa.responses.create(model=OA_MODEL, input=inputs, tools=oa_tools)
for item in resp.output:
if item.type == "function_call":
args = json.loads(item.arguments)
inputs.append(item)
inputs.append({"type": "function_call_output",
"call_id": item.call_id,
"output": get_order_status(**args)})
resp = oa.responses.create(model=OA_MODEL, input=inputs, tools=oa_tools)
```

Lưu ý đoạn này chỉ xử lý đúng một vòng gọi hàm, khác với vòng lặp có giới hạn ở bước 3. Nếu model đề xuất thêm lời gọi sau khi nhận kết quả, bạn cần bọc nó trong một vòng `for` tương tự, dừng khi `output` không còn item `function_call` nào.

Streaming trên Responses API cũng bật bằng `stream=True`, nhưng thứ bạn nhận về là các semantic event, mỗi event mang một kiểu riêng. Trước khi viết bất kỳ logic xử lý nào, hãy in hết `event.type` ra để xem luồng sự kiện trông thế nào:

```python
for event in oa.responses.create(model=OA_MODEL, input=inputs,
tools=oa_tools, stream=True):
print(event.type)
```

**Kiểm tra:** bạn sẽ thấy một chuỗi event có tên riêng cho từng loại. Hãy chọn đúng event chứa đoạn text mới, đối chiếu tên với docs, đừng đoán.

## Bước 5: Gemini đang đổi API, đừng copy code cũ

Với Gemini, cái bẫy nằm ở chỗ bạn chọn API nào. Docs hiện tại khuyên dự án mới dùng Interactions API và xếp `generateContent` vào nhóm legacy, nên các tutorial dùng `generateContent` trên mạng có thể đã lỗi thời. Khi gọi qua REST, key được truyền bằng header `x-goog-api-key`.

```bash
# Khung rút gọn: endpoint và body lấy từ trang Interactions API
curl "$GEMINI_ENDPOINT" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-H "content-type: application/json" \
-d @request.json
```

Bài này không viết sẵn vòng tool use cho Gemini, vì cấu trúc request của Interactions API bạn nên lấy thẳng từ docs. Nguyên tắc thì không đổi: Gemini không tự chạy hàm, nó chỉ đề xuất một lời gọi, còn ứng dụng của bạn chạy hàm và gửi kết quả về.

Mặc định, response chỉ trả về khi model sinh xong, nên muốn nhận từng phần thì bạn phải bật streaming.

**Bài tập mở rộng:** dựng lại vòng `get_order_status` trên Interactions API theo docs, với cùng câu hỏi về đơn A123, rồi so cách Gemini đề xuất lời gọi hàm với hai nhà cung cấp còn lại.

## Ba API, một bảng đối chiếu

| | Claude Messages | OpenAI Responses | Gemini |
|---|---|---|---|
| Streaming | `stream: true`, SSE | `stream=True`, semantic events | Bật streaming để nhận từng phần |
| Tín hiệu gọi tool | `stop_reason: "tool_use"` | item `function_call` trong `output` | Model đề xuất lời gọi hàm |
| Trả kết quả | `tool_result` + `tool_use_id` | `function_call_output` + `call_id` | App gửi kết quả về |
| Lịch sử | Client gửi lại toàn bộ | Có thể giữ state trên server | Theo API bạn chọn |

**Điểm mấu chốt:** Ở cả ba nhà cung cấp, model chỉ đề xuất lời gọi, còn code của bạn mới là bên chạy hàm, nên khi vòng tool use hỏng, chỗ đầu tiên nên kiểm tra là code của bạn.

## Những lỗi sẽ gặp ở dự án thật

Hay gặp nhất là cấu hình sampling cũ: config của khách hàng còn `temperature: 0.2` từ thời model trước thì model Claude mới sẽ trả 400. Kế đến là quên id, khi `tool_result` thiếu `tool_use_id` hoặc `function_call_output` thiếu `call_id`, model không biết kết quả thuộc lời gọi nào. Rồi còn chuyện nhồi vài chục tool vào mọi request và ngạc nhiên vì hoá đơn token tăng vọt.

Ngoài ra còn một quyết định kiến trúc mà bạn nên nói rõ với khách hàng. Anthropic định vị Messages API cho các agent loop tự viết, cần kiểm soát chi tiết, trong khi OpenAI cho biết Responses API giữ được reasoning giữa các lượt, điều Chat Completions không làm được, và chạy hosted tools ngay phía server.

Simon Willison cũng chỉ ra rằng Responses có thể quản lý state hội thoại trên server thay cho bạn.

Nếu bạn ở vị trí FDE, cách làm hợp lý là hỏi khách hàng hai câu ngay từ buổi đầu: lịch sử hội thoại được phép nằm ở đâu, và ai sẽ là người chạy tool.

Một ngân hàng muốn tự giữ toàn bộ log trong hệ thống của mình sẽ hợp với mô hình stateless hơn, còn một startup cần ra sản phẩm nhanh có thể muốn giao bớt phần quản lý state cho server.

## Biến bài tập thành bằng chứng trong CV

Một câu "có kinh nghiệm LLM" trong CV khó chứng minh được rằng bạn hiểu tầng nằm dưới framework, trong khi đó lại là tầng bạn phải gỡ khi framework hỏng ở chỗ khách hàng. Hãy đẩy script này lên GitHub với hai adapter Claude và OpenAI chung một interface `call_tool`, kèm README ghi lại từng lỗi 400 bạn gặp và cách sửa.

Trong CV, hãy ghi cụ thể rằng bạn đã tự viết vòng tool use trên Claude Messages và OpenAI Responses, và nếu đã làm bài tập mở rộng thì thêm cả Gemini Interactions API.

Khi đọc JD, hãy để ý các cụm như "integrate with multiple model providers" hay "build agent loops". Đó chính là bài tập bạn vừa làm, và giờ bạn có thể kể lại nó bằng các trường dữ liệu cụ thể chứ không chỉ bằng tên framework.

**Thử ngay tuần này:**

- Gọi Claude bằng curl với header x-api-key và anthropic-version, rồi gửi lượt thứ hai mà cố ý bỏ lịch sử để thấy model quên lượt trước
- Viết tool get_order_status giả lập và cho nó chạy được với cả Claude lẫn OpenAI Responses, sau đó in ra mọi event.type khi bật stream=True
- Đẩy lên GitHub một repo nhỏ có hai adapter Claude và OpenAI, README liệt kê từng lỗi bạn gặp và cách sửa, rồi gắn link vào CV

## Nguồn

- [Using the Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages)

- [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming)

- [Tool use with Claude](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)

- [Streaming API responses](https://developers.openai.com/api/docs/guides/streaming-responses)

- [Function calling](https://developers.openai.com/api/docs/guides/function-calling)

- [Why we built the Responses API](https://developers.openai.com/blog/responses-api/)

- [OpenAI API: Responses vs. Chat Completions](https://simonwillison.net/2025/Mar/11/responses-vs-chat-completions/)

- [Function calling with the Gemini API](https://ai.google.dev/gemini-api/docs/function-calling)

- [Gemini API | Google AI for Developers](https://ai.google.dev/gemini-api/docs)

- [Text generation | Gemini API | Google AI for Developers](https://ai.google.dev/gemini-api/docs/text-generation)
