上传试卷、打Boss,玩游戏:我用 TextIn xParse 做了一套“会解析试卷的刷题游戏“

背景

刷题产品的题库成本卡在"把纸质试卷变成结构化题目"这一步:公式转 LaTeX、选项拆开、考点标注、答案核验。这套流程此前只有教育公司养团队才干得起。个人开发者写死几百道题的工具,题刷完就废了。

另一边,Roguelike"爬塔---掉卡---构筑"的玩法被验证能让人上瘾,但游戏化学习产品有个通病:demo 里题目是手写的,接真实试卷就死。解析、切题、判定三座大山任何一座塌了,"游戏化"就只是贴皮。

题塔 PaperQuest 要做的事:

上传一份试卷 PDF,系统借 TextIn xParse 把它解析成带 LaTeX 公式的结构化 Markdown,切题、标注考点,再编译成一座三层的 Roguelike 知识塔。答对怪物掉血、连击暴击;答错怪物回血反击,掉一张错题卡进图鉴。通关后结算页给知识点雷达图和战绩海报。

到今天它跑着真实 xParse 解析,压着 5 份真实试卷、44 局真实对局数据,自动化测试 65 单测 + 65 接口 + 25 前端 + 83 E2E + 15 安全全绿,PC 和移动端真人走查 76 + 47 项逐项点过。


第一道坎:让试卷"进得来"

端点踩坑史

接 TextIn xParse 时端点对齐遇到三个问题:

第一个端点 https://api.textin.com/api/v1/xparse,参赛指南页给的,在申报账号下直接网关 404。

换成 https://api.textin.com/ai/service/v1/xparse,返回 40007------机器人平台专用接口,不适用。

改用 multipart 表单上传文件,40303:文件类型无法识别。

翻到官方文档 pdf_to_markdown 一节才确认:协议要求 POST 原始二进制流,Content-Type: application/octet-stream,鉴权走请求头 x-ti-app-id / x-ti-secret-code,不是表单上传。改对之后一次通过。

复制代码
POST https://api.textin.com/ai/service/v1/pdf_to_markdown?markdown_details=1&parse_mode=auto
x-ti-app-id: <appId>
x-ti-secret-code: <secret>
Content-Type: application/octet-stream
body: 文件原始字节

下面这张图是这两天跑的一次真实调用响应,x_request_id 和耗时都在,不是演示数据:

重点看 markdown_head 那一行:$1+1=\underline{}(5\text{fen})$------印刷体公式被自动转写成了 LaTeX。这一行是整个项目的地基。有了它,题面可以原样进数据库、原样渲染到游戏界面,学生看到的题和卷子上长一个样。没有这一步,后面所有"把试卷变成游戏"都是空中楼阁。

分层降级的解析管线

真实世界上传上来的东西很杂:PDF、图片、Word、txt,甚至还有 rtf 和 csv。管线按类型分层,任何一层掉链子都有下一层兜底:

复制代码
上传文件
  ├─ .txt/.md/.markdown ────────→ 直通(plaintext,零解析成本)
  ├─ .rtf/.csv/.json/.tex ──────→ 本地提取(local-extract)
  └─ pdf/docx/html/图片/Office ──→ ① TextIn xParse(主力,含 xRequestId 留痕)
                                   ② 失败则降级本地提取(local-fallback)
                                   ③ 本地也失败才报错

解析成功后不是直接入库,还有两步:切题(splitQuestions)和补全(enrichQuestions:标注知识点、AI 补缺失答案)。产物在解析预览页一览无余:

解析报告里那个"请求 ID fe9fe0ff...、耗时 1.6 秒、解析出 668 字符"不是写死的文案,是每次真实调用后从 xParse 响应里带回来的。比赛要求"有效结果证明",可复现比截图重要,所以每条真实试卷入库都带 parseMeta,随时能拿请求 ID 回查。


核心链路:从 Markdown 到一座"知识塔"

试卷进来了,接下来是最烧脑的一步:怎么把 7 道题编译成一座好玩的地下城?

编译器的不变量

编译结果的硬约束(compiler.js,20 个单测钉死):

  • 每座塔固定 3 层,每层至少 2 个房间;
  • 顶层必须是 Boss 房,首层必含 1 个"复习站"(商店房,答对回血 20);
  • 怪物 HP 由题目分值换算(分值×10,钳制上下限);
  • 非法题目的引用一律过滤------LLM 抽风也不能让塔里出现"幽灵怪"。

LLM 在真实模式下负责把题目按考点聚类、给房间起怪物名和嘲讽台词。同时实现了一套 mock 引擎:同样输入,两次编译结果完全一致。联调和 E2E 时测试不能被模型的随机性拖着走,mock 引擎是确定性测试替身。

战斗引擎:纯函数,为可测性让路

战斗规则全部压在 engine.js 一个纯函数里,无 IO、无随机数,意图轮换是确定的(attack→attack→heavy→heal 四循环,玩家可以预判):

  • 答对:伤害 = ATK25 + 连击奖励(min(连击×5, 15)),连击 ≥3 触发暴击(+50% ATK);
  • 答错:没有免费回合------怪物先回血 tier 10%,再按意图反击(heavy 1.5 倍、heal 回 15% 上限再半力反击);
  • 玩家 HP≤0 阵亡,怪物 HP≤0 掉卡、清层、弹"三选一祝福"。

Roguelike 的心流循环是爬层→出题→构筑→清层。复习站是给受挫玩家的喘息窗口------纯击杀节奏下连错三题容易弃疗。

判定器:本地快判 + LLM 精判

判分不能全甩给 LLM------每答一题一次网络请求,延迟和成本都吃不消。现在的分层是:

  1. 选择题:本地精确比对,零延迟零成本;
  2. 填空题:本地快判,归一化后严格等值,容忍"答案是25"这类前缀;
  3. 解答题:一律走 LLM(真实模式)或 mock 引擎做语义精判;
  4. LLM 挂了:降级启发式(数字重合度),至少不卡玩法。

这里有个真实的坑。第一版判题器为了"宽容",对填空用了"学生答案包含参考答案就判对"。测试期一个 case 把它打穿了:解答题答案 a=-2,学生提交 我答a=-2X------照样判对。抄了半句话的错题蒙混过关,直接污染正确率、伤害和通关判定。

修复方案是分区治理:解答题彻底不走本地字符串比对,填空收紧为严格等值。这个 bug 写进了回归测试(bugfix.api.test.js),现在是 9 条回归 case 之一。

防作弊:答案一寸不漏

教育产品最怕"答案从接口漏出去"。铁律写在了 runService 注释里:任何出参不得包含 answer / answerAnalysis。实现上:

  • 出题接口下发的是 sanitizeQuestion 之后的题面(题号、题干、选项、分值、考点),没有答案字段;
  • 连"有没有答案"都只回一个布尔值 hasAnswer,解析预览页明示"答案已隐藏";
  • E2E 和接口测试里各有一条断言,逐字段检查对局/战斗/挑战响应中不存在 answer 字样。

另外,变式题机制让"背答案"也失效:原题耗尽后,编译器从种子题生成结构相同、数值不同的变式,判定用新答案。


答错之后:错题卡与图鉴系统

刷题产品只解决"爽"是不够的,还得解决"记住"。PaperQuest 的答案在答错那一刻就已写好:掉落错题卡。

每答错一题,系统把该题凝成一张卡(题干、考点、怪物出处),进图鉴。图鉴里可以对卡牌发起升级挑战:重新作答变式题,答对星级 +1,最高 3 星封顶。这本质上是一个游戏化的间隔重复系统(SRS)。

结算页给出:答题数、正确率、伤害、掉落、知识点雷达图:

雷达图的数据来自对局中按考点的累计统计。家长看雷达图三秒钟就知道:孩子"函数概念"塌陷,"几何证明"饱满------这比一句"考得不理想"有用得多。


一个并发 bug

写真人走查脚本时,用并发请求压对战接口,抓到了一个竞态:两笔并发 answer,同一道题、同一个正确答案,一次判对一次判错,最终统计 answered=2, correct=1, wrong=1,还多掉了一张卡。

根因是 answer() 没有并发保护:请求 A 在 await judge() 期间,请求 B 拿到同一道题一起判;A 结算完把题换成了下一题,B 持旧引用继续结算,状态互相污染。真实 LLM 模式下判定耗时几百毫秒到几秒,这个窗口会被放大到必现。

修复很干脆:per-run 进程内锁,判定中的对局再来直接 409「正在判定中」。回归测试用 30ms 慢判复现窗口,断言只有一次结算、口径恒等。

这类 bug 单测摸不到,只在"像用户一样乱点"的走查里现形。这也是在单元测试之外坚持 5 层测试证据链的原因:

层 内容 现状
L1 单元(编译器/战斗引擎/判题/切题/工具) 65/65
L2 接口(supertest 全 API,含防作弊/级联删除) 65/65
L3 前端组件(渲染/Katex/成就系统/交互态) 25/25
L4 E2E(Playwright 全流程,含真实 xParse 上传) 83 过 +1 跳过
L5 安全(CSP/限流/上传白名单/XSS/密钥泄漏) 15/15

再加两条真人模拟走查:PC 端 76 项、移动端 47 项,逐一点、逐项断言 DOM。移动端两张实拍:

L1-L4 全绿时以为项目稳了。真人走查第一轮还是抓出两个 bug:移动端抽屉遮罩拦点击、统计口径不恒等。测试的真正价值不在"证明它对",而在高效地发现它错。


完整技术栈

  • 运行时 :Node.js 22 + Express 4,单端口同服 API 与前端产物(express.static 托管 web/dist,SPA fallback 一条路由搞定);
  • 前端 :Vue 3 + Vite + Pinia + KaTeX,MC 像素风全部手写 CSS(@fontsource/press-start-2p),音频 WebAudio 合成;
  • LLM 双引擎 :真实模式走 DeepSeek-V4.1-Flash(主)/SenseNova-6.8-Flash-Lite(备),超时/异常自动降级 mock 引擎,演示永远不翻车;mock 引擎同时是确定性测试替身;
  • 文档解析 :TextIn xParse pdf_to_markdown(raw body 协议),本地提取兜底 rtf/csv/json/tex 与降级场景;
  • 存储 :JSON 文件存储(tmp+rename 原子写,Windows EPERM 重试),损坏自动备份重建。零原生依赖,换 SQLite 只需替换一个文件;
  • 代码量:server/src 49 文件 3568 行;web/src 43 文件 3870 行;测试层(单测+接口+E2E)合计约 4570 行------测试代码和业务代码接近 1:1.3,这是让它跑真实数据的底气。

工程上几条取舍:

第一,Mock 不是玩具,是基础设施。它让 E2E 在 CI 里 4 分钟跑完 83 个 case,让"两次编译结果一致"成为可断言的性质。

第二,答案只存在"判定那一刻"。从切题到入库,答案字段在详情接口就被剥掉,只在判定器和结算统计的闭环里出现。

第三,数据小到不需要数据库,但边界要留好。JSON 存储明说"单用户场景",并为换存储留了替换点。


往前看

文档解析正在从"给人看"变成"给机器吃"。过去 OCR 的目标是识别文字;现在 xParse 这类服务输出的是带坐标、带层级、带 LaTeX 的结构化 Markdown------这是给后续系统消化的中间表示。意味着"一份 PDF 变成一个可交互应用"的成本,第一次降到了个人开发者的量级。

看到的方向至少有四个。一是个人题库即时化,任何人的讲义、培训材料、行业手册,都可以在分钟级变成刷题/闯关应用。二是错题的资产化,错题卡 + LLM 变式 + SRS 调度,三件事合起来就是自适应学习的引擎,当解析足够便宜,瓶颈从"内容数字化"转移到"学习科学本身"。三是过程性评价,游戏化对局天然积累过程数据:单题耗时、连击曲线、弃局率、意图预判准确率,这些比一张卷子更能刻画一个学习者的状态。四是内容合规与可信,解析层越强,伪造内容的风险也越高,解析结果的水印、出处回链(留 x_request_id)、以及 LLM 生成内容的显式标注,会从加分项变成必选项。

局限也要说清楚:判分对主观题仍然粗糙;xParse 对手写体、极端排版仍有错切;LLM 变式题需要防"变错"。这些是真实的工程债务,列在待办里。


尾声

这个项目是一次完整的验证:一个国内的文档解析 API,足以让个人开发者把"纸质世界"改造成"数字玩法"。上传、解析、编译、爬塔、通关------链路每一环都有真实数据、真实测试、真实 bug 和真实修复。


相关推荐
量子-Alex1 小时前
【大模型后训练】Does Fine-Tuning LLMs on New Knowledge Encourage Hallucinations
人工智能·语言模型·自然语言处理
空 白II1 小时前
10.11 大语言模型研究简报:OpenAI 公布 AI 数学研究新进展
人工智能·语言模型·openai
蓝瑟1 小时前
苏格拉底式提问法,也许是AI时代最稀缺的能力
人工智能
Csvn1 小时前
延迟别再看平均值了:AI 应用指标与告警实战(O03)
人工智能·aigc·agent
小宋10211 小时前
GRPO训练工具Agent:环境、Rollout、可验证奖励与失败复盘
人工智能·深度学习·机器学习
hunteritself1 小时前
10 层 4K 实时剪辑,国产工作站进入广电制作核心
大数据·前端·人工智能·深度学习·transformer
默_笙1 小时前
🧀 让大模型的答案"流"出来:NestJS AI 模块 + SSE + RxJS 实战
javascript·人工智能
Teable任意门互动1 小时前
优锘科技北京客户管理数字化落地案例|HAP明道云搭建客户管理平台-任意门互动科技(HAP 官方实施服务商)
大数据·人工智能·科技·信息可视化·信息与通信
Smoothcloud润云1 小时前
PyTorch推理请求超时与取消处理实战
人工智能·pytorch·python·深度学习·机器学习·gpu算力·gpu