Netflix 图搜索的 AI 进化笔记:从结构化 DSL 到自然语言
- 约 11 分钟读完 - 全文 4232 字Netflix 图搜索的 AI 进化笔记:从结构化 DSL 到自然语言
原文:The AI Evolution of Graph Search at Netflix: From Structured Queries to Natural Language(Alex Hutter、Bartosz Balukiewicz,2026-01-26,Graph Search 系列第四篇,前三篇讲平台本身) 目标读者:正在做或准备做 Text-to-SQL / Text-to-Query / 企业内自然语言搜索的工程师。 阅读目标:搞清楚 Netflix 这套方案里哪些是 LLM 的活、哪些是确定性工程的活,以及为什么这样分工。
一、要解决的问题:用户不想学你的 DSL
先把背景补齐。Netflix 内部的 Content and Business Products 应用群有几百个 UI,常见操作是过滤一张数据表。底层平台是 Graph Search:每个索引由一个 GraphQL query 定义,过滤条件用一套专门的 Graph Search Filter DSL 表达——字段名加比较或包含运算符(如 ==、IN)组成表达式,再用逻辑运算符(如 AND)拼成复杂语句。
问题在于用户和 DSL 之间隔着一层应用 UI:用户点 facet、填查询构建器,应用再把这些交互翻译成 DSL。这条链路有三个毛病,原文列得很坦白:
- 每个应用的输入组件都是各写各的,体验不一,对 DSL 的支持程度也不一,用户得逐个应用去学怎么用;
- 有些索引有几百个可过滤字段,领域专家明明知道自己要什么,却被「填一张巨大的表单」这件事拖住;
- 最根本的一条:用户用自然语言思考,不用查询构建器思考。 强制他们切换上下文,就是引入摩擦。
注意 Netflix 的定位决策:自然语言搜索是增强而不是替换现有应用,把能力做成工具供各应用按自己的领域风格接入。这个定位直接决定了后面「show your work」的设计——后面细说。
二、先把「正确」拆成三层
这篇文章我认为最有价值的部分,是它没有笼统地说「让 LLM 生成正确的查询」,而是把正确性拆成三层,每层用不同的手段治理:
- 语法正确(syntactic):生成的语句能按 DSL 文法解析。这层最简单,纯粹是形式问题。
- 语义正确(semantic):生成的语句要尊重字段类型、只能用索引里真实存在的字段(不许幻觉)、字段值若受受控词表约束则只能用词表内的值。这层需要对索引本身的了解。
- 语用正确(pragmatic):生成的过滤器真的捕捉到了用户意图吗?这层最难——用户搜「Dark」,是指那部德国科幻剧《暗黑》,还是想找「暗黑风格的剧」?
这个三分法的价值在于:前两层可以确定性校验,第三层不能。 原文承认他们没有对语用正确性做校验,也没有找到什么指令能让 LLM 在这层「表现更好」——唯一的手段是把上下文工程做好,再用产品设计兜住。承认 LLM 做不到什么,比宣称能做更重要。
三、上下文工程:双路 RAG 裁剪上下文
LLM 要生成语义正确的过滤语句,必须知道索引里有哪些字段、什么类型、可取什么值。索引由 GraphQL query 定义,所以大部分元数据可以直接从 GraphQL schema 推导:字段名(来自文档路径)、描述(schema 注释)、类型、合法值(enum 或受控词表)。受控词表(controlled vocabulary)是一类特殊字段类型:由领域专家维护的有限值集合,比如国家列表;词表里的每个值有唯一标识、显示名、描述和别名(AKA,例如「romcom」对应「Romantic Comedy」)。
天真的做法是把全部元数据塞进 prompt。简单场景能跑,但不扩展:几百个字段、上千个词表值全塞进去,延迟线性上升,正确率反而下降(大海捞针问题),而且结果变得不稳定。不给值也不行——LLM 会幻觉出不存在的值。于是 Netflix 用经典的 RAG 模式做上下文裁剪,而且是双路的:
flowchart TD
Q["用户问题<br/>自然语言"] --> CH["分块<br/>重叠切分策略"]
subgraph FR["字段 RAG"]
CH --> VS1["向量检索<br/>字段名 描述 类型的 embedding"]
VS1 --> TOP1["每块取 top K 字段"]
end
subgraph CR["受控词表 RAG"]
CH --> VS2["向量检索<br/>词表值的 embedding<br/>含别名 AKA"]
VS2 --> TOP2["每块取 top K 词表值<br/>按词表类型去重"]
end
TOP1 --> MG["合并 去重"]
TOP2 --> MG
MG --> CTX["裁剪后的上下文<br/>字段定义加匹配到的合法值"]
CTX --> LLM["LLM 加指令生成 DSL"]
style CTX fill:#fff3cd
两路各有一个容易忽略的设计细节:
- 字段 RAG 里,用户问题按重叠策略分块,每块独立向量检索取 top K,再合并去重。分块是为了防止长问题里后半截的意图被前半截的 embedding 稀释。
- 受控词表 RAG 不只是给字段补合法值,还能反向推断字段:问题里命中了某个词表的值,就说明相关字段该进上下文,哪怕字段 RAG 没把它捞出来。词表本身通过 GraphQL 定期拉取重建索引,跟上领域变化。
- 原文点了一句:RAG 的效果取决于一堆「杠杆」——reranking 策略、分块策略、embedding 模型的选择——系统性评估留给本系列后两篇。这是诚实的写法,也是大部分 RAG 项目真实的工作量所在。
上下文备好后,指令本身反而朴素(大意):「给定一个自然语言问题,基于以下索引字段及其元数据,生成语法、语义、语用正确的过滤语句。」语法规则写进指令保证第一层;要求 LLM 把生成内容锚定在提供的上下文里保证第二层;第三层只靠上下文工程间接改善。
四、生成之后:确定性校验兜底
LLM 输出不是终点。Netflix 在返回结果前做确定性校验:
- 语法层:用为这套 DSL 自建的 AST 解析器解析,解析失败即判定生成有问题。原文提到也评估过 LLM 的结构化输出模式,但自定义 DSL 不被原生支持,效果好坏参半,所以还是走了 AST 路线。
- 语义层:拿生成的 AST 对照索引元数据校验——字段是否存在、类型是否匹配、词表值是否合法。因为 AST 已经解析出来了、元数据在上下文工程阶段就是现成的,这一步几乎不增加延迟。
- 幻觉处理:检测到幻觉有两个出口——直接把错误返回给用户提示改写,或者把错误反馈给 LLM 自我纠正。后者增加生成耗时,要限制重试次数、谨慎使用。
这套「LLM 生成 + 确定性校验」的分工是全文我最喜欢的设计决策:LLM 负责它擅长的模糊理解,解析器和元数据负责它做不好的精确约束。 校验不通过不是系统失败,是系统正常工作。
五、信任问题:不校验语用,就展示推理
语用正确性没法自动校验,Netflix 的选择是把判断权交还用户,两个手段:
Show your work。 不把裸 DSL 字符串甩给非技术用户,而是把 AST 反射回现有的 UI 组件:LLM 生成 origin.country 等于 Germany,用户看到的「Country」下拉框就被预选成 Germany。用户一眼能看出系统理解得对不对,不对就用熟悉的 UI 控件微调。这个设计把「黑盒生成」变成了「可检查的初稿」——LLM 产出的不是答案,是表单的一次预填。
显式实体消歧(@mentions)。 像 Slack 一样,输入 @ 时从受控词表里直接搜实体,附带上映年份等元数据帮助确认。「When was @dark produced」里的 @dark 被显式锚定到 Series 词表,直接绕过 RAG 推断步骤、把上下文硬编码进去,语用正确性和用户信任同时提升。这是用一点点用户操作换掉一整段不确定的机器推理,非常划算的交换。
端到端架构把上面所有零件串起来,整体上 LLM 只占据中间一环,前后都是确定性工程:
flowchart LR
U["用户问题<br/>可选 @mentions"] --> PRE["预处理<br/>字段 RAG 加词表 RAG<br/>裁剪上下文"]
M["@mentions 显式实体"] -.->|"绕过推断<br/>硬编码上下文"| PRE
PRE --> GEN["LLM 生成<br/>DSL 过滤语句"]
GEN --> POST["后处理<br/>AST 语法校验<br/>元数据语义校验"]
POST -->|"检测到幻觉"| FB["报错或反馈重试<br/>限制次数"]
FB -.-> GEN
POST --> AST["返回 AST"]
AST --> UI["映射为 UI 组件<br/>Chips 和 Facets 预填"]
style GEN fill:#fff3cd
style POST fill:#e8f5e9
六、工程实践联想
把这篇的方案翻译成日常可搬的经验,五条:
- 先给「正确」分层,再谈方案。 语法、语义、语用三层各有各的治理手段,混在一起谈「准确率」只会得到一锅粥的指标。做 LLM 应用的第一件事应该是列出你这个场景的正确性分层,然后标注每层谁能负责——解析器、元数据、还是只能交给用户。
- 上下文是预算,不是仓库。 几百个字段全塞 prompt 导致正确率下降,这个反直觉结论值得记住:RAG 裁剪不只是为了省 token 和延迟,更是为了正确率。给 LLM 的信息应该按「刚好够用」来裁剪,多给是有代价的。
- 确定性校验是 LLM 应用的承重墙。 AST 解析器加元数据校验几乎零延迟,却能拦下所有幻觉字段。凡是输出有形式结构的场景(SQL、DSL、配置、代码),都应该配一个确定性校验层;LLM 的结构化输出模式可以作为补充,但自定义 DSL 场景下别指望它替代解析器。
- 把生成结果反射回 UI,是最便宜的信任工程。 不让用户读 DSL,而是预填他们熟悉的表单控件——生成的对错用户一眼可验,错了顺手就改。这比在 prompt 里再雕百分之几的准确率收益大得多。设计 LLM 功能时先问:用户怎么检查这个输出?
- 留一个显式消歧的旁路。 @mentions 的思路可以泛化:凡是 RAG 推断不确定的地方,看看能不能用一点低成本的用户输入(选择器、@提及、下拉)把不确定性硬消掉。机器推理不该解决所有问题,尤其是那些用户举手之劳就能解决的问题。
本篇小结
- 问题:Netflix 内部几百个应用靠 Graph Search Filter DSL 过滤数据,用户被迫逐应用学习查询构建器;自然语言搜索的目标是把「用户学 DSL」反转成「系统懂自然语言」。
- 框架:正确性拆成语法、语义、语用三层。语法靠 DSL 文法,语义靠索引元数据,语用没有自动校验手段,靠上下文工程加产品设计兜住。
- 机制:双路 RAG 做上下文裁剪——字段 RAG 按元数据 embedding 检索 top K 字段,受控词表 RAG 检索合法值并能反推字段;指令要求 LLM 锚定上下文生成 DSL。
- 兜底:AST 解析器做语法校验,元数据做语义校验,幻觉可报错或有限次反馈重试;结构化输出模式评估过但对自定义 DSL 效果参半。
- 信任:AST 反射回 UI 组件做「show your work」,@mentions 让用户显式锚定实体、绕过 RAG 推断。
- 分工原则:LLM 只做中间一环的模糊理解,预处理和后处理全是确定性工程——这是全文最值得抄的架构判断。
- 留白:RAG 杠杆的调优、生产环境评估、NER、意图路由、查询改写都留给系列后两篇,值得跟进。
参考资料
- The AI Evolution of Graph Search at Netflix: From Structured Queries to Natural Language(本文,2026-01-26)
- Netflix Graph Search 系列前三篇(part 1、2、3,讲平台本身的联邦数据搜索架构,原文开头有链接)
- GraphQL schema 内省文档(字段元数据推导的来源)