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

Цыкл паляпшэння агента на LangGraph і ацэнках LangSmith

Пакрокавы цыкл на LangGraph і LangSmith: збор трас, тры ўзроўні ацэнкі, кластарызацыя памылак, афлайн-наборы для рэгрэсіі і ацэнкі перад выпускам.

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

Крыніцы: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines

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

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

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

Цыкл працюе так:

  1. Агент обрабоўвае рэальны запіт корыстніка і стварае след, які пакрывае кожны вызов LLM, вызов інструмента і проміжныя рэзультаты.
  2. Перакананні, адбываюцыяся за дапамою коду, оцэнююць кожны след па фармату і правільнасці інструмента, суддзя LLM оцэнюе корыснасць і адносаванасць, а людзі пераглядаюць тонкія проблемы.
  3. Следы з нізкімі баламі групаваныя ў патэрны проблем, такія як неправы інструмент, адхыленне ў логіцы разумовых дзеянняў або непраўільнае зрозумеванне намеру.
  • Кожна повторувана проблема стае тэстовым віпадком офлайн-тэстування з відомою очыкуваною поведынкою.
  • Перш ніж віпускаты нову версію агента, яго тэстуюць у рамках розшыруваноі набору тэстыв.
  • Лішэ зміні, які покрашчуюць базову продуктывнісць, потрапляюць у фінальну версію, а кожна віправлена проблема залішаецца у наборы назавжды.
  • Саме так агент поступово покрашчуецца, замест того шчоб просто накопічуваты патчы. Шысць етапів ёсць такімі:

    • Етап 1, основа: агент LangGraph ReAct з функціею трэкування LangSmith.
    • Етап 2, збір даных: отрымання рэзультатыв працы у структурованому вігляды.
    • Етап 3, оцінка: перэвірка коду, оцінка ШЫ і людській аналіз.
    • Етап 4, віявлення закономірностэй: перэтворэння оціненіх даных на катэгорыі проблем.
    • Етап 5, наборы тэстыв офлайн: перэтворэння проблем на набір даных, стыйкій до рэгрэсіі.
  • Этап 6, закрыцья цыклу: адзінакоўка пры выдазе і падняць базовы рэвэл.
  • Этап 1: агент-фундамент

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

    Для цьго работы неабходны тры элементы:

    1. LangGraph StateGraph являецца базовым графам, які справляе пераказ между вузлом, які вырашае, што робіць, і вузлам, які запускае інструмент. Памочны функцыя create_react_agent абгортвае гэты шаблон.
  • Функцыя адстэйкінгу LangSmith запускаецца, калі задаёмая зменная сэрвэра LANGSMITH_TRACING=true. Пасля чаго кожны вызов LangChain або LangGraph выдае лог-запіс без якіх-леба змян у кодзе.
  • Інструменты — гэта функцыі на Python, адзораваныя знакам @tool з бібліятэкі LangChain, якія робяць кожны з іх доступным для агента па ўпамінанню і опісу, якія дапамагаюць выбраць правы інструмент.
  • Наладка сэрвэра

    Спачатку трэба запісаць бібліятэкі:

    # Install the libraries we need
    pip install langgraph langchain langchain-openai langsmith
    

    Інсталюецца чатыры пакеты. langgraph забезпечвае рантайм графаў і падготавленага агента ReAct. langchain даўае дэкаратар @tool і тыпы паведамленняў. langchain-openai абгортае мадэль чату OpenAI ў інтэрфейс LangChain. langsmith — гэта бібліятэка-кліент, якая вікорыстоўваецца для з’язку з бэкендам LangSmith для трэйсінгу і ацэнкі.

    Далей йдуюць зменныя сераўіса, якія увёлі трэйсінг без змены коду:

    export LANGSMITH_TRACING=true
    export LANGSMITH_API_KEY=your_langsmith_key
    export LANGSMITH_PROJECT=agent-improvement-loop
    export OPENAI_API_KEY=your_openai_key
    

    LANGSMITH_TRACING=true — гэта настройка. LANGSMITH_PROJECT называе праект, які групавае трэсы, і самэй гэта дазволяе ўпарцаваць іх пазней. Іншыя два параметры — гэта аутантыкі для LangSmith і прадаўца модэляў.

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

    Стварэнне агента

    Спачатку імпорты:

    # Step 1: Import the pieces we need
    from langchain_openai import ChatOpenAI
    from langchain_core.tools import tool
    from langgraph.prebuilt import create_react_agent
    

    Этыя імпорты прыносяць тры ўскладнення. ChatOpenAI абгружае API для завершэння чату з OpenAI для LangChain. Дэкаратар tool ператварае звычную функцію Python у функцыю, яку можа выкорыстоўваць агент. create_react_agent — гатовы падказчык з LangGraph, які стварае граф у стылі ReAct для вызову інструментаў.

    Потым два інструмента:

    # Step 2: Define two tools the agent can call
    @tool
    def get_account_info(account_id: str) -> str:
        """Look up basic information for an account by account_id."""
        fake_accounts = {
            "A100": "Account A100: plan=pro, status=active, email=ada@example.com",
            "A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
        }
        return fake_accounts.get(account_id, f"No account found with id {account_id}")
    
    @tool
    def get_recent_orders(account_id: str) -> str:
        """Return the three most recent orders for a given account_id."""
        fake_orders = {
            "A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
            "A200": "Orders for A200: no orders in the last 90 days",
        }
        return fake_orders.get(account_id, f"No orders for {account_id}")
    

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

    На завершанне агент асамблююцца:

    # Step 3: Wire the tools into a ReAct agent
    model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    tools = [get_account_info, get_recent_orders]
    agent = create_react_agent(model, tools)
    

    Мадэль чата ствараецца з тэмпературой нуль для адрадзення рынку. Артыкулы падаюць у спіс і перадаюцца разам з модэлью функцыі create_react_agent, яя вяртае скомпіляваны LangGraph StateGraph, які ужо мае крок разумавання, крок запуску артыкула і умовныя зв’язкі, якіе ўз’едынаюць іх.

    Тэставанне агента

    Просты запіт выканае тэставанне:

    # Step 4: Invoke the agent with a simple query
    result = agent.invoke({
        "messages": [{"role": "user", "content": "What plan is account A100 on?"}]
    })
    
    for message in result["messages"]:
        print(message.type, ":", message.content)
    

    У вызове перадаёцца слоўнік, які мае ключ messages. Гэта таму, што create_react_agent апыягаецца загадзя створанай схемай MessagesState, стан якой — гэта простае спіса чат-паведамленняў, які агент расшыроўвае пад час сваіх рашэнняў і вызоўу інструментаў. Пасля чаго цыкл выводзіць кожны тып паведамлення разам з яго контэнтам, тады весь ход задачы стае відным.

    Апэктыўны выхід:

    human : What plan is account A100 on?
    ai :
    tool : Account A100: plan=pro, status=active, email=ada@example.com
    ai : Account A100 is on the pro plan.
    

    Агент прыняў паведамленне ад корыстніка, пасля чаго выдала паведамленне ад AI, якое запусціла вызов інструмента (яго содэржынак пусты, таму што ў яму толькі вызов інструмента, без тэксту). Інструмент вернуў строку з данымі аб рахунку, і агент завершыў працу адпаведным адпаведзеннем. Адкрываючы LangSmith зараз, можна побачыць адну новую трэсу ў праекце agent-improvement-loop, пры чым кожны крок фіксуецца.

    Нічога ў тым кодзе не фіксавала гэтую трэсу. Як толькі у сераўе задаецца LANGSMITH_TRACING=true, кожны вызов адправляе вхідныя даны, выхідныя даны і проміжныя крокі да LangSmith. Самэ гэта можлівасць назірання і ёсць тым, ад чаго залежыць рэшта ціклу.

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

    Этап 2: збір следаў

    На этапе выкалываюцца следы з LangSmith і перадаюцца у код для аналізу. След — гэта адна повна праца агента, якая включае вхідныя даны, выходныя даны, вызывы інструментаў і час ўтрэмлення. Кліент LangSmith на Python перыядае працы з праекту, фільтруе іх па прыметах часовага перыоду і статусу, і прыменшвае іх у об’екты, якія можа выкарыстоўваць этап оцэнкі.

    Калькія тэрміны дапамагаюць лепш заразумець код:

    1. Праца — гэта адна елементарная частка запісанай працы, будзь то цэлы вызыв агента чы ўсерэдыні яго ўзначальны вызыв LLM. Адна інвокацыя агента стварае «дрэва» робат.
    2. Корневая праца — гэта праца высшага рангу аднаго вызыву агента, якая показваецца, калі трэба адна запісь на кожны запіт корыстніка.
    3. Праект — гэта прастор імен, да якога належыць праца; ён задаецца за дапамою LANGSMITH_PROJECT. Фільтрацыя па праекце дапамагае адсекаць працы з працы ў продакшэне ад робатаў пад час локальнай разработкі.

    Выкалыванне апошніх робатаў

    Спачатку ствараецца кліент:

    # Step 1: Create a LangSmith client
    from langsmith import Client
    from datetime import datetime, timedelta
    
    client = Client()
    

    Client імпортуецца з пакета langsmith і ствараецца без жадных аргументаў. Ён сам вычытвае LANGSMITH_API_KEY з сяродовішча, таму жадных аутантыкаў не пасылаецца.

    Потым пераказваюцца апошнія запускі root:

    # Step 2: List recent root runs from our project
    recent_runs = list(client.list_runs(
        project_name="agent-improvement-loop",
        is_root=True,
        start_time=datetime.utcnow() - timedelta(days=1),
    ))
    
    print(f"Found {len(recent_runs)} root runs in the last 24 hours")
    

    client.list_runs выклікаецца з трыма фільтрамі. project_name скарблівае запыт толькі для данага праекту, is_root=True залучае толькі вызовы агента высшага рэвэлю, а не кожны вкладны вызов LLM чы супутніх інструментаў, а start_time обмежвае рэзультаты першымі 24 гадзінамі. Ітэратар ўваходзіць у рэжым „ледзівага“ выканання, таму list() прымусвае яго ператворыцца на справжній спіс.

    Апэкаваны выхад:

    Found 42 root runs in the last 24 hours
    

    Зберагчыць толькі важлівыя поле

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

    # Step 3: Build a compact trace record from each run
    def compact_trace(run):
        return {
            "run_id": str(run.id),
            "inputs": run.inputs,
            "outputs": run.outputs,
            "error": run.error,
            "latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
                if run.end_time else None,
            "tool_calls": [
                child.name for child in client.list_runs(
                    trace_id=run.trace_id, run_type="tool"
                )
            ],
        }
    
    traces = [compact_trace(run) for run in recent_runs]
    print(f"Collected {len(traces)} compact traces")
    print("Example tool calls:", traces[0]["tool_calls"])
    

    Этый адаптар прымае неапдэтаваныя даныя і ператварае іх у маленькі слоўнік. Вхідныя і выходныя даны (run.inputs, run.outputs) маюць у сабе запыт корыстніка і фінальную адпаведзь, тады як run.error мае значэнне None у разе успеху і ў віглядзе строкі у разе неудачы. Затрымка вылічваецца шляхом вырахунку end_time за вырахунак start_time. Для вызоваў інструментаў патрэбен другі запыт у LangSmith з параметрам run_type="tool" і тым самым trace_id, што дае толькі даныя пра выконанні інструментаў у тым дрэве запытаў.

    Апэктыўны выход:

    Collected 42 compact traces
    Example tool calls: ['get_account_info']
    

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

    Перакананне колектара

    # Step 4: Inspect the first trace end to end
    import json
    print(json.dumps(traces[0], indent=2, default=str))
    

    Першыя даны выкладваюцца у відступленым фармаце JSON за дапамою json.dumps. Параметр default=str прыменяе функцыю json да тэкставання любых дадзеных, якія ёй нельгае серыялізаваць самай, што стосуецца тыпоў datetime і UUID, якія вяртае LangSmith.

    Апэктыўны выхід:

    {
      "run_id": "b1d2e3f4-...",
      "inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
      "outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
      "error": null,
      "latency_ms": 1423.51,
      "tool_calls": ["get_account_info"]
    }
    

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

    Стадыя 3: ацэнкаванне следоў

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

    1. Перакрантаванні на адміністрацыі коду — гэта быстрые дэтэрміністычныя функцыі, якія пераканальваюць структурныя факты, такія як тое, чы робат выкарыстаў правы інструмент або чы выходны матэрыял мае праглядзены фармат. Яны выяўляюць явныя аберанцы без жадных затрат.
    2. Суддзя-LLM — гэта другі мадэль, які оцэнюе суб’ектывныя характарыстыкі, такія як корыснасць, рэлевантнасць і тон. Ён можа працаваць з кожным запісам, але яго трэба настроіць па стандартам людскага суджэння, прычымо жадны не будзе яго верыць.
    3. Людская пераглядка дазволяе эксперту ў данай галіні аналізаваць тые запісы, якія автаматызацыя працягнула або оцэніла некансістэнтна. Гэта ўзвешаная правда, па якой настраіваюцца іншыя два слоі.

    Слою 1: правільнасць інструмента

    # Step 1: Define a rule that says which tool SHOULD be called for a given query
    import re
    
    def expected_tool_for_query(query: str) -> str:
        q = query.lower()
        if re.search(r"\b(order|shipment|shipped|return)\b", q):
            return "get_recent_orders"
        if re.search(r"\b(plan|account|status|email)\b", q):
            return "get_account_info"
        return "any"
    
    def score_tool_correctness(trace):
        query = trace["inputs"]["messages"][0]["content"]
        expected = expected_tool_for_query(query)
        actual = trace["tool_calls"][0] if trace["tool_calls"] else None
        if expected == "any":
            return 1.0
        return 1.0 if actual == expected else 0.0
    

    Заданы две функцыі. expected_tool_for_query прыкладвае рэгулярныя выразы да тэксту запіту, каб з’ясаваць, які інструмент павінен быў яго обрабацаваць. Гэта прымітная гістэрычная метода, якая падходзіць для выяўлення явных памылак маршрутацыі, але не для практычнага вікорыстоўвання. score_tool_correctness бера запіт корыстніка з логаў, вырахоўвае апэктывны інструмент і паўставляе яго з першым інструментам, які на самай працэ паклікаў агент. Неспадзянанне дае оцэнку 0.0, а спадзянанне — 1.0.

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

    Слой 2: суддзя на базе LLM для ацэнкі корыснасці

    # Step 2: Score qualitative properties with an LLM judge
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    
    judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    
    judge_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are an expert reviewer of AI agent responses. "
                   "Rate the response on a scale of 1 to 5 for helpfulness. "
                   "Respond with only a single integer, nothing else."),
        ("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
    ])
    
    def score_helpfulness(trace):
        question = trace["inputs"]["messages"][0]["content"]
        response = trace["outputs"]["messages"][-1]["content"]
        chain = judge_prompt | judge_model
        result = chain.invoke({"question": question, "response": response})
        try:
            return int(result.content.strip()) / 5.0
        except ValueError:
            return 0.0
    

    Маленькі ланцюгі LangChain ствараюцца праз перадачу шаблону запіту ў мадэль-суддзю. Палітры справы прымушваюць суддзю вяртаць толькі адзіне цэлое чысло з дыяпазону 1 да 5, таму рэзультат павінен быць дакладным. У функцыі score_helpfulness запіт корыстніка і фінальная адпаведзь агента запоўнююцца на базе даных па следам, ланцюг запускаецца, а цэлая цыфра прыбліжаецца да часовага значэння з дыяпазону 0 да 1, ўжо тады якое можа быць спінульована з рэйтынгам адзінства. Мадэль gpt-4o-mini выступае як суддзя, таму што ёй не трэба многа рэсурсоў і яна працуе быстра.

    Суддзі на адной базе LLM ёсць стахастычнымі, таму два запускі на аднаковых даных могу даць трохі разныя рэйтынгі. Рэкамендаціі компаніі Anthropic практыкуюць калібраванне суддзяў на адной базе дадзеных, пазначаных людзьмі, прытаму як толькі можна на іх пасполагаць, а трэція складовая саме такую базу стварае.

    Складовая 3: людская пераглядка як абзначэнняўная чэрга

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

    # Step 3: Model a human review signal
    def score_human(trace, human_labels: dict) -> float | None:
        run_id = trace["run_id"]
        return human_labels.get(run_id)
    
    human_labels = {
        "b1d2e3f4-...": 1.0,
        "c2e3f4g5-...": 0.0,
    }
    

    Функцыя шукае ідэнтыфікатор запуску ў слоўніку з атрыбутамі, заданымі людзьмі. Калі такі атрыбут існуе, ён вяртаецца, а калі ні — функцыя вяртае None, што означае, што запуск яшчэ не пераглянуты. У рэальных умовах такі слоўнік будзе знаходзіцца за API-інтэрфейсам чэргі анотацый LangSmith. Важнае — як паводзяцца рэйтынгі: ў іх мало значэнняў, таму што яны ёсць толькі для дзеяных запускаў; яны ёсць автаномнымі, таму што перакрываюць іншыя у разы незгоды; а таксама яны вплываюць на калібраванне судды-LLM.

    Аб’еднанне трохоў слоёў

    # Step 4: Run all three scorers and attach scores to each trace
    def enrich(trace):
        trace["scores"] = {
            "tool_correctness": score_tool_correctness(trace),
            "helpfulness": score_helpfulness(trace),
            "human": score_human(trace, human_labels),
        }
        return trace
    
    enriched = [enrich(t) for t in traces]
    print(json.dumps(enriched[0]["scores"], indent=2))
    

    Функцыя enrich дадае ўсе тры значэнні балоў у трэйс і вяртае яго. Яна прыменяецца да кожнага трэйса, засоўванага пад час збору, чым ствараецца спіс адзначаных трэйсоў, кожны з якіх мае структураваны слоўнік scores. Рэгулярны выраз, адаслужаны правіламі, вырабляе tool_correctness, суддзя LLM — helpfulness, а human — гэта чысло з паводлівым знакам або None.

    Апэкаваны выхід:

    {
      "tool_correctness": 1.0,
      "helpfulness": 0.8,
      "human": null
    }
    

    Тры ўскладнення не пагадваюцца ў своіх рэштатах. Параметр правільнасці інструмента паказвае, што быў выкліканы правільны інструмент, а суддзя LLM ставіць оцэнку 4 з 5 для адпаведнай адказу. Чалавечы рэвізор, які працюе пазней, можа сказаць, што інструмент быў правы, але формулюванне не падходзіла да тону брэнда. Кожны слой фіксуе тое, чаго не бачаюць іншыя, таму ўжыванне ўсіх слоёў дае болей шырокую адпаведнасць і больш точныя рэштаты за разныя цены.

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

    Этап 4: выявленне шаблонаў абыектаў

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

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

    Фільтрацыя за бягамі

    # Step 1: Keep only traces where at least one score is low
    def is_low_scoring(trace):
        scores = trace["scores"]
        if scores["human"] is not None and scores["human"] < 0.5:
            return True
        if scores["tool_correctness"] < 0.5:
            return True
        if scores["helpfulness"] < 0.6:
            return True
        return False
    
    failures = [t for t in enriched if is_low_scoring(t)]
    print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
    

    is_low_scoring адмініструе тры перэвіркі. Рэзультат, меншый за 0,5, позначае траўк як невдачу. У іншым случае автоматызаваныя оценкі візначаюць: якшо правільнасць інструмента ніжчая за 0,5, значыць агент направіў неправільна, а якшо корыснасць ніжчая за 0,6, значыць суддзя LLM не спадабалася адпаведź. Порогі можна налаштовваць, і гэтыя ёсць розумнымі стандартнымі значэннямі для першага проходжэння. Фільтр обрабоўвае паглубленыя траўкі з паканальнага етапу, каб стварыць набор невдач.

    Адказ, які чакаецца:

    9 of 42 traces flagged as low scoring
    

    Аб’яднанне неудач у катэгорыях

    Тут невеліка гістэрычная метода прызначае тагі неудачам. У рэальных умовах для ціяў можна выкарыстоваць методы кластэрування, або ж агента Insights Agent з LangChain, які робіць самэ гэта на адной з баз дадзеных LangSmith.

    # Step 2: Tag each failure with a category based on its scores
    def categorize(trace):
        scores = trace["scores"]
        if scores["tool_correctness"] < 0.5:
            return "wrong_tool"
        if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
            return "unhelpful_answer"
        if scores["human"] is not None and scores["human"] < 0.5:
            return "human_flagged"
        return "other"
    
    from collections import Counter
    categories = Counter(categorize(t) for t in failures)
    print(categories.most_common())
    

    Функцыя categorize пераглядае элементы па прыорытэтным парадку. Няправільны інструмент ёсць найбольш серйозной структурныя проблема, таму ён пазначаецца першым. Безполезны адказ, у яком быў выкарыстоўваны правільны інструмент, указвае на проблемы ў формулюванні запіта або генераванні адказу. Асоба, якая зазначыла абразок, а автаматызацыя яго працягнула, падпадае ў свой сабсклас. Стандартны клас Counter у Python підрахоўвае канцэнтрацыю кожной катагорыі, і ён імпортуецца працоўна там, колькі гэта ўпершыне выкарыстоўваецца.

    Апэктыванны выход:

    [('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
    

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

    Аналіз адзінаго шаблона

    # Step 3: Print the inputs and outputs for every wrong_tool failure
    wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
    for trace in wrong_tool[:3]:
        print("QUERY:", trace["inputs"]["messages"][0]["content"])
        print("TOOLS CALLED:", trace["tool_calls"])
        print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
        print("---")
    

    Няканае заблэкалася толькі до віпадакоў wrong_tool, а цыкл вывывае запіт корыстача, інстрыменты, якія факычна былі вызваны, таўзынкі для першых трох. Сюдзеў тут, дзе людзь абоягіякшы LLM вядзе доследжаць няканаласяе і стварае гіпазытлік, напрыклад, што кожны запіт з „subscription“ вызвае get_recent_orders замісто get_account_info. Гіпазытлік стае метой для палечыцявання.

    Апакслікаваны выхлед:

    QUERY: Is my subscription active?
    TOOLS CALLED: ['get_recent_orders']
    RESPONSE: I could not find any recent orders to confirm your subscription status.
    ---
    QUERY: Tell me about my subscription to your product
    TOOLS CALLED: ['get_recent_orders']
    RESPONSE: There are no recent orders for this account.
    ---
    

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

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

    Этап 5: наватраннія тэстовыя комплекты

    Хутчэй неудачы ператвараюцца ў статычныя тэсты. Кожная рэальная неудача ў працоўнай сітцы дадаецца да набора даных LangSmith як прыклад, які мае вхідны даныя (запыт карыстальніка), апэктыўнае паведанне (правільны інструмент або прыемлімая адпаведзь) і неабяжныя метаданы, такія як катэгорыя неудачы. Калі даныя зберагліся, яны ніколі не выходзяць, таму фіксаваная неудача не можа тыха вернуцца.

    Офлайн-ацэнка ў LangSmith складаецца з трох частак:

    1. Набор даных — гэта названая колекцыя прыкладоў, кожны з яых мае вхідныя даныя і, за выборам, апэктыўныя выхідныя даныя.
    2. Цэльная функцыя — гэта код, які тэстуецца; для агента гэта зазвычай яго метод .invoke.
    3. Ацэнкавальныя функцыі — гэта функцыі, якія оцэнююць выхідную даннę цэльной функцыі па кожным прыкладу, іх структура адпавядае структурэ функцый зэўсёды ацэнкі.

    Стварэнне набора даных

    # Step 1: Create a LangSmith dataset for our agent
    dataset_name = "agent-production-failures"
    
    dataset = client.create_dataset(
        dataset_name=dataset_name,
        description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
    )
    print(f"Created dataset: {dataset.id}")
    

    Той самы client з фазы збору дадзеных вызывае create_dataset з іменам і апісаннем, і вяртае об’ект дадзеных, чыё .id можна будзе выкарыстоўваць пазней. У рэальнай рэалізацыі гэта захоўвалася бы за дапамогою пераканання, чы не існуе ўжо такі об’ект, але дэманстрацыя прыпускае першы запуск.

    Апэктыўны выхід:

    Created dataset: 3f2a1b0c-...
    

    Ператвараць неудачы на прыклады

    # Step 2: Convert each failure trace into a dataset example
    examples = []
    for trace in failures:
        query = trace["inputs"]["messages"][0]["content"]
        expected_tool = expected_tool_for_query(query)
        examples.append({
            "inputs": {"question": query},
            "outputs": {"expected_tool": expected_tool},
            "metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
        })
    
    client.create_examples(dataset_id=dataset.id, examples=examples)
    print(f"Added {len(examples)} examples to {dataset_name}")
    

    Код пераглядае кожны след абярошчыны і стварае прыкладны словар з трыма полемі. inputs мае запіт корыстніка, outputs — адпаведна поведзенчаска характэрыстыка, якую трэба пераканаліць, тут гэта інструмент, які павінен быў вызвацца, а metadata — катэгорыю і первісны ID запуску, ўпэўніваючы такім чынам, што кожны прыклад можна будзе атрымаць з яго вучэльніка. Одна вызова client.create_examples з пакетам заваносіць усё.

    Апэкаваны выхід:

    Added 9 examples to agent-production-failures
    

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

    Атрыбутацыйны адказчык

    # Step 3: Write an evaluator that checks the agent's tool call against the expected tool
    def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
        actual_messages = outputs.get("messages", [])
        tool_called = None
        for m in actual_messages:
            if getattr(m, "tool_calls", None):
                tool_called = m.tool_calls[0]["name"]
                break
        expected = reference_outputs["expected_tool"]
        score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
        return {"key": "tool_match", "score": score}
    

    tool_match_evaluator выраховвае спяваны LangSmith: яго прымоўкі — inputs прызэрка, фактычныя outputs агента і reference_outputs. ы яго выхадныя месецы спакавае, знаходзе першы выхадны месец, які несе назву інстранга, тады выраховвае key і score. Назвы функцый і URL-якіясьтах захаваюцься абсалоўна як у LangSmith.

    Мэтавая функцыя

    # Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
    def run_agent(inputs: dict) -> dict:
        result = agent.invoke({
            "messages": [{"role": "user", "content": inputs["question"]}]
        })
        return result
    

    run_agent прыметна чынам застаўаецца мінімальным. У залежнасці ад inputs прыклада набора дадзеных, ён вызывае агента і вяртае тое, што ў фармаце сырога слоўніка, што падходзіць таму, чаго чакае функцыя evaluate з LangSmith. Агент, які вызываецца, быў створаны на стадыі фундаментаў.

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

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

    Стадыя 6: закрыцце ціклу

    Гэта ўраджэнне рэгрэсіі. Агент отрымае змяну, напрыклад, апошню версію опісу інструмента, прызначаную для выправлення бага з маршрутацыяю „падпіску“, які быў знайдзены раней. Перш чым запускаць продукт, весь набор дадзеных пра паказаныя раней неудачы проходзіць праз новую версію. LangSmith зберагае оба запускі як экспэрыменты і паказвае, прыклад за прыкладам, дзе новая версія павышыла качанне, а дзе ён паслабіўся. Продукт выпускаецца толькі тады, калі новая базовая версія перавышае старую, і калі нічога з таго, што было раней выправлена, не паслабіўся.

    Запуск ацэнкі базовай версіі

    # Step 1: Run the evaluator across the dataset with the current agent
    from langsmith.evaluation import evaluate
    
    experiment_results = evaluate(
        run_agent,
        data=dataset_name,
        evaluators=[tool_match_evaluator],
        experiment_prefix="agent-v1-baseline",
        max_concurrency=4,
    )
    

    evaluate імпортуецца з langsmith.evaluation. Ям даецца мета run_agent, назва набора дадзеных (якую LangSmith ператварае ў набор дадзеных з пярэднего этапа), спіс атрыбутаў, прыметнік эксперымента, які пазначае запуск, а таксама значэнне чатыро для паралельнага адкрыцця запускаў. Ён вяртае об’ект з рэзультатамі эксперымента, які вядомае да запуску ў LangSmith.

    Апэктыўны выход:

    View the evaluation results for experiment: 'agent-v1-baseline-...' at:
    https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
    
    9/9 runs | avg tool_match: 0.33
    

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

    Адправка парадксу і яго паўтарная перапытка

    Парадакс, запропанаванный на этапы аналізу шаблонаў, — гэта перапісаць докстрынг get_account_info на языке падпіскаў.

    # Step 2: Update the tool docstring to teach the agent about subscriptions
    @tool
    def get_account_info_v2(account_id: str) -> str:
        """Look up basic information for an account, including plan, status,
        subscription tier, and contact email. Use this tool for any question
        about the account itself, including subscription status."""
        fake_accounts = {
            "A100": "Account A100: plan=pro, status=active, email=ada@example.com",
            "A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
        }
        return fake_accounts.get(account_id, f"No account found with id {account_id}")
    
    tools_v2 = [get_account_info_v2, get_recent_orders]
    agent_v2 = create_react_agent(model, tools_v2)
    
    def run_agent_v2(inputs: dict) -> dict:
        result = agent_v2.invoke({
            "messages": [{"role": "user", "content": inputs["question"]}]
        })
        return result
    

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

    # Step 3: Run the evaluator on the v2 agent
    experiment_results_v2 = evaluate(
        run_agent_v2,
        data=dataset_name,
        evaluators=[tool_match_evaluator],
        experiment_prefix="agent-v2-docstring-fix",
        max_concurrency=4,
    )
    

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

    Апэктыўны рэзультат:

    View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
    https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
    
    9/9 runs | avg tool_match: 0.89
    

    Рэйтынг падняў з 0,33 да 0,89. Вясьмыя з дзевяціх адказоў былі выправлены, а дзевяты — гэта сапраўдна неодназначная запытка, якая можа патрабаваць адзначэння чалавекам.

    Before (v1):       After (v2):
    wrong_tool: 5      wrong_tool: 1
    unhelpful: 3       unhelpful: 0
    human: 1           human: 0
    avg score: 0.33    avg score: 0.89
    

    Калькольныя прычыны зробілі гэтыя параболенні надзеянымі:

    1. Начальная ацэнка дала конкрэтны бал пры тым, калі код яшчэ не быў зменены.
    2. Выправленне складалася з аднай тонкай правкі — адной докстрынгу.
    3. Паўторная ацэнка выявіла точны эфект гэтай правкі на адзнакаваныя рэальныя працоўкі.
    4. Набор дадзеных для тэстав заставался незменным, таму параболенні было адбывалася на роўных умовах.
    5. Кожны первыя працоўка цяпер фіксуецца як тэст, таму пазнейшыя змены не можаць ў тайны спанаваць яго.

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

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

    Адна цэлая ўседзённая выканання

    Адны працоўнік можа выконаць всю петлю адной раз пры наявнасці свежых дадзенняў з віробніцтва.

    # Full loop driver
    def run_improvement_loop():
        # 1. Pull traces
        runs = list(client.list_runs(
            project_name="agent-improvement-loop",
            is_root=True,
            start_time=datetime.utcnow() - timedelta(days=1),
        ))
        traces = [compact_trace(r) for r in runs]
    
        # 2. Enrich with scores
        enriched = [enrich(t) for t in traces]
    
        # 3. Find failures and categorize them
        failures = [t for t in enriched if is_low_scoring(t)]
        categories = Counter(categorize(t) for t in failures)
    
        # 4. Add failures to the dataset
        new_examples = [{
            "inputs": {"question": t["inputs"]["messages"][0]["content"]},
            "outputs": {"expected_tool": expected_tool_for_query(
                t["inputs"]["messages"][0]["content"])},
            "metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
        } for t in failures]
    
        if new_examples:
            client.create_examples(dataset_id=dataset.id, examples=new_examples)
    
        # 5. Re-evaluate the current agent against the full dataset
        result = evaluate(
            run_agent_v2,
            data=dataset_name,
            evaluators=[tool_match_evaluator],
            experiment_prefix="daily-regression",
        )
    
        return {
            "traces_collected": len(traces),
            "failures_found": len(failures),
            "by_category": dict(categories),
            "examples_added": len(new_examples),
        }
    
    print(run_improvement_loop())
    

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

    Апэктыўны выхід:

    {
      "traces_collected": 186,
      "failures_found": 12,
      "by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
      "examples_added": 12,
      "regression_score": 0.87
    }
    

    Што паказвае выходны рэзультат: за дзень было заследжана 186 прыемоў продукціі без жадных змян у кодзе агента, пры чым дванаццаць з іх мелі хоць адну нізкую оцэнку. Чысла некорысных адказоў зараз перавышае чысла неправільных вызоў інструментаў, што указвае на тое, што практыка керавання запытамі дала рэзультат, і што наступныя практыкі трэба застосавіць у працэсе стварэння адказоў. Рэгрэсійная оцэнка засталася на роўні 0,87, таму жадныя з змян за тыдзень не спрычынілі павтарэння ранейшых проблем. Дванаццаць новых прыкладоў было дадана да постояннага набора даных, дзе яны будуць служыць для запобежэння павтарэння.

    Пяць апдэйтаў, якія варта здзейсніць следам

    Кожны з іх адпавядае якой-небудзь функцыі LangChain, LangGraph або LangSmith, якую можна актывацыя без перабудовы вышэй упомянутай архітэктуры.

    1. Заменіце класыфікатор на адпаратнах выразах на эмбедынгі. OpenAIEmbeddings можа запрацавляць з кожным запітам, тады следы неудач скупляюцца за сэмантычным супадом, а не за правіламі ключоўых слоў, што дазволяе выявіць болей тонкія проблемы, такія як несупаданні тону.
    2. Заменіце вбудованага суддзю LLM на бібліятэку OpenEvals. У прыкладзе швыракаг запуску LangSmith вікорыстоўваецца openevals з готовымі запросамі, такімі як CORRECTNESS_PROMPT, які працуе лепшая, чым самастойна створаны суддзя, і паспелюе да варыянтав без апэляцый на рэферэнсы для онлайн-манітарынгу.
    3. Дадзіце можлівасць мнагатыповых ацэнкаў. Мнагатыповыя ацэнкі LangSmith запускаюць суддзю LLM па всіх дыялогах, а не толькі па адзіных адпаведзях, чым вядомае патэнціяўкі ў выводзе, якія працуе толькі па адзіных адпаведзях у чат-агентах.
  • Ўзайце агента Insights Agent з LangChain для адкрычэння шаблонаў у масштабе. Катэгорызатор на аднойчынных выразах справляецца з дзесяткамі абыектаў, якія не працуюць, але калі ўжо тыясчы, агент Insights Agent сама выдзельвае кластэры праектаў, аналізуе ўсё і автаматычна выяўляе способы абыектаў, якія не працуюць, прадаючы той самы структуру набора дадзенаў.
  • Адбывайце пераход на LangGraph праз поэтапную розгортку. Якшто застаўце версію v2 у праекце-канары з параметрам LANGSMITH_PROJECT=agent-canary на адзін дзень, а потым парабяруйце яе рэальныя рэзультаты з рэзультатамі контрольнага праекта, то ёй будзе заменена толькі тады, калі яна справдзіцца строго крашчай.
  • Пасля чаго цыкл зноў павтараецца, але ўжо на вышэйшай базовай лініі — з крашчымі дадзеннямі, крашчымі ацэнкамі і лепшым агентам.

    Спадневана літэратура

  • Закрытая сістэма оценкі для агента фото паверубе: ў чыхацеўскіяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяя