Структураваныя сістэмныя промпты для AI-агентаў на Rust: пласты, кэш і праверка дрэйфу
Будуем шматпластовы сістэмны промпт для агента на Rust: ідэнтычнасць, інструкцыі, правілы вываду, прыклады і свежы кантэкст, а таксама мяжа кэша і адбітак для CI.
Крыніцы: Building AI Agents in Rust — part 2
Першы агент зазвычай можа функцыянараваць за дапамою прампта, які ўскладнены толькі адной стрынговай літэралай, можа шасцюдзеў словамі: адпавядаць на запытанні пра праект на Rust і вызываць read_file, калі трэба пазрозумець змест файла. Гэта працуе таму, што існуе роўна адна змена, адны корыстнік і няма правіла ўсупрацоўвання адпаведзей. Потым справы пачынаюць накаплявацца. Другая змена значыць, што модэль должна навучыцца, калі трэба перыявляць файлы, а калі — чытаць іх. Трэцяя змена значыць, што трэба навучыць правілу вырашэння. Правіло выходу, такое як „цітаваць файл, які вы чытаўце“, патрабуе месца для рэалізацыі. Тэст на рэгрэсію, які пераканваецца, чы прампт даў тую ж адпаведзь на фіксаваным вхідным дадзенням, патрабуе спосабу з’явіцься, калі хтось віпадкова зменіў прампт. Кэшаванне прампта змушвае адзірваць, якія часткі зменяюцца ў кожны раз і якія — ні. Простая літэралай не можа граматычна распрацаваць нічога з гэтага.
Гэтая частка — структураваны запіт для агента пад назвай Eugene, які і здольны выконваць задачу: чатыры слоі, калькулятор для стварэння тэксту, кантэкст на кожны запіт, адзін прыклад з калькуляцыяй, хэш для выяўлення адхілень, а таксама статычны/дынамічны межы, каб кэш запітоў Anthropic платыў за персонажа аднойчы, а не ў кожны раз. Цыкл работы агента застаецца тым жа; зменяецца толькі фармат сістэмнага паведамлення.
Чаму структура дапамагае
Запіт — гэта код, ўсёлькі я керуе поведэннем, і кожна ў яму рэчкаплявочка конкуруе за увагу мадэлі. Дзяловы блок дужаўга і без размежавання часткова ігнаруецца, тады як короткі набор пазначаных частак чытаецца адзін за адным.
Гэта можа здавацца суевернам, але гэта зафіксавана практыка. Claude і яго аналогіяў навчаюцься на інструкцыях у звычайных фарматах: падзеленыя запрошэння сістэмы, запіты корыстувальця, якія ўтвараюць простыя просьбы, а таксама адпаведныя відповідзі асістэнта. Калі следаваць гэтым фарматам, мадэль менш невялікі час застаецца незрозумелай ў тым, што значыць кожны элемент, чаго яна менш схільная да галюцынацый, дае болей чыстыя выходны даныя і лепшы выбар засобаў.
Чатыры шары, паказаныя ніжэй, не ўсё ж такі едынственныя дапустымы структуры. Гэта самая маленькая структура, якая вытрымлівае змены, якія пераходзяць праз рэальных агентоў. Практычна тая ж сама структура выклікаецца ў запрошэннях сістэмы, якія Claude Code CLI стварае для себе, і гэта не ўзбачор: агенты, якія вытрымліваюць рэальных корыстувальцяў, часта выклікаюць тую ж самую структуру.
Шары
Ідэнтыфікацыя — гэта абзац пра тое, хто выступае, які вказывае на тон, экспертызу і пазычку. Прыклад: Яўген — асцярожны дапаможнік-дзеершавер у праекце Rust, які воліе чытаць код з самага аднаго джэрэла, чым здогадвацца. У яго не павінны быць ні правіл, ні фактов.
Інструкцыі — гэта правілы поведзення, кожна з яых ўжо сама по сабе ёсць пераверымаемай тэзой: выкорыстаўце list_files, каб дазнацца, што ў праекце, прычымляючыся да яго, але не прабуйце зноў вызваць функцыю, якая вярнула адказ пра памылку. Іх колькісць зрастае ў меру розвітку агента. Зберагайце іх у спісе, адтак як пункты легчэй дадаць і выдаліць, чым абзацы.
Абсалютныя правілы выходу опісваюць фармат адказу, а не яго змест: просты прозаічны тэкст без маркдауна, паведамленне пра файл, які быў прачытаны, у складках, адказы дужа 200 слоў. Яны прыменяюцца незалежна ад таго, калякі пытанне задаў карыстальніка. Разлучэнне іх ад інструкцый дазволяе змяніць фармат без парадоксальных наследкіў для функцыяналу, і навпакі.
Прыклады паказваюць, як выглядае адказ агента. Прыклад з калькамі выконвае за адну лінію тое, што для інструкцыі трэба за тры, таму ў яму відразу паказваецца гарны адказ у правільнам стылі, уключаючы цытату. Прыклады ўскладневаюць аідэнтычнасць агента, таму яны размешчаюцца ў статычнай частцы праз тэкст ідэнтыфікатора і інструкцый.
Кантэкст зберагае усё, што развічваецца з кожным запускам: дату, рэгіональна настройка карыстальніка, корневую дырэку праекта, навыкі, якія ў наступны момент доступны. Паколькі ён змінюецца, яго нельзя размешчаць у статычным стрэнгу. Ён знаходзіцца на сваёй сябе слойве пад межай кэшу і перабудовваецца з кожным запітам.
Тыпаваны будуўнік у Rust
У Rust структура ператвараецца на будуўнік, дзе кожны метод прадставляе аднойчы слой. Ён фіксуе, якія слоі ўсталяваны, а якія дынамічны, і візуалізуе межу між ямі.
let prompt = SystemPromptBuilder::new()
.identity("You are Eugene, a careful research assistant who answers \
questions about a Rust project. You prefer reading the source \
over guessing.")
.instruction("Use `list_files` to discover what is in the project before reading.")
.instruction("Use `read_file` to inspect a specific file. Do not call it on \
paths you have not seen listed.")
.instruction("If a tool returns an error, do not retry the same call.")
.output_constraints("Answer in plain prose. Cite the file you read in parentheses, \
for example: (src/main.rs).")
.example("What edition does Cargo.toml use?",
"I'll check Cargo.toml directly. (Cargo.toml) The project uses Rust edition 2024.")
.context(format!("<env>\ntoday: {today}\nproject_root: {sandbox}\n</env>"))
.build();
Кампайляр не можа многа пераканаліцаваць тут, але чытаемасць значная меры падыходзіць. Вы можэце працаваць з ланцоўкай і ведаць, што пакажае агент. Першыя пяць методаў утвараюць статычны прыфікс, а последні — дынамічны суфікс. Фарматаваны прыфікс выглядае так:
## Identity
You are Eugene, a careful research assistant ...
## Instructions
- Use `list_files` to discover what is in the project before reading.
- Use `read_file` to inspect a specific file. Do not call it on paths ...
- If a tool returns an error, do not retry the same call.
## Output
Answer in plain prose. Cite the file you read in parentheses ...
## Examples
Example 1:
User: What edition does Cargo.toml use?
Assistant: I'll check Cargo.toml directly. (Cargo.toml) ...
Суфікс ўцяжэйшы і цалкам дынамічны:
## Context
<env>
today: 2026-05-22
project_root: /Users/me/code/eugene
</env>
Загаловкі несу значэнне. "## Інструкцыі" паведамляюць модэлю, што пункты пад імі ўсё ў тым – правілы, а "## Кантэкст" указвае, што радкі пад імі – факты. Без іх два шары зліваюцца, і модэль можа спрыймаць факты як команды або наадворот. За дапамогою загаловак выбір інструментоў стае значна лепшым.
Тэгі у стыле XML, якія абрамляюць інфармацыю пра сэрאдавішча, працуюць так сама. Кантэйнер <env> ўжо тыясчы разоў быў зустрэчаны модэлем пад час навучэння, таму ён спрыймае тое, што знаходзіцца ўнутры, як стан пад час выканання, а не як кераванне. Claude Code CLI перадае свой тэрмінал, стан git, операцыйную сістэму і ідэнтыфікатор модэля ў свой сабскрыпцыя саме так, і гэта ўзор, які варта пераняць.
Ідэнтычнасць дае голас, а не правду
Гэты слой — той, які людзі частаеўш заўсёды зменяюць. Пачатківцы запоўняюць яго тверджэннямі, на прыклад, што яны ўзнаёмлівы інжынеры на мове Rust з дваццаццю годамі джэргону ў вядомыях тэхналогічных компаніях. Сказаць модэлю, што ён эксперт, не робіць яго такім. Айдэнтычнасць кантролюець спосаб выказвання: адзінатны даследнік-памагчык вагаецца перад тым, як гаварыць, інжынер, які дзейнаўча прымешчае рашэння, адпавядае стысла, а тэрплівы вучыцель все раз'ясняе.
Ён таксама вызначае ставленне да неяснасці. Фраза, якая прыказвае агенту воліць чытанне выхадных дакументаў замест адгадвання, ёсць справжнім рычагам, і модэль дзеўальна чашчэ выклікае функцыю read_file, калі такая фраза ўсёле, адтуды змienяецца тое, якая адпаведзь здаецца прыемнаю для персонажа. Спрыяйце айдэнтычнасці як кераванню дзеянням актара, а не як CV.
Інструкцыі — гэта спіс
Кожная інструкцыя павінна працаваць або не працаваць сама па сабе. Інструкцыю „Ўжыце list_files пры чытанні“ можна пераконтраляваць, адколі мадэль альбо спачатку яе выканала, альбо ні. Інструкцыю „Будзьце корысныя для карыстальніка“ нельга пераконтраляваць, таму яе трэба адхіліць.
Калі правіла суперсучаснацца, парадак мае значэнне, адколі мадэль чытае зверху в ніж, прычаму ранейшыя правіла задаюць аснову, а пазнейшыя яе уточняюць. Калі два правіла справжнім чынам суперсучаснаюцца, запіт ўскладнены, і мадэль выберае наадзвычайна. Паспрабуйце выправіць суперсучаснаванне ў самом запытэ, а не пад час выканання.
Для маленькага агента падходзяць тры-чатыры правіла. Дзесяць — цэлкам занадта, а дваццаць ўжо ёсць прычыной непрацэйнасці, адколі мадэль не можа запам’ятаваць усе і тыхо адхіляе частку з яых. Дзейсна дужы спіс зазвычай означае відсутнасць обмежэнняў для выходных даных або фактов кантэксту, якія зробілі б калькі правіла непатрэбнымі.
Обмежэнняя выходных даных аддзеляюць фармат ад поведэння
Вікорыстайце інструкцію, якая просіць відповісты ввічліво простою мовою, вказаты джэрэло у дужках і не перэвішчуваты 200 слів. У ній поеднуюцца тры аспекты: ввічлівісць ёсць частыною ідэнтычносты, а посілання і довжына адказы — частыною фармату віхідніх даных. Калі корыстувач просіць змініты фармат посілання, потрыбно відрэдагуваты лішэ одын радок у відповідному рывні, не перэчытуючы все іншэ.
Саме тут таксама застосовуецца структурованій фармат віхідніх даных. Калі наступна сістэма вімагае фармату JSON, рывень віхідніх даных мае надаваты відповідзь у гэтым фарматы, што відповідае наданій схемі. Калі корыстувач просіць навесты крокі, необхідно іх пронумеруваты. Функціонал залішаецца незміннім, тоды як фармат адаптуецца.
Прыклады показуюць, але не пояснююць
Одын прыклад часта замінюе кілька інструкцій:
.example(
"What edition does Cargo.toml use?",
"I'll check Cargo.toml directly. (Cargo.toml) The project uses Rust edition 2024.",
)
Эты прыклад паказуе фармат цітаты, адказвае на пытанне, чы годзіцца короткая адзначка, выказвае апэктыўную дужыну тэксту і дэманструея звязак между вызывам інструмента і цітатай. Нічога з гэтага няма ў інструкцыях, і гэтага не трэба.
Рызык — гэта занадта тыповае адучыцца модэлю. Якшто кожны прыклад стосуецца Cargo.toml, модэль становіцца чырплёваю ў яго. Неабходна выкарыстоўваць адзін-два прыклады, якія пакрываюць рэальны спектр вхідных дадзеных. Якшто не можна напісаць рэпрэзентатыўны прыклад, верагодна ў запіце чагосці недастаткова апісана.
Кантэкст перзначаецца з кожным запытам
Спачатку всі забываюць пра гэты аспект. У модэлю ёсць канцэнтрацыя знанняў, і ён не ведае сённяшню дату, назву вашага праекту чы якія файлы існуюць. Ён будзе здагадваліцца, і гэтыя здагадкі здаюцца правдападобнымі, хоць і ўсё ж некорэктнымі. „Верагодна серэдзіна 2024 года“ — гэта не практычная дата.
Рашэнне простае: ў кожны раз прыносіць неабходныя факты.
let today = OffsetDateTime::now_utc()
.date()
.format(&Iso8601::DATE)
.unwrap_or_else(|_| "unknown".into());
let sandbox = sandbox_root()?.display().to_string();
let prompt = system_prompt(&today, &sandbox);
Два факты: прыблізна паловы кілабайт токэнаў, і агент цяпер ведае, калі і дзе ён знаходзіцца. Косты мінімальныя, а тачнасць у пытаннях, чутлівых да даты або шляху, значна паднікае.
Слою кантэксту рэальнага агента быстра нарастае інформацыя: назвы корыстувача і праекту, актыўныя навыкі, сторанка, якая пераглядаецца, апошнія паведамлення пра адказы, найновейшы хэш коміту. Нічога з гэтага не ўзьята з думак або інструкцый; гэта даны, прызначаны для аднае запросу і падаюцыя з коду.
Межы кэша
Гэта тое, што адзінуе агента-хобі з тым, які можна запускаць за прыемныя кошты. API Anthropic адказвае праблемай кэшавання запытак: пасуйце поле system як масэвэк тэкстовых блакоў замест адной строкі, пазначыце блако з cache_control: {"type": "ephemeral"}, і ўсё, што знаходзіцца ў гэтым блако, будзе кэшавана на пяць хвілін. Кожны наступны запыт, які надае ідэнтычны прыметнік, будзе адпрацоўвацца з кэшу, і такія кэшаваныя токены вводу будуць коштаць прыблізна дзесятую частку звычайной цены.
Для агента, які працуе ў цыкле з шасцях раундав і мае персонажа з тыячай токенаў, гэта адначасова значыць скасаванне майжэ у пяці разоў плата за ввод у кожны раунд пасля першага, а таксама зменшэнне затрымкі. Эконамія зрастае з тым, насколькі стабільны ўсё, што ўключаецца ў запыт. Персонаж заўсёды застаецца стабільным, а дата сёння — ні.
let blocks = prompt.into_system_blocks();
// blocks[0] = { type: "text", text: <static prefix>, cache_control: {type: "ephemeral"} }
// blocks[1] = { type: "text", text: <dynamic suffix> } // no cache_control
Складчатая структура працюе сама сабе. Айдэнтыця, інструкцыі, абмежэнні выходу і прыклады ўсталены, кантэкст — дынамічны, і кожна частка адрасаваецца сама за сабой за своімі заголовкамі. Толькі першая частка несе маркер кэша, таму другая частка ператвараецца на новы токен кожны раз, пакуль персана застаецца у кэшы.
Самэй гэтай арыметыкі код Claude Code мае буквальны маркер SYSTEM_PROMPT_DYNAMIC_BOUNDARY, на якім базуецца логіка кэшавання. Усе, што зміняецца між раундамі, такія як з’ўязкі MCP пад час сесіі, перакрыцця мадэлі чыя мовныя прыярытэты, знаходзіцца ніжэй за гэтай лініяй. Статычны персана, які знаходзіцца вышэй, можа кэшавацца між разнымі корыстнікамі, калі гэта дазволяе структура складчатага коду.
Два правіла дапамагаюць падтрымліваць кэш у працэспрэтны стан. Прэфікс павінен быць абсолютна ідэнтычным між запытамі, таму даполніцкі прасоц, перакантрованая інструкцыя або іншы фармат даты зменяюць хэш, і вам даведзецца занова ствараць кэш. Кроме таго, прэфікс павінен складацца з 1024 токенав, прычыму Anthropic будзе яго кэшаваць. Маленькія персанажы не отрымаюць нічога, а вялікія — многа. Калі ваш персанаж короткі, дадзіце ўмест націнкі, якія заслуговаюць на свае месца, а не проста пустыя даныя.
У адпаведзе наводзіцца квітанція:
[turn 0] in=4 cache_read=0 cache_create=1247 out=89
[turn 1] in=3 cache_read=1247 cache_create=0 out=42
Установка значэння нуля створуе кеш, з якого потым чытаюцца даныя на кожному кроці. Під час распрацоўкі віводзяцца статыстыкі вікорыстання, оскількі кеш залішаецца невідымім докі не будзе віміраній.
Зберыгання дынамічніх секцій у памяці
Межа кешу дозволяе зберыгаты токені. Менша версія ідэі в памяці економіць рэсурсі обчыслювальноі потужносты. Дэякі дынамічні елементы вімагаюць значніх зусіль для створэння, напрыклад сканування файловоі сістэмі, завантажэння копіі памяці або віклік віддаленого сховішча налаштувань. Повторне выканання цых дый на кожному кроці марнуе час і ускладнюе аналіз даных.
Кожны рухомы раздзел трэба запамятаваць па імені і перасчыляць толькі праз явную анулювання, напрыклад, праз новы запіт корыстніка, /clear або /compact. У Claude Code код раздзеляе тыя раздзелы, якія можна кэшаваць (systemPromptSection), ад зменных (DANGEROUS_uncachedSystemPromptSection); для останніх неабяжна наявнасць аргумента reason, таму автар павінен практычна абяжаць прычыну разбівання кэша. Такое дзвянкайша назва ўсуперць: адзін зменны раздзел, які зменяецца ў кожны раунд, анулюе все, што знаходзіцца пасля яго, для таго запіту.
У агенце на Rust эквівалентам ён становіць рэўістар з элементамі Section { name, compute_fn, cache_break }, які адначываюцца пад час стварэння запіту, прычаму кожны рэзультат асоціюецца з ідэнтыфікаторам туру. Наразе крэйт eugene-prompt застаецца простым і перасчыляе кожны дынамічны раздзел пад час кожнага стварэння. Калі якісь з іх ўскладнены, пакуйце яго у сасветную кэш-сістэму і перадайце значэнне, адчыму задача крэйта — толькі фарматаванне, а не мемаізацыя.
Версійаванне і перагляды рэгрэсіі
Найвялікшы баг у запітах — гэты, які выйшоў неспадзевана. Хтось правяе рэчэнне, каб выправіць іншую проблему, поведзенне зміняецца і ламае сцэнарый, які ніхто не стэжыў, а жадных тэстаў няма, таму ніхто не паводзіцца.
Памагае «відблеск». Стваральнік гэшуе атрыбутаваны тэкст і прадае яго два разы: prefix_fingerprint — толькі для прыфікса, які выступае ключам кэшу, і fingerprint — для всего запіта.
const EXPECTED_PROMPT_FINGERPRINT: u64 = 0; // set after first successful run
if EXPECTED_PROMPT_FINGERPRINT != 0
&& prompt.fingerprint() != EXPECTED_PROMPT_FINGERPRINT
{
eprintln!(
"warning: system prompt fingerprint drifted (was {EXPECTED_PROMPT_FINGERPRINT}, \
is {}). Update the constant if the change was intentional.",
prompt.fingerprint()
);
}
У CI пры парабяранні фінгерпрэнта прыдатку з константай, калі яны разлічаюцца, CI падае скаргу, і вам трэба альбо прыняць змяну (адаптаваць константу і поясніць командзе прычыну), альбо адхіліць яе (вярнуць стары стан). У будзь-якім случае гэта стае відным. Той самы фінгерпрэнт, які заходзіць ад зворачэнняў, таксама паказвае точна, калі змяніўся прыдатак кэша, тады ваша наступная запітоўка не знаходзіць кэш.
Фінгерпрэнт неабходны, але недастатковы. Другаяя часць — гэта тэставанне працы: фіксаваныя вхідныя даны, фіксаваная прыемная праця і агент, які парабяруе выходны рэзультат з прыемным. Фінгерпрэнт ловіць 90% простых змян.
Рэзультат у Eugene
Цікл застаўся некалічытым. Разлікаецца структураваны запіт і спосаб, якім ён адправляецца па каналы зв’язку:
let prompt = system_prompt(&today, &sandbox);
let system_blocks = prompt.into_system_blocks();
let response = send(&http, &api_key, &system_blocks, &tools, &messages).await?;
Дзякуючы наявнасці list_files і read_file, інструкцыі i прыклад выучаюц агента як іх спраўляць адзін за другім. Калі запытаюць, што ў папке src, агент перыявляе елементы каталогу, выбірае паводлівы файл, чытае яго і адпавядае з цітатай. Калі запытаюць пра версію ў Cargo.toml, агент спачатку кажа, што ачыні маніфест, а потым называе версію (2024) з назвай файла ў складках. Размешчэнне цітаты — задача слоя выходных даных, перакананне заместо спадзявання — часць інструкцый, а стыль цітаты вылучна з прыклада. Кост вырахоўваецца як частка таго, што было б без меж кэша.
Вывыкі
Жорсткая закладкіце працюе добра, пакуль гэта не становіцца проблемай. Калі у вас ёсць калькі інструментаў, разныя фарматы выходных даных, калькі інжынераў, якія рэдагуюць інструкцыі, або якіясь бюджэтныя прагнення, структура выказваецца карыснаю. Шары не ўзначальваюцца працяжна: ідэнтычнасць — гэта хто, інструкцыі — гэта шта, выходны даныя — гэта як, а кантэкст — гэта зараз. Калі ўсё не пасходзіць пад адны з гэтых элементаў, то трэбаванні агента застаюцца нечыярымі, і тут прычына не ў структуры.
Межа кэшу ператварае структуру на асоблівую захоўку рэсурсаў. Інструмент для стварэння на основе запісаных дадзеных — гэта форма, яку Rust прыносіць безкоштовна, а «відблеск» — гэта дышуць тэставанне на зміны. Разам яны даюць вам сістэмную інструкцыю, яую можна рэдагаваць у пятнічны апоўначы без страху.
Два інструменты, які тут прысутны, ўсё ж застаюцца спецыяльнымі функцыямі ў оператывайнай запаведзі match; у іх няма спакульнага інтэфейсу, не выконваецца генераванне схемы, няма можлівасці для парадзей, а таксама няма спосабу, каб трэція сторона запускала інструмент, які агент мог бы выкарыстоўваць. Наступны логічны крок — стварэнне атрыбута Skill, рэестру, які керуе таблой дыстрыбюцыі і schemars для вылучэння JSON Schema з структуры Rust, такім чынам агент розвіваецца праз складанне, а не праз акумуляцыю.