场景
dam-scout 是我给 dam-hub 配的 AI 策展助手:用户说一句“挑一批适合做产品发布会的科技感横图”,它去素材库里找出合适的图,给出策展方案。这个需求一开始的实现思路是单次 LLM 调用:把用户的话和素材库的概要塞进 prompt,让模型直接给答案。
试了就知道为什么不行。素材库动辄几千张图,概要塞不进去;塞进去了,模型也不知道库里的图实际长什么样;更要命的是失败没有着力点——结果不好,你说不清是理解错了意图、检索没召回,还是生成环节跑偏了。意图、检索、生成三件事的失败模式完全不同,揉在一次调用里,错了没法定位,也就没法改进。
所以拆成流水线,用 LangGraph 编排了五个节点:意图分析 → 任务规划 → 向量召回 → 过滤去重 → 方案生成。这篇写每个节点在干什么、为什么用图而不是链、以及踩过的坑。
五个节点
意图分析:把自然语言翻译成结构化意图。dam-scout 归纳了四类意图——主题(“科技感”)、风格(“横图”“极简”)、数量(“一批”)、用途(“发布会”)。Prompt 要求模型只输出 JSON schema 约定的字段,不认识的字段丢弃。这一步是整条流水线的地基:意图提错,后面全部白跑。
任务规划:把意图拆成多路检索任务。比如“科技感横图”会拆成两路:一路按风格标签走元信息过滤,一路按“科技”语义走向量召回。单路检索的召回天花板很低——用户的词和素材的标注词很少完全对得上,多路取并集才能把召回率撑起来。规划节点输出的不是图,是一组可执行的检索任务描述。
向量召回:两套向量,各有分工。素材的元信息(标题、描述、标签拼接文本)在入库时用 DashScope 的 embedding 模型向量化,存进 ChromaDB;查询时把意图分析产出的关键词同样向量化,按余弦相似度取 Top-K。这里特意只对元信息向量化而不是图片本身——元信息是人写过的结构化描述,语义密度高、成本低;图片本身走向量需要多模态模型,在当时的量级下收益撑不起复杂度。
过滤去重:规则层和语义层各管一段。规则层硬过滤:空间权限(用户看不到没授权的图)、类型匹配(要横图就滤掉竖图);语义层软去重:相似度过高的候选图只保留一张,避免方案里出现十张差不多的图。这一步不涉及模型,纯粹是确定性的整理,所以它也最容易测试。
方案生成:把过滤后的候选集交给 LLM,生成一份有结构的策展方案:推荐理由、分组、每张图的用途说明。Prompt 里明确要求只基于候选集发言,不许幻想库里不存在的图——这是和生成类应用打交道的底线约束。
为什么用图不用链
拆完节点之后,最直接的实现是五个函数顺序调用。我选了 LangGraph,理由有三个,都和“会出错”有关。
状态传递有类型。 节点间传递的是一个 TypedDict 定义的状态对象:意图、检索任务、候选集、方案。每个节点声明自己读什么、写什么,新增节点时状态结构的变化是显式的,不用翻五个函数找谁改了哪个字段。
条件边承接失败。 意图分析失败(模型没按格式输出)走重试;召回结果为空走降级(放宽过滤条件再来一路);这些分支在链式调用里是嵌套的 if-else,在图里是显式的边。LLM 应用的执行路径天然不是直的,用图表达最诚实。
可观测。 LangGraph 的每个节点输入输出都能单独记录和回放。调 Prompt 的时候我改的就是单个节点:意图分析的输出不对,就只看意图分析的输入输出,不用从头跑整条流水线。这一点在调优期价值最大。
踩坑
embedding 维度和 collection 不一致。 最早 ChromaDB 建好库后换了一次 embedding 模型,维度变了,查询直接报错。向量库里存着旧模型的向量,查询用新模型的向量,比出来的相似度是噪音。解法没有捷径:换模型就要全量重灌,collection 命名里带上模型名和维度,物理上杜绝混用。
LLM 不守格式。 意图分析要求输出 JSON,模型偶尔会带上“好的,以下是分析结果:”这种前缀,或者输出带注释的伪 JSON。兜底做了两层:先正则剥掉明显的前后缀,再解析失败就走条件边重试,重试 Prompt 里追加一个失败样例。两层之后失败率降到可以接受,但没有降到零——所以意图分析节点的重试上限之外还有一条降级路径:用规则提取关键词直接进入检索,牺牲精度换取可用性。
召回质量差时先别怪模型。 有段时间召回结果总是不相关,第一反应是调 Prompt,折腾两天没起色。后来检查入库数据,发现是元信息拼接时把无意义的字段(系统编号、文件路径)也拼进去了,embedding 噪音很大。清洗入库文本后同样参数下召回质量明显好转。这件事给我的教训:RAG 系统里,数据质量的天花板比 Prompt 的天花板低得多,先检查数据。
效果
dam-scout 随 dam-hub 一起开源。五节点拆分后,最直接的变化是可调试:每次结果不好,都能定位到具体节点——是意图理解偏了,还是召回没召对,十分钟内有结论。单次调用的黑盒时代,这是想都不敢想的。
dam-scout:github.com/konlue/dam-scout。