【独立开发日记 006】Game-Odyssey 技术拆解:如何用 RAG + pgvector 构建语义搜索

以 Game Odyssey 为例,详解 RAG 系统的设计与实现。
什么是 RAG?
RAG(Retrieval-Augmented Generation,检索增强生成)是目前 AI 应用最主流的架构之一。
核心思想很简单:
先检索,再生成
LLM 的知识有截止日期,也不可能知道你的私有数据。RAG 的解决方案是:用户提问时,先从知识库检索相关内容,再把检索结果和问题一起喂给 LLM,让它基于这些上下文生成回答。
Game Odyssey 的 RAG 架构
关键技术点
1. Embedding:把文字变成向量
Embedding 是 RAG 的基础。它把文字映射到高维向量空间,语义相似的文字在空间中距离更近。
我用的是 Qwen3-Embedding-4B,通过 MLX 在 Mac 上本地运行:
# 生成 embedding
response = await client.post("/embed", json={"texts": [query]})
query_vector = response.json()["embeddings"][0] # 2560 维向量为什么选这个模型?
- 中文效果好(游戏数据大量中文)
- MLX 在 Apple Silicon 上性能优秀
- 2560 维够用,不会太慢
2. pgvector:PostgreSQL 的向量扩展
pgvector 让 PostgreSQL 支持向量类型和相似度搜索:
-- 创建向量列
CREATE TABLE game_embeddings (
game_id INTEGER PRIMARY KEY,
embedding_vector vector(2560)
);
-- 向量相似度搜索(余弦距离)
SELECT game_id,
1 - (embedding_vector <=> query_vector) as similarity
FROM game_embeddings
ORDER BY embedding_vector <=> query_vector
LIMIT 10;<=> 是余弦距离操作符,返回值越小越相似。
为什么用 pgvector 而不是 Pinecone、Milvus?
- 数据量不大(几千条游戏),pgvector 足够
- 减少技术栈复杂度,一个数据库搞定
- 免费、开源、易部署
3. Prompt 工程:让 LLM 听话
RAG 系统最头疼的问题是 LLM "自由发挥"。我遇到的坑:
问题 1:推荐了用户提到的游戏
用户: 推荐类似对马岛之魂的游戏
LLM: 我推荐对马岛之魂... // 不对!
解决:在 System Prompt 里明确约束
system_prompt = """你是游戏推荐助手。
【重要规则】
1. 只能推荐候选列表中的游戏
2. 如果用户提到"类似 XX",不要推荐 XX 本身
3. 推荐 3 款游戏并说明理由
"""问题 2:输出格式不稳定
有时 LLM 按格式输出,有时不按,导致解析失败。
解决:多层 fallback
# 1. 尝试解析格式化输出
if "---游戏ID---" in response:
ids = parse_formatted_ids(response)
# 2. fallback: 从文字中匹配游戏名
if not ids:
for game in candidates:
if game.title in response:
ids.append(game.id)4. 本地 LLM:Ollama + Qwen2.5
Ollama 让本地运行 LLM 变得很简单:
# 安装
brew install ollama
# 启动服务
ollama serve
# 下载模型
ollama pull qwen2.5:3bAPI 调用:
response = await client.post("/api/chat", json={
"model": "qwen2.5:3b",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
})为什么用 3B 模型?
- Mac Pro M1 Pro 16GB 跑得动
- 响应速度 2-5 秒,可接受
- 中文理解能力够用