<?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/tags/ai-%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 23 Jun 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/ai-%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90/index.xml" rel="self" type="application/rss+xml" /><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>RAG 本地素材库：从 Embedding 选型到召回调优</title>
      <link>https://ftmao.com/blog/posts/raginftmao/</link>
      <pubDate>Tue, 10 Feb 2026 11:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/raginftmao/</guid>
      <description>&lt;h1 id=&#34;rag-本地素材库从-embedding-选型到召回调优&#34;&gt;RAG 本地素材库：从 Embedding 选型到召回调优&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在我开发的方糖猫 AI 视频平台中，一个核心能力是「根据文案自动匹配视频素材」。用户的文案可能是「讲一个关于坚持的故事」，系统需要从本地素材库中找到最契合的画面。我们最终用 &lt;strong&gt;RAG（检索增强生成）&lt;/strong&gt; 的思路搭建了本地素材库。这篇文章会完整复盘从 Embedding 选型到召回调优的全过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一业务背景为什么需要-rag&#34;&gt;一、业务背景：为什么需要 RAG？&lt;/h2&gt;
&lt;p&gt;短视频生成平台里的素材通常有几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图片素材：背景图、人物图、概念图&lt;/li&gt;
&lt;li&gt;视频片段：B-roll、转场、动画&lt;/li&gt;
&lt;li&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;。一个海边日落镜头，可能被标注为「海」「日落」「风景」「旅行」，但用户文案写的是「人生低谷后的曙光」，关键词完全对不上。&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;RAG 的思路是：&lt;strong&gt;用 Embedding 把素材和查询都映射到同一向量空间，通过语义相似度做召回&lt;/strong&gt;。这样即使关键词不同，只要语义接近，就能被检索出来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二系统设计&#34;&gt;二、系统设计&lt;/h2&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 class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph S1[&amp;#34;素材入库&amp;#34;]
        A1[&amp;#34;原始素材&amp;#34;] --&amp;gt; A2[&amp;#34;内容提取&amp;#34;] --&amp;gt; A3[&amp;#34;Embedding&amp;#34;] --&amp;gt; A4[&amp;#34;ChromaDB&amp;#34;]
    end
    subgraph S2[&amp;#34;查询召回&amp;#34;]
        B1[&amp;#34;用户文案&amp;#34;] --&amp;gt; B2[&amp;#34;Embedding&amp;#34;] --&amp;gt; B3[&amp;#34;Top-K 检索&amp;#34;] --&amp;gt; B4[&amp;#34;重排&amp;#34;] --&amp;gt; B5[&amp;#34;返回结果&amp;#34;]
    end
    subgraph S3[&amp;#34;持续更新&amp;#34;]
        C1[&amp;#34;新增素材&amp;#34;] --&amp;gt; C2[&amp;#34;增量 Embedding&amp;#34;] --&amp;gt; C3[&amp;#34;索引更新&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;21-素材入库流程&#34;&gt;2.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 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;视觉特征提取（CLIP/ResNet）&amp;#34;] --&amp;gt; D[&amp;#34;文本描述生成&amp;#34;] --&amp;gt; E[&amp;#34;Embedding&amp;#34;] --&amp;gt; F[&amp;#34;写入 ChromaDB&amp;#34;]
    G[&amp;#34;文本片段&amp;#34;] --&amp;gt; H[&amp;#34;清洗/分块&amp;#34;] --&amp;gt; I[&amp;#34;Embedding&amp;#34;] --&amp;gt; F&lt;/code&gt;&lt;/pre&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;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:#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;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;retrieve_materials&lt;/span&gt;(query: str, top_k: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/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;# 1. 查询向量化&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    query_embedding &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; embedding_model&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;encode(query)
&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. 向量库初筛&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    candidates &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; chroma_collection&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        query_embeddings&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[query_embedding],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        n_results&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;top_k &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 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;        where&lt;span style=&#34;color:#f92672&#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;active&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:#75715e&#34;&gt;# 3. 重排序&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ranked &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; reranker&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rerank(query, candidates)
&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. 返回 Top-K&lt;/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; ranked[:top_k]&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;三embedding-选型实测&#34;&gt;三、Embedding 选型实测&lt;/h2&gt;
&lt;p&gt;Embedding 模型的选择直接决定召回质量。我们测试了以下几种：&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;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;text-embedding-ada-002&lt;/td&gt;
					&lt;td&gt;文本&lt;/td&gt;
					&lt;td&gt;1536&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;text-embedding-3-small&lt;/td&gt;
					&lt;td&gt;文本&lt;/td&gt;
					&lt;td&gt;1536&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;BGE-M3&lt;/td&gt;
					&lt;td&gt;文本&lt;/td&gt;
					&lt;td&gt;1024&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;OpenAI CLIP&lt;/td&gt;
					&lt;td&gt;多模态&lt;/td&gt;
					&lt;td&gt;512&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;中文 CLIP&lt;/td&gt;
					&lt;td&gt;多模态&lt;/td&gt;
					&lt;td&gt;512&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;31-文本-embedding-选型&#34;&gt;3.1 文本 Embedding 选型&lt;/h3&gt;
&lt;p&gt;因为我们的查询主要是中文文案，最终选择了 &lt;strong&gt;BGE-M3&lt;/strong&gt;，原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中文语义理解能力明显优于 OpenAI 的 Embedding&lt;/li&gt;
&lt;li&gt;支持本地部署，无调用成本&lt;/li&gt;
&lt;li&gt;向量维度 1024，检索速度和存储成本可接受&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-多模态-embedding-选型&#34;&gt;3.2 多模态 Embedding 选型&lt;/h3&gt;
&lt;p&gt;对于图片/视频素材，CLIP 系列可以把视觉内容和文本映射到同一空间。但实测发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通用 CLIP 对中文概念的支持较弱&lt;/li&gt;
&lt;li&gt;视频场景下，关键帧的质量直接影响召回效果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键帧用 &lt;strong&gt;中文 CLIP&lt;/strong&gt; 生成视觉 Embedding&lt;/li&gt;
&lt;li&gt;同时用图像描述模型生成一句话中文描述，再用 &lt;strong&gt;BGE-M3&lt;/strong&gt; 生成文本 Embedding&lt;/li&gt;
&lt;li&gt;向量库中同时存储两种 Embedding，查询时分别召回再融合&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四向量库设计&#34;&gt;四、向量库设计&lt;/h2&gt;
&lt;p&gt;我们选用 &lt;strong&gt;ChromaDB&lt;/strong&gt; 作为向量存储，主要考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部署简单，适合小团队&lt;/li&gt;
&lt;li&gt;支持元数据过滤（按项目、类型、状态筛选）&lt;/li&gt;
&lt;li&gt;Python 生态集成好&lt;/li&gt;
&lt;li&gt;支持内存模式和持久化模式，开发方便&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;41-collection-划分&#34;&gt;4.1 Collection 划分&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:#75715e&#34;&gt;# 按素材类型分 Collection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;image_collection &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; chroma_client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_or_create_collection(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;materials_image&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;video_collection &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; chroma_client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_or_create_collection(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;materials_video&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;text_collection &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; chroma_client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_or_create_collection(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;materials_text&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;分 Collection 的好处是查询更聚焦，也方便针对不同类型的素材调优。&lt;/p&gt;
&lt;h3 id=&#34;42-文档结构设计&#34;&gt;4.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;documents &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;id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;img_001&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;embedding&amp;#34;&lt;/span&gt;: image_embedding,
&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;metadata&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;image&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;project_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;proj_123&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;tags&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:#e6db74&#34;&gt;&amp;#34;file_url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;oss://ftmao/materials/img_001.jpg&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;width&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1920&lt;/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;height&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1080&lt;/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;active&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;usage_count&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;12&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;document&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:#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;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;召回调优的前提是有评测数据。我们标注了 200 条查询-素材对：&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;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 style=&#34;color:#f92672&#34;&gt;&amp;#34;relevant&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;img_001&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;img_015&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vid_008&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;irrelevant&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;img_099&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vid_045&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;ul&gt;
&lt;li&gt;&lt;strong&gt;Recall@K&lt;/strong&gt;：相关素材在 Top-K 中的占比&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Precision@K&lt;/strong&gt;：Top-K 中相关素材的占比&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MRR&lt;/strong&gt;：第一个相关素材的平均排名倒数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;52-embedding-调优&#34;&gt;5.2 Embedding 调优&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;Recall@10&lt;/th&gt;
					&lt;th&gt;Precision@10&lt;/th&gt;
					&lt;th&gt;MRR&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;关键词标签&lt;/td&gt;
					&lt;td&gt;42%&lt;/td&gt;
					&lt;td&gt;35%&lt;/td&gt;
					&lt;td&gt;0.18&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;BGE-M3 文本 Embedding&lt;/td&gt;
					&lt;td&gt;68%&lt;/td&gt;
					&lt;td&gt;54%&lt;/td&gt;
					&lt;td&gt;0.31&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;BGE-M3 + 中文 CLIP 融合&lt;/td&gt;
					&lt;td&gt;79%&lt;/td&gt;
					&lt;td&gt;63%&lt;/td&gt;
					&lt;td&gt;0.40&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;+ 重排序模型&lt;/td&gt;
					&lt;td&gt;86%&lt;/td&gt;
					&lt;td&gt;71%&lt;/td&gt;
					&lt;td&gt;0.48&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;53-查询改写&#34;&gt;5.3 查询改写&lt;/h3&gt;
&lt;p&gt;用户的原始查询往往比较抽象，比如「做一个励志视频」。直接用这个查询做向量检索，召回结果会比较散。&lt;/p&gt;
&lt;p&gt;我们的做法是让 LLM 先对查询做改写和扩展：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;输入：做一个励志视频
输出：
- 核心语义：励志、奋斗、坚持不懈
- 扩展查询：逆境中成长、永不放弃、追逐梦想、突破自我
- 情绪基调：激昂、温暖、希望&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;然后用扩展后的多个查询分别检索，最后合并结果。这个策略让 Recall@10 提升了约 8%。&lt;/p&gt;
&lt;h3 id=&#34;54-重排序模型&#34;&gt;5.4 重排序模型&lt;/h3&gt;
&lt;p&gt;初筛召回 30 个候选后，用轻量级重排序模型做二次排序。我们用的是 &lt;strong&gt;bge-reranker-base&lt;/strong&gt;，原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型小，CPU 推理足够快&lt;/li&gt;
&lt;li&gt;对中文语义匹配效果好&lt;/li&gt;
&lt;li&gt;可以 fine-tune 适应平台特定素材&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;素材库是动态变化的，每天都有新增、删除、状态变更。我们采用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增量更新&lt;/strong&gt;：新增素材立即 Embedding 入库&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量补偿&lt;/strong&gt;：每天晚上全量校验一次，修复漏入/错入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软删除&lt;/strong&gt;：素材状态改为 inactive，不立即物理删除，便于恢复和审计&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-缓存与去重&#34;&gt;6.2 缓存与去重&lt;/h3&gt;
&lt;p&gt;热门查询的 Embedding 结果会被缓存到 Redis，缓存 key 是查询文本的 hash。同时 TTS 结果也按文案 hash 去重，避免重复调用。&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;get_cached_or_compute&lt;/span&gt;(query: 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; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rag:query:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;hashlib&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;md5(query&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;encode())&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;hexdigest()&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; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(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; retrieve_materials(query)
&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;, json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps(result))  &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;    &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;63-多项目隔离&#34;&gt;6.3 多项目隔离&lt;/h3&gt;
&lt;p&gt;同一个平台可能服务多个创作者或项目，不同项目的素材库需要隔离。我们通过 &lt;code&gt;project_id&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;results &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; collection&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    query_embeddings&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[embedding],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    n_results&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    where&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;project_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;proj_123&amp;#34;&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;active&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;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;早期我们同时用了 OpenAI Embedding（1536 维）和 BGE-M3（1024 维），混在一个 Collection 里，结果查询时报维度错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：严格按模型维度分 Collection，每个 Collection 只存同一模型产出的向量。&lt;/p&gt;
&lt;h3 id=&#34;72-关键帧质量影响召回&#34;&gt;7.2 关键帧质量影响召回&lt;/h3&gt;
&lt;p&gt;视频素材如果关键帧选得不好（比如黑屏、转场瞬间），CLIP 生成的 Embedding 会偏离视频真实内容。&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;一个视频保存多个关键帧，查询时合并召回&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;用户查询只有一个词，比如「奋斗」，召回结果可能包含大量不相关但语义宽泛的素材。&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;提高重排序模型的权重&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;目前这套 RAG 素材库已经支撑方糖猫平台的核心素材检索能力，覆盖了图片、视频片段、文案片段三类素材。&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;新素材无需人工打标签即可被检索到&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;针对视频内容做更细粒度的时序检索&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;RAG 不只是 LLM 应用的一个热门概念，对于内容生成类平台来说，它是&lt;strong&gt;连接用户意图和内容资产的关键基础设施&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Embedding 选型要以业务语言为主&lt;/strong&gt;。中文场景不要迷信 OpenAI，BGE 系列性价比更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一定要有评测数据集&lt;/strong&gt;。没有数据就没有优化方向。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;召回 pipeline 通常是多阶段的&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;如果你也在做 RAG 相关的项目，欢迎交流 Embedding 选型和召回调优的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
