先说结论
没有风格进化的 Agent,用 100 次和用 1 次没区别 --- 每次都是"第一次见面"。
风格进化让 Agent 从"一次性工具"变成"越用越懂你的助手"。 这是 Agent 最有"Agent 味"的能力 --- 不是开发者写死的规则,是 Agent 从用户行为中自己学到的偏好。
在 self-media-agent 项目里,persona/style_learner.py 实现了完整的风格进化链路:
用户修改文章 → 提取偏好 → 合并积累 → 生成画像 → 注入下次生成
5 个方法,一条链路,让 Agent 的 Prompt 会进化。
| 方法 | 做什么 | 触发时机 |
|---|---|---|
extract_preference_from_revision |
从修改 diff 提取偏好 | 每次修改后自动 |
_merge_preferences |
合并去重,count 累加 | 每次提取后 |
build_profile |
汇总所有偏好生成画像 | 手动触发 |
apply_to_persona |
画像写入人设 | build_profile 后 |
build_style_hint |
构建注入 Prompt 的提示 | 每次生成时自动 |
一、为什么需要风格进化?
没有进化的 Agent
erlang
第1次:用户"太正式了,活泼点" → Agent 改了
第2次:生成新文章 → 又是正式风格(忘了上次说的)
第3次:用户"又说活泼点!" → Agent 又改了
第4次:生成新文章 → 又是正式风格...
用户每次都要重复同样的要求 --- Agent 没有学习能力。
有进化的 Agent
arduino
第1次:用户"太正式了,活泼点" → Agent 改了 → 提取偏好"语气:活泼"
第2次:生成新文章 → 自动注入"你偏好活泼语气" → 直接活泼风格
第3次:用户"emoji少一点" → Agent 改了 → 提取偏好"emoji:低频"
第4次:生成新文章 → 注入"语气活泼 + emoji低频" → 风格越来越精准
用户说一次,Agent 记住一辈子 --- 这就是进化的力量。
进化的本质
风格进化的本质是 Prompt 的动态进化 --- 不是开发者写死的 Prompt,是 Agent 从用户行为中学到的 Prompt:
arduino
开发者写的 Prompt: "你是一位美妆赛道的干货科普专家,风格亲和口语化"
Agent 学到的 Prompt:"【风格画像】你倾向于活泼口语化表达;emoji使用克制"
两段 Prompt 叠加,Agent 的输出越来越贴合用户偏好。
二、三层风格进化设计
项目设计了三层进化,从即时到长期到未来:
| 层级 | 触发方式 | 做什么 | 当前状态 |
|---|---|---|---|
| 第1层(即时) | 每次修改后自动 | 提取偏好标签 → 合并到 style_preferences | ✅ 已实现 |
| 第2层(手动) | 手动触发 | 汇总所有偏好 → LLM 生成风格画像 | ✅ 已实现 |
| 第3层(未来) | 发布后自动 | 发布数据反馈 → 强化/弱化风格特质 | ❌ 未实现 |
第1层:即时偏好提取(自动,每次修改)
↓ 积累
第2层:风格画像生成(手动,定期汇总)
↓ 永久
第3层:发布数据反馈(未来,发布后自动)
第1层是基础,第2层是升华,第3层是终极。 当前项目实现了前两层。
三、第1层:即时偏好提取
提取:让 LLM 从修改 diff 中读出偏好
style_learner.py 的 extract_preference_from_revision --- 用户说"太正式了,活泼点",LLM 从修改前后对比中提取结构化偏好:
python
class StyleLearner:
async def extract_preference_from_revision(self, revision: RevisionRecord) -> list[dict]:
system_prompt = (
"你是一位内容风格分析师。根据用户的修改建议和修改前后的文章变化,"
"提取用户的风格偏好。\n\n"
"输出格式:JSON 数组,每个元素包含:\n"
'- "category": 偏好类别(语气/emoji/段落/用词/结构/开头/结尾/数据偏好/情感/节奏)\n'
'- "value": 偏好值,如"活泼"、"低频"、"短"等\n'
'- "confidence": 置信度 0.0-1.0\n'
'- "evidence": 原始修改建议文本\n'
)
# 截取修改前后各 500 字,避免 token 过长
old_snippet = revision.original_body[:500]
new_snippet = revision.revised_body[:500]
user_prompt = (
f"# 修改建议\n{revision.suggestion}\n\n"
f"# 修改前(前500字)\n{old_snippet}\n\n"
f"# 修改后(前500字)\n{new_snippet}\n\n"
f"请提取风格偏好。"
)
result = await self.llm.generate(
system_prompt=system_prompt,
user_prompt=user_prompt,
temperature=0.3, # ← 低温度,提取要精确
)
return json.loads(result)
提取的输入输出:
json
输入:
suggestion = "太正式了,活泼点,少用书面语"
original_body = "综上所述,夏季防晒需要注意以下几点..."
revised_body = "总的来说,夏天防晒这几个坑你一定要知道..."
输出:
[
{"category": "语气", "value": "活泼", "confidence": 0.9, "evidence": "太正式了,活泼点"},
{"category": "用词", "value": "口语化", "confidence": 0.8, "evidence": "少用书面语"}
]
LLM 不只是看建议文本,还看修改前后的实际变化 --- 这样提取的偏好更准确。用户说"活泼点"但实际只改了一个词,置信度就低;如果整段都改了,置信度就高。
10 个偏好类别
语气、emoji、段落、用词、结构、开头、结尾、数据偏好、情感、节奏
覆盖了自媒体内容的所有风格维度 --- 从宏观的语气结构到微观的用词 emoji。
即时写入:chat.py 中的自动触发
每次用户修改文章,chat.py 自动提取偏好并写入人设 --- 用户完全无感知:
ini
# api/routes/chat.py --- 修改后自动提取偏好
new_prefs = await learner.extract_preference_from_revision(revision)
if new_prefs:
persona = state.repo.store.get_persona(content.persona_id)
existing = list(persona.style_preferences)
existing.extend(new_prefs)
merged = StyleLearner._merge_preferences(existing)
updated_persona = persona.model_copy(update={
"style_preferences": merged,
"updated_at": datetime.now(),
})
state.repo.store.save_persona(updated_persona)
logger.info(f"即时偏好更新: 人设 {persona.id} 新增 {len(new_prefs)} 个偏好")
用户改一篇文章 → 自动提取偏好 → 自动合并 → 自动写入人设 → 下次生成自动生效。 全程无需用户额外操作。
四、偏好合并算法:去重 + 置信度取最高 + 次数累加
用户多次说"活泼点",不应该有 3 条相同的偏好。_merge_preferences 做合并:
python
@staticmethod
def _merge_preferences(preferences: list[dict]) -> list[dict]:
"""合并去重偏好:相同 category+value 合并,confidence 取最高,count 累加"""
merged: dict[str, dict] = {}
for p in preferences:
key = f"{p.get('category', '')}:{p.get('value', '')}"
if key in merged:
merged[key]["confidence"] = max(merged[key]["confidence"], p.get("confidence", 0.5))
merged[key]["count"] = merged[key].get("count", 1) + 1
else:
merged[key] = {
"category": p.get("category", ""),
"value": p.get("value", ""),
"confidence": p.get("confidence", 0.5),
"count": 1,
"evidence": p.get("evidence", ""),
}
# 按置信度 * 出现次数排序
result = list(merged.values())
result.sort(key=lambda x: x.get("confidence", 0) * x.get("count", 1), reverse=True)
return result
合并规则图解
json
输入(3 次修改提取的偏好):
{"category": "语气", "value": "活泼", "confidence": 0.8} ← 第1次
{"category": "emoji", "value": "低频", "confidence": 0.7} ← 第2次
{"category": "语气", "value": "活泼", "confidence": 0.9} ← 第3次
合并后:
{"category": "语气", "value": "活泼", "confidence": 0.9, "count": 2} ← 合并,confidence 取最高,count 累加
{"category": "emoji", "value": "低频", "confidence": 0.7, "count": 1}
排序后(confidence × count):
语气:活泼 → 0.9 × 2 = 1.8 ← 排第一
emoji:低频 → 0.7 × 1 = 0.7
三个关键规则:
| 规则 | 作用 | 示例 |
|---|---|---|
| key = category:value | 相同偏好去重 | "语气:活泼" 出现 3 次 → 合并为 1 条 |
| confidence 取最高 | 保留最自信的判断 | 0.8 和 0.9 → 取 0.9 |
| count 累加 | 记录偏好频率 | 说 3 次"活泼" → count=3 |
| 按 confidence × count 排序 | 既自信又频繁的排前面 | 0.9×3 > 0.7×1 |
为什么用 confidence × count 排序? --- 只出现过 1 次但置信度高(0.9×1=0.9),和出现过 3 次但置信度低(0.5×3=1.5),后者更可靠 --- 用户反复说说明这是稳定偏好,不是一时兴起。
五、第2层:风格画像生成
第1层积累的是结构化标签 (style_preferences),第2层把这些标签汇总成自然语言画像 (style_profile)。
build_profile:汇总全流程
python
async def build_profile(self, persona: PersonaConfig) -> tuple[str, list[dict]]:
# 1. 收集该人设下所有修改记录
all_revisions = []
for session in self.store.list_chat_sessions():
if session.persona_id != persona.id:
continue
all_revisions.extend(session.revisions)
if not all_revisions:
return "", []
# 2. 从每条修改记录提取偏好
all_preferences = []
for rev in all_revisions:
prefs = await self.extract_preference_from_revision(rev)
all_preferences.extend(prefs)
# 3. 合并去重
merged = self._merge_preferences(all_preferences)
# 4. LLM 生成风格画像摘要
profile = await self._generate_profile_summary(persona, merged)
return profile, merged
4 步汇总:收集所有修改 → 逐条提取偏好 → 合并去重 → LLM 生成画像。
_generate_profile_summary:LLM 生成自然语言画像
python
async def _generate_profile_summary(self, persona, preferences) -> str:
system_prompt = (
"你是一位内容风格专家。根据用户的风格偏好列表,生成一段简洁的风格画像摘要。\n\n"
"要求:\n"
"1. 用第二人称('你倾向于...')\n"
"2. 按置信度从高到低组织\n"
"3. 语言简洁有力,每点不超过15字\n"
)
pref_text = "\n".join(
f"- {p['category']}:{p['value']}(置信度 {p.get('confidence', 0.5):.1f},出现 {p.get('count', 1)} 次)"
for p in preferences
)
result = await self.llm.generate(system_prompt=system_prompt, user_prompt=...)
return result.strip()
输入(结构化偏好列表):
diff
- 语气:活泼(置信度 0.9,出现 3 次)
- emoji:低频(置信度 0.8,出现 2 次)
- 段落:短(置信度 0.7,出现 1 次)
- 用词:口语化(置信度 0.8,出现 2 次)
输出(自然语言画像):
你倾向于活泼口语化表达;emoji使用克制;喜欢用数据支撑观点;段落简短有力
结构化标签 → 自然语言画像 --- 画像更简洁、更适合注入 Prompt。
apply_to_persona:写入人设
ruby
def apply_to_persona(self, persona, profile, preferences) -> PersonaConfig:
updates = {}
if profile:
updates["style_profile"] = profile
if preferences:
updates["style_preferences"] = preferences
updates["updated_at"] = datetime.now()
updated = persona.model_copy(update=updates) # ← 不可变更新
self.store.save_persona(updated)
return updated
用 model_copy 做不可变更新 --- 不修改原对象,创建新实例,安全且可追溯。
为什么第2层是手动触发?
python
# build_profile 要遍历所有会话 + 每条修改都调 LLM 提取偏好
for session in self.store.list_chat_sessions(): # 遍历所有会话
for rev in session.revisions: # 遍历所有修改
prefs = await self.extract_preference_from_revision(rev) # 每条调 LLM
开销大 --- 10 条修改记录 = 10 次 LLM 调用。所以第2层是手动触发(用户在界面上点"生成风格画像"),不是每次修改都跑。
但第1层是即时的 --- 每次修改只提取当前这条的偏好(1 次 LLM 调用),开销可控。
六、Prompt 注入:让进化生效
存了偏好不读等于没存。build_style_hint 在每次生成时自动注入:
less
@staticmethod
def build_style_hint(persona: PersonaConfig) -> str:
hints = []
# 1. 风格画像摘要(第2层产出)
if persona.style_profile:
hints.append(f"【风格画像】\n{persona.style_profile}")
# 2. 结构化偏好提醒(第1层产出,最多 8 条)
prefs = persona.style_preferences
if prefs:
pref_lines = []
for p in prefs[:8]: # ← 最多 8 条,防止 token 超限
cat = p.get("category", "")
val = p.get("value", "")
count = p.get("count", 1)
if count > 1:
pref_lines.append(f"- {cat}:{val}(你之前 {count} 次要求此调整)")
else:
pref_lines.append(f"- {cat}:{val}")
hints.append("【风格偏好提醒】\n" + "\n".join(pref_lines))
return "\n\n".join(hints)
注入后的 Prompt 效果:
diff
【风格画像】
你倾向于活泼口语化表达;emoji使用克制;喜欢用数据支撑观点;段落简短有力
【风格偏好提醒】
- 语气:活泼(你之前 3 次要求此调整)
- emoji:低频(你之前 2 次要求此调整)
- 用词:口语化(你之前 2 次要求此调整)
两个关键设计:
- 画像 + 偏好双重注入 --- 画像是浓缩摘要,偏好是详细列表,两者互补
prefs[:8]最多 8 条 --- 防止偏好太多导致 Prompt token 超限
注入点:content/body.py
makefile
# content/body.py --- 生成正文时自动注入
style_hint = StyleLearner.build_style_hint(persona)
if style_hint:
user_prompt += style_hint + "\n\n"
user_prompt += "请创作正文内容。"
每次生成正文前,自动检查人设有没有风格画像和偏好,有就注入。 用户不需要做任何事 --- 进化在后台自动生效。
"你之前 N 次要求此调整"的心理学
python
if count > 1:
pref_lines.append(f"- {cat}:{val}(你之前 {count} 次要求此调整)")
不只是告诉 LLM "用活泼语气",还告诉它 "用户之前 3 次要求此调整" --- 这给了 LLM 更强的信号:这不是随便说的,是用户反复强调的稳定偏好,要严格遵守。
七、第3层:发布数据反馈(未来)
前两层都是修改驱动 --- 用户手动修改后才学习。第3层是数据驱动 --- 发布后根据数据反馈自动调整。
当前缺失的闭环
当前:生成 → 用户修改 → 提取偏好 → 进化
未来:生成 → 发布 → 数据反馈 → 自动进化
第3层的设想
| 数据信号 | 推断偏好 | 自动调整 |
|---|---|---|
| 文章点赞高 | 当前风格受欢迎 | 强化当前风格特质 |
| 文章点赞低 | 当前风格不受欢迎 | 弱化当前风格特质 |
| 评论区说"太长" | 段落偏好更短 | 增加偏好 段落:短 |
| 评论区说"太正式" | 语气偏好更活泼 | 增加偏好 语气:活泼 |
| 完播率高(视频) | 脚本结构好 | 强化当前脚本结构 |
第3层让 Agent 不只从用户修改中学习,还从真实发布数据中学习 --- 这才是真正的"越用越懂你"。
但第3层需要对接平台数据 API,当前项目还没做 --- 这是 V2/V3 的方向。
踩坑总结
| 坑 | 根因 | 修复 |
|---|---|---|
| 偏好提取 JSON 格式不稳定 | LLM 输出不可控 | try/except 容错 + 去掉 markdown 代码块 |
| 画像太长 token 超限 | 偏好全量注入 | prefs[:8] 最多注入 8 条 |
| 用户多次说同样的话 | 偏好没去重 | _merge_preferences 合并,count 累加 |
| 偏好排序不合理 | 只按置信度排 | 改为 confidence × count,既自信又频繁的排前面 |
| 修改 diff 太长 | 全文传入 LLM | 截取前后各 500 字 |
| 第2层开销大 | 每条修改都调 LLM | 改为手动触发,不是每次修改都跑 |
| 画像生成失败 | LLM 异常 | 降级:手动拼接前 5 条偏好 |
| 进化不生效 | 存了偏好没注入 | build_style_hint 每次生成自动注入 |
经验总结
- 风格进化是 Agent 从"工具"变"助手"的关键跃迁 --- 不是开发者写死的规则,是 Agent 从用户行为中学到的偏好
- 三层进化:即时提取 → 画像生成 → 数据反馈 --- 第1层自动、第2层手动、第3层未来,逐步从被动到主动
- 偏好合并算法是核心 --- 去重 + 置信度取最高 + count 累加 +
confidence × count排序,既自信又频繁的排前面 - 进化的本质是 Prompt 进化 --- 画像和偏好注入生成 Prompt,Agent 的 Prompt 不是写死的,是从用户行为中学到的
- 存了不读等于没存 ---
build_style_hint在每次生成时自动注入,用户无感知,进化在后台自动生效
下篇预告
下一篇讲 对话式修改:Agent的人机协作模式 --- Chat as Interface,对话不是聊天,是最高效的人机协作方式。修改建议→LLM编辑→版本管理→内容更新。