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

Агентны RAG: няхай LLM вырашае, калі і дзе шукаць

Чаму статычны RAG не спраўляецца са шматкрокавымі і залежнымі ад часу пытаннямі і як агент LangGraph выбірае паміж Qdrant і вэб-пошукам — з абмежаваннямі і кэшам.

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

Крыніцы: Your RAG Pipeline Is Lying to You. The Problem Is Not the Embeddings — Agentic RAG

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

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

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

Лепшыя эмбедынгі гэта не выправяць. Калі-то дапамагае тое, што LLM сам вяршыць рашэнне, калі і дзе запрашваць інформацыю.

Проблемы з падходам «запрашаць раз, адпавядаць завжды»

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

Задайце запітанне «Калі які побачныя эфекты метформіну?», і сістэма дае адпаведны адказ. А цяпер задайце больш сложнае запітанне: якія взаімадзеянні з іншымі лекамі неяк нянедавно пазначыла FDA для метформіну ў супакоўцы з новымі прыемлівымі засобамі класу GLP-1, і чы рэгламенты, якія існуюць для паціѐентоў, пакрываюць іх? Для гэтага патрэбны внутранія дакументы і актуальныя рэгуляторныя даны, а таксама аналіз розніцы межы гэтымі дакументамі. Статычны RAG берае даны толькі з аднаго джэрага, а LLM проста пераказвае тое, што яго пачуў.

Гэта не гіпотэтычнае сценарые. У внутранім адліквідаванні эфектывнасці медычных рашэнняў [1] статычны RAG даў 89% рэлевантнасці адказаў да стабільных запытанняў, але ўпаў да 61%, калі запытанне вымагала супарабоцавання внутраніх клінічных дакументаў з зовнішнімі рэкамендаціямі па лечэнні. Простыя адліквідаванні не змоглі гэта выявіць, таму што адказы здаваліся правільнымі. Але на самай працэ ў яных не было актуальнасці.

Рашэнне даецца лёгка адказаць, але яго рэалізацыя ўскладнена: запрашэнне інфармацыі стае рашэннем, а не окрэмым крокам.

Што значыць агентны RAG

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

  • Умовнае запрашэнне інфармацыі. Калі кантэкст вялікі ўжо адпавядае на запыт, агент вообшча не запрашае інфармацыю.
  • Запрашэнне інфармацыі з калькуляцыямі. Агент запрашае інфармацыю, чытае яе, усвядомляе, што чагосці не хапяць, і вяртаецца за ўсём неабходным; кожны наступны крок залежыць ад таго, што было знайдзена ў пярэднім. Самэ так людзі веду даследавання.
  • Маршрутызацыя выклікання. Агент выбірае між хранальнікам вектараў, пошукам у інтэрнэце чыраструктурованай базе дадзэнняяў залежна ад запіту. Запыт „Што сказана ў нашай політыцы?“ направляецца да хранальніка вектараў, тады калі запыт „Што анунсавала FDA ў пярэдній тыдзень?“ — да інтэрнэту.
  • Нічога з гэтага не ўжо не ўяўляе сабою новую ідэю. У стацыі Yao і яго саавтараў (2022) была апісана схема перакрыстання крокаў аналізу з выклікамі інструментаў, а агентны RAG — гэта ReAct, прыменены да выкарыстоўвання інформацыі з баз дадзэнняяў [2].

    Няўялікія змяны адбыліся ў інструментах. LangGraph нарэшце дазволіў практычна выкарыстоўваць цыклы з розгалужэннямі. Tavily прыменяе API для пошуку ў інтэрнэце, створаны для агентаў, які вяртае контэнт, гатовы для апрацоўкі LLM, і стварыў адчутак, што можа обрабляць падаўжыце 100 мільйона запытоў у месяц з часам адпаведзі на 180 мс [3]. Дакладнейшая інфармацыя ў пастацыі Tavily 101.

    Тры ключовыя складовы

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

    1. Рутэр адпавядае на пытанне, якага роду запатэнне трэба. LLM аналізуе запытанне і выявляе, чыяхоў достатнік існуючы кантэкст, чыяхоў трэба дакументы, чыяхоў трэба інфармацыя з Інтэрнэту, чыі ўсе гэтыя элементы.
    2. Інструменты для пошуку — гэта істочнікі доказаў. Мінімум — два інструмента: адны для пошуку ў вектарным сховішчы внутрашніх дакументаў, другі для пошуку інфармацыі на Інтэрнэце. Кожны з іх — звычная функцыя, яку LLM вызывае з рэчымай запытання.
  • Перакантрольванне падставы спытвае з'ясаваць, чыякож доказы ўстаткуюць. Перш чым напісаць заканчоўны адказ, агент ацэнюе, чыякож тое, што ён знайшоў, ёсць достатнім і рэлевантным, а як ні, то зноў шукае. Статычныя пайплайны не можуць выражаць такі цыкл.
  • Функцыональная рэалізацыя

    Ёсць гібрыдны агент у LangGraph, які перыходзіць між векторным хранілішчам Qdrant і веб-пошукам Tavily. Ён выбірае інструмент, можа вызываць іх або ў парадку, або адразу, і зноў шукае, калі першы рэзультат не дае падставы для адказу. Частка, якая вікорыстоўвае Qdrant, следуе па інструкціях з інтэграцыі LangChain.

    from typing import Annotated, Literal
    from langgraph.graph import StateGraph, START, END
    from langgraph.prebuilt import ToolNode
    from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
    from langchain_core.tools import tool
    from langchain_openai import ChatOpenAI
    from langchain_qdrant import QdrantVectorStore
    from langchain_openai import OpenAIEmbeddings
    from tavily import TavilyClient
    from qdrant_client import QdrantClient
    import operator
    from typing import TypedDict, Sequence
    
    # --- State definition ---
    class AgentState(TypedDict):
        messages: Annotated[Sequence, operator.add]
        retrieval_count: int  # guard against infinite loops
    
    # --- Tool 1: Vector store retrieval (internal docs) ---
    qdrant_client = QdrantClient(url="http://localhost:6333")
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vector_store = QdrantVectorStore(
        client=qdrant_client,
        collection_name="internal_docs",
        embedding=embeddings,
    )
    @tool
    def retrieve_from_docs(query: str) -> str:
        """
        Search internal document knowledge base.
        Use for questions about internal policies, protocols, or proprietary knowledge.
        """
        results = vector_store.similarity_search(query, k=4)
        if not results:
            return "No relevant internal documents found for this query."
        return "\n\n---\n\n".join([
            f"[Source: {doc.metadata.get('source', 'internal')}]\n{doc.page_content}"
            for doc in results
        ])
    
    # --- Tool 2: Web retrieval (live data) ---
    tavily = TavilyClient(api_key="YOUR_TAVILY_API_KEY")
    @tool
    def retrieve_from_web(query: str) -> str:
        """
        Search the live web for current information.
        Use for recent news, regulatory updates, or anything that may have changed
        since the internal knowledge base was last updated.
        """
        results = tavily.search(
            query=query,
            max_results=5,
            search_depth="advanced",  # deep retrieval for better accuracy
        )
        if not results.get("results"):
            return "No relevant web results found."
        return "\n\n---\n\n".join([
            f"[Source: {r['url']}]\n{r['content']}"
            for r in results["results"]
        ])
    
    # --- Agent node ---
    tools = [retrieve_from_docs, retrieve_from_web]
    tool_node = ToolNode(tools)
    llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
    SYSTEM_PROMPT = """You are a research assistant with access to two retrieval tools:
    - retrieve_from_docs: internal document knowledge base (policies, protocols, proprietary data)
    - retrieve_from_web: live web search (recent news, regulatory updates, real-time information)
    For any question:
    1. Decide whether you need to retrieve information or can answer from context.
    2. If retrieval is needed, choose the right source - or call both if the question spans internal and live knowledge.
    3. After retrieving, check if the evidence is sufficient. If not, retrieve again with a refined query.
    4. Always cite your sources in the final answer.
    Do not hallucinate. If you cannot find sufficient evidence, say so explicitly."""
    def call_agent(state: AgentState) -> AgentState:
        messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(state["messages"])
        response = llm.invoke(messages)
        return {
            "messages": [response],
            "retrieval_count": state.get("retrieval_count", 0)
        }
    def update_retrieval_count(state: AgentState) -> AgentState:
        return {
            "messages": [],
            "retrieval_count": state.get("retrieval_count", 0) + 1
        }
    
    # --- Routing logic ---
    MAX_RETRIEVALS = 3  # prevent infinite loops
    def should_continue(state: AgentState) -> Literal["tools", "end"]:
        last_message = state["messages"][-1]
        # If the LLM called a tool and we haven't hit the loop guard, continue
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            if state.get("retrieval_count", 0) < MAX_RETRIEVALS:
                return "tools"
        return "end"
    
    # --- Build the graph ---
    graph_builder = StateGraph(AgentState)
    graph_builder.add_node("agent", call_agent)
    graph_builder.add_node("tools", tool_node)
    graph_builder.add_node("count_retrieval", update_retrieval_count)
    graph_builder.add_edge(START, "agent")
    graph_builder.add_conditional_edges(
        "agent",
        should_continue,
        {"tools": "tools", "end": END}
    )
    graph_builder.add_edge("tools", "count_retrieval")
    graph_builder.add_edge("count_retrieval", "agent")
    agent = graph_builder.compile()
    
    # --- Run it ---
    if __name__ == "__main__":
        result = agent.invoke({
            "messages": [
                HumanMessage(content=(
                    "What does our current patient protocol say about GLP-1 usage, "
                    "and are there any FDA safety updates issued in 2025 we should be aware of?"
                ))
            ],
            "retrieval_count": 0,
        })
        print(result["messages"][-1].content)
    

    Дзеяныя дэталі заслуговаюць на увагу.

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

    Поле retrieval_count у стане агента ёсць тым, што дазволяе перыякту завершыцца. Клас StateGraph з LangGraph перадае яго праз кожную пераход між вузламі, таму яно застаецца актуальным працягом усіяго дыялогу, а не толькі працягом аднаго цыклу.

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

    Калі агенты з RAG толькі пагіршуюць сітуацыю

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

    • Задзейсненне затрымкаў — галоўны абмежычны фактар. Кожны прыем паказаных дадзеных коштае ўсё больш аднае вызову модэлю плюс вызову інструменту. Для простага асістэнта для запытань і адпаведзей, дзе затрымка p95 должна застацца менейшай за секунду, агент, які можа выконваць два-тры разы цыкл, не ўзімаеся ў вярасць. Якша настроены статычны абробнік дадзеных ўжо ўсё шырэй і падходзіць.
    • База знанняяў ёсць стабільная і самодастатня. Калі на всі запытанні корыстувачаў можна адпаведаць з абавесцю на адсёлквэнты вектарнага сховішча, то дадаўшы агента, вы толькі падымаеце складнасць без прычыны. Спачатку пераканайцеся, што статычны прайсепт працуе эфектыва, выкарыстоўваючы RAGAS (артыкул). Якшо ступень вярнага адтворэння кантэксту перавышае 85%, а ступень адпаведнасці адпаведзей — 90%, тады агентычны кэйсівер ёсць зайвым.
  • Нельзя пакладацца на выбір інструментаў мадэлі. За дапамогою рэтэльна напісанага сістэмнага запіту GPT-4o і Claude Sonnet прыблізна у 88% случаў правільна пераводзяць даныя з дасягальных документаў і веб-рэсурсаў, за данымі тых сабе внутрашніх мераў. У рэшты 12% выклікаюцца помылкі у выкарыстоўванні джэрэл, якія важкаяшэ да дыягнастыкі, чым звычныя галюцынаціі, адтуды што адпаведны адказ ёсць правдападобным, проста знаходзіцца не там. З слабейшымі мадэлямі трэба празважыць выбір інструментаў раней, чым запускаць ў працу.
  • Даныя у рэальным часе не можна вважаць надзеянымі. Праця з веб-рэсурсамі стварае додатковую плошчу для атак. Tavily фільтруе запіты на включэнне зловмеснага коду ў свой API, але жадны фільтр не ёсць ідэальным, таму ўсё, што агент робіць на адной з веб-кантэнтав, трэба спрыяць як потэнцыяльна ворагоўскі матэрыял.
  • Выбірайце агентскі падход RAG, калі знанні походзяць з калькі джэрэл, а дзеякі з іх змінююцца. Пачніце з статычнага RAG і апгрэйдавайце яго, калі показнікі RAGAS выявяюць конкрэтныя недастаткі, якія можа выправіць гэты падход.

    З чаго пачаць

    Самы ўжо маленькі крок — менш чым пяць хвілін: дадзіце адзін інструмент Tavily да існуючага агента LangChain за дапамою from langchain_community.tools.tavily_search import TavilySearchResults. LangGraph пакуль не трэба. Актуалізуйце інструмент, напісце сістэмны запрос, які будзе вказываць, калі яго вызваць, запрацавайце пяць тэстовых запітоў і пазірце, калі ён будзе выбіраць рэзультаты.

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

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

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

    Джэрэлы

    [1] Внутршнія тэсты на практычным канале RAG у сферэ здравоцохранення з выкарыстоўваннем RAGAS 0.1.x на наборы з 200 клінічных запытанняў. Гэта не публікуемае даследжэнне.

    [2] Yao і іншыя (2022), ReAct: Сінергія разумовага моўчання і дзеяння ў мовных мадэлях, arXiv:2210.03629.

    [3] Блог Tavily, Tavily 101, з цыфрамі паздзейнення і об’ёму, указанымі на старонцы продукту у мае 2026 года.

    Спадні матэрыялы

  • grep Beat Vector Search в тэстах памяці агента, і ролю грае таксідоар таксама значна — Адзін з даследжэнняў 116 запытанняў LongMemEval паказаў, што grep працуе лепшая за метод вектарнага пошуку, але спосаб, якім таксідоар выдае рэзультаты, вплывае на точнасць не менш, чым сам прыемлівач даных. Што можна выняць з гэтага.
  • Expert RAG: HyDE, Компрэсія, Самапытанне і агентныя рабочыя практыкі — Чатыры прыглыбленыя тэхнікі пошуку для практычнага вікорыстання RAG у Haystack: HyDE, кантэкстная компрэсія, самапытанне і агентныя рабочыя практыкі, а таксама супакой, калі варта іх вікорыстоўваць.
  • Шар за шарам: праектаванне рэальнага каналу адзыскання дадзэнай у RAG для агентаў AI — Планаванне каналу RAG з выкарыстоўваннем контрактав SourceDocument і DocumentChunk, чанкаваннем, эмбедынгамі і ChromaDB, усё гэта пад кераваннем RetrievalService, які забезпечвае стабільнасць агента.
  • Праект RAG — гэта не адна вялікая файлавая структура на Python: структура папак, якая дазволяе рост — Раздзеліце дэмавы прыклад прыемлівання дадзэнай у RAG на часткі: прыемліванне, чанкаванне, эмбедынг, храненне вектараў, адзысканне, запыткі, кліент LLM, API, тэсты і логі, ўсё гэта, каб праект могаў раставаць за межы простага дэмавага стадыю.
  • Практычныя прыміткі: Чаму ваша RAG-паіплайнавая сістэма патрабуе пераранкер (і як — паследовы выкладкі практычных прымітак: Чаму ваша RAG-паіплайнавая сістэма патрабуе пераранкер (і як: контракты, перакрыцці і слоты для коду для команд, якія вікорыстоўваюць гэты патэрн).