FDE PulseViệc làm FDE đang mở 316Mới đăng 7 ngày qua 10Chủ đề nổi bật: Đào tạo kỹ năng FDE tại Đông Nam Á

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Observability cho hệ thống LLM: trace từng request, kiểm soát log và đối soát chi phí

Khách hỏi vì sao hóa đơn tuần này tăng vọt. Bạn chỉ trả lời được nếu mỗi lần gọi model đều để lại một span ghi token, tên tính năng và tên khách hàng.

Đồ hoạBa lớp observability cho ứng dụng LLM
  1. Span cho từng lần gọi modelgen_ai.request.model, input/output token, cache token, kèm app.customer_id, app.feature
  2. Log nội dung có kiểm soátChỉ ghi prompt khi opt-in, qua redaction, sampling và giới hạn payload
  3. Tổng hợp chi phí trong ứng dụngLangfuse nhóm usage, cost theo user, tag, loại ứng dụng; Datadog nhận dữ liệu OTel
  4. Đối soát với nhà cung cấpAdmin API: usage theo bucket 1m/1h/1d, cost USD theo ngày, theo workspace

Span ghi chi tiết từng request, lớp tổng hợp phân bổ chi phí, còn API nhà cung cấp dùng để đối soát tổng.

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Mỗi lần gọi model nên là một span mang tên model, input token, output token và các thuộc tính nghiệp vụ của riêng bạn.
  • Ghi chi phí trong ứng dụng cho từng request; dùng Usage & Cost Admin API của Anthropic để đối soát hằng ngày, không phải để đếm từng request.
  • Chỉ bật ghi nội dung prompt khi cần (opt-in), và nội dung đó phải đi qua sampling, giới hạn payload và redaction.
Chia sẻLinkedInFacebookX

Chatbot nội bộ của khách hàng đã chạy ổn được ba tuần. Rồi một sáng thứ Hai, trưởng nhóm tài chính bên họ gửi ảnh chụp hóa đơn API kèm đúng một câu hỏi: tuần trước tiền tăng vì tính năng nào, phòng ban nào dùng?

Nếu lúc đó bạn chỉ có vài dòng log print(response) rải rác trong code, bạn sẽ mất cả ngày để đoán. Nếu có trace, câu trả lời chỉ cần một câu truy vấn. Khách tin một FDE trả lời được ngay, còn FDE phải đoán thì thường kết thúc bằng một lời xin lỗi.

Bài này hướng dẫn bạn dựng observability cho một ứng dụng LLM theo ba lớp: trace để thấy request đi qua những đâu, log nội dung có kiểm soát, và chi phí được ghi ở hai nơi rồi đối soát với nhau.

Trace là xương sống, không phải log

Langfuse định nghĩa application tracing là việc ghi lại trọn vòng đời của một request khi nó đi qua hệ thống. Với ứng dụng LLM, vòng đời đó hiếm khi chỉ gồm một lần gọi model. Thường sẽ có bước truy xuất tài liệu, một hoặc vài lần gọi model, có khi thêm lời gọi tool của agent.

Đơn vị nhỏ nhất của trace là span. Hướng dẫn tracing LLM bằng OpenTelemetry của Braintrust mô tả span là một đơn vị công việc. Mỗi span gọi model mang các thuộc tính chuẩn như gen_ai.request.model, gen_ai.usage.input_tokens và gen_ai.usage.output_tokens.

Vì sao nên theo chuẩn thay vì tự đặt tên? Vì làm theo chuẩn thì sau này bạn vẫn tự do chọn backend. Datadog đã hỗ trợ native OpenTelemetry GenAI Semantic Conventions, tính từ phiên bản v1.37 của bộ quy ước này trở lên.

Bạn chỉ cần instrument một lần bằng OpenTelemetry, rồi gửi dữ liệu tới backend phù hợp, kể cả công cụ giám sát mà khách hàng đang dùng.

Lưu ý thực tế: bộ quy ước GenAI vẫn còn thay đổi, ít nhất là tính đến tháng 2/2026. Nó cũng đã chuyển sang một repository riêng của OpenTelemetry, còn trang semconv cũ không được duy trì nữa. Khi tra tên thuộc tính, bạn nên đọc ở repo mới.

Một span tốt trông như thế nào?

Lấy một ví dụ cụ thể: chatbot của khách nhận câu hỏi, truy xuất tài liệu, rồi gọi Claude. Đoạn Python dưới đây bọc lời gọi model trong một span. Các thuộc tính gen_ai.* lấy từ chuẩn, còn các thuộc tính app.* là quy ước riêng bạn tự đặt cho dự án.

from opentelemetry import trace
import anthropic

tracer = trace.get_tracer("support-bot")
client = anthropic.Anthropic()

def answer(question, docs, customer_id, feature):
    with tracer.start_as_current_span("chat") as span:
        span.set_attribute("gen_ai.request.model", MODEL)
        span.set_attribute("app.customer_id", customer_id)
        span.set_attribute("app.feature", feature)

        resp = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            messages=[{"role": "user",
                       "content": build_prompt(question, docs)}],
        )

        u = resp.usage
        span.set_attribute("gen_ai.usage.input_tokens", u.input_tokens)
        span.set_attribute("gen_ai.usage.output_tokens", u.output_tokens)
        span.set_attribute("app.cache_read_tokens",
                           u.cache_read_input_tokens or 0)
        span.set_attribute("app.cache_creation_tokens",
                           u.cache_creation_input_tokens or 0)
        return resp

Chính hai thuộc tính app.customer_id và app.feature mới trả lời được câu hỏi của bộ phận tài chính. Token mà thiếu ngữ cảnh nghiệp vụ thì chỉ là một con số. Khi mỗi span mang tên tính năng, bạn nhóm theo app.feature là thấy ngay tính năng nào ngốn token.

Tách riêng cache token cũng có lý do. Usage endpoint của Anthropic đo riêng input chưa cache, input đã cache, token tạo cache và output token. Nếu trace của bạn gộp tất cả vào một con số, bạn sẽ không bao giờ khớp được với số liệu của nhà cung cấp.

Chi phí phải ghi ở hai nơi

Nhiều kỹ sư nghĩ chỉ cần kéo dữ liệu chi phí từ nhà cung cấp là đủ. Thực ra không đủ, và tài liệu của chính nhà cung cấp cho thấy lý do.

Usage & Cost Admin API của Anthropic cho phép truy cập dữ liệu sử dụng và chi phí lịch sử của tổ chức bằng code, tương tự trang Usage và Cost trong Console. Nhưng dữ liệu không có ngay: thường khoảng 5 phút sau khi request hoàn tất nó mới xuất hiện, đôi khi còn trễ hơn.

Ngoài ra, Cost API chỉ trả về USD theo từng ngày, còn usage endpoint chia bucket theo 1m, 1h hoặc 1d. Dữ liệu được phân bổ theo workspace, không theo khách hàng cuối hay tính năng của bạn. Vì vậy nó hợp để đối soát, không hợp để tính tiền từng request.

Ghi trong ứng dụng (trace) API của nhà cung cấp
Độ chi tiết Từng request, từng span Bucket 1m/1h/1d cho usage; theo ngày cho cost
Phân bổ Theo khách hàng, tính năng, tag bạn tự gắn Theo workspace
Độ trễ Gần như tức thì Thường khoảng 5 phút, có khi lâu hơn
Mục đích Debug, phân bổ chi phí nội bộ Đối soát với hóa đơn cho bộ phận tài chính

Ở phía ứng dụng, Langfuse ghi usage và cost cho từng lần generation. Công cụ này có sẵn danh sách model phổ biến kèm tokenizer để tự suy ra chi phí. Khi bạn gửi kèm số liệu thật, giá trị đã ingest được ưu tiên hơn giá trị suy ra.

Metrics API của Langfuse còn tổng hợp được usage và cost theo user, tag hoặc loại ứng dụng.

Lời khuyên: luôn gửi số token thật lấy từ response thay vì để công cụ tự đếm. Con số do tokenizer suy ra chỉ là phương án dự phòng, đừng coi nó là nguồn sự thật.

Đối soát: cái bẫy Priority Tier và Admin key

Ý tưởng của job đối soát rất đơn giản. Mỗi sáng, bạn cộng token của ngày hôm qua từ trace, kéo số liệu cùng ngày từ usage endpoint rồi so hai con số. Nếu chênh lệch lớn, rất có thể còn đường gọi model nào đó chưa được instrument.

Có hai chi tiết dễ vấp. API này đòi Admin credentials, API key của workspace không dùng được, nên bạn cần xin quyền từ phía khách thật sớm, đừng để đến tuần nghiệm thu. Ngoài ra, chi phí Priority Tier dùng mô hình tính tiền khác và không có trong cost endpoint, nên phải theo dõi qua usage endpoint.

Log nội dung: chỉ bật khi có chủ đích

Khi debug một câu trả lời sai, bạn sẽ muốn xem nguyên văn prompt và completion. Quy ước GenAI của OpenTelemetry định nghĩa các thuộc tính ghi nội dung prompt theo dạng opt-in, tức là mặc định không ghi. Thiết kế này hợp lý, vì prompt của khách doanh nghiệp thường chứa tên người, số hợp đồng và dữ liệu nội bộ.

Hướng dẫn của Braintrust nhấn mạnh rằng tracing LLM trên production cần cấu hình cẩn thận sampling, batching, giới hạn payload và redaction. Thứ tự hợp lý là viết hàm che dữ liệu trước, đặt giới hạn kích thước payload, chọn tỷ lệ sampling cho phần nội dung, rồi mới bật ghi.

Về hiệu năng, SDK của Langfuse gửi dữ liệu trace bất đồng bộ ở background, nên tracing không chặn đường xử lý request. Nhưng bất đồng bộ không có nghĩa là miễn phí: payload càng lớn thì càng tốn băng thông và chi phí lưu trữ.

Tự làm theo thứ tự nào?

Trước tiên, liệt kê mọi chỗ trong code có gọi model, kể cả các lần gọi trong vòng lặp agent. Bọc từng chỗ bằng span có thuộc tính gen_ai.* chuẩn, cộng thêm hai, ba thuộc tính nghiệp vụ mà khách hàng thực sự quan tâm.

Tiếp theo, chọn backend theo hạ tầng của khách. Nếu cần một công cụ chuyên cho LLM, có sẵn phần tính chi phí, hãy dùng Langfuse. Nếu khách đã dùng Datadog, hãy gửi dữ liệu OpenTelemetry vào đó. Sau cùng mới dựng job đối soát hằng ngày và cấu hình redaction trước khi bật ghi nội dung.

Những lỗi hay gặp

Lỗi phổ biến nhất là chỉ trace lời gọi model chính, bỏ sót các lần gọi phụ như tóm tắt, phân loại hay retry. Job đối soát sẽ sớm làm lộ lỗi này, miễn là bạn có dựng job đó.

Lỗi thứ hai là coi Cost API như đồng hồ đo thời gian thực, rồi cảnh báo sai vì dữ liệu chưa về kịp. Lỗi thứ ba là bật ghi toàn bộ nội dung prompt ngay từ ngày đầu, để rồi phải xóa dữ liệu khi đội bảo mật của khách phát hiện.

Nếu bạn là developer Việt Nam muốn chuyển sang FDE, đây là kỹ năng dễ chứng minh. Trong CV, đừng viết “có kinh nghiệm observability”. Hãy viết rằng bạn đã gắn thuộc tính nghiệp vụ vào trace và trả lời được câu hỏi chi phí theo từng tính năng. Khi đọc JD, các từ như OpenTelemetry, Langfuse hay cost attribution là dấu hiệu công việc có mảng này.

Bài tập tuần này

Lấy một ứng dụng LLM nhỏ bạn từng viết, instrument nó theo đoạn code trên rồi chạy 50 request với hai giá trị app.feature khác nhau. Sau đó trả lời bằng một truy vấn duy nhất: tính năng nào tốn nhiều output token hơn, và gấp bao nhiêu lần?

Nếu làm được trong năm phút, bạn đã sẵn sàng cho buổi sáng thứ Hai có ảnh chụp hóa đơn kia.

7 nguồn
Đọc tiếp trên lộ trình · Chặng 6: Đo lườngOpenTelemetry cho ứng dụng LLM: một lần đo, gửi trace sang cả Datadog lẫn Langfuse tự hostKhi cả Datadog lẫn Langfuse đều đọc chung bộ thuộc tính GenAI của OpenTelemetry, FDE không còn phải chọn phe ngay từ dòng code đầu tiên.