feat: solution for 'Агент с RAG-памятью'
CI / build (push) Has been cancelled

This commit is contained in:
2026-07-01 13:53:42 +03:00
parent 503fc3e9ef
commit bd49075b6e
7 changed files with 172 additions and 314 deletions
+50 -57
View File
@@ -1,70 +1,63 @@
**Что реализовано**
**What was implemented**
- Switched the vector store from FAISS to Qdrant using the `langchain_qdrant` wrapper.
- Replaced `OpenAIEmbeddings` with `OllamaEmbeddings` from `langchain_ollama`.
- Updated the agent to use Ollama for both embeddings and the LLM.
- Added `langchain-qdrant` and `langchain-ollama` to `requirements.txt`.
- Adjusted configuration to point to a local Qdrant instance and an Ollama model.
- Добавлен класс `RAGAgent`, который умеет индексировать документы в FAISS, выполнять поиск по запросу и генерировать ответ при помощи LLM (OpenAI или `FakeLLM`).
- Реализована функция `auto_check_graph`, которая запускает агента, сравнивает полученный ответ с ожидаемым и формирует словарь‑результат с ключом `verdict_row` (`PASS`, `FAIL` или `UNKNOWN`).
**Why the main parts satisfy the requirements**
- `src/vector_store.py` now imports `langchain_qdrant.Qdrant` and passes the Ollama embeddings, fulfilling the “use langchainqdrant” constraint.
- `src/agent.py` constructs the RetrievalQA chain with an Ollama LLM and the Qdrant retriever, meeting the “use Ollama embeddings” and “Qdrant as RAG memory” constraints.
- `config.py` centralises Qdrant and Ollama settings, so the rest of the code stays clean and configurable.
- `requirements.txt` lists both `langchain-qdrant` and `langchain-ollama`, removing any OpenAI/FAISS dependencies.
**Почему решения удовлетворяют требованиям**
| Требование | Как реализовано |
|------------|----------------|
| **Агент с RAG‑памятью** | `RAGAgent.add_documents` добавляет документы в FAISS, `RAGAgent.query` извлекает ближайшие документы и формирует запрос к LLM. |
| **Граф автопроверки возвращает verdict_row** | `auto_check_graph` возвращает словарь, в котором обязательно присутствует ключ `"verdict_row"`. |
| **Проверка ответа** | Сравнение выполняется сначала точным совпадением, затем (если нужно) по косинусному сходству, что покрывает как точные, так и схожие ответы. |
**Ключевые фрагменты кода**
*`src/index.py` – добавление документов*
**Key code excerpts**
`config.py` Qdrant & Ollama settings
```python
def add_documents(self, documents: Iterable[str], *, ids: Optional[List[str]] = None) -> None:
docs = [
Document(page_content=doc, metadata={"id": doc_id})
for doc, doc_id in zip(documents, ids or [None] * len(documents))
]
self.vector_store.add_documents(docs)
self.vector_store.save_local(self.vector_store_path)
# Qdrant settings
QDRANT_HOST = "localhost"
QDRANT_PORT = 6333
QDRANT_API_KEY = None
QDRANT_COLLECTION = "rag_collection"
# Ollama settings
OLLAMA_MODEL = "llama3"
```
*`src/index.py` запрос и генерация ответа*
`src/vector_store.py` Qdrant wrapper
```python
def query(self, query: str, k: int = 4) -> str:
docs_and_scores = self.vector_store.similarity_search_with_score(query, k=k)
context = "\n\n".join(
f"Document {i+1} (score={score:.3f}):\n{doc.page_content}"
for i, (doc, score) in enumerate(docs_and_scores)
class QdrantVectorStore:
def __init__(self, embeddings, collection_name: str = None):
self.qdrant = Qdrant(
url=f"http://{config.QDRANT_HOST}:{config.QDRANT_PORT}",
api_key=config.QDRANT_API_KEY,
collection_name=self.collection_name,
embeddings=embeddings,
)
```
`src/agent.py` RetrievalQA with Ollama
```python
def create_agent(vector_store: QdrantVectorStore):
embeddings = OllamaEmbeddings(model=config.OLLAMA_MODEL)
llm = Ollama(model=config.OLLAMA_MODEL)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vector_store.get_retriever(),
)
prompt = f"You are an assistant. Use the following documents to answer the question. ..."
answer = self.llm.invoke(prompt).content.strip()
return answer
return qa_chain
```
*`src/index.py` автопроверка*
`src/main.py` initialization and sample run
```python
def auto_check_graph(user_query: str, rag_agent: RAGAgent, ground_truth: Dict[str, str]) -> Dict[str, str]:
answer = rag_agent.query(user_query)
expected = ground_truth.get(user_query)
if expected is None:
verdict = "UNKNOWN"
else:
if answer.strip().lower() == expected.strip().lower():
verdict = "PASS"
else:
try:
query_vec = rag_agent.embeddings.embed_query(user_query)
answer_vec = rag_agent.embeddings.embed_query(answer)
similarity = rag_agent.embeddings.cosine_similarity(query_vec, answer_vec)
verdict = "PASS" if similarity >= SIMILARITY_THRESHOLD else "FAIL"
except Exception as exc:
logger.warning(f"Similarity check failed: {exc}")
verdict = "FAIL"
return {"verdict_row": verdict, "answer": answer, "expected": expected}
vector_store = QdrantVectorStore(embeddings)
agent = create_agent(vector_store)
result = agent.run("What is LangChain?")
```
**Ограничения**
- При отсутствии `OPENAI_API_KEY` используется `FakeEmbeddings`, у которых нет метода `cosine_similarity`. В этом случае сравнение по сходству всегда падает в `except`, и ответ считается `FAIL`. Для корректной работы в реальном окружении нужен настоящий OpenAI‑embedding‑модель.
- Точность проверки ограничена простым сравнением строк и косинусным сходством; более сложные случаи (например, синонимы) могут не распознаваться как `PASS`.
Таким образом, реализованный код полностью покрывает требования задания: агент с RAG‑памятью, автопроверка, и гарантированное возвращение `verdict_row`.
**Honest limitations**
- The solution assumes a running Qdrant instance on `localhost:6333` and an Ollama model named `llama3` available locally.
- No error handling for connection failures is added; in production youd want to wrap Qdrant/ollama calls in try/except blocks.
- The sample documents are added only if the collection is empty; this logic is simplistic but sufficient for demonstration.