AH.Xưởng
☀ Chính ngọ
Kỷ Nguyên Nguyên BảnPhần 3/6

Ai nhồi chữ, ai vẽ đường

Anh Hoang26 tháng 9, 202632 phút đọc10 ảnh
Trong phần này · 8 mục
  1. Harness là gì?
  2. I. LLM-as-router và tập hành động đóng
  3. II. Từ router tối cao xuống worker node
  4. III. Đồ thị hữu hạn: sự ràng buộc của topology
  5. IV. State, reducer và hồi kết của cuộn tape vô danh
  6. V. Runtime giữ thời gian, thất bại và bằng chứng
  7. VI. Tấm gương từ lõi mô hình
  8. Cánh cửa xuống tầng đáy
Hand-drawn sketch of a graph: a model box with reasoning coiled inside it, a shortcut arrow from the model to the world's door crossed out, the only path to the door running through a highlighted human-review node and a padlock, and an audit strip running along the bottom

Lịch sử ứng dụng LLM trong vài năm qua có thể gói lại trong hai động từ: nhồi chữ và vẽ đường.

Giai đoạn đầu, gần như toàn bộ công sức dồn vào việc nhồi chữ cho đúng. Kỹ sư gọt prompt, chèn ví dụ, quy định định dạng; pipeline RAG hì hục truy xuất tài liệu rồi nhét vào context. Bài 01 đã đi hết chặng này: từ prompt engineering, Chain-of-Thought đến RAG, câu hỏi cốt lõi vẫn chỉ là cần bày những token nào trước mặt mô hình để lần dự đoán kế tiếp hữu ích hơn. LLM khi ấy là một cỗ máy đọc hiểu thụ động: nhận một context dọn sẵn, nhả ra một câu trả lời, rồi dừng.

Cuối năm 2022, ReAct kéo LLM ra khỏi vùng an toàn ấy. Đầu ra không còn chỉ là câu trả lời cuối cùng. Mô hình được sinh Action, nhận lại Observation, rồi dựa trên cuộn lịch sử vừa nối dài để chọn bước tiếp theo. Lần đầu tiên, cỗ máy không chỉ đọc những gì người khác nhồi cho nó; nó bắt đầu cầm bút vẽ đường cho chính tiến trình đang chạy.

Nhưng như Bài 02 đã mổ xẻ, cây bút ấy vẫn là văn xuôi. Một action phải được viết thành chuỗi, cắt ra bằng regex, đối chiếu với tên công cụ rồi mới được thực thi. Chỉ cần một dấu hai chấm lệch nhịp, một tham số sai kiểu, hay một câu xin lỗi lịch sự chen ngang là giao thức đứt. Tập công cụ có thể hữu hạn, nhưng cánh cửa giữa mô hình và runtime lại mở ra toàn bộ không gian chuỗi ký tự.

Ngày 13/6/2023, OpenAI ra mắt function calling.1 Lập trình viên khai báo hàm và nhận lại tên hàm cùng tham số trong một khối JSON, thay vì dò tìm chữ Action: giữa một đoạn văn. Lần đầu tiên, lời đề xuất gọi công cụ có một kênh truyền riêng.

Kênh riêng làm output có cấu trúc đáng tin hơn, nhưng chưa bảo đảm mọi lần sinh đều khớp schema. Phải sang năm 2024, hai mảnh ghép quyết định mới xuất hiện. Về phía kiến trúc, LangGraph (tháng 1/2024) đưa chu trình, state và checkpoint thành công dân hạng nhất của agent runtime.2 Về phía mô hình, Structured Outputs (tháng 8/2024) với cờ strict: true siết việc tuân thủ schema xuống tận tầng giải mã.3

Từ đó, ba thứ lần lượt dọn nhà. Luồng điều khiển rời cuộn hội thoại để thành mã nguồn. State rời chuỗi văn bản để thành dữ liệu định kiểu. Lời gọi công cụ rời văn xuôi để đi qua một kênh riêng.

Timeline từ 2020 đến 2025 chia hai giai đoạn nhồi chữ và vẽ đường, đánh dấu few-shot và RAG, ReAct, function calling, LangGraph, Structured Outputs, Workflow/Agent; bên dưới ba hàng cho thấy luồng điều khiển, state và lời gọi công cụ chuyển từ một dải văn xuôi sang một khối có ô
Hình 1. Ba cuộc dọn nhà: luồng điều khiển, state và lời gọi công cụ lần lượt rời văn xuôi để vào cấu trúc do code nắm.

Đây mới là cú bẻ lái quan trọng nhất. Mô hình vẫn giữ vai trò suy luận; hệ thống cũng không lùi về những pipeline if-else cứng nhắc. Thứ thay đổi là chỗ đặt quyền quyết định. Mô hình vẫn được đề xuất bước kế tiếp, nhưng mất hẳn cái quyền mặc nhiên biến mọi chuỗi nó vừa sinh ra thành hành động thật.

Và cú pháp đúng không bảo chứng cho logic đúng. Một khối JSON không thiếu dấu ngoặc nào nhưng gọi delete_database trong khi nghiệp vụ cần query_database không làm hệ thống thông minh hơn. Nó chỉ giúp sai lầm lọt qua parser trơn tru hơn.

Vì vậy phải dựng một ranh giới mới, và ranh giới ấy phải nằm trong mã nguồn:

Mô hình đề xuất. Runtime phân quyền. Công cụ thực thi. State ghi nhận. Verifier kiểm duyệt. Audit trail lưu bằng chứng.

Câu này đã khép lại Bài 02. Bài này đi tiếp từ đó, và gọi tên lớp mã nguồn giữ ranh giới ấy: harness. Harness engineering không tước năng lực suy luận của mô hình. Nó chỉ không để mô hình vừa chạy trên bản đồ, vừa tự vẽ thêm những con đường không ai kiểm soát.

Harness là gì?

Function calling mở ra một kênh có cấu trúc để mô hình đề xuất gọi công cụ. Nhưng một kênh truyền thông điệp chưa phải hệ thống điều khiển. Ai quyết định mô hình được nhìn thấy gì? Ai đóng khung những hành động nó được đề xuất? Ai kiểm tra đề xuất trước khi kích hoạt? Ai giữ tiến trình sống qua một cú crash, và ai lưu bằng chứng khi sự cố xảy ra?

Lớp mã nguồn trả lời những câu hỏi ấy mang một cái tên rất mộc của ngành phần mềm: harness.

Ngoài đời, harness là bộ đai an toàn của người leo núi. Nó không tạo ra lực kéo. Nó chỉ truyền lực theo hướng đã định, và giữ mạng người leo khi trượt chân. Trong phần mềm, ý niệm này đã có từ lâu dưới tên test harness: đoạn code chuẩn bị input, gọi module cần test, thu output, đo thời gian, cô lập môi trường và phân xử đạt hay trượt.

Trong kiến trúc agent, harness không chỉ bọc một lần gọi API. Nó ôm trọn vòng đời của tiến trình mà LLM tham gia. Ranh giới định nghĩa có thể xê dịch giữa các framework, nhưng một harness thực thụ phải gánh ít nhất năm trách nhiệm:

  • Quản trị context: quyết định phần nào của state được render vào ngữ cảnh ở mỗi lượt gọi.
  • Đóng khung không gian hành động: khai báo tập công cụ hoặc các nhánh mà mô hình được phép đề xuất.
  • Kiểm duyệt: xác thực đề xuất trước khi cho nó chạm vào công cụ thật.
  • Điều phối runtime: nắm luồng điều khiển, state, timeout, retry, checkpoint và các điểm dừng.
  • Khả năng kiểm toán: ghi đủ bằng chứng để quan sát, điều tra và phục hồi.
Sơ đồ harness là một khung nét đứt bao quanh vòng lặp: θ đề xuất, verifier kiểm duyệt, runtime phân quyền, tool thực thi, state ghi nhận, render dựng context rồi quay về θ; đề xuất bị từ chối rẽ sang một nhánh Error; bên dưới là dải audit trail nhận dấu vết từ các khâu
Hình 2. Mô hình là một bánh răng trong vòng lặp do harness giữ. Năm trách nhiệm (nhãn màu cam) nằm ở các khâu khác nhau, và mọi khâu đều để lại dấu trên audit trail.

Mô hình cung cấp sức mạnh suy luận. Harness quyết định sức mạnh ấy được truyền vào đâu, bị chặn ở đâu, và để lại dấu vết gì. LLM, suy cho cùng, chỉ là một bánh răng trong harness, không bao giờ là toàn bộ agent.

Harness cũng không phải tên một thư viện. LangGraph, LlamaIndex Workflows hay Temporal chỉ là nguyên liệu. Một vòng while tự viết, nếu có tập công cụ đóng, schema khắt khe, giới hạn số bước, timeout rõ ràng và cơ chế phân quyền, vẫn là một harness tốt. Ngược lại, import một framework đồ sộ không tự động mang lại những tính chất ấy. Điều định hình kiến trúc không nằm ở logo của thư viện, mà ở những ranh giới nào thật sự được code cưỡng chế.

Năm trách nhiệm trên cuối cùng quy về một câu hỏi: sau khi mô hình nhả chữ, điều gì thật sự được phép xảy ra? Sáu mục dưới đây trả lời câu hỏi ấy từ trong ra ngoài: tập lựa chọn (I), vai trò của mô hình (II), đường đi (III), bộ nhớ (IV), thời gian và bằng chứng (V), rồi một tấm gương từ chính lõi mô hình (VI).

I. LLM-as-router và tập hành động đóng

Router là thành phần đứng ở ngã rẽ và chọn nhánh kế tiếp. Dùng LLM cho việc này là một ý tưởng hay. Một email khiếu nại lộn xộn có thể cần chuyển sang kế toán, pháp chế hay hỗ trợ kỹ thuật; mô hình ngôn ngữ nắm ý định tốt hơn hàng chục khối if-else dò từ khóa.

Sự ngây thơ của agent đời đầu không nằm ở việc giao quyền chọn cho mô hình. Nó nằm ở không gian mà mô hình được chọn trong đó.

Về mặt hình thức, một router ánh xạ đầu vào vào một tập nhánh hữu hạn:

r:X→N.r: X \to N.

Tập NN phải được khai báo trước và đóng kín. ReAct nguyên bản, trên giấy, cũng có tập hành động hữu hạn: với HotpotQA chỉ gồm search, lookup và finish.4 Nhưng ở khâu thực thi, mô hình đề xuất hành động bằng văn xuôi. Tập công cụ trên giấy là ba phần tử, còn không gian chuỗi mà mô hình bơi trong đó là toàn bộ Σ∗\Sigma^*.

Ba công cụ, nhưng vô số chuỗi. Runtime vì thế phải gánh hai bài toán: chuỗi vừa sinh có chứa lệnh hay không, và nếu có thì lệnh ấy có nằm trong tập cho phép hay không.

Gọi Π\Pi là parser biến output τ\tau thành một hành động, hoặc trả về ⊥\bot khi thất bại. Một lượt định tuyến hỏng rơi vào đúng một trong ba biến cố:

Eε={Π(τ)=⊥}cú pháp hỏng,Eω={Π(τ)≠⊥, Π(τ)∉N}đúng định dạng nhưng ngoài tập,Eη={Π(τ)∈N, Π(τ) sai nghiệp vụ}đúng tập nhưng sai logic.\begin{aligned} E_\varepsilon &= \{\Pi(\tau)=\bot\} &&\text{c\char"FA{} ph\char"E1{}p h\char"1ECF{}ng,}\\ E_\omega &= \{\Pi(\tau)\neq\bot,\ \Pi(\tau)\notin N\} &&\text{\char"111{}\char"FA{}ng \char"111{}\char"1ECB{}nh d\char"1EA1{}ng nh\char"1B0{}ng ngo\char"E0{}i t\char"1EAD{}p,}\\ E_\eta &= \{\Pi(\tau)\in N,\ \Pi(\tau)\ \text{sai nghi\char"1EC7{}p v\char"1EE5{}}\} &&\text{\char"111{}\char"FA{}ng t\char"1EAD{}p nh\char"1B0{}ng sai logic.} \end{aligned}

Ba biến cố này rời nhau, không chồng lên nhau, nên xác suất của chúng cộng được:

P(hỏng)=ε+ω+η.P(\text{h\char"1ECF{}ng}) = \varepsilon + \omega + \eta.

Tổng này chỉ đo riêng quyết định định tuyến. Lỗi phân quyền, lỗi mạng hay crash hệ thống thuộc các tầng khác, và không được giấu vào phương trình này.

Function calling kéo ε\varepsilon xuống mạnh. Structured Outputs đi thêm một bước quyết định: nếu nhánh kế tiếp được khai báo bằng một enum, constrained decoding can thiệp ngay lúc lấy mẫu, loại mọi token không dẫn tới một giá trị hợp lệ. Production không còn phải cầu cho mô hình viết đúng tên hàm; tập lựa chọn đã bị khóa từ lúc sampling. Với decoder bảo chứng, ε≈0\varepsilon \approx 0 và ω≈0\omega \approx 0.

Chữ "xấp xỉ" ở đây không phải khiêm tốn cho có. Ngay trong bài giới thiệu Structured Outputs, OpenAI vẫn nêu hai lối thoát: mô hình có thể trả về một lời từ chối (refusal) thay vì object theo schema, và output có thể bị cắt ngang nếu chạm giới hạn token.3 Harness vẫn phải có nhánh cho hai trường hợp ấy. Chỉ là giờ đây chúng là những trường hợp có tên, không phải một đoạn văn xuôi bất định.

Tuy vậy, η\eta vẫn còn nguyên. Schema có thể ép trường next chỉ nhận review hoặc search, nhưng schema không biết email này thực sự cần review hay search. Một lời gọi hàm đúng kiểu vẫn có thể chứa một câu SQL sai, một số tiền sai, một mã khách hàng sai.

Một đám mây Σ* chứa mọi chuỗi mô hình có thể viết đi qua parser Π và rẽ ba hướng: ⊥ là lỗi cú pháp ε, delete_all là lỗi ngoài tập ω, và tập N gồm search, review, reply trong đó chỉ một lựa chọn đúng nghiệp vụ; bên dưới hai thanh xếp chồng cho thấy với văn xuôi và regex có đủ ε, ω, η, còn với typed channel và enum thì ε và ω gần như biến mất nhưng η vẫn còn nguyên
Hình 3. Ba biến cố rời nhau của một lượt định tuyến hỏng. Kênh có kiểu và enum bóp ε và ω về gần 0; η không đổi. Độ dài các thanh chỉ để minh họa.

Vì vậy harness mang hai nhiệm vụ tách biệt. Thứ nhất, biến mọi output ngoài tập thành một mã lỗi hệ thống, không bao giờ để nó trôi thành một hành động ngầm. Thứ hai, giữ quyền kiểm duyệt một action hợp lệ về cấu trúc trước khi cho nó chạm vào công cụ thật.

Mô hình vẫn được quyền chọn. Thứ bị tước đi không phải năng lực điều hướng, mà là quyền tự bịa ra một lựa chọn có khả năng thực thi.

II. Từ router tối cao xuống worker node

Đóng tập lựa chọn mới chỉ khóa được danh sách đường đi. Câu hỏi gắt hơn vẫn còn: ai giữ chìa khóa biến một đề xuất thành một tác động thật?

Trong một harness có kỷ luật, LLM bị giáng đúng một bậc: từ người cầm lái tiến trình thành một worker node nằm bên trong tiến trình. Nó nhận một phần state, xử lý, rồi nhả ra một đề xuất có cấu trúc. Đề xuất ấy không tự thực thi. Runtime phán xét có nhận nó không, node nào được đánh thức tiếp theo, và hệ thống dừng ở đâu.

Gọi ss là state toàn cục. Tại mỗi node vv:

  • πv(s)\pi_v(s) là phép chiếu, chỉ lọc ra những trường mà node vv được phép đọc;
  • renderv\mathrm{render}_v dựng context từ phần dữ liệu đã lọc;
  • parsev\mathrm{parse}_v ép output thành một object có cấu trúc.

Một lượt gọi mô hình khi ấy viết được thành:

τv∼Pθ(⋅∣renderv(πv(s))),yv=parsev(τv).\tau_v \sim P_\theta\big(\cdot \mid \mathrm{render}_v(\pi_v(s))\big), \qquad y_v = \mathrm{parse}_v(\tau_v).

Công thức này cũng khép lại một món nợ từ Bài 01. Chain-of-Thought, như đã phân tích ở đó, không phải state mà là context: mô hình viết thêm vào điều kiện của chính nó. Trong harness, chuỗi suy luận ấy được phép dài bao nhiêu cũng được, miễn là nó ở lại bên trong τv\tau_v, trong lòng node. Thứ duy nhất bước ra khỏi node là yvy_v, một object có kiểu. Suy luận là việc riêng của node; tập cạnh của đồ thị thì không phải chỗ để nó viết lên.

State s là một bảng các trường user_query, documents, draft, approval, retries, errors trong đó chỉ hai trường đầu được tô; phép chiếu π_v đưa chúng vào node v gồm render, θ và parse; một đường suy luận nét đứt cuộn bên trong node; y_v đi ra gặp runtime có ổ khóa, runtime chấp nhận thì mới ghi xuống tập cạnh E, còn đường từ node thẳng tới tập cạnh bị gạch
Hình 4. Worker node: đọc qua phép chiếu, suy nghĩ bao lâu tùy ý bên trong, nhưng chỉ nhả ra y_v. Chỉ runtime mới được đi tiếp trên tập cạnh.

Ở tầng ứng dụng, LLM được đối xử như một hàm không giữ trạng thái giữa các lượt: dữ liệu vào, một phân phối output ra. Nhà cung cấp API có thể tối ưu bằng KV cache hay thread, nhưng state mà hệ thống cần để phục hồi và kiểm toán không được bám vào bộ nhớ ngầm ấy. Nó phải nằm trong ss, được lưu xuống database, dưới quyền của runtime.

Trong bài Building effective agents (tháng 12/2024), Anthropic gọi tên một ranh giới rất gọn.5 Workflow là hệ thống trong đó LLM và công cụ được điều phối qua những đường code định sẵn. Agent là hệ thống trong đó LLM tự điều khiển tiến trình và cách dùng công cụ của mình.

Đó không phải vạch kẻ đen trắng, mà là hai đầu của một dải phổ kiểm soát. Vặn núm về phía workflow, ta có mạng các node nhỏ, rẽ nhánh bằng logic tĩnh, dễ test nhưng kém linh hoạt. Vặn về phía agent, ta có một đồ thị hình sao: LLM ở lõi, công cụ là vệ tinh. Nhưng ngay ở điểm linh hoạt nhất, mô hình vẫn không lướt tự do trên Σ∗\Sigma^*. Nó có thể chọn công cụ nhiều lần, nhưng tập công cụ do code cấp phép. Một dispatcher kiểm tra tên hàm, schema, quyền hạn và số bước tối đa trước khi cho thực thi.

Một thanh trượt từ workflow đến agent với núm vặn ở giữa; bên trái là chuỗi node nhỏ nối bằng mũi tên cố định, vài node chứa θ; bên phải θ ở tâm một vòng dispatcher nét đứt, năm công cụ vệ tinh nằm ngoài vòng, mỗi lối ra có ổ khóa, và một đề xuất tới send_email không có trong danh sách bị gạch ngay tại vòng
Hình 5. Dải phổ kiểm soát. Ở cả hai đầu, quyền thực thi cuối cùng thuộc về code: đường bê tông ở bên trái, vòng dispatcher ở bên phải.

Khác biệt lịch sử vì thế không nằm ở chỗ agent cũ dùng tập mở còn agent mới dùng tập đóng. ReAct năm 2022 cũng có danh sách action hữu hạn. Sự tiến hóa nằm ở cơ chế cưỡng chế. Đời đầu mô tả action bằng prompt, rồi trông vào một cái kéo parser cắt đúng chỗ. Harness trưởng thành nhốt action vào một kênh có kiểu, và bắt runtime đóng dấu trước khi cho gọi hàm.

Độ tự chủ của AI là một núm vặn. Nhưng đặt núm ở đâu thì quyền thực thi cuối cùng vẫn phải thuộc về mã nguồn.

III. Đồ thị hữu hạn: sự ràng buộc của topology

Vòng while vô định của các hệ thống cũ có thể thay bằng một đồ thị trạng thái. Nhưng gọi mọi đồ thị như vậy là DAG (đồ thị có hướng không chu trình) sẽ vô tình xóa đi đặc điểm quan trọng nhất của agent.

Agent sinh ra là để lặp: thử một bước, quan sát kết quả, sửa kế hoạch, rồi thử lại. Bài giới thiệu LangGraph nói thẳng mục tiêu là giúp tạo ra đồ thị có chu trình, thứ agent runtime thường cần.2 Một pipeline thẳng không vòng lặp chỉ là trường hợp đặc biệt.

Harness không cấm chu trình. Harness chỉ buộc chu trình phải có tên, có trạng thái, và có giới hạn.

Một topology cơ bản gồm ba loại thành phần:

  • Node: đơn vị tính toán, có thể là gọi LLM, gọi API hay chạy một đoạn Python.
  • Edge: tuyến đường được phép đi từ node này sang node khác.
  • Conditional edge: tuyến rẽ nhánh do code quyết định, dựa trên state hoặc trên một đề xuất đã qua kiểm duyệt của LLM.

Tại node vtv_t, mô hình có thể đề xuất đích đến yty_t. Runtime áp một luật chuyển nghiêm ngặt:

vt+1={yt,nếu yt∈succ(vt),Error(vt,yt),nếu yt∉succ(vt).v_{t+1} = \begin{cases} y_t, & \text{n\char"1EBF{}u } y_t \in \mathrm{succ}(v_t),\\[2pt] \mathrm{Error}(v_t, y_t), & \text{n\char"1EBF{}u } y_t \notin \mathrm{succ}(v_t). \end{cases}

Error\mathrm{Error} ở đây không phải một câu mắng mô hình bằng tiếng Anh rồi nhét lại vào context, như vòng xin lỗi ở Bài 02. Nó là một nhánh thật do lập trình viên định nghĩa: ghi log sự cố, kích hoạt fallback, hoặc hủy tiến trình.

Nếu chỉ nhìn luồng điều khiển, hệ thống trông như một máy trạng thái hữu hạn (FSM). Nhưng dữ liệu trong state có thể phình ra không giới hạn, như một danh sách tài liệu hay một chuỗi JSON, nên tên gọi chính xác hơn là máy trạng thái hữu hạn mở rộng (EFSM): luồng điều khiển hữu hạn, dữ liệu mở rộng, và các cạnh được canh bằng điều kiện trên dữ liệu.

Chu trình không phá vỡ tính hữu hạn của tập điều khiển; nó chỉ cho một lần chạy đi qua cùng một node nhiều lần. Agent loop vì thế không còn là một cuộc hội thoại tự nối dài chính nó. Nó thành một quỹ đạo quan sát được: đang ở node nào, đã lặp bao nhiêu lần, state biến đổi ra sao, điều kiện nào cho phép quay lại.

Để chặn vòng lặp vô tận, runtime đặt giới hạn số lần chuyển trạng thái; trong LangGraph là recursion_limit. Nhưng giới hạn số bước không phải giới hạn thời gian. Một node vẫn có thể treo cả tiến trình khi mạng nghẽn. Muốn harness giữ được tiến trình cho trọn, nó cần thêm timeout theo đồng hồ thật, cơ chế hủy, và giới hạn tài nguyên cho từng node.

Quyền lực của dominator

Lợi ích lớn nhất của topology là chuyển các tính chất an toàn từ lời dặn dò sang cấu trúc. Giả sử node Drafting_Email chỉ có hai cạnh ra:

Drafting_Email -> Human_Review
Drafting_Email -> Search_Docs

Dù LLM trả về một khối JSON hoàn hảo đề xuất Send_Email, runtime sẽ ném đi ngay, vì cạnh đó không có trong succ(Drafting_Email)\mathrm{succ}(\texttt{Drafting\_Email}). Không cần tốn token dặn mô hình "đừng gửi khi chưa duyệt". Cấu trúc đồ thị tự khiến con đường chưa qua duyệt không tồn tại.

Nói bằng ngôn ngữ lý thuyết đồ thị: Human_Review dominates Send_Email. Mọi đường đi từ điểm khởi đầu tới lúc gửi email đều phải xuyên qua điểm duyệt của con người.6

Nhưng cần nói cho đủ: dominator chỉ bảo đảm hệ thống đi qua Human_Review, không bảo đảm con người đã đồng ý. Muốn có điều thứ hai, cạnh Human_Review -> Send_Email phải là một conditional edge canh bằng approval_status == "approved", và trường ấy chỉ được ghi bởi đúng node duyệt, một luật mà mục IV sẽ giao cho reducer. Topology lo đường đi; điều kiện trên cạnh lo nội dung; quyền ghi lo ai được đổi nội dung ấy.

Đồ thị gồm Classify, Drafting_Email, Search_Docs, Human_Review và Send_Email; Drafting_Email và Search_Docs nối qua lại thành chu trình; Human_Review được tô nổi và có cạnh rejected quay về Drafting_Email; chỉ cạnh từ Human_Review tới Send_Email được canh bằng approval == approved; một đề xuất nét đứt từ Drafting_Email thẳng tới Send_Email bị gạch, kèm ghi chú JSON next Send_Email không thuộc succ nên thành Error; góc dưới có bộ đếm bước 7 trên 25
Hình 6. Human_Review dominates Send_Email. Đề xuất vượt rào, dù là JSON hoàn hảo, không có cạnh để đi; cạnh hợp lệ duy nhất còn được canh bằng điều kiện trên state.

Khi quy tắc an toàn chỉ nằm trong prompt, ta đang tung đồng xu xem mô hình có nghe lời không. Khi nó nằm trong topology, ta có thể kiểm chứng nó trên đồ thị trước cả khi hệ thống chạy.

Dĩ nhiên, chứng minh ấy chỉ đúng ở tầng ứng dụng. Nếu code bên trong một node tự mở socket và gửi email, topology vô dụng. Đó là việc của sandbox, và là chuyện của Bài 04.

IV. State, reducer và hồi kết của cuộn tape vô danh

Sự hỗn loạn của agent đời đầu không chỉ ở chỗ nó đi đâu, mà còn ở chỗ nó "nhớ" bằng cách nào.

Yêu cầu người dùng, luồng suy nghĩ, kết quả công cụ, lỗi runtime, cả những lời xin lỗi, tất cả bị nhét chung vào một mảng messages. Đó chính là cuộn unsigned tape ở Bài 02: dài ra sau mỗi vòng lặp, mọi loại dữ liệu chung một kênh, mọi tác giả viết lên cùng một bề mặt, và luật cập nhật (ai được sửa gì) chủ yếu dựa vào thỏa thuận ngầm.

Nói ReAct "không có state" là oan cho nó. Nó có state, nhưng là state bị làm phẳng thành một dòng lịch sử hội thoại. Nguồn gốc của từng mẩu dữ liệu và quyền ghi đè không được runtime bảo vệ.

Harness khép lại cuộn tape ấy. State trở thành một object có kiểu:

class AgentState(TypedDict):
    user_query: str
    documents: Annotated[list[DocumentRef], merge_dedup]
    draft: str
    approval_status: Literal["pending", "approved", "rejected"]

Chạy xong, node không trả về toàn bộ state mà chỉ nhả ra một bản cập nhật Δt\Delta_t. Với mỗi trường kk, một reducer RkR_k, thuần code, quyết định cách gộp giá trị mới:

st+1[k]=Rk(st[k], Δt[k]).s_{t+1}[k] = R_k\big(s_t[k],\ \Delta_t[k]\big).

Reducer có thể ghi đè, nối danh sách, lọc trùng, cộng bộ đếm, hoặc thẳng tay ném lỗi nếu bản cập nhật vi phạm một bất biến, chẳng hạn một node không phải Human_Review định ghi vào approval_status.

Nhờ đó, khi gọi mô hình ở node Writer, phép chiếu πWriter(s)\pi_{\text{Writer}}(s) chỉ lấy user_query và documents. Ba lần timeout ở các node trước không hề xuất hiện trong context của nó.

Bên trái là mảng messages gồm các ô user, thought, action, observation, error timeout, xin lỗi, lặp lại lộn xộn, chú thích một kênh không chữ ký; mũi tên sang bên phải là AgentState với các trường user_query, documents, draft, approval_status, retries, mỗi trường có reducer riêng như ghi đè, nối và lọc trùng, chỉ node duyệt ghi, cộng dồn; một bản cập nhật Δ từ node đi vào qua reducer; phép chiếu π_Writer lấy hai trường đầu dựng thành context, ghi chú một view không phải kho và không có ba lần timeout
Hình 7. Từ cuộn tape vô danh sang state có kiểu: mỗi trường có tên, có luật gộp, có người được phép ghi. Context chỉ còn là một view được dựng cho đúng node.

Đây là chỗ context engineering đạt tới hình dạng trưởng thành: từ nhồi chữ bằng tay sang nhồi chữ bằng kiến trúc.

hv=renderv(πv(s)).h_v = \mathrm{render}_v\big(\pi_v(s)\big).

Hãy đặt công thức này cạnh Pθ(xt+1∣ht)P_\theta(x_{t+1}\mid h_t) ở Bài 01. Mô hình vẫn chỉ nhìn thấy hh; không có gì thay đổi ở phía nó. Cái thay đổi là hh không còn do một người gõ prompt hay một cuộn hội thoại tự sinh ra, mà do một hàm có kiểu dựng lên từ state. Context không còn là bãi chứa toàn bộ ký ức của hệ thống. Nó chỉ là một view tạm thời, dựng đúng cho một node, đúng một thời điểm.

State có kiểu cũng không phải viên đạn bạc. Đừng coi TypeError là tường lửa. Một URL độc hại vẫn là một str hợp lệ, và qua được mọi validator kiểm kiểu. Reducer quản cách state thay đổi; validator quản hình dạng dữ liệu; policy quản quyền; sandbox quản giới hạn thực thi. Bốn lớp đi cùng nhau, không lớp nào thay được lớp nào.

Nhưng ít nhất, cuộn băng đã có chữ ký: trường có tên, cập nhật có nguồn gốc, phép gộp có luật, và context chỉ còn là một view phục vụ suy luận.

V. Runtime giữ thời gian, thất bại và bằng chứng

Khi luồng điều khiển đã nằm trong đồ thị và dữ liệu bền đã nằm trong state, runtime gánh phần khó còn lại: thời gian, sự cố, sự can thiệp của con người, và khả năng phục hồi.

Interrupt và checkpoint

Human-in-the-loop không thể chỉ là một lệnh input() treo màn hình console. Server sập là tiến trình mất theo. Trong harness, một điểm chờ cần hai cơ chế: interrupt để dừng ở một ranh giới đã định, và checkpoint để lưu state cùng vị trí trong đồ thị xuống database. Người duyệt bấm nút sau ba ngày, hệ thống chạy tiếp từ checkpoint. LangGraph, chẳng hạn, lưu checkpoint ở mỗi super-step, tức sau mỗi lượt các node chạy xong và trao quyền cho lượt kế.7

Nhưng chạy tiếp lại mở ra một bài toán quen thuộc của hệ phân tán: node vừa sập đã kịp gây ra tác động bên ngoài hay chưa? Nếu node trừ tiền thẻ vừa chạy xong nhưng crash trước checkpoint kế tiếp, lúc chạy lại nó có trừ tiền lần nữa không? Vì vậy các API gây tác động phải có tính lũy đẳng (idempotency): gọi lại nhiều lần với cùng một khóa chỉ có tác động như gọi một lần. Stripe làm điều này bằng idempotency key gắn vào mỗi request.8 Harness không tự giải được idempotency; nó chỉ có thể chuyền khóa xuống, còn bảo đảm phải được thiết kế ở tầng dịch vụ.

Tạm biệt prompt retry

Nếu gọi công cụ thất bại vì mạng nghẽn, dán stack trace vào prompt rồi bảo LLM "hãy thử lại" là hạ sách. Nó biến một lỗi hạ tầng thành bài kiểm tra đọc hiểu cho mô hình, đúng kiểu vòng xin lỗi của Bài 02. Harness tử tế phải có retry budget, exponential backoff, phân loại lỗi thử lại được và không thử lại được, và circuit breaker. Quyết định có thử lại hay không là một nhánh của runtime, không phải của mô hình.

Checkpoint ≠ audit trail

Hai thứ này hay bị gộp làm một, nhưng chúng trả lời hai câu hỏi khác nhau. Checkpoint trả lời "hệ thống chạy tiếp từ đâu?": nó chỉ giữ state ở các ranh giới super-step. Audit trail trả lời "ai đã đề xuất gì, runtime quyết định ra sao, công cụ thực sự đã làm gì?": nó giữ từng sự kiện, kể cả những sự kiện nằm giữa hai checkpoint, như lần gọi tiền bị crash ở trên.

Phía trên là trục thời gian với ba checkpoint s1, s2, s3 ở các ranh giới super-step, sau đó node charge_card chạy rồi crash trước khi có s4; một mũi tên resume từ s3 chạy lại charge_card kèm câu hỏi trừ tiền lần hai và ghi chú cần idempotency key; phía dưới là một bảng audit trail với dấu thời gian, tác nhân θ@Writer, runtime, tool và các dòng đề xuất charge_card, policy cho phép, gọi lần 1 với key, timeout 5,28 giây rồi retry 2 trên 3; góc phải có một chiếc đồng hồ
Hình 8. Checkpoint cho biết chạy tiếp từ đâu; audit trail cho biết chuyện gì đã xảy ra, kể cả giữa hai checkpoint. Đồng hồ nằm ở runtime.

Chiếc đồng hồ phải nằm ở runtime. Mô hình không tự biết một công cụ đã chạy mất năm giây, hay lệnh gọi API đang ở lần thử thứ ba. Những dữ kiện ấy không được để mô hình đoán từ văn xuôi; runtime phải ghi chúng vào audit log.

VI. Tấm gương từ lõi mô hình

Đến đây sẽ có người phản biện: harness chỉ là giàn giáo cho những mô hình còn non. Khi mô hình đủ mạnh, nó sẽ tự biết đường, không cần đồ thị hay luật lệ bên ngoài.

Để trả lời, hãy nhìn vào bên trong chính những mô hình mạnh nhất hiện nay, chẳng hạn kiến trúc Mixture of Experts (MoE) của DeepSeek-V3. Cần nói trước: harness không sinh ra từ MoE, và gating network không phải agent. Đây là một sự tương đồng về cách thiết kế, không phải bằng chứng nhân quả.

Mỗi lớp MoE của DeepSeek-V3 có một expert dùng chung và 256 expert được định tuyến.9 Khi một token đi vào, nó không chạy qua cả 256 expert. Gating network tính điểm tương hợp giữa token và từng expert:

si,t=σ(ut⊤ei),s_{i,t} = \sigma\big(\mathbf{u}_t^{\top}\mathbf{e}_i\big),

rồi áp luật TopK để chỉ gửi token tới 8 expert, và mỗi token đi tới tối đa 4 node máy. Đáng chú ý là cách DeepSeek-V3 cân bằng tải: một độ lệch bib_i được cộng vào điểm, nhưng chỉ dùng để chọn expert nào được gọi; trọng số nhân vào output của expert vẫn lấy từ điểm gốc si,ts_{i,t}.9 Điểm số là khuynh hướng của mô hình; quyền quyết định ai được gọi thuộc về một luật chọn nằm đè lên điểm số.

Hai cột cạnh nhau. Bên trái MoE gating: công thức s_i bằng sigma của u chuyển vị e_i, một dãy cột điểm cho các expert với tám cột cao nhất được tô, đi qua hộp luật chọn gồm cộng b_i, TopK bằng 8, tối đa 4 node, ra một dãy chỉ số trong tập 1 đến 256 với tám ô được tô. Bên phải harness: θ đề xuất, đi qua hộp luật chọn gồm succ(v), policy, max steps, ra một chỉ số trong N gồm draft, search, review với review được chọn. Dưới cùng ghi không bên nào in tên đích ra thành chữ rồi chờ regex cắt
Hình 9. Cùng một khuôn: điểm số, rồi luật chọn, rồi một chỉ số trong tập đóng. Ở tầng tensor, không ai để mô hình viết tên expert ra thành chữ.

Đặt gating network cạnh harness, có ba nét tương đồng:

  • Tập đích đóng. Router của MoE chọn chỉ số trong {1,…,256}\{1,\dots,256\}. Nó có thể định tuyến kém, nhưng không thể "ảo giác" ra expert thứ 257.
  • Luật đè lên điểm số. Điểm tương hợp thể hiện khuynh hướng, còn TopK, giới hạn số node và độ lệch bib_i tạo thành một luật chọn đứng trên điểm số thuần. Trong harness cũng vậy: LLM chấm điểm hoặc đề xuất, còn code của đồ thị giữ quyền áp policy để chọn đường.
  • Không đi qua văn xuôi. Định tuyến diễn ra trên tensor và trả về chỉ số. Không ai in tên expert ra màn hình rồi chờ regex cắt lấy. Biến cố ε\varepsilon đơn giản không tồn tại ở giao diện này.

Điều đáng học ở đây là một khuôn mẫu chung: thành phần xác suất có thể chấm điểm và đề xuất trong một không gian lựa chọn, nhưng một giao diện hữu hạn và một bộ luật xác định phải giữ quyền biến điểm số thành hành động.

Ở tầng trọng số, các nhà nghiên cứu không bao giờ để mô hình tự viết tên expert rồi hy vọng một parser đọc hiểu. Không có lý do gì ở tầng ứng dụng, nơi hành động có thể gửi email, sửa database hay chạy mã, chúng ta lại chấp nhận một tiêu chuẩn thấp hơn thế.

Cánh cửa xuống tầng đáy

Con đường của kiến trúc AI không phải là chuyện prompt, agent và workflow thay thế nhau. Qua mỗi chặng, ta lấy bớt một phần trách nhiệm khỏi chuỗi token để giao lại cho sự chặt chẽ của kỹ thuật phần mềm.

Prompt engineering tổ chức đầu vào. ReAct cho mô hình tham gia đề xuất bước đi. Function calling tách lệnh khỏi văn xuôi. State có kiểu, graph runtime và Structured Outputs biến những quy ước lỏng lẻo thành cấu trúc có tính cưỡng chế.

Harness kết tinh quá trình lột xác ấy:

  • Schema đóng khung đề xuất.
  • Dispatcher đóng tập hành động.
  • Đồ thị đóng tập đường đi.
  • Reducer đóng luật cập nhật state.
  • Runtime giữ nhịp thời gian, checkpoint và retry.
  • Audit log giữ bằng chứng.

Harness không làm mô hình bớt thông minh. Nó vạch rõ ranh giới nơi trí thông minh được phép biến thành hành động. Mô hình vẫn có thể là bộ não sáng giá nhất của hệ thống, nhưng không còn là kẻ nắm quyền tối cao.

Với harness, ε\varepsilon và ω\omega bị runtime chặn lại. Nhưng η\eta vẫn còn nguyên.

Một node hợp lệ vẫn có thể chạy một câu SQL xóa nhầm dữ liệu. Một đoạn Python do LLM viết vẫn có thể lặng lẽ mở kết nối mạng, đọc /etc/shadow hay tải mã độc về. Topology có thể tự hào tuyên bố "tôi không có cạnh nào tới Send_Email", nhưng hoàn toàn bất lực nếu code chạy bên trong node tự mở socket và gửi email vượt rào.

Đến đây, kiến trúc không thể chỉ phòng thủ ở tầng ứng dụng. Nó phải xuống tới tầng hệ điều hành.

Bài 04 sẽ đi xuống tầng ấy: sandbox, microVM và nguyên lý zero-trust, nơi đoạn code do AI viết chạy trong một máy ảo riêng như Firecracker, với nhân hệ điều hành riêng, mạng và hệ thống file bị cắt tới mức tối thiểu, để dù có muốn, nó cũng không chạm được vào máy chủ thật.

Mô hình đề xuất đường đi. Harness quyết định đường nào được phép tồn tại. Runtime quyết định bước nào được chạy. Và sandbox quyết định bước ấy được chạm tay tới đâu.

  1. OpenAI, Function calling and other API updates, 13/6/2023. ↩
  2. LangChain, LangGraph, 17/1/2024: LangGraph ra đời để "better enable creation of cyclical graphs, often needed for agent runtimes". ↩
  3. OpenAI, Introducing Structured Outputs in the API, 6/8/2024. Bài viết nêu riêng trường hợp mô hình từ chối (trường refusal) và output bị cắt khi chạm giới hạn token. ↩
  4. Shunyu Yao và cộng sự, ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023. Không gian hành động cho HotpotQA ở mục 3.1. ↩
  5. Anthropic, Building effective agents, 19/12/2024: "Workflows are systems where LLMs and tools are orchestrated through predefined code paths. Agents, on the other hand, are systems where LLMs dynamically direct their own processes and tool usage". ↩
  6. Về khái niệm dominator và cách tính cây dominator, xem Keith D. Cooper, Timothy J. Harvey, Ken Kennedy, A Simple, Fast Dominance Algorithm, Rice University, 2001. ↩
  7. LangChain, LangGraph: Checkpointers. ↩
  8. Stripe, Idempotent requests. ↩
  9. DeepSeek-AI, DeepSeek-V3 Technical Report, 2024, mục 2.1.2: "the bias term is only used for routing. The gating value, which will be multiplied with the FFN output, is still derived from the original affinity score". ↩

— espresso.hoagg

↑ Về đầu trang