1. 项目缘起与整体设计思路
拍照解题这个需求,最早是从家长辅导作业的场景里冒出来的。孩子拍一张数学题照片,系统自动识别题目、给出解题步骤和答案,听起来简单,但真正落地时会发现链路比想象中长:图像预处理、文字识别、题目结构化、解题推理、答案格式化输出,每一步都有坑。我这次用 DeepSeek 加 Dify 搭了一套完整的智能拍照解题工作流,部署在 ECS 上,从拍照到出答案控制在几秒内,实测下来稳定性可以接受。
先说清楚这套东西是什么。核心思路是把拍照解题拆成两个阶段:感知阶段负责把图片变成干净的文字,推理阶段负责把文字变成有步骤的解答。感知阶段用 OCR 完成,推理阶段交给 DeepSeek 大模型。Dify 在这里扮演的是编排层的角色,它把 OCR 服务、大模型调用、提示词模板、结果格式化这些环节串成一条可视化的工作流,改起来不用动代码,调参和换模型都很方便。
为什么选 Dify 而不是自己写一套后端?我试过纯代码方案,用 FastAPI 把 OCR 和模型调用硬编码在一起,前期跑得挺顺,但后面想换 OCR 引擎、想调整解题提示词、想加一个"只给答案不给步骤"的开关时,每次都要改代码重新部署。Dify 的工作流把每个环节做成独立节点,改提示词就是改一个文本框,换 OCR 就是换一个 HTTP 请求节点,这种灵活性在快速迭代阶段太重要了。而且 Dify 自带日志和调试面板,哪个节点慢了、哪个节点报错了,一眼就能看到,排查效率比翻服务器日志高得多。
选 DeepSeek 的理由也很直接。解题这个任务对模型的推理能力要求高,尤其是数学题,需要模型能一步步推导而不是直接蒙答案。DeepSeek 在推理类任务上的表现,配合它相对友好的调用成本,做拍照解题这种高频调用的场景很合适。另外 DeepSeek 支持较长的上下文,遇到题干很长的物理题、化学题也不会被截断。
ECS 的选择上,我用的是一台 4 核 8G 的通用型实例。OCR 服务本身不吃太多 CPU,但如果用本地 OCR 引擎,内存要给够;Dify 社区版跑起来大概占 2G 左右内存,加上 Docker 本身的开销,8G 是比较稳妥的起步配置。如果并发量不大,2 核 4G 也能跑,但调试阶段容易因为内存不足触发 OOM,反而浪费时间。
整套方案的适用人群其实挺广:想给孩子做个辅导工具的家长、想练手 AI 工作流的开发者、想验证 OCR 加 LLM 组合方案的产品经理,都能从这套流程里拿到可复用的东西。下面我把每个环节拆开讲,包括我踩过的坑和最后怎么解决的。
2. 核心环节拆解与关键技术点
2.1 OCR 识别:拍照解题的第一道关卡
OCR 是整条链路的入口,它的质量直接决定后面模型能不能看懂题目。我一开始图省事,直接用了一个通用的 OCR 接口,结果发现两个问题:一是数学公式里的符号识别不准,比如把乘号识别成字母 x,把分数线直接丢掉;二是手写体识别率低,孩子写的数字稍微潦草一点就认错。
后来我调整了策略,把 OCR 分成两步走。第一步用通用 OCR 把整张图里的文字全部提取出来,得到一个粗糙的文本;第二步针对数学场景做后处理,把常见的识别错误做映射替换。比如把孤立的"x"在数字语境下替换成乘号,把"÷"和"/"统一处理。这个后处理逻辑不复杂,但效果立竿见影。
关于 OCR 引擎的选择,我对比过几种方案。云端 OCR 接口识别率高、接入快,但按调用次数计费,高频使用成本不低;本地 OCR 引擎比如 PaddleOCR,免费且可离线,但需要自己部署和调优,对服务器资源有一定要求。我最后用的是本地部署的 PaddleOCR,配合自定义的后处理规则,在 ECS 上跑起来内存占用大概 1.5G,识别一张普通题目图片耗时在 1 秒左右。
这里有个关键细节:图片预处理。直接拿手机拍的原图去识别,效果往往不好,因为光线不均、角度倾斜、背景杂乱。我在 OCR 之前加了一个预处理节点,做三件事:灰度化、二值化、透视校正。灰度化是把彩色图转成黑白,减少干扰;二值化是把灰度图变成纯黑白,让文字和背景对比更明显;透视校正是把倾斜拍摄的图片拉正。这三步做完,OCR 的识别率能提升不少。预处理可以用 OpenCV 实现,代码不复杂,但效果很明显。
注意:二值化的阈值不要设死,不同光线条件下最优阈值不一样。我用的方法是先算图片的灰度直方图,取双峰之间的谷值作为阈值,这样自适应效果比固定阈值好很多。
2.2 Dify 工作流编排:把散件串成流水线
Dify 的工作流是整个方案的中枢。它的核心价值在于把 OCR、模型调用、结果处理这些独立能力编排成一条有向无环图,每个节点负责一件事,节点之间通过变量传递数据。这种设计的好处是关注点分离:调 OCR 的时候不用管模型怎么调,改提示词的时候不用管 OCR 怎么配。
我搭的工作流大致是这样的:开始节点接收用户上传的图片,然后进入图片预处理节点,接着是 OCR 识别节点,OCR 输出的文本进入一个条件判断节点,判断识别结果是否为空或者明显异常。如果异常,走一条分支返回"请重新拍摄"的提示;如果正常,进入 DeepSeek 解题节点,最后经过结果格式化节点输出。
这里重点说几个 Dify 工作流里的实操要点。第一,变量传递要清晰。Dify 里每个节点的输出都可以被后续节点引用,但引用的语法要写对,比如{{ocr_node.text}}这种形式。我一开始因为变量名写错,导致模型收到的是空字符串,排查了半天才发现是引用路径的问题。第二,条件分支要设好边界。判断 OCR 结果是否有效,不能只看是否为空,还要看文本长度是否过短、是否包含大量乱码字符。我设的规则是:文本长度小于 5 个字符,或者乱码字符占比超过 30%,就判定为识别失败。
第三,超时和重试要配置。OCR 节点和模型节点都可能因为网络波动或服务负载出现超时,Dify 允许给每个节点设置超时时间和重试次数。我给 OCR 节点设了 10 秒超时、重试 2 次,给模型节点设了 30 秒超时、重试 1 次。这样偶发的网络抖动不会直接导致整个流程失败。
2.3 DeepSeek 解题提示词设计:让模型按步骤思考
提示词是决定解题质量的关键。我试过很多版本,最后稳定下来的提示词结构是这样的:先给模型一个角色设定,告诉它是一位耐心的老师;然后给出解题要求,包括分步骤解答、每步说明理由、最后给出答案;接着是输出格式要求,用固定的标记区分步骤和答案;最后才是题目文本。
为什么要把角色设定放在最前面?因为 DeepSeek 这类模型对开头的指令比较敏感,角色设定能引导它进入"教学"模式,而不是简单地甩一个答案。我对比过,有角色设定的版本,解题步骤明显更详细,也更愿意解释每一步的依据。
输出格式这块我踩过坑。一开始没做格式约束,模型有时候把答案写在步骤中间,有时候用一堆花哨的符号,前端解析起来很麻烦。后来我强制要求模型用固定的格式输出,比如每一步用"步骤N:"开头,答案用"最终答案:"开头。这样前端可以用简单的字符串匹配把步骤和答案分开,渲染成不同的样式。
还有一个细节是题目类型的判断。拍照解题可能遇到数学、物理、化学、英语等不同科目,不同科目的解题逻辑不一样。我在提示词里加了一段,让模型先判断题目类型,再按对应科目的方法解答。比如数学题要写清楚公式和推导过程,英语题要解释语法点和词汇。这个判断不需要额外调用模型,就在同一个提示词里完成,成本几乎不增加。
提示:提示词里的示例很重要。我给模型塞了两个示例,一个数学题一个物理题,展示期望的输出格式。有了示例之后,模型输出格式的稳定性明显提升,基本不需要再做后处理纠正。
2.4 ECS 部署与网络配置:让服务稳定跑起来
ECS 上的部署我用的是 Docker Compose,把 Dify、OCR 服务、数据库、缓存这些组件都容器化。这样做的好处是环境隔离,每个服务有自己的依赖,不会互相干扰。Dify 官方提供了 docker-compose 模板,我在此基础上改了几处:把 OCR 服务加进去,调整了各服务的内存限制,配置了 Nginx 做反向代理。
Nginx 在这里的作用是统一入口和负载均衡。外部请求先到 Nginx,Nginx 根据路径转发到 Dify 或者 OCR 服务。比如/api/workflow转发到 Dify,/api/ocr转发到 OCR 服务。这样前端只需要知道一个地址,不用关心后端有几个服务。Nginx 的 location 配置我调了几次,主要是处理上传图片的大小限制和超时时间。默认的client_max_body_size是 1M,手机拍的照片经常超过这个大小,我改成了 10M。超时时间也从默认的 60 秒调到了 120 秒,避免大图处理时连接被断开。
ECS 的安全组配置也要注意。我只开放了 80 和 443 端口给外部访问,数据库和缓存的端口只在内部网络开放。这样即使某个服务有漏洞,也不会直接暴露到公网。另外我配了定期快照,每天凌晨自动备份一次,万一系统出问题可以快速回滚。
3. 完整实操流程与关键步骤实现
3.1 环境准备与依赖安装
先列一下我用的环境:ECS 实例是 4 核 8G,操作系统 Ubuntu 22.04,Docker 版本 24.0,Docker Compose 版本 2.20。这些版本不是硬性要求,但版本太老可能会遇到兼容性问题,建议 Docker 至少 20.10 以上。
第一步是装 Docker 和 Docker Compose。Ubuntu 上用 apt 装就行,但要注意 apt 源里的 Docker 版本可能比较老,我建议用官方脚本装。装完之后用docker --version和docker compose version确认一下。
第二步是拉取 Dify 的代码。Dify 社区版在代码托管平台上有仓库,直接 clone 下来就行。clone 之后进入 docker 目录,里面有个.env.example文件,复制成.env然后改配置。重点改这几个:数据库密码、Redis 密码、Dify 的访问地址。访问地址要填 ECS 的公网 IP 或者域名,不然前端调接口会跨域。
第三步是部署 OCR 服务。我用的是 PaddleOCR 的 Docker 镜像,拉下来之后写一个简单的 Flask 接口包一层,对外提供/ocr接口,接收图片返回识别文本。这个接口的代码不长,核心就是调 PaddleOCR 的识别方法,然后把结果拼成字符串返回。注意要处理一下异常,比如图片格式不对、图片太大等情况,返回明确的错误信息。
第四步是配置 Nginx。在 ECS 上装 Nginx,然后写一个配置文件,把 80 端口的请求按路径转发到不同服务。配置写完后用nginx -t检查语法,没问题就 reload。
3.2 Dify 工作流搭建详细步骤
登录 Dify 后台,创建一个新的工作流应用。工作流编辑器是可视化的,左边是节点面板,中间是画布,右边是节点配置。
从开始节点开始。开始节点需要定义输入变量,我定义了一个image变量,类型是文件,用来接收用户上传的图片。然后拖一个 HTTP 请求节点到画布上,用来调 OCR 服务。这个节点的配置里,URL 填 OCR 服务的地址,方法选 POST,Body 里把image变量传过去。注意 Body 的格式要选 form-data,因为传的是文件。
OCR 节点后面接一个代码节点,用来做文本后处理。Dify 的代码节点支持 Python,我在这里写了前面提到的错误映射逻辑,把常见的 OCR 识别错误纠正过来。代码节点的输入是 OCR 返回的原始文本,输出是处理后的文本。
代码节点后面接条件分支节点。条件设两个:文本长度是否大于 5,乱码占比是否小于 30%。两个条件都满足才走正常分支,否则走异常分支。异常分支直接连到结束节点,返回"识别失败,请重新拍摄"。
正常分支接 LLM 节点,也就是调 DeepSeek 的地方。在 LLM 节点里选模型提供商为 DeepSeek,填上 API Key,然后写提示词。提示词里用{{text}}引用前面代码节点输出的文本。模型参数方面,温度我设的 0.3,因为解题需要稳定和准确,不需要太多创造性;最大 token 设的 2000,足够容纳详细的解题步骤。
LLM 节点后面再接一个代码节点,用来解析模型输出,把步骤和答案分开。模型输出是纯文本,我用字符串匹配找到"最终答案:"的位置,前面的部分是步骤,后面的部分是答案,分别输出两个变量。
最后是结束节点,把步骤和答案两个变量返回给前端。整个工作流就搭完了,点右上角的"运行"可以测试,上传一张题目图片,看每个节点的输出是否符合预期。
3.3 前端对接与图片上传处理
前端这块我做得比较简单,就是一个 HTML 页面,包含一个文件上传控件和一个显示结果的区域。用户选图片后,前端把图片转成 FormData,POST 到 Dify 的工作流接口。Dify 的工作流接口需要 API Key,这个在 Dify 后台的应用设置里可以生成。
图片上传前我做了压缩。手机拍的原图动辄好几 M,直接上传慢而且浪费带宽。我用 Canvas 把图片最长边压到 1600 像素,质量压到 0.8,这样一张图大概 200-300K,上传速度明显提升,而且对 OCR 识别率几乎没有影响,因为 OCR 需要的是文字清晰度,不是像素数量。
接口返回的是 JSON,包含步骤和答案两个字段。前端拿到之后,步骤部分按行分割,每行渲染成一个段落;答案部分加粗显示。整个页面不需要框架,原生 JavaScript 就够了。
注意:Dify 的工作流接口是流式的,如果前端用普通的 fetch 拿到的可能是流式响应。我一开始没注意,解析 JSON 一直报错。后来改成用
response.text()拿到完整文本再解析,或者用支持流式的库处理。如果不需要流式效果,可以在 Dify 应用设置里关掉流式输出。
3.4 性能调优与成本控制
性能方面,瓶颈主要在 OCR 和模型调用两处。OCR 的优化空间在于图片预处理,把图片质量提上去,识别一次就能成功,不用重试。模型调用的优化在于提示词,提示词越精准,模型输出的无用内容越少,token 消耗越低。
成本控制我做了两件事。一是缓存,同样的题目图片如果重复上传,直接返回缓存结果,不重复调 OCR 和模型。缓存的 key 用图片的 MD5 值,存在 Redis 里,过期时间设 24 小时。二是限流,每个用户每分钟最多调 10 次,防止恶意刷接口。限流在 Nginx 层做,用limit_req模块配置。
实测下来,一张普通数学题图片,从上传到返回结果,平均耗时 3-5 秒。其中图片预处理 0.3 秒,OCR 1 秒左右,模型推理 2-3 秒,剩下的是网络传输和格式化。这个速度对于拍照解题场景是可以接受的,用户拍完照等几秒看到答案,体验还算流畅。
4. 常见问题与排查技巧实录
4.1 OCR 识别率低的排查思路
OCR 识别率低是最常见的问题,表现是模型收到的题目文本缺字、错字、乱码。排查的时候按这个顺序来:先看原图质量,如果图片本身模糊、光线暗、角度歪,那 OCR 再强也没用,得先解决拍摄问题;再看预处理是否生效,把预处理后的图片保存下来看一眼,确认二值化和校正有没有做对;最后看 OCR 引擎的配置,不同引擎对不同字体和语言的识别率不一样,可能需要换引擎或者调参数。
我遇到过一次识别率突然下降的情况,排查了半天发现是预处理里的二值化阈值被改错了,导致图片变成全黑。这种问题看预处理后的图片就能立刻发现,所以保留中间结果很重要,Dify 的每个节点输出都可以在调试面板里看到,排查时善用这个功能。
4.2 模型输出格式不稳定的处理
模型输出格式不稳定,表现为有时候有步骤有时候没有,有时候答案藏在步骤里。这个问题的根源通常是提示词不够明确。我的解决方法是加示例和加强制标记。示例让模型知道期望的输出长什么样,强制标记让模型有明确的格式锚点。
如果加了示例还是不稳定,可以试试降低温度参数。温度越低,模型输出越确定,格式越稳定。我一般把温度设在 0.2 到 0.4 之间,太低会导致答案死板,太高会导致格式飘忽。
还有一个技巧是后处理兜底。不管模型输出多稳定,前端解析时都要做容错。比如找不到"最终答案:"标记时,就把最后一段当作答案;步骤为空时,就把整个输出当作答案。这样即使模型偶尔抽风,前端也不会显示空白。
4.3 Dify 工作流报错的常见原因
Dify 工作流报错,我遇到过的原因有这么几类:变量引用错误,比如引用了不存在的节点输出,或者变量名拼写错误;节点超时,OCR 或模型响应太慢,超过了节点设置的超时时间;网络问题,Dify 访问 OCR 服务或模型接口时网络不通;资源不足,ECS 内存不够导致容器被 kill。
排查的时候先看 Dify 的日志,日志里会显示哪个节点报错、错误信息是什么。如果是变量引用错误,日志会提示找不到变量;如果是超时,日志会显示 timeout;如果是网络问题,日志会有连接失败的提示。根据日志定位到具体节点,再针对性解决。
提示:Dify 的工作流支持"单节点测试",可以只运行某一个节点看它的输出。调试的时候不用每次都跑完整流程,省时间。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| OCR 返回空文本 | 图片格式不支持或图片损坏 | 检查图片能否正常打开 | 转换图片格式为 JPG 或 PNG |
| OCR 文本乱码多 | 图片质量差或预处理参数不对 | 查看预处理后的图片 | 调整二值化阈值,重新拍摄 |
| 模型输出无步骤 | 提示词不够明确 | 检查提示词是否包含步骤要求 | 加强提示词,增加示例 |
| 工作流执行超时 | 节点超时设置过短或服务响应慢 | 查看各节点耗时 | 调大超时时间,优化服务性能 |
| 接口返回 401 | API Key 错误或过期 | 检查 Dify 应用设置里的 Key | 重新生成 API Key |
| 图片上传失败 | 图片超过大小限制 | 检查 Nginx 的 client_max_body_size | 调大限制或压缩图片 |
| 内存不足容器重启 | ECS 内存不够 | 查看系统内存使用情况 | 升级 ECS 配置或优化内存占用 |
4.5 几个我踩过的坑和独家技巧
第一个坑是中文编码问题。OCR 返回的文本如果包含中文,在 Dify 节点之间传递时可能出现编码错误,表现为乱码。解决方法是确保所有环节都用 UTF-8 编码,包括 OCR 服务的返回头、Dify 的配置、数据库的字符集。这个问题隐蔽性强,因为英文环境下不会出现,只有中文才会触发。
第二个坑是图片方向。手机拍的图片有时候带有 EXIF 方向信息,浏览器显示的时候会自动旋转,但 OCR 服务拿到的是原始数据,可能方向是错的。解决方法是在预处理阶段读取 EXIF 信息,根据方向标记旋转图片。这个坑我踩了很久才发现,因为浏览器里看图片是正的,但 OCR 识别出来全是乱的。
第三个技巧是用 Dify 的变量聚合。如果工作流里有多个分支,最后可以用变量聚合节点把不同分支的输出合并成一个,这样结束节点只需要处理一种数据结构,简化前端逻辑。
第四个技巧是给模型加"不确定"选项。有些题目图片模糊或者题目本身有歧义,模型硬答反而会给出错误答案。我在提示词里加了一条:如果题目无法辨认或信息不足,输出"无法确定答案"而不是强行解答。这样虽然有些题目得不到答案,但避免了错误答案误导用户,整体体验反而更好。
5. 方案扩展与个人实践体会
这套拍照解题工作流跑通之后,我做了几个扩展尝试。一个是多科目支持,在提示词里让模型先判断科目再解答,数学、物理、化学、英语都能处理,实测下来数学和物理效果最好,因为这两科的题目结构比较规范,OCR 识别率高,模型推理也有明确的逻辑链。英语题因为涉及长文本和语法分析,模型输出会偏长,需要调大 token 限制。
另一个扩展是错题本功能。把每次解题的记录存到数据库,包括题目图片、识别文本、解题步骤、答案。用户可以回看历史记录,也可以按科目筛选。这个功能不复杂,但实用性很强,尤其是给家长用的时候,能清楚看到孩子哪些知识点薄弱。
还有一个想法是接入知识库。Dify 支持知识库功能,可以把教材、公式手册、常见题型解析这些资料导入知识库,解题时让模型先检索知识库再解答。这样对于教材内的题目,答案会更准确,因为模型可以参考教材的原始表述。不过知识库的构建和维护需要投入精力,我目前还在试验阶段,效果好的话再单独写一篇分享。
最后说几个个人体会。第一,OCR 的质量决定上限。模型再强,如果 OCR 识别出来的题目是错的,答案必然错。所以在 OCR 上多花时间是值得的,预处理、后处理、引擎选型,每个环节都值得仔细调。第二,提示词要迭代。我现在的提示词是改了十几版之后的结果,每一版都是根据实际输出的问题调整的。不要指望一次写出完美的提示词,要持续观察输出、持续优化。第三,监控不能少。ECS 上的 CPU、内存、磁盘、网络都要监控,Dify 的工作流执行日志要定期看,及时发现潜在问题。我配了简单的告警,内存超过 80% 或者工作流失败率超过 5% 就发通知,这样不用天天盯着也能掌握系统状态。
这套方案目前跑在我自己的 ECS 上,日常给孩子辅导作业够用了。如果你也想搭一套,建议先从最小可用版本开始:一个 OCR 接口、一个 Dify 工作流、一个简单前端,跑通之后再逐步加功能。不要一上来就追求大而全,容易卡在某个环节失去信心。先把主链路跑通,看到效果,再慢慢优化,这样推进起来更顺。