Chúng tôi đã ngừng sử dụng các SDK client của Stripe, WorkOS và Slack, và đang trong quá trình chuyển đổi các phần còn lại. Thay vào đó, chúng tôi gọi trực tiếp REST API của họ thông qua một lớp wrapper nhỏ có tên là HttpBaseClient.
Nghe có vẻ ngược đời. SDK được cho là để tiết kiệm thời gian. Nhưng bạn đã nghe nói về việc AI đang chuyển nhiều mã hơn sang chạy native. Ý tưởng tương tự cũng áp dụng cho SDK. Tại sao?
- SDK che giấu các chi tiết thô, như header phản hồi HTTP và nội dung phản hồi thô, những thứ rất quan trọng đối với các agent đang gỡ lỗi hoặc gửi thông tin chi tiết cho nhóm upstream để điều tra.
- Chúng mong đợi phản hồi đúng đắn, nhưng thực tế lại nhận được phản hồi sai định dạng từ tường lửa, load balancer và gateway. Điều này che giấu các chi tiết gỡ lỗi mà chúng ta cần.
- SDK bỏ qua lực ép phải có một điểm đầu vào tập trung cho việc retry, xử lý lỗi và khả năng quan sát, cho phép các agent tự viết lại các mẫu của riêng chúng.
- Chúng mang theo nhiều thứ phình to, thường được tự động sinh ra từ OpenAPI, trong khi chúng ta chỉ sử dụng một tập con nhỏ các endpoint.
Hãy nghĩ về lý do tại sao SDK ra đời. Chúng được sinh ra vì các công ty muốn việc áp dụng trở nên dễ dàng: gửi một thư viện dùng chung, tiết kiệm thời gian cho khách hàng và che giấu công việc tích hợp lặp đi lặp lại. Nhưng AI đã thay đổi đường cong chi phí đó. Tích hợp với SDK giờ đây thường tốn công sức không kém gì việc gọi trực tiếp HTTP API, và việc viết một REST client tùy chỉnh cho chính xác các endpoint chúng ta sử dụng, đồng thời cung cấp khả năng quan sát thống nhất tốt hơn, là rẻ.
Trò chơi đập chuột không hồi kết

Những phản hồi "JSON" đáng yêu như vậy
Đây là cách bảng điều khiển lỗi Sentry của chúng tôi trông như thế nào. Chúng tôi thường phát hiện một tỷ lệ nhỏ các yêu cầu thất bại từ một đường dẫn mã nào đó, sau đó chúng tôi vá tạm chính đường dẫn đó để xử lý sự cố SDK cho người dùng. Mỗi ngày lại có một đường dẫn mã khác gặp lỗi không mong đợi; chúng tôi lại vá cái đó, và trò chơi không bao giờ kết thúc.
SDK rất dễ vỡ trong một chế độ lỗi sản xuất phổ biến: máy chủ báo có gì đó sai, nhưng phản hồi không phải là dạng JSON mà SDK mong đợi.
Một gateway Nginx quá tải trả về HTML bất ngờ. Cloudflare chặn một yêu cầu. Tường lửa đưa ra một trang giới hạn tốc độ. API của nhà cung cấp thường là JSON, nhưng thứ đứng trước nó không phải lúc nào cũng là API của nhà cung cấp.
Khi điều đó xảy ra, nhiều SDK cố gắng phân tích phản hồi, thất bại với lỗi phân tích chung chung và vứt bỏ các phần hữu ích. Nội dung thô biến mất. Văn bản trạng thái bị chôn vùi. Các header HTTP thường không được trả về theo cách có thể sử dụng được. Nếu bộ phận hỗ trợ của nhà cung cấp yêu cầu ID yêu cầu từ một header HTTP, chúng ta sẽ không có thông tin vì SDK không trả về nó.
Agent lạm dụng tính trực tiếp của SDK
Các cuộc gọi SDK client của chúng tôi giống như miền Tây hoang dã. Stripe có một mẫu. WorkOS có một mẫu khác. Slack có những đặc thù riêng. Các tích hợp khác có fetch thô, cuộc gọi SDK, logic retry tùy tiện, hoặc không có logic retry nào cả.
SDK làm cho điều sai trông có vẻ dễ dàng. Một agent luôn có thể đi thẳng đến client của nhà cung cấp và gọi stripe.customers.create(...) từ bất kỳ route nào. Điều đó có vẻ hiệu quả, nhưng nó bỏ qua nơi chia sẻ mà xác thực, retry, số liệu, nhật ký và dịch lỗi nên tồn tại. Chúng tôi có rất nhiều wrapper closure như thế này rải rác trong cơ sở mã:
1const response = await catchRateLimitError(() =>2 stripe.customers.retrieve(stripeCustomerId)3);
Nếu bạn bỏ sót một chỗ và quên không wrap cuộc gọi SDK, bạn sẽ gặp rắc rối. Một giới hạn tốc độ của Stripe và một giới hạn tốc độ của WorkOS có ý nghĩa giống nhau đối với sản phẩm của chúng tôi: upstream yêu cầu chúng tôi chậm lại. Nhưng ở cấp độ kiểu, chúng là các đối tượng hoàn toàn khác nhau. Một số mã bắt ngoại lệ dành riêng cho SDK. Một số mã bắt Error chung chung. Một số mã không bắt gì cả. Điều đó biến thành trò chơi đập chuột Sentry: sửa một lỗi 429 tại một điểm gọi, chờ cùng một loại lỗi xảy ra ở nơi khác.
SDK rất phình to
Các gói NPM openai và anthropic-ai/sdk được tự động sinh ra từ các đặc tả OpenAPI bởi Stainless. Stripe cũng được sinh ra từ đặc tả OpenAPI của Stripe. Đó là cách bạn mở rộng quy mô bảo trì một SDK công khai cho một API lớn qua hàng chục ngôn ngữ lập trình.
Nhưng một SDK công khai tuyệt vời phải phục vụ tất cả mọi người. Backend của chúng tôi không cần SDK của mọi người. Nó cần tám endpoint Stripe của chúng tôi, các endpoint người dùng và tổ chức của WorkOS và các phương thức Slack mà chúng tôi thực sự gọi. Stripe nặng 6.5 MB, workos-inc/node nặng 6.9 MB, slack/web-api nặng 7.7 MB và linear/sdk nặng 34 MB. Ở thái cực xa, googleapis lên tới 198 MB.
SDK được sinh ra mang theo toàn bộ nền tảng: hàng trăm phương thức, quá tải, trình trợ giúp phân trang, hành vi retry, phát hiện môi trường, các shim tương thích và các bề mặt cũ không thể biến mất vì ai đó, ở đâu đó, vẫn phụ thuộc vào chúng. Bên trong backend của chúng tôi, tính tổng quát đó thường là phình to. Tệ hơn, nó nằm giữa chúng tôi và dây.
Chúng ta quên rằng HTTP API là các hợp đồng API
Mọi người đôi khi nói về HTTP API như thể chúng là các chi tiết triển khai cấp thấp và SDK là giao diện ổn định thực sự. REST API công khai là một hợp đồng. Các nhà cung cấp không thể tùy tiện phá vỡ nó. Các tác giả SDK cũng biết điều này, bởi vì họ không thể buộc mọi khách hàng nâng cấp. Nhiều khách hàng vẫn chạy các phiên bản SDK cũ trong sản xuất, điều đó có nghĩa là hợp đồng dây cũ vẫn phải hoạt động.
HttpBaseClient của chúng tôi
HttpBaseClient là sự thay thế của chúng tôi cho SDK client. Các lớp con của Provider cung cấp các phần dành riêng cho nhà cung cấp: base URL, header xác thực, loại nội dung, ánh xạ lỗi và các phương thức hẹp cho các endpoint chúng tôi thực sự sử dụng. HttpBaseClient sở hữu phần còn lại: tuần tự hóa, phân tích cú pháp, lỗi vận chuyển, nhật ký có cấu trúc, số liệu, ánh xạ trạng thái và theo dõi thời lượng. Điều này thống nhất khả năng quan sát để mọi nhà cung cấp tuân theo các tiêu chuẩn nhất quán. Đây là hình dạng, được đơn giản hóa:
1abstract class HttpBaseClient<TEndpoint extends string> {2 protected abstract readonly baseUrl: string;34 protected constructor(private readonly dependency: string) {}56 protected abstract buildAuthHeaders(): Promise<Record<string, string>>;78 protected async request<TBody, TResponse>(config: {9 method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';10 path: string;11 endpoint: TEndpoint;12 body?: TBody;13 }): Promise<TResponse> {14 const url = `${this.baseUrl}${config.path}`;15 const headers = await this.buildAuthHeaders();16 const labels = {17 dependency: this.dependency,18 endpoint: config.endpoint,19 method: config.method,20 };21 const start = performance.now();2223 logInfo('upstream request starting', labels);2425 try {26 const response = await callWithMetrics(27 () =>28 fetch(url, {29 method: config.method,30 headers,31 body: config.body === undefined ? undefined : JSON.stringify(config.body),32 }),33 this.dependency,34 labels35 );3637 const body = await parseBody(response);38 if (!response.ok) throw this.mapHttpError(response, body);3940 logInfo('upstream request succeeded', {41 ...labels,42 statusCode: response.status,43 durationMs: performance.now() - start,44 });4546 return body as TResponse;47 } catch (cause) {48 logWarn('upstream request failed', {49 ...labels,50 durationMs: performance.now() - start,51 cause,52 });53 throw cause;54 }55 }56}5758// Sau đó, một wrapper Stripe trở nên nhỏ và rõ ràng:59enum StripeEndpoint {60 CustomersCreate = 'stripe/customers/create',61}6263class StripeHttpClient extends HttpBaseClient<StripeEndpoint> {64 protected readonly baseUrl = 'https://api.stripe.com';6566 constructor(private readonly apiKey: string) {67 super(ClientVendor.Stripe);68 }6970 createCustomer(body: { email: string; name: string }) {71 return this.request<typeof body, Stripe.Customer>({72 method: 'POST',73 path: '/v1/customers',74 endpoint: StripeEndpoint.CustomersCreate,75 body,76 });77 }7879 // Thêm các endpoint khác ở đây.80}
Đó là mẹo. Lớp này không cố gắng mô hình hóa tất cả Stripe. Nó mô hình hóa hành vi HTTP mà chúng tôi muốn mọi cuộc gọi của nhà cung cấp có. Stripe vẫn nhận mã hóa form. WorkOS vẫn nhận bearer auth và nội dung JSON. Slack vẫn nhận hành vi ok: false kỳ lạ trên HTTP 200. Nhưng phần còn lại của backend của chúng tôi thấy một hình dạng nhất quán.
Bạn có thể xem phiên bản dài hơn của HttpBaseClient của chúng tôi trông như thế nào tại đây, đã được sửa đổi để trở nên chung chung và dễ đọc hơn như mã ví dụ.
Nơi chúng tôi vẫn sử dụng SDK
Cách tiếp cận hiện tại của chúng tôi là kết hợp: sử dụng HTTP client của riêng mình trong thời gian chạy, nhưng vẫn giữ SDK ở những nơi mà các kiểu của chúng vẫn tiết kiệm thời gian. StripeHttpClient có thể trả về Stripe.Customer, SlackHttpClient có thể mượn các kiểu đối số của slack/web-api và các kiểu WorkOS vẫn có thể mô tả phản hồi dây.
Tôi hy vọng chúng tôi sẽ loại bỏ dần điều này theo thời gian. Khi AI trở nên tốt hơn trong việc tạo và duy trì chính xác các kiểu yêu cầu và phản hồi mà chúng tôi cần, lý do để giữ toàn bộ gói SDK chỉ vì các kiểu trở nên yếu hơn. Nhưng hành vi thời gian chạy là phần đau đớn, vì vậy đó là nơi quá trình di chuyển bắt đầu.
Chúng tôi vẫn sử dụng SDK khi SDK là ranh giới sản phẩm, không chỉ là một wrapper quanh REST. Khả năng quan sát là ví dụ rõ ràng nhất. Đối với Sentry, SDK xử lý đo lường thời gian chạy, thu thập lỗi, lan truyền phạm vi, siêu dữ liệu phát hành và các tích hợp mà chúng tôi không muốn tự làm. Điều đó khác với việc sử dụng SDK của nhà cung cấp như một client mỏng cho các cuộc gọi HTTP backend thông thường.
Không phải mọi API đều là REST qua HTTP, và điều đó không sao. Các cuộc gọi cơ sở dữ liệu là một ví dụ tốt. Sự trừu tượng hóa cấp thấp hơn của chúng tôi là BaseClient: nó cung cấp cho mỗi client cùng một hợp đồng về số liệu, nhật ký và xử lý lỗi, đồng thời cho phép một lớp con ghi đè "fetch" có nghĩa là gì đối với phương thức vận chuyển của nó.
Nơi chúng tôi đang hướng tới
Các nhà phát triển sẽ coi tài liệu API là hướng dẫn tích hợp thực sự và SDK là các triển khai tham khảo: các mẫu cho xác thực, payload, phân trang, retry và các trường hợp ngoại lệ. Phiên bản tiếp theo của "gửi SDK" có thể là "gửi một kỹ năng agent" dạy các agent cách gọi API chính xác, tái sử dụng các mẫu đúng và tránh đẩy mọi cuộc gọi thời gian chạy qua một gói của nhà cung cấp.





