Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 6 — Human-in-the-Loop: interrupt, duyệt và can thiệp giữa chừng

Đã đọc 0/8 mục · còn khoảng 3 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Agent tự động hoàn toàn nghe hấp dẫn, nhưng với hành động rủi ro — gửi email cho khách, xóa dữ liệu, thanh toán — bạn cần con người duyệt trước khi thực thi. LangGraph hỗ trợ điều này nhờ interrupt: tạm dừng graph, chờ input người, rồi tiếp tục đúng chỗ đã dừng. Cơ chế này dựa trực tiếp trên checkpoint ở bài 5, nên checkpointer là điều kiện bắt buộc.

Hai cách tạm dừng

  • Static interrupt: khai báo dừng trước/sau một node ngay lúc compile.
  • Dynamic interrupt: gọi hàm interrupt() ngay trong node khi cần, linh hoạt theo dữ liệu thực tế.

Static interrupt

app = builder.compile(
    checkpointer=checkpointer,
    interrupt_before=["send_email"]   # dừng ngay trước node này
)

config = {"configurable": {"thread_id": "t-1"}}
app.invoke(inputs, config)      # chạy tới trước send_email rồi dừng

# ... người xem xét state, quyết định ...
app.invoke(None, config)        # None = tiếp tục từ chỗ dừng

Khi graph dừng, state đã được checkpoint. Bạn kiểm tra bằng app.get_state(config) để xem agent "định làm gì", rồi resume bằng cách invoke với None.

Dynamic interrupt

from langgraph.types import interrupt, Command

def review_node(state) -> dict:
    decision = interrupt({"draft": state["draft"]})  # ném ra chờ người
    return {"approved": decision == "ok"}

# resume với dữ liệu người nhập
app.invoke(Command(resume="ok"), config)

interrupt(payload) gửi payload ra ngoài cho người xem, và khi bạn resume bằng Command(resume=value), chính value đó trở thành giá trị trả về của interrupt(). Nhờ vậy người dùng có thể duyệt, sửa, hoặc từ chối một cách linh hoạt.

Ba mẫu human-in-the-loop hay dùng

  • Approve/Reject: dừng trước hành động rủi ro, người bấm duyệt hay hủy.
  • Edit state: người sửa trực tiếp nội dung agent tạo trước khi tiếp tục (dùng app.update_state).
  • Cung cấp thông tin thiếu: agent hỏi, người trả lời, agent dùng câu trả lời đi tiếp.
# Mẫu edit state: người sửa bản nháp rồi mới chạy tiếp
app.update_state(config, {"draft": "Bản nháp đã người chỉnh"})
app.invoke(None, config)

Các bước triển khai một bước duyệt

  • Xác định node rủi ro cần chặn (gửi/xóa/thanh toán).
  • Bật checkpointer — bắt buộc, vì interrupt cần lưu state để chờ.
  • Chọn static (interrupt_before) hay dynamic (interrupt()) tùy độ linh hoạt cần thiết.
  • Xây UI hoặc kênh để người xem state đang chờ và ra quyết định.
  • Resume: invoke với None (static) hoặc Command(resume=...) (dynamic).
  • Ghi log ai đã duyệt, lúc nào — phục vụ audit và truy vết.

Checklist an toàn HITL

  • [ ] Mọi hành động không thể hoàn tác đều có bước duyệt trước đó.
  • [ ] Checkpointer bền được bật (không dùng Memory nếu phải chờ lâu).
  • [ ] thread_id giữ nguyên giữa lúc dừng và lúc resume.
  • [ ] Người duyệt thấy đủ ngữ cảnh để quyết định.
  • [ ] Có đường "từ chối" rõ ràng, không chỉ có "đồng ý".

Sai lầm thường gặp

  • Bật interrupt mà quên checkpointer: không có nơi lưu state chờ, cơ chế không hoạt động.
  • Resume với thread_id khác lúc dừng: agent không tìm thấy phiên đang chờ.
  • Chờ người bằng MemorySaver: nếu process restart trong lúc chờ, mất toàn bộ tiến trình. Dùng store bền.
  • Chỉ thiết kế nhánh "đồng ý": khi người từ chối, agent không biết làm gì. Luôn xử lý cả trường hợp reject.
  • Đặt bước duyệt sai chỗ — sau khi hành động rủi ro đã chạy — thì hoàn toàn vô nghĩa. Phải chặn trước hành động.
Giờ agent đã biết dừng lại hỏi người. Bài 7 sẽ gộp tất cả kỹ thuật đã học để xây một agent thực chiến hoàn chỉnh.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi