Інжынерыя абвязкі: асяроддзе выканання, якое ператварае LLM у агента
Што насамрэч прымушае агента кадавання працаваць: цыкл, дыспетчарызацыя інструментаў, якая перажывае памылковыя выклікі, і дазволы allow/ask/deny — на прыкладзе прыкладна 600 радкоў чыстага Python.
Крыніцы: Harness Engineering: Build the Runtime That Turns an LLM Into an Agent (Part 1/3)
Калі агент для кодавання працюе некорэктна, зазвычай вінаватым выступае сама модэль. Частае прычына — проблемы ў інструментальнай супорцы, якая выкорыстоўваецца для керування яе працэю.
Кожны, хто вжываў API для дапамогі у чаце, знае простую частку: апыліваеце паведамленні, атрымуее адпаведзь, можа дадае функцыю вызову. Складнай часткай ёсць пераход ад гэтага да агента, які можа знаходзіцца ў рэпазітарыі, рэдагаваць файлы, запускаць тэсты і восстанавляцца пасля таго, як ён чагось зламае. Інструментальная супорта падкрывае гэты пераход. Гэта асяроддзе выканання наваколо модэль: інструменты, правыя, сэндбоксы, апрацоўка кантэксту, керуванне памяцю і восстанавленне пасля адказоў. Модэль вырабляе толькі прыем і выдачу тэксту. Інструментальная супорта — гэта програма, якая павтарае гэты цыкл, выкананае інструменты, забезпечвае безпеку і ведае з вамі ў тэрмінале. Калі модэль — гэта двыгун, то інструментальная супорта — гэта сама машына.
Сама по сабе мадэль мовы не можа ачыць вашыя файлы, запускаць pytest, рэдагаваць код, прыгадаць размову з пасляпэўнага вечара чы стаяць на парадку, каб не напісаць rm -rf /. Усё, што яна можа, — гэта выдаваць наступныя запаведзі. Усё інша — гэта праца з інструментамі:
- Цікл: парапэўно вызываць мадэль, пакуль яна не перастане просіць пра інструменты.
- Інструменты: срэдства для работы (шэл, файлы, рэдагаванне).
- Дазволы: спытацца пры рызыкаваных дзеяннях і заблакаваць найбольш небяспечныя з іх.
- Сандбокс: змусіць небяспечныя дзеяння зазнаць нявяскоў на рэвэрсійнай сістэме.
- Кантэкст: трываць размову настаткі, каб яе можна было апрацаваць.
- Памяць: навыкі, спіс задач, файл сесіі, які можна пракрануць.
- Падагенты: дакледзіцца кан кодбэсу без заполнення галоўнай размовы.
У гэтым абзаце рассматрываюцца першыя тры элементы, якія ператвараюць чат-бота ў агента. Рэшта – сэндбоксаванне, кантэкстна адарботка, памяць і падагенты – ўсё гэта ўскладненні на той жа основны прынцып.
Чат-бот проты агента
Чат-асістэнт выконвае адну траекторыю: запыт увайшлі, адпаведзь выйшлі. Толькі такія інструменты, як Claude Code чыста рэжым агента у Cursor, працуюць інакш. Як толькі вы скажаце «перэрабіць гэты модуль», яны чытаюць файлы, запускаюць тэсты, правяць код, зноў запускаюць тэсты і лагодзяць тое, што было зламана. Прычыной ня ў тым, што мадэль болей розумная. Програма, якая працуе з модэлью, ведае, як ціклізаваць, вызываць інструменты і продовжваць работу, калі якісь крок не выйшоў.
Людзі пачалі называць гэта інжынерыям крэйнів, што ўсё-такі досыць паэлегантны тэрмін для простай ідэі: Claude Code, Cursor і Codex ёсць крашчымі крэйнамі настолькі ж, насколькі і крашчымі мадэлямі.
Прыклад дапамагае больш за адзінакі. Вы пішаце: "Вылечыце неякісны тэст у test_parser.py." І ўсё, што будзе пасля:
Turn 1: LLM reads the test file → bash("cat test_parser.py")
Turn 2: LLM runs the test to see error → bash("pytest test_parser.py")
Turn 3: LLM reads the source file → read_file("parser.py")
Turn 4: LLM edits the bug → str_replace("parser.py", old, new)
Turn 5: LLM re-runs the test → bash("pytest test_parser.py")
Turn 6: Tests pass → LLM says "Done! Fixed the off-by-one error in line 42."
Шысьць раундаў і шысьць рашэнняў, і ніхто нічога не клацнуў пасля першага запіту. Гэта прыклад таго, як прыстрой выпалняе сваю задачу.
Слоўнік
Некалькі тэрмінаў павтараюцца ў всём тэксце.
- LLM: функцыя, якая ператварае спіс паведамленняў у ўсё новае паведамленне. Яна не памятае пакананых раней вызоў API. Калі хочаце, каб здавалася, што ён памятае, трэба зноў адправіць історыю.
- Token: елемент тэксту, які лічыцца модэлем; ўсьмо гэта прыблізна тры чвярты англійскага слова. Фраза „Hello world“ складаецца з прыблізна двух токэнаў, а 100-радковы файл на Python — з прыблізна 500 токэнаў.
- Context window: колькісць токэнаў, якія модэль бачыць адразу. Значэнні прыблізна 128K для модэляў класу GPT-4 і адносна 200K для Claude здаюцца вельмі вялікімі, пакуль агент 40 разоў не запішае выход дадзеныя з
grep -rу чат.
{"name": "bash", "arguments": {"command": "ls"}}, пасля чым ваш Python запускае ls і вяртае рэзультат.call_llm(messages) ўздзейсніваецца незалежна, а ліст messages выступае як памяць.Няякога досвяду ў машыннам навучэнні не трэба. Дастатнекі базавы Python (функцыі, ціклы, слоўнікі, try/except), невялікі досвяд работы з API у стыле OpenAI і найпаўзначэйшыя команды шэлу (ls, grep, cat).
Эталонная рэалізацыя
Прыёмнік-прыклад складаецца з апошніх 600 ліній коду на Python без якіх-лібо фрэймворка, і ў яму вікорыстоўваюцца чатыры бібліятэкі: openai, rich, prompt-toolkit і pyyaml. Указаны ніжэй фрагменты ўсё ў тым самым фармаце рэальнага коду, а не псевдокоду, і напрыканце вы должны быце распазнаваць тыя ж часты ў програмах Claude Code або Cursor. Ля глыбэйшага выявлення працэсу клонуйце копію праекту і інсталюйце яе у рэжыме для рэдагавання:
git clone https://github.com/shrinidhi-mahishi/coding-harness.git
cd coding-harness
pip install -e .
Можа працаваць будзь-які канцэнтры, сумесны з OpenAI: OpenRouter, локальны сервер, сам OpenAI. Установіце настройкі ў ~/.agents/env:
BASE_URL=https://openrouter.ai/api/v1
API_KEY=sk-or-...
MODEL=deepseek/deepseek-v4-flash
Асалідныя зменныя аптэкі пераканалююць тыя файлы. Каманда coding-harness, заявленае ў pyproject.toml як точка запуску для coding_harness.agent:main, пачынае сесію тэрміналу. Вы пішаце, і внутрэшній цыкл працюе, пакуль у агента не застануцца больш вызоўаў інструментаў.
Усю структуру складае семь ідэй: цыкл агента («серцэбіт»), інструменты («раны»), правыя на выканання задач (хто можа чаго робіць), песак (безпека, якая забезпечваецца ядром), інжынірынг кантэксту (чыстае вікно), навыкі, задачы на выконанне і сесіі («памяць»), а таксама сабагенты (выкарыстоўванне кантэксту там, дзе ён можа быць знеісцяным). Лішыльве першыя тры з іх рассматрываюцца тут.
Файлы, якія маюць значэнне:
coding_harness/agent.py # two nested loops
coding_harness/llm.py # one API call + the system prompt
coding_harness/tools.py # tool functions + defensive execute()
coding_harness/permissions.py # allow / ask / deny
coding_harness/config.py # BASE_URL, API_KEY, MODEL
Няма базы дадзеных, веб-сервера чыра бібліятэкі для оркестрацыі. „Фрэймворк“ складаецца з двух цыклаў while True і словніку функцый інструментаў. Для першага чытання пераканаўцеся з config.py, llm.py, agent.py, tools.py і permissions.py у гэтай спраўе.
1. Цыкл агента
Claude Code, Cursor і Codex выкорыстоўваюць аднаковы цыкл:
- Адправіце розмову да модэля.
- Калі модэль просі інструмент, запусціце яго.
- Дадзіце рэзультат да розмовы і верніцеся да пункта 1.
- Калі інструменты ўжо не застаўся, работа завершана і паказваецца адпаведны адказ.
Чатбот адрабатвае адна поўная паўтарка, а агент продолжае работу пакуль задача не будзе завершана. Гэта ўсё разлікі, а большая частка застаючайся інжынернай працы прызначана для таго, каб цыкл не застаўся без канца.
Адзін момант плутае новякоў: модэлі не маюць стану. Яны не памятаюць пярэдні ход, таму кожны вызов API несе весь дыялог. Паведамленні выглядаюць так:
messages = [
{"role": "system", "content": "You are a coding agent..."},
{"role": "user", "content": "Fix the failing test"},
{"role": "assistant", "content": "I'll start by reading the test file."},
{"role": "tool", "content": "... contents of test file ..."},
]
Існуе чатыры ролі. Роль сістэмы выконвае постаўленыя інструкцыі, вы — корыстнік, асістэнт — гэта модэль, а інструмент выдае рэзультат, які ён прымае. Модэль прымае спіс і стварае наступны элемент, і гэты спіс являецца яе ўжоўды „памяцю“. Не існуе жаднага супернечытага другога сховішча, таму калі спіс некарэктны, то і агент таксама некарэктны.
Участвуюць два ціклы. Внутрэшній цікл пакрывае адну партыю дзеяння корыстніка і столькі партый дзеяння модэля, сколькі для гэтага патрэбна. Зовнішній цікл проста чакае на вхідны даны.
Гэта ў спрасцаванай форме внутрэшній цікл. У рэальным файле таксама выкарыстоўваюцца меры па адчысцэнню старых рэзультатаў інструментаў, дадаўчыце свежых нагадаў у канец кожнага запіту, адчысцэнню тымчасовых файлоў і можлівасці сціскання історыі.
while True:
message, usage = call_llm(messages)
messages.append(message.model_dump(exclude_none=True))
if message.content:
ui.agent(message.content)
# No tool calls? The agent is done thinking.
if not message.tool_calls:
break
# Execute each tool and feed the result back
for tool_call in message.tool_calls:
args, result = execute(tool_call)
ui.tool(tool_call.function.name, args, result)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
Код знаходзіцца ў agent.py. Функцыя call_llm(messages) адправляе всю історыю запытанняў, і мадэль вяртае тэкст, выклікі інструментаў або і тое, і іншае. Функцыя model_dump() проста ператварае об’ект API на слоўнік, які можна зберагчы. Калі выклікаў інструментаў няма, цыкл завершаецца. У іншым случае воны выкананы, паказаны, рэзультаты даданы, і цыкл пачынаецца зноў, ўжо калі мадэль бачыць, што адбылося, і можа вырашыць, чы робіць далей.
Зовнішній цыкл ўсё болей маленькі:
while True:
user_input = ui.ask()
if not user_input:
break
messages.append({"role": "user", "content": user_input})
# ... inner agent loop runs here ...
Ваш тэкст падае ў спіс, внутршній цыкл працуе да таго часу, пакуль агент не завершыць сваю працу, а пасля програма зноў чакае. Не існуе машыны станоў — толькі два вкладаныя выразы while True, якія ўжо ёсць у Claude Code і Cursor. Мадэль гаворыць або прасіць пра інструменты, рэзультаты ўжыцця інструментаў падаюцься назад у messages, і цыкл павтараецца.
2. Інструменты: рукі для мадэлі
Без інструментов цыкл — гэта проста чат-бот з дадатковымі крокамі. Інструменты — гэта спосаб, якім мадэль выкананае задачы: чытанне файлаў, запуск команд, напісанне коду, пошук у рэпазітарыі.
Уявіце сабе, калі прасіце гэніяльную людзіну вылечыць баг, калі яна сядзіць у замкнутай кімнаце без камп’ютара. Яна можа думаць, але не можа нічога трагнуць. Інструменты — гэта дзверы.
Багатыя навучальныя матэрыялы прыховваюць адну важную прамыслу: інструменты ніколі не выкананы самым модэлем. Модэль стварае запіт на кшталт „заўядзіце гэты каманду“, а сам Python выкананыя задачы і адправляе рэзультаты назад. Модэль не мае шэлу, толькі форму.
Інструмент складаецца з чатырох элементаў: назвы (bash, read_file), апісання, якое паведамляе модэль, калі яго трэба выкарыстоўваць, JSON Schema для аргументаў з обав’язковымі полямі і тыпамі, а таксама функцыі на Python, якая выкананыя задачы.
bash ўсё часоў ёсць як наймоцнейшы, так і найбольш вызываючы інструмент. Ось яго код з tools.py:
def bash(command: str) -> str:
"""Run a shell command and return its combined stdout and stderr."""
try:
result = sandbox.run(command)
except subprocess.TimeoutExpired as expired:
return (
f"Timed out after {expired.timeout}s and was killed. "
"Narrow it down - search inside the working directory rather than /."
)
return (result.stdout + result.stderr) or "(no output)"
Тое, што бачыць мадэль, — гэта схема:
{
"type": "function",
"function": {
"name": "bash",
"description": "Run a shell command and return its combined stdout and stderr.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to run",
}
},
"required": ["command"],
},
},
}
Калі мадэль хочаць паведамленне па каталог, яна выдае {"name": "bash", "arguments": {"command": "ls -la src/"}}. Код яго запускае, вяртае выходныя даны, а мадэль выбірае свой наступны крок.
Агент распаўляе сячына інструментамі, кожны з якіх — гэта функцыя плюс схема:
bashзапускае каманду шэлу праз сандбокс.read_fileчытае файл.write_fileстварае або заменяе файл.str_replaceвыконвае тонкую правку, заменяючы адну строчку на іншую.read_skillзавантажае кнігу інструкцыйSKILL.md.write_todosзаменяе весь спіс задач.taskзапускае падагента і вяртае толькі яго заканчоўны адказ.
Функцыя bash ўособліва проста: яна запускае sandbox.run з таймаутам у 60 секунд і вяртае даныя з stdout і stderr, або паведамленне пра таймаут, якое радзіць шукаты ў каталозе праекта, а не ў каталозе /.
Спрыяйце тому, каб вызовы інструментаў былі спрацьвоўваны як ненадзеяны вхідны даны
Большасць пастаў на тэму „Створыце агента за 20 ліній“ прыхоўдзяюць гэты аспект. Вызов інструмента — гэта тэкст, які напісаў мадэль, таму модэль абаранення падобная да веб-формы. Імя можа не існаваць, аргументы можа не быць правільным JSON, а тыпы — некорэктнымі. Будзь-якая з гэтых проблем можа завершыць сесію, калі код не падготаваны.
Мадэлі ствараюць халюцынаціі: яны выдумваюць назвы функцый і выдаюць несправны JSON. Аварыя змушвае корыстувальніка пачаць зноў. Калі вы выявіце проблему і вернёце чытальныя паведамлення пра адказку, мадэль часта прабуе знову і робіць гэта на дзівную лепа, як толькі вы даеце ёй саму адказку, а не стэк-трэйс.
execute() — захоўнік-дыспэтчэр, які таксама знаходзіцца ў tools.py, працюе па следуючай сэрыі:
- Аналізуе JSON; калі ён несправны, вяртае памятку пра адмоўленне з пазначэннем, што аргументы не ў фармаце валіднага JSON.
- Шукае назву інструмента; калі яна невядома, паказвае справжнія інструменты.
- Звертаецца да
permissions.check()для атрыбута deny, ask або allow. - Выклікае функцыю на Python.
- Ловіць
TypeErrorі будзь-яе іншае асоцыяцыю, вяртае рошток „Error: ...“ і ніколі не вяртае трабекст.
Короткая вярсія: гэты дыспэтчэр направляе вызов інструмента да функцыі, не спрычынюючы закінчэння сесіі.
def execute(tool_call):
name = tool_call.function.name
# The model might send broken JSON
try:
args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as broken:
return {}, f"Error: arguments were not valid JSON ({broken})."
# The model might hallucinate a tool name
if name not in TOOLS:
return args, f"Error: no tool named '{name}'. Available: {', '.join(TOOLS)}."
# Run through the permission layer, then call the function
try:
action, reason = check(name, args)
if action == "deny":
return args, f"Blocked by policy: {reason}"
if action == "ask" and not ui.approve(reason):
return args, "The user denied this tool call."
return args, TOOLS[name](**args)
except TypeError as mismatch:
return args, f"Error: wrong arguments for {name} ({mismatch})."
except Exception as failure:
return args, f"Error: {name} failed - {type(failure).__name__}: {failure}"
Дзякуючы таму, што мадэль стварыла інструмент пад назвай search_code, JSON-даныя ў парадку, але гэтае імя ня ў спіску TOOLS; таму дыспэтчэр адпавядае, што такога інструмента ня існуе, і перыядае наявныя інструменты: bash, read_file, write_file, str_replace, read_skill, write_todos і task. У наступны раз мадэль выкарыстоўвае bash з командай grep -r "auth" .. Нічога не зламалася, і гэта сама мета: некарэктны выклік інструмента ніколі не павінен зупініць працу сесіі. Адказы на адказы — гэта проста ўсё тое ж уведзенне.
3. Правыя: хто можа чаго робіць
Калі ёсць bash, агент становіць сабе ў парадку, але гэта таксама можа прывесты да выканання команды rm -rf / чы ў выкліканні вялікай канфідэнцыйнасці. Якшто агент можа выканаваць будзь-што, ён можа выдаліць файлы, выклаць непатрэбныя даныя чы выкліканыць зовнішнія рэсурсы. Якшо вы падтверджвалі кожную команду, вам даведзяся бы весь вечар пасвяціць на затверджэння простай команды ls. Патрэбны серэднія варыянты.
Адказ — тры рэвэны, як светаўкі на перакрэсці:
| Рэвэн | Значэнне | Прыклады |
|---|---|---|
| allow | выконваецца без звуку | ls, grep, cat, git status, pytest |
| ask | зупініцца на ваша чаканне | mkdir, python script.py, npm install |
| deny | блакуецца нават калі вы сагласны | rm, sudo, curl, wget, git push |
Правілы — гэта шаблоны глоба, дзе * падходзіць для любога значэння, і выграе апошнее падходзячае правіла. З файла permissions.py:
BASH_RULES = {
"*": "ask", # default: ask for everything
# read-only: let them through
"ls*": "allow",
"cat *": "allow",
"grep *": "allow",
"git status*": "allow",
"git diff*": "allow",
"pytest*": "allow",
# risky: never, even if the user says yes
"rm *": "deny",
"sudo *": "deny",
"curl *": "deny",
"git push*": "deny",
}
* пункт задае стандартны статус «запытайце». Каманды, якія можна выканаць толькі для чытання, дазволяюцца, а парадоксальныя каманды блакуецца без адзінаго выключэння; усе іншае, такія як python script.py або npm install, застаецца з статусам «запытайце».
Мадэль будзе прыкладваць хітрыя трукі. Каманда на кшталт grep foo | rm -rf / пачынаецца безнэбяжна, а заканчваецца катастрофічна. Таму пераканальнік дзеліць каманду на часткі па знаках |, ; і &&, вырахоўваючы значэнне з усіх частак з урахоўваннем лапок, і прыменяе найстрогейшы вердыкт.
def decide(command):
"""Rate every part of a compound command; the strictest verdict wins."""
verdicts = []
for part in split_command(command):
action = "ask"
for pattern, rule in BASH_RULES.items():
if fnmatch(part, pattern):
action = rule
verdicts.append(action)
for strictest in ("deny", "ask"):
if strictest in verdicts:
return strictest
return "allow"
У практыцы запуск grep foo | rm -rf / заблокаваны, таму што запуск rm заблокаваны. Запуск ls && mkdir test падтрымліваець, адколі mkdir не ў спіску дазволеных задач. Запуск cat file.py | grep TODO дазволены, таму што обе часты задачы ў рэжыме толькі для чытання.
Правы на выконанне і механізмы ізоляцыі выпалююць разныя задачы. Правы на выконанне стосуюцца корыстніка: чы гэта варта таго, каб вас перасканаваць? Яны не ўтвараюць абароны, таму што хітрая команда можа выглядаць як ls, але насправе быць чымось іншым. Механізм ізоляцыі ўтварае абарону, таму што ён відпавядае на пытанне, чы дзеянне взагалі дазволенае фізычна. Рэальны агент патрэбуе і таго, і другога.
Што гэта дае вам
Ціклы, захоўніцкія інструменты і правы на выконанне вялікім чыннікам дапамагаюць стварыць функцыональны кодавальны агент. Ён можа чытаць рэпазітары, запускаць тэсты, рэдагаваць файлы, запытаць перад выкананнем якой-небудзь нерозумной дзеянні і вярнуцца да нормальнага стану пасля некорэктнага вызову інструмента.
Гэта яшчэ не безпечна, таму што рашучы мадэль все ўсё можа завандажыць. Гэта таксама не эфектыва, адколі вікна кантэксту запоўняецца. А ў яе няма памяці, таму кожная сесія пачынаецца з нуля. Сандбокс на рэвэле ОС, які не дазволяе разрушным дзеянням выконвацца ў ядрэ, плюс мнагаслоўнае кераванне кантэкстам, якое дапамагае довгім сесіям застацца цілымі, — гэта наступныя логічныя крокі. Гэтыя ж прымітывы вікорыстоўваюць прылады для прыменення ў працэсе вырабніцтва, толькі без усіх таўранняў.