AI dạng tác nhân trong môi trường production: Vì sao agent của bạn vẫn trả lời dù công cụ đã chết

Phần mềm25 tháng 10, 2026·11 phút đọc

Khi một công cụ bị lỗi, agent AI vẫn có thể tiếp tục chạy và đưa ra câu trả lời đầy tự tin dù không có dữ liệu thực, trong khi bảng giám sát vẫn báo xanh. Bài viết phân tích loại lỗi "âm thầm" này và cách xử lý bằng code, cơ chế từ chối trả lời và giám sát vận hành.

AI dạng tác nhân trong môi trường production: Vì sao agent của bạn vẫn trả lời dù công cụ đã chết

Hãy hình dung một agent trả lời câu hỏi về hóa đơn khách hàng. Nó có một công cụ duy nhất là truy vấn một API nội bộ, và nhiệm vụ cần bốn lần tra cứu. Hai lần đầu chạy trơn tru. Đến lần thứ ba, thông tin xác thực đứng sau API đó hết hạn, và mọi lệnh gọi từ đó trở đi đều trả về lỗi 401.

Điều xảy ra tiếp theo mới là phần đáng lo. Agent gọi lại công cụ, rồi gọi lại lần nữa, mỗi lần chỉnh tham số một chút, cứ như thể API không hài lòng với cách diễn đạt của nó. Một lúc sau, nó từ bỏ công cụ và viết nên một câu trả lời gọn gàng, đầy tự tin bao trọn cả bốn khách hàng. Không một dòng nào trong câu trả lời đó thừa nhận rằng công cụ đã ngừng hoạt động ngay từ lần gọi thứ hai. Lần chạy kết thúc với trạng thái thành công, và bảng giám sát vẫn xanh.

Bài viết này nói về kiểu kết thúc im lặng đó. Nó chẳng giống chút nào những lỗi mà chúng ta thường lên kế hoạch đối phó trong các hệ thống AI dạng tác nhân, và tôi cho rằng chính vì thế mà nó tồn tại lâu đến vậy trong môi trường production.

Một thất bại kết thúc bằng trạng thái thành công

Phần lớn các cuộc thảo luận về agent trong production đều xoay quanh những thất bại ồn ào. Gartner dự báo hơn 40% dự án AI dạng tác nhân sẽ bị hủy trước cuối năm 2027, chủ yếu vì chi phí, giá trị kinh doanh không rõ ràng và kiểm soát rủi ro yếu. Điều đó hợp lý, nhưng đó là những thất bại kiểu gì rồi cũng có người nhận ra, bởi một hóa đơn khổng lồ hay một bài toán kinh doanh thiếu cơ sở sẽ lộ diện trong cuộc họp.

Một thông tin xác thực hết hạn lại là kiểu vấn đề ngược lại. Phần mềm thông thường có sẵn cách xử lý cố định: ném ra lỗi, gọi người trực, rồi dừng. Agent không có bản năng đó. Với một mô hình ngôn ngữ, thông báo lỗi chỉ là thêm một đoạn văn bản trong ngữ cảnh của nó, và nếu chỉ dẫn yêu cầu trả lời câu hỏi của người dùng, nó sẽ tìm cách làm điều đó, có dữ liệu hay không cũng mặc.

Đây là lý do tôi không tin rằng một mô hình thông minh hơn chính là giải pháp. Mô hình thông minh hơn chỉ viết câu trả lời thuyết phục hơn, khiến câu trả lời dựa trên hư không càng khó bị phát hiện. Điều tôi muốn hỏi là hệ thống xung quanh mô hình làm gì khi một công cụ chết, và liệu có thứ gì buộc agent phải dừng trả lời ngay tại thời điểm đó hay không.

Không phải mọi lỗi đều đáng được thử lại

Thật dễ có xu hướng đối xử với mọi lỗi công cụ theo cùng một cách, và đó chính là chỗ mọi thứ bắt đầu hỏng. Timeout, mất kết nối, giới hạn tần suất hay lỗi 503 thường sẽ ổn nếu bạn đợi một chút và thử lại. Nhưng lỗi 401, 403 hay 404 sẽ hỏng y hệt cả trăm lần liên tiếp, và mỗi lần thử lại chỉ đốt token trong khi agent tỏ ra bận rộn.

Còn một nhóm thứ ba mà tôi không giả vờ có thể giải quyết bằng code: lệnh gọi chỉ thành công một nửa, hay hành động không ai muốn lặp lại, ví dụ một khoản thanh toán có thể đã hoặc chưa đi qua. Một lớp bọc không thể phán đoán được điều đó. Điều tốt nhất nó làm được là dừng lại và chuyển vấn đề cho con người.

Mô hình không phân biệt được các nhóm này, bởi với nó chúng đều chỉ là những chuỗi lỗi. Vì vậy tôi tước quyết định đó khỏi tay mô hình. Việc phân loại diễn ra bằng code thuần trước khi mô hình thấy bất cứ thứ gì. Dưới đây là hàm tôi sẽ bắt đầu từ đó.

import logging
import random
import time
import requests

log = logging.getLogger("agent.tools")

# Các lỗi 4xx khác ngoài 429 sẽ không tự khỏi, nên không thử lại
RETRYABLE = {429, 500, 502, 503, 504}
MAX_TRIES = 3

class ToolDown(Exception):
    def __init__(self, status):
        super().__init__(f"tool failed with status {status}")
        self.status = status

def call_api(url, headers, params=None):
    for attempt in range(1, MAX_TRIES + 1):
        try:
            resp = requests.get(url, headers=headers, params=params, timeout=10)
        except (requests.Timeout, requests.ConnectionError):
            status = 0  # không có phản hồi, xử lý như lỗi 5xx chập chờn
        else:
            if resp.ok:
                return resp.json()
            status = resp.status_code
        if (status and status not in RETRYABLE) or attempt == MAX_TRIES:
            raise ToolDown(status)
        # jitter để cả một đội agent không thử lại đồng loạt
        time.sleep(2 ** attempt + random.random())

Vài điểm trong đoạn code đáng xem kỹ. Số lần thử lại có giới hạn cứng, bởi một công cụ chập chờn không được phép biến thành vòng lặp vô hạn. Timeout và mất kết nối cũng được xử lý, vì chúng không hề sinh ra mã trạng thái và rất dễ bị bỏ sót. Phần sleep có thêm chút nhiễu ngẫu nhiên, để một đội agent cùng đập vào một dịch vụ hỏng không thử lại đúng cùng một khoảnh khắc. Và lỗi 401 bị ném ngay từ lần đầu, vì chờ đợi không làm thông tin xác thực tự gia hạn. Có một lỗ hổng tôi cố ý để lại: một phản hồi 200 với body không phải JSON hợp lệ sẽ ném lỗi từ resp.json() và lọt qua lớp bọc, nên hãy bắt ValueError ở đó nếu công cụ của bạn có thể gặp tình huống này.

Một chi tiết nữa: một số server gửi kèm header Retry-After với lỗi 429, và tuân theo nó tốt hơn là chờ theo thời gian cố định. Tuy nhiên header này có thể là một ngày tháng chứ không chỉ là số giây, nên cần phân tích cẩn thận.

Dạy agent nói rằng "tôi không làm được"

Giới hạn số lần thử lại chặn được cơn bão, nhưng agent vẫn có thể mất công cụ rồi viết ra câu trả lời đầy tự tin. Vậy nên giải pháp có hai phần, và phần đầu là những gì mô hình được thấy khi một lệnh gọi thất bại. Lớp bọc dưới đây nằm giữa vòng lặp agent và các công cụ.

def run_tool(run, name, fn, **kwargs):
    """Thứ mà vòng lặp agent gọi thay vì gọi thẳng vào công cụ."""
    try:
        out = fn(**kwargs)
    except ToolDown as err:
        run["failed"].append((name, err.status))
        log.warning("tool %s failed with status %s", name, err.status)
        # mô hình chỉ nhận một dòng này thay vì body lỗi thô
        return f"TOOL_FAILED: {name} returned {err.status}. Do not guess. Tell the user this lookup failed."
    run["succeeded"] += 1
    return out

Mỗi lần chạy bắt đầu với run = {"succeeded": 0, "failed": []}. Mọi thất bại được ghi lại kèm tên công cụ và mã trạng thái, còn mô hình nhận một chỉ dẫn ngắn thay vì body lỗi thô. Đưa cho mô hình một lỗi 401 trần trụi, nó sẽ tự ứng biến, nên tôi đưa cho nó một câu ngắn bảo nó đừng đoán.

Phần thứ hai là một bước kiểm tra ở cuối lần chạy, đặt một câu hỏi thẳng thừng: có công cụ nào thực sự trả về câu trả lời không?

def final_answer(run, draft):
    # một công cụ trả về danh sách rỗng vẫn tính là đã trả lời
    if run["succeeded"] == 0:
        log.error("run ended with no successful tool call: %s", run["failed"])
        return "I could not reach the data needed to answer this, so I am not going to guess."
    return draft

Dòng chú thích ở giữa có mặt vì một sai lầm rất dễ mắc. Phiên bản đầu của bước bảo vệ này hỏi liệu kết quả có "truthy" hay không, nên một công cụ trả lời đúng rằng "không có hóa đơn nào khớp" bằng một danh sách rỗng lại bị coi là thất bại và kích hoạt phản hồi từ chối. Giờ bước bảo vệ đếm mọi lệnh gọi có trả về, kể cả khi kết quả rỗng.

Cảnh báo công bằng: bước bảo vệ này cố ý thô sơ, và nó có một lỗ hổng thật. Chỉ cần một lần thành công trong năm lệnh gọi là đủ để vượt qua, kể cả khi bốn lần thất bại chính là những lần mà câu trả lời phụ thuộc vào. Phiên bản chặt chẽ hơn sẽ nêu rõ mỗi nhiệm vụ cần công cụ nào, ví dụ câu hỏi này bắt buộc get_invoices phải thành công, và từ chối khi chính lệnh gọi đó thất bại. Tôi sẽ triển khai bản thô trước rồi siết lại khi đã biết công cụ nào quan trọng nhất.

Thông điệp từ chối cũng cần được chăm chút. Một câu trả lời nói rằng agent không hoàn thành được việc hữu ích với người dùng hơn nhiều so với một đoạn văn trôi chảy mà hóa ra lại sai. Nó cũng hữu ích cho chính bạn, vì nó biến một thất bại âm thầm thành thứ có thể đếm được.

Làm cho phiên bản im lặng trở nên hữu hình

Đây là lúc khả năng quan sát (observability) chứng minh giá trị, dù uptime không giúp được gì nhiều trong trường hợp này. Một agent có thể nằm sau một API với độ sẵn sàng hoàn hảo mà vẫn tiếp tục trả lời sau khi công cụ của nó hỏng ở mọi lần chạy, nên những con số đáng theo dõi là về việc agent thực sự đã làm gì trong từng lần chạy.

Bản ghi lần chạy ở trên đã chứa phần lớn những gì bạn cần: bao nhiêu lệnh gọi thành công và công cụ nào thất bại với mã trạng thái nào. Hai dòng log còn lại bao phần còn lại, một cảnh báo cho mỗi lệnh gọi thất bại và một lỗi mỗi khi bước bảo vệ từ chối một lần chạy. Từ đó bạn có thể rút ra ba góc nhìn kể được phần lớn câu chuyện: tỷ lệ các lần chạy kết thúc mà không có lấy một lệnh gọi công cụ thành công, công cụ và mã trạng thái nào thất bại nhiều nhất (một cụm 401 xuất hiện đột ngột thường nghĩa là một thông tin xác thực đã hết hạn), và số lần bước bảo vệ phải từ chối một lần chạy.

Con số cuối cùng là thứ tôi sẽ đưa lên bảng giám sát đầu tiên. Nếu nó giữ ở mức 0 suốt nhiều tháng, hoặc công cụ của bạn đáng tin cậy một cách đáng ngạc nhiên, hoặc bước bảo vệ chưa được nối vào đâu cả, và tôi sẽ kiểm tra khả năng thứ hai trước khi ăn mừng. Nếu nó đột ngột tăng, bạn vừa biết được về một thông tin xác thực hết hạn từ log của chính mình thay vì từ một khách hàng. Nếu bạn cũng muốn theo dõi số lần thử lại mỗi lần chạy, hãy thêm một dòng log trong vòng lặp thử lại của call_api, vì số lần thử lại tăng thường là dấu hiệu đầu tiên cho thấy một phụ thuộc đang âm thầm hỏng đi.

Toàn bộ những thứ này gói gọn trong một bản ghi nhỏ, hai dòng log và một quyết định về việc điều gì được tính là sự cố, rẻ hơn nhiều so với việc phát hiện muộn hàng tuần rằng agent của bạn đã và đang trả lời câu hỏi từ hư không.

Việc tôi sẽ làm vào thứ Hai

Nếu bạn vận hành một agent trong production, tôi sẽ bắt đầu từ việc kém hào nhoáng nhất: phân loại lỗi công cụ thành những lỗi đáng thử lại và những lỗi không. Rà qua các mã trạng thái mà công cụ của bạn có thể trả về, quyết định mỗi mã thuộc nhóm nào, và đặt quyết định đó vào code, bên ngoài prompt, nơi mô hình không thể cãi lại. Bất cứ thứ gì cần con người, như một khoản thanh toán có thể đã đi qua, nên dừng lần chạy thay vì bị thử lại.

Sau đó, thêm bước kiểm tra ở cuối lần chạy, kể cả ở dạng thô nhất, và cho agent một cách chấp nhận được để nói rằng nó không làm được việc. Tiếp theo, ghi log thất bại kèm tên công cụ và mã trạng thái để những lần từ chối rơi vào nơi có người sẽ nhìn tới.

Mô hình vẫn thông minh y như trước, và tôi thấy thế là ổn. Một agent tiếp tục trả lời sau khi công cụ hỏng thường không phải lỗi của mô hình, mà là vì chưa ai xây phần hệ thống nhận ra một công cụ đã im tiếng và nói thẳng ra điều đó. Hãy xây phần đó trước, và mô hình thông minh hơn sẽ có một nền tảng vững chắc để đứng lên.

Bài học cốt lõi: đừng bao giờ để mô hình tự quyết định khi nào nên thôi đoán. Trách nhiệm đó phải nằm trong code — nơi nó có thể bị kiểm tra, đếm được và cảnh báo.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗