山东大学软件学院-项目实训-个人开发日志(十):材料问答链路开发——文档解析、OCR兜底与持续追问完善

发布时间:2026/6/15 12:34:53

山东大学软件学院-项目实训-个人开发日志(十):材料问答链路开发——文档解析、OCR兜底与持续追问完善 引言前几周我已经把BabyMind的统一问答入口、RAG知识库、多Agent流式问答、语音输入输出等核心链路逐步打通。本周我的工作重点是把上传材料后直接提问等相关功能实现完整。在育儿场景中家长很多时候并不是只输入一句自然语言问题而是会直接拿出化验单、检查单、辅食配料表、体检材料来问。例如“这张血常规怎么看”“这份配料表适不适合宝宝吃”“这张检查报告需要马上去医院吗”。如果系统只能做普通文本问答实际使用价值会打折扣。因此这一周我主要围绕“材料问答”补齐了从文件上传、文档解析、上下文注入、流式回答清洗到前端持续追问体验的一整条链路。一、本周开发目标让问答真正接住“材料场景”这一阶段我给自己定的目标很明确不是简单增加一个附件按钮而是把材料问答做成完整能力1、支持图片、PDF、TXT、DOC、DOCX等常见材料格式上传2、后端能够稳定提取材料正文对扫描版文档提供OCR兜底3、问答请求不再把材料内容当作临时拼接文本而是作为正式字段进入后端处理链路4、材料在首轮提问之后仍然保留支持围绕同一份材料继续追问5、流式回答中去掉内部调度痕迹保证家长看到的是自然、干净、可直接理解的结论6、修复材料问答下“猜你想问”丢失的问题保证问答体验连续。二、先解决基础问题让后端真正能“读懂文件”这周我先从后端入手新增了材料解析接口/qa/attachments/parse。它的作用不是直接给出问答结果而是先把用户上传的文档转成可用文本再交给后续问答链路使用。当前这条解析链路支持以下几类文件1、TXT文本文件2、PDF文件3、DOC文件4、DOCX文件。为了避免用户上传超大文件拖垮服务我在解析服务里加了大小和文本长度控制。当前限制为最大10MB正文提取后最多保留30000字符同时额外生成一份预览摘要供后续检索和上下文拼接使用。在具体实现上我没有只做单一路径而是分成了“本地解析 远程兜底”两层。1、TXT文件采用多编码兼容解析避免用户上传GBK、UTF-16等文本时直接失败2、PDF优先用本地解析方式提取正文3、DOCX直接读取压缩包中的word/document.xml并解析正文内容4、对于扫描版PDF、传统DOC这类本地提取能力不稳定的文件再接入SophNet文档解析/OCR作为兜底。后端这段兜底逻辑的核心代码如下if not normalized_text and file_type in _REMOTE_PARSE_FILE_TYPES: remote_text, remote_failure_reason await _try_remote_document_parse( raw_bytesraw_bytes, filenamefilename, media_typeresponse_media_type, ) if remote_text: normalized_text _normalize_text(remote_text) if normalized_text: warnings.append(_remote_parse_warning(file_type))这段代码解决了一个很现实的问题很多家长手里的材料并不是标准电子文档而是截图、扫描件、医院导出的图片型PDF。如果没有这层兜底系统表面上支持上传实际上拿不到有效文本问答能力就会失效。三、把材料从临时上下文改成正式问答字段如果只是把解析后的文本拼接到用户问题后面虽然也能勉强工作但会带来两个问题1、前后端协议不清晰后续维护时很容易混乱2、不同Agent链路、不同问答入口不一定都能稳定识别这份材料。所以这周我把材料上下文从“临时拼接文本”升级成了正式请求字段。现在前后端的问答请求结构里已经新增了document字段专门承载上传材料的信息包括文件名、文件类型、正文内容、摘要、字数和解析提示。后端请求模型中的定义如下class QAAskRequest(BaseModel): question: QuestionText ... document: QADocumentContext | None None image_base64: str | None None这样做之后材料不再是某一条链路里的临时特殊处理而是成为问答系统的一等输入。无论是普通问答、流式问答还是路由后的健康、时间轴、营养模块都可以统一读取这份材料。四、材料不仅要传进去还要真正参与推理把材料作为正式字段传给后端只是第一步。真正关键的是模型在回答时要优先参考材料而不是把材料丢在一边继续泛泛而谈。因此我又补了一层材料上下文构造逻辑。在后端的document_context_service.py里我专门做了材料提示块构造把材料信息按固定结构注入用户消息中包括1、明确告诉模型“已附带一份用户上传材料请优先基于材料内容回答”2、如果材料不足以支撑结论要明确说明再补充通用建议3、禁止向家长暴露任何内部流程例如Agent、工具、数据库、同步动作等4、如果识别到材料像化验单、检查报告、表格数据就要求模型按“总体判断—关键异常项—下一步建议”的方式组织回答。这部分代码如下return ( [上传材料]\n f{build_document_prompt_block(document)}\n\n [家长当前问题]\n f{normalized_question} ).strip()此外我还让这份材料同时参与检索查询改写。也就是说系统不仅在生成回答时看材料在RAG检索阶段也会参考材料摘要提升检索结果和材料本身的相关性。这样一来问答链路从“问一句答一句”升级成了“用户问题 上传材料 检索知识库”三者共同驱动的回答方式。五、前端交互也一起重做支持直接传材料发问后端打通之后前端交互也不能停留在“上传完再自己输入一句话”的原始状态。这周我继续把材料问答的发送逻辑和界面体验补完整。首先在问答页面里我把文件选择器扩展成支持以下类型1、图片2、PDF3、TXT4、DOC5、DOCX。前端文件类型配置如下fileLauncher.launch( arrayOf( image/*, application/pdf, text/plain, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, ), )其次我新增了“默认材料提问语句”的机制。也就是说用户如果只上传材料、不手动输入问题也可以直接发送系统会自动补上一句默认提问“请帮我分析一下这份材料。”对应代码如下private fun buildOutgoingQuestion(question: String, attachment: QAAttachment?): PreparedQuestion? { val normalized question.trim() if (normalized.length 2) { return PreparedQuestion(requestText normalized, displayText normalized) } if (normalized.isBlank()) { val fallback attachment?.defaultQuestion ?: return null return PreparedQuestion( requestText fallback, displayText 请帮我分析这份材料, ) } return null }这里我还专门区分了requestText和displayText。请求真正发给后端的是完整提问句而聊天界面上显示的是更自然的“请帮我分析这份材料”避免把内部兜底逻辑直接暴露给用户。六、把“材料持续追问”做成真正可用的体验如果上传材料后只能问一轮那这个功能还是不够实用。家长真实的使用方式往往是先问一句“这张报告怎么看”然后继续追问“白细胞高是不是感染”“这种情况要不要去医院”“明天能不能打疫苗”。因此这周我把材料状态保留机制也补齐了。当前实现中文档解析成功后前端会保留当前材料并提示用户“当前材料会继续用于后续追问”。只要用户不主动替换或清空材料后续问题都会默认带着这份文档上下文继续发送。对应的状态文案也已经写进ViewModel里!extractedText.isNullOrBlank() - 已提取 ${charCount ?: extractedText.length} 字当前材料会继续用于后续追问前端发送请求时也会持续把这份材料重新组装进问答请求document buildDocumentRequest(pendingAttachment),这个改动的意义很直接问答不再是一轮一轮割裂的而是围绕同一份材料形成连续对话更接近实际的咨询过程。七、修复一个很实际的问题流式回答里不要出现内部系统话术材料问答链路打通后我在联调中发现一个问题由于后端走了多Agent和工具调用链路流式回答在某些情况下会把内部过程片段带出来比如“我先检索”“通知时间轴Agent”“数据库已更新”之类的话。这种内容对开发调试有价值但对普通家长来说完全是噪音甚至会破坏产品可信度。因此这周我专门增加了用户可见答案清洗逻辑。在answer_cleanup.py里我统一处理了这类内部表达包括1、替换时间轴Agent、营养Agent、Supervisor等内部词汇2、删除“我先查询”“工具调用”“数据库操作”“路由决策”等句子3、清理回答尾部未输出完整的内部短语残留4、保留真正需要展示给用户的正文和“猜你想问”部分。例如下面这组规则_DROP_PATTERNS: tuple[re.Pattern[str], ...] ( re.compile(r[^。\n]*(我先[^。\n]*(检索|查询|调用)[^。\n]*)[。]?, re.IGNORECASE), re.compile(r[^。\n]*(创建健康记录|健康记录已创建|未触发疫苗延期|触发疫苗延期)[^。\n]*[。]?, re.IGNORECASE), re.compile(r[^。\n]*(Agent|Supervisor|工具调用|路由决策|数据库操作)[^。\n]*[。]?, re.IGNORECASE), )这个清洗模块加上之后材料问答的最终输出明显干净了很多回答会更像一个面向家长的产品而不是后端调试窗口。八、继续补体验细节修复材料问答下“猜你想问”丢失这周还有一个前端细节问题也被我一起处理了。之前在流式问答场景下部分材料问题虽然主回答能够正常出来但回答下方的“猜你想问”推荐有时会丢失影响连续追问体验。我检查后发现问题不在模型没生成而是在前端SSE完成事件和原始流文本解析之间没有统一好优先级。因此我把完成事件里的suggestions显式接进来优先用后端返回的推荐问题如果后端这一轮没有带就再从流式文本中兜底解析。对应代码如下_suggestedQuestions.value event.suggestions.takeIf { it.isNotEmpty() } ?: parseSuggestions(streamingRawText)九、为材料问答准备测试样本验证不同格式链路为了验证这一套能力我还补了一批测试材料覆盖了不同格式和不同场景。目前仓库中的测试材料包括1、发热血常规测试报告PDF2、发热血常规测试报告DOC3、发热血常规测试报告DOCX4、发热血常规测试报告TXT5、配料表测试图片十、本周总结这周我完成的工作表面上看是在问答模块里增加了材料上传能力但本质上做的是一次完整的输入链路升级让BabyMind从“用户输入一句话我给一句回答”进一步走向“用户带着真实材料来问系统围绕材料持续给出结构化建议”。具体来说这一周我主要完成了以下几件事1、后端新增文档解析接口支持TXT、PDF、DOC、DOCX等格式2、为扫描版PDF和传统DOC补上OCR/远程解析兜底3、把材料上下文升级为正式请求字段统一接入问答链路4、让RAG检索和生成回答都能够参考上传材料5、前端支持直接选择文档并允许“只传材料不输入问题”直接发问6、保留当前材料用于后续连续追问7、清洗流式回答中的内部系统话术8、修复材料问答下“猜你想问”丢失的问题。到这一阶段BabyMind的问答能力已经不再局限于普通聊天而是开始具备接住真实育儿材料的能力。后续我准备继续围绕材料理解做进一步细化例如更稳定地识别检查报告中的关键指标、区分正常项与异常项、优化面向家长的解释方式并继续补齐更多真实场景测试。

相关新闻