Dokumentasi teknis yang dihasilkan secara otomatis oleh Large Language Models (LLM) sering kali mengandung halusinasi sintaksis, repetisi pola token, dan minimnya variasi struktur kalimat. Tanpa validasi otomatis, teks sintetis berkualitas rendah ini mencemari repositori kode. Solusinya adalah menerapkan CI linting konten AI dengan menganalisis pola statistik teks seperti N-gram repetitif, perkiraan perplexity, dan burstiness sebelum pull request di-merge.

Arsitektur Deteksi Pola Teks Berbasis Pangram

Pangram mengidentifikasi output AI dengan mengeksploitasi karakteristik deterministik LLM: kecenderungan memilih token dengan probabilitas kumulatif tertinggi. Teks alami manusia memiliki variasi panjang kalimat dan kosakata acak yang tinggi (burstiness tinggi, fluktuasi lokal perplexity tinggi). Sebaliknya, output LLM standar tanpa penyesuaian temperatur tinggi cenderung seragam dan meninggalkan jejak distribusi N-gram yang dapat diprediksi atau token watermark berbasis hash.

Metrik utama yang dievaluasi dalam linter ini meliputi:

  • N-Gram Repetition Ratio: Rasio token berulang pada tingkatan unigram, bigram, dan trigram dalam satu dokumen markdown. LLM sering kali mengulang frasa transisi tertentu secara berlebihan.
  • Sentence Length Variance (Burstiness): Standar deviasi dari panjang kata per kalimat. Manusia mencampur kalimat pendek dan panjang; AI menghasilkan panjang kalimat dengan distribusi Gaussian yang rapat.
  • Deterministic Heuristic Score: Nilai komposit normalisasi 0.0 hingga 1.0 yang menentukan apakah dokumen harus memicu hard failure di CI pipeline.

Implementasi Linter Deterministik (Python CLI)

Skrip berikut mengevaluasi file Markdown dengan menghitung variansi panjang kalimat dan distribusi N-gram tanpa memerlukan dependensi machine learning yang berat seperti PyTorch.

#!/usr/bin/env python3
import sys
import re
import math
from collections import Counter
from pathlib import Path

def tokenize_words(text):
    return re.findall(r'\b[a-zA-Z]{2,}\b', text.lower())

def get_sentences(text):
    return [s.strip() for s in re.split(r'[.!?]+', text) if len(s.strip()) > 0]

def calculate_burstiness(sentences):
    lengths = [len(tokenize_words(s)) for s in sentences if len(tokenize_words(s)) > 0]
    if len(lengths) < 2:
        return 0.0
    mean = sum(lengths) / len(lengths)
    variance = sum((x - mean) ** 2 for x in lengths) / (len(lengths) - 1)
    std_dev = math.sqrt(variance)
    return std_dev / mean if mean > 0 else 0.0

def calculate_ngram_diversity(words, n=2):
    if len(words) < n:
        return 1.0
    ngrams = [tuple(words[i:i+n]) for i in range(len(words)-n+1)]
    return len(set(ngrams)) / len(ngrams)

def analyze_document(file_path):
    text = Path(file_path).read_text(encoding='utf-8')
    # Hilangkan blok kode markdown agar tidak bias
    cleaned = re.sub(r'```.*?```', '', text, flags=re.DOTALL)
    cleaned = re.sub(r'`.*?`', '', cleaned)
    
    words = tokenize_words(cleaned)
    sentences = get_sentences(cleaned)
    
    if len(words) < 50:
        return True, 0.0, "Dokumen terlalu pendek untuk evaluasi statistik."

    burstiness = calculate_burstiness(sentences)
    bigram_diversity = calculate_ngram_diversity(words, n=2)
    
    # AI teks tipikal: low burstiness (< 0.4) dan repetisi N-gram tinggi (diversity < 0.65)
    # ponytail: naive heuristic threshold; upgrade to true n-gram language model if false positives occur
    is_synthetic = burstiness < 0.35 and bigram_diversity < 0.60
    
    return not is_synthetic, burstiness, bigram_diversity

if __name__ == '__main__':
    target = sys.argv[1] if len(sys.argv) > 1 else '.'
    failed = False
    
    for path in Path(target).rglob('*.md'):
        passed, burst, div = analyze_document(path)
        if not passed:
            print(f"[FAIL] {path} terdeteksi sintetis. Burstiness: {burst:.2f}, Bigram Diversity: {div:.2f}")
            failed = True
        else:
            print(f"[PASS] {path} (Burstiness: {burst:.2f}, Bigram Diversity: {div:.2f})")
            
    if failed:
        sys.exit(1)

Skipped: integrasi kalkulasi perplexity via HuggingFace transformers. Tambahkan library inference saat threshold heuristik menghasilkan terlalu banyak false negative pada dokumen non-teknis.

Konfigurasi GitHub Actions Workflow

Gunakan runner GitHub Actions untuk mengeksekusi skrip deteksi setiap kali ada pull request yang mengubah file dokumentasi pada direktori docs/.

name: AI Content Linting

on:
  pull_request:
    paths:
      - 'docs/**.md'
      - '**.md'

jobs:
  validate-content:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Execute Content Linting
        run: |
          python scripts/ai_content_linter.py docs/

Menangani False Positive dan Tuning Ambang Batas

Dokumentasi API, changelog, dan tabel referensi secara alami memiliki nilai burstiness rendah dan repetisi token tinggi karena struktur penulisan teknis yang kaku.

  • Exclusion Parsing: Selalu strip tag kode (```), inline backticks, dan tabel Markdown sebelum menghitung statistik N-gram.
  • Minimum Token Threshold: Hindari menganalisis file di bawah 50-100 kata. Sampel teks yang kecil mendistorsi deviasi standar panjang kalimat.
  • Adjusting Diversity Factor: Jika dokumentasi domain-spesifik sering memicu fail, turunkan ambang batas bigram_diversity dari 0.60 ke 0.50.