1. 项目缘起与整体架构拆解
餐饮行业的知识管理有个很尴尬的现状:菜谱、食材、口味偏好、过敏原、成本核算、供应链信息,这些东西散落在不同的文档、表格、甚至老师傅的脑子里。你想做一个能回答“这道菜能不能换成低钠酱油”“哪些菜同时含花生和海鲜”“给我推荐三个适合糖尿病人吃的川菜”这类问题的系统,靠关键词搜索根本搞不定。我最初接到的需求就是这么一个场景——给一个中型连锁餐饮品牌做内部知识库问答助手,要求能看懂图片(比如菜品照片、手写配方扫描件)、能理解文字、还能把食材、菜品、营养指标之间的关系串起来。
这个项目的核心思路其实就三件事:多模态解析、知识图谱构建、Agent 调度问答。LlamaIndex 在这中间扮演的是“胶水层”的角色,它把文档解析、向量索引、图索引、查询引擎、Agent 工具链全部串在一起。GPT 系列模型负责语义理解和生成,LlamaIndex 负责编排和检索,知识图谱负责结构化推理。三者缺一不可。
为什么不用纯向量检索?因为餐饮知识里有大量“关系型问题”。比如“宫保鸡丁的替代食材有哪些”,向量检索可能给你返回一堆提到宫保鸡丁的文档片段,但它没法告诉你“鸡胸肉→替代→鸡腿肉”这种明确的边关系。知识图谱就是干这个的。而多模态能力解决的是“非结构化输入”的问题——很多老菜谱是拍照存的,很多食材信息是表格图片,纯文本方案直接抓瞎。
整个系统的数据流是这样的:原始数据(PDF、图片、Excel、手写扫描件)→ 多模态解析层(GPT-4V 或类似视觉模型 + OCR)→ 结构化抽取(实体识别 + 关系抽取)→ 双索引构建(向量索引 + 图索引)→ Agent 查询路由(根据问题类型选择向量检索、图查询或混合检索)→ 答案生成与溯源。这个链路里每一步都有坑,后面我会逐个拆。
适合谁来参考这篇内容?如果你正在做垂直领域的知识库问答,尤其是涉及多模态输入和关系推理的场景,这套架构可以直接抄。如果你只是想做简单的文档问答,那可能用不上知识图谱这一层,但多模态解析和 Agent 路由的思路仍然有参考价值。小白也能看懂,我会尽量把每个技术选择背后的“为什么”讲清楚。
2. 多模态数据解析的核心细节与实操要点
2.1 为什么多模态解析是第一个拦路虎
餐饮行业的数据源极其杂乱。我拿到手的资料包括:PDF 菜谱(排版混乱)、手机拍的菜品照片(带手写标注)、Excel 食材成本表(合并单元格满天飞)、扫描版的老配方(字迹模糊)、甚至还有微信聊天记录截图(厨师长发的临时调整通知)。这些东西如果只靠传统 OCR,出来的文本基本没法用——表格结构丢失、手写体识别率低、图片里的食材和文字对不上号。
我的做法是分两层处理:第一层用视觉模型做整体理解,第二层用 OCR 做精确文本提取。视觉模型(比如 GPT-4V 级别的多模态模型)负责回答“这张图里有什么菜”“这个表格大概是什么结构”“手写标注写的是‘少放盐’还是‘少放糖’”。OCR 负责把确定的文字精确提取出来。两层结果做交叉验证,置信度低的字段标记为待人工确认。
这里有个关键细节:不要试图用一个模型解决所有问题。我试过直接用视觉模型做全量 OCR,结果在密集表格上错误率很高,而且成本扛不住。后来改成“视觉模型做区域定位和语义理解 + 轻量 OCR 做文字提取”,准确率和成本都好了很多。具体流程是:先用视觉模型识别图片中的区域类型(标题区、表格区、正文区、手写区),然后对不同区域用不同的 OCR 策略——印刷体用标准 OCR,手写体用专门的手写识别模型,表格区先做结构还原再做单元格提取。
2.2 实体抽取与关系定义的实操方法
多模态解析完之后,你拿到的是半结构化的文本块。接下来要做实体抽取。餐饮领域的核心实体类型我定义了这几类:菜品、食材、调料、营养指标、烹饪方式、口味标签、过敏原、成本项。关系类型包括:包含(菜品→食材)、替代(食材→食材)、禁忌(食材→人群)、搭配(菜品→菜品)、属于(菜品→菜系)。
抽取方式我用的是“LLM 抽取 + 规则校验”的混合方案。LLM 负责从非结构化文本里抽实体和关系,规则层负责校验——比如“盐”是调料不是食材,“花生”既是食材也是过敏原。规则层还负责归一化,比如“鸡胸肉”“鸡脯肉”“鸡大胸”统一映射到同一个实体 ID。
注意:实体归一化这一步千万别省。我一开始偷懒没做,结果知识图谱里出现了“鸡胸肉”“鸡脯肉”“鸡大胸”三个节点,查询的时候漏掉一大堆关系。后来加了一个同义词词典 + 向量相似度匹配的归一化层,问题才解决。
关系抽取的 prompt 我调了很多版,最后稳定下来的模板大概是这样的:
extract_prompt = """ 从以下文本中抽取餐饮领域的实体和关系。 实体类型:菜品、食材、调料、营养指标、烹饪方式、口味标签、过敏原、成本项 关系类型:包含、替代、禁忌、搭配、属于 输出格式:JSON 列表,每条包含 head、head_type、relation、tail、tail_type 文本内容:{text} """实测下来,few-shot 示例给 3-5 个就能让抽取准确率稳定在可接受范围。温度设 0.1 以下,避免模型自由发挥。
2.3 多模态对齐的坑与解法
多模态数据有个隐蔽的问题:图片和文本的对齐。比如一张菜品照片旁边有手写标注“这个版本减糖 20%”,如果你不把图片和标注关联起来,后面查询“减糖版本的菜有哪些”就找不到这条记录。我的做法是在解析阶段就给每个数据块打上来源标记(source_id),图片和它旁边的文本共享同一个 source_id。构建索引的时候,这个 source_id 会作为元数据存进去,查询时可以按来源过滤。
另一个坑是单位不统一。有的菜谱写“盐 5g”,有的写“盐 一小勺”,有的写“盐 适量”。我在实体抽取之后加了一个单位归一化层,把“一小勺”映射到“约 5ml”,“适量”标记为“未量化”并单独处理。这个层用规则 + LLM 混合做,规则覆盖常见单位,LLM 处理模糊表述。
3. 知识图谱构建与双索引协同的完整实现
3.1 图 Schema 设计与存储选型
知识图谱的 Schema 设计直接决定了后面能回答什么问题。我最终确定的图结构是:节点类型 8 种(菜品、食材、调料、营养指标、烹饪方式、口味标签、过敏原、成本项),边类型 5 种(包含、替代、禁忌、搭配、属于)。每个节点有属性,比如菜品的属性包括“难度”“烹饪时间”“辣度”“甜度”,食材的属性包括“类别”“季节”“存储方式”。
存储选型上,我用了 Neo4j 做图存储,原因是它的 Cypher 查询语言对关系推理非常友好。比如查“宫保鸡丁的替代食材”:
MATCH (d: Dish {name: '宫保鸡丁'})-[:CONTAINS]->(i: Ingredient)-[:SUBSTITUTE]->(alt: Ingredient) RETURN alt.name这条查询在 Neo4j 里毫秒级返回,换成向量检索你得做多跳推理,又慢又不准。但 Neo4j 也不是万能的——它不擅长模糊语义匹配。所以向量索引和图索引必须共存。
3.2 双索引构建:向量索引 + 图索引
LlamaIndex 的好处是它同时支持 VectorStoreIndex 和 KnowledgeGraphIndex,而且可以在同一个查询引擎里做路由。我的构建流程是:
- 向量索引:把解析后的文本块做 embedding,存入向量数据库。我用的是 OpenAI 的 text-embedding-3-small,维度 1536,性价比高。每个文本块的元数据里带上 source_id、实体 ID 列表、数据类型(菜谱/食材表/营养表)。
- 图索引:把抽取的实体和关系写入 Neo4j。LlamaIndex 的 KnowledgeGraphIndex 可以直接从文档构建图,但我建议自己控制抽取流程,因为 LlamaIndex 默认的抽取 prompt 对中文餐饮领域不够精准。
- 索引关联:向量索引的每个文本块元数据里存实体 ID,图索引的每个节点属性里存关联的文本块 ID。这样查询时可以在两个索引之间跳转。
实操心得:构建图索引的时候,批量写入比逐条写入快 10 倍以上。我一开始逐条写 Neo4j,一万个节点写了快半小时。后来改成每 500 条一个批次,用 UNWIND 批量写入,时间降到 3 分钟以内。
3.3 Agent 查询路由的设计逻辑
Agent 的核心任务是判断用户问题该走哪条检索路径。我把问题分成四类:
| 问题类型 | 示例 | 检索路径 |
|---|---|---|
| 事实型 | “宫保鸡丁的烹饪时间是多少” | 向量检索 + 元数据过滤 |
| 关系型 | “哪些菜同时含花生和海鲜” | 图查询 |
| 推理型 | “糖尿病人能吃的川菜有哪些” | 图查询 + 向量检索 + LLM 推理 |
| 多模态型 | “这张照片里的菜含什么过敏原” | 视觉模型 + 图查询 |
路由逻辑用 LLM 做意图分类,prompt 里给四类的定义和示例,让模型输出类别标签。实测准确率能到 90% 以上。分类之后,Agent 调用对应的查询引擎,拿到结果后再用 LLM 做答案生成和溯源。
这里有个细节:混合检索的排序策略。当一个问题同时走向量和图查询时,两路结果需要合并排序。我的做法是给图查询结果更高的权重(因为关系推理更精确),向量结果作为补充。具体权重是图 0.7、向量 0.3,这个比例可以根据业务调。
4. 常见问题排查与避坑经验实录
4.1 多模态解析阶段的典型问题
问题一:手写体识别率低。老配方上的手写字,标准 OCR 基本识别不出来。我的解法是先用视觉模型做“手写区域检测”,把包含手写的区域裁剪出来,单独送给手写识别模型。如果手写识别置信度低于阈值,标记为“待人工确认”,在系统里用黄色高亮显示。这样至少不会把错误数据写进知识库。
问题二:表格结构还原错误。合并单元格是表格解析的噩梦。我的做法是先用视觉模型判断表格的“逻辑结构”(几行几列、哪些单元格合并),然后用 OpenCV 做线条检测辅助定位,最后按逻辑结构提取单元格内容。这套组合拳下来,表格还原准确率从 60% 提到了 90% 左右。
问题三:图片和文本的关联丢失。前面提过,用 source_id 解决。但还有一个坑:如果一张图里有多道菜,每道菜旁边都有标注,你需要做“区域级”的关联,而不是“图片级”。我的做法是在视觉模型解析时输出每个区域的 bounding box,然后根据位置关系把文本标注和菜品区域配对。
4.2 知识图谱构建阶段的典型问题
问题一:实体重复。前面说过,归一化层必须做。我的归一化流程是:先查同义词词典,命中则直接映射;未命中则做向量相似度匹配,相似度高于 0.85 的合并;仍未有结果的,标记为新实体,人工审核后加入词典。
问题二:关系抽取遗漏。LLM 抽取关系时容易漏掉隐含关系。比如“这道菜用鸡腿肉代替鸡胸肉更嫩”,隐含了“鸡腿肉→替代→鸡胸肉”的关系,但 LLM 可能只抽到“鸡腿肉”和“鸡胸肉”两个实体,没抽到关系。我的解法是在 prompt 里明确要求“如果文本暗示了替代、禁忌、搭配关系,必须显式输出”,并给 few-shot 示例。另外加了一个后处理规则:如果两个食材实体在同一句话里出现且句子包含“代替”“换成”“改用”等词,自动补一条替代关系。
问题三:图查询性能下降。当图规模超过 10 万节点时,多跳查询开始变慢。我的优化措施包括:给常用查询字段建索引、限制查询跳数(默认最多 3 跳)、对高频查询结果做缓存。Neo4j 的 APOC 库里有缓存过程,可以直接用。
4.3 Agent 问答阶段的典型问题
问题一:路由分类错误。有些问题边界模糊,比如“宫保鸡丁和辣子鸡有什么区别”,既涉及事实(食材、做法)又涉及关系(对比)。我的解法是允许“混合路由”——当分类置信度低于阈值时,同时走向量和图查询,然后合并结果。代价是延迟增加,但准确率提升明显。
问题二:答案溯源困难。用户问“你怎么知道这道菜含花生”,系统需要给出出处。我的做法是在答案生成时,要求 LLM 输出引用的文本块 ID 和图节点 ID,然后在界面上展示对应的原文片段和图关系路径。这样用户能直接看到“花生”这个实体是从哪份文档里抽出来的。
问题三:多轮对话中的上下文丢失。用户先问“宫保鸡丁含什么”,再问“那它的替代食材呢”,第二问的“它”指代宫保鸡丁。我的解法是在 Agent 里维护一个对话状态,记录上一轮提到的实体,当前问题如果包含指代词,自动注入上一轮的实体信息。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 图片文字识别错误 | OCR 模型不适配手写/表格 | 检查识别置信度 | 分区处理 + 人工确认标记 |
| 实体重复 | 未做归一化 | 查图里同名不同 ID 的节点 | 同义词词典 + 向量相似度合并 |
| 关系遗漏 | LLM 抽取不完整 | 抽样检查抽取结果 | 优化 prompt + 后处理规则补全 |
| 图查询慢 | 跳数过多/无索引 | 看查询计划 | 限制跳数 + 建索引 + 缓存 |
| 路由分类错 | 问题边界模糊 | 看分类置信度 | 混合路由 + 阈值调优 |
| 答案无溯源 | 生成时未要求引用 | 检查 prompt | 强制输出引用 ID + 界面展示 |
5. 系统集成与性能优化的实操记录
5.1 整体集成架构
系统集成我采用的是 FastAPI 做服务层,LlamaIndex 做编排层,Neo4j 和向量数据库做存储层。API 设计上,对外暴露三个端点:/ingest(数据摄入)、/query(问答)、/feedback(用户反馈)。/ingest接收文件上传,触发多模态解析和图/向量索引构建。/query接收自然语言问题,返回答案和溯源信息。/feedback收集用户对答案的评价,用于后续优化。
集成过程中最大的坑是异步处理。多模态解析和图构建都是耗时操作,如果同步处理,API 会超时。我的做法是用 Celery 做任务队列,/ingest只负责接收文件并返回任务 ID,实际处理在后台异步执行。用户可以通过/task/{task_id}查询处理进度。
5.2 性能优化的几个关键点
嵌入批处理:向量化的时候,单条调用 embedding API 太慢。我改成每 100 条一批,批量调用,速度提升 5 倍以上。注意批次大小不要超过 API 限制,OpenAI 的 embedding API 单次最多 2048 条输入。
图查询缓存:高频查询(比如“常见过敏原列表”)的结果缓存在 Redis 里,TTL 设 1 小时。缓存命中率大概 40%,对降低延迟帮助很大。
LLM 调用优化:答案生成用的 LLM 调用是延迟大头。我做了两件事:一是用流式输出,让用户先看到部分答案;二是对简单问题(事实型)用小模型,复杂问题(推理型)用大模型。路由分类的时候顺便判断复杂度,简单问题走小模型,延迟从 3 秒降到 1 秒以内。
索引更新策略:数据不是一成不变的,新菜品、新食材需要增量更新。我的做法是向量索引支持增量插入,图索引支持增量节点和边。但要注意,增量更新后需要重建部分缓存,否则会返回旧结果。
5.3 成本控制的实操经验
这个系统跑起来,成本主要在三块:视觉模型调用、LLM 调用、embedding 调用。我的控制策略是:
- 视觉模型只在数据摄入时调用,查询时不调用(除非用户上传新图片)。摄入阶段做全量解析,查询阶段直接用已解析的数据。
- LLM 调用做缓存,相同问题 24 小时内直接返回缓存结果。
- Embedding 做本地缓存,相同文本不重复调用 API。
- 小模型能解决的问题不用大模型。
实测下来,月成本从最初的 2000 多降到了 600 左右,降幅 70%。
6. 后续扩展方向与个人实操体会
这套系统上线后跑了三个月,整体稳定。后续我打算在几个方向做扩展:一是加入时序知识,比如“夏季菜单”和“冬季菜单”的食材差异,这需要在图里加入时间维度;二是做多语言支持,因为有些供应商资料是英文的;三是把用户反馈闭环做得更细,比如用户纠正了某个实体关系,系统自动更新图。
我个人在实际操作中的体会是:知识图谱的质量比模型选型更重要。我试过换更大的 LLM,效果提升有限;但把实体归一化做好、把关系抽取的遗漏补上,问答准确率直接涨了 15 个百分点。另一个体会是:多模态解析不要追求全自动。手写体、模糊表格这些场景,留一个人工确认的入口,比强行自动化靠谱得多。最后再分享一个小技巧:构建图索引的时候,先把 Schema 定死,不要边抽边改。Schema 一变,之前抽的数据全要重来,代价太大。