Product Management
Đăng nhập
ESC

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

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

Bài 3 — Thiết kế State: TypedDict, reducer và channel gộp dữ liệu

Đã đọc 0/7 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.

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: add cho list thường, add_messages cho 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ệuHành vi mong muốnReducer
Trạng thái hiện tạiGhi đè(không cần)
Lịch sử hội thoạiNối + dedupe theo idadd_messages
Log/kết quả tích lũyNối thêmoperator.add
Tổng/điểm sốGộp theo công thứctự 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 add cho 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ó.
Nắm chắc reducer, bạn kiểm soát được cách dữ liệu tích lũy qua thời gian. Bài 4 sẽ dùng nền này để xây luồng rẽ nhánh và vòng lặp có điều kiện.

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