1. 为什么我要折腾一套拍照解题系统
拍照解题这个需求,最早是我家孩子上初中之后冒出来的。每天晚上做作业,遇到不会的题就喊我,我过去一看,有些题我还能讲,有些题我自己都得想半天。后来我就琢磨,能不能做一个东西,拍张照片,题目自动识别出来,然后给出解题思路和答案。市面上确实有这类App,但要么广告多,要么答案质量参差不齐,要么就是收费贵得离谱。作为一个搞技术的人,我第一反应就是自己搭一套。
这套系统的核心链路其实不复杂:拍照上传 → OCR文字识别 → 大模型理解题目并解答 → 返回结构化结果。听起来简单,但真要做稳定、做可用,中间有不少坑。我选择的技术栈是DeepSeek + Dify,原因后面会详细说。DeepSeek负责题目理解和解答,Dify负责编排整个工作流,OCR环节我用的是PaddleOCR做本地识别,整体跑在一台自己家里的服务器上。
这篇文章我会把整个实验过程完整拆开,从架构设计、环境准备、OCR接入、Dify工作流编排、DeepSeek提示词调优,到实际踩过的坑和排查方法,全部讲清楚。适合有一定动手能力、想自己搭一套智能解题系统的朋友参考。不需要你是AI专家,但基本的Linux操作和Docker使用得会。
注意:本文涉及的所有操作均在自有设备上完成,使用的模型和工具均为公开可获取的开源或商业API服务,请确保你的使用场景符合相关服务条款。
2. 整体架构设计与技术选型思路
2.1 为什么是DeepSeek加Dify这个组合
先说DeepSeek。我对比过几个主流大模型在数学题和理科题上的表现,DeepSeek在中文理科题目上的理解能力确实突出,尤其是带公式的题目,它能比较准确地还原解题步骤。而且DeepSeek的API价格相对便宜,对于我这种每天可能跑几十上百道题的使用频率来说,成本可控。另外DeepSeek支持较长的上下文,有些题目带图表描述或者多小问,上下文长了也不会丢信息。
再说Dify。Dify是一个开源的LLM应用开发平台,核心价值在于它把工作流编排这件事做得足够直观。我不需要写大量的后端代码来串联OCR、提示词组装、模型调用、结果解析这些环节,在Dify的可视化界面里拖拽节点就能完成。而且Dify支持知识库、变量传递、条件分支,对于解题这种需要根据题目类型走不同处理逻辑的场景非常合适。
OCR环节我选的是PaddleOCR,原因是它对中文的识别效果好,尤其是印刷体题目,准确率很高。而且它可以本地部署,不依赖外部服务,隐私性更好。有些朋友可能想用云端的OCR服务,也可以,在Dify里换一个HTTP请求节点就行,后面我会提一下怎么替换。
2.2 整体数据流是怎么走的
整个系统的数据流我画不了图,但可以用文字描述清楚:
- 用户在手机或电脑上拍照,通过一个简单的Web页面上传图片
- 后端服务接收到图片,调用PaddleOCR进行文字识别,得到题目的文本内容
- 将识别出的文本通过Dify的API触发工作流
- Dify工作流中,先对文本做清洗和格式化,然后组装成提示词
- 调用DeepSeek的API进行题目解答
- 将解答结果返回给前端,展示给用户
这个链路里,Dify承担的是编排和调度的角色,DeepSeek承担的是推理和生成的角色,PaddleOCR承担的是感知的角色。三者各司其职,耦合度低,任何一个环节出问题都容易定位和替换。
2.3 为什么不用端到端的多模态方案
你可能会问,现在多模态大模型这么强,直接把图片丢给模型不就行了,为什么还要OCR这一步?我实际测试过,多模态模型直接读题,对于清晰的印刷体题目效果还行,但一旦图片有倾斜、光照不均、手写痕迹,识别准确率就明显下降。而且多模态模型的token消耗更大,成本更高。用OCR先把文字提取出来,相当于把感知和推理解耦,OCR专注于把字认准,大模型专注于把题解对,各干各的擅长的活。实测下来,这种方案的稳定性和成本都更优。
3. 环境准备与基础服务搭建
3.1 硬件和系统环境说明
我用的是一台自己组的小服务器,配置不算高:Intel i5处理器,16GB内存,512GB固态硬盘,没有独立显卡。这个配置跑PaddleOCR的CPU版本完全够用,识别一张题目图片大概1到2秒。Dify和DeepSeek的调用都是走网络请求,对本地硬件要求不高。
操作系统用的是Ubuntu 22.04,Docker和Docker Compose提前装好。如果你用的是其他Linux发行版或者Windows的WSL,操作大同小异,核心是Docker环境要正常。
3.2 Dify的本地部署步骤
Dify的部署官方推荐用Docker Compose,我实际操作下来也是最省心的方式。步骤如下:
# 克隆Dify的代码仓库 git clone https://github.com/langgenius/dify.git # 进入docker目录 cd dify/docker # 复制环境变量文件 cp .env.example .env # 启动服务 docker compose up -d启动完成之后,浏览器访问http://你的服务器IP:3000,就能看到Dify的初始化页面。第一次访问需要设置管理员账号和密码,设置好之后登录进去。
注意:Dify默认占用的端口是3000和5001,如果你服务器上已经有其他服务占用了这些端口,需要在
.env文件里修改端口映射。我一开始就是3000端口被占用了,排查了半天才发现。
部署完成后,你需要在Dify的设置里配置模型供应商。进入“设置”→“模型供应商”,找到DeepSeek,填入你的API Key。DeepSeek的API Key在官网注册后可以获取,新用户通常有一定的免费额度,后续按量计费。
3.3 PaddleOCR服务的搭建
PaddleOCR我单独部署成一个HTTP服务,这样Dify可以通过HTTP请求节点调用它。安装PaddleOCR的步骤如下:
# 创建虚拟环境 python3 -m venv ocr_env source ocr_env/bin/activate # 安装PaddlePaddle和PaddleOCR pip install paddlepaddle pip install paddleocr # 安装Flask用于提供HTTP接口 pip install flask然后写一个简单的Flask服务来暴露OCR接口:
from flask import Flask, request, jsonify from paddleocr import PaddleOCR import os app = Flask(__name__) ocr = PaddleOCR(use_angle_cls=True, lang='ch') @app.route('/ocr', methods=['POST']) def recognize(): if 'image' not in request.files: return jsonify({'error': 'no image provided'}), 400 file = request.files['image'] img_path = '/tmp/uploaded.jpg' file.save(img_path) result = ocr.ocr(img_path, cls=True) texts = [] for line in result[0]: texts.append(line[1][0]) full_text = '\n'.join(texts) os.remove(img_path) return jsonify({'text': full_text}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5002)启动这个服务之后,你就有了一个本地的OCR接口,地址是http://你的服务器IP:5002/ocr。
提示:PaddleOCR第一次运行时会自动下载模型文件,需要保证网络通畅。模型文件大概几百MB,下载一次之后就会缓存在本地。
4. Dify工作流的核心编排细节
4.1 工作流节点的整体设计
在Dify里创建一个新的工作流应用,我给它起名叫“拍照解题助手”。整个工作流包含以下几个核心节点:
- 开始节点:接收两个输入变量,一个是
image_url(图片地址),一个是question_text(可选的文本题目,用于纯文字输入的场景) - HTTP请求节点:调用PaddleOCR服务,把图片转成文字
- 代码节点:对OCR结果做清洗,去掉多余的空格、换行,合并断行的公式
- 条件分支节点:判断题目类型,是数学题、物理题还是其他,走不同的提示词模板
- LLM节点:调用DeepSeek进行解答
- 结束节点:输出解答结果
这个设计里,条件分支节点是我后来加的。一开始所有题目都用同一个提示词,后来发现数学题和文科题的解答方式差别很大,数学题需要强调步骤推导,文科题需要强调要点归纳,所以拆成了不同的分支。
4.2 OCR结果的清洗与格式化
OCR出来的文本往往不干净,常见的问题有:公式里的符号识别错误、多行文字被拆散、题号后面多了空格、图片里的水印文字混进来了。我在代码节点里写了一段Python来做清洗:
def main(ocr_text: str) -> dict: import re # 去掉多余的空格和换行 text = re.sub(r'\s+', ' ', ocr_text) # 去掉常见的页眉页脚水印 text = re.sub(r'第\d+页', '', text) text = re.sub(r'共\d+页', '', text) # 修复常见的OCR错误 text = text.replace('×', 'x').replace('÷', '/') # 在题号前加换行,方便后续分段 text = re.sub(r'(\d+[\.、])', r'\n\1', text) return {'cleaned_text': text.strip()}这段代码看起来简单,但实际效果提升很明显。尤其是题号前加换行这一步,让后续的提示词组装能更清晰地识别出题目的边界。
实操心得:OCR清洗不要过度,有些“错误”其实是题目本身的特殊符号,过度替换反而会改变题意。我的原则是只处理明确的格式问题,不碰语义内容。
4.3 提示词模板的设计与调优
提示词是整个系统的灵魂。我前后改了十几版,最终稳定下来的模板大概是这样的:
你是一位经验丰富的中学理科老师,擅长用清晰的步骤解答题目。 请根据以下题目内容,给出详细的解题过程。 要求: 1. 先分析题目考查的知识点 2. 分步骤推导,每一步都要说明依据 3. 最终给出明确答案 4. 如果题目信息不完整,请指出缺少什么条件 题目内容: {{cleaned_text}}这个模板的关键在于角色设定和输出格式约束。角色设定为“中学理科老师”之后,DeepSeek的回答风格明显更贴近教学场景,不会跳步。输出格式的四条要求,让答案结构统一,前端展示的时候也更好排版。
对于文科类题目,我用了另一个模板,重点放在要点归纳和答题框架上,这里不展开。
4.4 变量传递与上下文管理
Dify工作流里,变量传递是通过节点的输入输出映射来完成的。开始节点的image_url传给HTTP请求节点,HTTP请求节点返回的OCR文本传给代码节点,代码节点输出的cleaned_text传给LLM节点。每个节点的输出变量名要对应好,不然会报变量未找到的错误。
我踩过的一个坑是:HTTP请求节点返回的是JSON格式,如果直接在LLM节点里引用,需要先解析出具体的字段。Dify的HTTP请求节点支持直接提取响应体中的字段,在配置里指定response.body.text这样的路径就行。
5. DeepSeek接入与解答质量调优
5.1 DeepSeek API的配置要点
在Dify的模型供应商设置里配置DeepSeek,需要填API Key和API Base URL。DeepSeek的API兼容OpenAI的接口格式,所以Dify里选择OpenAI兼容的供应商类型也能接。我直接选的DeepSeek供应商,配置更简单。
模型选择上,DeepSeek有不同版本,我日常用的是通用对话模型,数学能力足够。如果你对推理能力要求更高,可以切换到推理增强版本,但响应时间会更长,成本也更高。
参数方面,温度我设置在0.3左右。解题场景不需要太高的创造性,低温度能让输出更稳定、更聚焦。最大token数根据题目复杂度设置,一般800到1500够用,复杂的多小问题目可以设到2000。
5.2 解答质量的评估与迭代
怎么判断解答质量好不好?我的方法是准备一组测试题目,涵盖不同难度和类型,每次调整提示词或参数后,跑一遍测试集,人工评估答案的准确性和步骤完整性。
我最初的一版提示词,DeepSeek经常直接给答案,不写过程。后来在提示词里明确要求“分步骤推导,每一步说明依据”,情况就好多了。还有一个问题是,有些题目OCR识别有误,模型会基于错误的信息强行解答,后来我在提示词里加了“如果题目信息不完整或存在矛盾,请指出”,模型就会主动提示识别可能有问题。
注意:不要指望一次调优就完美。我的经验是,提示词的迭代周期至少需要一周,每天跑几十道题,积累足够的bad case之后再针对性修改。
5.3 成本控制的实际数据
我统计过一段时间的使用数据:平均每道题的OCR识别加DeepSeek解答,成本大概在几分钱。如果每天跑100道题,一个月下来也就几十块钱。相比市面上一些解题App的会员费,这个成本是可以接受的。当然,如果你用的是推理增强版本,成本会翻几倍,需要根据自己的需求权衡。
6. 实操过程中踩过的坑与排查方法
6.1 OCR识别不准的几种情况和处理
OCR识别不准是最常见的问题,我遇到的大致分三类:
第一类是图片质量问题,比如拍照时光线太暗、角度太斜。这种情况没有太好的技术解法,只能在前端加提示,引导用户拍清楚。我在上传页面加了一行提示文字:“请确保光线充足,题目文字清晰可见”。
第二类是公式和特殊符号识别错误,比如分数线的位置、根号的范围。PaddleOCR对这类二维结构的识别确实有局限。我的处理方式是在清洗环节做常见符号的替换,同时在提示词里告诉模型“OCR可能存在符号识别误差,请结合上下文理解”。
第三类是手写体识别。PaddleOCR对手写的支持有限,如果是手写题目,识别率会明显下降。这个目前没有特别好的办法,只能建议用户尽量输入印刷体题目。
6.2 Dify工作流常见的报错与解决
Dify工作流跑起来之后,我遇到过几个典型报错:
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| Variable not found | 节点间变量名不匹配 | 检查每个节点的输入变量名是否与上游输出一致 |
| HTTP request timeout | OCR服务响应超时 | 增加HTTP节点的超时时间,或检查OCR服务是否正常 |
| Model API error | DeepSeek API Key无效或额度不足 | 检查API Key和账户余额 |
| Workflow execution failed | 代码节点语法错误 | 在Dify的代码编辑器里先测试运行 |
这些报错在Dify的执行日志里都能看到详细信息,排查的时候先看日志,定位到具体节点,再针对性处理。
6.3 网络与部署相关的注意事项
Dify部署在内网服务器上,如果要从外网访问,需要做端口映射或者反向代理。我用的是Nginx做反向代理,配置里需要注意WebSocket的支持,不然Dify的某些实时功能会不正常。
另外,DeepSeek的API调用需要服务器能访问外网。如果你的服务器网络环境有限制,需要提前确认API地址是否可达。我一开始在内网测试环境里跑,怎么都调不通,后来发现是出口网络的问题。
提示:建议在Dify的工作流里加一个错误处理分支,当LLM调用失败时,返回一个友好的提示信息,而不是直接报错。这样用户体验会好很多。
7. 后续可以继续折腾的方向
这套系统目前跑得比较稳定,但还有不少可以优化的地方。我列几个自己打算继续做的方向,供你参考。
第一个是增加题目类型的自动分类。现在是用条件分支手动判断,后续可以训练一个小的分类模型,自动识别题目属于数学、物理、化学还是文科,然后走对应的提示词模板。
第二个是接入知识库。Dify本身支持知识库功能,我可以把教材的公式表、常见题型解法整理成知识库,让DeepSeek在解答时参考,提高答案的准确性和一致性。
第三个是增加错题本功能。把每次解答的题目和答案存下来,用户可以回顾,也可以定期复习。这个需要在Dify之外加一个数据库,工作量稍大,但价值很高。
第四个是优化前端体验。现在的上传页面很简陋,后续可以做成微信小程序或者App,拍照直接上传,体验会更流畅。
这套东西我从开始折腾到基本可用,大概花了两个周末的时间。中间踩了不少坑,但跑通之后确实省心,孩子现在遇到不会的题,拍一下就能看到详细的解题步骤,我也能从“人肉解题机”的角色里解放出来。如果你也想搭一套,建议先从最小可用版本开始,跑通主链路之后再逐步优化,不要一上来就追求完美。