# Thực hành RAG: khi hai tài liệu của khách nói khác nhau, bắt hệ thống nêu cả hai nguồn

> Trên các benchmark có người gán nhãn, mô hình nào cũng vấp khi ngữ cảnh tự mâu thuẫn, nên câu trả lời có thể chỉ kể một nửa câu chuyện mà người dùng không hề biết.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-rag-tai-lieu-mau-thuan-nhau/

Bộ benchmark WikiContradict có 253 mẫu do người gán nhãn, và trên bộ này mọi mô hình được thử đều khó viết ra câu trả lời phản ánh đúng việc ngữ cảnh đang mâu thuẫn. Chỗ vấp nặng nhất là xung đột ngầm, tức khi hai đoạn văn khác nhau nhưng không đoạn nào nói thẳng ra điều đó.

Ra khỏi phòng lab, đúng loại mâu thuẫn này nằm sẵn trong kho tài liệu của khách. Hai bản quy trình lệch nhau, một file PDF cũ chưa ai xóa, hay một wiki do hai team viết là chuyện thường gặp.

Benchmark MAGIC cho thấy cả mô hình mở lẫn mô hình đóng đều kém phát hiện xung đột giữa các ngữ cảnh, và một hệ thống không nhận ra mâu thuẫn thì khó có thể báo cho người dùng rằng còn một tài liệu khác nói ngược lại.

Năm bước dưới đây nhắm đúng vào chỗ hổng đó: gắn nhãn nguồn cho context, quy ước cách xử lý theo từng kiểu xung đột, viết prompt bắt trích dẫn trước khi trả lời, để trích dẫn ở dạng máy kiểm được, và chấm bằng bộ test của chính khách.

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

Bạn cần một pipeline RAG đang chạy, tức retriever trả về vài đoạn văn cho mỗi câu hỏi, quyền gọi một LLM và Python. Bạn cũng cần khoảng 10 đến 20 câu hỏi thật mà nhân viên của khách hay hỏi. Code trong bài đã được giản lược và không gắn với thư viện cụ thể nào, nên bạn sẽ phải nối nó vào pipeline của mình.

Cả bài dùng chung một ví dụ giả định. Khách là một chuỗi bán lẻ. File `chinh-sach-doi-tra-2025.pdf` ghi khách được đổi hàng trong 30 ngày, còn trang `FAQ cửa hàng` lại ghi 14 ngày. Nhân viên hỏi: "Khách được đổi hàng trong bao lâu?"

## Bước 1: Mỗi đoạn văn phải mang theo tên và ngày của nó

Benchmark MAGIC cũng ghi nhận mô hình thường không chỉ ra được chính xác điểm mâu thuẫn nằm ở nguồn nào. Nếu bạn chỉ dán text trần vào context, mô hình càng không có cách nào để nói "theo tài liệu A". Vì thế, việc đầu tiên là cho mỗi đoạn một mã, một tiêu đề và một ngày cập nhật.

```python
def format_context(chunks):
    parts = []
    for i, c in enumerate(chunks, start=1):
        parts.append(
            f'<doc id="D{i}" title="{c["title"]}" updated="{c["updated"]}">\n'
            f'{c["text"]}\n</doc>'
        )
    return "\n\n".join(parts)
```

**Kiểm tra:** in context của câu hỏi đổi hàng ra màn hình. Bạn phải thấy cả hai tài liệu, mỗi tài liệu có ngày riêng. Nếu chỉ thấy một tài liệu, vấn đề nằm ở retriever chứ chưa phải ở prompt. Còn nếu trường `updated` trống, hãy hỏi khách lấy ngày ở đâu trước khi đi tiếp.

## Bước 2: Mỗi kiểu xung đột cần một cách trả lời riêng

Bài báo DRAGged into Conflicts đưa ra một bảng phân loại các kiểu xung đột tri thức trong RAG, kèm hành vi mong muốn cho từng kiểu. Bảng dưới đây lấy ba kiểu trong số đó mà bạn sẽ phải quy ước với khách:

| Kiểu xung đột | Dấu hiệu ở site khách | Hệ thống nên làm gì |
|---|---|---|
| Thông tin lỗi thời | Hai phiên bản cùng tài liệu, khác ngày | Ưu tiên bản mới, có thể nhắc bản cũ |
| Thông tin sai | Một nguồn có nội dung nhiều khả năng sai hoặc gây hiểu lầm | Bỏ nguồn sai |
| Quan điểm hoặc kết quả khác nhau | Hai team, hai nghiên cứu, hai cách làm đều có lý | Nêu rõ sự bất đồng giữa các nguồn, không chọn một bên |

Ví dụ đổi hàng cho thấy vì sao không thể để mô hình tự đoán. Nếu FAQ chỉ là bản cũ chưa cập nhật thì đây là xung đột lỗi thời, câu trả lời là 30 ngày. Nhưng nếu 14 ngày là chính sách riêng của nhóm hàng điện tử, người dùng cần thấy cả hai con số.

Chỉ khách mới biết tài liệu nào có thẩm quyền. Vì thế, trước khi viết prompt, bạn hãy ngồi với người giữ tài liệu và ghi lại vài quy tắc, chẳng hạn "PDF chính sách luôn thắng FAQ" hay "wiki team A và team B đều hợp lệ".

Trong ví dụ của bài, giả sử khách xác nhận quy tắc sau: "FAQ cửa hàng và PDF chính sách đều còn hiệu lực; 14 ngày áp dụng cho hàng điện tử, 30 ngày cho các nhóm hàng còn lại". Với quy tắc đó, câu hỏi đổi hàng được xếp vào kiểu bất đồng, và câu trả lời phải nêu cả hai tài liệu.

## Bước 3: Bắt mô hình trích nguyên văn trước khi trả lời

Có hai bằng chứng ủng hộ bước này. Anthropic từng thử với Claude 2 và thấy việc yêu cầu mô hình trích đoạn liên quan trước khi trả lời làm độ chính xác tăng từ 0,939 lên 0,961, tức giảm 36% lỗi, đổi lại độ trễ tăng thêm một chút.

Nhóm tác giả DRAGged into Conflicts cũng ghi nhận rằng nhắc mô hình suy luận rõ ràng về xung đột trong tài liệu truy xuất giúp câu trả lời tốt hơn, dù vẫn còn nhiều chỗ để cải thiện.

Gộp hai kỹ thuật đó lại, bạn có một prompt mẫu như sau (đã giản lược):

```text
Dưới đây là các tài liệu, mỗi tài liệu có id, title và ngày updated.

{context}

Câu hỏi: {question}

Làm theo đúng thứ tự:
1. Trong <quotes>, chép nguyên văn mọi câu liên quan, ghi id ở đầu mỗi câu.
2. Trong <conflict>, cho biết các trích dẫn có mâu thuẫn không.
   Nếu có, xếp vào một kiểu: outdated | false | disagreement.
3. Trong <answer>, trả lời theo quy tắc:
   - outdated: dùng tài liệu mới hơn, nói rõ tài liệu cũ ghi khác.
   - false: bỏ tài liệu sai, nói rõ vì sao.
   - disagreement: nêu cả hai bên, mỗi bên kèm id.
Quy tắc thẩm quyền của khách: {customer_rules}
```

**Kiểm tra:** chạy lại câu hỏi đổi hàng, với `{customer_rules}` chứa quy tắc khách vừa xác nhận ở bước 2. Phần `` phải có cả câu chứa D1 và câu chứa D2. Nếu chỉ có một câu, mô hình đã chọn một bên ngay từ bước trích dẫn, và đó là lỗi bạn cần đưa vào bộ test.

## Bước 4: Trích dẫn phải là thứ máy kiểm được

Cho mô hình tự ghi id trong text là đủ để chạy thử, nhưng khó kiểm chứng ở quy mô lớn. Nếu dùng Claude, bạn có thể dùng tính năng Citations. Theo tài liệu của Anthropic, tính năng này trả về chính xác những đoạn văn làm căn cứ cho từng luận điểm, nhờ vậy bạn kiểm chứng được câu trả lời và hiển thị nguồn cho người dùng.

Khi xử lý xung đột, giá trị của nó nằm ở chỗ mỗi vế của câu trả lời gắn với một đoạn văn cụ thể. Giao diện có thể hiện "30 ngày" kèm link tới PDF, "14 ngày" kèm link tới FAQ. Cú pháp gọi API có thể thay đổi, nên bạn hãy đọc trang Citations hiện hành thay vì chép lại từ bài cũ.

**Điểm mấu chốt:** Khi hai tài liệu cãi nhau, việc của hệ thống là cho người dùng thấy cuộc cãi nhau đó.

## Bước 5: Chấm bằng bộ test xung đột của chính khách

CONFLICTS là bộ benchmark đầu tiên có nhãn kiểu xung đột ghi tường minh: 458 truy vấn, mỗi truy vấn kèm kết quả truy xuất và nhãn do chuyên gia gán. Bạn không cần đến 458 câu. Chỉ cần mượn cấu trúc của nó cho 20 câu của khách.

```json
{
  "query": "Khách được đổi hàng trong bao lâu?",
  "docs": ["D1", "D2"],
  "conflict_type": "disagreement",
  "must_cite": ["D1", "D2"]
}
```

Nhãn `disagreement` ở đây đến từ quy tắc khách đã xác nhận ở bước 2, không phải do bạn tự đoán. Nếu một ngày khách nói FAQ chỉ là bản cũ, bạn đổi nhãn thành `outdated` và cập nhật `must_cite` theo đúng quy tắc mới.

```python
import re

def cites_all(answer: str, case: dict) -> bool:
    found = set(re.findall(r"\bD\d+\b", answer))
    return all(doc_id in found for doc_id in case["must_cite"])

score = sum(cites_all(run(c["query"]), c) for c in cases) / len(cases)
```

Hàm `run` là pipeline của bạn. Khi dùng bản giản lược này, có hai điểm cần để ý.

Một là đừng thay regex bằng phép so chuỗi con kiểu `"D1" in answer`. Khi đó "D1" cũng khớp với "D10", và code trên tách từng id ra trước khi so chính là để tránh lỗi này.

Hai là nếu bạn dùng Citations API, id nguồn nằm trong phần trích dẫn của response chứ không nằm trong text. Lúc đó hãy đọc id từ phần trích dẫn thay vì quét câu trả lời.

Hãy chạy bộ test với prompt cũ, rồi với prompt mới, và ghi cả hai con số. Đừng kỳ vọng đạt điểm tuyệt đối: MAGIC cho thấy cả mô hình mở lẫn mô hình đóng đều phát hiện xung đột kém, nhất là khi cần suy luận qua nhiều bước.

Vì vậy, hãy cố ý đưa vào bộ test vài câu mà mâu thuẫn chỉ lộ ra khi ghép hai đoạn lại với nhau.

## Những lỗi khiến công sức trên đổ sông đổ biển

Lỗi thường gặp nhất nằm trước cả mô hình. Bước khử trùng lặp có thể gộp hai đoạn gần giống nhau và vô tình xóa mất bên còn lại, còn top-k quá nhỏ thì chỉ lấy về một phía. Trong cả hai trường hợp, prompt viết hay đến đâu cũng vô ích.

Lỗi thứ hai là coi mọi xung đột như một cuộc tranh luận. Nếu câu trả lời nào cũng kết bằng "tài liệu A nói thế này, tài liệu B nói thế kia", người dùng sẽ bực, nhất là khi bản cũ rõ ràng đã hết hiệu lực. Bảng ở bước 2 tồn tại chính là để tránh chuyện đó.

Lỗi thứ ba là chỉ kiểm tra câu trả lời có nghe hợp lý hay không. Một câu trả lời trôi chảy nhưng chỉ trích một nguồn vẫn là câu trả lời sai đối với một case `disagreement`, và chỉ hàm kiểm tra như `cites_all` mới bắt được lỗi này.

## Danh sách mâu thuẫn là thứ đầu tiên nên mang tới khách

Trong một đợt triển khai thật, bạn dùng kỹ năng này ngay từ tuần đầu. Hãy chạy bộ test xung đột và mang danh sách các cặp tài liệu mâu thuẫn tới gặp khách. Danh sách đó thường hữu ích với họ hơn cả con chatbot, vì nó cho họ biết phải dọn dẹp tài liệu nào.

Khi đọc mô tả công việc FDE hay applied AI, hãy để ý các từ như evals, grounding hay citations. Trong CV, hãy viết cụ thể bạn đã làm gì: dựng bộ test xung đột gồm bao nhiêu câu, đo tỷ lệ câu trả lời trích đủ nguồn trước và sau khi đổi prompt.

Đi phỏng vấn, hãy kể lại ví dụ 30 ngày và 14 ngày, rồi giải thích vì sao bạn không để mô hình tự chọn.

Mô hình sẽ còn mạnh hơn, nhưng khách vẫn sẽ có hai tài liệu nói khác nhau. Người được tin tưởng là người làm cho sự mâu thuẫn đó hiện ra rõ ràng thay vì che nó đi.

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

- Lấy 10 câu hỏi thật của khách và tìm trong kho tài liệu ít nhất 3 cặp đoạn nói khác nhau về cùng một chuyện
- Thêm tiêu đề và ngày cập nhật vào từng đoạn trong context, rồi chạy lại 3 câu hỏi đó để so sánh câu trả lời trước và sau
- Viết hàm kiểm tra câu trả lời có trích đủ các mã tài liệu bắt buộc hay không, rồi đưa nó vào bộ eval

## Nguồn

- [DRAGged into Conflicts: Detecting and Addressing Conflicting Sources in Search-Augmented LLMs](https://arxiv.org/abs/2506.08500)

- [DRAGged into Conflicts: Detecting and Addressing Conflicting Sources in Search-Augmented LLMs](https://arxiv.org/html/2506.08500v1)

- [WikiContradict: A Benchmark for Evaluating LLMs on Real-World Knowledge Conflicts from Wikipedia](https://arxiv.org/abs/2406.13805)

- [MAGIC: A Multi-Hop and Graph-Based Benchmark for Inter-Context Conflicts in Retrieval-Augmented Generation](https://arxiv.org/abs/2507.21544)

- [Citations](https://platform.claude.com/docs/en/build-with-claude/citations)

- [Claude 2.1 / Prompting long context (Anthropic)](https://www.anthropic.com/news/prompting-long-context)
