Go知识库与检索

项目目录

项目简介

WeKnora 是以 Go 为后端的文档知识库与智能问答平台,支持文档解析、图片内容提取、混合检索和带来源的回答。文档入库通过后台任务执行,检索时将命中片段与关联正文组装为回答上下文。

如何写进简历

图知 · 生产线点检图文知识平台 | 场景化示例

项目描述:基于 WeKnora,面向生产线点检人员按设备型号与部位查找接线图、操作规程和点检说明,返回带原图与出处的操作参考。

技术栈:Go、Gin、GORM、Redis、Asynq

主要工作:

  • 图文资料异步入库:利用 Redis/Asynq 分阶段执行文档解析、图片处理与索引,通过有限重试、最终失败回写和子任务计数归并进度,让点检手册的上传与后台处理状态分别可见。
  • 原图与识别子块关联:按图片策略生成 OCR 或描述子块,以 ParentChunkID 关联正文并保留原图和来源位置,让端子编号与接线说明进入文本检索。
  • 检索融合与操作上下文:融合向量与关键词候选,在聊天链路沿父子关系补齐操作正文并合并重复上下文;保留原始命中内容和引用身份,供点检人员回查适用条件与原图。

面试题与回答

  1. Asynq 如何组织异步任务执行、重试与故障恢复,与通用消息队列有何区别?

    展开收起

    核心回答

    在采用 Redis/Asynq 的部署中,WeKnora 将文档上传与耗时的解析、切分、索引分开执行,再通过图片处理和后处理任务推进文档状态。任务队列负责调度与重试,数据库中的文档记录负责展示业务进度;上传成功、Worker 返回成功、文档最终可用是不同层面的结论。

    文件上传后任务怎样执行?

    上传服务先保存文件和 pending 文档记录,再把租户、知识库、文档与文件路径放入 DocumentProcessPayload。Worker 的 ServeMux 将文档任务交给 ProcessDocument,图片任务和后处理任务交给各自处理器,并由不同阶段的队列与并发池消费。上传与入队、处理器注册。

    文档解析、切分和索引后,有图片则分派图片处理任务,之后衔接后处理;没有图片也有直接进入后处理的路径。文档与后处理选项均设置 MaxRetry(3),即首次失败后最多再重试三次,并设置执行超时。任务选项。

    失败后一定会重试吗?

    不会。Asynq 根据处理器返回值判断失败,不会自行理解数据库中的 failed:

    • 可重试的解析或索引错误返回 error,交给框架安排重试;已有完成记录、失效尝试等则按具体条件跳过。
    • 获取知识库失败的一条路径会把文档标成 failed 后返回 nil;没有配置可用解析器时,也有记录失败后返回空结果的路径。这些业务失败不会因为状态名是 failed 就自动触发框架重试。知识库读取失败、解析器未配置。

    处理器错误经现有中间件耗尽重试时,中间件记录死信,并尽力将适用的文档状态更新为失败,避免页面一直等待;回调还检查任务所属尝试,避免旧失败覆盖新处理。租约过期的恢复器可以直接归档任务,不经过这层处理器中间件;状态回写本身也可能失败。因此排查时要同时检查队列记录、文档状态与具体阶段错误,必要时由后续状态巡检纠偏。最终失败回写。

    怎样判断整份文档处理结束?

    有需要等待的增强子任务时,后处理阶段登记子任务数,将文档置于 finalizing;没有这类子任务时则直接完成。子任务到达可结算的终态后释放计数。数据库在同一事务内递减 pending_subtasks_count,并在计数归零且状态仍满足条件时推进文档状态。后处理登记、事务内归并。

    这里的归零表示规定的任务槽已经结算,部分最终失败也会释放槽;它不证明每一次可选的识别、摘要或增强调用都成功。检索可用性和可选增强结果需要分别观察。终态释放条件。

    当前实现还有什么可靠性边界?

    文件与文档记录保存后才入队,两者并不是一个数据库与 Redis 的原子事务。该版本入队失败时可以标记文档失败但仍返回 knowledge, nil,因为文件已经保存。因此调用方不能仅以 HTTP 成功判断任务已经开始。若要进一步完善,可以引入事务内待发送记录与补投扫描,并按业务键防重复;这是后续设计建议。

    此外,未配置 REDIS_ADDR 时走 Lite 执行器。虽然名为 SyncTaskExecutor,其 Enqueue 会启动进程内 goroutine,也有重试和最终失败回调,但没有 Redis 持久任务队列,不能承诺进程退出后任务仍会恢复。部署分支、Lite 执行器。

  2. 图文检索和语义库如何构建?

    展开收起

    核心回答

    WeKnora 可以把文档图片转成可检索的文字,再关联回原图和来源段落。以图片描述与文字识别这条路线为例,图片处理结果分别写入 image_caption、image_ocr 子块,使用文本内容建立索引;子块通过 ParentChunkID 关联包含图片的文本块,并携带原图信息。命中子块后,系统补齐周围正文,组装带图片引用、识别文字与描述的上下文。

    图片处理可以按开关执行,也可以选用 smartocr:先观察图片属性并生成描述,再按属性策略决定是否调用 OCR。因此,这条路线的关键是把图片变成可检索的语义文字,并保住图文关联,不依赖所有图片都进入共享图文向量空间。

    从图片到检索块

    文档解析后,ImageResolver.ResolveAndStore() 将图片写入存储并替换正文中的图片地址。多模态阶段按图片创建后台任务;ImageMultimodalService 读取原图,选择本次任务指定的处理流水线。图片提取与任务编排、单图处理。

    默认流水线按各自开关执行描述和 OCR;smartocr 则通过一次观察调用取得属性和描述,再由 DecideOCR() 决定是否继续识别文字。观察失败时,默认采用保守策略继续 OCR,这项回退也可配置。默认流水线、按需 OCR。

    有非空描述或识别文字时,处理器分别创建子块,将文字存入 Content,将图片地址等信息存入 ImageInfo,并保留来源定位和 ParentChunkID。在启用相应索引能力的知识库中,indexChunks() 把这些文字内容交给 Embedding 模型和检索引擎,提供描述匹配与文字匹配入口。子块构建、文字索引。

    命中后怎样找回正确图文

    合并阶段识别图片子块,先找到对应文本块;启用父子分块时,再补齐更大的 parent_text 内容。同时保留命中的识别文字,并按命中文本范围和图片地址收窄图片信息,避免把同一大段中的其他图片内容混进来。图片子块的父块解析。

    例如检索产品手册中的“故障灯闪三次如何处理”:图片描述可提供故障灯与操作流程的语义,OCR 可提供图中的错误码和步骤文字;召回后需要保留这张图及其对应说明。验证时同时检查错误码是否识别正确、是否关联到正确图片、前提条件是否来自同一段说明。

    与直接图片向量的区别

    该版本也支持单独开启图片向量:知识库启用 ImageVectorEnabled,且向量模型与调用协议支持图片输入时,才会创建 image_vector 子块并写入原图向量。这项能力默认关闭,是与图片描述索引并存的另一条检索通道。能力开关、原图向量索引。

    在普通问答流水线中,模型支持视觉输入时,系统会为图片向量命中的结果附加原图;仅命中描述不自动触发这项处理。对描述遗漏、数字读错等问题,应回查原图与图片处理结果,分别定位内容提取、召回和上下文关联的问题。原图附加条件。

  3. 父子检索如何工作,为什么在 RAG 中使用?

    展开收起

    核心回答

    父子检索把用于匹配的小块与补充完整语义的父段分开。WeKnora 在入库时保存文本父子关系,检索服务按 ParentChunkID 补取上下文,聊天合并阶段再把命中结果展开为父正文;图片的 OCR、描述子块也能沿关系找到正文和更大的父段。这样可以在命中具体文字或图意后,补回解释它所需的条件。

    父块按层次关联补取,邻块按文档顺序补取,关联块按已保存的关系补取,三者共同服务上下文组装。服务层返回结果和完整聊天链路还经过不同处理,不能把其中一处补取或过滤等同于最终交给模型的内容。

    上下文扩展前,混合检索分别收集向量与关键词列表,通过可配置权重的 RRF 按名次融合,并按 ChunkID 合并候选;两路结果都为空时返回空,存储检索出错时返回错误。只剩一路时走该路的去重和分数处理,不能当作两路仍在融合。后续父子补取负责扩展阅读上下文,不因此产生父段独立参与检索的相关性评分。候选融合入口、单路处理与加权 RRF、按块融合。

    入库时怎样建立父子关系?

    开启 EnableParentChild 后,文档先分成较大父块,再分成较小文本块。父块以 parent_text 类型保存到数据库,子块以 text 类型保存,通过 ParentChunkID 指向父块;进入文本检索索引的是子块,父块用于后续扩展。父子都保留原文范围和来源定位,便于回到原文。父子切分、父块保存和子块关联。

    图片还会产生另一层子块:非空识别文字保存为 image_ocr,描述保存为 image_caption,它们的 ParentChunkID 指向所在的文本块,同时保存 ImageInfo 和 SourceLocators。因此可能形成“图片子块 → 文本块 → 大父段”的两级关系。图片子块写入。

    命中后怎样补齐并避免重复?

    processSearchResults() 先批量读取原始命中块,并保存其分数、匹配类型和原始命中内容。未设置 SkipContextEnrichment 时,再由 collectEnrichmentChunkIDs() 收集三类 ID:

    • 当前块的 ParentChunkID。
    • RelationChunks 中保存的关联块。
    • 有前后链接的 text 块对应的 PreChunkID、NextChunkID。

    processedIDs 同时记录原始命中与已经安排补取的 ID,避免多个命中重复拉取同一块。若原始结果包含图片子块,第一轮可以找到其文本父块,随后再用 collectParentChunkIDs() 补取文本块的大父段;第二轮只沿父关系继续,不重复扩展所有邻块和关联块。批量补取与图片第二轮、关联收集与去重。

    聊天链路怎样真正展开父正文?

    聊天的 PluginMerge.OnEvent() 会在选取检索或重排结果、初步去重后调用 resolveParentChunks()。它直接按父 ID 批量读取块,不经过检索服务的结果类型过滤:

    • text → parent_text:将大父段正文与命中子块内容合并到结果的 Content,标记 ContentRewritten,并保留子块标识。
    • image_ocr、image_caption 等图片命中:先找文本父块,有大父段时再向上一层读取,将父正文、文本正文和原识别内容合并。图片信息按命中的文本子块限定,父正文中的图片引用也据此清理,避免把同一大父段中其他位置的图片一并带入。

    随后聊天层继续合并连续或重叠正文、补短上下文、做最终去重,并建立引用来源。例如用户问接线图中某个端子的含义,图片识别结果负责定位,父正文补充接线条件,原识别文字仍被保留,不会因替换成父正文而丢失。聊天合并顺序、父关系批量解析、文本与图片展开。

    补充块的分数和顺序怎样理解?

    在检索服务的补取记录中,新补取的父块沿用触发它的子块分数,并标记为 MatchTypeParentChunk;关联块和邻块若没有分数,组装时使用 0。这个父块分数是关联传递的记录,不是父段独立参与原始检索得到的相关性分数。 多个子块指向同一父块,也不是在这里重新计算父段的相关性。聊天层展开正文同样不会因此产生一次新的父段相关性评分。

    检索服务组装结果时,原始命中按输入顺序加入,补充块随后追加;这里没有对所有新增上下文做一次统一重排。显式重排入口则先对未扩展的候选进行重排,再补上下文并保留原始命中项的重排结果,扩展本身仍是另一阶段。分数传递、结果组装、重排后扩展。

    当前实现有哪些取舍与缺口?

    共享检索服务有一个需要注意的边界:processSearchResults() 补取大父块后,组装调用的 isSearchableChunk() 没有允许 parent_text,所以该父块不会作为独立结果返回。聊天链路仍能通过上述 resolveParentChunks() 直接取父块并展开正文,不能据此说整个项目无法返回父上下文。 如果调用方只消费共享检索接口,就要单独验证父段是否已进入返回内容;完善该接口时,应区分可直接检索的块与可用于上下文扩展的块。大父块类型、组装阶段的类型过滤、允许类型。

    扩展还会增加数据库读取和上下文长度。使用时应比较关闭扩展、仅补父关系、补父关系加邻块等方案,在相同文档与问题集上观察证据完整性、重复内容、上下文用量和回答条件是否正确。服务层的 processedIDs 只按块 ID 去重,聊天层还另做内容签名和部分重叠去重;即使去重完成,也不能把“大量补回正文”直接当成准确率提升。

    参考资料