2.1. Streaming: Real-time Agent Response
Agenda
Thời gian đọc ước tính: ~20 phút
Learning outcome:
- Giải thích được tại sao Streaming quan trọng hơn đối với UX của agent so với chatbot thông thường.
- Phân biệt được 4 stream mode cốt lõi:
updates,values,messages,custom— và biết khi nào dùng mode nào. - Implement được tool streaming với async generator (
async function*) để emit progress events. - Nhận biết được khi nào nên dùng
nostreamtag để kiểm soát luồng dữ liệu ra client.
Glossary & Vocabulary
1. Technical Terms (Thuật ngữ kỹ thuật):
| Term | Vietnamese Meaning & Quick Explain |
|---|---|
| Streaming | Truyền dữ liệu dần dần — thay vì đợi toàn bộ response xong mới gửi, server gửi từng phần nhỏ ngay khi có. |
| Stream Mode | Chế độ streaming — xác định loại dữ liệu nào được phát ra qua stream (state updates, LLM tokens, custom data...). |
| Token | Đơn vị nhỏ nhất LLM xử lý — thường là 1 từ hoặc vài ký tự. LLM sinh ra từng token một, không phải cả câu một lúc. |
| Async Generator | Hàm bất đồng bộ tạo ra nhiều giá trị theo thời gian — dùng async function* và yield trong JavaScript. |
updates mode | Stream cập nhật state sau mỗi bước — chỉ phần thay đổi, kèm tên node. |
values mode | Stream toàn bộ state sau mỗi bước — bao gồm cả những field không thay đổi. |
messages mode | Stream từng LLM token dưới dạng tuple [token, metadata] từ bất kỳ node nào có gọi LLM. |
custom mode | Stream dữ liệu tuỳ ý từ bên trong node — qua config.writer(). |
tools mode | Stream lifecycle events của tool execution: on_tool_start, on_tool_event, on_tool_end, on_tool_error. |
nostream tag | Tag đặc biệt để loại trừ LLM call khỏi messages stream — hữu ích cho internal processing. |
thread_id | ID phân biệt session — kết hợp với checkpointer để lưu state giữa các lần stream. |
2. Vocabulary Support (Từ vựng học thuật/B1+):
| Word | Meaning in Context |
|---|---|
| Progressive (adj) | Dần dần, từng bước một — hiển thị output khi nó được tạo ra. |
| Latency (n) | Độ trễ — thời gian từ khi gửi request đến khi nhận được phản hồi đầu tiên. |
| Emit (v) | Phát ra, gửi đi — node "emits" data vào stream. |
| Lifecycle (n) | Vòng đời — chuỗi các trạng thái một đối tượng trải qua từ lúc khởi tạo đến khi kết thúc. |
| Tuple (n) | Cặp giá trị — trong streaming, [token, metadata] là một 2-tuple. |
1. Vấn đề & Giải pháp
Vấn đề (Problem Statement):
- Agent LLM thường mất vài giây đến hàng chục giây để hoàn thành một tác vụ. Nếu không có streaming, người dùng nhìn vào màn hình trống và không biết điều gì đang xảy ra.
- Trong agent loop, LLM có thể gọi nhiều tool theo chuỗi. Nếu chỉ nhận output ở cuối, người dùng không theo dõi được tiến trình nào đang chạy.
- Không phải mọi output đ ều cần stream đến client — một số LLM call chỉ phục vụ internal processing (VD: phân loại intent) và stream ra sẽ gây nhiễu.
Giải pháp (Solution):
LangGraph cung cấp hệ thống streaming nhiều tầng với các stream modes độc lập. Developer chọn chính xác loại dữ liệu nào cần phát ra — từ state updates thô, đến từng token LLM, đến custom progress events từ tool.
2. Streaming Là Gì?
Định nghĩa kỹ thuật:
Streaming là cơ chế cho phép LangGraph phát ra dữ liệu liên tục trong quá trình thực thi graph, thay vì chỉ trả về kết quả cuối cùng sau khi toàn bộ quá trình hoàn tất.
Definition Anatomy — Giải phẫu định nghĩa:
- liên tục trong quá trình thực thi (incrementally during execution): Đây là điểm cốt lõi phân biệt streaming với polling. Dữ liệu được đẩy ra ngay khi có — không phải sau khi graph chạy xong.
- stream modes (chế độ luồng): Mỗi mode là một "kênh" khác nhau. Bạn có thể nghe nhiều kênh cùng lúc bằng cách pass array:
streamMode: ["updates", "messages"].
Luồng hoạt động của Streaming trong Agent:
Cài đặt:
npm install @langchain/langgraph @langchain/google-genai @langchain/core zod
3. Bốn Stream Mode Cốt Lõi
3.1. updates — Stream state updates sau mỗi bước
Mode đơn giản nhất. Sau mỗi node hoàn thành, phát ra phần state đã thay đổi kèm tên node.
// filename: agent/stream-updates.ts
import { StateGraph, StateSchema, START, END } from "@langchain/langgraph";
import * as z from "zod";
const State = new StateSchema({
topic: z.string(),
joke: z.string().default(""),
});
const graph = new StateGraph(State)
.addNode("refineTopic", (state) => {
return { topic: state.topic + " and cats" };
})
.addNode("generateJoke", (state) => {
return { joke: `Why did ${state.topic} cross the road?` };
})
.addEdge(START, "refineTopic")
.addEdge("refineTopic", "generateJoke")
.addEdge("generateJoke", END)
.compile();
// Chỉ nhận phần thay đổi — không phải toàn bộ state
for await (const chunk of await graph.stream(
{ topic: "ice cream" },
{ streamMode: "updates" }
)) {
for (const [nodeName, state] of Object.entries(chunk)) {
console.log(`Node "${nodeName}" updated:`, state);
}
}
// Output mong đợi:
Node "refineTopic" updated: { topic: "ice cream and cats" }
Node "generateJoke" updated: { joke: "Why did ice cream and cats cross the road?" }
Khi dùng updates: Dashboard theo dõi tiến trình của agent — bạn thấy từng bước mà không bị ngập trong toàn bộ state.
3.2. values — Stream toàn bộ state sau mỗi bước
Tương tự updates nhưng phát ra snapshot đầy đủ của state sau mỗi bước.
// filename: agent/stream-values.ts
for await (const chunk of await graph.stream(
{ topic: "ice cream" },
{ streamMode: "values" }
)) {
// chunk là toàn bộ state object tại thời điểm đó
console.log(`topic: ${chunk.topic}, joke: ${chunk.joke}`);
}
// Output mong đợi:
topic: ice cream and cats, joke:
topic: ice cream and cats, joke: Why did ice cream and cats cross the road?
Trade-off updates vs values:
updates | values | |
|---|---|---|
| Dữ liệu nhận được | Chỉ phần thay đổi | Toàn bộ state |
| Bandwidth | Thấp hơn | Cao hơn |
| Khi dùng | Log từng bước, progress tracking | Debug, cần snapshot đầy đủ tại mỗi bước |
3.3. messages — Stream từng LLM token
Mode này hoạt động như thế nào: mỗi khi LLM được gọi từ bất kỳ node nào trong graph, các token được phát ra ngay khi LLM sinh ra — dưới dạng tuple [messageChunk, metadata].
// filename: agent/stream-messages.ts
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { StateGraph, StateSchema, type GraphNode, START } from "@langchain/langgraph";
import * as z from "zod";
const model = new ChatGoogleGenerativeAI({ model: "gemini-2.5-flash" });
const MyState = new StateSchema({
topic: z.string(),
joke: z.string().default(""),
});
const callModel: GraphNode<typeof MyState> = async (state) => {
const response = await model.invoke([
{ role: "user", content: `Generate a short joke about ${state.topic}` },
]);
return { joke: response.content as string };
};
const graph = new StateGraph(MyState)
.addNode("callModel", callModel)
.addEdge(START, "callModel")
.compile();
// messages mode trả về tuple [token, metadata]
// metadata.langgraph_node cho biết token đến từ node nào
for await (const [messageChunk, metadata] of await graph.stream(
{ topic: "ice cream" },
{ streamMode: "messages" }
)) {
if (messageChunk.content) {
// In từng token ngay khi nhận — không cần đợi toàn bộ câu
process.stdout.write(messageChunk.content as string);
}
}
Lọc token theo node cụ thể
Khi graph có nhiều LLM calls, dùng metadata.langgraph_node để chỉ lấy token từ node mong muốn:
// filename: agent/stream-filter-by-node.ts
for await (const [msg, metadata] of await graph.stream(
{ topic: "cats" },
{ streamMode: "messages" }
)) {
// Chỉ lấy token từ node "writePoem", bỏ qua "writeJoke"
if (msg.content && metadata.langgraph_node === "writePoem") {
process.stdout.write(msg.content as string);
}
}