✨文末附开源仓库地址,做 AI 应用落地的同学可以直接拉下来跑。
⚠️ 本文为技术演示项目,仅供学习和交流参考,不建议直接用于生产环境。
当 LLM 遇见视觉模型,如何用 LangGraph 把它们串成一个完整的智能教育产品?
前言
做 AI 智能体(Agent)最容易卡在哪?不是模型调不通,是多个模型按业务逻辑串起来还能稳定跑、状态在节点间流转还能不丢。
工作流编排、条件路由、状态管理、流式输出------这些事线性链(LangChain 那种)搞不定,得上状态图。
最近在关注 LangGraph 工作流编排的落地实践,把这套问题在真实场景里跑了一遍:意图分类 → 条件路由 → 批改分支 / 解题分支 → SSE 流式输出,一个状态图串起 4 个模型,拍照上传自动批改评分或解题。
把状态丢失、意图被覆盖、SSE 格式错误这些坑都沉淀进这套可运行 Demo 里了,文末附开源仓库地址,做 AI 智能体落地的同学可以直接拉下来跑。
项目目标:为教育场景打造一套智能批改与解题系统,用户拍照上传试卷或题目,系统自动完成作业批改评分或题目解答。
技术挑战:
- 如何用 LangGraph 编排多个 AI 模型协同工作?
- 如何处理视觉理解 + 文本推理的混合任务?
- 如何实现 SSE 流式输出,让用户实时看到处理进度?
系统架构
整体设计

核心流程
整个系统的核心是一个 LangGraph 状态图工作流:
- 意图分类:用户输入文本,qwen-plus-latest 判断是"批改作业"还是"拍照解题"
- 条件路由:根据意图分流到不同分支
- 批改分支:图片 OCR 识别 → 逐题批改 → 提取分数公式 → 计算总分 → 汇总输出
- 解题分支:视觉模型识别题目 → 逐步解答 → 规范化排版输出
- SSE 流式输出:每个节点执行时实时推送进度到前端
使用演示
Step 1:拍照解题 --- 上传题目
用户输入题目描述并上传题目图片,系统自动识别意图为"解题":

如图:用户输入"这四道题答案是什么"并上传试卷图片,系统识别为解题意图。
Step 2:拍照解题 --- AI 解答
视觉模型识别题目内容并逐步解答,数学公式以标准 LaTeX 格式渲染显示:

如图:系统自动识别 4 道数学题(圆柱体积计算、体积比、直线截距方程、命题判断),逐题给出完整解答过程,LaTeX 公式正确渲染。
Step 3:作业批改 --- 上传试卷
用户选择"批改作业"并上传参考答案和学生试卷图片,系统自动识别意图为"批改":

如图:用户上传包含参考答案和学生作答的试卷图片,系统进入批改流程。
Step 4:作业批改 --- 批改结果
系统完成 OCR 识别、逐题批改、分数提取与计算,输出完整的批改报告:

如图:系统输出每道题的批改结果和得分,并汇总总分。
Step 5:统计看板,数据可视化
所有批改和解题记录自动沉淀为统计数据,看板展示核心指标和趋势图表:

如图:统计看板展示 4 个核心指标卡片(总批改次数、总解题次数、今日使用量、活跃用户),折线图展示近 7 天使用趋势,环形图展示功能使用分布,柱状图展示平均分分布。
技术选型思考
为什么选 LangGraph?
- 状态图模型:用节点和边描述工作流,比线性链更灵活
- 条件路由:根据中间结果动态决定下一步,完美适配"批改/解题"分支
- 状态累积:每个节点更新状态字典,最终获取完整结果
- 异步支持 :原生支持
astream,方便对接 SSE
为什么前端用 Vue 3?
- Composition API:复杂状态管理更清晰(聊天消息、会话列表、统计图表)
- 生态成熟:Element Plus 组件库开箱即用
- 图表支持:ECharts 集成方便,统计看板数据可视化
为什么用阿里云百炼?
- 模型齐全:视觉模型(qwen3-vl-plus)、文本模型(qwen3-max)、思考模型(qwen-flash)一站式配齐
- OpenAI 兼容:统一 API 格式,降低集成成本
- 国内访问:无需翻墙,延迟低
核心代码实现
1. LangGraph 工作流构建
python
from langgraph.graph import StateGraph, START, END
def build_workflow():
builder = StateGraph(GraphState)
# 添加节点
builder.add_node("classify_intent", classify_intent)
builder.add_node("check_grading_images", check_grading_images)
builder.add_node("check_solving_images", check_solving_images)
builder.add_node("image_ocr", image_ocr)
builder.add_node("grade_homework", grade_homework)
builder.add_node("solve_problems", solve_problems)
builder.add_node("normalize_output", normalize_output)
# 设置边
builder.add_edge(START, "classify_intent")
# 条件路由:根据意图分流
builder.add_conditional_edges(
"classify_intent",
route_by_intent,
{
"check_grading_images": "check_grading_images",
"check_solving_images": "check_solving_images",
"empty_intent_hint": "empty_intent_hint",
}
)
# 批改流程:OCR → 批改 → 提取分数 → 计算 → 汇总
builder.add_edge("image_ocr", "grade_homework")
builder.add_edge("grade_homework", "extract_formulas")
builder.add_edge("extract_formulas", "calculate_scores")
builder.add_edge("calculate_scores", "summarize_grading")
builder.add_edge("summarize_grading", END)
# 解题流程:解题 → 规范化 → 输出
builder.add_edge("solve_problems", "normalize_output")
builder.add_edge("normalize_output", END)
return builder.compile()
2. SSE 流式输出
每个工作流节点执行时,通过 SSE 实时推送进度到前端:
python
async def event_generator():
yield f"event: start\ndata: {json.dumps({'message': '开始处理'})}\n\n"
complete_state = dict(initial_state)
async for step in workflow.astream(initial_state):
for node_name, node_output in step.items():
complete_state.update(node_output)
progress_msg = step_messages.get(node_name, "处理中...")
yield f"event: progress\ndata: {json.dumps({'step': node_name, 'message': progress_msg})}\n\n"
final_output = complete_state.get("final_output", "")
yield f"event: done\ndata: {json.dumps({'content': final_output})}\n\n"
3. 前端 SSE 解析
javascript
let lastEvent = ''
for (const line of lines) {
if (line.startsWith('event:')) {
lastEvent = line.slice(6).trim()
} else if (line.startsWith('data:')) {
const data = JSON.parse(line.slice(5).trim())
if (lastEvent === 'progress') onProgress(data.step, data.message)
else if (lastEvent === 'done') onDone(data.content)
else if (lastEvent === 'error') onError(data.error)
}
}
4. LaTeX 数学公式渲染
AI 解题结果包含大量数学公式,前端使用 KaTeX 渲染:
javascript
// 内联公式: $...$
md.renderer.rules.inline_math = (tokens, idx) => {
return katex.renderToString(tokens[idx].content, { throwOnError: false })
}
// 块级公式: $$...$$
md.renderer.rules.block_math = (tokens, idx) => {
return `<div class="math-block">${katex.renderToString(tokens[idx].content, { displayMode: true, throwOnError: false })}</div>`
}
5. JSON 文件持久化存储
数据持久化采用轻量级 JSON 文件方案,服务重启后数据不丢失:
python
def _load_json(filename: str, default=None):
filepath = DATA_DIR / filename
if filepath.exists():
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
return default
def _save_json(filename: str, data):
with open(DATA_DIR / filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
踩坑记录
1. SSE 响应格式错误
问题 :AttributeError: 'dict' object has no attribute 'encode'
原因 :event_generator() 中 yield 的是 Python dict 对象,但 Starlette 的 StreamingResponse 期望字符串
解决:
python
# 错误
yield {"event": "progress", "data": json.dumps(...)}
# 正确 --- 标准 SSE 字符串格式
yield f"event: progress\ndata: {json.dumps(...)}\n\n"
2. LangGraph 状态丢失
问题 :历史记录和统计数据为空,intent 字段在 check_images 节点被覆盖为 "no_images"
原因 :LangGraph 的 astream 返回每个节点的增量更新,后续节点的状态会覆盖前面的字段
解决:在分类节点完成后立即保存原始意图,不被后续节点覆盖:
python
original_intent = ""
async for step in workflow.astream(initial_state):
for node_name, node_output in step.items():
complete_state.update(node_output)
if node_name == "classify_intent":
original_intent = node_output.get("intent", "")
# 使用原始意图判断是否保存历史记录
intent = original_intent or complete_state.get("intent", "")
3. KaTeX 公式渲染
问题 :AI 返回的数学公式($...$ 和 $$...$$)直接显示为纯文本
原因:markdown-it 默认不支持 LaTeX 数学公式
解决 :安装 katex + markdown-it-katex,自定义 inline_math 和 block_math 渲染规则
工程化实践
1. 配置管理
python
# API Key 从系统环境变量读取
DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "")
# 模型配置集中管理
MODEL_VISION = "qwen3-vl-plus" # 视觉理解
MODEL_TEXT = "qwen3-max" # 文本推理
MODEL_THINKING = "qwen-flash" # 思考模式
MODEL_CLASSIFIER = "qwen-plus-latest" # 意图分类
2. 前后端一体化部署
python
# FastAPI 挂载前端静态文件
frontend_dist = Path(__file__).parent.parent / "frontend" / "dist"
if frontend_dist.exists():
app.mount("/assets", StaticFiles(directory=frontend_dist / "assets"))
@app.get("/{full_path:path}")
async def serve_frontend(full_path: str):
return FileResponse(frontend_dist / "index.html")
3. 前端功能模块
- 智能对话:SSE 流式输出 + Markdown/LaTeX 渲染 + 图片上传(Base64)
- 历史记录:按类型/日期筛选,支持详情查看
- 统计看板:ECharts 图表(趋势折线图、分布环形图、分数柱状图)
- 用户管理:多角色(admin/teacher/student),Token 认证
- 数据导出:支持 JSON/CSV/Excel 格式导出
性能数据
| 任务 | 耗时 | 说明 |
|---|---|---|
| 意图分类 | ~1s | qwen-plus-latest |
| 图片 OCR | ~5s | qwen3-vl-plus 视觉模型 |
| 作业批改 | ~8s | qwen3-max 文本推理 |
| 分数计算 | ~3s | qwen-flash 思考模式 |
| 题目解答 | ~10s | qwen3-vl-plus + 思考模式 |
| 输出规范化 | ~5s | qwen-flash 思考模式 |
完整流程:解题任务(含图片识别+解答+规范化),总耗时约 15-20 秒。
总结
这个项目让我对 AI 应用落地有了更深的理解:
- 工作流编排是核心:LangGraph 的状态图模型比线性链更适合复杂业务场景
- 状态管理要谨慎:多节点工作流中,关键字段容易被后续节点覆盖,需要保护机制
- SSE 提升体验:流式输出让用户实时感知处理进度,避免长时间等待的焦虑
- 公式渲染很重要:教育场景中数学公式的正确显示直接影响用户体验
- 工程化决定成败:配置管理、持久化存储、前后端一体化,这些决定了系统能否真正可用
如果你也在做 AI 应用开发,希望这些经验对你有帮助。
源码仓库(已开源)
完整源码已开源至 Gitee,含可运行 Demo + 核心代码注释 + 踩坑修复方案:
项目地址 :Gitee - SmartGrader-AI ⭐ 欢迎 Star
仓库包含:
- LangGraph 状态图工作流(节点 + 边 + 条件路由)
- SSE 流式输出 + 前端解析
- 原始意图保护机制(防状态覆盖)
- KaTeX 数学公式渲染
- 多模型协同(视觉 + 文本推理 + 思考模式)
- Vue3 前端 + FastAPI 后端 + JSON 持久化
本 Demo 仅做技术复盘与学习演示,基于 LangGraph 工作流编排工程实践经验提炼,后续还会持续迭代补充更多场景优化。