State là trái tim của agent. Thiết kế state tốt thì graph gọn gàng; thiết kế ẩu thì bạn sẽ vật lộn với ghi đè dữ liệu và mất ngữ cảnh. Bài này tập trung vào cách state cập nhật và cơ chế reducer — thứ quyết định dữ liệu được gộp ra sao qua từng node.
Mặc định: ghi đè (overwrite)
Khi bạn khai báo một key thường trong TypedDict, node trả về key đó sẽ ghi đè giá trị cũ. Điều này đúng cho dữ liệu "một giá trị hiện tại" như category, answer, status — mỗi lần cập nhật là thay thế hoàn toàn.
Nhưng nhiều trường hợp bạn cần cộng dồn thay vì ghi đè — ví dụ danh sách tin nhắn của hội thoại. Nếu ghi đè, mỗi node sẽ xóa sạch lịch sử, agent lập tức "quên" những gì đã nói. Đây chính là lúc cần reducer.
Reducer: định nghĩa cách gộp cập nhật
Reducer là một hàm nhận (giá trị cũ, giá trị mới) và trả về giá trị đã gộp. Ta gắn reducer vào key bằng Annotated.
from typing import Annotated, TypedDict
from operator import add
from langgraph.graph.message import add_messages
class State(TypedDict):
# cộng dồn danh sách: mỗi node trả thêm phần tử
logs: Annotated[list[str], add]
# messages dùng reducer chuyên dụng, biết merge theo id
messages: Annotated[list, add_messages]
# key thường: ghi đè
status: str
Với logs: Annotated[list, add], node A trả {"logs": ["a"]} và node B trả {"logs": ["b"]} sẽ cho state cuối logs == ["a", "b"]. Không có reducer, logs chỉ còn ["b"] vì bị ghi đè.
add_messages là reducer đặc biệt cho hội thoại: nó nối tin nhắn mới, nhưng nếu tin nhắn có cùng id thì cập nhật thay vì nhân đôi — rất quan trọng khi bạn sửa lại một tin nhắn đã tồn tại thay vì thêm bản trùng.
Các bước thiết kế state
- Liệt kê mọi dữ liệu cần truyền giữa các node.
- Với mỗi key, tự hỏi: "cần ghi đè hay cộng dồn?" — để quyết định có cần reducer.
- Chọn reducer phù hợp:
addcho list thường,add_messagescho hội thoại, hoặc tự viết cho logic đặc biệt. - Đặt tên key theo nghiệp vụ, tránh tên chung chung như
data,result. - Giữ state phẳng và tối thiểu — chỉ chứa thứ mà nhiều node thực sự cần.
Viết reducer tùy biến
Khi cần gộp theo cách riêng, ví dụ giữ điểm cao nhất qua các vòng:
def keep_max(old: int, new: int) -> int:
return max(old or 0, new)
class State(TypedDict):
best_score: Annotated[int, keep_max]
Giờ dù node nào trả best_score, state luôn giữ giá trị lớn nhất. Reducer tùy biến mở ra rất nhiều mẫu: giữ giá trị mới nhất khác rỗng, gộp dict, khử trùng lặp danh sách...
Khung quyết định reducer
| Loại dữ liệu | Hành vi mong muốn | Reducer |
|---|---|---|
| Trạng thái hiện tại | Ghi đè | (không cần) |
| Lịch sử hội thoại | Nối + dedupe theo id | add_messages |
| Log/kết quả tích lũy | Nối thêm | operator.add |
| Tổng/điểm số | Gộp theo công thức | tự viết |
Sai lầm thường gặp
- Quên reducer cho messages: mỗi lượt gọi LLM ghi đè lịch sử, agent "quên" ngữ cảnh. Đây là bug số một của người mới với LangGraph.
- Dùng reducer
addcho key vốn cần ghi đè nhưstatus, khiến giá trị bị nối chuỗi hoặc gộp list một cách vô nghĩa. - State lồng nhau nhiều tầng khó áp reducer và khó debug — hãy giữ phẳng nhất có thể.
- Node trả về key không khai báo trong State: tùy cấu hình, dữ liệu có thể bị bỏ qua âm thầm và biến mất.
- Nhầm reducer là nơi chạy logic nghiệp vụ: reducer chỉ nên gộp dữ liệu, tuyệt đối không gọi LLM hay tool bên trong nó.