后端(模特上身图提示词): - build_model_tryon_prompt_refs 重写:穿戴/非穿戴分流(穿戴=真实穿身替换原衣, 非穿戴=手持/佩戴/使用不动原衣)、每张按 index 变化动作/场景/镜头、负面词尾接、 多图参考序号自适应(参考图1~N=商品,参考图N+1=模特) - 新增 _product_reference_urls:商品参考图真实上传图优先、排除 AI 生成图、可多张(≤3), 无真实图回落 cover - worker run_standalone_image_task 模特分支改用多图取图 + 传 index/n_product 前端(图片创作/工作室): - 生成数量改 1/2/4;图片比例新增「手动输入」(宽:高 两输入框) - 临时隐藏「商品库」按钮 - 模特卡:去掉 // 真人模特,标题改图片底部白字遮罩层 + 单行省略 - 工作室壳负边距对齐 .content padding,修复上下被遮挡/裁切 其他:并入此前未提交的商品页改动、脚本 Agent/格式实测文档与 demo
312 lines
15 KiB
Markdown
312 lines
15 KiB
Markdown
# 脚本 Agent 编排架构方案 · 动态知识装配(流式管道)
|
|
|
|
> 状态:设计方案(未落地代码) · 作者:架构评审 · 日期:2026-06-24
|
|
> 关联代码:[`apps/ai/script_agent.py`](../backend/apps/ai/script_agent.py) · [`apps/ai/services.py`](../backend/apps/ai/services.py) · [`skills/ecommerce-video-script/`](../backend/skills/ecommerce-video-script/)
|
|
> 关联文档:[脚本Agent流式SSE技术文档.md](脚本Agent流式SSE技术文档.md)(现状) · [出格式实测-模型产出汇总.md](出格式实测-模型产出汇总.md)(实测数据)
|
|
|
|
---
|
|
|
|
## 1. 背景与要解决的问题
|
|
|
|
### 1.1 现状
|
|
|
|
当前脚本生成把领域知识一次性全量灌入上下文:`load_ecommerce_skill()`(见 [script_agent.py:54](../backend/apps/ai/script_agent.py#L54))
|
|
用 `glob("*.md")` **无条件遍历全部 references**,整篇拼成系统提示词,每次对话(不论全自动/一句话/改稿/改一镜)
|
|
都把这 ~28K 字符全量发给模型。
|
|
|
|
SKILL.md 里虽写了一张「输入模式路由 / 参考资料索引」表(指明哪个品类该读哪几篇),但**该路由仅作为提示词
|
|
发给模型,后端并未按它选择性加载**——检索发生在模型的注意力里,不在后端。
|
|
|
|
### 1.2 随业务增长的问题
|
|
|
|
> **核心痛点:上下文随电商品类数量线性膨胀。**
|
|
|
|
现在 5 个 references = 28K 字符。未来叠加更多品类话术(美妆/食品/3C/服饰/家居/母婴/宠物/家电…)
|
|
与平台调性后,全量灌入会:
|
|
|
|
- **上下文臃肿**:单次请求 system prompt 可能涨到数十万字符,逼近/超出上下文窗口;
|
|
- **成本线性上涨**:每次都付全量知识的 token,即便这单商品只用得上其中一个品类包;
|
|
- **注意力稀释**:无关品类的话术挤占模型注意力,可能拉低相关品类的发挥;
|
|
- **缓存难命中**:动态拼接的大 prompt 难以稳定复用 prefix cache。
|
|
|
|
### 1.3 目标
|
|
|
|
把「静态全量灌入」改为「**按需动态装配**」:拿到商品需求后,**只加载与该商品相关的知识模块**,
|
|
打包给模型,流式生成,再经过滤/提取输出前端。**单次上下文只随"命中的 1-2 个品类包"走,不随品类总量膨胀。**
|
|
|
|
---
|
|
|
|
## 2. 设计目标(验收标准)
|
|
|
|
| # | 目标 | 可度量标准 |
|
|
| - | ---- | ---------- |
|
|
| G1 | 上下文不随品类总数膨胀 | 单次 system prompt 字数 ≈ 内核 + 命中模块,与品类总数解耦 |
|
|
| G2 | 输出格式永不因知识缺失而塌 | 任意路由结果下,输出契约恒在 prompt 中 |
|
|
| G3 | 全程流式 | 路由/装配/生成/提取每阶段都有 SSE 进度事件 |
|
|
| G4 | 运营可扩品类不改代码 | 加一个品类 = 加一个知识模块(文件/DB),无需改 Python |
|
|
| G5 | 不引入与任务不匹配的重型框架 | 沿用生成器流式编排,不上状态图运行时 |
|
|
| G6 | 平滑迁移 | 分阶段落地,每阶段可独立上线、可回滚 |
|
|
|
|
---
|
|
|
|
## 3. 总体架构:六段流式管道
|
|
|
|
```
|
|
商品需求(product + 前置条件)
|
|
│
|
|
▼ ① 路由 Router ────────── 识别品类/平台 → 决定加载哪些知识模块
|
|
│ SSE: tool router "识别品类:美妆洗护 · 平台:抖音"
|
|
│
|
|
▼ ② 装配 Assembler ─────── 取「内核 + 命中品类包 + 命中平台调性」
|
|
│ SSE: tool assemble "装配知识:内核+2模块 共 9.2K 字" ← 可见地证明未膨胀
|
|
│
|
|
▼ ③ 打包 Packager ──────── 内核(恒在) + 动态知识 + 商品上下文 + 输出契约
|
|
│
|
|
▼ ④ 生成 Generator ─────── 约束解码(tool/structured)流式出结构
|
|
│ SSE: reasoning(思考) / delta(口语前言)
|
|
│
|
|
▼ ⑤ 过滤提取 Extractor ─── 校验/归一/抽实体/扫违规词(强不变量,后端兜底)
|
|
│ SSE: tool extract "提取实体4个 · 自检通过"
|
|
│
|
|
▼ ⑥ 前端 Sink ──────────── draft / saved / summary / done
|
|
```
|
|
|
|
**与现状的本质差异**:②③ 从"glob 全部"变为"只装命中"。①②⑤ 是真实工作步骤,不再是装样子的工具卡。
|
|
|
|
---
|
|
|
|
## 4. 核心设计:知识分两层
|
|
|
|
把现有单块 28K 知识拆成**内核(恒在)+ 模块(动态)**两层。
|
|
|
|
### 4.1 内核 Kernel(每次必带,小而稳)
|
|
|
|
| 内容 | 来源(现状) |
|
|
| ---- | ---------- |
|
|
| 黄金结构(钩子→痛点→卖点→CTA)、档位×结构映射 | methodology.md 的结构部分 |
|
|
| **输出契约(铁律1):字段名锚定、JSON 形状、镜数规则** | SKILL.md 铁律1 + `_OUTPUT_PROTOCOL` |
|
|
| 写作红线:≤55字、违规词清单、口语化 | methodology.md 红线 + SKILL.md 铁律3 |
|
|
| 字段纪律:tone/role 枚举、entity 引用合法性 | SKILL.md 铁律1 |
|
|
|
|
> ⚠️ **输出契约必须在内核里,永远在。** 这是你们踩过的坑(skills 没进镜像→契约丢失→模型吐散文解析失败)
|
|
> 的正式解。无论路由加载了哪些品类包,格式硬底线都不会塌。现有 `_EXTRACT_OUTPUT_CONTRACT`
|
|
> (见 [services.py:385](../backend/apps/ai/services.py#L385))写死兜底,就是这一思想的雏形——把它正式化为"内核"。
|
|
|
|
### 4.2 品类模块 Module(按商品命中才加载,多而长)
|
|
|
|
| 模块类型 | 例 | frontmatter 选择维度 |
|
|
| ------- | -- | ------------------- |
|
|
| 品类话术 | 美妆/食品/3C/服饰/家居… | `applies_to: [category...]` |
|
|
| 平台调性 | 抖音/快手/小红书/视频号 | `platforms: [...]` |
|
|
| 钩子库分册 | 痛点提问/反差/数字冲击… | `tone: [...]` 或 always |
|
|
|
|
每个模块是一个独立的、带元数据索引的知识单元(不再是一坨大 concat)。
|
|
|
|
---
|
|
|
|
## 5. 路由机制:怎么选模块
|
|
|
|
三种机制,按本场景适配度排序。**推荐 rule-first 混合**。
|
|
|
|
### 5.1 元数据路由(主力 · 先落地这个)
|
|
|
|
把 SKILL.md 的路由表**从提示词搬进代码**:每个模块 frontmatter 声明它服务的品类/平台,
|
|
`select_knowledge(product)` 按 `product.category` / 平台前置条件命中。
|
|
|
|
- **优点**:零额外调用、确定性、可解释、可单测。电商商品基本都有 category 字段,**80% 情况足够**。
|
|
- **缺点**:新品类要维护映射——但加品类 = 加一个 `.md` 模块 + 写 frontmatter,**不改 Python**(满足 G4)。
|
|
|
|
### 5.2 向量检索 RAG(扩容兜底 · 品类破百再上)
|
|
|
|
把知识块 embedding,用商品上下文检索 top-K 相关片段。
|
|
|
|
- **优点**:处理模糊/新品类(novel category 自动匹配近邻),可无限扩。
|
|
- **缺点**:引入检索失败模式(检错块→知识缺失)、需 embedding 基建与运维。**别过早引入。**
|
|
|
|
### 5.3 LLM 路由(灵活但加跳)
|
|
|
|
用便宜快模型(如 doubao-lite)先分类"该商品属哪类、用哪套话术"。
|
|
|
|
- **优点**:最灵活,能理解复杂商品描述。**缺点**:多一次调用 + 延迟。
|
|
|
|
### 5.4 推荐:rule-first 混合
|
|
|
|
```
|
|
select_knowledge(product):
|
|
modules = [Kernel] # 恒在
|
|
hit = rule_match(product.category, platform) # 5.1 规则命中
|
|
if hit:
|
|
modules += hit
|
|
else:
|
|
modules += fallback() # 命中不到:回落(全量核心包 or 5.2 检索)
|
|
return modules
|
|
```
|
|
|
|
**先只做 5.1 规则版,留好 `fallback()` 接口**;品类规模或模糊度上来时,把 `fallback` 换成检索/LLM 路由。
|
|
|
|
---
|
|
|
|
## 6. 接口设计
|
|
|
|
### 6.1 知识模块结构(frontmatter 规范)
|
|
|
|
每个 reference 模块在文件头加 YAML frontmatter(或等价 DB 字段):
|
|
|
|
```markdown
|
|
---
|
|
id: playbook-beauty
|
|
type: category # core | category | platform | hook
|
|
applies_to: [美妆, 护肤, 洗护, 彩妆] # 命中这些 category 时加载
|
|
platforms: [] # 限定平台(空=不限)
|
|
keywords: [精华, 面膜, 口红, 防晒] # 检索/模糊命中用
|
|
priority: 10
|
|
enabled: true
|
|
---
|
|
(正文:该品类的话术、语气、卖点侧重…)
|
|
```
|
|
|
|
`core` 类型 = 内核,恒加载;其余按 `applies_to`/`platforms`/`keywords` 命中。
|
|
|
|
### 6.2 选择函数(替换 `load_ecommerce_skill`)
|
|
|
|
```python
|
|
# apps/ai/knowledge.py(新增)
|
|
@dataclass
|
|
class KnowledgeModule:
|
|
id: str
|
|
type: str # core|category|platform|hook
|
|
applies_to: list[str]
|
|
platforms: list[str]
|
|
keywords: list[str]
|
|
body: str
|
|
|
|
def load_registry() -> list[KnowledgeModule]:
|
|
"""扫 skills/ 下模块(含 frontmatter),或读 DB。缓存。"""
|
|
|
|
def select_knowledge(*, product, platform: str | None, mode: str) -> list[KnowledgeModule]:
|
|
"""rule-first:内核恒在 + 按 category/platform 命中品类包/平台调性;
|
|
命中不到走 fallback(全量核心 or 检索)。改稿模式可少带选题类模块。"""
|
|
|
|
def assemble_system_prompt(modules: list[KnowledgeModule]) -> tuple[str, int]:
|
|
"""拼 system prompt = 内核(置顶稳定,利于 prefix cache) + 动态模块。
|
|
返回 (prompt, 字数) —— 字数用于 SSE 上报,可见证明未膨胀。"""
|
|
```
|
|
|
|
`build_agent_messages`(见 [script_agent.py:111](../backend/apps/ai/script_agent.py#L111))
|
|
的 `system = load_ecommerce_skill() + _OUTPUT_PROTOCOL` 改为
|
|
`system, n = assemble_system_prompt(select_knowledge(...))`,其中输出契约并入内核。
|
|
|
|
### 6.3 SSE 事件扩展
|
|
|
|
在现有 `tool/reasoning/delta/draft/saved/summary/done` 基础上,让 ①②⑤ 成为**真实**工具卡:
|
|
|
|
| 事件 | 新增/变化 | 载荷 |
|
|
| ---- | -------- | ---- |
|
|
| `tool: router` | 新增 | `{label:"识别品类:美妆·抖音", status, meta:{category, platform}}` |
|
|
| `tool: assemble` | 新增 | `{label:"装配知识 内核+2模块 9.2K字", status, meta:{module_ids, chars}}` |
|
|
| `tool: generate` | 不变 | 约束解码流式 |
|
|
| `tool: extract` | 强化 | 真实体提取 + 违规词自检结果 |
|
|
|
|
> `assemble` 卡把"这次只装了 9.2K 而非 28K"**可观测地**展示给用户/运维,是 G1 的活体证明。
|
|
|
|
---
|
|
|
|
## 7. 生成与过滤提取(④⑤)
|
|
|
|
### 7.1 生成:约束解码,让 normalize 退居安全网
|
|
|
|
结合 [出格式实测-模型产出汇总.md](出格式实测-模型产出汇总.md) 的实测结论:
|
|
|
|
- 三模型(豆包/GPT-5.5/Gemini-3.1-pro)的 structured/tool 均可产出 `raw_clean✓` 的契约 JSON;
|
|
- **freeform 下三家原始输出全 `raw_clean✗`**(靠 `normalize_draft` fuzzy 抢救);
|
|
- **tool 策略跨模型 segments 键集完全同构**(最稳)。
|
|
|
|
→ 生成阶段改用 **tool/structured 约束解码**(schema 须补全 `dialogue/speaker/voice_ref` 等契约字段),
|
|
内核保留输出契约文字作双保险。`normalize_draft` 从"主力解析器"降为"安全网"。
|
|
|
|
> ⚠️ 约束解码与"先写口语前言"的 `_OUTPUT_PROTOCOL` 有张力(实测 GPT structured 被前言污染)。
|
|
> 落地时**对话气泡(前言/收尾)与结构稿分离**:结构走纯约束,气泡另起轻量一跳或用支持混合流的部件协议。
|
|
|
|
### 7.2 过滤提取:强不变量后端兜底
|
|
|
|
沿用并简化现有逻辑(约束解码后原始已干净,兜底压力骤降):
|
|
|
|
- 镜数对齐(=时长/15)、role/tone 枚举归一、entity_refs 合法性 —— `normalize_draft` 现有能力;
|
|
- 实体抽取(角色/场景)—— 复用 [services.py](../backend/apps/ai/services.py) 的 `run_extract_entities_task`;
|
|
- 违规词自检 —— 可前置为真校验节点(发现即标记/可触发重生成)。
|
|
|
|
---
|
|
|
|
## 8. 为什么不用 LangGraph
|
|
|
|
本管道是**线性流水线**(router→assemble→generate→extract→sink):**无环、无 reflect-retry、无多 agent 对话**。
|
|
线性 + 流式正是现有生成器范式的最佳 altitude。LangGraph 的状态图是为"有环/有分支/要回退/human-in-loop"
|
|
设计的,**此处上图属过度设计**。真正的编排升级点是"选择性装配"这一层抽象,而非更换运行时。
|
|
|
|
> 若未来产品要做「生成→自检违规词→自动修正→再检」的真闭环,或「编剧/审查/提取」多 agent 协作,
|
|
> 那时再评估 LangGraph / PydanticAI(类型契约+重试)/ 轻量自写 retry。当前不需要。
|
|
|
|
---
|
|
|
|
## 9. 分阶段迁移清单
|
|
|
|
每阶段可独立上线、独立回滚。
|
|
|
|
### Phase 1 · 知识分层 + 规则路由(止血膨胀,优先级最高)
|
|
- [ ] references 加 frontmatter(`type/applies_to/platforms/keywords`);抽出 `core` 内核。
|
|
- [ ] 新增 `apps/ai/knowledge.py`:`load_registry` / `select_knowledge`(规则版)/ `assemble_system_prompt`。
|
|
- [ ] `build_agent_messages` 改用 `assemble_system_prompt(select_knowledge(...))`;**输出契约并入内核**。
|
|
- [ ] SSE 增 `router`/`assemble` 真实工具卡(含字数)。
|
|
- [ ] 单测:命中/未命中/改稿模式各自加载了哪些模块;内核必含契约。
|
|
- **验收**:单次 system prompt 字数与品类总数解耦(G1);格式零回归。
|
|
|
|
### Phase 2 · 约束解码(让 normalize 退居安全网)
|
|
- [ ] 定义完整 `ScriptDraft` JSON Schema(含 dialogue/speaker/voice_ref)。
|
|
- [ ] 生成阶段切 tool/structured(按 provider 能力分流,实测已验证三家可行)。
|
|
- [ ] 对话气泡与结构稿分离,解决 `_OUTPUT_PROTOCOL` 与约束的张力。
|
|
- [ ] `normalize_draft` 降级为兜底;保留以防个别模型/中转站不合规。
|
|
- **验收**:三模型原始输出 `raw_clean✓`;normalize 命中率(需抢救比例)大幅下降。
|
|
|
|
### Phase 3 · 检索扩容(品类规模化后才做)
|
|
- [ ] 知识块 embedding + 向量库;`fallback()` 接入检索。
|
|
- [ ] 模糊/新品类召回评估。
|
|
- **验收**:新增品类无需改路由规则即可被正确召回。
|
|
|
|
### Phase 4(可选)· 自检闭环
|
|
- [ ] 违规词/字数校验做成真节点,不过则带错误自动重生成(轻量 retry,非全图)。
|
|
|
|
---
|
|
|
|
## 10. 风险与对策
|
|
|
|
| 风险 | 对策 |
|
|
| ---- | ---- |
|
|
| **R1 内核漏放契约 → 某品类下格式塌** | 内核**必含**完整输出契约;单测断言"任意路由结果都含契约";保留写死兜底。 |
|
|
| **R2 路由漏召(该加载却没加载)→ 模型瞎编** | 漏召比误召危险。规则命中不到**必须回落**(全量核心包 or 检索),严禁裸奔。 |
|
|
| **R3 约束解码与对话气泡冲突** | 结构稿走纯约束、气泡分离(见 7.1);或用支持混合流的部件协议。 |
|
|
| **R4 prefix cache 未命中,内核成本没摊薄** | 内核置顶且稳定;实测豆包/中转是否支持 prefix cache 再定。 |
|
|
| **R5 检索引入新失败模式** | Phase 3 才上;上之前用规则兜底;检索结果可解释、可回退规则。 |
|
|
|
|
---
|
|
|
|
## 11. 与现有代码映射(速查)
|
|
|
|
| 设计组件 | 现状 | 落点 |
|
|
| ------- | ---- | ---- |
|
|
| 内核 + 模块拆分 | `load_ecommerce_skill()` glob 全部 | 新 `apps/ai/knowledge.py` |
|
|
| 路由 `select_knowledge` | SKILL.md 路由表(给模型看) | 新 `knowledge.py`,规则实现 |
|
|
| 装配 `assemble_system_prompt` | 字符串拼全部 | 新 `knowledge.py`,内核+命中 |
|
|
| 打包 | `build_agent_messages` system 拼接 | 改 [script_agent.py:122](../backend/apps/ai/script_agent.py#L122) |
|
|
| 生成(约束解码) | freeform + `_OUTPUT_PROTOCOL` | 改 provider 调用,见 [providers/](../backend/apps/ai/providers/) |
|
|
| 过滤提取 | `normalize_draft` 当主力 | 降为安全网 [script_agent.py:307](../backend/apps/ai/script_agent.py#L307) |
|
|
| 流式编排 | `stream_script_agent` 生成器 | 沿用,增 router/assemble 事件 [script_agent.py:561](../backend/apps/ai/script_agent.py#L561) |
|
|
| 模块运营管理 | `references/*.md` 文件 | frontmatter 文件,或复用 `PromptTemplate` admin DB 模式 |
|
|
|
|
---
|
|
|
|
## 12. 一句话总结
|
|
|
|
把「静态全量灌」改成「**内核恒在 + 品类模块按需装配**」的**线性流式管道**:路由先用规则(SKILL 路由表搬进代码)、
|
|
扩容再上检索;生成换约束解码让 `normalize_draft` 退居安全网;流式编排沿用现有生成器,**不需要 LangGraph**。
|
|
如此品类再叠,单次上下文也只随"命中的一两个包"走,**不随品类总量膨胀**。
|