Галоўная / Артыкулы / Ад звычайнага тексту да перакананых об’ектаў: выбір парсера выходных дадзеных у LangChain

Ад звычайнага тексту да перакананых об’ектаў: выбір парсера выходных дадзеных у LangChain

Пораўняйце StrOutputParser, JsonOutputParser, StructuredOutputParser і PydanticOutputParser у ланцюгах LangChain, а таксама дакладзіце ведома, якую ступеню структуры кожны з іх насправды гарантуюць.

5418 слоў

Модель чата вяртае текст, і такій тэксты ўсё гаразд для чалавека, який чытае на экране. Колі адпаведзь трэба падсунуць яшчо адну запитанне, захаваць у базе дадзеных або викорыстаць у компоненте інтэрфейсу, патрабуецца чыгучыя форматы дадзеных: чыстая строчка, об’ект JSON або заданы тип дадзеных, які вже быў перакантролаваны. LangChain заполняе гэты прыём парамаратрамі выходных дадзеных, якія прыносяць розныя гарантіі. У цым кераванні ствараецца той самы невялікі проект Сонечной системы з чатырма парамаратрамі, пояснюецца кожная лінія кожной ланцоўкі, а ў заключэнні даёцца практычны критэрый для выбору між ямі.

Якщо вы хочаце абы заглобнейшыя вяскаванні пра тое, як парсеры работаюць разам з LCEL, раннаблі і памяцю, пераглядзіце «Ад сырога тексту да пайплайнаў у LangChain». Чырвоны фокус тут шчырэй: што на самай працо обяцвае кожны парсер і дзе заканчываецца гэтая обява.

Чаму сырой текст модэлю недастаткова

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

The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.

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

{
    "fact_1": "The Solar System contains eight planets.",
    "fact_2": "The Sun is at the center of the Solar System.",
    "fact_3": "The Solar System also contains moons, asteroids, and comets."
}

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

Raw LLM Response
       ↓
Output Parser
       ↓
Parsed Output

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

Чатыры парсеры, чатыры рэвэлі структуры

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

  • StrOutputParser ператварае сообщэнне моделі на звычны строк Python.
  • JsonOutputParser парсуе адпаведзь у значэнне, сумеснае з JSON, такое як слоўнік або ліст.
  • StructuredOutputParser дазваляе задаць назвы полей, якія модэль должна вернуць.
  • PydanticOutputParser описвае падзейванне данных за дапамою модэлі Pydantic і пераканальвае выходныя даны па яго стандартам.
  • Незалежна ад таго, калькі з іх выбраць, шлях данных застаецца тым самым:

    LLM Response
         ↓
    Output Parser
         ↓
    Parsed Output
    

    Зменяецца тое, што выходзіць у канцы, і ступень довер'я да гэтых данных.

    Прыклад роботы: адчытна звітнасць, потым кантактная статыстыка

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

    Solar System
         ↓
    LLM
         ↓
    Detailed Report
         ↓
    LLM
         ↓
    5-Line Summary
    

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

    StrOutputParser: калі вам патрэбен толькі текст

    StrOutputParser — гэта простейшы парсер, які пропонуе LangChain. Моделі чату вяртаюць AIMessage; гэты парсер выкаанае ўзяць з яго контент і дае вам звычную строку. Гэты інструмент падходзіць тады, калі наступным прыемнікам є чалавек, іншы запит або будзь-што інше, што проста патрэбуе прозы, і вам не трэба JSON або схемы.

    Інтеграцыя ў процес стварэння адчытны

    У проекте report-then-summary шагі выглядаюць так:

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

    Solar System
         ↓
    Prompt 1
         ↓
    LLM
         ↓
    StrOutputParser
         ↓
    Detailed Report
         ↓
    Prompt 2
         ↓
    LLM
         ↓
    StrOutputParser
         ↓
    5-Line Summary
    

    Версія OpenAI

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

    from langchain_openai import ChatOpenAI
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    model = ChatOpenAI()
    # 1st prompt -> detailed report
    template1 = PromptTemplate(
        template='Write a detailed report on {topic}',
        input_variables=['topic']
    )
    # 2nd prompt -> summary
    template2 = PromptTemplate(
        template='Write a 5 line summary on the following text. /n {text}',
        input_variables=['text']
    )
    parser = StrOutputParser()
    chain = template1 | model | parser | template2 | model | parser
    result = chain.invoke({'topic': 'Solar System'})
    print(result)
    

    Модель ствараецца з стандартнымі настройкамі. Функцыя load_dotenv(), якая знаходзіцца вышэй, запоўнюе ключ API з локальнага файла .env, таму ў скрыпцы няма жадных аутантыфікацыйных данных.

    model = ChatOpenAI()
    

    Першы шаблон прыймае адну зменную, topic, і прасіць падробны абзац на яе тэму.

    template1 = PromptTemplate(
        template='Write a detailed report on {topic}',
        input_variables=['topic']
    )
    

    Другі шаблоны чакаюць зменную пад назвай text, яка будзе прымкать атлас, і запрашваюць пяцірадковы яе апুстак.

    template2 = PromptTemplate(
        template='Write a 5 line summary on the following text. /n {text}',
        input_variables=['text']
    )
    

    Яшчэ адна маленькая памылка, якую варта выправіць, якщо вы копіюеце гэта: строка шаблону містіць /n, яка ўсё-такі ёсць буквальным слэшам заўсёды літера n, а не пераводкай лініі. Якщо хочаце, каб атлас пачынаўся на новай лініі, вжывайце \n. Моделі зазвычай працуюць у будзь-якім разе, але запрос, які вы думаеце, што надсылаете, павінен быть тым самым запросам, які вы насправдэўна надсылаете.

    Далей ідзе парсер. Одну екземпляр можна перыскорыстаць у калькі момантов у ланцузе, таму што ён не зберагае стан між вызывамі.

    parser = StrOutputParser()
    

    Лінія, якая з’едынае все разам, — это визначэнне ланцуза:

    chain = template1 | model | parser | template2 | model | parser
    

    Аператар пайпа ўтворае структуру складання LCEL (LangChain Expression Language): выход кожнага компонента становіць вхід для наступнага. Якщо компоненты распакаваны верткая, то порядак выконання будзе такім:

    template1
        ↓
    model
        ↓
    parser
        ↓
    template2
        ↓
    model
        ↓
    parser
    

    Зверніце увагу, што дае вам першы парсер. Пасля першага вызову моделі парсер вяртае звёстку у вигляде строкі, і самэ гэтае значэння заполняе {text} у другім шаблоне. Другі парсер выканае тую ж задачу для фінальнай адпаведзі, таму рэзультат ланцюга — сама стыктура, а не об’ект звесткі.

    Ланцюг пачынаецца з перадачы слоўніка, ключы якога адпавядаюць зменным вхідных даных першага шаблону:

    result = chain.invoke({'topic': 'Solar System'})
    

    Потым вы адобразжаеце рэзультат:

    print(result)
    

    Што вяртае ланцюг

    Значэнне, якое выдрукаваецца ў канцы, — это пяцірадковая статыска, атрыманая з сгенераванага звярнення. Паколькі последнім элементам є StrOutputParser, рэзультатам являецца звычайны Python-об’ект str:

    print(result)
    

    Тыповая рэализацыя дае ў вынік нечаў такога роду:

    1. The Solar System consists of the Sun and all objects that orbit it.
    2. It includes eight planets, along with dwarf planets, moons, asteroids, and comets.
    3. The four inner planets are rocky, while the outer planets are mostly gas or ice giants.
    4. The Sun contains most of the Solar System's mass and provides the energy that drives many processes.
    5. The Solar System is located in the Milky Way galaxy.
    

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

    Той самы процес з модэлем Hugging Face

    Процес «звярненне-статыска» таксама можа выконваліцца з відкрытым модэлем. У гэтай версіі выкарыстоўваецца HuggingFaceEndpoint, настроены ў бак google/gemma-2-2b-it, і ён абгортаецца ў ChatHuggingFace, тады як бы ён быў чат-модэлем:

    from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    
    load_dotenv()
    
    llm = HuggingFaceEndpoint(
        repo_id="google/gemma-2-2b-it",
        task="text-generation"
    )
    
    model = ChatHuggingFace(llm=llm)
    
    # 1st prompt -> detailed report
    template1 = PromptTemplate(
        template='Write a detailed report on {topic}',
        input_variables=['topic']
    )
    
    # 2nd prompt -> summary
    template2 = PromptTemplate(
        template='Write a 5 line summary on the following text. /n {text}',
        input_variables=['text']
    )
    
    prompt1 = template1.invoke({'topic': 'Solar System'})
    result = model.invoke(prompt1)
    
    prompt2 = template2.invoke({'text': result.content})
    result1 = model.invoke(prompt2)
    
    print(result1.content)
    

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

    Паралельна, два падходы выглядаюць так. З OpenAI і парсерам:

    OpenAI
    
    Prompt
      ↓
    ChatOpenAI
      ↓
    StrOutputParser
      ↓
    String
    

    З Hugging Face і ручным доступам:

    Hugging Face
    
    Prompt
      ↓
    ChatHuggingFace
      ↓
    result.content
      ↓
    String
    

    Оба варыянты заканчваюцца стрэлкамі. Скрыпт OpenAI паказвае, як парсер выкаанае гэту задачу ў рамках ланцоўкі, тады як скрыпт Hugging Face паказвае тую ж логіку прыемлень з іншым прадаўцам дадзеных без парсера. Нічога не заважае напісаць template1 | model | parser | template2 | model | parser таксама з модэлем Hugging Face; парсеру не важліва, які прадаўцы было створанае сообщэнне.

    Вывад з гэтага крока: выбірайце StrOutputParser калі прыемленьне патрэбна толькі як текст.

    JsonOutputParser: JSON без контракту

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

    Ён не прыме паведамленняя пра адпрацоўкю конкрэтнага формата. Без схемы ён прасіць модель даваць адказ у формате JSON, але не назначае, якія ключы павінны быць і які тип маюць маты ўсе ўзоркі. Два запускі з аднаковым запитам могу заканчыцца об’ектамі разнага формата, і ваш код павінен быть гатовым да такога.

    Як усе вяруецца

    У этым проекте ад модэлі прасяць пяць фактов пра Сонечную систему. Крокі такія:

    1. Стварыць JsonOutputParser.
    2. Запросіць інструкцыі па формате за дапамою get_format_instructions().
    3. Уключыць гэтыя інструкцыі ў запит.
    4. Адправіць запит модэлі.
    5. Дазволіць парсеру ператворыць адказ у значэнне на Python.
    Solar System
         ↓
    PromptTemplate
         ↓
    Format Instructions
         ↓
    LLM
         ↓
    JsonOutputParser
         ↓
    JSON Object
    

    Версія OpenAI

    Уся ланцоўка ўсьмохаецца за короткі час:

    from langchain_openai import ChatOpenAI
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain_core.output_parsers import JsonOutputParser
    
    load_dotenv()
    # Define Model
    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    parser = JsonOutputParser()
    template = PromptTemplate(
        template="Give me 5 facts about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={"format_instruction": parser.get_format_instructions()},
    )
    chain = template | model | parser
    result = chain.invoke({"topic": "Solar System"})
    print(result)
    

    Модель налаштавана з gpt-4.1-mini і тэмпературой 0, што дазваляе выходным даннем застацца настолькі ж перадбачальным, насколькі гэта можа модель. Гэты элемент будзе запісваць пяць фактов.

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

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

    parser = JsonOutputParser()
    

    Інструкцыі па формате выканаюць рэальную работу

    Перад запускам моделі парсер можа описаць формат, які ён чакае. Гэты опис праграмаваны чераз адзін вызов методу:

    parser.get_format_instructions()
    

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

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Утварэны шаблон мае два месца для заполнення:

    template = PromptTemplate(
        template="Give me 5 facts about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={
            "format_instruction": parser.get_format_instructions()
        },
    )
    
    • {topic} задаецца пад час вызову і пазначае тое, пра шта трэба адказаць.
    • {format_instruction} заздалегідь заполняецца інструкцыямі па форматаванні з боку парсера.

    Тады, калі вы вызываеце ланцюг з такім вхідным дадзеннем, модель отрымае як тэму, так і інструкцыю адказаць у формате JSON:

    {"topic": "Solar System"}
    

    Складанне і запуск ланцюга

    Сам ланцюг складаецца толькі з трохоў стадій:

    chain = template | model | parser
    

    Па порядку выканання:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    JsonOutputParser
          ↓
    Parsed JSON
    

    Шаблон атрыбутуеўае гэты праграмны запит, ChatOpenAI адказвае на яго, а JsonOutputParser парсуе адказ у дадзеныя на мове Python. Парсер таксама прыменшвае значэнне дзеянняя да дазнаек моделі, такіх як абгортаванне JSON у рамкі коду Markdown, якія ён адключае перад парсаваннем.

    Выклікайце яго з паведамленнем:

    result = chain.invoke({"topic": "Solar System"})
    

    І выведзіце тое, што вернулася:

    print(result)
    

    Што вернулася

    Рэзультат містіць пяць фактов у формате JSON. Скрыпта выводзіць толькі значэнне, таму няма стандартнага выходу, які можна було бы цитаваць; правдападобны адпаведзенне выглядае так:

    {
      "facts": [
        "The Solar System is centered around the Sun.",
        "There are eight recognized planets in the Solar System.",
        "The four inner planets are rocky planets.",
        "The outer planets include gas giants and ice giants.",
        "The Solar System is located in the Milky Way galaxy."
      ]
    }
    

    Такі формат, калі адна клучовая запісь facts містіць список, — адна з кальк усіх форматаў, якіе можа выбраць модэль. Іншы запуск можа вернуць fact_1, fact_2, fact_3, fact_4 і fact_5, або простаі список. Якщо код, які працуе далей, будзе адсылацца да result["facts"], ён перастане працаваць тады, калі модэль выберазь іншы формат.

    Аналіз прайму

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

    Prompt
      ↓
    JSON Format Instructions
      ↓
    LLM
      ↓
    JsonOutputParser
      ↓
    JSON
    

    Інструкцыі ствараецца сам паспраўчык:

    parser.get_format_instructions()
    

    Яны падаюць у запыт чераз частковую зменную:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Модель адпавядае, выкарыстоўваючы гэтыя інструкцыі, а паспраўчык ператварае адпаведзь у значэнне на Python. Ад початку да канца:

    Solar System
         ↓
    PromptTemplate
         ↓
    JSON Format Instructions
         ↓
    ChatOpenAI
         ↓
    JsonOutputParser
         ↓
    JSON Object
    

    Разлік з паканальным ўзламом помешчаецца у двух лініях. StrOutputParser стварае:

    StrOutputParser
        ↓
    Plain String
    

    тады як JsonOutputParser стварае:

    JsonOutputParser
        ↓
    JSON-compatible Structured Data
    

    Памятаць пра адзін патрабавак: яшчэ не існуе фіксованага шымату. Вы отрымаеце JSON, але полі і ўзвесці залежаць ад моделі. Да дапамоги: чырвоныя версіі JsonOutputParser таксама прыменяюць неабавязковы параметр pydantic_object, які дадае шымат да інструкцый формата, але у паданай тут форме, без параметраў, ён проста выкарыстоўвае правільны JSON.

    Варыянт Hugging Face

    Рабочы лямдж JSON адразу пераходзіць да модэлі Gemma. Няўстаноўляецца настройка модэлі; парсер, інструкцыі па формате і ланцоўка не змянююцца:

    from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain_core.output_parsers import JsonOutputParser
    
    load_dotenv()
    # Define the model
    llm = HuggingFaceEndpoint(
        repo_id="google/gemma-2-2b-it",
        task="text-generation"
    )
    model = ChatHuggingFace(llm=llm)
    parser = JsonOutputParser()
    template = PromptTemplate(
        template='Give me 5 facts about {topic} \n {format_instruction}',
        input_variables=['topic'],
        partial_variables={
            'format_instruction': parser.get_format_instructions()
        }
    )
    chain = template | model | parser
    result = chain.invoke({'topic': 'Solar System'})
    print(result)
    

    Пайплайн ідэнтычны, за выключэнням блока з модэллю:

    PromptTemplate
          ↓
    Hugging Face Model
          ↓
    JsonOutputParser
          ↓
    JSON Object
    

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

    StructuredOutputParser: называнне поль, якія вы чакаеце

    StructuredOutputParser выкарыстоўвае JSON на адной з лістаў поль, якія вы задаеце заздалегідь. Якщо JsonOutputParser проста кажа «адказ у формате JSON», то гэты парсер кажа «адказ у формате JSON з гэтымі ключамі».

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

    Праект «тры факты»

    У гэтым праекте запрашваюцца тры факты пра Сонячную систему, праз аднае поле на кожны факт:

    • fact_1 мае першы факт пра даную тэму.
    • fact_2 мае другі факт.
    • fact_3 мае трэці факт.

    Парсер ператварае гэтыя заданнія на інструкцыі, а пасля чытае адказ і пераканальваецца, што ён паспелаў да іх выпанення:

    Solar System
         ↓
    PromptTemplate
         ↓
    Predefined Field Schema
         ↓
        LLM
         ↓
    StructuredOutputParser
         ↓
    Structured JSON
    

    Версія OpenAI

    Цэўны скрыпт выглядае так:

    from langchain_openai import ChatOpenAI
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain.output_parsers import StructuredOutputParser, ResponseSchema
    
    load_dotenv()
    # Define Model
    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    schema = [
        ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
        ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
        ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
    ]
    parser = StructuredOutputParser.from_response_schemas(schema)
    template = PromptTemplate(
        template="Give 3 fact about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={"format_instruction": parser.get_format_instructions()},
    )
    chain = template | model | parser
    result = chain.invoke({"topic": "Solar System"})
    print(result)
    

    Модель застаецца той жа з конфігурацыяй gpt-4.1-mini, як і ранейш:

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

    Праўыя разлікі пачынаюцца зі спісу схем:

    schema = [
        ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
        ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
        ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
    ]
    

    Кожны элемент ResponseSchema дапамагае двум рошчынам:

    • name стае ключом у падсумковым слоўніку.
    • description паведамляе модэль, што саме должна містіць гэты ключ.

    Тры схемы даўаюць тры неабходныя ключы: fact_1, fact_2 і fact_3.

    Вы не ствараеце гэты парсер безпасабліва. Метод класа стварае яго на адной з схем:

    parser = StructuredOutputParser.from_response_schemas(schema)
    

    Як і у разе з парсерам JSON, інструкцыі па формату таксама выходзяць з парсера:

    parser.get_format_instructions()
    

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

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Шаблон запиту мае два звычныя месца для падставлення:

    template = PromptTemplate(
        template="Give 3 fact about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={
            "format_instruction": parser.get_format_instructions()
        },
    )
    

    {topic} заполняецца пад час вызову, а {format_instruction} містіць список поль, створаны на адпаведных схемах. Калі выклікаць функцыю з такімі даннымі, яны оба надаюцца модэлю:

    {"topic": "Solar System"}
    

    Адкліканне ланцюга

    Ланцюг складаецца з тых сабе трох паэтапаў, як і раней:

    chain = template | model | parser
    

    Калі парсер знаходзіцца на апошней пазіцыі:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    StructuredOutputParser
          ↓
    Structured JSON
    

    Шаблон стварае запит, модэль даёт адказ, а StructuredOutputParser выкарыстоўвае яго, каб выявіць заявленыя поля з адпаведнага адказу.

    result = chain.invoke({"topic": "Solar System"})
    
    print(result)
    

    Як выглядае выходны результат

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

    print(result)
    

    А прыкладны рэзультат выглядае так:

    {
        "fact_1": "The Solar System is centered around the Sun.",
        "fact_2": "There are eight recognized planets in the Solar System.",
        "fact_3": "The Solar System is located in the Milky Way galaxy."
    }
    

    Конкрэтныя элементы буду разнымі. Але набор ключоў не павінен зменяцца:

    fact_1
    fact_2
    fact_3
    

    Это ўласна перавага такога падходу пры парсінгу у пораўнанне з звычным JSON: ваша аплікацыя сама выбірае назвы поль, а не модэль. Якщо модэль прыховае адні з заданых ключоў, парсер выдае памылку, а не таямна вяртае іншую структуру дадзеных, што набагато лёгкае для адкуплення, чым KeyError чераз тры функціі пасля таго.

    Аналіз прайму

    Полны шлях, ад тэмы да рэзультата з ключамі:

    Solar System
         ↓
    PromptTemplate
         ↓
    ResponseSchema
         ↓
    Format Instructions
         ↓
    ChatOpenAI
         ↓
    StructuredOutputParser
         ↓
    {
        fact_1: ...,
        fact_2: ...,
        fact_3: ...
    }
    

    Усё пачынаецца з апісаў полей:

    ResponseSchema(
        name="fact_1",
        description="Fact 1 about the topic"
    )
    

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

    JsonOutputParser
           ↓
    JSON output
           ↓
    Structure can vary
    
    StructuredOutputParser
           ↓
    Predefined fields
           ↓
    More controlled structure
    

    Корача кажучы, JsonOutputParser служыць для отрымання JSON у загальнай форме, тады як StructuredOutputParser — для отрымання JSON з ключамі, якія вы запрашавалі.

    Є адзін ліміт, які варта чытальна адзначыць. У ResponseSchema ёсць атрыбут type, які за замовчаннем ўстановліваеся на string, але ён толькі зменяе формулюванне інструкцый. Парсер пераканальваецца, што ключы ўсё ж такі ўсунуты; ён не пераканальваецца у типах і дыяпазонах значэнняў. Якщо вам патрэбна, каб age быў цэлым числам, большым за певны порог, гэты парсер гэтага не будзе выкананы.

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

    Варыянт Hugging Face

    Версія Gemma пераўтарытае выкарыстоўвае тыя ж тры схемы і той самы спосаб будовы парсера:

    from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain.output_parsers import StructuredOutputParser, ResponseSchema
    
    load_dotenv()
    # Define the model
    llm = HuggingFaceEndpoint(
        repo_id="google/gemma-2-2b-it",
        task="text-generation"
    )
    model = ChatHuggingFace(llm=llm)
    schema = [
        ResponseSchema(name='fact_1', description='Fact 1 about the topic'),
        ResponseSchema(name='fact_2', description='Fact 2 about the topic'),
        ResponseSchema(name='fact_3', description='Fact 3 about the topic'),
    ]
    parser = StructuredOutputParser.from_response_schemas(schema)
    template = PromptTemplate(
        template='Give 3 fact about {topic} \n {format_instruction}',
        input_variables=['topic'],
        partial_variables={
            'format_instruction': parser.get_format_instructions()
        }
    )
    chain = template | model | parser
    result = chain.invoke({'topic': 'Solar System'})
    print(result)
    

    І той самы пайплайн:

    PromptTemplate
          ↓
    ChatHuggingFace
          ↓
    StructuredOutputParser
          ↓
    Structured JSON
    

    Разлікаецца толькі прадаўцам модэлю. Схема і парсер застаюцца тымі ж.

    PydanticOutputParser: структура плюс верыфікацыя

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

    Это выклікаецца за межы простага парсавання. Поле несуць сабэтыяяя рэальныя типы Python, а Field() можа дадзіць абмежэння, напрыклад, што цалое лічыба павінна перасягаць мінімум. Калі адпаведна адказа модэлі іх не выпанавляе, вытыкаецца асывянне, а не некалькаснае даныя.

    Чаму варта дадзіць дапамогу додатковай наладке

    • Абавязковасць схемы: адказа павінна адпаважаць чытко заданай структуры.
    • Безпека типу: поля выкарыстоўваюць типы Python, такія як str, int і float, а значэнні ў залежнасці ад гэтага пераканваныя або адхіленыя.
    • Параболка: Pydantic пераканвалівае кожная з абмежэнняў, якія вы задаеце.
    • Інтэграцыя ў ланцюжкі: яна падключаецца да запытанняў, модэляў і ланцюжкаў LCEL так сама, як і іншыя парсары.

    Праект з вымышленымі аб’ектамі

    У гэтым прыкладзе ад моделі запрашваецца стварыць персону з заданага месца, тут «Індія», з трыма палаткамі:

    • name — імя персоны.
    • age — вяк персоны.
    • city — горад, у яком яна жыве.

    Для палаткі age таксама ёсць абавянне: яна павінна быць большай за 18.

    Input
      ↓
    PromptTemplate
      ↓
    Pydantic Model
      ↓
    Format Instructions
      ↓
    LLM
      ↓
    PydanticOutputParser
      ↓
    Validated Pydantic Object
    

    Версія OpenAI

    Полны скрыпт:

    from langchain_openai import ChatOpenAI
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain_core.output_parsers import PydanticOutputParser
    from pydantic import BaseModel, Field
    
    load_dotenv()
    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    class Person(BaseModel):
        name: str = Field(description="Name of the person")
        age: int = Field(gt=18, description="Age of the person")
        city: str = Field(description="Name of the city of the person")
    parser = PydanticOutputParser(pydantic_object=Person)
    template = PromptTemplate(
        template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
        input_variables=["place"],
        partial_variables={"format_instruction": parser.get_format_instructions()},
    )
    chain = template | model | parser
    final_result = chain.invoke({"place": "Indian"})
    print(final_result)
    

    Ён следуе там жа самым прымерам, як і раней: задаецца модэль Person, яна передаецца PydanticOutputParser, а пасля парсер паў’язуецца з запитам і модэлью.

    Канфігурацыя модэлі не зменілася:

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

    Ключоўы элемент — это клас Pydantic:

    class Person(BaseModel):
        name: str = Field(description="Name of the person")
        age: int = Field(gt=18, description="Age of the person")
        city: str = Field(description="Name of the city of the person")
    

    Гэтае ўмовы для адпаведзі:

    • name павінна быць строкай.
  • age павінен быть цэлым лячынам, строгая што большы за 18.
  • city павінен быть строкай.
  • Field() прыўязвае як опис, чытальны для чалавека, які будзе прадстаўлены ў запитанні, так і абмежэнні, якія пераканальваюцца пасля парсавання. Сама абмежаная запоўненне:

    age: int = Field(gt=18, description="Age of the person")
    

    gt=18 значыць „больша за 18“, таму вік роўны 18 не праходзіць верыфікацыю. Якщо вы мелі на увазе „18 або больш“, іспользуйце ge=18 у замену.

    Парсер ствараецца праз перадачу класу, а не экземпляра:

    parser = PydanticOutputParser(pydantic_object=Person)
    

    Это паведамляе яму, які модэль выкарыстоўваць як для генеравання інструкцый, так і для верыфікацыі адпаведзі.

    Інструкцыйы па формату прыходзяць з таго ж методу, як і раней:

    parser.get_format_instructions()
    

    Для гэтага парсера ў іх ёсць JSON Schema, створаны з модэлю Pydantic, які включае апісанні поль і абмежэнне exclusiveMinimum для поля age. Яны дадаюцца через зменную partial, як і зазвычай:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Шаблон запиту:

    template = PromptTemplate(
        template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
        input_variables=["place"],
        partial_variables={"format_instruction": parser.get_format_instructions()},
    )
    

    {place} кантролюе, якога роду адзёбніка должен стварыць модэль. Выкліканне з гэтым вхідным дадзеннем запрашвае імя, возраст і горад вымышленага жытеля Індіі:

    {"place": "Indian"}
    

    Адклёканне ланцоўкі

    Ланцоўка застаецца у звычнай трывекторной структуре:

    chain = template | model | parser
    

    Гэты раз фінальны этап вяртае экзанпляр модэлю:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    PydanticOutputParser
          ↓
    Pydantic Object
    

    Шаблон стварае запит, модэль даёт адпаведны адказ, а PydanticOutputParser парсуе JSON і пераканае яго ў формате Person.

    final_result = chain.invoke({"place": "Indian"})
    
    print(final_result)
    

    Як выглядае выхідны результат

    Рэзультатам являецца об’ект Person, а не слоўнік. Яго выведэнне паказвае стандартную форму адображэння з боку Pydantic:

    name='Rahul Sharma' age=28 city='Mumbai'
    

    Значэння буду разнымі праз кожны запуск. Гарантіі ж не буду такімі:

    name → string
    age  → integer (> 18)
    city → string
    

    Сюды сама ясна праблескае разніца. JsonOutputParser прасіў толькі JSON, StructuredOutputParser задаў назвы поль, а PydanticOutputParser представляе весь контракт у вачынку рэальнага класу. Вы можете выкарыстоўваць автодополненне рэдагувальніка, каб з’ясаваць final_result.age, і быць впэўненымі, што гэта int, большы за 18, таму што ўсё інше спрычынала б паказанне памылкі верыфікацыі ўжо до таго, як код будзе запрацаваны.

    Аналіз прайходу даных

    З вхідных дадзенняў да аправеранага об’екта:

    "Indian"
        ↓
    PromptTemplate
        ↓
    Pydantic Model
        ↓
    Format Instructions
        ↓
    ChatOpenAI
        ↓
    PydanticOutputParser
        ↓
    Person Object
    

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

    class Person(BaseModel):
        name: str
        age: int
        city: str
    

    Клас перадаецца парсеру:

    PydanticOutputParser(pydantic_object=Person)
    

    Парсер генеруе інструкцыі на аднойчынку з модэлем, гэтыя інструкцыі включаюцца ў запит, модэль адпавядае, а парсер перакладае адпаведзь у об’ект Person і выканае пераказанне Pydantic на яго. Канцэптуальна:

    Pydantic Model
          ↓
    Defines Structure + Types + Constraints
          ↓
    LLM Response
          ↓
    PydanticOutputParser
          ↓
    Validated Pydantic Object
    

    У канцы вы отрымліваеце об’ект на Python, дакладныя даны якога гарантаванае адпаведнасць вашым правілам, што ўжо больш, чым можа запрадзець будзь-який слоўнік JSON.

    Адна з практычных наследзеў: неудача пераказання праказваецца як OutputParserException у ланцюгу. Паказваце, што трэба рабіць далей. Частыя варыянты — перапрыткі календара, адправка памылкі назад да модэля за дапамою OutputFixingParser з LangChain, або логаванне і вярненне безпечнага стандартнага значэння.

    Варыянт Hugging Face

    Версія Gemma выклекчвае той самы модель Person і перадае яго тому ж паспрацоўвальніку:

    from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
    from dotenv import load_dotenv
    from langchain_core.prompts import PromptTemplate
    from langchain_core.output_parsers import PydanticOutputParser
    from pydantic import BaseModel, Field
    
    load_dotenv()
    llm = HuggingFaceEndpoint(
        repo_id="google/gemma-2-2b-it",
        task="text-generation"
    )
    model = ChatHuggingFace(llm=llm)
    class Person(BaseModel):
        name: str = Field(description='Name of the person')
        age: int = Field(gt=18, description='Age of the person')
        city: str = Field(description='Name of the city the person belongs to')
    parser = PydanticOutputParser(pydantic_object=Person)
    template = PromptTemplate(
        template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
        input_variables=['place'],
        partial_variables={
            'format_instruction': parser.get_format_instructions()
        }
    )
    chain = template | model | parser
    final_result = chain.invoke({'place': 'Indian'})
    print(final_result)
    

    Працэс обробкі застаецца незменным:

    PromptTemplate
          ↓
    ChatHuggingFace
          ↓
    PydanticOutputParser
          ↓
    Pydantic Object
    

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

    Выбор правильнага паспрацоўвальніка

    Рашэнне залежыць ад таго, які ступень структуры і які ступень верыфікацыі насправды патрэбны корыстніку адпаведзі. Чырвоныя кожнага варыянта падсумаваны.

    StrOutputParser

    Ўжывайце яго, калі адпаведзь моделі — проста тэкст.

    • Найлепей падходзіць для: атласоў, адказаў, падсумакоў, адпаведзей у чаты.
    • Вернюе: строку.
    • Паспрацоўкі JSON: няма.
  • Схема: няма.
  • Адзначэнне правамоўнасці: няма.
  • JsonOutputParser

    Ўжывайце яго, калі вам патрэбны JSON-данныя, але вы можете прыміць або обрабацваць данныя з мензульным форматам.

    • Найлепейшы для: дакладнага структураванага выходу, гнучкіх пакетаў дадзеных.
    • Вернюе: слоўнік або спіс.
    • Парасинг JSON: так.
    • Схема: няма (у форме без аргументаў, якая выкорыстоўваецца тут).
    • Адзначэнне правамоўнасці: толькі перакананне, што данныя ў формате JSON.

    StructuredOutputParser

    Ўжывайце яго, калі ваш код чакае конкрэтныя ключы, такія як fact_1 да fact_3.

    • Найлепейшы для: простых записаў з вядомымі іменамі полей.
    • Вернюе: слоўнік з указанымі ключамі.
    • Парасинг JSON: так.
    • Схема: так, імены полей і ўпісы.
    • Адзначэнне правамоўнасці: толькі наявнасць ключа, без пераканання типу.

    PydanticOutputParser

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

    • Найлепейшы варыянт для: дадзеных, якія вы зберагаеце, обрабоўваеце або перадаеце API.
    • Вернюе: экземпляр вашага модэлю Pydantic.
    • Распакоўка JSON: так.
    • Шымат: так, усі типы і абмежэнні.
    • Параболка: так.

    Короткі ментальны модэль

    • StrOutputParser: дапаможыць можа толькі текст.
    • JsonOutputParser: падойдзе будзь-які правільны JSON.
    • StructuredOutputParser: JSON павінен мець гэтыя ключы.
    • PydanticOutputParser: пераканальваюцца гэтыя ключы, гэтыя типы і ўсе абмежэнні.

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

    Існуе ўтолькі што адна ўзлегка альтэрнатыва. Усе чатыры парсеры працуюць па принцыпу апісання формата ў запите, пасля чаго выконваюць парсінг тексту. Большасць модэляў чата таксама падтрымляюць натыўны структураваны выхід або вызыв інструментаў, што LangChain дае можлівасць за дапамогою with_structured_output() у модэлі. Калі ваш прадаўцы служб падтрымляе гэты падход, ён зазвычай є болей надзеяным для дакументаў у формате схемы, тады як парсеры, базаваныя на запітах, застаюцца корыстнымі для прадаўцоў і модэляў, якія гэтага не падтрымляюць.

    Ключовыя выводы

    • Парсеры выхідных даных ператвараюць сообщэння модэля ў значэнне, якое можа быць выкарыстана вашым кодам, і яны включаюцца ў ланцюгі LCEL за дапамогою аператара “|”.
    • StrOutputParser адключае обгортку сообщэння, тады текст аднаў модэля можа стаць вхідным дадзенням для наступнага запиту.
    • JsonOutputParser парсіруе JSON, але не выправляе яго структуру, якщо толькі вы не задасте схему.
  • StructuredOutputParser выправляе назвы каналаў чераз ResponseSchema, але не пераказвае тыпы значэнняў.
  • PydanticOutputParser спалюе процэс парсавання з пераказамі тыпаў і абмежэннямі, вяртаючы рэальны об’ект.
  • Інструкцыі па формату ўсё толькі яўляюцься текстам запыту: модель все ўсё можа іх ігнараваць, таму трэба ўрахоўваць можлівыя бяды па парсаванню і верыфікацыі, асобліва з меньшымі відкрытымі моделямі.
  • Змена прадастальніка не вплывае на парсар, што спрыяе прыемнаму паўтарным порэванню хоставаных і відкрытых модэляў для аднаго і тога ж заведама.
  • Калі адказы прыходзяць у надзеянай форме, наступны логічны крок — стварэнне большых рабочых практыкаў з калькольнікаў запытанняў, модэляў і парсероў, уключаючы секвэнцыйныя, паралельныя і умовныя ланцюгі, створаныя з RunnableParallel і RunnableBranch. Па гэтай тэме адзірніце складанне практыкаў LangChain з LCEL.