# 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 đó.

Bản gốc: https://fdetimes.net/vi/cong-cu/langgraph/

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`.

```python
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.

**Điểm mấu chốt:** Khi resume, node chạy lại từ đầu: thứ gì không được phép xảy ra hai lần thì phải nằm sau 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.

**Thử ngay tuần này:**

- Dựng một đồ thị LangGraph có đúng một node xin duyệt trước hành động giả lập 'hoàn tiền', resume bằng Command(resume=...) với hai giá trị: chấp nhận và từ chối.
- Cố ý đặt một lệnh in log hoặc ghi file trước interrupt, resume, rồi đếm số lần nó chạy để tự thấy hiện tượng node chạy lại.
- Viết lại một dòng trong CV mô tả việc bạn thiết kế điểm duyệt của con người cho agent, nêu rõ hành động nào bị chặn và vì sao.

## Nguồn

- [langchain-ai/langgraph (GitHub README)](https://github.com/langchain-ai/langgraph)

- [Interrupts – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/interrupts)

- [Interrupts – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/human-in-the-loop)

- [Persistence – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/persistence)

- [Making it easier to build human-in-the-loop agents with interrupt](https://www.langchain.com/blog/making-it-easier-to-build-human-in-the-loop-agents-with-interrupt)
