Бюджеты токенаў проты семантычных меж: як правільна роздзеляць чанкі RAG.
Выявіце, чым разлічаецца токенізація і чанкінг, якае месца кожна з іх займае ў процесе RAG, а таксама як роздзеляць документы за значэнням, вядучы паралельна підрахунок ўжоўважнення рэальным токенізаторам.
Крыніцы: Chunking vs Tokenization: The Two Splits Every AI Engineer Keeps Confusing
Пайплайн RAG можа выглядаць стабільным на кожным видзімным слое: з разумнымі эмбеддінгамі, правдападобнымі рэштункамі сяродзець і добра заполненым хранальнікам вектараў, пры чым ён все равно можа даваць модэлю фрагменты, якія зупіняюцца на паўзе. У багатьых такіх случаях прычына крыўдзі лежыць у кодзе для прыема дадзеных, дзе процедура разбівання была створена на некальконага падазрэнні ў тым, як текст ператвараецца на токены. Шырокі практычны кялік чыста раздзеляе гэтыя два падходы, паказвае, дзе кожны з іх знаходзіцца ў пайплайне, і апісвае метод разбівання, які спачатку аддае прыоритет значэнню, адночасна заставаючы точны контроль над кантамі токенаў.
Інжынеры часта кажуць такія фразы, як «разбіць на 512 токэнаў» або «разбіванне на чакі на адной основе токэнаў», прычаму гэтыя выразы спаўнююць два разныя процесу ў адны. Яны рашаюць разныя проблемы, выконваюцца на разных этапах і працююць по-разнаму. Толькі точна ведаючы, дзе заканчваецца адны процес і пачынаецца другі, можна правільна аналізаваць систему выкарыстоўвання дадзэных, а не проста налаштовваць параметры пакуль не паспяўнае значэнне оценкі.
Два процесу, два власнікі
Найбяжэйшыя корыстныя вакласыфікацыі ў такіх:
- Токэнаўванне — гэта ператварэння тексту на окремыя цэлычасловыя елементы, якія фактычна викорыстоўваецца модэлем. Яно належыць да модэля.
- Разбіванне на чакі — гэта раздзелэння дасяглікова на самастойныя часткі, якія можна вбудаваць, індексаваць і выкарыстоўваць. Яно належыць да вашай схемы обробкі і вашых дадзэных.
Токенізацыя выконвалася ўсё час, калі яны рэча прабывае да моделі, незалежна ад таго, думаеце вы пра гэта чы не. Фрагментаванне — это выбор, які вы робяце заздалегідь ўжо пра тое, як структураваць корпус, перш чым модель яго пабачыць. Першы процес є фіксаваным і здзейчай невидным; другі — гэта тое, што вы проектуеце, мерзіце і доўжаўлівайце прыблізна за тыя ж недзелі.
Гэтыя два процесы стаячы взаімаюцца. Раздзелвач, які ігнаруея прыемы токенізатора, будзе таямна ствараць некаліткія фрагменты. Усё-такі, гэта разныя крокі, і спрыяванне іх як адной рошчыні ёсць прычыной нейколькіх неспадзявана частых тонкіх багоў у процесе выкарыстоўвання дадзеных.
Як модель чытае тэкст
Ніяны з трансфармэрных LLM, незалежна ад ГПТ-4о, Claude, Llama чы іншых, якія выкарыстоўваюцца ў практыцы, не чытае слова. Яны чытаюць последоўнасць цэлых лічб, кожная з якіх паказвае на фіксаваны слоўнік, выучаны пад час трэнавання. Токенізатор — это детерміністычны алгорытм, які ператварае неапрануты текст у такую последоўнасць. Ён выбіраецца аднойчы, калі трэнуецца модель, і прыносится як частка елементаў модэлі. Яго нельзя зменіць за кожны запит.
Слоўнікі падслоў: BPE, WordPiece, SentencePiece
Большасць сучасных модэляў выкарыстоўвае токенізацыю падслоў, зазвычай з семей BPE, WordPiece чы SentencePiece. Аднойчыны прынцып — пачынаць з симвалаў чы неапранутых байтов і паступова з’еднаць найчастэйшыя суседзкія пары ў новыя елементы слоўніка, пакуль не будзе досягнута заданая велічына слоўніка, якая зазвычай станавіцься ад 32к да 128k елементаў чы больш.
Што гэта значыць у практычнай работе:
- Частае англійскія словы, такія як
the,isчыстаmodel, зазвычай перакладаюцца як адзін токен кожны. - Рэдкія словы дзелюцца на часткі, таму
tokenizationможа стацьtokenплюсization. - Незвычныя строкі, укладаючы ў сабе ідэнтыфікаторы коду, не-латынскія алфавіты, эмодзі і выдуманыя назвы продуктав, могу перакладацца як множнацьце токенав на адно слова, а інодзе — як майже адзін токен на кожны симвал.
Дзеўа прычына, чаму запэўненне на хінді або тамільскай можа коштаць у два-тры разы больш токенаў, чым запэўненне на англійскай з той самай значэннем, і чаму ідэнтыфікатор на кшталт xgboost_lstm_ensemble_v2 можа выкарыстоўваць або ўсё-такі восьма токенаў, калі планавалася ўсьмо два. Точныя спадзеўнікі залежнаць ад токенайзера, а новэйшыя слоўнікі скармалілі разліку для дзеяных мов, таму трэба мерыць з выкарыстоўваным модэлем. Для будзь-яга продукту, які прыслужвае рынкам неанглійскіх мов, напрыклад шара LLM, який падсумавляе дакументы індыйскай фірмовай біржы або логі замовэнняў брокера, гэта ёсць рэальны расход і элемент бюджету контексту, а не проста культурна цікавасць.
Чаму колькасць токенаў мае значэнне не толькі для расчытку
Колькасць токенаў вплывае не толькі на цэну запытку. Яны кантролююць троха рэчаў:
- Які розмеры вікна контексту вы викорыстоўваете. Вікно па 128 кабаўт прыметрабяецца па токэнах, а не па словах чы раскладках. Аб’ект з 1 000 слоў не мае фіксаванага кашту; яго колькасць токэнаў зміняецца залежна ад слоўніку, мовы і типу контэнту, адтолькі што код, проза і цыфры токэнацуюцца дужа разна.
- Над чым працюе атэнцыя. Трансфармеры працуюць з усіма пазіцыямі токэнаў, таму спосаб разбівання пасунку на токэны вплывае на тое, насколькі кохерентна модель можа вырашыць асоціяціі между його часткамі. Паталагічныя разбівання, якія паслабляюць розумеўненне, зустрічаюцца рэдка, але певныя форматыванні можу іх ствараць.
- Дзе вядзецца транкуляцыя. Калі вхідны тэкст перасягае розмеры вікна, ён адсекаецца на рэвэлі токэнаў, а не на межы раскладак чы аб’ектаў, якщо толькі вы самі не керуеце гэтым. Недбежная транкуляцыя можа заставіць слова незавершанымі.
Аднойчы пагляньце на своія сабэйты.
Якщо вы ніколи раней не працавалі з вашымі рэальнымі тэкстамі для вырабоцтва через tiktoken ад OpenAI чыстаўнік тэкста ад Anthropic, варта зробіць гэта хоч раз. Калі вы бачыце, як ваш шаблон запиту разбіваецца на неспадзяваныя фрагменты, абстракцыя ператвараецца на ўсё бол конкрэтную рэч. Синтаксі Markdown, складанні JSON і рамкі для коду є особліва прычынай такога явы: яны перетвараюць тэкст менш эфективна, чым звычны прозаічны текст.
У наведзеным нижэй фрагменте выкарыстоўваецца кодуванне cl100k_base, і після таго лічыцца колькість токэнаў у типовай командзе шэлу. Заўважыце, як флагі і расширэння файлаў зазвычай займаюць два-тры токэна, і саме такія элементы спрабуюць збільшыць кантэнт без таго, каб хто-небудзь гэта пазнаў.
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode("pip-compile requirements.in --output-file requirements.txt")
print(len(tokens), tokens)
# CLI flags and file extensions often split into 2-3 tokens each —
# this is exactly the kind of string that silently inflates your token budget
Як вы вяршыце выбір таго, што будзе прадстаўлена модэлю
Чанкаванне выконваецца на вышэйшым роўні, у канлайну працэсе, а не ў самой модэлі. Ён берае большы документ, такі як PDF, сторанку у Confluence, хранілню коду чыя-небудзь 40-стороннюй норматыўны циркуляр, і разбівае яго на меншыя елементы, якія можна вбудоўваць, індексаваць і самастоятельна выкарыстоўваць. Чанк являе сабою основную единіцу для роботы шара выкарыстоўвання дакументаў.
Два факторы змушуюць выкарыстоўваць чанкаванне, і ні адны з яных не стосуецца прымова токенізацыі:
- Дужа дугія эмбеддынгі становяцца нечыткімі. Якщо запакаваць занадта многія несувязаныя ідеі ў адны вектар, той стае сярэднім значэнням усіх іх і не падходзіць точна ні да чаго. Хорашы чанк мае адну цэлесообразную ідею, а не цэлы документ.
Стратэгіі чакання і тое, як кожная з іх не функцыонуе
Вікна фіксаванага розмеру
Спакойнейшы падход — гэта разрыванне тексту кожныя N токенамі або симваламі, зазвычай з певным перакрыцэнням між суседзяючымі часткамі. Большасць команд пачынае з гэтага, таму што для цього патрэбны лічба рядкоў коду. Наведаная нижэй функцыя кодуе весь текст, дзельніцьы ліст токенаваў на вікна розмерам chunk_size, декодуе кожнае вікно знова у текст і прыбліжаецца на chunk_size - overlap, так што суседзяючыя чакання маюць спільную межу.
def fixed_chunk(text, chunk_size=512, overlap=50):
tokens = enc.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = start + chunk_size
chunk_tokens = tokens[start:end]
chunks.append(enc.decode(chunk_tokens))
start += chunk_size - overlap
return chunks
Слабасць заключаецца ў тым, што гэтая функцыя не мае уявлення пра рэченне. Яна ставі межу посеред часткі рэчення, раздзеляючы тверджэнне ад доказа, які яго падтрымвае, або прыклад коду ад рэчення, яке його пояснюе. Гэтыя непূরныя фрагменты рэчэння падаюць у вектары, якія знаходзяцца межаючы з тэмамі, і паспраўна не адпавядаюць ім, што якраз і є сімптамам, описаным у пачатку. Є ўсьмо два дадатковыя застережэння: overlap павінен залишацца нижэй chunk_size, інакше цыкл ніколі не будзе праходзіць далей, а декодаванне будзь-каго фрагмента токена можа разлучыць багатобайтны символ на две частыні, ствараючы заменны символ на канце фрагмента.
Рэкурсыўнае раздзеленне па прыродных межах
Рекурсыўны супяроздзелвальнік вядома выбірае роздзелы па абрактам, якщо гэта нямагчыма — роздзелы па расэкам, а яшчэ якщо і гэта не спрацавае — роздзелы па словам; толькі тады ён вырабляе жорсткі роздзел, калі нічога іншаго не падходзіць. RecursiveCharacterTextSplitter з LangChain і парсеры вузлаў у LlamaIndex следуюць гэтаму прынцыпу. Для корпусаў, дзе пераважае проза, гэта ў значныяй меры лепшы стандартны варыянт, адколі спачатку уважаецца сэрцаўая структура дакумента, а ўжо пасля — ліміт размеру.
Семантычныя часткі
У працоўцы семантычнага часткавання замест таго, каб фіксаваць размер, аддзеляюцца отдэльныя расэкі аб малыя групы іх, пры чым ставіцца межа там, дзе косайна сэмабільнасці межуячых эмбеджаў падыходзіць да нуля, тое ўсюды, дзе зменшаецца тэматыка. Рэзультатныя часткі з’еднаныя за сэмантычным значэнням, а не за дужыною. Ценай гэтага є дадатковая пераробка эмбеджаў для кожнага дакумента пад час ўваходжэння.
Часткаванне з урахоўваннем структуры
Тэкст з вбудованай структурой, такім як Markdown з заголовкамі, файлы-выхадныя данні, спецыфікацыі API або дакументацыя шэмы, трэба раздзеляць па гэтай структуре. У коде явным межам ёстся визначэнне функцыі. У базе знаёмых дадзеных у формате Markdown межай ёстся раздел ##. Для чаго-то на кшталт файла README у багатаягентнай системе раздзелэнне па заголовках разделаў даёст значна большую колькасць зрозумелых дадзеных, чым можа даць лічба токэнаў. Болей дакладныя інструкцыі па зберагчэнні падтрымлюючых доказаў даўнае ў раздзеле «Chunking that preserves evidence».
Раздзелэнне пад кераваннем LLM
У дораганых варіянтах можна папрашаць LLM прачытаць дакумент і самам запропанаваць межы, стараючыся створыць часткі, кожная з якіх ўтварае самостойную елементацію і дае адказ на практычны запыт. Гэта медленна і дорога процедура, але яна ўзаемна практычная для высакацэнных корпусаў дакументаў, такіх як регуляторныя звесткі чыў контракты, дзе неправильны выкарыстоўвання інфармацыі мае рэальныя наследкі.
Дзе стыкаюцца два падходы
Інтэреснае — гэта месца, дзе стыкаюцься гэтыя концэпцыі, і яго можна выразіць ў адной фразе: размер частак зазвычай выражаецца ў токэнах, але межы частак павінны вытварацца на адной засадзе значэння, і гэтыя два крэатывы трымаюцься па протыяўнах направленнях.
Абсалютна перакладчыка на рэгліі 512 токенав з chunk_size=512 задовольняе ліміт бюджету, але ігнаруе значэнне тэкста. Раздзелэнне толькі на параграфы без жаданага верхньяго ліміту спрабоўвае захаваць значэнне, але не берае ў расчыт, што адны параграф можа мець 40 токенав, а наступны — 4 000. Такі вялікі параграф можа перасягнуць максымальную дужыню последовацтва для модэлі эмбеддінгу, і ў рэзультате контекст, які атрымліваецца, будзе чрэзвычайна неравнамірны па колькасці інфармацыі, яку несе кожны чак.
Таму правільны інструмент для раздзелэння тэкста рашыраецца з усімі гэтымі проблемамі, і то па фіксаванай архітэктуры:
- Спачатку выявляюцься сэмантычныя межы: разделы, параграфы, рэченні, блакі коду.
- Спаўнаюцься маленькія суседзячые елементы, пакуль дадаць наступны з іх не прыведзе да перасягнення ліміту токенав.
Функцыя нижэй реалізуе гэты порядак. Яна чакае sections, якія вядома ўжо былі раздзялены на параграфы чы галоўныя заголовкі. Для кожнага з іх вяршываецца рэальная дужына токена; якщо ўжыванне яго прыведзе да пераполнення чыннага фрагмента, накопілаваныя раздзелы выдаюцца як адны фрагмент. Раздзел, який сам па сабе перавышае max_tokens, раздзяляецца па межах токенав; усё інша дадаецца да чыннага фрагмента. Тое, што застаецца напасці, становіць фінальны фрагмент.
def chunk_by_tokens_with_semantic_bias(sections, max_tokens=512):
chunks, current, current_len = [], [], 0
for section in sections: # pre-split by paragraph/header
section_len = len(enc.encode(section))
if current_len + section_len > max_tokens and current:
chunks.append("\n".join(current))
current, current_len = [], 0
if section_len > max_tokens:
# single section too big — hard split on token boundaries, not chars
section_tokens = enc.encode(section)
for i in range(0, len(section_tokens), max_tokens):
chunks.append(enc.decode(section_tokens[i:i+max_tokens]))
else:
current.append(section)
current_len += section_len
if current:
chunks.append("\n".join(current))
return chunks
Ключова рашынка дыяжэнаў пры проектаванні — габарыты вимерваюцца за дапамогою токенізатора модэлі цяльвога умяшчання, а не ацэнкуюцца на аднойчыны з символамі, і прывучана разлука можа адбыцца толькі на межы токена, яку контролюе код. Адна деталізацыя, якую варта рассмотрыць: новая лінія, якая выкорыстоўваецца ў "\n".join, дадае прыблізна аднаго токена на кожны раздел, які не ўраховываецца у загальным падсчытку, таму трэба залишыць некалькі месца пад жорсткім лімітом модэлі.
Месца кожнага крока ў практычным процесе
З точкі зоры прастору, канкетаванне ўскладненняў являе сабою проблему на ранніх этапах. Ёна выклікаецца пад час прыёму даных, яшчэ до таго, як узяты ўваў у працу які-небудзь модель, а ўсе рэзультаты зберагаюцца. Токенізацыя — гэта проблема на пазнейшых этапах. Яна выклікаецца ў слою вводу моделі пад час кожнага запиту, уключаючы вызовы функцый эмбеддінгу пад час прыёму даных і вызовы функцый генеравання пад час аналізу запиту. Якша маючы гэта на увазе, можна зрозумець, який код трэба адкрыць, калі ўтворыцца проблема.
Разлік двух спосабоў выходу з ладу
Калі аналізуеце паслядкі некальканайшага адналёгчэння даных, два сімптамы выглядаюць падобна, але вказуюць на разныя слоі:
- Адналёгчаны фрагмент ўскладненняў заканчваецца раней, чым павінен. Гэта ўскладнення, выкліканае канкетаваннем. Логіка падзелу роздзяліла сэмантычную еўранцю, і парадкс стоіць у самам прыстрое падзелу, а не у выборе модэлі эмбеддінгу.
len(text) / 4, а рэальная колькасць выйшла вышэй.Частаю чынаму прычыны ставяць «паслабленую модель укладкі». Аднак рэдкая гэта справжня прычына; зазвычай проблема крыўцяе ў логіцы роботы з межамі або підлічэння токенав.
Чек-ліст для роботы
Пры стварэнні або пераглядзе каналу RAG або агента, разгляньце следуючыя пункты:
- Підлічыце токены за дапамою рэальнага токенізатора моделі, яку вы викорыстоўвайце. Для модэлей OpenAI наладзіце
tiktoken, а для модэлей з відкрытым кодам — токенізатор, прызначаны для данай моделі. Ніколі не викорыстоўвайцеlen(text) // 4як показнік для роботы.
Узагальненне
Токенаізацыя адпавядае на запит модэлі пра тое, як узагалі чытаць строку. Фрагментаванне адпавядае на ваш запит пра тое, якая ўсё ж мінімальная самодостатня еўрэкя, яку варта запрашваць. Перша ўсталена ў момент выбору модэлі; другая — гэта аспект дызайну, які ўжо цалкам належы да вашага кантролю, і зазвычай самэ тут распалоўваецца справжні інжынерны потэнцял у системах RAG.
Этыя два віда памылак таксама выражаюцца по-разнаму. Памылкі у підрахунку токенаў прыводзяць да незвычайнага скрашчэння тэксту і перакрасці бюджетаў, што прынеймна є видным. Памылкі у разбівце на часткі ствараюць сістэму, якая працуе без жадных памылак, але ўсё рава вяртае некаторую частку правильнага выразу, што значна сложней заўважыць. Адлічваюце з рэальным токенайзерам, спачатку дзеліце на часткі за значэнням і фіксаваце, чым была обоснована пазіцыя кожнай межы.