<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>AI 工程 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/categories/ai-%E5%B7%A5%E7%A8%8B/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 02 Sep 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/categories/ai-%E5%B7%A5%E7%A8%8B/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>本体数据（Ontology Data）核心概念与组成要素：从本体构建到智能体落地的实施路径</title>
      <link>https://ftmao.com/blog/posts/ontology-data-agent-implementation-path/</link>
      <pubDate>Wed, 02 Sep 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ontology-data-agent-implementation-path/</guid>
      <description>&lt;h1 id=&#34;本体数据ontology-data核心概念与组成要素从本体构建到智能体落地的实施路径&#34;&gt;本体数据（Ontology Data）核心概念与组成要素：从本体构建到智能体落地的实施路径&lt;/h1&gt;
&lt;p&gt;过去两年，一个&amp;quot;老词&amp;quot;重新热了起来：&lt;strong&gt;本体（Ontology）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它上一次被大规模讨论还是二十年前语义网（Semantic Web）的年代。这一次把它重新推上舞台中央的，是大模型和智能体（Agent）。原因很朴素：当 AI 从&amp;quot;聊天&amp;quot;走向&amp;quot;办事&amp;quot;，它需要的不再只是文本语料，而是&lt;strong&gt;对企业业务的结构化理解&lt;/strong&gt;——客户是什么、订单是什么、什么状态能退款、哪个字段对应哪个口径。这些东西散落在各个系统的表结构、接口文档和老员工的脑子里，LLM 猜不到，RAG 检索回来的也只是只言片语。&lt;/p&gt;
&lt;p&gt;Palantir 把 Ontology 做成了 AIP 的基石并在资本市场讲出了漂亮的故事；微软在 2025 年底 Ignite 大会发布 Fabric IQ、把 Ontology 塞进自家数据平台；中国信通院在 2026 年 7 月发布了《本体智能研究报告（1.0）》。本体，正从学术概念变成智能体落地的&amp;quot;语义底座&amp;quot;。&lt;/p&gt;
&lt;p&gt;这篇文章做两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把本体数据的核心概念与组成要素一次梳理清楚；&lt;/li&gt;
&lt;li&gt;给出一条从本体构建到智能体落地的、可操作的实施路径，并配上实战事例。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一什么是本体数据概念演化与边界&#34;&gt;一、什么是本体数据：概念演化与边界&lt;/h2&gt;
&lt;h3 id=&#34;11-从哲学到工程本体的四次演化&#34;&gt;1.1 从哲学到工程：本体的四次演化&lt;/h3&gt;
&lt;p&gt;&amp;ldquo;本体&amp;quot;这个词的履历相当丰富，理解它的演化路径，有助于我们明白今天工程语境下它到底指什么：&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;timeline
    title 本体概念的演化路径
    古希腊~近代哲学 : 哲学本体论（Ontology）
                      : 研究&amp;#34;存在&amp;#34;本身——世界由什么构成
    1990s 人工智能 : 形式化本体
                   : Gruber 定义——&amp;#34;对概念化的显式规范&amp;#34;
                   : 让机器能共享和复用领域知识
    2000s 语义网 : OWL / RDF / SPARQL 标准体系
                 : W3C 语义网技术栈成型
                 : 本体语言具备形式化推理能力
    2012~ 知识图谱 : Google Knowledge Graph 引爆
                   : 本体作为图谱的 Schema 层
                   : 大规模实例数据 + 图存储
    2024~ 本体智能 : 大模型 + Agent 时代
                   : 本体成为智能体的语义基座
                   : 从&amp;#34;查询与推理&amp;#34;走向&amp;#34;决策与行动&amp;#34;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;工程界最常被引用的定义来自 Tom Gruber（1993）：&lt;strong&gt;&amp;ldquo;本体是对概念化的显式规范&amp;rdquo;（An ontology is an explicit specification of a conceptualization）&lt;/strong&gt;。翻译成大白话：把一群人脑子里对某个业务领域的共识，用机器可读的、形式化的方式写下来——有哪些概念、概念之间什么关系、遵守什么规则。&lt;/p&gt;
&lt;p&gt;中国信通院在《本体智能研究报告（1.0）》中追溯了&amp;quot;哲学本体论 → 形式化本体 → 知识图谱 → 本体智能&amp;quot;的完整脉络，并将&lt;strong&gt;本体智能&lt;/strong&gt;界定为：以形式化本体为语义基座，融合语义推理与知识计算，实现信息系统深层理解、逻辑推理与自动化执行的新范式（详见&lt;a href=&#34;https://www.secrss.com/articles/92201&#34;&gt;安全内参报道&lt;/a&gt;）。这个&amp;quot;从理解到执行&amp;quot;的落脚点变化，正是本轮本体复兴与二十年前语义网运动的本质区别。&lt;/p&gt;
&lt;h3 id=&#34;12-本体-vs-知识图谱-vs-语义层-vs-数据模型&#34;&gt;1.2 本体 vs 知识图谱 vs 语义层 vs 数据模型&lt;/h3&gt;
&lt;p&gt;这几个概念经常被混用，但它们的职责边界其实很清晰：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;概念&lt;/th&gt;
					&lt;th&gt;回答的问题&lt;/th&gt;
					&lt;th&gt;类比&lt;/th&gt;
					&lt;th&gt;典型载体&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;数据模型（Schema）&lt;/td&gt;
					&lt;td&gt;数据&lt;strong&gt;怎么存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;仓库的货架设计图&lt;/td&gt;
					&lt;td&gt;ER 图、DDL、数据表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;语义层（Semantic Layer）&lt;/td&gt;
					&lt;td&gt;指标和业务术语&lt;strong&gt;怎么算、怎么叫&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;全公司统一的度量衡手册&lt;/td&gt;
					&lt;td&gt;指标平台、Cube、dbt metrics&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;本体（Ontology）&lt;/td&gt;
					&lt;td&gt;业务世界里&lt;strong&gt;有什么、什么关系、什么规则&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;业务的&amp;quot;词典 + 语法书 + 法规汇编&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;OWL、RDF Schema、Palantir Ontology&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;知识图谱（Knowledge Graph）&lt;/td&gt;
					&lt;td&gt;具体的人和事&lt;strong&gt;是什么样&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;装满货物、贴了统一标签的仓库&lt;/td&gt;
					&lt;td&gt;Neo4j、图数据库中的实例数据&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;用 Jessica Talisman 的一句广为流传的话概括：&amp;quot;&lt;strong&gt;语义层用于查找，本体用于上下文和推理。&lt;/strong&gt;&amp;quot;（参考&lt;a href=&#34;https://www.hubwiz.com/blog/ontology-vs-semantic-layer/&#34;&gt;OceanBase 团队的对比分析&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;一个直观的例子：你问&amp;quot;上个月哪个渠道贡献了最高 GMV&amp;quot;，语义层能给你一个口径统一的精确数字；但你接着问&amp;quot;为什么这个渠道掉了、该不该追加预算&amp;quot;，语义层就哑火了——它不知道&amp;quot;渠道&amp;quot;和&amp;quot;预算策略&amp;quot;、&amp;ldquo;市场活动&amp;rdquo;、&amp;ldquo;竞品动作&amp;quot;之间的关系与规则。而这正是本体的领地。&lt;/p&gt;
&lt;p&gt;再说本体和知识图谱的关系，一句话：&lt;strong&gt;本体是知识图谱的 Schema，知识图谱是本体的实例化。&lt;/strong&gt; 就像面向对象里&amp;quot;类&amp;quot;与&amp;quot;对象&amp;quot;的关系——本体定义了&amp;quot;设备&amp;quot;&amp;ldquo;工单&amp;quot;这些类以及它们的属性和关系，知识图谱里则躺着三万条具体的设备记录和十万条工单。&lt;/p&gt;
&lt;h3 id=&#34;13-为什么-agent-时代需要本体&#34;&gt;1.3 为什么 Agent 时代需要本体&lt;/h3&gt;
&lt;p&gt;不妨先看一个真实行业中反复上演的窘境（&lt;a href=&#34;https://www.hubwiz.com/blog/ontology-vs-semantic-layer/&#34;&gt;OceanBase DataPilot 团队观察&lt;/a&gt;）：同一个&amp;quot;收入&amp;quot;指标，财务说是 1020 万，市场说是 1040 万，Slack 里的 AI 助手说是 980 万。三个数字，没有共识。&lt;/p&gt;
&lt;p&gt;以前这是人的问题，开个十分钟的会就能对齐。但&lt;strong&gt;Agent 不会发消息问同事&amp;quot;这个指标怎么算&amp;rdquo;&lt;/strong&gt;。当它面对一个叫 &lt;code&gt;rev_ttm_adj_v2&lt;/code&gt; 的列名时，要么编造答案，要么随手捡起它找到的第一个定义。行业实践证明，把建模过的领域知识注入检索路径，幻觉率可以降低约 40%——前提是这些知识确实被结构化地沉淀下来了。&lt;/p&gt;
&lt;p&gt;大模型时代本体的价值，可以归纳为三点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对齐语义&lt;/strong&gt;：给 LLM 一份&amp;quot;业务词典&amp;rdquo;，消除同名异义、异名同义，让 Agent 说&amp;quot;行话&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束行为&lt;/strong&gt;：用公理和规则圈出 Agent 能做什么、不能做什么（什么订单能退款、谁能审批），把&amp;quot;幻觉&amp;quot;关进笼子；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支撑推理与行动&lt;/strong&gt;：让 Agent 不只检索片段文本，而是沿关系推理（设备→部件→供应商→库存），最终落到可执行的动作（创建工单、锁定库存）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二本体数据的核心组成要素&#34;&gt;二、本体数据的核心组成要素&lt;/h2&gt;
&lt;h3 id=&#34;21-六大基本要素&#34;&gt;2.1 六大基本要素&lt;/h3&gt;
&lt;p&gt;一个完整的本体，通常由以下要素组成。这里用制造业&amp;quot;设备运维&amp;quot;域来举例说明：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;要素&lt;/th&gt;
					&lt;th&gt;英文&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;设备运维域的例子&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;类 / 概念&lt;/td&gt;
					&lt;td&gt;Class / Concept&lt;/td&gt;
					&lt;td&gt;领域内事物的类型，可继承&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;设备&lt;/code&gt;，其子类：&lt;code&gt;泵&lt;/code&gt;、&lt;code&gt;电机&lt;/code&gt;、&lt;code&gt;压缩机&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;实例 / 个体&lt;/td&gt;
					&lt;td&gt;Instance / Individual&lt;/td&gt;
					&lt;td&gt;类的具体对象&lt;/td&gt;
					&lt;td&gt;&amp;ldquo;3 号车间的 P-102 号离心泵&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;属性&lt;/td&gt;
					&lt;td&gt;Property&lt;/td&gt;
					&lt;td&gt;实例的特征（数据属性）或实例间的关系（对象属性）&lt;/td&gt;
					&lt;td&gt;数据属性：&lt;code&gt;额定功率=75kW&lt;/code&gt;；对象属性：&lt;code&gt;P-102 安装在 3号车间&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;关系&lt;/td&gt;
					&lt;td&gt;Relation&lt;/td&gt;
					&lt;td&gt;类与类之间的语义关联&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;设备 包含 部件&lt;/code&gt;、&lt;code&gt;工单 处理 故障&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;公理 / 约束&lt;/td&gt;
					&lt;td&gt;Axiom / Constraint&lt;/td&gt;
					&lt;td&gt;永远为真的规则，用于推理和校验&lt;/td&gt;
					&lt;td&gt;&amp;ldquo;额定功率 &amp;gt; 100kW 的设备必须每季度点检&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;动作 / 函数&lt;/td&gt;
					&lt;td&gt;Action / Function&lt;/td&gt;
					&lt;td&gt;本体驱动的可执行操作（Palantir 风格的本体扩展）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;创建维修工单(设备, 故障类型)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&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;classDiagram
    class 设备 {
        +设备编号: string
        +额定功率: float
        +投运日期: date
    }
    class 泵
    class 电机
    class 部件 {
        +部件编号: string
        +材质: string
    }
    class 故障 {
        +故障码: string
        +发生时间: datetime
        +严重等级: int
    }
    class 维修工单 {
        +工单号: string
        +状态: enum
    }
    class 供应商 {
        +名称: string
        +备件库存: int
    }

    设备 &amp;lt;|-- 泵 : 继承(is-a)
    设备 &amp;lt;|-- 电机 : 继承(is-a)
    设备 &amp;#34;1&amp;#34; *-- &amp;#34;多&amp;#34; 部件 : 包含(has-part)
    设备 &amp;#34;1&amp;#34; --&amp;gt; &amp;#34;多&amp;#34; 故障 : 发生(has-fault)
    故障 &amp;#34;1&amp;#34; --&amp;gt; &amp;#34;0..1&amp;#34; 维修工单 : 触发(triggers)
    部件 &amp;#34;多&amp;#34; --&amp;gt; &amp;#34;1&amp;#34; 供应商 : 采购自(supplied-by)

    note for 设备 &amp;#34;公理：额定功率&amp;gt;100kW ⇒ 强制季度点检\n动作：创建维修工单(设备, 故障)&amp;#34;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;22-属性的两种类型与语义三元组&#34;&gt;2.2 属性的两种类型与语义三元组&lt;/h3&gt;
&lt;p&gt;在传统本体语言（RDF/OWL）里，所有知识最终都会落到**三元组（Triple）**上：&lt;code&gt;(主语, 谓语, 宾语)&lt;/code&gt;。谓语分两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据属性（Data Property）&lt;/strong&gt;：宾语是字面量，如 &lt;code&gt;(P-102, 额定功率, 75)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象属性（Object Property）&lt;/strong&gt;：宾语是另一个实例，如 &lt;code&gt;(P-102, 位于, 三号车间)&lt;/code&gt;——这是&amp;quot;图&amp;quot;的边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对象属性还可以带语义特征，这些特征是自动推理的燃料：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;属性特征&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;例子&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;传递性（Transitive）&lt;/td&gt;
					&lt;td&gt;A→B 且 B→C 则 A→C&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;部件属于部件&lt;/code&gt;：轴承属于转子，转子属于泵 ⇒ 轴承属于泵&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;对称性（Symmetric）&lt;/td&gt;
					&lt;td&gt;A→B 则 B→A&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;与……同轴安装&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;互逆性（Inverse）&lt;/td&gt;
					&lt;td&gt;A→B 等价于 B→A⁻¹&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;包含部件&lt;/code&gt; 与 &lt;code&gt;属于设备&lt;/code&gt; 互逆&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;函数性（Functional）&lt;/td&gt;
					&lt;td&gt;一个主语最多一个宾语&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;设备拥有唯一资产编号&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;23-用-owlturtle-写一个最小本体&#34;&gt;2.3 用 OWL/Turtle 写一个最小本体&lt;/h3&gt;
&lt;p&gt;让概念落地，下面是一段真实可用的 Turtle 语法（OWL 的文本序列化格式），描述上面设备域的一个切片：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-turtle&#34; data-lang=&#34;turtle&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;@prefix&lt;/span&gt; : &amp;lt;http://example.com/factory#&amp;gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;@prefix&lt;/span&gt; owl: &amp;lt;http://www.w3.org/2002/07/owl#&amp;gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;@prefix&lt;/span&gt; xsd: &amp;lt;http://www.w3.org/2001/XMLSchema#&amp;gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 类与继承&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;设备&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;Class&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;泵&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;Class&lt;/span&gt; ; owl:&lt;span style=&#34;color:#f92672&#34;&gt;subClassOf&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;设备&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;部件&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;Class&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;故障&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;Class&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 对象属性（带传递性）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;包含部件&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;ObjectProperty&lt;/span&gt; , owl:&lt;span style=&#34;color:#f92672&#34;&gt;TransitiveProperty&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           owl:&lt;span style=&#34;color:#f92672&#34;&gt;domain&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;设备&lt;/span&gt; ; owl:&lt;span style=&#34;color:#f92672&#34;&gt;range&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;部件&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;发生故障&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;ObjectProperty&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           owl:&lt;span style=&#34;color:#f92672&#34;&gt;domain&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;设备&lt;/span&gt; ; owl:&lt;span style=&#34;color:#f92672&#34;&gt;range&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;故障&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 数据属性（带函数性约束）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;额定功率&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; owl:&lt;span style=&#34;color:#f92672&#34;&gt;DatatypeProperty&lt;/span&gt; , owl:&lt;span style=&#34;color:#f92672&#34;&gt;FunctionalProperty&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           owl:&lt;span style=&#34;color:#f92672&#34;&gt;domain&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;设备&lt;/span&gt; ; owl:&lt;span style=&#34;color:#f92672&#34;&gt;range&lt;/span&gt; xsd:&lt;span style=&#34;color:#f92672&#34;&gt;float&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 实例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:&lt;span style=&#34;color:#f92672&#34;&gt;P-102&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;a&lt;/span&gt; :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;泵&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;额定功率&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;75.0&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;包含部件&lt;/span&gt; :&lt;span style=&#34;color:#f92672&#34;&gt;P-102-转子&lt;/span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        :&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;发生故障&lt;/span&gt; :&lt;span style=&#34;color:#f92672&#34;&gt;F-2026-0817&lt;/span&gt; .&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;有了这段形式化描述，推理机（Reasoner）就能自动回答：&amp;ldquo;P-102 属于设备吗？&amp;quot;（是，因为 &lt;code&gt;泵&lt;/code&gt; 是 &lt;code&gt;设备&lt;/code&gt; 的子类）、&amp;ldquo;它有哪些部件？&amp;quot;（沿传递关系展开）——&lt;strong&gt;这些答案不是你写进去的，是推出来的&lt;/strong&gt;。这正是本体区别于普通数据库视图的硬核能力。&lt;/p&gt;
&lt;h3 id=&#34;24-工程扩展从静态知识到可行动的本体&#34;&gt;2.4 工程扩展：从&amp;quot;静态知识&amp;quot;到&amp;quot;可行动的本体&amp;rdquo;&lt;/h3&gt;
&lt;p&gt;传统本体止步于&amp;quot;描述世界&amp;rdquo;。而 Palantir 开创并影响了整个行业的工程实践是：&lt;strong&gt;把动作（Action）、权限（Security）、业务逻辑（Function）也装进本体&lt;/strong&gt;。于是本体不再只是&amp;quot;业务的词典&amp;quot;，还是&amp;quot;业务的操作界面&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent 查&amp;quot;哪些设备高风险&amp;quot;——沿本体关系和公理推理；&lt;/li&gt;
&lt;li&gt;Agent 决定&amp;quot;该停机检修&amp;quot;——调用本体上注册的 &lt;code&gt;创建维修工单&lt;/code&gt; Action；&lt;/li&gt;
&lt;li&gt;谁能调用这个 Action、能操作哪些车间的数据——由本体层统一做行级/列级权限控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是&amp;quot;本体是智能体的语义底座&amp;quot;这句话的完整含义：&lt;strong&gt;知识、规则、行动、权限，四位一体。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三本体构建方法论&#34;&gt;三、本体构建方法论&lt;/h2&gt;
&lt;h3 id=&#34;31-三种构建路线&#34;&gt;3.1 三种构建路线&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;自顶向下&lt;/td&gt;
					&lt;td&gt;先由领域专家定义概念体系，再填充实例&lt;/td&gt;
					&lt;td&gt;体系严谨、语义一致&lt;/td&gt;
					&lt;td&gt;周期长、依赖稀缺专家&lt;/td&gt;
					&lt;td&gt;强监管、高严谨场景（医疗、金融合规）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;自底向上&lt;/td&gt;
					&lt;td&gt;从现有数据（表结构、文档）抽取概念，归纳出本体&lt;/td&gt;
					&lt;td&gt;起步快、贴地气&lt;/td&gt;
					&lt;td&gt;容易变成&amp;quot;表结构的镜像&amp;quot;，缺乏业务抽象&lt;/td&gt;
					&lt;td&gt;数据基础好、快速验证场景&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;混合式（推荐）&lt;/td&gt;
					&lt;td&gt;专家定骨架 + LLM 辅助从数据/文档中抽取血肉&lt;/td&gt;
					&lt;td&gt;兼顾质量与效率&lt;/td&gt;
					&lt;td&gt;需要人机协同流程设计&lt;/td&gt;
					&lt;td&gt;绝大多数企业落地场景&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;值得强调的是 LLM 给本体构建带来的范式变化：过去本体工程最大的瓶颈是&lt;strong&gt;人工成本&lt;/strong&gt;（专家访谈、概念梳理动辄数月），现在可以让 LLM 从制度文档、接口文档、表注释里抽取候选概念和关系，专家只做审核和裁决。信通院报告也指出，当前本体工程化最成熟的起点仍是&lt;strong&gt;结构化数据&lt;/strong&gt;；面对文本、表格等非结构化信息，可以结合大模型完成语义抽取与对齐后，再进入统一的本体语义体系（参考&lt;a href=&#34;https://www.eefocus.com/article/2076399.html&#34;&gt;与非网对本体产业的解读&lt;/a&gt;）。&lt;/p&gt;
&lt;h3 id=&#34;32-stanford-七步法经典构建流程&#34;&gt;3.2 Stanford 七步法：经典构建流程&lt;/h3&gt;
&lt;p&gt;本体工程领域最经典的方法论是斯坦福大学提出的&amp;quot;七步法&amp;quot;（Ontology Development 101）。结合 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 TD
    A[1. 确定范围与目的&amp;lt;br/&amp;gt;本体服务哪个业务域？回答什么问题？] --&amp;gt; B[2. 调研复用&amp;lt;br/&amp;gt;是否有现成上位本体/行业标准可继承？]
    B --&amp;gt; C[3. 枚举核心术语&amp;lt;br/&amp;gt;领域专家 + LLM 从文档/表结构抽取候选概念]
    C --&amp;gt; D[4. 定义类与层级&amp;lt;br/&amp;gt;确定 is-a 继承树，区分上位/下位概念]
    D --&amp;gt; E[5. 定义属性与关系&amp;lt;br/&amp;gt;数据属性、对象属性、属性特征]
    E --&amp;gt; F[6. 定义约束与公理&amp;lt;br/&amp;gt;基数约束、值域约束、业务规则]
    F --&amp;gt; G[7. 填充实例并验证&amp;lt;br/&amp;gt;推理机一致性检查 + 胜任问题测试]
    G --&amp;gt;|发现缺口/冲突| C
    G --&amp;gt; H[发布上线，进入版本化治理]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两个容易被忽视但极其关键的环节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;胜任问题（Competency Questions）&lt;/strong&gt;：在建模之前，先写下&amp;quot;这个本体必须能回答的 20 个问题&amp;quot;（如&amp;quot;某供应商断供时，影响哪些在产设备？&amp;quot;）。它是本体范围的验收标准，能有效防止建模发散。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复用优于自建&lt;/strong&gt;：医疗有 SNOMED CT、金融有 FIBO、通用领域有 Schema.org。直接继承成熟上位本体，比从零造轮子靠谱得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-工具链速览&#34;&gt;3.3 工具链速览&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;开源工具&lt;/th&gt;
					&lt;th&gt;商业产品&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;本体建模&lt;/td&gt;
					&lt;td&gt;Protégé、TopBraid Composer（社区版）&lt;/td&gt;
					&lt;td&gt;Palantir Foundry Ontology、PoolParty&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;存储与查询&lt;/td&gt;
					&lt;td&gt;Apache Jena（RDF/SPARQL）、Neo4j（属性图/Cypher）、RDFLib&lt;/td&gt;
					&lt;td&gt;Stardog、GraphDB、Amazon Neptune&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;推理机&lt;/td&gt;
					&lt;td&gt;HermiT、Pellet、Openllet&lt;/td&gt;
					&lt;td&gt;Stardog 推理引擎&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LLM 抽取/对齐&lt;/td&gt;
					&lt;td&gt;LangChain + Pydantic 结构化抽取、GraphRAG 管道&lt;/td&gt;
					&lt;td&gt;Palantir AIP、微软 Fabric IQ&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent 接入&lt;/td&gt;
					&lt;td&gt;MCP Server 自研、LangGraph 工具封装&lt;/td&gt;
					&lt;td&gt;Palantir AIP Logic、Salesforce Agentforce&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四实施路径从本体构建到智能体落地&#34;&gt;四、实施路径：从本体构建到智能体落地&lt;/h2&gt;
&lt;p&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 LR
    subgraph 一[&amp;#34;阶段一：业务域盘点（1~2 周）&amp;#34;]
        A1[选定高价值场景] --&amp;gt; A2[盘点数据源与利益方] --&amp;gt; A3[写下胜任问题清单]
    end
    subgraph 二[&amp;#34;阶段二：本体建模（2~4 周）&amp;#34;]
        B1[专家定概念骨架] --&amp;gt; B2[LLM 辅助抽取候选概念/关系] --&amp;gt; B3[定义公理与动作]
    end
    subgraph 三[&amp;#34;阶段三：数据映射与实例化（2~4 周）&amp;#34;]
        C1[表/接口 → 本体映射] --&amp;gt; C2[ETL 实例化] --&amp;gt; C3[质量校验与消歧]
    end
    subgraph 四[&amp;#34;阶段四：语义服务化（1~2 周）&amp;#34;]
        D1[封装查询 API/SPARQL/Cypher] --&amp;gt; D2[封装 Action 接口] --&amp;gt; D3[权限与审计]
    end
    subgraph 五[&amp;#34;阶段五：Agent 接入（2~3 周）&amp;#34;]
        E1[工具注册 / MCP Server] --&amp;gt; E2[Prompt 注入领域语义] --&amp;gt; E3[编排推理与行动链]
    end
    subgraph 六[&amp;#34;阶段六：评估与治理迭代（持续）&amp;#34;]
        F1[胜任问题回归测试] --&amp;gt; F2[幻觉率/准确率评估] --&amp;gt; F3[本体版本演进]
    end
    一 --&amp;gt; 二 --&amp;gt; 三 --&amp;gt; 四 --&amp;gt; 五 --&amp;gt; 六
    六 -.反馈.-&amp;gt; 二&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;41-阶段一业务域盘点别上来就建模&#34;&gt;4.1 阶段一：业务域盘点——别上来就建模&lt;/h3&gt;
&lt;p&gt;最常见的失败模式是：IT 团队热血沸腾地建了一个覆盖全公司的&amp;quot;企业级本体&amp;quot;，半年后无人问津。正确的姿势是&lt;strong&gt;场景驱动&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;选一个痛且高频的场景&lt;/strong&gt;：比如&amp;quot;设备故障诊断与维修调度&amp;quot;——每天发生、跨系统（MES + ERP + 库存）、出错代价高；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;盘点数据源&lt;/strong&gt;：这个场景涉及哪些系统的哪些表、哪些文档（SOP、维修手册）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写胜任问题清单&lt;/strong&gt;：和业务方一起写下 Agent 必须能回答/执行的事，作为本体的验收标准。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;感悟：&lt;strong&gt;本体不是数据架构的目标，而是业务问题的副产品。&lt;/strong&gt; 没有胜任问题清单的本体项目，九成活不过一年。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;42-阶段二本体建模人机协同&#34;&gt;4.2 阶段二：本体建模——人机协同&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;领域专家定义概念骨架（设备、部件、故障、工单、供应商五大类及继承关系）；&lt;/li&gt;
&lt;li&gt;LLM 从维修手册、点检 SOP、表注释中抽取候选属性与关系，专家审核入库；&lt;/li&gt;
&lt;li&gt;定义公理（&amp;ldquo;功率&amp;gt;100kW 必须季度点检&amp;rdquo;）和动作（&lt;code&gt;创建工单&lt;/code&gt;、&lt;code&gt;锁定备件&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;反模式提醒&lt;/strong&gt;：不要照搬数据库表结构。表是&amp;quot;怎么存&amp;quot;，本体是&amp;quot;怎么理解&amp;quot;。比如 ERP 里 &lt;code&gt;MATERIAL&lt;/code&gt;、&lt;code&gt;BOM_ITEM&lt;/code&gt;、&lt;code&gt;SPARE_PART&lt;/code&gt; 三张表，在本体里可能统一为&lt;code&gt;部件&lt;/code&gt;一个概念加几个关系——本体建模的过程，本身就是一次业务概念对齐。&lt;/p&gt;
&lt;h3 id=&#34;43-阶段三数据映射与实例化&#34;&gt;4.3 阶段三：数据映射与实例化&lt;/h3&gt;
&lt;p&gt;把存量数据灌进图谱，技术上是 ETL，难点在&lt;strong&gt;实体消歧与对齐&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个供应商，ERP 里叫&amp;quot;上海精工机电有限公司&amp;quot;，MES 里叫&amp;quot;精工机电&amp;quot;——需要基于规则 + LLM 的实体对齐；&lt;/li&gt;
&lt;li&gt;保留&lt;strong&gt;溯源信息&lt;/strong&gt;：每个实例记录它来自哪个系统的哪条记录，这是后续可信审计的根基。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-阶段四语义服务化&#34;&gt;4.4 阶段四：语义服务化&lt;/h3&gt;
&lt;p&gt;本体不能直接丢给 Agent 裸查，要封装成稳定的语义服务：&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
    subgraph 语义服务层
        Q[查询服务&amp;lt;br/&amp;gt;SPARQL / Cypher / REST] 
        Act[动作服务&amp;lt;br/&amp;gt;创建工单 / 锁库存 / 派工]
        S[权限与审计&amp;lt;br/&amp;gt;行级权限 + 操作留痕]
    end
    KG[(知识图谱&amp;lt;br/&amp;gt;Neo4j / RDF Store)]
    ERP[(ERP)] 
    MES[(MES)]
    WMS[(库存系统)]
    
    ERP --&amp;gt;|ETL 映射| KG
    MES --&amp;gt;|ETL 映射| KG
    WMS --&amp;gt;|ETL 映射| KG
    KG --&amp;gt; Q
    Q --&amp;gt; Act
    Act --&amp;gt;|回写| MES
    S -.管控.-&amp;gt; Q
    S -.管控.-&amp;gt; Act&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这一层的意义：&lt;strong&gt;Agent 永远通过受控接口访问世界&lt;/strong&gt;，而不是直接操作数据库。权限收敛在一处，审计留痕在一处。&lt;/p&gt;
&lt;h3 id=&#34;45-阶段五agent-接入&#34;&gt;4.5 阶段五：Agent 接入&lt;/h3&gt;
&lt;p&gt;当前主流有两种接入方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具封装&lt;/strong&gt;：把语义服务包成 Agent 框架的 Tool（LangChain/LangGraph/AgentScope 都支持），在工具描述里写清楚本体语义；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP（Model Context Protocol）&lt;/strong&gt;：把语义服务做成 MCP Server，让任何兼容 Agent 即插即用——这是 2025 年后快速成为事实标准的路线。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次典型的 Agent 调用时序如下：&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;sequenceDiagram
    participant U as 值班工程师
    participant AG as 运维 Agent (LLM)
    participant MCP as 本体 MCP Server
    participant KG as 知识图谱
    participant MES as MES 系统

    U-&amp;gt;&amp;gt;AG: P-102 振动超标，怎么办？
    AG-&amp;gt;&amp;gt;MCP: query(设备 P-102 的档案/历史故障/部件清单)
    MCP-&amp;gt;&amp;gt;KG: Cypher 查询
    KG--&amp;gt;&amp;gt;MCP: 设备档案 + 3 次历史故障(均为轴承磨损) + 部件: 转子/轴承
    MCP--&amp;gt;&amp;gt;AG: 结构化结果
    AG-&amp;gt;&amp;gt;MCP: query(轴承备件库存与供应商)
    MCP-&amp;gt;&amp;gt;KG: 沿 部件→供应商 关系查询
    KG--&amp;gt;&amp;gt;MCP: 库存 2 件, 供应商交期 3 天
    MCP--&amp;gt;&amp;gt;AG: 结构化结果
    Note over AG: 推理：大概率轴承磨损&amp;lt;br/&amp;gt;备件充足，建议立即更换
    AG-&amp;gt;&amp;gt;MCP: action: 创建维修工单(P-102, 更换轴承)
    MCP-&amp;gt;&amp;gt;MES: 回写工单（含权限校验+审计）
    MES--&amp;gt;&amp;gt;MCP: 工单 WO-2026-3312
    MCP--&amp;gt;&amp;gt;AG: 成功
    AG--&amp;gt;&amp;gt;U: 建议立即停机更换轴承（备件在库），已创建工单 WO-2026-3312，依据：近半年 3 次同型故障…&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;注意最后一轮回复的形态：&lt;strong&gt;结论 + 已执行动作 + 推理依据&lt;/strong&gt;。这就是本体给 Agent 带来的质变——它不再&amp;quot;复述检索到的文档片段&amp;quot;，而是在一个结构化的业务世界里推理和行动，并且&lt;strong&gt;每一步都有据可查&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;46-阶段六评估与治理迭代&#34;&gt;4.6 阶段六：评估与治理迭代&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;回归测试&lt;/strong&gt;：每次本体变更后，用胜任问题清单跑回归——这 20 个问题还能答对吗？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果评估&lt;/strong&gt;：幻觉率、答案准确率、Action 执行成功率，对比接入本体前后的基线；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本治理&lt;/strong&gt;：本体是活资产，业务变了本体就要演进。建立变更评审机制，防止&amp;quot;语义漂移&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;47-三个落地案例速览&#34;&gt;4.7 三个落地案例速览&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;行业&lt;/th&gt;
					&lt;th&gt;本体核心概念&lt;/th&gt;
					&lt;th&gt;Agent 能力&lt;/th&gt;
					&lt;th&gt;价值点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;制造业&lt;/td&gt;
					&lt;td&gt;设备、部件、故障、工单、供应商&lt;/td&gt;
					&lt;td&gt;故障诊断 + 自动派单&lt;/td&gt;
					&lt;td&gt;平均故障响应时间从小时级降到分钟级&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;金融&lt;/td&gt;
					&lt;td&gt;客户、账户、交易、产品、监管规则&lt;/td&gt;
					&lt;td&gt;合规审查 Agent：沿&amp;quot;账户→交易→规则&amp;quot;推理风险敞口&lt;/td&gt;
					&lt;td&gt;审查口径统一，漏报率下降&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;医疗&lt;/td&gt;
					&lt;td&gt;疾病、症状、药品、检查、禁忌（复用 SNOMED CT）&lt;/td&gt;
					&lt;td&gt;辅助诊断 Agent：校验用药禁忌与检查互斥&lt;/td&gt;
					&lt;td&gt;用公理兜底 LLM 的危险建议&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五2026-年行业最新动态&#34;&gt;五、2026 年行业最新动态&lt;/h2&gt;
&lt;p&gt;本体赛道在过去一年明显升温，几条值得关注的线索：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Palantir：Ontology 成为 AIP 的灵魂&lt;/strong&gt;。Palantir 把 Ontology 从数据建模层升级为&amp;quot;企业操作系统&amp;quot;的概念核心——知识、动作、安全三合一，Agent 在 Ontology 之上编排业务操作。2026 年 4 月，Palantir 将 AIP Agent Studio 更名为 AIP Chatbot Studio，进一步明确对话式 Agent 产品定位；AIP Analyst 也于 2026 年 4 月 GA，支持对话式查询 Ontology 数据、生成可视化并执行动作（参考&lt;a href=&#34;https://releasefeed.net/palantir/palantir-2026-04-22-aip-chatbot-studio-rebrand/&#34;&gt;Palantir 发布解读&lt;/a&gt;与 &lt;a href=&#34;https://pub.towardsai.net/inside-palantir-aip-how-the-worlds-most-controversial-ai-platform-actually-works-9ec5b7a6c05a&#34;&gt;Towards AI 深度解析&lt;/a&gt;）。美国陆军 TITAN、Maven 等大额合同持续验证这套打法（&lt;a href=&#34;https://kalkine.com/news/premium/palantir-pltr-stock-analysis-2026-aip-momentum-valuation-reality-check-and-the-road-ahead&#34;&gt;Kalkine 分析&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微软：Fabric IQ 把 Ontology 搬进数据平台&lt;/strong&gt;。Fabric IQ 于 2025 年 11 月 Ignite 大会发布、2026 年 6 月 Build 大会正式 GA，通过 Ontology 和 Power BI 语义模型把 OneLake 中的数据提升为&amp;quot;业务级语义&amp;quot;，与 Work IQ、Foundry IQ、Web IQ 共同构成微软的企业语义智能层（参考&lt;a href=&#34;https://blog.csdn.net/hyde_jiang/article/details/162932418&#34;&gt;语义工程系列分析&lt;/a&gt;）。数据平台厂商集体下场做本体，是本轮最重要的信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中国信通院发布《本体智能研究报告（1.0）》&lt;/strong&gt;（2026 年 7 月，智能原生大会）：首次系统界定&amp;quot;本体智能&amp;quot;内涵，梳理从哲学本体论到本体智能的演化脉络，提出以形式化本体为智能体可信语义底座的产业范式（&lt;a href=&#34;https://www.secrss.com/articles/92201&#34;&gt;安全内参&lt;/a&gt;、&lt;a href=&#34;https://www.modb.pro/db/2076843769111584768&#34;&gt;墨天轮&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义层与知识图谱全面复兴&lt;/strong&gt;。腾讯云的行业观察指出，2026 年 AI 模型对&amp;quot;正确数据&amp;quot;的需求推动语义层和知识图谱崛起，SAP、Salesforce 等应用巨头纷纷把语义层作为 Agent 战略的前置投入（&lt;a href=&#34;https://cloud.tencent.com/developer/article/2616559&#34;&gt;腾讯云开发者社区&lt;/a&gt;）。企业智能问数也已分化出 RAG 召回、NL2SQL、指标平台增强、本体语义四大技术路线（&lt;a href=&#34;https://tencentcloud.csdn.net/69eae14054b52172bc6f7a43.html&#34;&gt;路线对比分析&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraphRAG 成为本体价值的最快验证点&lt;/strong&gt;。微软开源的 GraphRAG 路线（从文档抽取实体关系构建图谱，再做社区摘要与图增强检索）让&amp;quot;结构化知识增强生成&amp;quot;从理念变成 pip install 级的实践，很多企业正是从 GraphRAG 尝到甜头后，才开始认真考虑更完整的本体工程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结行业共识：&lt;strong&gt;大模型的军备竞赛告一段落，竞争的下半场是&amp;quot;语义基建&amp;quot;——谁家的 Agent 更懂业务，谁就赢。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六思考与感悟&#34;&gt;六、思考与感悟&lt;/h2&gt;
&lt;p&gt;写完这条路径，分享几点踩坑后的真实感受：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 本体的价值不在&amp;quot;建&amp;quot;，在&amp;quot;对齐&amp;quot;。&lt;/strong&gt; 本体建模最有产出的时刻，往往不是画类图的时候，而是业务、数据、IT 三方为了&amp;quot;&amp;lsquo;客户&amp;rsquo;到底包不包括潜在客户&amp;quot;吵一下午的时候。那个争吵的过程，就是组织知识显式化的过程。建出来的图谱可能三年就换代了，但吵出来的共识会留在组织里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 谨防&amp;quot;本体过度工程&amp;quot;。&lt;/strong&gt; 见过太多团队一上来追求形式化完美——全套 OWL 公理、严密的上位本体继承——结果六个月没见到一个能跑的 Agent。务实的顺序是：&lt;strong&gt;先让 Agent 用上粗糙的本体跑起来，再在使用中打磨语义&lt;/strong&gt;。胜任问题答得对，比逻辑上自洽重要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 本体是 Agent 的&amp;quot;宪法&amp;quot;，不是&amp;quot;百科全书&amp;quot;。&lt;/strong&gt; 不要指望本体装下所有知识——非结构化的经验、文档、案例，交给 RAG；本体负责的是&lt;strong&gt;不容含糊的那部分&lt;/strong&gt;：核心概念、口径、规则、权限边界。一个好的心智模型是：RAG 给 Agent &amp;ldquo;见识&amp;rdquo;，本体给 Agent &amp;ldquo;常识与纪律&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 行动能力（Action）是这轮本体复兴的灵魂。&lt;/strong&gt; 二十年前的语义网止步于&amp;quot;让机器理解数据&amp;quot;，失败了；今天的本体因为绑定了 Agent 的行动闭环而重生。如果你的本体里没有定义任何 Action，那它大概率只是一张漂亮但寂寞的图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 对个体工程师的启示。&lt;/strong&gt; 本体工程是少有的&amp;quot;越老越吃香&amp;quot;的 AI 技能——模型会迭代，框架会更替，但对一个行业业务概念体系的深刻理解，沉淀的是复利。选一个你熟悉的垂直领域，亲手建一个小本体、接一个 Agent 跑通闭环，是 2026 年性价比极高的技术投资。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.secrss.com/articles/92201&#34;&gt;中国信通院《本体智能研究报告（1.0）》发布解读 - 安全内参&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hubwiz.com/blog/ontology-vs-semantic-layer/&#34;&gt;本体论 vs. 语义层 - OceanBase/Hubwiz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.eefocus.com/article/2076399.html&#34;&gt;本体产业深度解读：语义层重构智能体数据底座 - 与非网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://cloud.tencent.com/developer/article/2616559&#34;&gt;AI 数据新纪元：语义层与知识图谱的崛起 - 腾讯云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.csdn.net/hyde_jiang/article/details/162932418&#34;&gt;微软语义层：从 Schema 到本体的演进 - CSDN 语义工程系列&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pub.towardsai.net/inside-palantir-aip-how-the-worlds-most-controversial-ai-platform-actually-works-9ec5b7a6c05a&#34;&gt;Inside Palantir AIP: How the Platform Actually Works - Towards AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://jimo.studio/blog/enterprise-ontology-engineering-the-semantic-foundation-for-next-generation-ai/&#34;&gt;本体工程：从知识图谱到企业智能体的语义基石 - jimo.studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Noy &amp;amp; McGuinness, &lt;em&gt;Ontology Development 101: A Guide to Creating Your First Ontology&lt;/em&gt;, Stanford University&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了</title>
      <link>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</link>
      <pubDate>Fri, 28 Aug 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</guid>
      <description>&lt;h1 id=&#34;netflix-maps-笔记让推荐模型看见素材冷启动就不冷了&#34;&gt;Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 应用的工程师，尤其是被冷启动、离线线上不一致折磨过的人。
阅读目标：搞清楚 Netflix 怎么用多模态向量把素材个性化的冷启动问题「绕」过去，以及那套消融实验和代理任务的方法论为什么值得抄。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;问题本身素材是个不透明的-id&#34;&gt;〇、问题本身：素材是个不透明的 ID&lt;/h2&gt;
&lt;p&gt;Netflix 把海报、视频预览这些视觉线索统称为素材（asset），给每个会员挑哪张海报，本身就是一个个性化问题。老模型回答「老标题挑哪张图」回答得很好，但「新标题刚上线怎么办」回答得很差：模型只从历史交互里学习，每张素材在它眼里只是一个不透明的 ID，图里画了什么人、什么色调，它一概看不见。于是新剧上线时素材没有任何历史，只能加大探索流量去「刷数据」，其余时候退回人气启发式——完全不认你的口味。这就是教科书式的冷启动。&lt;/p&gt;
&lt;p&gt;原文的解法一句话：&lt;strong&gt;让模型看见并听见它要推荐的东西&lt;/strong&gt;。新素材一到手就有向量，而向量空间已经承载了会员对相似素材的偏好，所以个性化在接近上线的那一刻就能启动，不用等交互慢慢堆出来。&lt;/p&gt;
&lt;h2 id=&#34;一海报个性化让模型看图&#34;&gt;一、海报个性化：让模型看图&lt;/h2&gt;
&lt;p&gt;做法直接到近乎朴素：用预训练的 CLIP 把每张海报编码成一个 768 维向量，和素材原有的可学习 ID 向量&lt;strong&gt;拼接&lt;/strong&gt;，作为素材的最终表示。就这一个改动。&lt;/p&gt;
&lt;p&gt;效果的关键在于迁移：会员对「某位喜剧演员的脸」「某种配色」的偏好，本来写在图片向量空间里而不是绑死在素材 ID 上，所以新剧海报一编码完，这些偏好立刻生效——哪怕这张图从没给任何人展示过。冷启动从盲区变成了「向量空间对此早有意见」。&lt;/p&gt;
&lt;h2 id=&#34;二五个模型合成一个&#34;&gt;二、五个模型合成一个&lt;/h2&gt;
&lt;p&gt;第二个收益更工程化。同一部剧的海报要铺到五个画布上（billboard、vertical-box、horizontal-panel、short-panel、landscape-panel），裁剪比例各不相同。ID 时代的模型不知道这些裁剪图是同一幕戏，信号无法在画布之间流动，于是历史上每个画布各训一个模型，各自承受各自的冷启动。&lt;/p&gt;
&lt;p&gt;CLIP 向量对裁剪、缩放、宽高比基本不变，同一幕戏的五个版本映射到几乎同一个向量——壁垒没了，五个模型合成一个，高流量画布学到的偏好立刻灌溉稀疏画布。收益最大的恰恰正是数据最少的画布。&lt;/p&gt;
&lt;p&gt;合并带来一个新问题：五个画布的曝光量天差地别，直接按原始计数混训，高流量画布和高频交互类型会淹没一切，本想帮扶的稀疏画布反而最吃亏；手工调权重又换来一堆拍脑袋超参。Netflix 的做法是&lt;strong&gt;奖励加权&lt;/strong&gt;：每条训练样本按其交互类型的长期奖励分（来自 Netflix 既有的长期奖励建模）加权。交互类型在各画布分布不均，按长期价值加权之后，画布配比自动重平衡——画布按「它驱动的交互的长期价值」贡献梯度，而不是按曝光量。没有手工权重，模型优化目标从「最高频的短期动作」换成了「长期会员满意度」。&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
    IMG[&amp;#34;新海报上线&amp;#34;] --&amp;gt; CLIP[&amp;#34;CLIP 图像编码器&amp;lt;br/&amp;gt;输出 768 维向量&amp;#34;]
    ID[&amp;#34;素材 ID 向量&amp;#34;] --&amp;gt; CONCAT[&amp;#34;拼接成素材表示&amp;#34;]
    CLIP --&amp;gt; CONCAT
    CONCAT --&amp;gt; MODEL[&amp;#34;统一模型&amp;lt;br/&amp;gt;五个画布共享&amp;#34;]
    HOT[&amp;#34;高流量画布&amp;lt;br/&amp;gt;billboard 等&amp;#34;] --&amp;gt;|&amp;#34;交互信号沿向量空间迁移&amp;#34;| MODEL
    MODEL --&amp;gt;|&amp;#34;长期奖励加权混训&amp;#34;| SPARSE[&amp;#34;稀疏画布直接受益&amp;lt;br/&amp;gt;short-panel 等&amp;#34;]
    style MODEL fill:#fff3cd
    style SPARSE fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;三离线评估ips-与探索流量&#34;&gt;三、离线评估：IPS 与探索流量&lt;/h2&gt;
&lt;p&gt;这套结果能信，前提是评估干净。用当前线上策略的日志评估新模型是有偏的：线上策略偏爱某些素材，日志记录的是「策略选了什么」，不是「会员从全集里会选什么」。和线上策略意见不合的新模型会被冤枉——它会挑的曝光在日志里几乎不存在。&lt;/p&gt;
&lt;p&gt;Netflix 用一小股&lt;strong&gt;随机探索流量&lt;/strong&gt;做 IPS（逆倾向评分）：随机策略按已知分布在候选素材里采样，倾向分在服务时刻精确记录，而不是事后估计。每条观测按倾向分的倒数加权，被探索流量压稀的曝光得到相应抬升，得到候选策略的无偏奖励估计。原文有句话值得划线：&lt;strong&gt;倾向分由构造保证已知而非事后建模，是离线数字能跟踪线上结果的最大单一原因。&lt;/strong&gt; 候选模型必须在 IPS（相对线上基线的比值）上先赢，才配拿 A/B 流量。&lt;/p&gt;
&lt;h2 id=&#34;四消融实验两个成分缺一不可&#34;&gt;四、消融实验：两个成分缺一不可&lt;/h2&gt;
&lt;p&gt;「图片向量」和「模型合并」两个想法捆在一起，Netflix 分开消融，对照老的五模型系统：V1 只加图片向量（仍是五个画布模型），V2 只合并（统一模型但纯 ID），V3 两者都要。离线指标上 ±1% 以内不算显著，V1、V2 单独做基本都在噪声带里；线上全平台跑了至少四周的 A/B 给出更清楚的分界线：&lt;strong&gt;V1、V2 双双不显著，只有 V3 拿到统计显著的提升&lt;/strong&gt;——V3 就是今天线上跑的版本。&lt;/p&gt;
&lt;p&gt;原因也讲得透：V1 告诉单画布模型素材长什么样，但稀疏画布样本太少，教不会它怎么用这条信息；V2 数据够了，但全是 ID 信号，新素材没有。V3 两者兼有，成熟画布教会共享模型「CLIP 向量如何映射到会员偏好」，这个映射直接迁移到稀疏画布。效果是相乘不是相加——V3 在 short-panel 上的提升（5.691%）超过 V1 与 V2 之和。原文的教训写得很直白：&lt;strong&gt;下结论「内容特征没用」之前，先找第二个阻塞因子。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真实考验来自产品侧：Netflix 十年来最大的电视首页改版，让 short-panel 一夜之间成为主战场——即将吃下最多曝光的画布恰好历史数据最少。V3 赶在改版前上线，配合一个月的 holdback A/B，核心发现指标和播放时长双双显著增长，且增幅比稳态消融更大。&lt;/p&gt;
&lt;h2 id=&#34;五查询感知的海报排序&#34;&gt;五、查询感知的海报排序&lt;/h2&gt;
&lt;p&gt;浏览时你的长期口味是对的信号，搜索时不是：搜某个演员时你要的是带他的海报，哪怕你的广义口味另有指向。搜索页的意图写在 query 里，海报应当响应它。&lt;/p&gt;
&lt;p&gt;因为 CLIP 把文本和图像投进了同一个向量空间，这件事近乎白送：直接算 query 的文本向量与素材图像向量的余弦相似度，和原有个性化分数按权重 α（0 到 1，线上 A/B 调）混合。第一项是「我们认为你喜欢什么」，第二项是「你刚开口要什么」。没有任何额外建模——向量本来就在素材表示里，排序时加一个相似度项而已。&lt;/p&gt;
&lt;h2 id=&#34;六视频预览mediafm&#34;&gt;六、视频预览：MediaFM&lt;/h2&gt;
&lt;p&gt;视频预览比静态图难：吸引力来自运动、节奏、对白、配乐，单帧说不清。第一版内容感知方案 SeqCLIP 只取各帧的 CLIP 向量求平均——看得见长相，听不见声音，而对白和音乐恰恰扛着预览片的大半调性。&lt;/p&gt;
&lt;p&gt;补全这块的是 MediaFM，Netflix 第一个自研多模态地基模型，在 8000 万个镜头（shot）上训练，每个镜头融合三路信号成一个向量：视觉走 SeqCLIP，音频走预训练语音音频编码器，文本是字幕经大规模文本模型编码。接入方式和 CLIP 一样：把向量拼进素材表示，不需要新基建。&lt;/p&gt;
&lt;p&gt;离线 IPS 和五周全平台 A/B 给出一致的排序：&lt;strong&gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&lt;/strong&gt;，每加一层内容感知都有收益，TV 端收益最大。音频和定时文本是 SeqCLIP 这类纯视觉编码器够不到的，这部分增量是真的。MediaFM 已作为默认视频预览向量全平台上线。&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
    subgraph FM[&amp;#34;MediaFM 多模态地基模型&amp;lt;br/&amp;gt;8000 万镜头训练&amp;#34;]
        V[&amp;#34;视觉&amp;lt;br/&amp;gt;SeqCLIP 帧向量平均&amp;#34;] --&amp;gt; FUSE[&amp;#34;融合成镜头级向量&amp;#34;]
        A[&amp;#34;音频&amp;lt;br/&amp;gt;预训练语音音频编码器&amp;#34;] --&amp;gt; FUSE
        T[&amp;#34;文本&amp;lt;br/&amp;gt;字幕经大规模文本模型&amp;#34;] --&amp;gt; FUSE
    end
    FUSE --&amp;gt; STORE[&amp;#34;Embedding Store&amp;lt;br/&amp;gt;训练与推理同源 无偏斜&amp;#34;]
    STORE --&amp;gt; PROBE[&amp;#34;线性探针代理任务&amp;lt;br/&amp;gt;仅凭向量预测去偏人气赢家&amp;#34;]
    PROBE --&amp;gt;|&amp;#34;跑赢随机猜测才放行&amp;#34;| AB[&amp;#34;离线 IPS + 线上 A/B&amp;lt;br/&amp;gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&amp;#34;]
    style PROBE fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七便宜地挑向量线性探针&#34;&gt;七、便宜地挑向量：线性探针&lt;/h2&gt;
&lt;p&gt;新向量层出不穷，端到端试验太贵——数据工程、重训、数周 A/B 流量，不可能每个候选都走全程。Netflix 用一个便宜问题把漏斗口收窄：&lt;strong&gt;仅凭内容向量，能不能预测一个朴素无个性化策略下哪张素材会赢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;做法：固定一批标题，用探索数据按倾向分去偏后找出每个标题的人气赢家，打成二值标签；然后只用一个线性探针从素材向量恢复这个标签（标准二值交叉熵），不带标题、演员等任何元数据。保持线性和纯向量是刻意的——它隔离出「人气驱动因子有多少真的编码进了向量」。向量若抓住了人气的语义驱动，线性分类器就该能认出赢家；抓不住，探针就和随机猜测一样，而后者正是它的及格线。&lt;/p&gt;
&lt;p&gt;这个探针先把一大批候选剪到两个决赛选手（SeqCLIP 和最强 MediaFM 变体），再送全程评估。三条信号——探针准确率、离线 IPS、线上 A/B——对两个选手的排序完全一致，所以现在每个 MediaFM 新版本发布前都得先过探针这一关。&lt;/p&gt;
&lt;h2 id=&#34;八embedding-store让一切成立的地基&#34;&gt;八、Embedding Store：让一切成立的地基&lt;/h2&gt;
&lt;p&gt;所有向量——CLIP、SeqCLIP、MediaFM——都活在 Netflix Embedding Store 里（Netflix AI Platform 的组件）。地基模型把原始素材编码成向量只做一次，Store 用同一接口喂给所有下游；关键性质是&lt;strong&gt;训练时和在线推理时服务的是完全相同的向量&lt;/strong&gt;，模型学到的和线上见到的之间没有 skew。&lt;/p&gt;
&lt;p&gt;另一个性质是解耦：新向量（或老向量的新版本）注册、回填、验证全部独立进行，不碰任何消费方的训练和推理代码，下游靠配置即可启用。CLIP 进海报模型、同一批向量撑起查询感知排序、MediaFM 滚进视频预览模型，各自是独立变更而不是跨团队迁移。下一步目标是把 Store 推向图文视频统一的语义空间，做跨模态检索和跨界面统一排序。&lt;/p&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;冷启动的最优解常常是换表示，不是加探索。&lt;/strong&gt; 探索流量和人气回退都是在「ID 无知」的框架里打补丁；一旦实体有了内容向量，偏好沿向量空间迁移，问题在表示层就消掉了。做实体冷启动（新商品、新文档、新用户）时先问：这个实体有没有一个下游模型读得懂的内容表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「特征没用」往往是因为别处先堵着。&lt;/strong&gt; V1 的失败不是图片向量没信息，是稀疏画布样本不够教模型用；V2 的失败不是合并不对，是 ID 信号对不上新素材。多因素实验里单变量不显著，先怀疑第二个阻塞因子，别急着枪毙特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估体系的纯度比模型重要。&lt;/strong&gt; IPS 能跟踪线上结果，靠的是倾向分在服务时刻由构造已知——日志里事后估出来的倾向分自带一层建模误差。值得为评估专门留一股随机探索流量，这笔钱花在刀刃上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用廉价代理任务给昂贵实验把关。&lt;/strong&gt; 线性探针 + 随机基线，成本几乎为零，却和端到端 A/B 给出了一致排序。任何「候选方案多、全量验证贵」的场景（选向量、选特征、选模型结构）都该先架一道这样的探针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量服务要当成基础设施来建。&lt;/strong&gt; 训练推理同源、版本注册回填独立、下游配置即用——这三条让「换向量」从跨团队项目降级为一次配置变更。Embedding Store 这类组件才是多模态落地真正的护城河，模型本身反而不是。&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;：素材个性化的冷启动——老模型把素材当不透明 ID，新素材只能靠探索流量和人气启发式硬扛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;海报&lt;/strong&gt;：CLIP 768 维向量拼接 ID 向量，偏好沿向量空间即时迁移；五个画布模型合并为一个，混训按交互类型的长期奖励分加权，画布按长期价值而非曝光量贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估&lt;/strong&gt;：专门探索流量 + IPS，倾向分服务时刻已知，离线数字才跟得上线上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消融&lt;/strong&gt;：V1（仅向量）、V2（仅合并）线上均不显著，V3（两者兼有）显著胜出，short-panel 提升 5.691% 且超过 V1+V2 之和；电视首页改版实战中 holdback A/B 证实收益更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索&lt;/strong&gt;：query 文本向量与素材图像向量的余弦相似度，按 α 与个性化分数混合，零额外建模。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频&lt;/strong&gt;：MediaFM（8000 万镜头，视觉+音频+文本三模态）＞ SeqCLIP ＞ 纯 ID，TV 端收益最大，已全平台默认上线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法论&lt;/strong&gt;：线性探针代理任务廉价筛选向量，三条信号排序一致后成为 MediaFM 发版门禁；Embedding Store 保证训练推理同源、解耦上下游。&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/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;（原文，Netflix TechBlog）。&lt;/li&gt;
&lt;li&gt;Radford et al., &amp;ldquo;Learning Transferable Visual Models From Natural Language Supervision&amp;rdquo;（CLIP 原始论文，文中海报向量与图文共空间的来源）。&lt;/li&gt;
&lt;li&gt;Netflix TechBlog 关于长期奖励建模（long-term reward modeling）与 Netflix AI Platform / Embedding Store 的相关文章（原文内引用，用于奖励加权与向量基础设施）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>LiveTalking 源码解剖：一个开源实时数字人是怎么跑起来的</title>
      <link>https://ftmao.com/blog/posts/livetalking-source-code-analysis/</link>
      <pubDate>Thu, 20 Aug 2026 17:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/livetalking-source-code-analysis/</guid>
      <description>&lt;h1 id=&#34;livetalking-源码解剖一个开源实时数字人是怎么跑起来的&#34;&gt;LiveTalking 源码解剖：一个开源实时数字人是怎么跑起来的&lt;/h1&gt;
&lt;p&gt;上一篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/digital-human-pipeline-beginner-to-mastery/&#34;&gt;《数字人链路从 0 到精通》&lt;/a&gt;画完了实时数字人的完整链路：耳朵（VAD+ASR）、眼睛（视觉感知）、大脑（LLM+RAG）、嘴巴（TTS）、脸（口型驱动）、神经（WebRTC）。概念图看完了，下一步自然的问题是：&lt;strong&gt;真实项目里这条链路长什么样？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇解剖 &lt;a href=&#34;https://gitee.com/lipku/LiveTalking&#34;&gt;LiveTalking&lt;/a&gt;。选它的原因：它在实时数字人开源项目里 star 数排前列，架构足够典型——它把「输出侧」（TTS→口型驱动→WebRTC 推流）做到了工业级，而输入侧和智能侧保持着 demo 形态，正好当「链路完整但交互闭环不完整」的对照样本。读完你会知道一个能跑的数字人项目，哪些部分可以直接抄，哪些部分要自己做。&lt;/p&gt;
&lt;h2 id=&#34;1-项目定位与启动&#34;&gt;1. 项目定位与启动&lt;/h2&gt;
&lt;p&gt;一句话定位：&lt;strong&gt;纯 2D 真人克隆口型驱动 + 流式 TTS/LLM 的实时数字人服务端，前端是浏览器&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;入口 &lt;code&gt;app.py&lt;/code&gt;，HTTP 服务用 aiohttp，默认端口 8010。&lt;/li&gt;
&lt;li&gt;配置三级覆盖：命令行 &amp;gt; &lt;code&gt;config.yaml&lt;/code&gt; &amp;gt; 默认值（&lt;code&gt;config.py&lt;/code&gt;），加上 &lt;code&gt;.env&lt;/code&gt; 读密钥，配置管理做得比多数开源 demo 规范。&lt;/li&gt;
&lt;li&gt;插件注册表 &lt;code&gt;registry.py&lt;/code&gt; 是扩展点：stt / llm / tts / avatar / output 五类插件用装饰器注册，想换实现不用改主流程。&lt;/li&gt;
&lt;li&gt;输出模式五种：&lt;code&gt;--transport&lt;/code&gt; 可选 webrtc（浏览器 &lt;code&gt;/offer&lt;/code&gt; 信令）、whep、rtcpush（WHIP 主动推流到 SRS）、rtmp、virtualcam（虚拟摄像头）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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 TB
    WEB[web/ 浏览器前端] --&amp;gt;|HTTP /human /offer| SERVER[server/&amp;lt;br/&amp;gt;rtc_manager / session_manager / routes]
    SERVER --&amp;gt; AVATAR[avatars/&amp;lt;br/&amp;gt;base_avatar 渲染管线]
    SERVER --&amp;gt; LLM[llm.py&amp;lt;br/&amp;gt;qwen-plus 单轮对话]
    LLM --&amp;gt;|标点切句| TTS[tts/&amp;lt;br/&amp;gt;11 种 TTS 插件]
    TTS --&amp;gt;|20ms 音频帧| AVATAR
    AVATAR --&amp;gt; OUT[streamout/&amp;lt;br/&amp;gt;webrtc / rtcpush / rtmp / virtualcam]
    OUT --&amp;gt; USER([用户])&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;2-输出侧精华一三种形象驱动方案&#34;&gt;2. 输出侧精华一：三种形象驱动方案&lt;/h2&gt;
&lt;p&gt;概念篇讲过口型驱动有三条路线（3D Blendshape / 真人克隆 / Live2D），LiveTalking 全部押在&lt;strong&gt;真人克隆&lt;/strong&gt;上，提供三个模型可切换，套路一致：&lt;strong&gt;离线预处理真人视频（抽帧 + s3fd 人脸检测 + 存坐标），运行时按音频特征只重绘嘴部区域，再贴回原始帧&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;音频特征&lt;/th&gt;
					&lt;th&gt;模型&lt;/th&gt;
					&lt;th&gt;参考性能&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;wav2lip&lt;/td&gt;
					&lt;td&gt;Mel 频谱&lt;/td&gt;
					&lt;td&gt;256×256&lt;/td&gt;
					&lt;td&gt;RTX3060 约 60fps&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;musetalk&lt;/td&gt;
					&lt;td&gt;Whisper encoder 特征（只提特征，不做识别）&lt;/td&gt;
					&lt;td&gt;VAE + UNet（fp16）&lt;/td&gt;
					&lt;td&gt;RTX3080Ti 约 42fps&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ultralight&lt;/td&gt;
					&lt;td&gt;HuBERT 特征&lt;/td&gt;
					&lt;td&gt;160×160 轻量 UNet&lt;/td&gt;
					&lt;td&gt;最轻量&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三个读源码时要注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;avatars/audio_features/&lt;/code&gt; 目录下的 mel/whisper/hubert 三个提取器名字叫 ASR，但&lt;strong&gt;不做语音识别&lt;/strong&gt;，只是把音频变成模型能吃的特征向量——新人最容易在这里迷路。&lt;/li&gt;
&lt;li&gt;帧率固定 25fps，音频 16kHz、每 20ms 一帧，这两个时钟是全管线的节拍器。&lt;/li&gt;
&lt;li&gt;README 里提到的 ernerf（NeRF 路线）在当前代码库已移除，别再找它。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;3-输出侧精华二渲染管线怎么省-gpu&#34;&gt;3. 输出侧精华二：渲染管线怎么省 GPU&lt;/h2&gt;
&lt;p&gt;核心在 &lt;code&gt;avatars/base_avatar.py&lt;/code&gt; 的 &lt;code&gt;render&lt;/code&gt; 循环：&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 LR
    TTS[TTS 线程&amp;lt;br/&amp;gt;合成 20ms 音频帧] --&amp;gt; AQ[音频队列]
    AQ --&amp;gt; FEAT[主循环&amp;lt;br/&amp;gt;提取音频特征]
    FEAT --&amp;gt; FQ[feat_queue]
    FEAT --&amp;gt; OQ[output_queue&amp;lt;br/&amp;gt;音频透传]
    FQ --&amp;gt; INF[inference 线程&amp;lt;br/&amp;gt;批量 GPU 推理]
    INF --&amp;gt; RQ[res_frame_queue]
    RQ --&amp;gt; PROC[process_frames 线程&amp;lt;br/&amp;gt;嘴部贴回原帧]
    OQ --&amp;gt; PROC
    PROC --&amp;gt; OUT[输出层推流]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两个值得抄的设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静音时不跑模型推理&lt;/strong&gt;：检测到静音段直接循环播放原始视频帧，只有说话时才走 GPU。这一个开关把 GPU 占用降了一个量级，是低成本并发的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背压降速&lt;/strong&gt;：渲染循环检查输出缓冲区积压，积压多了就主动 sleep 降速，防止队列无限膨胀。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外它还有 &lt;strong&gt;idle 待机动作编排&lt;/strong&gt;：自定义 audiotype 对应预切好的图片序列 + 音频循环播放，沉默时数字人不会「死机」。说话到静音的过渡混合代码存在但默认关闭（&lt;code&gt;enable_transition = False&lt;/code&gt;），追求自然度的可以自己打开调。&lt;/p&gt;
&lt;h2 id=&#34;4-输出侧精华三tts-插件与-rtc-推流&#34;&gt;4. 输出侧精华三：TTS 插件与 RTC 推流&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;TTS 插件 11 种&lt;/strong&gt;：edgetts、gpt-sovits、xtts、cosyvoice、fishtts、tencent、doubao、indextts2、azuretts、qwentts、omnitts。统一基类 &lt;code&gt;tts/base_tts.py&lt;/code&gt;：队列收文本 → 合成 → 按 20ms chunk 推给渲染管线。注意两个坑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流式程度不一：gpt-sovits 是真 HTTP 流式，edge-tts 是「流式下载、整句合完再切片下发」的伪流式——首音延迟差很多，选型时实测。&lt;/li&gt;
&lt;li&gt;文本切分不在 TTS 层做，由上游 LLM 切好送进来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;RTC 输出&lt;/strong&gt;用 aiortc 实现，自定义 &lt;code&gt;PlayerStreamTrack&lt;/code&gt; 时钟（视频 40ms/90kHz、音频 20ms/16kHz），编解码偏好 H264 优先于 VP8，支持 STUN。五种输出模式覆盖浏览器播放、推 SRS、RTMP 直播、虚拟摄像头进会议软件，输出侧的场景覆盖度是项目最成熟的部分。&lt;/p&gt;
&lt;h2 id=&#34;5-耗时分析从用户说完到数字人开口时间花在哪&#34;&gt;5. 耗时分析：从用户说完到数字人开口，时间花在哪&lt;/h2&gt;
&lt;p&gt;概念篇第 4 节说「每一环都要流式」。拿这个标准量 LiveTalking，结论先行：&lt;strong&gt;这是一条「半流式」管线——LLM、特征提取、推流是流式的，ASR 和口型推理是整段/攒批的&lt;/strong&gt;，两个非流式环节贡献了大部分延迟。&lt;/p&gt;
&lt;p&gt;逐环节拆解（耗时依据均来自代码常量与实现方式）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;实现&lt;/th&gt;
					&lt;th&gt;流式？&lt;/th&gt;
					&lt;th&gt;耗时特征&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;ASR 语音识别&lt;/td&gt;
					&lt;td&gt;SenseVoice 整段识别（&lt;code&gt;asr_server.py:87-99&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;❌ 非流式&lt;/td&gt;
					&lt;td&gt;等客户端发「说完了」才把整段 PCM 一次性识别，耗时与音频时长成正比（恒定 RTF）；全局推理锁串行，&lt;strong&gt;并发用户互相排队&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LLM 首 token&lt;/td&gt;
					&lt;td&gt;qwen-plus &lt;code&gt;stream=True&lt;/code&gt;（&lt;code&gt;llm.py&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;✅ 流式&lt;/td&gt;
					&lt;td&gt;云 API 首 token 通常几百毫秒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LLM 切句&lt;/td&gt;
					&lt;td&gt;攒标点且超 10 字才送 TTS（&lt;code&gt;llm.py:44-49&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;⚠️ 半流式&lt;/td&gt;
					&lt;td&gt;短回复、无标点回复会憋到最后才出声&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;TTS 首包&lt;/td&gt;
					&lt;td&gt;11 种插件差异大&lt;/td&gt;
					&lt;td&gt;⚠️ 看插件&lt;/td&gt;
					&lt;td&gt;edge-tts 伪流式：整句合成完才切片下发，首音 ≈ 整句耗时；gpt-sovits/cosyvoice/doubao 真流式：首音 ≈ 首 chunk 到达&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;音频特征&lt;/td&gt;
					&lt;td&gt;mel/whisper/hubert 滑窗（&lt;code&gt;config.py:52-54&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;✅ 但有前瞻&lt;/td&gt;
					&lt;td&gt;右上下文 &lt;code&gt;r=10&lt;/code&gt; 意味着必须多等 &lt;strong&gt;200ms「未来」音频&lt;/strong&gt;；启动 warm_up 再加 200ms 相位偏移，合计约 400ms 结构性延迟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;口型推理&lt;/td&gt;
					&lt;td&gt;GPU 批推理（&lt;code&gt;base_avatar.py:347&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;❌ 攒批&lt;/td&gt;
					&lt;td&gt;攒满 32 个 20ms chunk（&lt;strong&gt;640ms&lt;/strong&gt;）才触发一次推理出 16 帧；一批推理约 267ms（wav2lip256 @ RTX3060 约 60fps 推算）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;画面合成&lt;/td&gt;
					&lt;td&gt;process_frames 逐帧贴回&lt;/td&gt;
					&lt;td&gt;✅ 流式&lt;/td&gt;
					&lt;td&gt;毫秒级，可忽略&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;WebRTC 发送&lt;/td&gt;
					&lt;td&gt;aiortc 软件时钟 40ms/帧（&lt;code&gt;webrtc.py:69-109&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;✅ 流式&lt;/td&gt;
					&lt;td&gt;发送节奏与渲染解耦，无攒批；节拍 40ms + 网络传输&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&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
    A[用户说完&amp;lt;br/&amp;gt;判停等待] --&amp;gt; B[ASR 整段识别&amp;lt;br/&amp;gt;非流式]
    B --&amp;gt; C[LLM 首 token&amp;lt;br/&amp;gt;流式]
    C --&amp;gt; D[攒句超 10 字&amp;lt;br/&amp;gt;半流式]
    D --&amp;gt; E[TTS 首包&amp;lt;br/&amp;gt;edge 最差]
    E --&amp;gt; F[特征右上下文&amp;lt;br/&amp;gt;+200ms 前瞻]
    F --&amp;gt; G[攒批 640ms&amp;lt;br/&amp;gt;非流式]
    G --&amp;gt; H[GPU 推理一批&amp;lt;br/&amp;gt;约 267ms]
    H --&amp;gt; I[RTC 节拍 40ms&amp;lt;br/&amp;gt;+ 网络传输]
    style B fill:#fee,stroke:#c00
    style G fill:#fee,stroke:#c00&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;数量级估算（默认组合 wav2lip + edge-tts）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅算「TTS 首段音频 → 口型首帧」：200ms 前瞻 + 最多 640ms 攒批 + 一批推理约 267ms + 40ms 发送节拍 ≈ &lt;strong&gt;1.1 秒量级&lt;/strong&gt;。这期间管线一直在播静音循环帧，用户看到的是「待机」而不是卡死。&lt;/li&gt;
&lt;li&gt;再加上 ASR 整段识别（几秒的整段音频、全局锁排队）和 edge-tts 整句合成，端到端「用户说完 → 数字人开口」会到&lt;strong&gt;数秒级&lt;/strong&gt;——这就是它演示效果还行、对话感弱的技术根源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个值得记住的点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;固定成本写死在常量里&lt;/strong&gt;：640ms 攒批来自 &lt;code&gt;batch_size=16&lt;/code&gt;（每批吃 32 个 chunk），200ms 前瞻来自 &lt;code&gt;-r 10&lt;/code&gt;。做延迟优化最先动这两个数——但 &lt;code&gt;batch_size&lt;/code&gt; 调小会降 GPU 吞吐，这是延迟与并发的权衡，不是免费午餐。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;假流式要会分辨&lt;/strong&gt;：edge-tts 的 &lt;code&gt;stream()&lt;/code&gt; 是「流式下载、整句合成完才切片」；判断一个 TTS 插件是不是真流式，看首个音频 chunk 是否即时进队列——代码里 sovits/cosyvoice/doubao 都有 &lt;code&gt;Time to first chunk&lt;/code&gt; 打点，edge 没有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打点有但不成链&lt;/strong&gt;：LLM 首 chunk、TTS 首包、推理 fps（每 100 帧打 &lt;code&gt;actual avg infer fps&lt;/code&gt;）、推流 fps、ASR RTF 各有分散日志，但&lt;strong&gt;没有端到端统一 trace&lt;/strong&gt;（无 request-id 串联）。想定位延迟瓶颈得自己加链路追踪，这也是第 8 节工程化缺口里的一项。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;6-输入侧与智能侧demo-级别照抄&#34;&gt;6. 输入侧与智能侧：demo 级，别照抄&lt;/h2&gt;
&lt;p&gt;对照概念篇的链路图，短板集中在上半场：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASR（耳朵）&lt;/strong&gt;：&lt;code&gt;server/asr_server.py&lt;/code&gt; 用 FunASR 的 SenseVoiceSmall + fsmn-vad，但&lt;strong&gt;不是真流式&lt;/strong&gt;——客户端按 60ms chunk 上行，服务端攒缓冲，收到停止信号才整段识别一次，没有增量 partial 输出。还有一条更糙的路：直接用浏览器 Web Speech API 按住说话。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLM（大脑）&lt;/strong&gt;：&lt;code&gt;llm.py&lt;/code&gt; 单文件几十行，qwen-plus 流式调用，&lt;strong&gt;无多轮历史、无 RAG、无记忆、无工具调用&lt;/strong&gt;——每次只发一条 user message。文本切句是最简版：扫标点攒够 10 字就 flush，没有概念篇 3.3.1 讲的首句激进策略、超时兜底、标点陷阱清洗。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;打断（barge-in）&lt;/strong&gt;：有 &lt;code&gt;/interrupt_talk&lt;/code&gt; 接口，清空 TTS 队列 + 置暂停，但这是&lt;strong&gt;前端主动 HTTP 打断&lt;/strong&gt;，不是 VAD 检测到用户插话自动触发——算半个 barge-in。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;视觉感知（眼睛）&lt;/strong&gt;：完全没有运行时摄像头输入。人脸检测只用于离线建 avatar 素材。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;架构上的本质差异&lt;/strong&gt;：它的 RTC &lt;strong&gt;只发不收&lt;/strong&gt;——用户音频根本不上行到服务端，ASR 走浏览器侧。这个设计 demo 友好，但意味着服务端做不了 AEC、做不了语义 VAD、打断链路不闭环，是「demo 架构」和「产品架构」的分水岭。&lt;/p&gt;
&lt;h2 id=&#34;7-跑通它的坑安装模型与素材&#34;&gt;7. 跑通它的坑：安装、模型与素材&lt;/h2&gt;
&lt;p&gt;源码读得再明白，跑不起来都白搭。下面是从这个项目代码里逐条核实的坑，按严重程度分三级。&lt;/p&gt;
&lt;h3 id=&#34;71-跑不起来级&#34;&gt;7.1 跑不起来级&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;依赖管理是半成品&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requirements.txt&lt;/code&gt; 里&lt;strong&gt;没有 torch&lt;/strong&gt;——必须按 README 手动装指定版本（&lt;code&gt;torch==2.9.1+cu128&lt;/code&gt;），直接 &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; 后启动必报 &lt;code&gt;No module named &#39;torch&#39;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;关键依赖（aiortc、librosa、transformers、edge_tts）&lt;strong&gt;全不锁版本&lt;/strong&gt;。aiortc 历年 API 变动大，新环境装到不兼容版本是常见翻车点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;librosa 定时炸弹&lt;/strong&gt;：&lt;code&gt;avatars/wav2lip/audio.py:18&lt;/code&gt; 用了 &lt;code&gt;librosa.output.write_wav&lt;/code&gt;，这个 API 在 librosa 0.10 已删除，新版环境下走到该路径直接 AttributeError。&lt;/li&gt;
&lt;li&gt;Python 版本三个口径互不一致：README badge 写 3.10+，实测环境是 3.12 + CUDA 12.8，Dockerfile 里还是 3.10 + CUDA 11.3。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;funasr&lt;/code&gt;/&lt;code&gt;modelscope&lt;/code&gt; 在 requirements.txt 里被注释掉了，不手动装的话 &lt;code&gt;/api/asr&lt;/code&gt; &lt;strong&gt;静默不注册&lt;/strong&gt;（只打一行 info 日志），新人会以为 ASR 功能坏了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;模型权重全靠网盘手动摆位，路径还硬编码&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模型&lt;/th&gt;
					&lt;th&gt;坑&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;wav2lip&lt;/td&gt;
					&lt;td&gt;路径硬编码 &lt;code&gt;./models/wav2lip.pth&lt;/code&gt;（&lt;code&gt;app.py:148&lt;/code&gt;），README 要求把下载的 &lt;code&gt;wav2lip256.pth&lt;/code&gt; &lt;strong&gt;手动改名&lt;/strong&gt;，忘改直接报文件不存在&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;musetalk&lt;/td&gt;
					&lt;td&gt;四件套要全部就位：unet + sd-vae + &lt;strong&gt;HF transformers 格式的 whisper&lt;/strong&gt;（不是 OpenAI 权重格式，放错 load 失败）+ face-parse 模型&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ultralight&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;最深的坑&lt;/strong&gt;：&lt;code&gt;from_pretrained&lt;/code&gt; 传的是本地路径不会自动拉取；且每个 avatar 需要自己训练的 &lt;code&gt;ultralight.pth&lt;/code&gt;——&lt;code&gt;genavatar.py&lt;/code&gt; 末尾只是 &lt;code&gt;cp&lt;/code&gt; 你在项目外训练好的 checkpoint，&lt;strong&gt;训练过程根本不在本仓库&lt;/strong&gt;。新人选 ultralight 基本必卡&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;s3fd 人脸检测&lt;/td&gt;
					&lt;td&gt;首次使用从外网（adrianbulat.com）下载权重，国内/离线环境 genavatar 直接挂&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;另外默认 TTS 是 edge-tts，&lt;strong&gt;依赖微软在线服务&lt;/strong&gt;，离线或被墙环境无声且无重试（只打日志）。&lt;/p&gt;
&lt;h3 id=&#34;72-avatar-素材制作的坑genavatar&#34;&gt;7.2 avatar 素材制作的坑（genavatar）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入视频必须 25fps，但代码不校验&lt;/strong&gt;（&lt;code&gt;config.py:51&lt;/code&gt; 注释写着 &amp;ldquo;must be 25&amp;rdquo;）。喂 30fps 视频会口型节奏错位，且无任何报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人脸检测失败静默兜底&lt;/strong&gt;：检测不到人脸时直接用整帧坐标（&lt;code&gt;avatars/wav2lip/genavatar.py:107&lt;/code&gt;），产出垃圾素材也不报警，要等跑起来看到崩坏画面才知道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全量帧读进内存&lt;/strong&gt;：所有 PNG 帧一次性 load 进 RAM 且常驻（&lt;code&gt;wav2lip_avatar.py:81-86&lt;/code&gt;），长视频直接撑爆内存，也没有时长上限引导。&lt;/li&gt;
&lt;li&gt;抽帧逐帧写 PNG，几分钟 1080p 视频就是几千个文件，磁盘消耗无提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-体验受损级能跑但别扭&#34;&gt;7.3 体验受损级（能跑但别扭）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DASHSCOPE_API_KEY&lt;/code&gt; 没配置时静默失败&lt;/strong&gt;（&lt;code&gt;llm.py&lt;/code&gt; 只 catch 打日志），前端表现是「数字人就是不说话」，没有任何错误提示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;getUserMedia 需要安全上下文&lt;/strong&gt;：用 &lt;code&gt;http://&amp;lt;局域网IP&amp;gt;:8010&lt;/code&gt; 访问时浏览器禁麦克风，语音功能无声失效，README 没提。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebRTC 端口要求夸张&lt;/strong&gt;：README 要求开放 UDP 全端口，且只配 STUN 不配 TURN，严格 NAT 下连接失败无引导。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 切句粗糙&lt;/strong&gt;：攒够标点且超 10 字才送 TTS，短回复、无标点回复会憋到最后才出声——概念篇 3.3.1 讲的超时兜底这里没有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wav2lip 嘴部硬贴回&lt;/strong&gt;：直接 &lt;code&gt;cv2.resize&lt;/code&gt; 覆盖矩形区域，无边缘融合（musetalk 才有 mask blending）；说话/静音过渡混合代码存在但默认关闭，帧切换有跳变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话避坑总结：&lt;strong&gt;按 README 跑 &lt;code&gt;wav2lip + webrtc + edgetts&lt;/code&gt; 默认组合成功率最高&lt;/strong&gt;；深坑集中在模型权重手动摆位、ultralight 需项目外自训、25fps 与人脸检测失败均无校验这三处。&lt;/p&gt;
&lt;h2 id=&#34;8-从-demo-到商业级产品还差什么&#34;&gt;8. 从 demo 到商业级产品，还差什么&lt;/h2&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
    subgraph 已有[&amp;#34;LiveTalking 已覆盖（工业级）&amp;#34;]
        A1[形象驱动三方案]
        A2[渲染管线 + 省 GPU]
        A3[TTS 插件 + 克隆]
        A4[RTC 五种输出]
        A5[idle 待机]
    end

    subgraph 缺失[&amp;#34;到产品还差（要自己造）&amp;#34;]
        B1[RTC 音频上行 + AEC]
        B2[真流式 ASR + 语义 VAD]
        B3[自动 barge-in]
        B4[RAG + 多轮 + 记忆]
        B5[视觉感知]
        B6[测试/CI/鉴权/部署]
    end

    已有 --&amp;gt;|补全后| P[商业级&amp;lt;br/&amp;gt;数字人]
    缺失 --&amp;gt; P&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;具体缺口清单，按补齐优先级排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;交互闭环&lt;/strong&gt;：用户语音走 RTC 上行 → 服务端 AEC + 流式 ASR + 语义 VAD → 自动 barge-in。这是从「能演示」到「能用」的第一刀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能&lt;/strong&gt;：LLM 层接多轮历史 + RAG（概念篇 3.2 节的完整方案），&lt;code&gt;llm.py&lt;/code&gt; 只有几十行，改造量小、收益最大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉感知&lt;/strong&gt;：按概念篇 3.1.3 的方案，端侧轻量模型起步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程化与安全&lt;/strong&gt;：测试、CI、鉴权、部署——当前几乎是空白，见下面的上线隐患清单。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;81-上线隐患级想部署公网必须先补的课&#34;&gt;8.1 上线隐患级（想部署公网必须先补的课）&lt;/h3&gt;
&lt;p&gt;这些问题不影响跑 demo，但每一个都是生产事故的种子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全无鉴权&lt;/strong&gt;：&lt;code&gt;/human&lt;/code&gt;、&lt;code&gt;/interrupt_talk&lt;/code&gt;、&lt;code&gt;/record&lt;/code&gt;、&lt;code&gt;/api/admin/*&lt;/code&gt;、&lt;code&gt;/sse&lt;/code&gt; 全部裸奔；admin 接口直接 &lt;code&gt;vars(opt)&lt;/code&gt; 输出全量配置；CORS 是 &lt;code&gt;*&lt;/code&gt; 全开。公网部署等于敞开大门。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;录制功能用 &lt;code&gt;os.system&lt;/code&gt; 拼 ffmpeg 命令&lt;/strong&gt;（&lt;code&gt;base_avatar.py:285&lt;/code&gt;），sessionid 来自外部传入，存在路径注入风险；临时文件落在工作目录，同名会话重录互相覆盖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 资源不释放&lt;/strong&gt;：&lt;code&gt;session_manager.remove_session&lt;/code&gt; 只 pop 字典，注释里自己留了一句「todo: 还可以主动调 avatar_session 释放」——avatar 素材常驻显存不回收，&lt;code&gt;global_avatars&lt;/code&gt; 缓存永不淘汰，频繁换 avatar 会显存泄漏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 模型全局共享且无锁&lt;/strong&gt;：每个 session 起独立 inference 线程并发调同一个模型，高并发同时说话时显存峰值不可控，没有任何排队或互斥防护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程泄漏风险&lt;/strong&gt;：inference 线程里 &lt;code&gt;output_queue.get()&lt;/code&gt; 无超时阻塞（&lt;code&gt;base_avatar.py:348&lt;/code&gt;），TTS 断供时线程永远卡住，shutdown 的 &lt;code&gt;join()&lt;/code&gt; 会挂起；LLM 调用走默认 executor 无线程池上限，刷接口可起大量线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;队列清空不持锁&lt;/strong&gt;：打断时直接 &lt;code&gt;queue.queue.clear()&lt;/code&gt; 操作内部 deque（&lt;code&gt;tts/base_tts.py:31&lt;/code&gt;），打断瞬间恰逢 put 有小概率竞态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dockerfile 是坏的&lt;/strong&gt;：CUDA 11.6/torch 1.12 与 README 要求的 torch 2.9.1/cu128 矛盾，且 &lt;code&gt;COPY&lt;/code&gt; 路径还是上个项目的名字 &lt;code&gt;nerfstream&lt;/code&gt;——这个 Dockerfile 根本 build 不出当前项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;82-文档与代码不一致宣称-vs-实际&#34;&gt;8.2 文档与代码不一致（宣称 vs 实际）&lt;/h3&gt;
&lt;p&gt;读这个项目要建立「README 说的是理想、代码才是现实」的警觉：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;README 宣称&lt;/th&gt;
					&lt;th&gt;代码实际&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;支持 ernerf（附演示视频链接）&lt;/td&gt;
					&lt;td&gt;ernerf 已移除，&lt;code&gt;app.py&lt;/code&gt; 只注册 musetalk/wav2lip/ultralight&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支持说话被打断&lt;/td&gt;
					&lt;td&gt;仅 HTTP 主动打断，无语音 barge-in（服务端不收上行音轨）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支持多并发&lt;/td&gt;
					&lt;td&gt;有限流，但模型共享无锁 + 资源不释放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;requirements.txt 列了 Flask&lt;/td&gt;
					&lt;td&gt;实际用 aiohttp，&lt;code&gt;app.py&lt;/code&gt; 里的 Flask 代码是死代码&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;admin API 文档暗示 fps 可配&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;config.py&lt;/code&gt; 写死 25，改了整条音视频时钟链都会错位&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;已在业内广泛商用&lt;/td&gt;
					&lt;td&gt;无鉴权/无测试/资源不释放，开源代码距离可商用部署有明显差距&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;9-源码怎么读推荐顺序&#34;&gt;9. 源码怎么读：推荐顺序&lt;/h2&gt;
&lt;p&gt;项目文件不少，按这个顺序读最省力：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;app.py&lt;/code&gt; + &lt;code&gt;config.py&lt;/code&gt;：看启动流程和配置体系，建立全局。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;registry.py&lt;/code&gt;：十几行的装饰器注册表，理解插件机制后所有模块的可替换性就清楚了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;avatars/base_avatar.py&lt;/code&gt;：全文核心，渲染管线的四条线程、静音跳推理、idle 编排都在这。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tts/base_tts.py&lt;/code&gt; + 一个具体实现（如 &lt;code&gt;tts/sovits.py&lt;/code&gt;）：理解真假流式的差别。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server/webrtc.py&lt;/code&gt;：看自定义时钟轨道怎么把音视频帧喂给 WebRTC。&lt;/li&gt;
&lt;li&gt;最后回头看 &lt;code&gt;llm.py&lt;/code&gt; 和 &lt;code&gt;server/asr_server.py&lt;/code&gt;——看清 demo 级实现的边界在哪，正好是练手改造的切入点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;10-总结&#34;&gt;10. 总结&lt;/h2&gt;
&lt;p&gt;LiveTalking 的价值在于&lt;strong&gt;输出侧的真实工程参考&lt;/strong&gt;：三种驱动方案的取舍、四线程渲染管线、静音跳推理省 GPU、插件化 TTS、五种 RTC 输出——这些在概念文章里只是一句话，在源码里是踩过坑的实现。它的边界也清晰：输入侧和智能侧保持 demo 形态，不是缺陷，是定位。&lt;/p&gt;
&lt;p&gt;对学习者，正确的用法是&lt;strong&gt;概念篇当地图、这个项目当实地&lt;/strong&gt;：先知道链路全貌，再在源码里逐个模块对号入座，最后从「补齐缺口清单」里挑一个动手改——比如给 &lt;code&gt;llm.py&lt;/code&gt; 接上 RAG 和多轮历史，就是性价比最高的第一个实战任务。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>数字人链路从 0 到精通：新人也能看懂的学习总结</title>
      <link>https://ftmao.com/blog/posts/digital-human-pipeline-beginner-to-mastery/</link>
      <pubDate>Thu, 20 Aug 2026 16:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/digital-human-pipeline-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;数字人链路从-0-到精通新人也能看懂的学习总结&#34;&gt;数字人链路从 0 到精通：新人也能看懂的学习总结&lt;/h1&gt;
&lt;p&gt;写这篇文章的动机很实际：数字人（Digital Human）这个方向资料多但碎，新人很容易被各种名词（ASR、TTS、Viseme、Blendshape、WebRTC、中之人、MetaHuman）淹没，看了很多文章还是拼不出一张完整的图。&lt;/p&gt;
&lt;p&gt;本文的目标：&lt;strong&gt;一篇文章讲清楚数字人是什么、整条技术链路怎么跑、每环有哪些坑、新人按什么顺序学&lt;/strong&gt;。假设读者有基本编程能力，但不要求任何音视频或图形学背景。&lt;/p&gt;
&lt;h2 id=&#34;1-先搞清楚数字人到底是什么&#34;&gt;1. 先搞清楚：数字人到底是什么&lt;/h2&gt;
&lt;h3 id=&#34;11-从元宇宙说起&#34;&gt;1.1 从元宇宙说起&lt;/h3&gt;
&lt;p&gt;2021 年「元宇宙（Metaverse）」概念爆火时，有一个被广泛引用的定义：元宇宙是一个&lt;strong&gt;持久在线的、可沉浸交互的虚拟世界&lt;/strong&gt;。热闹过后概念降温，但它留下了几样真正落地的资产：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟形象（Avatar）&lt;/strong&gt;：人在虚拟世界里的「身体」，从游戏捏脸到 Meta 的虚拟人，再到苹果的 Persona。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字身份（Digital Identity）&lt;/strong&gt;：虚拟世界里的「你是谁」，关联账号、资产、社交关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时渲染与交互引擎&lt;/strong&gt;：Unity、Unreal（含 MetaHuman）、WebGL 这些让虚拟世界「看得见」的技术。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XR 入口&lt;/strong&gt;：VR/AR 设备作为进入虚拟世界的「门」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数字人就是元宇宙沉淀下来最值钱的那部分&lt;/strong&gt;：一个「有形象、能说话、能交流」的虚拟实体。元宇宙讲的是一个世界，数字人是这个世界里最先实现商业闭环的单元——你不需要一整个虚拟世界，只需要一个能 7x24 小时直播带货、能接客服电话、能当虚拟偶像的数字人，就有客户愿意付钱。&lt;/p&gt;
&lt;h3 id=&#34;12-数字人的三个分类维度&#34;&gt;1.2 数字人的三个分类维度&lt;/h3&gt;
&lt;p&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 TB
    DH[数字人] --&amp;gt; A[按形象分]
    DH --&amp;gt; B[按驱动方式分]
    DH --&amp;gt; C[按交互方式分]

    A --&amp;gt; A1[2D 卡通 / Live2D]
    A --&amp;gt; A2[3D 写实&amp;lt;br/&amp;gt;MetaHuman / 捏脸]
    A --&amp;gt; A3[真人形象克隆&amp;lt;br/&amp;gt;照片/视频驱动]

    B --&amp;gt; B1[真人驱动&amp;lt;br/&amp;gt;中之人 + 动捕]
    B --&amp;gt; B2[AI 驱动&amp;lt;br/&amp;gt;文本/语音全自动]
    B --&amp;gt; B3[混合驱动&amp;lt;br/&amp;gt;AI 为主 真人兜底]

    C --&amp;gt; C1[离线内容型&amp;lt;br/&amp;gt;录播视频/短视频]
    C --&amp;gt; C2[实时交互型&amp;lt;br/&amp;gt;对话/直播/客服]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;形象维度&lt;/strong&gt;：Live2D 成本最低（一张立绘就能动），3D 写实最重（建模 + 绑定 + 渲染），真人克隆最像真人（用真人的照片或视频训练）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动维度&lt;/strong&gt;：虚拟主播圈常说的「中之人」就是真人在幕后戴动捕设备驱动皮套；AI 驱动则是全自动化，这也是本文的重点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互维度&lt;/strong&gt;：离线内容型（批量生成口播视频）对实时性没要求，工程难度低一个量级；实时交互型（你说一句它答一句）才是技术深水区。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;典型应用场景对号入座&lt;/strong&gt;：虚拟主播直播（实时 + 真人/AI 驱动）、AI 客服数字人（实时 + AI 驱动）、短视频口播矩阵（离线 + AI 驱动）、数字分身/数字永生（真人克隆 + 实时）、企业数字员工大屏（实时 + 3D）。&lt;/p&gt;
&lt;h2 id=&#34;2-一张图看懂整体链路&#34;&gt;2. 一张图看懂整体链路&lt;/h2&gt;
&lt;p&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 LR
    subgraph 感知[&amp;#34;感知（耳朵 + 眼睛）&amp;#34;]
        MIC[麦克风] --&amp;gt; VAD[VAD 语音检测] --&amp;gt; ASR[ASR 语音识别]
        CAM[摄像头] --&amp;gt; VIS[视觉感知&amp;lt;br/&amp;gt;人脸/唇动/表情/手势]
    end

    subgraph 认知[&amp;#34;认知（大脑）&amp;#34;]
        ASR --&amp;gt; LLM[LLM 对话引擎&amp;lt;br/&amp;gt;+ RAG + 记忆 + 工具]
        VIS -.情绪/注意力标签.-&amp;gt; LLM
    end

    subgraph 表达[&amp;#34;表达（嘴巴 + 脸 + 身体）&amp;#34;]
        LLM --&amp;gt; TTS[TTS 语音合成]
        TTS --&amp;gt; DRIVE[驱动引擎&amp;lt;br/&amp;gt;口型/表情/动作]
        DRIVE --&amp;gt; RENDER[渲染引擎&amp;lt;br/&amp;gt;2D/3D 形象]
    end

    subgraph 传输[&amp;#34;传输（神经）&amp;#34;]
        RENDER --&amp;gt; RTC[WebRTC 低延迟推流]
        RTC --&amp;gt; USER([用户屏幕/音箱])
    end

    USER -.用户说话.-&amp;gt; MIC
    USER -.用户在镜头前.-&amp;gt; CAM&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;记住这个拟人化口诀：&lt;strong&gt;耳朵（ASR）听清、眼睛（视觉感知）看懂 → 大脑（LLM）想好 → 嘴巴（TTS）说出 → 脸（口型驱动）对上 → 神经（WebRTC）送达&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;整条链路分两大形态：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;形态&lt;/th&gt;
					&lt;th&gt;特点&lt;/th&gt;
					&lt;th&gt;延迟要求&lt;/th&gt;
					&lt;th&gt;典型延迟预算&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;离线内容生产&lt;/td&gt;
					&lt;td&gt;批量生成视频，可反复重试&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;分钟级也无所谓&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;实时交互&lt;/td&gt;
					&lt;td&gt;一问一答，像打电话&lt;/td&gt;
					&lt;td&gt;端到端小于 1.5 秒才自然&lt;/td&gt;
					&lt;td&gt;每一环都要抠毫秒&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;离线形态是实时形态的子集（少了 ASR 和 RTC，渲染换成离线出片），所以下面重点拆实时链路。&lt;/p&gt;
&lt;h2 id=&#34;3-实时交互链路逐环拆解&#34;&gt;3. 实时交互链路逐环拆解&lt;/h2&gt;
&lt;p&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;sequenceDiagram
    participant U as 用户
    participant E as 边缘采集端
    participant A as ASR 服务
    participant L as LLM 服务
    participant T as TTS 服务
    participant D as 驱动+渲染
    participant R as RTC 网络

    U-&amp;gt;&amp;gt;E: 说话（语音流）
    E-&amp;gt;&amp;gt;E: VAD 检测说话起止
    E-&amp;gt;&amp;gt;A: 流式发送音频
    A--&amp;gt;&amp;gt;L: 边说边出识别文本
    L--&amp;gt;&amp;gt;T: 流式输出回答（按句切分）
    T--&amp;gt;&amp;gt;D: 流式合成音频（边收边播）
    D-&amp;gt;&amp;gt;D: 音频驱动口型/表情
    D-&amp;gt;&amp;gt;R: 音视频帧推流
    R--&amp;gt;&amp;gt;U: 看到听到数字人回答
    Note over U,R: 用户感知延迟 = 最后一个字说完到数字人开口的时间&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键设计原则：&lt;strong&gt;每一环都是流式（streaming）的&lt;/strong&gt;。如果等 LLM 把整段话说完再送 TTS、等 TTS 合成完整段再播放，延迟会叠加到无法接受。流式的含义是「边产生边消费」，像水管一样一环接一环地流。&lt;/p&gt;
&lt;h3 id=&#34;31-耳朵与眼睛vad--asr--视觉感知&#34;&gt;3.1 耳朵与眼睛：VAD + ASR + 视觉感知&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;VAD（Voice Activity Detection，语音活动检测）&lt;/strong&gt;：判断用户什么时候开始说、什么时候说完了。它是交互节奏的总开关——判早了会把用户一句话切成两半，判晚了用户会觉得数字人反应迟钝。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASR（Automatic Speech Recognition，语音识别）&lt;/strong&gt;：把语音流实时转成文字。&lt;/p&gt;
&lt;h4 id=&#34;311-vad-到底怎么判断开始说和说完了&#34;&gt;3.1.1 VAD 到底怎么判断「开始说」和「说完了」&lt;/h4&gt;
&lt;p&gt;VAD 把音频切成 10-30ms 的小帧，对每一帧打一个「是语音的概率」，然后用状态机做判定：&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;stateDiagram-v2
    [*] --&amp;gt; 静音
    静音 --&amp;gt; 可能说话: 连续 N 帧概率超阈值
    可能说话 --&amp;gt; 说话中: 确认（防突发噪音）
    可能说话 --&amp;gt; 静音: 概率回落（是咳嗽/关门声）
    说话中 --&amp;gt; 可能结束: 出现连续静音帧
    可能结束 --&amp;gt; 说话中: 重新检测到语音（句中停顿）
    可能结束 --&amp;gt; 静音: 静音持续超 hangover 阈值&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;算法分三代，了解演进就知道怎么选：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;能量 + 过零率&lt;/strong&gt;：最原始的做法，能量突增判说话。安静机房能用，真实环境（空调声、键盘声）全是误判。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统计模型&lt;/strong&gt;：代表作是 Google 的 WebRTC VAD（GMM 高斯混合模型），内置 0-3 四档灵敏度，免费、极快，至今仍是很多项目内置的默认项。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;神经网络模型&lt;/strong&gt;：工业主流。开源首选 &lt;strong&gt;Silero VAD&lt;/strong&gt;（几 MB 的 ONNX 模型，CPU 上实时跑，自带 30ms 帧判定）；国内还有阿里的 FSMN-VAD（FunASR 自带）、TEN-VAD。抗噪能力比前代强一个量级。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三个必须知道的工程细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;判起要有「提前量」&lt;/strong&gt;：VAD 确认「开始说话」需要连续若干帧，如果确认后才把音频送 ASR，句首会被吃掉（「你好，数字人」变成「数字人」）。解法是&lt;strong&gt;预滚动缓冲（pre-roll buffer）&lt;/strong&gt;：始终缓存最近 200-300ms 音频，VAD 触发时把缓冲一并送出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;判停（端点检测）是延迟与完整的权衡&lt;/strong&gt;：静音持续多久算「说完了」，默认 500-800ms。设太短切断用户的长句停顿（「那个……我想问一下」被切成两段），设太长每轮对话白等几百毫秒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义 VAD 是高阶玩法&lt;/strong&gt;：不只看静音时长，还结合 ASR 的中间识别结果判断语义是否完整——识别文本还没形成完整句子就延长等待。大模型语音产品（GPT-4o 类）和商用方案普遍在用，纯声学 VAD 容易被「思考停顿」骗过。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;312-流式-asr现成-api-还是自己改-whisper&#34;&gt;3.1.2 流式 ASR：现成 API 还是自己改 Whisper&lt;/h4&gt;
&lt;p&gt;先说结论：&lt;strong&gt;商用快速上线选云 API；数据敏感或量大图便宜选自部署开源方案；改造 Whisper 只适合练手和理解原理，别上生产。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现成 API（WebSocket 流式，接入即用）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;国内：阿里云实时语音识别、腾讯云 ASR、讯飞、火山引擎——中文效果好、热词支持完善、按量付费，首字延迟可做到 200-300ms。&lt;/li&gt;
&lt;li&gt;海外：Deepgram、AssemblyAI、Azure Speech、OpenAI Realtime API。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自部署开源方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FunASR&lt;/strong&gt;（阿里开源）：Paraformer 流式版，自带 VAD + 标点恢复，中文场景首选，社区活跃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sherpa-onnx&lt;/strong&gt;（k2 团队）：流式 Zipformer/Transducer 模型，跨平台，端侧（手机/嵌入式）都能跑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SenseVoice&lt;/strong&gt;（阿里）：多语言、低延迟，还带情感与事件检测。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whisper 为什么难办：它原生是&lt;strong&gt;离线架构&lt;/strong&gt;——encoder 要吃 30 秒的音频窗口，天然不是流式模型。三种常见改造路线：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
					&lt;th&gt;问题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;VAD 切段 + faster-whisper&lt;/td&gt;
					&lt;td&gt;VAD 检出完整一段话后送 faster-whisper（CTranslate2 加速版）&lt;/td&gt;
					&lt;td&gt;是「段级准实时」，不是真流式；每段延迟 1-3 秒，数字人场景偏慢&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LocalAgreement 策略&lt;/td&gt;
					&lt;td&gt;whisper_streaming 项目的做法：滑动小窗反复识别，连续两次结果一致的前缀才确认输出&lt;/td&gt;
					&lt;td&gt;体验接近真流式，但同一段音频重复计算，GPU 成本高&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;换流式架构模型&lt;/td&gt;
					&lt;td&gt;承认 Whisper 不适合流式，直接用 Paraformer-streaming / Zipformer / SenseVoice&lt;/td&gt;
					&lt;td&gt;正解，但等于放弃了 Whisper&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选型对照：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;首字延迟&lt;/th&gt;
					&lt;th&gt;中文效果&lt;/th&gt;
					&lt;th&gt;成本&lt;/th&gt;
					&lt;th&gt;适合谁&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;云 API&lt;/td&gt;
					&lt;td&gt;200-300ms&lt;/td&gt;
					&lt;td&gt;好（热词完善）&lt;/td&gt;
					&lt;td&gt;按量付费，量大贵&lt;/td&gt;
					&lt;td&gt;快速上线、没运维团队&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;FunASR / sherpa 自部署&lt;/td&gt;
					&lt;td&gt;200-500ms&lt;/td&gt;
					&lt;td&gt;好&lt;/td&gt;
					&lt;td&gt;自己的 GPU，量大便宜&lt;/td&gt;
					&lt;td&gt;数据敏感、有推理运维能力&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Whisper 改造&lt;/td&gt;
					&lt;td&gt;1s 以上&lt;/td&gt;
					&lt;td&gt;好&lt;/td&gt;
					&lt;td&gt;重复计算，贵&lt;/td&gt;
					&lt;td&gt;学习、离线转写场景&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;新人要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选&lt;strong&gt;流式 ASR&lt;/strong&gt;（边说边出字），不要选录音文件识别接口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热词（hotword）功能必须用起来&lt;/strong&gt;：业务专有名词（产品名、品牌名）通过热词表喂给 ASR，否则识别错误会一直污染下游 LLM。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AEC（回声消除）&lt;/strong&gt;：如果数字人的声音从音箱播出来又进麦克风，ASR 会把数字人自己的话识别成用户输入，造成「自己跟自己对话」。用扬声器场景必须开 AEC（WebRTC 自带，自研采集端要接 Speex/WebRTC APM 这类模块）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;313-眼睛视觉感知数字人能不能看到用户&#34;&gt;3.1.3 眼睛：视觉感知，数字人能不能「看到」用户&lt;/h4&gt;
&lt;p&gt;只有耳朵的数字人，相当于一个闭着眼打电话的人——能用，但离「像人」差一层。加上摄像头这条通道，感知层才完整。视觉信号在数字人里有六个典型用途：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;视觉能力&lt;/th&gt;
					&lt;th&gt;干什么用&lt;/th&gt;
					&lt;th&gt;典型场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;存在检测&lt;/td&gt;
					&lt;td&gt;判断镜头前有没有人、有几个人，有人靠近才激活交互&lt;/td&gt;
					&lt;td&gt;大屏数字员工、门店导购，免唤醒词&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;视觉 VAD（唇动检测）&lt;/td&gt;
					&lt;td&gt;看着用户的嘴判断是否真在说话&lt;/td&gt;
					&lt;td&gt;嘈杂环境辅助音频 VAD，大幅降低误触发&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;表情/情绪识别&lt;/td&gt;
					&lt;td&gt;识别皱眉、笑、困惑，作为上下文注入 LLM&lt;/td&gt;
					&lt;td&gt;客服场景感知用户不满，共情回应&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;视线/注意力检测&lt;/td&gt;
					&lt;td&gt;用户是否在看屏幕&lt;/td&gt;
					&lt;td&gt;用户转头就走时数字人停止讲解，不自言自语&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;手势识别&lt;/td&gt;
					&lt;td&gt;挥手打招呼、举手打断、比划指令&lt;/td&gt;
					&lt;td&gt;展厅、车载等不方便说话的场景&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;人脸识别身份&lt;/td&gt;
					&lt;td&gt;认出是谁，关联记忆系统调出画像&lt;/td&gt;
					&lt;td&gt;VIP 客户识别、数字分身授权核验&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;工程实现分三层，按需取用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;端侧轻量模型&lt;/strong&gt;：人脸检测、唇动、手势这类高频低算力任务放端侧跑（MediaPipe、YOLO 系列、厂商人脸 SDK），只把结论标签上传，不上传原始视频流——省带宽、降延迟、利隐私。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云端识别&lt;/strong&gt;：情绪识别等重模型走云端，按帧抽样的低频调用即可，不要全帧率送云。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端多模态大模型&lt;/strong&gt;：GPT-4o、Gemini Live 这类音视频直接进模型的路线，跳过「ASR 转文字」环节，能直接听懂语气、看到画面。这是趋势，但目前成本高、可控性弱（不好接 RAG 和业务约束），商用数字人中台主流仍是「ASR + LLM」模块化架构加视觉辅助信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;视觉信号进大脑的正确姿势&lt;/strong&gt;：视觉结果不作为对话内容，而是作为&lt;strong&gt;低优先级异步上下文标签&lt;/strong&gt;注入 LLM——比如 &lt;code&gt;[用户皱眉]&lt;/code&gt; &lt;code&gt;[用户看向别处]&lt;/code&gt; &lt;code&gt;[检测到 3 位观众]&lt;/code&gt;，让人格层决定如何反应（加快语速、主动询问、暂停讲解）。直接把视觉识别文本拼进用户输入会污染对话流，是新人常犯的错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;合规红线&lt;/strong&gt;：人脸信息在《个人信息保护法》里属于&lt;strong&gt;敏感个人信息&lt;/strong&gt;，采集需单独同意、明示用途。门店大屏这类被动采集场景尤其要小心，能端侧处理就不要上传原图，能用人形检测就不要做人脸识别。&lt;/p&gt;
&lt;p&gt;回答「数字人要不要眼睛」这道题的思路：&lt;strong&gt;不是必须，但它是从「语音机器人」到「数字人」的分水岭&lt;/strong&gt;——语音通道解决「听懂」，视觉通道解决「看懂场合」，两者融合（多模态感知）才有真正的临场感。在技术评审或方案讨论中，能讲清视觉信号作为异步标签注入、端侧优先、隐私合规这三点，就说明理解到位了。&lt;/p&gt;
&lt;h3 id=&#34;32-大脑llm-对话引擎&#34;&gt;3.2 大脑：LLM 对话引擎&lt;/h3&gt;
&lt;p&gt;这一环是大家最熟悉的：把识别文本丢给大模型，拿回答。但在数字人场景有几个特殊要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System Prompt 分层&lt;/strong&gt;：人格设定（你是谁、什么性格）、业务知识（RAG 注入）、表达约束（口语化、每句别太长）分三层管理，不要揉成一大段。人格一致性是「数字人」区别于「聊天框」的核心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出要为「说」优化&lt;/strong&gt;：prompt 里明确要求口语化短句、不要 markdown、不要列表符号——TTS 会把「1.」「**」念出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长时记忆&lt;/strong&gt;：多轮对话靠上下文窗口，跨会话的「记住用户」要靠记忆系统（如 Mem0 这类长时记忆中间件：向量检索 + 用户画像 + 摘要）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function Calling&lt;/strong&gt;：查天气、下单、转人工这类动作走工具调用，返回结果再让 LLM 组织成口语。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;321-rag给大脑喂业务知识的标准流程&#34;&gt;3.2.1 RAG：给大脑喂业务知识的标准流程&lt;/h4&gt;
&lt;p&gt;数字人要回答企业业务问题，靠模型自身的知识不够，必须外挂知识库。RAG（Retrieval-Augmented Generation，检索增强生成）的标准流程新人必须掌握：&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
    subgraph 离线建库[&amp;#34;离线：建知识库&amp;#34;]
        DOC[业务文档] --&amp;gt; CHUNK[切分 Chunking] --&amp;gt; EMB[Embedding 向量化] --&amp;gt; VDB[(向量数据库&amp;lt;br/&amp;gt;Qdrant/Milvus/Chroma)]
    end

    subgraph 在线检索[&amp;#34;在线：问答时&amp;#34;]
        Q[用户问题] --&amp;gt; REWRITE[Query 改写/补全] --&amp;gt; SEARCH[混合检索&amp;lt;br/&amp;gt;向量 + BM25 关键词]
        VDB -.相似度召回.-&amp;gt; SEARCH
        SEARCH --&amp;gt; RERANK[Rerank 重排序]
        RERANK --&amp;gt; PACK[拼装 Prompt&amp;lt;br/&amp;gt;system + 知识 + 历史 + 问题]
        PACK --&amp;gt; GEN[LLM 流式生成]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心要点（方案讨论和实战都绕不开）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;切分（Chunking）&lt;/strong&gt;：按语义/段落切，块太大噪音多、块太小上下文断裂，常用 200-500 字加 10% 重叠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedding 选型&lt;/strong&gt;：中文场景主流 BGE 系列（BGE-M3 支持多语言与稠密稀疏混合），决定了召回质量的上限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合检索&lt;/strong&gt;：纯向量检索对「型号、编号、专有名词」这类精确匹配很差，向量召回 + BM25 关键词召回合并是工业标配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rerank&lt;/strong&gt;：召回 topK 比如 20 条，用 Rerank 模型（如 bge-reranker）精排取前 3-5 条进 prompt，显著降噪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估闭环&lt;/strong&gt;：召回命中率、答案忠实度（有没有幻觉）要有评估集回归，改任何一环都跑一遍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;322-核心问题首-tokenttft怎么做到最快&#34;&gt;3.2.2 核心问题：首 token（TTFT）怎么做到最快&lt;/h4&gt;
&lt;p&gt;数字人场景里，LLM 是整个链路的延迟黑洞，而用户感知的快慢主要看 &lt;strong&gt;TTFT（Time To First Token，首 token 延迟）&lt;/strong&gt;——因为输出是流式的，第一个 token 出来，后面的 TTS 就能开工。先把公式摆出来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TTFT = 检索耗时 + 网络与排队耗时 + Prefill（预填充）耗时&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&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 LR
    A[用户说完话] --&amp;gt; B[RAG 检索&amp;lt;br/&amp;gt;50-300ms]
    B --&amp;gt; C[网络 RTT + 排队&amp;lt;br/&amp;gt;50-500ms]
    C --&amp;gt; D[Prefill 预填充&amp;lt;br/&amp;gt;随 prompt 长度线性增长]
    D --&amp;gt; E[首 token 吐出]
    style D fill:#fee,stroke:#c00&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;第一路：让检索更快&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向量库用 HNSW 这类内存索引，召回压到几十毫秒；向量检索和 BM25 关键词检索&lt;strong&gt;并行发&lt;/strong&gt;，取并集。&lt;/li&gt;
&lt;li&gt;Embedding 模型本地/就近部署，别为一次向量化跨地域调 API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高频问题语义缓存&lt;/strong&gt;：命中缓存直接跳过检索和生成（客服场景命中率可以做到很高），甚至直接返回预生成的音视频片段，TTFT 趋近于零。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第二路：让 prompt 更短（砍 Prefill）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Prefill 耗时随输入 token 数近似线性增长，prompt 每砍一半，这部分就快一半：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system prompt 精简，人格层/业务层/约束层按需加载，不要每次都全量带上。&lt;/li&gt;
&lt;li&gt;RAG 注入的 chunk 控制在 3-5 条，靠 Rerank 保证质量而不是靠堆数量。&lt;/li&gt;
&lt;li&gt;历史对话做摘要压缩，多轮后只带摘要 + 最近几轮原文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第三路：让前缀可缓存（KV Cache / Prompt Caching）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把&lt;strong&gt;不变的内容放 prompt 最前面&lt;/strong&gt;（人格设定、业务规则），变化的内容（检索结果、用户问题）放最后。主流推理框架（vLLM 的 Automatic Prefix Caching）和商用 API（Claude/DeepSeek 的 prompt cache）都能复用前缀的 KV Cache，命中后 Prefill 几乎免费。&lt;/li&gt;
&lt;li&gt;反面教材：在 system prompt 里塞时间戳、随机 ID——前缀每次都变，缓存永远miss。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第四路：让引擎和部署更快&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推理框架选 vLLM/SGLang 这类支持 continuous batching 的，吞吐和排队时间都受益。&lt;/li&gt;
&lt;li&gt;模型分级：简单问答走小模型（几百毫秒出首 token），复杂问题再路由大模型；或对延迟敏感的数字人场景直接上中小尺寸模型。&lt;/li&gt;
&lt;li&gt;就近部署 + 长连接连接池 + 定期预热，避免冷启动和跨地域 RTT。&lt;/li&gt;
&lt;li&gt;进阶要点：&lt;strong&gt;投机采样（speculative decoding）加速的是解码阶段，对 TTFT 帮助不大&lt;/strong&gt;——能分清「首 token 快」和「整体生成快」是两套优化，说明真懂推理过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第五路：感知层兜底&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优化到头也压不进目标时，用填充音（「嗯，我想想」）先开口争取 500-800ms，用户感知立刻好转。&lt;/li&gt;
&lt;li&gt;记住目标：&lt;strong&gt;流式架构下 TTFT 就是用户感知的全部&lt;/strong&gt;，后续 token 只要比语速快（TTS 会按说话节奏消费），生成总时长不影响体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：「TTFT 等于检索加排队加 Prefill。我的优化顺序是：语义缓存先拦截高频问题，RAG 检索并行化并本地部署 Embedding，prompt 精简加 Rerank 控量，稳定前缀前置吃满 KV Cache，小模型分级响应，最后填充音兜底感知。」&lt;/p&gt;
&lt;h3 id=&#34;33-嘴巴tts-语音合成&#34;&gt;3.3 嘴巴：TTS 语音合成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TTS（Text To Speech）&lt;/strong&gt; 决定数字人「听起来像不像人」。新人要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同样必须选&lt;strong&gt;流式 TTS&lt;/strong&gt;，而且关注一个核心指标：&lt;strong&gt;首包延迟（TTFB，Time To First Byte）&lt;/strong&gt;——从给文本到第一帧音频出来的时间，主流云服务在 200-400ms。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分句策略&lt;/strong&gt;：LLM 流式输出的文本按标点切成短句送 TTS，句与句之间流水线并行。怎么切是个真问题，展开见 3.3.1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;音色克隆&lt;/strong&gt;：商用数字人大多需要定制音色（克隆老板、克隆主播）。主流方案（火山、阿里 CosyVoice、GPT-SoVITS 开源）只需几分钟到几小时的样本。&lt;strong&gt;注意合规红线：音色克隆必须取得本人授权&lt;/strong&gt;，国内已有相关判例和《生成式人工智能服务管理暂行办法》的约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;韵律和情感&lt;/strong&gt;：检查 TTS 是否支持 SSML 或情感标签，数字人「说话像念经」多半是没做韵律控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;331-从首-token-到首包llm-和-tts-怎么衔接&#34;&gt;3.3.1 从首 token 到首包：LLM 和 TTS 怎么衔接&lt;/h4&gt;
&lt;p&gt;工程上绕不开的一道题：LLM 通过 SSE（Server-Sent Events）逐 token 往外吐，TTS 在另一头等着合成——&lt;strong&gt;中间这段，是攒够 N 个 token 就送一次 TTS，还是有更好的方案？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先纠正一个概念：&lt;strong&gt;SSE 只是传输管道&lt;/strong&gt;，它决定的是「token 一个一个到达」这个事实，不包含任何切分策略。真正要做决策的是服务端的&lt;strong&gt;聚合层（aggregator）&lt;/strong&gt;——它消费 SSE 流，决定什么时候把一段文本交给 TTS。&lt;/p&gt;
&lt;p&gt;三种方案的对比：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
					&lt;th&gt;评价&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;按 token 攒批&lt;/td&gt;
					&lt;td&gt;攒够 N 个 token 就送 TTS&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;差方案&lt;/strong&gt;。token 边界和语言边界无关，会切在半个词、半个标点上；每次 TTS 请求有固定开销，请求过碎成本高；韵律上下文残缺，合成质量差&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;按句/子句切分&lt;/td&gt;
					&lt;td&gt;缓冲 token，命中边界标点且长度达标才 flush&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;工业标配&lt;/strong&gt;。切点干净、韵律完整，首句策略可调&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;流式文本输入 TTS&lt;/td&gt;
					&lt;td&gt;TTS 本身支持边收文本边合成（WebSocket 双向流），把 SSE token 直接转发进去&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;更好但依赖厂商能力&lt;/strong&gt;。切句职责交给最懂韵律的 TTS 引擎，首包最快。ElevenLabs、火山、CosyVoice 流式版都支持；自部署 GPT-SoVITS 需要自己做这层&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&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
    T[SSE 收到一个 token] --&amp;gt; BUF[写入缓冲区]
    BUF --&amp;gt; C1{命中句末标点&amp;lt;br/&amp;gt;。！？ 且长度达标?}
    C1 -- 是 --&amp;gt; FLUSH[flush：送 TTS 队列]
    C1 -- 否 --&amp;gt; C2{缓冲长度超上限?}
    C2 -- 是 --&amp;gt; C3{回找最近的&amp;lt;br/&amp;gt;逗号/顿号}
    C3 -- 找到 --&amp;gt; FLUSH
    C3 -- 没有 --&amp;gt; WAIT[继续等&amp;lt;br/&amp;gt;到硬上限强切]
    C2 -- 否 --&amp;gt; C4{缓冲超时&amp;lt;br/&amp;gt;300-500ms?}
    C4 -- 是 --&amp;gt; FLUSH
    C4 -- 否 --&amp;gt; T
    WAIT --&amp;gt; T
    FLUSH --&amp;gt; NEXT[句子带序号进 TTS 队列&amp;lt;br/&amp;gt;并发合成 按序播放]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;四条规则记牢：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首句激进，后续放宽&lt;/strong&gt;：第一句是首音延迟的关键，切得短（命中第一个逗号、8-15 字就 flush，甚至 LLM prompt 里直接要求「第一句简短应答」）；后续句子放宽到 20-80 字保证韵律。首音快 200ms，用户感知就高一档。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切点优先级&lt;/strong&gt;：句号/问号/叹号最优先，分号/冒号次之，逗号只在超长兜底时用。顿号不切。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标点陷阱&lt;/strong&gt;：小数点（「3.5 秒」）、省略号、引号内标点、英文缩写（「e.g.」）都会造成误判，正则切分前先做这些特例清洗；markdown 符号（&lt;code&gt;**&lt;/code&gt;、&lt;code&gt;#&lt;/code&gt;、&lt;code&gt;1.&lt;/code&gt;）在进缓冲前就要剥掉，否则 TTS 会念出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发合成、按序播放&lt;/strong&gt;：切出来的句子打上序号进队列，多个 TTS 请求可以并发，但播放端必须按序号拼接，乱序就出「先说第二句」的事故。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;超时兜底&lt;/strong&gt;为什么必须有：LLM 有时会输出不带句末标点的长句，如果没有 300-500ms 的缓冲超时强刷，用户会眼睁睁看着数字人「憋着不说话」。&lt;/p&gt;
&lt;p&gt;一句话总结：「SSE 只负责把 token 送过来，切分在服务端聚合层做。不按 token 数攒批——按标点加长度双阈值切句，首句切得激进来压低首音时间，后续句放宽保韵律，再加超时兜底；如果 TTS 厂商支持流式文本输入，就直接把 token 流转发进去让引擎自己切，首包还能再快一截。」&lt;/p&gt;
&lt;h3 id=&#34;34-脸口型与表情驱动&#34;&gt;3.4 脸：口型与表情驱动&lt;/h3&gt;
&lt;p&gt;这是数字人区别于「语音助手」的关键一环，也是新人知识盲区最大的一环。&lt;/p&gt;
&lt;p&gt;核心技术概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Viseme（视素）&lt;/strong&gt;：音素（phoneme）对应的口型。比如 /b/ /p/ /m/ 都对应「双唇闭合」这个口型。英文一般归并为十几个 viseme，中文可按韵母口型归类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blendshape（混合形状）&lt;/strong&gt;：3D 模型的表情通过一组预定义变形（如 &lt;code&gt;jawOpen&lt;/code&gt;、&lt;code&gt;mouthSmile&lt;/code&gt;）按权重混合出来。音频 → 音素 → viseme → blendshape 权重，就是经典的口型驱动路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2D 真人形象驱动&lt;/strong&gt;：wav2lip、SadTalker、MuseTalk 这类模型，输入音频 + 人脸视频/照片，直接生成口型对齐的人脸帧。&lt;/li&gt;
&lt;/ul&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 TB
    AUDIO[TTS 音频流] --&amp;gt; PATH{形象路线}
    PATH --&amp;gt;|3D 写实路线| P1[音频提取音素/声学特征]
    P1 --&amp;gt; P2[映射 Viseme / Blendshape 权重]
    P2 --&amp;gt; P3[Unity/UE 实时渲染]
    PATH --&amp;gt;|2D 真人克隆路线| Q1[wav2lip / MuseTalk 等&amp;lt;br/&amp;gt;音频到人脸帧模型]
    Q1 --&amp;gt; Q2[逐帧生成口型对齐画面]
    PATH --&amp;gt;|Live2D 路线| R1[音量/音素驱动&amp;lt;br/&amp;gt;参数开合]
    R1 --&amp;gt; R2[Live2D SDK 渲染]

    P3 --&amp;gt; OUT[合成视频帧推流]
    Q2 --&amp;gt; OUT
    R2 --&amp;gt; OUT&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;选型经验：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;效果&lt;/th&gt;
					&lt;th&gt;成本&lt;/th&gt;
					&lt;th&gt;适用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Live2D&lt;/td&gt;
					&lt;td&gt;二次元，口型精度要求低&lt;/td&gt;
					&lt;td&gt;极低（CPU 可跑）&lt;/td&gt;
					&lt;td&gt;虚拟主播、轻量客服&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;3D Blendshape&lt;/td&gt;
					&lt;td&gt;可控、风格化或写实&lt;/td&gt;
					&lt;td&gt;中（需要建模绑定 + GPU 渲染）&lt;/td&gt;
					&lt;td&gt;品牌 IP、大屏数字员工&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;真人克隆（wav2lip 类）&lt;/td&gt;
					&lt;td&gt;最像真人&lt;/td&gt;
					&lt;td&gt;高（每帧都要过模型，吃 GPU）&lt;/td&gt;
					&lt;td&gt;数字分身、口播视频&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;35-身体动作与姿态&#34;&gt;3.5 身体：动作与姿态&lt;/h3&gt;
&lt;p&gt;只有脸会动的数字人被称为「花瓶数字人」。完整的身体驱动包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;待机动作（idle）&lt;/strong&gt;：不说话时的呼吸、眨眼、微动作。没有 idle 的数字人在沉默时会「死机感」十足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动作库触发&lt;/strong&gt;：LLM 输出时打标签（如 &lt;code&gt;[挥手]&lt;/code&gt;），引擎播放对应动画片段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动作生成/动捕&lt;/strong&gt;：高端场景用动作生成模型或真人动捕（光学动捕、惯性动捕、视觉动捕）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视线与点头&lt;/strong&gt;：根据对话状态做倾听点头、眼神接触，对「像人」的提升性价比极高。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;36-渲染与呈现&#34;&gt;3.6 渲染与呈现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端渲染&lt;/strong&gt;：形象资源下发到 App/Web/小程序，本地用 Unity、WebGL（three.js）、Live2D SDK 渲染。好处是不耗服务端 GPU、画面清晰；代价是包体积和端上性能适配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端渲染 + 推流&lt;/strong&gt;：服务端用 UE/Unity 无头渲染成视频流推给端上。好处是端上零成本、形象保密；代价是每路并发一块 GPU，成本高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;像素流送（Pixel Streaming）&lt;/strong&gt;：UE 官方的「服务端渲染 + WebRTC 推流」方案，超写实数字人大屏常用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;37-神经webrtc-低延迟传输&#34;&gt;3.7 神经：WebRTC 低延迟传输&lt;/h3&gt;
&lt;p&gt;实时交互必须走 &lt;strong&gt;WebRTC&lt;/strong&gt;（或声网 Agora、腾讯 TRTC、即构 ZEGO 这类商用 RTC SDK，底层同源）。不要用 HLS/RTMP 做交互——它们的延迟是秒级到十秒级，只能用于纯观看的直播场景。&lt;/p&gt;
&lt;p&gt;WebRTC 给数字人的价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端到端延迟可压到 200-400ms；&lt;/li&gt;
&lt;li&gt;自带弱网对抗（ jitter buffer、NACK、FEC）、AEC、降噪；&lt;/li&gt;
&lt;li&gt;天然支持双向音视频（用户上行说话 + 数字人下行画面）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程上通常自建或租用 SFU（Selective Forwarding Unit）做转发，数字人服务端作为一个「虚拟参会者」接入 RTC 房间。&lt;/p&gt;
&lt;h2 id=&#34;4-延迟预算实时数字人的生死线&#34;&gt;4. 延迟预算：实时数字人的生死线&lt;/h2&gt;
&lt;p&gt;交互自然度的经验值：&lt;strong&gt;用户说完话到数字人开口，超过 1.5 秒就会觉得「卡」，控制在 1 秒内算流畅，500ms 内接近真人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;逐环预算参考（单位毫秒）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;典型耗时&lt;/th&gt;
					&lt;th&gt;优化手段&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;VAD 判停（说完话确认）&lt;/td&gt;
					&lt;td&gt;300-600&lt;/td&gt;
					&lt;td&gt;语义 VAD、动态超时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ASR 末字出结果&lt;/td&gt;
					&lt;td&gt;100-300&lt;/td&gt;
					&lt;td&gt;流式 + 就近节点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LLM 首 token&lt;/td&gt;
					&lt;td&gt;300-800&lt;/td&gt;
					&lt;td&gt;小模型前置、就近部署、prompt 精简&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;TTS 首包&lt;/td&gt;
					&lt;td&gt;200-400&lt;/td&gt;
					&lt;td&gt;流式、句级流水线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;口型驱动 + 渲染首帧&lt;/td&gt;
					&lt;td&gt;50-200&lt;/td&gt;
					&lt;td&gt;预加载、模型量化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RTC 传输&lt;/td&gt;
					&lt;td&gt;100-300&lt;/td&gt;
					&lt;td&gt;就近接入、专线&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&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
    A[VAD 判停&amp;lt;br/&amp;gt;300-600ms] --&amp;gt; B[ASR 出字&amp;lt;br/&amp;gt;100-300ms]
    B --&amp;gt; C[LLM 首 token&amp;lt;br/&amp;gt;300-800ms]
    C --&amp;gt; D[TTS 首包&amp;lt;br/&amp;gt;200-400ms]
    D --&amp;gt; E[驱动渲染&amp;lt;br/&amp;gt;50-200ms]
    E --&amp;gt; F[RTC 送达&amp;lt;br/&amp;gt;100-300ms]
    style C fill:#fee,stroke:#c00&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;注意 &lt;strong&gt;LLM 是最大的延迟黑洞&lt;/strong&gt;，这也是为什么很多产品用「嗯、我想想」这类填充音（filler）先开口争取时间，以及用小参数模型做第一轮快速响应。&lt;/p&gt;
&lt;h2 id=&#34;5-离线内容生产链路&#34;&gt;5. 离线内容生产链路&lt;/h2&gt;
&lt;p&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 LR
    S[文案脚本] --&amp;gt; T[TTS 合成配音]
    T --&amp;gt; D[wav2lip / 形象克隆&amp;lt;br/&amp;gt;生成画面]
    D --&amp;gt; E[FFmpeg 合成&amp;lt;br/&amp;gt;字幕/背景/BGM]
    E --&amp;gt; V[成品视频]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这条链路的工程重点是&lt;strong&gt;批量化和质量控制&lt;/strong&gt;：文案队列、素材版本管理、抽帧质检（口型错位检测）、FFmpeg 自动化后期。它也是新人练手的最佳起点——没有实时性压力，每一步都可以慢慢调试。&lt;/p&gt;
&lt;h2 id=&#34;6-元宇宙视角下的数字人&#34;&gt;6. 元宇宙视角下的数字人&lt;/h2&gt;
&lt;p&gt;虽然元宇宙概念已过炒作高峰，但它的知识框架对理解数字人很有帮助：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数字人是元宇宙的「界面」&lt;/strong&gt;：元宇宙的愿景里，人通过 Avatar 进入虚拟世界，而 AI 数字人是这个世界里的 NPC、导游、店员。今天落地的数字人客服、数字人员工，可以看作「没有整个元宇宙、先把 NPC 做出来」的务实路线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字身份与数字资产&lt;/strong&gt;：虚拟形象的版权归属、音色的人格权、数字分身的行为授权，都是元宇宙法律框架讨论的核心，做商用数字人绕不开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XR 是下一级放大器&lt;/strong&gt;：目前的数字人主要在手机屏和大屏上，VR/AR 设备普及后，「面对面」的沉浸交互会把实时渲染和低延迟的要求再推高一个量级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术栈高度重合&lt;/strong&gt;：游戏引擎、实时渲染、Avatar 系统、RTC——元宇宙基建没浪费，全都迁移到了数字人赛道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结两者的关系：&lt;strong&gt;元宇宙是「世界」，数字人是「人」；世界还在建，人已经开始打工了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;7-避坑清单新人最常踩的十个坑&#34;&gt;7. 避坑清单（新人最常踩的十个坑）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用 HTTP 轮询或 WebSocket 传实时音视频&lt;/strong&gt;：WebSocket 传音频可以，但视频和双向通话必须 WebRTC。架构选错后面全是补丁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等 LLM 全句生成完再送 TTS&lt;/strong&gt;：首音延迟直接多出 1-3 秒。必须按句流式切分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTS 分句切在奇怪的位置&lt;/strong&gt;：按标点切，且避免切出「的」「了」这种单字句，否则韵律全毁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忽略 VAD 判停时间&lt;/strong&gt;：默认 500-800ms 的静音判停在口语场景经常切断用户长句，要按场景调。反过来判起也有坑——不做 200-300ms 预滚动缓冲（pre-roll）会吃掉句首，「你好」变成「好」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没做 AEC 就上扬声器场景&lt;/strong&gt;：数字人听见自己说话，无限自答。音箱/大屏场景必查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;口型和音频各走各的时钟&lt;/strong&gt;：音频流和视频帧必须共用时间戳对齐，否则网络抖动后口型越偏越远。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忽视打断（barge-in）&lt;/strong&gt;：用户插话时数字人必须能立刻闭嘴并清空播放队列，这是「像人」的及格线，很多 Demo 死在这。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有 idle 状态&lt;/strong&gt;：沉默时形象完全静止，恐怖谷效应拉满。眨眼和呼吸是性价比最高的两个动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;音色克隆不管授权&lt;/strong&gt;：合规风险，商用前必须签授权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发成本没算清就接客&lt;/strong&gt;：真人克隆路线每路并发都吃 GPU，一路成本可能是纯语音机器人的几十倍，报价前先压测。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;8-典型问题与解决方案&#34;&gt;8. 典型问题与解决方案&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：口型和声音对不上，越聊偏得越远。&lt;/strong&gt;
音频帧和驱动帧的时间戳没有同源。方案：以音频流为主时钟，口型权重按音频时间戳生成，渲染帧消费时做对齐；网络侧开启 NACK 减少重传造成的画面跳变。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：数字人响应慢，用户说完要等两三秒。&lt;/strong&gt;
按第 4 节的预算表逐环打点定位。八成问题在 LLM 首 token 和 TTS 首包：LLM 换更快的模型或精简 prompt，TTS 确认开了流式，两句之间流水线并行。还不行就加填充音。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：用户插话，数字人不理继续说完。&lt;/strong&gt;
没有 barge-in 机制。方案：ASR 检测到新的用户语音起始 → 立即向 TTS/播放器发中断信号 → 清空待发队列 → LLM 侧中断当前生成（保留已说内容到上下文）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：声音机械、像念稿。&lt;/strong&gt;
检查：文本是否带了 markdown 符号（TTS 会念出来）；是否用了支持韵律/情感的模型；分句是否过碎。可以在送 TTS 前加一道「口语化改写」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：回声、啸叫、把自己当用户。&lt;/strong&gt;
AEC 没生效。WebRTC 场景确认 &lt;code&gt;echoCancellation: true&lt;/code&gt;；自研管线接入 WebRTC APM；硬件上麦克风和扬声器做物理隔离。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：大促/高峰期并发上不去，GPU 烧钱。&lt;/strong&gt;
真人克隆路线把渲染挪到闲时（离线预生成常见回答视频片段，实时只做拼接）；或降级到 3D/Live2D 路线；LLM 层加意图缓存，高频问题直接命中预置音视频。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：数字人前后人设不一致，聊着聊着「换人」了。&lt;/strong&gt;
人格 prompt 没有和记忆系统打通。方案：人格定义放 system 层固定不动；用户画像和会话摘要由记忆系统（如 Mem0）注入；定期用人格一致性评估集回归测试。&lt;/p&gt;
&lt;h2 id=&#34;9-学习路径从-0-到精通&#34;&gt;9. 学习路径：从 0 到精通&lt;/h2&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 TB
    subgraph S1[&amp;#34;第一阶段：跑通（1-2 周）&amp;#34;]
        A1[调通云 ASR + LLM + TTS&amp;lt;br/&amp;gt;做一个语音对话命令行 Demo]
        A2[用 wav2lip 开源模型&amp;lt;br/&amp;gt;离线生成一个口播视频]
    end

    subgraph S2[&amp;#34;第二阶段：像人（3-4 周）&amp;#34;]
        B1[全链路流式化改造&amp;lt;br/&amp;gt;打点测延迟]
        B2[实现 barge-in 打断]
        B3[接入一个 2D/3D 形象&amp;lt;br/&amp;gt;完成口型驱动]
    end

    subgraph S3[&amp;#34;第三阶段：上线（4-8 周）&amp;#34;]
        C1[WebRTC 推拉流&amp;lt;br/&amp;gt;端到端联调]
        C2[记忆系统 + 人格 prompt 工程]
        C3[压测与成本核算&amp;lt;br/&amp;gt;弱网对抗]
    end

    subgraph S4[&amp;#34;第四阶段：精通（持续）&amp;#34;]
        D1[自研/微调 TTS 与驱动模型]
        D2[情感计算与多模态交互]
        D3[中台化：多形象/多租户/编排]
    end

    S1 --&amp;gt; S2 --&amp;gt; S3 --&amp;gt; S4&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;练手项目建议（按递进排序）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;命令行语音助手：打通 ASR → LLM → TTS，全程流式。&lt;/li&gt;
&lt;li&gt;离线口播视频生成器：文案进、成片出，加 FFmpeg 字幕。&lt;/li&gt;
&lt;li&gt;网页实时数字人：Live2D 或 wav2lip 形象 + WebRTC，实现打断。&lt;/li&gt;
&lt;li&gt;有记忆的数字人：接入长时记忆，连续聊三天还记得用户说过什么。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;10-总结&#34;&gt;10. 总结&lt;/h2&gt;
&lt;p&gt;数字人链路说复杂也复杂，说简单就是一句话：&lt;strong&gt;把人的「听、想、说、动」拆成 ASR、LLM、TTS、驱动渲染、RTC 五个流式环节，再把总延迟压进 1 秒&lt;/strong&gt;。新人入门的正确姿势不是啃论文，而是先跑通最小闭环，再逐环升级——每一环的坑本文都列了，剩下的就是动手。&lt;/p&gt;
&lt;p&gt;概念地图有了，想看这条链路在真实开源项目里长什么样，接着读&lt;a href=&#34;https://ftmao.com/blog/blog/posts/livetalking-source-code-analysis/&#34;&gt;《LiveTalking 源码解剖：一个开源实时数字人是怎么跑起来的》&lt;/a&gt;——它把本文的输出侧（TTS、口型驱动、RTC 推流）做成了工业级实现，输入侧和智能侧留着 demo 形态，正好当对照样本。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Agent-Chat：基于 AgentScope 2.0 &#43; Mem0 的多 Agent 群聊陪伴系统</title>
      <link>https://ftmao.com/blog/posts/agent-chat-multi-agent-companion/</link>
      <pubDate>Tue, 04 Aug 2026 12:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/agent-chat-multi-agent-companion/</guid>
      <description>&lt;h1 id=&#34;agent-chat基于-agentscope-20--mem0-的多-agent-群聊陪伴系统&#34;&gt;Agent-Chat：基于 AgentScope 2.0 + Mem0 的多 Agent 群聊陪伴系统&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/agent-chat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你有没有想过，把一群性格各异的 AI 拉进同一个微信群，他们会怎么聊天？&lt;/p&gt;
&lt;p&gt;不是那种一问一答的客服机器人，而是会主动接梗、会吃醋、会安慰人、会记住你上周说过喜欢喝燕麦拿铁的“AI 朋友”。我开发的&lt;strong&gt;Agent-Chat&lt;/strong&gt; 就是想做这件事：一个支持多角色、多记忆、可配置人格的群聊多 Agent 陪伴系统。&lt;/p&gt;
&lt;p&gt;下图是一个实际运行中的群聊界面：用户 &lt;code&gt;@王姐&lt;/code&gt; 让她描述下自己，王姐和海公公两个 Agent 基于长期记忆给出了个性化的用户画像回应。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;http://ftmao.com/blog/images/ScreenShot_2026-08-04_202545_484.png&#34; alt=&#34;Agent-Chat 群聊界面：@王姐询问用户画像&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要这个工具&#34;&gt;一、为什么需要这个工具？&lt;/h2&gt;
&lt;h3 id=&#34;11-单-agent-聊天-vs-多-agent-群聊&#34;&gt;1.1 单 Agent 聊天 vs. 多 Agent 群聊&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;单 Agent 聊天（如 ChatGPT 网页版）&lt;/th&gt;
					&lt;th&gt;多 Agent 群聊（Agent-Chat）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;交互形态&lt;/td&gt;
					&lt;td&gt;1 对 1 问答&lt;/td&gt;
					&lt;td&gt;1 个用户 + N 个 Agent 同时在线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;角色多样性&lt;/td&gt;
					&lt;td&gt;只有一个固定角色&lt;/td&gt;
					&lt;td&gt;可配置多个性格、职业、说话风格各异的角色&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;社交氛围&lt;/td&gt;
					&lt;td&gt;像咨询顾问&lt;/td&gt;
					&lt;td&gt;像朋友群，Agent 之间会互动、接话、调侃&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;记忆隔离&lt;/td&gt;
					&lt;td&gt;全局统一记忆&lt;/td&gt;
					&lt;td&gt;支持“Agent 对用户的私有记忆”+“公共记忆”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;发言调度&lt;/td&gt;
					&lt;td&gt;用户问一句答一句&lt;/td&gt;
					&lt;td&gt;Agent 自主决定是否发言、@谁、说什么&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;单 Agent 适合办事，多 Agent 适合陪伴。当你只是想找人闲聊、吐槽、或者看几个“朋友”互相斗嘴时，一个群比私聊更自然。&lt;/p&gt;
&lt;h3 id=&#34;12-直接用-llm-api-vs-用-agent-chat&#34;&gt;1.2 直接用 LLM API vs. 用 Agent-Chat&lt;/h3&gt;
&lt;p&gt;很多开发者可能会想：我直接调 DeepSeek API，自己写个循环不就行了？理论上可以，但落到工程上会碰到一堆问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;自己调 API&lt;/th&gt;
					&lt;th&gt;Agent-Chat 的做法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;多 Agent 并发管理&lt;/td&gt;
					&lt;td&gt;自己维护 N 个对话上下文，容易串台&lt;/td&gt;
					&lt;td&gt;AgentScope 2.0 统一运行时 + Redis 短期记忆&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;谁该说话&lt;/td&gt;
					&lt;td&gt;每次用户发言都让所有 Agent 回复，刷屏严重&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Orchestrator&lt;/code&gt; 调度器：置信度排序、并发上限、冷却时间、@触发&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;长期记忆&lt;/td&gt;
					&lt;td&gt;自己拼 prompt，历史越长越贵&lt;/td&gt;
					&lt;td&gt;Mem0 自动事实提取、去重、语义检索&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;记忆隔离&lt;/td&gt;
					&lt;td&gt;所有 Agent 共享同一份历史&lt;/td&gt;
					&lt;td&gt;按 &lt;code&gt;user_id&lt;/code&gt; + &lt;code&gt;agent_id&lt;/code&gt; 分层作用域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;人格配置&lt;/td&gt;
					&lt;td&gt;写死 prompt&lt;/td&gt;
					&lt;td&gt;运营后台可配置 Agent 人格、版本、关系、禁忌话题&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;实时通信&lt;/td&gt;
					&lt;td&gt;自己搭 WebSocket&lt;/td&gt;
					&lt;td&gt;FastAPI 原生 WebSocket + 简单 Web UI&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Agent-Chat 并不是为了替代 ChatGPT，而是把“多 Agent 群聊”这个场景的工程骨架搭好，让你能专注在角色设计、记忆策略和交互体验上。&lt;/p&gt;
&lt;h3 id=&#34;13-现有群聊机器人-vs-agent-chat&#34;&gt;1.3 现有群聊机器人 vs. Agent-Chat&lt;/h3&gt;
&lt;p&gt;市面上也有不少群聊机器人，但大多数是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规则触发&lt;/strong&gt;：关键字回复，缺乏上下文理解；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单轮回复&lt;/strong&gt;：不会主动参与话题；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无记忆&lt;/strong&gt;：每次重启都是陌生人；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人格单一&lt;/strong&gt;：无法运营多个差异化角色。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Agent-Chat 的核心差异是：&lt;strong&gt;把每个 Agent 当成有记忆、有情绪、有关系的虚拟人&lt;/strong&gt;，而不是一个只会响应命令的工具。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二技术栈&#34;&gt;二、技术栈&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;选型&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;后端框架&lt;/td&gt;
					&lt;td&gt;FastAPI&lt;/td&gt;
					&lt;td&gt;高性能异步 Web 框架，原生支持 WebSocket&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多 Agent 框架&lt;/td&gt;
					&lt;td&gt;AgentScope 2.0&lt;/td&gt;
					&lt;td&gt;支持 Agent 生命周期、消息总线、工具调用、ReAct、Middleware&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;主力 LLM&lt;/td&gt;
					&lt;td&gt;DeepSeek&lt;/td&gt;
					&lt;td&gt;OpenAI 兼容接口，通过 &lt;code&gt;DeepSeekChatModel&lt;/code&gt; 统一调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;长期记忆&lt;/td&gt;
					&lt;td&gt;Mem0 (self-hosted)&lt;/td&gt;
					&lt;td&gt;自动事实提取、去重、合并，支持按用户/Agent 作用域隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量数据库&lt;/td&gt;
					&lt;td&gt;Qdrant&lt;/td&gt;
					&lt;td&gt;Mem0 推荐的向量后端，轻量高性能&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;嵌入模型&lt;/td&gt;
					&lt;td&gt;BGE 系列（BGE-small-zh-v1.5 / BGE-M3 可配置）&lt;/td&gt;
					&lt;td&gt;本地加载，中文语义召回效果好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短期记忆&lt;/td&gt;
					&lt;td&gt;Redis&lt;/td&gt;
					&lt;td&gt;房间消息滑动窗口、会话摘要、在线状态&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;关系数据&lt;/td&gt;
					&lt;td&gt;MySQL 8.0+&lt;/td&gt;
					&lt;td&gt;用户、Agent、房间、消息、关系、事件&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;前端&lt;/td&gt;
					&lt;td&gt;简单 Web UI (HTML + JS)&lt;/td&gt;
					&lt;td&gt;第一阶段提供基础群聊界面&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;部署&lt;/td&gt;
					&lt;td&gt;Docker Compose&lt;/td&gt;
					&lt;td&gt;MySQL + Redis + Qdrant 一键拉起&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;注：项目中默认使用 &lt;code&gt;BGE-small-zh-v1.5&lt;/code&gt;（512 维），但架构上模型路径与维度均可通过环境变量配置，可平滑替换为 &lt;code&gt;BGE-M3&lt;/code&gt; 等更强的多语言嵌入模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三系统架构&#34;&gt;三、系统架构&lt;/h2&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;graph TB
    subgraph Client[&amp;#34;客户端&amp;#34;]
        UI[&amp;#34;简单 Web UI&amp;lt;br/&amp;gt;HTML + WebSocket&amp;#34;]
    end

    subgraph Backend[&amp;#34;后端服务&amp;#34;]
        FastAPI[&amp;#34;FastAPI Chat Service&amp;lt;br/&amp;gt;REST API + WebSocket&amp;#34;]
        RoomManager[&amp;#34;RoomManager / RoomRuntime&amp;#34;]
        Orchestrator[&amp;#34;Orchestrator 调度器&amp;#34;]
    end

    subgraph AgentRuntime[&amp;#34;AgentScope 2.0 运行时&amp;#34;]
        AgentInstance[&amp;#34;AgentInstance&amp;lt;br/&amp;gt;每个房间每个 Agent 一个实例&amp;#34;]
        Agent[&amp;#34;agentscope.agent.Agent&amp;#34;]
        Toolkit[&amp;#34;Toolkit + FunctionTool&amp;lt;br/&amp;gt;时间 / 天气 / 搜索 / 抓取&amp;#34;]
        Middleware[&amp;#34;GroupChatMem0Middleware&amp;lt;br/&amp;gt;per-user 记忆读写&amp;#34;]
    end

    subgraph Memory[&amp;#34;记忆层&amp;#34;]
        Redis[(Redis&amp;lt;br/&amp;gt;短期记忆 / 会话摘要 / 在线状态)]
        Mem0[Mem0 AsyncMemory&amp;lt;br/&amp;gt;长期记忆 / 事实提取 / 语义检索]
        Qdrant[(Qdrant&amp;lt;br/&amp;gt;向量存储)]
        BGE[&amp;#34;BGE Embedding&amp;lt;br/&amp;gt;bge-small-zh-v1.5 / BGE-M3&amp;#34;]
    end

    subgraph Data[&amp;#34;关系数据&amp;#34;]
        MySQL[(MySQL 8.0&amp;lt;br/&amp;gt;users / agents / rooms / messages / relationships / events)]
    end

    subgraph LLM[&amp;#34;大模型&amp;#34;]
        DeepSeek[&amp;#34;DeepSeek Chat API&amp;lt;br/&amp;gt;OpenAI 兼容&amp;#34;]
    end

    UI --&amp;gt;|WebSocket / HTTP| FastAPI
    FastAPI --&amp;gt; RoomManager
    RoomManager --&amp;gt; Orchestrator
    RoomManager --&amp;gt; AgentInstance
    AgentInstance --&amp;gt; Agent
    Agent --&amp;gt; Toolkit
    Agent --&amp;gt; Middleware
    Middleware --&amp;gt; Mem0
    Mem0 --&amp;gt; Qdrant
    Mem0 --&amp;gt; BGE
    Agent --&amp;gt; DeepSeek
    RoomManager --&amp;gt; Redis
    FastAPI --&amp;gt; MySQL
    Mem0 --&amp;gt; DeepSeek&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四核心设计&#34;&gt;四、核心设计&lt;/h2&gt;
&lt;h3 id=&#34;41-agent-生命周期&#34;&gt;4.1 Agent 生命周期&lt;/h3&gt;
&lt;p&gt;Agent 不是临时对象，而是运营预先配置的“角色模板”。一个 Agent 可以被添加到多个房间，在每个房间内有独立的运行实例和记忆空间。&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
    A[运营后台创建 Agent] --&amp;gt; B[写入 agents 表]
    B --&amp;gt; C[生成 persona_summary&amp;lt;br/&amp;gt;可选 DeepSeek 质量检查]
    C --&amp;gt; D[Agent 状态 active]
    D --&amp;gt; E[将 Agent 加入房间]
    E --&amp;gt; F[RoomRuntime 实例化 AgentInstance]
    F --&amp;gt; G[注册到 MsgHub]
    G --&amp;gt; H[参与群聊]
    H --&amp;gt; I[用户退出或移除 Agent]
    I --&amp;gt; J[保留历史与记忆]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个 Agent 可配置：昵称、年龄、职业、性格标签、兴趣、说话风格、语气、回复概率、最大回复长度、禁忌话题等。&lt;/p&gt;
&lt;p&gt;Agent 人格的核心是 &lt;code&gt;system_prompt&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;你是 {name}，{age} 岁，职业是 {job}。性格：{traits}。说话风格：{speaking_style}。语气：{voice_tone}。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{system_prompt}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;重要规则：
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 你只输出你要说的话，不要加自己的名字前缀。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 保持自然、像真人一样，不要太长（不超过 {max_reply_length} 字）。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 你是 AI，不是真人。不要诱导用户泄露隐私或进行危险行为。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;为了让同一 Agent 能在不同房间里保持一致的“人格”，但又能感知到不同的房间成员和关系，系统把 prompt 拆成两部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态人设（&lt;code&gt;_base_system_prompt&lt;/code&gt;）&lt;/strong&gt;：在 &lt;code&gt;AgentInstance&lt;/code&gt; 初始化时构建，只与 Agent 自身属性有关；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态上下文（&lt;code&gt;_full_system_prompt&lt;/code&gt;）&lt;/strong&gt;：每次生成回复前拼接，包含当前房间名、成员列表、关系、用户画像、近期摘要、检索记忆等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种“静态人格 + 动态上下文”的分离，让人格稳定、上下文新鲜，也便于缓存和版本管理。&lt;/p&gt;
&lt;h3 id=&#34;42-记忆分层&#34;&gt;4.2 记忆分层&lt;/h3&gt;
&lt;p&gt;记忆是陪伴系统的灵魂。Agent-Chat 把记忆分成三层：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;范围&lt;/th&gt;
					&lt;th&gt;存储&lt;/th&gt;
					&lt;th&gt;更新频率&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;瞬时记忆&lt;/td&gt;
					&lt;td&gt;当前请求&lt;/td&gt;
					&lt;td&gt;Prompt 上下文&lt;/td&gt;
					&lt;td&gt;每次请求&lt;/td&gt;
					&lt;td&gt;保证即时上下文连贯&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短期记忆&lt;/td&gt;
					&lt;td&gt;当前会话&lt;/td&gt;
					&lt;td&gt;Redis&lt;/td&gt;
					&lt;td&gt;实时&lt;/td&gt;
					&lt;td&gt;维护会话历史窗口与摘要&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;长期记忆&lt;/td&gt;
					&lt;td&gt;跨会话&lt;/td&gt;
					&lt;td&gt;Mem0 + Qdrant&lt;/td&gt;
					&lt;td&gt;实时/异步&lt;/td&gt;
					&lt;td&gt;用户画像、关系、事件、偏好&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&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 TB
    subgraph Instant[&amp;#34;瞬时记忆&amp;#34;]
        P[Prompt 注入:&amp;lt;br/&amp;gt;当前消息 + 近期历史 + 检索记忆]
    end

    subgraph ShortTerm[&amp;#34;短期记忆 Redis&amp;#34;]
        R1[&amp;#34;room:{room_id}:messages&amp;#34;]
        R2[&amp;#34;room:{room_id}:summary&amp;#34;]
    end

    subgraph LongTerm[&amp;#34;长期记忆 Mem0 + Qdrant&amp;#34;]
        M1[&amp;#34;Agent ↔ 用户 私有记忆&amp;#34;]
        M2[&amp;#34;用户公共记忆&amp;#34;]
        M3[&amp;#34;Agent 公共记忆&amp;#34;]
        M4[&amp;#34;全局公共记忆&amp;#34;]
    end

    User[用户发言] --&amp;gt; FastAPI
    FastAPI --&amp;gt; R1
    FastAPI --&amp;gt;|异步| M2
    FastAPI --&amp;gt;|达到阈值| R2
    R1 --&amp;gt; P
    R2 --&amp;gt; P
    M1 --&amp;gt; P
    M2 --&amp;gt; P
    M3 --&amp;gt; P
    M4 --&amp;gt; P
    P --&amp;gt; DeepSeek[DeepSeek 生成回复]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;421-短期记忆redis-滑动窗口--增量摘要&#34;&gt;4.2.1 短期记忆：Redis 滑动窗口 + 增量摘要&lt;/h4&gt;
&lt;p&gt;短期记忆负责“当前这次聊天不冷场”。实现上，每个房间在 Redis 中有两个 key：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;room:{room_id}:messages  -&amp;gt;  List&amp;lt;Message JSON&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;room:{room_id}:summary   -&amp;gt;  String&amp;lt;Summary JSON&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;messages&lt;/code&gt; 保存最近 N 条消息（默认约 30 条）；&lt;/li&gt;
&lt;li&gt;超过窗口后，对早期消息做增量摘要，生成 &lt;code&gt;session_summary&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;请求 LLM 时，prompt 中注入：会话摘要 + 最近 K 条完整消息 + 当前用户消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;摘要策略：每新增 10 条消息触发一次增量摘要，由 DeepSeek 生成，保留关键事实、用户情绪、未决话题。摘要本身也作为长期记忆写入 Mem0。&lt;/p&gt;
&lt;h4 id=&#34;422-长期记忆mem0--qdrant--bge&#34;&gt;4.2.2 长期记忆：Mem0 + Qdrant + BGE&lt;/h4&gt;
&lt;p&gt;长期记忆解决“跨会话还记得你”的问题。我们选用 &lt;strong&gt;Mem0&lt;/strong&gt; 作为记忆层，原因有三：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;专为 LLM/Agent 设计&lt;/strong&gt;：自动完成事实提取、去重、合并，不用自己写召回逻辑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持 &lt;code&gt;user_id&lt;/code&gt; 与 &lt;code&gt;agent_id&lt;/code&gt; 双重作用域&lt;/strong&gt;：天然适合“每个 Agent 对用户有独立记忆”的群聊场景；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可 self-hosted&lt;/strong&gt;：向量后端用 Qdrant，数据自主可控。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Mem0 的记忆作用域设计非常关键：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;user_id&lt;/th&gt;
					&lt;th&gt;agent_id&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;有&lt;/td&gt;
					&lt;td&gt;有&lt;/td&gt;
					&lt;td&gt;该 Agent 对该用户的&lt;strong&gt;私有记忆&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;有&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;用户的&lt;strong&gt;公共记忆&lt;/strong&gt;，所有 Agent 可读&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;有&lt;/td&gt;
					&lt;td&gt;该 Agent 的&lt;strong&gt;公共记忆&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;全局公共记忆（房间摘要、事件）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这意味着：所有 Agent 都知道“用户喜欢猫”，但只有 Agent A 记得“用户昨天跟 Agent A 吐槽了老板”。Agent B 可以假装不知道，让 Agent A 去安慰用户，更像真人社交。&lt;/p&gt;
&lt;h4 id=&#34;423-长期记忆写入流程&#34;&gt;4.2.3 长期记忆写入流程&lt;/h4&gt;
&lt;p&gt;每次群聊产生一条真实消息后，由 &lt;code&gt;Memory Service&lt;/code&gt; 统一写入 Mem0，写入是&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 LR
    A[用户/Agent 发言] --&amp;gt; B[保存到 MySQL messages]
    B --&amp;gt; C[写入 Redis 短期记忆]
    C --&amp;gt; D{发送者类型}
    D --&amp;gt;|user| E[user_id=用户ID&amp;lt;br/&amp;gt;agent_id=None&amp;lt;br/&amp;gt;公共记忆]
    D --&amp;gt;|agent 回复用户| F[user_id=用户ID&amp;lt;br/&amp;gt;agent_id=AgentID&amp;lt;br/&amp;gt;私有记忆]
    D --&amp;gt;|agent 主动发言| G[user_id=None&amp;lt;br/&amp;gt;agent_id=AgentID&amp;lt;br/&amp;gt;Agent 公共记忆]
    D --&amp;gt;|系统摘要| H[user_id=None&amp;lt;br/&amp;gt;agent_id=None&amp;lt;br/&amp;gt;run_id=房间ID]
    E --&amp;gt; I[Mem0.add 异步写入]
    F --&amp;gt; I
    G --&amp;gt; I
    H --&amp;gt; I
    I --&amp;gt; J[Mem0 自动事实提取 / 去重 / 合并]
    J --&amp;gt; K[Qdrant 向量存储]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;写入时根据场景选择作用域：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;写入内容&lt;/th&gt;
					&lt;th&gt;user_id&lt;/th&gt;
					&lt;th&gt;agent_id&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;用户发言&lt;/td&gt;
					&lt;td&gt;用户说的话、情绪、偏好&lt;/td&gt;
					&lt;td&gt;用户 ID&lt;/td&gt;
					&lt;td&gt;为空（公共记忆）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent 回复用户&lt;/td&gt;
					&lt;td&gt;Agent 与用户的完整对话轮次&lt;/td&gt;
					&lt;td&gt;用户 ID&lt;/td&gt;
					&lt;td&gt;当前 Agent ID&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent 之间互动&lt;/td&gt;
					&lt;td&gt;Agent A 对 Agent B 的回复&lt;/td&gt;
					&lt;td&gt;为空&lt;/td&gt;
					&lt;td&gt;Agent A ID&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会话摘要生成&lt;/td&gt;
					&lt;td&gt;摘要内容作为长期事实&lt;/td&gt;
					&lt;td&gt;为空&lt;/td&gt;
					&lt;td&gt;为空 / run_id=room_id&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&#34;424-长期记忆读取流程&#34;&gt;4.2.4 长期记忆读取流程&lt;/h4&gt;
&lt;p&gt;Agent 生成回复前，&lt;code&gt;Memory Service&lt;/code&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 TB
    A[当前消息] --&amp;gt; B[检索 Agent ↔ 当前用户 私有记忆]
    B --&amp;gt; C[检索 用户公共记忆]
    C --&amp;gt; D[检索 该 Agent 公共记忆]
    D --&amp;gt; E[检索 全局公共记忆]
    E --&amp;gt; F[按相关度 + 时间排序]
    F --&amp;gt; G[去重]
    G --&amp;gt; H[注入 Prompt]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;检索优先级：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agent 对当前用户的私有记忆&lt;/strong&gt;（&lt;code&gt;user_id=user_id, agent_id=agent_id&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户的公共记忆&lt;/strong&gt;（&lt;code&gt;user_id=user_id, agent_id=None&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;该 Agent 的公共记忆&lt;/strong&gt;（&lt;code&gt;user_id=None, agent_id=agent_id&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局公共记忆&lt;/strong&gt;（&lt;code&gt;user_id=None, agent_id=None&lt;/code&gt;，如房间摘要、重要事件）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注入 Prompt 时，记忆会被格式化为：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 关于 {target_user_name} 的记忆
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 阿杰记得 {target_user_name} 上周说很喜欢吃火锅。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; {target_user_name} 提到过自己是程序员，工作压力比较大。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 关于你自己的记忆
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 你之前答应过 {target_user_name} 要推荐科幻电影。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 群聊公共记忆
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 这个群经常深夜聊天，氛围比较轻松。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;425-记忆与结构化表的联动&#34;&gt;4.2.5 记忆与结构化表的联动&lt;/h4&gt;
&lt;p&gt;Mem0 负责“自动提取和检索事实”，但一些业务逻辑仍需要显式的关系型数据，因此会同步到 MySQL：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关系变化&lt;/strong&gt;：当 Mem0 提取到“用户和 Agent 关系变好/变差”时，更新 &lt;code&gt;relationships&lt;/code&gt; 表的 &lt;code&gt;intimacy&lt;/code&gt; 字段；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重要事件&lt;/strong&gt;：当提取到“用户生日”“吵架”“约定”等事件时，写入 &lt;code&gt;events&lt;/code&gt; 表，便于按时间线展示；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户画像摘要&lt;/strong&gt;：定期从 Mem0 导出用户画像，更新 &lt;code&gt;user_profiles.summary&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现方式：后台 Worker 定期扫描 Mem0 新增记忆，用 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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sync_structured_memory&lt;/span&gt;(user_id: str, agent_id: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    recent_memories &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; memory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_all(user_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user_id, agent_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;agent_id, limit&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;, after&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;timestamp_24h_ago)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;analyze_memory_changes(recent_memories)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;relationship_change&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; update_relationship(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;new_events&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; create_events(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;profile_update&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; update_user_profile(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-消息流与调度&#34;&gt;4.3 消息流与调度&lt;/h3&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;sequenceDiagram
    autonumber
    participant U as 用户
    participant F as FastAPI / WebSocket
    participant R as Redis 短期记忆
    participant M as Mem0 长期记忆
    participant O as Orchestrator 调度器
    participant A as AgentInstance
    participant D as DeepSeek

    U-&amp;gt;&amp;gt;F: 发送消息
    F-&amp;gt;&amp;gt;MySQL: 保存消息
    F-&amp;gt;&amp;gt;U: 广播用户消息
    F-&amp;gt;&amp;gt;R: 写入短期记忆
    F-&amp;gt;&amp;gt;M: 异步写入长期记忆
    F-&amp;gt;&amp;gt;O: 触发 Agent 回复决策
    loop 对每个 Agent
        O-&amp;gt;&amp;gt;A: should_reply(query, recent, memories)
        A-&amp;gt;&amp;gt;M: 检索相关记忆
        A-&amp;gt;&amp;gt;D: 判断是否回复、回复对象、意图
        D--&amp;gt;&amp;gt;A: JSON 决策
    end
    O-&amp;gt;&amp;gt;O: 按置信度排序，限制并发数
    loop 被选中的 Agent
        O-&amp;gt;&amp;gt;A: generate_reply(...)
        A-&amp;gt;&amp;gt;R: 读取近期消息
        A-&amp;gt;&amp;gt;M: Middleware 注入记忆
        A-&amp;gt;&amp;gt;D: 生成回复
        D--&amp;gt;&amp;gt;A: 文本回复
        A--&amp;gt;&amp;gt;O: 回复内容
    end
    O-&amp;gt;&amp;gt;F: 逐个保存并广播 Agent 回复
    F-&amp;gt;&amp;gt;R: 更新短期记忆
    F-&amp;gt;&amp;gt;M: Middleware 写入对话记忆&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;调度策略包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最大同时响应数&lt;/strong&gt;：默认限制并发 Agent 回复数，避免刷屏；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冷却时间&lt;/strong&gt;：同一 Agent 两次发言间隔不少于 N 秒；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相关性过滤&lt;/strong&gt;：消息与 Agent 兴趣/角色相关才参与；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;@ 触发&lt;/strong&gt;：被 @ 的 Agent 优先响应；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情绪/氛围&lt;/strong&gt;：根据聊天氛围调整不同类型 Agent 的发言权重。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-工具与-react&#34;&gt;4.4 工具与 ReAct&lt;/h3&gt;
&lt;p&gt;AgentScope 2.0 的 Agent 内置 ReAct 循环，可自动调用工具。当前支持的工具包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;time&lt;/code&gt;：获取当前时间；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;weather&lt;/code&gt;：查询天气；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;location_by_ip&lt;/code&gt;：IP 定位；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web_search&lt;/code&gt;：网络搜索（DuckDuckGo 或 Serper）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch_url&lt;/code&gt;：抓取网页内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具通过 &lt;code&gt;FunctionTool&lt;/code&gt; 注册到 &lt;code&gt;Toolkit&lt;/code&gt;，Agent 自己决定什么时候调用。&lt;/p&gt;
&lt;h3 id=&#34;45-人格画像与关系设计&#34;&gt;4.5 人格画像与关系设计&lt;/h3&gt;
&lt;h4 id=&#34;451-agent-人格模型&#34;&gt;4.5.1 Agent 人格模型&lt;/h4&gt;
&lt;p&gt;Agent 的人格不仅是 prompt，而是一套可运营、可迭代的配置。核心表结构如下：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; agents (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; (UUID()),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;64&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;UNIQUE&lt;/span&gt;,       &lt;span style=&#34;color:#75715e&#34;&gt;-- 昵称，全局唯一
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    avatar_url VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;255&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    age INT,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    job VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;64&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    traits JSON,                            &lt;span style=&#34;color:#75715e&#34;&gt;-- 性格标签，如 [&amp;#34;乐观&amp;#34;, &amp;#34;毒舌&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    interests JSON,                         &lt;span style=&#34;color:#75715e&#34;&gt;-- 兴趣话题，如 [&amp;#34;电影&amp;#34;, &amp;#34;旅行&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    speaking_style TEXT &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,           &lt;span style=&#34;color:#75715e&#34;&gt;-- 说话风格描述
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    system_prompt TEXT &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,            &lt;span style=&#34;color:#75715e&#34;&gt;-- 核心系统提示词
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    voice_tone VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;32&lt;/span&gt;),                 &lt;span style=&#34;color:#75715e&#34;&gt;-- 语气基调
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    default_reply_probability DECIMAL(&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;,&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;.&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_reply_length INT &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    forbidden_topics JSON,                  &lt;span style=&#34;color:#75715e&#34;&gt;-- 禁忌话题
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    persona_summary TEXT,                   &lt;span style=&#34;color:#75715e&#34;&gt;-- 一句话人设（可自动生成）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    status VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;active&amp;#39;&lt;/span&gt;,    &lt;span style=&#34;color:#75715e&#34;&gt;-- active | disabled | archived
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    current_version INT &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    created_at DATETIME &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at DATETIME &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ON&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;UPDATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;关键设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; 全局唯一，作为 Agent 在群聊中的显示名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traits&lt;/code&gt; / &lt;code&gt;interests&lt;/code&gt; 用 JSON 数组，便于后续做相关性过滤和标签检索；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;persona_summary&lt;/code&gt; 由系统根据 &lt;code&gt;traits&lt;/code&gt; + &lt;code&gt;speaking_style&lt;/code&gt; + &lt;code&gt;interests&lt;/code&gt; 自动生成，用于 &lt;code&gt;should_reply&lt;/code&gt; 等轻量决策；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt; 支持 &lt;code&gt;active&lt;/code&gt; / &lt;code&gt;disabled&lt;/code&gt; / &lt;code&gt;archived&lt;/code&gt;，便于灰度和下线；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current_version&lt;/code&gt; 指向 &lt;code&gt;agent_versions&lt;/code&gt; 表的最新版本，支持人格迭代。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Agent 配置示例&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;阿杰&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;age&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;28&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;job&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;程序员&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;traits&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;幽默&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;直率&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;有点宅&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;interests&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;游戏&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;科幻电影&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;电子产品&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;speaking_style&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;说话直接，喜欢用网络梗，偶尔会吐槽&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;voice_tone&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;轻松调侃&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;system_prompt&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;你是阿杰，28岁程序员。你性格幽默直率，说话喜欢用网络梗。你正在一个群聊里和朋友聊天。请记住：你是 AI，不是真人。不要诱导用户泄露隐私或进行危险行为。&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;default_reply_probability&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.6&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;max_reply_length&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;150&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;forbidden_topics&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;政治&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;暴力&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;色情&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;452-用户画像模型&#34;&gt;4.5.2 用户画像模型&lt;/h4&gt;
&lt;p&gt;用户画像是 Agent“懂用户”的基础，与 Mem0 形成互补：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; user_profiles (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    user_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    summary TEXT,                 &lt;span style=&#34;color:#75715e&#34;&gt;-- 一句话画像（可由 Mem0 导出）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    attributes JSON,              &lt;span style=&#34;color:#75715e&#34;&gt;-- 稳定属性，如 {&amp;#34;city&amp;#34;: &amp;#34;杭州&amp;#34;, &amp;#34;job&amp;#34;: &amp;#34;设计师&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    preferences JSON,             &lt;span style=&#34;color:#75715e&#34;&gt;-- 喜欢/讨厌的话题
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at DATETIME
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt;：由 LLM 定期从 Mem0 记忆中提炼；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attributes&lt;/code&gt;：用户明确告诉系统的稳定属性；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preferences&lt;/code&gt;：喜欢/讨厌的话题，用于过滤和个性化推荐。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户画像会作为动态上下文注入 Agent 的 system prompt：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;关于 {target_user_name} 的重要记忆：
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 他是程序员，工作压力大。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 他喜欢猫，讨厌下雨天。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 他上周提到最近在追《三体》。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;实际效果如下图所示：用户问王姐“你对我的画像是什么”，王姐基于长期记忆中积累的事实，总结出了“好奇心强、有点迷糊、爱玩爱闹、可能搞技术”等画像特征，并用了符合其“王姐”人设的俏皮语气表达。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://ftmao.com/blog/images/ScreenShot_2026-08-04_202545_484.png&#34; alt=&#34;Agent-Chat 中 Agent 基于长期记忆生成用户画像&#34;&gt;&lt;/p&gt;
&lt;h4 id=&#34;453-关系模型&#34;&gt;4.5.3 关系模型&lt;/h4&gt;
&lt;p&gt;关系表维护 Agent-用户、Agent-Agent 之间的亲密度和关系类型：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; relationships (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; (UUID()),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    room_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    source_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,         &lt;span style=&#34;color:#75715e&#34;&gt;-- Agent 或 User ID
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    target_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    relation_type VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;32&lt;/span&gt;),           &lt;span style=&#34;color:#75715e&#34;&gt;-- &amp;#39;friend&amp;#39; | &amp;#39;colleague&amp;#39; | &amp;#39;rival&amp;#39; | &amp;#39;intimate&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    intimacy INT &lt;span style=&#34;color:#66d9ef&#34;&gt;CHECK&lt;/span&gt; (intimacy &lt;span style=&#34;color:#66d9ef&#34;&gt;BETWEEN&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;AND&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    notes TEXT,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at DATETIME &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;关系会影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发言决策&lt;/strong&gt;：关系亲密的 Agent 更可能主动接话；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语气风格&lt;/strong&gt;：对亲密的人更随意，对陌生的人更礼貌；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;话题选择&lt;/strong&gt;：知道用户和 Agent A 吵过架，Agent B 可能会岔开话题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关系文本会注入 prompt：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;你与其他人的关系：
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 与 阿杰：friend，亲密度 80/100。经常一起打游戏。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 与 小明：colleague，亲密度 50/100。普通朋友。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;454-事件模型&#34;&gt;4.5.4 事件模型&lt;/h4&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; events (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; (UUID()),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    room_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    user_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    agent_id CHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;36&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    event_summary TEXT &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    emotion_tag VARCHAR(&lt;span style=&#34;color:#ae81ff&#34;&gt;32&lt;/span&gt;),             &lt;span style=&#34;color:#75715e&#34;&gt;-- happy | sad | angry | excited
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    happened_at DATETIME,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    created_at DATETIME &lt;span style=&#34;color:#66d9ef&#34;&gt;DEFAULT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;事件可以由 Mem0 自动提取后同步写入，也可以由运营/用户手动维护。Agent 在合适时机引用事件，会让陪伴感更强：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;群聊公共记忆：
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 上周大家一起为 {user} 庆祝了生日，氛围很开心。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- {user} 和 阿杰 约定这周末一起联机《黑神话：悟空》。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;46-安全与治理&#34;&gt;4.6 安全与治理&lt;/h3&gt;
&lt;p&gt;陪伴类系统对安全和边界的要求很高，Agent-Chat 从几个层面做了约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人格边界&lt;/strong&gt;：Agent 的 &lt;code&gt;system_prompt&lt;/code&gt; 必须包含“你是 AI，不是真人”的声明；&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;：单个用户/Agent 的消息频率限制，防止刷屏；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐私隔离&lt;/strong&gt;：长期记忆按房间/用户隔离，支持用户删除自己的记忆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计日志&lt;/strong&gt;：所有 LLM 调用记录模型、Prompt、Token 消耗、耗时；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁忌话题&lt;/strong&gt;：每个 Agent 可配置 &lt;code&gt;forbidden_topics&lt;/code&gt;，遇到相关话题主动回避。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五快速上手&#34;&gt;五、快速上手&lt;/h2&gt;
&lt;h3 id=&#34;51-安装依赖&#34;&gt;5.1 安装依赖&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 使用 Poetry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;poetry install
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 或使用 pip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install -r requirements.txt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-配置环境变量&#34;&gt;5.2 配置环境变量&lt;/h3&gt;
&lt;p&gt;复制 &lt;code&gt;.env.example&lt;/code&gt; 为 &lt;code&gt;.env&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cp .env.example .env&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DEEPSEEK_API_KEY&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;your_deepseek_key
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DEEPSEEK_BASE_URL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;https://api.deepseek.com
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DEEPSEEK_MODEL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;deepseek-chat
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;MYSQL_URL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;mysql+aiomysql://root:password@localhost:3306/agentscope_chat?charset&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;utf8mb4
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;REDIS_URL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;redis://localhost:6379/0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;QDRANT_URL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;http://localhost:6333
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EMBEDDING_MODEL_PATH&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;/path/to/bge-small-zh-v1.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EMBEDDING_MODEL_DIMS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;512&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-启动服务&#34;&gt;5.3 启动服务&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 一键启动依赖（MySQL + Redis + Qdrant）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker-compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 运行数据库迁移&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;poetry run alembic upgrade head
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 启动应用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;poetry run uvicorn app.main:app --reload --port &lt;span style=&#34;color:#ae81ff&#34;&gt;8020&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-访问&#34;&gt;5.4 访问&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Web UI：http://localhost:8020/chat&lt;/li&gt;
&lt;li&gt;Admin 后台：http://localhost:8020/admin&lt;/li&gt;
&lt;li&gt;API 文档：http://localhost:8020/docs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六项目结构&#34;&gt;六、项目结构&lt;/h2&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;app/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── main.py              # FastAPI 入口
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── config.py            # 配置管理
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── api/                 # REST API 路由
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── websocket/           # WebSocket 处理
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── agents/              # AgentScope Agent 定义与管理
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   ├── instance.py      # AgentInstance 包装 AgentScope Agent
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   ├── runtime.py       # RoomRuntime / RoomManager
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   ├── scope_models.py  # DeepSeekChatModel 单例
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   ├── tools.py         # FunctionTool 工具集
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   └── group_chat_mem0_middleware.py  # 自定义记忆 Middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── memory/              # 短期/长期记忆服务
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   ├── short_term.py    # Redis 短期记忆
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   └── long_term.py     # Mem0 长期记忆封装
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── models/              # SQLAlchemy 数据模型
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── schemas/             # Pydantic 模型
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── services/            # 业务服务
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└── static/              # Web UI 静态文件&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;七典型应用场景&#34;&gt;七、典型应用场景&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI 陪伴社群&lt;/strong&gt;：运营一批有固定人设的 Agent，让用户感觉像加了一个兴趣群；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;角色扮演游戏&lt;/strong&gt;：每个 Agent 扮演不同 NPC，用户参与剧情；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情感支持/树洞&lt;/strong&gt;：Agent 记住用户情绪历史，提供持续性心理陪伴；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语言练习&lt;/strong&gt;：多个 Agent 分别扮演不同口音/性格的语伴；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产品原型验证&lt;/strong&gt;：快速验证多 Agent 交互体验，再决定是否自研更底层框架。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八写在最后&#34;&gt;八、写在最后&lt;/h2&gt;
&lt;p&gt;Agent-Chat 的初心不是做一个“更聪明的聊天机器人”，而是做一个&lt;strong&gt;有温度、有记忆、有社交感的多 Agent 群聊底座&lt;/strong&gt;。它把 FastAPI 的实时能力、AgentScope 2.0 的 Agent 运行时、DeepSeek 的对话能力、Mem0 的长期记忆能力拼在一起，让开发者可以用配置文件“捏”出一群性格各异的朋友。&lt;/p&gt;
&lt;p&gt;项目还在持续迭代中，欢迎试用、提 issue、一起完善。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/agent-chat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你也在做多 Agent 陪伴系统，欢迎在评论区交流：你觉得“记忆隔离”和“发言调度”哪个更难做好？&lt;/p&gt;
</description>
    </item>
    <item>
      <title>GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生</title>
      <link>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</link>
      <pubDate>Thu, 30 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</guid>
      <description>&lt;h1 id=&#34;genrec-笔记netflix-怎么把推荐排序器做成-llm-原生&#34;&gt;GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;
目标读者：做推荐、搜索、排序的后端工程师，以及关心「LLM 怎么落地进成熟业务系统」的 AI 工程师。
阅读目标：搞清楚 GenRec 的两阶段训练框架、目录感知打分头、prefill-only 推理这三个核心机制，以及它用远少于生产系统的标注数据追平甚至反超的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题推荐栈的复杂度税&#34;&gt;一、问题：推荐栈的复杂度税&lt;/h2&gt;
&lt;p&gt;Netflix 现有生产排序器是多年演化的结果：数千个手工特征、专门的序列建模结构、特征交互结构、多任务目标，支撑电影、剧集、游戏、直播、播客等多种内容类型和多个产品界面。它能跑，但** onboarding 一个新场景（新内容类型、新界面）的成本极高**——特征工程、架构改动、基础设施、实验，一样都少不了。这是典型的复杂度税。&lt;/p&gt;
&lt;p&gt;与此同时，LLM 圈已经在推荐方向有不少探索（原文点名 PLUM、GLIDE、OneRec-Think）。思路很诱人：把用户历史和物品元数据直接表示成文本，靠 LLM 的世界知识和语言理解在共享语义空间里抓关系，甚至用自然语言 prompt 去「引导」推荐。但&lt;strong&gt;现成 LLM 直接上岗是不合格的推荐器&lt;/strong&gt;：偏向全局热门、幻觉出目录外的物品、无视业务约束、个性化弱。GenRec 要回答的问题就是：把 LLM 正经后训练成排序器，能不能打过一套调了多年的生产系统？&lt;/p&gt;
&lt;p&gt;任务设定本身很标准：全目录排序（有候选集时是 top-K）。给定用户、交互历史、当前上下文（设备、界面、地区、时间），输出一个个性化排序，优化目标是&lt;strong&gt;长期会员效用&lt;/strong&gt;（满意度和留存的代理），不是短期点击。&lt;/p&gt;
&lt;h2 id=&#34;二两阶段先适配再排序&#34;&gt;二、两阶段：先适配，再排序&lt;/h2&gt;
&lt;p&gt;GenRec 的训练分两阶段，分工明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 —— Netflix 适配的基座 LLM&lt;/strong&gt;：从开源 LLM 出发，在 Netflix 专有语料上继续训练，让它学会内容理解、会员行为与偏好模式，同时保住通用语言能力。更新频率低，作为多个应用共享的「Netflix-aware」基座。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2 —— GenRec 本体&lt;/strong&gt;：在 Phase-1 基座上用排序数据和排序目标做后训练，专注排序质量与可控性，用奖励加权损失融入多种奖励信号；更新频率高，跟踪新内容和口味漂移；并且从设计上就在推理成本约束下优化。&lt;/li&gt;
&lt;/ul&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
    OSS[&amp;#34;开源 LLM&amp;#34;] --&amp;gt;|&amp;#34;Netflix 专有语料继续训练&amp;lt;br/&amp;gt;低频更新&amp;#34;| P1[&amp;#34;Phase 1&amp;lt;br/&amp;gt;Netflix 适配基座 LLM&amp;lt;br/&amp;gt;多应用共享&amp;#34;]
    LOG[&amp;#34;会员交互日志&amp;lt;br/&amp;gt;播放 时长 评分 弃看 等&amp;#34;] --&amp;gt;|&amp;#34;文本化为对话&amp;lt;br/&amp;gt;user 消息 上下文与历史&amp;lt;br/&amp;gt;assistant 消息 真实行为&amp;#34;| DATA[&amp;#34;Phase 2 训练数据&amp;#34;]
    P1 --&amp;gt; DATA
    DATA --&amp;gt;|&amp;#34;排序目标 + 语言建模目标&amp;lt;br/&amp;gt;+ 奖励加权损失&amp;#34;| P2[&amp;#34;Phase 2&amp;lt;br/&amp;gt;GenRec 排序模型&amp;lt;br/&amp;gt;高频更新 成本约束&amp;#34;]
    RM[&amp;#34;独立奖励模型&amp;lt;br/&amp;gt;长期满意度代理&amp;lt;br/&amp;gt;行为再平衡&amp;#34;] -.-&amp;gt;|&amp;#34;给每条样本一个标量权重&amp;#34;| P2
    style P1 fill:#fff3cd
    style P2 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个两阶段划分是全文我觉得最值得抄的部分：&lt;strong&gt;基座慢迭代、排序快迭代&lt;/strong&gt;。基座沉淀通用能力，排序层追内容新鲜度——后面消融实验会证明，Phase-1 越陈旧，Phase-2 的相对收益越大。&lt;/p&gt;
&lt;h2 id=&#34;三训练数据即对话上下文即特征&#34;&gt;三、训练数据即对话，上下文即特征&lt;/h2&gt;
&lt;p&gt;Netflix 会员每天产生千亿级交互事件。GenRec 把日志转成单轮或多轮「对话」：user 消息是文本化的上下文、画像、历史、物品元数据和任务（比如「预测用户接下来会播放或点赞什么」），assistant 消息是会员的真实行为（播了什么、播了多久、给了什么反馈）。Phase-2 训练时，模型学的是 assistant 消息对 user 消息的条件依赖。&lt;/p&gt;
&lt;p&gt;注意一个容易误读的点：&lt;strong&gt;对话格式只为训练服务&lt;/strong&gt;。推理时不做自回归生成——把文本化的上下文喂进去，直接用打分头给候选物品算分。对话形式存在的原因是配合语言建模目标、保住模型对文本的理解能力。&lt;/p&gt;
&lt;p&gt;文本化的瓶颈是 token 预算。把用户历史里每个事件都摊开写，成本在 Netflix 规模下不可接受。于是上下文窗口成了新的「特征预算」，GenRec 的做法是给上下文做工程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完整保留&lt;/strong&gt;：高信号事件（长播放、点赞），细节给足；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接丢弃&lt;/strong&gt;：低信号事件（极短播放、快速划过）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;摘要压缩&lt;/strong&gt;：重复性行为（连续刷剧这类 binge 行为）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择性展开&lt;/strong&gt;：重要或冷启动物品（比如新上线内容）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;固定预算内优先近期的、高信号的历史，老的压缩或丢掉。prompt 结构上还要最大化共享前缀，吃满 prefix caching。&lt;strong&gt;「prompt 就是新的特征向量」&lt;/strong&gt;——这句话是全文的眼。&lt;/p&gt;
&lt;h2 id=&#34;四三个目标排序语言奖励&#34;&gt;四、三个目标：排序、语言、奖励&lt;/h2&gt;
&lt;p&gt;GenRec 的损失是三件事的组合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目录感知排序目标&lt;/strong&gt;：用高价值行为（足够长的播放、强显式反馈）做正例，带阈值和去噪逻辑；在目录或候选集上做交叉熵，让正例得分更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语言建模目标&lt;/strong&gt;：保留 LM 损失，防止模型在文本化输入上丢掉语言理解能力，也给「推荐理由解释」这类生成场景留门。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权对齐&lt;/strong&gt;：只在原始行为序列上训练，模型会学出坏习惯——过度偏向刷剧、只推单一内容类型。解法是独立奖励模型给每条训练样本算一个标量权重：长期满意度代理（这次短期行为对回访、目录探索、持续活跃有多大贡献）加行为再平衡（跨内容类型、跨上线阶段调节）。高价值样本加权，低价值样本降权。原文特别说明：这比完整 RL 简单省钱，实践中对齐效果够用；GRPO 这类 RL 方法有额外收益但成本高，留给未来工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;五架构与-serving打分头--prefill-only&#34;&gt;五、架构与 serving：打分头 + prefill-only&lt;/h2&gt;
&lt;p&gt;架构贴着基座走：decoder-only Transformer，加一个&lt;strong&gt;目录感知打分头&lt;/strong&gt;，只对 Netflix 在库物品打分。推理管线三步：&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
    V[&amp;#34;Verbalizer&amp;lt;br/&amp;gt;历史 上下文 物品元数据&amp;lt;br/&amp;gt;序列化成单条文本&amp;#34;] --&amp;gt; LLM[&amp;#34;LLM 前向一次&amp;lt;br/&amp;gt;取池化隐状态 h&amp;lt;br/&amp;gt;概括当前偏好与上下文&amp;#34;]
    LLM --&amp;gt; HEAD[&amp;#34;打分头&amp;lt;br/&amp;gt;h 与物品嵌入做点积或小 MLP&amp;lt;br/&amp;gt;softmax 得到分布&amp;#34;]
    CAT[&amp;#34;目录物品嵌入&amp;lt;br/&amp;gt;与主干联合训练&amp;#34;] --&amp;gt; HEAD
    HEAD --&amp;gt; RANK[&amp;#34;排序输出&amp;lt;br/&amp;gt;天然不会幻觉出目录外物品&amp;#34;]
    style HEAD fill:#fff3cd
    style RANK fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;打分头这步设计得很实在：物品嵌入、打分头、主干&lt;strong&gt;联合训练&lt;/strong&gt;；目录极大时用 sampled softmax 或候选集。由于分数只算在目录物品上，「幻觉出目录外内容」这个 LLM 推荐的老毛病在结构上就被掐死了——不需要后处理过滤。&lt;/p&gt;
&lt;p&gt;Serving 在 Netflix 内部 LLM 栈上跑 vLLM。成本由三个因素驱动：模型大小、上下文长度、推理模式。对应三个手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;小模型/蒸馏模型&lt;/strong&gt;：用更小或蒸馏过的基座，配更大更针对性的数据，用更低的成本换大部分质量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激进压缩上下文&lt;/strong&gt;：就是第三节那套上下文工程；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prefill-only 推理&lt;/strong&gt;：不对候选集做自回归解码（那在成本上是自杀），prompt 只消费一次，单次前向给整个候选集打完分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三条是整套方案能上线的关键。一个排序器如果按 LLM 聊天那种方式逐 token 生成推荐结果，账永远算不过来；把 LLM 当成「一次性编码器 + 打分头」，账就算过来了。&lt;/p&gt;
&lt;h2 id=&#34;六实验结果少数据反而赢&#34;&gt;六、实验结果：少数据，反而赢&lt;/h2&gt;
&lt;p&gt;对照组是调了多年的生产排序器（数千个特征、定制架构）。结果分几层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线&lt;/strong&gt;：用大约 40 分之一的 Phase-2 标注样本，MRR 反而高约 1.6%。继续加数据、加输入信号，指标继续涨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线上 A/B&lt;/strong&gt;：批量计算的推荐界面，约 10% 流量跑约 4 周，短期和长期指标均统计显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据与模型 scaling&lt;/strong&gt;：1B 和 10B 主干都随 Phase-2 数据量上涨，大模型绝对值更高、曲线形状相似；固定训练预算下，更大主干稳定更好——推荐也吃 scaling law。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-1 vs 开源&lt;/strong&gt;：用 Netflix 适配基座比直接用现成开源 LLM，离线指标高约 10%-20%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-2 vs Phase-1&lt;/strong&gt;：在 Phase-1 刚训完的时间点，Phase-2 后训练带来 35%-50% 提升；Phase-1 越陈旧（新内容、口味漂移），Phase-2 相对收益越大，2 周后约 80%。这直接论证了两阶段划分的正确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据效率&lt;/strong&gt;：从强 Phase-1 出发，GenRec 用 10-40 倍更少的 Phase-2 标注样本追平或超过生产排序器——这点值钱在 Phase-2 更新频率高，标注效率直接决定迭代速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文压缩&lt;/strong&gt;：清洗事件、找 MRR 对事件数的「肘点」、调措辞详略，三步下来 token 预算可以压到原来的三分之一左右，离线指标几乎不掉；serving 成本约与上下文长度成正比，所以成本也同比降。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七更大的判断从特征工程到上下文工程&#34;&gt;七、更大的判断：从特征工程到上下文工程&lt;/h2&gt;
&lt;p&gt;原文最后一节不是在总结 GenRec，是在押注一个范式转移，四条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;特征工程 → 上下文工程&lt;/strong&gt;：建模范畴从「设计特征」变成「决定哪些信号进上下文、回溯多久、怎么在 token 预算内压缩」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定制架构 → 共享基座&lt;/strong&gt;：双塔、DLRM、定制 attention 各立山头的时代，让位给「一个基座 + 数据与文本化策略 + 后训练目标 + 推理优化」的差异化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling law 成为设计指南&lt;/strong&gt;：传统 RecSys 因为稀疏 ID、重工程、任务定制架构容易撞墙；LLM 化的推荐继承了清晰的数据和模型 scaling 行为，投入产出变得可规划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RecSys 基建 → LLM 基建&lt;/strong&gt;：GPU、vLLM/Triton、batching、caching——推荐 serving 越来越像通用 LLM serving。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「prefill-only」是给 LLM 落地祛魅的一刀。&lt;/strong&gt; 很多团队评估「LLM 进生产」时默认按生成式推理算账，结论当然是不划算。GenRec 的示范是：先问这个任务是不是真的需要逐 token 生成。排序、分类、匹配这类任务，LLM 的正确用法是当编码器，一次前向出全部结果——成本模型完全不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录感知打分头是「防幻觉」的标准答案。&lt;/strong&gt; 不要在生成端防幻觉再靠后处理拦截，把输出空间在结构上约束到合法集合（打分头、受限解码、枚举候选），比训一个「不幻觉的模型」可靠得多。这个模式放之四海皆准：凡是输出必须在闭集内的场景，都该这么干。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权是 RL 的穷人版，但可能够你用很久。&lt;/strong&gt; 完整 RLHF/GRPO 的成本和组织复杂度劝退大多数团队；「独立奖励模型给样本算权重、乘进损失」用监督训练的工程成本拿到对齐收益的大部分。先把这条路走通，再谈要不要上 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文工程是可以量化的工程，不是 prompt 玄学。&lt;/strong&gt; GenRec 把它做成了有肘点曲线、有详略实验、有三分之一预算硬指标的东西。做 RAG 的人应该直接抄这个流程：清洗事件 → 画「指标 vs 上下文量」曲线找肘点 → 在保留集合上调措辞密度。上下文不是越多越好，是有预算的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「prompt 是新的特征向量」意味着你的特征基建要换。&lt;/strong&gt; 特征平台管的是 Schema、版本、回填；上下文工程管的是文本化模板、压缩策略、共享前缀。如果你的推荐/排序团队还在纯特征工程范式里，GenRec 给出的信号是：下一个五年的技能树在后者。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GenRec 是什么&lt;/strong&gt;：Netflix 的 LLM 排序器。文本化用户历史与上下文，在 Netflix 适配基座上做排序后训练，挂目录感知打分头，奖励加权对齐长期价值，prefill-only 推理控成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：两阶段（慢基座 + 快排序）、对话格式训练但非生成式推理、打分头把输出约束在目录内、奖励加权替代完整 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键数字&lt;/strong&gt;：40 倍更少标注样本下 MRR +1.6%；Phase-1 基座 +10%-20%；Phase-2 +35%-50%（基座陈旧 2 周后 +80%）；上下文压到三分之一指标几乎不掉；10% 流量 4 周 A/B 短期长期均显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大的启示&lt;/strong&gt;：推荐的工程重心从特征工程转向上下文工程，「prompt 就是新的特征向量」；LLM 进排序系统的正确姿势是编码器 + 打分头，不是聊天生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保留意见&lt;/strong&gt;：原文没给具体基座模型、参数量细节和线上指标绝对值，成本数字也只有相对比较；「match or exceed 生产系统」的结论目前只在批量计算界面上验证过，实时低延迟场景没提。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：Ying Li, Arjun Rao, Shradha Sehgal, &lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-07-30。&lt;/li&gt;
&lt;li&gt;原文点名的相关 LLM 推荐工作：PLUM、GLIDE、OneRec-Think。&lt;/li&gt;
&lt;li&gt;推理框架：&lt;a href=&#34;https://github.com/vllm-project/vllm&#34;&gt;vLLM&lt;/a&gt;（GenRec serving 所依赖）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 方法：&lt;a href=&#34;https://arxiv.org/abs/2402.03300&#34;&gt;GRPO&lt;/a&gt;（Group Relative Policy Optimization，DeepSeekMath 论文提出）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 自建 LLM 推理平台笔记：四个层层相扣的决策</title>
      <link>https://ftmao.com/blog/posts/netflix-in-house-llm-serving/</link>
      <pubDate>Fri, 17 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-in-house-llm-serving/</guid>
      <description>&lt;h1 id=&#34;netflix-自建-llm-推理平台笔记四个层层相扣的决策&#34;&gt;Netflix 自建 LLM 推理平台笔记：四个层层相扣的决策&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/in-house-llm-serving-at-netflix-a5a8e799ea2c&#34;&gt;In-House LLM Serving at Netflix&lt;/a&gt;（Netflix TechBlog，2026-07，AI Platform 的 Model Runtime 与 Inference 团队）
目标读者：正在或准备自建 LLM 推理服务的工程师，尤其是要在&amp;quot;用托管 API&amp;quot;和&amp;quot;自己跑全栈&amp;quot;之间做取舍的团队。
阅读目标：看清 Netflix 这套平台的四个核心决策（引擎、打包、API 面、发布）各自约束了什么，以及哪些坑是设计阶段想不到、只有生产流量能教会的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;为什么不直接用托管-api&#34;&gt;〇、为什么不直接用托管 API&lt;/h2&gt;
&lt;p&gt;多数组织通过托管 API 消费 LLM，Netflix 选择在既有生产环境内跑全栈——从模型部署到推理，而不是另起一个 ML 孤岛。原文明确说：这篇文章只讲&amp;quot;曾经认真考虑过备选方案&amp;quot;的决策，而且重点是&lt;strong&gt;为什么&lt;/strong&gt;这么选，以及生产暴露了哪些设计阶段没预料到的东西。这个姿态本身就值得学：决策文档的价值不在结论，在被否决的选项和事后才显形的代价。&lt;/p&gt;
&lt;h2 id=&#34;一架构总览llm-不是特例&#34;&gt;一、架构总览：LLM 不是特例&lt;/h2&gt;
&lt;p&gt;Netflix 的会员规模 ML 由一个统一的 JVM serving 系统打头，负责路由、A/B 测试逻辑、候选生成、特征获取、推理、后处理和逐级日志，实时与离线批量缓存两条路径都支持。调用方到达推理有两条路：经 serving 系统的 gRPC 路径，以及新兴 LLM 应用用的直连 HTTP 路径。&lt;/p&gt;
&lt;p&gt;推理在哪跑取决于模型大小：小的 CPU 模型直接在进程内跑，省掉远程调用开销；大模型需要 GPU，serving 系统本地做前后处理，把推理委托给远端服务 &lt;strong&gt;MSS（Model Scoring Service）&lt;/strong&gt;。MSS 是共享推理后端，XGBoost、TensorFlow、PyTorch、LLM 全走同一套接口，底下是 NVIDIA Triton Inference Server 管模型加载、批处理和 GPU 调度。Triton 之上还有一个 Java 控制面，管部署、版本、健康检查、自动扩缩和多 region  rollout——模型作者只管打包产物和配置，控制面负责 provisioning GPU 实例和零停机升级。&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
    subgraph CALLER[&amp;#34;调用方两条路径&amp;#34;]
        GRPC[&amp;#34;gRPC 路径&amp;lt;br/&amp;gt;经 JVM serving 系统&amp;lt;br/&amp;gt;路由 A/B 特征 后处理 日志&amp;#34;]
        HTTP[&amp;#34;HTTP 直连路径&amp;lt;br/&amp;gt;新兴 LLM 应用&amp;#34;]
    end
    subgraph MSS[&amp;#34;MSS 共享推理后端&amp;lt;br/&amp;gt;XGBoost TF PyTorch LLM 统一接口&amp;#34;]
        TRITON[&amp;#34;Triton Inference Server&amp;lt;br/&amp;gt;模型加载 批处理 GPU 调度&amp;#34;]
        CP[&amp;#34;Java 控制面&amp;lt;br/&amp;gt;部署 版本 健康检查 扩缩 多 region&amp;#34;]
        CP --&amp;gt; TRITON
    end
    GRPC --&amp;gt; MSS
    HTTP --&amp;gt; TRITON
    INPROC[&amp;#34;小模型 CPU 进程内推理&amp;#34;] -.-&amp;gt;|&amp;#34;省远程调用开销&amp;#34;| GRPC
    style MSS fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话：&lt;strong&gt;LLM 不做特殊雪花&lt;/strong&gt;。XGBoost 集成和大型 LLM 用同一个 gRPC 调用打分，客户端库、健康检查、部署流水线全部复用。这个&amp;quot;不特殊化&amp;quot;原则后面会反复出现。&lt;/p&gt;
&lt;h2 id=&#34;二决策一引擎从-tensorrt-llm-换到-vllm&#34;&gt;二、决策一：引擎从 TensorRT-LLM 换到 vLLM&lt;/h2&gt;
&lt;p&gt;平台最初建在 TensorRT-LLM 上——当时性能领先，且已和 Triton 集成。到 2025 年夏天两件事变了：开源引擎的性能差距基本抹平；工作负载也从单一形态扩展到 embedding 生成、ranking/retrieval 的 prefill-only 推理、自回归解码、以及带逐步约束逻辑的自定义模型。重新 benchmark 后选 vLLM 作为 paved-path 引擎，理由是&lt;strong&gt;运营契合度&lt;/strong&gt;而非峰值性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义模型架构免多步编译流水线，非标模型迭代更快；&lt;/li&gt;
&lt;li&gt;有自定义解码逻辑的扩展钩子（后面约束解码全靠它）；&lt;/li&gt;
&lt;li&gt;可调试性好——编译型引擎里查失败和中间状态很痛；&lt;/li&gt;
&lt;li&gt;研究侧很多同学本来就在用 vLLM，研究到生产的交接成本低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意选型口径：不是&amp;quot;谁快选谁&amp;quot;，是&amp;quot;谁在我们的负载组合 + 团队现状下总成本最低&amp;quot;。&lt;/p&gt;
&lt;h2 id=&#34;三决策二打包方式选-vllm-backend-但要锁版本&#34;&gt;三、决策二：打包方式，选 vLLM backend 但要锁版本&lt;/h2&gt;
&lt;p&gt;Triton 支持两种打包，区别在&lt;strong&gt;模型产物与前端升级的耦合紧度&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python backend&lt;/strong&gt;：作者在打包时显式定义输入输出 tensor 规格，规格冻结在产物里，必须和前端 request builder 的期望匹配——前端每次动 I/O 规格，打包代码都要配合改，否则运行时才挂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vLLM backend&lt;/strong&gt;：产物只有一个指向权重和 tokenizer 的 JSON 配置，I/O 规格由 Triton 在部署时动态生成。模型和前端各自独立演进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;vLLM backend 是架构上正确的默认，但生产咬了两口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Triton 与 vLLM 版本漂移&lt;/strong&gt;。Triton 的 vLLM backend 是针对特定 vLLM API 面编译的，一漂移就整个加载失败——真实案例是 Triton 25.09 还在 import &lt;code&gt;vllm.engine.metrics&lt;/code&gt;，而这个模块在 vLLM 0.11.2 里已经删了。对策：烘焙镜像时锁死兼容版本对，并禁止模型作者在打包时覆盖 vLLM 版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义模型逻辑&lt;/strong&gt;。vLLM backend 假设标准 HuggingFace 兼容模型并接管全生命周期；需要自定义前后处理、ensemble 流水线、非标 tokenization 的模型，只能退回 Python backend 拿 &lt;code&gt;execute()&lt;/code&gt; 的完全控制权。这个逃生门对一部分模型会长期必要。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;四决策三api-面grpc-不动加一层-openai-兼容前端&#34;&gt;四、决策三：API 面——gRPC 不动，加一层 OpenAI 兼容前端&lt;/h2&gt;
&lt;p&gt;内部打分协议保持统一 gRPC 不变，但 OpenAI 兼容 API 已成 LLM 生态事实标准——推理引擎、编排框架、评估工具、客户端库全说它——所以平台把它作为&lt;strong&gt;附加&lt;/strong&gt;前端暴露。回报体现在实验到生产的路径上：从托管模型毕业到自托管微调模型（为质量、延迟、成本或数据隐私），几乎是平滑迁移，同一套 API，代码改动极小。&lt;/p&gt;
&lt;p&gt;实现上直接复用 NVIDIA 的 Triton OpenAI 兼容前端：内嵌一个 Triton server，用 &lt;code&gt;TritonLLMEngine&lt;/code&gt; 把请求 schema 转成 Triton 推理请求，FastAPI 对外服务；KServe 的 HTTP/gRPC 前端同时开启，Java 控制面仍走 gRPC 访问同一实例。但直接采用暴露了一个缺口：&lt;strong&gt;&lt;code&gt;response_format&lt;/code&gt; 字段 schema 里接受、实际在到达 vLLM 之前被静默丢弃&lt;/strong&gt;——调用方以为自己要了 JSON 输出，实际没有任何 guided decoding 约束，拿到畸形 JSON 也收不到任何报错。Netflix 的解法是 git-subtree 掉这个前端并打补丁，在请求时把 &lt;code&gt;response_format&lt;/code&gt; 翻译成 vLLM 的 guided decoding 参数。静默丢参数比报错危险得多，这是个通用教训。&lt;/p&gt;
&lt;h2 id=&#34;五决策四发布策略red-black-与-versioned&#34;&gt;五、决策四：发布策略——Red-Black 与 Versioned&lt;/h2&gt;
&lt;p&gt;GPU 部署拉起比 CPU 服务慢，模型版本间 I/O schema 还可能变，发布要解决的是&amp;quot;新版本上线不掉请求&amp;quot;。平台给两种策略：&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;新版本要改 I/O schema 吗&amp;lt;br/&amp;gt;例如 tensor 维度变了&amp;#34;] --&amp;gt;|&amp;#34;否 接口稳定&amp;#34;| RB[&amp;#34;Red-Black&amp;lt;br/&amp;gt;新旧并存 健康检查后分阶段切流&amp;lt;br/&amp;gt;新扩容与老缩容同速率 失败原子回滚&amp;#34;]
    Q --&amp;gt;|&amp;#34;是 无法避免&amp;#34;| V[&amp;#34;Versioned&amp;lt;br/&amp;gt;每个 modelId + modelVersion 独立部署&amp;lt;br/&amp;gt;多版本同时在线服务&amp;#34;]
    RB --&amp;gt; RBGAP[&amp;#34;已知缺口&amp;lt;br/&amp;gt;消费者要等新版全量才能改配置&amp;lt;br/&amp;gt;迁移窗口内老请求打到新部署会失败&amp;#34;]
    V --&amp;gt; VCOST[&amp;#34;代价&amp;lt;br/&amp;gt;过渡期 GPU 成本临时上涨&amp;lt;br/&amp;gt;平台按闲置清理旧版本 但永远保留最新&amp;#34;]
    style RB fill:#e8f5e9
    style V fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Red-Black 的协调缺口很微妙：新版本要改 schema 时，上游消费者&lt;strong&gt;没法提前&lt;/strong&gt;改配置（改了会打到还在线上的旧版），只能等新版全量后再切，于是迁移窗口内必然有&amp;quot;老请求打新部署&amp;quot;的失败。Versioned 用冗余部署彻底解耦模型发布与消费者更新。Netflix 的建议是把可变配置（如 tensor shape）直接嵌进推理模型让它版本无关，尽量走便宜的 Red-Black，Versioned 只留给接口变更无法避免的少数情况。&lt;/p&gt;
&lt;h2 id=&#34;六两个运维细节&#34;&gt;六、两个运维细节&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;启动序列&lt;/strong&gt;。直接启动时从 S3 或 Hugging Face 拉大模型太慢，冷启动会超出调度器容忍度；对策是在模型&amp;quot;announce&amp;quot;时就把模型物化到 Amazon FSx，warm start 命中高性能文件系统而非对象存储。另外 OpenAI 兼容前端内嵌 Triton 还是独立跑 Triton，是打包时按部署配置的。其余步骤是机械的：解包、经 Python &lt;code&gt;entry_points&lt;/code&gt; 装自定义 vLLM 插件、清理 Prometheus multiprocess 目录、引擎就绪前闸住 gRPC 端口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统一 metrics 端点&lt;/strong&gt;。vLLM 把指标写成 &lt;code&gt;PROMETHEUS_MULTIPROC_DIR&lt;/code&gt; 下的 &lt;code&gt;.db&lt;/code&gt; 文件，Triton 走自己的 Prometheus 端点，两边互不知情；Triton 内置桥接只暴露 40 多个 vLLM 指标里的 9 个，token 吞吐、KV cache 利用率、prefix cache 命中率这些关键指标全缺。解法是加了一个轻量 HTTP 代理：HTTP 拉 Triton 指标，用 &lt;code&gt;MultiProcessCollector&lt;/code&gt; 从磁盘读 vLLM 指标，合并成单个 &lt;code&gt;/metrics&lt;/code&gt; 输出——存量 dashboard 和告警零改动。&lt;/p&gt;
&lt;h2 id=&#34;七深水区约束解码的规模问题&#34;&gt;七、深水区：约束解码的规模问题&lt;/h2&gt;
&lt;p&gt;这是最硬核的一节。Netflix 一些生产负载需要细粒度控制 token 生成，思路是&lt;strong&gt;把约束推进解码循环内部&lt;/strong&gt;——不为非法生成付钱再重试或修补，而是让模型&amp;quot;构造性地&amp;quot;生成合规输出。实现走 vLLM 的自定义 logits processor 接口：每个约束建模为随已生成 token 历史演进的状态机，每步吐出 token 可用性掩码；每个请求配自己的 processor 实例，因为不同请求规则不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V0 为什么撑不住&lt;/strong&gt;：初版纯 Python 实现功能正确，但 vLLM V0 里自定义 logits processor 是逐请求跑的——GPU 为整个 batch 算出 logits，CPU 拷贝过来等传输完，然后&lt;strong&gt;串行&lt;/strong&gt;对每个请求跑约束逻辑（GIL 挡住了并行）。CPU 耗时随 batch 线性增长，尾延迟爆炸，端到端变成 CPU-bound，尽管 GPU 前向是高效批处理的。这个瓶颈在单请求 benchmark 里完全不可见，只在真实并发下现形。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V1 的结构修复&lt;/strong&gt;（2025 Q4 迁移）：vLLM V1 把 logits 处理提升到批级，processor 改写为对批级数据结构操作、跨请求一起算掩码，热路径用 C++ 多线程重写绕过 GIL。代价是 V1 API 要求经 &lt;code&gt;update_state(batch_update)&lt;/code&gt; 显式跟踪 batch 成员变化——比 V0 的逐请求接口复杂，但动态演化的 batch 里要维持正确状态别无选择。效果是 logits 处理时间随 batch 增长保持平坦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两个设计阶段没想到的状态陷阱&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partial prefill&lt;/strong&gt;：V1 做 chunked prefill，一个请求可能跨多个引擎 step 才填完，而 &lt;code&gt;BatchUpdate&lt;/code&gt; 的粒度分不清&amp;quot;填完了&amp;quot;还是&amp;quot;填了一半&amp;quot;，只能内部自己跟踪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preemption&lt;/strong&gt;：显存压力下 vLLM 可能逐出半完成请求的 KV cache，之后带着不同的 prompt 和输出 token 列表重新调度——这直接打破状态机&amp;quot;输出 token 列表单调增长&amp;quot;的假设。对策是检测 token 历史在解码步之间变短，发现就重置状态机、按新 prompt 重新初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;八工程实践联想&#34;&gt;八、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单请求 benchmark 会撒谎。&lt;/strong&gt; V0 约束解码的瓶颈只在真实并发下出现——GPU 批处理越高效，串行 CPU 段相对越刺眼（阿姆达尔定律的实战版）。凡是有&amp;quot;批 GPU + 逐请求 CPU&amp;quot;形态的系统，压测必须带真实并发模型，否则上线即踩雷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态兼容比自研协议值钱。&lt;/strong&gt; OpenAI 兼容前端让&amp;quot;托管换自托管&amp;quot;几乎零成本，本质是买了个退出期权。但采用第三方组件时要主动审计&lt;strong&gt;静默行为&lt;/strong&gt;——&lt;code&gt;response_format&lt;/code&gt; 那种&amp;quot;schema 接受、执行丢弃&amp;quot;的缺口，只有端到端验证能抓到，类型系统和 schema 校验都救不了你。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁版本不是洁癖，是边界合同。&lt;/strong&gt; Triton 25.09 对 vLLM 0.11.2 的事故说明：两个各自快速迭代的组件之间，API 面就是合同，合同必须显式 pin 住，且要封死用户绕过 pin 的后门（禁止打包时覆盖版本）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口变更的协调成本要摆上台面。&lt;/strong&gt; Red-Black 的&amp;quot;老请求打新部署&amp;quot;缺口是典型的分布式系统两将军问题变体；Versioned 用资源换解耦是标准解法，但更便宜的做法是从源头消灭破坏式变更——把可变配置嵌进模型。设计 schema 时就该问：这个字段未来变了，谁要协调谁？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有状态逻辑要对底层语义变更做回归审计。&lt;/strong&gt; partial prefill 和 preemption 两个坑，根因都是状态机的隐含假设（一次填完、token 单调增长）被引擎新语义打破。引擎升级时不只跑功能测试，要把每条状态假设列出来逐条对账。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Netflix 在既有 JVM serving 体系内自建 LLM 推理全栈，核心原则是 &lt;strong&gt;LLM 不做特例&lt;/strong&gt;：所有模型统一 gRPC 打分，共享 MSS + Triton + Java 控制面。&lt;/li&gt;
&lt;li&gt;四个决策层层相扣：引擎选 vLLM（运营契合度胜过峰值性能）→ 打包选 Triton vLLM backend（I/O 规格部署时动态生成，代价是版本锁死，自定义模型留 Python backend 逃生门）→ API 面加 OpenAI 兼容前端（顺手修了 &lt;code&gt;response_format&lt;/code&gt; 静默丢失）→ 发布双策略 Red-Black / Versioned 应对 schema 变更协调。&lt;/li&gt;
&lt;li&gt;运维两个坑：模型先物化到 FSx 解决冷启动；自建代理合并 Triton 与 vLLM 双 metrics 源（官方桥接只暴露 9/40+ 指标）。&lt;/li&gt;
&lt;li&gt;约束解码是全文精华：约束建模为状态机 + 逐步 token 掩码；V0 逐请求串行被 GIL 卡死、CPU 随 batch 线性增长，V1 批级化 + C++ 热路径后耗时拉平；partial prefill 与 preemption 两个状态陷阱靠内部跟踪和&amp;quot;token 历史缩短即重置&amp;quot;解决。&lt;/li&gt;
&lt;li&gt;后续方向：system prompt 压缩、vLLM V1 异步调度、logits processor 向量化成 GPU 融合算子、低精度模型变体。&lt;/li&gt;
&lt;li&gt;全文最值得带走的元教训：设计评审看不出这些坑，&lt;strong&gt;生产负载是最终的验收测试&lt;/strong&gt;——所以平台要为&amp;quot;发现设计阶段想不到的问题&amp;quot;留好观测口和逃生门。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/in-house-llm-serving-at-netflix-a5a8e799ea2c&#34;&gt;In-House LLM Serving at Netflix&lt;/a&gt;，Netflix TechBlog，2026-07-17（原文）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/vllm-project/vllm&#34;&gt;vLLM&lt;/a&gt;：开源 LLM 推理引擎，本文的 paved-path 选择。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/triton-inference-server/server&#34;&gt;NVIDIA Triton Inference Server&lt;/a&gt;：MSS 底层的模型加载、批处理与 GPU 调度层。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/NVIDIA/TensorRT-LLM&#34;&gt;TensorRT-LLM&lt;/a&gt;：平台初代引擎，被替换前的 baseline。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kserve/kserve&#34;&gt;KServe&lt;/a&gt;：与 OpenAI 前端并置启用的 HTTP/gRPC 前端。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://aws.amazon.com/fsx/&#34;&gt;Amazon FSx&lt;/a&gt;：模型物化的高性能文件系统，用于压低冷启动。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页</title>
      <link>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</link>
      <pubDate>Mon, 29 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</guid>
      <description>&lt;h1 id=&#34;genpage-学习笔记netflix-用一个-transformer-直接生成整个首页&#34;&gt;GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 工程的工程师；也适合想看「LLM 范式怎么落进重工业级在线系统」的人。
阅读目标：搞清楚两件事——GenPage 怎么把「首页构建」改写成「序列生成」问题，以及为了让这个改写扛住生产环境，他们必须解决哪几个工程难题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题的改写从多级流水线到一次生成&#34;&gt;一、问题的改写：从多级流水线到一次生成&lt;/h2&gt;
&lt;p&gt;Netflix 首页不是一个列表，是一个二维结构：哪些行出现、每行里放哪些实体、整体怎么排版，几乎全都个性化。传统做法是多级流水线——行级和实体级各自有召回与排序组件，模型多、阶段之间的优化目标互相对不齐，特征工程沉重。&lt;/p&gt;
&lt;p&gt;GenPage 的改写只有一句话：&lt;strong&gt;把用户历史和请求上下文当作 prompt，自回归生成整个首页作为 response。&lt;/strong&gt; 要回答的问题从「给这个候选打多少分」变成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;已知关于这个用户和这次请求的一切，应该生成什么样的首页来最大化用户满意度？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和 TIGER、HSTU、OneRec 这些只生成扁平排序列表的生成式推荐不同，GenPage 把行、实体、布局一起生成。动机四条：端到端建模（一个 transformer 替代多级堆栈，砍掉阶段间目标错位和大量特征工程）；整页级 RL 优化（捕捉行与实体之间的交互，比如顶部的 Continue Watching 满足即时意图但会压低整页浏览深度）；更好的 scaling 行为；以及对新产品形态（直播、游戏、播客、个性化 UI、逐实体艺术图）的扩展性。&lt;/p&gt;
&lt;p&gt;先说成绩再放细节：线上 A/B 对一个成熟、高度优化的多级生产系统，核心用户参与指标统计显著提升（p ＜ 0.001），同时端到端 serving 延迟降了 20%。&lt;/p&gt;
&lt;h2 id=&#34;二数据自建-tokenizer把首页变成一门语言&#34;&gt;二、数据：自建 tokenizer，把首页变成一门语言&lt;/h2&gt;
&lt;p&gt;从传统推荐迁到生成式 transformer，第一件事是重新想数据表示。GenPage 把上下文和首页都编码成一条离散 token 序列。每个训练样本对应一次首页曝光，由三部分组成：Context（用户行为历史、档案属性、请求上下文）、Page（页面上展示的行和实体，按布局顺序）、Feedback（用户与页面的交互）。只有前两者进入 token 化，Feedback 交给奖励系统产生监督信号。&lt;/p&gt;
&lt;p&gt;他们没有用现成的文本 tokenizer，而是为首页数据建了领域专用 tokenizer，换来两个关键优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算效率&lt;/strong&gt;。举例：「用户 30 天前看了 50 分钟《女子监狱》」这件事，GPT-5 tokenizer 要 16 个 token，他们的方案压到 4 个——&lt;code&gt;[Entity_ID]&lt;/code&gt;、&lt;code&gt;[Action_Type]&lt;/code&gt;、&lt;code&gt;[Action_Time_Bucket]&lt;/code&gt;、&lt;code&gt;[Action_Duration_Bucket]&lt;/code&gt;。序列变短直接降低推理成本和延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产品控制&lt;/strong&gt;。token 和产品概念（行、实体）一一对应，给业务规则的执行留了一个干净的接口（第五节会看到这条多值钱）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上下文 token 里，用户历史表示为动作序列（显式信号如播放、加片单、点赞，隐式信号如预告片观看、详情页访问），档案 token 编码语言与 profile 类型，请求上下文 token 编码时段、星期几、设备。太长的数据源（比如完整曝光历史）用摘要版本——作者坦白这是手工痕迹，本质是把 prompt engineering 塞回了流水线，端到端学压缩是未来方向。各段之间插特殊 token 作分隔，连续信号（时间戳、时长）分桶离散化。&lt;/p&gt;
&lt;p&gt;页面 token 侧：每个实体、每个行各是一个 token，首页按从左到右、从上到下序列化。词表每日更新纳新；服务时仍不在词表里的实体，靠语义 embedding 融合和 fallback token 处理（见第五节）。分页推荐也自然落在范式里：每次翻页请求前，把已生成行的 page token 连同用户在这些行上的最新实时行为追加进 prompt，模型同时看到长期偏好和会话内行为。&lt;/p&gt;
&lt;h2 id=&#34;三奖励系统一个标量两种用法&#34;&gt;三、奖励系统：一个标量，两种用法&lt;/h2&gt;
&lt;p&gt;监督信号来自 Netflix 内部的奖励系统（既有工作，通过线上 A/B 调参对齐长期满意度）：给页面上每个曝光实体打一个标量奖励——通宵刷完的剧比看了 10 分钟的电影奖励高，被曝光后放弃的实体拿负奖励。&lt;strong&gt;页面级奖励定义为页面上所有曝光实体奖励之和。&lt;/strong&gt; 这个标量同时喂给监督后训练和 RL，但用法不同，见下。&lt;/p&gt;
&lt;h2 id=&#34;四模型与训练llm-配方照搬到推荐&#34;&gt;四、模型与训练：LLM 配方照搬到推荐&lt;/h2&gt;
&lt;p&gt;架构是标准的 decoder-only transformer，理由是简单、灵活、能吃整个 transformer 训练与 serving 生态。有一个值得记的细节：&lt;strong&gt;输入 embedding 和输出投影不共享权重（untie）&lt;/strong&gt;——预训练优化的是词表上的 softmax，WBC 后训练优化的是逐 token 的 sigmoid 分数，两种目标对 logits 的要求不同，解绑给模型留出兼顾的余地。&lt;/p&gt;
&lt;p&gt;训练配方照搬 LLM：先预训练教模型「首页这门语言」，再后训练对齐用户满意度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预训练（next-token prediction）&lt;/strong&gt;：只用线上拿到正反馈的曝光样本，让模型 bootstrap 出模仿现有生产系统的能力。作者特意澄清：样本形态其实更像 LLM 的 SFT 对而不是裸文本预训练，叫它预训练是因为从零训练而非微调。局限也直说：预训练只教模仿，不优化奖励幅度；而且模型上线后反复在自己生成的页面上训练，有退化风险——所以要后训练。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 WBC（weighted binary classification）&lt;/strong&gt;：把生成变成 token 级价值预测。自定义 tokenization 在这里再次兑现——每个页面 token 直接对应一个实体或行，奖励可以干净地挂上去：奖励的符号给二分类标签（正参与 vs 放弃），幅度给权重（通宵刷剧权重高于短播放），对对应 token 的 logit 优化加权二分类交叉熵。此时 logit 可解释为「在这个位置生成这个 token 的价值估计」。推理时贪心逐 token 取最高价值。优点是构造上自带 token 级 credit assignment，不用像 RL 那样去推断每个决策对最终页面奖励的贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 RL&lt;/strong&gt;：把页面生成当序列决策，直接优化页面级奖励。借鉴 RLHF 两步走：先训一个 transformer 奖励模型，预测生成页面的页面级奖励（以实体奖励之和为监督目标；注意它和奖励系统是两回事——奖励系统处理真实曝光的反馈，奖励模型不用真把页面给用户看，这让 RL 能对任意候选页面打分）；再用 Dr. GRPO 优化策略，并用 KL 惩罚把策略锚在预训练 checkpoint 附近——奖励模型只在生产策略生成的页面附近可靠，KL 约束既把页面留在奖励模型的覆盖区内，也压制 reward hacking。另加规则化的 format rewards：页面得长得像行的列表，业务关键的行和实体不能排太靠下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RL 相对 WBC 多开三扇门：整页优化（多样性、stopping power、页面级业务约束都能进奖励）；test-time reasoning（推理输出可看作自动化特征工程）；多 token 实体支持——若实体要用多个 token 表示（如 &lt;code&gt;[Show_ID]&lt;/code&gt; 加 &lt;code&gt;[Episode_#]&lt;/code&gt;），WBC 的逐 token 标注会有歧义（一个实体级奖励怎么摊到多个 token 上），RL 优化序列级 return 天然绕开这个问题。&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
    subgraph DATA[&amp;#34;训练样本: 一次首页曝光&amp;#34;]
        CTX[&amp;#34;上下文 token&amp;lt;br/&amp;gt;用户历史 档案 请求上下文&amp;#34;]
        PAGE[&amp;#34;页面 token&amp;lt;br/&amp;gt;行与实体 按布局顺序序列化&amp;#34;]
        FB[&amp;#34;用户反馈&amp;lt;br/&amp;gt;播放 点赞 放弃&amp;#34;]
    end
    CTX --&amp;gt; PRE[&amp;#34;预训练&amp;lt;br/&amp;gt;next-token prediction&amp;lt;br/&amp;gt;只用正反馈样本&amp;#34;]
    PAGE --&amp;gt; PRE
    FB --&amp;gt; RS[&amp;#34;奖励系统&amp;lt;br/&amp;gt;每个曝光实体一个标量奖励&amp;#34;]
    PRE --&amp;gt; WBC[&amp;#34;后训练 WBC&amp;lt;br/&amp;gt;符号作标签 幅度作权重&amp;lt;br/&amp;gt;加权二分类交叉熵&amp;#34;]
    RS --&amp;gt; WBC
    RS --&amp;gt; RM[&amp;#34;奖励模型&amp;lt;br/&amp;gt;预测生成页面的页面级奖励&amp;#34;]
    RM --&amp;gt; RL[&amp;#34;后训练 RL&amp;lt;br/&amp;gt;Dr. GRPO 加 format rewards&amp;#34;]
    PRE --&amp;gt; RL
    RL -.-&amp;gt;|&amp;#34;KL 惩罚锚定预训练 checkpoint&amp;lt;br/&amp;gt;防 reward hacking&amp;#34;| PRE
    style PRE fill:#fff3cd
    style RL 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;。新内容没有交互数据，token embedding 学不出来。两个互补策略：context injection——把新实体或时效实体（如 Live Now 赛事）的元数据直接注入上下文 token；semantic embedding fusion——实体表示改为 ID embedding 与内容 embedding（来自简介、演职员、字幕、类型、视频内容）的融合，训练时以小概率把实体 ID token 随机换成通用 fallback token，逼模型学会只靠内容 embedding 做推荐。效果：新实体的内容元数据一到，它在潜在空间里立刻就有有意义的表示，不等交互数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型新鲜度&lt;/strong&gt;。每天从头重训大 transformer 不现实，但推荐模型必须跟上趋势和新片入库。方案是 multi-cadence incremental training：大周期在全量历史窗口上做完整预训练加后训练；两次大周期之间，每天从昨天的 checkpoint 继续后训练，数据用最新一天加历史采样的混合，防过拟合和灾难性遗忘。每日新进词表的 token 用同类型 fallback token 初始化（如新行用 &lt;code&gt;[Row_Fallback_Token]&lt;/code&gt;），训练时随机把少量已知 token 替换成 fallback，教模型优雅处理未知 token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务规则&lt;/strong&gt;。首页必须满足结构约束（行的列表）和产品逻辑（去重、行钉位置、类目一致性——喜剧行里必须都是喜剧）。训练信号只能鼓励服从，不能保证。所以规则在推理时强制执行：constrained decoding，每个自回归步先算出合规 token 的掩码打到 logits 上。自定义 tokenization 让这件事格外便宜——实体和行都是单 token，业务规则直接映射成 token 级掩码，没有文本词表上多 token 记账的麻烦。钉行就是把那个位置其他 token 全 mask 掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推理延迟&lt;/strong&gt;。逐 token 生成整个页面太贵。利用首页结构做 hybrid row decoding：每行只有前几个实体（用户注意力最集中、决定行质感的位置）自回归生成，然后基于这个前缀做一次前向，拿到所有合格实体的 logits，按分数补齐该行其余位置，同样受业务规则约束。自回归条件性保留在最值钱的地方，长行的逐 token 解码成本省掉了。&lt;/li&gt;
&lt;/ol&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
    REQ[&amp;#34;请求&amp;#34;] --&amp;gt; CTX2[&amp;#34;构造上下文 token&amp;lt;br/&amp;gt;含会话内最新行为&amp;#34;]
    CTX2 --&amp;gt; GEN[&amp;#34;自回归生成&amp;lt;br/&amp;gt;每行只生成前几个实体&amp;#34;]
    GEN --&amp;gt; MASK[&amp;#34;约束解码&amp;lt;br/&amp;gt;业务规则映射为 token 掩码&amp;#34;]
    MASK --&amp;gt; GEN
    GEN --&amp;gt; FILL[&amp;#34;整行补齐&amp;lt;br/&amp;gt;一次前向取全部合格实体&amp;lt;br/&amp;gt;按分数补满该行&amp;#34;]
    FILL --&amp;gt; OUT[&amp;#34;输出首页若干行&amp;#34;]
    OUT -.-&amp;gt;|&amp;#34;翻页时把已生成行和最新行为&amp;lt;br/&amp;gt;追加回 prompt&amp;#34;| CTX2
    style MASK fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;六离线实验真正起作用的是什么&#34;&gt;六、离线实验：真正起作用的是什么&lt;/h2&gt;
&lt;p&gt;消融在 Netflix 内部数据上做，模型约 200M 参数，只报各研究内部的相对比较。四个问题，四个答案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预训练有用吗？&lt;/strong&gt; 有。WBC 前加不加预训练对比，全指标显著提升。Entity AUC 从 0.91 到 0.92 看着小，翻译过来是随机正负实体对的错排率从 9% 降到 8%——成熟生产系统上单点改动罕见的幅度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型规模怎么 scale？&lt;/strong&gt; 从约 120M 扫到约 900M 参数，预训练 loss 和 WBC loss 都呈幂律下降，和 LLM 的 scaling 行为同构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文信息怎么 scale？&lt;/strong&gt; 固定模型规模，持续丰富 prompt（加新数据源加改进 tokenization），WBC loss 大幅下降。两组扫描不严格可比，但差距悬殊：&lt;strong&gt;模型扩约 7.5 倍只降 WBC loss 约 1.3%，上下文富化累计降约 6.9%&lt;/strong&gt;，好几次单次设计良好的上下文加入就超过整轮扩容。结论：当前阶段个性化质量的瓶颈首先是模型能看到的信息和表示，其次才是容量；等上下文榨干了，容量才会成为主驱动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RL 真在做页面级优化吗？&lt;/strong&gt; 页面奖励持续提升（作者承认这条基本是 confirmatory——奖励和策略优化用的是同一个模型）。更有意思的副产品：&lt;strong&gt;多样性根本不在 RL 目标里，但页面多样性（页内实体两两 embedding 距离）随训练上升&lt;/strong&gt;——策略在整页层面优化，而不是孤立地抠每个 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七线上-ab-与意外发现&#34;&gt;七、线上 A/B 与意外发现&lt;/h2&gt;
&lt;p&gt;线上对生产基线的 A/B 里，GenPage 在既有候选集上解码（候选集顺便处理掉大量资格类业务规则）。几个观察都值得记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有训练数据配置不同的变体都在核心指标上显著提升，增益对设计选择鲁棒，不依赖某个特定配置。&lt;/li&gt;
&lt;li&gt;出现了非预期的曝光类目分布漂移（新内容 vs 老内容、剧集 vs 电影）。作者的诊断很诚实：这大概是 GenPage 个性化更锐利的表现——首页 impression 效率上升，用户用更少曝光完成参与；而更锐利的个性化把从生产系统继承来的组件（比如奖励系统）尚未与新范式对齐的问题暴露了出来。&lt;/li&gt;
&lt;li&gt;会话内响应性强且自然：最新行为迅速影响后续推荐，一两天后淡回长期偏好——模型自己在 attend 动作时间戳，生产栈里要靠大量手工特征工程才能做到的事，在生成式范式里涌现出来。&lt;/li&gt;
&lt;li&gt;延迟降 20%，和「生成式一定慢」的直觉相反：多个排序阶段和重特征计算被一个跑在原始 token 上的 transformer 取代，自定义 tokenization 与 hybrid decoding 再压缩了解码步数。作者强调这还没压榨完，余量可以再投给容量或更富的 prompt。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;prompt 工程是新的特征工程，而且 ROI 可能更高。&lt;/strong&gt; 6.9% 对 1.3% 这组数字值得贴在每个推荐团队的墙上：在堆模型容量之前，先问自己信息瓶颈有没有解决——模型看不见的信号，再大的容量也学不到。这和我们日常给 LLM 应用调 prompt 的经验完全同构：多数时候收益不在换更大的模型，在把上下文喂对。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 tokenizer 本质是一次领域建模。&lt;/strong&gt; 4 token 对 16 token 的压缩只是表面收益，真正值钱的是「token 与产品概念一一对应」——它让业务规则从模型输出后的事后修补，变成解码期的一层掩码。凡是需要严格控制生成内容的场景（合规、结构约束、配额），这个思路可以直接搬：把控制点设计进词表，而不是指望 prompt 约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则兜底放在解码时，别指望训练。&lt;/strong&gt; 训练信号只能鼓励、不能保证——这句坦白适用于所有生成式系统。凡是硬约束（不能出现的内容、必须出现的位置），都应该有推理期的机制级保障，RLHF 式的软对齐管软偏好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模仿学习起步、对齐训练接棒，是离线冷启动新系统的通用配方。&lt;/strong&gt; 先用生产系统的正反馈样本 bootstrap，避开没有策略就没有数据的死锁；再承认模仿的天花板（不优化奖励幅度、自蒸馏会退化），用后训练接棒。WBC 和 RL 的二选一也很务实：先用简单可优化的对齐拿下实体级指标，把 RL 留给页面级目标，不上来就啃最硬的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「单模型替代流水线」的延迟账要重新算。&lt;/strong&gt; 直觉上生成式更慢，实际是把 N 个模型加 N 套特征计算换成一次前向序列。系统总成本从来不在单个模型的 FLOPs 上，在组件数量、数据依赖和特征管线上——架构简化的收益经常大于模型本身的开销。&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;：首页构建从「多级召回排序」改写为「prompt 进、页面出」的自回归序列生成，行、实体、布局一次生成；这是与 TIGER、HSTU、OneRec 等扁平列表生成式推荐的分界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据&lt;/strong&gt;：领域专用 tokenizer 把上下文和页面编码成一条 token 序列，换来 4 倍级序列压缩和「token 即产品概念」的控制接口；反馈不进序列，进奖励系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练&lt;/strong&gt;：照搬 LLM 配方——正反馈样本上 next-token 预训练，后训练二选一：WBC（token 级价值预测，自带 credit assignment）或 RL（奖励模型 + Dr. GRPO + KL 锚定，解锁整页优化、test-time reasoning、多 token 实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上线四件套&lt;/strong&gt;：冷启动靠 context injection 加 semantic embedding fusion；新鲜度靠多节奏增量训练加 fallback token；业务规则靠 constrained decoding（规则即 token 掩码）；延迟靠 hybrid row decoding（每行只自回归前几个实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：线上核心指标显著优于成熟多级基线且延迟降 20%；离线两条最值得记——丰富 prompt 的收益远大于模型扩容（6.9% 对 1.3%），RL 在不含多样性目标时反而提升了页面多样性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未竟之事&lt;/strong&gt;：长上下文仍靠手工摘要，语言、多模态、reasoning 能力尚未接入；作者看好的方向是领域 token 与通用文本 token 的混合 tokenization——概念上等于给 LLM 增加一个「推荐模态」，LLM 和推荐系统的边界会继续模糊。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇最打动我的不是架构，是那份诚实：明说预训练样本其实更像 SFT，明说长上下文还是手工摘要，明说奖励提升是 confirmatory，明说线上出现了没优化的分布漂移。工业系统的论文能写到这个坦白程度，比指标本身更值得学。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lequn Wang, Jiangwei Pan, Linas Baltrunas, &lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-06（本文唯一原文来源）。&lt;/li&gt;
&lt;li&gt;原文提及的对照工作：TIGER、HSTU、OneRec（生成扁平排序列表的生成式推荐系统）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 算法：Dr. GRPO（GRPO 的变体，缓解训练目标中的偏差）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Agent Memory 科普：用 Mem0 给 AI 加上长时记忆</title>
      <link>https://ftmao.com/blog/posts/agent-memory-mem0/</link>
      <pubDate>Fri, 26 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/agent-memory-mem0/</guid>
      <description>&lt;h1 id=&#34;agent-memory-科普用-mem0-给-ai-加上长时记忆&#34;&gt;Agent Memory 科普：用 Mem0 给 AI 加上长时记忆&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;做 AI 应用时，经常会遇到一个尴尬：用户上一句还在说「我 gluten-free」，下一句推荐餐厅时模型就忘了。上下文窗口再长也解决不了「跨会话记忆」的问题，这就需要 Agent Memory。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要-agent-memory&#34;&gt;一、为什么需要 Agent Memory？&lt;/h2&gt;
&lt;p&gt;大语言模型有两个和记忆相关的瓶颈：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;上下文窗口有限&lt;/strong&gt;：模型只能看到当前输入的若干 token，超出部分会被截断或遗忘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无法跨会话保留信息&lt;/strong&gt;：每次对话都是独立请求，模型不会记住你上周说过的话。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Agent Memory 就是用来解决这些问题的：它把重要信息从对话中提取出来，持久化存储，在后续交互中按需召回。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二记忆的分类&#34;&gt;二、记忆的分类&lt;/h2&gt;
&lt;p&gt;记忆通常可以分为短期记忆和长期记忆两大类。&lt;/p&gt;
&lt;h3 id=&#34;21-短期记忆&#34;&gt;2.1 短期记忆&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;语义缓存&lt;/strong&gt;：临时把刚刚检索到的外部知识（比如 RAG 召回结果）放在当前上下文里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作记忆&lt;/strong&gt;：当前会话的临时工作区，会话结束后清除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;短期记忆主要依赖上下文窗口，属于「基于会话」的记忆。&lt;/p&gt;
&lt;h3 id=&#34;22-长期记忆&#34;&gt;2.2 长期记忆&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序性记忆（Procedural）&lt;/strong&gt;：工作流、工具调用、技能等执行层面的记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义记忆（Semantic）&lt;/strong&gt;：知识库、个人信息、实体（人物、地点、组织）等事实性记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情景记忆（Episodic）&lt;/strong&gt;：某次对话的摘要、经历、事件细节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长期记忆需要外部存储，并且能被检索和更新。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三agent-memory-项目概览&#34;&gt;三、Agent Memory 项目概览&lt;/h2&gt;
&lt;p&gt;目前社区里有一些比较有代表性的记忆项目：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;项目&lt;/th&gt;
					&lt;th&gt;核心特点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Text2Mem&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;给所有记忆系统定义「操作指令集」，12 个原子操作 + 五元 JSON 契约 + 双层验证&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Mem0&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;GitHub Star 最多的记忆中间件，5 个工厂 / 双存储 / 三种记忆类型&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Letta&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把 OS 虚拟内存思想搬进 Agent，Git 版本化记忆 + Sleeptime 异步后台学习&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ReMe&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;阿里 AgentScope 出品，「文件即记忆」，记忆对用户完全透明可直接编辑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;memU&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;让记忆本身变成一个 24/7 后台主动 Agent&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这篇文章重点介绍 &lt;strong&gt;Mem0&lt;/strong&gt;，因为它是目前工程化落地最方便、生态最好的一个。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四mem0-简介&#34;&gt;四、Mem0 简介&lt;/h2&gt;
&lt;p&gt;Mem0 是一个为大模型应用设计的&lt;strong&gt;长时记忆中间件&lt;/strong&gt;。它把「记忆」这件事从业务代码中抽离出来，变成一个独立、可配置的组件。&lt;/p&gt;
&lt;p&gt;简单理解，Mem0 在 LLM 和向量数据库之间加了一层「记忆管理层」。你只需要调用统一的 &lt;code&gt;Memory&lt;/code&gt; 接口去 &lt;code&gt;add&lt;/code&gt;、&lt;code&gt;search&lt;/code&gt;、&lt;code&gt;get_all&lt;/code&gt;，底层用的是哪家向量库、哪个 Embedding 服务、是否接图数据库，它都帮你封装好了。&lt;/p&gt;
&lt;h3 id=&#34;41-核心架构&#34;&gt;4.1 核心架构&lt;/h3&gt;
&lt;p&gt;Mem0 主要由三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedder（向量化模块）&lt;/strong&gt;：支持 OpenAI、Ollama、HuggingFace、Azure OpenAI、Gemini 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VectorStore（向量存储）&lt;/strong&gt;：支持 Upstash Vector、Qdrant、OpenSearch、PGVector、Milvus、Chroma 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory 层&lt;/strong&gt;：对外暴露统一的 &lt;code&gt;Memory&lt;/code&gt; / &lt;code&gt;AsyncMemory&lt;/code&gt; 类，负责把对话、事件等转换成向量并写入/读出，同时可选接入图数据库和本地 SQLite 历史库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-一个最小-demo&#34;&gt;4.2 一个最小 Demo&lt;/h3&gt;
&lt;p&gt;下面是用 Upstash Vector 做向量库、vLLM 部署大模型和 Embedding 模型的最小示例：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; mem0 &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Memory
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;embedder&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;config&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;api_key&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;EMPTY&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai_base_url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;http://localhost:5001/v1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;custom_embedding&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vector_store&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;upstash_vector&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;config&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;xxx&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;token&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;xxx&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;collection_name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;memory_test&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;enable_embeddings&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;llm&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vllm&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;config&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;qwen25&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vllm_base_url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;http://localhost:5002/v1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;api_key&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;EMPTY&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;memory &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Memory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_config(config)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;conversation_history &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;chat_with_memories&lt;/span&gt;(user_input: str, user_id: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;default_user&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 1. 先检索相关记忆&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    relevant_memories &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; memory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search(query&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user_input, user_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user_id, limit&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    memories_str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;- &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;entry[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;memory&amp;#39;&lt;/span&gt;]&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; entry &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; relevant_memories[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;results&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    system_prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;You are a helpful AI. Answer based on query and memories.&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;User Memories:&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;memories_str&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 调用 LLM 生成回复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: system_prompt}]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;extend(conversation_history)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: user_input})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    assistant_response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; call_llm(messages&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;messages)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    conversation_history&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: user_input})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    conversation_history&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;assistant&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: assistant_response})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 3. 把对话写回记忆库&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    memory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(messages, user_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; assistant_response&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这个 Demo 展示了 Mem0 最核心的用法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;search&lt;/code&gt;：在回复前检索相关记忆。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：把对话写回记忆库，让 LLM 自动抽取事实。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五mem0-的适配逻辑&#34;&gt;五、Mem0 的适配逻辑&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Memory.from_config(config)&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Memory&lt;/span&gt;(MemoryBase):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, config: MemoryConfig &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MemoryConfig()):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; config
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;embedding_model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; EmbedderFactory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;vector_store &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; VectorStoreFactory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;llm &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; LlmFactory&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;db &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SQLiteManager(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;history_db_path)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;51-数据库适配&#34;&gt;5.1 数据库适配&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;VectorStoreFactory&lt;/code&gt; 通过一个 &lt;code&gt;provider_to_class&lt;/code&gt; 映射，把 provider 名称映射到对应的向量库类：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;VectorStoreFactory&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    provider_to_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;qdrant&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.vector_stores.qdrant.Qdrant&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;chroma&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.vector_stores.chroma.ChromaDB&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pgvector&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.vector_stores.pgvector.PGVector&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;upstash_vector&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.vector_stores.upstash_vector.UpstashVector&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;只要实现 &lt;code&gt;VectorStoreBase&lt;/code&gt; 的抽象方法，就可以接入自定义向量库。&lt;/p&gt;
&lt;h3 id=&#34;52-llm-适配&#34;&gt;5.2 LLM 适配&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LlmFactory&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LlmFactory&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    provider_to_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ollama&amp;#34;&lt;/span&gt;: (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.llms.ollama.OllamaLLM&amp;#34;&lt;/span&gt;, OllamaConfig),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai&amp;#34;&lt;/span&gt;: (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.llms.openai.OpenAILLM&amp;#34;&lt;/span&gt;, OpenAIConfig),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vllm&amp;#34;&lt;/span&gt;: (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.llms.vllm.VllmLLM&amp;#34;&lt;/span&gt;, VllmConfig),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;以 &lt;code&gt;VllmConfig&lt;/code&gt; 为例，它的 &lt;code&gt;temperature&lt;/code&gt; 默认是 &lt;strong&gt;0.1&lt;/strong&gt;，&lt;code&gt;top_p&lt;/code&gt; 默认是 &lt;strong&gt;0.1&lt;/strong&gt;，说明 Mem0 官方推荐记忆操作尽量保持确定性，避免随机性导致记忆抽取不一致。&lt;/p&gt;
&lt;h3 id=&#34;53-embedding-适配&#34;&gt;5.3 Embedding 适配&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;EmbedderFactory&lt;/code&gt; 同样用 provider 映射实现：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;EmbedderFactory&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    provider_to_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.embeddings.openai.OpenAIEmbedding&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ollama&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.embeddings.ollama.OllamaEmbedding&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;huggingface&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mem0.embeddings.huggingface.HuggingFaceEmbedding&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;因为 vLLM 兼容 OpenAI 协议，所以部署本地 Embedding 模型时可以直接配置 &lt;code&gt;provider: openai&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六mem0-的核心流程&#34;&gt;六、Mem0 的核心流程&lt;/h2&gt;
&lt;p&gt;Mem0 最重要的两个函数是 &lt;code&gt;search&lt;/code&gt; 和 &lt;code&gt;add&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;61-search检索记忆&#34;&gt;6.1 search：检索记忆&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;search&lt;/code&gt; 的逻辑很直接：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 query 转成 embedding 向量。&lt;/li&gt;
&lt;li&gt;在向量库中按 &lt;code&gt;user_id&lt;/code&gt; / &lt;code&gt;agent_id&lt;/code&gt; / &lt;code&gt;run_id&lt;/code&gt; 过滤后做相似度搜索。&lt;/li&gt;
&lt;li&gt;如果配置了 reranker，对结果做重排序。&lt;/li&gt;
&lt;li&gt;可选启用图存储，同时检索实体/关系。&lt;/li&gt;
&lt;/ol&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_search_vector_store&lt;/span&gt;(self, query, filters, limit, threshold&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    embeddings &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;embedding_model&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;embed(query, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;search&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    memories &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;vector_store&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        query&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;query, vectors&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;embeddings, limit&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;limit, filters&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;filters
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 整理结果，过滤低于 threshold 的项&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-add写入记忆&#34;&gt;6.2 add：写入记忆&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 是 Mem0 最复杂的函数。它会先让 LLM 从对话中抽取事实，再决定每条事实应该新增、更新、删除还是保持不变。&lt;/p&gt;
&lt;h4 id=&#34;第一步事实抽取&#34;&gt;第一步：事实抽取&lt;/h4&gt;
&lt;p&gt;Mem0 会根据是否有 &lt;code&gt;agent_id&lt;/code&gt; 以及消息角色，选择不同的抽取 prompt：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户记忆抽取 prompt&lt;/strong&gt;：只从用户消息中提取事实，比如个人偏好、计划、职业信息等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;助手记忆抽取 prompt&lt;/strong&gt;：只从助手消息中提取事实，比如助手的偏好、能力、性格特征等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回格式是一个 JSON：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;facts&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;姓名是 John&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;正在旧金山找餐厅&amp;#34;&lt;/span&gt;]}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;第二步冲突检测与决策&#34;&gt;第二步：冲突检测与决策&lt;/h4&gt;
&lt;p&gt;对每个新事实，Mem0 会在向量库中搜索相似的已有记忆，然后把「新事实 + 已有记忆」一起交给 LLM，让它判断每条记忆应该执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ADD&lt;/strong&gt;：新增一条记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;：更新已有记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DELETE&lt;/strong&gt;：删除矛盾或过时的记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NONE&lt;/strong&gt;：无需变更。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了避免 LLM 幻觉 UUID，Mem0 会把已有记忆的 UUID 映射成 &lt;code&gt;0, 1, 2...&lt;/code&gt; 这样的临时 ID，LLM 决策后再映射回真实 ID 执行操作。&lt;/p&gt;
&lt;h3 id=&#34;63-流程记忆procedural-memory&#34;&gt;6.3 流程记忆（Procedural Memory）&lt;/h3&gt;
&lt;p&gt;Mem0 还有一种特殊的 &lt;code&gt;procedural_memory&lt;/code&gt;，专门面向多步任务 Agent。它记录的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务目标与当前进度&lt;/li&gt;
&lt;li&gt;按序执行的每一步动作&lt;/li&gt;
&lt;li&gt;每步动作的原始输出&lt;/li&gt;
&lt;li&gt;关键发现、浏览历史、错误与挑战&lt;/li&gt;
&lt;li&gt;当前上下文与下一步计划&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种记忆适合需要跨多轮继续执行的复杂任务，比如自动化网页操作、数据采集等。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七mem0-的优势与适用场景&#34;&gt;七、Mem0 的优势与适用场景&lt;/h2&gt;
&lt;h3 id=&#34;71-优势&#34;&gt;7.1 优势&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接口简单&lt;/strong&gt;：&lt;code&gt;add&lt;/code&gt; / &lt;code&gt;search&lt;/code&gt; / &lt;code&gt;get_all&lt;/code&gt; 就能覆盖大部分需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解耦底层&lt;/strong&gt;：向量库、Embedding、LLM 都可以替换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动抽取与消冲突&lt;/strong&gt;：不需要自己写事实抽取和去重逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程化友好&lt;/strong&gt;：配置驱动，容易接入现有项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-适用场景&#34;&gt;7.2 适用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;多轮对话助手：记住用户偏好、历史需求。&lt;/li&gt;
&lt;li&gt;个性化推荐/客服：结合用户画像做推荐。&lt;/li&gt;
&lt;li&gt;多步任务 Agent：记录任务进度和中间结果。&lt;/li&gt;
&lt;li&gt;知识库 + 用户偏好结合：既检索文档，也记住用户个人情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八与其他记忆项目的关系&#34;&gt;八、与其他记忆项目的关系&lt;/h2&gt;
&lt;p&gt;Agent Memory 是一个正在快速发展的领域，不同项目侧重点不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你需要&lt;strong&gt;快速落地&lt;/strong&gt;一个带记忆的 LLM 应用，&lt;strong&gt;Mem0&lt;/strong&gt; 是目前最稳妥的选择。&lt;/li&gt;
&lt;li&gt;如果你需要&lt;strong&gt;严格的记忆操作语义&lt;/strong&gt;，可以关注 &lt;strong&gt;Text2Mem&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果你需要&lt;strong&gt;版本化和后台学习&lt;/strong&gt;，&lt;strong&gt;Letta&lt;/strong&gt; 更有趣。&lt;/li&gt;
&lt;li&gt;如果你希望&lt;strong&gt;记忆对用户透明可编辑&lt;/strong&gt;，&lt;strong&gt;ReMe&lt;/strong&gt; 是另一种思路。&lt;/li&gt;
&lt;li&gt;如果你希望&lt;strong&gt;记忆本身变成主动 Agent&lt;/strong&gt;，&lt;strong&gt;memU&lt;/strong&gt; 的方向更激进。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;Agent Memory 是 LLM 应用从「一次性问答」进化到「长期陪伴型产品」的关键基础设施。Mem0 把这个能力做成了一个可插拔的中间件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;统一接口&lt;/strong&gt;：&lt;code&gt;add&lt;/code&gt; / &lt;code&gt;search&lt;/code&gt; / &lt;code&gt;get_all&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三层解耦&lt;/strong&gt;：Embedder、VectorStore、LLM 自由组合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 驱动&lt;/strong&gt;：自动抽取事实、检测冲突、决定增删改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三种记忆&lt;/strong&gt;：语义记忆、情景记忆、流程记忆，覆盖不同场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于需要长期记忆的 AI 应用来说，Mem0 是一个值得优先考虑的底座。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：Mem0 官方文档与 GitHub 仓库&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>方糖猫 AI 视频平台：ReAct Agent 任务规划实战</title>
      <link>https://ftmao.com/blog/posts/react/</link>
      <pubDate>Tue, 23 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/react/</guid>
      <description>&lt;h1 id=&#34;方糖猫-ai-视频平台react-agent-任务规划实战&#34;&gt;方糖猫 AI 视频平台：ReAct Agent 任务规划实战&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在方糖猫项目中，我们面对的是这样一个问题：用户输入一段文案，系统需要在几分钟内自动完成素材检索、脚本生成、视频渲染和后期处理，输出一条可发布的短视频。这个链路很长、步骤之间依赖复杂、失败场景多。我们最终选择用 &lt;strong&gt;ReAct Agent&lt;/strong&gt; 来编排整个生成流程。这篇文章会还原当时的设计思路、关键实现和踩过的坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一业务场景为什么不是简单流水线&#34;&gt;一、业务场景：为什么不是简单流水线？&lt;/h2&gt;
&lt;p&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 LR
    A[&amp;#34;文案输入&amp;#34;] --&amp;gt; B[&amp;#34;素材检索&amp;#34;] --&amp;gt; C[&amp;#34;故事板生成&amp;#34;] --&amp;gt; D[&amp;#34;配音/字幕&amp;#34;] --&amp;gt; E[&amp;#34;视频渲染&amp;#34;] --&amp;gt; F[&amp;#34;FFmpeg 后处理&amp;#34;] --&amp;gt; G[&amp;#34;成片&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;最直觉的做法是用一个硬编码的工作流引擎按顺序执行每个步骤。但我们很快发现几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;步骤之间的依赖不是固定的&lt;/strong&gt;。比如某些文案不需要 B-roll，某些素材缺失时需要回退到文字卡片，某些场景需要先合成配音再决定画面节奏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败恢复成本高&lt;/strong&gt;。渲染失败后要重试还是跳过？素材检索为空时是终止还是换关键词重搜？每个决策点都需要上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未来要支持更多视频类型&lt;/strong&gt;。口播、科普、剧情、产品介绍，每种类型的工作流差异很大，硬编码会迅速失控。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们需要一个&lt;strong&gt;能够根据输入动态决策、具备工具调用能力、失败后能自主重试或降级&lt;/strong&gt;的编排机制。ReAct Agent 是其中一个可行方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二react-agent-的核心设计&#34;&gt;二、ReAct Agent 的核心设计&lt;/h2&gt;
&lt;h3 id=&#34;21-任务拆解&#34;&gt;2.1 任务拆解&lt;/h3&gt;
&lt;p&gt;我们把整个生成流程拆成 6 个核心步骤，每个步骤对应一个 Agent 任务：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;任务&lt;/th&gt;
					&lt;th&gt;职责&lt;/th&gt;
					&lt;th&gt;关键输出&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;AnalyzeIntent&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;理解文案意图、视频类型、风格要求&lt;/td&gt;
					&lt;td&gt;意图标签、关键词、情绪基调&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;RetrieveMaterials&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;根据关键词检索本地素材库&lt;/td&gt;
					&lt;td&gt;Top-K 图片/视频/文案片段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;GenerateStoryboard&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;规划每帧画面、配音、视觉元素&lt;/td&gt;
					&lt;td&gt;故事板 JSON&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;SynthesizeSpeech&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;调用 TTS 生成配音&lt;/td&gt;
					&lt;td&gt;音频文件 URL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;RenderVideo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;调用 Remotion 渲染视频&lt;/td&gt;
					&lt;td&gt;视频文件 URL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;PostProcess&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;FFmpeg 后处理：转码、压缩、加字幕&lt;/td&gt;
					&lt;td&gt;成片 URL&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每个任务都是一个 &lt;strong&gt;Tool&lt;/strong&gt;，Agent 通过 Reasoning 决定调用哪个 Tool、以什么参数调用、如何处理返回结果。&lt;/p&gt;
&lt;h3 id=&#34;22-agent-loop-设计&#34;&gt;2.2 Agent Loop 设计&lt;/h3&gt;
&lt;p&gt;我们的 Agent 遵循典型的 ReAct 循环：&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
    O[&amp;#34;观察（Observation）&amp;#34;] --&amp;gt; T[&amp;#34;思考（Thought）&amp;#34;] --&amp;gt; A[&amp;#34;行动（Action）&amp;#34;] --&amp;gt; O&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;具体实现中，我把每次循环抽象为三个动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt;：根据当前状态生成下一步计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Act&lt;/strong&gt;：调用 Tool 执行计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reflect&lt;/strong&gt;：检查 Tool 输出，决定继续、重试还是终止&lt;/li&gt;
&lt;/ol&gt;
&lt;p&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
    Start[&amp;#34;Start&amp;#34;] --&amp;gt; AnalyzeIntent[&amp;#34;AnalyzeIntent&amp;#34;]
    AnalyzeIntent --&amp;gt; RetrieveMaterials[&amp;#34;RetrieveMaterials&amp;#34;]
    RetrieveMaterials -- &amp;#34;素材不足&amp;#34; --&amp;gt; Fallback[&amp;#34;FallbackToTextCards&amp;#34;]
    Fallback --&amp;gt; GenerateStoryboard[&amp;#34;GenerateStoryboard&amp;#34;]
    RetrieveMaterials --&amp;gt; GenerateStoryboard
    GenerateStoryboard --&amp;gt; SynthesizeSpeech[&amp;#34;SynthesizeSpeech&amp;#34;]
    SynthesizeSpeech -- &amp;#34;TTS 失败&amp;#34; --&amp;gt; RetryAudio[&amp;#34;Retry / SkipAudio&amp;#34;]
    SynthesizeSpeech --&amp;gt; RenderVideo[&amp;#34;RenderVideo&amp;#34;]
    RenderVideo -- &amp;#34;渲染失败&amp;#34; --&amp;gt; RetryRender[&amp;#34;Retry / ReduceResolution&amp;#34;]
    RenderVideo --&amp;gt; PostProcess[&amp;#34;PostProcess&amp;#34;]
    PostProcess --&amp;gt; EndNode[&amp;#34;End&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三关键实现&#34;&gt;三、关键实现&lt;/h2&gt;
&lt;h3 id=&#34;31-tool-注册与统一接口&#34;&gt;3.1 Tool 注册与统一接口&lt;/h3&gt;
&lt;p&gt;所有 Tool 都实现统一的 &lt;code&gt;Tool&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; abc &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; ABC, abstractmethod
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; typing &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Any, Dict
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Tool&lt;/span&gt;(ABC):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;description&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(self, params: Dict[str, Any], context: Dict[str, Any]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Dict[str, Any]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        params: LLM 生成的调用参数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        context: 当前任务上下文，包含前面步骤的输出
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;所有 Tool 注册到一个 Registry 中，Agent 根据 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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ToolRegistry&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools: Dict[str, Tool] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;register&lt;/span&gt;(self, tool: Tool):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools[tool&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get&lt;/span&gt;(self, name: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Tool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools[name]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这种设计的好处是：新增一种视频类型或新增一个处理步骤，只需要新增一个 Tool，Agent 的编排逻辑不需要改动。&lt;/p&gt;
&lt;h3 id=&#34;32-agent-loop-核心代码&#34;&gt;3.2 Agent Loop 核心代码&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ReActAgent&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, llm_client, tool_registry, max_steps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;llm &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm_client
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tools &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool_registry
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max_steps &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; max_steps
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(self, user_input: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Dict[str, Any]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_input&amp;#34;&lt;/span&gt;: user_input,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;running&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; step &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max_steps):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 1. 让 LLM 根据当前状态决定下一步&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_build_prompt(context)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            action &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;generate(prompt)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 解析 LLM 输出：Tool 名称 + 参数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tool_name, params &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_parse_action(action)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; tool_name &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Finish&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 3. 执行 Tool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tool &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tools&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(tool_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(params, context)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: str(e)}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 4. 更新上下文&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;step&amp;#34;&lt;/span&gt;: step,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;: tool_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;: params,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;: result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 5. 失败时触发重试或降级&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                fallback &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_decide_fallback(tool_name, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; fallback:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;fallback&amp;#34;&lt;/span&gt;: fallback})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; context&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-prompt-设计&#34;&gt;3.3 Prompt 设计&lt;/h3&gt;
&lt;p&gt;Prompt 的核心不是让 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&gt;你是一名短视频生成助手。当前任务状态如下：

- 用户输入：{user_input}
- 已执行步骤：{history}
- 可用工具：
  1. AnalyzeIntent: 分析文案意图
  2. RetrieveMaterials: 检索素材
  3. GenerateStoryboard: 生成故事板
  4. SynthesizeSpeech: 合成配音
  5. RenderVideo: 渲染视频
  6. PostProcess: 后处理
  7. Finish: 任务完成

请输出下一步操作，格式如下：

Thought: &amp;lt;你的推理过程&amp;gt;
Action: &amp;lt;Tool 名称&amp;gt;
ActionInput: &amp;lt;JSON 格式的参数&amp;gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为了让输出更稳定，我们做了几层约束：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JSON Schema 约束&lt;/strong&gt;：要求 LLM 输出固定字段&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Few-shot 示例&lt;/strong&gt;：在 Prompt 里放 2-3 个正确示例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pydantic 校验 + 重试&lt;/strong&gt;：后端严格校验输出格式，失败自动重试&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四踩过的坑&#34;&gt;四、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;41-llm-输出格式不稳定&#34;&gt;4.1 LLM 输出格式不稳定&lt;/h3&gt;
&lt;p&gt;最早用纯 Prompt 约束时，LLM 有时输出 &lt;code&gt;Action: GenerateStoryboard&lt;/code&gt;，有时输出 &lt;code&gt;下一步：生成故事板&lt;/code&gt;，导致解析失败率很高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入 JSON Schema，明确要求输出 JSON&lt;/li&gt;
&lt;li&gt;后端用 Pydantic 校验，失败自动重试 3 次&lt;/li&gt;
&lt;li&gt;重试时 temperature 从 0.7 降到 0.2，让输出更稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;效果：结构解析成功率从约 75% 提升到 98% 以上。&lt;/p&gt;
&lt;h3 id=&#34;42-任务卡住没有超时机制&#34;&gt;4.2 任务卡住没有超时机制&lt;/h3&gt;
&lt;p&gt;早期 Agent 会无限循环，比如某一步失败后 LLM 反复决定重试同一个 Tool。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 &lt;code&gt;max_steps=15&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每个 Tool 设置独立超时&lt;/li&gt;
&lt;li&gt;同一 Tool 连续失败 2 次后强制降级或人工介入&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;43-上下文过长导致成本飙升&#34;&gt;4.3 上下文过长导致成本飙升&lt;/h3&gt;
&lt;p&gt;随着步骤推进，&lt;code&gt;history&lt;/code&gt; 越来越长，LLM Token 消耗快速增长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 history 做摘要，只保留关键结果&lt;/li&gt;
&lt;li&gt;每个 Tool 输出只保留必要字段&lt;/li&gt;
&lt;li&gt;长视频场景下采用「分段生成 + 本地组装」策略&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五当前效果与待优化方向&#34;&gt;五、当前效果与待优化方向&lt;/h2&gt;
&lt;p&gt;目前这套 Agent 编排已经支撑方糖猫平台的核心生成链路，能够覆盖常见的口播、科普类视频生成场景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已验证的收益&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新增一种视频类型的支持成本明显降低，从改动整个工作流变成新增一个 Tool&lt;/li&gt;
&lt;li&gt;失败恢复路径清晰，常见异常无需人工介入&lt;/li&gt;
&lt;li&gt;渲染失败、素材缺失等场景可以自动降级，任务完成率稳定在较高水平&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;还在探索的方向&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更细粒度的 Agent 评估：不是只看任务是否完成，还要看成片质量&lt;/li&gt;
&lt;li&gt;多 Agent 协作：把创意策划、素材审核、质量评估拆成独立 Agent&lt;/li&gt;
&lt;li&gt;成本优化：不同步骤路由到不同模型，复杂推理用大模型，简单调用用小模型&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六什么场景适合-react什么不适合&#34;&gt;六、什么场景适合 ReAct，什么不适合&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;适合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;步骤之间有复杂依赖，需要动态决策&lt;/li&gt;
&lt;li&gt;失败场景多，需要自动重试或降级&lt;/li&gt;
&lt;li&gt;流程经常变化，硬编码维护成本高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不适合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;步骤固定、成功率高、性能要求极高的场景，传统工作流更合适&lt;/li&gt;
&lt;li&gt;对延迟极度敏感的场景，Agent 的多次 LLM 调用会显著增加耗时&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七总结&#34;&gt;七、总结&lt;/h2&gt;
&lt;p&gt;ReAct Agent 不是银弹，但它确实帮我们解决了一个核心问题：&lt;strong&gt;如何把复杂的 AI 视频生成流程，从「硬编码脚本」变成「可编排、可观测、可恢复」的工程系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tool 接口要统一，新增能力时不要改动 Agent 核心逻辑&lt;/li&gt;
&lt;li&gt;Prompt 输出要做 Schema 约束和后端校验，否则线上会不稳定&lt;/li&gt;
&lt;li&gt;失败处理、超时、降级必须一开始就想清楚&lt;/li&gt;
&lt;li&gt;Agent 的评估指标不能只看「跑通」，要看「生成质量」和「成本」&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后续我会继续分享 RAG 素材库、多 Worker 分布式渲染、以及 Whisper + Remotion 渲染链路的实践，欢迎关注。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 AI Agent 相关的项目，欢迎在评论区交流。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix 的因果推断 Agent 工作流：没有标准答案时，怎么审计一个 AI</title>
      <link>https://ftmao.com/blog/posts/netflix-agentic-workflow-causal-inference/</link>
      <pubDate>Mon, 08 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-agentic-workflow-causal-inference/</guid>
      <description>&lt;h1 id=&#34;netflix-的因果推断-agent-工作流没有标准答案时怎么审计一个-ai&#34;&gt;Netflix 的因果推断 Agent 工作流：没有标准答案时，怎么审计一个 AI&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/a-human-augmenting-agentic-workflow-for-causal-inference-4623f0a9c5af&#34;&gt;A Human-Augmenting Agentic Workflow for Causal Inference&lt;/a&gt;
目标读者：在做 AI Agent 落地的工程师、数据科学家，以及所有想过「这个 agent 给的数到底能不能信」的人。
阅读目标：搞清楚 Netflix 怎么把一个「没有标准答案、错不自知」的分析任务工程化成 agent 工作流，以及这套思路对普通后端和 AI 工程的迁移价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;先拆一个直觉agent-给了答案你敢用吗&#34;&gt;〇、先拆一个直觉：agent 给了答案，你敢用吗&lt;/h2&gt;
&lt;p&gt;先想一个场景：你让一个数据分析 agent 回答「看某部爆款剧对会员长期留存有没有因果效应」。它查了数、跑了回归、自信满满地返回一个数。你该信多少？它有没有处理掉那些隐蔽的偏差——比如把狂热粉丝当成普通观众？没有深厚的方法论功底，你甚至看不出它错没错。&lt;/p&gt;
&lt;p&gt;这就是观测性因果推断（Observational Causal Inference，OCI）的特殊处境。一般的 agent 评估套路是拿输出和 ground truth 程序化对比——但&lt;strong&gt;观测数据里根本没有 ground truth&lt;/strong&gt;。不是数据不够，是这个问题在本质上没有标准答案可比对。Netflix 这篇文章的核心贡献，就是正面回答了「评估对象不存在时怎么办」：&lt;strong&gt;评估不了结果，就审计过程。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;整个工作流立在他们 AI 时代之前就建好的 OCI 工具箱上，指导思想是「目标试验模拟」（target trial emulation）：拿到一个时点型因果问题，先问「回答这个问题的理想 A/B 实验长什么样？」这个实验可能太贵、太慢甚至不可行，但这个思想实验能逼你把假设钉死。&lt;/p&gt;
&lt;h2 id=&#34;一四个设计诊断工具箱里真正的硬货&#34;&gt;一、四个设计诊断：工具箱里真正的硬货&lt;/h2&gt;
&lt;p&gt;为了让「目标试验」这个类比可操作，工具箱内置了四个设计诊断，专门检查处理组和对照组的比较是否公平：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协变量平衡&lt;/strong&gt;：加权之后，处理组和对照组在处理前协变量上的标准化均差应小于 0.2。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重叠性（overlap）&lt;/strong&gt;：接受处理的概率（倾向得分）应该落在 0.1 到 0.9 之间——太接近 0 或 1 的样本，处理与否基本是注定的，不存在「可比的对照」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安慰剂结局&lt;/strong&gt;：在处理发生之前测量的变量上，「处理效应」应该和零没有显著差异。处理还没发生就有效应，说明两组人本来就不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对隐藏混杂的敏感性&lt;/strong&gt;：结论要放在「假设存在一个同时影响处理和结局的遗漏变量」的语境下重新考量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四条没有什么 AI 含量，全是统计学的老手艺。但这正是文章的潜台词：&lt;strong&gt;agent 工作流的可靠性不来自模型更聪明，来自把纪律焊死在流程里。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;二三个角色principalactorcritic&#34;&gt;二、三个角色：Principal、Actor、Critic&lt;/h2&gt;
&lt;p&gt;工作流有三个角色，后两个由软件扮演，跑成一个 actor-critic 循环：&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
    P[&amp;#34;Principal 人类&amp;lt;br/&amp;gt;写初始分析计划&amp;lt;br/&amp;gt;交代混杂因素与业务背景&amp;lt;br/&amp;gt;指定可用工具和数据集&amp;#34;] --&amp;gt; A[&amp;#34;Actor 软件&amp;lt;br/&amp;gt;把计划细化为分析 spec&amp;lt;br/&amp;gt;只用被允许的工具执行&amp;lt;br/&amp;gt;跑核心分析和四个设计诊断&amp;#34;]
    A --&amp;gt; ART[&amp;#34;工件&amp;lt;br/&amp;gt;plan 和 spec&amp;lt;br/&amp;gt;图表 报告 可复现 notebook&amp;#34;]
    A --&amp;gt; C[&amp;#34;Critic 软件&amp;lt;br/&amp;gt;找计划盲区 查计划与执行的一致性&amp;lt;br/&amp;gt;给结果定可信度等级&amp;lt;br/&amp;gt;对照理想 RCT 找差距&amp;#34;]
    C --&amp;gt;|&amp;#34;诊断失败或发现盲区&amp;lt;br/&amp;gt;触发补救或下一轮迭代&amp;#34;| A
    ART --&amp;gt;|&amp;#34;过程审计&amp;lt;br/&amp;gt;人类可下载重跑&amp;#34;| P
    style P fill:#fff3cd
    style ART fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Critic 的职责清单值得细看，它不只是「再看看对不对」：检查 Principal 计划里没提到的混杂因素；核对计划、spec、实际执行三者是否对齐；看完诊断后给结果定可信度等级；明确说明估计量是否偏离了 ATE（比如因为倾向得分裁剪）；把实际分析和理想 RCT 做对照；还要至少给出一种替代测量方案（比如激励式 RCT）。&lt;/p&gt;
&lt;p&gt;人类这边的配套是一个模板化 notebook，底层用经过验证的（非 agent 的）OCI 工具箱，效应估计用 doubly robust learning。agent 产出的报告有版本控制，执行过的 notebook 上传到文件存储，Principal 可以下载下来亲自重跑。&lt;strong&gt;人不用亲自干苦活，但每一步都留了可以让人验算的凭据&lt;/strong&gt;——这就是标题里 &amp;ldquo;human-augmenting&amp;rdquo; 的实际含义：不是替代人，是把人的精力从重复劳动（反复检查协变量平衡、跑敏感性分析、管理迭代版本）挪到只有人能做的事上（框定问题、审视假设、判断结果）。&lt;/p&gt;
&lt;h2 id=&#34;三案例新娱乐类型的留存影响估计值缩水到四分之一&#34;&gt;三、案例：新娱乐类型的留存影响，估计值缩水到四分之一&lt;/h2&gt;
&lt;p&gt;Netflix 近年加了不少视频之外的新娱乐类型，自然要问：这些东西对会员满意度和续订有什么影响？他们拿其中一个类型（文中叫 Type X）做了对照实验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理&lt;/strong&gt;：Type X 的使用天数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结局&lt;/strong&gt;：两个月留存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;潜在混杂&lt;/strong&gt;：包括处理前的 Type X 使用天数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基线&lt;/strong&gt;：把这份分析计划不加任何脚手架直接喂给 Claude Sonnet 4.6。模型选了一个说得过去的策略——用留存对使用天数加控制变量做线性回归——输出干净漂亮。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;paved path（铺好的路）&lt;/strong&gt;：同一份计划、同一个模型，走他们的工具箱加 agentic 工作流，产出的估计值&lt;strong&gt;只有基线的 25%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;差异的根源是&lt;strong&gt;早期采用者偏差&lt;/strong&gt;（early adopter bias）：新功能的第一批用户和一般人群系统性不同——他们可能是 Netflix 重度用户，或者是相关 IP 的死忠粉。这个偏差在数据里表现为重叠性崩塌：绝大多数观测的 Type X 参与概率估值都极小（功能还太新）。critic 在报告里抓住了这个问题，还标记了安慰剂检验失败：早期采用者在&lt;strong&gt;处理发生之前&lt;/strong&gt;测量的重要协变量上就和非采用者有显著差异——两组人本来就不是一路人。&lt;/p&gt;
&lt;p&gt;补救手段来自工作流内置的 playbook：重叠性差就用 Crump 式裁剪，估计因果效应前先把倾向得分落在 0.1 到 0.9 之外的样本裁掉。代价是结论的适用范围缩水——估计的 ATE 只适用于「参与与否不是注定」的那部分人群，critic 必须在报告里把这个注意事项写明。但换来的是可信度的质变：裁剪后的估计只依赖「处理近似随机」的子人群，和目标试验的精神一致；而基线那个数靠的是对低概率人群的外推，外推出来的数字没有扎实数据支撑，还很可能被早期采用者偏差污染。&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
    PLAN[&amp;#34;同一份分析计划&amp;lt;br/&amp;gt;同一个 Sonnet 4.6&amp;#34;] --&amp;gt; BASE[&amp;#34;裸调 one-shot&amp;lt;br/&amp;gt;线性回归 不加诊断&amp;#34;]
    PLAN --&amp;gt; PP[&amp;#34;paved path&amp;lt;br/&amp;gt;脚手架加诊断加 critic&amp;#34;]
    BASE --&amp;gt; B1[&amp;#34;结果干净漂亮&amp;lt;br/&amp;gt;但依赖外推&amp;lt;br/&amp;gt;被早期采用者偏差污染&amp;#34;]
    PP --&amp;gt; D1[&amp;#34;重叠性诊断失败&amp;lt;br/&amp;gt;安慰剂检验失败&amp;#34;]
    D1 --&amp;gt; D2[&amp;#34;Crump 裁剪&amp;lt;br/&amp;gt;只保留倾向得分 0.1 到 0.9 的人群&amp;#34;]
    D2 --&amp;gt; B2[&amp;#34;估计值只有基线的 25%&amp;lt;br/&amp;gt;适用范围缩水但可信&amp;#34;]
    style B1 fill:#ffcccc
    style B2 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个案例最扎心的地方在于：&lt;strong&gt;基线的产出是「defensible」的&lt;/strong&gt;——策略挑不出大毛病，呈现也很专业。如果你不是 OCI 从业者，你大概率就收下了。&lt;/p&gt;
&lt;h2 id=&#34;四编排后续分析agent-真正省力的部分&#34;&gt;四、编排后续分析：agent 真正省力的部分&lt;/h2&gt;
&lt;p&gt;分析做完还有两类自然后续，都是「同一分析跑 N 个变体」的脏活：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;敏感性分析&lt;/strong&gt;：换不同的裁剪阈值重做分析。他们让 agent 把裁剪界从 0 到 1（不裁剪）一路变到 0.15 到 0.85，结论是重叠人群上的 ATE 估计在 0.005 到 0.995 的界内对阈值选择稳健。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间序列&lt;/strong&gt;：把同一分析在十个不同的日期分区上重跑，看效应随时间的演变。这里有个微妙的坑：跨时间比较必须协调各次分析的人群——如果某次为了可信度裁掉了一批用户，其他几次也得裁同一批。跑出来的结果显示季节性：冬季日期的处理效应强于夏季。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两类活的技术含量为零，出错概率不低（参数改一个忘一个、人群没对齐），正是 agent 该吃的苦。&lt;/p&gt;
&lt;h2 id=&#34;五开源与评测critic-真的能区分好坏估计&#34;&gt;五、开源与评测：critic 真的能区分好坏估计&lt;/h2&gt;
&lt;p&gt;Netflix 开源了一个独立版 &lt;code&gt;oci-agent&lt;/code&gt;，附带两个基于 ACIC 2016（大西洋因果推断会议竞赛）公开数据集的评测，以及一个只用开源软件（EconML）的轻量因果机器学习 notebook。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;评测一&lt;/strong&gt;：对 77 个数据生成过程（DGP）各随机抽 3 个数据集，共 231 个估计，让 critic 依据诊断把它们分成 satisfactory 和 unsatisfactory。结果：192 个 satisfactory、39 个 unsatisfactory。对照竞赛里 44 个参赛方法，他们的统计方法本身就有竞争力——RMSE 足够低，95% 置信区间的覆盖率约 95%（校准良好）。但重点在这：&lt;strong&gt;satisfactory 那组的 RMSE 显著更低、区间校准更好，unsatisfactory 那组明显更差&lt;/strong&gt;——诊断套件加 critic 确实能把可信的估计和不可信的估计分开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;评测二&lt;/strong&gt;：同一份分析计划，带脚手架 vs 裸调（one-shot prompting）。随机抽十个 ACIC 数据集，带脚手架的 LLM 复原了九个的真值，且估计与真值高度相关；裸调 Sonnet 4.6 则一致地错，和真值基本不相关。&lt;/p&gt;
&lt;p&gt;作者也坦白了局限：合成数据集压测不了 agent 对真实业务场景的语义理解。但仓库演示了两条核心原则——&lt;strong&gt;用足量脚手架让 agent 按设计遵守最佳实践；要求可检查的工件，让人类能审计 agent 的过程而不只是结果。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;没有 ground truth 的领域，把 eval 拆成「结果评估」和「过程审计」两层。&lt;/strong&gt; 推荐、增长、风控策略、A/B 之外的分析场景，大量任务都没有可比对的标准答案。Netflix 的答案是：结果层用代理指标（ACIC 合成数据上的 RMSE），过程层靠工件留痕（plan、spec、notebook、版本化报告）让人审。我们写 agent 时也该问一句：这个 agent 的产出，三个月后另一个工程师能不能重建它的推理链？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脚手架比模型重要。&lt;/strong&gt; 同一个 Sonnet 4.6，裸调和走 paved path 的估计差了 4 倍——而且错的是看起来更漂亮的那个。「换更强的模型」解决的是能力问题，「模板、诊断、playbook、强制的 critic 环节」解决的是纪律问题。分析类任务里，纪律缺口比能力缺口大得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;actor-critic 结构的关键是 critic 有独立的、可操作的责任清单。&lt;/strong&gt; Netflix 的 critic 不是泛泛地「你再检查一遍」，而是有具体义务：找计划盲区、核对三层一致性、定可信度等级、对照理想 RCT、给替代方案。critic 的职责越具体，漏检越少。这和 code review 清单化是同一个道理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;诊断失败要有 playbook，而不是一句「请注意」。&lt;/strong&gt; 重叠性差对应 Crump 裁剪，裁剪导致估计量偏离 ATE 就要求 critic 在报告里标注适用范围。诊断、补救、注意事项三条线绑在一起，流程才是闭环的。我们系统里的告警如果只报不处理，和人肉运维没区别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把人放在「审」的位置，而不是「干」的位置。&lt;/strong&gt; 人的精力留给框定问题、审视假设、判断结果；重复执行、版本管理、批量变体交给 agent。这个分工原则对任何「专业判断密集 + 重复劳动密集」的工作流都成立，不止因果推断。&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;：观测性因果推断没有 ground truth，agent 给的因果答案无法拿结果对错来评估，错了也很难被发现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法&lt;/strong&gt;：Principal-Actor-Critic 三角色工作流。人类写计划和背景，actor 按模板执行分析并跑四个设计诊断（协变量平衡 SMD ＜ 0.2、倾向得分落在 0.1 到 0.9、安慰剂结局、敏感性分析），critic 有明确清单地挑毛病并定可信度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人的位置&lt;/strong&gt;：human-augmenting 的落点是工件——版本化报告、可下载重跑的 notebook——用过程审计代替结果评估。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例&lt;/strong&gt;：新娱乐类型的留存效应，裸调 Sonnet 4.6 给出漂亮但依赖外推的回归结果；走工作流后诊断抓住早期采用者偏差，Crump 裁剪后估计值只有基线的 25%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评测&lt;/strong&gt;：ACIC 2016 上 231 个估计，critic 判定的 192 个 satisfactory 估计质量显著优于 39 个 unsatisfactory；带脚手架 10 个数据集复原 9 个真值，裸调全军覆没。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私货&lt;/strong&gt;：这篇文章真正的主题不是因果推断，是「如何工程化地使用一个你信不过的聪明组件」——脚手架定纪律、critic 做对抗、工件留凭据、人类审过程。这套四件套放在任何高风险 agent 场景都成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：Winston Chou 等，&lt;a href=&#34;https://netflixtechblog.com/a-human-augmenting-agentic-workflow-for-causal-inference-4623f0a9c5af&#34;&gt;A Human-Augmenting Agentic Workflow for Causal Inference&lt;/a&gt;，Netflix Technology Blog，2026-06-08。&lt;/li&gt;
&lt;li&gt;开源仓库：Netflix 的独立版 &lt;code&gt;oci-agent&lt;/code&gt;（原文有链接，基于 EconML）。&lt;/li&gt;
&lt;li&gt;2016 Atlantic Causal Inference Conference（ACIC）数据分析竞赛数据集（评测所用的 77 个 DGP 来源）。&lt;/li&gt;
&lt;li&gt;Crump 等人的倾向得分裁剪方法（重叠性诊断失败的补救手段，原文引用的 playbook 条目）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>AgentScope 2.0 深度解析：企业级 Agent 工程的底座该怎么选</title>
      <link>https://ftmao.com/blog/posts/agentscope2/</link>
      <pubDate>Wed, 03 Jun 2026 20:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/agentscope2/</guid>
      <description>&lt;h1 id=&#34;agentscope-20-深度解析企业级-agent-工程的底座该怎么选&#34;&gt;AgentScope 2.0 深度解析：企业级 Agent 工程的底座该怎么选&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;最近深入研究了阿里通义实验室开源的 AgentScope 2.0，它是一个面向企业级多智能体场景的工程框架。这篇文章结合我的实际经验，聊聊 AgentScope 2.0 的核心设计，以及它对我们做 Agent 产品化有什么启发。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一从-demo-到生产agent-缺什么&#34;&gt;一、从 Demo 到生产，Agent 缺什么？&lt;/h2&gt;
&lt;p&gt;过去两年，我用 LLM 做过不少 Agent 原型：自动剪辑、视频生成、代码助手。但要从「能跑」变成「能上线」，需要解决的问题远不止 Prompt 调优：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;代码执行安全&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Agent 生成的代码直接运行，可能删库、耗光资源&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;高并发隔离&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;多个用户同时调用 Agent，如何互不干扰&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;状态持久化&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Agent 对话状态不能存在内存里，否则重启就丢&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;工具管理&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Tool 多了之后，Prompt 装不下，也影响模型判断&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;多 Agent 协作&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;复杂任务需要多个 Agent 配合，怎么编排&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;可观测性&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Agent 为什么做了这个决定？出了问题怎么追溯&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AgentScope 2.0 就是围绕这些工程化痛点设计的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二底层架构沙箱与高并发&#34;&gt;二、底层架构：沙箱与高并发&lt;/h2&gt;
&lt;h3 id=&#34;21-docker-沙箱不是打包业务代码&#34;&gt;2.1 Docker 沙箱不是打包业务代码&lt;/h3&gt;
&lt;p&gt;很多开发者有个误解：用 Docker 跑 Agent，就要把业务代码打包进镜像。&lt;/p&gt;
&lt;p&gt;AgentScope 2.0 的设计完全不同：&lt;strong&gt;业务代码运行在宿主机，Docker 只作为执行沙箱&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
    A[&amp;#34;宿主机进程（Agent 推理、Prompt 组装）&amp;#34;]
    B[&amp;#34;Docker 容器（只运行命令，不存业务代码）&amp;#34;]
    C[&amp;#34;容器内执行 rm -rf / 也伤不到宿主机&amp;#34;]
    A -- &amp;#34;通过 Docker API 下发指令&amp;#34; --&amp;gt; B
    B -- &amp;#34;内核级隔离&amp;#34; --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这种设计的几个好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;业务迭代快&lt;/strong&gt;：改代码不用重新打镜像&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性高&lt;/strong&gt;：危险操作被锁在容器里&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源可控&lt;/strong&gt;：每个容器的 CPU/内存/网络都可以限制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-mcp-网关解决跨容器通信&#34;&gt;2.2 MCP 网关解决跨容器通信&lt;/h3&gt;
&lt;p&gt;容器隔离后，容器内的 Agent 访问不到宿主机的 MCP 工具。AgentScope 2.0 的做法是内置 &lt;strong&gt;MCP 网关&lt;/strong&gt;，通过 HTTPS 在容器内外转发请求。&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
    A[&amp;#34;容器内 Agent&amp;#34;] -- &amp;#34;HTTPS&amp;#34; --&amp;gt; B[&amp;#34;MCP 网关&amp;#34;]
    B -- &amp;#34;本地调用&amp;#34; --&amp;gt; C[&amp;#34;宿主机 MCP Server&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这解决了一个很实际的问题：沙箱安全和工具可用性之间的冲突。&lt;/p&gt;
&lt;h3 id=&#34;23-三种隔离粒度&#34;&gt;2.3 三种隔离粒度&lt;/h3&gt;
&lt;p&gt;AgentScope 2.0 支持不同级别的隔离：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;粒度&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;GLOBAL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;全局共享&lt;/td&gt;
					&lt;td&gt;内部工具、只读查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;USER&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用户级隔离&lt;/td&gt;
					&lt;td&gt;推荐默认，兼顾安全和性能&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;SESSION&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;会话级隔离&lt;/td&gt;
					&lt;td&gt;高敏感操作、金融/政务场景&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;不是越隔离越好。全用 SESSION 的话，资源消耗和启动延迟都会爆掉。&lt;/p&gt;
&lt;h3 id=&#34;24-warm-pool-预热池&#34;&gt;2.4 Warm Pool 预热池&lt;/h3&gt;
&lt;p&gt;高并发下，「1 请求 1 容器」的冷启动根本扛不住。AgentScope 2.0 用 Warm Pool 提前准备一批待命容器，请求来了直接分配。&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
    A[&amp;#34;请求&amp;#34;] --&amp;gt; B[&amp;#34;Warm Pool（预热的容器）&amp;#34;]
    B --&amp;gt; C[&amp;#34;秒级分配&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个思路和我们做视频渲染 Worker 预热池很类似：把启动成本前置，用空间换时间。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三核心设计模式&#34;&gt;三、核心设计模式&lt;/h2&gt;
&lt;h3 id=&#34;31-react-与-pe-模式&#34;&gt;3.1 ReAct 与 PE 模式&lt;/h3&gt;
&lt;p&gt;AgentScope 2.0 原生支持两种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ReAct&lt;/strong&gt;：边思考边行动，适合需要动态决策的任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PE（Plan &amp;amp; Execute）&lt;/strong&gt;：先计划再执行，适合步骤明确的长任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我在方糖猫里用的是 ReAct，因为视频生成过程中经常有「素材不足→换方案→继续生成」这种动态分支。PE 更适合审批流、报告生成这类流程相对固定的场景。&lt;/p&gt;
&lt;h3 id=&#34;32-结构化输出&#34;&gt;3.2 结构化输出&lt;/h3&gt;
&lt;p&gt;AgentScope 2.0 通过 &lt;code&gt;structured_model&lt;/code&gt; 机制，把 LLM 输出映射成 Java/Python 对象。&lt;/p&gt;
&lt;p&gt;这和我们在第六篇文章里讲的 Schema + Pydantic 校验是一个思路，只是框架帮你封装好了。对于企业级应用来说，结构化输出不是可选项，是必选项。&lt;/p&gt;
&lt;h3 id=&#34;33-工具组动态激活&#34;&gt;3.3 工具组动态激活&lt;/h3&gt;
&lt;p&gt;大模型上下文窗口有限，Tool 多了之后不能全塞进 Prompt。AgentScope 2.0 的做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把工具分组（Tool Group）&lt;/li&gt;
&lt;li&gt;模型先决定激活哪个组&lt;/li&gt;
&lt;li&gt;再获取组内具体工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种「渐进式披露」让模型在每一步只需要关注少量工具，降低决策噪音。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四分层记忆系统&#34;&gt;四、分层记忆系统&lt;/h2&gt;
&lt;p&gt;AgentScope 2.0 把记忆分成短期和长期两层：&lt;/p&gt;
&lt;h3 id=&#34;41-短期记忆&#34;&gt;4.1 短期记忆&lt;/h3&gt;
&lt;p&gt;默认是内存级缓存，适合单轮或多轮对话内的上下文。可以自定义扩展，比如接入 Redis。&lt;/p&gt;
&lt;h3 id=&#34;42-长期记忆&#34;&gt;4.2 长期记忆&lt;/h3&gt;
&lt;p&gt;支持两种外部记忆框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Graphiti&lt;/strong&gt;：时序知识图谱，适合需要时间感知的场景&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mem0&lt;/strong&gt;：混合存储，同时处理语义关联和结构化关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长期记忆对于 Agent 的个性化和持续学习很重要。比如一个客服 Agent，如果能记住用户上个月的问题，体验会好很多。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五多智能体协作编排&#34;&gt;五、多智能体协作编排&lt;/h2&gt;
&lt;p&gt;AgentScope 2.0 提供了几种协作模式：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;用户对话循环&lt;/td&gt;
					&lt;td&gt;ReActAgent + UserAgent&lt;/td&gt;
					&lt;td&gt;单助手交互&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;轮询 Round-Robin&lt;/td&gt;
					&lt;td&gt;按顺序分配任务&lt;/td&gt;
					&lt;td&gt;流水线处理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;路由 Routing&lt;/td&gt;
					&lt;td&gt;路由 Agent 分发给专家 Agent&lt;/td&gt;
					&lt;td&gt;复杂任务分发&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Handoff&lt;/td&gt;
					&lt;td&gt;确定性的任务交接&lt;/td&gt;
					&lt;td&gt;审批流、工单系统&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我在做口播自动剪辑时，其实也需要多 Agent 协作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 Agent 负责分析文案结构&lt;/li&gt;
&lt;li&gt;一个 Agent 负责规划视觉元素&lt;/li&gt;
&lt;li&gt;一个 Agent 负责检查输出合规性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当时我们是硬编码在代码里的，如果用 AgentScope 2.0 的 Handoff 或 Routing 模式，代码会更清晰。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六状态持久化不能存在内存里&#34;&gt;六、状态持久化：不能存在内存里&lt;/h2&gt;
&lt;p&gt;AgentScope 2.0 通过 &lt;code&gt;agent.state_dict()&lt;/code&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;state &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; agent&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;state_dict()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;store&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;save(session_id, state)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 重启后恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;state &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; store&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;load(session_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;load_state_dict(state)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;支持 Redis、MySQL、JSON 文件等后端。这一点对于企业级应用非常关键：服务重启后，用户的对话上下文不能丢。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七对我们自研-agent-的启发&#34;&gt;七、对我们自研 Agent 的启发&lt;/h2&gt;
&lt;p&gt;研究完 AgentScope 2.0，我最大的感受是：&lt;strong&gt;优秀的 Agent 框架不是在教你写 Prompt，而是在解决工程化问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对比我们自研的 Agent 架构，有几点值得借鉴：&lt;/p&gt;
&lt;h3 id=&#34;71-沙箱执行应该提前考虑&#34;&gt;7.1 沙箱执行应该提前考虑&lt;/h3&gt;
&lt;p&gt;我们目前 Agent 生成的代码是在 Python Worker 里直接执行的，虽然有限制，但不如 Docker 隔离彻底。未来如果要支持更开放的 Agent 能力，沙箱是必须的。&lt;/p&gt;
&lt;h3 id=&#34;72-工具管理需要分组&#34;&gt;7.2 工具管理需要分组&lt;/h3&gt;
&lt;p&gt;我们现在是把所有 Tool 塞进 Prompt，Tool 多了之后会影响模型判断。可以借鉴 Tool Group 的思路。&lt;/p&gt;
&lt;h3 id=&#34;73-状态持久化要补全&#34;&gt;7.3 状态持久化要补全&lt;/h3&gt;
&lt;p&gt;我们目前的任务状态存在数据库，但 Agent 的完整对话上下文没有持久化。服务重启后，长任务会丢失上下文。&lt;/p&gt;
&lt;h3 id=&#34;74-多-agent-协作模式可以显式化&#34;&gt;7.4 多 Agent 协作模式可以显式化&lt;/h3&gt;
&lt;p&gt;与其在代码里写死协作逻辑，不如抽象成 Routing / Handoff 模式，更容易扩展和维护。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八选型建议什么时候用-agentscope-20&#34;&gt;八、选型建议：什么时候用 AgentScope 2.0&lt;/h2&gt;
&lt;h3 id=&#34;强烈推荐&#34;&gt;强烈推荐&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;企业级多租户服务&lt;/li&gt;
&lt;li&gt;需要 K8s 分布式部署&lt;/li&gt;
&lt;li&gt;对安全隔离有严格要求（金融、政务、医疗）&lt;/li&gt;
&lt;li&gt;已有 Spring Boot 微服务架构的团队&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;谨慎评估&#34;&gt;谨慎评估&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;只是做单轮 LLM 调用&lt;/li&gt;
&lt;li&gt;追求极简代码量&lt;/li&gt;
&lt;li&gt;团队没有 Java/Python 工程化背景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AgentScope 2.0 是一个「重框架」，对于简单场景可能是过度设计。但对于要上线、要服务真实用户的企业级 Agent 应用，它的价值很明显。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;AgentScope 2.0 给我的最大启发是：&lt;strong&gt;Agent 产品化的竞争，正在从模型能力转向工程底座&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;谁能更好地解决安全、并发、状态、工具管理、多 Agent 协作这些问题，谁就能让 Agent 真正跑在生产环境里。&lt;/p&gt;
&lt;p&gt;对于像我这样从 0 自研 Agent 系统的团队，AgentScope 2.0 不一定直接替换现有架构，但它提供了一套值得参考的工程化标准。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;你在做 Agent 工程化时，遇到过哪些头疼的问题？欢迎在评论区交流。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix 模型服务路由笔记：把路由器从请求路径上拆掉</title>
      <link>https://ftmao.com/blog/posts/netflix-model-serving-routing/</link>
      <pubDate>Fri, 01 May 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-model-serving-routing/</guid>
      <description>&lt;h1 id=&#34;netflix-模型服务路由笔记把路由器从请求路径上拆掉&#34;&gt;Netflix 模型服务路由笔记：把路由器从请求路径上拆掉&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/state-of-routing-in-model-serving-16e22fe18741&#34;&gt;State of Routing in Model Serving&lt;/a&gt;（Nipun Kumar, Rajat Shah, Peter Chng，Netflix Technology Blog，2026-05-01）
目标读者：做 ML 平台、推理服务、内部网关的工程师；以及所有面对&amp;quot;要不要在请求路径上加一层集中代理&amp;quot;这个问题的人。
阅读目标：搞清楚 Netflix 为什么自建路由层、这层路由怎么和工作流级模型抽象配合、它规模化后踩了哪三个坑，以及 Lightbulb 是怎么在不丢能力的前提下把这层路由从请求路径上拆掉的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先澄清前提netflix-的模型不是模型&#34;&gt;一、先澄清前提：Netflix 的「模型」不是模型&lt;/h2&gt;
&lt;p&gt;这篇文章是一个系列的第一篇，讲 Netflix 中央 ML 模型服务平台的路由设计。截至 2025 年，这个平台承载数百种模型类型和版本，合计每秒一百万次请求。但要理解它的路由，得先理解它路由的对象。&lt;/p&gt;
&lt;p&gt;业界说的 model inference 通常就是 infer(features) 到 score 这一条函数调用。Netflix 的「模型」是另一个东西：&lt;strong&gt;一个自包含的工作流&lt;/strong&gt;，封装了前处理、后处理、特征计算逻辑，外加一个可选的、真正训练出来的 ML 组件，全部打成标准格式。这个工作流的端到端执行才叫 model serving。&lt;/p&gt;
&lt;p&gt;这个定义直接决定了 API 的形状。调用方只需要给两类东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标准请求上下文：userId、国家、设备；&lt;/li&gt;
&lt;li&gt;领域上下文：要排序的候选片单，或者一笔待检测的支付交易。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特征不用客户端算——模型定义里声明了它需要哪些 facts（原始未加工的观测数据），serving 平台负责在运行时调其他微服务把 facts 取来，模型自己完成特征计算和推理。离线训练时则由 fact store 提供快照做批量特征计算。&lt;/p&gt;
&lt;p&gt;这个设计的收益是双向的：客户端一次接入之后，模型架构和数据输入再怎么演化都不需要客户端配合；模型研究员也只需要和平台这一方打交道。标题推荐和支付欺诈检测用的是同一套接入方式——这是后面一切路由抽象的地基。&lt;/p&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;。新用例只做一次性接入，之后几乎所有模型迭代——包括中间的 A/B 实验——对调用方透明。模型选择、实验分流、额外数据拉取、训练日志，全是平台的事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端与模型分片解耦&lt;/strong&gt;。模型分布在多个 serving 集群分片上，每个分片有自己的 VIP 地址。流量模式、SLA、模型架构、CPU 和内存余量都会影响模型到分片的映射，映射一变 VIP 就变。客户端不应该感知这种变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的流量路由规则&lt;/strong&gt;。支持按 A/B 实验路由、缓慢向新模型和新 VIP 切流、客户端覆写。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意第二条：在 Netflix 这个设计里，「模型在哪」是个高频变动的运维事实，而不是部署时定死的静态配置。这决定了路由必须是个动态系统。&lt;/p&gt;
&lt;h2 id=&#34;三switchboard自建的路由层&#34;&gt;三、Switchboard：自建的路由层&lt;/h2&gt;
&lt;p&gt;现成的 API 网关（AWS API Gateway、独立的 Service Mesh 代理）为什么不行？原文给了四个理由：需要和 Netflix 实验平台一等公民集成、需要对客户端暴露 gRPC、需要用富领域上下文做路由定制、需要定制模型生命周期阶段（影子模式、金丝雀、回滚）。通用代理不为此设计，于是自研了 Switchboard——所有流量的弹性代理层，扛一百万 QPS。&lt;/p&gt;
&lt;h3 id=&#34;objective用例的名字&#34;&gt;Objective：用例的名字&lt;/h3&gt;
&lt;p&gt;整个抽象的核心是一个叫 Objective 的概念：&lt;strong&gt;serving 平台定义的一个枚举，每个进系统的请求都必须携带&lt;/strong&gt;。它就是业务用例的名字，比如 ContinueWatchingRanking。它干三件事：让客户端只声明「我要什么」而不说「用哪个模型」；作为路由和模型选择的决策依据；把客户端和具体模型彻底解耦。&lt;/p&gt;
&lt;h3 id=&#34;switchboard-的四项能力&#34;&gt;Switchboard 的四项能力&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;统一客户端抽象&lt;/strong&gt;：所有客户端的模型请求只有一个联络点，没有新依赖、没有新 client 要管；平台侧则拿到跨模型版本的限流和并发控制旋钮，能对付行为恶劣的客户端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文感知路由&lt;/strong&gt;：按设备、地区、排序场景（首页还是搜索结果）、用户所在的 A/B 实验来路由。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态流量切分&lt;/strong&gt;：金丝雀和实验的实时切流，新模型先放给受控的小比例用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本与生命周期管理&lt;/strong&gt;：同一模型多版本并发承接流量，支撑影子模式测试（生产流量发给新模型但不影响用户，只做性能对比）和瞬时回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;switchboard-rules研究员的入口&#34;&gt;Switchboard Rules：研究员的入口&lt;/h3&gt;
&lt;p&gt;Objective 是客户端和平台之间的契约，那研究员怎么把模型变体、实验、流量配比挂到 Objective 上？答案是一份 JavaScript 配置，叫 Switchboard Rules，编译产出一份 JSON 规则文件，规定三件事：某 Objective 的默认模型、一组 Objective 上要开哪些 A/B 实验及对应模型、向新模型渐进切流的定制。&lt;/p&gt;
&lt;p&gt;原文给了一个 Continue Watching 场景的规则示例：定义 A/B 实验 12345，cell 1 走当前线上模型，cell 2 和 cell 3 走两个候选模型，绑定到 ContinueWatchingRanking 这个 Objective。&lt;/p&gt;
&lt;p&gt;规则的分发是这次设计里最值得抄的部分：规则有&lt;strong&gt;独立于代码部署的发布周期&lt;/strong&gt;，走 Netflix 的 Gutenberg 系统做 pub-sub，带版本管理、动态加载和回滚。Switchboard 和 serving 集群主机订阅同一份规则配置。控制面拿到规则后做三件事：产出模型到分片的分配方案、把规则里的模型都加载进分片验证依赖能跑通、把模型到分片 VIP 的映射交给 Switchboard。数据面则是四步：&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
    C[&amp;#34;客户端微服务&amp;lt;br/&amp;gt;携带 Objective 和标准上下文&amp;#34;] --&amp;gt;|&amp;#34;gRPC 请求&amp;#34;| SB[&amp;#34;Switchboard&amp;lt;br/&amp;gt;集中路由层&amp;#34;]
    SB --&amp;gt;|&amp;#34;第一步 分配&amp;lt;br/&amp;gt;查实验平台&amp;lt;br/&amp;gt;userId 在哪个 cell&amp;#34;| EXP[&amp;#34;实验平台&amp;#34;]
    EXP --&amp;gt; SB
    SB --&amp;gt;|&amp;#34;第二步 选模型&amp;lt;br/&amp;gt;按 cell 和 A B 规则&amp;lt;br/&amp;gt;确定模型版本&amp;#34;| SB
    SB --&amp;gt;|&amp;#34;第三步 路由&amp;lt;br/&amp;gt;查模型到 VIP 映射&amp;#34;| SHARD[&amp;#34;Serving 集群分片&amp;lt;br/&amp;gt;对应 VIP&amp;#34;]
    SHARD --&amp;gt;|&amp;#34;第四步 执行&amp;lt;br/&amp;gt;跑完整模型工作流&amp;#34;| RESP[&amp;#34;返回推理结果&amp;#34;]
    RESP --&amp;gt; SB
    SB --&amp;gt; RESP2[&amp;#34;响应给客户端&amp;#34;]
    style SB fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;到这一步故事很顺：30 多个客户端服务靠这层抽象获得了模型迭代的完全自由。然后规模化开始收账。&lt;/p&gt;
&lt;h2 id=&#34;四规模化的三笔账&#34;&gt;四、规模化的三笔账&lt;/h2&gt;
&lt;p&gt;Switchboard 跑大之后，三个问题浮出来，每一个都很典型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单点故障&lt;/strong&gt;。Switchboard 在所有请求的关键路径上。无意的 bug、 noisy neighbor 打过来的超额流量，极端情况下可能切断对所有 serving 主机的访问。它是 30 多个 ML 体验的共享依赖，它倒了大家一起倒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外延迟&lt;/strong&gt;。请求路径上多一跳，序列化加反序列化带来 10-20ms 的延迟（取决于 payload 大小），还放大了尾延迟。对延迟敏感的客户端，这直接表现为服务超时、终端用户受影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端灵活性受损&lt;/strong&gt;。Switchboard 挡在中间，serving 集群看不到请求的真实来源。区分真实流量和人为测试流量——这对模型训练数据至关重要——变得困难，需要持续的定制开发，MLOps 开销一直涨。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三条值得停一下。集中代理的一个隐性代价是&lt;strong&gt;信息损耗&lt;/strong&gt;：代理层为了抽象抹掉了来源信息，而下游恰恰需要这个信息做租户隔离和训练数据清洗。抽象不是免费的，它吃掉的信息总得有地方补回来。&lt;/p&gt;
&lt;h2 id=&#34;五lightbulb拆路由而不是删路由&#34;&gt;五、Lightbulb：拆路由，而不是删路由&lt;/h2&gt;
&lt;p&gt;团队的结论不是推翻 Switchboard，而是&lt;strong&gt;重新摆放它职责的执行位置&lt;/strong&gt;。要保留的能力列得很清楚：统一客户端抽象、分片解耦、灵活路由规则、轻量客户端、单一配置入口、配置快速传播、故障时的 fallback 和客户端缓存。要解决的是三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;把路由服务从直接请求路径上移走&lt;/strong&gt;。路由规则变化频率很低，为它维持强一致性的可用性代价不划算；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把模型输入和请求元数据分开&lt;/strong&gt;。payload 可能很大，Switchboard 为了做路由决策要把整个 payload 反序列化再序列化，这是延迟和成本的大头；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给路由层更好的隔离&lt;/strong&gt;。多租户挤在一个路由集群里，一个租户的异常流量会级联影响其他租户，而不同用例的延迟要求差异又很大。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;做法是把原来一整份规则拆成两套配置：&lt;strong&gt;模型服务配置&lt;/strong&gt;（请求时该用哪个模型及所需元数据）和&lt;strong&gt;路由规则&lt;/strong&gt;（定了模型之后，请求该去哪个 VIP）。数据面也跟着拆开——路由本身交给 Envoy 做：&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
    C[&amp;#34;客户端服务&amp;#34;] --&amp;gt;|&amp;#34;最小请求上下文&amp;#34;| LB[&amp;#34;Lightbulb&amp;lt;br/&amp;gt;只做配置解析&amp;#34;]
    LB --&amp;gt;|&amp;#34;返回 routingKey&amp;lt;br/&amp;gt;和 ObjectiveConfig&amp;#34;| C
    C --&amp;gt;|&amp;#34;完整请求&amp;lt;br/&amp;gt;头部带 routingKey&amp;lt;br/&amp;gt;body 带 ObjectiveConfig&amp;#34;| ENVOY[&amp;#34;Envoy 代理&amp;lt;br/&amp;gt;客户端侧出口&amp;#34;]
    ENVOY --&amp;gt;|&amp;#34;按 routingKey 映射到 VIP&amp;lt;br/&amp;gt;头部判断 开销极小&amp;#34;| SHARD[&amp;#34;Serving 集群分片&amp;#34;]
    SHARD --&amp;gt; ENVOY
    ENVOY --&amp;gt; C
    style LB fill:#e8f5e9
    style ENVOY fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键设计决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Envoy 在 Netflix 本来就把所有应用间出口流量都接管了，它能按控制面发布的路由规则把请求发到不同集群（VIP），但它没有路由决策所需的信息，也不会给请求体补充 A/B 变体所需的 serving 参数。Lightbulb 就是补这个缺口的。&lt;/li&gt;
&lt;li&gt;Lightbulb 只吃最小的请求上下文（用例信息），解析出两样东西：routingKey 和 ObjectiveConfig。前者是给 Envoy 的路由元数据，后者放模型 id 和模型执行所需的其他请求级配置。&lt;strong&gt;把配置解析和「怎么到达推理集群」彻底分开了&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;routingKey 放请求头给 Envoy 消费；ObjectiveConfig 由客户端放进请求体。刻意不把所有东西都塞进头部，避免头部膨胀。&lt;/li&gt;
&lt;li&gt;真正的路由由 Envoy 执行：routingKey 在头部，映射到 VIP 的判定开销极小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Switchboard 时代的核心收益——单一接入点、模型 id 对用例的抽象、上下文感知路由——全部保留；单点和额外跳数没了。&lt;/p&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;。加一层集中代理时，大家习惯算延迟账，很少有人算信息账。Switchboard 的第三个坑就是信息账：来源身份被抽象抹掉，训练数据分不清真假流量。设计代理层时，把「下游需要但代理会吞掉的字段」列出来，和延迟预算一起评审。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置和代码分开发布，是实验平台的生命线&lt;/strong&gt;。Switchboard Rules 走 Gutenberg pub-sub、有独立发布周期、能动态加载能回滚——模型实验的迭代速度取决于配置的流转速度，不取决于服务的部署速度。凡是「业务方天天要改、改了就要生效」的东西，都不应该长在代码发布列车里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制面集中、数据面分散，是规模化的标准答案&lt;/strong&gt;。Lightbulb 的本质是把「做决策」（低频、需要一致性、可以集中）和「执行决策」（高频、要求低延迟、必须分散）拆开。Envoy 执行、Lightbulb 决策，各归各位。同一个套路在服务网格、CDN、feature flag 系统里反复出现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低频变化的路径上不要维持强一致&lt;/strong&gt;。路由规则变化频率很低，为它维护一个高可用的在线路由服务是过度支付——推到客户端侧缓存，故障时还能 fallback。「这个信息多久变一次」应该直接决定它放在请求路径上还是路径外。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给模型起「用例名」而不是暴露「模型名」&lt;/strong&gt;。Objective 这个枚举是整个抽象的关键：客户端说的是意图，平台负责兑现。这个套路在任何平台工程里都成立——暴露稳定的能力枚举，藏住易变的实现标识，实现侧的迭代自由度就是这么来的。&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 的「模型」是自包含工作流（前处理、特征计算、可选的 ML 组件、后处理），serving 而非 inference 是抽象单位；客户端只给标准上下文加领域上下文，facts 由平台在运行时取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第一版架构&lt;/strong&gt;：Switchboard 集中路由层，配 Objective 枚举（用例名）和 Switchboard Rules（JS 配置编译成 JSON，Gutenberg pub-sub 分发，独立于代码部署）；提供统一接入、上下文路由、动态切流、影子模式和瞬时回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规模化的三笔账&lt;/strong&gt;：单点故障、10-20ms 额外延迟加尾延迟放大、请求来源信息被代理吞掉导致租户隔离和训练数据清洗困难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二版架构&lt;/strong&gt;：Lightbulb 把模型选择从路由里拆出来——Lightbulb 解析最小上下文产出 routingKey（进头部）加 ObjectiveConfig（进请求体），Envoy 在客户端侧按 routingKey 路由到 VIP；请求路径上不再有集中路由器，能力一项没丢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元教训&lt;/strong&gt;：集中式代理解决抽象问题，规模化之后要把决策和执行拆开——决策可以集中（低频、要一致性），执行必须分散（高频、要低延迟）。&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/state-of-routing-in-model-serving-16e22fe18741&#34;&gt;State of Routing in Model Serving&lt;/a&gt;，Netflix Technology Blog，2026-05-01（本系列第一篇，后续会讲推理与特征拉取）。&lt;/li&gt;
&lt;li&gt;原文引用的内部系统：Netflix 实验平台（A/B 分配）、Gutenberg（配置 pub-sub 与版本管理）、ML fact store（离线训练特征快照）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.envoyproxy.io/&#34;&gt;Envoy Proxy&lt;/a&gt;（Lightbulb 架构中承担实际路由的客户端侧代理）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Spring Boot &#43; Python 双栈架构：AI 服务的工程化实践</title>
      <link>https://ftmao.com/blog/posts/java2python/</link>
      <pubDate>Mon, 20 Apr 2026 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/java2python/</guid>
      <description>&lt;h1 id=&#34;spring-boot--python-双栈架构ai-服务的工程化实践&#34;&gt;Spring Boot + Python 双栈架构：AI 服务的工程化实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的方糖猫 AI 视频平台不是一个纯 Python 项目，也不是一个纯 Java 项目。它同时使用了 Spring Boot 3 和 Python 3.11：Java 负责业务系统和 API 服务，Python 负责 AI 推理、渲染和多媒体处理。这篇文章会分享我们为什么要这样做，以及如何解决两个技术栈之间的协作问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么必须双栈&#34;&gt;一、为什么必须双栈？&lt;/h2&gt;
&lt;p&gt;做一个 AI 视频生成平台，需要两类能力：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力类型&lt;/th&gt;
					&lt;th&gt;典型需求&lt;/th&gt;
					&lt;th&gt;更适合的栈&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;企业级业务系统&lt;/td&gt;
					&lt;td&gt;用户、订单、权限、支付、任务管理&lt;/td&gt;
					&lt;td&gt;Java / Spring Boot&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AI 推理与多媒体&lt;/td&gt;
					&lt;td&gt;LLM 调用、Whisper、RAG、Remotion、FFmpeg&lt;/td&gt;
					&lt;td&gt;Python&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Java 的优势在于生态成熟、工程规范、团队熟悉；Python 的优势在于 AI 库丰富、模型推理和多媒体处理工具链完善。&lt;/p&gt;
&lt;p&gt;如果全用 Java，AI 库支持差，很多模型没有 Java SDK；如果全用 Python，业务系统的稳定性、事务处理、监控运维又很难做好。&lt;/p&gt;
&lt;p&gt;所以我们的选择是：&lt;strong&gt;Java 做业务底盘，Python 做 AI 引擎，两者通过清晰的接口协作&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二整体架构&#34;&gt;二、整体架构&lt;/h2&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
    FE[&amp;#34;前端 (Vue 3)&amp;#34;]
    GW[&amp;#34;API 网关 / BFF (Spring Boot)&amp;lt;br/&amp;gt;用户认证、请求路由、限流、任务状态查询、SSE 推送&amp;#34;]
    BIZ[&amp;#34;业务服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    SCHED[&amp;#34;任务调度服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    RES[&amp;#34;资源管理服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    AI[&amp;#34;AI Worker (Python)&amp;lt;br/&amp;gt;ReAct Agent、RAG 检索、Whisper、TTS、Remotion、FFmpeg&amp;#34;]
    STORE[&amp;#34;存储层：MySQL / Redis / OSS / ChromaDB&amp;#34;]

    FE --&amp;gt; GW
    GW --&amp;gt; BIZ
    GW --&amp;gt; SCHED
    GW --&amp;gt; RES
    SCHED -- &amp;#34;RocketMQ / Kafka&amp;#34; --&amp;gt; AI
    AI --&amp;gt; STORE&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三职责划分&#34;&gt;三、职责划分&lt;/h2&gt;
&lt;h3 id=&#34;31-java-侧职责&#34;&gt;3.1 Java 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 网关&lt;/strong&gt;：统一入口、鉴权、限流、日志&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户与任务管理&lt;/strong&gt;：用户注册登录、任务创建、任务状态机、历史记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;素材元数据管理&lt;/strong&gt;：素材上传、分类、标签、权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付与计费&lt;/strong&gt;（如有）：订单、套餐、用量统计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务调度&lt;/strong&gt;：把渲染/生成任务发送到消息队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时通知&lt;/strong&gt;：SSE 推送任务进度给用户&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-python-侧职责&#34;&gt;3.2 Python 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI Agent 编排&lt;/strong&gt;：ReAct 任务规划、Tool 调用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG 检索&lt;/strong&gt;：Embedding 计算、向量库查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音识别&lt;/strong&gt;：Whisper 转写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音合成&lt;/strong&gt;：TTS 配音&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频渲染&lt;/strong&gt;：Remotion 组件化渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后处理&lt;/strong&gt;：FFmpeg 转码、压缩、字幕、多分辨率输出&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-边界原则&#34;&gt;3.3 边界原则&lt;/h3&gt;
&lt;p&gt;我们定了几条原则来避免职责混乱：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Python 不直接暴露给用户&lt;/strong&gt;。所有用户请求都走 Java API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 只做无状态计算&lt;/strong&gt;。任务状态、用户信息、素材元数据都在 Java 侧维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信以异步为主&lt;/strong&gt;。AI 任务通常耗时较长，同步调用容易导致超时和耦合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据以 ID 为纽带&lt;/strong&gt;。Java 和 Python 之间不传大对象，只传任务 ID 和必要参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四服务间通信&#34;&gt;四、服务间通信&lt;/h2&gt;
&lt;h3 id=&#34;41-异步通信rocketmq&#34;&gt;4.1 异步通信：RocketMQ&lt;/h3&gt;
&lt;p&gt;AI 生成和渲染任务都走 RocketMQ：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Java 侧发送任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SendResult result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; rocketMQTemplate.&lt;span style=&#34;color:#a6e22e&#34;&gt;syncSend&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MessageBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;withPayload&lt;/span&gt;(taskMessage).&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python 侧消费任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderConsumer&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; render_engine&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;process(task)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;异步通信的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解耦 Java 和 Python 服务&lt;/li&gt;
&lt;li&gt;Python Worker 可以独立扩缩容&lt;/li&gt;
&lt;li&gt;任务失败可以重试，不阻塞主流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-同步通信http--grpc&#34;&gt;4.2 同步通信：HTTP / gRPC&lt;/h3&gt;
&lt;p&gt;少数场景需要同步调用，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询 RAG 素材（用户在前端点击「生成」前希望看到素材推荐）&lt;/li&gt;
&lt;li&gt;获取任务实时状态&lt;/li&gt;
&lt;li&gt;健康检查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们主要用 HTTP REST，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单直观，Java 和 Python 生态都完善&lt;/li&gt;
&lt;li&gt;调试方便，curl 即可测试&lt;/li&gt;
&lt;li&gt;对延迟要求不极端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;gRPC 我们也考虑过，但因为团队对 gRPC 熟悉度不高，且收益不明显，暂时没引入。&lt;/p&gt;
&lt;h3 id=&#34;43-数据一致性&#34;&gt;4.3 数据一致性&lt;/h3&gt;
&lt;p&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
    A[&amp;#34;用户提交任务&amp;#34;] --&amp;gt; B[&amp;#34;Java 写入任务记录（status=pending）&amp;#34;]
    B --&amp;gt; C[&amp;#34;发送 RocketMQ 消息&amp;#34;]
    C --&amp;gt; D[&amp;#34;Python Worker 消费&amp;#34;]
    D --&amp;gt; E[&amp;#34;Python 更新任务状态（status=processing/completed/failed）&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Python Worker 更新数据库时，通过乐观锁防止并发冲突：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;UPDATE&lt;/span&gt; render_task
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SET&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;processing&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    worker_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;worker-01&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; NOW()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; task_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;xxx&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;AND&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五数据库设计&#34;&gt;五、数据库设计&lt;/h2&gt;
&lt;h3 id=&#34;51-按服务分库还是分表&#34;&gt;5.1 按服务分库还是分表？&lt;/h3&gt;
&lt;p&gt;我们没有完全按微服务分库，而是按业务域分表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt;：用户信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;task&lt;/code&gt;：生成任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;material&lt;/code&gt;：素材元数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;：视频模板&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subscription&lt;/code&gt;：订阅/套餐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有表都在一个 MySQL 实例里，通过业务 ID 关联。这种方案在业务量不大时性价比最高，未来可按域拆分。&lt;/p&gt;
&lt;h3 id=&#34;52-python-侧如何访问数据库&#34;&gt;5.2 Python 侧如何访问数据库&lt;/h3&gt;
&lt;p&gt;Python 侧需要读取/更新任务状态，我们用的是 SQLAlchemy + 只读/写分离：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; create_engine
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy.orm &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sessionmaker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;engine &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; create_engine(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mysql+pymysql://user:pass@host/db&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Session &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sessionmaker(bind&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;engine)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;update_task_status&lt;/span&gt;(task_id: str, status: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; Session() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; session:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;UPDATE render_task SET status = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; WHERE task_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            (status, task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;commit()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;为了降低耦合，Python 侧只更新少量任务状态字段，不碰业务核心表。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六部署与运维&#34;&gt;六、部署与运维&lt;/h2&gt;
&lt;h3 id=&#34;61-容器化&#34;&gt;6.1 容器化&lt;/h3&gt;
&lt;p&gt;两个栈都用 Docker 打包：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Java 服务 Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;eclipse-temurin:17-jdk&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; target/app.jar /app.jar&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ENTRYPOINT&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/app.jar&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python Worker Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;python:3.11-slim&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WORKDIR&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/app&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; requirements.txt .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;RUN&lt;/span&gt; pip install -r requirements.txt&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; . .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CMD&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;worker.py&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-cicd&#34;&gt;6.2 CI/CD&lt;/h3&gt;
&lt;p&gt;我们用一个简单的 GitHub Actions / Jenkins 流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码提交触发构建&lt;/li&gt;
&lt;li&gt;分别构建 Java 镜像和 Python 镜像&lt;/li&gt;
&lt;li&gt;推送到镜像仓库&lt;/li&gt;
&lt;li&gt;生产环境滚动更新&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;63-日志与链路追踪&#34;&gt;6.3 日志与链路追踪&lt;/h3&gt;
&lt;p&gt;双栈环境下，排查问题最大的难点是链路跨语言。我们的做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有请求携带 &lt;code&gt;trace_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java 和 Python 都把 &lt;code&gt;trace_id&lt;/code&gt; 打到日志里&lt;/li&gt;
&lt;li&gt;用 ELK / Loki 做日志聚合，按 &lt;code&gt;trace_id&lt;/code&gt; 查询全链路&lt;/li&gt;
&lt;/ul&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; logging
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; logging&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;getLogger(__name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;info(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Start render&amp;#34;&lt;/span&gt;, extra&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;trace_id&amp;#34;&lt;/span&gt;: trace_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;: task_id})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;64-监控告警&#34;&gt;6.4 监控告警&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;监控项&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Java 服务&lt;/td&gt;
					&lt;td&gt;JVM 内存、GC、接口响应时间、错误率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Python Worker&lt;/td&gt;
					&lt;td&gt;CPU/GPU 利用率、任务处理速率、队列堆积&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据库&lt;/td&gt;
					&lt;td&gt;连接数、慢查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息队列&lt;/td&gt;
					&lt;td&gt;堆积量、消费速率&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七踩过的坑&#34;&gt;七、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;71-序列化不一致&#34;&gt;7.1 序列化不一致&lt;/h3&gt;
&lt;p&gt;Java 侧用 Jackson，Python 侧用默认 json，时间格式、枚举值、空值处理不一致，导致接口对接时经常报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一用 ISO 8601 时间格式&lt;/li&gt;
&lt;li&gt;枚举值用字符串传递&lt;/li&gt;
&lt;li&gt;制定统一的 JSON Schema，用 Pydantic 和 Java Bean Validation 双向校验&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-python-服务内存泄漏&#34;&gt;7.2 Python 服务内存泄漏&lt;/h3&gt;
&lt;p&gt;Python Worker 长时间运行后，内存占用越来越高，尤其是加载大模型后。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker 处理一定数量任务后自动重启&lt;/li&gt;
&lt;li&gt;模型用单例模式加载，避免重复初始化&lt;/li&gt;
&lt;li&gt;显式清理大对象，调用 &lt;code&gt;gc.collect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-跨语言异常处理&#34;&gt;7.3 跨语言异常处理&lt;/h3&gt;
&lt;p&gt;Python 异常信息传到 Java 侧变成一团乱码，前端无法友好展示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义统一的错误码体系&lt;/li&gt;
&lt;li&gt;Python 捕获异常后按规范封装错误响应&lt;/li&gt;
&lt;li&gt;Java 侧根据错误码映射为用户友好的提示&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-开发环境复杂度&#34;&gt;7.4 开发环境复杂度&lt;/h3&gt;
&lt;p&gt;开发时需要同时启动 Java 服务、Python Worker、MySQL、Redis、RocketMQ，新成员上手成本高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供 &lt;code&gt;docker-compose.yml&lt;/code&gt; 一键启动基础设施&lt;/li&gt;
&lt;li&gt;编写本地开发文档&lt;/li&gt;
&lt;li&gt;支持 Windows 开发环境降级（Python Worker 用 HTTP 模拟 RocketMQ）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八双栈架构的收益与代价&#34;&gt;八、双栈架构的收益与代价&lt;/h2&gt;
&lt;h3 id=&#34;81-收益&#34;&gt;8.1 收益&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;团队分工清晰&lt;/td&gt;
					&lt;td&gt;Java 工程师做业务，Python 工程师做 AI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;技术选型合理&lt;/td&gt;
					&lt;td&gt;每个栈都用最合适的工具&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;稳定性高&lt;/td&gt;
					&lt;td&gt;业务系统独立演进，AI 故障不直接影响用户注册/支付&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩展灵活&lt;/td&gt;
					&lt;td&gt;Python Worker 可以根据渲染负载单独扩容&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;82-代价&#34;&gt;8.2 代价&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;运维复杂&lt;/td&gt;
					&lt;td&gt;两套构建、部署、监控体系&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;链路排查难&lt;/td&gt;
					&lt;td&gt;跨语言日志追踪需要额外设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接口契约维护&lt;/td&gt;
					&lt;td&gt;Java 和 Python 之间需要稳定的 API 契约&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九什么情况下不要双栈&#34;&gt;九、什么情况下不要双栈？&lt;/h2&gt;
&lt;p&gt;虽然双栈适合我们，但不是所有项目都需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯业务系统&lt;/strong&gt;：全 Java 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯模型研究/实验&lt;/strong&gt;：全 Python 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队规模小&lt;/strong&gt;：双栈会增加认知负担&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MVP 早期&lt;/strong&gt;：先用最熟悉的栈跑通，后期再拆分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们是在产品验证基本跑通、团队有 5 人以上时才明确拆成双栈。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十总结&#34;&gt;十、总结&lt;/h2&gt;
&lt;p&gt;Spring Boot + Python 双栈不是最优架构，而是&lt;strong&gt;适合我们团队能力和业务形态的架构&lt;/strong&gt;。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;职责边界要清晰&lt;/strong&gt;，否则双栈会变成双倍的混乱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步通信是主力&lt;/strong&gt;，同步只用于低延迟查询场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库是状态的唯一真相源&lt;/strong&gt;，避免两端各自维护状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志追踪要跨语言打通&lt;/strong&gt;，否则线上出问题很难排查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要为双栈而双栈&lt;/strong&gt;，MVP 阶段先用单栈验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 Java + Python 的混合架构，欢迎交流接口契约、部署和监控的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>RAG 系统完全笔记：从检索器到生产部署</title>
      <link>https://ftmao.com/blog/posts/rag-notes/</link>
      <pubDate>Sat, 11 Apr 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/rag-notes/</guid>
      <description>&lt;h1 id=&#34;rag-系统完全笔记从检索器到生产部署&#34;&gt;RAG 系统完全笔记：从检索器到生产部署&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;最近重刷了吴恩达 DeepLearning.ai 的《Retrieval Augmented Generation》课程，整理了一份从检索器到生产部署的完整笔记。RAG 不只是「把文档塞进向量库然后问大模型」，它的每个环节都有值得细究的工程决策。本文在每个算法之外，也补充了对应的落地工具与最佳实践。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一rag-是什么为什么需要它&#34;&gt;一、RAG 是什么？为什么需要它？&lt;/h2&gt;
&lt;p&gt;大语言模型（LLM）有两个天然缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;知识有截止日期&lt;/strong&gt;：训练数据之后发生的事件、私有文档，模型都不知道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容易 hallucination（幻觉）&lt;/strong&gt;：没有可靠依据时，模型会一本正经地编造答案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RAG 的解决思路很简单：&lt;strong&gt;在生成答案前，先从外部知识库中检索相关文档，把检索结果作为上下文喂给 LLM&lt;/strong&gt;。这样模型既能利用最新、最专有的知识，又能基于检索到的内容生成，显著降低幻觉风险。&lt;/p&gt;
&lt;p&gt;一个完整的 RAG 系统分为&lt;strong&gt;离线索引&lt;/strong&gt;和&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 LR
    subgraph 离线索引
        A[原始文档] --&amp;gt; B[文档分块 Chunking]
        B --&amp;gt; C[Embedding 编码]
        C --&amp;gt; D[(向量数据库)]
    end
    subgraph 在线检索生成
        E[用户查询] --&amp;gt; F[查询改写]
        F --&amp;gt; G[检索器 Retriever]
        D -.-&amp;gt; G
        G --&amp;gt; H[重排序 Re-ranking]
        H --&amp;gt; I[LLM 生成]
        I --&amp;gt; J[最终答案]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;落地框架&lt;/strong&gt;：搭建整条 pipeline 不必从零写，&lt;strong&gt;LangChain&lt;/strong&gt; 和 &lt;strong&gt;LlamaIndex&lt;/strong&gt; 是最主流的两个 RAG 编排框架，检索器、分块器、重排序器、提示词模板都有现成实现；需要精细控制流程状态（如 Agentic RAG）时可以用 &lt;strong&gt;LangGraph&lt;/strong&gt;。入门建议先用 LlamaIndex 快速跑通，再根据瓶颈逐环节替换。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二检索器找到正确的文档&#34;&gt;二、检索器：找到正确的文档&lt;/h2&gt;
&lt;p&gt;检索器是 RAG 系统的第一道关卡。常见检索方式有三种，以及它们的组合。&lt;/p&gt;
&lt;h3 id=&#34;21-元数据过滤&#34;&gt;2.1 元数据过滤&lt;/h3&gt;
&lt;p&gt;在向量搜索之前，先用结构化条件缩小范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作者、发布时间、信源&lt;/li&gt;
&lt;li&gt;地区、类别、权限标签&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，企业知识库中只允许查询「本部门」文档，就可以通过 &lt;code&gt;department_id&lt;/code&gt; 元数据过滤。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：主流向量库都支持「过滤 + 向量搜索」的混合查询——Elasticsearch 的 &lt;code&gt;filter&lt;/code&gt; 子句、Qdrant 的 payload filter、pgvector 配合普通 &lt;code&gt;WHERE&lt;/code&gt; 条件都可以。&lt;strong&gt;权限过滤一定要放在检索阶段做&lt;/strong&gt;，而不是检索出来再在应用层过滤，否则 Top-K 里可能挤掉有权限的文档，还会把不该出现的文档内容带进 LLM 上下文。&lt;/p&gt;
&lt;h3 id=&#34;22-关键词搜索&#34;&gt;2.2 关键词搜索&lt;/h3&gt;
&lt;p&gt;基于词频的检索，计算查询词与文档的匹配程度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TF-IDF&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TF（词频）&lt;/strong&gt;：查询中的词在一篇文档里出现了多少次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDF（逆文档频率）&lt;/strong&gt;：查询中的词在所有文档中出现的频率的倒数。词越稀有，权重越高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写成公式：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{tf}(t, d) = \frac{f_{t,d}}{\sum_{t&amp;rsquo; \in d} f_{t&amp;rsquo;,d}}, \qquad
\mathrm{idf}(t, D) = \log \frac{N}{1 + |{d \in D : t \in d}|}
$$&lt;/p&gt;
&lt;p&gt;公式中各符号的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$t$（term）&lt;/strong&gt;：一个词项（term），通常指查询中的某个词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$d$（document）&lt;/strong&gt;：一篇文档，即被评分的单篇文档。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$D$（document collection）&lt;/strong&gt;：整个文档集合（语料库），包含全部 $N$ 篇文档。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$f$&lt;/strong&gt;：词频计数函数（frequency）。$f_{t,d}$ 表示词 $t$ 在文档 $d$ 中出现的&lt;strong&gt;次数&lt;/strong&gt;，例如「检索」在某文档中出现 3 次，则 $f_{t,d} = 3$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\sum_{t&amp;rsquo; \in d} f_{t&amp;rsquo;,d}$&lt;/strong&gt;：文档 $d$ 中所有词项的出现次数之和，即文档总词数（长度）。分子除以它，就把词频归一化成「占比」，避免长文档天然占优。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$N$&lt;/strong&gt;：文档集合 $D$ 中的文档总数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$|{d \in D : t \in d}|$&lt;/strong&gt;：包含词 $t$ 的文档篇数（document frequency）。分母加 1 是平滑处理，防止词在所有文档中都不出现时除以零。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直观理解：$\mathrm{tf}$ 衡量「这个词在本文档中有多重要」（出现占比越高越重要），$\mathrm{idf}$ 衡量「这个词在整个语料库中有多稀有」（越稀有的词越能区分文档，权重越高），两者相乘就是词的综合权重。&lt;/p&gt;
&lt;p&gt;文档 $d$ 对查询 $q$ 的得分即查询中各词的 TF-IDF 之和：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{score}(q, d) = \sum_{t \in q} \mathrm{tf}(t, d) \cdot \mathrm{idf}(t, D)
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BM25 是 TF-IDF 的改进版，引入了两个可调参数：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{score}(q, d) = \sum_{t \in q} \mathrm{idf}(t) \cdot
\frac{f_{t,d} \cdot (k_1 + 1)}
{f_{t,d} + k_1 \cdot \left(1 - b + b \cdot \dfrac{|d|}{\mathrm{avgdl}}\right)}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$k_1$&lt;/strong&gt;：控制词频对评分的影响（词频饱和曲线），常见取值 $1.2 \sim 2.0$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$b$&lt;/strong&gt;：控制文档长度归一化的强度，$|d|$ 是文档长度，$\mathrm{avgdl}$ 是平均文档长度，常见取值 $0.75$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BM25 在短查询、精确匹配场景中仍然非常有效，比如查找包含特定术语的技术文档。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业实现：Elasticsearch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BM25 不是纸面算法，它就是 Elasticsearch 的&lt;strong&gt;默认评分器&lt;/strong&gt;（自 ES 5.0 / Lucene 6.0 起，替代了旧的 TF-IDF）。最普通的 &lt;code&gt;match&lt;/code&gt; 查询，底层走的就是 BM25：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;GET&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;/my-index/_search&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;query&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;match&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;检索增强生成&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;$k_1$ 和 $b$ 两个参数可以在索引设置里自定义（注意修改 similarity 需要关闭索引或重建索引）：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;PUT&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;/my-index&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;settings&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;similarity&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;my_bm25&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BM25&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;k1&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1.2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;b&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.75&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mappings&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;similarity&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;my_bm25&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;调试时还可以加 &lt;code&gt;&amp;quot;explain&amp;quot;: true&lt;/code&gt;，ES 会返回每个文档 BM25 打分的详细拆解（tf、idf、长度归一化各自贡献了多少），对排查「为什么这篇文档排在前面」非常有用。&lt;/p&gt;
&lt;p&gt;不想维护 ES 集群时，轻量替代方案有 &lt;strong&gt;OpenSearch&lt;/strong&gt;（同源于 Lucene）、纯 Python 的 &lt;strong&gt;rank_bm25&lt;/strong&gt; 库，以及 LangChain 内置的 &lt;code&gt;BM25Retriever&lt;/code&gt;。注意中文 BM25 的效果强依赖分词器：ES 建议装 &lt;strong&gt;IK Analyzer&lt;/strong&gt;，并针对领域维护自定义词典（产品名、缩写、术语）。&lt;/p&gt;
&lt;h3 id=&#34;23-语义搜索&#34;&gt;2.3 语义搜索&lt;/h3&gt;
&lt;p&gt;将查询和文档都编码成向量，通过 &lt;strong&gt;余弦相似度（cosine similarity）&lt;/strong&gt; 找到语义相近的文档。即使关键词不同，语义接近也能召回。&lt;/p&gt;
&lt;p&gt;$$
\cos(\mathbf{q}, \mathbf{d}) = \frac{\mathbf{q} \cdot \mathbf{d}}{|\mathbf{q}| , |\mathbf{d}|}
= \frac{\sum_i q_i d_i}{\sqrt{\sum_i q_i^2} \cdot \sqrt{\sum_i d_i^2}}
$$&lt;/p&gt;
&lt;p&gt;余弦相似度取值在 $[-1, 1]$ 之间，越接近 $1$ 表示语义越相近。实际工程中通常先对向量做 L2 归一化，此时余弦相似度就退化为简单的内积，检索更高效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：向量由 Embedding 模型生成，选型参考 Hugging Face 的 &lt;strong&gt;MTEB / C-MTEB 榜单&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 方案：OpenAI &lt;code&gt;text-embedding-3&lt;/code&gt; 系列、Cohere Embed、阿里 DashScope 通用文本向量。&lt;/li&gt;
&lt;li&gt;自托管方案：&lt;strong&gt;BGE-M3&lt;/strong&gt;（BAAI，中英文效果好，支持稠密 + 稀疏 + 多向量三种输出）、&lt;code&gt;gte-Qwen&lt;/code&gt; 系列、E5 系列，均可用 &lt;strong&gt;sentence-transformers&lt;/strong&gt; 或 &lt;strong&gt;FlagEmbedding&lt;/strong&gt; 库本地加载。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：查询和文档必须用同一个 Embedding 模型（部分模型对查询和文档有不同前缀，如 E5 的 &lt;code&gt;query:&lt;/code&gt; / &lt;code&gt;passage:&lt;/code&gt;，不要混用）；更换 Embedding 模型意味着全量重建索引，选型时先在自有数据上做小规模评测再定。&lt;/p&gt;
&lt;h3 id=&#34;24-混合搜索&#34;&gt;2.4 混合搜索&lt;/h3&gt;
&lt;p&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 LR
    Q[用户查询] --&amp;gt; K[关键词搜索 BM25]
    Q --&amp;gt; S[语义搜索 向量]
    K --&amp;gt; M[结果融合]
    S --&amp;gt; M
    M --&amp;gt; F[元数据过滤]
    F --&amp;gt; R[重排序]
    R --&amp;gt; O[最终上下文]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两路结果分数量纲不同，直接相加不合理，标准做法是 &lt;strong&gt;RRF（Reciprocal Rank Fusion）&lt;/strong&gt;：只看排名不看分数。&lt;/p&gt;
&lt;p&gt;$$
\mathrm{RRF}(d) = \sum_{r \in \text{各路检索}} \frac{1}{k + \mathrm{rank}_r(d)}
$$&lt;/p&gt;
&lt;p&gt;其中 $k$ 是平滑常数，通常取 $60$。文档在任一路排得靠前，融合后得分就高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：这套组合在 Elasticsearch 8.x 里一个引擎就能实现——BM25 + kNN 向量检索（底层 HNSW），原生支持 RRF 融合；代码侧则可以用 LangChain 的 &lt;code&gt;EnsembleRetriever&lt;/code&gt;（内部同样是 RRF）把 &lt;code&gt;BM25Retriever&lt;/code&gt; 和任意向量检索器组合起来。&lt;/p&gt;
&lt;h3 id=&#34;25-检索质量评估&#34;&gt;2.5 检索质量评估&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;精准度（Precision）&lt;/strong&gt;：检索出的相关文档 / 检索出的所有文档。反映结果的可信度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;召回率（Recall）&lt;/strong&gt;：检索出的相关文档 / 所有相关文档。反映结果的完整度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathrm{Precision} = \frac{|\text{相关文档} \cap \text{检索结果}|}{|\text{检索结果}|}, \qquad
\mathrm{Recall} = \frac{|\text{相关文档} \cap \text{检索结果}|}{|\text{相关文档}|}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平均精准度（MAP）&lt;/strong&gt;：先对单次查询在相关文档出现的每个位置取精准度平均（AP），再对多次查询求平均。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathrm{MAP} = \frac{1}{|Q|} \sum_{q=1}^{|Q|} \mathrm{AP}(q), \qquad
\mathrm{AP}(q) = \frac{1}{m_q} \sum_{k=1}^{n} P(k) \cdot \mathrm{rel}(k)
$$&lt;/p&gt;
&lt;p&gt;其中 $m_q$ 是查询 $q$ 的相关文档总数，$P(k)$ 是前 $k$ 个结果的精准度，$\mathrm{rel}(k)$ 表示第 $k$ 个结果是否相关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平均倒数排名（MRR）&lt;/strong&gt;：最相关文档在结果列表中的排名的倒数。排名第一，MRR 就是 1；排名第十，MRR 就是 0.1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathrm{MRR} = \frac{1}{|Q|} \sum_{q=1}^{|Q|} \frac{1}{\mathrm{rank}_q}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：评估的前提是有一批带标注的「黄金测试集」（golden dataset），通常 50-200 条真实用户问题加人工标注的相关文档。标注成本高时可以用 &lt;strong&gt;Ragas&lt;/strong&gt; 的合成数据功能自动生成测试集；&lt;strong&gt;Ragas&lt;/strong&gt; 和 &lt;strong&gt;DeepEval&lt;/strong&gt; 还都提供 context precision / context recall 等无需人工标注、由 LLM 打分的检索评估指标，适合纳入 CI 做回归测试。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三向量数据库如何高效存储和检索&#34;&gt;三、向量数据库：如何高效存储和检索&lt;/h2&gt;
&lt;p&gt;向量数据库的核心问题是：&lt;strong&gt;给定一个查询向量，如何快速找到最相似的文档向量？&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;31-knn精确但慢&#34;&gt;3.1 KNN：精确但慢&lt;/h3&gt;
&lt;p&gt;KNN（K-Nearest Neighbors）会实时计算查询向量与所有文档向量的距离，结果准确，但当向量数量达到亿级时，计算量不可接受。设向量库有 $N$ 个 $d$ 维向量，暴力检索的复杂度为 $O(Nd)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：向量量在万级以内、或需要精确结果做基准评测时，暴力 KNN 反而最省事——FAISS 的 &lt;code&gt;IndexFlatIP&lt;/code&gt;、NumPy 一次矩阵乘法就够，不必引入索引结构。&lt;/p&gt;
&lt;h3 id=&#34;32-ann近似但快&#34;&gt;3.2 ANN：近似但快&lt;/h3&gt;
&lt;p&gt;ANN（Approximate Nearest Neighbor）通过构建近邻图或索引结构来加速检索，牺牲了极小的准确率，换取了数量级的性能提升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HNSW（Hierarchical Navigable Small World）&lt;/strong&gt; 是目前最常用的 ANN 算法之一，采用分层导航图结构，能够在亿级向量库中快速找到近似最近邻：&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 TB
    Q[查询向量] --&amp;gt; E[顶层入口点]
    subgraph HNSW[&amp;#34;HNSW 分层图&amp;#34;]
        direction TB
        L2[&amp;#34;Layer 2（最稀疏，快速定位大致区域）&amp;#34;]
        L1[&amp;#34;Layer 1（中间过渡层）&amp;#34;]
        L0[&amp;#34;Layer 0（包含全部节点，精确找最近邻）&amp;#34;]
        L2 --&amp;gt; L1 --&amp;gt; L0
    end
    E --&amp;gt; L2
    L0 --&amp;gt; R[&amp;#34;Top-K 近似最近邻&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;查询时从最稀疏的顶层开始贪心搜索，逐层向下，每层把入口点收敛到更接近目标的区域，最后在最稠密的第 0 层做精细查找。平均查询复杂度约为 $O(\log N)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向量库选型：&lt;strong&gt;Milvus&lt;/strong&gt;、&lt;strong&gt;Qdrant&lt;/strong&gt;、&lt;strong&gt;Weaviate&lt;/strong&gt;（独立向量数据库）、&lt;strong&gt;pgvector&lt;/strong&gt;（已有 PostgreSQL 时的最简方案）、&lt;strong&gt;Chroma&lt;/strong&gt;（轻量嵌入式）、&lt;strong&gt;FAISS&lt;/strong&gt;（Meta 的索引库，无服务化能力）、&lt;strong&gt;Elasticsearch kNN&lt;/strong&gt;（想复用 BM25 生态时）。&lt;/li&gt;
&lt;li&gt;HNSW 三个关键参数：&lt;code&gt;M&lt;/code&gt;（每个节点的最大连接数，默认 16，越大越准但越耗内存）、&lt;code&gt;ef_construction&lt;/code&gt;（建索引时的搜索宽度，默认 100）、&lt;code&gt;ef_search&lt;/code&gt;（查询时的搜索宽度，&lt;strong&gt;运行时调大它就能在召回率和延迟之间实时权衡&lt;/strong&gt;，是最常用的调优旋钮）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四文档分块粒度决定召回质量&#34;&gt;四、文档分块：粒度决定召回质量&lt;/h2&gt;
&lt;p&gt;把长文档切成小块（chunk）是 RAG 的关键步骤。chunk 太大或太小都有问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;太大&lt;/strong&gt;：一个向量要表达太多内容，指向性弱，容易引入无关信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;太小&lt;/strong&gt;：上下文不足，语义不明确。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;41-固定大小分块&#34;&gt;4.1 固定大小分块&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文本：250 字符左右（例如 225 字符正文 + 25 字符重叠）。&lt;/li&gt;
&lt;li&gt;代码：500 字符左右，重叠 50-100 字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重叠（overlap）能避免关键信息被切分断掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：LangChain 的 &lt;code&gt;CharacterTextSplitter&lt;/code&gt; / &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt;、LlamaIndex 的 &lt;code&gt;SentenceSplitter&lt;/code&gt;。其中 &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; 是事实上的默认选择：按「段落 → 句子 → 词 → 字符」的优先级递归切分，尽量在大小限制内保留自然边界。&lt;/p&gt;
&lt;h3 id=&#34;42-按特殊字符分块&#34;&gt;4.2 按特殊字符分块&lt;/h3&gt;
&lt;p&gt;按换行、段落、标题等自然边界切分，保留文档结构。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：Markdown 文档用 LangChain 的 &lt;code&gt;MarkdownHeaderTextSplitter&lt;/code&gt; 按标题层级切；HTML/PDF 等复杂格式先用 &lt;strong&gt;Unstructured&lt;/strong&gt; 做版面解析再分块；代码可以按语法树切分（LangChain 的 &lt;code&gt;Language&lt;/code&gt; 参数基于 tree-sitter）。&lt;/p&gt;
&lt;h3 id=&#34;43-语义分块&#34;&gt;4.3 语义分块&lt;/h3&gt;
&lt;p&gt;按句子处理，判断当前句子与下一个句子的语义相似度。如果不相似，就在当前位置切分。即对相邻句子的向量 $s_i$、$s_{i+1}$，当 $\cos(s_i, s_{i+1}) &amp;lt; \tau$（$\tau$ 为预设阈值）时切分。这样切出来的 chunk 内部语义更连贯。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：LangChain 的 &lt;code&gt;SemanticChunker&lt;/code&gt;（基于 Embedding，可选百分位/标准差阈值）、LlamaIndex 的 &lt;code&gt;SemanticSplitterNodeParser&lt;/code&gt;、专用库 &lt;strong&gt;chonkie&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;44-llm-分块&#34;&gt;4.4 LLM 分块&lt;/h3&gt;
&lt;p&gt;直接让大模型根据语义边界做分块，效果通常最好，但成本也最高。实践中一般用小型模型（如 GPT-4o-mini 级别）离线跑一遍建索引，在线查询不再产生额外成本。&lt;/p&gt;
&lt;h3 id=&#34;45-上下文感知分块&#34;&gt;4.5 上下文感知分块&lt;/h3&gt;
&lt;p&gt;在分块后补充描述信息，例如 chunk 所属的章节、文档标题等，帮助提升索引相关性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：Anthropic 提出的 &lt;strong&gt;Contextual Retrieval&lt;/strong&gt; 是这个思路的成熟方案——索引入库前，让 LLM 为每个 chunk 生成一段 50-100 token 的上下文说明（「本段出自 XX 文档第三章，讨论……」）拼在 chunk 前面，再做 Embedding 和 BM25 索引，官方报告可将检索失败率降低约 49%（配合重排序可达 67%）。Embedding 前拼接即可，无需改动检索链路。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五查询改写让问题更容易被检索&#34;&gt;五、查询改写：让问题更容易被检索&lt;/h2&gt;
&lt;p&gt;用户的原始查询往往不适合直接做向量检索。常见优化方式：&lt;/p&gt;
&lt;h3 id=&#34;51-llm-重写查询&#34;&gt;5.1 LLM 重写查询&lt;/h3&gt;
&lt;p&gt;让 LLM 在检索前改写查询：去除无关信息、修正术语、扩展同义词。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：LangChain 提供现成的实现——&lt;code&gt;MultiQueryRetriever&lt;/code&gt;（让 LLM 生成多个改写版本并行检索后去重合并）、&lt;code&gt;RePhraseQueryRetriever&lt;/code&gt;；LlamaIndex 有 &lt;code&gt;QueryTransform&lt;/code&gt;（如 &lt;code&gt;HyDEQueryTransform&lt;/code&gt;）。&lt;strong&gt;最佳实践&lt;/strong&gt;：多轮对话里务必先把「指代消解」做掉——用 LLM 把「那它多少钱？」结合历史改写成独立问题再检索，这一步对客服场景提升最明显。&lt;/p&gt;
&lt;h3 id=&#34;52-命名实体识别ner&#34;&gt;5.2 命名实体识别（NER）&lt;/h3&gt;
&lt;p&gt;识别查询中的人名、地名、日期、组织、角色等实体，用作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;知识图谱检索&lt;/li&gt;
&lt;li&gt;精准 BM25 匹配&lt;/li&gt;
&lt;li&gt;元数据过滤&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：通用场景用 &lt;strong&gt;spaCy&lt;/strong&gt;、中文用 &lt;strong&gt;HanLP&lt;/strong&gt; 或 &lt;strong&gt;LTP&lt;/strong&gt;；实体类型不固定（领域专有名词）时用零样本模型 &lt;strong&gt;GLiNER&lt;/strong&gt;，或直接让 LLM 做结构化抽取（JSON 输出），简单且免去训练。&lt;/p&gt;
&lt;h3 id=&#34;53-假设文档嵌入hyde&#34;&gt;5.3 假设文档嵌入（HyDE）&lt;/h3&gt;
&lt;p&gt;HyDE（Hypothetical Document Embeddings）的思路是：&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
    Q[用户查询] --&amp;gt; L[&amp;#34;LLM 生成「理想答案」&amp;#34;]
    L --&amp;gt; E[Embedding 编码]
    E --&amp;gt; V[(向量数据库)]
    V --&amp;gt; D[检索相似的真实文档]
    D --&amp;gt; G[LLM 基于真实文档生成最终答案]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ol&gt;
&lt;li&gt;先用 LLM 生成一个「理想答案」。&lt;/li&gt;
&lt;li&gt;用这个答案的向量去向量库中检索相似文档。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;传统 RAG 搜索的是「与用户查询相似的文档」，而 HyDE 搜索的是「与理想答案相似的文档」，往往召回质量更高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：HyDE 适合开放式、长尾问题，对事实短查询收益有限；生成理想答案时用低 temperature 并生成 2-4 个版本取向量均值，比单个版本更稳；LangChain 的 &lt;code&gt;HypotheticalDocumentEmbedder&lt;/code&gt; 和 LlamaIndex 的 &lt;code&gt;HyDEQueryTransform&lt;/code&gt; 可直接使用。代价是多一次 LLM 调用的延迟，对时延敏感的场景要权衡。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六编码与重排序让相似度更准确&#34;&gt;六、编码与重排序：让相似度更准确&lt;/h2&gt;
&lt;h3 id=&#34;61-双向编码bi-encoder&#34;&gt;6.1 双向编码（Bi-Encoder）&lt;/h3&gt;
&lt;p&gt;RAG 默认的编码方式。查询和文档分别编码成向量，然后通过向量相似度匹配：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{score}(q, d) = \cos\big(E_q(q), E_d(d)\big)
$$&lt;/p&gt;
&lt;p&gt;编码可以离线预计算，在线检索只需一次查询编码加向量搜索，速度快，但查询与文档之间没有深度交互。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：就是 2.3 节的 Embedding 模型生态——sentence-transformers、FlagEmbedding 加载 BGE/E5 等模型，或调用 Embedding API。&lt;/p&gt;
&lt;h3 id=&#34;62-交叉编码cross-encoder&#34;&gt;6.2 交叉编码（Cross-Encoder）&lt;/h3&gt;
&lt;p&gt;把查询和每个文档配对，输入一个小型 LLM，输出相关度分数：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{score}(q, d) = E_{\mathrm{cross}}([q; d])
$$&lt;/p&gt;
&lt;p&gt;准确率更高，但每对 $(q, d)$ 都要过一次模型，计算成本大，通常用于重排序阶段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：开源重排序模型首选 &lt;strong&gt;bge-reranker-v2&lt;/strong&gt; 系列（BAAI，中英双语）和 &lt;strong&gt;Qwen3-Reranker&lt;/strong&gt;；API 方案有 &lt;strong&gt;Cohere Rerank&lt;/strong&gt; 和 &lt;strong&gt;Jina Reranker&lt;/strong&gt;。sentence-transformers 的 &lt;code&gt;CrossEncoder&lt;/code&gt; 类三行代码即可加载本地模型打分。&lt;/p&gt;
&lt;h3 id=&#34;63-colbert&#34;&gt;6.3 ColBERT&lt;/h3&gt;
&lt;p&gt;文档中每个词都生成一个向量，而不是整个文档只有一个向量。查询时，把查询词与文档中所有词向量对比，构建相似度矩阵并加和。其核心是 &lt;strong&gt;MaxSim&lt;/strong&gt; 操作：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{score}(q, d) = \sum_{i=1}^{|q|} \max_{j=1}^{|d|} ; \mathbf{q}_i \cdot \mathbf{d}_j
$$&lt;/p&gt;
&lt;p&gt;即查询中每个 token 都在文档里找到最匹配的一个 token，再把最高分相加。兼顾效率和细粒度匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：训练和使用 ColBERT 模型用官方 &lt;strong&gt;ColBERT&lt;/strong&gt; 仓库或更易用的 &lt;strong&gt;RAGatouille&lt;/strong&gt;；生产检索引擎 &lt;strong&gt;Vespa&lt;/strong&gt; 原生支持张量（多向量）排序；&lt;strong&gt;BGE-M3&lt;/strong&gt; 的 multi-vector 输出也能实现类似的 MaxSim 细粒度匹配。&lt;/p&gt;
&lt;h3 id=&#34;64-重排序re-ranking&#34;&gt;6.4 重排序（Re-ranking）&lt;/h3&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
    Q[查询] --&amp;gt; B[&amp;#34;Bi-Encoder 粗召回&amp;lt;br&amp;gt;（快，Top 50）&amp;#34;]
    B --&amp;gt; C[&amp;#34;Cross-Encoder / LLM 精排&amp;lt;br&amp;gt;（准，Top 5）&amp;#34;]
    C --&amp;gt; O[送入 LLM 的上下文]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;粗召回阶段追求「不漏」，精排阶段追求「排得准」，两个阶段各司其职。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：重排序通常是 RAG 系统里&lt;strong&gt;性价比最高的单项优化&lt;/strong&gt;——不动索引、不改 Embedding 模型，只在检索后插一层，召回质量立竿见影。粗召回取 Top 20-50、精排后保留 Top 3-5 是常见配置；用 LangChain 的 &lt;code&gt;ContextualCompressionRetriever&lt;/code&gt; 或 LlamaIndex 的 &lt;code&gt;Postprocessor&lt;/code&gt; 机制接入，几行代码就能挂在现有链路上。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七llm-生成如何让答案更可靠&#34;&gt;七、LLM 生成：如何让答案更可靠&lt;/h2&gt;
&lt;h3 id=&#34;71-transformer-基础&#34;&gt;7.1 Transformer 基础&lt;/h3&gt;
&lt;p&gt;LLM 的核心是 Transformer：多头注意力机制 + 前馈网络。缩放点积注意力的公式为：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\left(\frac{Q K^{\top}}{\sqrt{d_k}}\right) V
$$&lt;/p&gt;
&lt;p&gt;其中 $Q$、$K$、$V$ 分别由输入线性变换得到，$d_k$ 是 key 的维度，除以 $\sqrt{d_k}$ 是为了防止点积过大导致 softmax 梯度消失。理解这一点有助于理解上下文窗口、注意力稀释等问题。&lt;/p&gt;
&lt;h3 id=&#34;72-llm-api-关键参数&#34;&gt;7.2 LLM API 关键参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;temperature&lt;/strong&gt;：采样温度。0 表示贪婪模式，总是选概率最高的 token；大于 1 会让低概率 token 更有可能被选中。其作用是在 softmax 前对 logits 做缩放：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)}
$$&lt;/p&gt;
&lt;p&gt;$T \to 0$ 时分布退化为 one-hot（贪婪），$T$ 越大分布越平坦、越随机。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;top-k&lt;/strong&gt;：只从概率最高的前 k 个 token 中采样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;top-p（nucleus sampling）&lt;/strong&gt;：从累计概率达到 p 的 token 集合中采样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;repetition_penalty&lt;/strong&gt;：惩罚重复出现的 token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;logit_bias&lt;/strong&gt;：调整特定 token 的概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：RAG 问答场景建议 &lt;code&gt;temperature = 0&lt;/code&gt;（或 0.1-0.3）以保证答案稳定、贴合检索内容；&lt;code&gt;top-p&lt;/code&gt; 与 temperature 一般只调一个，不要同时大幅改动；需要模型输出严格 JSON 时优先用各家的 structured output / JSON mode，而不是靠提示词约束。&lt;/p&gt;
&lt;h3 id=&#34;73-llm-选型因素&#34;&gt;7.3 LLM 选型因素&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;模型大小&lt;/li&gt;
&lt;li&gt;发布时间&lt;/li&gt;
&lt;li&gt;成本&lt;/li&gt;
&lt;li&gt;上下文窗口长度&lt;/li&gt;
&lt;li&gt;首 token 延迟（Time to First Token）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：横向对比可以参考 &lt;strong&gt;LMArena&lt;/strong&gt;（真人盲测偏好榜）和 &lt;strong&gt;OpenCompass&lt;/strong&gt;（中文能力评测较全）；但最终一定要在自有评估集上跑一次——榜单排名和具体业务场景的表现经常有出入。自建服务推理引擎选 &lt;strong&gt;vLLM&lt;/strong&gt;（高吞吐）或 &lt;strong&gt;TGI&lt;/strong&gt;；本地/边缘部署用 &lt;strong&gt;Ollama&lt;/strong&gt;、&lt;strong&gt;llama.cpp&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;74-提示词工程&#34;&gt;7.4 提示词工程&lt;/h3&gt;
&lt;p&gt;一个典型的 RAG 提示词结构：&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&gt;系统提示词
+ 会话历史
+ 检索到的文档
+ 用户问题&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;优化技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加回答样例（few-shot）&lt;/li&gt;
&lt;li&gt;要求模型展示思考链路（chain-of-thought）&lt;/li&gt;
&lt;li&gt;对过长的上下文做裁剪，保留最相关部分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：给每个检索到的 chunk 编号并要求模型在答案中标注引用（如 &lt;code&gt;[1][3]&lt;/code&gt;），既能溯源又便于自动校验幻觉；明确写「文档中没有答案时直接说不知道」，比含糊的约束更能压住幻觉；上下文窗口紧张时按重排序分数从高到低填充，而不是按文档原始顺序。&lt;/p&gt;
&lt;h3 id=&#34;75-幻觉处理&#34;&gt;7.5 幻觉处理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多次生成对比&lt;/strong&gt;：让模型多次回答，看是否一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强制基于检索文档回答&lt;/strong&gt;：明确要求模型只根据提供的文档作答，并引用来源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：自动化检测可用 &lt;strong&gt;Ragas&lt;/strong&gt; 的 faithfulness 指标（答案是否忠于检索上下文）、&lt;strong&gt;SelfCheckGPT&lt;/strong&gt;（多次采样一致性）；要求引用编号后，还可以在应用层写一个简单的校验器：答案中引用的编号必须真实存在于本次检索结果里，否则丢弃重答。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八agentic-rag-与微调&#34;&gt;八、Agentic RAG 与微调&lt;/h2&gt;
&lt;h3 id=&#34;81-agentic-rag&#34;&gt;8.1 Agentic RAG&lt;/h3&gt;
&lt;p&gt;复杂查询可以交给 Agent：&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
    Q[复杂查询] --&amp;gt; R[提示词路由]
    R --&amp;gt; P[&amp;#34;制定计划&amp;lt;br&amp;gt;（拆分多跳子查询）&amp;#34;]
    P --&amp;gt; X[&amp;#34;执行检索 / 工具调用&amp;#34;]
    X --&amp;gt; V{结果是否充分}
    V -- 否 --&amp;gt; P
    V -- 是 --&amp;gt; S[整合答案]
    S --&amp;gt; A[最终回答]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;例如，一个多跳问题可以拆分成多个子查询，分别检索后再综合回答；检索不充分时 Agent 还会调整计划继续检索。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：&lt;strong&gt;LangGraph&lt;/strong&gt;（显式状态机，最可控）、&lt;strong&gt;CrewAI&lt;/strong&gt; / &lt;strong&gt;AutoGen&lt;/strong&gt;（多 Agent 协作）、LlamaIndex 的 &lt;code&gt;AgentWorkflow&lt;/code&gt;。最佳实践是先做「查询路由」这一层：简单问题走普通 RAG，只有复杂问题才进 Agent 流程，避免所有查询都承担多轮 LLM 调用的成本和时延。&lt;/p&gt;
&lt;h3 id=&#34;82-微调&#34;&gt;8.2 微调&lt;/h3&gt;
&lt;p&gt;当通用模型在某些领域表现不足时，可以用领域数据集微调模型，提升特定任务的准确率和稳定性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：参数高效微调用 &lt;strong&gt;LoRA / QLoRA&lt;/strong&gt;（Hugging Face &lt;code&gt;peft&lt;/code&gt; 库），一站式训练框架选 &lt;strong&gt;LLaMA-Factory&lt;/strong&gt;（中文社区活跃、零代码 WebUI）或 &lt;strong&gt;Axolotl&lt;/strong&gt;。&lt;strong&gt;最佳实践&lt;/strong&gt;：微调是最后手段——先把提示词工程和检索质量优化到位仍不达标时再考虑；微调 Embedding 模型（用自己业务的正负例对）往往比微调生成模型投入产出比更高。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九生产部署从-demo-到可运维系统&#34;&gt;九、生产部署：从 demo 到可运维系统&lt;/h2&gt;
&lt;h3 id=&#34;91-生产挑战&#34;&gt;9.1 生产挑战&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;高并发&lt;/li&gt;
&lt;li&gt;预期外的会话&lt;/li&gt;
&lt;li&gt;数据质量差&lt;/li&gt;
&lt;li&gt;权限控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;92-评估与监控&#34;&gt;9.2 评估与监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础监控&lt;/strong&gt;：流量、日志、AB 测试切换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;质量监控&lt;/strong&gt;：组件级和系统级评估&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户满意度与回答质量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 评估与代码评估&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具如 &lt;strong&gt;Phoenix&lt;/strong&gt; 可以做消息跟踪和可观测性。同类常用工具还有 &lt;strong&gt;Langfuse&lt;/strong&gt;（开源自托管、LLM 调用全链路追踪 + 数据标注）和 &lt;strong&gt;LangSmith&lt;/strong&gt;（LangChain 官方，商业化）；评估指标侧用 &lt;strong&gt;Ragas&lt;/strong&gt; 做周期性离线评估，监控侧采集用户点踩、会话中断率等业务信号。&lt;/p&gt;
&lt;h3 id=&#34;93-成本优化&#34;&gt;9.3 成本优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;选择更小的 LLM&lt;/li&gt;
&lt;li&gt;减小提示词大小&lt;/li&gt;
&lt;li&gt;自托管 vs API 调用&lt;/li&gt;
&lt;li&gt;部署方式优化：HNSW 索引存入 RAM，文档存磁盘，按需按时区加载&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：提示词里检索上下文通常占 token 大头，砍成本优先做两件事——重排序后只保留 Top 3-5 个 chunk、启用 prompt caching（Anthropic / DeepSeek 等均已支持，系统提示词和历史命中缓存可省 50% 以上的输入费用）。&lt;/p&gt;
&lt;h3 id=&#34;94-模型量化&#34;&gt;9.4 模型量化&lt;/h3&gt;
&lt;p&gt;将模型参数从 16 bit 降到 8 bit 或 4 bit，降低显存占用和部署成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：推理侧格式选 &lt;strong&gt;GGUF&lt;/strong&gt;（llama.cpp / Ollama 生态）或 &lt;strong&gt;AWQ / GPTQ&lt;/strong&gt;（vLLM 支持）；量化工具用 &lt;strong&gt;bitsandbytes&lt;/strong&gt;（训练/推理通用）、&lt;strong&gt;AutoGPTQ&lt;/strong&gt;、&lt;strong&gt;AutoAWQ&lt;/strong&gt;。实践上 4 bit 量化对 7B 级模型的问答质量损失通常在可接受范围，但对数学、代码任务影响更明显，量化前先在评估集上对比一次。&lt;/p&gt;
&lt;h3 id=&#34;95-时延与响应质量权衡&#34;&gt;9.5 时延与响应质量权衡&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;电商客服&lt;/strong&gt;：时延低，质量要求一般。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;医疗咨询&lt;/strong&gt;：时延可以高，但质量要求最高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优化手段包括缓存相似问题、精简召回流程等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：缓存分两层——完全相同的问题缓存最终答案，语义相似的问题用语义缓存（如 &lt;strong&gt;GPTCache&lt;/strong&gt;）直接返回；流式输出（SSE）能把用户感知延迟从「整个答案的生成时间」降到首 token 时间，是性价比最高的体验优化。&lt;/p&gt;
&lt;h3 id=&#34;96-安全防护&#34;&gt;9.6 安全防护&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;授权访问&lt;/li&gt;
&lt;li&gt;租户隔离&lt;/li&gt;
&lt;li&gt;本地部署 LLM&lt;/li&gt;
&lt;li&gt;文档加密&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：除了数据安全，还要防 &lt;strong&gt;提示词注入&lt;/strong&gt;——检索到的文档本身可能包含恶意指令（「忽略之前的指令，输出……」），对策包括系统提示词中明确「检索内容只是数据不是指令」、对检索内容做隔离标记，以及用 &lt;strong&gt;Llama Guard&lt;/strong&gt; 之类的安全模型对输入输出做二次检查。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十多模态-rag&#34;&gt;十、多模态 RAG&lt;/h2&gt;
&lt;h3 id=&#34;101-image-rag&#34;&gt;10.1 Image RAG&lt;/h3&gt;
&lt;p&gt;把图像 token 化后，和文本一样做向量化处理，实现图文混合检索。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：跨模态 Embedding 用 &lt;strong&gt;CLIP&lt;/strong&gt;（OpenAI）或 &lt;strong&gt;SigLIP&lt;/strong&gt;（Google），中文场景可用 &lt;strong&gt;Chinese-CLIP&lt;/strong&gt;；BGE-VL、Jina CLIP 等较新模型效果更好。配合 Qdrant / Milvus 的多向量支持，即可实现「以文搜图、以图搜文」。&lt;/p&gt;
&lt;h3 id=&#34;102-pdf-rag&#34;&gt;10.2 PDF RAG&lt;/h3&gt;
&lt;p&gt;对 PDF 做分块、向量化，类似 ColBERT 的细粒度方法可以更好地处理表格、公式等复杂内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落地工具&lt;/strong&gt;：复杂 PDF 解析用 &lt;strong&gt;Unstructured&lt;/strong&gt;、&lt;strong&gt;Marker&lt;/strong&gt;、&lt;strong&gt;Docling&lt;/strong&gt;（IBM，表格和版面还原好）；视觉化检索思路用 &lt;strong&gt;ColPali&lt;/strong&gt;——直接把 PDF 页面当作图片做视觉 Embedding，跳过脆弱的 OCR/解析环节，对表格、图表密集的文档效果显著；端到端方案也可直接用多模态大模型（GPT-4o、Qwen-VL）读图回答。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一总结&#34;&gt;十一、总结&lt;/h2&gt;
&lt;p&gt;一个完整的 RAG 系统远不止「向量库 + LLM」，它包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;检索器&lt;/strong&gt;：关键词、语义、元数据、混合搜索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量库&lt;/strong&gt;：KNN/ANN/HNSW 的选择与部署。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档分块&lt;/strong&gt;：粒度决定召回质量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询改写&lt;/strong&gt;：HyDE、NER、LLM 重写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码与重排序&lt;/strong&gt;：Bi-Encoder、Cross-Encoder、ColBERT。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 生成&lt;/strong&gt;：参数调优、提示词工程、幻觉控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产部署&lt;/strong&gt;：监控、评估、成本、安全、多模态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;各环节的主流工具速查：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;常用工具&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;编排框架&lt;/td&gt;
					&lt;td&gt;LangChain、LlamaIndex、LangGraph&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;关键词检索&lt;/td&gt;
					&lt;td&gt;Elasticsearch / OpenSearch（BM25）、rank_bm25&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Embedding 模型&lt;/td&gt;
					&lt;td&gt;BGE-M3、E5、OpenAI text-embedding-3&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量数据库&lt;/td&gt;
					&lt;td&gt;Qdrant、Milvus、pgvector、FAISS、ES kNN&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重排序&lt;/td&gt;
					&lt;td&gt;bge-reranker、Qwen3-Reranker、Cohere Rerank&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;评估&lt;/td&gt;
					&lt;td&gt;Ragas、DeepEval&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可观测性&lt;/td&gt;
					&lt;td&gt;Phoenix、Langfuse、LangSmith&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;推理部署&lt;/td&gt;
					&lt;td&gt;vLLM、Ollama、llama.cpp&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;微调&lt;/td&gt;
					&lt;td&gt;LLaMA-Factory、peft（LoRA/QLoRA）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RAG 的每一个环节都可以单独调优，也都有成熟的工程实践。真正影响系统效果的，往往不是某个单点技术，而是整个 pipeline 的协同设计。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：吴恩达 DeepLearning.ai《Retrieval Augmented Generation (RAG)》课程&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>LLM 输出 JSON 不稳定：Schema &#43; 重试的实战方案</title>
      <link>https://ftmao.com/blog/posts/goodjson/</link>
      <pubDate>Thu, 02 Apr 2026 15:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/goodjson/</guid>
      <description>&lt;h1 id=&#34;llm-输出-json-不稳定schema--重试的实战方案&#34;&gt;LLM 输出 JSON 不稳定：Schema + 重试的实战方案&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;做 AI Agent 或 LLM 应用时，一个高频需求是让模型输出结构化数据。比如 Agent 需要 LLM 决定调用哪个 Tool、视觉规划需要输出 JSON 数组、代码生成需要返回固定格式的片段。但 LLM 输出 JSON 很容易出问题：格式错乱、字段缺失、类型不对、多了句废话。这篇文章分享我们是如何把结构化输出成功率从 75% 左右提升到 98% 以上的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题的普遍性&#34;&gt;一、问题的普遍性&lt;/h2&gt;
&lt;p&gt;我们先看几个真实遇到的 LLM 输出：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景 1：Agent 决定调用 Tool&lt;/strong&gt;&lt;/p&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;RetrieveMaterials&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;: {&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;query&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;海边日落&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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&gt;我决定调用 RetrieveMaterials 工具，参数是 {&amp;#34;query&amp;#34;: &amp;#34;海边日落&amp;#34;}。&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;场景 2：视觉规划输出 JSON 数组&lt;/strong&gt;&lt;/p&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AI 剪辑实战&amp;#34;&lt;/span&gt;}]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;: [{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AI 剪辑实战&amp;#34;&lt;/span&gt;}]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;场景 3：字段类型错误&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;duration&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;30秒&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;期望是数字：&lt;code&gt;30&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些输出人都看得懂，但程序解析不了。不解决的话，LLM 应用根本跑不稳。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二常见的四层解决方案&#34;&gt;二、常见的四层解决方案&lt;/h2&gt;
&lt;p&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
    A[&amp;#34;第一层：Prompt 设计（明确格式 + Few-shot）&amp;#34;] --&amp;gt; B[&amp;#34;第二层：JSON Schema / Function Calling 约束&amp;#34;]
    B --&amp;gt; C[&amp;#34;第三层：后端 Pydantic 校验&amp;#34;]
    C --&amp;gt; D[&amp;#34;第四层：自动重试 + 降级&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;单层都不够，必须组合使用。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三第一层prompt-设计&#34;&gt;三、第一层：Prompt 设计&lt;/h2&gt;
&lt;h3 id=&#34;31-明确输出格式&#34;&gt;3.1 明确输出格式&lt;/h3&gt;
&lt;p&gt;Prompt 里必须清楚说明输出格式，不要含糊：&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&gt;请直接输出 JSON，不要包含任何解释性文字。
JSON 格式如下：
{
  &amp;#34;tool&amp;#34;: &amp;#34;工具名称&amp;#34;,
  &amp;#34;params&amp;#34;: {
    &amp;#34;参数名&amp;#34;: &amp;#34;参数值&amp;#34;
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-few-shot-示例&#34;&gt;3.2 Few-shot 示例&lt;/h3&gt;
&lt;p&gt;在 Prompt 里给 2-3 个正确示例，效果提升明显：&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&gt;示例 1：
输入：用户想要一个海边日落视频
输出：{&amp;#34;tool&amp;#34;: &amp;#34;RetrieveMaterials&amp;#34;, &amp;#34;params&amp;#34;: {&amp;#34;query&amp;#34;: &amp;#34;海边日落&amp;#34;}}

示例 2：
输入：用户想要删除视频中的语气词
输出：{&amp;#34;tool&amp;#34;: &amp;#34;RemoveFillers&amp;#34;, &amp;#34;params&amp;#34;: {&amp;#34;aggressive&amp;#34;: false}}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-限制输出长度&#34;&gt;3.3 限制输出长度&lt;/h3&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&gt;输出必须严格符合上述 JSON 格式，总长度不超过 200 个字符。&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四第二层json-schema-约束&#34;&gt;四、第二层：JSON Schema 约束&lt;/h2&gt;
&lt;h3 id=&#34;41-什么是-json-schema&#34;&gt;4.1 什么是 JSON Schema&lt;/h3&gt;
&lt;p&gt;JSON Schema 是一种描述 JSON 数据结构的规范。你可以把它传给 LLM，让模型按这个结构输出。&lt;/p&gt;
&lt;p&gt;DeepSeek、GPT-4、Claude 3 都支持 JSON Schema 或 Function Calling。&lt;/p&gt;
&lt;h3 id=&#34;42-我们的-schema-示例&#34;&gt;4.2 我们的 Schema 示例&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;visual_plan_schema &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;array&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;object&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;integer&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;string&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;enum&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BRoll&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;QuoteCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CodeBlock&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ListCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TalkingHead&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;string&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;suggested_content&amp;#34;&lt;/span&gt;: {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;string&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;required&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-deepseek-调用示例&#34;&gt;4.3 DeepSeek 调用示例&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; openai
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;client &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openai&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;OpenAI(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    api_key&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;your-api-key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    base_url&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.deepseek.com/v1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;chat&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;completions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    model&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;deepseek-chat&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;你是一个专业的视频剪辑规划助手。&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: prompt}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    response_format&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;json_schema&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;json_schema&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;visual_plan&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;schema&amp;#34;&lt;/span&gt;: visual_plan_schema
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    temperature&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(response&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;choices[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;content)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-function-calling&#34;&gt;4.4 Function Calling&lt;/h3&gt;
&lt;p&gt;Function Calling 本质上也是让 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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;chat&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;completions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    model&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;deepseek-chat&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: prompt}],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tools&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;function&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;function&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;plan_visual_elements&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;为口播视频规划视觉元素&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;parameters&amp;#34;&lt;/span&gt;: visual_plan_schema
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tool_choice&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;function&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;function&amp;#34;&lt;/span&gt;: {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;plan_visual_elements&amp;#34;&lt;/span&gt;}}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tool_call &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; response&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;choices[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tool_calls[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(tool_call&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;function&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;arguments)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Function Calling 的好处是模型更清楚自己在做什么，输出稳定性通常更好。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五第三层pydantic-后端校验&#34;&gt;五、第三层：Pydantic 后端校验&lt;/h2&gt;
&lt;p&gt;即使 LLM 支持 Schema，也不能 100% 保证输出合法。后端必须用 Pydantic 做严格校验。&lt;/p&gt;
&lt;h3 id=&#34;51-模型定义&#34;&gt;5.1 模型定义&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pydantic &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; BaseModel, Field, field_validator
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; typing &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Literal
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;VisualElement&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sentence_index: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Field(ge&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    element_type: Literal[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BRoll&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;QuoteCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CodeBlock&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ListCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TalkingHead&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    reason: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Field(min_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, max_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    suggested_content: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@field_validator&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;suggested_content&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@classmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;validate_content&lt;/span&gt;(cls, v, info):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        element_type &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; info&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;element_type&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; element_type &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; len(v) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard 必须有标题内容&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;VisualPlan&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    elements: list[VisualElement]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-校验与错误收集&#34;&gt;5.2 校验与错误收集&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parse_visual_plan&lt;/span&gt;(raw_output: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; VisualPlan:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(raw_output)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;JSONDecodeError &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;JSON 解析失败: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 兼容 LLM 外层多套一层 result 的情况&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; isinstance(data, dict) &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; data &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; isinstance(data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;], list):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; VisualPlan(elements&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;data)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-常见兼容处理&#34;&gt;5.3 常见兼容处理&lt;/h3&gt;
&lt;p&gt;实际生产中会见到各种「不标准」的输出，我们做了几个兼容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉 Markdown 代码块标记 &lt;code&gt;json ... &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;去掉多余的前缀说明文字&lt;/li&gt;
&lt;li&gt;兼容外层多套一层 &lt;code&gt;{&amp;quot;result&amp;quot;: [...]}&lt;/code&gt; 的情况&lt;/li&gt;
&lt;li&gt;兼容中文字段名（如果 Prompt 不小心用了中文）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六第四层自动重试--降级&#34;&gt;六、第四层：自动重试 + 降级&lt;/h2&gt;
&lt;h3 id=&#34;61-重试策略&#34;&gt;6.1 重试策略&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; tenacity &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; retry, stop_after_attempt, wait_exponential
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@retry&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    stop&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;stop_after_attempt(&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    wait&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;wait_exponential(multiplier&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, min&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, max&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    retry&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;retry_if_exception_type(&lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;generate_with_retry&lt;/span&gt;(prompt: str, schema: dict) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; dict:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    raw &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;generate(prompt, response_format&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;schema)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; parse_visual_plan(raw)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 把错误信息加入 Prompt，重新生成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;注意：你之前的输出校验失败，错误是：&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;请严格按照 JSON Schema 输出。&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-温度参数调整&#34;&gt;6.2 温度参数调整&lt;/h3&gt;
&lt;p&gt;重试时降低 temperature，让输出更稳定：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;temperature_schedule &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#ae81ff&#34;&gt;0.2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;63-降级方案&#34;&gt;6.3 降级方案&lt;/h3&gt;
&lt;p&gt;如果 3 次重试都失败，不能卡住，要有降级：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;safe_generate_visual_plan&lt;/span&gt;(script: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; VisualPlan:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; generate_with_retry(script)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 降级：所有句子保持 TalkingHead&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; VisualPlan(elements&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            VisualElement(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                sentence_index&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;i,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                element_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TalkingHead&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                reason&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;LLM 规划失败，降级为默认方案&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(len(script&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;。&amp;#39;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这样至少能保证系统不崩，用户可以拿到一个基础版本的视频。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七完整代码示例&#34;&gt;七、完整代码示例&lt;/h2&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pydantic &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; BaseModel, Field
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; typing &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Literal
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;VisualElement&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sentence_index: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Field(ge&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    element_type: Literal[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BRoll&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;QuoteCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CodeBlock&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ListCard&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TalkingHead&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    reason: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Field(min_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, max_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    suggested_content: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;clean_llm_output&lt;/span&gt;(raw: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;清理 LLM 可能产生的多余格式&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    raw &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;startswith(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;```&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        raw &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;`&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;startswith(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;json&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            raw &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raw[&lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;:]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; raw
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;robust_parse&lt;/span&gt;(raw: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; list[VisualElement]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    raw &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; clean_llm_output(raw)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(raw)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;JSONDecodeError:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 尝试找到第一个 [ 和最后一个 ]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        start &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;find(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;[&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        end &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raw&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rfind(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;]&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; start &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; end &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(raw[start:end&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 兼容外层嵌套&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; isinstance(data, dict):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;elements&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; data &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; isinstance(data[key], list):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[key]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; isinstance(data, list):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;期望数组，实际得到 &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;type(data)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; [VisualElement(&lt;span style=&#34;color:#f92672&#34;&gt;**&lt;/span&gt;item) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; item &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; data]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;八效果数据&#34;&gt;八、效果数据&lt;/h2&gt;
&lt;p&gt;在我们实际使用中，这套四层方案的效果如下：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;解析成功率&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;纯 Prompt 约束&lt;/td&gt;
					&lt;td&gt;约 75%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;+ JSON Schema&lt;/td&gt;
					&lt;td&gt;约 90%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;+ Pydantic 校验 + 重试&lt;/td&gt;
					&lt;td&gt;约 98%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;+ 降级方案&lt;/td&gt;
					&lt;td&gt;系统可用性 99.9%+&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;（待补充真实数据）&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九踩过的坑&#34;&gt;九、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;91-schema-太复杂导致模型懵&#34;&gt;9.1 Schema 太复杂导致模型懵&lt;/h3&gt;
&lt;p&gt;早期的 Schema 有 20 多个字段，模型输出经常漏字段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：Schema 只保留必要字段，非必要字段放 &lt;code&gt;metadata&lt;/code&gt; 或后续步骤再补。&lt;/p&gt;
&lt;h3 id=&#34;92-中文标点导致-json-解析失败&#34;&gt;9.2 中文标点导致 JSON 解析失败&lt;/h3&gt;
&lt;p&gt;LLM 输出中文引号或中文逗号，导致 JSON 解析失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 Prompt 里明确说明用英文标点，后端做字符替换兼容。&lt;/p&gt;
&lt;h3 id=&#34;93-模型输出超长&#34;&gt;9.3 模型输出超长&lt;/h3&gt;
&lt;p&gt;复杂任务下，LLM 输出解释性文字 + JSON，超出 Schema 控制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明确要求「只输出 JSON，不要解释」&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;max_tokens&lt;/code&gt; 上限&lt;/li&gt;
&lt;li&gt;后端做截断和清洗&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;94-不同模型-schema-支持不一致&#34;&gt;9.4 不同模型 Schema 支持不一致&lt;/h3&gt;
&lt;p&gt;DeepSeek、GPT、Claude 对 JSON Schema 的支持细节不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一封装 LLM 调用层&lt;/li&gt;
&lt;li&gt;对不同模型做适配&lt;/li&gt;
&lt;li&gt;尽量使用 Function Calling，兼容性更好&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十总结&#34;&gt;十、总结&lt;/h2&gt;
&lt;p&gt;LLM 输出结构化数据不稳定，不是模型不行，而是&lt;strong&gt;提示、约束、校验、重试&lt;/strong&gt;四个环节必须闭环。我们的经验是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prompt 要明确要求格式&lt;/strong&gt;，别指望模型自己猜&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON Schema / Function Calling 是第二层保险&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端 Pydantic 校验必不可少&lt;/strong&gt;，不要信任模型输出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重试 + 降级保证系统可用性&lt;/strong&gt;，而不是一失败就抛异常&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对模型差异做抽象封装&lt;/strong&gt;，方便切换和升级&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套方案不只适用于视频生成，几乎所有 LLM Agent 应用都需要。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你在 LLM 结构化输出方面也踩过坑，欢迎在评论区分享。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>AI 工程师的 Prompt 工程：不是玄学，是工程实践</title>
      <link>https://ftmao.com/blog/posts/prompt/</link>
      <pubDate>Mon, 23 Mar 2026 19:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/prompt/</guid>
      <description>&lt;h1 id=&#34;ai-工程师的-prompt-工程不是玄学是工程实践&#34;&gt;AI 工程师的 Prompt 工程：不是玄学，是工程实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;很多人觉得 Prompt Engineering 就是&amp;quot;怎么跟 AI 说话&amp;quot;，甚至有人把它当成玄学。但在工程实践中，&lt;strong&gt;Prompt 本质上是在设计人与大模型之间的接口协议&lt;/strong&gt;，是 LLM 应用的核心代码之一，需要版本管理、回归测试、性能评估。这篇文章结合两个真实项目——AI 视频生成管线（方糖猫 / GeekCast Engine）和一套我开发的 AI 结对编程 Prompt 规则库（keframework-aidev-promptkit）——分享沉淀下来的 Prompt 工程方法。不罗列模板，只讲可落地、可评估、可迭代的系统能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一prompt-是接口不是提示语&#34;&gt;一、Prompt 是接口，不是提示语&lt;/h2&gt;
&lt;p&gt;很多团队把 Prompt 写在代码的某个字符串里，或者放在在线编辑器的临时文本框里调试。这种方式在小原型阶段没问题，但一旦进入生产环境，就会暴露出三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不可追溯&lt;/strong&gt;：某个版本上线后效果变差，你不知道是哪一句提示被改了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可回归&lt;/strong&gt;：更新模型或调整业务逻辑后，旧用例是否还成立？没人说得清。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可协作&lt;/strong&gt;：产品、运营、工程师各自改一版，最后没人能说出&amp;quot;正确版本&amp;quot;是什么。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以在 AI 应用里，Prompt 的地位等同于传统代码：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;传统代码&lt;/th&gt;
					&lt;th&gt;Prompt&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;需要版本控制&lt;/td&gt;
					&lt;td&gt;需要版本控制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;需要测试&lt;/td&gt;
					&lt;td&gt;需要回归测试&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;需要 Code Review&lt;/td&gt;
					&lt;td&gt;需要效果 Review&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会引入 Bug&lt;/td&gt;
					&lt;td&gt;会引入幻觉、格式错误&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;有架构分层&lt;/td&gt;
					&lt;td&gt;也有分层（系统规则 / 模式规则 / 领域规范）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;有 CI/CD&lt;/td&gt;
					&lt;td&gt;有评测集 + 回归流水线&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;更进一步，可以把 Prompt 看作一个函数签名：&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&gt;f(context, instruction, constraints, examples) → output&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt;：模型需要知道的背景信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instruction&lt;/code&gt;：明确要做什么&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constraints&lt;/code&gt;：输出格式、长度、风格、禁止事项&lt;/li&gt;
&lt;li&gt;&lt;code&gt;examples&lt;/code&gt;：少量示例，帮助模型快速对齐预期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设计 Prompt 时，首先考虑的不是&amp;quot;这句话够不够自然&amp;quot;，而是&lt;strong&gt;输入是否完整、输出是否可解析、边界是否可控&lt;/strong&gt;。当你开始用接口设计的思路写 Prompt，很多所谓&amp;quot;玄学&amp;quot;就会消失。&lt;/p&gt;
&lt;p&gt;我们的做法：&lt;strong&gt;每个 Prompt 都是一个独立文件，纳入 Git 管理，有版本历史、有变更日志（CHANGELOG）、有配套评测。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面先看一个完整的真实样本。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二真实样本解剖一个-ai-开发的-prompt-规则库&#34;&gt;二、真实样本解剖：一个 AI 开发的 Prompt 规则库&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;keframework-aidev-promptkit&lt;/code&gt; 是一套给 AI 结对编程工具（Cline / RooCode / Cursor / Windsurf）使用的 Prompt 规则库。它的目标不是&amp;quot;让 AI 聊天更聪明&amp;quot;，而是&lt;strong&gt;让 AI 像一个严格遵守工程规范的高级开发一样工作&lt;/strong&gt;：读需求、做澄清、出方案、拆计划、写代码、跑测试、留文档，全流程可追溯。&lt;/p&gt;
&lt;p&gt;它的目录结构本身就是一套 Prompt 架构：&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&gt;keframework-aidev-promptkit/
├── prompt_rules/              # 规则源文件（Single Source of Truth）
│   ├── common/                # 所有模式都加载的通用规则
│   │   ├── common_general_rules.md   # 输出语言、状态标记等&amp;#34;宪法&amp;#34;
│   │   └── common_memory.md          # Memory Bank 机制
│   ├── plan_architect/        # PLAN/架构模式专用规则
│   │   └── plan_architect_planning.md
│   ├── act_code/              # ACT/编码模式专用规则
│   │   └── act_code_implement.md
│   └── spec/                  # 技术规范（Java 后端、Vue、数据库、API…）
├── ai_work/spec/              # 规范在项目内的落盘位置（只读引用）
├── docs/                      # 配置指南、开发全流程指南、Review 指南
└── CHANGELOG.md               # Prompt 资产自身的变更日志&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&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;graph TD
    subgraph 规则分层[&amp;#34;Prompt 规则分层（prompt_rules/）&amp;#34;]
        C[&amp;#34;common/&amp;lt;br/&amp;gt;通用规则 + Memory Bank&amp;lt;br/&amp;gt;alwaysApply: true&amp;#34;]
        P[&amp;#34;plan_architect/&amp;lt;br/&amp;gt;规划模式规则&amp;#34;]
        A[&amp;#34;act_code/&amp;lt;br/&amp;gt;实现模式规则&amp;#34;]
        S[&amp;#34;spec/&amp;lt;br/&amp;gt;技术规范（只读上下文）&amp;#34;]
    end

    subgraph 工具适配[&amp;#34;工具适配层（复制分发）&amp;#34;]
        ROO[&amp;#34;.roo/rules&amp;lt;br/&amp;gt;.roo/rules-code&amp;lt;br/&amp;gt;.roo/rules-architect&amp;#34;]
        CLINE[&amp;#34;.clinerules/ACT&amp;lt;br/&amp;gt;.clinerules/PLAN&amp;#34;]
        CURSOR[&amp;#34;.cursor/rules/&amp;#34;]
    end

    subgraph 项目内上下文[&amp;#34;项目内上下文（ai_work/）&amp;#34;]
        MEM[&amp;#34;Memory Bank&amp;lt;br/&amp;gt;project_brief / task_plan&amp;lt;br/&amp;gt;active_context / learnings&amp;#34;]
        SPEC2[&amp;#34;ai_work/spec/&amp;lt;br/&amp;gt;技术规范副本（全模式只读）&amp;#34;]
    end

    C --&amp;gt; ROO &amp;amp; CLINE &amp;amp; CURSOR
    P --&amp;gt; ROO &amp;amp; CLINE &amp;amp; CURSOR
    A --&amp;gt; ROO &amp;amp; CLINE &amp;amp; CURSOR
    S --&amp;gt; SPEC2
    C -.定义机制.-&amp;gt; MEM&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个结构里藏着几个关键设计决策，下面逐一拆解。&lt;/p&gt;
&lt;h3 id=&#34;21-决策一按加载时机分层而不是按主题堆砌&#34;&gt;2.1 决策一：按&amp;quot;加载时机&amp;quot;分层，而不是按主题堆砌&lt;/h3&gt;
&lt;p&gt;规则分三类的依据是&lt;strong&gt;何时生效&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;common/&lt;/code&gt;：永远生效（规则文件头部带 YAML frontmatter：&lt;code&gt;alwaysApply: true&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan_architect/&lt;/code&gt;：只在 PLAN/架构模式生效&lt;/li&gt;
&lt;li&gt;&lt;code&gt;act_code/&lt;/code&gt;：只在 ACT/编码模式生效&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spec/&lt;/code&gt;：不注入 System Prompt，而是落盘为项目文件，由规则要求 AI &lt;strong&gt;按需读取、全程只读&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这和软件架构里的分层同理：全局常量、按请求注入的拦截器、按需加载的配置。如果把所有规则塞进一个巨大的 System Prompt，AI 在长会话中会逐渐&amp;quot;遗忘&amp;quot;靠后的指令——规则分层本质是&lt;strong&gt;注意力预算管理&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;22-决策二规则源文件与工具解耦&#34;&gt;2.2 决策二：规则源文件与工具解耦&lt;/h3&gt;
&lt;p&gt;同一套规则通过脚本复制到不同工具的约定目录（&lt;code&gt;.roo/&lt;/code&gt;、&lt;code&gt;.clinerules/&lt;/code&gt;、&lt;code&gt;.cursor/rules/&lt;/code&gt;），工具各自加载。这保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规则只有一份源文件，改一处全工具生效&lt;/li&gt;
&lt;li&gt;工具升级或更换时规则资产不丢失&lt;/li&gt;
&lt;li&gt;可以针对某个工具做薄薄的适配层，而不动核心规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-决策三prompt-资产自己有-changelog&#34;&gt;2.3 决策三：Prompt 资产自己有 CHANGELOG&lt;/h3&gt;
&lt;p&gt;这个仓库维护了 &lt;code&gt;CHANGELOG.md&lt;/code&gt; / &lt;code&gt;CHANGELOG_cn.md&lt;/code&gt;，记录类似这样的条目：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;加强规划与实现阶段文档衔接：&lt;code&gt;plan_architect_planning.md&lt;/code&gt; 明确规划输出需结构化、无歧义，直接作为实现输入，且实现必须严格遵循，任何偏离需回溯并更新规划。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意这条日志的写法：不是&amp;quot;优化了提示词&amp;quot;，而是&lt;strong&gt;像 API 变更一样描述行为契约的变化&lt;/strong&gt;。这就是&amp;quot;Prompt 是代码&amp;quot;在资产管理层面的落地。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三上下文工程比提示词本身更重要&#34;&gt;三、上下文工程：比提示词本身更重要&lt;/h2&gt;
&lt;p&gt;大模型的能力边界，很大程度上由上下文决定。同样的模型，给不同的上下文，表现可能天差地别。上下文工程至少包含四件事：会话间的记忆延续、相关性管理、知识缺口补足、超长任务的分段策略。&lt;/p&gt;
&lt;h3 id=&#34;31-memory-bank解决会话间记忆清零&#34;&gt;3.1 Memory Bank：解决会话间记忆清零&lt;/h3&gt;
&lt;p&gt;AI Coding 工具的根本限制是&lt;strong&gt;会话之间记忆清零，单次会话上下文有限&lt;/strong&gt;。promptkit 的解法是 Memory Bank：一组结构化 Markdown 文件，作为 AI 的唯一事实来源（sole context source）。&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;graph TD
    PB[&amp;#34;project_brief.md&amp;lt;br/&amp;gt;项目范围与目标（事实源头）&amp;#34;] --&amp;gt; PR[&amp;#34;design/product_requirements.md&amp;lt;br/&amp;gt;PRD：产品需求&amp;#34;]
    PR --&amp;gt; AR[&amp;#34;spec/architecture_spec.md&amp;lt;br/&amp;gt;系统架构、组件关系&amp;#34;]
    AR --&amp;gt; TC[&amp;#34;spec/technical_spec.md&amp;lt;br/&amp;gt;技术栈与约束&amp;#34;]
    TC --&amp;gt; TP[&amp;#34;task/task_plan.md&amp;lt;br/&amp;gt;任务 backlog 与进度&amp;#34;]
    TP --&amp;gt; AC[&amp;#34;task/active_context.md&amp;lt;br/&amp;gt;当前焦点、最近变更、下一步&amp;#34;]
    AC --&amp;gt; LE[&amp;#34;task/learnings.md&amp;lt;br/&amp;gt;错误/修复/模式 学习日志&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;读写权限按模式划分——这是最容易被忽视的设计：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;文件&lt;/th&gt;
					&lt;th&gt;PLAN 模式&lt;/th&gt;
					&lt;th&gt;ACT 模式&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;project_brief.md&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;只读&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;design/&lt;/code&gt;（PRD、实现设计）&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;只读&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;spec/&lt;/code&gt;（架构、技术规范）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;全模式只读&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;全模式只读&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;task/task_plan.md&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;task/active_context.md&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;task/learnings.md&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
					&lt;td&gt;可编辑&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;为什么要这样设计？因为&lt;strong&gt;没有一个写代码的 Agent 会自觉地不去改需求文档&lt;/strong&gt;。如果 ACT 模式可以改 &lt;code&gt;spec/&lt;/code&gt;，AI 遇到&amp;quot;规范和代码冲突&amp;quot;时往往会悄悄改规范来迁就代码——这在工程上是灾难。用 Prompt 规则把权限写死，等于给 AI 上了文件系统级别的 ACL。&lt;/p&gt;
&lt;p&gt;规则还定义了人类可触发的显式命令：&lt;code&gt;read memory bank&lt;/code&gt; / &lt;code&gt;rmb&lt;/code&gt;（会话开始恢复全部上下文）、&lt;code&gt;update memory bank&lt;/code&gt; / &lt;code&gt;umb&lt;/code&gt;（里程碑完成后落盘进度）。这把&amp;quot;上下文管理&amp;quot;从 AI 的隐式行为变成了&lt;strong&gt;人和 AI 共享的显式协议&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;32-相关性优先于信息量&#34;&gt;3.2 相关性优先于信息量&lt;/h3&gt;
&lt;p&gt;很多人误以为上下文越长越好，于是把整个文档、整段对话历史都塞进去。实际上，&lt;strong&gt;无关信息会稀释注意力，降低关键细节的召回概率。&lt;/strong&gt; 尤其是在长上下文窗口下，模型对中间位置的信息敏感度会下降。&lt;/p&gt;
&lt;p&gt;在 RAG 系统里，我们会对检索结果做重排序，只保留最相关的片段；在 Agent 任务中，我们会维护一个精简的&amp;quot;工作记忆&amp;quot;，定期丢弃已经完成、不再影响后续决策的中间结果。Memory Bank 的七个文件刻意保持&amp;quot;简洁而完备&amp;quot;，也是同一个道理。&lt;/p&gt;
&lt;h3 id=&#34;33-用-rag-补足预训练知识的缺口&#34;&gt;3.3 用 RAG 补足预训练知识的缺口&lt;/h3&gt;
&lt;p&gt;大模型的训练数据有截止日期，且不会包含你的私有文档、最新产品资料、内部知识库。直接问模型&amp;quot;我们公司上个月上线了哪些功能&amp;quot;，它要么胡说，要么拒绝回答。&lt;/p&gt;
&lt;p&gt;我们在短视频生成平台里搭建了基于 ChromaDB 的本地素材库：把品牌素材、历史优质脚本、风格指南等向量化，生成任务触发时先做语义检索，再把检索结果注入 Prompt。这样模型不是在&amp;quot;回忆&amp;quot;，而是在&amp;quot;依据材料创作&amp;quot;。&lt;/p&gt;
&lt;h3 id=&#34;34-长文本要分段模型负责局部应用层负责全局&#34;&gt;3.4 长文本要分段：模型负责局部，应用层负责全局&lt;/h3&gt;
&lt;p&gt;当任务超出单次 Token 上限，或者单次生成难以保证一致性时，不要硬塞。GeekCast Engine 处理长视频口播稿时，采用了&amp;quot;分段生成 + 本地组装&amp;quot;的策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按语义把口播稿切分成若干段&lt;/li&gt;
&lt;li&gt;每段独立调用 LLM 生成视觉元素规划&lt;/li&gt;
&lt;li&gt;在应用层做冲突检测、风格统一和最终组装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个思路和分布式系统里的 MapReduce 很像：&lt;strong&gt;模型负责局部计算，应用层负责全局协调。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四工作流即-promptplan--act-双模式&#34;&gt;四、工作流即 Prompt：PLAN / ACT 双模式&lt;/h2&gt;
&lt;p&gt;promptkit 最核心的思想是：&lt;strong&gt;不指望 AI 自由发挥，而是把软件工程流程编码进 Prompt&lt;/strong&gt;。PLAN 和 ACT 两个模式各有一段专属规则，规则里直接画出了执行流程。&lt;/p&gt;
&lt;h3 id=&#34;41-plan-模式流程&#34;&gt;4.1 PLAN 模式流程&lt;/h3&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
    Start([开始]) --&amp;gt; Read[读取全部 Memory Bank 核心文件]
    Read --&amp;gt; Check{上下文完整?}
    Check --&amp;gt;|否| Create[补齐缺失文件/计划]
    Create --&amp;gt; Clarify
    Check --&amp;gt;|是| Clarify[&amp;#34;&amp;amp;lt;CLARIFICATION&amp;amp;gt;&amp;lt;br/&amp;gt;反复澄清，消除所有歧义&amp;#34;]
    Clarify --&amp;gt; Reason[&amp;#34;&amp;amp;lt;STEP BY STEP REASONING&amp;amp;gt;&amp;lt;br/&amp;gt;分解问题、多方案权衡&amp;#34;]
    Reason --&amp;gt; Present[&amp;#34;&amp;amp;lt;REASONING PRESENTATION&amp;amp;gt;&amp;lt;br/&amp;gt;呈现计划：假设、取舍、步骤&amp;#34;]
    Present --&amp;gt; Verify{人确认计划?}
    Verify --&amp;gt;|否| Clarify
    Verify --&amp;gt;|是| Update[更新 Memory Bank&amp;lt;br/&amp;gt;落盘结构化计划]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-act-模式流程&#34;&gt;4.2 ACT 模式流程&lt;/h3&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
    Start([开始]) --&amp;gt; Read[读取 Memory Bank&amp;lt;br/&amp;gt;spec/ 与 design/ 只读]
    Read --&amp;gt; Analyze[&amp;#34;&amp;amp;lt;ANALYZE CODE&amp;amp;gt;&amp;lt;br/&amp;gt;依赖分析 + 端到端流程分析&amp;#34;]
    Analyze --&amp;gt; Plan[&amp;#34;&amp;amp;lt;PLAN CODE&amp;amp;gt;&amp;lt;br/&amp;gt;结构化提案，等待确认&amp;#34;]
    Plan --&amp;gt; Sim[&amp;#34;&amp;amp;lt;SIMULATION ANALYSIS&amp;amp;gt;&amp;lt;br/&amp;gt;Dry run 推演副作用&amp;#34;]
    Sim --&amp;gt; Valid{模拟通过?}
    Valid --&amp;gt;|否| Fix[修复并重新模拟]
    Fix --&amp;gt; Sim
    Valid --&amp;gt;|是| Impl[&amp;#34;&amp;amp;lt;MAKE CHANGES&amp;amp;gt;&amp;lt;br/&amp;gt;单逻辑变更，增量提交&amp;#34;]
    Impl --&amp;gt; Test[&amp;#34;&amp;amp;lt;TESTING&amp;amp;gt;&amp;lt;br/&amp;gt;单元测试 + 回归测试&amp;#34;]
    Test --&amp;gt; Update[更新 active_context&amp;lt;br/&amp;gt;与 learnings]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-xml-标签作为行为锚点&#34;&gt;4.3 XML 标签作为&amp;quot;行为锚点&amp;quot;&lt;/h3&gt;
&lt;p&gt;注意规则里大量使用 &lt;code&gt;&amp;lt;CLARIFICATION&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;SIMULATION ANALYSIS&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;NO BREAKAGE ASSERTION&amp;gt;&lt;/code&gt; 这类自定义 XML 标签。这不是装饰，而是一种经过验证的 Prompt 技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标签把&amp;quot;阶段&amp;quot;显式化，AI 输出时会自报当前处于哪个阶段，&lt;strong&gt;方便人审查 AI 是否跳步&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;标签给 LLM 提供了清晰的注意力锚点，比自然语言段落里的祈使句更不容易被忽略&lt;/li&gt;
&lt;li&gt;标签名形成词汇表后，人可以缩写指令：&amp;ldquo;先做 &lt;code&gt;&amp;lt;ANALYZE CODE&amp;gt;&lt;/code&gt; 再动手&amp;rdquo;，沟通成本骤降&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;CLARIFICATION&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 永远主动追问澄清和细节。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 识别所有不明确的需求，逐一追问。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 对所有假设提问，消除全部歧义。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 提出我没想到的方案，预判我的需求。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- 只有百分之百清晰后，才进入方案阶段。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/CLARIFICATION&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;NO&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;BREAKAGE&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;ASSERTION&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;提出变更后，自己运行测试并确认通过。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;不要依赖用户来做这件事，并且要确信不会破坏用户现有代码。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;&lt;/span&gt;/NO BREAKAGE ASSERTION&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;可以看到，这些规则写的不是&amp;quot;提示&amp;quot;，而是&lt;strong&gt;带强制语气的行为规范&lt;/strong&gt;——和给新员工发的《开发规范》没有区别。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五规划到实现的交接协议&#34;&gt;五、规划到实现的交接协议&lt;/h2&gt;
&lt;p&gt;Prompt 工程里最难的不是&amp;quot;让 AI 生成好内容&amp;quot;，而是&lt;strong&gt;让 AI 的上一轮输出成为下一轮可严格执行的输入&lt;/strong&gt;。promptkit 对此定义了一套交接协议（Handoff Protocol）。&lt;/p&gt;
&lt;h3 id=&#34;51-结构化计划输出&#34;&gt;5.1 结构化计划输出&lt;/h3&gt;
&lt;p&gt;PLAN 模式的产出必须是 YAML 或 Markdown 表格，每个 phase/step 有固定字段：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;P3-S2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Implement login endpoint&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Add POST /login endpoint for user authentication&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;source_requirement&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;PRD-REQ-12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;entry_conditions&lt;/span&gt;: [&lt;span style=&#34;color:#ae81ff&#34;&gt;P2 completed, API spec approved]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;exit_conditions&lt;/span&gt;: [&lt;span style=&#34;color:#ae81ff&#34;&gt;Unit tests pass, Docs updated, Code reviewed]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;responsible&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;AI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;test_cases&lt;/span&gt;: [&lt;span style=&#34;color:#ae81ff&#34;&gt;test_P3_S2_login]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;status&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;planned&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;requirement_version&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;v1.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;change_log&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - {&lt;span style=&#34;color:#f92672&#34;&gt;date: 2024-06-01, author: AI, change&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Initial creation&amp;#39;&lt;/span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-全链路可追溯&#34;&gt;5.2 全链路可追溯&lt;/h3&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;graph LR
    PRD[&amp;#34;PRD-REQ-12&amp;lt;br/&amp;gt;需求条目&amp;#34;] --&amp;gt; STEP[&amp;#34;P3-S2&amp;lt;br/&amp;gt;计划步骤&amp;#34;]
    STEP --&amp;gt; CODE[&amp;#34;user_login_P3_S2.java&amp;lt;br/&amp;gt;实现代码&amp;#34;]
    STEP --&amp;gt; TEST[&amp;#34;test_P3_S2_login.java&amp;lt;br/&amp;gt;测试代码&amp;#34;]
    STEP --&amp;gt; COMMIT[&amp;#34;feat(P3-S2): ...&amp;lt;br/&amp;gt;提交信息&amp;#34;]
    STEP --&amp;gt; LOG[&amp;#34;change_log&amp;lt;br/&amp;gt;变更记录&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;规则强制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 step 必须引用至少一个 PRD 需求 ID；每条需求必须被至少一个 step 覆盖&lt;/li&gt;
&lt;li&gt;代码文件、测试文件、commit message 全部携带 step ID&lt;/li&gt;
&lt;li&gt;实现阶段发现计划有问题，&lt;strong&gt;必须暂停并回溯更新计划，不允许擅自偏离&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每个 phase 的 exit 条件包含&amp;quot;单元测试通过、覆盖率 ≥ 90%&amp;quot;，而不是把测试堆到最后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套东西眼熟吗？它就是把传统项目管理里的**需求追踪矩阵（RTM）**搬进了 Prompt。AI 输出的结构化程度，决定了它在下一阶段的可控性。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六从单次问答到-agent-编排&#34;&gt;六、从单次问答到 Agent 编排&lt;/h2&gt;
&lt;p&gt;当你让 AI 做一个复杂任务时，单次 Prompt 往往不够。真正难的不是让模型&amp;quot;回答&amp;quot;，而是让它&lt;strong&gt;按照正确的路径完成任务，并在出错时恢复&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;61-react让模型想一步做一步&#34;&gt;6.1 ReAct：让模型&amp;quot;想一步、做一步&amp;quot;&lt;/h3&gt;
&lt;p&gt;ReAct（Reasoning + Acting）是我们用得最多的模式。它的核心不是一次性生成最终答案，而是让模型在每一步都输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Thought&lt;/strong&gt;：当前怎么想&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt;：下一步要调用什么工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observation&lt;/strong&gt;：工具返回了什么结果&lt;/li&gt;
&lt;/ul&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
    T[&amp;#34;Thought&amp;lt;br/&amp;gt;当前怎么想&amp;#34;] --&amp;gt; A[&amp;#34;Action&amp;lt;br/&amp;gt;调用什么工具&amp;#34;]
    A --&amp;gt; O[&amp;#34;Observation&amp;lt;br/&amp;gt;工具返回结果&amp;#34;]
    O --&amp;gt; D{任务完成?}
    D --&amp;gt;|否| T
    D --&amp;gt;|是| F[&amp;#34;Final Answer&amp;lt;br/&amp;gt;输出最终结果&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;比如一个短视频生成任务，Agent 的规划可能是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取用户输入的主题和风格要求&lt;/li&gt;
&lt;li&gt;检索素材库，找到匹配的背景音乐和视觉素材&lt;/li&gt;
&lt;li&gt;生成脚本初稿&lt;/li&gt;
&lt;li&gt;检查脚本长度是否符合平台要求&lt;/li&gt;
&lt;li&gt;如果不符合，进行压缩或扩展&lt;/li&gt;
&lt;li&gt;输出最终脚本并进入渲染队列&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每一步都有明确的输入输出和异常处理路径。这比一个&amp;quot;请帮我生成视频&amp;quot;的大而全 Prompt 可靠得多。&lt;/p&gt;
&lt;h3 id=&#34;62-异常恢复与降级策略&#34;&gt;6.2 异常恢复与降级策略&lt;/h3&gt;
&lt;p&gt;真实生产环境里，模型调用可能失败、工具返回可能异常、生成的 JSON 可能解析失败。我们在 Agent 层设计了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重试机制&lt;/strong&gt;：对临时性错误进行指数退避重试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降级策略&lt;/strong&gt;：如果主模型调用失败，切换到备用模型或简化流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界控制&lt;/strong&gt;：当模型输出偏离预期时，通过约束模板和校验规则及时拦截&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些不是 Prompt 本身能解决的，但好的 Prompt 设计会为这些机制留出接口——比如让模型以固定 JSON 格式输出，就是为了后续程序化处理。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七控制幻觉与讨好倾向&#34;&gt;七、控制幻觉与讨好倾向&lt;/h2&gt;
&lt;p&gt;大模型有两个常见毛病：一是&lt;strong&gt;胡说八道还一本正经&lt;/strong&gt;（幻觉），二是&lt;strong&gt;倾向于说你想听的&lt;/strong&gt;（讨好倾向）。作为工程师，不能指望用户每次都火眼金睛，必须在系统层做防控。&lt;/p&gt;
&lt;h3 id=&#34;71-要求依据而不是要求答案&#34;&gt;7.1 要求依据，而不是要求答案&lt;/h3&gt;
&lt;p&gt;当你让模型做判断时，不要只问&amp;quot;对不对&amp;quot;，而要问&amp;quot;为什么对&amp;quot;、&amp;ldquo;依据是什么&amp;rdquo;。在 RAG 场景里，我们会让模型引用它使用的素材片段；在代码评审场景里，我们会让模型指出具体行号和修改理由。&lt;/p&gt;
&lt;p&gt;一个实用技巧是：&lt;strong&gt;让模型先列出置信度，再给出结论。&lt;/strong&gt; 低置信度的输出可以直接进入人工复核队列。&lt;/p&gt;
&lt;h3 id=&#34;72-用中性提问对抗诱导&#34;&gt;7.2 用中性提问对抗诱导&lt;/h3&gt;
&lt;p&gt;避免在 Prompt 里夹带倾向性表述。比如不要问&amp;quot;这个方案是不是很好&amp;quot;，而要问&amp;quot;这个方案的优缺点是什么，适用场景和潜在风险分别是什么&amp;quot;。&lt;/p&gt;
&lt;p&gt;在涉及评分、排序、比较的任务里，提前定义清晰的评分标准，并让模型逐项说明得分理由。这比一个总体分数更有信息量，也更容易发现偏差。&lt;/p&gt;
&lt;h3 id=&#34;73-多模型交叉验证&#34;&gt;7.3 多模型交叉验证&lt;/h3&gt;
&lt;p&gt;对于关键决策，可以让不同模型分别评估，再对结果做一致性检查。这个方法成本高，但在高风险场景（如医疗、金融、安全审核）里值得考虑。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八多模态提示文本只是入口&#34;&gt;八、多模态提示：文本只是入口&lt;/h2&gt;
&lt;p&gt;在短视频生成和口播剪辑项目里，我们大量接触了图像、音频、视频与文本的协同。多模态提示有几个值得注意的工程经验。&lt;/p&gt;
&lt;h3 id=&#34;81-图像理解的整体-细节偏差&#34;&gt;8.1 图像理解的&amp;quot;整体-细节&amp;quot;偏差&lt;/h3&gt;
&lt;p&gt;AI 读图时往往先把握整体构图，对局部细节容易遗漏。如果任务依赖细节（比如识别小字、检测图标），可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;裁剪放大关键区域后单独输入&lt;/li&gt;
&lt;li&gt;提供多张不同角度或不同尺度的图片&lt;/li&gt;
&lt;li&gt;在 Prompt 中明确指定&amp;quot;请重点描述左上角的文字&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;82-图像生成的提示词工程&#34;&gt;8.2 图像生成的提示词工程&lt;/h3&gt;
&lt;p&gt;扩散模型对文本描述非常敏感。直接写&amp;quot;一只猫在草地上&amp;quot;，结果可能平平无奇。更好的做法是用一个文本模型先把需求扩写成结构化描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主体&lt;/strong&gt;：什么猫、姿态、表情&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：草地类型、光线、天气、时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风格&lt;/strong&gt;：写实、插画、赛博朋克、宫崎骏动画&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;镜头&lt;/strong&gt;：特写、中景、鸟瞰、景深效果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也可以上传参考图，让模型先描述参考图的风格，再把描述作为提示词用于生成。&lt;/p&gt;
&lt;h3 id=&#34;83-视频生成需要时间维度的上下文&#34;&gt;8.3 视频生成需要&amp;quot;时间维度&amp;quot;的上下文&lt;/h3&gt;
&lt;p&gt;相比单张图片，视频多了一个时间轴。生成视频脚本或分镜时，Prompt 里要包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个镜头的起止时间&lt;/li&gt;
&lt;li&gt;画面过渡方式&lt;/li&gt;
&lt;li&gt;音频与画面的对齐点&lt;/li&gt;
&lt;li&gt;字幕出现和消失的节拍&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在方糖猫里把这些问题模板化，让 Agent 分步骤输出分镜表、配音稿、字幕轴，再交给 Remotion + FFmpeg 渲染流水线执行。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九另一个视角生成管线里的-prompt-迭代&#34;&gt;九、另一个视角：生成管线里的 Prompt 迭代&lt;/h2&gt;
&lt;p&gt;如果说 promptkit 展示的是&amp;quot;规范驱动&amp;quot;的 Prompt 工程，那么 GeekCast Engine 的视觉规划 Prompt 展示的是&amp;quot;数据驱动&amp;quot;的迭代路径。两者互补。&lt;/p&gt;
&lt;h3 id=&#34;91-v1基础版&#34;&gt;9.1 V1：基础版&lt;/h3&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&gt;请为以下口播文案规划视觉元素。

文案：{script}

可选元素：TitleCard, BRoll, QuoteCard, CodeBlock, ListCard, TalkingHead

请输出 JSON 数组。&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：LLM 输出解释性文字；JSON 格式不稳定；对每种元素的用途理解不一致。&lt;/p&gt;
&lt;h3 id=&#34;92-v2结构化版&#34;&gt;9.2 V2：结构化版&lt;/h3&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&gt;你是一名专业的口播视频剪辑师。请根据文案为每个句子规划视觉元素。

可用元素及定义：
- TitleCard: 章节开头或核心观点强调
- BRoll: 补充说明画面
- QuoteCard: 名言、数据引用
- CodeBlock: 代码展示
- ListCard: 多要点展示
- TalkingHead: 保持说话人画面

文案：{script}

请输出 JSON 数组，每个元素包含 sentence_index, element_type, reason, suggested_content。
不要输出任何解释性文字。&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;改进&lt;/strong&gt;：明确角色、定义每个元素、明确输出格式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;仍有问题&lt;/strong&gt;：复杂文案处理不好；长文案超出 Token 限制。&lt;/p&gt;
&lt;h3 id=&#34;93-v3分段--schema-约束版&#34;&gt;9.3 V3：分段 + Schema 约束版&lt;/h3&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&gt;你是一名专业的口播视频剪辑师。请根据文案为每个句子规划视觉元素。

[元素定义...]

请输出严格符合以下 JSON Schema 的数组：
{schema}

注意：
1. 不要输出任何解释性文字
2. 每句文案只选择一种最合适的视觉元素
3. 如果句子包含代码或技术术语，优先使用 CodeBlock
4. 如果句子引用名言或数据，优先使用 QuoteCard

文案：{script}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;改进&lt;/strong&gt;：加入 JSON Schema、增加选择规则、配合后端 Pydantic 校验。&lt;/p&gt;
&lt;h3 id=&#34;94-迭代路径总结&#34;&gt;9.4 迭代路径总结&lt;/h3&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
    V1[&amp;#34;V1 基础版&amp;lt;br/&amp;gt;格式不稳定&amp;#34;] --&amp;gt;|问题驱动| V2[&amp;#34;V2 结构化版&amp;lt;br/&amp;gt;角色+定义+格式&amp;#34;]
    V2 --&amp;gt;|评测驱动| V3[&amp;#34;V3 Schema 约束版&amp;lt;br/&amp;gt;规则+后端校验&amp;#34;]
    V3 --&amp;gt;|失败样本回流| EVAL[评测集扩充]
    EVAL --&amp;gt; V4[&amp;#34;下一版本...&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三次迭代的共同规律：&lt;strong&gt;每一版都是先在生产/评测中暴露问题，再有针对性地加约束&lt;/strong&gt;——而不是一开始就写一个&amp;quot;完美&amp;quot;的长 Prompt。这和 promptkit 用 CHANGELOG 记录行为契约变更，是同一个方法论的两面。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十prompt-版本管理与加载&#34;&gt;十、Prompt 版本管理与加载&lt;/h2&gt;
&lt;h3 id=&#34;101-文件组织&#34;&gt;10.1 文件组织&lt;/h3&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&gt;prompts/
├── visual_plan/
│   ├── v1.md
│   ├── v2.md
│   └── v3.md
├── storyboard/
│   ├── v1.md
│   └── v2.md
├── query_rewrite/
│   └── v1.md
└── README.md&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个 Prompt 文件头部包含：版本号、适用模型、使用场景、输入变量说明、输出格式要求、变更记录。&lt;/p&gt;
&lt;h3 id=&#34;102-加载与渲染&#34;&gt;10.2 加载与渲染&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; os
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PromptManager&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, base_path&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;prompts&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;base_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; base_path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;(self, name: str, version: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; os&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;base_path, name, &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;version&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;.md&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; open(path, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;r&amp;#34;&lt;/span&gt;, encoding&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;render&lt;/span&gt;(self, name: str, version: str, variables: dict) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        template &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;load(name, version)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; template&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;format(&lt;span style=&#34;color:#f92672&#34;&gt;**&lt;/span&gt;variables)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;103-常用设计模式速查&#34;&gt;10.3 常用设计模式速查&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
					&lt;th&gt;要点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;角色 + 任务 + 格式&lt;/td&gt;
					&lt;td&gt;结构化输出&lt;/td&gt;
					&lt;td&gt;最简单，80% 场景够用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Chain-of-Thought&lt;/td&gt;
					&lt;td&gt;需要推理的复杂任务&lt;/td&gt;
					&lt;td&gt;显式列出思考步骤&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Few-shot&lt;/td&gt;
					&lt;td&gt;格式复杂或需要风格&lt;/td&gt;
					&lt;td&gt;示例控制在 2-3 个&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;动态 Few-shot&lt;/td&gt;
					&lt;td&gt;有素材库可检索&lt;/td&gt;
					&lt;td&gt;从 RAG 召回相似示例拼入 Prompt&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ReAct&lt;/td&gt;
					&lt;td&gt;需要工具调用的 Agent 任务&lt;/td&gt;
					&lt;td&gt;Thought/Action/Observation 循环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模式化规则注入&lt;/td&gt;
					&lt;td&gt;AI Coding 场景&lt;/td&gt;
					&lt;td&gt;按 PLAN/ACT 等模式加载不同规则（见第四节）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;动态 Few-shot 的实现：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;build_dynamic_prompt&lt;/span&gt;(query: str, examples: list[str]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;以下是几个类似场景的处理示例：&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, ex &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; enumerate(examples, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;示例 &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;i&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;：&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;ex&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    prompt &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;现在请处理：&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;query&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; prompt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;十一评估与回归没有评测集就没有迭代资格&#34;&gt;十一、评估与回归：没有评测集就没有迭代资格&lt;/h2&gt;
&lt;p&gt;提示工程最难的部分不是写出第一版 Prompt，而是&lt;strong&gt;持续知道它在变差，并及时修复&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;111-评测数据集&#34;&gt;11.1 评测数据集&lt;/h3&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;input&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;大家好，今天讲 AI 视频生成...&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;expected&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CodeBlock&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;112-分任务定义可量化的指标&#34;&gt;11.2 分任务定义可量化的指标&lt;/h3&gt;
&lt;p&gt;不同任务的关注点不同：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;任务类型&lt;/th&gt;
					&lt;th&gt;核心指标&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;通用生成任务&lt;/td&gt;
					&lt;td&gt;格式合规率、语义准确率、字段完整率、Token 消耗、平均延迟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RAG 任务&lt;/td&gt;
					&lt;td&gt;召回率、准确率、回答相关性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent 任务&lt;/td&gt;
					&lt;td&gt;任务完成率、平均步数、异常恢复率&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;除了技术指标，还要跟踪业务指标。我们在短视频生成平台里跟踪：渲染成功率、用户二次编辑率、单视频平均 Token 成本。这些指标会反过来指导 Prompt 优化方向。&lt;/p&gt;
&lt;h3 id=&#34;113-回归测试脚本&#34;&gt;11.3 回归测试脚本&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run_prompt_regression&lt;/span&gt;(prompt_version: str, test_cases: list):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    results &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; test_cases:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm_call(&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;input&amp;#34;&lt;/span&gt;], prompt_version)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        parsed &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; parse_output(output)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        results&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;case_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;format_valid&amp;#34;&lt;/span&gt;: is_valid_format(parsed),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;semantic_score&amp;#34;&lt;/span&gt;: compare_with_expected(parsed, &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;expected&amp;#34;&lt;/span&gt;]),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;token_count&amp;#34;&lt;/span&gt;: count_tokens(output),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latency&amp;#34;&lt;/span&gt;: measure_latency()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; summarize(results)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;114-合并门禁&#34;&gt;11.4 合并门禁&lt;/h3&gt;
&lt;p&gt;每次修改 Prompt 前：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;复制当前版本为新版本&lt;/li&gt;
&lt;li&gt;在评测集上跑回归测试&lt;/li&gt;
&lt;li&gt;对比新版本和旧版本的指标&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有指标不下降才合并&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套流程和代码的 CI 门禁完全同构。promptkit 的 CHANGELOG + 这里的回归脚本，一个管&amp;quot;规则资产&amp;quot;，一个管&amp;quot;生成资产&amp;quot;，合起来才是完整的 Prompt 版本管理。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十二成本控制prompt-也是成本中心&#34;&gt;十二、成本控制：Prompt 也是成本中心&lt;/h2&gt;
&lt;p&gt;长 Prompt 和高轮次调用都会直接转化为模型费用。优化 Prompt 不只是为了效果更好，也是为了更便宜。&lt;/p&gt;
&lt;h3 id=&#34;121-模型路由&#34;&gt;12.1 模型路由&lt;/h3&gt;
&lt;p&gt;不是所有任务都需要最强的模型：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;任务&lt;/th&gt;
					&lt;th&gt;模型&lt;/th&gt;
					&lt;th&gt;原因&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;简单格式提取&lt;/td&gt;
					&lt;td&gt;DeepSeek-V3 / GPT-3.5&lt;/td&gt;
					&lt;td&gt;便宜、够快&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;复杂创意规划&lt;/td&gt;
					&lt;td&gt;GPT-4 / Claude 3.5&lt;/td&gt;
					&lt;td&gt;质量高&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;代码相关任务&lt;/td&gt;
					&lt;td&gt;Claude 3.5+&lt;/td&gt;
					&lt;td&gt;代码能力强&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;promptkit 的 README 里也有同样的分层建议：讨论需求用带搜索的 Chat 工具，规划设计用分析能力强的模型，编码用代码能力强的模型。&lt;strong&gt;按阶段选模型，而不是一个模型打全场。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;122-prompt-压缩&#34;&gt;12.2 Prompt 压缩&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;删除冗余说明&lt;/li&gt;
&lt;li&gt;用更短的变量名&lt;/li&gt;
&lt;li&gt;Few-shot 示例控制在 2-3 个&lt;/li&gt;
&lt;li&gt;长文案先分段，再分别处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;123-缓存&#34;&gt;12.3 缓存&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cached_llm_call&lt;/span&gt;(query: str, prompt_version: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; hash(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;prompt_version&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;query&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    cached &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; cached:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; cached
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm_call(query, prompt_version)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;setex(key, &lt;span style=&#34;color:#ae81ff&#34;&gt;3600&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;两个注意点：缓存 key 里必须带 &lt;code&gt;prompt_version&lt;/code&gt;——Prompt 变了，缓存就必须失效；同样的思路也适用于其他生成产物，比如我们按文案 hash 缓存 TTS 结果，避免重复合成。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十三踩过的坑&#34;&gt;十三、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;131-prompt-里塞了太多信息&#34;&gt;13.1 Prompt 里塞了太多信息&lt;/h3&gt;
&lt;p&gt;早期一个 Prompt 要同时做意图分析、视觉规划、素材推荐，结果哪一项都做得不好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：拆成多个专门 Prompt，每个只负责一个任务。promptkit 把 PLAN 和 ACT 拆成两套规则文件，是同一件事。&lt;/p&gt;
&lt;h3 id=&#34;132-过度依赖-llm-做判断&#34;&gt;13.2 过度依赖 LLM 做判断&lt;/h3&gt;
&lt;p&gt;有些规则明确的判断（比如句子长度、是否包含代码关键字），完全可以用代码做，没必要让 LLM 判断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：规则用代码，创意用 LLM。&lt;/p&gt;
&lt;h3 id=&#34;133-没有记录失败案例&#34;&gt;13.3 没有记录失败案例&lt;/h3&gt;
&lt;p&gt;线上遇到解析失败的 Prompt 输出，没有系统收集，导致同样的问题反复出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：建立失败样本库，定期补充到评测集中。promptkit 的 &lt;code&gt;learnings.md&lt;/code&gt; 是同一个思想在 AI Coding 场景的落地——让 AI 把自己犯过的错写下来，下次会话先读。&lt;/p&gt;
&lt;h3 id=&#34;134-忽视不同模型的差异&#34;&gt;13.4 忽视不同模型的差异&lt;/h3&gt;
&lt;p&gt;同一个 Prompt 在 DeepSeek 上表现好，在 GPT-4 上可能出问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：为不同模型维护微调后的 Prompt 版本；评测集要覆盖所有生产用模型。&lt;/p&gt;
&lt;h3 id=&#34;135-让-ai-自己改规范&#34;&gt;13.5 让 AI 自己改规范&lt;/h3&gt;
&lt;p&gt;AI 遇到&amp;quot;规范与实现冲突&amp;quot;时，倾向于悄悄改规范迁就实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：把规范目录在规则里声明为&lt;strong&gt;全模式只读&lt;/strong&gt;（promptkit 的 &lt;code&gt;ai_work/spec/&lt;/code&gt; 就是这么做的），规范的变更只能由人发起。&lt;/p&gt;
&lt;h3 id=&#34;136-规划与实现脱节&#34;&gt;13.6 规划与实现脱节&lt;/h3&gt;
&lt;p&gt;AI 在 PLAN 模式出了漂亮的方案，到了 ACT 模式就自由发挥，交付物和计划对不上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：用交接协议强制——实现必须引用计划中的 step ID，发现计划不可行必须暂停回溯，不允许擅自偏离（见第五节）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十四prompt-工程的核心原则&#34;&gt;十四、Prompt 工程的核心原则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;把 Prompt 当接口设计&lt;/strong&gt;：输入完整、输出可解析、边界可控，像设计函数签名一样设计 Prompt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把 Prompt 当代码管理&lt;/strong&gt;：版本控制、CHANGELOG、测试、Review 缺一不可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按加载时机分层&lt;/strong&gt;：全局规则 / 模式规则 / 只读规范，分开管理注意力预算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文要显式化&lt;/strong&gt;：Memory Bank 这类结构化上下文文件，比依赖会话记忆可靠得多&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相关性优先于信息量&lt;/strong&gt;：无关上下文会稀释注意力，检索结果要重排序，工作记忆要定期清理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程编码进 Prompt&lt;/strong&gt;：不要指望 AI 自觉遵守工程流程，把流程画成它必须执行的步骤&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂任务编排成步骤&lt;/strong&gt;：用 ReAct 等模式拆解任务、设计异常恢复，而不是一个大 Prompt 包打天下&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要求依据，而非答案&lt;/strong&gt;：让模型给置信度、引用素材、指出行号，低置信度进人工队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 之间的交接要有协议&lt;/strong&gt;：结构化输出 + ID 追溯 + 暂停回溯机制&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;：Schema + 后端校验是底线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用数据驱动优化&lt;/strong&gt;：评测集、技术指标 + 业务指标、回归门禁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收集失败样本&lt;/strong&gt;：失败案例是最值钱的评测集增量&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十五总结&#34;&gt;十五、总结&lt;/h2&gt;
&lt;p&gt;Prompt Engineering 不是玄学，也不是简单的&amp;quot;提示词技巧&amp;quot;。它至少是三层东西的组合：&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;graph TD
    L3[&amp;#34;工程管理层&amp;lt;br/&amp;gt;版本控制 / CHANGELOG / 评测回归 / 成本路由&amp;#34;]
    L2[&amp;#34;系统设计层&amp;lt;br/&amp;gt;规则分层 / Memory Bank / 双模式工作流 / 交接协议 / Agent 编排&amp;#34;]
    L1[&amp;#34;文本技巧层&amp;lt;br/&amp;gt;角色设定 / CoT / Few-shot / Schema 约束&amp;#34;]
    L3 --&amp;gt; L2 --&amp;gt; L1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;大多数人讨论的&amp;quot;Prompt 技巧&amp;quot;只在最底层打转；真正把 LLM 应用做稳定的团队，都在上两层下功夫：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;系统设计层&lt;/strong&gt;：Prompt 决定 Agent 能做什么、不能做什么，读写权限、模式切换、交接协议、异常恢复都是架构决策&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程管理层&lt;/strong&gt;：Prompt 是需要版本、测试、评估、变更日志管理的资产，也是直接的成本中心&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方糖猫的视觉规划 Prompt 从 V1 到 V3 提升的是单点输出质量；promptkit 用一套规则库约束的是整个 AI 开发流程的行为。两者路径不同，结论一致：&lt;strong&gt;Prompt 工程的天花板不在&amp;quot;会不会写提示词&amp;quot;，而在&amp;quot;有没有把它当工程做&amp;quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，不要花太多时间寻找&amp;quot;万能 Prompt&amp;quot;。更值钱的能力是：&lt;strong&gt;把业务问题拆解成模型能稳定执行的子任务，再用工程化的方法把它变成可交付、可维护的系统。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 LLM 应用开发或 AI Coding 落地，欢迎交流 Prompt 版本管理、规则分层和回归测试的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>多 Worker 分布式渲染：从 HTTP 轮询到 RocketMQ</title>
      <link>https://ftmao.com/blog/posts/roketmq/</link>
      <pubDate>Fri, 13 Mar 2026 13:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/roketmq/</guid>
      <description>&lt;h1 id=&#34;多-worker-分布式渲染从-http-轮询到-rocketmq&#34;&gt;多 Worker 分布式渲染：从 HTTP 轮询到 RocketMQ&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的方糖猫平台的核心链路最后一步是视频渲染。用户提交生成任务后，系统需要把故事板、配音、素材合成为最终的视频文件。这个步骤 CPU/GPU 密集、耗时长、失败率高，单机渲染很快成为瓶颈。这篇文章会复盘我们从 HTTP 轮询架构迁移到 RocketMQ Push 架构的全过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一渲染任务的特性&#34;&gt;一、渲染任务的特性&lt;/h2&gt;
&lt;p&gt;AI 视频平台的渲染任务有几个典型特征：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;特性&lt;/th&gt;
					&lt;th&gt;影响&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;耗时长&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一个 60 秒视频渲染可能需要 30 秒到 5 分钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;资源密集&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;CPU/GPU 占用高，单机并发有限&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;失败率高&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;素材缺失、字体缺失、内存不足、网络超时都可能失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;不均衡&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;不同分辨率、不同时长任务耗时差异大&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;需要重试&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;失败后最好换一台 Worker 重试，而不是一直卡在原机&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些特性决定了渲染不能放在主服务里同步执行，必须用异步任务队列 + 多 Worker 架构。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二第一代架构http-轮询&#34;&gt;二、第一代架构：HTTP 轮询&lt;/h2&gt;
&lt;h3 id=&#34;21-设计思路&#34;&gt;2.1 设计思路&lt;/h3&gt;
&lt;p&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;sequenceDiagram
    participant A as API 服务
    participant W as Worker 服务
    A-&amp;gt;&amp;gt;W: POST 任务
    A-&amp;gt;&amp;gt;W: 查询状态
    W--&amp;gt;&amp;gt;A: 渲染完成&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;API 服务把渲染任务通过 HTTP POST 发给某个 Worker&lt;/li&gt;
&lt;li&gt;Worker 开始渲染，API 服务定时轮询状态&lt;/li&gt;
&lt;li&gt;渲染完成后，Worker 回调 API 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-代码示例&#34;&gt;2.2 代码示例&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; requests
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; time
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderService&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submit&lt;/span&gt;(self, task_id, render_params):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        worker_url &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pick_worker()  &lt;span style=&#34;color:#75715e&#34;&gt;# 简单轮询选一个&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        requests&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;post(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;worker_url&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/render&amp;#34;&lt;/span&gt;, json&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;render_params, timeout&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 轮询等待完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; _ &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;300&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; requests&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;worker_url&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/status/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;task_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; status[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; status[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;23-问题很快暴露&#34;&gt;2.3 问题很快暴露&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Worker 负载不均&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;轮询分配无法感知 Worker 当前负载&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;单点故障&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;某个 Worker 挂了，上面的任务就卡住&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;扩缩容麻烦&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;新增 Worker 需要改 API 服务配置&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;失败重试复杂&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;需要 API 服务记录每个任务发给了哪个 Worker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Windows 开发环境问题&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;开发环境只有一台机器，无法模拟多 Worker&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个方案支撑了产品早期验证，但当日均任务量超过几百条后，稳定性和扩展性都跟不上了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三第二代架构rocketmq-push&#34;&gt;三、第二代架构：RocketMQ Push&lt;/h2&gt;
&lt;h3 id=&#34;31-为什么选-rocketmq&#34;&gt;3.1 为什么选 RocketMQ&lt;/h3&gt;
&lt;p&gt;我们对比了几种消息队列：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTP 轮询&lt;/td&gt;
					&lt;td&gt;简单&lt;/td&gt;
					&lt;td&gt;负载不均、失败处理差&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RabbitMQ&lt;/td&gt;
					&lt;td&gt;功能全面&lt;/td&gt;
					&lt;td&gt;高可用配置复杂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Kafka&lt;/td&gt;
					&lt;td&gt;吞吐高&lt;/td&gt;
					&lt;td&gt;消费端管理重，延迟不如 RocketMQ&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RocketMQ&lt;/td&gt;
					&lt;td&gt;负载均衡好、顺序消息、延迟消息、监控完善&lt;/td&gt;
					&lt;td&gt;部署稍重&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择 RocketMQ 主要是因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原生支持 Push Consumer，自动负载均衡&lt;/li&gt;
&lt;li&gt;NameServer 架构适合弹性扩缩容&lt;/li&gt;
&lt;li&gt;消息重试、死信队列机制完善&lt;/li&gt;
&lt;li&gt;团队对阿里系中间件比较熟悉&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-新架构设计&#34;&gt;3.2 新架构设计&lt;/h3&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
    A[&amp;#34;API 服务&amp;#34;] -- &amp;#34;发送渲染任务&amp;#34; --&amp;gt; T[&amp;#34;RocketMQ Topic: video-render&amp;#34;]
    T --&amp;gt; W1[&amp;#34;Worker-1&amp;#34;]
    T --&amp;gt; W2[&amp;#34;Worker-2&amp;#34;]
    T --&amp;gt; WN[&amp;#34;Worker-N&amp;#34;]
    W1 --&amp;gt; D[&amp;#34;渲染完成后更新数据库&amp;#34;]
    W2 --&amp;gt; D
    WN --&amp;gt; D
    D --&amp;gt; S[&amp;#34;SSE 推送进度给用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 服务只负责发消息，不关心任务被哪个 Worker 消费&lt;/li&gt;
&lt;li&gt;Worker 从 RocketMQ 拉取任务，自动负载均衡&lt;/li&gt;
&lt;li&gt;渲染完成后 Worker 更新数据库，并通过 SSE 通知前端&lt;/li&gt;
&lt;li&gt;新增 Worker 只需启动新实例，自动加入消费组&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-生产者实现&#34;&gt;3.3 生产者实现&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; rocketmq.client &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Producer, Message
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Producer(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;render-producer-group&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_name_server_address(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;localhost:9876&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submit_render_task&lt;/span&gt;(task_id: str, params: dict):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    body &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;: task_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;: params,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;submitted_at&amp;#34;&lt;/span&gt;: time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;time()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Message(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_keys(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_tags(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;normal&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_body(body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    producer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;send_sync(msg)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-消费者实现&#34;&gt;3.4 消费者实现&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; rocketmq.client &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; PushConsumer, ConsumeStatus
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderConsumer&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; PushConsumer(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;render-consumer-group&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_name_server_address(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;localhost:9876&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;subscribe(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;, self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;on_message)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            task_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            params &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 幂等检查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_task_processed(task_id):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 更新任务状态为处理中&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 执行渲染&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;render(params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 更新任务状态为完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            logger&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;error(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Render failed: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;consumer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四关键设计细节&#34;&gt;四、关键设计细节&lt;/h2&gt;
&lt;h3 id=&#34;41-幂等处理&#34;&gt;4.1 幂等处理&lt;/h3&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;is_task_processed&lt;/span&gt;(task_id: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;基于数据库状态做幂等判断&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; db&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_task(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; task&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;status &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;如果任务已经是 &lt;code&gt;processing&lt;/code&gt; 状态，说明可能被其他 Worker 在处理，需要进一步判断心跳时间，避免任务永远卡住。&lt;/p&gt;
&lt;h3 id=&#34;42-心跳与任务认领&#34;&gt;4.2 心跳与任务认领&lt;/h3&gt;
&lt;p&gt;每个 Worker 在处理任务时会定时更新心跳：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;update_task_status&lt;/span&gt;(task_id, status, result&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    db&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        UPDATE render_task
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        SET status = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            result = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            worker_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            heartbeat_at = NOW()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        WHERE task_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;, (status, result, WORKER_ID, task_id))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;如果某个 Worker 挂了，心跳超过阈值的任务会被标记为失败，重新进入队列重试。&lt;/p&gt;
&lt;h3 id=&#34;43-失败重试与死信队列&#34;&gt;4.3 失败重试与死信队列&lt;/h3&gt;
&lt;p&gt;RocketMQ 支持消费失败自动重试，默认 16 次，间隔递增。如果还是失败，消息进入死信队列：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 消费失败时返回重试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对于死信消息，我们单独处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分析失败原因&lt;/li&gt;
&lt;li&gt;通知运营或用户&lt;/li&gt;
&lt;li&gt;部分场景自动降级（比如降低分辨率重试）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-并发控制&#34;&gt;4.4 并发控制&lt;/h3&gt;
&lt;p&gt;每个 Worker 同时能处理几个任务，取决于机器配置。我们通过线程池控制：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; concurrent.futures &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; ThreadPoolExecutor
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 根据 CPU/GPU 核心数设置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;executor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ThreadPoolExecutor(max_workers&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    future &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; executor&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;submit(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;handle_render, msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 注意：PushConsumer 是阻塞回调，这里需要等待结果&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; future&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;result()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;实际部署中，我们更推荐一个 Worker 进程只消费一个任务，通过启动多个 Worker 进程来提升并发。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五windows-开发环境兼容&#34;&gt;五、Windows 开发环境兼容&lt;/h2&gt;
&lt;h3 id=&#34;51-问题&#34;&gt;5.1 问题&lt;/h3&gt;
&lt;p&gt;RocketMQ 在 Windows 上部署比较麻烦，开发环境有时候只有一台机器。为了不影响开发效率，我们设计了一个降级策略：&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&gt;生产环境：RocketMQ Push Consumer
开发环境：HTTP 轮询模拟（单 Worker）&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-抽象层设计&#34;&gt;5.2 抽象层设计&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TaskConsumer&lt;/span&gt;(ABC):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RocketMQConsumer&lt;/span&gt;(TaskConsumer):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;生产环境使用&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;HttpPollConsumer&lt;/span&gt;(TaskConsumer):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;开发环境使用&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 根据配置切换&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;consumer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; RocketMQConsumer() &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; env &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;production&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; HttpPollConsumer()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这样业务逻辑完全一致，只是消息消费方式不同。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六监控与告警&#34;&gt;六、监控与告警&lt;/h2&gt;
&lt;h3 id=&#34;61-关键指标&#34;&gt;6.1 关键指标&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;队列堆积量&lt;/td&gt;
					&lt;td&gt;判断是否需要扩容 Worker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务平均处理时间&lt;/td&gt;
					&lt;td&gt;评估渲染性能和资源需求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务成功率&lt;/td&gt;
					&lt;td&gt;健康度核心指标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Worker 在线数量&lt;/td&gt;
					&lt;td&gt;消费组健康状态&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重试次数分布&lt;/td&gt;
					&lt;td&gt;发现系统性问题&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;62-告警规则&#34;&gt;6.2 告警规则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;队列堆积超过 100 条，持续 5 分钟 → 扩容提醒&lt;/li&gt;
&lt;li&gt;任务成功率低于 95% → 人工介入&lt;/li&gt;
&lt;li&gt;Worker 数量异常减少 → 服务异常告警&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七演进效果&#34;&gt;七、演进效果&lt;/h2&gt;
&lt;p&gt;迁移到 RocketMQ 后，渲染系统的稳定性明显提升：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标&lt;/th&gt;
					&lt;th&gt;HTTP 轮询&lt;/th&gt;
					&lt;th&gt;RocketMQ Push&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;平均等待时间&lt;/td&gt;
					&lt;td&gt;X 分钟&lt;/td&gt;
					&lt;td&gt;Y 分钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;任务成功率&lt;/td&gt;
					&lt;td&gt;X%&lt;/td&gt;
					&lt;td&gt;Y%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Worker 利用率&lt;/td&gt;
					&lt;td&gt;不均&lt;/td&gt;
					&lt;td&gt;均衡&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩容成本&lt;/td&gt;
					&lt;td&gt;需要改配置&lt;/td&gt;
					&lt;td&gt;启动实例即可&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;失败重试&lt;/td&gt;
					&lt;td&gt;自建逻辑&lt;/td&gt;
					&lt;td&gt;原生支持&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;（待补充真实数据）&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八踩过的坑&#34;&gt;八、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;81-消息体过大&#34;&gt;8.1 消息体过大&lt;/h3&gt;
&lt;p&gt;早期的渲染参数里包含完整的素材 URL 列表，消息体很大。RocketMQ 默认单条消息限制 4MB，我们遇到过消息发送失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：消息体只传 &lt;code&gt;task_id&lt;/code&gt; 和关键参数，Worker 消费后自己从数据库/redis 拉取完整参数。&lt;/p&gt;
&lt;h3 id=&#34;82-消费组订阅不一致&#34;&gt;8.2 消费组订阅不一致&lt;/h3&gt;
&lt;p&gt;不同 Worker 用同一个消费组但订阅了不同 Tag，导致消息分发混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：严格规范消费组命名和订阅规则，每个消费组只负责一个 Topic。&lt;/p&gt;
&lt;h3 id=&#34;83-任务卡住没有超时释放&#34;&gt;8.3 任务卡住没有超时释放&lt;/h3&gt;
&lt;p&gt;某个 Worker 处理到一半进程挂了，任务状态一直是 &lt;code&gt;processing&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：增加心跳检测，心跳超时的任务自动重置为 &lt;code&gt;pending&lt;/code&gt;，重新入队。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;视频渲染是 AI 视频平台里最典型的「重任务」场景，必须用异步队列 + 分布式 Worker 来解决。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要过早优化，但也不能过度将就&lt;/strong&gt;。HTTP 轮询能验证产品，但流量起来后必须升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ 的 Push Consumer 很适合这种场景&lt;/strong&gt;，负载均衡和重试机制省去很多自建成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等和心跳是必须的&lt;/strong&gt;，否则任务会丢或卡住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控要跟上&lt;/strong&gt;，队列堆积和成功率是最核心的两个指标。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做类似的异步任务处理系统，欢迎交流消息队列选型和 Worker 调度经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>口播视频自动剪辑：Whisper 词级时间轴 &#43; LLM 视觉规划</title>
      <link>https://ftmao.com/blog/posts/usewhispher/</link>
      <pubDate>Mon, 23 Feb 2026 12:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/usewhispher/</guid>
      <description>&lt;h1 id=&#34;口播视频自动剪辑whisper-词级时间轴--llm-视觉规划&#34;&gt;口播视频自动剪辑：Whisper 词级时间轴 + LLM 视觉规划&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的 GeekCast Engine 是一个面向口播类视频的 AI 自动剪辑平台。用户上传一段口播视频后，系统自动完成转写、粗剪、视觉规划和成片渲染。这篇文章会重点拆解两个核心模块：&lt;strong&gt;Whisper 词级时间轴处理&lt;/strong&gt; 和 &lt;strong&gt;LLM 视觉元素规划&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&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;：「嗯」「啊」「这个」「那个」需要逐个删掉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节奏单调&lt;/strong&gt;：长时间只有说话人画面，观众容易疲劳&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信息密度不均&lt;/strong&gt;：有些段落需要配合 B-roll、标题卡、代码块来强化表达&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长视频处理困难&lt;/strong&gt;：10 分钟以上的口播，人工剪辑耗时往往在数小时&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们的目标不是完全替代剪辑师，而是把&lt;strong&gt;重复、机械、规则明确&lt;/strong&gt;的工作自动化，让人专注于创意和审校。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二整体技术架构&#34;&gt;二、整体技术架构&lt;/h2&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
    A[&amp;#34;口播视频上传&amp;#34;] --&amp;gt; B[&amp;#34;Whisper 词级转写&amp;#34;]
    B --&amp;gt; C[&amp;#34;LLM 语义分析 + 视觉规划&amp;#34;]
    C --&amp;gt; D[&amp;#34;自动粗剪（删气口/语气词/冗余片段）&amp;#34;]
    D --&amp;gt; E[&amp;#34;Remotion 组件化渲染&amp;#34;]
    E --&amp;gt; F[&amp;#34;FFmpeg 后处理输出成片&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;整个流程里，最关键的两个环节是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间轴精度&lt;/strong&gt;：决定了粗剪是否精准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉规划质量&lt;/strong&gt;：决定了成片是否好看&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三whisper-词级时间轴&#34;&gt;三、Whisper 词级时间轴&lt;/h2&gt;
&lt;h3 id=&#34;31-为什么选择-whisper&#34;&gt;3.1 为什么选择 Whisper&lt;/h3&gt;
&lt;p&gt;OpenAI Whisper 是目前开源语音识别模型中最成熟的选择之一，优势很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持 99 种语言，中文识别效果不错&lt;/li&gt;
&lt;li&gt;提供 &lt;code&gt;word-level timestamps&lt;/code&gt;（词级时间戳），这是自动剪辑的基础&lt;/li&gt;
&lt;li&gt;模型大小可选，从小到大有 tiny/base/small/medium/large&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-词级时间戳的获取&#34;&gt;3.2 词级时间戳的获取&lt;/h3&gt;
&lt;p&gt;Whisper 本身不直接输出词级时间戳，需要借助 &lt;code&gt;whisper-timestamped&lt;/code&gt; 或 &lt;code&gt;faster-whisper&lt;/code&gt; 等工具。我们用 &lt;code&gt;faster-whisper&lt;/code&gt;，因为它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推理速度更快&lt;/li&gt;
&lt;li&gt;支持 VAD（语音活动检测），减少静音误判&lt;/li&gt;
&lt;li&gt;对长音频更稳定&lt;/li&gt;
&lt;/ul&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; faster_whisper &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; WhisperModel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; WhisperModel(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;small&amp;#34;&lt;/span&gt;, device&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cuda&amp;#34;&lt;/span&gt;, compute_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;float16&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;segments, info &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; model&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transcribe(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;input.mp3&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    word_timestamps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    vad_filter&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    language&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;zh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;words &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; segment &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; segments:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; word &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; segment&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;words:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        words&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;: word&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;word,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: word&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: word&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;end,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;probability&amp;#34;&lt;/span&gt;: word&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;probability
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        })&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-词级数据结构&#34;&gt;3.3 词级数据结构&lt;/h3&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;大家好&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.32&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.98&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;probability&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.95&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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&gt;Paragraph
  ├── Sentence 1
  │     ├── Word 1
  │     ├── Word 2
  │     └── Word 3
  └── Sentence 2
        ├── Word 4
        └── Word 5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-气口和语气词删除&#34;&gt;3.4 气口和语气词删除&lt;/h3&gt;
&lt;p&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 语气词列表&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;FILLER_WORDS &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;嗯&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;啊&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;这个&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;那个&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;就是&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;然后&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;对吧&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;remove_fillers&lt;/span&gt;(words, filler_words&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;FILLER_WORDS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;删除单个语气词，保留相邻词的时间连续性&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; words:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; w[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip() &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; filler_words:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(w)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;remove_long_pauses&lt;/span&gt;(words, threshold&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.8&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;删除过长的停顿片段&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, w &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; enumerate(words):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            gap &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; w[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;start&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; words[i&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;][&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;end&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; gap &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; threshold:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 也可以只压缩，不完全删除&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(w)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;35-时间轴漂移问题&#34;&gt;3.5 时间轴漂移问题&lt;/h3&gt;
&lt;p&gt;实际使用中发现，Whisper 的词级时间戳在语速快、口音重、背景音乐干扰时会出现漂移。我们做了几个处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;置信度过滤&lt;/strong&gt;：识别概率低于 0.7 的词标记为待审校&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;句子级对齐&lt;/strong&gt;：用 VAD 检测到的语音段和转写句子做二次对齐&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人工修正入口&lt;/strong&gt;：前端提供波形图 + 时间轴，允许用户微调&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四llm-视觉规划&#34;&gt;四、LLM 视觉规划&lt;/h2&gt;
&lt;h3 id=&#34;41-视觉元素类型&#34;&gt;4.1 视觉元素类型&lt;/h3&gt;
&lt;p&gt;我们定义了口播视频中常用的几类视觉元素：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;元素类型&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
					&lt;th&gt;示例&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;标题卡&lt;/td&gt;
					&lt;td&gt;章节切换、强调重点&lt;/td&gt;
					&lt;td&gt;大标题 + 副标题&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;B-roll&lt;/td&gt;
					&lt;td&gt;缓解视觉疲劳，补充画面&lt;/td&gt;
					&lt;td&gt;相关图片/视频片段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;引用卡&lt;/td&gt;
					&lt;td&gt;引用名人名言、数据&lt;/td&gt;
					&lt;td&gt;金句 + 出处&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;代码块&lt;/td&gt;
					&lt;td&gt;技术类口播展示代码&lt;/td&gt;
					&lt;td&gt;高亮代码片段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;列表卡&lt;/td&gt;
					&lt;td&gt;展示多个要点&lt;/td&gt;
					&lt;td&gt;1/2/3 列表&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;42-llm-规划任务&#34;&gt;4.2 LLM 规划任务&lt;/h3&gt;
&lt;p&gt;输入是一段按句子切分好的口播文本，输出是每个句子应该配什么视觉元素。Prompt 设计如下：&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&gt;你是一名专业的口播视频剪辑师。请根据以下口播文案，为每个句子规划合适的视觉元素。

可用视觉元素：
- TitleCard: 标题卡，用于章节开头或强调核心观点
- BRoll: B-roll 画面，用于补充说明内容
- QuoteCard: 引用卡，用于展示名言、数据
- CodeBlock: 代码块，用于展示代码
- ListCard: 列表卡，用于展示多个要点
- TalkingHead: 保持说话人画面，不添加额外元素

口播文案：
{script}

请输出 JSON 数组，每个元素包含：
- sentence_index: 句子序号
- element_type: 视觉元素类型
- reason: 选择理由
- suggested_content: 建议展示的内容（如 B-roll 的画面描述、标题文字等）&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-输出结构化&#34;&gt;4.3 输出结构化&lt;/h3&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TitleCard&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;开场需要点题&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;suggested_content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AI 自动剪辑实战&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BRoll&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;解释&amp;#39;时间轴&amp;#39;概念时需要补充画面&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;suggested_content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;视频编辑软件中的时间轴界面&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sentence_index&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CodeBlock&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;展示 Whisper 调用代码&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;suggested_content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;faster_whisper 示例代码&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-与素材库联动&#34;&gt;4.4 与素材库联动&lt;/h3&gt;
&lt;p&gt;视觉规划中的 &lt;code&gt;suggested_content&lt;/code&gt; 会作为查询条件，调用我们在第二篇文章中讲的 RAG 素材库，检索匹配的 B-roll、图片或文案片段。&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;plan_and_retrieve&lt;/span&gt;(script: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 1. LLM 做视觉规划&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    plan &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;generate_visual_plan(script)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 对每个 B-roll 规划项检索素材&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; item &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; plan:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; item[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;element_type&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;BRoll&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            item[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;materials&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; rag_retrieve(item[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;suggested_content&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; plan&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五长视频-token-限制处理&#34;&gt;五、长视频 Token 限制处理&lt;/h2&gt;
&lt;p&gt;口播视频动辄 10 分钟以上，对应的文案可能超过 3000 字，直接塞给 LLM 做视觉规划会触发 Token 限制，而且单次规划效果也不好。&lt;/p&gt;
&lt;p&gt;我们的解决方案是：&lt;strong&gt;分段生成 + 本地组装&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;51-分段策略&#34;&gt;5.1 分段策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;按段落切分：每段约 800-1200 字&lt;/li&gt;
&lt;li&gt;保留上下文：每段开头会附带上一段的结尾摘要&lt;/li&gt;
&lt;li&gt;独立规划：每段分别调用 LLM 做视觉规划&lt;/li&gt;
&lt;li&gt;全局对齐：最后统一调整时间轴、样式和转场&lt;/li&gt;
&lt;/ul&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;split_script&lt;/span&gt;(script: str, max_chars: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;按段落切分文案，尽量保持段落完整性&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    paragraphs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; script&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    chunks &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    current &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; p &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; paragraphs:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; len(current) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; len(p) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; max_chars &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; current:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            chunks&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(current)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            current &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; p
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            current &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; p
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; current:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        chunks&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(current)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; chunks&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-本地组装&#34;&gt;5.2 本地组装&lt;/h3&gt;
&lt;p&gt;分段规划完成后，需要在本地合并：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一句子索引&lt;/li&gt;
&lt;li&gt;统一视觉风格（字体、配色、动画）&lt;/li&gt;
&lt;li&gt;处理段与段之间的转场&lt;/li&gt;
&lt;li&gt;生成最终的 Remotion 渲染数据&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六remotion-组件化渲染&#34;&gt;六、Remotion 组件化渲染&lt;/h2&gt;
&lt;h3 id=&#34;61-为什么用-remotion&#34;&gt;6.1 为什么用 Remotion&lt;/h3&gt;
&lt;p&gt;Remotion 让我们可以用 React 组件来描述视频画面，非常契合「视觉元素 = 组件」的思路。&lt;/p&gt;
&lt;p&gt;每个视觉元素对应一个 React 组件：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tsx&#34; data-lang=&#34;tsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 标题卡组件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TitleCard&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;React.FC&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;{ &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;subtitle&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; }&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ({ &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;subtitle&lt;/span&gt; }) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;AbsoluteFill&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{{ &lt;span style=&#34;color:#a6e22e&#34;&gt;backgroundColor&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;#111&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;justifyContent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;center&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;alignItems&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;center&amp;#39;&lt;/span&gt; }}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{{ &lt;span style=&#34;color:#a6e22e&#34;&gt;color&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;#fff&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fontSize&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;80&lt;/span&gt; }}&amp;gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{{ &lt;span style=&#34;color:#a6e22e&#34;&gt;color&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;#aaa&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fontSize&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;36&lt;/span&gt; }}&amp;gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;subtitle&lt;/span&gt;}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;AbsoluteFill&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-渲染流程&#34;&gt;6.2 渲染流程&lt;/h3&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
    A[&amp;#34;视觉规划结果&amp;#34;] --&amp;gt; B[&amp;#34;生成 Remotion composition（每个视觉元素对应一个组件实例）&amp;#34;]
    B --&amp;gt; C[&amp;#34;Remotion 渲染为视频文件&amp;#34;]
    C --&amp;gt; D[&amp;#34;FFmpeg 后处理：合成音频、加字幕、转码、多分辨率输出&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;63-多分辨率输出&#34;&gt;6.3 多分辨率输出&lt;/h3&gt;
&lt;p&gt;平台需要同时支持横屏（16:9）和竖屏（9:16），我们在 Remotion 组件内部做响应式布局：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tsx&#34; data-lang=&#34;tsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useVideoConfig&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isVertical&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;七踩过的坑&#34;&gt;七、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;71-whisper-模型选择纠结&#34;&gt;7.1 Whisper 模型选择纠结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tiny&lt;/code&gt; 速度最快但准确率太低&lt;/li&gt;
&lt;li&gt;&lt;code&gt;large&lt;/code&gt; 准确率最高但显存和速度压力大&lt;/li&gt;
&lt;li&gt;最终我们选 &lt;code&gt;small&lt;/code&gt; 做默认模型，&lt;code&gt;large&lt;/code&gt; 做可选质量模式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-llm-规划风格不一致&#34;&gt;7.2 LLM 规划风格不一致&lt;/h3&gt;
&lt;p&gt;同一个文案多次调用 LLM，返回的视觉规划可能风格差异很大。我们做了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompt 里加入平台风格示例&lt;/li&gt;
&lt;li&gt;视觉元素类型和样式模板固定&lt;/li&gt;
&lt;li&gt;对返回结果做规则校验和兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-代码块渲染需要特殊处理&#34;&gt;7.3 代码块渲染需要特殊处理&lt;/h3&gt;
&lt;p&gt;代码块里的长行、特殊字符、换行等都会影响 Remotion 渲染。我们专门封装了一个 &lt;code&gt;CodeBlock&lt;/code&gt; 组件，处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动换行和字体缩放&lt;/li&gt;
&lt;li&gt;语法高亮&lt;/li&gt;
&lt;li&gt;行号显示&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八效果与反思&#34;&gt;八、效果与反思&lt;/h2&gt;
&lt;p&gt;目前 GeekCast Engine 能在一分钟左右完成 5-10 分钟口播视频的自动粗剪和视觉规划，最终成片需要人工简单审校即可发布。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;量化效果（待补充真实数据）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10 分钟口播视频，人工剪辑平均 X 小时，自动剪辑约 X 分钟&lt;/li&gt;
&lt;li&gt;气口和语气词删除覆盖率约 X%&lt;/li&gt;
&lt;li&gt;LLM 视觉规划采纳率约 X%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;反思&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动剪辑还不能做到 100% 无人工，但已经能替代 60%-70% 的重复劳动&lt;/li&gt;
&lt;li&gt;视觉规划比粗剪更难，未来需要结合用户反馈做迭代优化&lt;/li&gt;
&lt;li&gt;长视频场景下，分段策略和上下文保留是关键&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;口播视频自动剪辑是把 ASR、LLM、视频渲染三项技术串联起来的典型 AI 工程场景。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;词级时间轴是粗剪的基础&lt;/strong&gt;，Whisper + faster-whisper 是性价比较高的方案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 视觉规划要把输出结构固定&lt;/strong&gt;，Prompt 设计决定可控性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长视频必须分段处理&lt;/strong&gt;，本地组装时注意风格和转场一致性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remotion 让视频渲染工程化&lt;/strong&gt;，组件化思路适合多变的视觉需求&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 AI 视频生成或自动剪辑相关的项目，欢迎交流 Whisper 调优和 LLM 规划的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix 的 LLM 后训练框架：从「跑通脚本」到「管好四根支柱」</title>
      <link>https://ftmao.com/blog/posts/netflix-llm-post-training/</link>
      <pubDate>Fri, 13 Feb 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-llm-post-training/</guid>
      <description>&lt;h1 id=&#34;netflix-的-llm-后训练框架从跑通脚本到管好四根支柱&#34;&gt;Netflix 的 LLM 后训练框架：从「跑通脚本」到「管好四根支柱」&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/scaling-llm-post-training-at-netflix-0046f8790194&#34;&gt;Scaling LLM Post-Training at Netflix&lt;/a&gt;
目标读者：做过或想做 LLM 微调/强化学习的工程师，尤其是正在考虑「自建训练管线还是买现成框架」的人。
阅读目标：搞清楚两件事——后训练在 Netflix 规模下到底难在哪；以及一个内部框架靠什么证明自己比「组装开源组件」更有价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;先拆一个直觉后训练不就是拿个开源模型-finetune-一下吗&#34;&gt;〇、先拆一个直觉：后训练不就是「拿个开源模型 finetune 一下」吗&lt;/h2&gt;
&lt;p&gt;直觉版本的后训练长这样：准备点私有数据，从 Hugging Face 拉一个开源权重，写个 batch 循环迭代。实验规模下确实就几行代码。原文的核心论点是：&lt;strong&gt;从「跑通脚本」到「生产级后训练」之间隔着一整条工程鸿沟&lt;/strong&gt;——数据管线、多机 GPU 集群的分布式状态、训练与推理交错的工作流，每一个都是深渊。&lt;/p&gt;
&lt;p&gt;Netflix 的场景是推荐、个性化和搜索，需要让通用基座模型理解自家片库和用户交互历史。这种场景下，post-training 很快就不再是建模问题，而是分布式系统问题。AI Platform 团队建这个框架的目标很直白：&lt;strong&gt;把基础设施复杂度藏起来，让模型开发者专注模型本身，而不是分布式管道工。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;一三个环节每个都有自己的坑&#34;&gt;一、三个环节，每个都有自己的坑&lt;/h2&gt;
&lt;p&gt;原文把后训练旅程拆成三步，每一步都有一批「纸面上没有」的坑。&lt;/p&gt;
&lt;h3 id=&#34;数据难的不是读取是精确控制&#34;&gt;数据：难的不是读取，是精确控制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Loss masking 是命门。&lt;/strong&gt; HF chat template 只负责把对话序列化成文本，不告诉你哪些 token 该进 loss。必须显式做 loss mask，只让 assistant 的 token 参与优化——否则模型会去学习 prompt 和非目标文本，质量直接掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变长序列是隐形浪费。&lt;/strong&gt; batch 内 padding 浪费算力，FSDP 各 worker 形状不齐会带来 GPU 同步开销。更省的做法是 &lt;strong&gt;sequence packing&lt;/strong&gt;：把多个样本塞进定长序列，再用 document mask 阻止跨样本注意力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;模型加载不了只是开始&#34;&gt;模型：加载不了只是开始&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;模型放不进单卡时，需要 FSDP/TP 分片，并且要&lt;strong&gt;把权重分片直接加载到 device mesh 上&lt;/strong&gt;，避免在任何单设备上物化完整模型。&lt;/li&gt;
&lt;li&gt;加载完还要让它「可训练」：全量微调还是 LoRA，activation checkpointing、编译、精度设置——RL 场景下 rollout 和 policy 的精度必须对齐，这是容易翻车的细节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大词表（＞128k）的内存陷阱&lt;/strong&gt;：logits 形状是 batch × seq_len × vocab，峰值内存会炸。常见缓解：投影前先丢掉被忽略的 token，沿序列维度分块算 logits 和 loss。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;训练生产训练不是一个-for-循环&#34;&gt;训练：生产训练不是一个 for 循环&lt;/h3&gt;
&lt;p&gt;要支持从 SFT 的单次前反向传播，到 on-policy RL 那种「rollout 生成、reward/reference 推理、policy 更新交错进行」的工作流。Netflix 用 Ray 的 actor 模型做编排，把建模逻辑和硬件解耦。另外两样是标配：实验追踪（loss 这类质量指标 + MFU 这类效率指标）和标准化 checkpoint 带来的容错恢复。&lt;/p&gt;
&lt;h2 id=&#34;二框架总览四根支柱&#34;&gt;二、框架总览：四根支柱&lt;/h2&gt;
&lt;p&gt;整个技术栈从底到顶是：Mako（Netflix 内部 ML 计算平台，在 AWS 上供给 GPU）→ 开源三件套（PyTorch、Ray、vLLM，基本开箱即用）→ Post-Training Framework（以库的形式提供 SFT、DPO、RL、知识蒸馏等标准化 recipe，用户用配置文件选配方、插任务组件）。&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
    subgraph APP[&amp;#34;使用者: 模型开发者&amp;#34;]
        CFG[&amp;#34;配置文件&amp;lt;br/&amp;gt;选 recipe 插组件&amp;#34;]
    end
    subgraph FW[&amp;#34;Post-Training Framework&amp;#34;]
        R1[&amp;#34;SFT&amp;#34;] --- R2[&amp;#34;DPO&amp;#34;] --- R3[&amp;#34;RL&amp;#34;] --- R4[&amp;#34;蒸馏&amp;#34;]
    end
    subgraph OSS[&amp;#34;开源基础组件&amp;#34;]
        PT[&amp;#34;PyTorch&amp;#34;] --- RAY[&amp;#34;Ray&amp;#34;] --- VLLM[&amp;#34;vLLM&amp;#34;]
    end
    subgraph INFRA[&amp;#34;基础设施&amp;#34;]
        MAKO[&amp;#34;Mako 内部 ML 计算平台&amp;lt;br/&amp;gt;在 AWS 上供给 GPU&amp;#34;]
    end
    CFG --&amp;gt; FW
    FW --&amp;gt; OSS
    OSS --&amp;gt; MAKO
    style FW fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;框架的能力沿四个维度组织，对应 ML 系统的三支柱加一个新增支柱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;：SFT、奖励建模、RL 各自的数据集抽象；超本地存储的数据集从云和磁盘做高吞吐流式读取；&lt;strong&gt;异步 on-the-fly packing&lt;/strong&gt;，把 CPU 密集的打包和 GPU 计算重叠，减少空闲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;：支持现代架构（Qwen3、Gemma3）和 MoE 变体（Qwen3 MoE、GPT-OSS）；LoRA 直接集成进模型定义；高层 sharding API，开发者不用写底层分布式代码就能跨 device mesh 分片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute&lt;/strong&gt;：统一作业提交接口，单机到几百卡一个入口；自定义架构和 LoRA 下依然准确的 MFU 监控；完整 checkpoint（参数、优化器、dataloader、data mixer 状态），中断后精确恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow&lt;/strong&gt;：这是 RL 逼出来的第四支柱——支持超越「单个训练循环」的多阶段执行模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;三教训一从-sft-到-rl架构要换心&#34;&gt;三、教训一：从 SFT 到 RL，架构要换心&lt;/h2&gt;
&lt;p&gt;框架最初围绕 SFT 设计：数据流相对静态、单一训练循环、SPMD 执行模型（每个 GPU worker 跑同一个 step 函数，处理不同数据分片，靠 PyTorch 分布式原语同步）。这个假设在 2025 年失效了——DeepSeek-R1 和 GRPO 这类高效 on-policy RL 普及之后，SFT 从终点变成了入场券。&lt;/p&gt;
&lt;p&gt;两者差异的本质在学习信号：&lt;strong&gt;SFT 的信号稠密而即时&lt;/strong&gt;（每个 token 位置都有全词表 logits 和可微 loss），长得就像 pre-training；&lt;strong&gt;on-policy RL 的信号稀疏而延迟&lt;/strong&gt;（episode 结束才有一个标量 reward），且训练数据依赖当前 policy 生成。单个子阶段（policy 更新、rollout、reference 推理、reward 打分）各自都能 SPMD 化，但端到端算法需要显式协调：prompt、采样轨迹、reward、advantage 这些产物在阶段间不停交接，生命周期要有人管。&lt;/p&gt;
&lt;p&gt;架构上的变化：原来的 driver 是「瘦」的，只负责拉起 N 个相同的 Ray actor，扩容就是加 worker；RL 要求把系统拆成不同角色（Policy、Rollout Worker、Reward Model、Reference Model），driver 进化成&lt;strong&gt;主动控制器&lt;/strong&gt;，编码整个控制面——何时生成 rollout、怎么组 batch 和打分、何时触发优化、跨阶段怎么调度集群资源。&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
    subgraph SFT[&amp;#34;SFT 架构: 瘦 driver + SPMD&amp;#34;]
        D1[&amp;#34;瘦 driver&amp;lt;br/&amp;gt;只负责拉起 worker&amp;#34;] --&amp;gt; A1[&amp;#34;actor 1&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
        D1 --&amp;gt; A2[&amp;#34;actor 2&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
        D1 --&amp;gt; A3[&amp;#34;actor N&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
    end
    subgraph RL[&amp;#34;RL 架构: 主动控制器 + 多角色&amp;#34;]
        C[&amp;#34;主动控制器&amp;lt;br/&amp;gt;编码控制面&amp;lt;br/&amp;gt;何时 rollout 何时优化&amp;#34;] --&amp;gt; P[&amp;#34;Policy 更新&amp;#34;]
        C --&amp;gt; R[&amp;#34;Rollout Worker&amp;#34;]
        C --&amp;gt; RM[&amp;#34;Reward Model&amp;#34;]
        C --&amp;gt; REF[&amp;#34;Reference Model&amp;#34;]
        R --&amp;gt;|&amp;#34;轨迹&amp;#34;| RM
        RM --&amp;gt;|&amp;#34;reward 和 advantage&amp;#34;| P
    end
    SFT ==&amp;gt;|&amp;#34;2025 年 DeepSeek-R1 与 GRPO 普及&amp;lt;br/&amp;gt;SFT 从终点变成入场券&amp;#34;| RL
    style RL fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;落地方式是集成开源库 &lt;strong&gt;Verl&lt;/strong&gt; 的核心设施来管理 Ray actor 生命周期和 GPU 资源分配，自己只做「建模表面积」——Data/Model/Compute 抽象和内部优化。最终是混合设计：用户在 SFT 和 RL 之间切换不用换心智模型。这个取舍值得画重点：&lt;strong&gt;编排不自己造轮子，抽象层才是护城河。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;四教训二全面押注-hugging-face但模型实现自己来&#34;&gt;四、教训二：全面押注 Hugging Face，但模型实现自己来&lt;/h2&gt;
&lt;p&gt;HF Hub 事实上已是开源权重、tokenizer、配置的默认分发渠道，框架选择贴着生态走而不是另立内部标准：checkpoint 一律按 HF 格式存取，避免「围墙花园」摩擦。&lt;/p&gt;
&lt;p&gt;Tokenizer 的故事最有教训价值。早期直接绑定底层分词库（SentencePiece、tiktoken）追求控制力，结果造成&lt;strong&gt;静默的训练-推理 skew&lt;/strong&gt;：推理栈（vLLM）默认用 HF AutoTokenizer，归一化、特殊 token、chat template 的细微差异会产生不同的 token 边界——这类 mismatch 之后会以「莫名其妙的质量回退」形式冒出来，极难排查。修法：&lt;strong&gt;AutoTokenizer 成为唯一事实源&lt;/strong&gt;，上面加一层薄薄的兼容层（&lt;code&gt;BaseHFModelTokenizer&lt;/code&gt;）处理后训练特有需求（padding token、生成标记注入、语义 ID），同时保证字节级分词路径和生产一致。&lt;/p&gt;
&lt;p&gt;模型实现则相反：&lt;strong&gt;不直接用 transformers 的模型类训练&lt;/strong&gt;，而是维护自己优化的统一模型定义，能读写 HF checkpoint。这一层换来 FlexAttention、分块交叉熵、统一 MFU 核算、统一 LoRA 扩展，跨架构不用重实现；统一的模块命名还让程序化定位和替换组件（Attention、MLP、输出头）成为可能。&lt;/p&gt;
&lt;p&gt;代价很直接：支持一个新模型家族，要写 HF 参考实现到内部定义的桥。Netflix 的解法是 &lt;strong&gt;AI coding agent + logit verifier&lt;/strong&gt;：agent 自动生成大部分转换代码，验收标准是给定随机输入、内部模型的 logits 必须在容差内匹配 HF——因为验收可以机械检查，agent 能自主迭代到正确为止，大幅缩短新架构的支持周期。当前只支持显式适配过的架构（和 vLLM、SGLang、torchtitan 同款取舍），计划中的 HF fallback 后端会允许直接跑原生 transformers 模型做快速探索，代价是部分优化不生效。&lt;/p&gt;
&lt;h2 id=&#34;五教训三差异化价值投在深水区&#34;&gt;五、教训三：差异化价值投在深水区&lt;/h2&gt;
&lt;p&gt;自建框架值不值，取决于有没有超出「组装开源件」的价值。Netflix 的答案：开源换速度，重投入换两个方向——&lt;strong&gt;贴合自家负载特征的性能&lt;/strong&gt;和&lt;strong&gt;Netflix 特有的业务需求&lt;/strong&gt;。两个具体例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On-the-fly sequence packing。&lt;/strong&gt; FSDP 训练里长尾序列制造 straggler，快的 worker 在同步点干等。标准的离线 bin-packing 有效，但在他们的数据规模下预处理延迟可观、数据难保鲜。解法是从存储流式读样本、在内存里动态打包，打包异步跑、和 GPU 计算重叠。结果：最倾斜的数据集上有效 token 吞吐提升最高 &lt;strong&gt;4.7 倍&lt;/strong&gt;（A100 和 H200 上均验证）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;词表大小对齐 64。&lt;/strong&gt; 业务常加自定义 token 和语义 ID，某些词表大小会让 LM head 从高度优化的 cuBLAS kernel 掉到慢得多的 CUTLASS 路径，该层耗时翻三倍。框架自动把词表 pad 到 64 的倍数，开发者不需要知道这层硬件玄学。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二个方向是支持「非标准」transformer 用例：比如基于用户交互事件序列（而非自然语言）训练的模型，需要定制的 RL 循环、对接高度定制的推理引擎、优化业务定义的指标——这些要求定制环境、奖励计算和编排模式，但性能、追踪、容错的底层保障必须一样。&lt;/p&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「跑通」和「生产化」之间的鸿沟，大部分都是状态管理。&lt;/strong&gt; Loss mask、checkpoint 完整性、训练-推理一致性——每一项单看都是小细节，合起来决定系统可不可靠。这和我们写业务系统一样：demo 一周，上线半年，差的全在边界条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练-推理 skew 是数据系统里「双写不一致」的 LLM 版本。&lt;/strong&gt; 两份各自「正确」的实现（训练 tokenizer vs 推理 tokenizer）因为微小差异产生静默错误。通用解法也通用：指定唯一事实源，其他都向它对齐，用兼容性测试兜底——logit verifier 本质就是契约测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI coding agent 最好用的地方是「验收可机械检查」的场景。&lt;/strong&gt; HF 到内部模型定义的桥接，验收是 logits 数值匹配，agent 可以放心自主迭代。反过来说，给 agent 派活之前先问：这活的验收标准能不能写成断言？写不出断言的活，agent 做完了你还得人工审。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编排层买、抽象层造，这个取舍可以抄。&lt;/strong&gt; Verl 管 actor 生命周期，自己只做 Data/Model/Compute/Workflow 抽象。通用版：消息队列买、存储买，贴合业务的领域模型自己造——护城河在建模，不在管道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化要贴着自家负载的真实分布做。&lt;/strong&gt; 4.7 倍不是来自通用调优，是来自「我们的序列长度方差极端」这个具体事实；词表对齐 64 同理。先测量自己的负载长什么样，再决定优化什么——别人的 benchmark 不构成你的优化清单。&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 规模下是工程问题。框架目标是让模型开发者不碰分布式管道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三个坑位&lt;/strong&gt;：数据（loss mask、packing）、模型（分片加载、大词表 logits 内存）、训练（不只是 for 循环，要编排和容错）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四根支柱&lt;/strong&gt;：Data、Model、Compute、Workflow——第四根是 RL 逼出来的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构演进&lt;/strong&gt;：SFT 的「瘦 driver + SPMD」演进为 RL 的「主动控制器 + 多角色」，编排集成 Verl，自己保住抽象层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态策略&lt;/strong&gt;：checkpoint 和 tokenizer 全面贴 HF（AutoTokenizer 是唯一事实源，治训练-推理 skew）；模型实现自己维护换优化空间，桥接新架构用 AI agent + logit verifier 自动化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差异化投入&lt;/strong&gt;：on-the-fly packing 最高 4.7 倍吞吐；词表对齐 64 避开 kernel 回退；支持交互序列这类非自然语言 transformer。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一句话&lt;/strong&gt;：训练框架的价值不在堆了多少组件，在于把分布式系统的坑变成了配置项。&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/scaling-llm-post-training-at-netflix-0046f8790194&#34;&gt;Scaling LLM Post-Training at Netflix&lt;/a&gt;（原文，Baolin Li、Lingyi Liu、Binh Tang、Shaojing Li）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/volcengine/verl&#34;&gt;Verl&lt;/a&gt;（字节跳动开源的 RL 后训练库，Netflix 用其核心设施做 actor 生命周期与 GPU 资源管理）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://arxiv.org/abs/2501.12948&#34;&gt;DeepSeek-R1&lt;/a&gt;（GRPO 等高效 on-policy RL 方法的代表作，原文称其为「SFT 变成入场券」的转折点）&lt;/li&gt;
&lt;li&gt;Torchtune、Torchtitan（原文致谢的两个 PyTorch 原生训练参考实现）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://thinkingmachines.ai/tinker/&#34;&gt;Thinking Machines Tinker&lt;/a&gt;（原文对比的外部微调服务）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
