开发日志(十二):RAG项目排查及开发成果
一、问题背景
在开发多模态菜单 RAG 系统时,我逐渐发现:
AI 项目最难排查的问题,往往不是模型算法本身,而是模型、依赖、环境、数据和前后端接口共同造成的链路问题。
整个项目包含如下环节:
Flutter 上传菜单
→ FastAPI 接收图片
→ Qwen 多模态识别
→ JSON 解析
→ Document 构造
→ Embedding
→ Chroma 入库
→ 用户偏好读取
→ 相似度检索
→ Prompt 拼接
→ LLM 生成
→ Flutter 展示
只要其中任意一个环节异常,最终表现出来的现象都可能是:
- 菜单上传失败;
- 页面一直加载;
- 菜品能够显示,但问答没有内容;
- 后端接口返回 500;
- 提示缺少 API Key;
- 提示找不到
chromadb; - 检索结果为空;
- 大模型生成与菜单无关的答案。
因此,我没有只围绕最终报错修改代码,而是按照整条调用链逐层排查。
二、问题一:API Key 名称不一致
1. 问题现象
项目运行时出现类似错误:
OPENAI_API_KEY is not set
但项目实际调用的是 Qwen 服务,并且已经配置了:
QWEN_API_KEY
这很容易让人产生疑问:
明明使用的是 Qwen,为什么程序还在找 OPENAI_API_KEY?
2. 原因分析
在 LangChain 生态中,一些兼容 OpenAI 协议的模型类默认读取:
OPENAI_API_KEY
即使底层供应商是 Qwen,只要使用了 OpenAI 兼容客户端,而初始化时没有显式传入 api_key,SDK 就可能继续读取默认环境变量。
另外,系统包含多个模型:
- 多模态识别模型;
- Embedding 模型;
- Chat 模型。
可能出现多模态模型正确读取了 QWEN_API_KEY,但 Embedding 模型仍然读取 OPENAI_API_KEY 的情况。
这就是典型的:
识别接口能运行
向量入库却失败
3. 解决方法
对每个模型都显式传递配置,而不是依赖 SDK 自动读取环境变量。
import os
QWEN_API_KEY = os.getenv("QWEN_API_KEY")
QWEN_BASE_URL = os.getenv("QWEN_BASE_URL")
if not QWEN_API_KEY:
raise RuntimeError("QWEN_API_KEY 未配置")
初始化聊天模型:
chat_model = ChatOpenAI(
api_key=QWEN_API_KEY,
base_url=QWEN_BASE_URL,
model="qwen-plus"
)
初始化 Embedding 模型:
embeddings = OpenAIEmbeddings(
api_key=QWEN_API_KEY,
base_url=QWEN_BASE_URL,
model="text-embedding-v3"
)
4. 排查经验
不能只检查“项目有没有 API Key”,而要检查:
每一个模型实例实际读取的是哪个环境变量
聊天模型能运行,不代表 Embedding 模型也已经正确配置。
三、问题二:虚拟环境与 Python 解释器不一致
1. 问题现象
已经通过 pip 安装了依赖,但启动项目时仍然报错:
ModuleNotFoundError: No module named 'chromadb'
或者:
ModuleNotFoundError: No module named 'langchain_chroma'
2. 原因分析
在 Windows 开发环境中,常见情况是:
- 在一个 Python 环境中安装依赖;
- 在另一个 Python 环境中启动服务;
- VS Code 选择的解释器与 PowerShell 中的解释器不同;
pip命令对应全局 Python;python命令对应虚拟环境,或者反过来。
所以“安装成功”并不意味着当前服务进程可以访问该依赖。
3. 检查当前解释器
where python
where pip
python -c "import sys; print(sys.executable)"
python -m pip --version
检查包是否安装在当前解释器中:
python -m pip show chromadb
python -m pip show langchain-chroma
与直接使用 pip install 相比,更推荐:
python -m pip install chromadb langchain-chroma
因为它可以保证依赖安装到当前 python 对应的环境中。
4. 正确激活虚拟环境
.\venv\Scripts\Activate.ps1
激活后终端前通常会显示:
(venv)
再执行:
python -m pip install -r requirements.txt
python -m uvicorn main:app --reload
5. 排查经验
出现依赖缺失时,不应立刻重复安装,而应先确认三个路径是否一致:
Python 解释器路径
pip 安装路径
项目实际启动路径
四、问题三:页面有菜品,但 RAG 检索为空
1. 问题现象
菜单上传成功,Flutter 页面也可以正常显示识别结果。
但进入结果页提问时,后端返回:
没有找到相关菜单内容
或者模型只能给出泛化回答。
2. 原因分析
这类问题说明菜单展示链路正常,但知识库链路没有完成。
可能原因包括:
- 识别完成后没有调用向量入库;
- 入库函数出现异常但被忽略;
documents数组为空;- 入库和检索使用了不同的 Chroma 路径;
- 入库时的
menu_id与问答时不一致; - 服务重启后使用了临时向量库;
- Embedding 模型调用失败;
- 检索过滤条件错误。
3. 增加分阶段日志
我在关键节点增加了日志:
print(f"识别菜品数量: {len(items)}")
print(f"构造文档数量: {len(documents)}")
print(f"当前菜单 ID: {menu_id}")
print("开始执行向量入库")
print("向量入库完成")
检索时输出:
print(f"用户问题: {question}")
print(f"检索菜单 ID: {menu_id}")
print(f"检索结果数量: {len(results)}")
通过这种方式,可以判断问题具体发生在:
识别阶段
JSON 解析阶段
Document 构造阶段
Embedding 阶段
Chroma 写入阶段
检索阶段
4. 保证上传后自动入库
菜单处理接口应明确执行:
items = await parse_menu(image)
documents = build_documents(items, menu_id)
vector_store.add_documents(documents)
不能只返回:
return {"items": items}
5. 统一持久化路径
避免不同文件使用不同相对路径:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
CHROMA_DIR = BASE_DIR / "chroma_db"
所有向量服务均引用同一个 CHROMA_DIR。
6. 排查经验
“页面能显示菜单”只能说明图片识别成功,并不能证明 RAG 可以使用。
完整成功标准应该同时包括:
识别成功
结构化成功
入库成功
检索成功
生成成功
五、问题四:旧接口与新接口同时存在
1. 问题现象
后端已经开发了新的菜单处理接口:
/api/v1/menu/process
但 Flutter 仍然调用旧接口:
/upload
结果是菜单图片可能上传成功,但不会进入新的多模态识别和向量入库逻辑。
2. 解决方法
统一检查 Flutter 中所有上传地址,包括:
- 首页上传;
- 相册选择;
- 拍照上传;
- 重新识别;
- 测试页面;
- Service 层封装。
将其统一切换为:
Uri.parse('$baseUrl/api/v1/menu/process')
同时检查请求字段名称是否与后端一致:
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/api/v1/menu/process'),
);
request.files.add(
await http.MultipartFile.fromPath(
'file',
imagePath,
),
);
如果后端参数名为 file,前端也必须使用 file,不能继续使用旧字段名 image。
3. 排查经验
接口升级不能只修改后端路由,还必须同步检查:
请求地址
请求方法
字段名称
鉴权 Header
响应数据结构
异常状态码
六、问题五:后端返回成功,但 Flutter 解析失败
1. 问题现象
后端日志显示 HTTP 200,Flutter 却提示:
FormatException
或者结果页没有数据。
2. 原因分析
常见原因包括:
- 后端返回字段由
data改成items; tags有时是数组,有时是字符串;price有时是数字,有时是字符串;- 中文响应没有通过 UTF-8 解码;
- 某些字段为
null; - 前端模型类仍按照旧接口定义解析。
3. 后端统一返回结构
{
"success": true,
"menu_id": "menu_xxx",
"items": [],
"message": "菜单识别成功"
}
4. Flutter 做兼容解析
final data = jsonDecode(
utf8.decode(response.bodyBytes),
);
final rawItems = data['items'];
if (rawItems is! List) {
throw const FormatException('items 字段格式错误');
}
菜品模型中为可缺失字段设置默认值:
factory MenuItem.fromJson(Map<String, dynamic> json) {
final rawTags = json['tags'];
return MenuItem(
nameOriginal: json['name_original']?.toString() ?? '',
nameZh: json['name_zh']?.toString() ?? '',
description: json['description']?.toString() ?? '',
price: json['price']?.toString() ?? '',
tags: rawTags is List
? rawTags.map((e) => e.toString()).toList()
: <String>[],
);
}
5. 排查经验
AI 模型生成的数据可能不稳定,因此前后端都要具备容错能力。
后端负责尽量标准化,前端负责避免单个异常字段造成整个页面崩溃。
七、问题六:模型回答出现菜单外内容
1. 问题现象
用户询问菜单推荐时,模型回答了检索结果中不存在的菜品。
2. 原因分析
可能存在以下问题:
- Prompt 没有限制回答范围;
- 检索结果为空时仍然调用模型;
- 菜单上下文与用户问题之间界限不清楚;
- top-k 设置过小,没有检索到足够信息;
- 没有将菜名、价格等关键字段写入 Document;
- 模型过度依赖自身知识。
3. 强化 Prompt 约束
你只能根据“当前菜单检索结果”回答。
禁止:
1. 编造菜单中不存在的菜品;
2. 编造价格;
3. 编造未提供的配料;
4. 使用其他餐厅或通用菜单知识替代当前菜单。
如果检索结果无法回答,请直接说明:
“当前菜单信息不足,无法确定。”
4. 检索为空时直接兜底
if not documents:
return {
"answer": "当前菜单中没有检索到足够的信息,暂时无法回答这个问题。",
"sources": []
}
不要在没有上下文的情况下继续让模型自由生成。
5. 排查经验
降低幻觉不能只依赖一句“不要编造”,而需要同时控制:
数据入库质量
检索结果质量
Prompt 约束
空结果处理
输出校验
八、问题七:用户偏好与推荐结果没有真正关联
1. 问题现象
用户已经设置“不吃辣”或“对花生过敏”,但系统仍然推荐相关菜品。
2. 原因分析
最初实现中,用户偏好可能只被显示在前端,没有真正传入后端;或者已经传入后端,但仅作为普通描述放在 Prompt 末尾,优先级不够。
3. 对偏好进行分级
我将偏好分成两类。
强约束
- 过敏原;
- 宗教饮食限制;
- 明确忌口;
- 素食或纯素要求。
弱偏好
- 喜欢辣;
- 喜欢清淡;
- 偏好甜食;
- 价格倾向;
- 菜品类型偏好。
在 Prompt 中要求模型优先处理强约束:
安全规则:
1. 过敏原和明确饮食限制的优先级最高;
2. 不得主动推荐可能违反强约束的菜品;
3. 菜品配料不明确时,应提示用户向餐厅确认;
4. 一般口味偏好只能在满足安全限制后考虑。
4. 排查经验
个性化推荐不是把用户资料简单附加在问题后面,而是需要建立明确的决策优先级。
九、建立分层排查方法
经过多次调试后,我将整个系统拆成五层进行验证。
第一层:运行环境
检查:
虚拟环境是否激活
Python 解释器是否正确
依赖是否安装
环境变量是否加载
模型服务地址是否正确
第二层:模型调用
分别测试:
多模态模型是否能识别图片
Embedding 是否能生成向量
Chat 模型是否能返回文字
不能只测试其中一个模型。
第三层:数据处理
检查:
模型输出是否为合法 JSON
字段是否统一
Document 是否为空
metadata 是否包含 menu_id
第四层:向量数据库
检查:
Chroma 是否成功初始化
写入数量是否正确
持久化路径是否统一
相似度检索是否有结果
过滤条件是否正确
第五层:前后端联调
检查:
Flutter 请求地址是否正确
HTTP 方法是否正确
token 是否携带
字段名称是否匹配
响应结构是否一致
异常是否被捕获
这种分层方法比直接盯着最终错误信息更有效,因为它可以快速确定故障所在范围。
十、最终实现结果
完成上述问题修复后,项目实现了完整业务闭环。
1. 菜单上传与识别
用户可以在 Flutter 客户端选择菜单图片并上传。
后端通过 Qwen 多模态模型识别菜名、中文翻译、价格、描述和标签。
2. 菜单自动入库
识别结果经过标准化处理后,被转换为 LangChain Document,并通过 Embedding 写入 Chroma。
3. 结果页真实问答
用户可以直接针对当前菜单提问,例如:
哪一道菜不辣?
有没有素食主食?
我对花生过敏,可以吃哪些菜?
帮我推荐一道价格较低的菜。

4. 用户偏好融合
后端读取用户的过敏原、忌口和口味偏好,将其与菜单检索结果共同传入 Prompt。
5. 回答范围受到限制
系统要求模型只根据当前菜单回答,检索结果不足时明确提示,降低菜单外幻觉。
6. 前端异常兜底
Flutter 页面增加了:
- 加载状态;
- 请求失败提示;
- 空结果提示;
- 重复点击限制;
- 网络异常处理;
- 模型服务不可用提示。
十一、项目成果总结
最终系统不再是彼此独立的几个模型演示,而是一套可以实际运行的菜单智能助手。
项目完成了以下技术整合:
多模态图片理解
+ 结构化信息抽取
+ LangChain 文档构建
+ Embedding 向量化
+ Chroma 持久化
+ RAG 检索增强问答
+ 用户偏好融合
+ FastAPI 接口编排
+ Flutter 移动端交互
整个开发过程让我认识到:
RAG 系统的核心难点,并不只是“检索加生成”,而是确保数据能够稳定地从输入端流动到输出端。
只有当图片识别、JSON 解析、向量入库、知识检索、Prompt 构造、模型生成和前端展示全部连通时,这个功能才算真正完成。
这也是本项目最大的工程价值:它将多模态模型、向量数据库和移动端业务系统组合成了一条真实可用的 AI 应用链路,而不是停留在单个接口或实验脚本阶段。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)