Tool calling Function calling Agents Đa nhà cung cấp OpenAI compatible

Gọi tool di động giữa OpenAI, Claude và Gemini

Xây agent gọi tool di động giữ một tools schema trong khi routing Claude, Gemini và OpenAI qua gateway tương thích.

AveMujica API 9 phút đọc

Gọi tool khả chuyển nghĩa là agent định nghĩa tool một lần — thường bằng dạng tools của OpenAI Chat Completions — rồi gọi chúng qua model OpenAI, Claude, Gemini hoặc nhà cung cấp khác mà không duy trì ba dialect schema phân kỳ trong application code. Cách đáng tin cậy không phải “hy vọng mọi SDK gốc khớp nhau”, mà là chuẩn hóa một client contract, kiểm tra đối số trước side effect và định tuyến inference qua gateway tương thích OpenAI để ánh xạ khác biệt nhà cung cấp ở biên.

Bài này tập trung vào ánh xạ trường, vòng lặp tool call trả về và ma trận không tương thích. Governance MCP rộng hơn, auth, platform logging và định tuyến tool nhiều chặng thuộc về các bài riêng như định tuyến nhà cung cấp LLM bằng MCP server và định tuyến tool agent bằng MCP gateway.

Vì sao tool schema trở thành vấn đề đa nhà cung cấp

Function calling không còn optional với agent. Ngay khi model có thể request lookup_customer hoặc create_ticket, hệ thống có surface API thứ hai: tool interface. Interface này ổn định hơn bất kỳ brand model nào, nên không nên viết lại mỗi lần thử Claude hoặc Gemini.

Format native vẫn khác:

  • OpenAI mô tả tool, tool_choice và message tool call trong hướng dẫn function calling.
  • Messages API gốc của Anthropic dùng block tool-use / tool-result; con đường tương thích OpenAI SDK chấp nhận client kiểu OpenAI để đánh giá nhưng không phải khuyến nghị production dài hạn của Anthropic cho phần lớn ứng dụng.
  • Gemini document function calling trong Google AI docs và cung cấp OpenAI-compatible endpoint.

Nếu mỗi agent microservice nói một native dialect khác, platform tích lũy N converter, N log format và N failure mode. Tính di động bắt đầu bằng việc chọn một dialect cho product code.

Bảng ánh xạ trường nhanh

Giữ product code trên dialect OpenAI Chat Completions. Gateway hoặc adaptor thường ánh xạ gần như sau; tên có thể đổi theo phiên bản, vì vậy hãy kiểm tra trước khi phụ thuộc trường hợp biên:

Khái niệmOpenAI Chat CompletionsClaude Messages gốcGemini, function calling / tương thích OpenAI
Catalog tooltools[].type=function + function.{name,description,parameters}tools[] với name, description, input_schemafunction_declarations / tools kiểu OpenAI trên endpoint tương thích
Bắt buộc hoặc tự độngtool_choice: none / auto / required / tool có têntool_choice với dạng riêng của Claudetool_config của provider hoặc tool_choice kiểu OpenAI trên endpoint tương thích
Model yêu cầu gọimessage.tool_calls[] với id, function.name, function.arguments là chuỗi JSONBlock content[] loại tool_use với id, name, input là objectPhần functionCall hoặc tool_calls kiểu OpenAI trên endpoint tương thích
App trả kết quảMessage role=tool + tool_call_id + chuỗi contentBlock tool_result tham chiếu tool_use_idPhần function response / role=tool kiểu OpenAI trên endpoint tương thích
Gọi song songNhiều mục tool_callsNhiều block tool_useNhiều function call khi được bật
Schema strictstrict / structured outputs riêng providerStructured outputs gốc; tương thích OpenAI bỏ qua strictXem tài liệu structured output Gemini hiện tại; đừng giả định OpenAI strict

Quy tắc thực dụng: chuẩn hóa về OpenAI khi đưa vào product code, dù gateway nói Claude hay Gemini gốc ở thượng nguồn. Log và eval harness khi đó so sánh một dạng duy nhất.

JSON Schema giữ được tính khả chuyển

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "minLength": 1},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["city"],
      "additionalProperties": false
    }
  }
}

Giữ name ổn định, description chính xác, enum nhỏ. Tính di động chết khi mỗi thí nghiệm nhà cung cấp rename city thành location.

Vòng client tham chiếu với kiểm tra thật

Chỉ gọi json.loads chưa phải validation. Hãy dùng allowlist tên tool cùng JSON Schema hoặc Pydantic trước mọi side effect:

from openai import OpenAI
import json
from typing import Any, Callable

# pip install jsonschema
from jsonschema import Draft202012Validator

client = OpenAI(
    api_key="GATEWAY_API_KEY",
    base_url="https://your-gateway.example.com/v1",
)

TOOL_PARAMS = {
    "get_weather": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "minLength": 1},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
        },
        "required": ["city"],
        "additionalProperties": False,
    }
}

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city",
            "parameters": TOOL_PARAMS["get_weather"],
        },
    }
]

def get_weather(city: str, unit: str = "celsius") -> dict:
    return {"city": city, "unit": unit, "temperature": 22}


HANDLERS: dict[str, Callable[..., dict]] = {
    "get_weather": get_weather,
}


def parse_and_validate(name: str, raw_arguments: str) -> dict[str, Any]:
    if name not in HANDLERS or name not in TOOL_PARAMS:
        raise ValueError(f"tool_not_allowed:{name}")
    try:
        args = json.loads(raw_arguments or "{}")
    except json.JSONDecodeError as exc:
        raise ValueError(f"invalid_json:{name}") from exc
    if not isinstance(args, dict):
        raise ValueError(f"arguments_not_object:{name}")
    Draft202012Validator(TOOL_PARAMS[name]).validate(args)
    return args


MODEL = "claude-sonnet"  # gateway alias; swap to gemini-flash or gpt-* without code rewrites
messages = [{"role": "user", "content": "Weather in Lyon in celsius?"}]

response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

msg = response.choices[0].message
messages.append(msg)

if msg.tool_calls:
    for call in msg.tool_calls:
        try:
            args = parse_and_validate(call.function.name, call.function.arguments)
            result = HANDLERS[call.function.name](**args)
        except Exception as exc:
            result = {"error": "tool_validation_failed", "detail": str(exc)}
        messages.append(
            {
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            }
        )

    final = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)

Chỉ đổi model alias để thử Gemini hoặc OpenAI. Giữ nguyên tool executor và JSON Schema. Đó là lợi ích cốt lõi của tính khả chuyển.

Đoạn response cần assert trong test

Hop đầu thành công nên có dạng sau, đã rút gọn trường:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Lyon\",\"unit\":\"celsius\"}"
            }
          }
        ]
      }
    }
  ]
}

Harness cần assert: tên tool nằm trong allowlist, arguments là chuỗi JSON object, schema hợp lệ, và hop thứ hai nhận role=tool với cùng tool_call_id.

Ma trận không tương thích: hãy test, đừng giả định

Failure modeDấu hiệuCách xử lý an toàn
Đối số gần giống JSONdấu phẩy cuối, nháy đơntừ chối; trả nội dung lỗi tool; không dùng eval
Thiếu trường bắt buộclỗi validation schematrả lỗi tool có cấu trúc; cho model retry một lần
Tên tool lạmodel bịa getWeathertrượt allowlist; không bao giờ thực thi
arguments rỗng"" hoặc {} khi cần trườngvalidation thất bại
Fan-out song song5+ tool call trong một lượtgiới hạn concurrency, nhất là mutation
Bỏ qua tool_choice=requiredtrả văn bản thay vì tool callcoi là task thất bại khi side effect bắt buộc
strict=true trên tương thích Claudebị bỏ qua âm thầm theo tài liệu Anthropicluôn kiểm tra cục bộ
Đối số tool streaming một phầnchuỗi đối số bị phân mảnhbuffer tới sự kiện tool call cuối
Provider từ chối toolchỉ trả văn bản safety / policyđóng an toàn; không bịa tool call trong app
Retry idempotentcùng create_ticket chạy hai lầndedupe key tại lớp tool

Tự động hóa kiểm tra schema. Giữ chấm điểm chất lượng chủ quan ngoài portability gate.

Điều vẫn hỏng khi nhà cung cấp thay đổi

Tính hợp lệ của đối số

Model có thể phát enum sai hoặc thiếu trường bắt buộc dù schema trông đơn giản. Hệ thống khả chuyển kiểm tra cùng JSON Schema trước side effect. Không bao giờ chạy tool thanh toán hoặc ghi dữ liệu bằng đối số chưa kiểm tra chỉ vì “model thường tuân thủ”.

Tool call song song

Nhà cung cấp khác nhau về việc nhiều tool có thể chạy trong một lượt và thứ tự kết quả ở request sau. Giới hạn fan-out cho mutation. Ưu tiên tool tuần tự khi hành động nghiệp vụ không giao hoán.

Semantics của tool_choice

auto, none, required và buộc tool có tên không được triển khai giống nhau ở mọi nơi. Đưa từng mode vào ma trận test khi sản phẩm phụ thuộc tool bắt buộc.

Tool strict có cấu trúc và văn bản tự do

Structured output strict kiểu OpenAI, structured output gốc của Claude và Gemini là các sản phẩm khác nhau. Trên lớp tương thích OpenAI của Anthropic, strict bị bỏ qua. Luôn lập kế hoạch validation ở ứng dụng, bất kể ngôn ngữ tiếp thị của nhà cung cấp.

Streaming cùng tool

Tool call streaming thường tới dưới dạng chuỗi đối số một phần. Hãy buffer tới khi call hoàn tất rồi mới validate. Failover giữa luồng sau khi tool đã thay đổi trạng thái là bug ứng dụng, không phải bug model.

Idempotency của side effect

Tool khả chuyển vẫn cần idempotency key bền vững cho thao tác create và thanh toán. Nếu không, retry của model và client sẽ gửi trùng.

Trách nhiệm gateway với portable tools

Một AI gateway nên:

  • chấp nhận một dialect tool đầu vào, thường là OpenAI tools
  • ánh xạ sang upstream Claude, Gemini hoặc OpenAI mà không buộc mọi ứng dụng học format gốc
  • công bố model alias ổn định để test harness không hardcode snapshot ID của provider
  • phát usage log gồm model alias và finish reason cho các lượt có tool
  • áp dụng scope cho key để batch key không thể gọi agent có nhiều thao tác mutation

Phần chọn kiến trúc proxy hay gateway được trình bày trong LLM proxy và AI gateway. Cấu hình SDK riêng cho Claude và Gemini nằm trong dùng Claude với OpenAI SDK và dùng Gemini với OpenAI SDK.

Ma trận khả chuyển gồm tám trường hợp

Chạy các trường hợp sau với alias OpenAI, Claude và Gemini qua cùng một gateway client:

  1. Không cần tool, chỉ trả văn bản
  2. Một tool bắt buộc với đối số hợp lệ
  3. Thiếu đối số bắt buộc
  4. Enum không hợp lệ
  5. Tên tool không xác định
  6. Gọi song song hai tool nếu sản phẩm cho phép
  7. Buộc tool_choice tới một tool có tên
  8. Retry idempotent cho tool mutation

Tiêu chí đạt cho trường hợp 3–5 và 8 phải dựa trên hành vi validation hoặc executor, không phải “model xin lỗi nghe hợp lý”.

Quy tắc thiết kế giữ tool di động

  • Một tên, một nghĩa. Đừng dùng quá tải search cho các hệ thống không liên quan.
  • Ưu tiên JSON đơn giản. Union lồng nhau và blob free-text khổng lồ làm giảm độ tin cậy giữa các model.
  • Version tool rõ ràng bằng create_ticket_v2 thay vì âm thầm đổi nghĩa.
  • Tách retrieval tool khỏi mutation tool; áp dụng auth và idempotency chặt hơn cho mutation.
  • Giới hạn fan-out và recursion. Vòng lặp tool không giới hạn là sự cố chi phí và an toàn.
  • Log I/O của tool kèm redaction. Không có bằng chứng thì không thể debug tính khả chuyển.

Hướng dẫn LLM của OWASP vẫn áp dụng: excessive agency và sensitive data exposure thường xuất hiện đầu tiên trong hệ thống có tool.

Smoke test bằng curl

curl https://your-gateway.example.com/v1/chat/completions \
  -H "Authorization: Bearer $GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash",
    "messages": [{"role": "user", "content": "What is the weather in Lyon?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }],
    "tool_choice": "auto"
  }'

Kiểm tra choices[0].message.tool_calls. Lặp lại với alias Claude và OpenAI. Khác biệt về việc có gọi tool hay không là tín hiệu sản phẩm; khác biệt về dạng response envelope là lỗi ánh xạ của gateway.

Nguồn chính thức

Kiểm tra lần cuối: 2026-07-15. Hãy xác minh lại tool_choice, tool call song song và hỗ trợ strict schema của từng provider trước khi mở rộng side effect của agent.

Câu hỏi thường gặp

Mọi team có cần học native tool format mỗi nhà cung cấp?

Không với product application code. Giữ một ingress schema—thường OpenAI tools—và cô lập quirk nhà cung cấp trong gateway hoặc một adapter library.

OpenAI-compatible tool calling có đủ cho production agent?

Đây là chiến lược transport và schema mạnh, nhưng production vẫn cần allowlist, schema validation, authZ, idempotency và budget. Tính tương thích không thay thế các control đó.

MCP có thay portable function calling không?

Bổ sung cho nhau: MCP chuẩn hóa cách tool được expose cho host, còn model provider vẫn cần một cách yêu cầu invocation. Chi tiết topology và governance nằm trong các bài về MCP routing đã liên kết ở trên.

Chạy ma trận trước khi bật failover đa nhà cung cấp

Lấy một production tool schema, chạy ma trận tám trường hợp trên alias OpenAI, Claude và Gemini qua /v1/chat/completions, rồi sửa các khoảng trống validation trước khi bật failover đa nhà cung cấp cho agent đó. Chỉ mở rộng traffic sau bước này.