# Dựng agent phân loại ticket Jira: tự tạo issue khi chắc chắn, chuyển người duyệt khi độ tin cậy thấp

> Khó nhất không phải là bắt model gắn nhãn cho ticket. Khó nhất là quyết định lúc nào code được tự tạo việc trên Jira của khách, và lúc nào phải dừng lại để hỏi người.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-agent-xu-ly-ticket-jira-co-nguoi-duyet/

Thử hình dung một agent tự tạo issue trên Jira của khách, rồi gắn nhầm một sự cố đăng nhập nghiêm trọng thành "feature request" mức thấp. Rất có thể đội hỗ trợ của khách sẽ không bao giờ tin agent đó nữa.

Vì vậy, với bài toán phân loại ticket, câu hỏi khó nhất không phải là "model có gắn nhãn đúng không", mà là "khi nào code được phép tự hành động".

Bài này hướng dẫn bạn dựng một agent nhỏ làm đúng ba việc: phân loại ticket qua một tool có schema chặt, tự tạo issue khi đủ chắc chắn, và chuyển cho người duyệt khi độ tin cậy thấp hoặc khi hành động vượt ranh giới rủi ro.

Khi khách muốn "đưa AI vào quy trình hỗ trợ", đây là một bài toán nhỏ, đo được và đáng làm thử trước tiên.

## Bạn cần chuẩn bị gì?

Bạn cần quyền truy cập Claude API, một instance Jira Cloud thử nghiệm có ít nhất một project, Python, và vài chục ticket cũ đã được gắn nhãn đúng để đối chiếu. Code dưới đây đã được giản lược. Phần xác thực, gửi HTTP và lưu log được thay bằng các hàm giữ chỗ để bạn tự nối vào thư viện quen dùng.

## Bước 1: description của tool chính là bộ quy tắc phân loại

Một tool gồm `name`, `description`, `input_schema` và trường tuỳ chọn `input_examples`. Hãy dồn công sức vào description: nói rõ tool làm gì, khi nào dùng và hành xử ra sao. Anthropic gọi mô tả chi tiết là yếu tố quan trọng nhất quyết định chất lượng của tool.

Với agent phân loại, bạn cần định nghĩa từng nhãn và từng mức ưu tiên ngay trong description, không chỉ liệt kê tên nhãn. Đặt tên tool có tiền tố dịch vụ, như `jira_...`, để model không bị lẫn khi agent sau này dùng thêm hệ thống khác.

```json
{
  "name": "jira_triage_ticket",
  "description": "Phân loại một ticket hỗ trợ trước khi tạo issue Jira. Dùng cho MỌI ticket mới. category: 'access' = không đăng nhập được hoặc không có quyền; 'bug' = tính năng chạy sai so với mô tả; 'billing' = hoá đơn, thanh toán; 'feature_request' = đề nghị tính năng mới. priority: 'critical' = nhiều khách hàng không dùng được sản phẩm; 'high' = một khách bị chặn và không có cách tạm; 'medium' = có cách tạm; 'low' = lỗi thẩm mỹ hoặc câu hỏi. confidence: từ 0 đến 1. evidence: trích ngắn câu trong ticket làm căn cứ.",
  "strict": true,
  "input_schema": {
    "type": "object",
    "properties": {
      "category": {"type": "string", "enum": ["access", "bug", "billing", "feature_request"]},
      "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
      "confidence": {"type": "number", "minimum": 0, "maximum": 1},
      "evidence": {"type": "string"}
    },
    "required": ["category", "priority", "confidence", "evidence"],
    "additionalProperties": false
  }
}
```

Cờ `strict: true` bật kiểm tra schema, nên nhãn trả về luôn nằm trong enum. Trường `evidence` chỉ xin một câu trích ngắn làm căn cứ, không xin suy luận từng bước. Người duyệt cần bằng chứng để đối chiếu nhanh, không cần đọc cả một đoạn lập luận.

**Kiểm tra:** chạy thử năm ticket mẫu. Mỗi kết quả phải có đủ bốn trường, nhãn nằm trong enum và `confidence` nằm trong khoảng từ 0 đến 1.

## Bước 2: ép gọi tool, nhưng chuẩn bị cho lúc không ép được

Tham số `tool_choice` với kiểu `tool` bắt model gọi đúng một tool. Tuy vậy, không phải model hay cấu hình nào cũng hỗ trợ ép gọi tool. Tài liệu của Anthropic còn ghi rõ Sonnet 5.5 trả lỗi 400 khi dùng `any` hoặc `tool`.

```python
# Giản lược: chỉ phần cấu hình liên quan đến tool
request = {
    "tools": [TRIAGE_TOOL],
    "tool_choice": {"type": "tool", "name": "jira_triage_ticket"},  # bỏ dòng này nếu model của bạn trả 400
}
```

Vì thế, hãy kiểm tra model bạn đang dùng trước khi dựa vào việc ép gọi. Nếu không ép được, code phải coi một câu trả lời không có lời gọi tool là tín hiệu cần chuyển cho người, chứ không cố đoán nhãn từ văn bản tự do.

## Bước 3: logic chuyển người phải nằm ngoài model

Đây là bước quan trọng nhất. Đừng để model tự quyết khi nào cần chuyển người, vì nó không làm việc đó một cách nhất quán. Phần kích hoạt phải là code của bạn.

Cũng đừng chỉ dựa vào điểm tin cậy: một bài phân tích trên dev.to về cơ chế leo thang cho agent gọi đó là một sai lầm phổ biến. Hãy thêm điều kiện chuyển người khi hành động dự kiến vượt ranh giới rủi ro, mà ở đây là ticket critical.

```python
THRESHOLD = 0.85          # điểm xuất phát, sẽ hiệu chỉnh ở bước 5
RISKY_PRIORITIES = {"critical"}

def route(triage):
    if triage is None:
        return "human", "no_tool_call"
    if triage["priority"] in RISKY_PRIORITIES:
        return "human", "risk_boundary"
    if triage["confidence"] < THRESHOLD:
        return "human", "low_confidence"
    return "auto", "passed_checks"
```

Thử hình dung hai ticket. Ticket A viết "sau khi đổi mật khẩu, cả ba công ty con không đăng nhập được", và model trả về `access`, `critical`, độ tin cậy 0.93. Ticket B viết "nút xuất file hơi lệch", và model trả về `bug`, `low`, độ tin cậy 0.62.

Nếu chỉ nhìn độ tin cậy, ticket A sẽ đi thẳng vào Jira, dù đó chính là loại ticket khách muốn có người đọc trước. Hàm `route` ở trên chặn cả hai ticket, nhưng vì hai lý do khác nhau. Lý do đó phải được ghi lại, vì bước 5 cần đến nó.

**Điểm mấu chốt:** Model gợi ý nhãn; code của bạn mới quyết định khi nào được tự hành động.

**Kiểm tra:** viết unit test cho cả ba nhánh chuyển người trước khi nối vào Jira.

## Bước 4: tạo issue và ghi lại mọi quyết định

Với Jira Cloud, bạn tạo issue bằng một lệnh POST tới endpoint `/rest/api/3/issue` trên instance của mình. Phản hồi trả về id, key và một endpoint REST của issue vừa tạo. Hãy ghi lại cả ba, cùng với lý do `route()` đưa ra cho từng ticket, kể cả những ticket không tạo issue.

```python
# Giản lược: jira_post(), send_to_reviewer(), audit_log(), audit_update() là hàm giữ chỗ.
# Tên trường trong payload cần đối chiếu với cấu hình project của khách.
def handle(ticket, triage):
    decision, reason = route(triage)
    record = {"ticket_id": ticket["id"], "triage": triage,
              "route": decision, "reason": reason,
              "issue_id": None, "issue_key": None, "issue_url": None,
              "reviewer_category": None, "reviewer_priority": None}
    if decision == "auto":
        payload = {"fields": {
            "project": {"key": "SUP"},
            "summary": ticket["title"],
            "issuetype": {"name": "Task"},
            "labels": [triage["category"], triage["priority"]],
        }}
        resp = jira_post("/rest/api/3/issue", payload)
        record.update(issue_id=resp["id"], issue_key=resp["key"], issue_url=resp["self"])
    else:
        send_to_reviewer(ticket, triage, reason)
    audit_log(record)

def on_review(ticket_id, category, priority):
    # gọi khi người duyệt chốt nhãn cho ticket
    audit_update(ticket_id, reviewer_category=category, reviewer_priority=priority)
```

Khi khách hỏi vì sao một issue SUP-… lại có nhãn này, bạn mở bản ghi ra: key, nhãn, độ tin cậy, câu bằng chứng và lý do định tuyến đều nằm ở đó, nên đối soát chỉ mất vài giây. Hai trường `reviewer_*` chính là nguyên liệu cho bước tiếp theo.

**Kiểm tra:** tạo một issue thật trên project thử nghiệm, mở key vừa nhận được trong Jira và so nhãn với log. Sau đó cho một ticket rơi vào nhánh người duyệt, gọi `on_review` và xác nhận bản ghi đã có nhãn người duyệt chọn.

## Bước 5: ngưỡng không phải con số đặt một lần

Ngưỡng tin cậy là điểm chia việc: quyết định trên ngưỡng được tự xử lý, quyết định dưới ngưỡng cần người can thiệp. Con số 0.85 ở bước 3 chỉ là điểm xuất phát.

Hướng dẫn của Galileo về giám sát agent nhấn mạnh rằng hiệu chỉnh là việc làm liên tục, và tỷ lệ chuyển người mục tiêu phải lấy từ phân phối ticket thật của chính bạn, không phải từ con số chung của ngành.

Log ở bước 4 giúp việc này trở nên cụ thể. Sau một tuần, hãy so `triage` với `reviewer_category` và `reviewer_priority`. Bạn sẽ thấy những nhãn nào model hay sai dù độ tin cậy cao, và những nhãn nào model gần như luôn đúng dù độ tin cậy chỉ ở mức trung bình. Đó là dữ liệu để nâng hoặc hạ `THRESHOLD`, thậm chí đặt ngưỡng riêng cho từng nhãn.

Hãy theo dõi tỷ lệ chuyển người như một chỉ số sức khỏe của hệ thống, tách theo từng giá trị của `reason`. Nếu tỷ lệ `low_confidence` tăng vọt sau khi khách ra mắt sản phẩm mới, nhiều khả năng description của tool đang thiếu một nhãn.

## Những lỗi hay gặp

Lỗi đầu tiên là tách thành quá nhiều tool nhỏ. Khi agent cần thêm việc cập nhật hay bình luận issue, hãy gom các thao tác liên quan vào một tool có tham số `action` để model ít nhầm hơn khi chọn tool.

Lỗi thứ hai là đọc độ tin cậy do model tự báo như một xác suất đã được hiệu chỉnh. Thực ra nó chỉ là một tín hiệu cần đối chiếu với quyết định của người duyệt.

Lỗi thứ ba là giấu lý do chuyển người. Người duyệt cần biết ticket bị chặn vì rủi ro hay vì độ tin cậy thấp, vì hai trường hợp đòi hỏi mức chú ý khác nhau.

## Ở dự án thật với khách, việc này trông ra sao?

Ở chỗ khách, phần code trong bài nhiều khả năng không phải phần khó nhất. Phần khó hơn là ngồi với trưởng nhóm hỗ trợ để viết định nghĩa "critical" của chính họ vào description, và thống nhất ai sẽ là người duyệt.

Nhóm của bạn cũng phải chốt thế nào là ranh giới rủi ro. Việc nên làm đầu tiên là xin 50 ticket cũ đã được người phân loại, rồi đo agent trên đó trước khi chạm vào Jira thật.

Nếu bạn đang muốn chuyển sang vai trò FDE, đây là một dự án portfolio rất tốt. Trong CV, đừng chỉ ghi "dựng agent phân loại ticket". Hãy ghi rằng bạn đã tách logic leo thang khỏi model, đã chọn ngưỡng ra sao, và tỷ lệ chuyển người thay đổi thế nào sau khi hiệu chỉnh.

Khi đọc job description, những cụm từ như "human-in-the-loop", "tool use" hay "integrations với hệ thống của khách" đều đang nói về kỹ năng này.

Một agent biết lúc nào phải dừng lại thường được khách tin nhanh hơn một agent lúc nào cũng tự hành động. Vì thế, hãy dành cho hàm `route()` nhiều thời gian hơn cả phần prompt.

**Thử ngay tuần này:**

- Lấy 50 ticket cũ đã đóng từ một dự án Jira thử nghiệm, viết description cho tool phân loại rồi so nhãn model gắn với nhãn thật.
- Viết hàm route() tách khỏi lời gọi model, kèm unit test cho ba trường hợp: model không gọi tool, ưu tiên critical và độ tin cậy dưới ngưỡng.
- Ghi vào log mọi quyết định của agent, gồm lý do route() trả về, key của issue (nếu có) và nhãn người duyệt chọn, để tuần sau tính tỷ lệ chuyển người.

## Nguồn

- [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)

- [Creating a Jira Cloud issue in a single REST Call](https://www.atlassian.com/blog/developer/creating-a-jira-cloud-issue-in-a-single-rest-call)

- [How to Build Human-in-the-Loop Oversight for Production AI Agents](https://galileo.ai/blog/human-in-the-loop-agent-oversight)

- [Escalation Triggers for LLM Agents: Why Confidence Alone Is Not Enough](https://dev.to/dipbhi/escalation-triggers-for-llm-agents-why-confidence-alone-is-not-enough-3bj)
