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

Аналіз крока выкарыстоўвання дадзеных: супарэння шасці інструментаў для абліквіднасці на аднам агенце RAG

Адэнт RAG быў аналізаваны за дапамой шасцячых інструментаў для абэрвазабілітэту LLM: Opik, MLflow, Langfuse, Phoenix, AcruxCore і Helicone, паўстаўляючы паўтарэнне адзінакоў, OpenTelemetry і проксі.

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

Крыніцы: Tracing a RAG pipeline: how six LLM observability tools handle the step that isn’t a model call

Retrieval-augmented generation — гэта дваэтапны працэс, і толькі адзін з этапаў ўключае вызов модэлю. Ёсць можнацьце пазнакамліцца, як інструменты для аналізу фіксуюць другую частку працэсу, што было здзейснена праз шасць платформ: Opik, MLflow, Langfuse, Phoenix, AcruxCore і Helicone.

Чаму працэс адзыскання дадзеных праходае за межы звычнага кэшування

Вызов адзінаго модэлю лёгка аналізаваць: дастатнек адкоррэктаваць кліент OpenAI, і кожны вызов chat.completions.create() стае часткай аналізаваемага перыоду. Дадзенні, неабходныя для працы модэлю, адзыскваюцца, модэль вызываецца, і той жа спосаб адкоррэктавання павінен павінавацься ўсё. У практыцы ж ён павінаваецься толькі палову працэсу.

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

Короткая апৱіс рэзультатаў

  • Дэкаратары домінуюць. Opik, MLflow і Langfuse кожны з іх ператварае функцыю на крок, які можна сочыць, дадаўшы адну лінію коду, не чынячы змян у самай функцыі.
  • OpenTelemetry працуе ў двух месцах. Phoenix і AcruxCore оба падтрымліваюць фармат OTLP, таму адну ручна створаную елементацыю можна адправіць у кожны з іх, змініўшы толькі канецпункт. Той самы практычны патэрн быў выкананы ў обох сістэмах.
  • У шляху запіту знаходзяцца два продукты. Проксі Helicone і гейтвей AcruxCore фіксуюць перыод работы модэлі без выкарыстоўвання коду кліента, але жадны з іх не зафіксавае крок, які ніколі не з’яўляецца у выкарыстоўванні прадаўця модэлі.
  • Helicone не мае спосабу атрымання даных, адколі проксі ёсць ўсім яго продуктом, і не існуе SDK, які б фіксаваў такі крок.
  • MLflow прысвячае наібольшую увагу тэрмінологіі перыодаў работы модэлі, пры чым існуе панздзесят пяць тыпаў, сярод яых RERANKER, GUARDRAIL і EVALUATOR; гэтыя тыпы лічыцца на адной з базаў SDK, а не на дакументацыі.
  • Што такое тып перыода работы модэлі

    След можна спачатку уявіць як спіс этапаў, які працяўае запіт праходзіць. Кожная стадыя — гэта елемент, які мае назву, час пачатку і заканчэння, а таксама вхідныя і выхідныя даны. Гэта паказвае, што працавало і сколькі часу гэта трывало, але не характэр работы: елемент пад назвай step_2, які трывае 900 мс, можа быць як запітам да SQL, так і вызовам LLM або запітам HTTP. Разлікаваць іх — задача тыпу елемента, тага, які гэта візначае. Кожны інструмент візначае замкнуты сет катэгорыяў, і кожны елемент атрымоўвае адну з іх.

    Возьмімо прыклад агента RAG. Адзін запіт дае тры елементы:

    ┌────────────────┬────────────────────────────────┬────────────────┐
    │   Span name    │          What it did           │   Span type    │
    ├────────────────┼────────────────────────────────┼────────────────┤
    │ rag-agent      │ the whole request              │ agent or chain │
    ├────────────────┼────────────────────────────────┼────────────────┤
    │ search_docs    │ queried Chroma for four chunks │ retrieval      │
    ├────────────────┼────────────────────────────────┼────────────────┤
    │ ChatCompletion │ called gpt-4o-mini             │ llm            │
    └────────────────┴────────────────────────────────┴────────────────┘
    

    У даным случае search_docs — гэта проста якірна назва, выбраная распрацоўшчыкам, а процэс выкарыстоўвання дакументаў адбываецца з пярэдніцы значэнняў, якія дазволяе SDK. Праглядач паказвае гэта як чып Retrieval пры search_docs і чып LLM пры рэзультатах адгукнення.

    Тып мае значэнне, а не толькі назва, каліжо ўстаткуванне працюе на яго адзычыну. Ён выбірае іконку і макет: так чып Retrieval паказвае завантажаныя дакументы, а чып мадэлі — колькасць токенав і ўскладнення. Гэта таксама дазволяе фільтрацыю, таму запыт на кшталт «Калія зусібныя крокі выкарыстоўвання дакументаў трывалі больш за секунду?» будзе працаваць для всіх справаздач, нават калі распрацоўшчыкі давалі своім функцыям іншыя назвы. Кроме таго, гэта спрыяе аналізу: ступень затрымкі, якая выйшла з процэсу выкарыстоўвання дакументаў проты стварэння тэкста, стае пытаннем у столцы «Тып».

    Дыялекты разніцаюцца па дужыне. AcruxCore падтрымлівае семь: llm, tool, retrieval, embedding, agent, chain і іншыя. MLflow падтрымлівае падзесят пяць, сярод яых RERANKER, GUARDRAIL, EVALUATOR, PARSER і MEMORY. Opik падтрымлівае толькі чатыры: general, tool, llm і guardrail.

    Експерыментуемы агент

    Агент спецыяльна просты. Ён індэксуе пяць стораніц дакументацыі OpenTelemetry, раздзеленых на фрагменты па прыблізна 1 000 сімвалаў кожны, укладае іх за дапамогою text-embedding-3-small і зберагае ў колекцыі Chroma ў памяці. Одна фіксаваная запытка пра тое, што такое span і як аб’екты parent і child spans складаюць trace, запрашвае чатыры фрагменты, а gpt-4o-mini пішчыць адпаведны адказ на адной з іх.

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

    def retrieve_context(collection, question: str, top_k: int = 4) -> str:
        [query_embedding] = embed_texts([question])
        results = collection.query(query_embeddings=[query_embedding], n_results=top_k)
        chunks = results["documents"][0]
        sources = [m["source"] for m in results["metadatas"][0]]
        return "\n\n".join(f"[{src}]\n{chunk}" for src, chunk in zip(sources, chunks))
    

    Гэта функцыя, крок разбівання на часткі і вызов практыкавання не змінююцца пад час усіх шасоў. Ядзейная змянна — гэта інструментарыя, якая іх абгортае. Кожны інструмент працаваў на локальным камп’ютары: пяць з іх было розмішанае ў Docker, а таксама AcruxCore. LangSmith і W&B Weave былі прынятні, таму што існуюць толькі як сервісы на хосты. Судзячы па яго SDK, LangSmith мог бы стаць чатвертым прыкладам дэкаратара з семью тыпамі запуску, але так як ніякіх следаў не было зафіксавана, нема чаго далей стверджваць пра яго.

    Шаблон 1: дэкаратар

    Сюды ж збіраюцца всі інструменты, і гэта цэлкам логічна: тэла функцыі застаюцца абсолютна такімі, якія напісаны. Код, які ужо існуе, проста пазначаецца анотаціямі. Фармат Opik падходзіць да яго документацыі:

    @opik.track(name="search_docs", project_name=PROJECT_NAME)
    def search_docs(collection, query: str) -> str:
        return rag_core.retrieve_context(collection, query)
    

    MLflow мае той самыя форму з прыўязаным тыпам прастранства:

    @mlflow.trace(name="search_docs", span_type=SpanType.RETRIEVER)
    def search_docs(collection, query: str) -> str:
        return rag_core.retrieve_context(collection, query)
    

    Langfuse адказвае функцыяю @observe; яе легкая адзначэнняе, калі спачатку падае на прыклады з кантэкст-менеджэрамі:

    @observe(name="search_docs", as_type="retriever")
    def search_docs(collection, query: str) -> str:
        return rag_core.retrieve_context(collection, query)
    

    Выклік модэлі аўтаматычна ўтварае структуру ў всіх трохох прыемах. Кожны з іх супляе адрэдагаваны кліент OpenAI (track_openai(), mlflow.openai.autolog() і langfuse.openai), таму перыод генеравання залежыць ад таго, якая функцыя з дэкоратарамі зараз выканана. Такім чынам, адны дэкоратар стварае дрэво двух роўней глыбі.

    Langfuse даў самы апрануты вынік. Перыод пошуку дакументаў за дапамогою функцыі retriever-тыпу трываў 1,05 секунды, а перыод генеравання, які ён сам зафіксав, — 4,14 секунды, пры чым сумы віткоў і токенаў былі аб’еднаны. У MLflow тыя ж два этапы паказваюць час пошуку у 0,47 секунды і час генеравання Completions у 3,37 секунды. Opik таксама стварае правільнае дрэво, але ў яго ёсць візуальная недагоднасць: перыады trace і root маюць аднаковую назву, таму rag-agent паказаны два разы пад элементам search_docs.

    Шаблон 2: кантэкстны менеджар відпаведальнага за працу

    Langfuse таксама адкрывае сам об’ект span, які ўжоўцяецца тады, калі межы span не збігаюцца з межамі функцыі:

    with langfuse.start_as_current_observation(name="search_docs", as_type="span") as span:
        context = retrieve_context(collection, QUESTION)
        span.update(input={"query": QUESTION}, output={"context": context})
    

    Гэты спосаб дае тачнасць, адколі вхідныя і выхідныя даны можна задаць самэй час, а апвалк можа паважаць частку функцыі. Што трапляецца, так гэта втрата раздзелення, таму што кода для трэйсінгу опынуецца ўнутрь бізнес-логікі. Калі елементам для аналізу ёст тая самая функцыя, @observe дае той самы рэзультат без гэтых недакладнасцей.

    Шаблон 3: стандартны OpenTelemetry

    У Phoenix няма саеўнага дэкаратара. Відразу вікорыстоўваецца трэйсер OpenTelemetry, а інструмент OpenInference автаматычна абгортае кліента, таму вызов для генеравання дадзеных застаецца ўнутрь іншага вызову:

    with tracer.start_as_current_span("search_docs") as span:
        context = retrieve_context(collection, QUESTION)
        span.set_attribute("output.value", context)
    

    Працэзнае прынцыпа заключаецца ў тым, што нічга заўсёды не належыць Phoenix. Гэта стандартныя фрагменты OTel, таму тая ж самая апаратура можа супляваць любы бэкэнд, сумесны з OTLP, як толькі змяніцца канец-тэп. Што стосуецца гэтага тверджэння, яго было рэальна перапрацаваць, адколі перэноснасць часта ёсць на паперы, але не функцыонуе на практыцы. AcruxCore таксама падтрымлівае OTLP, таму той самы прайсепт быў адпрацоўан для яго: ідэнтычны код with tracer.start_as_current_span(...), ідэнтычны інструментар, новы канец-тэп.

    tracer_provider = otel.register(service_name="rag-agent-observability")
    OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
    

    Кожная платформа мае функцыю register(), якая аб’яднае TracerProvider, BatchSpanProcessor і экспортар OTLP у адны вызов. Жадна з эйтых функцый не ўмовна, і ручна створаныя пайплайны працуюць з обомі. Спаны былі перэданы правільна, з правільным вкладчычам. Атрыбут OpenInference RETRIEVER стаў тыпам Retrieval, а автаматычна зафіксаваны спан модэлю паказаў сам модэль і 1,341 токен.

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

    Шаблон 4: прыем дадзеных па зовнішнім каналу

    Паралельна OTLP, AcruxCore працюе за пятым падходам, якога няма у іншых засобах. У працэ замест таглядзення за процэсам код опісвае спаны і публікуе іх. ID трэйса з’едынае усё разам: яго перадаюць у гейтвей, і спан, напісаны на сервернай стороне, прыўязуецца да таго ж трэйса. Шырокія падходы допамагаюць адзін другаму.

    reported = await hub.traces.ingest({
        "name": "rag-agent",
        "spans": [{
            "spanId": "search_docs", "name": "search_docs", "kind": "retrieval",
            "input": {"query": QUESTION}, "output": {"context": context},
            "startTime": started, "endTime": _now(),
        }],
    }, wait=False)                                # buffered, returns at once
    result = await hub.gateway.chat(
        "openai/gpt-4o-mini", messages, provider=PROVIDER,
        trace={"trace_id": reported.trace_id},   # generation lands in the same trace
    )
    

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

    Проблема крыўціцца ў флагу wait=False. У стандартным рэжыме ingest() чакае на свой POST-запыт і вяртае ідэнтыфікатор трэйса, створаны серверам, таму запыт выкананае два раунд-трыпы па сеціі прычыму до пачатку вызоў модэлю. Калі wait=False, даныя падаюць у локальны буфер SDK, а ідэнтыфікатор ствараецца на стороне кліента. Нічога не падтверджваецца на месцы вызоў, а невыпаны запыт фіксуецца як адзін з адзвярнень і выкарыстоўваецца не ўсё, падобна да экспортара OTel. Выбор стаўіцца межу медленным і гучным рэагаваннем на адзвярненне або шырокім і тыхім рэагаваннем на адзвярненне. Кожны з варыянтаў ёсць правядзімы, як толькі выбор будзе свядомым.

    Шаблон 5: насчыт запыту

    Два з інструментаў знаходзяцца ў самай вызове, і ў рэзультаце сегмент модэля выдаўаецца безкоштовна. Helicone паказвае гэта якнайяснейша: як толькі зменіце base_url так, каб ён вялі да яго, заполненне запіту фіксуецца без жаднага коду SDK. Гейтвей AcruxCore таксама дэйствуе аднойчы. Вызов, які праходзіць праз gateway.chat(), мае сегмент llm, які запісвае сервер, таму кліенту не патрэбны жадныя додатковыя засобы.

    У такога падходу ёсць адна перавага і адна обмежэння, якія ёсць спаканнымі для обох варыянтав. Усе, што знаходзіцца ў шляху запіту, бачыць толькі HTTP-трафік, надзеўны да падаўчыка модэляў. Запіт Chroma не выклікае такога трафіку, таму ён застаецца невідным незалежна ад таго, які інструмент вікорыстоўваецца.

    Яны разніцяюцца тым, што іх супаноўвае. Шлюз AcruxCore прыймае ідэнтыфікатор трацу, які дазволяе фрагменту, зазначанаму праз іншы маршрут, OTel або ingest(), выступаць як вышэйшы элемент, так што фрагмент модэлю, створаны серверам, опінуецца ў той самы трацу. Helicone праставляе толькі проксі, а не SDK для абгортавання будзь-якай функцыі. Гэта залішае два варыянты: або прыміты, што выкарыстоўвання трацу не адбываецца, або стварыць фальшывы фініш, каб яго перадаць, пры чым гэты варыянт заполнівае тэлеметрыю вымыслам. Таму тачнае тверджэнне ёсць суворэйшым, чым проста немагчымасць проксі адстэпліваць выкарыстоўвання трацу: проксі сам по сабе не можа гэтага зрабіць, і значэнне гэтага залежыць ад таго, чы існуе другі маршрут.

    Адзін аспект, які трэба врахавіць пад выкананне, неякі не стосуецца дыяграматыкі: Helicone, які быў запусканы самастойна пад час гэтага тэста, адпавядаў на запыт пра ручны журнал з кодам 500, таму што не было зменнай S3_REGION, што завадзіла зафіксаваць хоць бы частку модэлі. Гэта якраз недагоднасць гэтага конкрэтнага варыянту, а не самага падходу.

    Што можна назваць спанам

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

    MLflow значнаю мерай пераважае ў гэй катэгорыі. RERANKER, GUARDRAIL, EVALUATOR, PARSER і MEMORY — усе гэтыя элементы ўбудованы, тады калі як у Opik ёсць чатыры тыпы, а усё інша праблемы класіфікуюцца як метаданы. У AcruxCore семь элементаў падтрымляюць тэхналогію RAG, але без болей дзеяроганізаванага падходу. Phoenix успадковвае свой спіс з стандарту OpenInference, таму гэты спіс ёсць стандартным для всіх інструментаў, які следуюць гэтаму стандарту, і гэта не ўласная функцыя Phoenix.

    Гэты радак часта неправільна ацэніваецца на сторонніх стороніцах з маркетынгавай інфармацыяй. Все спіскі можна пераканацца ў співпаўненні з установленымі версіямі за калькі ліній коду:

    from mlflow.entities import SpanType
    from openinference.semconv.trace import OpenInferenceSpanKindValues
    
    print([v for k, v in vars(SpanType).items() if isinstance(v, str) and not k.startswith("_")])
    print([e.value for e in OpenInferenceSpanKindValues])
    

    Langfuse выконвае додатковыя заходы ўбачлівасці. Яго ObservationTypeLiteral, які знаходзіцца ў langfuse._client.span, складаецца з вярнутых ад внутраніх Literal тыпаў, таму typing.get_args вяртае внутранія псевданія заместа страк, і рэзультат неабходна рэкурсыўна сплюшчаць. Гэта пояснюе, чаму даныя з яго дасягледа часта не збігаюцца з дасягледамі SDK.

    Выбір

    • MLflow падходзіць командам, якія хочуць точную семантыку зараз і аналіз пазней. Яго слоўнік значна шырэй, хоча весь комплекс засобаў ўжо большы, чым у чыстага інструмента для трэйсінгу.
    • Opik дазволяе застаўіць правільную структуру ў той жа вечар, без зайвых формальнасцей, і мае толькі чатыры тыпы span.
    • Langfuse прыносіць як дэкаратар, так і можлівасць ручнага выйсця з адпаведным строгім тыпаваннем, але за цэну двух набораў дасягледаў.
    • Phoenix падае для команд, які можаць зменіць бэкенды або ужо выкарыстоўваюць OTel. У яго няма API-інтэграцыі з постаўчыкамі, і для таго, каб праз першы раз з’явіўся рэзультат, трэба ўжо ўсё настаўіць.
    • AcruxCore падходзіць, калі неабходна, каб мадэль сама стварыла рэзультат, але ў той жытак трэба зберагчы запіс. У яго ёсць тры можна складваць маршруты, слоўнік, меньшы за MLflow, Phoenix чы Langfuse, а таксама функцыя буферавання дадзеных, якая тылу не падае сігналу пра адмову.
    • Helicone падходзіць для пайплайна, які фактычна ўключае толькі вызов мадэлі. Для абрабоцкі дадзеных не трэба коду, а крокі, які не ўзаемадзейнуюць з модэлю, не маюць куды буць зберагчаныя.

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