Practical notes: Создание самокорректирующегося ИИ-агента с использованием Self-RAG и LangGraph
Пошаговая инструкция по Practical notes: Создание самокорректирующегося ИИ-агента с использованием Self-RAG и LangGraph: контракты, проверки и готовые блоки кода для команд, внедряющих эту схему.
Источники: Build a Self-Correcting AI Agent with Self-RAG & LangGraph
В этом руководстве пошагово показан путь от сырья до готовой системы для создания самокорректирующегося ИИ-агента с использованием Self-RAG и LangGraph. Основное внимание уделяется практическим шагам, четким проверкам и коду, который можно просто добавить в репозиторий без необходимости догадываться о намерениях автора.
Почему стандартные пайплайны RAG хрупки
Чтобы понять, почему стандартные пайплайны RAG хрупки, необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Конфигурацию следует хранить отдельно от кода приложения: файлы с настройками, хранилища секретов и флаги функций должны находиться в одном месте, которое операторы могут проверять, не читая весь код пайплайна. Необходимо указывать конкретные фрагменты текста, на которых основан ответ; без таких ссылок операторы не смогут отличить галлюцинации от пробелов в индексации.
Линейный пайплайн: получение данных и предположения
Для линейной системы обработки: перед изменением кода необходимо определить исходные данные, ответственного за выполнение шага и критерии завершения. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Необходимо одновременно задокументировать успешный сценарий работы и сценарий восстановления. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не этапом последующей доработки. Указывайте конкретные фрагменты текста, на которых основан ответ. Без цитат операторы не смогут отличить галлюцинации от проблем с индексацией.
[User Query] ──> [Vector Database] ──> [Top-K Chunks] ──> [LLM Prompt] ──> [Final Answer]
Анатомия тихой неудачи
Для анализа «тихой неудачи» необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Лучше использовать небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое шага причина неудачи должна указывать на конкретную ответственность, а не на запутанную структуру обработки данных. Указывайте те части текста, которые легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от проблем с индексацией.
Коренная причина: слепое доверие
Чтобы устранить коренную причину — слепое доверие, необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения. Указывайте те фрагменты, которые на самом деле легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
Решение: внедрение саморефлексивных циклов
Для решения проблемы: с помощью саморефлексивных циклов необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Регистрируйте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости заранее предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Указывайте конкретные фрагменты текста, на которых основан ответ. Без цитат операторы не могут отличить галлюцинации от пробелов в индексации. Для решения проблемы: с помощью саморефлексивных циклов необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Документируйте как успешный, так и восстановительный пути выполнения. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью производственной среды.
ct, не позже при обработке на польском.При работе над проектом «От слепого поиска к саморефлексии: паттерн Self-RAG» сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и что происходит при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг не срабатывает, причина должна быть связана с одной конкретной функцией, а не с запутанной цепочкой операций. Оценивайте уровень воспроизведения информации на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
Три основы самокоррекции
При работе над книгой «Три столпа самокоррекции» сначала запишите контракт: необходимые входные данные, сигнал успеха и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения задачи. Измеряйте уровень воспроизведения ответов на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
[ Query ]
│
▼
1. Is Retrieval Needed? ──(No)──► Generate directly from parametric memory
│
(Yes)
▼
[ Retrieve ]
│
▼
2. Are Docs Relevant? ──(No)──► Reformulate search query & retry
│
(Yes)
▼
[ Generate ]
│
▼
3. Is Answer Faithful? ──(No)──► Regenerate or extract better context
│
(Yes)
▼
[ Final Output ]
Токены для размышлений как сигналы управления
При использовании токенов отражения в качестве сигналов управления сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды. Храните в кэше стабильные инструкции системы и схемы инструментов. Повторная отправка одинаковых данных — частая причина неоправданных расходов. При использовании токенов отражения в качестве сигналов управления сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Документируйте как успешный, так и путь восстановления. Повторные попытки, проверки со стороны оператора и обработка некорректных сообщений являются частью продукта, а не элементами последующей доработки.
Архитектура самоотражения
Архитектура саморефлексии работает наилучшим образом, когда рассматривается как измеримая структура. Соберите один пример успешного выполнения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Предпочитайте небольшие, тестируемые единицы вместо обширных скриптов. При сбое какого-либо шага причина должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Разделяйте политику разбиения на части и политику извлечения данных. Изменение одной из них не должно приводить к переписыванию другой при изменении показателей качества.
graph TD
Start[User Query] --> DecideNeed{Is Retrieval Needed?}
DecideNeed -- No --> GenerateDirect[Generate Output]
DecideNeed -- Yes --> Retrieve[Retrieve Documents]
Retrieve --> GradeDocs[Grade Document Relevance]
GradeDocs -- Irrelevant --> Fallback[Rewrite Query / Fallback]
GradeDocs -- Relevant --> GenerateRef[Generate Response with Context]
GenerateRef --> GradeFaith[Grade Faithfulness & Hallucinations]
GradeFaith -- Hallucinated --> GenerateRef
GradeFaith -- Faithful --> GradeUtility[Grade Utility]
GradeUtility -- Low Utility --> Rewrite[Refine Response]
GradeUtility -- High Utility --> End[Final Answer]
Сравнение конвейеров и рабочих процессов
Сравнение конвейеров обработки данных и рабочих потоков наиболее эффективно при рассмотрении их как измеримых показателей. Соберите один эталонный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия создаваемым элементам, определите критерии успешного выполнения и не допускайте молчаливого частичного завершения задачи. Разделяйте политику разбиения данных на части и политику их извлечения. Изменение одной из них не должно приводить к переписыванию другой при изменении показателей качества.
Проектирование с использованием LangGraph: от линейных цепочек к машинам состояний
Архитектура с LangGraph: от линейных цепочек к автоматам состояний работает наилучшим образом, если рассматриваться как измеримая поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Разделяйте политику разбиения данных на части и политику поиска. Изменение одной из них не должно принуждать к переписыванию другой при изменении показателей качества. Архитектура с LangGraph: от линейных цепочек к автоматам состояний работает наилучшим образом, если рассматриваться как измеримая поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Документируйте одновременно успешный путь выполнения и путь восстановления. Повторные попытки, проверка человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки.
Linear RAG: [Query] ──> [Retrieve] ──> [Generate] ──> [Response] (No correction)
Self-RAG: [Query] ──> [Retrieve] ──> [Evaluate] ──┬─ (Valid) ──> [Response]
▲ │
└─────── (Invalid) ──────┘ (Loop & Refine)
Ограничения линейных цепочек
При работе с ограничениями линейных цепочек необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Лучше использовать небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое шага причина должна быть связана с конкретной областью ответственности, а не с запутанной структурой обработки данных. Указывайте те части текста, которые лежат в основе ответа. Без цитат операторы не смогут отличить галлюцинации от проблем с индексацией.
Раскрытие тайн машинного состояния ИИ
Чтобы прояснить принципы работы автомата состояний ИИ, необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия соответствующим элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения задачи. Указывайте те фрагменты текста, которые легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
┌──────────────────────┐
│ Start │
└──────────┬───────────┘
▼
┌──────────────┐ ┌──────────────────────┐
│ Formulate │◄─┤ Retrieve Docs ├─┐
│ New Query │ └──────────────────────┘ │
└──────▲───────┘ │
│ ▼
┌──────┴───────┐ ┌──────────────────────┐ │
│ Re-Retrieve │ │ Grade Relevance │─┘
└──────────────┘ └──────────┬───────────┘
▼
┌──────────────────────┐
│ Generate Answer │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ Grade Hallucination │
└──────────────────────┘
Внутренняя структура основных компонентов LangGraph
В руководстве «Inside the LangGraph Core Components» необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости заранее предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Указывайте конкретные фрагменты текста, на которых основан ответ. Без цитат операторы не могут отличить галлюцинации от пробелов в индексации. В руководстве «Inside the LangGraph Core Components» необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Документируйте одновременно успешный сценарий работы и сценарий восстановления. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не последующими улучшениями.
При работе над реализацией центрального состояния графа сначала запишите контракт: необходимые входные данные, сигнал о успехе и то, что происходит при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Перед настройкой подсказок измерьте уровень воспроизведения ответов на фиксированном наборе вопросов — частая смена подсказок редко помогает улучшить качество поиска.
from typing import List, TypedDict
class GraphState(TypedDict):
"""
Represents the central state of our Self-RAG agent.
This TypedDict serves as the single source of truth passed between
each node in the LangGraph execution flow.
"""
question: str # The original user query
generation: str # The most recent answer generated by the LLM
documents: List[str] # Relevant text passages retrieved from our database
steps: List[str] # An audit trail tracking the sequence of execution steps
Создание графа, часть 1: определение узлов состояния и действий
При работе над разделом «Создание графа, часть 1: Определение узлов состояния и действий» сначала запишите контракт: необходимые входные данные, сигнал о успехе и то, что происходит при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения задачи. Оцените уровень воспроизводимости на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
Понимание оркестрации на основе состояний
При изучении концепции оркестрации на основе состояний сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Запишите также время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Перед настройкой подсказок измерьте точность воспроизведения ответов на фиксированный набор вопросов. Частая смена подсказок редко помогает улучшить качество поиска. При изучении концепции оркестрации на основе состояний сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Одновременно задокументируйте успешный сценарий работы и сценарий восстановления. Повторные попытки, проверка человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки.
+-------------------------------------------------------------+
| GRAPH STATE |
| - question: str |
| - documents: List[Document] |
| - generation: str |
| - relevance_grade: str |
+-------------------------------------------------------------+
|
+----------------------+----------------------+
| | |
v v v
+--------------+ +--------------+ +--------------+
| Retrieve | | Grade Docs | | Generate |
| Node | | Node | | Node |
+--------------+ +--------------+ +--------------+
Проектирование реестра общего состояния
Проектирование реестра общего состояния наиболее эффективно, когда его рассматривают как измеримую структуру. Сначала соберите один эталонный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию, прежде чем расширять объем работ. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое какого-либо шага причина должна быть связана с конкретной функцией, а не с запутанной последовательностью операций. Разделяйте правила формирования частей данных и правила их извлечения. Изменение одних не должно вынуждать переписывать другие при изменении показателей качества.
from typing import List, Dict, Any
from typing_extensions import TypedDict
from langchain_core.documents import Document
class GraphState(TypedDict):
"""
Represents the state of our Self-RAG graph.
Attributes:
question: The user query string.
documents: A list of retrieved document chunks.
generation: The generated answer from the LLM.
relevance_grade: The evaluation score of our documents ('yes' or 'no').
"""
question: str
documents: List[Document]
generation: str
relevance_grade: str
Узел извлечения данных: взаимодействие с базами знаний
Узел извлечения: взаимодействие с базами знаний наиболее эффективно, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример, один случай сбоя и запись о откате перед расширением объема работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия создаваемым элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Разделяйте политику разбиения данных на части и политику извлечения. Изменение одной из них не должно приводить к переписыванию другой при изменении показателей качества.
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.embeddings import FakeEmbeddings
# Initialize a mock vector store to simulate a production knowledge base
embeddings = FakeEmbeddings(size=1536)
vectorstore = InMemoryVectorStore(embeddings)
# Seed our vector store with sample documentation chunks
vectorstore.add_documents([
Document(page_content="LangGraph is a library for building stateful, multi-actor applications with LLMs.", metadata={"source": "docs"}),
Document(page_content="Self-RAG is an architecture that introduces self-reflection and grading into retrieval chains.", metadata={"source": "paper"}),
Document(page_content="Vector databases index text embeddings to enable fast semantic similarity search.", metadata={"source": "tech-blog"})
])
# Create a retriever interface
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
def retrieve(state: GraphState) -> Dict[str, Any]:
"""
Retrieve documents from the vector database based on the current question.
Args:
state (GraphState): The current state of the graph.
Returns:
dict: An update to the state containing the retrieved documents.
"""
print("--- NODE: RETRIEVING DOCUMENTS ---")
question = state["question"]
# Perform the semantic vector search
retrieved_docs = retriever.invoke(question)
# Return the updated document list to the graph state
return {"documents": retrieved_docs}
Узел генерации: синтез с учетом контекста
Узел генерации: синтез с учетом контекста работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример вывода, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Разделяйте политику разбиения на части и политику поиска данных. Изменение одной из них не должно принуждать к переписыванию другой при изменении показателей качества. Узел генерации: синтез с учетом контекста работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример вывода, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Документируйте одновременно успешный путь выполнения и путь восстановления. Повторные попытки, проверка человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.language_models.fake import FakeListLLM
# Set up a mock LLM that simulates generating structured responses
# In production, replace this with: from langchain_openai import ChatOpenAI
llm = FakeListLLM(responses=[
"Self-RAG is an advanced architecture that uses self-reflection and document grading to improve retrieval accuracy."
])
def generate(state: GraphState) -> Dict[str, Any]:
"""
Generate an answer using the retrieved documents as grounding context.
Args:
state (GraphState): The current state of the graph.
Returns:
dict: An update to the state containing the generated answer.
"""
print("--- NODE: GENERATING ANSWER ---")
question = state["question"]
documents = state["documents"]
# Format the retrieved documents into a single context block
context = "\n\n".join([doc.page_content for doc in documents])
# Construct the instruction prompt
prompt = ChatPromptTemplate.from_template(
"You are an assistant for question-answering tasks.\n"
"Use the following pieces of retrieved context to answer the question.\n"
"If you do not know the answer, say that you do not know.\n"
"Question: {question}\n"
"Context: {context}\n"
"Answer:"
)
# Format the prompt with our state variables and run the model
chain = prompt | llm
generation = chain.invoke({"question": question, "context": context})
# Save the generated response to the state
return {"generation": generation}
Узел оценки документов: инспектор качества
Для узла документов оценки: инспектор качества должен определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Лучше использовать небольшие, тестируемые единицы вместо обширных скриптов. При сбое шага причина должна указывать на конкретного ответственного, а не на запутанную цепочку операций. Указывайте те фрагменты текста, которые легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
+-------------------+
| Retrieved Docs |
+-------------------+
|
v
+-----------------------+
| Is Doc Relevant? |
+-----------------------+
/ \
Yes / \ No
/ \
v v
+-------------------+ +------------------+
| Keep in State | | Filter / Discard |
| (Pass to Gen) | | (Trigger Loop) |
+-------------------+ +------------------+
from pydantic import BaseModel, Field
from langchain_core.output_parsers import JsonOutputParser
class DocumentGrade(BaseModel):
"""Binary score for document relevance assessment."""
binary_score: str = Field(
description="Relevance score for the document. Must be 'yes' or 'no'."
)
def grade_documents(state: GraphState) -> Dict[str, Any]:
"""
Assess whether the retrieved documents are relevant to the user question.
Filters out any irrelevant documents from the state.
Args:
state (GraphState): The current state of the graph.
Returns:
dict: Updated documents list and a global relevance grade flag.
"""
print("--- NODE: GRADING RETRIEVED DOCUMENTS ---")
question = state["question"]
documents = state["documents"]
# Use a mock parser to simulate LLM grading
# In production, use: llm.with_structured_output(DocumentGrade)
parser = JsonOutputParser(pydantic_object=DocumentGrade)
filtered_docs = []
has_relevant_doc = "no"
# Evaluate each document individually
for doc in documents:
# Construct a strict grading prompt
grade_prompt = (
f"Question: {question}\n"
f"Document: {doc.page_content}\n"
"Assess if this document is relevant to the question. Output JSON with key 'binary_score' as 'yes' or 'no'."
)
# Simulating LLM evaluation logic (in production, run the LLM chain here)
# For our mock run, if the query terms overlap with the document, mark 'yes'
is_relevant = any(term.lower() in doc.page_content.lower() for term in question.split())
score = "yes" if is_relevant else "no"
if score == "yes":
print(f" -> DOCUMENT GRADING: [RELEVANT] - Source: {doc.metadata.get('source')}")
filtered_docs.append(doc)
has_relevant_doc = "yes"
else:
print(f" -> DOCUMENT GRADING: [NOT RELEVANT] - Source: {doc.metadata.get('source')}")
# Update state with filtered documents and set the control flag
return {
"documents": filtered_docs,
"relevance_grade": has_relevant_doc
}
Создание графа, часть 2: Связывание решений с условными ребрами
Для построения графа, часть 2: Связывание решений с условными рёбрами — определите входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного завершения работы. Цитируйте те фрагменты, которые на самом деле легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
+------------------+
| route_question |
+--------+---------+
|
+--------------+--------------+
| |
[web_search] [retrieve]
| |
+--------------> +------------+-------------+
| grade_documents |
+------------+--------------+
|
+-----+-----+
| Decision |
+-----+-----+
|
+-----------------+-----------------+
| |
(transform_query) (generate)
^ |
| +------+------+
| | Decision |
| +------+------+
| |
+--- [hallucination / unsat] -------+------> [__end__]
Маршрутизатор точки входа: сначала анализ запроса
Для маршрутизатора входной точки: сначала проверьте запрос. Определите входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости заранее предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Укажите те фрагменты текста, которые на самом деле легли в основу ответа. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации. Для маршрутизатора входной точки: сначала проверьте запрос. Определите входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Документируйте одновременно успешный путь выполнения и путь восстановления. Повторные попытки, человеческое вмешательство и обработка неработающих сообщений являются частью
товар, а не позже его отладка.# router.py
from typing import Literal
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from state import AgentState
# Define the structured output schema for our router
class RouteQuery(BaseModel):
"""Route a user query to the most authoritative datasource."""
datasource: Literal["vectorstore", "web_search"] = Field(
...,
description="Given a user question choose to route it to web search or a vectorstore.",
)
def route_question(state: AgentState) -> str:
"""
Routes the initial question to either the vectorstore retrieval node or web search.
Args:
state (AgentState): The current graph state containing the user query.
Returns:
str: The next node to execute ("vectorstore" or "web_search").
"""
print("--- ROUTING QUESTION ---")
question = state["question"]
# Initialize our LLM with structured output mapping to our Pydantic schema
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_router = llm.with_structured_output(RouteQuery)
# Create the routing system prompt
system_prompt = """You are an expert router directing user questions to a vectorstore or web search.
The vectorstore contains highly technical documentation regarding LangGraph, LangChain, and state machines.
Use the vectorstore for these specialized topics. Otherwise, use web_search for general knowledge or current events."""
route_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}"),
])
# Run the router chain
router_chain = route_prompt | structured_router
decision = router_chain.invoke({"question": question})
if decision.datasource == "web_search":
print("--- ROUTING TO WEB SEARCH ---")
return "web_search"
print("--- ROUTING TO VECTORSTORE ---")
return "retrieve"
Ворота контекста: оценка релевантности документов
При работе над «Воротами контекста: оценка релевантности документов» сначала запишите условия контракта: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список поможет избежать лжи при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Измеряйте показатель воспроизведения на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
# context_gate.py
from state import AgentState
def decide_to_generate(state: AgentState) -> str:
"""
Evaluates the document grading state to choose the next step.
If any documents were graded as relevant, we proceed to answer generation.
If all documents were deemed irrelevant, we rewrite the query to try again.
Args:
state (AgentState): The current state of the agent.
Returns:
str: Next node to execute ("generate" or "transform_query").
"""
print("--- ASSESSING DOCUMENT RELEVANCE ---")
# Retrieve the decision variable set by the grading node
run_web_search = state.get("run_web_search", False)
if run_web_search:
# No documents were relevant, we must rewrite and search the web
print("--- DECISION: ALL DOCUMENTS IRRELEVANT, ROUTING TO QUERY TRANSFORMATION ---")
return "transform_query"
# We found relevant documents, we can proceed safely to generation
print("--- DECISION: RELEVANT DOCUMENTS FOUND, ROUTING TO GENERATE ---")
return "generate"
Цикл самокоррекции: проверка точности и полезности
При работе над «Циклом самокоррекции: проверка точности и полезности» сначала запишите условия соглашения: необходимые входные данные, сигнал о успехе и то, что происходит при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как соглашение между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения задачи. Оцените уровень воспроизведения ответов на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска информации.
+-------------------------+
| Answer Generated |
+------------+------------+
|
v
+--------------------------------+
| Is the answer grounded in |
| retrieved context? |
+----------------+---------------+
|
+---------------+---------------+
| |
[No] [Yes]
| |
v v
+-------------------+ +---------------------+
| Re-route to: | | Is the answer |
| "generate" | | actually useful? |
+-------------------+ +----------+----------+
|
+--------------+--------------+
| |
[No] [Yes]
| |
v v
+-------------------+ +------------------+
| Re-route to: | | Re-route to: |
| "transform_query" | | "__end__" |
+-------------------+ +------------------+
# self_correction.py
from typing import Literal
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from state import AgentState
class GradeHallucinations(BaseModel):
"""Binary score for hallucination present in generation."""
binary_score: Literal["yes", "no"] = Field(
...,
description="Answer is grounded in the facts, 'yes' or 'no'",
)
class GradeAnswer(BaseModel):
"""Binary score to assess if answer addresses question."""
binary_score: Literal["yes", "no"] = Field(
...,
description="Answer addresses the question, 'yes' or 'no'",
)
def grade_generation_v_documents_and_question(state: AgentState) -> str:
"""
Grades the generation for hallucinations and utility.
Args:
state (AgentState): The current graph state.
Returns:
str: Next step path ("supported" -> check utility, "not supported" -> regenerate, "not useful" -> rewrite query)
"""
print("--- CHECKING FOR HALLUCINATIONS ---")
question = state["question"]
documents = state["documents"]
generation = state["generation"]
# Initialize evaluator LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ----------------------------------------
# STEP 1: Groundedness (Hallucination) Check
# ----------------------------------------
hallucination_grader = llm.with_structured_output(GradeHallucinations)
hallucination_system_prompt = """You are an objective grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts.
Give a binary score of 'yes' if the generation is strictly grounded in and supported by the provided facts. Otherwise, give 'no'."""
hallucination_prompt = ChatPromptTemplate.from_messages([
("system", hallucination_system_prompt),
("human", "Set of facts: \n\n {documents} \n\n LLM generation: {generation}"),
])
# Format document contents into a single string context
doc_context = "\n\n".join([doc.page_content for doc in documents])
hallucination_chain = hallucination_prompt | hallucination_grader
res = hallucination_chain.invoke({"documents": doc_context, "generation": generation})
if res.binary_score == "no":
print("--- DECISION: HALLUCINATION DETECTED! RE-TRYING GENERATION ---")
return "generate"
print("--- DECISION: GENERATION IS GROUNDED IN DOCUMENTS ---")
# ----------------------------------------
# STEP 2: Utility (Answer Relevance) Check
# ----------------------------------------
answer_grader = llm.with_structured_output(GradeAnswer)
answer_system_prompt = """You are an objective grader assessing whether an LLM generation addresses and resolves the user's question.
Give a binary score of 'yes' if the answer directly addresses and completely answers the user's question. Otherwise, give 'no'."""
answer_prompt = ChatPromptTemplate.from_messages([
("system", answer_system_prompt),
("human", "User question: \n\n {question} \n\n LLM generation: {generation}"),
])
answer_chain = answer_prompt | answer_grader
res_answer = answer_chain.invoke({"question": question, "generation": generation})
if res_answer.binary_score == "yes":
print("--- DECISION: ANSWER IS USEFUL AND ACCURATE. ENDING GRAPH ---")
return "useful"
print("--- DECISION: ANSWER DOES NOT FULLY ADDRESS QUESTION. REWRITING QUERY ---")
return "not useful"
Сборка и компиляция StateGraph
При работе над сборкой и компиляцией StateGraph сначала запишите условия использования: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Очевидность затрат с самого начала предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды. Измеряйте точность ответов на фиксированный набор вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска. При работе над сборкой и компиляцией StateGraph сначала запишите условия использования: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Документируйте одновременно успешный и восстановительный сценарии работы. Повторные попытки, проверка человеком и обработка неработающих сообщений являются частью продукта, а не элементами последующей доработки.
# graph_builder.py
from langgraph.graph import END, StateGraph
from state import AgentState
# Import node functions from Part 1
from nodes import retrieve, generate, transform_query, web_search, grade_documents
# Import conditional routing edges
from router import route_question
from context_gate import decide_to_generate
from self_correction import grade_generation_v_documents_and_question
# Initialize our state-based graph
workflow = StateGraph(AgentState)
# ----------------------------------------
# 1. Register All Computational Nodes
# ----------------------------------------
workflow.add_node("retrieve", retrieve)
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("generate", generate)
workflow.add_node("transform_query", transform_query)
workflow.add_node("web_search", web_search)
# ----------------------------------------
# 2. Configure Node Routing & Transitions
# ----------------------------------------
# The entry point of our execution is dynamic, decided by the routing edge
workflow.set_conditional_entry_point(
route_question,
{
"web_search": "web_search",
"retrieve": "retrieve",
},
)
# After retrieval, we must immediately grade our retrieved documents
workflow.add_edge("retrieve", "grade_documents")
# Once documents are graded, the Context Gate decides where to go
workflow.add_conditional_edges(
"grade_documents",
decide_to_generate,
{
"transform_query": "transform_query",
"generate": "generate",
},
)
# If query transformation occurs, we execute a web search to gather new data
workflow.add_edge("transform_query", "web_search")
# After web searching, we feed those newly acquired documents back to the grade step
workflow.add_edge("web_search", "grade_documents")
# Once generation occurs, our Self-Correction edge decides the final quality check
workflow.add_conditional_edges(
"generate",
grade_generation_v_documents_and_question,
{
"generate": "generate", # Loop back to fix hallucination
"not useful": "transform_query", # Loop back to rewrite query and search
"useful": END, # Exit cleanly
},
)
# ----------------------------------------
# 3. Compile and Run the Graph
# ----------------------------------------
app = workflow.compile()
# visualize.py
from graph_builder import app
try:
# Print the plain text visualization of the state machine architecture
app.get_graph().print_ascii()
except Exception as e:
print(f"ASCII generation failed: {e}")
+----------------+
| __start__ |
+-------+--------+
|
+---------+---------+
v v
+------------+ +------------+
| web_search | | retrieve |
+-----+------+ +-----+------+
| |
| v
| +-----+------+
| | grade_docs |
| +-----+------+
| |
+---------+---------+
|
v
+---------+---------+
| decide_to_gen |
+----+---------+----+
| |
+----------+ +----------+
v v
+-----+------+ +-----+------+
| transform_q| | generate |
+-----+------+ +-----+------+
| |
| v
| +---------+---------+
| | self_correct |
| +----+----+----+----+
| | | |
+--------------------------+ | +---> [__end__]
v
(regenerate)
Инженерный синтез
Метод инженерного синтеза работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример реализации, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работы. Предпочитайте небольшие, проверяемые единицы кода вместо обширных скриптов. При сбое какого-либо шага причина должна быть связана с конкретной ответственностью, а не с запутанной структурой обработки данных. Разделяйте правила разбиения кода на части и правила его извлечения. Изменение одних не должно принуждать к переписыванию других при изменении показателей качества.
Практическое применение
Практическое применение работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия создаваемым элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Разделяйте политику разбиения на части и политику извлечения данных. Изменение одной из них не должно приводить к переписыванию другой при изменении показателей качества.
Роботы для продвинутой поддержки клиентов
Роботы для продвинутой поддержки клиентов работают наилучшим образом, когда их рассматривают как объекты с измеримыми показателями. Соберите один идеальный пример общения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Записывайте время выполнения операций, а также стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общим средам. Разделяйте политику разбиения данных на части и политику их поиска. Изменение одной из них не должно приводить к необходимости переписывания другой при изменении показателей качества. Роботы для продвинутой поддержки клиентов работают наилучшим образом, когда их рассматривают как объекты с измеримыми показателями. Соберите один идеальный пример общения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Документируйте как успешный, так и восстановительный пути работы системы. Повторные попытки, проверка человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами, добавляемыми позже.
Автоматизированные системы проверки фактов
Для автоматизированных систем проверки фактов необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Лучше использовать небольшие, тестируемые единицы вместо обширных скриптов. При сбое шага он должен указывать на конкретную причину, а не на сложную структуру обработки данных. Указывайте те фрагменты текста, на которых основан ответ. Без цитат операторы не смогут отличить галлюцинации от проблем с индексацией.
Динамические двигатели рекомендаций
Для динамических двигателей рекомендаций необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия результатам работы, определите критерии успеха и не допускайте молчаливого частичного завершения задачи. Укажите конкретные фрагменты текста, на которых основан ответ. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
Помощники в научных исследованиях
Для помощников в научных исследованиях необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости заранее предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Указывайте те части текста, которые фактически легли в основу ответа. Без цитат операторы не могут отличить галлюцинации от пробелов в индексации. Для помощников в научных исследованиях необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Документируйте одновременно успешный путь выполнения и пути восстановления. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не последующими улучшениями.
Когда следует использовать тот или иной подход?
При решении вопроса «Когда следует использовать тот или иной подход?» сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода большим скриптам. Если какой-то шаг не сработает, причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций. Перед настройкой запросов измерьте уровень воспроизведения ответов на фиксированном наборе вопросов. Частая смена формулировок запросов редко помогает улучшить качество поиска.
Ограничения в производственной среде: затраты, задержки и возможности мониторинга
При работе над «Production Guardrails: Costs, Latency, and Observability» сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и последствия частичной неудачи. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам кода, определите критерии успеха и не допускайте безупречного, но частичного выполнения задачи. Измеряйте степень воспроизводимости на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
Балансировка затрат и задержек с помощью асимметричного маршрутизирования
При работе над балансировкой затрат и задержек с использованием асимметричного маршрутизирования сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Очевидность затрат с самого начала предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Измеряйте точность получения ответов на фиксированный набор вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска. При работе над балансировкой затрат и задержек с использованием асимметричного маршрутизирования сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Документируйте одновременно успешный и восстановительный сценарии работы. Повторные попытки, проверка человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки.
Выключатели аварийного останова в состоянии LangGraph
Выключатели аварийного останова в состоянии LangGraph работают наилучшим образом, когда их рассматривают как измеримую структуру. Сначала соберите один эталонный пример вывода, один случай сбоя и записку о возврате к предыдущему состоянию, прежде чем расширять объём работы. Предпочитайте небольшие, тестируемые единицы вместо обширных скриптов. При сбое какого-либо шага причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций. Разделяйте политику разбиения данных на части и политику их извлечения. Изменение одной из них не должно приводить к переписыванию другой при изменении показателей качества.
from typing import Dict, TypedDict, Literal
# Define graph state with a loop counter to act as a circuit breaker
class GraphState(TypedDict):
question: str
generation: str
documents: list[str]
loop_step: int # Tracks iterations to prevent runaway execution costs
MAX_RETRIES = 3
def grade_generation_route(state: GraphState) -> Literal["web_search", "finalize", "max_retries_exceeded"]:
"""
Evaluates the quality of generation and routes the agent accordingly.
Why: Acts as a deterministic circuit breaker when LLMs get stuck in correction loops.
"""
current_step = state.get("loop_step", 0)
# 1. Evaluate circuit breaker before making any expensive LLM calls
if current_step >= MAX_RETRIES:
return "max_retries_exceeded"
# 2. Simulated grader check (In production, this queries your grading model)
is_accurate = False # Assume we failed the grade in this step
if is_accurate:
return "finalize"
else:
# Route back for retrieval/generation rewrite
return "web_search"
Структурированные промпты для детерминированной оценки
Структурированные промпты для детерминистичной оценки работают наилучшим образом, когда их рассматривают как измеримую поверхность. Соберите один идеальный пример выполнения, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема задачи. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия соответствующим элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Установите лимит токенов на каждый ход и на всю сессию. Инструменты агентов активно расширяют контекст; строгие ограничения предотвращают появление неожиданных счетов.
from langchain_core.pydantic_v1 import BaseModel, Field
class GradeDocument(BaseModel):
"""Binary score for document relevance assessment."""
binary_score: str = Field(
description="Relevance score for the document. Must be 'yes' or 'no'."
)
explanation: str = Field(
description="A brief, single-sentence justification for the given score."
)
Отслеживание выполнения графа с помощью двигателей наблюдаемости
Отслеживание выполнения графа с использованием двигателей наблюдаемости работает наилучшим образом, когда рассматривается как измеримая поверхность. Сохраните один идеальный пример выполнения, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Разделяйте политику разбиения данных на части и политику их извлечения. Изменение одной из них не должно принуждать к переписыванию другой при изменении показателей качества. Отслеживание выполнения графа с использованием двигателей наблюдаемости работает наилучшим образом, когда рассматривается как измеримая поверхность. Сохраните один идеальный пример выполнения, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Документируйте одновременно успешный путь выполнения и путь восстановления. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не элементами последующей доработки.
[User Query] ──> [retrieve] ──> [grade_documents] (Score: No)
│
▼
[finalize] <── [generate] <── [web_search] (Updated State: Loop Count = 1)
Что раскрывает рабочий процесс, управляемый состоянием
Чтобы понять, что раскрывает рабочий процесс, управляемый состоянием, необходимо заранее определить входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Лучше использовать небольшие, тестируемые единицы вместо обширных скриптов. При сбое шага он должен указывать на конкретную причину, а не на запутанную структуру процесса. Указывайте те части текста, которые фактически легли в основу ответа. Без цитат операторы не смогут отличить вымысел от проблем с индексацией.
Чек-лист операций
Чек-лист операций наиболее эффективен, когда рассматривается как измеримая основа. Соберите один эталонный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ.
Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, которое операторы могут проверять, не читая весь кодовый граф.
Разделяйте политику разбиения на части и политику извлечения данных. Изменение одной не должно принуждать к переписыванию другой при изменении показателей качества.
Внедряйте проверку человеком для операций, связанных с расходами или изменением производственных данных. Настройки, сделанные во время компиляции, не гарантируют полноты обработки данных в бизнес-процессах.
Напишите краткий руководство: как обновлять ключи, как опустошать очередь, как откатывать последнюю загрузку данных.
Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Укажите названия файлов, определите критерии успеха и не допускайте молчаливого частичного завершения работы.
Перед повышением уровня стека заморозьте версии, сохраните эталонный вариант данных для критических процессов и убедитесь в наличии шагов для отката. В совместных средах необходимы ограничения на частоту запросов, проверки принадлежности ресурсов и четко определенный ответственный за обновление секретов. Лучше предпочесть надежность, несмотря на её простоту, чем креативные, но единоразовые демонстрации.
Примечание к пакету eeb69aedbcbc: не включайте ключи поставщиков в репозиторий, установите лимит токенов на сессию и храните транскрипции рядом с фиксами для оценки, чтобы последующие замены моделей оставались сопоставимыми.