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

ReAct, cắt chuỗi và những kỳ vọng quá lớn đối với công nghệ còn quá non trẻ

Anh Hoang26 tháng 9, 202646 phút đọc9 ảnh
Trong phần này · 9 mục
  1. I. Trước ReAct đã có router
  2. II. ReAct: Khi context trở thành một vòng lặp
  3. III. Toolformer và ngã rẽ của tool use
  4. IV. Parser: Mép cắt giữa lời nói và hành động
  5. V. Khi lỗi parser quay lại dưới dạng một câu nói
  6. VI. Phòng ban ảo: Một tầm nhìn đến quá sớm
  7. VII. Unsigned tape: Khi substrate chưa theo kịp tầm nhìn
  8. VIII. Function calling, Structured Outputs và phần còn thiếu
  9. Kết
Hand-drawn sketch of a model writing a long tape of text cells, scissors cutting out one cell that alone reaches the world's door, the tape looping back into the model, a small loop of crossed-out speech bubbles, and three identical reviewers all ticking above one shared blind spot

Bài trước dừng lại ở một giới hạn căn bản: prompt có thể điều hướng câu trả lời, RAG có thể đưa thêm tài liệu vào context, nhưng cả hai chủ yếu vẫn thay đổi điều kiện của phép dự đoán:

Pθ(xt+1∣ht).P_\theta(x_{t+1}\mid h_t).

Người viết prompt sắp chữ bằng tay. Pipeline RAG tìm rồi đưa chữ vào bằng máy. Cách làm khác nhau, nhưng tác động trực tiếp vẫn nằm ở hth_t, tức phần context mà mô hình nhìn thấy trước khi sinh token tiếp theo.

Đến đây, một ý tưởng gần như không thể tránh khỏi xuất hiện: nếu context là thứ quyết định bước tiếp theo, tại sao không để mô hình tự xây context cho chính nó? Hãy cho nó viết kế hoạch, chọn công cụ, đọc kết quả, sửa kế hoạch rồi lặp lại. Context khi ấy không còn là một khối tĩnh do con người chuẩn bị trước. Nó trở thành một cuộn băng được nối dài theo thời gian.

Đó là khoảnh khắc agent bước vào sân khấu.

Tầm nhìn khi ấy không hề nhỏ. Một agent có thể quan sát, lập kế hoạch, gọi công cụ, tự kiểm tra và phối hợp với các agent chuyên môn khác. Nếu thành công, phần mềm sẽ không còn chỉ chờ con người bấm từng nút; nó có thể tổ chức công việc thành một chuỗi hành động có mục tiêu.

Vấn đề không nằm ở tầm nhìn. Vấn đề là tầm nhìn ấy xuất hiện khi LLM còn quá non trẻ, còn tool interface và runtime dành cho agent gần như chưa kịp hình thành. Trong làn sóng 2022 và 2023, phần lớn hệ thống phải ghép tương lai ấy từ những vật liệu rất thô: mô hình sinh văn bản, parser cắt một đoạn văn bản thành lệnh, runtime gọi hàm rồi dán kết quả trở lại context.

ReAct và các multi-agent systems đầu tiên vì thế nên được nhìn như những bản phác thảo kiến trúc đi trước substrate. Chúng nhìn thấy khá đúng tương lai: reasoning phải nối với action; action phải nhận feedback; công việc lớn phải được chia thành vai trò. Nhưng chúng phải thực hiện tầm nhìn đó bằng những model chưa ổn định về format, context còn ngắn, tool calling chưa thành chuẩn và cơ chế phục hồi chủ yếu dựa vào việc yêu cầu model thử nói lại.

Bài viết này mổ xẻ khoảng cách lịch sử ấy: từ một model đề xuất hành động bằng ngôn ngữ đến một hệ thống thực thi hành động có cấu trúc, có quyền hạn, có trạng thái và có bằng chứng.

Ranh giới ấy từng nằm trên một đoạn regex.

I. Trước ReAct đã có router

Tháng 5 năm 2022, Karpas và cộng sự công bố kiến trúc MRKL, viết tắt của Modular Reasoning, Knowledge and Language.1 AI21 Labs đồng thời giới thiệu Jurassic-X, bản triển khai MRKL của chính họ.

Trực giác của MRKL rất rõ: không nên buộc một mô hình ngôn ngữ làm mọi việc. Hệ thống có thể gồm nhiều module chuyên biệt, chẳng hạn máy tính, cơ sở dữ liệu, kho tri thức hoặc bộ suy luận. Một router nhận yêu cầu bằng ngôn ngữ tự nhiên, chọn module thích hợp và chuyển các tham số cần thiết cho module ấy. Nếu không có chuyên gia phù hợp, yêu cầu mới được đưa về mô hình ngôn ngữ tổng quát.

Điểm đáng chú ý nằm ở sự phân công. Máy tính chịu trách nhiệm tính toán. Cơ sở dữ liệu chịu trách nhiệm trả bản ghi. Mô hình ngôn ngữ không cần giả vờ làm một chiếc máy tính kém tin cậy khi hệ thống đã có một chiếc máy tính thật.

Tuy nhiên, MRKL cũng chỉ ra ngay chỗ khó nhất: từ một câu tiếng người, làm sao lấy được đúng tham số mà module chuyên biệt cần dùng? Với câu hỏi số học, hệ thống không chỉ phải chọn calculator; nó còn phải trích đúng phép toán và đúng toán hạng. Với một API, hệ thống phải xác định đúng tên hàm, đúng trường dữ liệu và đúng kiểu của từng giá trị.

Nhóm AI21 không xem đây là việc có thể giải quyết chỉ bằng một lời dặn khéo. Trong thí nghiệm trích xuất toán hạng, họ dùng J1-Large 7B với prompt tuning: giữ nguyên trọng số của mô hình đã tiền huấn luyện và chỉ học thêm mười prompt token. Phụ lục của bài báo còn đặt prompt tuning cạnh few-shot prompting với mười ví dụ, và kết luận rằng hiệu năng của cách làm few-shot "bị giới hạn": khi con số dài hơn những gì có trong ví dụ, few-shot tụt rất mạnh, còn prompt tuning vẫn giữ được độ chính xác cao.

MRKL vì thế chứa một trực giác quan trọng mà làn sóng agent về sau tiếp tục theo đuổi: router phải trở thành một thành phần thật của hệ thống, không chỉ là một giọng văn. Muốn biến câu chữ thành lệnh, ta phải thiết kế và đánh giá giao diện giữa mô hình với module thực thi. Đây chính là phần substrate còn thiếu mà các thử nghiệm đời đầu buộc phải mô phỏng bằng prompt.

Cuối năm 2022, LangChain xuất hiện và nhanh chóng phổ biến cách xây ứng dụng bằng các chuỗi gọi mô hình, công cụ và bộ nhớ. Những agent kiểu MRKL hoặc zero-shot ReAct trong LangChain dùng một giải pháp rất thực dụng: prompt yêu cầu mô hình sinh theo khuôn Thought, Action, Action Input; sau đó một đoạn regex nằm ngay trong mã agent đọc chuỗi ấy để tạo AgentAction. Phải đến giữa tháng 4 năm 2023, phần cắt chuỗi này mới được tách thành một lớp output parser riêng.

Giải pháp này rẻ, linh hoạt và dễ thay đổi. Nó cũng chuyển gánh nặng từ một router được huấn luyện và đánh giá riêng sang một hợp đồng bằng văn xuôi. Kiến trúc vẫn có router và tool, nhưng quyết định định tuyến cùng phần giải thích của mô hình được sinh trên một kênh text, rồi mới được tách ra ở cuối.

Chính lựa chọn ấy đặt nền cho thời kỳ agent đầu tiên: một kiến trúc hướng tới tương lai, được dựng tạm bằng text interface của thời điểm đó.

II. ReAct: Khi context trở thành một vòng lặp

Tháng 10 năm 2022, Yao và cộng sự đưa ReAct, viết tắt của Reasoning and Acting, lên arXiv.2 Thay vì chỉ sinh chuỗi suy luận như Chain-of-Thought, hoặc chỉ phát lệnh như các phương pháp acting-only, ReAct đan xen ba loại bước:

  • Thought: mô hình ghi lại suy luận, kế hoạch hoặc cách điều chỉnh chiến lược.
  • Action: mô hình chọn một hành động hợp lệ trong môi trường.
  • Observation: môi trường trả kết quả để mô hình dùng ở bước kế tiếp.

Thought ở đây không phải thứ gì mới. Nó chính là chuỗi Chain-of-Thought mà bài trước đã mổ xẻ: một scratchpad mô hình tự viết vào context, không phải state của hệ thống. Cái mới của ReAct nằm ở hai nhịp còn lại.

Ta có thể mô tả một vòng lặp ReAct bằng mô hình khái niệm sau. Gọi hth_t là context ở bước tt, τt\tau_t là chuỗi do mô hình sinh, Π\Pi là parser và E\mathcal E là môi trường:

τt∼Pθ(⋅∣ht),\tau_t\sim P_\theta(\cdot\mid h_t),
at=Π(τt),a_t=\Pi(\tau_t),
ot=E(at),o_t=\mathcal E(a_t),
ht+1=ht⊕encode⁡(τt)⊕encode⁡(ot).h_{t+1}=h_t\oplus \operatorname{encode}(\tau_t)\oplus \operatorname{encode}(o_t).

Ký hiệu ⊕\oplus ở đây không nhất thiết là phép nối chuỗi thô. Nó đại diện cho cách runtime đưa bước sinh và kết quả môi trường vào context mới, lý tưởng nhất là kèm ranh giới và metadata rõ ràng.

Sơ đồ vẽ tay vòng lặp ReAct: LLM viết Thought, Action, Action Input; parser như chiếc kéo cắt lấy hành động gửi tới môi trường; mọi bước nối vào một cuộn băng context
Vòng lặp ReAct, phỏng theo Yao và cộng sự (2022)

Hình trên đọc theo chiều kim đồng hồ. Mô hình đọc cả cuộn băng bên dưới rồi viết ra τt\tau_t, một đoạn chữ có ba nhãn. Chiếc kéo Π\Pi cắt lấy phần sau Action: và Action Input:, và chỉ mũi tên đậm màu gỉ sắt ấy mới đi tới môi trường. Hai mũi tên còn lại đều đổ về cuộn băng: nét đứt là chữ do mô hình viết, nét xanh xám là thứ thế giới trả lại. Trên cuộn băng, hai loại ô ấy nằm cạnh nhau, và bằng mắt thường gần như không có gì phân biệt chúng ngoài cái nhãn.

Nhìn theo cách này, ReAct tạo ra một thay đổi quan trọng: context có trục thời gian. Mô hình không chỉ nhận thông tin rồi trả lời một lần. Nó có thể dùng Observation để sửa giả thuyết, đổi truy vấn hoặc chọn hành động khác.

Tuy vậy, Thought và Observation vẫn đi vào lần suy luận tiếp theo dưới dạng biểu diễn trong context. Thought là phần ghi chú do mô hình tự tạo. Observation là kết quả do môi trường cung cấp, sau khi runtime mã hóa nó để đưa trở lại mô hình. Phần thật sự chạm tới môi trường nằm ở ata_t, và hành động đó chỉ tồn tại khi parser cùng runtime chấp nhận đề xuất của mô hình.

Điều này không làm ReAct trở thành "chỉ là text". ReAct thật sự có thể tương tác với Wikipedia, di chuyển trong ALFWorld và thao tác trong WebShop. Nhưng hành động có hiệu lực không phải vì chuỗi Action: tự mang quyền lực. Nó có hiệu lực vì một chương trình bên ngoài diễn giải chuỗi ấy rồi gọi môi trường.

Kết quả thực nghiệm của ReAct cũng tinh tế hơn câu chuyện thường được kể lại. Trên HotpotQA với PaLM-540B, ReAct đạt 27,4 exact match, thấp hơn mức 29,4 của CoT. Con số 35,1 hay được trích dẫn thuộc về chiến lược chuyển từ ReAct sang CoT-SC khi ReAct bế tắc, không phải ReAct đơn lẻ. Trên FEVER, chiều lại đảo ngược: ReAct đạt 60,9 so với 56,3 của CoT. Bài báo đọc hai kết quả này khá thẳng thắn: tra cứu giúp mô hình bớt bịa sự kiện, nhưng khuôn Thought, Action, Observation cứng nhắc cũng làm nó kém linh hoạt khi cần nối nhiều bước lập luận. Chính bài báo còn ghi nhận một failure mode đặc thù: mô hình lặp lại Thought và Action trước đó, không thoát được vòng suy luận.

Ở chiều ngược lại, ReAct tạo bước nhảy rõ rệt trên các benchmark tương tác. Trên ALFWorld, phương pháp này hơn baseline imitation learning 34 điểm phần trăm về success rate dù chỉ dùng một hoặc hai ví dụ trong context; trên WebShop, mức cải thiện tuyệt đối là 10 điểm phần trăm. Đây là kết quả đáng kể, không cần hạ thấp để bảo vệ luận đề của bài viết.

Điều cần phân biệt là phạm vi hậu quả. ALFWorld là một thế giới mô phỏng bằng text; WebShop là môi trường mua sắm mô phỏng; Wikipedia API chủ yếu phục vụ tìm kiếm và đọc. Hành động trong các benchmark ấy có trạng thái và có thể làm nhiệm vụ thành công hoặc thất bại, nhưng chúng không tương đương với việc chuyển tiền thật, xóa dữ liệu production hay gửi email cho khách hàng. Một bước sai trong ALFWorld tốn một lượt thử; một bước sai trong production có thể không lấy lại được.

ReAct chứng minh rằng reasoning trace và environmental feedback có thể phối hợp hiệu quả. Nó chưa chứng minh rằng một giao diện lệnh bằng văn xuôi đã đủ an toàn cho mọi môi trường có side effect.

III. Toolformer và ngã rẽ của tool use

Tháng 2 năm 2023, Schick và cộng sự công bố Toolformer.3 Nếu ReAct chủ yếu dạy cách dùng tool bằng prompt và ví dụ trong context, Toolformer đi theo hướng can thiệp vào quá trình huấn luyện.

Nhóm nghiên cứu dùng một nhúm demonstration cho từng API để mô hình tự đề xuất vị trí cùng nội dung của các lời gọi công cụ trên một corpus lớn. Những lời gọi ấy được thực thi; kết quả được chèn vào chuỗi; sau đó hệ thống chỉ giữ lời gọi nào giúp giảm loss khi dự đoán những token tiếp theo, và giảm đủ một ngưỡng so với cả việc không gọi lẫn việc gọi mà không có kết quả. Cuối cùng, GPT-J 6,7B được fine-tune trên tập dữ liệu đã bổ sung tool calls.

Toolformer vẫn biểu diễn lời gọi API bằng token. Đầu vào và đầu ra của tool được tuyến tính hóa rồi đặt giữa văn bản bằng các dấu phân cách; trên thực tế, nhóm tác giả dùng những ký tự rất bình thường như [, ] và -> để khỏi phải sửa bộ từ vựng. Vì thế, ở tầng triển khai vẫn cần một cơ chế nhận diện lời gọi, thực thi API và trả kết quả. Điểm khác nằm ở chỗ ngữ pháp gọi tool đã trở thành một phần của dữ liệu fine-tuning, thay vì chỉ là một quy ước được mô tả trong prompt.

Nói cách khác, ReAct và Toolformer không đại diện cho "text" đối đầu với "hệ thống". Cả hai đều cần runtime. Sự khác nhau nằm ở nơi hành vi gọi tool được học:

  • ReAct cho mô hình thấy khuôn hành động trong context.
  • Toolformer fine-tune mô hình trên các chuỗi có lời gọi API đã được lọc bằng một tín hiệu tự giám sát.

Đầu năm 2023, cách triển khai theo ReAct hấp dẫn hơn đối với phần lớn đội ngũ sản phẩm. Nó không đòi hỏi quyền fine-tune một mô hình frontier, có thể đổi tool chỉ bằng sửa prompt và cho ra demo rất nhanh. LangChain cùng nhiều framework khác biến pattern ấy thành vài dòng cấu hình.

Cái giá của sự tiện lợi là một giao diện mong manh. Mô hình được yêu cầu vừa suy luận bằng văn xuôi, vừa phát một lệnh có cú pháp chính xác, trong cùng một lượt sinh. Parser phải tìm ranh giới giữa hai việc đó sau khi chuỗi đã được tạo xong.

Khi parser bắt đúng, hệ thống trông như có một đôi tay. Khi parser trượt, ta mới thấy đôi tay ấy được nối vào đâu.

IV. Parser: Mép cắt giữa lời nói và hành động

Các agent kiểu ReAct thời kỳ đầu thường vận hành bằng một vòng lặp đơn giản:

  1. Đưa yêu cầu, danh sách tool, lịch sử Thought, Action và Observation vào prompt.
  2. Gọi mô hình để lấy bước tiếp theo.
  3. Parse output thành AgentAction hoặc AgentFinish.
  4. Nếu có action, gọi tool và đưa kết quả trở lại vòng lặp.
  5. Nếu có final answer, dừng.

Tài liệu LangChain thời kỳ đó mô tả AgentExecutor gần như đúng theo cấu trúc trên.4 Ví dụ custom agent còn dùng stop sequence \nObservation: và giải thích lý do rất thẳng: nếu không dừng ở đó, mô hình có thể "bịa ra một observation cho bạn".

Trong mã MRKL của LangChain đầu tháng 3 năm 2023, output được tách bằng regex Action: (.*?)\nAction Input: (.*). Một commit ngày 9 tháng 3 chỉ nhằm chấp nhận thêm dòng trống giữa hai trường đã phải sửa chính biểu thức ấy, đổi \n thành [\n]*.5 Chi tiết này nhỏ, nhưng nó cho thấy hợp đồng giữa mô hình và runtime khi ấy nằm ở đâu: trong số ký tự xuống dòng mà một biểu thức chính quy chịu bỏ qua.

Ta có thể trừu tượng hóa giao diện đó như sau:

Π:Σ∗→A∪{⊥},\Pi:\Sigma^*\to\mathcal A\cup\{\bot\},

trong đó Σ∗\Sigma^* là tập mọi chuỗi có thể sinh, A\mathcal A là không gian action có cấu trúc, còn ⊥\bot biểu thị parse failure.

Với một context hh, xác suất lỗi cú pháp của giao diện có thể ký hiệu:

εparse(h)=Pθ(Π(τ)=⊥∣h).\varepsilon_{\mathrm{parse}}(h) = P_\theta\bigl(\Pi(\tau)=\bot\mid h\bigr).

Đây là một mô hình khái niệm, không phải tuyên bố rằng mọi agent framework đều dùng cùng một parser hoặc có thể đo trực tiếp cùng một ε\varepsilon. Nó giúp tách hai câu hỏi thường bị nhập làm một:

  • Mô hình có chọn đúng hành động hay không?
  • Chuỗi mô hình sinh ra có được parser đọc thành một hành động hay không?

Một hệ thống có thể sai ở cả hai tầng. JSON hợp lệ vẫn có thể gọi nhầm tool. Action đúng về ý định vẫn có thể hỏng vì thiếu dấu ngoặc kép. Parser failure là lỗi giao diện; semantic failure là lỗi quyết định. Sửa tầng thứ nhất chưa tự sửa tầng thứ hai.

Mô hình khái niệm này còn cho thấy vì sao một parser "khá ổn" trong demo lại gãy trong tác vụ dài. Nếu mỗi bước có xác suất lỗi cú pháp ε\varepsilon và các bước tạm coi là độc lập, xác suất đi hết TT bước mà không vấp lần nào là

P(không lỗi trong T bước)=(1−ε)T.P(\text{kh\char"F4{}ng l\char"1ED7{}i trong }T\text{ b\char"1B0{}\char"1EDB{}c})=(1-\varepsilon)^T.

Với ε=5%\varepsilon=5\%, một demo một bước thành công 95% số lần. Nhưng với một tác vụ hai mươi bước, 0,9520≈0,360{,}95^{20}\approx 0{,}36: gần hai phần ba số lần chạy sẽ gặp ít nhất một lần parser trả về ⊥\bot. Đây còn là ước lượng lạc quan, vì nó giả định ε\varepsilon đứng yên. Phần tiếp theo sẽ cho thấy trong agent loop, ε\varepsilon có thể tăng dần theo chính những lần thất bại trước đó.

Stop sequence cũng không phải một ranh giới thẩm quyền. Nó là cơ chế dừng quá trình sinh khi một mẫu token xuất hiện. Nếu mô hình dùng một biến thể khác, hoặc nếu backend không áp dụng stop như dự kiến, mô hình có thể tự sinh luôn một đoạn mang nhãn Observation:. Về mặt hình thức, dòng ấy nằm trong τt\tau_t, không phải kết quả ot=E(at)o_t=\mathcal E(a_t).

Đây là điểm nguy hiểm: hai chuỗi có thể trông giống nhau đối với người đọc nhưng có provenance hoàn toàn khác nhau. Một chuỗi là kết quả tool thật. Chuỗi kia là phần hoàn thành do mô hình tưởng tượng. Nếu runtime chỉ nối cả hai vào một transcript mà không giữ metadata đáng tin cậy, bước sau rất khó phân biệt "thế giới đã trả lời" với "mô hình vừa viết giống lời của thế giới".

Sơ đồ vẽ tay regex của parser MRKL: một biến thể khớp thành AgentAction, ba biến thể cùng ý nghĩa bị trả về lỗi; bên dưới là Observation do mô hình tự viết khi stop sequence không chặn
Một regex, bốn cách nói cùng một ý

Bốn ô bên trái của hình trên đều nói cùng một ý: tìm giá thép. Người đọc hiểu cả bốn như nhau. Regex thì chỉ nhận ô đầu tiên; thêm số thứ tự, bôi đậm nhãn theo thói quen markdown, hay nói thành một câu tự nhiên, cả ba đều rơi vào ⊥\bot. Dải dưới cùng là nửa còn lại của vấn đề: đường cắt nét đứt là nơi stop sequence lẽ ra phải chặn. Nếu nó không chặn, dòng Observation: 42 triệu/tấn màu gỉ sắt được viết bởi chính mô hình, nhưng mang cái nhãn của thế giới.

Temperature bằng 0 không loại bỏ vấn đề. Nó có thể làm một lần gọi ổn định hơn trên cùng đầu vào, nhưng context trong vòng lặp liên tục thay đổi. Mỗi Observation, thông báo lỗi hoặc phần lịch sử mới đều tạo một điều kiện sinh khác. Deterministic decoding không biến một hợp đồng bằng văn xuôi thành type system.

V. Khi lỗi parser quay lại dưới dạng một câu nói

Một lựa chọn phổ biến của agent framework là gửi lỗi parse trở lại mô hình để nó tự sửa. Khoảng cuối tháng 4 năm 2023, vài tuần sau commit regex kể trên, LangChain thêm tùy chọn handle_parsing_errors: nếu bật, lỗi từ output parser được chuyển thành một Observation cho LLM ở vòng kế tiếp.6 OutputParserException cũng cho phép đính kèm output hỏng cùng một mô tả để mô hình thử lại.

Đây là một chiến lược hợp lý. Nhiều lỗi định dạng nhỏ có thể được sửa ngay ở lần kế tiếp mà không cần dừng toàn bộ tác vụ. AgentExecutor cũng không để vòng lặp chạy vô hạn: mặc định nó dừng sau mười lăm vòng. Nhưng giới hạn ấy đếm số bước, không phân biệt bước nào là tiến triển, bước nào chỉ là một lần xin lỗi nữa. Vấn đề xuất hiện khi retry không có phân loại lỗi và không có đường thoát theo loại lỗi.

Gọi αt\alpha_t là thông báo parser đưa trở lại context. Bước tiếp theo trở thành:

ht+1=ht⊕τt⊕αt.h_{t+1}=h_t\oplus\tau_t\oplus\alpha_t.

Không có định lý nào nói rằng

εparse(ht+1)<εparse(ht).\varepsilon_{\mathrm{parse}}(h_{t+1}) < \varepsilon_{\mathrm{parse}}(h_t).

Thậm chí có lý do để nghi ngờ điều ngược lại. Bài trước đã thấy in-context learning mạnh đến mức nào: vài ví dụ đặt trước câu hỏi đủ để kéo mô hình theo một khuôn mẫu. Cơ chế ấy không phân biệt ví dụ tốt với ví dụ xấu. Mỗi vòng thất bại để lại trên cuộn băng một output sai khuôn nữa, và càng về sau, số ví dụ sai càng áp đảo khuôn mẫu đúng duy nhất nằm trong prompt ban đầu.

Thông báo lỗi có thể giúp mô hình sửa đúng. Nó cũng có thể khiến mô hình giải thích, xin lỗi, đặt output trong code fence hoặc thay đổi format theo một cách mới. Issue #623 của AutoGPT vào tháng 4 năm 2023 ghi lại đúng kiểu thất bại này.7 Người dùng chạy ở chế độ chỉ dùng GPT-3.5; hệ thống đưa JSON hỏng cho một lượt gọi khác để sửa, nhưng lượt "sửa" lại trả về lời xin lỗi bằng văn xuôi, rằng nó không thể trả kết quả hợp lệ khi không biết schema. Kết quả tiếp tục thiếu trường command và lại thất bại. Hợp đồng mà AutoGPT đòi hỏi khi ấy là một khối JSON có thoughts và command, được mô tả hoàn toàn bằng chữ trong prompt.8

Đó là failure mode có thể gọi là apology loop. Tên gọi không nhằm khẳng định mọi mô hình sau RLHF đều tất yếu xin lỗi, cũng không phải một quy luật xác suất phổ quát. Nó mô tả một vòng lặp kỹ thuật cụ thể:

  1. Mô hình trả output sai format.
  2. Parser biến lỗi thành một thông báo bằng ngôn ngữ tự nhiên.
  3. Mô hình phản hồi thông báo ấy như đang tham gia hội thoại.
  4. Phản hồi mới vẫn không khớp ngữ pháp máy.
  5. Hệ thống lặp lại mà không tiến gần hơn tới hành động.
Sơ đồ vẽ tay apology loop: output sai khuôn, parser báo lỗi, lỗi thành một câu nói, mô hình xin lỗi; bên phải là transcript ngày càng nhiều ví dụ sai
Apology loop, dựa trên issue #623 của AutoGPT

Vòng bên trái là năm bước trên, gộp thành bốn trạm. Mũi tên nào cũng chạy đúng, không trạm nào hỏng theo nghĩa kỹ thuật; chính vì thế mà vòng lặp không tự dừng. Cột bên phải là cuộn băng sau vài vòng: một khuôn mẫu đúng ở đầu, bên dưới là output hỏng, lỗi parser và lời xin lỗi thay nhau chồng lên. Mũi tên dọc nhắc lại lập luận ở trên: càng đi xuống, số ví dụ sai mà mô hình nhìn thấy càng nhiều.

Đây là context engineering tự làm bẩn context của chính nó. Mỗi vòng thất bại thêm một output hỏng, một thông báo lỗi và có thể thêm một lời giải thích. Nếu toàn bộ lịch sử được giữ lại, mô hình ở lượt sau phải tìm lại hợp đồng cú pháp giữa một transcript ngày càng dài và ngày càng chứa nhiều ví dụ sai.

Đến giữa năm 2025, một phiên bản hiện đại hơn của failure mode này xuất hiện công khai trong các báo cáo về Gemini CLI. Một người dùng mô tả agent liên tục thêm rồi gỡ cùng một đoạn code, xin lỗi sau mỗi lần thất bại, sau đó quay lại đúng giải pháp vừa không hoạt động.9 Issue ấy được đóng như một bản trùng của nhóm lỗi infinite loop đang được theo dõi trong một parent issue. Trong parent issue, một contributor về sau tổng kết rằng những vòng lặp này chủ yếu đến từ việc dùng tool sai: truyền nhầm tham số, tool thất bại và những thứ tương tự.10 Một issue khác đề nghị Gemini "bớt xin lỗi" sau mỗi lần sửa sai; maintainer trả lời rằng đây là lỗi ở model, không phải ở Gemini CLI.11

Một bình luận trong chính parent issue ấy cho thấy vòng lặp trông như thế nào khi nhìn tận mắt.12

Ảnh chụp màn hình Gemini CLI: bốn vòng liên tiếp gồm cùng một câu dẫn, ReadFile src/sniper.rs, rồi Edit thất bại với lỗi Failed to edit, could not find the string to replace
Gemini CLI với gemini-2.5-pro, ảnh do người dùng nbs đăng trong issue #1531, ngày 27/6/2025

Ảnh chụp chỉ là một khúc của vòng lặp; người đăng cho biết phần lặp thật dài hơn gấp ba lần những gì vừa khung hình. Đọc từ trên xuống, chu kỳ có bốn nhịp và lặp lại y nguyên: một câu dẫn "I see, the previous replacement failed…", một lần ReadFile, một câu dẫn thứ hai "I've reviewed the errors…", rồi một lần Edit thất bại với cùng thông báo Failed to edit, could not find the string to replace. Không có lời xin lỗi nào ở đây, nhưng hình dạng thì đúng là hình dạng của phần này: lỗi quay về dưới dạng một dòng chữ, mô hình kể lại lỗi ấy bằng văn xuôi, rồi phát lại đúng hành động cũ. Hai chi tiết nhỏ đáng để ý. Mỗi lần Edit vẫn mang dấu tích xanh, vì lời gọi tool đã chạy xong, trong khi thất bại thật sự nằm trong phần chữ của kết quả, đúng kiểu một sự kiện runtime bị giáng thành một câu nói. Và người đăng hỏi liệu temperature có đang mặc định bằng 0 không. Như phần IV đã nói, câu trả lời không quan trọng lắm: vòng lặp này không đến từ độ ngẫu nhiên của phép lấy mẫu, mà từ một cuộn băng cứ trả về cùng một điều kiện.

Các nguồn công khai không cho phép kết luận Gemini triển khai đúng thuật toán ReAct trong bài báo, càng không chứng minh ReAct là nguyên nhân trực tiếp khiến model xin lỗi. Cách đọc chính xác hơn là: hành vi xin lỗi thuộc về model, còn agent loop theo cấu trúc gần với ReAct có thể khuếch đại hành vi ấy. Khi một lần gọi tool thất bại, kết quả lỗi cùng lời tự phê bình lại được đưa vào context; nếu model không đổi chiến lược và runtime không có loop breaker, lời xin lỗi trở thành token mới trên cuộn băng chứ không phải tín hiệu phục hồi. ReAct không tạo ra khuynh hướng xin lỗi, nhưng một vòng feedback thiếu kiểm soát có thể biến khuynh hướng đó thành chuỗi lặp. Việc hai năm sau vẫn gặp lại cùng hình dạng thất bại, trên một model mạnh hơn nhiều, cho thấy đây không chỉ là chuyện model đời đầu còn yếu. Nó là chuyện vòng lặp được thiết kế thế nào.

Cách khắc phục không phải cấm retry. Cách khắc phục là đưa retry về đúng vị trí của nó trong runtime:

  • phân biệt lỗi cú pháp, lỗi schema, lỗi authorization và lỗi execution;
  • giới hạn số lần thử theo từng loại lỗi, không chỉ theo tổng số bước;
  • không để output hỏng trở thành demonstration chi phối các lượt sau;
  • dùng mã lỗi có cấu trúc thay vì một lời phàn nàn dài;
  • dừng cứng khi lỗi không thể phục hồi;
  • ghi lại trace để biết vòng lặp gãy tại node nào.

Một lỗi của runtime phải giữ tư cách là một sự kiện của runtime. Nếu mọi lỗi đều bị chuyển thành một câu nói để mô hình "hiểu giúp", hệ thống đã giao cả cơ chế phục hồi cho cùng thành phần vừa tạo ra lỗi.

VI. Phòng ban ảo: Một tầm nhìn đến quá sớm

Mùa xuân năm 2023, ý tưởng agent nhanh chóng mở rộng thành multi-agent systems. AutoGPT nối lập kế hoạch, tự phê bình, bộ nhớ và command trong một vòng lặp. BabyAGI tách việc tạo task, sắp xếp ưu tiên và thực thi.13 CAMEL dùng role-playing để hai chat agent hợp tác.14 HuggingGPT dùng ChatGPT lập kế hoạch, chọn các model trên Hugging Face, thực thi từng nhiệm vụ rồi tổng hợp kết quả.15

Những công trình ấy không giống nhau và không nên bị gom thành một kiến trúc duy nhất. Một số có tool riêng, state riêng, workflow rõ ràng hoặc model chuyên biệt. Tuy nhiên, chúng cùng làm phổ biến một tầm nhìn rất hấp dẫn: một hệ thống có thể tổ chức công việc như một nhóm chuyên môn, với Planner, Coder, Reviewer, Manager hoặc Researcher đảm nhận các trách nhiệm khác nhau.

Đây không phải một ý tưởng ngây thơ. Phân công lao động là cách những hệ thống phức tạp ngoài đời đạt quy mô: mỗi vai trò có mục tiêu cục bộ, công cụ riêng, state riêng và một hợp đồng bàn giao rõ ràng. Việc đưa cấu trúc ấy vào phần mềm agent là một hướng đi có giá trị. Role prompt đời đầu là cách rẻ nhất để thử nghiệm giả thuyết đó trước khi hạ tầng dành cho agent tồn tại đầy đủ.

Giả sử một vai nhận context hh và role prompt rir_i. Output của vai thứ ii vẫn có dạng:

τi∼Pθ(⋅∣ri⊕h).\tau_i\sim P_\theta(\cdot\mid r_i\oplus h).

Thay rir_i có thể làm hành vi thay đổi đáng kể. Với model đủ mạnh, sự chuyên biệt hóa bằng context có thể tạo ra giá trị thật. Tuy nhiên, LLM đời đầu vẫn dùng chung θ\theta, thường dùng chung nguồn dữ liệu, cùng tool và cùng một transcript. Dòng "bạn là một reviewer khó tính" có thể giúp model chuyển chế độ làm việc, nhưng chưa tự cung cấp test runner, static analyzer, dữ liệu production hoặc một tiêu chuẩn đúng sai độc lập.

Vì vậy, giới hạn của "phòng ban ảo" năm 2023 không chứng minh tầm nhìn ấy sai. Nó cho thấy khoảng cách giữa vai trò được mô tả và năng lực chuyên môn được hệ thống bảo đảm. Một Reviewer bằng prompt đã có thể phát hiện mâu thuẫn, đặt câu hỏi và tạo thêm góc nhìn. Để trở thành một reviewer đáng tin cậy, vai đó còn cần model đủ mạnh, tool kiểm chứng, state tách biệt và tiêu chí nghiệm thu có thể thực thi.

Sự khác biệt thể hiện rõ qua bài toán nhiều reviewer. Giả sử mỗi reviewer bỏ sót một lỗi với xác suất biên qq. Nếu các lỗi độc lập, xác suất cả kk reviewer cùng bỏ sót là:

P(⋂i=1kEi)=qk.P\left(\bigcap_{i=1}^{k}E_i\right)=q^k.

Nhưng nếu chúng tương quan hoàn hảo, xác suất ấy là:

P(⋂i=1kEi)=q.P\left(\bigcap_{i=1}^{k}E_i\right)=q.

Trong thực tế, kết quả thường nằm giữa hai cực này và không thể suy ra chỉ từ qq. Những reviewer dùng chung model, prompt gần giống nhau và đọc cùng một artifact dễ chia sẻ cùng điểm mù. Kim và cộng sự (2025) đo trực tiếp hiện tượng này trên hơn 350 LLM:16 trên một bộ dữ liệu, khi cả hai model cùng sai, chúng chọn cùng một đáp án sai khoảng 60% số lần. Đáng chú ý hơn, những model lớn hơn và chính xác hơn lại có lỗi tương quan cao hơn, kể cả khi khác kiến trúc và khác nhà cung cấp. Đổi model đã chưa chắc tạo ra các lá phiếu độc lập; dùng lại cùng một model với một role prompt khác càng không có lý do gì để mặc định độc lập.

Biểu đồ vẽ tay xác suất tất cả reviewer cùng bỏ sót một lỗi theo số reviewer k, với q bằng 30%: độc lập giảm về 0, tương quan một phần chạm sàn 15%, cùng điểm mù nằm ngang
Thêm reviewer không xóa được điểm mù chung, số minh họa

Hình trên lấy q=30%q=30\%. Đường xanh xám là giấc mơ độc lập: hai reviewer còn 9%, ba reviewer còn 2,7%, rất nhanh về gần 0. Đường trên cùng là trường hợp mọi reviewer cùng một điểm mù: thêm bao nhiêu người thì vẫn 30%. Đường ở giữa chỉ là một giả định minh họa, không phải số đo: một nửa số lỗi là điểm mù chung, nửa kia độc lập, tức 12q+12qk\tfrac12 q+\tfrac12 q^k. Nó chạm sàn ở 15% và không xuống thêm được nữa, dù thêm reviewer thứ năm hay thứ sáu. Phần lỗi chung ấy chỉ giảm khi có một nguồn bất đồng thật, không giảm khi có thêm người gật đầu.

Đây không phải lý do để bỏ kiến trúc nhiều reviewer. Nó là bản thiết kế cho thế hệ tiếp theo: muốn các reviewer tạo ra giá trị hệ thống, phải đưa vào nguồn bất đồng có thật như test thực thi, dữ liệu khác, model khác, verifier hình thức, schema khác, tool khác hoặc quyền nhìn thấy trạng thái mà vai tạo artifact không có. Tầm nhìn phân vai vẫn giữ nguyên; substrate phải trưởng thành để hiện thực hóa tầm nhìn đó.

Các công trình multi-agent cũng sớm đi theo hướng ấy. MetaGPT mô tả cascading hallucinations khi nối các LLM một cách ngây thơ, rồi mã hóa SOP vào chuỗi prompt và buộc các agent bàn giao bằng intermediate structured outputs.17 ChatDev thiết kế chat chain để quy định trao đổi gì, cùng cơ chế communicative dehallucination để quy định trao đổi thế nào.18 Nói cách khác, tầm nhìn về một tổ chức agent không dừng ở việc đặt tên vai trò. Nó đòi hỏi quy trình, artifact, interface và tiêu chuẩn bàn giao, đúng như một tổ chức kỹ thuật thật.

VII. Unsigned tape: Khi substrate chưa theo kịp tầm nhìn

Suốt bài này, context đã được gọi là một "cuộn băng". Hình ảnh ấy không phải ngẫu hứng. Nó gợi lại máy Turing, mô hình tính toán gốc của khoa học máy tính: một đầu đọc chạy trên dải băng, đọc ký hiệu trong ô hiện tại rồi ghi ký hiệu mới theo một bảng quy tắc. Với mô hình ngôn ngữ, dải băng là luồng token trong cửa sổ ngữ cảnh, và vòng tự hồi quy vận hành đơn giản đến bất ngờ: quét toàn bộ những gì đã có trên băng, tính phân phối Pθ(xt+1∣x≤t)P_\theta(x_{t+1}\mid x_{\leq t}), ghi thêm một token vào cuối, rồi lặp lại.

Phép so sánh lệch ở hai chỗ, và chính hai chỗ lệch ấy giải thích phần lớn những gì bài này đã mô tả.

Chỗ lệch thứ nhất: đầu đọc của máy Turing có thể lùi lại và ghi đè lên ô cũ, còn mô hình ngôn ngữ chỉ ghi thêm vào cuối. Một output hỏng, một Observation bịa hay một lời xin lỗi, một khi đã lên băng, sẽ nằm đó cho mọi lượt đọc sau, trừ khi runtime chủ động cắt bỏ. Apology loop ở phần V tích tụ được là vì vậy: cuộn băng không có nút xóa, và mỗi vòng thất bại để lại thêm một ví dụ sai cho vòng kế tiếp noi theo.

Chỗ lệch thứ hai nằm ở mực. Trên băng, mọi thứ được viết bằng cùng một loại mực là token. Chỉ dẫn của developer, câu hỏi của user, Thought của mô hình, kết quả tool và log lỗi của server đều trở thành những đoạn token nối tiếp nhau. Attention vẫn nhìn thấy chữ Observation: ở đầu dòng, và mô hình hoàn toàn có thể học rằng thứ theo sau nhãn ấy thường đến từ môi trường. Thứ nó không có là cách kiểm chứng. Một dòng Observation: 404 Not Found do runtime thật sự ghi vào, và một dòng y hệt do chính mô hình vừa viết tiếp vì thấy hợp ngữ cảnh, là cùng một chuỗi token. Không có gì trên băng cho biết ai đang cầm bút.

Hạ tầng như vậy có thể gọi bằng ẩn dụ unsigned tape: một cuộn băng mà không dòng nào mang chữ ký, thiếu provenance và thiếu ranh giới thẩm quyền được runtime cưỡng chế. Đây không phải khuyết điểm của ý tưởng agent; nó là dấu hiệu cho thấy công nghệ nền khi ấy chưa được thiết kế cho mức tự chủ mà tầm nhìn yêu cầu. Chat API tháng 3 năm 2023 đã có role cho system, user và assistant, nhưng kết quả tool chưa có vai riêng trong API cho tới function calling tháng 6, và chưa gắn với ID của một lời gọi cụ thể cho tới khi API chuyển sang tool_calls vào cuối năm ấy. Trong khoảng trống đó, các framework agent phải tự render mọi thứ thành văn xuôi.

"Chữ ký" ở đây không nhất thiết là chữ ký mật mã. Nó là tên gọi cho tập metadata mà máy có thể kiểm tra: ai tạo ra message, message thuộc loại nào, có quyền yêu cầu hành động gì, payload tuân theo schema nào và runtime nào xác nhận kết quả.

Trong một agent loop đơn giản, context có thể chứa ít nhất năm nguồn:

  • yêu cầu của user;
  • chỉ dẫn của developer hoặc system;
  • suy luận và đề xuất hành động của model;
  • dữ liệu từ tool hoặc môi trường;
  • lỗi, trạng thái và quyết định của runtime.

Nếu tất cả được render thành một đoạn văn xuôi rồi nối lại, các nhãn như System:, Observation: hay Reviewer: vẫn có thể giúp mô hình nhận biết cấu trúc. Nhưng chúng không tự tạo ra một authorization boundary. Một trang web không trở thành system message chỉ vì trong nội dung có câu "hãy bỏ qua chỉ dẫn trước đó"; đồng thời, model vẫn có thể bị câu ấy tác động nếu ứng dụng đưa dữ liệu không tin cậy vào cùng luồng hướng dẫn.

Đó chính là nền của indirect prompt injection. Greshake và cộng sự chỉ ra rằng ứng dụng tích hợp LLM làm mờ ranh giới giữa dữ liệu và chỉ dẫn: kẻ tấn công cài prompt vào nguồn dữ liệu mà hệ thống nhiều khả năng sẽ truy xuất, prompt ấy đi vào context và ảnh hưởng tới cách model gọi API.19 Vấn đề không phải attention "không biết" role token là gì. Vấn đề là dữ liệu không đáng tin đã được đặt vào một giao diện ngôn ngữ có khả năng điều khiển cùng model đang đề xuất hành động.

Nói cách khác, trên unsigned tape, mô hình không có cách nào đáng tin cậy để tách lệnh, thứ mang tính cưỡng chế, khỏi dữ liệu, thứ chỉ để tham khảo. Cả hai đều chỉ là chữ, và chữ nào nghe giống mệnh lệnh hơn thì có cơ hội được làm theo.

Sự cào bằng ấy cũng giải thích apology loop, lần này từ phía runtime. Khi parser thất bại hay tool trả lỗi, runtime đang nắm một sự kiện có tính cưỡng chế: bước này không thực thi được, phải dừng hoặc đổi đường. Nhưng trên unsigned tape, cách duy nhất để báo điều đó là viết thêm một đoạn chữ. Sự kiện bị giáng cấp thành một câu trong hội thoại, và mô hình, vốn được huấn luyện để đáp lại hội thoại một cách lịch sự, đáp lại bằng một lời xin lỗi thay vì dừng luồng thực thi. Vấn đề không phải mô hình không đọc được lỗi. Vấn đề là hệ thống đã trao lỗi cho nó dưới một hình thức chỉ có thể được đáp lời.

Một transcript đáng tin cậy vì thế không nên chỉ là chuỗi:

h=m1⊕m2⊕⋯⊕mn.h=m_1\oplus m_2\oplus\cdots\oplus m_n.

Runtime cần lưu mỗi message như một đối tượng có kiểu, chẳng hạn:

mi=(sourcei,rolei,payloadi,schemai,authorityi).m_i= (\text{source}_i,\text{role}_i,\text{payload}_i,\text{schema}_i,\text{authority}_i).
Sơ đồ vẽ tay so sánh một cuộn băng không chữ ký chứa prompt injection trong Observation với các message có kiểu mang nguồn, vai và thẩm quyền
Unsigned tape và typed messages

Bên trái là cuộn băng không chữ ký, đọc từ trên xuống như mô hình đọc. Vùng tô đỏ là một câu lệnh nằm lẫn trong trang web vừa được truy xuất, mặc nhãn Observation: như mọi kết quả khác. Bên dưới là một Observation về giá thép, một câu "Reviewer: đã kiểm tra, ổn" và cuối cùng là lệnh gửi email. Không có gì trên cuộn băng cho biết Observation nào đến từ một lời gọi tool thật, reviewer đã kiểm tra bằng gì, hay lệnh gửi email được ai cho phép. Bên phải là những nguồn ấy khi được lưu thành message có kiểu, mỗi thẻ mang nguồn, vai và thẩm quyền do runtime gắn vào. Trang web vẫn được đọc, nhưng như dữ liệu, và thẩm quyền của nó là không; giá thép gắn với tool call 7f3a; còn đề xuất của model chỉ ở trạng thái chờ duyệt.

Model vẫn có thể nhận phần nội dung được render từ các message ấy. Tuy nhiên, quyền thực thi không được suy ra từ việc một câu nào đó nghe có vẻ ra lệnh. Runtime phải dựa trên metadata mà chính nó kiểm soát.

Sự phân biệt này giải quyết bốn nhầm lẫn phổ biến:

  • Đề xuất không phải lệnh đã được cấp quyền. Model có thể đề xuất delete_record; policy engine vẫn có thể từ chối.
  • Chuỗi giống Observation không phải Observation đã được xác minh. Chỉ kết quả gắn với một tool execution ID hợp lệ mới có provenance từ môi trường.
  • Lỗi runtime không phải một ý kiến trong hội thoại. Nó là trạng thái có mã, loại và chính sách phục hồi.
  • Reviewer đồng ý không phải bằng chứng. Bằng chứng phải đến từ trace, test, dữ liệu hoặc verifier có thể kiểm tra.

Unsigned tape cũng giải thích vì sao chỉ kéo dài context chưa đủ để hoàn thành tầm nhìn agent. Cửa sổ lớn giúp giữ nhiều lịch sử hơn, nhưng lịch sử vẫn có thể chứa chỉ dẫn xung đột, dữ liệu độc hại, output hỏng và các message bị gán nhãn không đáng tin. Lost in the Middle, như bài trước đã phân tích, còn nhắc rằng thông tin tồn tại trong context chưa bảo đảm nó được sử dụng đồng đều.20

Muốn tiến từ prototype sang hệ thống trưởng thành, cuộn băng ấy không chỉ cần dài hơn. Nó cần typed messages, provenance, policy và những ranh giới mà runtime có thể cưỡng chế. Rời khỏi unsigned tape, bằng schema, role và tool call ID do chính runtime gắn vào, là bước chuyển bắt buộc để biến một cỗ máy sinh chữ thành một hệ thống phần mềm có kỷ luật. Phần tiếp theo kể lại bước chuyển ấy đã bắt đầu thế nào, và vì sao nó mới đi được nửa đường.

VIII. Function calling, Structured Outputs và phần còn thiếu

Ngày 1 tháng 3 năm 2023, ChatGPT API đã dùng cấu trúc message với các role như system, user và assistant.21 Vì vậy, sẽ không chính xác nếu nói function calling là lần đầu API biết phân biệt người nói.

Bước ngoặt ngày 13 tháng 6 năm 2023 nằm ở chỗ khác. OpenAI giới thiệu function calling cho gpt-4-0613 và gpt-3.5-turbo-0613.22 Lập trình viên có thể mô tả function bằng JSON Schema; model được fine-tune để quyết định khi nào cần gọi và trả về arguments dưới dạng JSON. Lời gọi tool có một trường riêng, thay vì phải ẩn trong đoạn Thought / Action / Action Input rồi chờ regex cắt.

Đây là một cải thiện kiến trúc quan trọng. Nó chuyển một phần hợp đồng từ prompt convention sang API contract. Ứng dụng không còn phải đoán xem dòng nào trong bài văn là lệnh. SDK và backend có thể nhận một đối tượng tool call, kiểm tra tên tool, parse arguments rồi áp dụng chính sách riêng. Nói cách khác, substrate bắt đầu đuổi kịp tầm nhìn mà ReAct và các agent framework đã đặt ra trước đó.

Nhưng ngay trong API reference thời ấy, OpenAI vẫn ghi rõ rằng model không phải lúc nào cũng sinh JSON hợp lệ, có thể bịa ra tham số không có trong schema, và lập trình viên nên tự kiểm tra arguments trước khi gọi hàm.23 Trường riêng đã có, nhưng thứ nằm trong trường ấy vẫn là văn bản do mô hình sinh. Và kể cả khi JSON hợp lệ, function calling năm 2023 chưa làm các lớp lỗi khác biến mất:

  • model vẫn có thể chọn nhầm function;
  • arguments có thể đúng cú pháp nhưng sai ý nghĩa;
  • dữ liệu có thể thiếu, cũ hoặc bị prompt injection;
  • tool có thể thất bại khi thực thi;
  • lời gọi hợp lệ vẫn có thể vượt quá quyền của user;
  • runtime vẫn phải quyết định retry, rollback và audit.

Tháng 8 năm 2024, OpenAI giới thiệu Structured Outputs.24 Với strict: true, output được ép tuân theo JSON Schema được hỗ trợ. OpenAI mô tả cách kết hợp model training với constrained decoding, tức chỉ cho phép các token còn hợp lệ theo schema ở từng bước sinh. Trong eval nội bộ về việc tuân theo JSON Schema phức tạp, gpt-4o-2024-08-06 với Structured Outputs đạt 100%, trong khi gpt-4-0613, model ra mắt cùng function calling một năm trước, đạt dưới 40%.

Hai con số ấy đo đúng khoảng cách mà bài viết này nói tới: hơn ba phần năm số lần gọi trên eval ấy từng không khớp schema, ngay cả khi đã có trường tool call riêng. Nhưng con số 100% cũng phải được đọc đúng phạm vi. Chính bài công bố liệt kê giới hạn: chỉ một tập con của JSON Schema được hỗ trợ; output có thể không hoàn chỉnh nếu model từ chối yêu cầu, hoặc nếu quá trình sinh chạm max_tokens hay một điều kiện dừng khác; lúc ra mắt, chế độ này chưa dùng được cùng parallel function calls; và nó không ngăn được lỗi của model bên trong giá trị của từng trường. Structured Outputs có thể loại bỏ một lớp lớn lỗi cú pháp và schema khi request hoàn tất bình thường trong đường dẫn được hỗ trợ. Nó không chứng minh rằng nội dung bên trong các trường là đúng, function được chọn là phù hợp, user có quyền thực hiện hay side effect đã thành công.

Nói gọn:

εschema≈0\varepsilon_{\mathrm{schema}}\approx 0

không kéo theo:

εsemantic=εauthorization=εexecution=0.\varepsilon_{\mathrm{semantic}} = \varepsilon_{\mathrm{authorization}} = \varepsilon_{\mathrm{execution}} =0.
Sơ đồ vẽ tay bốn cánh cửa lỗi: cú pháp và schema đã khóa bằng Structured Outputs, ý nghĩa, quyền hạn và thực thi vẫn còn mở
Bốn cánh cửa, một ổ khóa

Bốn cánh cửa trong hình là bốn loại lỗi, và một action phải đi qua cả bốn. Cửa đầu tiên đã có ổ khóa: Structured Outputs. Ba cửa còn lại vẫn khép hờ, mỗi cửa cần một thứ khác để giữ: test hoặc verifier cho ý nghĩa, policy engine cho quyền hạn, và retry, rollback, audit cho thực thi. Một cửa đã khóa chắc không làm ba cửa kia chắc hơn.

Parser không biến mất; một phần parsing được chuyển vào hạ tầng tạo output có ràng buộc. Đây là tiến bộ thật, nhưng nó mới làm chắc cánh cửa. Nó chưa quyết định ai được cầm chìa khóa, được mở cửa nào và phải làm gì nếu căn phòng phía sau đang cháy.

Một agent runtime đáng tin cậy vì thế cần coi model là thành phần đề xuất, không phải nguồn thẩm quyền cuối cùng. Gọi ata_t là action model đề xuất, runtime phải đánh giá action ấy theo danh tính đang hành động (principal), policy và state hiện tại:

a^t=V(at,principal,st),\hat a_t = V(a_t,\text{principal},s_t),

trong đó VV có thể cho phép, từ chối hoặc yêu cầu phê duyệt. Chỉ action đã được chấp nhận mới đi vào môi trường:

(ot,st+1,et)=E(a^t,st).(o_t,s_{t+1},e_t) = \mathcal E(\hat a_t,s_t).

Ở đây, oto_t là kết quả trả về, st+1s_{t+1} là trạng thái mới và ete_t là event dùng cho trace hoặc audit. Khi đó, hệ thống không chỉ biết model đã nói gì. Nó biết ai yêu cầu, policy nào đã được áp dụng, tool nào thật sự chạy, trạng thái nào thay đổi và bằng chứng nào được lưu lại.

Sơ đồ vẽ tay harness: model đề xuất delete_record, trạm V đối chiếu principal, policy và trạng thái, rồi cho phép tới môi trường, từ chối bằng lỗi có mã, hoặc chờ người duyệt; môi trường ghi sự kiện vào audit trail
Model đề xuất, runtime phân quyền

Hình trên là hai công thức vừa rồi, vẽ thành đường đi. Model đề xuất delete_record(42); đề xuất ấy dừng ở hình lục giác VV, nơi runtime đối chiếu principal, policy và trạng thái sts_t. Từ đó có ba lối ra. Được cho phép thì action mới chạm tới E\mathcal E, và môi trường trả về đủ ba thứ: kết quả oto_t, trạng thái mới st+1s_{t+1} và một event ete_t ghi vào audit trail. Bị từ chối thì model nhận lại một lỗi có mã, DENIED: scope, chứ không phải một lời trách. Còn lối thứ ba là chờ người duyệt. So với hình đầu tiên của bài, chiếc kéo đã được thay bằng một trạm kiểm soát.

Đó mới là nền của harness.

Kết

Prompt engineering thay đổi cách mô hình được hỏi. Context engineering thay đổi thứ mô hình được nhìn thấy. ReAct tiến thêm một bước: để output của lượt trước cùng kết quả môi trường xây context cho lượt sau. Multi-agent systems tiếp tục tổ chức quá trình ấy thành nhiều vai trò và nhiều lượt bàn giao.

Nhìn từ hiện tại, rất dễ chê những prototype năm 2022 và 2023 là mong manh. Cách đánh giá ấy bỏ qua điều quan trọng nhất: chúng đã nhận ra đúng hình dạng của hệ thống tương lai trước khi các thành phần cần thiết xuất hiện đầy đủ.

ReAct nhìn thấy vòng lặp giữa reasoning, action và observation. Toolformer nhìn thấy nhu cầu dạy model cách gọi API. AutoGPT, BabyAGI, CAMEL, HuggingGPT và các hệ thống multi-agent nhìn thấy khả năng phân rã mục tiêu, chuyên biệt hóa vai trò và phối hợp qua nhiều bước. Tầm nhìn ấy không sai. Kỳ vọng rằng những LLM còn non trẻ, chạy trên parser bằng regex và transcript bằng text, có thể lập tức thực hiện tầm nhìn ấy một cách đáng tin cậy mới là điều đi quá nhanh.

Những năm tiếp theo lần lượt bổ sung các mảnh còn thiếu: model tuân thủ instruction tốt hơn, context dài hơn, function calling, Structured Outputs, tool protocol, state management, observability và policy engine. Mỗi lớp mới không thay thế tầm nhìn agent; nó làm tầm nhìn ấy bớt phụ thuộc vào may mắn.

Ranh giới của một hệ thống trưởng thành vẫn cần được giữ rất rõ:

Model đề xuất. Runtime phân quyền. Tool thực thi. State ghi nhận. Verifier kiểm tra. Audit trail lưu bằng chứng.

Đó không phải lời phản bác các phòng ban ảo. Đó là điều kiện để chúng trở thành những phòng ban thật sự có năng lực trong một hệ thống phần mềm.

ReAct và multi-agent systems đã nhìn thấy tương lai khá sớm. Phần còn thiếu không phải tầm nhìn, mà là những model cùng runtime đủ trưởng thành để gánh nổi tầm nhìn ấy.

Bài tiếp theo sẽ đi tiếp từ đây: harness quản lý tool, quyền hạn, state, retry, rollback và audit trail như thế nào để biến bản phác thảo của ReAct thành một hệ thống có thể vận hành mà không đánh mất khả năng kiểm soát.

  1. Ehud Karpas và cộng sự, MRKL Systems: A modular, neuro-symbolic architecture that combines large language models, external knowledge sources and discrete reasoning, AI21 Labs, 2022. Thiết lập prompt tuning ở mục 3.2.2; so sánh với few-shot ở Phụ lục A. ↩
  2. Shunyu Yao và cộng sự, ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023. Số liệu HotpotQA và FEVER ở Bảng 1. ↩
  3. Timo Schick và cộng sự, Toolformer: Language Models Can Teach Themselves to Use Tools, NeurIPS 2023. ↩
  4. LangChain, notebook custom_llm_agent.ipynb ở phiên bản v0.0.200, tháng 6/2023. ↩
  5. LangChain, commit 7eba828, "Harrison/update regex (#1534)", 9/3/2023 (giờ UTC). ↩
  6. LangChain, AgentExecutor.handle_parsing_errors. Tùy chọn này xuất hiện giữa bản v0.0.150 và v0.0.160. ↩
  7. AutoGPT, issue #623, "Memory Feature seems to not work on gpt3only mode", 9/4/2023. ↩
  8. AutoGPT v0.2.2, promptgenerator.py, phần response_format. ↩
  9. Gemini CLI, issue #4086, 14/7/2025. ↩
  10. Gemini CLI, issue #1531, "[TOOL LOOP] CLI getting stuck in tool loop". Phần tổng kết được thêm vào mô tả issue tháng 12/2025. ↩
  11. Gemini CLI, issue #5165, "Make Gemini Less Apologetic", 30/7/2025. ↩
  12. Gemini CLI, bình luận của nbs trong issue #1531, 27/6/2025. Ảnh được dùng nguyên bản để bình luận, bản quyền thuộc người đăng. ↩
  13. Yohei Nakajima, BabyAGI, tháng 4/2023. ↩
  14. Guohao Li và cộng sự, CAMEL: Communicative Agents for "Mind" Exploration of Large Language Model Society, NeurIPS 2023. ↩
  15. Yongliang Shen và cộng sự, HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face, NeurIPS 2023. ↩
  16. Elliot Kim, Avi Garg, Kenny Peng, Nikhil Garg, Correlated Errors in Large Language Models, ICML 2025. ↩
  17. Sirui Hong và cộng sự, MetaGPT: Meta Programming for A Multi-Agent Collaborative Framework, ICLR 2024. ↩
  18. Chen Qian và cộng sự, ChatDev: Communicative Agents for Software Development, ACL 2024. ↩
  19. Kai Greshake và cộng sự, Not what you've signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection, 2023. ↩
  20. Nelson F. Liu và cộng sự, Lost in the Middle: How Language Models Use Long Contexts, TACL 12 (2024). ↩
  21. OpenAI, Introducing ChatGPT and Whisper APIs, 1/3/2023. ↩
  22. OpenAI, Function calling and other API updates, 13/6/2023. ↩
  23. OpenAI, API reference cho function_call.arguments, bản tháng 7/2023: "the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema". ↩
  24. OpenAI, Introducing Structured Outputs in the API, 6/8/2024. ↩

— espresso.hoagg

↑ Về đầu trang