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

Công cụ

LangGraph và interrupt: cách bắt agent dừng lại xin phép trước khi làm việc quan trọng

Ở công ty khách hàng, câu hỏi khó nhất thường không phải agent có làm được hay không, mà là ai bấm nút cho phép nó làm, và hàm interrupt của LangGraph được viết ra cho đúng khoảnh khắc đó.

Đồ hoạMột vòng xin duyệt với interrupt trong LangGraph
  1. 1Node chạy tới interruptĐồ thị tạm dừng trước hành động quan trọng, gửi payload cần duyệt ra ngoài
  2. 2Checkpointer lưu stateState của thread được lưu thành checkpoint; production cần checkpointer bền vững
  3. 3Người duyệt quyết địnhCon người xem payload và chọn đồng ý hay từ chối, có thể sau nhiều giờ
  4. 4Resume với cùng thread_idGọi Command(resume=...) với thread_id cũ; thread_id mới sẽ mở luồng trống
  5. 5Node chạy lại từ đầuinterrupt trả về quyết định; code trước interrupt chạy thêm một lần

Agent dừng lại, lưu state theo thread_id, rồi chạy tiếp khi người duyệt gửi quyết định về qua Command(resume=...).

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Hàm interrupt tạm dừng đồ thị tại một điểm do bạn chỉ định, chờ đầu vào từ bên ngoài, rồi nhận kết quả duyệt qua Command(resume=...).
  • Muốn dùng interrupt thì phải có checkpointer, và production cần loại bền vững; thread_id quyết định bạn chạy tiếp luồng cũ hay mở luồng mới.
  • Khi resume, node chạy lại từ đầu, nên mọi thao tác không idempotent phải nằm sau interrupt.
Chia sẻLinkedInFacebookX

Hãy hình dung agent bạn deploy cho khách hàng đã soạn xong lệnh hoàn tiền, và chỉ còn một bước nữa là gửi đi. Người phụ trách bên khách hàng muốn tự xem lại trước khi tiền ra khỏi tài khoản, có khi sau vài phút, có khi phải đến sáng hôm sau.

Đội LangChain đã viết thẳng lý do cho chuyện này trên blog chính thức ngày 14/12/2024: agent có thể mạnh, nhưng không hoàn hảo. Hàm interrupt của LangGraph ra đời từ chính nhận định đó, và theo đội LangChain, nó được thiết kế để chạy trong môi trường production.

Với một FDE, đây không phải chi tiết kỹ thuật phụ. Khi agent chạm vào tiền hay hệ thống thật của khách hàng, bạn nên thiết kế sẵn một chỗ để con người nói “khoan đã” ngay từ đầu, thay vì đợi khách hàng đòi hỏi.

LangGraph là gì, và nó không phải là gì?

LangGraph tự giới thiệu là framework điều phối mức thấp để xây, quản lý và triển khai agent có trạng thái, chạy dài hạn. Công cụ này do LangChain Inc, đội đứng sau LangChain, xây dựng, nhưng bạn có thể dùng nó mà không cần đến LangChain. Mã nguồn được phát hành theo giấy phép MIT.

Chữ “mức thấp” cần đọc kỹ. LangGraph không đưa cho bạn một agent dựng sẵn, mà đưa cho bạn các khối để tự lắp: đồ thị các bước, trạng thái đi qua từng bước, và chỗ để dừng.

Hai lợi ích được nhóm phát triển nhấn mạnh là durable execution, tức agent sống sót qua lỗi và chạy được lâu, và human-in-the-loop, tức kiểm tra và sửa trạng thái agent ở bất kỳ điểm nào khi đang chạy.

Ba mảnh ghép cho một điểm dừng

Theo tài liệu chính thức, interrupt cho phép tạm dừng đồ thị tại những điểm cụ thể và chờ đầu vào bên ngoài rồi mới đi tiếp. Muốn dừng được thì phải có chỗ lưu trạng thái, nên tài liệu yêu cầu một checkpointer và khuyến nghị dùng checkpointer bền vững khi lên production.

Checkpointer lưu trạng thái của mỗi thread thành các checkpoint. Cùng một cơ chế đó phục vụ nhiều việc: giữ mạch hội thoại, human-in-the-loop, time travel và chịu lỗi. Mảnh thứ ba là thread_id: dùng lại giá trị cũ thì tiếp tục đúng checkpoint đó, dùng giá trị mới thì mở một thread hoàn toàn mới với state rỗng.

Còn quyết định của người duyệt đi vào bằng cách nào? Giá trị bạn truyền vào Command(resume=...) trở thành giá trị trả về của chính lời gọi interrupt. Nhìn từ bên trong node, interrupt giống một lời gọi hàm bình thường: gọi xong thì chờ, và thứ trả về chính là quyết định của người duyệt.

Một node xin duyệt, viết tối giản

Theo tài liệu, cách dùng interrupt phổ biến nhất là dừng trước một hành động quan trọng để xin duyệt. Đoạn dưới đây là một ví dụ minh họa đủ để chạy thử: một đồ thị một node, checkpointer lưu trong bộ nhớ, lần gọi đầu dừng ở interrupt, lần gọi sau resume bằng cùng thread_id.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command

class State(TypedDict):
    so_tien: int
    ket_qua: str

def thuc_hien_hoan_tien(state):
    print("Đã hoàn tiền:", state["so_tien"])

def duyet_hoan_tien(state: State):
    quyet_dinh = interrupt({
        "hanh_dong": "hoàn tiền",
        "so_tien": state["so_tien"],
    })
    if quyet_dinh == "dong_y":
        thuc_hien_hoan_tien(state)   # chỉ chạy SAU khi đã được duyệt
    return {"ket_qua": quyet_dinh}

builder = StateGraph(State)
builder.add_node("duyet_hoan_tien", duyet_hoan_tien)
builder.add_edge(START, "duyet_hoan_tien")
builder.add_edge("duyet_hoan_tien", END)

# InMemorySaver chỉ để thử trên máy; production cần checkpointer bền vững
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "yeu-cau-hoan-tien-001"}}

# Lần chạy đầu: đồ thị dừng tại interrupt, state được lưu theo thread_id
ket_qua = graph.invoke({"so_tien": 100}, config=config)
print(ket_qua["__interrupt__"])   # payload đang chờ người duyệt

# Khi người duyệt bấm nút, gọi lại với CÙNG config (cùng thread_id)
graph.invoke(Command(resume="dong_y"), config=config)

Phía giao diện duyệt có thể là bất cứ thứ gì khách hàng đang dùng. Việc của bạn là nhận payload từ interrupt, hiển thị cho đúng người, rồi gửi quyết định về qua Command(resume=...) với đúng thread_id. Gửi nhầm sang thread_id mới, bạn sẽ nhận về một luồng trống chứ không phải lệnh hoàn tiền đang chờ.

Cái bẫy nằm ở dòng code phía trên interrupt

Tài liệu cảnh báo rõ: khi resume, node chạy lại từ đầu, nên mọi dòng code đứng trước interrupt sẽ chạy thêm một lần nữa. Nguyên tắc rút ra là không đặt thao tác không idempotent trước interrupt.

Thử hình dung bạn đặt lệnh gửi email “yêu cầu của anh đang chờ duyệt” ngay trước interrupt. Người duyệt bấm đồng ý, node chạy lại, và khách hàng nhận email thứ hai. Thay email bằng một lệnh ghi vào hệ thống kế toán, lỗi này đủ để làm mất lòng tin của cả dự án.

Cách sửa đơn giản nhất là chuyển lệnh gửi email xuống sau interrupt, hoặc tách nó ra khỏi node để phía giao diện duyệt lo việc thông báo. Nếu buộc phải giữ nó phía trên, hãy làm nó idempotent: chẳng hạn ghi lại theo thread_id rằng email đã gửi, và kiểm tra dấu đó trước khi gửi lần nữa.

Khi nào nên chọn LangGraph?

Hãy cân nhắc LangGraph khi quy trình của khách hàng có các bước rõ ràng, có hành động không thể rút lại, và có người chịu trách nhiệm duyệt nhưng không ngồi chờ sẵn. Khả năng chạy dài hạn và lưu trạng thái là thứ giúp agent chờ cả đêm mà không mất việc đang làm dở.

Cái giá là bạn phải tự quyết nhiều thứ, vì đây là công cụ mức thấp: chọn checkpointer bền vững cho production, quản lý thread_id gắn với từng yêu cầu nghiệp vụ, và rà từng node xem dòng nào sẽ chạy lại. Nếu quy trình chỉ là một lượt hỏi đáp không cần ai duyệt, phần lớn sức mạnh này bạn sẽ không dùng đến.

Học gì trước, và ghi vào CV thế nào?

Thứ tự hợp lý gồm ba chặng: checkpointer cùng thread_id, rồi cặp interrupt và Command(resume=...), cuối cùng mới là bài học về node chạy lại. Hai chặng đầu là cơ chế, chặng cuối là thứ phân biệt người từng chạy thật với người chỉ đọc tài liệu.

Khi đọc JD cho vị trí FDE hay AI engineer, hãy để ý các cụm như “human-in-the-loop”, “stateful agent” hay “long-running workflow”. Trong CV, thay vì ghi “đã dùng LangGraph”, hãy viết cụ thể bạn đặt điểm duyệt trước hành động nào, state được lưu ở đâu, và bạn xử lý chuyện resume ra sao.

Khách hàng hiếm khi hỏi agent của bạn thông minh đến đâu. Họ hỏi nó sẽ dừng ở đâu, và ai được quyền cho nó đi tiếp.

5 nguồn
Đọc tiếp trên lộ trình · Chặng 3: AI ứng dụngInstructor: khi LLM trả sai schema, gửi lỗi ngược lại và hỏi thêm lần nữaMỗi khách hàng chạy một provider khác nhau, nên FDE cần một lớp kiểm soát schema không phụ thuộc vào API của bất kỳ hãng nào, và cặp Pydantic cùng Instructor được dựng cho đúng việc đó.