LangChain trên API tương thích OpenAI
Hai trường ChatOpenAI cần để nói chuyện với endpoint này, vì sao invoke, stream và tool calling chạy nguyên xi, và vì sao cache read giữ cho vòng lặp agent rẻ.
LangChain không cần biết nó đang nói chuyện với chúng tôi. Chat model OpenAI của nó đọc base URL ngay từ constructor của chính nó, còn gateway này trả lời đúng theo wire format của OpenAI — nên một chain viết cho OpenAI chạy được ở đây ngay khi đổi hai tham số. Không có gì bên dưới là bản fork của langchain-openai, một lớp bọc quanh nó hay một bản vá của nó: vẫn đúng package bạn đang dùng, chỉ là được cấu hình lại.
Hai trường thật sự quan trọng
ChatOpenAI nhận một danh sách tham số dài và gần như tất cả cứ để nguyên. Chỉ hai cái thì không — địa chỉ nó gửi tới, và cái key nó gửi kèm:
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="terra",
base_url="https://aifromu.com/v1",
api_key=os.environ["AIFROMU_API_KEY"],
)
print(llm.invoke("Name three uses for a cache read.").content)
Base URL đó do chính trang này in ra chứ không phải gõ tay vào đây. Nó là địa chỉ mà bản triển khai này thật sự phục vụ, nên đoạn code chạy được với đúng host bạn đang đọc nó trên đó, thay vì với một hostname ai đó đã nhét cứng vào ví dụ từ lâu. Còn key thì lấy từ environment, vì cùng một lý do mà key luôn được lấy từ đó: một key viết thẳng vào constructor là một key đã nằm trong repository, và xoay vòng nó sau đó nghĩa là sửa code chứ không phải đổi một biến.
Mọi thứ còn lại của object đều là của thư viện. invoke trả về message đã hoàn tất và .content là phần chữ; đưa vào một danh sách message cũng được như đưa một chuỗi trần; ChatPromptTemplate | llm | StrOutputParser() ghép lại y hệt như trước. Không có phần nào trong đó là thứ chúng tôi được phép đổi — và đó chính là toàn bộ lý do để phục vụ wire format của người khác thay vì bịa ra một cái của riêng mình.
Streaming, tool và structured output
llm.stream("...") đưa câu trả lời về từng mảnh một, đúng lúc model viết ra. Các server-sent event đi thẳng qua gateway này tới tiến trình của bạn, nên vòng lặp bạn viết quanh chúng vẫn là vòng lặp của chính thư viện:
for chunk in llm.stream("Explain a cache read in two sentences."):
print(chunk.content, end="", flush=True)
Cứ dùng nó cho mọi thứ dài. Một lượt sinh chữ chạy tới vài phút phải đẩy được cái gì đó lên đường truyền trước khi một kết nối nằm im bị đóng ở đâu đó giữa tiến trình của bạn và model, và streaming chính là thứ đẩy — bài hướng dẫn về streaming nói rõ cái mốc đó nằm ở đâu và phải làm gì với nó.
Tool calling và structured output không có mục riêng ở đây, và đó mới là điều đáng nói. bind_tools và with_structured_output vẫn dựng đúng những request body mà chúng vẫn dựng, gateway phục vụ đúng wire format của OpenAI, và câu trả lời quay về đúng cái hình dạng LangChain vốn đã parse được. Không có gì phải port, cũng không có gì phải lách.
Nên gọi tên model nào
model= nhận một trong bốn alias, chọn theo việc cần làm chứ không theo model của ai: astra cho suy luận đầu bảng trên cửa sổ 1 triệu token, sol cho suy luận sâu và coding khó, terra cho việc trợ lý hằng ngày — RAG, agent, tool, coding, và đó là lý do nó nằm trong đoạn code ở trên — còn luna cho bản nháp nhanh, phân loại và chat khối lượng lớn.
Một chain vốn đã gửi tên model kiểu OpenAI thì không phải sửa từng chỗ gọi. Những cái tên đó được nhận trên đường truyền và được đọc thành đúng hạng cân mà chúng thuộc về: một cái tên mini hay nano là lệnh gọi rẻ, khối lượng lớn của ai đó và được phục vụ trên luna; một cái tên gpt-6 là tầng đầu bảng của năm nay và được phục vụ trên astra; còn dòng 4 thông thường nằm giữa hai bên, trên terra. Câu trả lời vọng lại đúng chuỗi bạn đã gửi, nên client của bạn vẫn nhận ra đó là request của chính nó — và một cái tên không khớp luật nào thì bị từ chối chứ không được đoán bừa: đoán là tính sai tiền theo cả hai chiều cùng lúc, mà chiều nào cũng chỉ lộ ra khi có người ngồi đọc sổ.
Chain chạy vòng lặp, và cache read
Một agent là một vòng lặp, và vòng lặp thì gửi lại phần đầu của nó. Mỗi lượt của agent executor lại mang system prompt, các định nghĩa tool và toàn bộ transcript từ đầu ngược lên đường truyền; một chain RAG đặt cùng một đoạn hướng dẫn trước mỗi passage lấy về cũng làm đúng chuyện đó ở quy mô nhỏ hơn. Chính cái phần đầu ấy, chứ không phải câu trả lời, mới là chỗ ngốn tiền của một chain chạy dài.
Và đó cũng là phần rẻ khi gửi lại. Một token đọc lại từ prefix đã cache được tính bằng một phần mười mức input của chính model đó, nên một system prompt giữ nguyên từng byte qua mỗi lượt chính là thứ giữ cho vòng lặp agent rẻ. Hãy xếp message theo đúng thứ tự ấy: phần không bao giờ đổi lên trước, tài liệu lấy về và lượt của người dùng xuống sau.
Không có chỗ nào trong đó bắt bạn phải tin suông. Portal liệt kê từng request, nên phần tiết kiệm hiện ra ngay chỗ bạn nhìn thấy được — từng lượt một, đối chiếu với chính lần chạy đã tiêu nó.
Một token đọc lại từ prefix đã cache được tính bằng một phần mười mức input của chính model đó, nên một system prompt giữ nguyên từng byte qua mỗi lượt chính là thứ giữ cho vòng lặp agent rẻ.