JSON schema dan grammar ga: ixtiyoriy kalit tuzog'i
Yetti soatlik xato
3-bobda grammar ni "birinchi to'siq" deb atadik: model faqat grammar ruxsat bergan tokenlarni yoza oladi. 8-bobda toollar va ularning variantlarini ko'rdik. Bu bob — shu ikkisi uchrashgan joyda yuz bergan eng qimmat xato haqida.
tez9 modeli yetti soat o'qitildi. Birinchi test natijasi — eski modeldan ancha yomon. Birinchi xulosa: "tez9 yomon o'qitilgan". Bu xulosa noto'g'ri edi. Model o'z ishini to'g'ri o'rgangan edi — unga yozishga ruxsat berilmagan edi.
Buni tushunish uchun grammar qanday yasalishini ko'rishimiz kerak. Va buni taxmin bilan emas, llama.cpp ning o'z kodi bilan qilamiz.
llama.cpp ning o'z kodi bilan
Model serveri JSON schema ni olib, uni grammar ga aylantiradi. Bu ishni llama.cpp dagi bitta
funksiya qiladi: json_schema_to_grammar. U serverning o'zi ishlatadigan kutubxonada turibdi
(libllama-common.so). Shu funksiyani chaqiradigan o'n qatorli dastur yozamiz — model ham,
server ham kerak emas:
shu faylni yaratinggrammar.cpp
#include "json-schema-to-grammar.h"
#include <iostream>
#include <iterator>
int main() {
std::string text((std::istreambuf_iterator<char>(std::cin)), std::istreambuf_iterator<char>());
std::cout << json_schema_to_grammar(common_json::parse(text));
}
Uni kompilyatsiya qilamiz. Uchta sarlavha fayli llama.cpp ning b11946 versiyasidan olingan va
kitob papkasida saqlangan (datasets/llama_b11946); kutubxona — Tez benchmarklari ishlatgan
llama.cpp ning o'zi:
LIB=/mnt/d/Tez_spikes/llama-b11946-linux-cpu g++ -std=c++17 -O1 -I../datasets/llama_b11946 grammar.cpp -L$LIB -lllama-common -Wl,-rpath,$LIB -o grammar echo '{"type": "object", "properties": {"b": {"type": "string"}, "a": {"type": "string"}}, "required": ["a"]}' | ./grammar
a-kv ::= "\"a\"" space ":" space string
b-kv ::= "\"b\"" space ":" space string
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
root ::= "{" space a-kv ( "," space ( b-kv ) )? space "}"
space ::= | " " | "\n"{1,2} [ \t]{0,20}
string ::= "\"" char* "\""
Bu — GBNF formatidagi grammar: har bir qator bitta qoida. Oxiridagi root qatoriga qarang.
Schema da kalitlar b, keyin a tartibida yozilgan. Lekin grammar boshqacha tartib chiqardi:
avval a (u majburiy, required), keyin b (ixtiyoriy, qavs ichida va ? bilan).
Ya'ni: llama.cpp majburiy kalitlarni oldinga, ixtiyoriylarini orqaga qo'yadi — schema da
qanday yozilganidan qat'iy nazar. Bu grammar bilan {"b": ..., "a": ...} deb yozib bo'lmaydi.
Model { dan keyin faqat "a" ni yoza oladi.
Bir qarashda bu kichik narsa. JSON da kalitlar tartibi ahamiyatsiz-ku?
tez9 da nima bo'ldi
JSON uchun — ahamiyatsiz. Model uchun — juda muhim.
tez9 track ida Tezga yangi imkoniyat qo'shildi: butun jadval bo'yicha bitta jami ("jami kredit
qancha?"). Buning uchun group_and_total ning group_by argumenti ixtiyoriy qilindi: bo'lsa —
guruhlab jamlaydi, bo'lmasa — bitta jami beradi. Mantiqan to'g'ri o'zgarish.
Lekin u schema ni qanday o'zgartirganini ko'raylik. O'sha paytdagi shaklni qo'lda yozamiz:
import json import subprocess def grammar(schema): return subprocess.run([str(ISH / "grammar")], input=json.dumps(schema), capture_output=True, text=True, check=True).stdout tez9_shakli = { "type": "object", "properties": { "group_by": {"type": "array", "items": {"type": "string"}}, "value_column": {"type": "string"}, "agg": {"enum": ["sum", "count", "avg"]}, }, "required": ["value_column", "agg"], } print(next(q for q in grammar(tez9_shakli).splitlines() if q.startswith("root")))
root ::= "{" space value-column-kv "," space agg-kv ( "," space ( group-by-kv ) )? space "}"
group_by endi faqat oxirida kelishi mumkin. Model esa nimani o'rgangan? Training ma'lumotidagi
har bir group_and_total javobida kalitlar qaysi tartibda turganini sanaymiz — tez9 ning o'z
training faylidan:
from collections import Counter birinchi_kalit = Counter() for qator in open(Path.home() / "tez_runs/tez9/data/train.jsonl", encoding="utf-8"): javob = json.loads(json.loads(qator)["messages"][-1]["content"]) for qadam in javob.get("steps", []): if qadam["tool"] == "group_and_total" and "group_by" in qadam["args"]: birinchi_kalit[next(iter(qadam["args"]))] += 1 print("group_by bor javoblarda birinchi kalit:", dict(birinchi_kalit))
group_by bor javoblarda birinchi kalit: {'group_by': 4977}
Model minglab marta {"group_by": ... deb boshlashni o'rgangan. Endi { dan keyin u "group_by"
yozmoqchi — grammar esa faqat "value_column" ga ruxsat beradi. Model majburan value_column
yozadi, keyin agg — va endi u o'zi hech qachon ko'rmagan holatda. Oxirida group_by ni qo'shish
imkoni bor, lekin model buni o'rganmagan. Natija: guruhlash tushib qoladi, va "filiallar bo'yicha
jami" o'rniga bitta umumiy jami chiqadi.
O'lchov
Bu taxminni saqlangan natijalar bilan tekshiramiz. Eski grammar bilan tez9 faqat 11 ta test to'plamida ishga tushirilgan edi. Shu savollarda tez8 va tez9 ni solishtiramiz, va tez9 ning yangi xatolari ichida "kutilgan javobda guruhlash bor, model yozgan javobda yo'q" holatlarini sanaymiz:
import glob toplamlar = sorted(Path(f).stem.split("_cpu_")[1] for f in glob.glob(str(TEZ / "results/tez9_cpu_*.jsonl"))) jami = tez8_togri = tez9_togri = yangi_xato = guruh_tushgan = 0 for toplam in toplamlar: savollar = {s["id"]: s for s in json.load(open(TEZ / f"accuracy/suites/{toplam}/questions.json", encoding="utf-8"))} t8 = {j["id"]: j for j in map(json.loads, open(TEZ / f"results/tez8_cpu_{toplam}.jsonl", encoding="utf-8"))} t9 = {j["id"]: j for j in map(json.loads, open(TEZ / f"results/tez9_cpu_{toplam}.jsonl", encoding="utf-8"))} for i, javob in t9.items(): jami += 1 tez8_togri += t8[i]["correct"] tez9_togri += javob["correct"] if t8[i]["correct"] and not javob["correct"]: yangi_xato += 1 kutilgan = json.dumps(savollar[i]["expect"]) if ('"group_by"' in kutilgan or '"group"' in kutilgan) and not ('"group_by"' in javob["output"] or '"group"' in javob["output"]): guruh_tushgan += 1 print(f"{len(toplamlar)} to'plam, {jami} savol: tez8 {tez8_togri}, tez9 (eski grammar) {tez9_togri}") print(f"tez9 ning yangi xatolari: {yangi_xato}, shundan guruhlash tushib qolgani: {guruh_tushgan}")
11 to'plam, 480 savol: tez8 472, tez9 (eski grammar) 446 tez9 ning yangi xatolari: 28, shundan guruhlash tushib qolgani: 20
Yangi xatolarning katta qismi — aynan guruhlash tushib qolgan javoblar. Model yomon o'qitilmagan edi. Grammar unga o'rgangan tartibda yozishga ruxsat bermagan edi.
Ikkinchi xato: noto'g'ri solishtirish
Birinchi xulosa nega "tez9 yomon" bo'lgan? Chunki tez9 ning yangi grammar bilan olingan natijalari tez8 ning eski grammar bilan olingan natijalari bilan solishtirilgan edi. Ikki narsa bir paytda o'zgargan — model ham, grammar ham. Xatolar jurnalida bu ikki alohida qator (1 va 2), va ulardan ikkita qoida chiqdi: schema yoki grammar o'zgarsa — haqiqiy grammar ni yasab, kalit tartibini train qilish ma'lumoti bilan solishtir; modellarni solishtirganda — bir xil grammar, bir xil savollar, grammar o'zgargan bo'lsa ikkala modelni qayta ishga tushir.
Bu jurnalda tez9 ning yangi xatolaridan "taxminan 24 tasi" guruhlash deb yozilgan. Yuqoridagi kod aniq qoida bilan qayta sanaydi, va uning soni boshqacha bo'lishi mumkin: jurnaldagi son o'sha kuni qo'lda ko'rib chiqilgan edi. Kitobda o'lchangan son turadi.
Tuzatish: har bir kalit majburiy
Yechim — grammar ni "aldash" emas, schema ni boshqacha yasash. Agar kalit ixtiyoriy bo'lgani uchun oxirga surilsa, unda ixtiyoriy kalit bo'lmasin. Buning o'rniga har bir mumkin bo'lgan kalitlar to'plami uchun alohida variant yasaladi, va har bir variant ichida hamma kalit majburiy.
prompt/build.py dagi funksiya:
import inspect from prompt import build print(inspect.getsource(build._key_choices))
def _key_choices(args):
optional = [name for name, spec in args.items() if spec.get("optional")]
for kept in product((True, False), repeat=len(optional)):
dropped = {name for name, keep in zip(optional, kept) if not keep}
yield {name: spec for name, spec in args.items() if name not in dropped}
group_and_total da ikkita ixtiyoriy argument bor: group_by va share. Ikkalasi ham bor yoki
yo'q bo'lishi mumkin — demak to'rtta kalitlar to'plami. Har biri alohida variant bo'ladi, va har
birida hamma kalit required. Endi llama.cpp hech narsani surmaydi: hamma kalit majburiy bo'lsa,
u schema dagi tartibni saqlaydi.
Hozirgi Tez schema sini 3-bobdagi kredit jadvali uchun yasab, group_and_total variantlarini
ko'raylik va butun schema ni grammar ga aylantiramiz:
kontekst = build.load_context(TEZ / "prompt/domains/bank_loans") schema = build.output_schema(kontekst) variantlar = [v for v in schema["anyOf"][0]["properties"]["steps"]["items"]["anyOf"] if v["properties"]["tool"]["const"] == "group_and_total"] for v in variantlar: args = v["properties"]["args"] print(f"kalitlar: {list(args['properties'])} majburiy: {args['required'] == list(args['properties'])}") qoidalar = grammar(schema).splitlines() guruhli = next(q for q in qoidalar if q.endswith("-args-share-kv space \"}\"") and "group-by-kv" in q) print() print(f"schema: {len(json.dumps(schema)):,} belgi, grammar: {len(qoidalar)} qoida") print("group_by li variantning qoidasi:") print(guruhli.split(" ::= ")[1].replace(guruhli.split("-args")[0] + "-args-", ""))
kalitlar: ['group_by', 'value_column', 'agg', 'share'] majburiy: True
kalitlar: ['group_by', 'value_column', 'agg'] majburiy: True
kalitlar: ['value_column', 'agg', 'share'] majburiy: True
kalitlar: ['value_column', 'agg'] majburiy: True
kalitlar: ['group_by', 'value_column', 'agg', 'share'] majburiy: True
kalitlar: ['group_by', 'value_column', 'agg'] majburiy: True
kalitlar: ['value_column', 'agg', 'share'] majburiy: True
kalitlar: ['value_column', 'agg'] majburiy: True
schema: 48,781 belgi, grammar: 971 qoida
group_by li variantning qoidasi:
"{" space group-by-kv "," space value-column-kv "," space agg-kv "," space share-kv space "}"
Variantlar sakkizta: to'rtta kalitlar to'plami, har biri ikki xil (8-bobdagi sum/avg faqat
sonli ustun bilan, count esa istalgan ustun bilan). Hammasida kalitlar majburiy. Va grammar
qoidasida group_by endi birinchi turibdi — training ma'lumotidagi tartibda.
Narxi bor: variantlar soni ixtiyoriy argumentlar soni bilan ikki barobardan o'sadi. Shuning uchun toollarda ixtiyoriy argumentlar kam saqlanadi. Bu kredit jadvali uchun butun schema bir necha o'n ming belgi va mingga yaqin qoida bo'ldi. 6-bobdagi o'lchovda grammar ning vaqt narxi medianada 2 foiz atrofida edi.
Qoidani test bilan qotirish
Bu xato yana takrorlanmasligi kerak. Kimdir keyinroq yangi toolga ixtiyoriy argument qo'shsa, grammar yana tartibni buzishi mumkin. Shuning uchun Tezda test bor: har bir variant ichidagi hamma kalit majburiy bo'lishi shart. Uni kitob yig'ilayotganda ishga tushiramiz:
cd /mnt/c/Users/Admin/Desktop/Offline/spikes/spike2_3_model PYTHONDONTWRITEBYTECODE=1 "$OLDPWD/.venv/bin/python" -m pytest prompt/test_build.py -q -p no:cacheprovider -k "every_key_is_required" 2>&1 | tail -1
27 passed, 340 deselected in 2.97s
Tuzatish eski modelga zarar qildimi
Oxirgi savol: yangi grammar tez8 ni yomonlashtirmadimi? Yuqoridagi 11 ta to'plamda tez8 ning eski grammar bilan va yangi grammar bilan olingan natijalarini, hamda tez9 ning yangi grammar bilan natijasini solishtiramiz:
def ball(prefiks): togri = jami_savol = 0 for toplam in toplamlar: javoblar = [json.loads(q) for q in open(TEZ / f"results/{prefiks}_cpu_{toplam}.jsonl", encoding="utf-8")] togri += sum(j["correct"] for j in javoblar) jami_savol += len(javoblar) return f"{togri}/{jami_savol}" for prefiks, izoh in (("tez8", "tez8, eski grammar"), ("tez8g", "tez8, yangi grammar"), ("tez9", "tez9, eski grammar"), ("tez9g", "tez9, yangi grammar")): print(f"{izoh:22s} {ball(prefiks)}")
tez8, eski grammar 511/520 tez8, yangi grammar 511/520 tez9, eski grammar 446/480 tez9, yangi grammar 506/520
tez8 — ikkala grammar da bir xil. tez9 esa yangi grammar bilan katta farq bilan tiklandi. Eski grammar da tez9 ning bitta to'plami to'liq ishga tushmagan (savollar soni kamroq) — shuning uchun uning maxraji boshqacha.
Lekin bu yerda halol bo'lish kerak: tiklangan tez9 ham tez8 dan yutmadi. To'liq testda (745 savol) tez9 — 698, tez8 — 707. Grammar tuzatilgach ham tez9 yutqazdi, va buning sabablari boshqa — 21-bob mavzusi.
Xulosa
- llama.cpp JSON schema ni grammar ga aylantirganda majburiy kalitlarni oldinga, ixtiyoriylarini orqaga qo'yadi. Buni llama.cpp ning o'z funksiyasini chaqirib ko'rdik.
- tez9 da
group_byixtiyoriy qilindi. Modelgroup_byni birinchi yozishni o'rgangan edi, grammar esa unga ruxsat bermadi — natijada guruhlash tushib qoldi. - Birinchi xulosa noto'g'ri edi, chunki ikki xil grammar dagi natijalar solishtirilgan.
- Tuzatish: har bir kalitlar to'plami uchun alohida variant, ichida hamma kalit majburiy
(
_key_choices). Qoida test bilan qotirilgan. - Tuzatish tez8 ga zarar qilmadi. tez9 tiklandi, lekin baribir yutmadi.
Keyingi bobda plan bajarilgandan keyingi qismga o'tamiz: DuckDB engine va "bir xil natija" qanday tekshiriladi.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.