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