【AI项目】用 LangGraph 构建智能批改与解题系统:条件路由、SSE流式输出Agent工作流实践


✨文末附开源仓库地址,做 AI 应用落地的同学可以直接拉下来跑。

⚠️ 本文为技术演示项目,仅供学习和交流参考,不建议直接用于生产环境。

当 LLM 遇见视觉模型,如何用 LangGraph 把它们串成一个完整的智能教育产品?


前言

做 AI 智能体(Agent)最容易卡在哪?不是模型调不通,是多个模型按业务逻辑串起来还能稳定跑、状态在节点间流转还能不丢

工作流编排、条件路由、状态管理、流式输出------这些事线性链(LangChain 那种)搞不定,得上状态图。

最近在关注 LangGraph 工作流编排的落地实践,把这套问题在真实场景里跑了一遍:意图分类 → 条件路由 → 批改分支 / 解题分支 → SSE 流式输出,一个状态图串起 4 个模型,拍照上传自动批改评分或解题。

把状态丢失、意图被覆盖、SSE 格式错误这些坑都沉淀进这套可运行 Demo 里了,文末附开源仓库地址,做 AI 智能体落地的同学可以直接拉下来跑。

项目目标:为教育场景打造一套智能批改与解题系统,用户拍照上传试卷或题目,系统自动完成作业批改评分或题目解答。

技术挑战

  • 如何用 LangGraph 编排多个 AI 模型协同工作?
  • 如何处理视觉理解 + 文本推理的混合任务?
  • 如何实现 SSE 流式输出,让用户实时看到处理进度?

系统架构

整体设计

核心流程

整个系统的核心是一个 LangGraph 状态图工作流

  1. 意图分类:用户输入文本,qwen-plus-latest 判断是"批改作业"还是"拍照解题"
  2. 条件路由:根据意图分流到不同分支
  3. 批改分支:图片 OCR 识别 → 逐题批改 → 提取分数公式 → 计算总分 → 汇总输出
  4. 解题分支:视觉模型识别题目 → 逐步解答 → 规范化排版输出
  5. 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 应用落地有了更深的理解:

  1. 工作流编排是核心:LangGraph 的状态图模型比线性链更适合复杂业务场景
  2. 状态管理要谨慎:多节点工作流中,关键字段容易被后续节点覆盖,需要保护机制
  3. SSE 提升体验:流式输出让用户实时感知处理进度,避免长时间等待的焦虑
  4. 公式渲染很重要:教育场景中数学公式的正确显示直接影响用户体验
  5. 工程化决定成败:配置管理、持久化存储、前后端一体化,这些决定了系统能否真正可用

如果你也在做 AI 应用开发,希望这些经验对你有帮助。


源码仓库(已开源)

完整源码已开源至 Gitee,含可运行 Demo + 核心代码注释 + 踩坑修复方案:

项目地址Gitee - SmartGrader-AI ⭐ 欢迎 Star

仓库包含:

  • LangGraph 状态图工作流(节点 + 边 + 条件路由)
  • SSE 流式输出 + 前端解析
  • 原始意图保护机制(防状态覆盖)
  • KaTeX 数学公式渲染
  • 多模型协同(视觉 + 文本推理 + 思考模式)
  • Vue3 前端 + FastAPI 后端 + JSON 持久化

本 Demo 仅做技术复盘与学习演示,基于 LangGraph 工作流编排工程实践经验提炼,后续还会持续迭代补充更多场景优化。

相关推荐
RobinDevNotes1 小时前
K8s+Megatron-LM+vLLM打通超大模型训练全链路
人工智能·kubernetes
资源大佬星 课it1 小时前
人工智能机器学习系统班,人工智能深度学习系统班
人工智能
连线Insight1 小时前
从谷歌到百度,AI正在打开搜索增长空间
人工智能
DevNo1 小时前
旅行素材太多不会剪?三款手机AI工具实测体验
人工智能
TMT星球1 小时前
伽利略机器人亮相WRC 2026,突破形态局限发布“陆行具身系统”Galileo X
大数据·人工智能·机器人
是大乔家的1 小时前
AI漫剧用什么软件制作?知漫剧批量生成连载实战分享
人工智能
IT小白杨1 小时前
当MCP遇上指纹浏览器:AI自然语言驱动隔离环境的实践思路
人工智能·经验分享·selenium·安全·业界资讯·指纹浏览器
fthux1 小时前
被主流遮住的世界:那些不常见却值得认识的编程语言
人工智能·ai·开源·github
向上的车轮1 小时前
DeepSeek Harness 详解:从“一切皆插件“到本地跑通 AI Agent 运行时
人工智能