Галоўная / Артыкулы

Выкананне правіл агента ў коде: PreToolUse, PostToolUse і Stop Hooks

Дазнаецеся, чаму разрэшэнне для агентаў LLM павінна належаць да дэтэрміністычных хукіў вызоўа інструментаў, як безпечна адмовіць у вызоўах, і як абгорнуць дыспэтчара без рекурсіі.

Выдавец: REACTAPP.TOP12 хв чытання2721 слоў

Крыніцы: LLM Agents, The Way Nobody Told You — Part 8: Hooks

Прампт с пазначэнням "толькі преміум-пользоватары можаць запрашваць вярненне грошэй" выглядае як правіла, але для модэлю ён ўособліва рэкамендацыя. Ён будзе выпалнявацца практычна для кожнага запиту, але можа не быць выпалняным у рэдкіх случаях, калі запит сформульованы досыць нестандартна. У этай статыце показана, як пераказваць правілы, якія дзейсна маюць значэнне, з прампта ў хукі — маленькія фрагменты дэтерміністычнага коду, якія запускаюцца пасля кожнага вызову інструмента, могуць бачыць доверлівы контэкст сэрвера і можуць зупініць дзеянне пры яго запуску. До канца вы зразумеете, які тип хука належыць вжываць для кожнай задачы, як адмовіць у вызое без наражання розмовы на парадоксы, і як безпечна прыўязваць хукі да існуючага дыстрыбютора інструментоў.

Як правілы прампта не вырашаюць задач у практыцы

Уявіце агента службы падтрымкі клэянта разам з спецыялістам па вярненні грошаў. У інструкцыях зазначаеца, што толькі клэйнты преміум-уровня могу ствараць заявкі на вярнення, а клэйнтам безкоштовных тарифаў трэба вважліва сказаць, што вярнення грошаў не ўключанае ў ўмовы ўжоўвачэння. Тэставанне праглядаецца нормальна: безкоштовныя корыстувачы адхіляюцца, а клэйнты преміум-уровня отрымаюць файл з заявкай.

Потым корыстувач безкоштовнага тарифа прасылае скаргу, напісаную нестандартным спосабам. Яна выглядае як запит на вярнення грошаў, хоча на самай працы стосуецца іншага віда кредыту на аблікаванні. Модель, якая створана для дапамоги, але зараз не впевнена ў формулюванні, вырашае, што ситуацыя дастаткова схожая, і так стварае файл з заявкай. Нічога ў правілах не было двусмысловага для чалавека, але клэйнт, якому ніколі не было дазволена ствараць заявкі, тепер яе мае.

Гэта тая категорыя хуків, якія існуюць для ўсунення проблем. Модель не паслабае у выпанаванні інструкцыйяў; яна працуе на апартэйніцы. Правілле, якое працуе для 99% вхідных дадзеных, можа не спрацаваць для часткі засталых 1%, і ў рэальных умовах гэта праблема прояўляецца. Калі наявна рэальная цена за наяўнасць правалу, яго неабходна прымусова выкананні за дапамогою коду, який запускаецца ў кожны момент і з якім модель не можа спарчвацца.

Тры точкі хукавання ў жыцёвым цикле вызову інструменту

Хукі прыўязуюцца да конкрэтных момантаў у жыцёвым цикле вызову інструменту. Їх трохі, і кожны з іх мае свой завадзек:

  • PreToolUse запускаецца перад выкананнем інструменту. Ён можа адмовіць у вызыве, перапісаць його вхідныя даны або проста стварыць абсалютнае спостэрэнне за яным.
  • PostToolUse запускаецца пасля таго, як інструмент выкананы. Ён можа толькі стварыць спостэрэння: запісаць, аудытуваць або паведаміць. Ён не можа змяніць тое, што вже сталася.
  • Stop запускаецца канчыцей сесіі і адпрацоўвае чысткаванне, падсумкі і збережэння данных.
  • Компактны спосаб прыгадаць іх — гэта тое, што PreToolUse запытвае, чы хтосьці, каб ўсё гэта адбылася, PostToolUse фіксуе, што гэта адбылася, а Stop пазначае канчатак сесіі.

    Этыя ролі не ўзаемназаменны. Автарызацыя належыць да PreToolUse, таму што пераказ у PostToolUse выкананы пасля таго, як дзеянне вже прабілася. Логаванне аудыту належыць да PostToolUse, таму што запіс, напісаны ў PreToolUse, фіксуе толькі намер; вызов можа ўсё ж быць адхіленым або зазнаць нявярнага результата, таму лог будзе описваць рэчі, якія ніколи не адбыліся. Спаўнайце кожную задачу з хукам, чыё часавое падзеянне яй падходзіць.

    Магчымасці, автарызацыя і правілы — гэта разныя пераказы

    Падазроўваю, вы хочаце дадаць тры новыя правілы да інструмента для карточак справ:

    • Правіла А: для карточкі прыбутку неабяжна неняпустая спіс доказаў.
    • Правіла Б: кліянтам з безплатнага тарифу не разрэшана ствараць карточкі прыбутку.
    • Правіла В: стварэнне карточкі прыбутку блакуецца з 2:00 да 4:00 ранку пад час ўсё тыя жыццёвых запускаў.

    Кожна з гэтых правілаў адхоўляе якісь з вызываў інструменту, пры тым яны належаць у трох разных месцах. Дапамагае сортаваць кожнае правілу ў адну з чатырох категорый:

    • Магчымасць, тое ўсё, чы рэшты є внутршна супарадныя, знаходзіцца ў рэалізацыі інструменту. «Спіс доказаў павінен быць неняпусты» — гэта перакананне магчымасці.
    • Разрэшэнне, тое ўсё, чы гэты вызывальнік можа гэта зрабіць зараз, знаходзіцца ў хуку PreToolUse. «Безплатныя корыстнікі не можаць ствараць карточкі прыбутку» — гэта разрэшэнне.
  • Палітры, тыя являюцца правіламі для всей арганізацыі, таксама знаходзяцца ў хуку PreToolUse. «Забараніць запіс пад час перыоду развяртання» — гэта палітра.
  • Зявы, тыя являюцца запісамі таго, што адбылася, знаходзяцца ў хуку PostToolUse. «Запісваць кожны запіс у журнал аудыту» — гэта зява.
  • Прыдатны спосаб: інструмент запытае, чы магчыма адрабатваць операцыю з аргументамі, якія ён отрымаў, тады калі хук запытае, чы адправальніку разрашаецца адрабатваць яе ў гэты момент.

    Чаму такая падзел настае на аднойчынку з тым, што можа бачыць кожны слой

    Гэта разлікаванне не ў стылістычных целях; яно вынікае з дадзеных, якія мае кожны слой. Інструмент отрымае толькі аргументы, якія генераваў модэль. Ён можа параўнаць гэтыя аргументы між сабою, паказваючы, чы список не пусты або чы кантэнт являецца паазітыўным, але у ён няма нічога іншага, чым можна было б іх параўняць.

    Хук отрымае тыя ж аргументы, плюс доверлівы контэкст, створаны серверам: ідэнтыфікатор аутентызаванага пользователя, рэальны ўраг корыстніка, час ідзе і ідэнтыфікатор сесіі. Шырокі контэкст знаходзіцца ў об’екте HookContext, створанам на пачатку сесіі і пасля таго зафіксаванам.

    Маючы гэта ў руках, хук можа думаць так: модель хоча вызваць lookup_user з параметрам user_id='u_999', але сесія належыць u_004, таму вызов адхоўваецца. Адпаведны прынт немагчыма зробіць, таму што адпаведны инструмент ніколі не бачыць, каму належыць сесія. Автарызацыя завжды значыць паўраненне недоверлівага выходу моделі з доверлівым станам сервера, і толькі хукі маюць гэта ўсё.

    Раздзеленне доверлівага контэксту і недоверлівых вхідных дадзенняў

    HookContext — это стана сервера. Яны ўласны код стварае яго, калі пачынаецца сесія, на адказе з рэсурсаў, якімі вы керуеце, такіх як шар аутантыкацыі, менеджер сесій і база дадзеных. Ён заўсёды незменны працэс сесіі, і ні корыстнік, ні модэль не можаюць яго паўніста змяніць. Наведзены нижэй frozen dataclass апісвае мінімум неабходнаўых дадзеных:

    @dataclass(frozen=True)
    class HookContext:
        user_id: str         # set from your auth layer
        tier: Literal["free", "premium"]
        session_id: str
    

    frozen=True выплывае рэальную працу: як толькі об’ект створыўся, яго нельга змяніць жадным спосабам, укладзеным у код, ваколькі і ў разе наявнасці бага ў хуку.

    tool_input, на адварот, — это словнік, які модэль размешчае ў сваёй частцы tool_use. Ставіцеся да яго абсалютна так сама, як і да тэлу прыходзячага HTTP-запиту. Корыстнік можа прабачыць модэлю стварыць практычна што заваўсе там, за дапамогою введэння паведамленняў або проста за дапамогою стойкага зберагання дадзеных.

    Перакантрольванне праваў парушыць гэтыя два элементы. Чы роўнае user_id, якое задае модель, HookContext.user_id? Чы правіла HookContext.tier == "free" выключаюць можлівасць викорыстання гэтага інструмента для запісу? Прынцып, які забезпечвае безпеку системы, прост: рашэння па кантролі безпекі должна базавацца на HookContext, а не на чым-небудзь, што задае модель.

    Адмова ў прийоме вызыву без перарыву размовы

    Відразу зрозумелы спосаб адмовы ў прийоме вызыву — праігнораваць яго. Але з API для вызыву інструментаў, якія выкарыстоўваюць формат паведамленняў Anthropic, гэта не працюе. Кожны блок tool_use, які выдае асистэнт, должен быць адпаведзены блакам tool_result, які ў наступным паведамленні корыстувальця несе той самы tool_use_id. Якщо аднаго з іх не адпаведзець, наступны запит API завершыцца з адмоўай 400.

    Такім чынам, адказ працоўкі PreToolUse павінен даць синтэтычны рэзультат, які заменяе выход дадзеныя ад інструмента. Ніжэй паказаны слоўнік на Python (зверніце ўвагу на True) з пазначэнням пра бяду, а таксама паведамленнем, які поясняе, чым была адхілена запрос і што робіць заместа таго:

    {
      "type": "tool_result",
      "tool_use_id": "toolu_01ABC",
      "is_error": True,
      "content": "BLOCKED: free-tier users cannot call prepare_case_file. Suggest upgrade or route to general FAQ."
    }
    

    Гэта задовольняе умовы API, і модель спрацоўвае з гэтым як з будзь-якім іншым адзначэнням. Яна бачыць, што запрос быў адхілены з певнай прычыны, перапрашае і прасіць выбраць іншы спосаб, і размова продаважваецца як звычайна.

    Два аспекты спакойна дапамагаюць гэтаму функцыонаваць.

    Адносны прэфікс ператварае адхіленні на сігнал

    Прыдзеак BLOCKED: не ўскладненне, якое выкалічвае API. Цэлая традыцыя. Адносова знакі важнае дапамагае модэлю распазнаць нябяжаную адмову, а не тымчасовую некарэйсамасць, тады ён зменяе падход уместо таго, каб перапрынуў той самы вызов. Той жа знак дапамагае апарату для ацэнкі пазначыць нябяжаныя рашэнні ад справжніх калекцый інструментаў пазней. Традыцыя простаг тэксту стае сігналам, які можа прачытаць машына.

    Паведамленне пра адмову — это запит

    Модэль стварыць сваю наступную адпаведзь на аднойчыну з тым тэкстам, які вы вярнёте, таму пішыце яго з той жа атрыбутабнасцю, калі пішаце запит. Проста фраза "Denied" не дае яму нічога для працы. Паведамленне, у яком называецца прычына (бесплатны ўровень не можа выкарыстоўваць гэты інструмент) і пропануецца альтернатывы (апгрэйд чы загальныя вопыты), паведамляе яму і пра тое, чаму вызов зазнаў некарэйсамасці, і пра тое, што можна запропанаваць заместа таго.

    Прыўязка хуків праз абгорткаванне дыспэтчара

    Большасць агентаў вялікі час уже маюць дыспэтчара з функцыяй call_tool, якай прыме блок tool_use і запускае адпаведную функцыю на Python. Найменш утруднёны спосаб добавіць хукі — это загорнуць тую функцыю, а не перапісваць дыспэтчара. Першы крок — заставіць сабе рэферэнс на аднойчыны версію, прытымкуючыся ні да чаго іншага:

    original_call_tool = dispatcher.call_tool   # save the reference FIRST
    

    Потым неабходна з’явіць загорнутак, які запускае ланцюг хукіў навакола зберажанай аднойчыны версіі, і установіць яго на месца старой функцыі. У публікуемым фрагменте коду выразы return result і перазначэнне dispatcher.call_tool = hooked_call_tool з’едынены ў адну лінію; на самай працэ это два разлычных заповедзі, прытымкуючыся перазначэнне знаходзіцца за межамі тела функцыі.

    def hooked_call_tool(name, args):
        event = build_event(name, args)
        if denial := registry.run_pre(event):
            return synthetic_error_result(denial)   # PreToolUse denied
        result = original_call_tool(name, args)     # call the saved original
        registry.run_post(event, result)
        return resultdispatcher.call_tool = hooked_call_tool   # NOW replace it
    

    Загорнутак выканае пяць крокаў:

    1. Ператварыць вызов інструмента на запаведзь хука.
    2. Запустыць кожны хук типу PreToolUse.
  • Якщо хтары з іх адміністрацый наскаржыцца, неабяжна вернуць синтэтычны рэзультат аднойчы; рэальны інструмент ніколі не будзе запускацца.
  • У іншым случае неабяжна вызваць зберожаны первісны інструмент і зафіксаваць яго рэзультат.
  • Запускайце кожны хук PostToolUse для аудыту, паведамленняў і логавання.
  • Зберагаць першы, чым заменіць

    Шаблон, які трэба прыменяць, — гэта зберагачча первіснага інструмента першы, чым яго заменіць, і гэта правило адносна як да декоратараў, так і да мідлвэраў і інтэрцептораў. Падумайце, што будзе, як вы праігноруеце зберожаную копію і запішыце dispatcher.call_tool = lambda *a, **kw: dispatcher.call_tool(*a, **kw). Функцыя lambda шукае dispatcher.call_tool толькі пад час своего запуску, а ў тым часе гэты атрыбут вядомае на саму функцыю lambda. Кэральны код вызвае сябе, а потым знову сябе, пакуль першы вызов інструмента не завершыцца безканальной рэкурэнцыяй і перепамятайванням стака.

    Перадзначэнне адпраўнага коду зменнай спачатку значыць, што клозура зберагае аднасоўленне да рэальнага об’екта функцыі, а не да імені, якое пазней будзе перадаўана. Таму вайпер завжды прабівае да справжняй рэалізацыі.

    Чаму хукі прыграюць правілам запитоў і статычным спискам адзінакоў

    Існуе тры звычныя спосабы заварыць агента не робіць чаго-небудзь, і кожны з іх неяк працуе:

    • Інструкцыя запита, такая як "не робіць X", можа быць проста проігноравана, калі модель не впевнена.
    • Фільтрацыя адзінакоў, тое ўсуненне адзінака з списоку цэлым, ёсць статычная і не можа реагаваць на стан пад час выконання.
    • Хукі є дэтерміністычныя і ведаюць пра стан пад час выконання.

    Фільтрацыя інструментаў застаецца значным элементам. Якщо спецыяліст у безкоштовнай версіи ніколі не патрабуе функцыю create_jira_ticket, не трэба даваць яму гэты інструмент. Модель не може стварыць блок tool_use для інструмента, пра які ёй ніколи не сказана, і можлівасць, якой у моделі няма, надзейнейшая, чым інструкцыя, якую ёй можна занедбаць.

    Фільтрацыя не падходзіць для правіл, які залежаць ад сесіі. Тое, чы рулер у безкоштовнай версіи, залежыць ад таго, хто ўвійшаў у систему: код агента ідэнтычны, але разныя сесіі дазволяюць розныя дзеянні. Статычны список інструментаў не можа гэта выражыць; хукі можа.

    Надзейны падход аб’еднвае ўсе тры элементы:

    1. Фільтрацыя інструментаў на рэвэле спецыяліста, для інструментаў, якія спецыяліст ніколі не патрабуе.
    2. Кераванне за дапамогою запитоў для мякіх прыярытэтаў і загальных падказак.
    3. Хукі для правіл, якія должны дзейсніцца без выключэння пад час роботы.

    Ёсць падобны падход у рамках фрэймворку — паглядзіце, як LangChain guardrails і middleware рашыраюцься з той жа метай.

    Адна паверхня інструментаў означае адны слой рэалізацыі

    Hooks даюць максымальны эфект, калі кожная дзеянне агента моделюецца як вызов інструмента: делегаванне пад-агентам, запыты зовнішніх API, запыты да базы дадзеных і пошук у базе знанняў. Кэпсула hooked_call_tool тады бачыць усе іх, включаючы інструменты, дадзеныя пазней, а автарызацыя, правілы і аудыт прыменяюцца аднакова да всіх дзеянаў на паверхні.

    Якбы делегаванне задачі падзецелювальнам агенту выконвалася як прымытны вызов функцыі, шар хукава ніколі б гэта не пазірэў. Для маршрутызацыі патрабавалася бы другая механізм адмаўлення, што значыць два системы для падтрымкі і два месца, дзе можуць сявацца багі. Проектаванне ўніфікаванага інструментальнага интерфейсу з самага пачатку робіць хукава незначным дадаткам; ўстановка іх пазней ператварае яе на большую переработку.

    Як дзейнае ланцоўка хукав PreToolUse

    Рэальныя системы скупляюць калькі хукав PreToolUse — напрыклад, перакананне па рангу, перакананне па вікну развяртання, падтрымка ID пользователя і ліміт выдатака. Яны запускаюцца па рэчы з такімі семантамі:

    • Перша адмова мае перавагу. Яна перерывае ланцоўку, таму пасляльнія хукавы не запускаюцца, як і сам інструмент.
    • Разрешэнне не перерывае ланцоўку. Кожны хук павінен працаваць, каб вызов могў продазвацца.
  • Выконваюцца змены. Hook B прымае данні так, як іх перапісаў hook A.
  • Распрацоўваеце ланцуг па цэне і значэнні критычнасці, ставячы спачатку дешавыя, важлівыя перакрыцці. Перакранчаванне, чы ў пользователя є безкоштовны тариф, выкарыстоўвае адзін поле; перакранчаванне тысячнаго ліміту витрац організацыі можа заставіць знадобіцца запит да базы дадзеных. Выканчаванне дешавага перакранчавання спачатку запобегае непатрэбной роботе з вызвамі, якія і так будуць адхілены, тады как ставленне найболей селектыўных умов спачатку ў клазу WHERE SQL.

    Што павінна адбыцца, калі сам хук зламаецца

    Кожны тип хука патрабуе разнай політыкі адказа на неудачы, і ўзяць яе неправільна стварае справжнія багі, а не проблемы з стылам:

    • Паказанні бягунковых адказоў PreToolUse павінны перадавацца далей. Зламаны хук-політыка — гэта дыфект. Адказваць, што процес працаваў, калі насправе не працаваў, горш за тое, каб проста зупініцца, таму трэба зупініць агента пакуль політыка не будзе вылечана.
    • Паказанні каштоўнасці PostToolUse трэба прыхавляць. Неудача логару перагледу не павінна блакануць запит корыстніка, што ў даны момант мае большое значэнне, чым самы запіс у логу. Запісваце неудачу на адзінам чанеле і продавайце працу.
    • Паказанні каштоўнасці Stop трэба прыхавляць. Сесія так і так завершваецца, таму найкращы варыянт — запісаць неудачу чысткі і выйсці.

    Хук PreToolUse, які тыха захопляе своия власныя асывентаціі, ёсць слабым месам у абароне: перакананне не было выканана, вызов пройшоў, а логі перагледу не даюць жадных пояснэнняў. Хук PostToolUse, які дазволяе асывентаціям выйсці, ператварае кожную перыводку логавання на перыводку самага агента.

    Дзе знаходзіцца логі перагледу

    У простай наладзе запісваюцца два файлы, якія можна толькі дадаўаць:

    • data/audit_log.jsonl — файл, які создае PostToolUse пасля кожнага вызову інструмента; у ям знаходзяцца назва інструмента, вхідныя даны, рэзультат, флаг памкнутка і час стварэння.
    • data/session_summaries.jsonl — файл, які ствараецца адно раз пры кожнай сесіі за дапамою хука Stop; у ям зберагаецца адна строка статыстык высокага рэвэля для кожнай размовы.

    Лог аудыту быстра стае найцэннейшым файлам у проекте. Ён ствараецца з самага пачатку і служыць для адарыўнасці, аналізу, вычыслення костоў, наладкі проблем і стварэння панелей керування на адной аснове правды. У працоўным режыме зазвычай тыя ж записы кідаюцца у надзеяны сховішч, але формат дакумента застаецца тым жа.

    Галоўныя выводы

    • Інструкцыі для запуску є рэкамендацыйнымі; будзь-яе правіла, якое па ўразе якога є рэальныя наследкі, павінна быць прымененае ў кодзе.
  • Уключыце пераконтроль на адэкватнасць вхідных дадзеных у самы інструмент, автарызацію та правілы — у PreToolUse, а аудыт — у PostToolUse.
  • Кожна рашынка па канспектам безпекі должна базавацца на зафіксаваным контэксте сярод сервера, а вхідныя даны інструмента модэлі трэба спрытваць як недоверлівыя даны запиту.
  • Адражыце выклікі, вярнуўшы синтэтычны tool_result з чысткім прыфіксам, каб размова заставалася валідной і модэль могла восстанавіцца.
  • Абгорніце дыстраб’ютор, спачатку зберагчы выначальную функцію, і направляйце кожныя дзеянні агента через гэты ўсё той жа інструмент.
  • Дазвольце неудачам у PreToolUse зупініць агента, а неудачы у PostToolUse та Stop — ліставаць і ігнараваць.