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

Трасіроўка блог-канвеера на LangGraph з Langfuse: спаны, ацэнкі, папярэджанні

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

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

Крыніцы: Building Multi-Agent System From Scratch — Part 6: Observability and Debugging

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

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

Выбіранне таго, што ўважаецца тракой

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

blog-pipeline
├── research-agent
│   ├── search-web
│   └── research-model
├── writer-agent
│   └── writer-model
├── citation-check
│   └── citation-review-model
└── reviewer-agent
    └── reviewer-model

Langfuse работае з трыма іменнікамі. Trace — цэх адначасовы запуск ад пачатку да канца. Observation — гэта адзін крок унутрь яго, які можа быць фрагментам (логікая прыемлівкі), запускам (выклік LLM) або запісам (мініятурны маркер). Session спаюе калькі trace-аў, напрыклад, усі спробы па аднам і там жа тэме блога.

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

Настройка Langfuse

Створыце праект Langfuse і задаце яго аутантыфікацыйныя даны праз зменныя сяродовышча. Секрэтны ключ ніколі не трэба пісаць у файлах коду.

pip install -U langfuse

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com"
export LANGFUSE_TRACING_ENVIRONMENT="development"

langfuse пакет — гэта Python SDK. Два ключа аўтэнтыфікуюць дапрыгу пры працэўванні з праектам, а LANGFUSE_BASE_URL выбірае месца размешчэння. У прыкладзе выкарыстоўваецца хмарны рэгіон ЕС, таму за патрэбы можна заменіць яго на іншы рэгіон або самастойна размешчаную інстанцыю.

Атрыбут сяродовішча дапамагае раздзеляць следы з развіцця ад тых, што ствараюцца ў прыметнай сяродовішчы. Короткія, стабільныя назвы, такія як development, staging і production, добра падходзяць. Без іх експерыменты і рэальныя запускі статэйка падаюць у адну відзнаку, чым ускладняецца аптыварка.

Падключэнне працоўніка адпаведзення

Langfuse поставляе працоўнік адпаведзення LangChain. LangGraph будуецца на механізмах LangChain для запуску і адпаведзення, таму той самы працоўнік фіксуе падзеі, якія выкалываюцься з графамі, ланцоўкамі, інструментамі і мадэлямі:

from langfuse import get_client, propagate_attributes
from langfuse.langchain import CallbackHandler


langfuse = get_client()
langfuse_handler = CallbackHandler()

get_client() стварае кліента на адказе змяны сяродовішча. Адпрацоўвальнік павінен быць створаны аднойчы пад час запуску і вікорыстоўваны знову па кожным запуску. Ён слухае звычкі LangChain і перадае іх як спазырання, што усуняе патрэбу вручную сочыць за кожным вызывам модэлю.

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

Вызыв графа абгортваецца ў корневым спане, пры чым адпрацоўвальнік перадаецца праз настройкі LangGraph:

def run_blog_pipeline(topic: str, blog_id: str, user_id: str):
    initial_state = {
        "topic": topic,
        "audience": "developers new to agent systems",
        "research_brief": "",
        "sources": [],
        "open_questions": [],
        "article_draft": "",
        "review_feedback": "",
        "citation_issues": [],
        "approved": False,
        "revision_count": 0,
        "status": "researching",
    }

    with langfuse.start_as_current_observation(
        as_type="span",
        name="blog-pipeline",
        input={"topic": topic, "audience": initial_state["audience"]},
    ) as pipeline_span:
        with propagate_attributes(
            trace_name="blog-pipeline",
            session_id=blog_id,
            user_id=user_id,
            tags=["blog-pipeline", "langgraph"],
            version="1.0.0",
            metadata={"workflow": "research-write-review"},
        ):
            trace_id = langfuse.get_current_trace_id()
            result = graph.invoke(
                initial_state,
                config={"callbacks": [langfuse_handler]},
            )

        pipeline_span.update(
            output={
                "status": result["status"],
                "approved": result["approved"],
                "revision_count": result["revision_count"],
            }
        )

    return result, trace_id

Некалькі дэталей гэтага абгародніка маюць значэнне. Зовнішній вызов start_as_current_observation() стварае корневую абглядку, а ўсі параметры, якія ёй падаюць, залічваюцца спецыяльна кампактнымі — толькі тэма і аудыянс. Гэтае можна пазначыць за мгновенне ў спісе следаў, таму даныя пра внутрані стан не патрабуюцца там.

Функцыя propagate_attributes() дадае атрыбуты кожнай вкладзенай абглядцы. Значэнне blog_id служыць ідэнтыфікаторам сесіі, які агрупавае праказы чыстаўкі або падзеяванні, якія выконваюцца з адной статыей. Значэнне version відображае працэс выканання і момент публікацыі рэзультата, таму, калі змянюецца працэс, следы можна фільтраўваць па версіі, а не проста за здогадкамі.

Апрацоўшчык праходзіць праз стандартны config LangGraph і там самы аўтаматычна збірае дэталі на рэвэлю фрэймворку: кожны вызов вузла, генераванне LLM, вызов інструмента, вхідныя/выходныя даны, час адпаведзі, колькасць токенаў і ўскладнення.

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

Спаны на рэвэлі бізнес-рэвэля ў агентах

Аўтаматычны трэйсінг паказвае, што мадэль была вызвана. Ён не паказвае, што кожны спецыяліст перадаў наступнаму. Для гэтага трэба дадаць лёгкі спан унутры вузла агента і перадаць аргумент config у глыбокія вызовы LangChain або LangGraph, каб апрацоўшчык заставаўся ў тым жа кантэксце трагу:

from langchain_core.runnables import RunnableConfig


def research_node(state: BlogState, config: RunnableConfig) -> dict:
    with langfuse.start_as_current_observation(
        as_type="span",
        name="research-agent",
        input={"topic": state["topic"], "audience": state["audience"]},
    ) as span:
        result = research_agent.invoke(
            {"topic": state["topic"], "audience": state["audience"]},
            config=config,
        )

        span.update(
            output={
                "source_count": len(result["sources"]),
                "open_question_count": len(result["open_questions"]),
                "brief": result["research_brief"],
            }
        )

    return {
        "research_brief": result["research_brief"],
        "sources": result["sources"],
        "open_questions": result["open_questions"],
        "status": "writing",
    }

Этот элемент дапамагае функцыі адпаведнага адпрацоўвання вызову і не заменяе яе. Ён дадае значэнне, якое фрэймворк не можа з’ясаваць, а самэй — што означае "доследжэнне завершылася успехам" для гэтага працэсу. Хутчэй ён фіксуе колькасць выхадных дасягненняў, колькасць нерашытых пытанняў і короткі апіс, які даесяць пісцачу.

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

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

Адвары і памылкі

Не кожны нестандартны рэзультат ўскладнюе працу. Сумнівны істочнік, выкараны бюджэт на рэвізію або непадтрымваная цітата таксама маюць значэнне і павінны выказвацца як адвар.

def record_source_assessment(assessment: SourceAssessment) -> None:
    if assessment.suspicious_content:
        langfuse.update_current_span(
            level="WARNING",
            status_message="Untrusted source contained agent-directed instructions.",
        )


def record_pipeline_failure(error: Exception) -> None:
    langfuse.update_current_span(
        level="ERROR",
        status_message=f"Pipeline failed: {type(error).__name__}",
    )

Langfuse падтрымлівае рэжымы логаў для абсарвэйшанняў. WARNING прызначаны для тых запускаў, якія завершыліся, але трэбуюць перагляду, тады калі ERROR выкорыстоўваецца для справжніх абяканняў. Паведамленні пра статусе должны опісваць проблему з працай়ом, не копіюючы чутлівы тэкст адпаведна секрэты ў тэлеметрыю.

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

Оцэнка рэзультата

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

result, trace_id = run_blog_pipeline(
    topic="How AI agents use tools",
    blog_id="blog-ai-tools-001",
    user_id="philip",
)

if trace_id:
    langfuse.create_score(
        trace_id=trace_id,
        name="review_approved",
        value=1 if result["approved"] else 0,
        data_type="BOOLEAN",
        comment=result["status"],
    )

    langfuse.create_score(
        trace_id=trace_id,
        name="revision_count",
        value=float(result["revision_count"]),
        data_type="NUMERIC",
    )

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

Жодэн з цых показніків не ёсць абсолютною істыною, оскількі рэцензент можа помілятыся. Оцінка, вказана пасля прочытання остаточноі версіі, можа з’явітыся пізнішэ. Важліво фіксуваты всі зміні, шчоб можна було порывнюваты якісць і аналізуваты рэзультаты, а не покладатыся лішэ на кілька запам’ятовуваніх прыкладыв.

Процедура віправлення помілок

Калі артыкул віявляецца слабкою, перэгляньце іі історыю змін і поставце собі наступні запітання:

  1. Чы пройшов граф відповіднім шляхом? Перэвірце роздылі «Досліджэння», «Автор», «Перэвірка посілань» і «Рэцензент», а таксама будзь-які цыклі корэкцій.
  • Насколькі чыста была перадача даных для доследжэння? Перагляньце колькасць істочнікаў, нераскрытыя пытанні, цытатаў і саму анотацыю.
  • Дзе з’явілася памылка? Спаруцьце параметры кожнага вызову — запіт, рэзультат, час адпаведзення, выкарыстоўванне токенаў і актывацыю інструменту — з парамі праз прэдыдушчы крок.
  • Памылка мадэлі чы сістэмы? Затрымка і слабкія цытаты выказваюць патрэбу абсалютна разных способаў усунення.
  • Чы працавала сістэма захоплення? Якшчы істочнік выглядаў падазроўна чы твэрджэнне не мела падтрымкі, чы граф выводзіў на перагляд чы людскую атрыбутацыю замест на затверджэння?
  • Саме таму важна правільная структура логаў. Якшчы названыя дрэвы ператвараюць фразу „Блог быў некарэктны“ на точную заключэння, напрыклад „Авар перадаў твэрджэнне, якога не было ў анотацыі, пасля другага перагляду.“

    Забэрэжыць чутлівыя даны

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

    Золатая правіла: запісвайце столькі, сколькі трэба, каб поясніць рашэнне, і не больш, чым можна безпечна заставіць.

    Што отрымае канал

    Кожны запуск цяпер заставляе аперацыйныя запісы:

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