【独立开发日记 007】Game-Odyssey 踩坑复盘:那些想不到的 Bug

做 AI 应用最痛苦的不是技术难,而是各种意想不到的细节问题。
做 Game Odyssey 的过程中,遇到了不少让人抓狂的问题。这篇文章把最典型的几个坑分享出来,希望能帮到同样在做 RAG 应用的朋友。
坑 1:Ollama 连接失败
现象
启动后端,调用聊天 API,返回:
{"detail": "All connection attempts failed"}前端显示"抱歉,发生了错误。请稍后重试。"
排查
看后端日志:
httpx.ConnectError: All connection attempts failed
原来是连接 Ollama(localhost:11434)失败了。
原因
后端启动时,Ollama 服务没有运行。这在开发时经常发生——重启电脑后忘了启动 Ollama。
解决
1. 添加友好错误提示
except httpx.ConnectError as e:
error_msg = (
"无法连接到 AI 模型服务。"
"请确保 Ollama 服务正在运行。"
"启动命令: ollama serve"
)
raise Exception(error_msg)2. 前端显示具体错误
把后端的错误信息透传给用户,而不是笼统的"发生错误"。
教训
对外部依赖的连接失败,一定要给出明确的错误信息和解决建议。
坑 2:图片全挂了
现象
游戏卡片的图片全部显示不出来,控制台报 CORS 错误:
Access to image at 'https://img1.gamersky.com/xxx.jpg' from origin 'http://localhost:5173' has been blocked by CORS policy
原因
游戏图片来自外部域名(gamersky.com),浏览器的同源策略阻止了跨域请求。
解决
后端添加图片代理
@router.get("/images/proxy")
async def proxy_image(url: str):
# 验证 URL 在白名单域名
if not is_allowed_domain(url):
raise HTTPException(403, "不允许的图片域名")
# 代理请求图片
async with httpx.AsyncClient() as client:
response = await client.get(url)
return Response(
content=response.content,
media_type=response.headers.get("Content-Type")
)前端自动走代理
function getProxiedImageUrl(url: string): string {
if (needsProxy(url)) {
return `/api/v1/images/proxy?url=${encodeURIComponent(url)}`
}
return url
}教训
涉及外部资源时,CORS 是绑定会遇到的问题。要么让资源方配置 CORS,要么自己做代理。
坑 3:推荐结果与卡片不一致
现象
用户问"推荐类似对马岛之魂的游戏":
- 文字推荐:原神、巫师3、地平线
- 卡片显示:对马岛之魂、原神、巫师3
卡片里居然有用户提到的"对马岛之魂"!
原因
这是两个问题叠加:
问题 A:卡片显示的是向量检索结果,不是 LLM 推荐的
向量检索找"最相似"的游戏,《对马岛之魂》和它自己当然最相似,所以排第一。
问题 B:LLM 推荐的游戏和卡片对不上
LLM 可能"自由发挥"推荐了检索结果之外的游戏。
解决
1. 检索更多,让 LLM 选
# 检索 10 个候选
candidates = await search_similar_games(query, limit=10)
# 让 LLM 从中选 3 个
prompt = f"""
候选游戏:{candidates}
用户需求:{query}
请选择 3 款最合适的推荐。
注意:如果用户提到"类似 XX",不要推荐 XX 本身。
"""2. 解析 LLM 返回的游戏 ID
让 LLM 按格式输出:
---游戏ID--- 123,456,789 ---游戏ID结束---
后端解析这些 ID,再返回对应的游戏信息。
3. 从用户问题中提取游戏名
def extract_game_titles_from_query(query: str) -> set:
"""提取用户问题中提到的游戏"""
excluded = set()
# 匹配《游戏名》格式
matches = re.findall(r'《([^》]+)》', query)
excluded.update(matches)
# 匹配"类似 XX"格式
matches = re.findall(r'类似([^\s,,。]+)', query)
excluded.update(matches)
return excluded4. 多层 fallback
如果 LLM 没按格式输出,就从文字中匹配游戏名。
教训
RAG 系统的"检索"和"生成"是两个独立过程,要确保最终展示的内容一致。LLM 的输出不可控,必须有 fallback 策略。
坑 4:LLM 输出带格式标记
现象
推荐文字里出现了 **ID:76** 这样的标记:
- ID:76 对马岛之魂 (Ghost of Tsushima):这款游戏...
原因
Prompt 里要求 LLM 输出游戏 ID,但它把 ID 也写进了推荐文字里。
解决
后处理时清理这些标记:
# 清除 ID 标记
response_text = re.sub(r'\*?\*?\[?ID:\d+\]?\*?\*?\s*', '', response_text)教训
LLM 的输出永远不可预测。任何格式要求都可能被"创造性"地执行。要在后处理时清理可能的噪音。
总结
做 RAG 应用的几个关键经验:
- 外部依赖要有降级方案:Ollama 没启动、网络不通,都要给用户明确提示
- CORS 问题要提前考虑:涉及外部资源就做代理
- 检索和生成要对齐:确保最终展示的内容一致
- LLM 输出要后处理:清理格式噪音、多层 fallback