← 返回
AI技术

基于 FalkorDB 图的食品召回 Agent,实际覆盖 33 家门店

✍️ zhirenhun 📅 2026/10/18 👁 155 阅读 ⏱ 34 分钟
基于 FalkorDB 图的食品召回 Agent,实际覆盖 33 家门店

当供应商报告受污染的批次时,哪些店铺需要下架?我们构建了一个 AI 智能体,能够基于 FalkorDB 中的批次、返工和交付图谱来回答这个问题。在我们的演示批次上,它定位了 71 家店铺中的 33 家,展示每家店铺背后的记录,在记录用尽时向人工询问,并记住所学内容以便下次使用。

Live demo(点击 “Watch a recall”,无需设置)· Code· 为 Graph Hacks(WeMakeDevs × FalkorDB)而构建,Track 01:能够基于连接数据行动的智能体。

简要版本


问题

供应商致电:“L-204 批次的胡萝卜丁检测出李斯特菌。” 上个月您曾在汤和炖菜中使用过这种胡萝卜。目前哪些店铺的货架上仍有这些产品?

没人能仅凭记忆回答这个问题,因为答案是一条链条。该批次进入各个批次。一个批次的剩余料在后续批次中被返工(重复使用)。批次转化为产品批次,产品批次又流向配送中心和店铺。每个环节都有记录:收货日志、批次表、返工备注、交付记录。共有四套系统,手动将它们关联起来需要几天时间。

时间紧迫。一旦您认为该产品存在造成严重危害的合理可能性,您就有 24 小时向 FDA 的可报告食品登记处提交报告。此外,FDA 的食品可追溯性规则 FSMA 204 要求在收到请求后 24 小时内提供批次级别的记录。

因此,大多数公司会在两种不佳的方案中选择其一。要么他们对所有可能发货的店铺进行下架,这会浪费食品,放大新闻报道,并让消费者对召回不那么重视;要么他们按批次召回,但因某些交付记录中缺少批次代码而遗漏店铺。后者正是导致人们生病的原因。

我们构建了什么

Recall Tracer 在第一小时内完成了一名优秀召回协调员的工作。您提供批次代码,它将返回需要下架的店铺、未找到关联记录的店铺,以及待人审批的店铺通知。

它先从过去的事件中汲取教训,并扣留尚未发货的任何产品。接着顺着批次向前追溯到每家门店,评估哪批产品导致了扩散,并核对没有批次码门店的交付记录。如果对这些无法判断,则向协调员询问。最后,它将已放行门店的产品逆向追溯到原料,起草通知,并保存所学到的内容。

为什么使用 Agent 而不是脚本?因为召回情况每次都不相同。一次批次经过了返工,另一次已送达但从未使用,第三次批次到达门店却在记录中没有批次码。脚本要么执行不适用的检查,要么漏掉必要的检查。

为什么使用图而不是相信大模型?因为决策必须经得起审计师、监管者以及刚刚丢失一天销售额的店长的考验。于是我们在第一天制定了一条规则,并且从未打破:

图来决定,大模型不决定。Claude 选择下一步要运行的检查,进行两次判断(提出的召回分类和计划),并解释结果。Cypher 决定哪些门店受到影响。最终结果由工具输出构建,绝不是由大模型直接输入的。


过去几个月的真实事件

我们的演示公司 Northfield Foods 是虚构的。问题却是真实的。这三个案例来源于今年公开的 CDC、FDA 和 openFDA 记录,其中两个已被载入应用中的第二个 FalkorDB 图。

苜蓿芽:一个种子批次,三家公司,十六个州。 STEC 大肠杆菌和沙门氏菌 Agona 的爆发导致十六个州的七十六人生病,其中六人住院(CDC 和 FDA,最后更新 2026 年 10 月 1 日)。一个种子批次 SAL2-24JB 将三家公司的召回联系在一起:International Sprout Holdings,种子供应商(H-1341-2026,八月二十三日,四十三千七百九十九磅),Todd's Seeds(H-1404-2026,九月十一日,全国范围)以及 Berlin Seeds(FDA 公告,九月二十三日,四十四个州)。文件中没有说明这些属于同一批次。将批次码设为节点,所有三起召回都挂在此节点下。我们进行了检查:在从 2,647 条召回记录中解析出的 281 个批次码中(openFDA 提供 2,646 条,加上 Berlin 公告),SAL2-24JB 是唯一被多家公司命名的批次码。图还显示出一个空白:十六个州中有生病案例的十个州不在种子供应商的发货名单上。它们出现在 Todd's Seeds 的名单上,对应的召回是在十九天后。

奶粉:一种原料,8家公司,16天内22次召回。2026年4月20日,California Dairies 因可能存在沙门氏菌而召回了脱脂干奶粉。接下来的16天里,又有七家公司召回了用这种原料制作的调味料、面包丁、贝果脆片和酱料混合物,这些产品分别以Kroger、Giant Eagle和Aramark等品牌销售。源头公司自己的召回名单将其美国分销标注为“未知且待定”。虽然没有人生病,但发起召回的公司自己也将自身的分销标注为未知。如果把批次级记录当作边,第一次召回就已经能够显示出所有下游产品。

软奶酪:召回未提及的州。与requesón(一种软式里科塔奶酪)相关的李斯特菌爆发导致15人感染,其中14人住院,1人死亡。CDC将此次爆发追溯到Clover Hill Dairy,该公司从2026年6月1日起累计发布了18起I类召回(最严重的召回类别,针对可能造成严重危害的产品),涉及马里兰州、纽约州和弗吉尼亚州。此外,伊利诺伊州也有人感染,但该公司的任何召回分销名单中都没有提及伊利诺伊州。将病例州和召回州放在同一家公司节点上进行比较。在我们的应用中,这只需要一个Cypher查询。

每次都是同样的形状。受污染的原料在其他产品和公司之间流动,追踪它的记录存在,但没有人能够足够快地将它们连接起来。公共记录止步于公司。公司内部,批次级记录(FSMA 204 要求的那种)可以用一个查询回答‘哪些商店收到了这一批次?’这就是我们自动化的部分。


我们如何使用 FalkorDB 构建智能体

图

(Supplier)-[:SUPPLIED]->(Lot)-[:USED_IN]->(Batch)-[:REWORKED_INTO]->(Batch)
(Batch)-[:PRODUCED]->(ProductLot)-[:SHIPPED_TO]->(Store)
(Product)-[:SHIPPED_UNTRACKED]->(Store)        // a delivery logged with no lot code
(ProductLot)-[:HELD_AT]->(Site | DC)           // unshipped product
(Incident)-[:CONCERNS]->(Lot)
(Memory)-[:ABOUT]->(Supplier | Store | Batch | Site | DC)  // lessons from earlier incidents

这张技术文章配图描述了一个供应链管理系统的流程图,包括供应商、批量、产品、存储和其他相关实体的交互

边只会随时间向前指向,因而不存在环路,且有限长度的可变匹配是安全的。缺失的批次码也是边(SHIPPED_UNTRACKED),这把“哪些仓库的记录无法清除”变成了查询而不是猜测。

每个工具都是一个 Cypher 查询

该智能体拥有 13 个工具,每个工具对应一个图查询任务。正向追踪是一个单一匹配:

MATCH path = (l:Lot {code: $lot})-[:USED_IN|REWORKED_INTO|PRODUCED|SHIPPED_TO*1..8]->(st:Store)
RETURN st.id AS store, [n IN nodes(path) | coalesce(n.code, n.id)] AS path

它沿着 L-204 追溯最多 8 步,包括将批次 B-03 的剩余料投入 B-09 的返工步骤。它返回的路径就是我们对每家门店展示的证据。逆向追溯则相反,从每家已清理门店的产品批次倒推到原料,以确认其中没有掺入问题批次。

图算法作为工具

追溯能告诉你批次流向了哪里,却不能直接指出应改进工艺的环节。为此,agent 调用 FalkorDB 的 algo.betweenness,该算法能找出供应路径经过最多的节点。对 L-204 而言,该节点正是批次 B-03,下游有 16 家门店,正是该批次的剩余料被返工使用。修正该环节的返工规则,后续召回规模就会变小。

这个问题曾让我们吃亏。Betweenness 会随机抽样节点,导致每次运行的排名都会变化。固定的 samplingSeed 使其结果可重复,如今我们加入了测试以保证这一点。

Agent 规划自身路径

最初的版本只是按编号列表执行步骤。它能运行,但本质是一个挂载了模型的工作流。我们把提示词改写为目标:拉取所有可能存有受污染产品的门店。对被遗漏的门店进行逆向检查。仅拉取记录所必需的数据。这样,下一次事件的启动会更智能。

现在,agent 会根据上一步的返回结果选择下一个检查点,并在每次工具调用前写下一句话,说明它刚刚学到了什么以及为什么这是下一步。以下是记录中 L-204 运行的中间部分,完全按照追溯所示。

[8.9s]  "The hold covers 93 kg of raw carrot and 2,652 units across 5 locations.
         Next I'm tracing forward to find every store that received product."
        trace_forward      -> 4 batches, 7 product lots, 31 stores reached        FalkorDB 0.8 ms

[11.6s] "The trace reached 31 stores through 4 batches, and B-03 was reworked into B-09.
         I'm ranking chokepoints and checking for gap stores at the same time."
        rank_chokepoints   -> B-03 is the chokepoint: betweenness 175, 16 stores downstream   3.3 ms
        coverage_gaps      -> 2 stores got Garden Vegetable Stew with no lot code      0.94 ms

[15.5s] "Two stores received Garden Vegetable Stew with no lot code, so I'm asking
         the coordinator whether to pull it there."
        ask_coordinator    -> Coordinator chose "Pull it there too"

[21.7s] "The coordinator chose to pull, so I'm running the recall scope with the gap stores included."
        minimal_recall     -> 33 of 71 stores, 2,256 units; 2 gap stores added        2.23 ms

[26.2s] "The scope is 33 stores to recall and 38 cleared. I'm checking the cleared
         stores' 9 distinct lots backward, independently."
        trace_backward     -> 8 product lots walked back; 0 contain L-204            1.36 ms

四批货,四次不同的运行:

批次 图上显示的情况 智能体的处理
L-204 4 个批次、7 个产品批号、31 家门店,返工 B-03→B-09,2 家门店缺批号 排定瓶颈优先级,询问协调员,从 71 家中召回了 33 家
L-205 同一供应商;记忆显示此前有 2 家门店漏过批号 提前核查配送记录,点名了惯犯。协调员选择“仅标记”,因此只召回 71 家中的 38 家,未含漏批号的门店
L-143 单一批次,所有配送都带批号 跳过瓶颈排序,也无需询问:召回 57 家门店中的 10 家
L-108 批号从未使用 冻结 202 公斤,召回范围设为零家门店,并保存经验教训

记录查不到时,由人来兜底

有些门店的货来自没有批号的配送,图上无法判定是否需要召回,这属于业务决策。于是智能体会停下来询问——通过一个在本地运行、与 MCP 工具并排的ask_coordinator工具。问题会出现在追踪记录里,附带两个按钮:“同时召回该门店”或“仅标记”。如果 90 秒内无人应答,智能体会按指示采用安全的默认选项(召回)并说明原因。召回分级、冻结解除和通知发送,也都要等人工确认。

能改变下一次运行的记忆

每次事件结束时,智能体会被要求把 2 到 4 条经验教训写成Memory节点,每个节点通过一条ABOUT边指向相关联的供应商、门店、批次、工厂或配送中心。下一次事件的第一步就是recall_memory。处理 L-205(与 L-204 同一供应商)时,智能体读到 ST-013 和 ST-021 曾漏过批号,于是提前核查这两家的配送记录,并将其标记为重复问题。改变的不只是总结,而是整个计划。

疫情爆发侧也写入同一份记忆。爆发处理智能体从病例出发,找出能解释这些病例的批次;完成后,流水线会为该批次的供应商写入一条经验。当协调员打开这个批次时,事件处理智能体在第一步就读到这条经验。两个智能体从不互相调用,交接只是图中的一个节点和一条边。

MCP 和堆栈

所有 13 个工具运行在我们自己的 MCP 服务器上,通过 stdio 向我们的智能体提供服务,同时通过 Streamable HTTP(API 上的 /mcp,网页应用上的 /api/mcp),这样 Claude Code、Claude Desktop 或任何 MCP 客户端都可以使用它们。爆发智能体仅获得 2 个只读工具,因此即使出错也无法放置保留。事件智能体获得全部 13 个工具以及本地的 ask_coordinator。这些智能体是运行 Claude Opus 5.5 的 AI SDK ToolLoopAgent,API 使用 Hono 通过 Server-Sent Events 流式传输每一步,应用则是使用 Cytoscape 进行记录网络的 Next.js。每次实时运行都会被记录,因此可以在不需要 API 密钥的情况下重播。


FalkorDB 为何是 Track 01 的合适选择

如果您正在评审 Track 01,以下是从需求到代码的映射:

Track 01 重点 在 Recall Tracer 中
多跳推理 批次 → 批次 → 返工 → 产品批次 → 商店,在单个 Cypher 匹配中最多可达 8 跳,另外还包括对已清空商店的反向检查
图查询和算法作为工具 rank_chokepoints 运行 algo.betweenness;其他所有工具都是 Cypher 查询
通过 MCP 进行工具选择 我们的 MCP 服务器上有 13 个工具;智能体根据图返回的结果选择和跳过它们,并说明原因
基于图的操作 放置保留,以提出的分类打开事件,设置召回范围,起草通知,保存经验教训
可解释的结果 每个商店都有其记录路径;每一步都显示其 Cypher 和 FalkorDB 耗时;结果来源于工具输出

除了需求之外还有三个原因。可追溯性本身就是一个图,是一条“this went into that”的链条,因此在 Cypher 中,一个追踪就是一种模式,而在 SQL 中它则是未知深度的自连接堆栈。它足够快,能够在智能体循环内部运行:L-143 运行执行了 22 次图查询,在 FalkorDB 上总共花费了 11 毫秒,因此智能体只会等待模型。此外,内存存储在与数据相同的图中,读取它只是另一次查询,每个智能体都能看到它。同一个引擎既保存了 2,647 条召回记录,也保存了上述事件来源的 CDC 页面。


应用之旅

应用按照协调员的三个步骤工作:从疾病报告中查找来源,根据供应商通知打开事件,然后确定范围并批准通知。以下是L-204运行后的事件页面。代理路径位于顶部,范围显示在卡片中,右侧调查日志展示每一步及其原因和对应的Cypher。

批次L-108已送达但从未使用。代理对全部202公斤实施了扣押,向前追溯后未发现任何问题,于是将范围设为零家门店。被跳过的步骤被划掉,每个步骤旁标注代理原因。

实记录页面对公开CDC和openFDA数据提出相同问题。这是软奶酪爆发事件:涉及公司、其18次召回、它们点名的州,以及唯一一个有病例但未出现在任何分发名单中的州。

事件页面还有两个功能。“重放追踪”会按照原始时间以0.5×、1×或2×的速度回放记录的运行,而记录网络仅显示播放头位置时代理所达到的内容。日志下方,Agent内存面板展示代理从之前事件中读取的信息以及为下一次事件保存的内容,对应图中的Memory节点。

此外,还有一个爆发页面,可将病例追溯至共享批次;另有一个基于同一套工具的聊天界面,在起草通知前会显示Approve和Deny按钮。


如何使用

这是一个原型。Northfield Foods及其工厂、门店和批次均来自种子生成器的测试数据。实记录页面上的爆发和召回均为真实公开记录。数据从未发送到任何地方。通知会保持草稿状态,直至获得人工批准,即使批准后也仅标记为已批准。

观看录制的运行。 Open recall-tracer.onrender.com/outbreak 并在顶部栏点击“观看回放”。它会以原始时间回放 L-204 的一次真实录制运行,时长约 50 秒。Incident 页面还提供了 L-205、L-143 和 L-108 的运行链接,以便您比较路径。无需安装,也不需要 API 密钥。

自行运行。 您需要 Docker 和一个 Anthropic API 密钥。

git clone https://github.com/VinayakVispute/recall-tracer
cd recall-tracer
cp .env.example .env            # add ANTHROPIC_API_KEY
docker compose up --build       # FalkorDB + app; seeds the demo graph on start

打开 http://localhost:3000。即使没有密钥,仪表盘、图形视图和已记录的运行仍然可以使用。接着进入“供应商通知”,输入 L-204,选择危害类型,观察追踪。当 Agent 询问没有批次码的商店时,选择 “Pull it there too” 或 “Flag only”。确认召回分类,批准通知,并将符合 FDA 样式的出货记录导出为 CSV。随后尝试 L-205;Agent 会记住它所学到的内容。

使用您自己的 Agent 提供的工具。 同样的 13 种工具通过 MCP 提供:

{ "recall-tracer": { "url": "http://localhost:3000/api/mcp" } }

在公开部署中,实时代理每访客每日运行次数受限,以防共享链接产生过高费用。回放永不受限。


它仍然会出错的地方

  • “已清除”表示没有记录将商店与批次关联。环境污染和相邻批次不在图中,因此这不能证明产品安全。
  • 获得无批次代码产品的商店只能被下架或标记。图中没有可用来清除它们的信息,真正的协调员会按产品和日期窗口来限定范围。
  • 召回分类只是一个提议。智能体尚未看到实验室结果,而 FDA 负责分配等级,因此每次都需要人工确认。
  • 如果在 90 秒内没有人回答协调员的问题,系统就会拉取。这是安全的默认行为,但仍然是基于计时器的猜测。
  • 单位指已交付的数量,而非库存数量。我们没有销售数据。
  • 检查顺序仍由模型决定。我们的测试仅确保每项必需检查都以有效顺序执行,而非检查是否为最佳顺序。

我们学到了什么

  1. 尽早决定谁来决策。 “图来决策,模型不决策” 在讨论开始前就平息了十几个争论。每当模型想输入一个数字时,规则就说不,结果是审计师可以核查的。
  2. 目标胜过清单。 编号步骤提示对每个批次执行相同的检查。目标提示跳过了 L-143 的瓶颈排名并未提出任何问题,因为图中没有理由这样做。这就是工作流与智能体的区别。
  3. 记忆必须改变计划,而不仅仅是总结。 L-205 运行是我们首次看到先前事件的教训改变了智能体后续行为。在此之前,记忆只是装饰。
  4. 图算法是工具,工具需要测试。 Betweenness 随机抽样节点,导致顶部瓶颈在各次运行之间变化。固定种子和一次测试解决了问题。智能体调用的任何东西都需要得到相同的答案两次。
  5. 缺失的数据也是数据。 将 “未附批次代码的交付” 作为边,使召回中最棘手的问题变成了一行查询,并让智能体有理由去询问人类。

这将往何处去

召回的难点不在于追溯,而在于记录。图模型已经符合 FSMA 204 的关键数据要素,因此下一步是使用 ERP 或仓库导出的真实数据,而不是我们的生成器。之后:增加更多种类的危害,每种都有其自身的分类规则和通知措辞,并在人工批准后发送真实通知。

在此之前,打开演示,运行 L-204,观看智能体决定谁会被拉走。

如果你构建能够执行真实操作的智能体,你如何决定模型可以决定什么?我们很想知道你划定的界限在哪里。如果这对你有帮助,在 repo 上点个星星表示支持。


链接与来源

真实事件的来源:CDC 和 FDA 疫情调查(苜蓿芽,2026;软奶酪李斯特菌,2026),openFDA 食品执法报告(召回 H-1341-2026、H-1404-2026 以及加州乳业召回 H-0698 至 H-0700-2026),以及 FDA 召回通知。所有数据均为公布后数字,已于 2026 年 10 月 17 日和 18 日核对。

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

graphhacks falkordb ai graphdatabase