Q&A через Search API и HyDE re-ranking
Строим RAG поверх внешнего Search API (NewsAPI): генерация вариантов запроса моделью, получение статей, re-ranking через HyDE (Hypothetical Document Embeddings), ответ с источниками.
Архитектура: Search → Re-rank → Answer
Вместо векторной БД используем внешний Search API. Пайплайн состоит из трёх шагов:
- Search — модель генерирует несколько вариантов запроса
- Re-rank — фильтруем по семантической близости через HyDE
- Answer — генерируем ответ с ссылками на источники
Шаг 1: Расширение запроса
from openai import OpenAI
import json
client = OpenAI()
GPT_MODEL = "gpt-3.5-turbo"
def json_gpt(prompt: str) -> dict:
response = client.chat.completions.create(
model=GPT_MODEL,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
USER_QUESTION = "Who won the NBA championship? Who was the MVP?"
QUERIES_INPUT = f"""Generate search queries for: {USER_QUESTION}
Format: {{"queries": ["q1", "q2", "q3"]}}"""
queries = json_gpt(QUERIES_INPUT)["queries"]
Шаг 2: HyDE Re-ranking
HyDE (Hypothetical Document Embeddings) — модель придумывает идеальный ответ, затем ищем статьи, похожие на этот гипотетический ответ, а не на вопрос.
from numpy import dot
def embeddings(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="text-embedding-ada-002", input=texts
)
return [d.embedding for d in response.data]
HA_INPUT = f"""Generate a hypothetical answer to: {USER_QUESTION}
Use placeholders like NAME did X. Format: {{"hypotheticalAnswer": "..."}}"""
hyp_answer = json_gpt(HA_INPUT)["hypotheticalAnswer"]
hyp_emb = embeddings([hyp_answer])[0]
article_embs = embeddings([f"{a['title']} {a['description']}" for a in articles])
scores = [dot(hyp_emb, e) for e in article_embs]
top_articles = sorted(zip(articles, scores), key=lambda x: x[1], reverse=True)[:5]
Шаг 3: Генерация ответа
formatted = [{"title": a["title"], "url": a["url"]} for a, _ in top_articles]
ANSWER_INPUT = f"Answer based on TOP_RESULTS: {formatted}\nQUESTION: {USER_QUESTION}"
stream = client.chat.completions.create(
model=GPT_MODEL,
messages=[{"role": "user", "content": ANSWER_INPUT}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
OpenAI embeddings возвращаются нормализованными, поэтому dot product == cosine similarity — дополнительная нормализация не нужна.
Реализуйте Q&A пайплайн поверх любого публичного Search API (например, Wikipedia API или DuckDuckGo). Запустите 3 вопроса. Сравните качество ответов с HyDE и без (прямой поиск по тексту вопроса).
Скопируйте и адаптируйте под свой контекст. Текст в треугольных скобках — то, что нужно заменить.
from openai import OpenAI
from numpy import dot
import json, requests
client = OpenAI()
def json_gpt(prompt):
r = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role":"user","content":prompt}],
response_format={"type":"json_object"},
)
return json.loads(r.choices[0].message.content)
def get_embedding(text):
return client.embeddings.create(
model="text-embedding-ada-002", input=[text]
).data[0].embedding
def hyde_rerank(question, articles):
hyp = json_gpt(f'Hypothetical answer to: {question}. Format: {{"hypotheticalAnswer":"..."}}')["hypotheticalAnswer"]
h_emb = get_embedding(hyp)
scored = [(a, dot(h_emb, get_embedding(a["title"]))) for a in articles]
return sorted(scored, key=lambda x: x[1], reverse=True)[:5]Искать по тексту вопроса напрямую — пользователи пишут вопросы, а хорошие ответы звучат иначе. HyDE решает это. Не дедуплицировать статьи из нескольких поисковых запросов — один и тот же материал занимает несколько слотов контекста.
Нормализованные OpenAI-эмбеддинги: dot product == cosine similarity, экономит вычисления. Генерируйте 5-10 вариантов запроса — охват значительно лучше одного запроса.
Ответы на вопросы по актуальным данным (новости, документация), где нет предзаполненной векторной БД. Аугментация любого существующего поискового движка.
Закрытые корпоративные данные без внешнего Search API. Когда нужна точная цитата — HyDE может выбрать семантически близкий, но фактически неверный источник.