Kiến Trúc Đằng Sau Một Hệ Thống Eval Vòng Lặp Kín Cho Agent Đa Phương Thức
| Một kiến trúc tham chiếu cho bất kỳ agent kiểu "sinh ra rồi tự chấm" (generate-then-judge) nào ở quy mô lớn — minh họa qua pipeline xử lý ảnh sản phẩm thật của Uber Eats, nhưng được thiết kế để lắp vào video, audio, hay text đều được.

Phần lớn bài viết về eval chỉ dừng ở mức "chạy agent, chấm điểm output, rồi log lại đâu đó." Điều thực sự đáng chú ý ở hệ thống mà Uber Eats xây để nâng cấp nội dung của merchant là: phần eval không phải một tiến trình phụ gắn thêm vào agent — nó là một subsystem hạng nhất, có đầy đủ thành phần riêng: một service gán nhãn, một benchmark runner, một diagnoser, một config registry, và một đường rollback. Cái agent thực sự sinh ra nội dung có khi chỉ chiếm một phần ba cả hệ thống. Phần còn lại là bộ máy giữ cho nó luôn "trung thực."
Trường hợp công khai của Uber là ảnh cụ thể — họ đã triển khai tính năng nâng cấp ảnh bằng AI trong bộ công cụ cho merchant, cùng với mô tả menu do AI viết và tóm tắt review. Nhưng không có phần nào trong kiến trúc dưới đây thực sự chỉ dành riêng cho ảnh. Đổi bộ sinh nội dung (generator) thành một model dựng lại video, làm sạch/dub audio, hay viết lại văn bản, thì mọi thành phần — router, vòng lặp retry có giới hạn, golden dataset, subsystem sửa lệch (drift-correction) — vẫn giữ nguyên hình dạng.
Thứ duy nhất thay đổi theo từng loại nội dung là những gì nằm bên trong hai khối "generate" và "judge."
Đó chính là luận điểm của bài viết này: đây là một kiến trúc tham chiếu cho bất kỳ hệ thống sinh-rồi-tự-chấm nào vận hành ở quy mô lớn, và ảnh món ăn chỉ tình cờ là ví dụ công khai được tài liệu hóa đầy đủ nhất.
Hệ thống, nhìn theo thành phần, không phải theo câu chuyện
Bỏ qua phần kể chuyện, pipeline gồm bốn service nối với nhau qua một log dùng chung và một config store dùng chung:
- Routing service — bộ phân loại + chấm theo rubric trên bất kỳ loại input nào (hoặc kết hợp nhiều loại) mà agent xử lý. Quyết định xử lý-hay-bỏ qua.
- Generation service — một bộ sinh (tạo ra ứng viên output) được bọc trong một vòng lặp retry có QA gác cổng, giới hạn bởi số lần lặp tối đa.
- Publish-gate service — một lượt QA cuối cùng, toàn diện hơn, cộng thêm kiểm tra chính sách, độc lập với QA bên trong vòng lặp generation.
- Eval/tuning subsystem — hoàn toàn không nằm trong đường xử lý request thời gian thực. Nó chạy offline và bán liên tục, theo dõi ba service còn lại để phát hiện lệch (drift) và viết lại config của chúng.
Đường xử lý runtime (routing → generation → publish gate) và eval subsystem tách biệt về mặt kiến trúc, nhưng được nối với nhau qua hai tài nguyên dùng chung: một event log phẳng, thống nhất mà mọi service runtime đều ghi vào, và một config/prompt store mà các service runtime đọc còn eval subsystem thì ghi. Sự tách biệt đó là lý do eval subsystem có thể cập nhật hành vi của agent mà không cần redeploy đường runtime — đó là một lượt đẩy config, không phải một lượt đổi code.

Cùng một khung xương, bốn loại nội dung khác nhau
Trước khi đi sâu vào từng thành phần, nên nói rõ luôn phần tổng quát hóa này, vì rất dễ đọc "generation service" rồi ngầm hiểu chỉ là "bộ chỉnh sửa ảnh." Đây là cùng một khung xương, lắp vào bốn loại nội dung — chỉ hai bước tô đậm là khác nhau theo từng loại, mọi thứ còn lại trong pipeline giữ nguyên:

Mọi thứ nằm sau judge() — quyết định routing, vòng lặp retry-kèm-feedback, pass@K, golden dataset, diagnoser, prompt optimizer reflect/synthesize — đều được viết một lần và dùng lại cho cả bốn hàng. Đó chính là đòn bẩy kỹ thuật thực sự của cách thiết kế này: bạn không xây bốn hệ thống, bạn xây một hệ thống với bốn cặp generate()/judge() có thể cắm-rút.
Thành phần 1: routing service
Về mặt chức năng, đây là một bộ phân loại với hợp đồng structured-output, không phải một lượt gọi model tự do. Input: nội dung ứng viên cộng với một blob metadata/mô tả văn bản (danh mục, ngữ cảnh). Output: một object có cấu trúc — mô tả của model về những gì nó thấy hoặc nghe hoặc đọc, cộng với một pass/fail chấm theo rubric — mà một hàm router xác định (deterministic) sẽ tiêu thụ để đưa ra một trong hai (hoặc, ở setup nâng cao hơn, N) quyết định định tuyến.

Thành phần 2: generation service như một vòng lặp retry có giới hạn
Đây là phần dễ làm sai nhất nếu bạn thiết kế nó như một lượt gọi model đơn lẻ thay vì một vòng lặp có điều kiện dừng rõ ràng. Hình dạng thực tế, không phụ thuộc loại nội dung:
generate_prompt(input, mô_tả, chỉ_thị_từ_routing) → gen_prompt
generate(input, gen_prompt) → candidate_output
qa_gate(input, candidate_output) → {pass, feedback}
Nếu qa_gate fail, feedback được gắn thêm vào input của lượt gọi tiếp theo và vòng lặp chạy lại — tối đa K lần. Nếu không bao giờ pass trong K lần lặp, service trả về "no-op, giữ nguyên bản gốc" thay vì ép publish một kết quả nửa vời. Hành vi dừng đó là một quyết định thiết kế đáng nói riêng: điểm thất bại của một vòng lặp retry không giới hạn trước một QA gate không chỉ là chi phí, mà là bộ sinh có thể học cách "làm hài lòng" gate bằng những cách chẳng liên quan gì đến chất lượng thật — từ nôm na cho việc này là reward hacking, và nó xuất hiện trong các vòng lặp generate-rồi-chấm lặp lại ngay cả khi không có tín hiệu huấn luyện RL tường minh, chỉ đơn thuần từ việc bộ sinh điều kiện hóa theo feedback của gate qua từng lượt lặp.

Metric cho thành phần này là pass@K — tỷ lệ pass theo số lần lặp — đây là tín hiệu trung thực hơn một tỷ lệ pass phẳng, vì nó cho biết vòng lặp feedback có thực sự hữu ích không (pass@K nên tăng theo K) hay các lần retry chỉ là nhiễu (pass@K phẳng theo K nghĩa là feedback chẳng dạy được gì cho bộ sinh cả).
Thành phần 3: publish gate, và vì sao nó không thừa so với QA trong vòng lặp
Về kiến trúc, đây là một QA service thứ hai, độc lập, đặt sau vòng lặp generation, kiểm tra tuân thủ chính sách cộng thêm một lượt kiểm tra chất lượng toàn diện hơn.

Lý do nó là một thành phần riêng thay vì một ngưỡng chặt hơn bên trong qa_gate của vòng lặp là phòng thủ theo lớp (defense in depth): các lớp độc lập với các kiểu thất bại khác nhau sẽ bắt được những thứ khác nhau, và không lớp nào cần phải hoàn hảo.
Đây cũng chính là lý do người ta tách riêng bộ integration test khỏi unit test, hay tách riêng linter khỏi type checker — mỗi lớp được phép có lỗ hổng miễn là các lỗ hổng đó không trùng nhau.
Thành phần 4: bootstrapping harness offline
Trước khi bất kỳ thành phần runtime nào ra mắt phiên bản đầu tiên, có một pipeline riêng, chạy một lần (hoặc chạy lại định kỳ): lấy một mẫu phân tầng của nội dung (theo bất kỳ chiều nào quan trọng với lĩnh vực — địa lý, danh mục, cấp độ chất lượng), gửi cho người gán nhãn theo một rubric cố định, khách quan (được thiết kế có chủ đích để giảm thiểu độ lệch giữa các người gán nhãn), rồi lưu kết quả thành một golden dataset.
Mọi thành phần runtime đều được benchmark trên bộ dữ liệu này trước khi ra mắt, và mọi lượt auto-tune sau đó cũng phải được xác thực lại trên cùng bộ dữ liệu trước khi một config mới được phép lên production.

Thành phần 5: drift-correction subsystem — phần thực sự mới mẻ

Đây là phần biến một agent được tune một lần thành một hệ thống luôn đúng theo thời gian, và nên được mô tả như một service topology riêng thay vì một bước "auto-tune" đơn lẻ:
- Một sampler lấy traffic production theo nhịp cố định và gửi nó cho cùng pipeline gán nhãn của con người được dùng cho golden dataset.
- Một comparator so sánh output của agent với nhãn người mới nhất và chỉ ra các điểm sai khác.
- Một diagnoser tiêu thụ các điểm sai khác đó và làm phần việc khó thực sự: quy trách nhiệm một điểm sai khác về một thành phần thượng nguồn cụ thể (routing, generation, hay publish gate), vì một thất bại mà khách hàng nhìn thấy có thể bắt nguồn từ nhiều bước trước điểm nó bị phát hiện.
- Một prompt optimizer, tự nó là hai sub-agent nối tiếp — một tầng reflect gom cụm các điểm sai khác, loại nhiễu, và rút ra một pattern có tính hệ thống thay vì phản ứng theo từng case riêng lẻ, và một tầng synthesize lấy pattern đó cộng với config hiện tại của thành phần rồi tạo ra một config ứng viên mới.
- Một benchmark runner xác thực lại config mới trên golden dataset (thành phần 4) trước khi bất cứ thứ gì được ra mắt.
- Một agent registry lưu các phiên bản config; các service runtime lấy phiên bản mới nhất đã pass vào lượt request tiếp theo của chúng, và rollback chỉ đơn giản là trỏ registry về lại một phiên bản trước đó.
Điểm đáng rút ra về kiến trúc ở đây: không thành phần riêng lẻ nào (sampler, comparator, diagnoser, optimizer, benchmark runner, registry) mới mẻ nếu xét riêng — chúng đều là các thành phần MLOps/eval-infra tiêu chuẩn. Điều được cấu trúc có chủ đích là toàn bộ chuỗi chạy chỉ-bằng-config, không cần deploy code để thay đổi hành vi agent, và không cần con người để chạy một chu kỳ — con người chỉ tham gia ở bước gán nhãn, đó là một thao tác dữ liệu, không phải một cổng quyết định.
So với phần còn lại của lĩnh vực eval-infra
Không ý tưởng riêng lẻ nào ở đây thực sự lạ lẫm nếu bạn theo dõi mảng multimodal-agent-evals rộng hơn, và điều đó hữu ích để đánh giá xem kiến trúc này đến đâu là đặc thù của Uber, đến đâu là một pattern đáng tái sử dụng cho bất kỳ loại nội dung nào.
Drift detection như một kỷ luật đã khá thiết lập trong các hệ thống LLM production nói chung, và tài liệu hướng dẫn vận hành cho vòng đời gen-AI của AWS trình bày về cơ bản cùng hình dạng hai lớp mà hệ thống này dùng: bắt một lệch thống kê trước (trong phiên bản của AWS, so sánh phân bố embedding của các prompt production với một baseline ổn định), rồi đưa mẫu bị gắn cờ đó cho thứ gì đó có thể giải thích tại sao nó lệch — AWS gợi ý một bước phân loại kiểu LLM-as-judge, về chức năng chính là công việc mà diagnoser ở trên đang làm, chỉ khác là khoanh vùng theo "thành phần nào" thay vì "chủ đề nào." Hình dạng hai lớp đó — một trigger thống kê rẻ, rồi một bước thông minh hơn để giải thích trigger — xuất hiện đủ nhiều trong các bài viết độc lập về giám sát LLM production để đáng được xem là gần với một pattern mặc định, không phải một phát minh riêng của một công ty.
Phần đặc thù cho đa phương thức thì ít được giải quyết tốt hơn, nhưng cũng không chỉ riêng cho một loại nội dung nào. Đội Dynamics 365 của Microsoft gặp đúng vấn đề gốc rễ này khi xây một điểm số đánh giá cho các agent trung tâm chăm sóc khách hàng trải dài voice, text, và visual: một con số chất lượng tổng hợp duy nhất che mất chỗ nào một agent đa phương thức thực sự thất bại, nên họ tách điểm số thành các chiều hiểu-suy luận-phản hồi riêng biệt, cụ thể vì, theo lời họ, sự khác biệt giữa các chiều đó "phân biệt được các kiểu thất bại... điều mà một metric đơn lẻ không làm được." Đó cũng chính là lý do bất kỳ hệ thống sinh-rồi-chấm nào cũng cần các trục eval phù hợp với từng loại nội dung — độ trung thực và tính đầy đủ cho output hình ảnh, tính chính xác và đúng giọng văn cho text, tính liên tục cho video, độ dễ nghe cho audio — cộng thêm một trục chất lượng chung "có tốt không." Một điểm chất lượng đơn lẻ, hay một eval harness chỉ xây cho một loại nội dung, sẽ không có chỗ riêng để bắt kiểu thất bại đặc thù của loại nội dung mà nó không được thiết kế cho.
Nửa còn lại — reflect-rồi-synthesize như một prompt optimizer hai tầng — cũng là một pattern quen thuộc từ tài liệu tối ưu hóa prompt rộng hơn: tách bước rút ra insight tổng quát hóa được từ các thất bại (reflect) khỏi bước biến insight đó thành một thay đổi cụ thể (synthesize), thay vì yêu cầu một lượt gọi model làm cả hai việc cùng lúc. Việc tách hai bước này giữ cho bước "insight" có thể tái sử dụng qua nhiều lần sửa nhỏ, thay vì phải suy ra lại một lý do mới, có khi không nhất quán, mỗi lần config bị viết lại. Không có gì trong cách tách này là đặc thù cho một loại nội dung cả — cùng hình dạng reflect/synthesize áp dụng được dù các điểm sai khác đang được gom cụm là về độ trung thực của ảnh hay tính chính xác của văn bản.
Phần chứng minh vì sao guardrail thực sự cần thiết
Đáng để nối lại phần này với kết quả sản phẩm thực tế, vì kiến trúc chỉ thực sự có ý nghĩa khi đặt trong bối cảnh đó: trường hợp của Uber đã ra mắt như một tính năng thật, hướng đến người dùng, và phản ứng công chúng chia đúng theo ranh giới mà hệ thống này được thiết kế để xử lý.
Thông báo của chính bộ phận công cụ merchant của Uber cho biết phần lớn merchant dùng công cụ AI của họ — 78%, theo số liệu của chính họ — thấy hiệu quả. Cùng lúc đó, các bài đưa tin về đợt ra mắt lại khá hoài nghi, có nơi thẳng thừng gọi ảnh món ăn có bàn tay AI là "slop," và nêu đúng mối lo về niềm tin mà các bước kiểm tra độ trung thực của hệ thống này tồn tại để ngăn chặn — rằng nội dung được sinh ra có thể không đại diện đúng cho thứ thực sự được giao.
Cả hai điều đó cùng đúng một lúc chính là cái căng thẳng mà kiến trúc ở trên được xây để quản lý, bất kể đang bảo vệ loại nội dung nào: các guardrail không phải là sự thận trọng lý thuyết, chúng là phản ứng trực tiếp trước một vấn đề niềm tin đã được ghi nhận công khai với nội dung do AI sinh ra nói chung.
Những điều có thể tái sử dụng, như các quyết định kiến trúc
- Tách đường runtime khỏi eval subsystem, chỉ nối với nhau qua một log và một config store. Đây là điều cho phép việc tune diễn ra mà không cần redeploy, và là quyết định duy nhất khiến "học liên tục" trở nên khả thi thay vì một dự án retrain thủ công, định kỳ.
- Cho mọi vòng lặp generate-rồi-chấm một điều kiện dừng rõ ràng với một lối thoát no-op, không chỉ là một số lần retry tối đa. "Bỏ cuộc và giữ bản gốc" là một kết quả hợp lệ, có chủ đích — không phải một trạng thái thất bại — và cần là một nhánh hạng nhất trong state machine, không phải một chi tiết thêm vào sau.
- Xếp chồng các lớp QA độc lập thay vì cố làm cho một lớp hoàn hảo. Hai lớp kiểm tra không hoàn hảo, được thiết kế độc lập, bắt được nhiều hơn một lớp được tune kỹ càng, vì lỗ hổng của chúng hiếm khi trùng nhau.
- Tách "tìm ra pattern" khỏi "viết bản sửa" trong bất kỳ vòng lặp auto-tune nào. Một tầng reflect và một tầng synthesize là hai lượt gọi riêng biệt sẽ cho ra các bản sửa nhất quán, tái sử dụng được hơn là một lượt gọi vừa chẩn đoán vừa vá cùng lúc.
- Một tập tham chiếu cố định, được gán nhãn bởi con người là thứ giữ cho một hệ thống tự tune không lệch dần theo chính output gần đây của nó. Mọi thay đổi config tự động nên được chặn lại bởi một bộ dữ liệu không đổi, không phải bởi vài mẫu production gần nhất trông như thế nào.
- Thiết kế cặp
generate()/judge()là bề mặt duy nhất đặc thù theo loại nội dung trong cả hệ thống. Mọi thứ khác — eval của routing, vòng lặp retry, golden dataset, drift correction, registry — nên được viết một lần và dùng lại dù agent đang chỉnh ảnh, dựng video, làm sạch audio, hay viết lại văn bản.
Nguồn
- Building Closed-Loop Evals for a Multimodal Agent at Scale
- Smarter Tools for Restaurants to Help Them Thrive — Uber Newsroom
- Uber Eats Will Use AI To Make Restaurant Food Photos Look Better Than They Are — Yahoo Tech
- Uber Eats is stuffing AI slop into your meal — Engadget
- AI and Food Delivery Photos: Companies and Customers Court Controversy — Today.com
- Detecting drift in production applications — AWS Prescriptive Guidance
- Multimodal Agent Score: A New Standard for Evaluating AI Agents — Microsoft Dynamics 365 Blog
- Demystifying evals for AI agents — Anthropic Engineering
All Rights Reserved