Галоўная / Артыкулы / Шукайце кнігі інструкцый, не прыкладвайце іх: маленькі пат у RAG у LangChain з апавяннямымі істочнікамі

Шукайце кнігі інструкцый, не прыкладвайце іх: маленькі пат у RAG у LangChain з апавяннямымі істочнікамі

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

2886 слоў

Шукайце ў кнігах інструкцый замест таго, каб запрашваць вялікі блакіты

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

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

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

indexing time
Markdown runbooks
  -> Document objects with source metadata
  -> smaller chunks
  -> embeddings
  -> vector store

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

request time
ticket classification
  -> focused search query
  -> retriever
  -> a few runbook passages with source IDs
  -> recommendation chain
  -> source-ID check

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

Тыкет, які викорыстоўваецца ў всім

Прыклад, які практыкуецца, — цэх тыкета, у яком пісаецца: запиты на расчытанне ў ЕС не работаюць пасля аплыву 14:05, а логі checkout-api паказваюць „database connection refused“. Його структураваная класыфікацыя выглядае так:

{
  "ticket_id": "INC-2048",
  "summary": "EU checkout fails after a deployment.",
  "category": "database",
  "severity": "SEV2",
  "service": "checkout-api",
  "confidence": 0.91,
  "evidence": ["database connection refused"],
  "missing_information": ["Affected request percentage"]
}

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

Шасць частак процэса адзысквання

Кожная частка пагаджаецца з дэфініцыяй прытаманной яе выкарыстоўванню.

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

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

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

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

Адаптаванне рэзультатаў. Як толькі да адаптавальніка даеце тэкст запиту, ён вяртае об’екты Document. Це шырэйя ідея, чым вектарны пошук: за аднаковым інтэрфейсам можна выкорыстоўваць падбіранне за ключовымі словамі, хранэнне вектараў, гібрыдную службу пошуку або власны API для пошуку.

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

Адаптаванне рэзультатаў — гэта не памяць

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

Структура і наладка проекта

Снімак стану на гэты ўражок мае такую структуру:

langchain-helpdesk/
├── app.py
├── chains.py
├── embeddings.py
├── retrieval.py
├── schemas.py
├── workflow.py
├── runbooks/
│   ├── checkout-database.md
│   ├── identity-access.md
│   └── payment-latency.md
└── tests/
    └── test_retrieval.py

З’явіце пакеты:

python -m pip install -U \
  langchain \
  langchain-openai \
  langchain-text-splitters \
  pydantic \
  python-dotenv \
  pytest

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

Шаг 1: напісаць інструкцыі, якія можна адрасаваць

Інструкцыя пракрэдытавання пачынаецца з рядка з заглавлём:

# Checkout database connection failures

Пасуюць служба, власнік і кузов:

Service: checkout-api
Owner: commerce-platformUse this runbook when checkout-api reports database connection refused,
pool exhaustion or authentication failures.## First checks1. Compare the active database host, port and database name with the
   previous successful deployment.
2. Confirm that the application secret points to the expected
   credential version.
3. Test network reachability from the application environment without
   printing the credential.
4. Compare connection-pool limits with the database connection limit.

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

Крок 2: запৱніце кожны файл у Дакумент

Уключыце load_runbooks у retrieval.py. Спачатку яму патрэбен памочны функцыя для парадку:

from pathlib import Path

А тады сам заўантажвач:

from langchain_core.documents import Document
def load_runbooks(runbook_dir: Path) -> list[Document]:
    documents: list[Document] = []    for path in sorted(runbook_dir.glob("*.md")):
        text = path.read_text(encoding="utf-8")
        title = text.splitlines()[0].removeprefix("# ").strip()
        service_line = next(
            line for line in text.splitlines()
            if line.startswith("Service:")
        )
        service = service_line.split(":", 1)[1].strip()        documents.append(
            Document(
                page_content=text,
                metadata={
                    "source_id": path.stem,
                    "title": title,
                    "service": service,
                    "path": str(path),
                },
            )
        )    if not documents:
        raise ValueError(f"No Markdown runbooks found in {runbook_dir}")    return documents

Практычныя аспекты:

  • glob("*.md") выбірае файлы у формате Markdown у аднам вядомым каталогу.
  • sorted(...) забезпечвае аднаковы порядак у локальным індексе. Якасць пошуку не павинна залежаць ад порядку файловай системы, але аднаковы вхідны даны спрыяюць лёгкам адлагоўванню.
  • Файл декодуецца у формате UTF-8.
  • Выяўленне заголовка і інформаціі пра сервіс делаецца спроставана, таму што прыкладныя файлы адпавядаюць аднам фіксаванаму формату. Рэальны загрузчык павінен пераканацца ў правільнасці метаданных і паведамляць пра некоректныя файлы, а не рабіць заўважэння наослуп.
  • page_content зберагае текст, які можна шукаты.
  • metadata зберагае поля, якія павінны быць пры кожным наступным фрагменте.
  • Паўнаважная пераканальніце памогае утримаць порядак, калі індекс застаецца порожнім.

Шаг 3: раздзеліце дакументы

Іспользуйце RecursiveCharacterTextSplitter:

from langchain_text_splitters import RecursiveCharacterTextSplitter
def split_runbooks(documents: list[Document]) -> list[Document]:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=420,
        chunk_overlap=60,
        add_start_index=True,
    )
    return splitter.split_documents(documents)

chunk_size=420 запрашвае часткі паразвычайна 420 симвалаў, што падходзіць для гэтага маленькага прыкладу. chunk_overlap=60 павтарае часткуцькі текст межы частакамі, што дазволяе заліць выражэнне праза ў сваёй адпаведнасці, калі межа падпадае ў середзіну. Большая перакрыцча не значыць апошней карысткі: гэта означае больш тексту, які трэба зберагчы, больш работы з усадкамі і больш рэзультатаў з майже тыпавымі данымі.

За дапамогою add_start_index=True кожная частка фіксуе офсэт симвала, дзе яна пачалася, тады вы можете вярнуцца да асловнага файла. Функцыя split_documents вяртае новыя об’екты Document, а кожная частка успадковуе мета-даны свайго абяцелі.

Выбір розмеру часткі

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

Крок 4: стварэнне прыстрою выдачы даных з векторным храненнем

Ёжы прыклад заставіць маленькім, прыстаўка викорыстоўвае InMemoryVectorStore:

from langchain_core.vectorstores import InMemoryVectorStore
def build_vector_retriever(runbook_dir: Path, embeddings, *, k: int = 3):
    chunks = split_runbooks(load_runbooks(runbook_dir))
    store = InMemoryVectorStore.from_documents(
        chunks,
        embedding=embeddings,
    )
    return store.as_retriever(search_kwargs={"k": k})

from_documents уключае і зберагае часткі, а as_retriever адкрывае стандартны інтэфейс для выкарыстоўвання дасягальнікав. k=3 запрашвае тры дакументы. Гэта не значыць, што ўсе тры є релевантнымі, і гэта не гарантуе, што сярод яных будзе правы аблакат.

Програма стварае уключэнні прадастальнікаў:

from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
    model=os.environ["EMBEDDING_MODEL"]
)
retriever = build_vector_retriever(
    Path(__file__).parent / "runbooks",
    embeddings,
)

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

Шаг 5: стварыць спецыялізаваны запит на аднойчыны падставе надзеяных полей

Не ведзіце пошук з вялічезным наборам даных усьго стану прыемлівання. Выберыце толькі неабходныя поля для класыфікацыі:

def build_query(classification: TicketClassification) -> str:
    values = [
        classification.service or "unknown service",
        classification.category,
        classification.summary,
        *classification.evidence,
    ]
    return " | ".join(values)

Для квитка на несколькі поўзды запит выглядае працоўна так:

checkout-api | database | EU checkout fails after a deployment. |
database connection refused

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

Шаг 6: перадача метаданых падчас канвертавання

Рэзультатам работы прыемніка являюцца об’екты Document, і кожны з яных павінен стаць аб’ектам з указаным типам:

class RetrievedPassage(BaseModel):
    source_id: str
    title: str
    service: str
    start_index: int
    text: str

Потым супарабяце Document з ім:

def to_passage(document: Document) -> RetrievedPassage:
    return RetrievedPassage(
        source_id=str(document.metadata["source_id"]),
        title=str(document.metadata["title"]),
        service=str(document.metadata["service"]),
        start_index=int(document.metadata.get("start_index", 0)),
        text=document.page_content.strip(),
    )

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

Шаг 7: стварэнне суцэльнага вхіднага даных для рекамендацый

Адпаведны модэлю практык утримвае гэты контракт:

class RecommendationInput(BaseModel):
    ticket_id: str
    ticket_summary: str
    category: Category
    severity: Severity
    service: str | None
    evidence: list[str]
    passages: list[RetrievedPassage] = Field(
        min_length=1,
        max_length=4,
    )

Калі не вяртаецца жадных пасажоў, немае чыяснай рэкамендаціі:

def prepare_recommendation_input(
    classification: TicketClassification,
    passages: list[RetrievedPassage],
) -> RecommendationInput:
    if not passages:
        raise ValueError("At least one runbook passage is required")
    return RecommendationInput(
        ticket_id=classification.ticket_id,
        ticket_summary=classification.summary,
        category=classification.category,
        severity=classification.severity,
        service=classification.service,
        evidence=classification.evidence,
        passages=passages[:4],
    )

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

Шаг 8: запытайце пра ідэнтыфікаторы вучоў у структураваным выходзе

Шэма рэзультата включае цітаты:

class GroundedRecommendation(BaseModel):
    explanation: str = Field(min_length=20, max_length=500)
    next_checks: list[str] = Field(min_length=1, max_length=4)
    source_ids: list[str] = Field(min_length=1, max_length=4)
    needs_human_review: bool

У запрасе напісана такая тэкст:

Use only the supplied ticket fields and runbook passages.
Every recommended check must be supported by at least one supplied passage.
Return the source_id values you used.
Do not claim that a check or production change was performed.

Інструкції щодо запиту допомагають, але перевірки на рівні застосунку все одно мають значення.

Крок 9: відхилити цитати, які не було отримано під час пошуку

Після того, як автор виконає пошук, необхідно порівняти його ідентифікатори джерел з отриманими уривками:

available = {passage.source_id for passage in passages}
unknown = set(recommendation.source_ids) - available
if unknown:
    raise ValueError(
        "Recommendation cited sources that retrieval did not return: "
        + ", ".join(sorted(unknown))
    )

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

Шаг 10: запярэдзіць правільную роботу процеса

Код, які выкарыстоўваецца пад час запиту, ёсць простым:

def run_retrieval_workflow(
    classification: TicketClassification,
    retriever,
    recommendation_chain,
) -> RetrievalResult:
    query = build_query(classification)
    passages = retrieve_passages(retriever, query)
    values = prepare_recommendation_input(classification, passages)
    recommendation = recommendation_chain.invoke(values)
    available = {passage.source_id for passage in passages}
    unknown = set(recommendation.source_ids) - available
    if unknown:
        raise ValueError("Recommendation cited an unavailable source")    return RetrievalResult(
        query=query,
        recommendation=recommendation,
        passages=passages,
    )

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

Што выходзіць

Рэзультат выглядае так:

{
  "query": "checkout-api | database | EU checkout fails after a deployment. | database connection refused",
  "recommendation": {
    "explanation": "The error matches the checkout database connection runbook.",
    "next_checks": [
      "Compare the active database host, port and database name with the previous successful deployment.",
      "Confirm that the application secret points to the expected credential version."
    ],
    "source_ids": ["checkout-database"],
    "needs_human_review": false
  },
  "passages": [
    {
      "source_id": "checkout-database",
      "title": "Checkout database connection failures",
      "service": "checkout-api",
      "start_index": 0
    }
  ]
}

API можа ператварыць кожны ідэнтыфікатор выхадных даных у гіператлас, але толькі пасля пераканання, што чырговы корыстувач мае право яго ачыць.

Тэставанне выявлення даных без оплата за эмбеддынгі

У супутнім зразку дадзеных є KeywordEmbeddings — невялікі тэстовы заменнік, який включае фіксаваны слоўнік:

VOCABULARY = (
    "database",
    "connection",
    "checkout",
    "deployment",
    "credential",
    "pool",
    "payment",
    "latency",
    "timeout",
    "identity",
    "access",
    "role",
    "claim",
)

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

passages = retrieve_passages(
    retriever,
    build_query(classification()),
)
assert passages[0].source_id == "checkout-database"
assert passages[0].service == "checkout-api"
assert passages[0].start_index >= 0

Другія меры былі застосаваны да двух з трох пазначаных запыткаў:

score = hit_at_k(
    retriever,
    [
        ("checkout database connection refused", "checkout-database"),
        ("payment provider latency timeout", "payment-latency"),
        ("identity role claim access denied", "identity-access"),
    ],
)
assert score == 1.0

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

У троцьцы тэста вводзіцца фальшывая рэкамендацыя, якая аднасецца да not-returned, і процес павінен яе адхіліць. Заўсёды запускайце все з:

pytest -q

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

3 passed

Назвы типаў неудач у выкарыстоўванні дадзеных

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

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

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

    Калі векторны пошук — гэта неправы інструмент

    Деякі пошуки не вимагаюць векторів. Точний ідентифікатор квитка має знаходитися у звичайному запиті до бази даних, а відома назва послуги безпосередньо відповідає запису в каталозі. Для точних кодів помилок та рідкісних ідентифікаторів краще використовувати пошук за ключовими словами. Якщо компанія вже має сервіс пошуку, який індексує відповідні документи та забезпечує контроль прав доступу, слід його використовувати. Векторний пошук — це лише один із кількох інструментів.

    Що ще бракує для впровадження

    Щоб компанія могла покладатися на цей підхід, все ще бракує кількох речей:

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

    Пазицыя прыкладнэца дапамогі

    Тепер ён можа шукаць у контролюванай колекцыі інструкцый і захоўваць свае выкананні:

    classification
      -> focused query
      -> retriever
      -> typed passages with metadata
      -> recommendation
      -> source-ID check
    

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

    Да дальнейшага чытання:

    • https://docs.langchain.com/oss/python/langchain/retrieval
    • https://docs.langchain.com/oss/python/langchain/knowledge-base
    • https://docs.langchain.com/oss/python/integrations/splitters/index
    • https://docs.langchain.com/oss/python/integrations/retrievers