Тры ўпарцеленыя засобы LangChain, якія не дазваляюць агентам вываліваць секрэты
Забараніце агенту адпраўляць ключы API да модэлю чырага выкасываць файлы без запіту: PIIMiddleware, HumanInTheLoopMiddleware, ліміты вызоў і чаму парадак мідлвэра вялік адзначэнне для безпекі.
Занесенне чаго-небудзь чутлівага ў вікно чату — гэта простая задача, але лёгкая і забыць пра гэта. Можа, файл патрапіў у ChatGPT для дыбаггіну, і пасля аправлення вы застылі, думаючы, чы не было ў ям API-клуча, пароля да базы дадзеных чы токена кліента. Вы перачыталі паведамленне, закрылі вкладку і рашылі, што, верагатна, усё гаразд. Але гэта, верагатна, не супакоўвае з правіламі.
Агент толькі пагоршвае сітуацыю, адтакоў які робіць тое ж самае аўтаматычна, многа разоў за сесію і без паведамлення вам. Як толькі ям даюць доступ да кодавой базы для перагляду, ён у канцэ настаўіць файл .env, які містыць ў сабе ўсё такое, як OPENAI_API_KEY=sk-proj-.... Без жадных захоўнікаў гэты клуч прайходзіць без змян да мовнага модэля, патрапляе ў логі трэйсу і застаёцца ў запісах прадавця. Чы гэты клуч будзе праходзіць далей, у набор для тонкай налады чы ў службу логавання, яку вы не бачыце, з вашай стороны немагчыма дазнацца.
Чыраз-пяц разоў пасля таго той жа агент вызывае інструмент для чысткі і удаляе каталог, пра які вы ніколі не згадвалі, тыхо фіксуючы delete_directory(path="./tmp_backups") і праходзячы далей. Вы адкрываеце гэта, калі файл, які вам патрэбен, знікае.
Гэта два адзелёных бяглыя канфлікты, без бага і без значных халюцынацый. Агент проста не меў чыстай межы межу тым, што ён мог урабіць, і тым, што ён павінен урабіць. Рашэннем являецца мідлвэр, і тыя ж ідэі лежачыя в основе захоўненняў у інструментах такіх як Cursor і GitHub Copilot. Цікава, як іх можна аднесці да вашага сабеўскага агента.
Што робіць мідлвэр
Уявіце агента як лінію віробнца. Даныя надходзяць ад вас, з файлоў і з рэзультатаў пошуку, праходзяць да модэлю, які прымечае рашэнні, якія ператвараюцца ў дзеяння. Мідлвэр — гэта пункт контролю на гэйной лініі. Ён перэхопляе даныя, адбывае перакананне чы іх трансфармацію, а потым праходзіць іх далей без змян, зменяе іх чы зупіняе.
Час размешчэння ёсць усё. Калі запаздзілі, чувствягучыя даныя ужо перайшлі межу. Калі занадта рана, вы пераглядаеце не тое.
Секрэтны скануванне Copilot адбываецца на стадыўцы выходу. Перш чым прапанова з’явіцца ў вашам рэдагары, яго пераканалівае прыстрой для пашуку шаблонаў, і все, што мае форму ключа, які пачынаецца з sk-, ghp_ або AKIA, засланяецца. Функцыя Review Changes у Cursor адбываецца перад кожным запісам файла: выконанне чыста завяршваецца, паказваецца разлік і агент не можа продаваць далей, пакуль вы не схваліце. Калі вы адмовіце, запіс не адбудзецца, і агент шукае іншы спосаб.
Одна частка захоўвае тое, што выходзіць з модэлю, а другая — тое, што модэль можа зрабіць. LangChain дазволяе стварыць аба варыянты, а таксама задаць ліміты рэсурсаў.
Захаванне секрэтнаў: PIIMiddleware
Ваш агент ніколі не працюе безпасэчна з файлавой сістэмай. Ён бачыць толькі тое, што падае ў яго вікно кантэксту, тое есць тое, што вы запісвалі, і тое, што вяртаюць яго інструменты. Таму чувствітлыя даны можуць прайсці толькі двумя способамі: вы іх вклеюеце, альбо інструмент чытае іх з дыска, напрыклад, калі сканаванне каталога ачынае файл .env. У обох случаях даны праходзяць праз шар мідлвэра, прычыму модэль нічога не бачыць, і самэль гэты момент і ўтрымлівае PIIMiddleware.
Існуе флаг apply_to_tool_results, які большасць нарадчыкаў вжывае без адказаў. Яго стандартны значэнне — False. Увядзіце толькі apply_to_input=True, і тады будзе пакрыты случай, калі вы вклеюеце чувствітлыя даны, тады як случай, калі агент чытае файл, застаецца неконтрольаваным. Агент з правамі на доступ да файла з радасцю збірае цэлы файл .env як рэзультат работы інструмента, і модэль яго отрымае.
Мінімальная правільная настройка выглядае так:
from langchain.agents.middleware import PIIMiddleware
PIIMiddleware(
"email",
strategy="redact", # replaces match with [REDACTED_EMAIL]
apply_to_input=True, # scans what you type
apply_to_tool_results=True, # scans what tools return - never skip this
)
Ключы API ёсць тыя, якія спрычынаюць рэальныя проблемы, і жадны датчык запасоў не ведае фармата вашага ключа, таму вы павінны запрацаваць рэгулярны выраз. Штырый пакрывае распашчатыя прэфіксы:
import re
API_KEY_PATTERN = r"(?:sk-|ghp_|AKIA)[a-zA-Z0-9]{20,48}"
# sk- → OpenAI and Anthropic keys
# ghp_ → GitHub personal access tokens
# AKIA → AWS access key IDs
PIIMiddleware(
"api_key",
detector=API_KEY_PATTERN,
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True,
)
Мідлвэрф практыкуе чатыры стратэгіі:
redactзаменяе падпадаючы тэкст на[REDACTED_FIELDNAME]. Ён падходзіць для логаў, трэйсоў і однонаправленых пайплайнаў.maskзаменяе сімвалы на звёздкі, але застаўляе дыяпазон дужоў, што ўжыткова, калі хочаце паказаць, што значэнне існуе, але не афарызаваць яго.blockвыклікае асключэнне і завершае весь працэс. Ожывайце яго, калі тольерантнасць равная нулю, у сэрвісах з высокай безпекой.hashзаменяе тэкст на дэтэрміністычны хеш, што корыстна, калі трэба следзіць за аб’ектам без зберагання яго значэння.
Большасць людзей супакоўваюцца, што block — гэта найбезпечнейшы варыянт, але для рэзультатаў працы з інструментамі гэта часта вярны выбор. Напрыклад, якшо агент чытае 30 файлаў падчас сканування, а ў аднам з іх ёсць адрэса электронной пошты ў коментары, block завершае всю сесію. Функцыя redact усувае гэты даныя і дазволяе працу продаваць, што практычна завжды ёсць бажаным рашэнням. Вярны block толькі тады, калі сама наявнасць чутлівых дадзэнняў значыць, што агент дасяг ў месца, куды яму не трэба было прасунуцца.
Прабавіце сабе: якшо задана настройка apply_to_input=True, а apply_to_tool_results застаецца ў свайм стандартным стані, якая конкрэтная сітуацыя застанецца без захавання? Вы павінны магчымае адказаць за адну фразу.
Зупінка тыхоўых відалень: HumanInTheLoopMiddleware
Верніцеся да выдалены каталог. HumanInTheLoopMiddleware — гэта патэрн Cursor’s Review Changes, ператворэны ў кодовую прымітыву. Перш чым запускаецца які-небудзь знашчальны або маніпуляцыйны інструмент, агент на час зупіняецца і запісвае свой стан. Вы бачыце точна, што ён намераваецца зрабіць, і павінны яго санкціонаваць, прытаму што нічога не будзе выканана.
Зупінка — гэта лёгкая частка. Адзначэнне працы — гэта складная частка. Працава ў том жа стане, у якім быў агент, можа пасля гадоў чы дзён і пасля перазапуску процэса, вымагае, каб стан размовы, незавершаная дзеянне і месца агента у яго логічных выводах былі запісаны ў якомусь надзеямым сховішчы. Гэта сховішча называецца checkpointer. Без яго выкананне перэрвы прыводзіць да знікнення стану, і наступны вызов пачынаецца з нуля, таму механізм санкцыявання проста завершае сесію.
Пауза выйшлі пасля таго, як мадэль прыняла рашэнне, што рабіць, але да таго, як запускаецца які-небудзь інструмент. Гэта ўсё, што можна выкорыстаць, калі ўжо прынятае рашэнне відна і нічага неабяжнага не адбылася.
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.sqlite import SqliteSaver
# The checkpointer is not optional. Without it, resume is impossible.
with SqliteSaver.from_conn_string("./review_agent.db") as checkpointer:
agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[read_file, list_directory, write_file, search_codebase],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True, # always pause before writing
"read_file": False, # reading is safe - no pause needed
"list_directory": False,
"search_codebase": False,
}
),
],
checkpointer=checkpointer, # saved to SQLite, persists across restarts
)
Продакцыя пасля затверджэння выглядае так:
# First call — agent hits the interrupt at write_file and pauses
result = agent.invoke(
{"messages": [{"role": "user", "content": "Review and fix the config files"}]},
config={"configurable": {"thread_id": "session-001"}}
# thread_id ties the saved state to this specific session
)
# result.interrupted == True
# result.pending_tool_call == {"name": "write_file", "args": {"path": "src/config.py", ...}}
# You show this to the user and wait for approval
# User approves - resume the same thread
final_result = agent.invoke(
Command(resume=True),
config={"configurable": {"thread_id": "session-001"}}
# same thread_id - loads state from the checkpointer and picks up where it stopped
)
Адзін факт можа «з’есць» вечар. interrupt_on={"write_file": True} падае ў співпаданне з назвай інструмента як точным рэчаменем. Калі ваш інструмент насправды называецца file_write або update_file, перэрыв ніколі не выйшае, і вызов пройдзе без жадных наследків. Пераканаўцеся ў лог-файлах апісу пра рэальную назву, калі інструмент запускаецца, і падберыце параметры па ёй.
Два ліміты для двух спосабаў абыякавасці
У кожным кроку агент вызывае мадэль, якая выбірае дзеянне і іноды запускае інструменты, пры чым адзіны вызов мадэлі можа адразу запусціць калькі інструментных вызоў. Гэта разлучныя рэсурсы, і адні не врачуецца ў другія.
ModelCallLimitMiddleware лічыць колькісць вызоваў модэля, тады як ToolCallLimitMiddleware лічыць колькісць выкананняў інструментаў. Дзесяць вызоваў модэля, кожны з якіх пераходзіць у тры паралельныя вызовы інструментаў, даўае ў суме 30 выкананняў, і сама межа для вызоваў модэля ніколі гэтага не зазначыць. Гэта таксама працюе і у зворачным напрамку: агент, які ціклічна выкарыстоўвае сябе для рашэння задач, продовжае вызываць модэль, не чыпаючы інструменты, таму лічылка інструментаў застаецца равной нулю і ніколі не перакроюеся.
from langchain.agents.middleware import ModelCallLimitMiddleware, ToolCallLimitMiddleware
ModelCallLimitMiddleware(
max_calls=30,
on_limit="raise", # raises MaxCallsExceeded - catch this in your application
)
ToolCallLimitMiddleware(
max_calls=60,
on_limit="raise",
)
Іспользуйце оба варыянты, адколькі кожны пакрывае тое, чаго не можа іншы.
Што стосуецца on_limit="raise" проты on_limit="return": выберыце "return" для інтэрактыўной сесіі, дзе частковы рэзультат можа быць корыстным, напрыклад, стасунак таго, што было пераглянута ўжо. Выберыце "raise", калі незавершаны рэзультат могаў бы нарабіць проблемы ў подальшых кроках.
Таксама будзьце астаткавы з ціфрамі. Ліміт max_calls=10 здаецца безпечным прагам для шыранага тэсту, але агент з багатакроковым планам можа выкорыстаць 8 вызоў ужо за першы свой крок. У такім случае ліміт перарве яго ў серэдзіне задання і будзе вярнуць скрацаны рэзультат, і вам можа знадобіцца 20 хвілін, каб зрозумець прычыну, перш чым логі ўсё поясняць. Рашэнне не ў тым, каб скасаваць ліміт, а ў тым, каб задаць яго на рэальныя потрэбы задання.
Парадак — гарантыя безпекі
Мідлвэры запускаюцца за той порадк, які вы ўказалі, і гэта не проста дробная дэталь. Калі які-небудзь мідлвэр, які перапісвае або структуруець тэкст паведамлення, знаходзіцца раней за PIIMiddleware, то перакрытчык PII скануе тэкст, які ужо быў зменены. Калі змяны стыснулі, перафразавалі або перэструктуравалі тэкст так, што ваш шаблон больш не падходзіць, перакрытчык яго працягае праз, а секрэт ужо знаходзіцца ў кантэксце мадэлі, і перакрытчык працюе пасля настання шкоды.
Для стака безпекі тут порадок такі:
middleware=[
# PIIMiddleware always first — it must see raw, untransformed data
PIIMiddleware("api_key", detector=API_KEY_PATTERN, strategy="redact",
apply_to_input=True, apply_to_tool_results=True),
PIIMiddleware("email", strategy="redact",
apply_to_input=True, apply_to_tool_results=True),
# Limits next - exact position within the group is flexible
ModelCallLimitMiddleware(max_calls=30, on_limit="raise"),
ToolCallLimitMiddleware(max_calls=60, on_limit="raise"),
# Human-in-the-loop last in the safety group
# (it fires in the after_model hook regardless of list position,
# but last is a readable convention)
HumanInTheLoopMiddleware(interrupt_on={"write_file": True}),
]
Спачатку трэба спрыяць, каб спіс быў розглядваны як трубопрацес, а не як рэйстры плагінаў. Чатверты элемент не отрымае первоначальнага прыемлівання. Ён отрымае тое, што залішылі элементы з 1-га па 3-й. Перамясціце PIIMiddleware пад усё, што вплывае на содэржанне прыемлівання, і гарантыя тады знікне.
Іншая перапактовка: што паспяўце, калі PIIMiddleware будзе распакаваны пасля мідлвэра, які стварае апূরненне історыі размовы? Калі вы можэце адпаведаць без неабходнасці праскроліваць верх, значы правіло парадку ўжо даследжанае.
Стварэнне агента для перагляду коду
Цяпер з’едыніце усё гэта ў агента, які будзе дзейсцоваць як комерчыя інструменты. У ёму чатыры інструменты: read_file, list_directory, write_file і search_codebase. Іх абгрунтаваюць тры слоі безпекі: секрэты ніколі не дастаюцца да мадэлі, запісы патрабуюць вашага затверджэння, а выкарыстоўванне рэсурсаў мае строгі ліміт.
from langchain.agents import create_agent
from langchain.agents.middleware import (
PIIMiddleware,
HumanInTheLoopMiddleware,
ModelCallLimitMiddleware,
ToolCallLimitMiddleware,
)
from langgraph.checkpoint.sqlite import SqliteSaver
# Custom pattern for secrets common in codebases
API_KEY_PATTERN = r"(?:sk-|ghp_|AKIA)[a-zA-Z0-9]{20,48}"
# sk- → OpenAI / Anthropic keys
# ghp_ → GitHub personal access tokens
# AKIA → AWS access key IDs
with SqliteSaver.from_conn_string("./review_agent.db") as checkpointer:
agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[read_file, list_directory, write_file, search_codebase],
middleware=[
# 1. Catch secrets before they reach the model - on both surfaces
PIIMiddleware(
"api_key",
detector=API_KEY_PATTERN,
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True, # this is the one that catches .env reads
),
PIIMiddleware(
"email",
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True,
),
# 2. Hard resource limits - stops infinite loops and runaway sessions
ModelCallLimitMiddleware(max_calls=30, on_limit="raise"),
ToolCallLimitMiddleware(max_calls=60, on_limit="raise"),
# 3. Approval gate - nothing gets written without your explicit sign-off
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True,
"read_file": False,
"list_directory": False,
"search_codebase": False,
}
),
],
checkpointer=checkpointer, # required for interrupt/resume to work
)
Калі агент хочаць напісаць файл, працэс выглядае так:
Agent: I'd like to update src/config.py to fix the circular import.
Here is what I plan to write:
--- src/config.py ---
from typing import Optional
from pydantic import BaseModel
class Settings(BaseModel):
debug: bool = False
database_url: str = "sqlite:///app.db"
...
Approve this change? (yes/no)
User: yes
Agent: Written. Moving to tests/test_config.py next.
The agent cannot touch anything silently. Every write surfaces for review before it happens. Secrets in any file the agent reads are replaced with [REDACTED_API_KEY] before reaching the model — which also means they never appear in the approval prompt you read. What you are reviewing is already clean.
Есць два простыя расшырэння, якія варта знаць. Якшча база коду мае даныя корыстніка, напрыклад номеры тэлефонаў у тэстовых фіксатарах, дадзіце элемент PIIMiddleware("phone", strategy="redact") раней, чым элемент для электронных паштоў. Кожны дэтэктар PII працуе незалежна з тым самым вхідным дадзеннем, таму парадак унутры групы не мае значэння. А калі вы хочаце, каб працэс завершыўся паважна, а не раптам, зменіце on_limit="raise" на on_limit="return" у обох мідлвэрах для контролю лімітаў, і агент вернуе тыя частковыя рэзультаты, якія у ўсём разе ёсць.
Цяпер агент не можа выкрацаваць секрэты для мадэлі, не можа зменяць файлы без вашага разрашэння і не можа працаваць па-за установленым лімітом рэсурсоў. Жыцьцёвае абмежэнне застаецца: агент не можа падтрымваць цэлеспрамованасць працы працоўнай сесіі. Аналіз сотняў файлоў з дзесяткамі вызоваў інструментоў заполніць вікна кантэксту прычынай таго, што агент не зможа завершыць работу, таму ён втрачае прыгляд да таго, што ўжо расследаваў, а яго прыказы становяцца неконтрольаванымі. эта адна проблема, не з той, якую ўсунулі тут — адносна контролю над тым, чаму можа прыкладзіцца агент.