@xdev-asia/xdev-knowledge-mcp 1.0.57 → 1.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/blog/ai/minimax-danh-gia-chi-tiet-nen-tang-ai-full-stack-trung-quoc.md +450 -0
- package/content/blog/ai/nvidia-dli-generative-ai-chung-chi-va-lo-trinh-hoc.md +894 -0
- package/content/metadata/authors/duy-tran.md +2 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/01-deep-learning-foundations/lessons/01-bai-1-pytorch-neural-network-fundamentals.md +790 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/01-deep-learning-foundations/lessons/02-bai-2-transformer-architecture-attention.md +984 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/01-bai-3-unet-architecture-denoising.md +1111 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/02-bai-4-ddpm-forward-reverse-diffusion.md +1007 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/03-bai-5-clip-text-to-image-pipeline.md +1037 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/01-bai-6-llm-inference-pipeline-design.md +929 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/02-bai-7-rag-retrieval-augmented-generation.md +1099 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/03-bai-8-rag-agent-build-evaluate.md +1249 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/04-agentic-ai-customization/lessons/01-bai-9-agentic-ai-multi-agent-systems.md +1357 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/04-agentic-ai-customization/lessons/02-bai-10-llm-evaluation-lora-fine-tuning.md +1867 -0
- package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/index.md +237 -0
- package/data/quizzes/nvidia-dli-generative-ai.json +350 -0
- package/data/quizzes.json +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 019c9619-nv01-p4-l09
|
|
3
|
+
title: 'Bài 9: Agentic AI & Multi-Agent Systems'
|
|
4
|
+
slug: bai-9-agentic-ai-multi-agent-systems
|
|
5
|
+
description: >-
|
|
6
|
+
Agent abstraction: perception → reasoning → action loop.
|
|
7
|
+
Cognitive architectures: ReAct, Plan-and-Execute, LATS.
|
|
8
|
+
LangGraph: stateful graph-based agent orchestration.
|
|
9
|
+
Multi-agent systems: supervisor, hierarchical, swarm patterns.
|
|
10
|
+
Build production-ready multi-agent application.
|
|
11
|
+
duration_minutes: 90
|
|
12
|
+
is_free: true
|
|
13
|
+
video_url: null
|
|
14
|
+
sort_order: 9
|
|
15
|
+
section_title: "Part 4: Agentic AI & LLM Customization"
|
|
16
|
+
course:
|
|
17
|
+
id: 019c9619-nv01-7001-c001-nv0100000001
|
|
18
|
+
title: 'Luyện thi NVIDIA DLI — Generative AI with Diffusion Models & LLMs'
|
|
19
|
+
slug: luyen-thi-nvidia-dli-generative-ai
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<h2 id="1-agent-abstraction">1. Agent Abstraction — LLM + Memory + Tools + Planning</h2>
|
|
23
|
+
|
|
24
|
+
<h3 id="1-1-agent-la-gi">1.1. Agent là gì?</h3>
|
|
25
|
+
|
|
26
|
+
<p>Bài 8 đã giới thiệu <strong>ReAct agent</strong> đơn giản — LLM chọn tool rồi trả lời. Bài 9 mở rộng sang <strong>Agentic AI</strong>: hệ thống nơi LLM đóng vai trò <strong>bộ não trung tâm</strong> — tự lên kế hoạch, chọn công cụ, phản hồi từ kết quả, và phối hợp với nhiều agent khác.</p>
|
|
27
|
+
|
|
28
|
+
<p>Một <strong>Agent</strong> gồm 4 thành phần cốt lõi:</p>
|
|
29
|
+
|
|
30
|
+
<ul>
|
|
31
|
+
<li><strong>LLM (Brain)</strong> — suy luận, ra quyết định, sinh text</li>
|
|
32
|
+
<li><strong>Memory</strong> — ngắn hạn (conversation buffer) và dài hạn (vector store, database)</li>
|
|
33
|
+
<li><strong>Tools</strong> — hàm mà agent gọi được: search, calculator, API, code execution</li>
|
|
34
|
+
<li><strong>Planning</strong> — lên kế hoạch (plan), chia nhỏ task, phản hồi (reflect) khi có lỗi</li>
|
|
35
|
+
</ul>
|
|
36
|
+
|
|
37
|
+
<pre><code class="language-text">
|
|
38
|
+
Agent Abstraction — Core Components
|
|
39
|
+
══════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
┌──────────────────────┐
|
|
42
|
+
│ USER │
|
|
43
|
+
│ (Task / Query) │
|
|
44
|
+
└──────────┬───────────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌────────────────────────────────────────────────────────┐
|
|
48
|
+
│ AGENT │
|
|
49
|
+
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
|
|
50
|
+
│ │ Planning │ │ LLM Core │ │ Memory │ │
|
|
51
|
+
│ │ │◄─┤ (Brain) ├─►│ Short-term: chat │ │
|
|
52
|
+
│ │ Decompose│ │ Reasoning │ │ Long-term: VDB │ │
|
|
53
|
+
│ │ Reflect │ │ Decisions │ │ Episodic: logs │ │
|
|
54
|
+
│ └──────────┘ └─────┬─────┘ └──────────────────┘ │
|
|
55
|
+
│ │ │
|
|
56
|
+
│ ┌────────┼────────┐ │
|
|
57
|
+
│ ▼ ▼ ▼ │
|
|
58
|
+
│ ┌────────┐┌───────┐┌────────┐ │
|
|
59
|
+
│ │Search ││ Code ││ API │ ◄── Tools │
|
|
60
|
+
│ │Engine ││ Exec ││ Calls │ │
|
|
61
|
+
│ └────────┘└───────┘└────────┘ │
|
|
62
|
+
└────────────────────────────────────────────────────────┘
|
|
63
|
+
</code></pre>
|
|
64
|
+
|
|
65
|
+
<h3 id="1-2-perception-reasoning-action-loop">1.2. Perception → Reasoning → Action → Observation Loop</h3>
|
|
66
|
+
|
|
67
|
+
<p>Mọi agent đều chạy theo một vòng lặp cơ bản:</p>
|
|
68
|
+
|
|
69
|
+
<ol>
|
|
70
|
+
<li><strong>Perception</strong> — nhận input (user query, tool output, environment feedback)</li>
|
|
71
|
+
<li><strong>Reasoning</strong> — LLM suy luận: "Tôi cần làm gì tiếp? Dùng tool nào? Đã đủ info chưa?"</li>
|
|
72
|
+
<li><strong>Action</strong> — thực hiện hành động: gọi tool, sinh text, trả lời user</li>
|
|
73
|
+
<li><strong>Observation</strong> — nhận kết quả từ action, đưa lại vào bước Perception → lặp lại</li>
|
|
74
|
+
</ol>
|
|
75
|
+
|
|
76
|
+
<pre><code class="language-text">
|
|
77
|
+
Agent Loop — Perception → Reasoning → Action → Observation
|
|
78
|
+
═══════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
|
|
81
|
+
│ PERCEPTION │────►│ REASONING │────►│ ACTION │
|
|
82
|
+
│ │ │ │ │ │
|
|
83
|
+
│ User query │ │ "Which tool?" │ │ Call tool │
|
|
84
|
+
│ Tool output │ │ "Enough info?"│ │ Generate text │
|
|
85
|
+
│ Error msg │ │ "Need retry?" │ │ Return answer │
|
|
86
|
+
└──────┬───────┘ └───────────────┘ └───────┬───────┘
|
|
87
|
+
▲ │
|
|
88
|
+
│ ┌───────────────┐ │
|
|
89
|
+
└───────────│ OBSERVATION │◄──────────────┘
|
|
90
|
+
│ │
|
|
91
|
+
│ Tool result │
|
|
92
|
+
│ Error / OK │
|
|
93
|
+
└───────────────┘
|
|
94
|
+
Loop tiếp tục cho đến khi có Final Answer
|
|
95
|
+
</code></pre>
|
|
96
|
+
|
|
97
|
+
<h3 id="1-3-levels-of-agency">1.3. Levels of Agency</h3>
|
|
98
|
+
|
|
99
|
+
<p>Không phải mọi ứng dụng LLM đều cần full agent. NVIDIA DLI phân biệt rõ các mức độ <strong>agency</strong>:</p>
|
|
100
|
+
|
|
101
|
+
<table>
|
|
102
|
+
<thead>
|
|
103
|
+
<tr><th>Level</th><th>Pattern</th><th>LLM Role</th><th>Example</th></tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
<tr><td>L0 — No agency</td><td>Simple prompt → response</td><td>Text generator</td><td>Chatbot trả lời FAQ</td></tr>
|
|
107
|
+
<tr><td>L1 — Tool use</td><td>LLM chọn 1 tool</td><td>Router</td><td>Function calling API</td></tr>
|
|
108
|
+
<tr><td>L2 — Single agent</td><td>ReAct loop, multi-step</td><td>Planner + executor</td><td>RAG Agent (Bài 8)</td></tr>
|
|
109
|
+
<tr><td>L3 — Multi-agent</td><td>Nhiều agents phối hợp</td><td>Coordinator</td><td>Supervisor + workers</td></tr>
|
|
110
|
+
<tr><td>L4 — Autonomous</td><td>Self-improving, long-running</td><td>Autonomous system</td><td>AI Scientist, Devin</td></tr>
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
|
|
114
|
+
<blockquote><p><strong>Exam tip:</strong> "LLM tự chia nhỏ task, gọi nhiều tools, lặp lại khi cần" → <strong>Agent (L2+)</strong>. "Nhiều LLM phối hợp, mỗi cái chuyên một việc" → <strong>Multi-Agent (L3)</strong>. DLI exam thường hỏi: "What differentiates an agent from a chain?" → Agent có <strong>dynamic control flow</strong> (LLM quyết định bước tiếp theo), chain có <strong>fixed control flow</strong>.</p></blockquote>
|
|
115
|
+
|
|
116
|
+
<figure><img src="/storage/uploads/2026/04/nvidia-dli-bai9-multi-agent-system.png" alt="Multi-Agent System — Orchestrator, Specialized Agents, LangGraph State Machine" loading="lazy" /><figcaption>Multi-Agent System — Orchestrator, Specialized Agents, LangGraph State Machine</figcaption></figure>
|
|
117
|
+
|
|
118
|
+
<h2 id="2-cognitive-architectures">2. Cognitive Architectures cho LLM Agents</h2>
|
|
119
|
+
|
|
120
|
+
<h3 id="2-1-react">2.1. ReAct — Reasoning + Acting</h3>
|
|
121
|
+
|
|
122
|
+
<p><strong>ReAct</strong> (đã giới thiệu Bài 8) xen kẽ <strong>Thought</strong> (suy nghĩ) với <strong>Action</strong> (hành động). Ưu điểm: đơn giản, transparent. Nhược điểm: không có planning dài hạn — agent chỉ nghĩ <em>bước tiếp theo</em>, không nhìn toàn cảnh.</p>
|
|
123
|
+
|
|
124
|
+
<h3 id="2-2-plan-and-execute">2.2. Plan-and-Execute</h3>
|
|
125
|
+
|
|
126
|
+
<p><strong>Plan-and-Execute</strong> tách rõ hai giai đoạn: (1) Planner LLM lên kế hoạch toàn bộ trước, (2) Executor LLM thực hiện từng bước. Sau mỗi bước, Planner có thể <strong>replan</strong> (điều chỉnh kế hoạch).</p>
|
|
127
|
+
|
|
128
|
+
<pre><code class="language-text">
|
|
129
|
+
Plan-and-Execute Architecture
|
|
130
|
+
══════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
User: "Phân tích doanh thu Q3, so sánh với Q2, viết báo cáo"
|
|
133
|
+
│
|
|
134
|
+
▼
|
|
135
|
+
┌─────────────────────────────────────────────┐
|
|
136
|
+
│ PLANNER LLM │
|
|
137
|
+
│ Plan: │
|
|
138
|
+
│ Step 1: Retrieve Q3 revenue data │
|
|
139
|
+
│ Step 2: Retrieve Q2 revenue data │
|
|
140
|
+
│ Step 3: Calculate Q2→Q3 change │
|
|
141
|
+
│ Step 4: Write comparison report │
|
|
142
|
+
└─────────────────────┬───────────────────────┘
|
|
143
|
+
│
|
|
144
|
+
┌─────────────┼─────────────┐
|
|
145
|
+
▼ ▼ ▼
|
|
146
|
+
Execute S1 Execute S2 Execute S3 ...
|
|
147
|
+
(retriever) (retriever) (calculator)
|
|
148
|
+
│ │ │
|
|
149
|
+
└─────────────┼─────────────┘
|
|
150
|
+
│
|
|
151
|
+
▼
|
|
152
|
+
┌───────────────┐
|
|
153
|
+
│ REPLAN? │──► Nếu step fail → adjust plan
|
|
154
|
+
│ All done? │──► Nếu done → Step 4: report
|
|
155
|
+
└───────────────┘
|
|
156
|
+
</code></pre>
|
|
157
|
+
|
|
158
|
+
<h3 id="2-3-lats">2.3. LATS — Language Agent Tree Search</h3>
|
|
159
|
+
|
|
160
|
+
<p><strong>LATS</strong> kết hợp <strong>Monte Carlo Tree Search (MCTS)</strong> với LLM reasoning. Thay vì đi theo 1 path (ReAct), LATS explore nhiều nhánh giải pháp, đánh giá từng nhánh bằng LLM, rồi chọn nhánh tốt nhất. Giống như LLM chơi cờ — suy nghĩ trước nhiều bước.</p>
|
|
161
|
+
|
|
162
|
+
<h3 id="2-4-reflexion">2.4. Reflexion — Learn from Mistakes</h3>
|
|
163
|
+
|
|
164
|
+
<p><strong>Reflexion</strong> thêm bước <strong>self-reflection</strong>: sau khi hoàn thành task, agent tự đánh giá kết quả → nếu sai, viết "bài học" vào memory → thử lại với kinh nghiệm từ lần trước. Đây là dạng <em>in-context learning</em> qua self-feedback.</p>
|
|
165
|
+
|
|
166
|
+
<h3 id="2-5-comparison-table">2.5. So sánh Cognitive Architectures</h3>
|
|
167
|
+
|
|
168
|
+
<table>
|
|
169
|
+
<thead>
|
|
170
|
+
<tr><th>Architecture</th><th>Planning</th><th>Execution</th><th>Strength</th><th>Weakness</th></tr>
|
|
171
|
+
</thead>
|
|
172
|
+
<tbody>
|
|
173
|
+
<tr><td><strong>ReAct</strong></td><td>Step-by-step (myopic)</td><td>Interleaved think+act</td><td>Simple, transparent</td><td>No global planning, can loop</td></tr>
|
|
174
|
+
<tr><td><strong>Plan-and-Execute</strong></td><td>Upfront full plan</td><td>Sequential execution</td><td>Global view, fewer LLM calls</td><td>Plan có thể outdated after steps</td></tr>
|
|
175
|
+
<tr><td><strong>LATS</strong></td><td>Tree search (explore)</td><td>Best-first search</td><td>Explores alternatives, robust</td><td>Very expensive (nhiều LLM calls)</td></tr>
|
|
176
|
+
<tr><td><strong>Reflexion</strong></td><td>Trial-and-error + memory</td><td>Execute → reflect → retry</td><td>Learns from mistakes</td><td>Slow convergence, needs evaluator</td></tr>
|
|
177
|
+
</tbody>
|
|
178
|
+
</table>
|
|
179
|
+
|
|
180
|
+
<blockquote><p><strong>Exam tip:</strong> "Agent cần suy nghĩ toàn bộ kế hoạch trước khi thực hiện" → <strong>Plan-and-Execute</strong>. "Agent thử nhiều hướng giải quyết, chọn tốt nhất" → <strong>LATS</strong>. "Agent tự đánh giá kết quả và cải thiện" → <strong>Reflexion</strong>. "Agent xen kẽ suy nghĩ và hành động" → <strong>ReAct</strong>. Đề DLI thường ưu tiên hỏi <strong>ReAct</strong> và <strong>Plan-and-Execute</strong> vì hai kiến trúc này phổ biến nhất.</p></blockquote>
|
|
181
|
+
|
|
182
|
+
<h2 id="3-langgraph">3. LangGraph — Stateful Graph-Based Agent Orchestration</h2>
|
|
183
|
+
|
|
184
|
+
<h3 id="3-1-tai-sao-langgraph">3.1. Tại sao cần LangGraph?</h3>
|
|
185
|
+
|
|
186
|
+
<p><strong>AgentExecutor</strong> trong LangChain (Bài 8) là "black box" — khó customize control flow. <strong>LangGraph</strong> là thư viện của LangChain cho phép xây dựng agent dạng <strong>directed graph</strong>: mỗi node là một bước xử lý, edges định nghĩa flow, conditional edges cho phép rẽ nhánh dựa trên state.</p>
|
|
187
|
+
|
|
188
|
+
<table>
|
|
189
|
+
<thead>
|
|
190
|
+
<tr><th>Feature</th><th>AgentExecutor</th><th>LangGraph</th></tr>
|
|
191
|
+
</thead>
|
|
192
|
+
<tbody>
|
|
193
|
+
<tr><td>Control flow</td><td>Fixed ReAct loop</td><td>Custom graph — bạn thiết kế flow</td></tr>
|
|
194
|
+
<tr><td>State management</td><td>Hidden internal state</td><td>Explicit <code>TypedDict</code> state</td></tr>
|
|
195
|
+
<tr><td>Multi-agent</td><td>Không hỗ trợ native</td><td>First-class: mỗi agent = sub-graph</td></tr>
|
|
196
|
+
<tr><td>Human-in-the-loop</td><td>Hạn chế</td><td>Built-in: interrupt, approve, edit</td></tr>
|
|
197
|
+
<tr><td>Persistence</td><td>No built-in</td><td>Checkpointer: save/resume state</td></tr>
|
|
198
|
+
<tr><td>Streaming</td><td>Basic</td><td>Event-by-event streaming</td></tr>
|
|
199
|
+
<tr><td>Debug</td><td>Trace via LangSmith</td><td>Graph visualization + LangSmith</td></tr>
|
|
200
|
+
</tbody>
|
|
201
|
+
</table>
|
|
202
|
+
|
|
203
|
+
<h3 id="3-2-core-concepts">3.2. Core Concepts — StateGraph, Nodes, Edges</h3>
|
|
204
|
+
|
|
205
|
+
<p>LangGraph xây dựng trên 3 khái niệm:</p>
|
|
206
|
+
|
|
207
|
+
<ul>
|
|
208
|
+
<li><strong>State</strong> — TypedDict giữ toàn bộ data chuyền giữa các nodes. Mỗi node đọc/ghi state.</li>
|
|
209
|
+
<li><strong>Nodes</strong> — Python functions. Input: state → Output: partial state update (chỉ fields cần update).</li>
|
|
210
|
+
<li><strong>Edges</strong> — kết nối giữa nodes. <code>add_edge(A, B)</code> = always go A→B. <code>add_conditional_edges(A, func)</code> = func quyết định đi đâu.</li>
|
|
211
|
+
</ul>
|
|
212
|
+
|
|
213
|
+
<pre><code class="language-text">
|
|
214
|
+
LangGraph Concepts
|
|
215
|
+
══════════════════════════════════════════════════════════
|
|
216
|
+
|
|
217
|
+
State = TypedDict(messages, plan, results, ...)
|
|
218
|
+
────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
START ──► [Node: agent] ──conditional──► [Node: tools]
|
|
221
|
+
│ │
|
|
222
|
+
│ (if done) │ (tool result)
|
|
223
|
+
▼ │
|
|
224
|
+
END ◄───────────────────────────┘
|
|
225
|
+
|
|
226
|
+
Nodes: Python functions that read/write State
|
|
227
|
+
Edges: Static (always) or Conditional (function decides)
|
|
228
|
+
</code></pre>
|
|
229
|
+
|
|
230
|
+
<h3 id="3-3-code-basic-langgraph-agent">3.3. Code: Basic LangGraph Agent</h3>
|
|
231
|
+
|
|
232
|
+
<pre><code class="language-python">
|
|
233
|
+
from typing import TypedDict, Annotated, Sequence
|
|
234
|
+
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
|
235
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
236
|
+
from langchain_core.tools import tool
|
|
237
|
+
from langgraph.graph import StateGraph, END
|
|
238
|
+
from langgraph.prebuilt import ToolNode
|
|
239
|
+
import operator
|
|
240
|
+
|
|
241
|
+
# === 1. Define State ===
|
|
242
|
+
class AgentState(TypedDict):
|
|
243
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
244
|
+
|
|
245
|
+
# === 2. Define Tools ===
|
|
246
|
+
@tool
|
|
247
|
+
def search_docs(query: str) -> str:
|
|
248
|
+
"""Search internal documents for company information."""
|
|
249
|
+
# Simulate retrieval
|
|
250
|
+
docs = {
|
|
251
|
+
"leave": "Employees get 12 days annual leave per year.",
|
|
252
|
+
"refund": "Refund within 30 days with original receipt.",
|
|
253
|
+
}
|
|
254
|
+
for key, val in docs.items():
|
|
255
|
+
if key in query.lower():
|
|
256
|
+
return val
|
|
257
|
+
return "No relevant documents found."
|
|
258
|
+
|
|
259
|
+
@tool
|
|
260
|
+
def calculator(expression: str) -> str:
|
|
261
|
+
"""Calculate mathematical expressions."""
|
|
262
|
+
try:
|
|
263
|
+
return str(eval(expression)) # production: use safe eval
|
|
264
|
+
except Exception as e:
|
|
265
|
+
return f"Error: {e}"
|
|
266
|
+
|
|
267
|
+
tools = [search_docs, calculator]
|
|
268
|
+
|
|
269
|
+
# === 3. Define LLM with tools ===
|
|
270
|
+
llm = ChatNVIDIA(
|
|
271
|
+
model="meta/llama-3.1-70b-instruct",
|
|
272
|
+
temperature=0.1
|
|
273
|
+
).bind_tools(tools)
|
|
274
|
+
|
|
275
|
+
# === 4. Define Nodes ===
|
|
276
|
+
def agent_node(state: AgentState) -> dict:
|
|
277
|
+
"""LLM decides: call tool or respond."""
|
|
278
|
+
response = llm.invoke(state["messages"])
|
|
279
|
+
return {"messages": [response]}
|
|
280
|
+
|
|
281
|
+
tool_node = ToolNode(tools)
|
|
282
|
+
|
|
283
|
+
# === 5. Define Routing ===
|
|
284
|
+
def should_continue(state: AgentState) -> str:
|
|
285
|
+
last_message = state["messages"][-1]
|
|
286
|
+
if last_message.tool_calls:
|
|
287
|
+
return "tools" # LLM wants to call a tool
|
|
288
|
+
return "end" # LLM is done, return answer
|
|
289
|
+
|
|
290
|
+
# === 6. Build Graph ===
|
|
291
|
+
graph = StateGraph(AgentState)
|
|
292
|
+
|
|
293
|
+
graph.add_node("agent", agent_node)
|
|
294
|
+
graph.add_node("tools", tool_node)
|
|
295
|
+
|
|
296
|
+
graph.set_entry_point("agent")
|
|
297
|
+
graph.add_conditional_edges("agent", should_continue, {
|
|
298
|
+
"tools": "tools",
|
|
299
|
+
"end": END,
|
|
300
|
+
})
|
|
301
|
+
graph.add_edge("tools", "agent") # after tool → back to agent
|
|
302
|
+
|
|
303
|
+
app = graph.compile()
|
|
304
|
+
|
|
305
|
+
# === 7. Run ===
|
|
306
|
+
result = app.invoke({
|
|
307
|
+
"messages": [HumanMessage(content="Chính sách nghỉ phép là gì?")]
|
|
308
|
+
})
|
|
309
|
+
print(result["messages"][-1].content)
|
|
310
|
+
</code></pre>
|
|
311
|
+
|
|
312
|
+
<h3 id="3-4-human-in-the-loop">3.4. Human-in-the-Loop</h3>
|
|
313
|
+
|
|
314
|
+
<p>LangGraph hỗ trợ <strong>interrupt</strong> trước khi thực hiện action nguy hiểm — ví dụ: gửi email, xóa dữ liệu, thực thi code. Agent tạm dừng, chờ user approve, rồi tiếp tục.</p>
|
|
315
|
+
|
|
316
|
+
<pre><code class="language-python">
|
|
317
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
318
|
+
|
|
319
|
+
# Compile with checkpointer for interruption + resume
|
|
320
|
+
checkpointer = MemorySaver()
|
|
321
|
+
app = graph.compile(
|
|
322
|
+
checkpointer=checkpointer,
|
|
323
|
+
interrupt_before=["tools"] # pause BEFORE executing tools
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Run — sẽ dừng trước node "tools"
|
|
327
|
+
config = {"configurable": {"thread_id": "user-123"}}
|
|
328
|
+
result = app.invoke(
|
|
329
|
+
{"messages": [HumanMessage(content="Delete file report.pdf")]},
|
|
330
|
+
config=config,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Inspect pending tool call
|
|
334
|
+
pending = result["messages"][-1].tool_calls
|
|
335
|
+
print(f"Agent wants to: {pending}")
|
|
336
|
+
# → Agent wants to: [{'name': 'delete_file', 'args': {'path': 'report.pdf'}}]
|
|
337
|
+
|
|
338
|
+
# Human approves → continue
|
|
339
|
+
final = app.invoke(None, config=config) # resume from checkpoint
|
|
340
|
+
</code></pre>
|
|
341
|
+
|
|
342
|
+
<h3 id="3-5-checkpointing">3.5. Checkpointing — Save & Resume State</h3>
|
|
343
|
+
|
|
344
|
+
<p><strong>Checkpointer</strong> lưu state sau mỗi node, cho phép:</p>
|
|
345
|
+
|
|
346
|
+
<ul>
|
|
347
|
+
<li><strong>Resume</strong> — agent crash giữa chừng → load checkpoint → chạy tiếp</li>
|
|
348
|
+
<li><strong>Time travel</strong> — quay lại bất kỳ checkpoint nào → thử lại với input khác</li>
|
|
349
|
+
<li><strong>Human-in-the-loop</strong> — pause, đợi user, resume (như code trên)</li>
|
|
350
|
+
<li><strong>Multi-turn</strong> — giữ conversation history qua nhiều turns</li>
|
|
351
|
+
</ul>
|
|
352
|
+
|
|
353
|
+
<blockquote><p><strong>Exam tip:</strong> "Build agent with custom control flow, conditional branching" → <strong>LangGraph</strong> (không phải AgentExecutor). "Pause agent execution for human approval" → <strong>interrupt_before + checkpointer</strong>. "Save agent state, resume later" → <strong>LangGraph checkpointing</strong>. DLI C-FX-25 thường hỏi: "Why use LangGraph over AgentExecutor?" → <strong>Custom flow, multi-agent, persistence, human-in-the-loop</strong>.</p></blockquote>
|
|
354
|
+
|
|
355
|
+
<h2 id="4-multi-agent-patterns">4. Multi-Agent Patterns</h2>
|
|
356
|
+
|
|
357
|
+
<h3 id="4-1-tai-sao-multi-agent">4.1. Tại sao cần Multi-Agent?</h3>
|
|
358
|
+
|
|
359
|
+
<p>Một single agent với 20+ tools sẽ gặp vấn đề: <strong>tool selection confusion</strong> (quá nhiều tool, LLM chọn sai), <strong>prompt quá dài</strong> (phải nhét hết instructions), <strong>khó debug</strong> (không rõ agent fail ở bước nào). Multi-agent giải quyết bằng cách <strong>chia nhỏ</strong>: mỗi agent chuyên một nhiệm vụ với ít tools hơn.</p>
|
|
360
|
+
|
|
361
|
+
<h3 id="4-2-supervisor-pattern">4.2. Supervisor Pattern</h3>
|
|
362
|
+
|
|
363
|
+
<p>Một <strong>Supervisor agent</strong> (LLM) nhận task từ user, phân công cho các <strong>Worker agents</strong>, thu thập kết quả, và tổng hợp câu trả lời.</p>
|
|
364
|
+
|
|
365
|
+
<pre><code class="language-text">
|
|
366
|
+
Supervisor Pattern
|
|
367
|
+
══════════════════════════════════════════════════════════
|
|
368
|
+
|
|
369
|
+
┌──────────────┐
|
|
370
|
+
│ USER │
|
|
371
|
+
└──────┬───────┘
|
|
372
|
+
│
|
|
373
|
+
▼
|
|
374
|
+
┌────────────────────────┐
|
|
375
|
+
│ SUPERVISOR AGENT │
|
|
376
|
+
│ (Orchestrator LLM) │
|
|
377
|
+
│ │
|
|
378
|
+
│ Decides: │
|
|
379
|
+
│ • Which worker next? │
|
|
380
|
+
│ • All done? │
|
|
381
|
+
│ • Need to re-route? │
|
|
382
|
+
└────┬──────┬──────┬─────┘
|
|
383
|
+
│ │ │
|
|
384
|
+
┌────────┘ │ └────────┐
|
|
385
|
+
▼ ▼ ▼
|
|
386
|
+
┌─────────────┐┌─────────────┐┌─────────────┐
|
|
387
|
+
│ Researcher ││ Coder ││ Reporter │
|
|
388
|
+
│ Agent ││ Agent ││ Agent │
|
|
389
|
+
│ ││ ││ │
|
|
390
|
+
│ Tools: ││ Tools: ││ Tools: │
|
|
391
|
+
│ • web_search││ • python ││ • write_doc │
|
|
392
|
+
│ • doc_search││ • shell ││ • format │
|
|
393
|
+
└─────────────┘└─────────────┘└─────────────┘
|
|
394
|
+
</code></pre>
|
|
395
|
+
|
|
396
|
+
<h3 id="4-3-hierarchical-pattern">4.3. Hierarchical Pattern</h3>
|
|
397
|
+
|
|
398
|
+
<p><strong>Hierarchical</strong> mở rộng Supervisor: mỗi worker có thể là supervisor của sub-workers. Phù hợp cho organization phức tạp — ví dụ: CEO agent → Manager agents → Specialist agents.</p>
|
|
399
|
+
|
|
400
|
+
<pre><code class="language-text">
|
|
401
|
+
Hierarchical Multi-Agent
|
|
402
|
+
══════════════════════════════════════════════════════════
|
|
403
|
+
|
|
404
|
+
┌──────────────────────┐
|
|
405
|
+
│ TOP SUPERVISOR │
|
|
406
|
+
│ (Project Manager) │
|
|
407
|
+
└───┬─────────────┬────┘
|
|
408
|
+
│ │
|
|
409
|
+
┌────────┘ └────────┐
|
|
410
|
+
▼ ▼
|
|
411
|
+
┌──────────────┐ ┌──────────────┐
|
|
412
|
+
│ RESEARCH │ │ ENGINEERING │
|
|
413
|
+
│ SUPERVISOR │ │ SUPERVISOR │
|
|
414
|
+
└──┬───────┬───┘ └──┬───────┬───┘
|
|
415
|
+
│ │ │ │
|
|
416
|
+
▼ ▼ ▼ ▼
|
|
417
|
+
[Web [Paper [Backend [Frontend
|
|
418
|
+
Searcher] Analyzer] Dev] Dev]
|
|
419
|
+
</code></pre>
|
|
420
|
+
|
|
421
|
+
<h3 id="4-4-swarm-pattern">4.4. Swarm Pattern</h3>
|
|
422
|
+
|
|
423
|
+
<p><strong>Swarm</strong> (OpenAI Swarm concept) — không có supervisor. Agents tự chuyển tiếp (handoff) cho nhau dựa trên context. Agent A nhận ra "task này thuộc chuyên môn Agent B" → tự handoff.</p>
|
|
424
|
+
|
|
425
|
+
<h3 id="4-5-debate-pattern">4.5. Debate Pattern</h3>
|
|
426
|
+
|
|
427
|
+
<p><strong>Debate</strong> — hai hoặc nhiều agents tranh luận về một câu hỏi. Mỗi agent đưa ra quan điểm, phản bác quan điểm kia. Cuối cùng, một Judge agent chọn kết luận tốt nhất. Pattern này improve reasoning quality cho câu hỏi phức tạp.</p>
|
|
428
|
+
|
|
429
|
+
<h3 id="4-6-pattern-comparison">4.6. So sánh Multi-Agent Patterns</h3>
|
|
430
|
+
|
|
431
|
+
<table>
|
|
432
|
+
<thead>
|
|
433
|
+
<tr><th>Pattern</th><th>Control Flow</th><th>Communication</th><th>Best For</th></tr>
|
|
434
|
+
</thead>
|
|
435
|
+
<tbody>
|
|
436
|
+
<tr><td><strong>Supervisor</strong></td><td>Centralized — supervisor routes</td><td>Hub-and-spoke</td><td>Clear task delegation, moderate complexity</td></tr>
|
|
437
|
+
<tr><td><strong>Hierarchical</strong></td><td>Multi-level supervision</td><td>Tree structure</td><td>Complex orgs, many specialized sub-teams</td></tr>
|
|
438
|
+
<tr><td><strong>Swarm</strong></td><td>Decentralized — agents handoff</td><td>Peer-to-peer</td><td>Customer service, routing, flexible flow</td></tr>
|
|
439
|
+
<tr><td><strong>Debate</strong></td><td>Round-robin argumentation</td><td>Broadcast + judge</td><td>Complex reasoning, fact verification</td></tr>
|
|
440
|
+
</tbody>
|
|
441
|
+
</table>
|
|
442
|
+
|
|
443
|
+
<blockquote><p><strong>Exam tip:</strong> "One LLM routes tasks to specialized agents" → <strong>Supervisor</strong>. "Agents hand off to each other without central control" → <strong>Swarm</strong>. "Multiple agents argue, a judge decides" → <strong>Debate</strong>. "Nested supervisors managing sub-teams" → <strong>Hierarchical</strong>. DLI exam thường tập trung: <strong>Supervisor pattern</strong> vì là phổ biến nhất trong production.</p></blockquote>
|
|
444
|
+
|
|
445
|
+
<h3 id="4-7-code-supervisor-multi-agent">4.7. Code: Supervisor Multi-Agent với LangGraph</h3>
|
|
446
|
+
|
|
447
|
+
<pre><code class="language-python">
|
|
448
|
+
from typing import TypedDict, Annotated, Literal, Sequence
|
|
449
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
450
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
451
|
+
from langchain_core.tools import tool
|
|
452
|
+
from langgraph.graph import StateGraph, END
|
|
453
|
+
from langgraph.prebuilt import ToolNode
|
|
454
|
+
import operator
|
|
455
|
+
|
|
456
|
+
# === State ===
|
|
457
|
+
class MultiAgentState(TypedDict):
|
|
458
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
459
|
+
next_agent: str
|
|
460
|
+
|
|
461
|
+
# === Worker Tools ===
|
|
462
|
+
@tool
|
|
463
|
+
def web_search(query: str) -> str:
|
|
464
|
+
"""Search the web for current information."""
|
|
465
|
+
return f"[Web Result] Top findings for '{query}': ..."
|
|
466
|
+
|
|
467
|
+
@tool
|
|
468
|
+
def run_python(code: str) -> str:
|
|
469
|
+
"""Execute Python code and return output."""
|
|
470
|
+
try:
|
|
471
|
+
exec_globals = {}
|
|
472
|
+
exec(code, exec_globals)
|
|
473
|
+
return str(exec_globals.get("result", "Code executed successfully."))
|
|
474
|
+
except Exception as e:
|
|
475
|
+
return f"Error: {e}"
|
|
476
|
+
|
|
477
|
+
@tool
|
|
478
|
+
def write_report(content: str) -> str:
|
|
479
|
+
"""Format content into a professional report."""
|
|
480
|
+
return f"=== REPORT ===\n{content}\n=== END ==="
|
|
481
|
+
|
|
482
|
+
# === Worker Agents ===
|
|
483
|
+
researcher_llm = ChatNVIDIA(
|
|
484
|
+
model="meta/llama-3.1-70b-instruct", temperature=0.1
|
|
485
|
+
).bind_tools([web_search])
|
|
486
|
+
|
|
487
|
+
coder_llm = ChatNVIDIA(
|
|
488
|
+
model="meta/llama-3.1-70b-instruct", temperature=0.0
|
|
489
|
+
).bind_tools([run_python])
|
|
490
|
+
|
|
491
|
+
reporter_llm = ChatNVIDIA(
|
|
492
|
+
model="meta/llama-3.1-70b-instruct", temperature=0.3
|
|
493
|
+
).bind_tools([write_report])
|
|
494
|
+
|
|
495
|
+
# === Supervisor ===
|
|
496
|
+
supervisor_llm = ChatNVIDIA(
|
|
497
|
+
model="meta/llama-3.1-70b-instruct", temperature=0.0
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
WORKERS = ["researcher", "coder", "reporter"]
|
|
501
|
+
|
|
502
|
+
def supervisor_node(state: MultiAgentState) -> dict:
|
|
503
|
+
"""Supervisor decides which worker to route to next."""
|
|
504
|
+
system_prompt = f"""You are a supervisor managing these workers: {WORKERS}.
|
|
505
|
+
Given the conversation, decide which worker should act next,
|
|
506
|
+
or if the task is complete respond with FINISH.
|
|
507
|
+
Respond with ONLY the worker name or FINISH."""
|
|
508
|
+
|
|
509
|
+
messages = [SystemMessage(content=system_prompt)] + state["messages"]
|
|
510
|
+
response = supervisor_llm.invoke(messages)
|
|
511
|
+
next_agent = response.content.strip().lower()
|
|
512
|
+
|
|
513
|
+
if next_agent not in WORKERS:
|
|
514
|
+
next_agent = "FINISH"
|
|
515
|
+
return {"next_agent": next_agent}
|
|
516
|
+
|
|
517
|
+
def researcher_node(state: MultiAgentState) -> dict:
|
|
518
|
+
system = SystemMessage(content="You are a research specialist. "
|
|
519
|
+
"Use web_search to find information. Be thorough.")
|
|
520
|
+
response = researcher_llm.invoke([system] + state["messages"])
|
|
521
|
+
return {"messages": [response]}
|
|
522
|
+
|
|
523
|
+
def coder_node(state: MultiAgentState) -> dict:
|
|
524
|
+
system = SystemMessage(content="You are a Python coding specialist. "
|
|
525
|
+
"Use run_python to execute code for analysis and calculations.")
|
|
526
|
+
response = coder_llm.invoke([system] + state["messages"])
|
|
527
|
+
return {"messages": [response]}
|
|
528
|
+
|
|
529
|
+
def reporter_node(state: MultiAgentState) -> dict:
|
|
530
|
+
system = SystemMessage(content="You are a report writer. "
|
|
531
|
+
"Use write_report to create formatted reports from gathered info.")
|
|
532
|
+
response = reporter_llm.invoke([system] + state["messages"])
|
|
533
|
+
return {"messages": [response]}
|
|
534
|
+
|
|
535
|
+
# === Routing ===
|
|
536
|
+
def route_supervisor(state: MultiAgentState) -> str:
|
|
537
|
+
next_agent = state.get("next_agent", "FINISH")
|
|
538
|
+
if next_agent == "FINISH":
|
|
539
|
+
return "end"
|
|
540
|
+
return next_agent
|
|
541
|
+
|
|
542
|
+
# === Build Graph ===
|
|
543
|
+
graph = StateGraph(MultiAgentState)
|
|
544
|
+
|
|
545
|
+
graph.add_node("supervisor", supervisor_node)
|
|
546
|
+
graph.add_node("researcher", researcher_node)
|
|
547
|
+
graph.add_node("coder", coder_node)
|
|
548
|
+
graph.add_node("reporter", reporter_node)
|
|
549
|
+
graph.add_node("researcher_tools", ToolNode([web_search]))
|
|
550
|
+
graph.add_node("coder_tools", ToolNode([run_python]))
|
|
551
|
+
graph.add_node("reporter_tools", ToolNode([write_report]))
|
|
552
|
+
|
|
553
|
+
graph.set_entry_point("supervisor")
|
|
554
|
+
|
|
555
|
+
# Supervisor routes to workers
|
|
556
|
+
graph.add_conditional_edges("supervisor", route_supervisor, {
|
|
557
|
+
"researcher": "researcher",
|
|
558
|
+
"coder": "coder",
|
|
559
|
+
"reporter": "reporter",
|
|
560
|
+
"end": END,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
# Workers → tool nodes → back to supervisor
|
|
564
|
+
for worker in WORKERS:
|
|
565
|
+
def make_router(w):
|
|
566
|
+
def router(state):
|
|
567
|
+
last = state["messages"][-1]
|
|
568
|
+
if hasattr(last, "tool_calls") and last.tool_calls:
|
|
569
|
+
return f"{w}_tools"
|
|
570
|
+
return "supervisor"
|
|
571
|
+
return router
|
|
572
|
+
graph.add_conditional_edges(worker, make_router(worker), {
|
|
573
|
+
f"{worker}_tools": f"{worker}_tools",
|
|
574
|
+
"supervisor": "supervisor",
|
|
575
|
+
})
|
|
576
|
+
graph.add_edge(f"{worker}_tools", worker)
|
|
577
|
+
|
|
578
|
+
app = graph.compile()
|
|
579
|
+
|
|
580
|
+
# === Run ===
|
|
581
|
+
result = app.invoke({
|
|
582
|
+
"messages": [HumanMessage(
|
|
583
|
+
content="Research NVIDIA H100 GPU specs, calculate price-performance "
|
|
584
|
+
"ratio vs A100, and write a comparison report."
|
|
585
|
+
)],
|
|
586
|
+
"next_agent": "",
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
for msg in result["messages"]:
|
|
590
|
+
print(f"[{msg.type}] {msg.content[:200]}...")
|
|
591
|
+
</code></pre>
|
|
592
|
+
|
|
593
|
+
<h2 id="5-build-production-ready">5. Build Production-Ready Multi-Agent App</h2>
|
|
594
|
+
|
|
595
|
+
<h3 id="5-1-research-assistant">5.1. Research Assistant — Complete Example</h3>
|
|
596
|
+
|
|
597
|
+
<p>Xây dựng <strong>Research Assistant</strong> hoàn chỉnh với 3 agents: <strong>Researcher</strong> (tìm thông tin), <strong>Coder</strong> (phân tích data), <strong>Reporter</strong> (viết báo cáo). Có error handling, retry logic, và structured output.</p>
|
|
598
|
+
|
|
599
|
+
<pre><code class="language-python">
|
|
600
|
+
from typing import TypedDict, Annotated, Sequence, Optional
|
|
601
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
|
|
602
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
603
|
+
from langchain_core.tools import tool
|
|
604
|
+
from langgraph.graph import StateGraph, END
|
|
605
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
606
|
+
import operator
|
|
607
|
+
import json
|
|
608
|
+
|
|
609
|
+
# === Enhanced State ===
|
|
610
|
+
class ResearchState(TypedDict):
|
|
611
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
612
|
+
research_data: Optional[str] # collected research
|
|
613
|
+
analysis_result: Optional[str] # code analysis output
|
|
614
|
+
final_report: Optional[str] # formatted report
|
|
615
|
+
current_agent: str
|
|
616
|
+
iteration: int # track iterations to prevent loops
|
|
617
|
+
|
|
618
|
+
MAX_ITERATIONS = 10
|
|
619
|
+
|
|
620
|
+
# === Tools ===
|
|
621
|
+
@tool
|
|
622
|
+
def search_arxiv(query: str) -> str:
|
|
623
|
+
"""Search academic papers on arxiv for research topics."""
|
|
624
|
+
return json.dumps({
|
|
625
|
+
"papers": [
|
|
626
|
+
{"title": f"Paper on {query}", "abstract": f"Study of {query}...",
|
|
627
|
+
"year": 2025, "citations": 42},
|
|
628
|
+
]
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
@tool
|
|
632
|
+
def search_web(query: str) -> str:
|
|
633
|
+
"""Search the web for current news, blog posts, documentation."""
|
|
634
|
+
return json.dumps({
|
|
635
|
+
"results": [
|
|
636
|
+
{"title": f"Latest news: {query}", "snippet": f"Updated info on {query}..."},
|
|
637
|
+
]
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
@tool
|
|
641
|
+
def execute_analysis(code: str) -> str:
|
|
642
|
+
"""Run Python code for data analysis. Variable 'result' will be returned."""
|
|
643
|
+
exec_globals = {}
|
|
644
|
+
try:
|
|
645
|
+
exec(code, exec_globals)
|
|
646
|
+
return str(exec_globals.get("result", "Executed OK, no 'result' variable."))
|
|
647
|
+
except Exception as e:
|
|
648
|
+
return f"Error: {e}"
|
|
649
|
+
|
|
650
|
+
@tool
|
|
651
|
+
def generate_report(title: str, sections: str) -> str:
|
|
652
|
+
"""Generate a formatted markdown report from title and section content."""
|
|
653
|
+
return f"# {title}\n\n{sections}\n\n---\nGenerated by Research Assistant"
|
|
654
|
+
|
|
655
|
+
# === Agent Nodes ===
|
|
656
|
+
base_llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct")
|
|
657
|
+
|
|
658
|
+
def researcher_node(state: ResearchState) -> dict:
|
|
659
|
+
llm = base_llm.bind_tools([search_arxiv, search_web])
|
|
660
|
+
system = SystemMessage(content=(
|
|
661
|
+
"You are a research specialist. Search for papers and web results "
|
|
662
|
+
"to gather comprehensive information. Summarize findings clearly."
|
|
663
|
+
))
|
|
664
|
+
response = llm.invoke([system] + list(state["messages"]))
|
|
665
|
+
|
|
666
|
+
# If no tool calls, research is done — extract data
|
|
667
|
+
if not response.tool_calls:
|
|
668
|
+
return {
|
|
669
|
+
"messages": [response],
|
|
670
|
+
"research_data": response.content,
|
|
671
|
+
"current_agent": "supervisor",
|
|
672
|
+
}
|
|
673
|
+
return {"messages": [response], "current_agent": "researcher_tools"}
|
|
674
|
+
|
|
675
|
+
def coder_node(state: ResearchState) -> dict:
|
|
676
|
+
llm = base_llm.bind_tools([execute_analysis])
|
|
677
|
+
context = state.get("research_data", "No research data yet.")
|
|
678
|
+
system = SystemMessage(content=(
|
|
679
|
+
f"You are a data analyst. Use the research data below to perform "
|
|
680
|
+
f"analysis with Python code.\n\nResearch Data:\n{context}"
|
|
681
|
+
))
|
|
682
|
+
response = llm.invoke([system] + list(state["messages"]))
|
|
683
|
+
|
|
684
|
+
if not response.tool_calls:
|
|
685
|
+
return {
|
|
686
|
+
"messages": [response],
|
|
687
|
+
"analysis_result": response.content,
|
|
688
|
+
"current_agent": "supervisor",
|
|
689
|
+
}
|
|
690
|
+
return {"messages": [response], "current_agent": "coder_tools"}
|
|
691
|
+
|
|
692
|
+
def reporter_node(state: ResearchState) -> dict:
|
|
693
|
+
llm = base_llm.bind_tools([generate_report])
|
|
694
|
+
research = state.get("research_data", "N/A")
|
|
695
|
+
analysis = state.get("analysis_result", "N/A")
|
|
696
|
+
system = SystemMessage(content=(
|
|
697
|
+
f"You are a report writer. Create a professional report.\n"
|
|
698
|
+
f"Research:\n{research}\n\nAnalysis:\n{analysis}"
|
|
699
|
+
))
|
|
700
|
+
response = llm.invoke([system] + list(state["messages"]))
|
|
701
|
+
|
|
702
|
+
if not response.tool_calls:
|
|
703
|
+
return {
|
|
704
|
+
"messages": [response],
|
|
705
|
+
"final_report": response.content,
|
|
706
|
+
"current_agent": "supervisor",
|
|
707
|
+
}
|
|
708
|
+
return {"messages": [response], "current_agent": "reporter_tools"}
|
|
709
|
+
|
|
710
|
+
def supervisor_node(state: ResearchState) -> dict:
|
|
711
|
+
iteration = state.get("iteration", 0) + 1
|
|
712
|
+
if iteration > MAX_ITERATIONS:
|
|
713
|
+
return {
|
|
714
|
+
"messages": [AIMessage(content="Max iterations reached. Returning results.")],
|
|
715
|
+
"current_agent": "FINISH",
|
|
716
|
+
"iteration": iteration,
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
system = SystemMessage(content="""You are a project supervisor. Based on the current state:
|
|
720
|
+
- If no research data → route to "researcher"
|
|
721
|
+
- If research done but no analysis → route to "coder"
|
|
722
|
+
- If analysis done but no report → route to "reporter"
|
|
723
|
+
- If report is ready → respond "FINISH"
|
|
724
|
+
Respond with ONLY one of: researcher, coder, reporter, FINISH""")
|
|
725
|
+
|
|
726
|
+
response = base_llm.invoke([system] + list(state["messages"]))
|
|
727
|
+
next_agent = response.content.strip().lower()
|
|
728
|
+
|
|
729
|
+
valid = ["researcher", "coder", "reporter", "finish"]
|
|
730
|
+
if next_agent not in valid:
|
|
731
|
+
next_agent = "researcher" # default fallback
|
|
732
|
+
|
|
733
|
+
return {"current_agent": next_agent, "iteration": iteration}
|
|
734
|
+
|
|
735
|
+
# === Routing ===
|
|
736
|
+
def route_from_supervisor(state: ResearchState) -> str:
|
|
737
|
+
agent = state.get("current_agent", "FINISH")
|
|
738
|
+
if agent in ["researcher", "coder", "reporter"]:
|
|
739
|
+
return agent
|
|
740
|
+
return "end"
|
|
741
|
+
|
|
742
|
+
def route_from_worker(worker_name: str):
|
|
743
|
+
def router(state: ResearchState) -> str:
|
|
744
|
+
current = state.get("current_agent", "supervisor")
|
|
745
|
+
if current == f"{worker_name}_tools":
|
|
746
|
+
return f"{worker_name}_tools"
|
|
747
|
+
return "supervisor"
|
|
748
|
+
return router
|
|
749
|
+
|
|
750
|
+
# === Build Graph ===
|
|
751
|
+
from langgraph.prebuilt import ToolNode
|
|
752
|
+
|
|
753
|
+
graph = StateGraph(ResearchState)
|
|
754
|
+
|
|
755
|
+
graph.add_node("supervisor", supervisor_node)
|
|
756
|
+
graph.add_node("researcher", researcher_node)
|
|
757
|
+
graph.add_node("coder", coder_node)
|
|
758
|
+
graph.add_node("reporter", reporter_node)
|
|
759
|
+
graph.add_node("researcher_tools", ToolNode([search_arxiv, search_web]))
|
|
760
|
+
graph.add_node("coder_tools", ToolNode([execute_analysis]))
|
|
761
|
+
graph.add_node("reporter_tools", ToolNode([generate_report]))
|
|
762
|
+
|
|
763
|
+
graph.set_entry_point("supervisor")
|
|
764
|
+
|
|
765
|
+
graph.add_conditional_edges("supervisor", route_from_supervisor, {
|
|
766
|
+
"researcher": "researcher",
|
|
767
|
+
"coder": "coder",
|
|
768
|
+
"reporter": "reporter",
|
|
769
|
+
"end": END,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
for worker in ["researcher", "coder", "reporter"]:
|
|
773
|
+
graph.add_conditional_edges(worker, route_from_worker(worker), {
|
|
774
|
+
f"{worker}_tools": f"{worker}_tools",
|
|
775
|
+
"supervisor": "supervisor",
|
|
776
|
+
})
|
|
777
|
+
graph.add_edge(f"{worker}_tools", worker)
|
|
778
|
+
|
|
779
|
+
# Compile with checkpointing
|
|
780
|
+
checkpointer = MemorySaver()
|
|
781
|
+
app = graph.compile(checkpointer=checkpointer)
|
|
782
|
+
|
|
783
|
+
# === Execute ===
|
|
784
|
+
config = {"configurable": {"thread_id": "research-001"}}
|
|
785
|
+
result = app.invoke(
|
|
786
|
+
{
|
|
787
|
+
"messages": [HumanMessage(
|
|
788
|
+
content="Research the latest advances in mixture-of-experts (MoE) "
|
|
789
|
+
"models, analyze their parameter efficiency compared to "
|
|
790
|
+
"dense models, and write a summary report."
|
|
791
|
+
)],
|
|
792
|
+
"current_agent": "",
|
|
793
|
+
"iteration": 0,
|
|
794
|
+
},
|
|
795
|
+
config=config,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# Print final report
|
|
799
|
+
print(result.get("final_report", result["messages"][-1].content))
|
|
800
|
+
</code></pre>
|
|
801
|
+
|
|
802
|
+
<h3 id="5-2-production-best-practices">5.2. Production Best Practices</h3>
|
|
803
|
+
|
|
804
|
+
<table>
|
|
805
|
+
<thead>
|
|
806
|
+
<tr><th>Practice</th><th>Why</th><th>Implementation</th></tr>
|
|
807
|
+
</thead>
|
|
808
|
+
<tbody>
|
|
809
|
+
<tr><td><strong>Max iterations</strong></td><td>Prevent infinite loops</td><td><code>iteration</code> counter in state, check at supervisor</td></tr>
|
|
810
|
+
<tr><td><strong>Error handling</strong></td><td>Tool failures shouldn't crash agent</td><td>try/except in tools, return error message</td></tr>
|
|
811
|
+
<tr><td><strong>Checkpointing</strong></td><td>Resume after crash</td><td><code>MemorySaver</code> (dev) / <code>SqliteSaver</code> (prod)</td></tr>
|
|
812
|
+
<tr><td><strong>Structured output</strong></td><td>Reliable routing decisions</td><td>Constrain supervisor output to valid choices</td></tr>
|
|
813
|
+
<tr><td><strong>Observability</strong></td><td>Debug multi-agent is hard</td><td>LangSmith tracing, log each node entry/exit</td></tr>
|
|
814
|
+
<tr><td><strong>Timeout per node</strong></td><td>Single node shouldn't block</td><td>Set timeout on LLM calls and tool executions</td></tr>
|
|
815
|
+
<tr><td><strong>Human-in-the-loop</strong></td><td>Critical actions need approval</td><td><code>interrupt_before</code> on dangerous tool nodes</td></tr>
|
|
816
|
+
</tbody>
|
|
817
|
+
</table>
|
|
818
|
+
|
|
819
|
+
<blockquote><p><strong>Exam tip:</strong> "How to prevent agent infinite loops?" → <strong>max_iterations + iteration counter</strong>. "How to debug multi-agent systems?" → <strong>LangSmith tracing + logging</strong>. "Agent crash recovery?" → <strong>Checkpointing with persistent storage</strong>. Production deployment → <strong>LangGraph Platform</strong> (managed) hoặc <strong>LangServe</strong> (self-hosted).</p></blockquote>
|
|
820
|
+
|
|
821
|
+
<h2 id="6-dli-cfx25-overview">6. DLI C-FX-25 — Agentic AI Course Overview</h2>
|
|
822
|
+
|
|
823
|
+
<h3 id="6-1-course-structure">6.1. Course Structure</h3>
|
|
824
|
+
|
|
825
|
+
<p>Course <strong>C-FX-25: "Building Agentic AI Applications"</strong> là module nâng cao trong DLI, tập trung vào xây dựng hệ thống Agentic AI production-ready. Course bổ sung cho S-FX-15 bằng cách đi sâu vào agent architectures.</p>
|
|
826
|
+
|
|
827
|
+
<table>
|
|
828
|
+
<thead>
|
|
829
|
+
<tr><th>Module</th><th>Topics</th><th>Hands-On</th></tr>
|
|
830
|
+
</thead>
|
|
831
|
+
<tbody>
|
|
832
|
+
<tr><td>Module 1</td><td>Agent fundamentals, ReAct, tool calling</td><td>Build single agent with NVIDIA NIM</td></tr>
|
|
833
|
+
<tr><td>Module 2</td><td>LangGraph introduction, StateGraph</td><td>Implement custom agent graph</td></tr>
|
|
834
|
+
<tr><td>Module 3</td><td>Multi-agent architectures</td><td>Build supervisor multi-agent system</td></tr>
|
|
835
|
+
<tr><td>Module 4</td><td>Advanced: memory, planning, eval</td><td>Production deployment exercise</td></tr>
|
|
836
|
+
</tbody>
|
|
837
|
+
</table>
|
|
838
|
+
|
|
839
|
+
<h3 id="6-2-assessment-focus">6.2. Assessment Focus Areas</h3>
|
|
840
|
+
|
|
841
|
+
<p>C-FX-25 assessment tập trung vào <strong>hands-on implementation</strong>:</p>
|
|
842
|
+
|
|
843
|
+
<ul>
|
|
844
|
+
<li><strong>LangGraph StateGraph</strong> — define state, nodes, conditional edges</li>
|
|
845
|
+
<li><strong>Tool integration</strong> — bind tools to LLM, handle tool calls</li>
|
|
846
|
+
<li><strong>Supervisor routing</strong> — implement supervisor logic, route to workers</li>
|
|
847
|
+
<li><strong>Checkpointing</strong> — save/restore agent state</li>
|
|
848
|
+
<li><strong>Human-in-the-loop</strong> — interrupt_before, approve, resume</li>
|
|
849
|
+
</ul>
|
|
850
|
+
|
|
851
|
+
<h3 id="6-3-key-apis">6.3. Key APIs to Memorize</h3>
|
|
852
|
+
|
|
853
|
+
<table>
|
|
854
|
+
<thead>
|
|
855
|
+
<tr><th>API / Concept</th><th>Usage</th></tr>
|
|
856
|
+
</thead>
|
|
857
|
+
<tbody>
|
|
858
|
+
<tr><td><code>StateGraph(State)</code></td><td>Create graph with typed state</td></tr>
|
|
859
|
+
<tr><td><code>graph.add_node(name, func)</code></td><td>Add processing node</td></tr>
|
|
860
|
+
<tr><td><code>graph.add_edge(A, B)</code></td><td>Always route A → B</td></tr>
|
|
861
|
+
<tr><td><code>graph.add_conditional_edges(A, func, map)</code></td><td>Route based on function output</td></tr>
|
|
862
|
+
<tr><td><code>graph.set_entry_point(name)</code></td><td>Set starting node</td></tr>
|
|
863
|
+
<tr><td><code>graph.compile(checkpointer=...)</code></td><td>Compile graph, optional checkpointer</td></tr>
|
|
864
|
+
<tr><td><code>ToolNode(tools)</code></td><td>Pre-built node that executes tool calls</td></tr>
|
|
865
|
+
<tr><td><code>MemorySaver()</code></td><td>In-memory checkpointer (dev only)</td></tr>
|
|
866
|
+
<tr><td><code>interrupt_before=[node]</code></td><td>Pause before executing node</td></tr>
|
|
867
|
+
<tr><td><code>llm.bind_tools(tools)</code></td><td>Attach tools to LLM for function calling</td></tr>
|
|
868
|
+
</tbody>
|
|
869
|
+
</table>
|
|
870
|
+
|
|
871
|
+
<blockquote><p><strong>Exam tip:</strong> C-FX-25 assessment yêu cầu <strong>viết code LangGraph từ đầu</strong>. Nhớ rõ pattern: (1) Define State TypedDict, (2) Define nodes as functions, (3) Add nodes + edges, (4) Compile + run. Không cần thuộc lòng API nhưng phải hiểu <strong>flow</strong>: state travels through nodes, conditional edges route dynamically.</p></blockquote>
|
|
872
|
+
|
|
873
|
+
<h2 id="7-cheat-sheet">7. Cheat Sheet</h2>
|
|
874
|
+
|
|
875
|
+
<table>
|
|
876
|
+
<thead>
|
|
877
|
+
<tr><th>Concept</th><th>Key Point</th></tr>
|
|
878
|
+
</thead>
|
|
879
|
+
<tbody>
|
|
880
|
+
<tr><td>Agent components</td><td>LLM + Memory + Tools + Planning</td></tr>
|
|
881
|
+
<tr><td>Agent loop</td><td>Perception → Reasoning → Action → Observation</td></tr>
|
|
882
|
+
<tr><td>Agent vs Chain</td><td>Agent = dynamic flow (LLM decides); Chain = fixed flow</td></tr>
|
|
883
|
+
<tr><td>ReAct</td><td>Interleave Thought + Action + Observation. Simple, myopic</td></tr>
|
|
884
|
+
<tr><td>Plan-and-Execute</td><td>Plan upfront → execute steps → replan if needed</td></tr>
|
|
885
|
+
<tr><td>LATS</td><td>Tree search over reasoning paths. Expensive but robust</td></tr>
|
|
886
|
+
<tr><td>Reflexion</td><td>Execute → self-reflect → retry with lessons</td></tr>
|
|
887
|
+
<tr><td>LangGraph</td><td>StateGraph: nodes + edges + conditional routing</td></tr>
|
|
888
|
+
<tr><td>LangGraph State</td><td>TypedDict shared across all nodes</td></tr>
|
|
889
|
+
<tr><td>Conditional edges</td><td>Router function decides next node</td></tr>
|
|
890
|
+
<tr><td>Checkpointing</td><td>MemorySaver (dev), SqliteSaver (prod). Enable resume</td></tr>
|
|
891
|
+
<tr><td>Human-in-the-loop</td><td>interrupt_before=[node] + compile with checkpointer</td></tr>
|
|
892
|
+
<tr><td>Supervisor pattern</td><td>Central LLM routes to specialized worker agents</td></tr>
|
|
893
|
+
<tr><td>Hierarchical</td><td>Nested supervisors — tree of agents</td></tr>
|
|
894
|
+
<tr><td>Swarm</td><td>Decentralized handoffs, no central supervisor</td></tr>
|
|
895
|
+
<tr><td>Debate</td><td>Agents argue, judge decides. Better reasoning</td></tr>
|
|
896
|
+
<tr><td>Max iterations</td><td>Always set to prevent infinite agent loops</td></tr>
|
|
897
|
+
<tr><td>ToolNode</td><td>LangGraph pre-built node to execute tool calls</td></tr>
|
|
898
|
+
<tr><td>C-FX-25 focus</td><td>LangGraph coding, multi-agent, checkpointing, HITL</td></tr>
|
|
899
|
+
</tbody>
|
|
900
|
+
</table>
|
|
901
|
+
|
|
902
|
+
<h2 id="8-practice-questions">8. Practice Questions — Coding</h2>
|
|
903
|
+
|
|
904
|
+
<p><strong>Q1: Build a basic LangGraph ReAct agent</strong></p>
|
|
905
|
+
<p>Xây dựng một LangGraph agent đơn giản với 2 tools: <code>search_docs</code> (tìm tài liệu) và <code>calculator</code> (tính toán). Implement đầy đủ: State, agent node, tool node, conditional edge routing, compile và run.</p>
|
|
906
|
+
|
|
907
|
+
<details>
|
|
908
|
+
<summary>Xem đáp án Q1</summary>
|
|
909
|
+
|
|
910
|
+
<pre><code class="language-python">
|
|
911
|
+
from typing import TypedDict, Annotated, Sequence
|
|
912
|
+
from langchain_core.messages import BaseMessage, HumanMessage
|
|
913
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
914
|
+
from langchain_core.tools import tool
|
|
915
|
+
from langgraph.graph import StateGraph, END
|
|
916
|
+
from langgraph.prebuilt import ToolNode
|
|
917
|
+
import operator
|
|
918
|
+
|
|
919
|
+
# 1. State
|
|
920
|
+
class AgentState(TypedDict):
|
|
921
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
922
|
+
|
|
923
|
+
# 2. Tools
|
|
924
|
+
@tool
|
|
925
|
+
def search_docs(query: str) -> str:
|
|
926
|
+
"""Search internal knowledge base for relevant documents."""
|
|
927
|
+
return f"Found: Documentation about {query} — key facts here."
|
|
928
|
+
|
|
929
|
+
@tool
|
|
930
|
+
def calculator(expression: str) -> str:
|
|
931
|
+
"""Calculate a mathematical expression."""
|
|
932
|
+
return str(eval(expression))
|
|
933
|
+
|
|
934
|
+
tools = [search_docs, calculator]
|
|
935
|
+
|
|
936
|
+
# 3. LLM with tools
|
|
937
|
+
llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct", temperature=0.0)
|
|
938
|
+
llm_with_tools = llm.bind_tools(tools)
|
|
939
|
+
|
|
940
|
+
# 4. Nodes
|
|
941
|
+
def agent_node(state: AgentState) -> dict:
|
|
942
|
+
response = llm_with_tools.invoke(state["messages"])
|
|
943
|
+
return {"messages": [response]}
|
|
944
|
+
|
|
945
|
+
tool_node = ToolNode(tools)
|
|
946
|
+
|
|
947
|
+
# 5. Router
|
|
948
|
+
def should_continue(state: AgentState) -> str:
|
|
949
|
+
last = state["messages"][-1]
|
|
950
|
+
if hasattr(last, "tool_calls") and last.tool_calls:
|
|
951
|
+
return "tools"
|
|
952
|
+
return "end"
|
|
953
|
+
|
|
954
|
+
# 6. Build graph
|
|
955
|
+
graph = StateGraph(AgentState)
|
|
956
|
+
graph.add_node("agent", agent_node)
|
|
957
|
+
graph.add_node("tools", tool_node)
|
|
958
|
+
graph.set_entry_point("agent")
|
|
959
|
+
graph.add_conditional_edges("agent", should_continue, {
|
|
960
|
+
"tools": "tools",
|
|
961
|
+
"end": END,
|
|
962
|
+
})
|
|
963
|
+
graph.add_edge("tools", "agent")
|
|
964
|
+
|
|
965
|
+
app = graph.compile()
|
|
966
|
+
|
|
967
|
+
# 7. Run
|
|
968
|
+
result = app.invoke({
|
|
969
|
+
"messages": [HumanMessage(content="What is 25 * 4 + 100?")]
|
|
970
|
+
})
|
|
971
|
+
print(result["messages"][-1].content)
|
|
972
|
+
</code></pre>
|
|
973
|
+
</details>
|
|
974
|
+
|
|
975
|
+
<p><strong>Q2: Implement human-in-the-loop with LangGraph checkpointing</strong></p>
|
|
976
|
+
<p>Modify agent từ Q1 để thêm <strong>human-in-the-loop</strong>: agent pause trước khi thực thi tools, user có thể approve hoặc reject. Demonstrate: (1) compile với checkpointer + interrupt_before, (2) run và thấy agent pause, (3) resume execution.</p>
|
|
977
|
+
|
|
978
|
+
<details>
|
|
979
|
+
<summary>Xem đáp án Q2</summary>
|
|
980
|
+
|
|
981
|
+
<pre><code class="language-python">
|
|
982
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
983
|
+
|
|
984
|
+
# Reuse graph from Q1, compile with HITL
|
|
985
|
+
checkpointer = MemorySaver()
|
|
986
|
+
app_hitl = graph.compile(
|
|
987
|
+
checkpointer=checkpointer,
|
|
988
|
+
interrupt_before=["tools"] # Pause BEFORE tool execution
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
# Run — agent will pause before calling tools
|
|
992
|
+
config = {"configurable": {"thread_id": "hitl-demo-001"}}
|
|
993
|
+
result = app_hitl.invoke(
|
|
994
|
+
{"messages": [HumanMessage(content="Calculate 1000 / 4")]},
|
|
995
|
+
config=config,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
# Agent paused — inspect what it wants to do
|
|
999
|
+
last_msg = result["messages"][-1]
|
|
1000
|
+
print("Agent wants to call:")
|
|
1001
|
+
for tc in last_msg.tool_calls:
|
|
1002
|
+
print(f" Tool: {tc['name']}, Args: {tc['args']}")
|
|
1003
|
+
|
|
1004
|
+
# User approves → resume (pass None to continue from checkpoint)
|
|
1005
|
+
final_result = app_hitl.invoke(None, config=config)
|
|
1006
|
+
print("\nFinal answer:", final_result["messages"][-1].content)
|
|
1007
|
+
|
|
1008
|
+
# If user REJECTS → could modify state or stop here
|
|
1009
|
+
# To reject: simply don't call invoke(None, config)
|
|
1010
|
+
</code></pre>
|
|
1011
|
+
</details>
|
|
1012
|
+
|
|
1013
|
+
<p><strong>Q3: Build a Supervisor multi-agent system</strong></p>
|
|
1014
|
+
<p>Implement <strong>Supervisor pattern</strong> với 2 workers: <code>researcher</code> (sử dụng web_search tool) và <code>writer</code> (sử dụng write_report tool). Supervisor nhận task từ user, route đến worker phù hợp, thu thập kết quả. Implement routing logic với conditional edges.</p>
|
|
1015
|
+
|
|
1016
|
+
<details>
|
|
1017
|
+
<summary>Xem đáp án Q3</summary>
|
|
1018
|
+
|
|
1019
|
+
<pre><code class="language-python">
|
|
1020
|
+
from typing import TypedDict, Annotated, Sequence
|
|
1021
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
1022
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
1023
|
+
from langchain_core.tools import tool
|
|
1024
|
+
from langgraph.graph import StateGraph, END
|
|
1025
|
+
from langgraph.prebuilt import ToolNode
|
|
1026
|
+
import operator
|
|
1027
|
+
|
|
1028
|
+
class SupervisorState(TypedDict):
|
|
1029
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
1030
|
+
next: str
|
|
1031
|
+
|
|
1032
|
+
@tool
|
|
1033
|
+
def web_search(query: str) -> str:
|
|
1034
|
+
"""Search the internet for information."""
|
|
1035
|
+
return f"Search results for '{query}': ..."
|
|
1036
|
+
|
|
1037
|
+
@tool
|
|
1038
|
+
def write_report(content: str) -> str:
|
|
1039
|
+
"""Write and format a professional report."""
|
|
1040
|
+
return f"=== Report ===\n{content}\n=== End ==="
|
|
1041
|
+
|
|
1042
|
+
llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct", temperature=0.0)
|
|
1043
|
+
|
|
1044
|
+
def supervisor(state: SupervisorState) -> dict:
|
|
1045
|
+
sys = SystemMessage(content=(
|
|
1046
|
+
"You are a supervisor. Workers: researcher, writer. "
|
|
1047
|
+
"Route to appropriate worker or say FINISH if task complete. "
|
|
1048
|
+
"Respond with ONLY: researcher, writer, or FINISH."
|
|
1049
|
+
))
|
|
1050
|
+
resp = llm.invoke([sys] + list(state["messages"]))
|
|
1051
|
+
next_val = resp.content.strip().lower()
|
|
1052
|
+
if next_val not in ["researcher", "writer"]:
|
|
1053
|
+
next_val = "FINISH"
|
|
1054
|
+
return {"next": next_val}
|
|
1055
|
+
|
|
1056
|
+
def researcher(state: SupervisorState) -> dict:
|
|
1057
|
+
r_llm = llm.bind_tools([web_search])
|
|
1058
|
+
sys = SystemMessage(content="You are a researcher. Use web_search.")
|
|
1059
|
+
resp = r_llm.invoke([sys] + list(state["messages"]))
|
|
1060
|
+
return {"messages": [resp]}
|
|
1061
|
+
|
|
1062
|
+
def writer(state: SupervisorState) -> dict:
|
|
1063
|
+
w_llm = llm.bind_tools([write_report])
|
|
1064
|
+
sys = SystemMessage(content="You are a report writer. Use write_report.")
|
|
1065
|
+
resp = w_llm.invoke([sys] + list(state["messages"]))
|
|
1066
|
+
return {"messages": [resp]}
|
|
1067
|
+
|
|
1068
|
+
def route(state: SupervisorState) -> str:
|
|
1069
|
+
n = state.get("next", "FINISH")
|
|
1070
|
+
return n if n in ["researcher", "writer"] else "end"
|
|
1071
|
+
|
|
1072
|
+
# Build
|
|
1073
|
+
g = StateGraph(SupervisorState)
|
|
1074
|
+
g.add_node("supervisor", supervisor)
|
|
1075
|
+
g.add_node("researcher", researcher)
|
|
1076
|
+
g.add_node("writer", writer)
|
|
1077
|
+
g.add_node("research_tools", ToolNode([web_search]))
|
|
1078
|
+
g.add_node("writer_tools", ToolNode([write_report]))
|
|
1079
|
+
|
|
1080
|
+
g.set_entry_point("supervisor")
|
|
1081
|
+
g.add_conditional_edges("supervisor", route, {
|
|
1082
|
+
"researcher": "researcher",
|
|
1083
|
+
"writer": "writer",
|
|
1084
|
+
"end": END,
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
# Researcher flow
|
|
1088
|
+
def route_researcher(state):
|
|
1089
|
+
last = state["messages"][-1]
|
|
1090
|
+
if hasattr(last, "tool_calls") and last.tool_calls:
|
|
1091
|
+
return "research_tools"
|
|
1092
|
+
return "supervisor"
|
|
1093
|
+
|
|
1094
|
+
g.add_conditional_edges("researcher", route_researcher, {
|
|
1095
|
+
"research_tools": "research_tools",
|
|
1096
|
+
"supervisor": "supervisor",
|
|
1097
|
+
})
|
|
1098
|
+
g.add_edge("research_tools", "researcher")
|
|
1099
|
+
|
|
1100
|
+
# Writer flow
|
|
1101
|
+
def route_writer(state):
|
|
1102
|
+
last = state["messages"][-1]
|
|
1103
|
+
if hasattr(last, "tool_calls") and last.tool_calls:
|
|
1104
|
+
return "writer_tools"
|
|
1105
|
+
return "supervisor"
|
|
1106
|
+
|
|
1107
|
+
g.add_conditional_edges("writer", route_writer, {
|
|
1108
|
+
"writer_tools": "writer_tools",
|
|
1109
|
+
"supervisor": "supervisor",
|
|
1110
|
+
})
|
|
1111
|
+
g.add_edge("writer_tools", "writer")
|
|
1112
|
+
|
|
1113
|
+
app = g.compile()
|
|
1114
|
+
|
|
1115
|
+
result = app.invoke({
|
|
1116
|
+
"messages": [HumanMessage(content="Research AI trends 2025 and write a report")],
|
|
1117
|
+
"next": "",
|
|
1118
|
+
})
|
|
1119
|
+
print(result["messages"][-1].content)
|
|
1120
|
+
</code></pre>
|
|
1121
|
+
</details>
|
|
1122
|
+
|
|
1123
|
+
<p><strong>Q4: Add Plan-and-Execute to a LangGraph agent</strong></p>
|
|
1124
|
+
<p>Implement <strong>Plan-and-Execute</strong> pattern: (1) Planner node tạo danh sách steps từ user query, (2) Executor node thực hiện từng step, (3) Replanner node kiểm tra progress và điều chỉnh plan nếu cần. Lưu plan trong state.</p>
|
|
1125
|
+
|
|
1126
|
+
<details>
|
|
1127
|
+
<summary>Xem đáp án Q4</summary>
|
|
1128
|
+
|
|
1129
|
+
<pre><code class="language-python">
|
|
1130
|
+
from typing import TypedDict, Annotated, Sequence, List, Optional
|
|
1131
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
1132
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
1133
|
+
from langgraph.graph import StateGraph, END
|
|
1134
|
+
import operator, json
|
|
1135
|
+
|
|
1136
|
+
class PlanExecState(TypedDict):
|
|
1137
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
1138
|
+
plan: List[str] # list of steps
|
|
1139
|
+
current_step: int # index of current step
|
|
1140
|
+
step_results: List[str] # results of each step
|
|
1141
|
+
done: bool
|
|
1142
|
+
|
|
1143
|
+
llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct", temperature=0.0)
|
|
1144
|
+
|
|
1145
|
+
def planner_node(state: PlanExecState) -> dict:
|
|
1146
|
+
"""Create a plan from user request."""
|
|
1147
|
+
sys = SystemMessage(content=(
|
|
1148
|
+
"You are a planner. Break the user's request into 3-5 concrete steps. "
|
|
1149
|
+
"Return ONLY a JSON array of strings, e.g. [\"step1\", \"step2\"]."
|
|
1150
|
+
))
|
|
1151
|
+
resp = llm.invoke([sys] + list(state["messages"]))
|
|
1152
|
+
try:
|
|
1153
|
+
plan = json.loads(resp.content)
|
|
1154
|
+
except json.JSONDecodeError:
|
|
1155
|
+
plan = [resp.content]
|
|
1156
|
+
return {"plan": plan, "current_step": 0, "step_results": []}
|
|
1157
|
+
|
|
1158
|
+
def executor_node(state: PlanExecState) -> dict:
|
|
1159
|
+
"""Execute the current step of the plan."""
|
|
1160
|
+
step_idx = state["current_step"]
|
|
1161
|
+
plan = state["plan"]
|
|
1162
|
+
if step_idx >= len(plan):
|
|
1163
|
+
return {"done": True}
|
|
1164
|
+
|
|
1165
|
+
current = plan[step_idx]
|
|
1166
|
+
sys = SystemMessage(content=(
|
|
1167
|
+
f"Execute this step: {current}\n"
|
|
1168
|
+
f"Previous results: {state['step_results']}\n"
|
|
1169
|
+
"Provide a concise result."
|
|
1170
|
+
))
|
|
1171
|
+
resp = llm.invoke([sys] + list(state["messages"]))
|
|
1172
|
+
new_results = list(state["step_results"]) + [resp.content]
|
|
1173
|
+
return {
|
|
1174
|
+
"step_results": new_results,
|
|
1175
|
+
"current_step": step_idx + 1,
|
|
1176
|
+
"messages": [resp],
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
def replanner_node(state: PlanExecState) -> dict:
|
|
1180
|
+
"""Check progress, adjust plan if needed."""
|
|
1181
|
+
if state["current_step"] >= len(state["plan"]):
|
|
1182
|
+
return {"done": True}
|
|
1183
|
+
|
|
1184
|
+
sys = SystemMessage(content=(
|
|
1185
|
+
f"Plan: {state['plan']}\n"
|
|
1186
|
+
f"Completed: {state['current_step']}/{len(state['plan'])}\n"
|
|
1187
|
+
f"Results so far: {state['step_results']}\n"
|
|
1188
|
+
"Should the remaining plan continue as-is? "
|
|
1189
|
+
"Reply 'CONTINUE' or provide updated remaining steps as JSON array."
|
|
1190
|
+
))
|
|
1191
|
+
resp = llm.invoke([sys])
|
|
1192
|
+
if "CONTINUE" in resp.content.upper():
|
|
1193
|
+
return {"done": False}
|
|
1194
|
+
try:
|
|
1195
|
+
remaining = json.loads(resp.content)
|
|
1196
|
+
new_plan = state["plan"][:state["current_step"]] + remaining
|
|
1197
|
+
return {"plan": new_plan, "done": False}
|
|
1198
|
+
except json.JSONDecodeError:
|
|
1199
|
+
return {"done": False}
|
|
1200
|
+
|
|
1201
|
+
def route_after_exec(state: PlanExecState) -> str:
|
|
1202
|
+
if state.get("done", False):
|
|
1203
|
+
return "end"
|
|
1204
|
+
return "replanner"
|
|
1205
|
+
|
|
1206
|
+
def route_after_replan(state: PlanExecState) -> str:
|
|
1207
|
+
if state.get("done", False):
|
|
1208
|
+
return "end"
|
|
1209
|
+
return "executor"
|
|
1210
|
+
|
|
1211
|
+
# Build graph
|
|
1212
|
+
g = StateGraph(PlanExecState)
|
|
1213
|
+
g.add_node("planner", planner_node)
|
|
1214
|
+
g.add_node("executor", executor_node)
|
|
1215
|
+
g.add_node("replanner", replanner_node)
|
|
1216
|
+
|
|
1217
|
+
g.set_entry_point("planner")
|
|
1218
|
+
g.add_edge("planner", "executor")
|
|
1219
|
+
g.add_conditional_edges("executor", route_after_exec, {
|
|
1220
|
+
"replanner": "replanner",
|
|
1221
|
+
"end": END,
|
|
1222
|
+
})
|
|
1223
|
+
g.add_conditional_edges("replanner", route_after_replan, {
|
|
1224
|
+
"executor": "executor",
|
|
1225
|
+
"end": END,
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
app = g.compile()
|
|
1229
|
+
|
|
1230
|
+
result = app.invoke({
|
|
1231
|
+
"messages": [HumanMessage(
|
|
1232
|
+
content="Analyze the pros and cons of microservices architecture "
|
|
1233
|
+
"and recommend when to use it vs monolith."
|
|
1234
|
+
)],
|
|
1235
|
+
"plan": [],
|
|
1236
|
+
"current_step": 0,
|
|
1237
|
+
"step_results": [],
|
|
1238
|
+
"done": False,
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
for i, res in enumerate(result["step_results"]):
|
|
1242
|
+
print(f"Step {i+1}: {res[:150]}...")
|
|
1243
|
+
</code></pre>
|
|
1244
|
+
</details>
|
|
1245
|
+
|
|
1246
|
+
<p><strong>Q5: Implement error handling and retry logic in multi-agent system</strong></p>
|
|
1247
|
+
<p>Thêm <strong>error handling</strong> vào multi-agent system: (1) Tool failures return error message thay vì crash, (2) Agent nhận error → retry với strategy khác (max 2 retries), (3) Agent state track số lần retry. Implement node wrapper pattern.</p>
|
|
1248
|
+
|
|
1249
|
+
<details>
|
|
1250
|
+
<summary>Xem đáp án Q5</summary>
|
|
1251
|
+
|
|
1252
|
+
<pre><code class="language-python">
|
|
1253
|
+
from typing import TypedDict, Annotated, Sequence, Dict
|
|
1254
|
+
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
|
1255
|
+
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
|
1256
|
+
from langchain_core.tools import tool
|
|
1257
|
+
from langgraph.graph import StateGraph, END
|
|
1258
|
+
from langgraph.prebuilt import ToolNode
|
|
1259
|
+
import operator
|
|
1260
|
+
|
|
1261
|
+
class RobustState(TypedDict):
|
|
1262
|
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
|
1263
|
+
error_count: int
|
|
1264
|
+
max_retries: int
|
|
1265
|
+
|
|
1266
|
+
# Tools with error handling built-in
|
|
1267
|
+
@tool
|
|
1268
|
+
def risky_api_call(endpoint: str) -> str:
|
|
1269
|
+
"""Call an external API that might fail."""
|
|
1270
|
+
import random
|
|
1271
|
+
if random.random() < 0.5:
|
|
1272
|
+
raise ConnectionError(f"API {endpoint} unreachable")
|
|
1273
|
+
return f"API response from {endpoint}: success data"
|
|
1274
|
+
|
|
1275
|
+
@tool
|
|
1276
|
+
def safe_search(query: str) -> str:
|
|
1277
|
+
"""Search with built-in error handling."""
|
|
1278
|
+
return f"Results for {query}: ..."
|
|
1279
|
+
|
|
1280
|
+
# Wrap tools with error handling
|
|
1281
|
+
def safe_tool_node(tools):
|
|
1282
|
+
"""ToolNode wrapper that catches errors and returns error messages."""
|
|
1283
|
+
base_node = ToolNode(tools)
|
|
1284
|
+
def wrapper(state: RobustState) -> dict:
|
|
1285
|
+
try:
|
|
1286
|
+
return base_node.invoke(state)
|
|
1287
|
+
except Exception as e:
|
|
1288
|
+
error_msg = AIMessage(content=f"Tool error: {str(e)}. Try different approach.")
|
|
1289
|
+
return {
|
|
1290
|
+
"messages": [error_msg],
|
|
1291
|
+
"error_count": state.get("error_count", 0) + 1,
|
|
1292
|
+
}
|
|
1293
|
+
return wrapper
|
|
1294
|
+
|
|
1295
|
+
tools = [risky_api_call, safe_search]
|
|
1296
|
+
llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct", temperature=0.0)
|
|
1297
|
+
llm_with_tools = llm.bind_tools(tools)
|
|
1298
|
+
|
|
1299
|
+
def agent_node(state: RobustState) -> dict:
|
|
1300
|
+
error_count = state.get("error_count", 0)
|
|
1301
|
+
max_retries = state.get("max_retries", 2)
|
|
1302
|
+
|
|
1303
|
+
# If too many errors, give up gracefully
|
|
1304
|
+
if error_count >= max_retries:
|
|
1305
|
+
return {"messages": [AIMessage(
|
|
1306
|
+
content="I encountered multiple errors. Here's what I could gather "
|
|
1307
|
+
"from successful attempts: " +
|
|
1308
|
+
" | ".join(m.content for m in state["messages"][-3:])
|
|
1309
|
+
)]}
|
|
1310
|
+
|
|
1311
|
+
# Add retry context if there were errors
|
|
1312
|
+
msgs = list(state["messages"])
|
|
1313
|
+
if error_count > 0:
|
|
1314
|
+
msgs.append(HumanMessage(
|
|
1315
|
+
content=f"Previous attempt failed ({error_count}/{max_retries} retries). "
|
|
1316
|
+
"Try a different tool or approach."
|
|
1317
|
+
))
|
|
1318
|
+
|
|
1319
|
+
response = llm_with_tools.invoke(msgs)
|
|
1320
|
+
return {"messages": [response]}
|
|
1321
|
+
|
|
1322
|
+
def should_continue(state: RobustState) -> str:
|
|
1323
|
+
last = state["messages"][-1]
|
|
1324
|
+
error_count = state.get("error_count", 0)
|
|
1325
|
+
max_retries = state.get("max_retries", 2)
|
|
1326
|
+
|
|
1327
|
+
# Stop if max retries exceeded
|
|
1328
|
+
if error_count >= max_retries and not (
|
|
1329
|
+
hasattr(last, "tool_calls") and last.tool_calls
|
|
1330
|
+
):
|
|
1331
|
+
return "end"
|
|
1332
|
+
|
|
1333
|
+
if hasattr(last, "tool_calls") and last.tool_calls:
|
|
1334
|
+
return "tools"
|
|
1335
|
+
return "end"
|
|
1336
|
+
|
|
1337
|
+
# Build
|
|
1338
|
+
g = StateGraph(RobustState)
|
|
1339
|
+
g.add_node("agent", agent_node)
|
|
1340
|
+
g.add_node("tools", safe_tool_node(tools))
|
|
1341
|
+
g.set_entry_point("agent")
|
|
1342
|
+
g.add_conditional_edges("agent", should_continue, {
|
|
1343
|
+
"tools": "tools",
|
|
1344
|
+
"end": END,
|
|
1345
|
+
})
|
|
1346
|
+
g.add_edge("tools", "agent") # tool result → back to agent
|
|
1347
|
+
|
|
1348
|
+
app = g.compile()
|
|
1349
|
+
|
|
1350
|
+
result = app.invoke({
|
|
1351
|
+
"messages": [HumanMessage(content="Call the user-data API endpoint")],
|
|
1352
|
+
"error_count": 0,
|
|
1353
|
+
"max_retries": 2,
|
|
1354
|
+
})
|
|
1355
|
+
print(result["messages"][-1].content)
|
|
1356
|
+
</code></pre>
|
|
1357
|
+
</details>
|