Вбудоўванне агента LangChain у FastAPI: інструменты, пошук інструкцый, трансляцыя па стрыму.
Створыце асистента ў самай працоўнікавой програме з FastAPI і LangChain: PDF-інструкцыю ў ChromaDB, якая выкладваецца як інструмент, контэкст для кожнага корыстувальця, історыю з пунктамі контролю і адпаведзі, якія трансляюцца па стрыму.
Калі аплікацыя вырастае за межы колькісці калькаў, яе дасягненні начынаюць расплывацца, і корыстувачы перестаюць яе чытаць. 20-сторонній інструкцыйны дапамога, які поясняе функцыі, настройкі і правілы домэна, ёсць ценным, але толькі якщо людзі можаць знайсці ў яму адпаведзенне без даганяння. Памочны прыстрой, вбудованный у продукт, можа заполніць гэты прыемак: ён адпавядае на запитанні «Як гэта працюе?» з інструкцыйнага дапамогі і «Што ёсць у маём проекте?» на адпаведныя даны самай аплікацыі.
У гэтым кялікаванні практычна адпрацоўаная версія такога асистэнта. Вы падключыце агента LangChain да службы FastAPI, задастэ ему адну засобу для чытання дадзэнняў прыкладнення і другую засобу для пошуку PDF-інструкцыі, якая зберагаецца ў ChromaDB, перадастэ аутентыфікованага пользователя агенту праз запит, будзеце зберагаць історыю размовы за дапамой checkpointer з LangGraph і будзеце перадаваць адпаведныя адказы кліенту. Па дарозе мы пакажам, якія ў мінімальным коде є прасоўкі, якія трэба усунуць, каб ён працаваў належна, і што трэба змяніць перш чым ён будзе викорыстоўваны у працэсе.
Сценарый і складовыя
Уявіце команду, якая стварае інструмент для проектавання фотавольтаічных установак. Спачатку продукт быў просты, але пазней да яго дадаліся панелі, інвертары, расчынкі вартасці вырабоцтва, планы дахоў і дужа довгі список правілаў проектавання. Інструкцыя да яго зараз мае больш чым 20 стороніц. Постаюць два типу запытанняў:
- Запытанні пра сам продукт: што робіць тая чыгунка, дзе знаходзіцца настаўканне, якае правіле прыменяецца. Адказы ў дасведчэннях.
- Запытанні пра сабстоячую роботу пользователя: які інвертар ён выбраў, сколькі энергіі прадае яго система, якія дахы ён выкартаваў. Адказы знаходзяцца ў базе дадзенаў прыемніка, і жадны модэль за замовчанням іх не ведае.
Retrieval-Augmented Generation (RAG) распрацоўвае першы тип: індексуе мануал, шукае адпаведныя фрагменты калі прыходзіць запытанне і передае іх модэлю як контекст. Адзінкі распрацоўваюць другі тип: маленькія функцыі, якія модэль можа вызваць, каб запрашаць дадзеныя з прыемніка. Спалучыўшы гэтыя два элементы, вы отрымваеце асистента, які можа як поясніць продукт, так і прааналізаваць конкрэтны проект.
Рэальная система, яка стоіць за гэтым сценарыем, распаўляе ў сабе ўжо больш за лічбу інструментаў і значна большую логіку домэну. Тое, што прадстаўлена далей, спецыяльна скорачана да самага сэрца, каб архітектура заставалася видна:
- FastAPI адкрывае HTTP API і выявляе, хто ўсё-такі є апылальнікам.
- Агент LangChain (які працуе на LangGraph) керуе цыклам разумовых вырахункаў і станом кантакту.
- LLM адгукваецца на кожны запит і выявляе, чы робіцца неабходна інформацыя з зовнішніх источнікаў.
- Інструменты даюць агенту кантроліруемы доступ да функцыйнасці прыкладнага програмнага забезпечэння.
- RAG дазваляе агенту шукать інформацыю ў дасяглівых документах.
- ChromaDB храніць часткі інструкцый і выкарыстоўвае вектарны пошук.
- Стрімінг доставляе токены кліенту, пакалі ўсё яшчэ генеруецца адпаведны адказ.
Цянейка такога падчынства заключаецца ў тым, што асистэнт працуе ўнутры вялікага застосунку з рэальнымі корыстнікамі та рэальнымі дадзеннямі, а не як звычны чат-бот, які працуе праза ў яго.
Структура проекту
Кожны аспект працы мае свой пакет: маршрутызацыя HTTP, аутэнтыкацыя, логіка агента, інструменты та прайсепт RAG. Чыгунтае кожны файл маленькім і дапамагае зрозумець, дзе патрабуецца новая функцыяльнасць.
project/
│
├── main.py
├── .env
├── .gitignore
│
├── auth/
│ ├── __init__.py
│ └── dependencies.py
│
├── routers/
│ ├── __init__.py
│ └── chat.py
│
├── llm/
│ ├── __init__.py
│ ├── agent.py
│ ├── context.py
│ ├── orchestrator.py
│ ├── prompts.py
│ ├── provider.py
│ │
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── demo_tool.py
│ │ └── manual_tool.py
│ │
│ └── rag/
│ ├── __init__.py
│ ├── config.py
│ ├── context.py
│ │
│ ├── ingestion/
│ │ ├── __init__.py
│ │ ├── loader.py
│ │ ├── chunker.py
│ │ ├── chroma.py
│ │ └── indexer.py
│ │
│ └── retrieval/
│ ├── __init__.py
│ └── retriever.py
│
├── scripts/
│ ├── __init__.py
│ └── index_manual.py
│
├── docs/
│ └── manual.pdf
│
└── chroma_data/ (*generated locally, not commited or deployed)
Дзеянне кожнага элемента:
main.pyстварае застосунак FastAPI.auth/мае імітаваны элемент аутэнтыкацыі.routers/мае канцэнтры HTTP.llm/мае все, што стосуецца агента.llm/tools/мае функцыі, якія агент можа вызваць.
llm/rag/ — гэта кантэйнер з процесам выкарыстоўвання дадзейнаў, які разделяецца на ingestion/ (завантажэнне, разбіўка на часткі і стварэнне індекса для PDF) і retrieval/ (запыт да ChromaDB).scripts/ — гэта кантэйнер з командамі, якія вы запускаеце вручную, напрыклад, для стварэння індекса дасяглівайных матэрыялаў.docs/ — гэта кантэйнер з адпаведным PDF-файлам.chroma_data/ — гэты кантэйнер ствараецца локальна і ніколі не должен быть запісаны ў версійную систему чы розпавешчаны.Вы не ствараеце ўсё гэта адразу. Порядак стварэння: спачатку шар API, потым модель, пасля чого інструменты, далей кантэйнер з процесам RAG, пасля чога контекст, стрімінг і історыя запытоў.
Шаг 1: Шэлек FastAPI з фіктывным выкарыстоўальнікам
Спачатку запытайцеся ўсім, што патрэбна проекту. У списку ўключаны веб-сервер, LangChain і LangGraph, чат-кліент сумесны з OpenAI, ChromaDB, прыстрой для завантажэння PDF і засобы для разбівкі тексту, а таксама python-dotenv для налаштавання.
pip install fastapi uvicorn langchain langgraph langchain-openai chromadb langchain-community langchain-text-splitters pypdf python-dotenv
Модэль будзе доступна через OpenRouter, таму створыце файл .env у корні проекта, які будзе мячыць ключ API.
OPENROUTER_API_KEY=your_api_key_here
Негайна дадзіце .env у файл .gitignore. Ключ, які потрапляе ў кантроль версій, трэба вважаць выкрадзеным.
Тэпячак запуску аплікацыі
main.py застаецца мінімалным. У ёму ствараецца аплікацыя і рэгіструецца маршрут чату; тут не патрабуецца нічога, што стосуецца модэлі чы агента.
from fastapi import FastAPI
from routers.chat import chat_router
app = FastAPI(
title="AI Agent Demo",
)
app.include_router(chat_router)
Першы канцэнтр чату
У файле routers/chat.py задаце маршрут пад прыметнікам /chat з адным маршрутом POST. Наразе ён толькі відтварае прыйнятыя паведамленні, што дапамага пераканацца, што всё працуе, прычаму ще не задействавана ШІ.
from fastapi import APIRouter
chat_router = APIRouter(
prefix="/chat",
tags=["Chat"],
)
@chat_router.post("")
def ask_ai(
message: str,
):
return {
"message": message,
}
Заўважкі: калі на маршруце POST няма модэлю для тэла пакета, то message: str змушвае FastAPI чытаць яго з рэчы запита. Гэта зручна для працы ў Swagger UI, але для рэальнага кліента зазвычай прыймаюць JSON-тэло, заданае за дапамогою модэлі Pydantic, адтакулькі рэчы запита практычна маюць ліміт дужоў і практычна не выкарыстоўваюцца ў логах доступу.
Штучная залежнасць для аутэнтыкаціі
У продакшн-проектах аплікацыя пераверчае куку сесіі або JWT і завантажвае данні пользователя з базы дадзеных. У нашам прыкладзе ўсё гэта заменяецца спецыяльным заголовкам. Створыце файл auth/dependencies.py, у яком будзе мала класа-данные MockUser і функцыя get_current_user, якая чытае заголовак X-Demo-User і адхіляе запит з кодам 401, якщо ён не прысутны.
from dataclasses import dataclass
from fastapi import Header, HTTPException
@dataclass
class MockUser:
id: str
name: str
def get_current_user(
x_demo_user: str | None = Header(default=None),
) -> MockUser:
if x_demo_user is None:
raise HTTPException(
status_code=401,
detail="Missing X-Demo-User header",
)
return MockUser(
id=x_demo_user,
name=x_demo_user,
)
У FastAPI пры впрыске залежнасцяў тепер данні корыстніка перадаюцца на канецчык. Для цього достатня проста заявіць параметр за дапамою Depends(get_current_user).
from fastapi import APIRouter, Depends
from auth.dependencies import (
MockUser,
get_current_user,
)
chat_router = APIRouter(
prefix="/chat",
tags=["Chat"],
)
@chat_router.post("")
def ask_ai(
message: str,
current_user: MockUser = Depends(
get_current_user,
),
):
return {
"user": current_user.name,
"message": message,
}
Кліент ідэнтыфікуе ся, адправляючи заголовак у такам формате:
X-Demo-User: user-123
Пры кожнай запытце FastAPI спачатку выкананая get_current_user(), а рэзультат у вигляде об’екта MockUser пераданы ў ask_ai. Важлівай часткай дизайна є разпад задач: FastAPI адпавядае за аутэнтыкацію, а шар AI проста отрымае об’ект корыстніка, якому можа даваць доверле. Пазней самэ це яго об’ект корыстніка дазволяе інструментам вяртаць даны, якія належаць праваму чалавеку. Замена макетнай аутэнтыкаціі на рэальную пазней зменяе толькі гэтую адну залежнасць.
Шаг 2: Падключэнне моделі через OpenRouter
Калі є рабочы канцэнтры і вядомы абяротчы, сервісу патрабуецца модэль. OpenRouter адкрывае API, сумесны з OpenAI, таму клас ChatOpenAI з LangChain можа працаваць з яму, як толькі вы указаеце base_url на OpenRouter і перадасте свой ключ OpenRouter.
Занесіце гэта ў файл llm/provider.py. Ён чытае файл .env, быстра паводзіцца бядай з чытальным абэронам, якщо ключ не існуе, і загортае ключ у SecretStr з бібліятэкі Pydantic, ўтаму ён не будзе випадкова выдрукаваны ў логах чы рэпрезентаціях.
import os
from dotenv import load_dotenv
from pydantic import SecretStr
from langchain_openai import ChatOpenAI
load_dotenv()
api_key = os.getenv(
"OPENROUTER_API_KEY",
)
if not api_key:
raise RuntimeError(
"OPENROUTER_API_KEY environment variable is not set."
)
model = ChatOpenAI(
model="YOUR_MODEL",
api_key=SecretStr(api_key),
base_url="https://openrouter.ai/api/v1",
)
Пакалі ключ прадаёцца з сярэды выканання, ён ніколі не праказуецца у коде. На гэтым этапе вы вялікі час вже моглі бы надсылаць запиты да модэль і атрымляць адпаведныя адказы, але гэта будзе звычайны вызов LLM. Мета — стварыць агента, які сам можа вырашыць, калі йому патрабуецца інструмент.
Выбір модэль
Аргумент model — это простае ідэнтыфікатар моделі OpenRouter, таму вы можете зменіць модельы без змены рэшты коду. Пры пораўненні кандыдатаў пераканайцеся:
- наявнасці падтрымкі вызываў інструментаў, ад карысткі якога залежыць агент;
- наявнасці падтрымкі стрімавання;
- размаху вікна контэкста;
- лімітах частоты запытоў;
- наявнасці безкоштовнага тарифу.
OpenRouter прысвячае дзеякія моделі безкоштовна, што стаўкаеся пад час експерыментаў. Каталог регулярна зменяецца, таму перагледзіце чынны список і фільтруйце безкоштовныя моделі, замест таго каб паследаваць фіксаваныя рэкамендацыі. Што бы вы не выбралі, гэта будзе практычна включанае ў констрактар:
model = ChatOpenAI(
model="YOUR_MODEL_ID",
api_key=SecretStr(api_key),
base_url="https://openrouter.ai/api/v1",
)
Напрыклад, якщо каталог паказвае ідэнтыфікатар, такі як той, што паказаны нижэй (прыклад взятычас напісання; ён можа больш не быць доступны), вы паследуеце гэты самы строк як model:
google/gemma-4-26b-a4b-it:free
Пам’ятайце, што безкоштовныя моделі выкарыстоўваюць спакульную працэзгольнасць. Яны можу стаць обмежаныя па частоте запытоў або стаць недоступнымі, часта самэй у найгоршы момант падчас дамавання прыклада. Змена на іншую модель або падключэнне вашага сопрацоўнічага ключа через OpenRouter зазвычай рашае гэтыя проблемы. Для практычнага выкарыстоўвання выбірайце моделі на аднойчынку з аспектамі надзеямоўнасці, можлівасцей, затрымкай і вартасцю, а не толькі за цэнай.
Крок 3: Ад моделі да агента
Прямы запыт да моделі — цэхае адносу: текст корыстніка прыходзіць, а пасля — рэсультат. Агент дадае цикл прыняцтва рашэння. Модель аналізуе запыт, вяршыць, чы можа адразу даўаць адпаведзь чы ёй спачатку трэба што-небудзь, прайшоўшы за дапамогай інструмента, як тое неабходна, чытае рэсультат і толькі пасля таго даўае заканчыцковую адпаведзь. Прыбліжна:
- просты запыт: корыстнік, потым LLM, потым адпаведзь;
Модуль агента
У файле llm/agent.py функцыя create_agent з бібліятэкі LangChain стварае агента на базе моделі і системнага запрошэння. Унутранне ён стварае граф LangGraph, який автаматычна запускае цикл работы моделі і інструментаў.
from langchain.agents import create_agent
from llm.provider import model
from llm.prompts import SYSTEM_PROMPT
agent = create_agent(
model=model,
system_prompt=SYSTEM_PROMPT,
)
Этот агент яшчо не мае інструментаў, таму ёго праця дужа супадае з працёй самай моделі. Спачатку трэба даўць яму інструкцыі.
Системнае запрошэння
У файле llm/prompts.py знаходзіцца короткае запрошэння, якое паведамляе модель пра ўсё, для чаго яна прагледзецца, забараняе ёй выдумваць дадзеныя і вказывае, які тип запытку адпаведае якому типу пошуку.
SYSTEM_PROMPT = """
You are an AI assistant for our demo application.
You help users understand the application and navigate the system.
Never invent data.
When information about the demo system
is required, use the available application tools.
When answering questions about the application,
use the documentation search tool.
Always answer in clear, conversational language.
""".strip()
Запрошэння встановляе два джераны правды:
- Дадзеныя прыкладнення (тое, што ёсць у акаунце пользователя) прадаюцца з інструментаў прыкладнення;
- Знання працы програмы (як функціонуе продукт) адбываецца за дапамою пошуку ў дасягліх документах.
Аднаго уточнення прыяжджае перш чым продаваць далей. Інструменты і RAG апісаны окольна, таму што так легча ўсё зрозумець, але ў готовым агенте сам пошук у дасягліх документах ёсць інструментам. Не існуе другога механізма: агент бачыць спіс функцый, якія можна вызваць, а пошук у інструкцыях — адна з іх.
Шаг 4: Першы інструмент
Інструмент — це функцыя, якую агент мае право вызваць. Самэ гэта дазволяе архітектураы расшырвацца: замест таго, каб усі даны пра програму падаваліся ў запит, вы выкладзеце конкрэтныя операцыі і дазволіце моделі запрашваць іх толькі тады, калі для цього є патрэба.
Для дамавання прыкладу, llm/tools/demo_tool.py апісвае інструмент, які вяртае фіксаваны блак а інформацыі пра проект.
from langchain.tools import tool
@tool
def get_my_demo_data() -> str:
"""
Return information about the demonstration data.
This is just for demo data. But in production, make a more detailed instruction.
"""
return """
Project: Aperture Analytics Dashboard
Owner: Jordan Lee
Status: In Progress
Team size: 6
Budget: $84,000
Deadline: 2026-11-15
Description: An internal dashboard for visualizing customer usage
metrics, built with FastAPI and React, integrating with the
company's data warehouse.
""".strip()
Здесь важны два моменты. Декоратар @tool перетварае звычную функцыю Python у інструмент LangChain, адказваючы за ўзяць яе назву і схему аргументаў з падпіса функцыі. А докстрайн становіць опис інструмента, які чытае модель пад час адліквідацыі, чы робіць вызов. У рэальной системе такі опис патрабуе серйознай увагі: трэба чытка пазначыць, што вяртае інструмент, калі гэта дапаможна, а калі — ні. Нечыткі опис ўскладнюе агенту правильна выбраць інструмент чы проста не вызваць яго ўзагалі.
Ўрадзіце інструмент, перадаўшы яго ў функцыю create_agent:
from langchain.agents import create_agent
from llm.provider import model
from llm.prompts import SYSTEM_PROMPT
from llm.tools.demo_tool import get_my_demo_data
agent = create_agent(
model=model,
tools=[
get_my_demo_data,
],
system_prompt=SYSTEM_PROMPT,
)
У вашам кодзе няма правіла, якое вялічыць момент запуску функцыі. Калі корыстнік спытае: «Якія у мяне даныя для дэмаварынку?», модель разумее, што ёй патрэбны інформацыі, спецыфічныя для абанку, і вызывае get_my_demo_data(). Калі корыстнік спытае: «Што такое дэмаварынак?», пошук не патрэбен, і модель адпавядае безпосередна. Гэты выбор выконваецца ў кожны момент, усередзінь у цыкл агента.
Шаг 5: Стварэнне каналу RAG для ручнага ведамства
Агент тепер можа запрашваць даныя прыкладнага програмнага забезпечэння, але ён яшчэ нічога не ведае пра тое, як функцыонуе продукт. Падключэнне 20-сторанковага ручнага ведамства ў запрос системы будзе спрацоўваць на змарнаванне токенав у кожны запит і створыць проблемы з ўтриманнем яго у актуальным стане. RAG пазбегае обох проблем.
Важна точна інформацыя пра тое, што такое RAG і чаго ён не ўключае. Нічога не трэнуўцца і не налаштоввалася на адной з дакументацый. У момент запиту система шукае ў інструкцыі тыя фрагменты, якія наіболей актуальныя для запиту, і падае гэтыя фрагменты модэлю як контекст, а модэль дае адпаведныя адказы на ўсё гэта.
Процес складаецца з двух фаз:
- Прыём дадзеных – гэта процес, які выконваецца окрема ад веб-дзяловой прыкладнай програмы: завантажваецца PDF, яны дзелююцца на часткі, а гэтыя часткі разам з ўсунутымі з них дадзеннямі зберагаюцца ў ChromaDB.
- Пошук і выкарыстоўванне дадзеных – гэта процес, які выконваецца пад час самага запиту: берэцца запыт, ведаецца пошук у ChromaDB, збіраюцься найкращыя часткі дадзеных, і яны падаюцься модэлю.
Якщо вы хочаце болей шырокага адзору на этыя канцэпціі, апвюха блога пра систему адзысквання свежых знань за запитам даст болей дакладную інфармацыю; тут мы сфокусаваны на самай рэалізацыі.
Завантажэнне PDF
llm/rag/ingestion/loader.py выкарыстоўвае PyPDFLoader з LangChain, які ператварае кожную сторонку PDF у Document.
from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader
PDF_PATH = Path("docs/manual.pdf")
def load_manual():
loader = PyPDFLoader(
str(PDF_PATH),
)
documents = loader.load()
return documents
Document мае два элементы: выдзялены тэкст у page_content і словнік metadata, який описвае, звядзе гэты тэкст прыйшоў. Самэ гэтае metadata пазней дазволяе адпаведзі цітаваць конкрэтную сторонку. Канцэптуальна, кожная завантажаная сторонка выглядае так:
Document
├── page_content
│ └── "To create a new demo data..."
│
└── metadata
├── source: docs/manual.pdf
└── page: 12
PyPDFLoader зазвычай фіксуе як індэкс page, вылічваецца з нуля, так і page_label, які лёгкая да чытання. Контэкстны будульчык, паказаны нижэй, выкарыстоўвае page_label, які падходзіць да номераў сторанак, якія бачыць чытальнікі у PDF.
Разбіўка сторанак на часткі
Пошук усіх сторанак, не кажучы вже пра цэлы документ як адну частку, дае грубыя рэзультаты. llm/rag/ingestion/chunker.py разбівае документы за дапамою RecursiveCharacterTextSplitter.
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
)
from langchain_core.documents import Document
def chunk_documents(
documents: list[Document],
) -> list[Document]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150,
separators=[
"\n\n",
"\n",
". ",
" ",
"",
],
)
return splitter.split_documents(
documents,
)
Спалер стараецца дзелаць фрагменты па 1 000 симвалаў з перакрыцчам у 150 симвалаў. Спіс разлучальніках працуе па порядку: ён воліе робіць разлучэнне на межах параграфаў, потым на перерывах ліній, потым на канцох рэчэнняў, потым на праслойках, і толькі як апошняе рашэнне — усередзіне слова. Перакрыцча існуе таму, што адна-едзіная фактаза можа распаўляцца на два часткі ў месцы разлучэння; павтарэнне невялікага фрагмента тексту з обох бакоў зменшвае верыгоднасць таго, што адпаведнае рэчэнне будзе разрэзана на два.
Гэтыя цыфры ўсьмо толькі пункты выйскі, а не правілы. Правёры розмер залежыць ад таго, як напісаны вашы дакументы і насколькі точна патрэбна ўзяць інфармацыю, таму трэба спрыятаць іх як значэнняя, якія трэба налаштаваць па реальныя задачы. Артыкул блогу пра разлучэнне дакументаў, якое захоўвае доказы даёт болей дэталяв пра гэты компроміс.
Стойкая колекцыя Chroma
llm/rag/ingestion/chroma.py ачыяе PersistentClient, які зберагае даныя на дыску і вяртае калекцыю дасобніка, ствараючы яе праз першыя выкарыстоўванні.
import chromadb
from llm.rag.config import (
CHROMA_PATH,
MANUAL_COLLECTION_NAME,
)
def get_chroma_client():
return chromadb.PersistentClient(
path=CHROMA_PATH,
)
def get_manual_collection():
client = get_chroma_client()
return client.get_or_create_collection(
name=MANUAL_COLLECTION_NAME,
)
Шляхі і назвы беруцца з llm/rag/config.py, які чытае зменныя сераўіса і, якщо гэта немагчыма, выкарыстоўвае разумныя стандартныя значэння:
import os
CHROMA_PATH = os.getenv(
"CHROMA_PATH",
"./chroma_data",
)
MANUAL_PATH = os.getenv(
"MANUAL_PATH",
"docs/manual.pdf",
)
MANUAL_COLLECTION_NAME = os.getenv(
"MANUAL_COLLECTION_NAME",
"manual",
)
Дадзіце адпаведныя запісы ў .env:
CHROMA_PATH=./chroma_data
MANUAL_PATH=docs/manual.pdf
MANUAL_COLLECTION_NAME=manual
Ніякі модель абмесцавання не налаштована нідзе, і гэта ўмысна для дамавайнага прыкладу. Калі калекцыя ствараецца без явнай функцыі абмесцавання, Chroma выкарыстоўвае свой вбудованы стандарт: кожны раз, калі дадаюцца дакументы, Chroma сама вырахоўвае ўсё неабходнае абмесцаванне і зберагае яго разам з тэкстам і метаданымі. Стандартная модель працуе локальна і запрашваецца праз першыя выкарыстоўванні, таму першы запуск індэксавання можа націснуць, пакуль вона завантажваецца.
Рэзультатам являецца локальны, стойкі сховыш вектараў у каталозе chroma_data/. Паколькі ён цэлыя час складаецца з даных PDF, яго трэба дадаць у файл .gitignore разам з .env.
Задача індексавання
llm/rag/ingestion/indexer.py спаўнае крокі прыймання дадзеных.
from pathlib import Path
from llm.rag.config import MANUAL_PATH
from llm.rag.ingestion.loader import load_manual
from llm.rag.ingestion.chunker import chunk_documents
from llm.rag.ingestion.chroma import get_manual_collection
def index_manual():
collection = get_manual_collection()
if collection.count() > 0:
print(
f"Manual already indexed "
f"({collection.count()} chunks)."
)
return
manual_path = Path(
MANUAL_PATH,
)
if not manual_path.exists():
raise FileNotFoundError(
f"Manual not found: {manual_path}"
)
documents = load_manual()
print(
f"Loaded {len(documents)} pages."
)
chunks = chunk_documents(
documents,
)
print(
f"Created {len(chunks)} chunks."
)
collection.add(
ids=[
f"manual-chunk-{i}"
for i in range(len(chunks))
],
documents=[
chunk.page_content
for chunk in chunks
],
metadatas=[
chunk.metadata
for chunk in chunks
],
)
print(
f"Stored {len(chunks)} chunks."
)
Разберам, што ён рабіць. Ён ачынае калекцыю і вяртаеся раней, якщо ў ёй уже є фрагменты дадзеных, што робіць паўтарныя запускі безнэпакойнымі. Ён пераканаецца, чы ўсё ж такі існуе PDF, а як не тады — выдае чыстае паведамленне пра адмоўку. Потым ён завантажвае сторунки, дзеліць іх на фрагменты і дадае ўсё ў Chroma за аднім вызывам, выкарыстоўваючы стабільныя ідэнтыфікаторы (manual-chunk-0, manual-chunk-1 і т. д.), тексты фрагментаў і ўсю ўпорну інфармацыю пра яе. Паведамленняя пра прагрэс паказваюць, сколькі сторунак і фрагментаў было оброблена.
Адзіныя проблема выйшла з гэтаго раннега вяртацца: якщо вы зменіце інструкцыю та запусціце скрыпт зноў, ніч не будзе адбывацца, таму што калекцыя не пустая. Чыба з’явіліся змены, трэба выдаліць калекцыю (або катэгорію chroma_data/) прычыму переіндексавання, або заменіць захіст на логіку, якая навмесна адбывае дадзеныя чыстаючы або перабудовваючы іх.
Спіс не паказвае scripts/index_manual.py; яму проста трэба імпортаваць index_manual та вызваць яго. Запусціце яго аднойчы як модуль з корневага катэгоріі проекта:
python -m scripts.index_manual
Этый адміністратыўны запуск чытае PDF, дзельвяе яго, уключае этыя дзелі і зберагае іх. Рэзультаты роботы тэрміналу паказваюць колькасць зберажаных дзелей. У спрощанай дамаве інструкцыя ў вачыні — цэфаровы файл на адной сторонце, які містіць адзін правіл: «Данныя дамавы можна даць толькі адміністратарскім пользователям», што дастаткова, каб паказаць, чы робота з адзысканням данных функцыональна. Пасля гэтага запуск FastAPI ўжо не вплывае на PDF, адтакулькі вектары вже зберагліся.
Запиты да колекцыі
Сама індэксацыя нічога не дае агенту; яму патрэбны способы для пошуку. Файл llm/rag/retrieval/retriever.py абгортае API запитаў Chroma.
from dataclasses import dataclass
from typing import Any
from llm.rag.ingestion.chroma import (
get_manual_collection,
)
@dataclass
class RetrievedChunk:
content: str
metadata: dict[str, Any]
distance: float
def retrieve_manual(
query: str,
n_results: int = 5,
) -> list[RetrievedChunk]:
collection = get_manual_collection()
results = collection.query(
query_texts=[query],
n_results=n_results,
include=[
"documents",
"metadatas",
"distances",
],
)
documents = results["documents"] or []
metadatas = results["metadatas"] or []
distances = results["distances"] or []
retrieved_chunks = []
for document, metadata, distance in zip(
documents[0],
metadatas[0],
distances[0],
):
retrieved_chunks.append(
RetrievedChunk(
content=document,
metadata=dict(metadata)
if metadata else {},
distance=distance,
)
)
return retrieved_chunks
Функцыя адправляе запит як query_texts, прасіць да пяці рэзультатаў і запрашоўвае дакументы, ўсю іх мета-інфармацыю і адстані межу ўсімі ямі. Chroma вяртае аднай список за кожны запит, таму код чытае індэкс [0] кожнага поля; параметры or [] служыць для захавання ад відсутнасці палей. Кожны знайдзены рэзультат упаковываецца як клас RetrievedChunk, тады кантэнт залишнага коду не залежыць ад формату адпаведзі Chroma.
Паколькі запит захаваны ў там жа модэлі, што і схованы фрагменты, паўтарэнне ёнога є семантычным. Запыт на кшталт "Як дадаць новыя даны для дэмаверкі?" знаходзіць аблака пра стварэнне чы розпаўненне даных для дэмаверкі, нават якщо там ніколі не вжываюцца словы "дадаць новыя". Расстоянне паказвае, насколькі близкая кожная знаходка; чым меньша цяжар, тым больша сэмантычная адпоўнасць. Гэтае значэнне ўжыткова пазней, калі хочаце адкласіць слабыя паўтарэнні, замест таго каб завжды падаваць модэлі пяць фрагментаў.
Калі гэта ўсталяна, частка адзысквання дакументаў у RAG працюе. Што застаёцца — такім ёсць перадача рэзультатаў модэлі.
Ператварэнне фрагментаў у контекст
llm/rag/context.py фарматуе знаходкі ў адну строчку, яю можа прачытаць модэль.
from llm.rag.retrieval.retriever import (
RetrievedChunk,
)
def build_context(
chunks: list[RetrievedChunk],
) -> str:
context_parts = []
for chunk in chunks:
page = chunk.metadata.get(
"page_label",
)
context_parts.append(
f"Source: User Guide, page {page}\n"
f"{chunk.content}"
)
return "\n\n---\n\n".join(
context_parts,
)
Кожны частак пачатается лінійкай коду, у якой указваецца назва інструкцыі для корыстувача та ўпорядак старонкі, а частакі разлучаны роздзелачам. Лінійка коду дапамагае модэлю паведаміць, звядзе жоўткі адпаведны адказ, і дае корыстувачам можлівасць яго пераканацца.
Шаг 6: Адкрыцья інструкцыі як інструмента
Процес завершаны: старонкі запрашаюцца та дзельняцца на частакі, частакі зберагаюцца ў ChromaDB, можна знайсці та сформатаваць неабходныя з іх. Аднак агент нічога не ведае пра існаванне всього гэтага. Самэль тут і практычнае значэнне паканальных архітектурных прымет: пошук дакументацыі становіцца проста ўсьмою іншым інструментам.
llm/tools/manual_tool.py адзначае search_user_manual, які прыймае запит, адзначае пяць чакункоў і вяртае іх у форматаваным контэксте. Якщо нічога не вяртаецца, ён вяртае явны паведамленне пра тое, што інструкцыя не адпаведае запыту, чым даём модэлю шчырую інфармацыю для перадачы заместо порожняй строчкі.
from langchain.tools import tool
from llm.rag.context import build_context
from llm.rag.retrieval.retriever import retrieve_manual
@tool
def search_user_manual(
query: str,
) -> str:
"""
Search the application user manual.
Use this tool when the user asks about application
behavior, instructions, rules, limitations, or
how something works.
"""
chunks = retrieve_manual(
query=query,
n_results=5,
)
if not chunks:
return (
"The manual does not contain enough "
"information to answer this question."
)
return build_context(
chunks,
)
Як і ранейш, докстрынг являе сабою анаунс інструмента для модэля. У ям сказана, што трэба вжываць гэты інструмент для запытаў пра поведзенне, інструкцыі, правілы, абмежэння і спосаб працы розных элементаў, што падтрымлівае настройкі системы.
Тепер зарэгіструйце оба інструмента ў агенты:
from langchain.agents import create_agent
from llm.provider import model
from llm.prompts import SYSTEM_PROMPT
from llm.tools.demo_tool import (
get_my_solar_system,
)
from llm.tools.manual_tool import (
search_user_manual,
)
agent = create_agent(
model=model,
tools=[
get_my_solar_system,
search_user_manual,
],
system_prompt=SYSTEM_PROMPT,
)
Зверніце увагу на оператыў імпорту ў тым списку: яна аднасецца да get_my_solar_system, назвы з цэлага прыкладу практычнаеўпрацоўвання, тады калі модуль дамовай прылады визначае get_my_demo_data. Ожывайце get_my_demo_data як у оператыў імпорту, так і ў списку tools, інакша модуль не будзе могчыць быць імпортаваным.
Чаму дизайн на адной прыладзе застаёцца эфектывным праз рост прыкладу практычнаеўпрацоўвання
Агенту ніколі не трэба ўжываць вялічезны запит, які описвае ўсё, што знае прыклад практычнаеўпрацоўвання. У замяну ён отрымае сфармаваныя, кантролюемыя можлівасці. Дааджэнне новай функцыяй асистанту значыць напісанне новай прылады і яе рэгістрацыю; слой HTTP не змінюецца. Дама застаёцца толькі адной прыладай для обробкі дадзеных і адной прыладай для документацыі, таму шаблон лёгкі да разумення, але тая ж структура падтрымвае значна большы набор прылад у цэлым прыкладзе практычнаеўпрацоўвання.
Крок 7: Перадача аутентыфікованага пользователя агенту
Дэмаверны ўстатку яшчэ вяртае даныя, захаваныя у кодзе. Справжні ўстаток павінен ведаць, хто запытаўся, і аплікацыя вже гэта ведае: FastAPI выканалі распакаванне інформацыі пра пользователя ў завіснасці аутэнтыкацыі. Недастатковай часткай є перадача гэтага пользователя ў процес запуску агента. LangChain называе гэта контекстам выканання.
Задаце формат контексту ў файле llm/context.py:
from dataclasses import dataclass
from auth.dependencies import MockUser
@dataclass
class AgentContext:
user: MockUser
Гэты об’ект падаецца, калі запускаецца агент, і гэта разлік мае значэнне. Пользователь — это інформацыя, прыналежная конкрэтнаму запыту. Яна належыць да текущага HTTP-запыту, а не да розмовы, і яе ніколі не трэба зберагаць у вачынку, якую можа прачытаць або перапісаць модель. Храненне яе праза історыю вачынакоў таксама значыць, што запит не можа змусіць агента дзеяць як іншы пользователь. У цэлай аплікацыі той самы об’ект контексту таксама містіць такія элементы, як сесія базы дадзеных і ID проекта, який выконуецца.
Код дэмана зупіняецца на визначэнні класу, таму залишаецца два з’ѐеднання, якія трэба створыць, і варта адзін раз пераканацца ў чынный документацыі LangChain, каб з’ясавіць точны API. Першае — адзначыць схему пад час стварэння агента, зазвычай за дапамою параметра context_schema=AgentContext для функцыі create_agent. Другае — прабачыць інструментам яе чытаць: у LangChain 1.x інструмент можа прыймати параметр часу выконання (напрыклад, пазначанный як ToolRuntime[AgentContext]) і чытаць данні корыстувальніка з яго атрыбута context, які залишаецца недоступным для моделі сярод аргументаў інструмента. Самэ тут справжняя функцыя get_my_demo_data шукала б записы па user.id.
Шаг 8: Шар оркестрацыі для стрімування
Замест таго, каб вызваць агента заўтрачаннем у роутэре, практыкуйце взаімадзейнасць у файле llm/orchestrator.py. Тады роутэр застаецца сфокусаваным на HTTP, а оркестрабілер карае за тым, як паведамленне ператворыцца на запуск агента.
from collections.abc import Iterator
from langchain_core.messages import (
AIMessage,
AIMessageChunk,
BaseMessage,
ToolMessage,
)
from langchain_core.runnables import RunnableConfig
from llm.agent import agent
from llm.context import AgentContext
def chat_stream(
user_message: str,
user,
) -> Iterator[str]:
config: RunnableConfig = {
"configurable": {
"thread_id": f"user:{user.id}",
}
}
context = AgentContext(
user=user,
)
for chunk, metadata in agent.stream(
{
"messages": [
{
"role": "user",
"content": user_message,
}
]
},
config=config,
context=context,
stream_mode="messages",
):
if not isinstance(
chunk,
BaseMessage,
):
continue
if isinstance(
chunk,
ToolMessage,
):
continue
if not isinstance(
chunk,
(
AIMessage,
AIMessageChunk,
),
):
continue
if isinstance(
chunk.content,
str,
):
yield chunk.content
У гэтым функцыі много коду, таму разберайце яго па частках.
ID аддзінку выбірае размову
Першы блакет стварае настройкі запуску:
config = {
"configurable": {
"thread_id": f"user:{user.id}",
}
}
Контралектор LangGraph храніць стан размовы па ключу thread_id. Кожны запуск з тым самым ID нітка продовжвае тую ж размову, і ўсі ў яй ўключаныя поведамленні можна прачытаць пазней. Тут ID нітка вылучаецца з ID пользователя, што значыць, кожны пользователь мае роўна адну размову. Гэта падходзіць для дамавай; у рэальным прыкладзе практычнае застосоўванне ствараць бы правільныя ID размов, дазволіць калькі размов на адного пользователя і пераканацца за кожны запит, што апытчык ёсць власнік нітка, да якой вырабляецца доступ.
У описе прыяжджаецца, што існуе checkpointer, але жадны з фрагментаў агента яго не пасканалі. Без яго thread_id не мае ніякога адзынака і нічаго не запам’ятваецца межы запытамі. Створыце адзін InMemorySaver у файле llm/agent.py і перадаце яго функцыі create_agent через аргумент checkpointer, каб і агент, і функцыі для запісу історыі моглі імпортаваць той самы экземпляр.
Контэкст выканання перавозится разам з запуском
Далей оркестрайтар упаковвае данні корыстніка ў об’ект контэксту:
context = AgentContext(
user=user,
)
Этот об’ект перадаецца як context= да функцыі agent.stream(). Чераз гэты шлях ідентыфікат, створаны ў FastAPI, доходзіць да агента, а чераз яго — да інструментаў.
Фільтрацыя стрэму
agent.stream() вызываецца з параметрам stream_mode="messages", што дае пары частака паведамлення і метаданых, калі модель стварае токены. Не всё з таго потока павінна дася да вядома корыстніку. Цикл пропускае все, што не ў формате паведамлення LangChain, пропускае об’екты ToolMessage (сырыя выходны даны інструмента, такія як запрашаны ручны текст), залишае толькі паведамлення ІІ і частакі такіх паведамленняў, а ў разе, калі ўместа яго є звычны рэчыскап, выдае ўсё яго содержанне. Контэнт, які дзеякія прадаўцы выдаюць у вачынку списку частак, тыхо пропускаецца праз гэтыя перакананні, таму, якщо вы зменіце модель і пабачыце порожнія адпаведзі, там і трэба шукать прычыну.
Шаг 9: Верненне потокавай адпаведзі
Рашчыранне chat_stream() як гэнератора было намераным. Чаканне на выключны адказ пры надсылці байта заставляе корыстніка стаяць і сазерцаць індыкатор загрузкі, а адказы LLM могу займаць калькі секунд. Стрімаванне паказвае першыя словы практычна адразу, што робіць асистента значна болей чутлівым да запитоў.
StreamingResponse з FastAPI прымае гэнератор безпосередна. Адкорректаваць routers/chat.py:
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from auth.dependencies import (
MockUser,
get_current_user,
)
from llm.orchestrator import chat_stream
chat_router = APIRouter(
prefix="/chat",
tags=["Chat"],
)
@chat_router.post("")
def ask_ai(
message: str,
current_user: MockUser = Depends(
get_current_user,
),
):
return StreamingResponse(
chat_stream(
user_message=message,
user=current_user,
),
media_type="text/plain",
)
Адказ надсылаецца у формате text/plain, пры чым кожны выданы фрагмент запісваецца у з’язок як толькі ён ствараецца. Пакалькі chat_stream ёсць звычным (сінхронным) гэнераторам, Starlette ітеруе яго ў рабочай ніткі, таму ён не блакае цыкл адбывання падзей. Якщо пазней будзе трэба структураваныя падзеі на кліенте (напрыклад, каб паказаць „шукаю інструкцыю...“ пад час запуску інструмента), Server-Sent Events ёсць наступным логічным крокам.
Паўная шляха запытку зараз выглядае так: кліент адправляе даны на /chat, FastAPI атрыбутуе права доступу вызывачу, chat_stream() запускае роботу агента, модель вырашае, чы гэта трэба вызваць інструмент, інструмент выконвуецца і вяртае свой рэзультат, модель запісвае адпаведную адказу, а токены вяртаюцца назад да кліента.
Ключовая асоблівасць закладзена ў тым, што FastAPI ніколі сам не запускае модель. Рутэр выкарыстоўвае протакол HTTP, оркестрайтар керуе агентам, агент вырашае, якія даны ў яго ёсць, а інструменты здійснюють запыты. Кожны слой можа змяніцца без парадузеў для іншых.
Шаг 10: Чытанне історыі размовы
Пакалі стан агента зберагаецца пасля кожнай роўні, можна паказаць вяртаючымся корыстніку яго паканальную размову. Для гэтага выкарыстоўваюцься две дапаможныя функцыі.
Чытанне неапранутага кэш-файла
Першая функцыя завантажае найновейшы пункт перапрацоўкі для ниткі і вяртае канал messages, або порожній список, якща нітка ніколі не была выкарыстоўвана:
def get_conversation_messages(
thread_id: str,
) -> list[BaseMessage]:
config: RunnableConfig = {
"configurable": {
"thread_id": thread_id,
}
}
checkpoint = checkpointer.get(
config,
)
if checkpoint is None:
return []
return checkpoint[
"channel_values"
].get(
"messages",
[],
)
Якщо вы копіюеце гэты код, паспрабуйце выправіць індентацыю прызначэння config: яна павінна быць індентавана ў тэле функцыі, інакш Python выдае памятку пра адную. Функцыі таксама неабходна імпортуваць BaseMessage, RunnableConfig і спяльную прымэру checkpointer.
Тое, што вяртаецца, — это неасоблівы стан агента, і ў яму є больш, чым толькі чат, які памятае корыстнік. Калі агент вызывае інструмент, LangGraph фіксуе паведамленне AI, якое містіць вызыв інструмента, і околачысцовае паведамленне інструмента, якое містіць рэзультат. Это деталі реалізацыі, якія фронтэнд не павінен аналізаваць.
Форматаванне паведамленняў для адзірвання
Другая функцыя стварае відобразэнне для пользователя:
def get_conversation_messages_for_display(
thread_id: str,
) -> list[dict[str, str]]:
display = []
for message in get_conversation_messages(
thread_id,
):
if isinstance(
message,
HumanMessage,
):
content = _extract_text_content(
message.content,
)
if content.strip():
display.append(
{
"type": "human",
"content": content,
}
)
continue
if isinstance(
message,
AIMessage,
):
content = _extract_text_content(
message.content,
)
if content.strip():
display.append(
{
"type": "ai",
"content": content,
}
)
return display
Яна зберагае толькі об’екты HumanMessage і AIMessage, выкарыстоўвае ўсё тэкста з іх, апускае тыя, якія ў порожнім стане, і вяртае простыя словары з паказаннем type і content. Паведамленні з інструментамі ніколі не праказваюцца, таму што яны не належаць ні да однаго з двух прынятных типаў. Таксама апускаюцца порожнія паведамленні AI, што мае значэнне, адтуды ў паведамленнях AI, якія толькі запрашаюць выкананне інструмента, зазвычай няма тэкста.
Цей процес выкарыстоўвае дапаможны функцыя _extract_text_content, які не паказваецца. Яго задача — вярнуць тэкст без змян, калі гэта строка, а калі гэта спіс частак тэкста — з’едыніць іх разам. Таксама неабходна імпортацыя класаў HumanMessage і AIMessage.
Канцэнтр па історыі
Адрэсацыя атракцію дасвядчэння праз маршрут GET у routers/chat.py. Ён адгэтуе той самы ID ніткі, які выкарыстоўвае канцэнтр паведамленняў, і вяртае яго разам з паведамленнямі.
@chat_router.get("/current")
def get_current_conversation(
current_user: MockUser = Depends(
get_current_user,
),
):
thread_id = (
f"user:{current_user.id}"
)
messages = (
get_conversation_messages_for_display(
thread_id,
)
)
return {
"thread_id": thread_id,
"messages": messages,
}
Інтэрфейс чату можа вызваць гэта, калі адкрываецца старонка, і адразу паказаць ўжо існуючую розмову, прычыму корыстнік нічога не запісаў:
GET /chat/current
Адпаведзь выглядае так:
{
"thread_id": "user:user-123",
"messages": [
{
"type": "human",
"content": "How much demo data do I have?"
},
{
"type": "ai",
"content": "You currently have 18 demo data."
}
]
}
Чаму не вяртаць первісны стан
Внутранній стан агента і розмова, якую бачыць корыстнік, — гэта разныя рэчы. Па мере дадавання новых функцый стан заполняецца вызовамі інструментаў, рэзультатамі іх работы, проміжнымі крокамі, метаданаямі модэля і іншымі дакументацыйнымі данымі. Вяртанне ўсьго гэтага спрыяе з’ѐеднанню фронтэнду з внутраннямі агента і можа прывести да выклакання рэзультатаў інструментаў, якія не планавалася паказваць. Бэкенд должен визначыць, што ўважаецца публічной історыяй розмовы, і вяртаць толькі гэта.
Як праходзіць адзін запит з початку да канца
Калі всі элементы ў сваіх месцах, важна чыткая адпаведнасць пра адну важлівую характэрыстыку: модель ніколі не падчыняецца вашай базе даных чыра PDF-файлам. Яна можа толькі запросіць запуск певнага інструмента. Інструмент, які працюе як звычны код на Python з стандартнымі механізмамі контролю доступу, выконвае неабяжлівую задачу і вяртае текст, які потым викорыстоўваецца модэллю для стварэння адпаведнай адказы. Саме гэтыя межы запэўняюць безпеку асистента, калі яго вбудовваюць у прыкладнасць, якая работае з рэальнымі данымі.
Прабаўка ў Swagger UI
FastAPI автаматычна стварае інтерактыўную документацыю, таму пад час тэставання не трэба окалічнаго кліента. Запусціце сервер:
python -m uvicorn main:app --reload
Потым ачніце інтерактыўную документацыю API, якая выдаецца пад адресай /docs, установіце заголовак X-Demo-User і прабавіце тры варыянты взаімадзея.
- Запит працы з дадзейнамі, напрыклад, запит пра тое, якія дэмонстрацыйныя даны ў вас є. Агент должен выявіць, што патрэбна дэмонстрацыйная аплікацыя, вызваць яе і даць адказ на адпаведныя пытанні на адной з вярнуцыхся інформацый. У повноцэнным прыкладзе аплікацыі такі ж інструмент запытвае пра рэальныя записі корыстніка.
- Запит пра дакументацыю, напрыклад, запит пра тое, хто можа отрымаць дэмонстрацыйныя даны. Агент должен выконаць ручны пошук, знайсці відпаведную правілу і адказаць, што толькі адміністратары можаць гэта зрабіць.
- Тэрмінал історыі,
GET /chat/current, які должен вярнуць паслядовнасць запытанняў і адказаў ад чалавека і ШІ па вышэйзгаданым двум пытанням, без жадных запісаў апрацоўкі інструментамі.
Якщо першыя два запытанні даюць адказы, базаваныя на рэзультатах работы інструменту, а трэці паказвае чысты транскрыпт, значыць усі элементы працуюць правільна.
Перш чым пераводзіць у працэйную среду
У дэме спецыяльна спроставаны калькіляванні каля калькілявацьых элементаў. Їх трэба перагледзець занова, прычаму рэальныя корыстнікі пачну выкарыстоўваць сервіс.
Стойкі стан размовы
InMemorySaver ідеальна падходзіць для разработкі, але весь тыя данні, якія ёна зберагае, зникаюць пасля перзапуску процэсу, і яе нельга выкарыстоўваць для абмены между калькілявацьымі інстанцамі, якія знаходзяцца за лоад-балансэрам. Неабходна выкарыстоўваць чекпойнт, падтрымваны базай дадзенаў або іншым стойкім сховішчам, каб размовы застаўаліся пасля аплыванняя і кожная інстанцыя бачыла той самы стан. Чаго насправды зберагае інструмент InMemorySaver і як гэта делаецца, можна дазнайсці ў апісанні з блога па якім чыну InMemorySaver арганізавана чекпойнты, запісы і блобы.
Рэальная аплыванняя вектарнага сховішча
Местный каталог Chroma падзе для дэманстрацыі працэйнага лянцуга, але ён не являецца інфраструктурой для працы ў рэальных умовах. Запускайте Chroma як стойкую службу або перейдзіце на кераваны база дадзеных вектарных данных, якая паспрацоўвае з вашай тэхналогіяй. Што б вы не выбралі, гэта должна быць стойкая система, якая падтрымвае рэзервна копіюванне і ў якую можна з’явіцца з кожнага экземпляра прыкладнай програмы.
Індексаванне не ўключаецца ў API
У дэманстрацыі вже правільна выконана адна важлівая рашынка: індексаванне — це околачны скрыпт, які запускаецца окрема, а API толькі здзеймае даны.
python -m scripts.index_manual
Сервер ніколі не завантажвае PDF, не дзельніць яго і не вырахоўвае ембеддынгі пад час запуску. Індексаванне адбываецца паўтарна, без зв’язку з серверам; здзейманне дадзеных — частка обслужвання запиту. Раздзелэнне эйх праблемаў значыць, што API не трэба пераглядаць змяны ў дакументацыі чы перабудаваць што-небудзь.
У працэйнай суперактыўнасці трэба перейść да наступнага крока і запускать той самы код індексавання як спецыяльную задачу прыемкі дакументаў, якая запускаецца з пайплайна развёртывання, за раскладам, або калі працоўнік падключае новую дакументацыю. Архітектура застаецца тая ж; задача проста становіцца автаматызаванай, можлівай да паўторнага запуску і незалежна развёртыванай. Тады адпаведальнасці чыста дзеляцца:
- Задача прыемкі: завантажвае дакументы, раздзеляе іх на часткі, уключае ў структуру, апдэйтавае сховішча вектароў.
- Сэрвіс FastAPI: прымае запиты, выкарыстоўвае адпаведныя часткі дакументаў, генеруе адпаведныя адказы.
- Сховішча вектароў: зберагае індексаваныя версіі дакументаў, якія выкарыстоўваюцца пад час запытанняў.
Памяркавайце пры автаматызацыі індексатора пра захоўку ранньага вярнення; задача, якая мовчкі прыпускае перыяд індексавання, горша за тая, якая ўзагалі не выканана.
Экспліцытная модель уключэння
Адаптаванне пад звычайную функцію умяшчэння Chroma дапамагае захаваць дэму без дадзейнай настройкі, але у прыметный системе трэба явна выбраць і настроіць свой модель умяшчэння. Гэта дазволяе павтараць результаты і дае можлівасць кантролюваць якосць, выкалычкі, час адпаведзення і месца вычыслення умяшчэння. Є адна непрыемлівая правіла: для індексавання і для запыткаў павінен быць выкорыстаны той самы модель умяшчэння. Яго змена значыць перы індексаванне всего.
Візуабільнае адазначэнне і кантроль каштоўкаў
Калі агент запускаецца, важна бачыць, што ён зрабіў, так сама як і тое, каб ён працаваў. Адна запытка можа включаць калькі модэляў, адну або больш заходаў з інструментамі і крок адзыскання пры тым, як будзе даўана фінальная адпаведзь. Журналаванне толькі фінальнай адпаведзі дае мало інфармацыі, калі ўтворяюцца проблемы. Неабходна задокументаваць весь процес:
- Заходы з інструментамі: якія інструменты былі выкорыстаны, з якімі аргументамі і сколькі часу кожны з іх працаваў.
Мэтаспадчынам ёсць тое, каб агент ніколі не быў «чорнай скрынкай». Для кожнага запуску вы должны магчымае сказаць, што ён зрабіў, якія інструменты ён выкарыстаў, сколькі часу падышоў кожны крок і дзе ён зазнаў неудачы. Конкрэтныя інструменты залежаць ад вашай тэхнічной палітыки; прынцып жа не залежыць.
Ключовыя выводы
- Вам не патрэбна велікая платформа AI, каб дадаць корыстнага асистента да застосунку з вялікай колькасцю дадзенняў. Пачніце з найменшага набору элементаў, якія рашаюць рэальную проблему.
- Спрыяйце пошуку дакументацыі як аднаму з інструментаў. Тады у агента будзе едны, аднаковы спосаб размяшчаць як ведамасці пра продукт, так і даныя корыстніка.
- Зберагаюце ідэнтычнасьць у контексте выканання, а не ў паведамленнях. Корыстнік належыць да запиту, і інструменты должны чытаць яго з таму.
- Раздзеляйце HTTP, оркестрацыю, агента і інструменты. Кожны слой застаецца маленькім, а дадаванне новай можлівасьці значыць дадаванне новага інструмента.
Спадзяючыся матэрыялы
- Гібрыдная памяць агента: спаўненне BM25 і пошуку вектора з RRF у Python — Дазвольце дазнацца, чаму чысты пошук вектора не падходзіць як памяць агента, як Reciprocal Rank Fusion спаўнюе BM25 і ўжоцьвёрдзеныя рэзультаты ў Python, і калі падсумкі GraphRAG дапамагаюць.
- Проектаванне чатырх-яруснай памяці агента з выкарыстоўванням LangGraph і Amazon Bedrock — Навучыцеся ствараць у агентах LLM рабочую, эпізодычную, семантычную і процедурную памяць на платформах Bedrock і LangGraph, а таксама захіщаць яе ад занятых дадзенняў, выцекаў персональных даных і іншых атак.
- Дазволаць Gemini выбіраць источнік: FAISS, Tavily і прымусовыя адпаведзі ва LangGraph — Створыце простую рабочую схему на LangGraph, у якой Gemini направляе кожны запит або да базы знанняў FAISS, або на пошук у Інтернете за дапамогою Tavily, або да прымусовай адпаведзі, з викорыстоўванням механізма структураванага выходу.
- Адрасаванне запыткаў між інструментамі SQL і пошукам у Інтернете за дапамой агента Gemini — Как чыніць агент для вызываў інструментаў з LangChain на Vertex AI выбір між трыма інструментамі SQLite для перакладу текста ў SQL і рэальным пошукам у Інтернете, а таксама проблемы з дадзеннямі, залежнасцямі і аутантыкацыяй, якія можна спакаваць.
- Стрымаванне агентаў LangGraph у React без вывароту внутрашняй інфармацыі інструментаў — Как серыялізаваць выходны данны astream_events з LangGraph для Server-Sent Events, маскаваць чутлівыя данні вызываў інструментаў у FastAPI і адображаць прыемныя значкі інструментаў у React.
- Адзыякаванне паведамлення агента: рэзультаты, траекторыі і зворотныя запускі прадукцыі — Как адзыякаваць системы-агенты за межамі фінальнай адказы: картаць LangChain, LangGraph і LangSmith па ролях, адзыякаваць траекторыі і контэкст, а таксама уключаць проблемы прадукцыі ў процес адзыякавання.