# Chạy bộ eval năm lần trước khi báo điểm cho khách hàng

> Dù đặt temperature bằng 0, chạy lại cùng một bộ eval vẫn có thể cho điểm khác. Bài này hướng dẫn đo độ chênh đó và báo cho khách một khoảng điểm có cơ sở thống kê.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-do-nhieu-eval-chay-lai-nhieu-lan-khoang-tin-cay/

Thử hình dung bạn chạy bộ eval 200 câu cho khách hàng 5 lần liên tiếp, không đổi một dòng code nào. Năm lần cho ra 81%, 84%, 79%, 83% và 82%. Nếu chỉ chạy một lần và vô tình gặp lần ra 79%, có thể bạn đã báo khách rằng hệ thống "chưa đạt 80%".

Những con số trên là giả định, nhưng hiện tượng thì có thật. Nhóm kỹ sư của Indeed ghi nhận rằng ngay cả ở temperature 0, sai khác về phần cứng, cách batching và hành vi API vẫn có thể khiến cùng một input cho ra output khác nhau. Một lần chạy chỉ là một lần lấy mẫu, chưa đủ để coi là kết quả đo.

Với FDE, đây là vấn đề về uy tín. Khách hàng sẽ ra quyết định go-live hay mua thêm dựa trên con số bạn báo. Bài này đi qua sáu bước, làm được trên laptop trong một buổi chiều: chạy bộ eval năm lần, đo độ nhiễu, rồi báo điểm kèm khoảng tin cậy.

## Bạn sẽ làm ra gì, và cần chuẩn bị gì?

Kết quả cuối cùng là một script Python thuần, chỉ dùng thư viện chuẩn. Script đọc kết quả chạy lặp và in ra ba thứ: điểm trung bình kèm khoảng tin cậy 95%, khoảng tin cậy tính bằng cluster bootstrap, và hiệu số theo cặp giữa mô hình mới với baseline.

Bạn cần một bộ eval đã có, mỗi câu được chấm 0 hoặc 1, hoặc một điểm số liên tục. Kèm theo đó là một hàm gọi hệ thống của bạn và chỗ lưu kết quả dạng JSONL. Code dưới đây là bản rút gọn để minh họa, nên bạn cần ghép vào harness thật của mình.

## Bước 1–2: giữ cố định mọi thứ, rồi chạy lặp

OneUptime khuyên nên giữ cố định mọi thứ đo được độc lập, và chỉ lặp lại phần ngẫu nhiên. Cụ thể là khóa phiên bản prompt, bộ dữ liệu, phiên bản mô hình và grader. Nếu đổi grader giữa hai lần chạy, bạn đang đo hai thứ cùng lúc và không còn tách được độ nhiễu.

Tiếp theo là chạy lặp. Anthropic, trong hướng dẫn thống kê cho eval, khuyên lấy mẫu lại câu trả lời từ cùng một mô hình nhiều lần để giảm phương sai. Indeed gợi ý chọn k=3 hoặc k=5 lần chạy cho mỗi input nếu ngân sách cho phép.

```python
import json
# run_eval là hàm của bạn: gọi hệ thống, chấm điểm, trả về 0/1
with open("runs.jsonl", "w") as f:
    for run in range(5):
        for q in questions:
            score = run_eval(q)
            f.write(json.dumps({"qid": q["id"], "run": run, "score": score}) + "\n")
```

**Kiểm tra:** file phải có đúng N × 5 dòng, và mỗi `qid` xuất hiện đúng 5 lần. Thiếu dòng thường là dấu hiệu có lỗi timeout bị nuốt mất. Lỗi kiểu này làm điểm lệch đi mà không ai nhận ra.

## Bước 3: vì sao 1.000 dòng không phải 1.000 quan sát?

Đây là lỗi phổ biến nhất. Theo Indeed, dữ liệu chạy lặp có cấu trúc gồm N input độc lập, mỗi input có k lần chạy tương quan với nhau. Năm câu trả lời cho cùng một câu hỏi khó thường cùng sai. Nếu coi chúng là năm quan sát độc lập, khoảng tin cậy sẽ hẹp hơn thực tế.

Cách xử lý đơn giản nhất là lấy trung bình theo từng câu trước, rồi tính sai số trên N giá trị trung bình đó. Anthropic dùng công thức 1,96 × SEM: cộng và trừ đại lượng này vào điểm trung bình để có khoảng tin cậy 95%.

```python
import json, statistics
from collections import defaultdict

by_q = defaultdict(list)
for line in open("runs.jsonl"):
    r = json.loads(line)
    by_q[r["qid"]].append(r["score"])

q_means = [statistics.mean(v) for v in by_q.values()]
mean = statistics.mean(q_means)
sem = statistics.stdev(q_means) / len(q_means) ** 0.5
print(f"{mean:.3f}  CI95: {mean-1.96*sem:.3f} – {mean+1.96*sem:.3f}")
```

Quay lại ví dụ ở đầu bài. Trung bình của năm điểm theo lần chạy là 81,8%. Vì câu nào cũng chạy đủ 5 lần, con số này trùng với trung bình của 200 giá trị trung bình theo câu mà script tính ra.

Độ chênh 79–84% giữa các lần chạy và SEM đo hai thứ khác nhau. Độ chênh cho biết một lần chạy đơn lẻ dao động bao nhiêu; SEM cho biết bản thân điểm trung bình 81,8% chắc chắn đến đâu, khi 200 câu cũng chỉ là một mẫu trong vô số câu khách có thể hỏi.

Vì thế SEM được tính trên 200 giá trị trung bình theo câu, đúng như dòng `sem` trong code, chứ không tính trên 5 điểm theo lần chạy. Giả sử SEM tính theo cách đó là 1,5 điểm phần trăm. Khi đó 1,96 × 1,5 = 2,94, và khoảng tin cậy 95% là từ 78,86% đến 84,74%.

Kết quả này rõ ràng hơn nhiều so với một con số 79% hay 84% đứng một mình. Trường hợp nhiều câu dùng chung một đoạn văn, như bộ đọc hiểu với năm câu hỏi trên cùng một tài liệu, Anthropic khuyên gom cụm theo đơn vị ngẫu nhiên hóa, chẳng hạn theo đoạn văn. Khi đó bạn thay `qid` bằng `passage_id` khi nhóm.

## Bước 4: kiểm tra chéo bằng cluster bootstrap

Công thức 1,96 × SEM là bản xấp xỉ. Khi bộ eval nhỏ hoặc điểm dồn sát 0% hay 100%, bạn nên kiểm tra chéo bằng bootstrap. Indeed đã thử năm chiến lược bootstrap và thấy cách resample nguyên cụm input đạt đúng độ phủ danh nghĩa trong mô phỏng, đồng thời không bị rộng thừa như các cách còn lại.

"Độ phủ danh nghĩa" ở đây nghĩa là một khoảng gọi là 95% thì thực sự chứa giá trị thật khoảng 95% số lần. Hàm dưới đây nhận `by_q` từ bước 3, tức dict theo `qid` với danh sách điểm các lần chạy.

```python
import random, statistics

def cluster_bootstrap(by_q, B=2000, seed=0):
    rng = random.Random(seed)
    keys = list(by_q)
    stats = []
    for _ in range(B):
        sample = [rng.choice(keys) for _ in keys]  # rút nguyên câu, kèm cả 5 lần chạy
        stats.append(statistics.mean(statistics.mean(by_q[k]) for k in sample))
    stats.sort()
    return stats[int(0.025 * B)], stats[int(0.975 * B)]
```

**Kiểm tra:** khoảng từ bootstrap nên gần với khoảng tính ở bước 3. Nếu hai kết quả lệch nhau nhiều, hãy xem lại cách nhóm cụm trước khi báo cho ai.

## Bước 5: mô hình mới có tốt hơn thật không?

Khách hàng hiếm khi hỏi "điểm là bao nhiêu". Họ thường hỏi "bản mới có tốt hơn bản cũ không". Anthropic chỉ ra rằng khi so hai mô hình trên cùng một bộ câu, ta loại được phương sai đến từ độ khó của câu hỏi.

Lúc đó chỉ còn phương sai của câu trả lời. OneUptime cũng khuyên dùng hiệu số theo cặp, vì hai hệ thống đã gặp cùng các ca.

Trong đoạn code dưới, `cand` và `base` là hai dict theo `qid`, mỗi giá trị là danh sách điểm các lần chạy, nạp từ file kết quả của mô hình mới và baseline đúng như cách tạo `by_q` ở bước 3.

```python
diffs = [statistics.mean(cand[q]) - statistics.mean(base[q]) for q in base]
d = statistics.mean(diffs)
se = statistics.stdev(diffs) / len(diffs) ** 0.5
print(f"Δ = {d:.3f}  CI95: {d-1.96*se:.3f} – {d+1.96*se:.3f}")
```

Cách đọc kết quả rất đơn giản. Nếu khoảng tin cậy chứa số 0, bạn chưa có bằng chứng rằng bản mới tốt hơn. Câu nên nói với khách là "chưa phân biệt được". Đừng nói "tăng 1,2 điểm".

**Điểm mấu chốt:** Một điểm eval không kèm khoảng tin cậy chỉ là một lần lấy mẫu, chưa phải kết quả đo.

## Bước 6: đặt ngưỡng sau, không đặt trước

Nhiều đội đặt cổng CI kiểu "dưới 80% thì chặn release" từ ngày đầu. OneUptime cho rằng ngưỡng chỉ nên đặt sau khi đã có nghiên cứu về phương sai. Ở ví dụ trên, độ nhiễu khiến điểm dao động từ 79% đến 84%. Một ngưỡng 80% đặt khi chưa đo nhiễu sẽ chặn release một cách ngẫu nhiên.

Nếu khoảng tin cậy vẫn quá rộng thì nên làm gì? Theo Indeed, chạy thêm lần cho mỗi input giúp giảm nhiễu bên trong input và thường rẻ hơn, nhưng hiệu quả có giới hạn.

Ngược lại, thêm input mới thì khoảng tin cậy tiếp tục hẹp lại. Anthropic cũng hướng dẫn áp dụng power analysis để tính trước số câu cần có. Vì vậy, viết thêm câu hỏi đáng giá hơn chạy lần thứ sáu.

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

Lỗi dễ mắc nhất là tính SEM trên toàn bộ N × k dòng như thể chúng độc lập, khiến khoảng tin cậy trông chắc chắn hơn thực tế. Kế đến là để grader, prompt hoặc dữ liệu thay đổi giữa các lần chạy.

Cũng hay gặp là so hai mô hình bằng hai khoảng tin cậy riêng lẻ thay vì hiệu số theo cặp. Cách đó bỏ phí lợi thế của việc hai bên chạy trên cùng bộ câu.

## Kỹ năng này xuất hiện thế nào ở chỗ khách hàng?

Trong buổi review với khách, con số nên báo có dạng: "81,8%, khoảng tin cậy 95% từ 78,9% đến 84,7%, trên 200 câu × 5 lần chạy". Câu này cho khách biết nên tin con số đến mức nào. Nó cũng chặn trước tình huống tuần sau điểm "tụt" 3 điểm và bạn phải giải thích rằng đó chỉ là nhiễu.

Nếu bạn đang nhắm tới vị trí FDE, hãy để ý những JD nhắc đến "evaluation", "eval harness" hoặc "model quality". Trên CV, đừng chỉ ghi "xây bộ eval". Một dòng cụ thể hơn có thể là: "Chạy bộ eval 200 câu × 5 lần, báo điểm kèm khoảng tin cậy 95% và đặt lại ngưỡng chặn release dựa trên độ nhiễu đo được."

Ai cũng chạy được bộ eval. Điều khách hàng nhớ là ai báo được con số mà tuần sau không phải đính chính.

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

- Lấy bộ eval đang dùng, giữ nguyên prompt, dữ liệu và grader, chạy 5 lần, rồi ghi lại điểm thấp nhất và cao nhất giữa các lần.
- Thêm vào báo cáo eval tiếp theo một dòng theo mẫu: điểm trung bình, khoảng tin cậy 95%, số câu và số lần chạy.
- Rà các ngưỡng CI đang chặn release. Ngưỡng nào được đặt khi chưa đo độ nhiễu thì đánh dấu để xem lại.

## Nguồn

- [A statistical approach to model evaluations](https://www.anthropic.com/research/statistical-approach-to-model-evals)

- [Bootstrap Confidence Intervals for LLM Evaluation](https://engineering.indeedblog.com/blog/2026/07/bootstrap-confidence-intervals-for-llm-evaluation/)

- [Why LLM Eval Scores Change: Measure Variance Before Setting Thresholds](https://oneuptime.com/blog/post/2026-08-31-measure-llm-eval-variance-thresholds/view)
