本项目参考juejin.cn/post/751749… 本文用于记录笔记。
效果展示
项目架构
本项目采用前后端分离架构:
- 前端: Next.js 13 + React 18 + TypeScript
- 后端: Fastify + Node.js + TypeScript
- AI服务: 通义千问VL API
功能特性
- 📸 拍照识别: 使用相机拍照识别食物
- 📁 图片上传: 从相册选择图片进行识别
- 🤖 AI识别: 集成通义千问VL API进行智能识别
- 📊 嘌呤分析: 提供详细的嘌呤含量信息
- 💡 饮食建议: 根据嘌呤含量给出健康建议
- 🚀 高性能后端: 基于Fastify的高性能API服务
项目结构
project/
├── app/ # Next.js前端应用
│ ├── page.tsx # 主页面
│ ├── layout.tsx # 布局组件
│ └── test-camera/ # 相机测试页面
├── components/ # React组件
│ ├── ui/ # 基础UI组件
│ ├── CameraCapture.tsx # 相机拍照组件
│ ├── RecognitionPage.tsx # 识别页面
│ └── ...
├── lib/ # 工具库
│ └── recognition-api.ts # 识别API服务
├── backend/ # 后端服务
│ ├── src/
│ │ └── server.ts # Fastify服务器
│ ├── public/ # 静态文件
│ └── package.json # 后端依赖
├── package.json # 前端依赖
└── README.md # 项目说明
API端点
后端API (http://localhost:3001)
GET /health- 健康检查GET /api/info- 服务器信息POST /api/recognize- 食物识别POST /api/upload- 图片上传GET /api/foods- 食物列表
前端页面 (http://localhost:3000)
/- 主应用页面/test-camera- 相机功能测试页面
使用说明
拍照识别
- 点击"拍照识别"按钮
- 允许浏览器访问相机
- 将食物放在取景框内
- 点击拍照按钮
- 确认照片后开始识别
图片上传
- 点击"从相册选择"按钮
- 选择要识别的食物图片
- 点击"开始识别"按钮
技术栈
前端
- 框架: Next.js 13 + React 18
- UI组件: Radix UI + Tailwind CSS
- 相机功能: react-webcam
- 状态管理: Zustand
- 类型安全: TypeScript
后端
- 框架: Fastify
- 语言: TypeScript
- 插件: CORS, Multipart, Static
- 日志: Pino
- 开发: Nodemon + ts-node
一、数据准备
数据来自国家卫生健康委办公厅关于印发成人高尿酸血症与痛风食养指南(2024年版)等4项食养指南的通知
下载PDF并使用MinerU将PDF转化为markdown。接着让ai根据 @成人高尿酸血症与痛风食养指南 (2024 年版).md 的食物嘌呤数据和需求文档@prd.md里面的食物数据结构,生成一份数据,并获取对应的 image 图片,保存在 imgs 目录下,同时生成foods.json文件。
二、前端
前端开发使用Bolt.new这个工具,用GitHub登录,将需求文档复制到对话框内,就可以得到我们想要的前端,如果觉得页面不好看,可以接着让bolt继续美化。
将代码同步到GitHub该项目的仓库中,再使用git clone将代码拉到本地,在终端安装node.js环境。接着输入指令npm install和npm run dev,得到前端链接。
三、后端
后端选用大模型服务平台百炼控制台阿里云的通义千问模型。
点击API参考再点击左侧的通义千问获取一个API-key
将通义千问这个页面保存为qwen.md,让ai参考生成后端代码保存在backend目录下,创建一个基于fastify框架的server,接入图像理解能力。
要求:
- 现在在 @/backend 的后端服务器环境中调用ai能力,
- 使用 .env 文件保存API_KEY,并使用环境变量中的DASHSCOPE_API_KEY.并且.env文件不能提交到git上,提交到git的可以用.env.example文件作为举例供供用户参考
- 要求使用openai的sdk,并且前端上传base64的图片
- 后端返回值要求返回json格式,返回的数据能够渲染识别结果中的字段,包括:食物/嘌呤值/是否适合高尿酸患者/食用建议/营养成分估算
- 在 @/backend 目录下创建 api.md 文件,记录后端接口文档
四、联调
前端的拍照图片是用的模拟的接口,没有真正的调用后端的接口,所以需要换成真正的后端接口。
前端修改需求说明
-
后端URL环境变量配置
- 在项目根目录添加环境变量配置,区分开发和生产环境
- 开发环境默认使用
http://localhost:3000/api - 生产环境通过环境变量
REACT_APP_API_BASE_URL动态获取后端地址 - 前端服务需根据环境变量自动切换请求的API地址
-
食物识别接口适配
-
根据
@api.md文档规范调整食物识别接口 -
修改
@identify-page.tsx中的请求逻辑,确保符合以下要求:- 请求方法:POST
- 数据格式:multipart/form-data
- 请求路径:
/food/identify - 响应数据结构适配(包含食物名称、置信度、热量及营养成分)
-
五、部署
租一个最基础的腾讯云服务器来获取公网IP。
将服务器重装系统,重装为Ubuntu,设置密码(记住用户名和密码用于登录putty,用户名一般为ubuntu),在本地安装putty。
此处填入公网ip,点击open。在本地上下载winscp,用于从本地上传文件到服务器,打开winscp连接上服务器后,将该project上传到ubuntu目录下
在服务器中进入/home/ubuntu/project,输入Linux命令下载node.js
# 1. 下载 Node.js 官方 LTS 版本(适用于大多数 Linux 发行版)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
# 2. 安装 Node.js 和 npm
sudo apt-get install -y nodejs
安装完成后验证:
bash
node -v # 检查 Node.js 版本
npm -v # 检查 npm 版本
下载成功后输入命令
npm install
npm run build
npm start
打开服务器防火墙,端口3000(自定义)
在浏览器中输入http://公网IP:3000 就可以进入到嘌呤识别助手的页面了。
六、扩展为小程序
该项目也可以继续做成一个微信小程序
在根目录下创建一个miniprogram文件夹
miniprogram 文件夹,包含:
- 首页:美观的启动页面,展示功能特性
- web-view页面:嵌入你的H5应用
- 配置文件:小程序必需的配置文件
问题
微信小程序的 web-view 组件有严格的访问限制。微信小程序的 web-view 只能访问已配置的业务域名,而且必须是 HTTPS 协议,不能是 HTTP。所以服务器上运行出来的http://公网IP:3000 是不行的,要转化为https。
解决方法:
在Linux服务器上安装ngrok
请在你的Linux服务器上执行以下命令:
1. 添加ngrok仓库
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
2. 安装ngrok
sudo apt update && sudo apt install ngrok
3. 注册ngrok账号(免费)
- 访问 ngrok.com/
- 注册一个免费账号
- 获取你的authtoken
4. 配置authtoken
ngrok config add-authtoken 你的authtoken
5. 启动隧道
ngrok http 3000
启动后的操作
ngrok启动后会显示类似这样的信息:
Forwarding https://abc123.ngrok.io -> http://localhost:3000
然后需要:
- 复制这个HTTPS地址(如:abc123.ngrok.io)
- 修改小程序配置:
- 在 miniprogram/app.js 中修改 serverUrl
- 在微信小程序后台配置业务域名
- 测试访问:
- 在手机浏览器中访问这个HTTPS地址
- 确认能正常打开你的H5页面
接下来:
1. 注册并获取AppID
- 在微信公众平台注册小程序账号,完成信息填写。
- 进入“开发管理”->“开发设置”,复制你的AppID。
2. 配置小程序项目
- 在你的项目 miniprogram/project.config.json 文件中填入AppID。
- 在 miniprogram/app.js 中,把 serverUrl 改成你的 H5 公网 HTTPS 地址(如:abc123.ngrok.io)。
3. 配置业务域名(企业小程序才有)
- 登录微信公众平台,进入“开发管理”->“开发设置”。
- 在“业务域名”处添加你的 H5 域名(如:abc123.ngrok.io)。
- 注意:个人小程序没有“业务域名”功能,只能用体验版测试,不能正式上线web-view。
4.在服务器上启动前端H5服务
5. 本地开发与测试
-
下载并安装微信开发者工具
-
用微信开发者工具导入 miniprogram 文件夹,填入AppID。
- 点击“编译”,在开发者工具中预览和调试。
- 用手机微信扫码开发者工具的二维码,体验小程序。
6. 上传代码
- 在微信开发者工具中,点击“上传”按钮,填写版本号和备注,上传代码到微信后台。
7. 提交审核
8. 审核通过后发布
七、大模型微调
该项目的后端使用的是阿里云通义千问的视觉语言模型,具体配置如下:
主要模型
- 默认模型: qwen-vl-plus
- API类型: 阿里云通义千问VL(视觉语言模型)
对于该模型识别不准确的情况,有两个方案可以使其识别准确。
方案一:使用Few-shot Learning
什么是Few-shot Learning?
Few-shot Learning(少样本学习)是一种通过提供少量示例来让AI模型学习的技术,无需修改模型参数,通过精心设计的提示词来提升效果。
工作原理
1. 传统方法 vs Few-shot Learning
# 传统方法:
用户:识别这张图片
AI:这是苹果
# Few-shot Learning:
系统:先给AI看几个示例
示例1:这是苹果,低嘌呤,1.2mg/100g
示例2:这是三文鱼,高嘌呤,250mg/100g
示例3:这是菠菜,中嘌呤,80mg/100g
用户:识别这张图片
AI:这是苹果,低嘌呤,1.2mg/100g(参考了示例)
2. 核心优势
✅ 无需微调 - 不修改模型参数
✅ 成本低 - 只需要API调用费用
✅ 速度快 - 立即可用
✅ 效果好 - 可以提升10-30%准确率
✅ 易维护 - 只需要更新示例
实施步骤
1. 准备示例数据
创建高质量示例
# 示例格式:
{
"name": "苹果",
"image": "苹果.jpg",
"purine_level": "low",
"purine_content": "1.2mg/100g",
"description": "苹果是一种常见的水果,富含维生素C和膳食纤维。",
"advice": "高尿酸患者可以适量食用,建议每天1-2个。",
"nutrition": {
"calories": "52kcal/100g",
"protein": "0.3g/100g",
"fat": "0.2g/100g",
"carbohydrates": "14g/100g",
"fiber": "2.4g/100g"
}
}
示例选择策略
# 1. 相似度选择(推荐)
- 根据目标食物选择相似示例
- 水果类选择水果示例
- 海鲜类选择海鲜示例
# 2. 随机选择
- 随机选择几个示例
- 适合通用场景
# 3. 手动选择
- 根据经验选择最佳示例
- 适合特定场景
2. 构建提示词
系统提示词
system_prompt = """你是一个专业的食物营养分析专家,专门为高尿酸患者提供食物建议。
请分析用户上传的食物图片,并提供以下信息:
1. 准确识别食物名称
2. 评估嘌呤含量等级(高/中/低)
3. 提供具体的嘌呤含量数值
4. 判断是否适合高尿酸患者食用
5. 给出详细的食用建议
6. 估算营养成分
## 嘌呤含量评估标准:
- 高嘌呤(high): >150mg/100g - 痛风患者应避免
- 中嘌呤(medium): 50-150mg/100g - 痛风患者应限制
- 低嘌呤(low): <50mg/100g - 痛风患者可适量食用
请严格按照JSON格式返回结果。"""
示例提示词
example_prompt = """示例:识别这张图片中的食物
<image>
data:image/jpeg;base64,{image_base64}
</image>
用户:请识别这张图片中的食物,并提供嘌呤含量信息。
助手:这是苹果,属于低嘌呤食物,每100g含有1.2mg嘌呤。
苹果是一种常见的水果,富含维生素C和膳食纤维。
高尿酸患者可以适量食用,建议每天1-2个。
营养成分(每100g):
- 热量:52kcal/100g
- 蛋白质:0.3g/100g
- 脂肪:0.2g/100g
- 碳水化合物:14g/100g
- 膳食纤维:2.4g/100g"""
3. 运行Few-shot Learning
基本使用
# 1. 设置环境变量
export DASHSCOPE_API_KEY="your_api_key_here"
# 2. 运行脚本
python few_shot_learning.py
配置参数
@dataclass
class FewShotConfig:
# API配置
api_key: str = "your_api_key"
model: str = "qwen-vl-plus"
# Few-shot配置
max_examples: int = 5 # 最多使用多少个示例
example_selection_method: str = "similarity" # 示例选择方法
# 超参数
temperature: float = 0.7 # 控制输出随机性
max_tokens: int = 1000 # 最大输出长度
top_p: float = 0.9 # 控制输出多样性
优化策略
1. 示例质量优化
选择高质量示例
# 好的示例特征:
✅ 图片清晰 - 食物特征明显
✅ 信息完整 - 包含所有必要信息
✅ 格式一致 - 输出格式统一
✅ 覆盖全面 - 涵盖不同食物类别
✅ 数据准确 - 营养信息准确
示例数量优化
# 示例数量建议:
- 最少:3个示例
- 推荐:5-8个示例
- 最多:10个示例(避免token超限)
2. 提示词优化
结构优化
# 提示词结构:
1. 系统角色定义
2. 任务要求说明
3. 输出格式规范
4. 参考示例
5. 目标问题
格式优化
# 输出格式规范:
{
"foodName": "食物名称",
"purineLevel": "high|medium|low",
"purineContent": "具体数值",
"suitableForGout": true|false,
"advice": "食用建议",
"nutritionEstimate": {
"calories": "热量",
"protein": "蛋白质",
"fat": "脂肪",
"carbohydrates": "碳水化合物",
"fiber": "膳食纤维"
},
"confidence": 0.95
}
3. 参数调优
Temperature调优
# Temperature参数:
- 0.1-0.3: 输出更确定,适合精确任务
- 0.4-0.7: 平衡确定性和创造性
- 0.8-1.0: 输出更多样,适合创意任务
# 食物识别推荐:0.5-0.7
Top_p调优
# Top_p参数:
- 0.1-0.5: 输出更保守
- 0.6-0.9: 平衡保守和多样
- 0.9-1.0: 输出更多样
# 食物识别推荐:0.8-0.9
实际应用
1. 集成到现有系统
修改AI服务
// 在 ai-service.ts 中集成Few-shot Learning
export class AIService {
private fewShotLearner: FewShotLearner;
constructor() {
this.fewShotLearner = new FewShotLearner(config);
}
async recognizeFood(base64Image: string): Promise<FoodRecognitionResult> {
// 使用Few-shot Learning
const result = await this.fewShotLearner.recognize(base64Image);
return result;
}
}
配置环境变量
# .env 文件
DASHSCOPE_API_KEY=your_api_key_here
FEW_SHOT_ENABLED=true
FEW_SHOT_EXAMPLES=5
FEW_SHOT_METHOD=similarity
2. 性能监控
监控指标
# 关键指标:
- API调用成功率
- 平均响应时间
- 识别准确率
- 用户满意度
- API调用成本
日志记录
# 记录内容:
- 输入图片信息
- 选择的示例
- API响应结果
- 处理时间
- 错误信息
常见问题
1. Token超限
# 解决方案:
- 减少示例数量
- 简化示例内容
- 压缩图片质量
- 优化提示词长度
2. 识别不准确
# 解决方案:
- 增加相关示例
- 优化提示词
- 调整参数
- 收集更多数据
3. 响应时间慢
# 解决方案:
- 减少示例数量
- 优化图片大小
- 使用缓存
- 并行处理
总结
Few-shot Learning是一种高效、低成本、易维护的模型优化方法,适合此食物识别项目
few_shot_learning.py伪代码:
# 1. 配置模块
CLASS FewShotConfig:
api_key: string = null
base_url: string = "https://dashscope.aliyuncs.com/compatible-mode/v1"
model: string = "qwen-vl-plus"
foods_data_path: string = "../data/foods.json"
img_dir: string = "../public/imgs"
max_examples: int = 5
example_selection_method: string = "similarity" # [similarity|random|manual]
output_dir: string = "./few_shot_output"
temperature: float = 0.7
max_tokens: int = 1000
top_p: float = 0.9
# 2. 核心处理模块
CLASS ExampleSelector:
METHOD __init__(config):
self.config = config
self.examples = LOAD_HARDCODED_EXAMPLES()
METHOD select_examples(target_food):
IF config.method == "random":
RETURN RANDOM_SAMPLE(examples, max_examples)
ELIF config.method == "similarity":
RETURN FIND_SIMILAR_FOODS(target_food, examples)
ELSE:
RETURN FIRST_N(examples, max_examples)
CLASS PromptBuilder:
METHOD __init__(config):
self.selector = ExampleSelector(config)
METHOD build_prompt(target_image_base64, target_food):
examples = selector.select_examples(target_food)
system_prompt = GENERATE_SYSTEM_INSTRUCTION()
example_texts = []
FOR example IN examples:
example_text = FORMAT_EXAMPLE(example)
example_texts.APPEND(example_text)
RETURN COMBINE_PROMPT(system_prompt, example_texts, target_image_base64)
CLASS FewShotLearner:
METHOD __init__(config):
self.config = config
self.prompt_builder = PromptBuilder(config)
INIT_OUTPUT_DIR()
METHOD recognize_food(image_path):
image_base64 = ENCODE_IMAGE_TO_BASE64(image_path)
prompt = prompt_builder.build_prompt(image_base64)
api_response = CALL_DASHSCOPE_API(prompt)
IF api_response.success:
RETURN PARSE_RESPONSE(api_response)
ELSE:
RETURN ERROR_RESULT()
METHOD batch_recognize(image_dir):
FOR image IN LIST_IMAGES(image_dir):
result = recognize_food(image)
SAVE_RESULT(result)
RETURN SUMMARY_RESULTS()
# 3. 预定义数据示例
FUNCTION LOAD_HARDCODED_EXAMPLES():
RETURN [
{
"name": "苹果",
"image": "apple.jpg",
"purine_level": "low",
"nutrition": {...}
},
... # 其他示例
]
# 4. 主流程
FUNCTION MAIN():
config = FewShotConfig()
learner = FewShotLearner(config)
# 单图测试
IF EXISTS(test_image):
result = learner.recognize_food(test_image)
LOG(result)
# 批量处理
results = learner.batch_recognize(config.img_dir)
SAVE_SUMMARY(results)
运行few_shot_learning.py
方案二:进行模型微调
由于阿里云的qwen-vl-plus模型是不支持下载的,所以选用支持下载到本地的 Qwen-VL-Chat模型。
Qwen-VL-Chat模型功能特点
✅ 多模态能力:图像+文本 ✅ 对话能力:支持问答交互 ✅ 中文理解:对中文支持优秀 ✅ 图像理解:可以识别和分析图片 ✅ 文本生成:可以输出详细分析
与qwen-vl-plus模型的相似度
功能对比:
当前模型(qwen-vl-plus)
├── 图像识别 ✅
├── 文本分析 ✅
├── 中文支持 ✅
├── 对话能力 ✅
└── 营养分析 ✅
Qwen-VL-Chat
├── 图像识别 ✅
├── 文本分析 ✅
├── 中文支持 ✅
├── 对话能力 ✅
└── 营养分析 ✅
本地Qwen-VL-Chat模型微调步骤
第一步:环境准备和依赖安装
1.1 安装必要的Python包
1.2 验证环境
- 检查CUDA是否可用
- 验证GPU内存(建议16GB+)
- 确认Python版本(3.8+)
第二步:数据准备和格式化
2.1 数据收集
- 使用您现有的食物图片库(../public/imgs/)
- 确保每张图片都有对应的食物信息
- 检查图片质量和格式(建议JPG/PNG)
2.2 数据标注格式
为每张图片创建标准化的标注,包含:
- 食物名称
- 嘌呤含量
- 健康建议
- 营养成分
- 适合高尿酸患者的程度
2.3 数据分割
- 训练集:80%的数据用于训练
- 验证集:10%的数据用于验证
- 测试集:10%的数据用于最终测试
2.4 数据格式转换
将数据转换为模型训练所需的格式:
- 图片路径
- 文本描述
- 目标输出格式
第三步:LoRA配置和模型准备
3.1 LoRA参数配置
- r值:LoRA的秩,控制适配器的大小(建议8-32)
- alpha值:缩放参数(通常设为2r)
- dropout:防止过拟合(建议0.1-0.3)
- target_modules:指定要微调的层(Qwen-VL的注意力层)
3.2 模型加载配置
- 加载预训练的Qwen-VL-Chat模型
- 设置数据类型(float16以节省内存)
- 配置设备映射(GPU/CPU分配)
- 启用梯度检查点以节省内存
3.3 分词器配置
- 加载对应的tokenizer
- 设置pad_token
- 配置最大长度限制
- 处理特殊标记
第四步:训练配置和参数设置
4.1 训练超参数
- 学习率:1e-4到5e-5(LoRA通常使用较小学习率)
- 批次大小:根据GPU内存调整(1-4)
- 训练轮数:3-10轮(避免过拟合)
- 权重衰减:0.01-0.1
- 梯度裁剪:1.0
4.2 优化器配置
- 使用AdamW优化器
- 设置学习率调度器
- 配置warmup步数
- 设置权重衰减
4.3 损失函数
- 使用标准的语言模型损失
- 只计算目标文本的损失
- 忽略输入部分的损失
第五步:开始LoRA微调
5.1 训练循环设置
- 初始化训练循环
- 设置验证频率
- 配置早停机制
- 设置模型保存策略
5.2 训练过程监控
- 实时显示训练损失
- 监控验证损失
- 跟踪学习率变化
- 记录GPU使用情况
5.3 检查点保存
- 定期保存模型检查点
- 保存最佳模型
- 记录训练日志
- 保存LoRA适配器权重
第六步:训练过程管理
6.1 训练监控指标
- 训练损失:应该逐渐下降
- 验证损失:防止过拟合
- 学习率:按调度器变化
- GPU内存:确保不溢出
6.2 过拟合检测
- 观察验证损失趋势
- 如果验证损失上升,考虑早停
- 调整正则化参数
- 增加训练数据
第七步:模型评估和优化
7.1 训练后评估
- 在测试集上评估模型
- 计算准确率、召回率等指标
- 对比微调前后的效果
- 分析错误案例
7.2 模型性能测试
- 测试推理速度
- 检查内存使用
- 验证输出质量
- 测试不同输入格式
7.3 模型优化
- 如果效果不理想,调整超参数
- 增加训练数据
- 优化数据预处理
- 调整LoRA配置
第八步:模型保存和部署准备
8.1 保存微调后的模型
- 保存完整的LoRA适配器
- 保存模型配置
- 保存tokenizer
- 创建模型信息文件
8.2 模型验证
- 测试保存的模型
- 验证功能完整性
- 检查输出质量
- 确认性能指标
第九步:集成到现有系统
9.1 替换API调用
- 修改现有的API调用代码
- 替换为本地模型推理
- 调整输入输出格式
- 更新错误处理
9.2 系统集成
- 更新服务配置
- 修改前端调用
- 调整日志记录
- 更新监控系统