RAG Solutions
Knowledge Check Answers
-
RAG adds relevant documents to the prompt at query time — no training needed, easy to update. Fine-tuning retrains the model on your data — expensive, slow, but changes model behavior and tone.
-
Chunking keeps each embedding focused on a single topic. A long document about "Tutoring, Financial Aid, and Counseling" would produce a muddy embedding. Chunking splits it so "Tutoring" gets its own vector, "Financial Aid" gets its own, etc.
-
Cosine similarity measures the angle between two vectors. 1.0 = same direction (similar meaning), 0.0 = unrelated, -1.0 = opposite.
-
384 dimensions is a balance between accuracy and speed. More dimensions capture more nuance but use more storage and compute. 384 works well for sentence-level search.
-
The AI falls back to its general knowledge. The RAG system still works — it just won't have relevant context. The prompt instructs the AI to say if the context doesn't help.
Challenge 1 Solution
# Switch to all-mpnet-base-v2 (768 dimensions)
MODEL_NAME = "all-mpnet-base-v2"
embedder = SentenceTransformer(MODEL_NAME)
# Update Document model
from pgvector.sqlalchemy import Vector
class Document(db.Model):
...
embedding = db.Column(Vector(768), nullable=False)
# Re-index
db.drop_all()
db.create_all()
index_documents(db, Document)
Challenge 2 Solution
from sqlalchemy import text
def hybrid_search(db, query, table, top_k=5):
query_vec = embed_text(query)
# Vector search
vector_results = (
db.session.query(table, (1 - table.embedding.cosine_distance(query_vec)).label("score"))
.order_by(table.embedding.cosine_distance(query_vec))
.limit(top_k * 2)
.all()
)
# Keyword search
sql = text("""
SELECT *, ts_rank(to_tsvector('english', content),
plainto_tsquery('english', :query)) as score
FROM document
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', :query)
ORDER BY score DESC
LIMIT :limit
""")
kw_results = db.session.execute(sql, {"query": query, "limit": top_k * 2}).fetchall()
# Merge with weights
seen = set()
merged = []
for r in vector_results:
seen.add(r[0].id)
merged.append((r[0], r.score * 0.7))
for r in kw_results:
weight = r.score * 0.3 if r.id not in seen else 0
merged.append((r, weight))
merged.sort(key=lambda x: x[1], reverse=True)
return merged[:top_k]
Challenge 3 Solution
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def search_with_rerank(db, query, table, top_k=20, rerank_k=5):
results = search_similar_chunks(db, query, table, top_k=top_k)
pairs = [(query, r["content"]) for r in results]
scores = reranker.predict(pairs)
for r, s in zip(results, scores):
r["rerank_score"] = float(s)
results.sort(key=lambda r: r["rerank_score"], reverse=True)
return results[:rerank_k]
Challenge 4 Solution
import requests
from bs4 import BeautifulSoup
def scrape_clark_news():
url = "https://www.clark.edu/news-and-media/24-7/"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
articles = []
for item in soup.select(".story-item"):
title_el = item.select_one("h3 a")
if not title_el:
continue
title = title_el.get_text(strip=True)
url_path = title_el.get("href", "")
excerpt_el = item.select_one(".description")
excerpt = excerpt_el.get_text(strip=True) if excerpt_el else ""
articles.append({
"title": title,
"content": excerpt,
"source": f"https://www.clark.edu{url_path}",
"category": "news",
})
return articles
Challenge 5 Solution
Modify the /api/rag-chat endpoint to include conversation history:
@api.route("/rag-chat", methods=["POST"])
def rag_chat():
data = request.get_json()
user_message = data.get("message", "")
if not user_message:
return jsonify({"error": "Message is required"}), 400
user_msg = Message(content=user_message, sender="user")
db.session.add(user_msg)
db.session.commit()
try:
results = search_similar_chunks(db, user_message, Document, top_k=5)
context = build_rag_context(results)
recent = Message.query.order_by(Message.timestamp.desc()).limit(6).all()
recent.reverse()
messages = [{"role": "system", "content": "You are a helpful assistant for Clark College. Use the context to answer."}]
if context:
messages.append({"role": "system", "content": f"Context:\n{context}"})
for msg in recent[:-1]:
role = "user" if msg.sender == "user" else "assistant"
messages.append({"role": role, "content": msg.content})
messages.append({"role": "user", "content": user_message})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
ai_reply = response.choices[0].message.content
ai_msg = Message(content=ai_reply, sender="ai")
db.session.add(ai_msg)
db.session.commit()
return jsonify({
"user_message": user_msg.to_dict(),
"ai_message": ai_msg.to_dict(),
"evidence": [{"title": r["title"], "similarity": round(r["similarity"], 3)} for r in results],
})
except Exception as e:
return jsonify({"error": str(e)}), 500