Lewati ke konten utama

Query v2: faithfulness beranotasi

:::info Aktif di produksi POST /query/v2 dan POST /query/v2/stream aktif di produksi berdampingan dengan /query sejak 23 Okt 2026, dan /query/v2 adalah route yang menjadi dasar ke depan. Autentikasinya sama dengan /query: header X-API-Key dan JWT bertanda tangan mitra di Authorization: Bearer. /query tidak berubah. Rancangan: ADR-0051 dan ADR-0052. :::

/query menahan setiap kalimat yang tidak dikonfirmasi oleh juri faithfulness, dan menolak seluruh jawaban bila terlalu sedikit yang tersisa. /query/v2 menjalankan agen yang sama dengan tool, cakupan, riwayat, dan anggaran yang sama, serta prompt yang sama kecuali satu: agen diminta menulis jawabannya sebagai segmen. Setelah agen selesai, v2 menandai, bukan menahan:

  1. Sitasi. Agen mengembalikan daftar segmen berurutan, masing-masing dengan text, kind (fact atau framing), passage yang dirujuk (cites), dan layout (paragraph, heading, list_item, continue, table_header, table_row). Server menyusun answer dari segmen-segmen itu sebagai Markdown, sehingga offset setiap segmen tepat. Segmen fakta yang tidak merujuk apa pun ditandai uncited. Setiap segmen pembingkai (kalimat pengantar seperti "Berikut rinciannya:", salam, "tidak ditemukan dalam dokumen") diperiksa dalam satu panggilan model per jawaban yang melihat pertanyaannya, tanpa aturan kata atau angka di depannya; pembingkai yang menyatakan fakta yang perlu sitasi, atau menjawab pertanyaan, ditandai uncited, demikian pula setiap pembingkai bila panggilan itu gagal. Segmen pembingkai yang merujuk passage diperiksa sebagai fakta terhadap passage tersebut.
  2. Faithfulness. Juri terpisah memeriksa setiap segmen bersitasi terhadap passage yang dirujuknya, ditampilkan dengan identitas dokumen yang sama dengan yang dilihat agen (rujukan hukum, bagian, nomor, tahun, jenis peraturan, penerbit), atau untuk hasil portal atau MCP: dataset, sumber, periode, dan panggilan yang menghasilkannya. Juri juga melihat apa yang dilihat pembaca di sekitar segmen (judul bagian, kalimat pengantar daftar, kepala tabel; satu baris tabel diperiksa utuh), dan tidak pernah melihat pertanyaannya. Juri bersifat ketat: segmen bernilai supported hanya bila angkanya memiliki skala (ribu, juta, miliar, triliun), satuan, periode, entitas, dan kondisi yang sama dengan sumbernya; nilai yang sama dengan penulisan berbeda (871.667.219.294 dan Rp871,67 miliar) tetap didukung. Hasilnya berupa tanda pada segmen; segmen tidak pernah dihapus.
  3. Government Policy Guard. Membaca jawaban utuh, seperti pada /query. Hanya pemeriksaan ini yang dapat mengganti jawaban.

Pembaca memverifikasi segmen bertanda dengan membuka sitasinya. Klien wajib menampilkan tanda tersebut: segmen unsupported yang tampil tanpa tanda akan terlihat seolah terverifikasi. Aturan yang wajib diikuti klien ada di Kontrak klien.

Permintaan​

Body-nya sama dengan body /query (lihat API Query, Query Streaming, dan Search) dengan satu perbedaan: offer_artifacts hanya menerima false, karena /query/v2 tidak membuat berkas. include_grounding dan include_provenance hanya untuk JSON, seperti pada /query.

Setiap pemanggil menerima anotasi; tidak perlu opt-in. accept_annotations diterima dan diabaikan: field ini wajib sebagai opt-in pada 21 hingga 23 Okt 2026, dihapus atas keputusan produk, dan tetap dikenali agar klien yang masih mengirimnya tetap berfungsi. Gunakan /query bila klien tidak dapat menampilkan anotasi.

curl -sS "$ACEH_API/query/v2" \
-H "X-API-Key: $ACEH_API_KEY" -H "Authorization: Bearer $PARTNER_JWT" \
-H "Content-Type: application/json" \
-d '{"query": "Berapa anggaran pendidikan Aceh tahun 2023?"}'

Playground prompt​

POST /playground memungkinkan administrator terverifikasi mencoba satu prompt agent-editorial tanpa menerbitkannya. Body menerima field yang sama dengan /query/v2 dan menambahkan string wajib editorial_prompt (1–2.000 karakter):

curl -sS "$ACEH_API/playground" \
-H "X-API-Key: $ACEH_API_KEY" -H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"editorial_prompt": "Jawab dengan jelas dan ringkas dalam Bahasa Indonesia.",
"query": "Apa tugas utama Pemerintah Aceh dalam dokumen ini?",
"document_ids": ["<UUID dokumen>"]
}'

Rute ini menjalankan agen query-v2, retrieval, alat, pemeriksaan akses, batas waktu dan panggilan model, sitasi, pemeriksaan faithfulness annotate-v2, serta Government Policy Guard yang sama. Hanya prompt editorial yang diganti, dan hanya untuk request tersebut. Rute memerlukan shared API key dan principal admin terverifikasi; rute gagal tertutup dengan 503 saat identitas terverifikasi dinonaktifkan dan dibatasi per administrator.

Respons mempertahankan field query-v2 yang masih berlaku dan menambahkan playground_run_id, published: false, dan latency_ms. conversation_id dan message_id sengaja tidak ada: percakapan, pesan, memori, feedback, versi atau label prompt, dan analitik jawaban biasa tidak diterbitkan. Server hanya menyimpan receipt administratif kecil berisi identifier, status, jumlah, dan waktu—tidak pernah menyimpan prompt kandidat, pertanyaan, jawaban, atau teks passage sumber. Run yang berhasil tidak mengubah prompt untuk request /query atau /query/v2 berikutnya.

Respons​

Setiap field /query yang maknanya tetap dipertahankan: answer, conversation_id, message_id, trace_id, history_truncated, history_exchanges_in_view, citations, tool, tool_executed, fallback_used, fallback_reason, serta field opt-in grounding, used_docs, tool_call_audit, model_call_log. Dihapus, karena menggambarkan gate yang tidak dimiliki v2: claim_map, dropped_claims_count, dropped_claim_reasons, provenance_complete. Ditambahkan: schema (selalu "annotate-v2", kontrak yang diikuti body ini), claims, framing, dan faithfulness. Dipertahankan dengan makna v1 tetapi dihitung dari verdict v2: claims_supported, claims_total, dan grounding_rate, yaitu tingkat keterverifikasian klaim (Claim grounding rate), sehingga satu badge dapat ditampilkan dari kedua rute.

Pada v2, claims_supported menghitung klaim supported dan claims_total menghitung semua klaim kecuali from_conversation, yang mengulang percakapan dan tidak diperiksa terhadap sumber: klaim unsupported, unverified, uncited, atau invalid_citation menurunkan nilainya. grounding_rate bernilai null bila total itu 0, seperti pada penolakan sebelum ada klaim yang diperiksa. Blokir Policy Guard tidak menerbitkan klaim, tetapi, seperti pada v1, nilainya tetap menghitung klaim yang diperiksa sebelum blokir (hanya jumlah; tanpa teks klaim). Tiga kunci yang sama ada di faithfulness.grounding (juga pada frame SSE annotations, dan tersimpan di v2_faithfulness). Ini bukan angka faithfulness (ADR-0042); faithfulness.verification tetap menjadi status banner tingkat jawaban.

{
"schema": "annotate-v2",
"answer": "Berikut datanya. Anggaran pendidikan 2023 adalah Rp 5 miliar. Naik 9 persen dari 2022.",
"citations": [{"document_title": "Qanun Anggaran", "chunk_id": "…", "document_id": "…"}],
"fallback_used": false,
"fallback_reason": null,
"claims": [
{"text": "Anggaran pendidikan 2023 adalah Rp 5 miliar.", "citation_keys": ["P1"],
"citations": [{"citation_key": "P1", "citation": {"chunk_id": "…"}}],
"verdict": "supported", "unverified_reason": null, "invalid_citation_keys": [],
"kind": "fact", "layout": "continue",
"start": 17, "end": 61, "start_utf16": 17, "end_utf16": 61},
{"text": "Naik 9 persen dari 2022.", "citation_keys": ["P1"],
"citations": [{"citation_key": "P1", "citation": {"chunk_id": "…"}}],
"verdict": "unsupported", "unverified_reason": null, "invalid_citation_keys": [],
"kind": "fact", "layout": "continue",
"start": 62, "end": 86, "start_utf16": 62, "end_utf16": 86}
],
"framing": [
{"text": "Berikut datanya.", "layout": "paragraph",
"start": 0, "end": 16, "start_utf16": 0, "end_utf16": 16}
],
"faithfulness": {"mechanism": "annotate-v2", "judge_status": "complete",
"framing_check": "complete", "claims_total": 2,
"supported": 1, "unsupported": 1, "unverified": 0, "uncited": 0,
"invalid_citation": 0, "from_conversation": 0,
"unverified_reasons": {"deadline": 0, "budget": 0, "judge_unavailable": 0, "unparseable": 0,
"disputed": 0},
"framing_total": 1, "unlocated_total": 0, "verification": "partial",
"grounding": {"claims_supported": 1, "claims_total": 2, "grounding_rate": 0.5}},
"claims_supported": 1,
"claims_total": 2,
"grounding_rate": 0.5
}
verdictArtiTampilan wajib
supportedJuri menilai passage yang dirujuk mendukung segmen tersebut.Satu-satunya tanda positif: "terverifikasi", beserta sitasinya.
unsupportedJuri menilai passage yang dirujuk tidak mendukungnya, dan pemeriksaan ulang atas segmen itu saja menegaskannya (penolakan yang tidak terkonfirmasi menjadi unverified dengan alasan disputed)."Belum diverifikasi", misalnya "tidak didukung sumber"; chip sitasinya tidak boleh terbaca sebagai bukti.
unverifiedJuri tidak dapat memutuskan; unverified_reason menyebutkan sebabnya. Tidak pernah dianggap lulus."Belum diverifikasi".
uncitedMenyatakan fakta tetapi tidak merujuk apa pun (termasuk pembingkai yang oleh pemeriksaan pembingkai dinilai dapat diperiksa)."Belum diverifikasi", misalnya "tanpa sumber".
invalid_citationSemua passage yang dirujuknya tidak diperlihatkan kepada agen."Belum diverifikasi"; sitasinya tidak dapat diselesaikan.
from_conversationMengulang isi percakapan ini (ucapan pengguna, atau jawaban sebelumnya yang terverifikasi).Tanda netral tersendiri, "dari percakapan, bukan dokumen". Jangan pernah ditampilkan seperti supported: premis pengguna yang diulang tidak dikonfirmasi dokumen mana pun.

Segmen yang tidak menyatakan apa pun (salam, "Berikut ringkasannya:", baris kepala tabel) bukan klaim: segmen itu dikembalikan di framing, masing-masing dengan text, layout, dan keempat offset yang sama, tanpa putusan. Klaim dan pembingkai bersama-sama menutup setiap karakter answer kecuali tata letak yang ditulis server di antara segmen (spasi, penanda ## dan - , garis tegak tabel dan baris pembatasnya); teks di luar semua rentang akan dihitung dalam unlocated_total. answer tidak memuat label [P1]: klien menempatkan chip sitasi sebuah klaim di end-nya, dari citation_keys-nya. Setiap offset adalah posisi di dalam answer yang sudah disusun, end eksklusif, dan answer[start:end] sama persis dengan text rentang itu, termasuk penekanan Markdown:

FieldArti
start, endOffset dalam code point Unicode (indeks string Python).
start_utf16, end_utf16Rentang yang sama dalam code unit UTF-16: pakai ini sebagai indeks string JavaScript (answer.slice(start_utf16, end_utf16)). Nilainya berbeda dari start/end setelah emoji atau karakter apa pun di luar Basic Multilingual Plane.
kindfact atau framing menurut agen. Klaim framing adalah pembingkai yang oleh pemeriksaan pembingkai dinilai dapat diperiksa, atau pembingkai yang merujuk passage.
layoutPosisi segmen: paragraph, heading, list_item, continue (paragraf atau butir yang sama), table_header, table_row (sel dipisah |).

faithfulness.verification adalah status tingkat jawaban, dihitung oleh server:

verificationKapan
fullSetidaknya satu klaim, setiap klaim supported, dan unlocated_total bernilai 0.
partialSetidaknya satu klaim supported, dan ada hal lain: klaim dengan putusan lain (termasuk from_conversation) atau teks yang tidak tertutup.
noneTidak ada klaim supported, termasuk penolakan, salam tanpa apa pun untuk diperiksa, dan giliran yang tidak diperiksa sama sekali.

framing_total menghitung rentang framing. unlocated_total menghitung potongan teks answer yang tidak tertutup klaim maupun pembingkai; penolakan atau pemberitahuan, yang tidak membawa anotasi, adalah satu potongan seperti itu.

judge_status bernilai complete, unavailable (setidaknya satu segmen bersitasi berstatus unverified), atau skipped (tidak ada segmen bersitasi). framing_check sama untuk satu-satunya pemeriksaan pembingkai: unavailable berarti setiap segmen yang seharusnya diperiksanya ditampilkan uncited, tidak pernah diloloskan sebagai pembingkai.

unverified_reason bernilai null kecuali pada klaim unverified, yang berisi salah satu dari deadline (waktu permintaan habis sebelum batch juri untuk klaim itu kembali; batch yang sudah kembali tetap mempertahankan putusannya), budget (jatah panggilan model permintaan sudah habis), judge_unavailable (juri gagal atau timeout), unparseable (juri tidak memberi putusan yang dapat dipakai), atau disputed (juri batch menilai segmen tidak didukung, tetapi pemeriksaan ulang atas segmen itu saja tidak menegaskannya, sehingga segmen tidak ditampilkan sebagai didukung maupun tidak didukung). Pemeriksaan ulang yang gagal atau kehabisan waktu memberi sebabnya sendiri (deadline, budget, …). faithfulness.unverified_reasons menghitung masing-masing. Agen berhenti cukup awal agar juri tetap mendapat bagian waktu dan panggilan model permintaan.

invalid_citation_keys berisi passage yang dirujuk sebuah segmen tetapi tidak diperlihatkan kepada agen, atau tidak boleh dikutip. Segmen dengan setidaknya satu rujukan valid dinilai terhadap rujukan validnya dan mempertahankan putusan itu; hanya segmen yang semua rujukannya tidak valid yang berstatus invalid_citation.

fallback_used menyatakan apakah API menerbitkan penolakan, bukan apakah jawaban dapat dipercaya: false dengan verification: "none" adalah jawaban terbit yang tidak terverifikasi sama sekali. Nilainya true untuk penolakan: policy_blocked (Policy Guard), atau salah satu alasan tingkat loop dari /query (query_budget_exhausted, model_timeout, context_budget_exhausted, agent_output_limit (di /query/v2 hanya bila putaran alat paksa pada pertanyaan lanjutan yang diajukan ulang mencapai batas keluarannya; pemberitahuan tidak-sempat-menjawab yang diterbitkan), mcp_source_unavailable, source_out_of_scope, source_scope_unavailable, unparseable_completion). source_scope_empty adalah penolakan sebelum loop yang dijelaskan di bagian Galat. Penolakan tidak memiliki claims. unparseable_completion juga menjadi hasilnya bila segmen agen masih tidak valid setelah satu kali regenerasi; keluaran yang tidak valid tidak pernah diterbitkan.

context_budget_exhausted berarti bahan sumber yang dibutuhkan pertanyaan tidak muat dalam konteks model, sehingga jawaban dihentikan sebelum ditulis dan tidak ada yang diperiksa. Laporan faithfulness-nya membawa "checked": false, baik pada respons JSON maupun frame SSE annotations: tampilkan sebagai "belum diperiksa", jangan sebagai pemeriksaan yang gagal atau lolos. Respons lain tidak menyertakan checked; ketiadaannya jangan dibaca sebagai lolos. Teks pemberitahuannya adalah salinan sistem tetap, bukan pesan timeout.

Galat​

Kode status sama dengan /query untuk penyebab yang sama (400, 403, 404, 413, 502; 401 untuk kunci yang tidak ada; 422 untuk body yang tidak valid), ditambah 409: sebuah percakapan menjadi milik rute yang pertama menjawab di dalamnya. Melanjutkan percakapan /query di /query/v2, atau sebaliknya, menghasilkan 409. Percakapan kosong yang dibuat dengan POST /conversations diambil oleh rute mana pun yang menjawab lebih dulu. Mengubah sumber atau pilihan MCP sebuah percakapan tidak melepaskannya.

Dua penyebab berbeda: bila bukti yang dikumpulkan agen tidak lagi muat dalam jendela konteks model, /query/v2 menjawab 200 dengan fallback_reason: "context_budget_exhausted", sedangkan /query menjawab 413. Penolakan yang sama berlaku bila alat sudah berjalan tetapi tidak satu pun hasilnya muat dalam konteks. Selection bernama yang tidak memiliki dokumen ter-index menjawab 200 dengan fallback_reason: "source_scope_empty"; API menerbitkan teks penolakan Bahasa Indonesia yang tetap sebelum alat berjalan atau panggilan model apa pun, termasuk klasifikasi intent dan tren, juga untuk pertanyaan ingatan dalam percakapan lanjutan. Selection yang tidak dapat diakses pemanggil juga memiliki nol dokumen ter-index dan menerima penolakan ini. Respons 404 dari /query/v2 berarti alat yang berjalan tidak mengembalikan apa pun yang dapat dibaca, termasuk pencarian yang gagal atau dihentikan pada tenggat alat. Respons 413 dari /query/v2 membawa teks tetap, tidak pernah pesan galat dari server model.

Streaming​

POST /query/v2/stream mengirim anotasi sebelum teks jawaban: tool.started / tool.finished selama agen bekerja, lalu message.start, trace, citation, kemudian guard.blocked atau passage, annotations, answer.delta, lalu done. Jawaban sudah ditampung dan diperiksa Guard sebelum frame pertama, sehingga setiap putusan sudah final ketika teks dimulai.

event: message.start
data: {"conversation_id": "…", "message_id": "…", "schema": "annotate-v2"}

event: citation
data: {"chunk_id": "…", "document_title": "Qanun Anggaran", …, "citation_key": "P1"}

event: passage
data: {"kind": "chunk", "chunk_id": "…", "citation_key": "P1", "text": "…", …}

event: annotations
data: {"mechanism": "annotate-v2", "claims_total": 2, "supported": 1, …,
"framing_total": 1, "unlocated_total": 0, "verification": "partial",
"grounding": {"claims_supported": 1, "claims_total": 2, "grounding_rate": 0.5},
"fallback_used": false, "fallback_reason": null,
"claims": [{"start": 17, "end": 61, "start_utf16": 17, "end_utf16": 61,
"kind": "fact", "layout": "continue", "citation_keys": ["P1"],
"verdict": "supported", "unverified_reason": null,
"invalid_citation_keys": []}, …],
"framing": [{"start": 0, "end": 16, "start_utf16": 0, "end_utf16": 16,
"layout": "paragraph"}]}

event: answer.delta
data: {"text": "Berikut datanya. Anggaran …"}

event: done
data: {"finish_reason": "stop", "history_truncated": false}

Frame annotations membawa jumlah faithfulness (termasuk verification) di tingkat atasnya, fallback_used dan fallback_reason, serta offset di atas untuk setiap klaim dan setiap rentang pembingkai, tanpa teksnya: gabungan frame answer.delta sama persis dengan answer, sehingga offset berlaku untuk teks yang di-stream. Setiap frame citation dan setiap frame passage chunk membawa citation_key tempat passage itu diperlihatkan (frame passage tool sudah membawanya), sehingga setiap citation_keys sebuah klaim dapat dipasangkan dengan sumber yang dibuka pembaca untuk memverifikasinya; kunci yang tercantum di invalid_citation_keys tidak berpasangan dengan apa pun. Event ini bukan faithfulness milik /query/stream: frame itu menghitung kalimat yang dihapus, dan klien v1 yang membaca jumlah v2 dengan nama itu akan melihat "tidak ada yang dihapus". Kesalahan setelah annotations mengakhiri stream dengan frame error; tanda untuk teks yang sudah diterima sudah tiba.

Kontrak klien​

Klien /query/v2 wajib mengikuti aturan berikut. Aturan ini menjadikan "terverifikasi" sebagai status positif, sehingga tidak ada kegagalan (frame yang hilang, rentang yang tidak dapat ditempatkan, klien yang mengabaikan sebuah field) yang dapat terlihat seperti jawaban terverifikasi.

  1. Periksa kontraknya. Baca schema (body JSON, atau message.start pada stream) dan perlakukan nilai selain "annotate-v2" sebagai belum terverifikasi.
  2. Tandai teks terverifikasi secara positif. Hanya klaim supported yang ditampilkan sebagai terverifikasi ("terverifikasi"). Setiap putusan lain, dan setiap teks answer di luar semua rentang klaim dan pembingkai, ditampilkan sebagai "belum diverifikasi". Pembingkai juga tidak terverifikasi; ia tidak memerlukan tanda sendiri, tetapi tidak boleh terlihat seperti tanda terverifikasi.
  3. from_conversation bersifat netral. Tampilkan dengan tandanya sendiri, "dari percakapan, bukan dokumen", jangan pernah seperti supported.
  4. Banner. Setiap kali verification bukan "full", tampilkan pemberitahuan tingkat jawaban yang tetap terlihat bersama jawaban, yang menyatakan jawaban belum sepenuhnya terverifikasi.
  5. Tanpa anotasi, tanpa kepercayaan. Pada stream, jawaban yang frame annotations-nya tidak diterima ditampilkan sebagai "belum diverifikasi" seluruhnya, beserta banner.
  6. fallback_used bukan tanda kepercayaan. Nilai itu menandakan penolakan; jawaban tanpa fallback hanya seterverifikasi yang dinyatakan verification.

Percakapan​

Giliran berikutnya hanya melihat apa yang sudah terverifikasi: riwayat yang diberikan kepada model, passage tindak lanjut yang dibaca ulang, dan ekstraksi memori jangka panjang hanya memakai segmen supported (serta segmen pembingkai atau percakapan) dari jawaban /query/v2 sebelumnya. Satu-satunya pengecualian, hanya untuk riwayat model, adalah jawaban sebelumnya yang tidak memiliki satu pun pernyataan terverifikasi: jawaban itu dikutip utuh setelah catatan bahwa tidak ada yang terverifikasi dan isinya bukan bukti, sehingga model tahu apa yang dilihat pembaca alih-alih membaca jawaban kosong. Tindak lanjut membaca dokumen bernama secara utuh hanya bila ia merujuk tepat satu dokumen sebelumnya (atau satu dokumen dipilih); bila jawaban sebelumnya mengutip beberapa dokumen, pencarian agen mengutamakan dokumen-dokumen itu (paling banyak enam, yang terbaru lebih dulu, dan hanya dokumen yang dikutip segmen supported); teks pencarian tetap milik agen sendiri, tanpa tambahan judul. Tampilan percakapan bagi pembaca tetap berupa jawaban tersimpan yang utuh.