Стрымінг у LangChain Deep Agents: жывыя планы, выклікі інструментаў і токены
Замяняем блакавальны invoke на стрым, каб карыстальнікі бачылі працу агента: тры рэжымы стрыму, прасторы імёнаў для субагентаў і фільтрацыя токенаў падсумавання.
Крыніцы: LangChain Deep Agents: Your AI Agent Is Not Frozen. It Just Cannot Tell You It Is Working. II
Мало чаго так шыбкая разрушае доверле, як заціпенелы экран. Калі корыстнік не можа з’ясавіць, чы робат працуе, застраг чы ужо не працуе, ён прыпускае найгоршае. Стрымінг заменяе гэтыя мовчанні на жывыя сігналы: план, вызовы інструментаў і словы праказуюцца ў момент ўзнікнення, і людзі можаць бачыць, што ўсё яшчэ відбываецца.
Чаму invoke прыменшвае актыўнасць глыбокага праца
Спробуйце запустыць глыбокага праца для рэальнага заведама, калі хтось стаўіць спагляданне. Вызов праходзіць праз invoke, пасля чаго экран застаецца нерухомым на чатыры дзесяткі секунд. Унутры робат чытае файлы, складае план, запускае інструменты і аналізуе сітуацыю. Корыстнік бачыць толькі нерухомыя элементы і індыкатор загрузкі, што можа значыць як аківную працу, так і тыхі збой. Паколькі гэтыя станы выглядаюць аднакова, людзі перазавантажваюць стораніцу і падаюць заявку пра тое, што працэс застраг.
Нічога не было павішана. У агента проста не было спосабу для самаўыказвання. Блакі invoke працуюць за сваёй сутнасцю так, што віконваюць увесь цыкл непазорна і вяртаюць адно готавое рашэнне, што абсалютна падходзіць для скрыпту чыра кран-задачы, якія ніхто не сочыць. Гэта неправы выбар, калі з іншай стороны сядзіць чалавек.
Рашэнне застае цыкл без змян. Ён заменяе блокуючы выклік на стрым, так што планы, выклікі інструментаў і токены праказуюцца у рэальны час.
З invoke на stream
Стрымінг тычыцца ціха да invoke. Уместо чакання на фінальны стан вы віконваеце цыкл над agent.stream(...), і кожны фрагмент прыходзіць, калі цыкл выканае якісь дзеянні. Асінхронны аналаг agent.astream(...) мае той самы фармат і яго іменавае веб-сервер.
Ментальная мадэль ўзмежаваная. Надзеяная структура LangGraph, якая знаходзіцца пад глыбокім агентам, выдае запіс пры кожным кроку, а потокавая сістэма перадае гэтыя запісы ў момент ўтворэння. Гэта не іншы агент і не іншы цыкл, а проста вікна ў той агент, які вы вялікі час ведаеце.
Спачатку трэба выясніць адну дэталь, калі ўсі прыклады залежаць ад яе: фармат потоку v2, які актывацыюецца за дапамогою version="v2". Кожны фрагмент — гэта маленькі слоўнік з трыма ключамі:
typeпазначае, якіга роду ёсць запіс.ns, тое есць простор імен, пазначае, хто яго створыў.dataмае ў сабе самы даны.
Гэтыя тры ключы застаюцца тымі ж, незалежна ад таго, што вы перэдаеце па потоку, што робіць код чытабельным, а не суматанкай з розпаковкай туплів.
Тры рэжымы, тры высоты
Стрым — гэта не адна пожарная магістраля. Настройка stream_mode выбірае тып адбываюцыхся змян, пры чым тры распалоўкі адпавядаюць трым роўнем дэталізаціі.
updates — гэта перспектыва па кроках. Кожны раз, калі завершаецца вузел графа, прыходзіць адна змяна, што на практыцы значыць, што агент выканаў свой крок або запрацаваў інструмент. Гэты роўнь падходзіць для логу прагрэсу: ён дастатній, каб паказаць, як агент выканае свою працу, не перадмольваючы чытальніка тымількі токенамі, і ён ўзгодны як стандартны выбор для адночытнаеі інформацыі тыпу „гэта тое, што зараз робіць агент“.
messages — гэта спосаб працы з гляду на токены. Рэзультаты модэлю прыходзяц часткам па меры ўтварэння, так сама, як і калі пішчыцель-асістэнт піша. Ожывайце яго, калі чалавек чытае тэкст агента такім, які ён з’яўляецца. Сюды таксама прыходзяц вызовы інструментаў стрымінгу, таму вы можэце аанунсаваць "вызов read_file" у момент, калі модэль прыняла рашынку пра вызов, а не пасля яго завершэння.
values — гэта спосаб працы з кадрамі стану. Пасля кожнага крока вы атрымляеце весь стан агента, а не толькі тое, што змянілася. Гэта болей тэхнічна складнае і рэдкая патрэба, але гэта правы выбар, калі ў кожны момент важна весь картэн, напрыклад, каб паказаць жывы спіс задач агента або яго рабочыя файлы.
Працэўка з спісам рэжымаў дазволяе атрымаць калькі з іх адразу, і самэ так зазвычай функцыонуюць выкалібраваныя інтэрфейсы: updates керуе показчыкам этапы, а messages задае відны тэкст. Просты прыклад — чыстыя лог-запісы пра прыбылі з updates:
for chunk in agent.stream(
{”messages”: [{”role”: “user”, “content”: “Summarize the repo’s failing tests”}]},
stream_mode=”updates”,
version=”v2”,
):
if chunk[”type”] == “updates”:
for node_name in chunk[”data”]:
print(f”step: {node_name}”)
Выдрукаваець паказвае спіс крокаў графа, які выкананы, — адна лінія за кожны крок модэлю і за кожны запуск інструмента. Экран, які раней здаваўся застойным, цяпер явна працюе. Якшча заместыць stream_mode на "messages", вы будзеце атрымляць сам тэкст, які можна будзе выдрукаваць падчас таго, калі агент пішаць.
Разлік галоўнага агента ад яго падагентаў
Глыбокі агент рэдка працуе сам праўільна дзейсна дзялей. Як толькі ён перадае падзадачу падагенту, два чы роўна больш агентаў адразу вырабляюць выходны матэрыял. Калі усё адправіць у адну стрэму, рэзультат будзе хаосам: частка рэчы ад коордынатора, калькі токенав ад доследніка, вызов інструмента з невядомага месца. Поле ns выправляе гэта. Кожны фрагмент несе такі ідэнтыфікатор, калі увімкнута стрымінгаванне падграфаў з параметрам subgraphs=True.
Правілле прыменяць лёгка. Калі прастор імена пусты, значыць, галоўны агент выдала запіс. Калі ён містыць сегмент tools:, значыць, гэта зрабіў падагент, і ўказваюцца назвы таго сегмента. Пасля чаго выходныя даны можна сортаваць па месцу выдання: тут тэкст коордынатора, а роба кожнага падагента — у сваёй частцы.
for chunk in agent.stream(
{”messages”: [{”role”: “user”, “content”: “Research and summarize quantum computing”}]},
stream_mode=”messages”,
subgraphs=True, # ①
version=”v2”,
):
if chunk[”type”] == “messages”: # ②
token, metadata = chunk[”data”] # ③
is_subagent = any(s.startswith(”tools:”) for s in chunk[”ns”]) # ④
source = “subagent” if is_subagent else “main” # ⑤
if token.content:
print(f”[{source}] {token.content}”, end=”“, flush=True) # ⑥
У тым фрагменце важны шас дэталей. Перша — subgraphs=True являецца тым, што надае ns значэння, аджэ без яго запісі сабагента не маюць жаднага розрывальнага прастора імен. Друга — галузь зберагае толькі запісі messages, таму што гэты цыкл направляе токены, а не крокі. Трэця — навантажэнне кожнага фрагмента распаковываецца ў токен плюс яго метаданы. Чатвёрта — справжнюю працу выканае перакананне прастора імен: фрагмент tools: гдзе-небудзь у ns пазначае токен як той, што прыйшоў ад сабагента. Пятая — атрыбуты, якія выйшлі, стаюць чытаемымі пазначкамі выходу. Шостая — выведанне з такой пазначкай як прыфікс ператварае адны заплутаны стрым у два стрымы з атрыбутамі.
Кожны токен практычна перад выдрукаваннем пазначаецца яго создаўцам, таму калі коордынатор перадае даныя доследніку, гэта выглядае як два пазначаныя стрыму. Полная веб-інтэрфейсна сістэма — гэта болей складная задача, але іменаванне прастора імен у кожным фрагменце ёсць тым самым сігналам, які чытае фронтэнд на базе React.
Аддатковыя токены, якія не належаць агенту
Гэта парадоксальна рэч, якая дзівіць людзей пад час першага довгага стрымування. Чырпак для керавання сабой сконцентруе старэйшыя паведамленні, калі розмова набліжаецца да межы модэлю. Этап концентрацыі таксама ёсць вызовам модэлю, таму ён выдае токены, якія потым потрапляюць у стрым messages разам з рэальнай адпаведздзю агента. Калі гэты стрым падае безпосерэдня да корыстніка, той пабачыць, як агент раптам чытае падсумак сваей історыі, і гэта выглядае як баг.
На ўдачу, токены маюць атрыбуты. Кожны здачы messages мае метаданы, а токены для стварэння апূরкі несу значэнне lc_source у варыянце "summarization". Іх трэба праўіць, а рэшту заліць.
for chunk in agent.stream(
{”messages”: [{”role”: “user”, “content”: “...”}]},
stream_mode=”messages”,
version=”v2”,
):
token, metadata = chunk[”data”]
if metadata.get(”lc_source”) == “summarization”:
continue
# ... handle real agent tokens here
Дастык з метаданымі дапамагае разлічыць агента, які размовляе з корыстнікам, ад агента, які працуе самаўтонна. Апрацоўваецца толькі першы варыянт. Гэта простая задача, якая дае значны плюс у павышэнні якосці.
Назіранне за адбываннем задачы проты падтрымкі размовы
Стрымаванне і размовы часта плутаюцца, таму трэба чытка паводзіць межу. Усё, што напісана вышэй, стосуецца назірання за адбываннем адной задачы. Кожны вызов stream, як і кожны вызов invoke, за замовчаннем ёсць одноразовым. Агент выпаўняе задачу, завершае ёю і забывае пра ёю, таму наступны вызов пачынаецца без жадных спогадаў пра пакананую раней размову.
Падтрыманне розмовы працюючай паэтапа, каб фраза «пасправіць і іншы тэст» працавала, таму што агент памятае першую пасправку, — гэта адзінэцкі механізм. Ён выкарыстоўвае ніткі і пункт контролю, што ўскладнюея слой, дзе стойкі граф зберагае свой стан. Неабходна разліка: стрымінг дае можлівасць бачыць працу ў момент експлуатацыі, а ніткі — працюванне паэтапа. Яны незалежныя, і часта будзе неабходна як адна, так і другая.
Маленькі інтэрфейс тэрміналу, які паказвае працу
Далей неабходна з’едыніць усё ў консоль, якую можна будзе рэальна запусціць. Яна должна выдаваць адну лінію за кожны крок, калі агент працюе, і стрымаваць тэкст агента ў момент яго запісу, таму трэба запітаць пра два рэжымы і перэходзіць на відпаведны рэжым на адпаведным кроці.
from deepagents import create_deep_agent
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
system_prompt="You are a code-maintenance assistant for a small Python repo.",
) # ①
prompt = "List the files you'd look at first to find a failing test, and explain why."
for chunk in agent.stream(
{"messages": [{"role": "user", "content": prompt}]},
stream_mode=["updates", "messages"], # ②
version="v2",
):
if chunk["type"] == "updates": # ③
for node_name in chunk["data"]:
print(f"\n[step] {node_name}")
elif chunk["type"] == "messages": # ④
token, metadata = chunk["data"]
if metadata.get("lc_source") == "summarization": # ⑤
continue
if token.content:
print(token.content, end="", flush=True) # ⑥
Код ў короткім фармаце, але кожны яго элемент выпалюе сваю ролю. Структура застаецца незменнай, адколькі create_deep_agent стварае агента так сама, як і раней. Два рэжымы, запрошаныя ў аднам вызове, значыць, што адна петля можа падаць як паказчык крока, так і поток тэксту. Калі фрагмент являе сабою запуск змены updates, петля выведае рэчып [step] для вузла, які ўжо завершыў свою працу. Калі ж гэта запуск змены messages, петля выведае створаны прозаічны тэкст. Перакананне ў стварэнні апускання з паказванага раней раздзела знаходзіцца ўнутрь гэтага розгалёту, таму функціі адпаведальныя за адарботку ніколі не дасягаюць тэрміналу, а порожніе токены прахідаюць перад выведам.
Рэзультатам являецца рэчык [step] кожны раз, калі агент рухаецца вперад, які перакрытыя з яго тэкстам, які прыходзіць по аднам сімволам, пры чым фільтр ужо прыняты, таму нічога не прасачыцца. Гэта не красава, але гэта чыста правда. Пользоватэль можа бачыць, што агент жывы, і можа спаказваць, як ён думае.
Наветнейшыя выданні Deep Agents апавяжаюць вышэйшы API для гэтага, agent.stream_events(...) у version="v3", з тыпаванымі праекстраціямі, такімі як stream.subagents. Документацыя спаказвае новыя прыклады ўжыцця да яго, таму што кожная праекстрацыя ёсць самастоятным адчытным ітэратаром, і няма прычыны самостоятэльна робіць разгалужэння на stream_mode або дэкадаваць прасторы імен. Тым чым варта спачатку выучыць механізм stream_mode, таму што праекстрацыі базуюцца на тых сабе запісах. Перад тым, як адхіліцца да большага інтэрфейсу, пераканайцеся ў актуальных документацыях.
Спіс задач, якія трэба адкрыць сёньдзе
- Пачаткайце з пошуку кожнага
invoke, які чакаюць на адміністрацыю, і заменіце іх наagent.stream(...)у рэжымеupdates. Простыя лог-запісы все ж лепшыя, чым порожній экран. - Паглядзіце на адзін рэальны фрагмент. Запусціце з параметрам
version="v2", выведзіце рэзультат і прачытайце поляtype,nsіdata, ўбедзіцеся, што структура ўсё ж зрозумелая. - Перш чым рэальныя корыстнікі побачыць рэзультат, адхроніце всі фрагменты
messages, у якіх метаданы паўтараюцьlc_source == "summarization". - Калі агент перадае задачы падагентам, установіце
subgraphs=Trueі сортавайце рэзультаты па іменаванні прастора, кожны агент — у сваёй групе. - Зберагачыце маленькую консоль, падобную да вышэйшай, у рэпазітарыі. Гэта дазволіце вам сочыць за кожным новым запускам функцыяй, а не толькі спадзявацца на пярэдніе поведамленні.
Што змянілася і што — ні
Чысты рэзультат: блакуячы выклік ператворыўся ў працуючы стрым. stream_mode задае ступень дэталізаціі, ns паказвае, хто стварыў кожную падзею, фільтр аптыварэння адключае власны шум апранату, і цяпер ясна, што спостэрэнне за запускам і запамятованне запуску — гэта разныя проблемы.
Нічога не змянілася ў самам агенце чы роце. Едыным дадаткам была вікна, і гэтае вікна пазначае межу между інструментам, якім люды давяраюць, і тым, якія яны падтрымліваюць у паніцы. Калі агент паказвае свою работу, корыстувачы веруюць, што ён працуе.
Таму наступны раз, калі хтось адзвярнёцца, што агент „застраг“, прыхіліцеся не да аналізатора, а спачатку спробавайце стрым. Агент увесь час „гаварыў“. Йому была патрэбна каналізацыя.
Спадневана літэратура
- Сістэма файлаў вашаг агента — гэта рашэнне: поясненні бэкендаў Deep Agents — Як Лангчэйн Deep Agents бэкенды выбіраюць, што застаецца: пяць вбудоўаных рашэнняў, механізм распаўнення CompositeBackend, проблемы прастору імен для багатадзейнавых сховішчаў, а таксама шаблоны сэндбоксаў.
- Beta-версія Managed Deep Agents: спосаб развертвання агентаў Лангчэйн на адной дырэктарыі — Managed Deep Agents Лангчэйн ператварае папку праекта на хоставаны агент. Чым займаецца кожны файл, як функціонуе механізм развертвання mda, і якія недагадкі ёсць ў бета-версіі, якія трэба ведаць.