Từ solutions engineer sang FDE: viết lại một PoC đến chuẩn production
Bản demo từng làm khách hàng gật gù sẽ hỏng ngay ở file CSV lỗi đầu tiên, và ở vai trò FDE, người phải sửa nó là bạn.
Chuyển sang FDE là chuyển từ cột trái sang cột phải: code phải chịu được thực tế và bạn làm chủ hệ thống đang chạy.
Đồ hoạ: FDE Times
Tóm tắt nhanh
- Từ solutions engineer lên FDE là đổi từ code demo sang code production, và tự chịu trách nhiệm về kết quả deployment.
- Cách luyện hiệu quả nhất: lấy một PoC có sẵn rồi thêm xử lý lỗi, test, tài liệu kiến trúc và deploy nó vào môi trường thật.
- Ở phỏng vấn FDE, bạn phải giải thích được vì sao chọn cách deploy đó, chứ không chỉ cho thấy code chạy được.
Bạn vừa demo xong một công cụ phân loại ticket hỗ trợ bằng LLM, và phía khách hàng rất hài lòng. Thứ Hai tuần sau công cụ được đưa vào production. Đến ba giờ sáng, một file CSV có dòng trống và model trả về một câu chào thay vì JSON, thế là cả pipeline dừng.
Nếu bạn là solutions engineer, việc của bạn thường kết thúc khi hợp đồng được ký. Còn ở vị trí FDE, người phải dậy sửa lúc ba giờ sáng chính là bạn.
Bài hướng dẫn chuyển nghề của fde.academy tóm sự khác biệt này thành hai thay đổi: bạn viết code production thay vì code demo, và bạn chịu trách nhiệm về kết quả deployment thay vì bàn giao lại sau khi bán xong.
Tin tốt là khoảng cách này có hình dạng rõ ràng, và bạn có thể luyện từng phần một. Phần dưới đi qua một PoC thật ngắn, sửa dần nó đến chuẩn production, rồi chỉ cách thể hiện kỹ năng đó trong CV và khi phỏng vấn.
Làm chủ thiết kế chưa phải là làm chủ hệ thống
Aced, khi so sánh hai vai trò, mô tả rằng FDE dành phần lớn tuần làm việc để viết code production. Trong khi đó, solutions architect làm chủ bản thiết kế chứ không làm chủ hệ thống đang chạy. Nếu hệ thống hỏng hoặc không mang lại giá trị, việc khắc phục là trách nhiệm trực tiếp của FDE.
Sundeep Teki, trong hướng dẫn phỏng vấn FDE năm 2026, nói còn thẳng hơn. Bạn không tạo ticket, không đổ lỗi cho team khác, mà tự sửa. Với người xuất thân từ consulting, đây mới là thay đổi lớn nhất, lớn hơn cả chuyện học thêm cú pháp hay framework.
fde.academy có một câu đáng nhớ: PoC chỉ cần chứng minh một điểm, còn code production phải chịu được thực tế. “Thực tế” ở đây là dữ liệu bẩn, mạng chập chờn, API trả về thứ bạn không lường trước, và một người khác phải đọc code của bạn sáu tháng sau.
Một PoC sáu dòng sẽ hỏng ở đâu?
Hãy thử hình dung đoạn code demo cho buổi gặp khách hàng ở trên:
import csv, json
from llm import client
for row in csv.DictReader(open("tickets.csv")):
out = client.complete(f"Phân loại ticket: {row['text']}")
print(row["id"], json.loads(out)["label"])
Trên mười ticket mẫu đã chọn sẵn, đoạn code chạy rất mượt. Nhưng đem nó vào môi trường thật thì có ít nhất năm chỗ sẽ hỏng. Ticket trống vẫn bị gửi lên model, ticket dài vài trang có thể vượt giới hạn context, và API timeout là cả vòng lặp chết.
Model trả về văn bản thường thì json.loads ném lỗi. Còn nếu model bịa ra một nhãn không tồn tại, nhãn đó vẫn lặng lẽ đi tiếp xuống hệ thống phía sau.
Bản sửa lại không cần thông minh hơn. Nó chỉ cần coi mỗi chỗ có thể hỏng là chuyện chắc chắn sẽ xảy ra. Đầu tiên, khai báo rõ những nhãn hợp lệ và một logger riêng:
import json, logging, time
from llm import client
LABELS = {"billing", "bug", "account", "other"}
log = logging.getLogger("triage")
Tiếp theo, tách một lần gọi model thành hàm riêng, có cắt độ dài input, đặt timeout và kiểm tra nhãn trả về:
def call_model(text: str) -> str | None:
out = client.complete(f"Phân loại ticket: {text[:4000]}", timeout=20)
label = json.loads(out).get("label")
if label in LABELS:
return label
log.warning("nhãn lạ: %r", label)
return None
Cuối cùng là hàm classify, lo phần input trống, retry có giãn cách và lối thoát khi mọi lần thử đều hỏng:
def classify(text: str, retries: int = 3) -> str:
if not text or not text.strip():
return "other"
for attempt in range(retries):
try:
label = call_model(text)
if label:
return label
except (TimeoutError, json.JSONDecodeError) as e:
log.warning("lần %d thất bại: %s", attempt + 1, e)
time.sleep(2 ** attempt)
return "needs_review"
Hãy để ý dòng cuối. Hệ thống không crash mà đưa ticket vào hàng chờ needs_review để con người xem lại.
Đây là một quyết định nghiệp vụ, và bạn phải thống nhất nó với khách hàng: họ chấp nhận bao nhiêu ticket đi vào hàng chờ đó mỗi ngày? Code production luôn chứa những câu hỏi kiểu này, trong khi code demo bỏ qua chúng.
Tiếp theo là test cho đúng những tình huống vừa liệt kê:
def test_empty_ticket_goes_to_other():
assert classify(" ") == "other"
def test_non_json_reply_goes_to_review(monkeypatch):
monkeypatch.setattr(client, "complete", lambda *a, **k: "xin chào")
assert classify("Tôi bị trừ tiền hai lần", retries=1) == "needs_review"
Hai test này mất chưa tới mười phút để viết. Nhưng chúng biến sự cố lúc ba giờ sáng thành một dòng log cảnh báo mà bạn đọc lúc chín giờ sáng.
Bốn bước đưa PoC lên production
Lời khuyên thực tế nhất của fde.academy là lấy một PoC có sẵn rồi xây lại nó theo chuẩn production. Họ chia việc thành bốn bước: thêm xử lý lỗi, viết test, viết tài liệu kiến trúc và deploy vào một môi trường thật. Ví dụ trên mới làm xong hai bước đầu.
Bước thứ ba hay bị xem nhẹ. Tài liệu kiến trúc không cần dài, chỉ cần trả lời được dữ liệu đi vào từ đâu, ra đi đâu, gọi những dịch vụ ngoài nào và khi hỏng thì xem log ở đâu. Với ví dụ phân loại ticket, một trang như sau là đủ để bắt đầu:
# triage-service
Vào: ticket từ hệ thống của khách (id, text)
Ra: một nhãn trong {billing, bug, account, other, needs_review}
Gọi ngoài: API LLM, timeout 20 giây, retry tối đa 3 lần
Khi hỏng: xem logger "triage", đếm số ticket needs_review mỗi ngày
Người vận hành: tên bạn, cách liên hệ khi có sự cố
Người đọc trang này có thể là kỹ sư phía khách hàng, những người sẽ vận hành cùng bạn. Dòng cuối là dòng quan trọng nhất, vì nó ghi rõ ai làm chủ hệ thống.
Bước thứ tư mới thật sự kiểm tra năng lực của bạn. Bài viết của Coursera về AI engineer liệt kê việc biến model thành API để các ứng dụng khác tích hợp và việc quản lý hạ tầng production là trách nhiệm cốt lõi. Với ví dụ trên, phần bọc classify thành endpoint, lưu trong file api.py, chỉ cần vài dòng:
from fastapi import FastAPI
from pydantic import BaseModel
from triage import classify
app = FastAPI()
class Ticket(BaseModel):
id: str
text: str
@app.post("/classify")
def classify_ticket(t: Ticket) -> dict:
return {"id": t.id, "label": classify(t.text)}
@app.get("/health")
def health() -> dict:
return {"ok": True}
Endpoint /health là thứ bạn gắn vào công cụ giám sát để biết dịch vụ còn sống. Nhưng code nằm trên laptop thì chưa tính là deploy. Bước tiếp theo là đóng gói service để nó chạy giống hệt nhau ở mọi nơi, chẳng hạn bằng một Dockerfile ngắn:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
Rồi build, chạy và tự kiểm tra như một người dùng bên ngoài:
docker build -t triage-service .
docker run -d -p 8000:8000 -e LLM_API_KEY="$LLM_API_KEY" triage-service
curl http://localhost:8000/health
curl -X POST http://localhost:8000/classify \
-H "Content-Type: application/json" \
-d '{"id": "t-1", "text": " "}'
Để ý cờ -e: API key đi vào qua biến môi trường, không bao giờ nằm trong code hay image. Lệnh curl cuối gửi một ticket trống, đúng ca mà bản demo từng làm sập pipeline, và lần này bạn phải nhận về nhãn other.
Khi image chạy ổn trên máy, hãy đưa nó lên một máy chủ hoặc dịch vụ chạy container mà người khác gọi được qua mạng, rồi gắn /health vào công cụ giám sát. Ở chỗ khách hàng, đó thường là hạ tầng họ đang dùng sẵn, nên câu hỏi đầu tiên nên đặt ra là service sẽ sống ở đâu và ai được quyền xem log.
Vòng phỏng vấn sẽ kiểm tra đúng khoảng trống này
Theo hướng dẫn của Sundeep Teki, vòng coding của FDE có độ khó khoảng LeetCode medium nhưng được đặt trong bối cảnh khách hàng. Ở Palantir, bài toán được đặt trong ngữ cảnh một thứ bạn đang xây cho người dùng cuối.
Vòng technical deep dive thì kéo dài 60 phút, không viết code, và chấm điểm dựa trên câu hỏi làm rõ, khả năng tìm nguyên nhân gốc và cách bạn cân nhắc trade-off.
Coursera cũng ghi nhận điều tương tự ở phỏng vấn AI engineer: bạn sẽ phải giải thích lý do chọn cách phát triển, deploy và mở rộng một giải pháp. Vòng deep dive không đòi bạn gõ code, nhưng nó vẫn đòi kinh nghiệm vận hành.
Nếu bạn chưa từng tự deploy, phần trade-off sẽ lộ ra ngay: bạn nói được “nên có retry”, nhưng không trả lời được nên retry bao nhiêu lần và vì sao.
Thêm một bẫy nữa khi đọc job description. Hướng dẫn của Teki cho biết Anthropic không dùng chức danh FDE mà tuyển Solutions Architect cho team Applied AI, một vị trí pre-sales có nhiệm vụ trở thành người cố vấn kỹ thuật mà khách hàng tin tưởng. Chức danh không cho bạn biết ai làm chủ production.
Khi đọc JD, hãy tìm những cụm như “own deployment”, “on-call”, “ship to production”; nếu JD chỉ toàn “workshop” và “demo” thì đó vẫn là vị trí tư vấn.
Những lỗi người chuyển nghề hay mắc
Lỗi phổ biến nhất là để một demo thật bóng bẩy trong CV thay vì một hệ thống đang chạy. Dòng “xây demo chatbot cho 5 khách hàng” nói lên ít điều hơn dòng “deploy API phân loại ticket, có test, retry và hàng chờ review thủ công, đang vận hành”.
Nhà tuyển dụng FDE muốn thấy bạn từng làm chủ một hệ thống đang chạy, chứ không chỉ một bản thiết kế.
Lỗi thứ hai là dùng try/except để nuốt mọi lỗi cho code chạy qua. Như vậy là che lỗi chứ không phải xử lý lỗi. Mỗi nhánh fallback phải có log và phải dẫn đến một trạng thái mà ai đó nhìn thấy được, như needs_review trong ví dụ.
Lỗi thứ ba là nghĩ mình phải có bằng cấp mới chuyển được. Coursera, khi viết về nghề AI engineer chứ không riêng FDE, cho rằng không nhất thiết phải có bằng, và người chuyển nghề có thể vào nghề qua dự án và chứng chỉ.
Nếu nhắm tới FDE, lời khuyên là chọn dự án giống ví dụ ở trên: một service bạn tự deploy, có test và tài liệu đi kèm.
Lỗi cuối cùng tinh vi hơn: khi gặp sự cố, theo phản xạ bạn đi tìm team chịu trách nhiệm. Ở vai trò FDE, team đó chính là bạn.
Khoảng cách giữa consultant và FDE nằm ở câu hỏi bạn tự đặt ra sau buổi demo. Một bên hỏi khách hàng có thích không, còn bên kia hỏi thứ gì sẽ làm hệ thống này hỏng đêm nay.