企业级多模态知识库:从文件入库、跨模态检索到可追溯回答
这篇文章专门补充"多模态"这一条面试主线。你需要掌握的不是 OCR、ASR、MinIO 这些词的定义,而是能回答:
一张图片、一张表格、一段录音或视频,怎样变成可检索的知识?查到以后,模型依据什么回答?怎样保证资料没有串版本、串权限,引用能回到正确位置?
本文沿用你的企业知识库学习笔记,按完整系统的职责和数据流讲解,不按开发阶段组织。接口、字段、状态名和流程代码均为教学设计示意,不是宣称仓库已经存在同名实现。学习笔记与独立示例的依据放在文末,具体实现状态以项目 README 为准。
本篇不要求从头背到尾。先按问题选择一条阅读路径;需要理解 DeepAgents 中间件与 LangGraph 的通用执行机制,再回到项目总览文章,不重复学习两套术语。
| 我现在想弄懂什么 | 最短阅读路径 | 读完应该能做什么 |
|---|---|---|
| 整体怎么跑 | 第 4.2、10.5 节 | 画出一次上传、索引和问答的对象流 |
| 图片究竟怎样被搜到 | 第 6、9 节 | 区分文本化检索、视觉向量与模型核对 |
| 视频怎样定位和核对 | 第 8、11.6 节 | 解释时间轴、帧覆盖、实际模型输入 |
| 来源 ID 为什么不够 | 第 10.2、10.5、11.5 节 | 从一次索引命中追到正确版本与位置 |
| 企业级难点怎么回答 | 第 12 至 15 节 | 解释失败、撤权、发布和排错取舍 |
| 读懂后怎么练、面试怎么说 | 第 18、19 节 | 先口述,再用免费断言验证来源契约 |
第 5 至 8 节是按媒体分类的查阅材料;第 17、18 节是简历与口述准备。先走通一条链,再补细节,避免把格式、模型和数据库名称混成一份待背清单。
1. 先用一句话建立正确理解
企业级多模态知识库,不是把各种文件都交给大模型,而是把不同媒体整理成"可检索的表示 + 可回看的原始证据",再在权限和版本约束下回答问题。
这里有三个不同的东西:
| 名称 | 回答哪个问题 | 例子 |
|---|---|---|
| 原始资料 | 公司实际保存了什么 | PDF、照片、录音、视频 |
| 检索表示 | 怎样从大量资料里找到它 | OCR 文本、图片描述、转录文本、向量 |
| 回答证据 | 这次结论具体依据什么 | PDF 第 8 页的表格、视频 12:10 附近的画面 |
图片描述有助于找到图片,但不等于图片本身;向量有助于排序,但不等于事实;文件 URL 能定位对象,但不等于用户有权访问。
如果面试时只说"图片用 OCR,音频用 ASR,全部转 Markdown 再向量化",你解释了入库的一部分,还没有解释视觉检索、原图理解、定位、权限和质量。
2. 多模态到底有哪几层能力
先把四种经常被混称为"多模态"的能力拆开:
| 能力 | 实际发生了什么 | 是否自动具备以图搜图 |
|---|---|---|
| 多模态交互 | 用户说话,ASR 转文字;回答再 TTS 转语音 | 否 |
| 多模态资料文本化入库 | 图片 OCR/描述、音频转录、视频转录和画面描述进入文本 RAG | 否 |
| 跨模态向量检索 | 使用兼容的图文编码器,让文字查询与图片表示可以比较 | 取决于模型和检索链路 |
| 基于原始媒体的多模态推理 | 召回后把原图、裁剪区域或视频片段交给视觉模型核对 | 不等于建立了检索索引 |
这四层可以组合,不是后一层自动包含前面所有工程能力。
例如,"说话问制度,系统朗读回答"是语音交互;即使知识库里只有文字,也能做到。反过来,知识库收录了很多培训录音,也可以只提供文字问答,不开放麦克风。
再看三种"文字搜图片":
- 搜图片文件名,例如"海棠 A 户型"。
- 搜 OCR 或图片描述,例如"墙角有蓝色涂层"。
- 用图文双编码器,把文字查询向量与图片向量比较。
前两种也能用文字找到图片,但不等于第三种直接跨模态检索。面试要讲清使用了哪条链路,不能把结果界面相似当成底层原理相同。
3. 用一组企业资料贯穿全文
以下名称、内容与时间均为自编教学样例,不是项目运行结果。
| 资料 | 内容 | 需要保留的证据位置 |
|---|---|---|
| 案例手册 PDF 第 3 版 | 第 8 页有案例照片、材料表与施工说明 | 页码、图片区域、表格与行列 |
| 案例现场照片 | 墙角、管根及涂层的可见外观 | 原图、所属案例、必要的裁剪区域 |
| 售后培训录音 | 讲解登记、核实、派单的流程 | 对应转录的开始与结束时间 |
| 施工培训视频 | 12:10 至 12:34 演示某一步操作 | 视频时间段、相关画面与音轨 |
| 售后制度 PDF 第 5 版 | 当前生效的受理流程 | 文档版本、具体条款 |
| 采购底价表 | 采购部门专用价格 | 当前销售用户无权访问 |
用户上传一张照片,问:
这张照片接近哪个案例?培训视频中怎么讲这一步?这个案例的材料由谁供应,售后应该怎么解释?
你需要把它拆成几个可验证的问题:
- "接近哪个案例"需要图像检索或图片描述检索,候选仍需确认。
- "视频怎么讲"需要检索相关时间段,再核对转录或画面。
- "材料由谁供应"需要案例文档或有来源的图谱关系,不能凭照片猜。
- "售后怎么解释"需要当前有效制度,不能直接照抄可能过时的培训口述。
- 所有查询都不能接触当前用户不可见的采购底价。
不同问题决定需要哪种证据,不是所有问题都要把所有媒体过一遍。
4. 一张图看完整链路
text
资料入库
上传/受控导入
-> 身份、文件与可见范围校验
-> MinIO 保存原始对象,PostgreSQL 登记文档版本
-> 后台解析任务
PDF/Word -> 段落、表格、页图、内嵌图片
图片 -> OCR、描述、可选视觉向量
音频 -> 带时间的转录
视频 -> 音轨转录、片段、关键画面及描述
-> 统一证据单元与来源定位
-> 文本分片/文本向量/全文索引/可选视觉索引
-> 质量检查与索引检查
-> 发布可检索版本
用户问答
问题 + 可选查询图片
-> 可信身份与数据范围
-> DeepAgents 判断需要什么工具
-> LangGraph 执行受约束的检索流程
文本混合检索 / 视觉检索 / 必要的关系查询
-> 合并去重、重排、授权与版本复核
-> 按需加载原图、完整表格或视频时间窗
-> 模型依据证据回答
-> 引用服务再次鉴权,展示对应页/图片/时间段
这里最值得注意的是:先便宜地找候选,再有选择地读取较贵的媒体证据。
不会每次提问都重新 OCR 全部图片,也不会把公司所有视频直接塞进模型。入库时做可复用的处理;问答时只处理与当前问题有关、用户有权查看的内容。
4.1 各组件负责什么
| 组件 | 本专题中的职责 |
|---|---|
| NestJS | 上传、资料管理、身份鉴权、检索服务和媒体预览接口 |
| 后台任务执行器 | 耗时解析、转码、OCR/ASR、生成索引、失败重试 |
| MinIO | 私有保存原件、页图、抽取图片、转码或预览对象 |
| PostgreSQL | 文档版本、权限、证据定位、任务状态、发布清单 |
| pgvector | 文本向量与明确区分空间的可选视觉向量 |
| Elasticsearch | OCR、正文、描述、转录等文本的全文检索 |
| Neo4j | 案例、材料、供应商等有来源的关系,不负责理解像素 |
| DeepAgents | 问题规划、受控工具调用、必要时委派子任务 |
| LangGraph | 检索路由、融合、证据检查与有上限的补查 |
| LangSmith/OpenEvals、Langfuse | 效果实验与调用观测;不代替人工标注和安全控制 |
队列可以采用项目生态中的 Redis/BullMQ 等方案,但"装了队列"不等于任务可靠。幂等、版本检查和状态恢复仍是业务代码的职责。
4.2 先走一遍最小闭环,再看后面的细节
暂时只问:"这张图接近哪个案例,视频怎样介绍这一步?"下面沿用同一组教学资料,假设图像候选经核对关联到了海棠 A:
- 接收问题。 查询图片保存为当前用户可访问的临时对象,不自动加入公共知识库;服务端从登录态取得租户与身份。
- 找图片候选。 同空间的图像编码器生成查询向量,pgvector 在允许范围内返回案例图片候选及证据单元 ID,不返回一个凭空生成的答案。
- 确认来源。 候选关联到案例手册第 3 版第 8 页;复核当前权限、版本及业务信息,不能仅凭外观认定就是同一案例。
- 找视频位置。 用确认后的案例信息和问题主题,检索视频转录/画面描述,得到第 2 版视频的 12:10 至 12:34。
- 按需核对画面。 问题涉及动作,才读取该窗口的必要帧或短片段;只问讲解原话时,转录可能已足够。读取前仍校验权限。
- 回答并给引用。 回答附案例页和视频时间段;用户点击时由来源服务重新鉴权,再打开对应原件,不把永久公开 URL 当引用。
你可以先记住这六步。后面的章节解释的,就是其中每一步怎样避免丢信息、读错位置或越权。
5. PDF、Word 和表格:难点不只是能读出字
5.1 为什么不把所有 PDF 都送去 OCR
PDF 可能是带文字层的电子文档,也可能每页都是扫描图片,还可能两者混合。
对有可靠文字层的页面,直接提取通常更便宜,也避免额外 OCR 误识别;对扫描页再做 OCR。文件扩展名相同,不代表每页解析方式相同。
合理的解析输出不是只有一长串字符,而是保留:
text
第 8 页
标题:海棠 A 户型
正文区域:案例介绍
图片区域:现场照片
表格区域:材料名称、型号、供应商
页脚:内部培训资料
需要恢复的内容包括阅读顺序、标题层级、表格、图片和页码。双栏内容若按错误顺序拼接,字符识别全部正确,语义仍然可能错乱。
Word 可以依据标题、段落和表格结构定位;若没有稳定页码,不应凭空编出页码。可以引用章节、段落或转换预览后的版本化页码,并明确这种页码属于哪个预览版本。
5.2 OCR、版面分析和图片理解不是一件事
| 处理 | 解决的问题 | 不能自动解决的问题 |
|---|---|---|
| OCR | 识别图中的文字及位置 | 不保证知道哪行对应哪个表头 |
| 版面分析 | 区分标题、段落、图片、表格及阅读顺序 | 不保证识别内容正确 |
| 表格结构识别 | 恢复行列、合并单元格与表头关系 | 不保证数字、单位完全正确 |
| 视觉模型理解 | 描述画面、解释局部关系 | 不保证可以精确抄写小字或算准表格 |
这些可以由不同工具完成,也可能由一个服务提供组合能力。组件名字不重要,输出是否满足证据需求才重要。
5.3 表格为什么容易让 RAG 出错
假设原表是:
| 材料 | 型号 | 供应商 | 计量单位 |
|---|---|---|---|
| 防水材料 | W-A | 供应商甲 | 桶 |
| 瓷砖胶 | T-B | 供应商乙 | 袋 |
如果机械切片切成:
text
片段一:材料 型号 供应商 计量单位 防水材料 W-A
片段二:供应商甲 桶 瓷砖胶 T-B 供应商乙 袋
模型可能不知道"供应商甲"对应哪种材料。不是模型太笨,而是系统把行列关系切没了。
更合适的处理是保留结构,同时为检索生成带表头的行级表达:
text
案例:海棠 A;表:材料清单;材料:防水材料;
型号:W-A;供应商:供应商甲;计量单位:桶。
这一行有助于召回;回答精确字段时再依据结构化单元格核对。表格较大可以按行组分片,并携带表头、表名、必要的合并单元格上下文;不是无限地把整张表塞进一个 chunk。
对跨页表格,需要判断后一页是否是同一张表、表头是否延续。对财务或统计表,还要保留币种、单位、"万元"等倍率、脚注和负数表示。
**不要把大模型生成的表格概述当成精确数字的唯一依据。**概述适合检索主题,具体数字应回到单元格或有依据的结构化结果。
5.4 图表要比普通图片多保留什么
柱状图、折线图的含义依赖横纵轴、图例、单位、时间范围与脚注。只有"销售额上升"一句描述,无法回答"哪一季度、多少、同比还是环比"。
如果图中只有像素而没有原始数据,模型读出的数值可能只是估计。系统应区分"原数据明确给出的值"和"从图形估读的值",不能把两者包装成一样精确。
5.5 页码和坐标怎样支持引用
一条页面证据可以关联:
json
{
"docId": "doc_case",
"documentVersion": 3,
"unitId": "unit_table_materials_row_1",
"locator": {
"kind": "page_region",
"page": 8,
"sourceAssetId": "asset_case_pdf",
"region": {
"referenceAssetId": "asset_case_page_8",
"renditionVersion": "page_render_v1",
"bbox": [0.08, 0.52, 0.92, 0.68]
}
},
"text": "海棠 A:防水材料 W-A,供应商甲,单位为桶。"
}
这里约定页码从 1 开始;所有页面、图片和帧的区域,都相对 referenceAssetId + renditionVersion 对应的校正后画面归一化到 0 至 1,原点在左上,顺序为左、上、右、下。它是教学约定,实际项目需统一解析器与预览器的坐标系。
这样前端可以打开第 3 版第 8 页,并高亮材料表区域。只存"第 8 页"也能引用,但定位粒度更粗;并非每份资料都必须做到单元格级高亮。
6. 图片:OCR、描述和视觉向量各有用处
6.1 同一张图片为什么需要多种表示
以现场照片为例:
| 表示 | 可能得到的内容 | 主要用途 |
|---|---|---|
| 原始图像 | 原始像素和分辨率 | 人工预览、视觉模型核对 |
| OCR 文本 | 包装或标识上的型号文字 | 精确型号检索 |
| 图片描述 | 墙角、管根、可见涂层等描述 | 文本语义检索 |
| 视觉向量 | 编码器得到的特征数组 | 外观相似、图文跨模态候选召回 |
| 来源定位 | 所属案例、原 PDF 页码、裁剪位置 | 案例关联与证据追溯 |
不要求每张图都生成所有表示。纯标识截图可能 OCR 已足够;以图搜图才需要考虑视觉编码;精确的图纸和小字则可能需要高分辨率区域处理。
6.2 图片描述为什么不能取代原图
描述是一次有损转换。模型可能没有描述用户关心的局部,小字可能未读到,也可能错误地给可见现象补充因果解释。
假设描述是"浴室墙角有蓝色涂层"。用户问"管根有没有加强层",描述没有提到,不代表原图没有。正确动作是按需查看原图或对应裁剪区域,不是用描述的缺失证明画面中的缺失。
图片本身也有边界:一张完工照片看不到隐蔽层;看见某种颜色不等于知道品牌、型号和施工合规性。这些需要材料单、施工记录或检验资料。
视觉相似只证明"看起来相关",不证明"业务身份相同"或"工程事实成立"。
6.3 两条可以并行的召回通道
text
文字问题
-> 文本 embedding + ES
-> 搜图片 OCR/描述
-> 图片候选
文字问题
-> 兼容图文模型的文字编码器
-> 搜同空间的图片向量
-> 图片候选
第二条需要模型明确支持图文共享空间。CLIP 类双编码器可以用来理解这种机制,但你的笔记没有指定已接入哪一个具体视觉模型,因此这里不是已使用 CLIP 的声明。
如果用户上传查询图片:
text
查询图片
-> 同一视觉空间的图片编码器
-> 相似图片候选
-> 关联案例、文档与原图核对
也可以先给查询图片生成描述,再走文本 RAG;这是一条可用的替代路径,但会丢失描述未覆盖的视觉细节,不等价于直接以图搜图。
6.4 图片太大或细节太小时怎么办
直接缩小整图可能让合同小字、图纸标注、界面按钮不可读。可以先看整体,再对相关区域做高分辨率 OCR 或裁剪分析。
裁剪不是另一个不明来源的图片。应保留父图 ID、裁剪区域和处理版本,避免引用只指到一张用户找不到的临时小图。
原图只用于证据核对时,不必把所有裁剪都长期做成独立检索条目;是否建索引取决于重复使用价值和成本。
7. 音频:检索的是转录,依据仍要能回到录音
7.1 一条长录音怎样进入检索
text
录音原件
-> 校验格式、时长与处理权限
-> 必要的格式转换/声道处理
-> ASR
-> 带时间的语句或段落
-> 按主题组织的检索片段
-> 文本向量 + ES
例如:
json
{
"docId": "doc_after_sales_audio",
"documentVersion": 1,
"unitId": "unit_audio_18",
"text": "先登记客户问题,再由售后核实,随后安排派单。",
"locator": {
"kind": "time_range",
"sourceAssetId": "asset_audio_original",
"startMs": 145000,
"endMs": 163000
},
"representationKind": "transcript",
"extractionVersion": "asr_pipeline_v1"
}
用户问售后流程,文本检索命中这段就能引用"录音 02:25 至 02:43"。不需要先找音频向量,才能检索录音里讲的话。
7.2 为什么不能只有完整转录,没有分段时间
完整转录适合归档,但太长,不利于精确召回,也不能让用户直接听到相关位置。
可以按语句停顿、主题和长度组织片段,保留时间范围;边界附近可以有适度重叠。片段内不宜跨越过多主题,不能为了固定秒数把一句话从中间截断。
使用分块 ASR 时,要把局部时间加回该块在原音频中的偏移,并处理边界重复。否则每块都从 00:00 开始,检索结果看似有时间戳,点击却会跳错。
7.3 ASR 错了怎么办
常见错误包括材料型号、企业术语、人名、数字,以及噪声和多人重叠说话造成的漏识别。
可以用领域词表、合适的识别配置、音频质量检查、低质量片段标记和人工纠正来改善。纠正后的转录也要版本化,更新相关索引,而不是只改展示页面。
不能把 ASR 输出当成无误原文。特别是金额、型号、时间要求等精确事实,需要结合其他可靠资料核对。
说话人分离通常表示"说话人 A/B"的聚类,不等于识别出了真实姓名。除非有明确授权和额外身份依据,不能把声音相似直接当成人员身份。
7.4 音频检索的能力边界
"找讲到退款流程的录音"可以转录检索;"找有机器异响的录音"未必能靠 ASR,因为异响不是语言。
后者需要音频事件识别或合适的音频 embedding。这是另一项能力,不能因为收录了音频文件就说已经支持。
TTS 是回答输出的合成,不参与录音资料的知识入库。面试时不要把 ASR/TTS 对称出现,误讲成两者都是检索模块。
8. 视频:必须把声音、画面与时间对齐
8.1 只做 ASR 会漏掉什么
视频中的人可能说"注意这里",实际动作只在画面中;幻灯片可能写出型号,讲解者只说"这一款"。
因此,只转录音轨,能检索"讲了什么",却不能完整支持"画面展示了什么"。
一条合理的处理链路是:
text
原视频
-> 媒体信息与时长
-> 提取音轨 -> ASR -> 带时间的转录
-> 分镜/时间窗/关键画面 -> OCR、画面描述、可选视觉向量
-> 按共同时间轴关联
-> 视频片段的检索表示与来源定位
FFmpeg 可作为媒体提取和转码工具的例子,但它不负责理解视频语义;视频理解模型也不代替文件校验、时间轴和任务管理。
8.2 怎样抽画面才合理
| 策略 | 优点 | 局限 |
|---|---|---|
| 固定间隔取帧 | 简单,成本可控 | 可能错过短暂动作 |
| 按镜头变化取帧 | 有助于覆盖不同场景 | 一个镜头内发生的细节变化可能漏掉 |
| 依据转录和问题扩展取帧 | 聚焦相关位置 | 依赖先前召回,不能替代全局覆盖 |
| 较密集片段分析 | 对动作、过程更有帮助 | 成本、时延与模型支持要求更高 |
这些策略可以组合,不存在一个适用于所有视频的"最佳每几秒一帧"。
尤其要记住:**稀疏取帧没看到一个事件,不足以证明整段视频都没发生。**对"是否每一步都按规范完成"这样的审核问题,需要更明确的覆盖规则和人工复核,而不是给出超出证据范围的保证。
8.3 时间轴怎样对齐
可以把视频原件的毫秒时间作为统一坐标。例如:
text
片段:12:10 至 12:34
转录:12:11 至 12:19,讲解"接下来处理管根"
画面:12:14,显示相应位置
画面:12:23,演示后续操作
音轨若经过裁剪,要记录相对原视频的起始偏移;视频若转码、变速或剪辑,也要维护对应版本的时间映射。帧位置应依据媒体时间戳,而不是简单假定"帧号 / 固定帧率"永远准确。
检索返回的是原视频的时间位置;预览若使用转码副本,也要保证映射一致。
8.4 片段表示可以是什么样
json
{
"docId": "doc_training_video",
"documentVersion": 2,
"unitId": "unit_video_step_7",
"locator": {
"kind": "time_range",
"sourceAssetId": "asset_video_original",
"startMs": 730000,
"endMs": 754000
},
"representations": [
{
"representationId": "rep_video_transcript_1",
"unitId": "unit_video_step_7",
"kind": "transcript",
"sourceAssetIds": ["asset_video_original"],
"extractionVersion": "asr_pipeline_v1",
"text": "接下来处理管根,注意操作顺序。"
},
{
"representationId": "rep_video_frame_734000",
"unitId": "unit_video_step_7",
"kind": "caption",
"sourceAssetIds": ["asset_frame_734000"],
"extractionVersion": "frame_caption_v1",
"atMs": 734000,
"text": "演示人员正在指向管根周边区域。"
}
]
}
这里故意没有直接把声音和画面拼成一个无来源段落。保留两种表示,才能知道一句话是"讲解者说的"还是"模型从画面概括的"。
为了检索,可以额外生成片段级摘要,但摘要要关联它依据的转录与帧;它仍不是新的独立原始事实。
8.5 为什么查询时还可能需要看更多帧
入库时的摘要只能覆盖常见信息。用户问到局部细节时,可以先定位相关时间窗,再在预算内读取邻近画面或短片段核对。
如果调用的模型只支持图片,就按时间顺序提供相关帧,并说明覆盖范围;如果模型支持视频输入,则按其实际接口传入受控片段。不能把"给了三帧"说成模型已经完整观看了整段视频。
8.6 不要把同一个视频的十帧当成十份证据
相邻帧通常高度相关。若它们占满 Top-K,会挤掉制度和案例资料,也会制造"很多来源都证实了"的假象。
可按父视频与时间窗合并候选,限制每个来源占比,保留必要的连续性。融合粒度应是业务相关的证据单元,而不是永远按每一帧独立排序。
9. 跨模态向量:最容易说错的底层原理
9.1 pgvector 不会自动让系统懂图片
pgvector 提供向量存储、距离计算和索引能力。向量具有什么语义,来自生成它的 embedding 模型,而不是数据库。
文字向量与图片向量即使都是 768 维,也可能处于完全不同的坐标体系。把它们放在同一张表,不会自动变成可比较的表示。
可以类比:两个坐标都写成三个数字,但一个表示米制位置,一个表示 RGB 颜色。数字个数一样,直接算距离没有你需要的含义。
9.2 真正的图文检索需要什么
一个经过对齐训练的图文双编码器,包含兼容的文字编码器与图像编码器:
text
文字"墙角的管根处理"
-> 该图文模型的文字编码器 -> 查询向量
现场照片
-> 同一图文模型的图片编码器 -> 图片向量
两者按该模型要求的预处理、归一化和距离度量比较
不能把通用文本 embedding 模型 A 的向量,拿去搜视觉模型 B 的图片向量。模型名称、版本、预处理与度量方式都可能影响兼容性。
部分视觉向量适合外观相似,却不擅长型号文字、精确布局或复杂动作。多语言查询的表现也需要单独验证,不能假定某个模型对中文业务术语天然有效。
9.3 怎样组织索引更清晰
一种容易理解的设计是:
| 索引 | 写入什么 | 查询向量由谁生成 |
|---|---|---|
| 文本向量索引 | 正文、OCR、描述、转录的文本 embedding | 同一文本空间的查询编码 |
| 全文索引 | 同类文本及来源元数据 | ES 对查询文本做全文匹配 |
| 视觉向量索引 | 图片/关键帧的视觉 embedding | 同空间的图像编码或兼容文字编码 |
它们都可以关联同一组文档和证据单元,但不需要混成一个向量列。不同维度、模型版本或距离度量的向量,应有明确的表、分区或独立索引边界。
至少记录 embeddingSpaceId,其定义应包含模型与版本、维度、预处理/归一化及距离度量配置。不能只记录"768 维"。
如果更换模型,旧向量通常不能直接与新查询向量比较。需要构建新索引、评估、切换检索配置,再清理旧空间;不要在同一索引里混写不兼容向量。
数据库到底执行了哪一步
以图搜图时,先由图片编码器得到查询向量;数据库只负责在允许的候选里计算距离并返回 ID。下面是假设采用余弦距离的 SQL 示意,不是项目已有表结构:
sql
SELECT e.unit_id, e.embedding <=> $1::vector AS distance
FROM image_space_v1_embeddings AS e
JOIN documents AS d
ON d.tenant_id = e.tenant_id
AND d.id = e.doc_id
AND d.published_version = e.document_version
WHERE e.tenant_id = $2
AND d.deleted_at IS NULL
AND d.status = 'PUBLISHED'
AND can_read_document($3, d.tenant_id, d.id)
ORDER BY e.embedding <=> $1::vector
LIMIT $4;
这里约定:视觉表只保存一个兼容空间、维度固定的向量;$1 是该空间编码得到的查询向量;$2/$3 是服务端提供的租户和用户;$4 是经上限校验的候选数量。can_read_document 代表需要自行实现的当前权限判断,不是 PostgreSQL 内置函数。
结果里的 unit_id 再关联原图、案例和定位。数据库没有执行图片理解,也没有自动确认它们是同一个案例。文字搜图片时,把第一步换为同一图文空间的文字编码器,而不是随便用默认文本 embedding。
这段查询表达正确的权限和版本条件,不保证在每个数据规模下都有理想执行计划。生产还要验证索引、过滤与性能;同一文档版本重跑解析时,也要按有效索引代次限定产物。数据层可以再配合 RLS 等防线,不能只靠模型"记得加 WHERE"。
9.4 RRF 和 Rerank 在多模态里怎么使用
不同通道的原始分数通常不能直接相加:BM25、文本余弦分数与视觉分数的尺度和意义不同。
可以把候选映射到统一证据单元,再用基于名次的 RRF 融合。RRF 解决分数不可直接比较的问题,不保证不同通道同等可靠,也不能修复错配的向量空间。
例如,同一案例图片的 OCR 命中、描述命中和视觉命中,应先识别共同的父图片/证据单元。不能因为保存了三种表示,就算作三份独立材料。
Rerank 也要匹配输入能力:
- 文本 Reranker 可以重排 OCR、描述和转录,但看不到未写进文字的图像细节。
- 对视觉相关性需要支持图文输入的重排方法,或对少量候选做视觉模型核对。
- 对精确型号,可以保留关键词约束,不能让"看着像"压过明确的型号不一致。
融合后不是永远取相似度最高的几条,还要兼顾来源多样性、版本、权限和问题需要的证据类型。
10. 统一数据模型:不是"所有东西都叫 chunk"
10.1 四层身份足够建立基本模型
| 层次 | 含义 | 例子 |
|---|---|---|
| 文档及版本 | 业务管理与授权的主体 | 案例手册第 3 版 |
| 媒体对象 Asset | MinIO 中的原件或衍生文件 | 原 PDF、抽取图片、视频关键帧 |
| 证据单元 Unit | 可独立定位的一段内容 | 某表的一行、一个图片区域、一段视频 |
| 检索表示 Representation | 为查找生成的派生内容 | OCR、描述、转录、不同空间的向量 |
同一 Unit 可以有多种 Representation;同一 Document 可以拥有多个 Asset。来源关系不能只靠文件名猜。
"把 PDF 的图片提取到 MinIO 并替换为 URL"只解决了部分展示问题。还需要知道图片属于哪个文档版本、在第几页、谁有权看、描述由哪个处理版本产生。
10.2 一个简化的数据契约
下面只展示定位与表示的契约;权限关系、对象位置和处理任务由服务端业务表维护,不让模型自行填写。
typescript
type Region = {
referenceAssetId: string;
renditionVersion: string;
bbox: [number, number, number, number];
};
type Locator =
| {
kind: "text_section";
sectionId: string;
}
| {
kind: "page_region";
sourceAssetId: string;
page: number;
region?: Region;
}
| {
kind: "image_region";
sourceAssetId: string;
region?: Region;
}
| {
kind: "time_range";
sourceAssetId: string;
startMs: number;
endMs: number;
};
type EvidenceUnit = {
tenantId: string;
docId: string;
documentVersion: number;
unitId: string;
locator: Locator;
sourceAssetIds: string[];
};
type RetrievalRepresentation = {
representationId: string;
unitId: string;
kind: "body" | "ocr" | "caption" | "transcript" | "visual";
text?: string;
sourceAssetIds: string[];
extractionVersion: string;
atMs?: number;
};
上面的契约省略了向量数据本体。可以把它理解为另一类关联记录:
typescript
type VectorRecord = {
representationId: string;
embeddingSpaceId: string;
vector: number[];
};
这是传输形态;数据库实际使用 pgvector 向量列,并按空间建立表/分区与索引,不是把数组作为普通 JSON 存进去就完成向量检索。representationId 将向量连回 OCR、描述或视觉表示,再通过 unitId 连回来源;写入时验证维度、有限数值和空间配置。
为什么不用一个 source_id 解决所有问题?它可以标识来源文档,但不同时包含"第几版、第几页、哪个区域或哪段时间"。字段名可以不同,语义不能缺。
sourceAssetIds 表示证据涉及哪些媒体对象;它们的真实对象键由可信服务查询。服务端要校验定位合法、Asset 归属正确,不能因为模型给出一个 ID 就直接放行。
Region 采用第 5.5 节统一的归一化坐标约定,并绑定具体预览/派生版本;图像经 EXIF 旋转、镜像或裁剪后,不能把旧坐标直接套在新图上。裁剪区域需映射回父图,视频局部证据还要同时保留原视频时间窗、帧时间戳与帧的派生版本。
这里为便于阅读用了一个简化类型;实际运行时还应校验:文字表示有非空正文,视觉表示引用真实媒体,时间位于对应源时长内,区域满足 0 <= left < right <= 1 和 0 <= top < bottom <= 1。只有 TypeScript 字段还不够。
10.3 版本不止一种,分别防止什么问题
| 版本 | 什么变化会触发 | 防止的问题 |
|---|---|---|
| 文档版本 | 原始业务资料变化 | 引用第 3 版却展示第 4 版 |
| 解析/分片版本 | OCR、ASR、布局或切片方案变化 | 重跑后段落变了但索引仍混用 |
| embedding 空间版本 | 编码器或预处理配置变化 | 不兼容向量混查 |
| 权限修订 | 用户、角色、部门或可见范围变化 | 缓存和旧索引继续暴露资料 |
权限变化不必重做 OCR 和 embedding,但必须及时改变可访问性。相同内容 hash 也不代表可以跨租户共享授权,内容去重与权限是两回事。
10.4 Markdown 在其中到底是什么
Markdown 是统一文本和展示结构的一种格式,不是证据身份系统,也不是媒体数据库。
正确理解是:
text
Markdown:可读的正文、标题、表格和媒体占位引用
结构化元数据:版本、页码、区域、时间、权限与处理来源
MinIO:原始文件和衍生媒体
检索索引:从上述内容生成的可查询表示
检索 embedding 通常编码文本内容,而不是把全部 metadata 一并向量化。权限要作为结构化过滤条件,不能写成"本资料仅财务可见"后期待模型替你执行。
不要把短期签名 URL 长期写入 Markdown:过期后会失效,也可能进入日志。可以保存稳定的媒体引用 ID,由预览服务在鉴权后解析为可用访问方式。
10.5 用一份对象账本把所有 ID 串起来
先统一两篇文章的词:总览的 chunkId 指文本分片;这里的 unitId 是更通用的可定位内容身份。简单实现可以让一个文本分片对应一个 Unit;一个分片若覆盖多个来源单元,则明确保存 supportUnitIds,不能假定父图片、OCR 段和字幕永远是同一个 ID。
下面只取案例 PDF 第 3 版与培训视频第 2 版,列出这次问题用到的教学产物:
| 谁创建 | 产物/身份 | 保存在哪里 | 下一步谁用 |
|---|---|---|---|
| Nest 文档服务 | doc_case/v3、doc_training_video/v2 与当前权限 |
PostgreSQL | Worker、查询与来源服务 |
| 上传/对象服务 | asset_case_pdf、asset_video_original 两个原件 |
MinIO;Asset 归属登记在 PG | 解析 Worker |
| 任务投递器 | ingest_case_3、ingest_video_2 |
持久任务记录及队列 | 后台 Worker,不是主 Agent |
| 解析 Worker | 一张案例图片、asset_frame_734000、asset_frame_743000 |
MinIO;父对象/处理版本登记在 PG | 视觉编码、原图核对和预览 |
| 入库服务 | unit_table_materials_row_1、unit_case_photo_1、unit_video_step_7 |
PG,分别带页/区域/时间 | 检索与来源服务 |
| 解析/入库服务 | rep_case_table_1、rep_photo_caption_1、rep_video_transcript_1 等 |
PG 的正文/表示记录 | embedding、全文索引 |
| 索引 Worker | 表示 ID + 空间版本 + 向量;相同来源的全文条目 | pgvector、ES | 授权范围内检索 |
| 本次问答协调器 | run_q42、evidence_set_q42 |
本次运行状态;需要历史则持久化 | 主/子 Agent、最终回答 |
| 证据/引用服务 | c42_case、c42_video 到确定版本与 Unit 的映射 |
答案的来源快照 | 前端点击引用 |
表里没有规定每个段落都变成一个磁盘文件。正文和 Markdown 可以是 PG 字段,队列任务、向量行、SSE 事件也不是文件。在这个刻意简化的媒体清单里,MinIO 是 2 个原件 + 3 个抽取图片/帧,共 5 个对象;若保留音轨、页图或转码,还会增加对象。一次任务没有固定"生成几个文件"的通用答案,应该按保存策略列产物清单。
普通问答主要创建运行记录、证据快照和最终答案;不会每问一次都复制整份 PDF 和视频,也不必总生成 Markdown 报告。Agent 工作笔记是否落文件,是另外的产品决定。
再跟着一条具体命中走:
text
索引命中 rep_video_transcript_1
-> 读取表示记录:正文 + unit_video_step_7
-> 读取 Unit:doc_training_video / v2 / 730000..754000ms
-> 权威服务检查:该版本是否用于本次检索、用户是否可见
-> 封装本次 Evidence:正文、定位、必要媒体引用
-> 登记到 evidence_set_q42,分配 c42_video
-> 主 Agent 使用正文与引用 ID,不接收 MinIO 内部键
-> 点击 c42_video,来源服务重新授权并打开 v2 对应时间段
这是服务端逐步查询和校验的结果,不是模型从 source_id 自动推导出来的。检索后回查属于业务服务调用,通常不需要为每一步再调用一次 LLM。
最后要区分两种版本规则:**新检索选当前已发布资料;历史答案按当时版本回看。**历史引用是否仍可打开,还取决于当前权限与历史保留策略,不应因为已发布新版本就悄悄把旧引用改指新版本,也不意味着任何旧版永远可访问。
11. 一次多模态问题具体怎么执行
11.1 先判断需要哪种查找,不是直接决定答案
对第 3 节的问题,可以形成这样的任务分解:
| 子问题 | 首选工具/证据 | 不应采用的捷径 |
|---|---|---|
| 照片接近哪个案例 | 视觉候选或图片描述检索,再关联案例 | 根据照片外观直接认定业务身份 |
| 视频怎样讲这一步 | 转录/描述召回时间窗,必要时检查画面 | 把整段视频摘要当逐秒记录 |
| 材料供应商是谁 | 材料表或有原文来源的图谱关系 | 从包装颜色猜供应商 |
| 售后怎么解释 | 当前有效制度,培训资料辅助说明 | 默认口述培训比制度更权威 |
用户上传的查询图片也应通过受控上传服务接收。它可能只在当前会话临时使用,不应默认进入公司公共知识库,也不能默认写入长期记忆。
工具参数可以允许模型提供问题和已授权的查询图片 ID。租户、用户、权限与预算由服务端请求上下文提供,不能让模型自己填写 tenantId 或任意对象 URL。
11.2 检索流程可以怎样组织
text
prepare:验证查询图片归属,绑定可信身份和任务预算
-> route:判断文本、视觉、关系查询需求
-> recall:
文本混合召回
按需视觉召回
按需受控图谱查询
-> group:按共同来源/证据单元合并,限制重复帧
-> authorize:复核候选当前版本与可见性
-> rerank:使用匹配模态能力的排序方法
-> inspect:必要时加载原图/表格/视频窗口核对
-> check:
证据足够 -> 返回证据包
有明确缺口且预算允许 -> 定向补查
无依据或预算耗尽 -> 返回已确认部分与缺口
这是 LangGraph 可以显式管理的业务状态流。每个节点仍需调用实际检索、权限或媒体服务,画出图并不等于这些功能已经自动存在。
向量、ES 和图谱后端查询本身就应绑定租户和可见范围;这里的 authorize 是应对权限变化、旧索引和组合证据的复核,不是先全库读取并发给模型,再用最后一道过滤补救。
也要区分安全与召回:数据库查询有正确权限约束,不代表近似向量索引在高选择性过滤下仍能召回足够候选。需要根据 pgvector 的版本、索引和查询计划验证,可增加受控候选量或在适当数据规模使用精确检索;不能为补足数量而取消权限过滤。
11.3 证据包返回什么
不把整个原视频、所有截图、所有 ASR 文本和子 Agent 的完整对话都回传给主 Agent。更合适的结果是:
text
已确认:
案例候选:海棠 A,依据图片相似与案例说明;仍需业务信息核对
供应商:材料表中记录为供应商甲
视频:相关讲解位于第 2 版 12:10 至 12:34
售后:以制度第 5 版相关条款为准
附带:
可引用的证据 ID、文档版本、定位
必要的小段原文或媒体引用
证据类型与不确定之处
尚未解决的问题
"只回传结果"不是把所有证据也删除。主 Agent 需要足够依据,才能忠实整合回答;不必接收的是无关原文集合和子任务执行噪声。
如果最终回答还需要原图理解,就提供经授权的必要原图或受控媒体引用;如果转录已足够回答流程问题,则无需再加载视频。
11.4 为什么既做 RAG,又保留来源读取
RAG 的工作是从大量资料中找少量候选。来源读取的工作是对已定位资料做局部展开或核对。
这和你之前问"既然有 source_id,为什么还要读文件"是同一个问题:
- 结果正文足够:直接作为回答依据,不重读原件。
- 表头缺失或条款不完整:按版本补取相邻结构。
- 用户问图片局部:读取相关原图区域,而不是只看描述。
- 用户要求视频演示:依据时间段读取相关画面,或者提供播放器定位。
source_id 负责追溯关系,不会凭空携带全部原文。MinIO 负责保存媒体,不负责从海量媒体中按问题筛选。
DeepAgents 的工作区 read_file 主要用于任务笔记等受控文件访问,不能被当成绕过业务权限、随便读取 MinIO 对象的工具。知识来源应通过受控的来源服务访问。
11.5 最后怎样生成一个可信引用
模型可以选用服务端提供的证据 ID,但不能自由编出"第 8 页"当事实。服务端维护证据 ID 到文档版本、页码/区域/时间段的映射,并校验回答使用的引用确实来自当前证据集。
前端点开引用时:
text
点击 citationId
-> 来源服务识别用户
-> 校验引用归属、资料权限与版本保留规则
-> 解析为页图/原图/播放器定位
-> 返回受控预览或短期访问方式
答案依据第 3 版,预览也必须能回到第 3 版;不能悄悄打开第 4 版。若历史版本已按规则删除或禁止访问,应明确提示来源不可用,而不是跳到一个看似相同的新位置。
11.6 "把画面送给模型"究竟传了什么
这是文本 RAG 转向视觉核对时最容易漏掉的一步:**把一串 MinIO URL 写在普通文本里,不等于模型真的收到了图片像素。**视觉向量也不能代替图片作为视觉模型输入。
一个受控的 inspect_media 工具可以只让模型提出:
json
{
"unitId": "unit_video_step_7",
"question": "这个时间窗的画面能确认哪些可见操作?"
}
服务端根据 Unit 查出视频版本与时间范围,而不是接收模型指定的任意 URL。帧数、像素、时间窗上限来自可信策略和当前预算,不由模型随意扩大。
text
inspect_media(unitId, question)
-> 授权、版本与可用能力检查
-> 确定本次实际读取的帧/区域,原子预留额度
-> 媒体服务读取并校验,必要时裁剪、缩放
-> provider 适配器组装真正的多模态消息
-> VLM 返回观察与限制
-> 校验引用帧属于本次实际输入,补回可信来源字段
例如服务端已授权并加载两张帧图,得到带 frameId、atMs 和 dataUrl 的小对象。对支持该消息格式的 LangChain 聊天模型,可以这样组装输入:
javascript
import { HumanMessage } from "@langchain/core/messages";
function buildVisualMessages(question, authorizedFrames) {
const content = [{ type: "text", text: question }];
for (const frame of authorizedFrames) {
content.push({
type: "text",
text: "frameId=" + frame.frameId + "; atMs=" + frame.atMs,
});
content.push({
type: "image_url",
image_url: { url: frame.dataUrl },
});
}
return [new HumanMessage({ content })];
}
这里的 dataUrl 由可信媒体服务从已校验的图像字节构造,不是用户随意提供的地址。真正调用由配置好的视觉模型执行,例如把这些 messages 交给其 invoke,并传入受支持的取消信号。示例只展示消息组装,不执行付费调用,也不说明所有 provider 都支持相同格式;格式和模型能力需由适配器验证。
对图片模型,这代表"按顺序看了这些帧",不代表看完原视频。输入还应包含实际覆盖范围与问题约束。若用短期 URL 而非内联字节,仍要处理供应商能否下载、过期、外部数据许可和日志泄露;不能把 URL 放开权限来凑通流程。
输出也要保留链路:
| 字段 | 谁决定 | 含义 |
|---|---|---|
observations |
模型生成,服务端校验结构 | 仅描述证据支持的可见内容 |
supportingFrameIds |
模型选择,必须是实际输入帧的子集 | 每项观察依据哪些画面 |
limitations |
模型/规则共同产生 | 看不清、未覆盖、不能确认的内容 |
unitId/version/atMs |
服务端根据帧映射补回 | 不让模型编造来源位置 |
model/preprocessVersion |
调用器记录 | 哪个模型和预处理产生了观察 |
结构校验只证明字段合法,不证明观察一定正确。VLM 输出属于派生判断,不会因为带帧 ID 就自动升级成原始业务事实。主 Agent 最终拿到精简观察、来源和限制,不必再次接收同一组大图;确需独立核对时,再通过相同受控工具读取。
任务取消、预算耗尽、局部超时都应返回可区分状态,不能统一返回空数组再让主 Agent 猜。完整结果状态和流式终止约定,见总览第 5.3、9.7 节。
12. 企业级入库:长任务要可恢复,不是一个上传接口包办
12.1 为什么需要后台任务
几页文字和两小时视频的处理时间、资源消耗完全不同。让 HTTP 请求一直等待,容易超时,也很难重试。
合理的职责划分:
text
上传接口:接收、校验、登记、确认原件已保存,返回任务 ID
后台执行器:解析、转码、OCR/ASR、索引
查询接口/事件:报告任务状态与失败原因
上传接口返回成功,最多代表资料已接收,不代表知识已经可检索。
12.2 状态和发布条件要分开
例如,版本处理状态可以是:
text
RECEIVED
-> PROCESSING
-> 质量检查 + 必要索引检查
-> READY
-> PUBLISHED
某一步失败 -> RETRY_WAIT / FAILED / NEEDS_REVIEW
删除或撤回 -> REVOKED
同时记录每个步骤的状态,而不是只留一个"失败":
text
音轨提取:成功
ASR:成功
关键帧处理:失败
文本索引:成功
视觉索引:未完成
此时可以允许"转录文本可检索",但前提是产品明确支持这种降级,并记录当前能力;不能显示"视频理解完成",更不能对画面问题假装有依据。
发布条件随资料用途定义。例如视觉搜索必需的视觉索引失败,则不能声称该版本支持以图搜图;图谱若只是增强通道,可以允许正文检索发布而将关系能力标为未就绪。
12.3 重试为什么不能重复建一堆分片
队列通常不能保证任务永远只执行一次。网络中断、超时和 Worker 重启都可能造成重复执行。
任务和产物要有稳定身份,可以用下列信息组合生成幂等键:
text
租户 + 文档 ID + 文档版本 + 处理步骤 + 配置版本 + 输入摘要
重复任务应复用已成功的同一输入产物,或按确定的产物清单覆盖/更新,不能每重试一次就随机新增一套 chunk。
这不是简单的"发现文件存在就跳过":文件可能写了一半,配置可能已经变更。成功状态、校验摘要和产物清单必须能够相互对应。
如果文档状态写入成功、投递队列失败,就可能永远不处理。可以把文档状态和待投递任务事件放在同一 PostgreSQL 事务中,再由投递器可靠发送;重复投递交给消费幂等处理。
12.4 更新、删除与旧任务并发时怎么办
假设第 3 版视频还在处理,用户上传第 4 版。第 3 版任务稍后完成,不能把系统重新切回第 3 版。
可以为一次处理建立运行代次和产物清单。完成时检查预期文档版本/代次,发布时由数据库条件更新确认当前允许发布的是哪一代。任务的锁过期后,旧 Worker 也不能靠迟到的写入覆盖新结果。
删除同理:
- 先在权威数据库将资料标为不可检索、不可预览。
- 取消或失效相关处理任务。
- 查询与任务发布都检查撤回状态。
- 再异步清理 ES、向量、衍生图片、转码和缓存。
撤回要立即阻止新访问;所有磁盘、备份和外部服务中的物理删除是否完成,是另一个受保留策略与审计约束的问题,不能混为一谈。
12.5 为什么不能用一个事务解决四个存储
PostgreSQL 事务可以覆盖业务表与本库的 pgvector 数据,但不会自动覆盖 ES、Neo4j 和 MinIO。
因此要有权威版本状态、可恢复任务和明确发布条件。在检索返回前核对当前有效版本,清理残留数据可以异步;不能把"ES 里还能搜到"当成可以使用的依据。
如果用单路检索作为降级模式,也要明确是哪种能力缺失。降级不等于无条件发布一份索引不完整的新版本。
13. 权限与隐私:原文件私有还远远不够
13.1 权限必须沿着衍生关系传递
原合同不可见,它的 OCR、表格文本、图片描述、关键帧、摘要、图谱关系也不能突然变成公开资料。
text
受限原件
-> 受限 OCR / 转录 / 描述
-> 受限向量与全文条目
-> 受限缩略图、裁剪图、转码视频
-> 受限引用与缓存
不一定要在每个对象上复制完整 ACL,也可以保存父文档关系,由权威权限服务解析;关键是所有访问路径都能找到正确授权主体。
若一份摘要综合了多个来源,不能简单取来源权限的并集,让能看其中一份的人看到全部摘要。要么要求用户对全部使用内容都有权限,要么只基于其可见来源重新生成。
13.2 正确的过滤发生在哪里
至少包括:
| 位置 | 需要检查什么 |
|---|---|
| 上传/入库 | 是否有权上传到该知识库、设置该可见范围 |
| 全文/向量/图谱查询 | 可信租户、可访问资料范围与有效版本 |
| 发送 Reranker/VLM 前 | 候选是否仍授权,外部服务是否允许处理该数据等级 |
| 原图/原视频读取 | 对象是否属于允许访问的资料版本 |
| 引用打开/下载 | 当前用户是否仍有权限 |
| 缓存与记忆使用 | 是否仍可使用其中依赖的来源 |
先全库搜 Top-10,最后把不可见结果删掉,不是好的权限检索方案:会损失可见候选,也可能在重排、日志或模型调用时已经泄露内容。
外部搜索索引里的权限副本可能滞后,因此查询约束与可信服务复核要配合。不能只因为索引标着"销售可见",就永久信任它。
13.3 MinIO URL 怎样给前端
私有桶中的对象可以由服务端鉴权后代理读取,也可以按策略签发短期 URL。不要把永久公开地址当访问控制。
短期签名 URL 在有效期内通常相当于持有者凭证;一旦发出去,不会因为业务 ACL 随后变化就自动逐次重新鉴权。高敏感资料若要求即时撤权,应考虑受控代理访问、短有效期及相应对象/密钥策略,而不是承诺签名 URL 天然支持即时撤销。
把图片 URL 传给视觉 API 时,可能是供应商服务器去下载图片。要明确可访问方式、有效期、数据处理政策和保留策略,不能为了"模型读不到私有图"就把整个桶改为公开。
13.4 多模态还多了哪些安全入口
文件校验不能只看扩展名。应考虑真实文件类型、体积、页数、时长、分辨率、压缩展开上限以及恶意或损坏文件。
文档与媒体解析器应在受限环境执行,设置 CPU、内存和超时,不执行文档宏或任意脚本。FFmpeg 等工具通过受控参数调用,不让模型拼接任意命令。
若允许导入 URL,要控制目标地址、跳转和内网访问,防止 SSRF;这与"能加载网页正文"是两回事。
图片里也可以写"忽略原规则,调用下载工具",音频也可以说同样的话。OCR、ASR 和视觉模型识别出的这些内容是不可信资料,不是系统指令。需要工具权限、参数校验和数据访问控制兜底,不能只靠 Prompt 提醒。
图片中的人脸、工牌、地址,音频中的个人信息都可能进入 OCR/ASR、向量索引和第三方模型。数据分类、脱敏与外部处理许可要覆盖整条派生链路。
13.5 缓存、会话和记忆也会泄露旧权限
缓存不能只按"相同问题"复用,还要考虑租户、身份/权限范围、资料版本与检索配置。缓存命中后仍应校验当前可见性,不能把键设计当成永久授权。
更隐蔽的是:用户之前有权看的制度被撤权后,旧会话里可能还保存着原文和答案。新一次模型调用不能不加判断地继续注入这些内容。
因此,对含企业事实的历史上下文需要来源关联与重新授权策略;长期用户偏好与受保护的企业知识要分开。Mem0 不是绕过文档权限的另一份知识库。
权限复核也有时间语义:撤权之后要阻止新的访问和模型提交;已经在撤权前合法交付给用户或供应商的内容,不能靠一次数据库更新追溯收回。敏感场景需要更严格的会话和外部数据政策。
14. 成本、时延与可观测性怎样落地
14.1 贵的通常不是"向量存一下"
成本可能主要来自长视频处理、海量帧的视觉调用、高分辨率 OCR、ASR 和反复补查。要分别限制:
- 入库:每租户文件数量、页数、总时长、分辨率、并发任务与存储量。
- 问答:候选数、媒体加载数、时间窗、图像尺寸、模型 token 与工具次数。
- 后台:解码资源、API 速率、重试次数与排队量。
不能只统计 modelCallCount:一次纯文本调用和一次包含多张高分辨率图片的调用,资源和价格可能差很多。
14.2 怎样控制一次请求的总预算
主 Agent、子 Agent 和检索图共享一次任务的预算。每个可能产生费用或重资源消耗的操作前,由可信服务原子预留额度;失败则返回明确的预算耗尽结果。
图结束时看一下"是否超额",不能防止多个并发子任务一起越界。预留后还需要根据实际用量结算和处理失败,具体金额取决于供应商计费,不能假定所有失败调用都免费。
视频问题先召回时间窗,再按需读帧,就是一种合理的预算分配;但节省成本必须与覆盖范围一起说明,不能把少看画面包装成理解能力不变。
14.3 Trace 应该记录哪些信息
text
requestId / ingestionJobId
用户范围标识与权限修订,不记录凭证
文档版本、证据单元 ID、处理版本、embeddingSpaceId
各检索通道耗时、候选数、选中来源
实际查看的图像区域/视频时间窗
模型、工具、重试、失败原因、usage 与估算成本
最终引用与未能回答的缺口
Trace 记录调用和数据流,不等于公开模型的内部思考过程。
LangSmith/Langfuse 的 SDK 或回调负责采集运行信息;pytest 等测试工具负责检查事件字段和流程是否符合预期,不是 pytest 自己替 Agent 生成完整运行轨迹。
DeepAgents 中间件可以统一记录模型/工具调用边界和任务预算事件。入库任务则是另一类后台 Trace,靠文档 ID、版本和任务 ID 关联,而不是把两个生命周期硬装成一个 Agent 对话。
日志默认不应包含全量原件、签名 URL、音频、人脸图像和用户隐私。记录 ID 能追溯,不代表要把整个企业知识库复制到外部观测平台。
14.4 超时、中断和降级
用户取消问答,应传播到尚未完成的检索、媒体加载和模型请求;已产生的第三方费用未必能取消。后台入库是否随页面关闭而取消,应是独立业务决策。
视觉服务不可用时,可以回答已有文字证据能支持的部分,并说明无法核对画面。不能用"未找到问题"代替"没有完成检查"。
流式输出也不应先输出未经证据支持的关键结论,随后再补引用。可以先发送处理状态;正文结论应依据已获得的证据生成。
15. 怎样评估多模态,才不是只展示一个成功案例
15.1 评估要分层,否则不知道该修哪里
| 层次 | 关注什么 | 可用的检查方式 |
|---|---|---|
| 入库解析 | 字、表、声音是否正确转换 | OCR 字符错误、表格单元格/单位检查、ASR CER/WER |
| 定位 | 是否指向正确位置 | 页码、区域、时间范围与标注比对 |
| 检索 | 是否找到有用证据 | Recall@K、MRR/nDCG、跨模态候选的人工相关性标注 |
| 回答 | 是否依据证据、回答了问题 | 忠实度、正确性、帮助性与引用核对 |
| 安全与生命周期 | 是否越权、引用旧版、残留可访问 | 自动化拒绝测试、版本更新/撤权/删除场景 |
| 资源 | 是否符合使用成本与时延约束 | 分模态时延分布、费用、失败率与重试量 |
CER/WER 用于文字/语音转录质量,但中文分词和数字规范化会影响结果,要固定计分规则。单个工具给出的"置信度"也不一定可跨模型直接比较。
表格不仅看识别了多少字符,还要看数字有没有配错行、单位是否保留。定位也不只是"来源文件对了":视频文件正确、时间跳错,同样不好用。
15.2 怎样建立一个有用的数据集
不要只问"这份文档说了什么"。可以覆盖:
| 用例 | 应该验证什么 |
|---|---|
| 用同义词找制度 | 文本语义检索 |
| 用精确型号找图片 | OCR 与关键词约束 |
| 上传照片找相似案例 | 视觉检索,区分相似与同一身份 |
| 查询表格数字 | 表头、单位、结构和来源 |
| 查询录音中某个术语 | ASR 术语与时间定位 |
| 问只在画面里出现的信息 | 不能只靠音轨转录 |
| 问抽帧间隙的短暂动作 | 检验覆盖局限与"不确定"回答 |
| 问无权限图片中的内容 | 过滤、媒体读取与引用访问均拒绝 |
| 旧培训与新制度冲突 | 版本与权威来源选择 |
| 删文件后问同一个问题 | 旧索引、缓存与历史记忆不能复活资料 |
数据集记录问题、查询模态、测试身份范围、相关证据、正确页/区域/时间、参考回答或预期拒绝原因。一个问题可能有多组有效证据,不应假定只有唯一的一条 chunk 才算正确。
检索 Recall@K 的分母应是标注中定义的相关证据集合,不是随意把"库里总文件数"放进去。对视频定位可以参考时间区间重叠等指标,但时间重叠不等于内容相关,仍需核对语义。
15.3 LangSmith/OpenEvals 在哪里发挥作用
可以把"应用收到问题,输出答案和证据包"封装为评估 Target,使用固定数据集运行不同检索配置。
文本实验比较 chunk、召回、融合和重排;视觉实验还要固定模型、图片预处理、抽帧策略与时间窗。记录配置版本,不然两次分数不具有可比性。
评委也要看对材料:
- 只给文字的评委,可以判断回答是否符合转录或描述,但不能核实原图有没有那个细节。
- 判断视觉忠实度,需要能处理对应媒体的评估器或人工复核。
- 检查越权,主要依据确定的权限策略与自动断言,不靠 LLM 评委"觉得没有泄露"。
你的学习示例使用文本 context 的相关性、忠实度与帮助性评委,原理可以复用;它不自动成为多模态评估器。
"离线评估"指在固定测试集上跑实验,不等于断网、不调用真实模型;"在线观测"指记录运行行为,也不自动证明回答正确。二者解决不同问题。
15.4 一个失败如何归因
问题:"视频里有没有展示对管根的处理?"
text
没有召回该视频:
检查转录/描述是否包含主题、索引与路由是否正确。
召回了视频,但只有音轨:
检查画面解析是否完成,是否需要视觉通道。
取到了相关片段,却没取到短暂动作:
检查抽帧和时间窗覆盖,不先怪回答 Prompt。
模型看到了画面仍编造:
检查视觉能力、证据约束与不确定性策略。
答对了但引用跳错:
修时间偏移和来源映射,不把它统计成完整成功。
如果没有真实实验结果,就讲评估维度、数据集与归因方法,不编造"准确率提升多少"。一个好故事不需要虚构数字来支撑。
15.5 面试官说"用户反馈答案错了",按什么顺序排查
先找 runId,再按数据流倒推,不先改 Prompt:
| 检查顺序 | 看哪些观测信号 | 下一步动作 |
|---|---|---|
| 1. 身份、版本与路由 | 授权范围修订、有效版本、选择了哪些通道 | 分清本就不可见、路由漏选,还是资料未发布 |
| 2. 入库能力与召回 | 解析/视觉状态、各路候选、必要的授权拒绝原因 | 索引不存在先查入库任务;有索引再查 query、分片与召回 |
| 3. 融合与重排 | 各路原排名、统一 ID、RRF 后与重排后的位置 | 查是否丢掉重复项的贡献、帧占满候选或精排误判 |
| 4. 实际媒体输入 | 实际加载的帧、区域、时间窗与预处理版本 | 区分"没看见证据"和"看见却理解错了" |
| 5. 最终证据与答案 | evidenceSetId、结论对应引用、终止状态 |
查资料冲突、生成越界、引用错位或中断误标成功 |
这些观测供授权维护者排障,不向普通用户展示隐藏资料的名称、存在性或拒绝计数。排查发现权限过严,应修正业务规则,不能临时去掉过滤以换取更多召回。
例如"视频文字能搜到,画面核对一直为空":先看视觉处理/发布能力,再看是否路由到视觉工具和读取了哪些帧;若视觉索引从未就绪,去查 Worker 的处理任务,而不是让主 Agent 反复重试同一次检索。
16. 设计模式与 Agent:哪些抽象真的有帮助
16.1 格式适配器:不同解析器,统一产物契约
PDF、音频和视频的底层处理完全不同,但上层可以接收统一的定位和文本产物。
下面是最小的接口设计示意,不包含任何真实 OCR/ASR 客户端,也不执行模型调用:
typescript
type MediaKind = "document" | "image" | "audio" | "video";
type ParseInput = {
tenantId: string;
docId: string;
documentVersion: number;
assetId: string;
kind: MediaKind;
};
type ParsedUnit = {
localId: string;
locator: Locator;
sourceAssetIds: string[];
representations: Array<{
localId: string;
kind: RetrievalRepresentation["kind"];
text?: string;
sourceAssetIds: string[];
extractionVersion: string;
atMs?: number;
}>;
};
interface MediaParser {
parse(input: ParseInput): Promise<ParsedUnit[]>;
}
function createParserRouter(
parsers: Record<MediaKind, MediaParser>,
) {
return {
parse(input: ParseInput): Promise<ParsedUnit[]> {
return parsers[input.kind].parse(input);
},
};
}
各适配器内部调用自己的文档解析、OCR、ASR 或视频服务。统一接口减少入库主流程对具体供应商的依赖,但不会消除不同模态的质量检查和资源限制。
例如一个图片 Unit 可以同时返回 OCR、caption 和 visual 三个表示;视频 Unit 可以返回转录与多个帧描述。不能只返回一个不注明来源的 text,让后续模块猜它是什么。
解析器先返回本次产物的局部身份;入库服务核对 Asset 归属、定位和格式,再按文档版本、处理代次与局部身份分配稳定的 unitId/representationId。向量 Worker 只消费已登记的表示,按明确空间生成向量;不是由每个解析器各自随机写 ES 和 pgvector。
顺序上,sourceAssetIds 只能引用已登记的稳定 Asset。派生帧或裁剪图先由对象/入库服务登记,再写入正式 Unit/Representation;若解析器尚未保存派生物,应返回局部 localAssetRef 与产物清单,由入库服务先建立 Asset、再解析引用,不能用临时路径伪装正式 Asset ID。
实际代码还需校验输入类型、检查文件授权、验证返回定位,并处理超时和失败;不能把 TypeScript 类型检查当成对不可信输入的运行时验证。
路由不是策略本身,具体如何选择
上面的 Router 只查注册表,逻辑上类似一个集中管理的 switch(kind),不必把这一行夸成复杂设计模式。真正可替换的是各解析器、分片和媒体检查策略:
| 输入信号或问题需求 | 选择什么处理 | 失败或不确定时怎么办 |
|---|---|---|
| PDF 页面有可靠文字层和结构 | 原生文字/结构解析 | 抽取质量异常的页转版面/OCR,不重复处理全部页 |
| 扫描页、乱码或阅读顺序异常 | OCR + 版面恢复 | 记录低质量页,必要时人工复核,不冒充全文可靠 |
| 用户问表格精确字段 | 带表头召回,再核对单元格与单位 | 结构不可靠时回看原表或说明不能确认 |
| 图片主要是型号/标识 | OCR + 关键词约束 | 错别字核对原图,语义相似不替代精确值 |
| 用户要相似外观/以图搜图 | 兼容视觉编码与视觉索引 | 不可用时可用描述作有限替代,明确能力降低 |
| 用户问视频讲解原话 | 转录检索与时间定位 | ASR 不可靠时回听/核对,不无条件升级所有视频处理 |
| 用户问画面动作或局部 | 时间窗召回后受控视觉检查 | 覆盖不足就扩展或说明缺口,不由未观察推出不存在 |
"文字层可靠"需要用样本文档检查抽取覆盖、异常字符、阅读顺序、表格等信号,阈值来自自己的测试集,不在没有实验时编一个通用数值。所选能力还要进入发布清单:文字可用不代表视觉也已就绪。
新增解析供应商时,主要改对应 Adapter 和配置;改变"什么情况下调用它"时,改选择策略与测试;Agent 继续调用同一个业务工具。代价是要维护统一产物契约和能力状态,所以不应为了模式名称增加无实际替换需求的层级。
16.2 常见模式怎样对应具体职责
| 模式/架构方式 | 放在哪里 | 真实收益 |
|---|---|---|
| Adapter 适配器 | 不同解析器、embedding 和模型供应商 | 隔离接口差异,统一业务契约 |
| Strategy 策略 | 文本/视觉检索、分片和抽帧选择 | 根据问题与资料选择处理方法 |
| Registry/Factory 路由 | 依据受信文件类型选解析器 | 主流程不散落供应商判断 |
| Pipeline/状态机 | 入库与 LangGraph 检索步骤 | 显式状态、失败恢复、条件分支与退出 |
| Middleware 包装/拦截 | Agent 模型和工具调用边界 | 统一观测、预算与受控校验 |
| NestJS 依赖注入 | 将业务服务与客户端组装 | 测试时替换适配器,配置与业务分离 |
这些是针对职责的解释,不是"用了框架所以自动实现了所有模式"。没有真实替换需求时,用简单函数和清晰接口即可,不必为每个格式造一套复杂继承体系。
16.3 DeepAgents 中间件到底参与哪一段
它可以在模型/工具调用前后做日志、计数、预算预留、调用限制和结果包装;针对媒体工具,还可以记录"本次读取了哪张图片、哪一段时间窗"。
但三个边界要分清:
- 中间件不负责替代 OCR、ASR、视觉编码器。
- 中间件可以调用权限服务做统一拦截,但权限事实和最终数据约束仍由可信业务服务掌握。
- 中间件不会自动把默认共享文件系统变成每个子 Agent 的隔离工作区。
因此,不要说"我用一个日志中间件就实现了企业级多模态安全"。它是统一横切能力的挂载点,不是所有业务机制的替代品。
16.4 LangGraph 为什么不是再造一个主 Agent
DeepAgents 本身建立在 LangGraph 之上。这里再用业务检索图,是为了把"文本/视觉路由、召回、融合、证据检查、有限补查"封装成清晰工具。
主 Agent 决定任务需要什么;检索图按显式规则执行查证。主 Agent 可以直接调用检索工具,复杂任务才按需委派子 Agent。
不用为 PDF、图片、音频、视频各造一个 Agent。大量入库工作是确定性的文件处理任务,普通 Worker 与解析服务更容易控制成本、重试和测试。
16.5 什么时候子 Agent 才有价值
例如,同时研究"案例材料来源""售后制度变化""培训演示依据"三条相对独立的问题。每个子 Agent 接收必要问题与依赖结果,使用同样受权限约束的工具,最后返回精简结论、引用和缺口。
每个格式一个子 Agent,并不天然比统一工具更好。是否委派由任务复杂性与上下文需求决定,不由文件后缀决定。
主、子 Agent 的独立聊天上下文能减少无关历史污染;工作区隔离和文档访问权限还需要分别实现。上下文隔离不是权限隔离,也不意味着主 Agent 失去必要的来源读取能力。
17. 对照简历:每句话要解释到哪一层
当前简历的多模态条目是:
接入文档解析、OCR / 图片理解、ASR 与视频分片,将异构资料统一为 Markdown 检索文本;以 MinIO 管理原文件与图片,保留文档、分片及附件的来源关联。
它的核心是多模态资料文本化入库、原件管理与来源关联。这句话本身没有承诺已经完成任意以图搜图、音频事件检索和视频完整动作审核。
| 简历词语 | 面试要能解释的内容 | 不要顺手夸大的内容 |
|---|---|---|
| 文档解析 | 文字层与扫描页、结构和分片、表格处理 | 所有 PDF 都能无误恢复 |
| OCR / 图片理解 | 读字与画面描述分工,保留原图核对 | 只靠照片判断材料品牌与工程合规 |
| ASR | 转录、分段时间、术语错误和原音频定位 | 自动准确识别人名与所有声学事件 |
| 视频分片 | 音画时间对齐、取帧、片段表示和覆盖范围 | 抽几帧就完整理解所有动作 |
| Markdown 检索文本 | 统一文本产物,metadata 单独结构化 | Markdown 自动提升检索准确率 |
| MinIO 管理原件 | 私有对象、衍生媒体、受控预览 | 对象 URL 天然安全且能即时撤销 |
| 来源关联 | 文档版本、Asset、Unit、页/区域/时间 | 一个 source_id 解决所有定位 |
如果进一步讨论图文双编码器和视觉向量,要明确说这是另一条检索链路,并讲清实际模型和验证依据;不能用"多模态"三个字自动推导所有能力已经具备。
学习时可以完整理解架构,面试时按能拿出代码、示例与验证的部分陈述。这里的技术解释不是把未集成模块自动变成既成事实。
需要准备"哪段代码能展示、它证明了什么、还不能证明什么"时,统一参考总览第 11.5 节的证据表,不把本文的模型输入示意、SQL 或离线练习混称为新平台真实集成结果。
18. 面试怎么介绍,怎样应对追问
18.1 一分钟介绍主线
下面是架构原理的口述模板,具体采用了哪些实现,要与你能展示的证据一致:
我把多模态知识库分成入库和问答两条链路。入库时,文档保留标题、表格和页码;图片用 OCR 和描述形成检索文本;音频做带时间的 ASR;视频把音轨转录、画面与时间段关联。原件和衍生媒体放在 MinIO,PostgreSQL 保存版本、权限和定位,文本进入 pgvector 与 ES。
问答先在用户权限内召回,再融合和重排。文字证据足够就直接回答,涉及画面细节时才按定位加载原图或视频窗口核对。跨模态向量检索需要兼容的图文编码器,不是把图片存进 pgvector 就自动获得。
企业级的重点还包括派生内容继承权限、长任务可恢复、更新删除不串版本,以及按解析、检索、回答和引用分层评估。
不是每次都要一口气讲完整篇。先交代主线,面试官追问哪一段,再用本项目的数据流展开。
18.2 "都转成文字,那为什么叫多模态?是不是伪多模态?"
先答:
多模态资料入库与直接跨模态检索是不同能力。图片、音视频经过 OCR、描述和转录进入文本 RAG,是有效的多模态知识管理方式,但会有信息损失。我不会把它等同于完整视觉检索。
追问"损失怎么弥补":
保留原始媒体与页、区域、时间定位。文本用于高效召回;需要视觉细节时,再读取原图或片段核对。以图搜图则另用合适的视觉空间。
关键不是争论名词,而是明确系统到底支持哪类问题、哪类问题仍有限制。
18.3 "为什么不用一个多模态大模型直接读所有文件?"
小规模临时问答可以直接读,知识库面对重复查询和大量资料,需要避免反复解析与超长上下文。索引负责筛选,原始媒体负责核对。这样也更容易做权限过滤、版本管理、引用和成本控制。
若对方继续问"直接长上下文不也行":
它是可选方案,要按资料规模、查询次数、成本、时延与证据定位比较;不是 RAG 永远更好。但它不会自动解决授权、更新和原件追溯问题。
18.4 "图片 OCR 和图片描述有什么差别?"
OCR 主要读文字,例如型号和标识;描述提取可观察的场景和对象关系,便于语义搜索。两者都可能遗漏信息,所以保留原图。精确型号优先核对文字,不能只凭概述。
追问"VLM 不是也能 OCR 吗":
可以,具体工具可能同时提供这些能力,但精确文字、表格结构、位置和成本表现仍需验证。逻辑职责分开,不要求一定用两套供应商。
18.5 "文本向量和图片向量能不能直接一起搜?"
只有模型保证处于兼容空间才可以。维度相同不代表语义坐标相同。正文向量和视觉向量需要明确模型、版本、预处理与距离度量;不同空间分别召回,再按来源融合。
追问"pgvector 怎么做":
它负责保存向量和距离检索。我会按空间建立明确的索引边界,并记录 embeddingSpaceId;换模型先重建新空间、评估再切换,不把不兼容向量混进旧索引。
18.6 "以图搜图返回第一张,就是同一个案例吗?"
不是。相似度是候选相关性,不是身份认证。先返回候选,再结合案例元数据、可见原图和业务记录确认;仅凭相似照片不能断定供应商、材料型号或具体工程。
追问"找不到完全相同图片怎么办":
可以提供相近候选和不确定性,或者请求补充案例编号等信息,不强行给出唯一身份。
18.7 "视频为什么不只做 ASR?"
ASR 找得到讲了什么,找不到只在画面里出现的操作、幻灯片或标识。所以需要把转录和画面按统一时间轴关联;查询时定位时间窗,必要时进一步读取画面。
追问"每隔几秒抽一帧够不够":
取决于任务。检索培训主题与检查短暂动作不同。稀疏取帧可以降低成本,却可能漏掉瞬时事件;没有看到不能证明没有发生。
18.8 "表格转 Markdown 不就行了?"
简单表格可以,但还要保留表头、行列关系、单位、合并单元格和来源。切片时可以生成带表头的行级文本来召回,精确数字再核对结构化单元格或原表,不能只依据摘要。
追问"跨页表格呢":
需要识别是否延续同一张表并补全上下文;不能确定时标记待核对,不把两页数字直接拼成确定关系。
18.9 "为什么有 RAG 还要读原图、原视频?"
检索表示是压缩后的索引。RAG 负责找相关位置,原媒体用于补足表示未保留的细节。证据足够时不重复读取;需要核对局部或展示出处时才读取。
追问"这是不是先检索再全量读文件":
不是全量重读。应该按版本和定位读取相邻段落、图片区域或有限视频窗口,并受同一权限和预算约束。
18.10 "企业级权限和普通登录有什么区别?"
登录证明身份,RBAC 控制能做什么;数据权限决定能看哪些资料。全文、向量、图谱、OCR、关键帧和预览都要受同一资料权限约束,不能只控制上传和下载接口。
追问"撤权后呢":
权威权限先改变,新检索和来源读取立即按它判断;索引与缓存可以异步清理,但旧权限副本不能继续放行。历史上下文和签名 URL 的有效期也需要相应策略。
18.11 "视频处理中间失败,会不会要全部重新处理?"
不应默认重头来。按文档版本、步骤、输入和配置保存状态与产物清单,成功步骤可以复用,失败步骤按幂等键重试。最后检查发布条件,避免一半新数据、一半旧数据直接参与问答。
追问"更新和旧任务同时完成呢":
完成和发布时检查运行代次与预期版本,条件更新成功才切换有效版本,迟到的旧任务不能覆盖新资料。
18.12 "多模态评估怎么做?用 LLM 打分就够吗?"
不够。先看 OCR/ASR 和表格结构,再看召回、定位与回答。视觉问题要有原图或片段标注,只看转录的文本评委不能验证画面。越权和版本错误用确定性测试,不交给模型凭感觉判断。
追问"LangSmith 起什么作用":
它组织固定数据集、实验运行和对比。评估器决定测什么,Target 决定被测系统;工具平台不会替我定义业务正确性。
18.13 "DeepAgents 和 LangGraph 在多模态里分别干什么?"
DeepAgents 负责规划与受控工具调用,简单问题直接检索,复杂问题按需委派。LangGraph 把检索路由、融合、证据核对和有限补查组织成显式状态流。OCR/ASR/转码主要是普通解析服务,不需要每种格式一个 Agent。
追问"中间件是不是负责多模态解析":
不负责解析算法。它适合在模型和工具调用边界统一挂日志、预算和校验;真正的数据权限和媒体处理仍由可信业务服务执行。
18.14 "你认为这个项目最难的是什么?"
可以选一条真正理解并能展示依据的主线,不要说所有模块都最难:
我认为难点是让不同格式的检索结果都能回到正确、当前且有权限的原始证据。比如视频转录命中了,还要确保时间轴对齐;图片描述命中了,还要能看到同一版本原图;权限变了,OCR、关键帧、缓存和引用也不能继续暴露。
因此我会围绕文档版本、证据单元和来源定位建立统一契约,再做检索和发布,而不是先堆多个模型,最后才补引用。
若被问具体做过什么,就回到实际代码、样例和测试,不用架构模板代替事实。
19. 从口述练习走到免费验证
练习一:型号没有搜到
图片上印着型号,OCR 把字母 O 识别成数字 0。用户按正确型号检索无结果。
你应该能说出:这是解析质量影响精确检索,不一定是 embedding 问题;可以核对原图、纠正识别、重建相关表示,并保留处理版本。描述语义召回可辅助,但不能保证还原精确型号。
练习二:画面有,摘要没有
用户问到图片角落的某个标识,描述只写了整体场景。
你应该能说出:召回后按来源读取原图或局部,不把"摘要没写"当成"图片没有";必要时补充细粒度索引,但不要求每个新问题都全库重解析。
练习三:视频跳错位置
ASR 把第二个音频块里的 00:12 直接作为原视频时间返回。
你应该能说出:缺少块在原视频中的时间偏移;这是定位与时间映射问题,不是回答模型措辞问题。
练习四:两种 embedding 都是同一维度
有人建议把通用文本向量和另一家的视觉向量直接放到一个索引里搜。
你应该能说出:维度是存储形状,不是空间兼容证明;要检查模型对齐与预处理,分空间检索再融合。
练习五:原件撤权了,答案缓存还在
搜索接口已经拒绝资料,缓存命中却直接返回过去的回答,历史会话还带着原文。
你应该能说出:授权必须覆盖缓存与历史上下文;记录依赖来源并复核当前可见性,不能只给检索接口加一次过滤。
练习六:视频没有抽到一个动作
系统只取了少量关键帧,面试官问能否回答"视频里从未出现这个动作"。
你应该能说出:不能把有限采样的未观察当全片不存在。应说明覆盖范围,按任务要求扩展检查或交人工核实,必要时拒绝绝对结论。
先把这六题不看稿讲清,再做下面的跨模块场景和免费实验。会复述不等于已经检查过数据契约。
练习七:视频转录成功,视觉处理失败
用户问"视频画面展示了哪个动作",但后台只有 ASR 成功,帧处理任务失败。
你需要区分四个位置:文档/能力服务报告视觉未就绪;Worker 根据幂等任务重试;检索图不能凭转录假装看过画面;Agent 可以给出转录支持的有限说明,不能通过反复调用检索把失败变成成功。
如果这个问题答成"让主 Agent 再 OCR 一次整个视频",说明仍然混淆了资料准备和当前问答。
一个不用模型、数据库和真实企业文件的小实验
目标只验证"命中 -> 来源 -> 引用 -> 当前授权"这条链,不测试 OCR、ASR、embedding、视觉理解或检索准确率。固定命中是手工 fixture,不是真实向量搜索结果。
输入采用自编材料:一条材料表记录、一条视频转录及其来源。下面代码可由支持 ES Module 的 Node.js 直接运行,无第三方依赖、不读写文件、不联网:
javascript
// offline-lab: source-contract
import assert from "node:assert/strict";
const documents = new Map([
["doc_case", {
tenantId: "t_demo", publishedVersion: 3,
retainedVersions: new Set([3]), revoked: false,
}],
["doc_training_video", {
tenantId: "t_demo", publishedVersion: 2,
retainedVersions: new Set([2]), revoked: false,
}],
]);
const units = new Map([
["unit_table_materials_row_1", {
docId: "doc_case", documentVersion: 3,
locator: { kind: "page_region", page: 8 },
}],
["unit_video_step_7", {
docId: "doc_training_video", documentVersion: 2,
locator: { kind: "time_range", startMs: 730000, endMs: 754000 },
}],
]);
const representations = new Map([
["rep_case_table_1", {
unitId: "unit_table_materials_row_1",
text: "防水材料 W-A;供应商甲;计量单位为桶。",
}],
["rep_video_transcript_1", {
unitId: "unit_video_step_7", text: "接下来处理管根。",
}],
]);
const principal = {
tenantId: "t_demo",
allowedDocIds: new Set(["doc_case", "doc_training_video"]),
};
function authorize(docId, who) {
const doc = documents.get(docId);
if (!doc || doc.revoked || doc.tenantId !== who.tenantId
|| !who.allowedDocIds.has(docId)) {
throw new Error("not_accessible");
}
return doc;
}
function loadHit(representationId, who) {
const rep = representations.get(representationId);
const unit = rep && units.get(rep.unitId);
if (!unit) throw new Error("not_accessible");
const doc = authorize(unit.docId, who);
if (doc.publishedVersion !== unit.documentVersion) {
throw new Error("stale_index");
}
return structuredClone({
...unit, unitId: rep.unitId, representationId, text: rep.text,
});
}
const evidenceSetId = "evidence_set_q42";
const citations = new Map([
["c42_case", loadHit("rep_case_table_1", principal)],
["c42_video", loadHit("rep_video_transcript_1", principal)],
]);
function openCitation(citationId, who) {
const snapshot = citations.get(citationId);
if (!snapshot) throw new Error("not_accessible");
const doc = authorize(snapshot.docId, who);
if (!doc.retainedVersions.has(snapshot.documentVersion)) {
throw new Error("source_unavailable");
}
return structuredClone({ evidenceSetId, ...snapshot });
}
let passed = 0;
function check(name, body) {
body();
passed++;
console.log("PASS " + name);
}
check("hit preserves unit", () => {
assert.equal(loadHit("rep_case_table_1", principal).unitId,
"unit_table_materials_row_1");
});
check("citation preserves page", () => {
assert.equal(openCitation("c42_case", principal).locator.page, 8);
});
check("citation preserves time", () => {
assert.equal(openCitation("c42_video", principal).locator.startMs, 730000);
});
check("cross-tenant access denied", () => {
assert.throws(() => openCitation("c42_case", {
...principal, tenantId: "t_other",
}), /not_accessible/);
});
check("ASR block offset restored", () => {
const originalMs = 720000 + 12000;
assert.equal(originalMs, 732000);
});
check("new search and historical citation differ", () => {
const doc = documents.get("doc_case");
doc.publishedVersion = 4;
doc.retainedVersions.add(4);
assert.throws(() => loadHit("rep_case_table_1", principal), /stale_index/);
assert.equal(openCitation("c42_case", principal).documentVersion, 3);
});
check("removed historical source denied", () => {
documents.get("doc_case").retainedVersions.delete(3);
assert.throws(() => openCitation("c42_case", principal), /source_unavailable/);
});
check("revoked source denied", () => {
documents.get("doc_training_video").revoked = true;
assert.throws(() => openCitation("c42_video", principal), /not_accessible/);
});
console.log(passed + " source-contract checks passed.");
预期是 8 项 PASS,最后输出 8 source-contract checks passed.。其中时间偏移题只是验证已知偏移的算术,不是在评测 ASR 时间戳质量;所有数据均在内存中,只用于把来源约束讲清楚,不是建议把企业检索库换回内存。
Windows PowerShell 中可以直接运行下面的命令:它从本文读取上述实验代码,在内存中执行,不需要新建脚本文件。前提是 node --version 可用;这里使用支持 structuredClone 的 Node.js 运行时。
powershell
@'
import { readFileSync } from "node:fs";
const article = readFileSync(
"E:/docresearch-agent/docs/MULTIMODAL_KNOWLEDGE_BASE_INTERVIEW.md",
"utf8",
);
const match = article.match(/^\/\/ offline-lab: source-contract\r?\n([\s\S]*?)^```/m);
if (!match) throw new Error("Offline lab not found");
await import("data:text/javascript;base64," +
Buffer.from(match[1]).toString("base64"));
'@ | node --input-type=module
实际生产契约会比这个 fixture 更完整,例如租户级唯一键、Asset 归属、运行代次、运行时 schema、真实 ACL 和持久化。这里故意不装数据库或下载大模型,是为了把你首先需要理解的那条链单独测出来。
然后不看代码完成三个操作:
- 画出
rep_video_transcript_1 -> unit_video_step_7 -> doc_training_video/v2 -> c42_video,标注每一步属于哪个可信模块。 - 解释为什么新检索拒绝旧版本命中,但旧答案在权限和保留规则允许时仍可引用旧版。
- 给实验增加"未知引用 ID"或"用户失去文档权限"的拒绝断言,说明它不能证明什么,例如不能证明视觉模型判断正确。
能运行这段契约检查,再把它与第 10.5 节的真实存储职责对应,你就从"听懂名词"前进到"能验证一个明确不变量"。完整系统的效果仍要接上真实解析、检索和媒体输入后另行评估。
20. 学习依据与进一步阅读
本专题基于本地学习仓库提交 4fe6191 对应材料整理,下面的链接固定到该版本,避免后续笔记变化影响阅读。
| 材料 | 本文依据 | 不能据此推导的结论 |
|---|---|---|
| 企业知识库规划笔记 | 四类资料、视觉嵌入与 OCR/描述、Markdown、MinIO、权限、整体选型 | 所有企业能力已集成并经过生产验证 |
| RAG Document 与 metadata 示例 | 检索内容与 metadata 分离 | 示例已具备页区域、时间、ACL 的完整 schema |
| 混合检索学习笔记 | 多路召回、共同片段身份、重排 | 原 Milvus 示例已改成 pgvector 或已经实现 RRF |
| ASR/TTS 学习笔记 | 录音、ASR、流式语音交互的基础链路 | 语音交互样例等于批量音视频知识入库 |
| LangSmith RAG 评估笔记 | 固定数据集、分项评价与失败归因 | 文本评委已经验证了原始图像和视频 |
| 项目总览文章 | DeepAgents、中间件、LangGraph、权限与检索的完整分工 | 文中的教学接口自动成为产品实现 |
PDF 版面恢复、表格定位、视觉空间组织、视频时间轴、幂等发布、派生权限和多模态评测,是在笔记需求上展开的工程设计讲解,不是把这些内容倒写成仓库已有功能。
最后把整篇收束为一条链:
原件保存 -> 保留结构的解析 -> 带来源的检索表示 -> 权限内召回 -> 按需核对原媒体 -> 有版本、有定位的引用 -> 分层评估与可恢复维护。
能沿着这条链解释正常情况、失败情况和能力边界,才是"理解企业级多模态",而不只是记住一串技术名词。