baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
145
backend/app/services/ai_engine/preference_aggregator.py
Normal file
145
backend/app/services/ai_engine/preference_aggregator.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
偏好飞轮聚合(preference_aggregator)
|
||||
扒自:Clover架构方案.md §偏好飞轮怎么转 + PRD §8
|
||||
三层继承:L1 公司品牌基线 > L2 矩阵号人设(二期)> L3 个人手感
|
||||
聚合最近 FLYWHEEL_LOOKBACK 条 events → prompt 片段注入文案生成
|
||||
|
||||
关键:
|
||||
- 按 product_id 分开学(素颜霜偏好不串精华)
|
||||
- 信号不足 FLYWHEEL_COLD_START 条时,用产品档案冷启动
|
||||
- 返回结构对齐 API契约 GET /tasks/{id}/preference/context
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
from .constants import FLYWHEEL_LOOKBACK, FLYWHEEL_COLD_START
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def aggregate_preference_context(
|
||||
events: list[dict],
|
||||
product: dict,
|
||||
workspace_id: int,
|
||||
product_id: int,
|
||||
) -> dict:
|
||||
"""
|
||||
输入:最近 preference_events 行(已按 workspace_id+product_id 过滤)
|
||||
输出:{recent_preference, reject_reasons, injected_count, prompt_fragment}
|
||||
prompt_fragment 直接注入文案生成 prompt
|
||||
"""
|
||||
# 按 product_id 过滤(防串货)
|
||||
relevant = [
|
||||
e for e in events
|
||||
if e.get("workspace_id") == workspace_id and e.get("product_id") == product_id
|
||||
][:FLYWHEEL_LOOKBACK]
|
||||
|
||||
injected_count = len(relevant)
|
||||
|
||||
if injected_count < FLYWHEEL_COLD_START:
|
||||
# 冷启动:用产品档案静态基线
|
||||
return _cold_start(product, injected_count)
|
||||
|
||||
# ── 统计最常选角度(text_select + approve 信号)
|
||||
angle_counts: Counter = Counter()
|
||||
reject_reasons: list[str] = []
|
||||
|
||||
for e in relevant:
|
||||
sig_type = e.get("signal_type", "")
|
||||
angle = str(e.get("angle_label", "")).strip()
|
||||
weight = int(e.get("signal_weight", 1))
|
||||
|
||||
if sig_type in ("text_select", "approve") and angle:
|
||||
angle_counts[angle] += weight
|
||||
elif sig_type == "reject_with_reason":
|
||||
reason = str(e.get("reason", "")).strip()
|
||||
if reason:
|
||||
reject_reasons.append(reason)
|
||||
|
||||
# 取权重最高的角度
|
||||
top_angles = [a for a, _ in angle_counts.most_common(3)]
|
||||
# 取最近3条打回原因
|
||||
recent_rejects = reject_reasons[-3:] if reject_reasons else []
|
||||
|
||||
# ── 拼 prompt 片段(三层继承:L1>L2>L3,一期只跑L1+L3)
|
||||
prompt_fragment = _build_prompt_fragment(top_angles, recent_rejects, product)
|
||||
|
||||
# ── 人类可读摘要(前端"本次已注入"显示)
|
||||
if top_angles:
|
||||
pref_summary = f"最近偏好角度:{'、'.join(top_angles)}(已选{injected_count}次信号)"
|
||||
else:
|
||||
pref_summary = f"已注入{injected_count}条偏好信号"
|
||||
|
||||
return {
|
||||
"recent_preference": pref_summary,
|
||||
"reject_reasons": recent_rejects,
|
||||
"injected_count": injected_count,
|
||||
"prompt_fragment": prompt_fragment, # 注入 generate_text_variants extra_rules
|
||||
}
|
||||
|
||||
|
||||
def _cold_start(product: dict, injected_count: int) -> dict:
|
||||
"""信号不足时用产品档案基线"""
|
||||
angles = product.get("text_angles") or []
|
||||
style = product.get("style_tone", "素人分享风")
|
||||
fragment = ""
|
||||
if angles:
|
||||
fragment = f"优先覆盖以下文案角度:{'、'.join(angles[:3])}。风格调性:{style}。"
|
||||
return {
|
||||
"recent_preference": f"冷启动(历史信号{injected_count}条,不足{FLYWHEEL_COLD_START}条),使用产品档案基线",
|
||||
"reject_reasons": [],
|
||||
"injected_count": injected_count,
|
||||
"prompt_fragment": fragment,
|
||||
}
|
||||
|
||||
|
||||
def _build_prompt_fragment(
|
||||
top_angles: list[str],
|
||||
reject_reasons: list[str],
|
||||
product: dict,
|
||||
) -> str:
|
||||
"""
|
||||
组装注入文案 prompt 的片段
|
||||
越积累越精准:1次=全靠基线;10次=知道偏好角度;30次=措辞从"供参考"升为明确指令
|
||||
"""
|
||||
lines: list[str] = []
|
||||
if top_angles:
|
||||
lines.append(f"【偏好角度参考】历史选择偏好:{'、'.join(top_angles)},请优先采用这些角度方向。")
|
||||
if reject_reasons:
|
||||
formatted = ";".join(f"「{r}」" for r in reject_reasons)
|
||||
lines.append(f"【打回原因参考】以下问题请主动规避:{formatted}。")
|
||||
# L1 品牌基线(产品档案 custom_prompt)
|
||||
custom = (product.get("custom_prompt") or "").strip()
|
||||
if custom:
|
||||
lines.append(f"【品牌基线】{custom}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def collect_preference_event(
|
||||
signal_type: str,
|
||||
user_id: int,
|
||||
workspace_id: int,
|
||||
product_id: int,
|
||||
angle_label: str = "",
|
||||
reason: str = "",
|
||||
weights: dict[str, int] | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
构造 preference_event 行(由业务接口内部调用,不暴露给前端)
|
||||
返回待插 DB 的字段 dict
|
||||
"""
|
||||
from .constants import FLYWHEEL_WEIGHTS
|
||||
w_map = weights or FLYWHEEL_WEIGHTS
|
||||
weight = w_map.get(signal_type, 0)
|
||||
return {
|
||||
"signal_type": signal_type,
|
||||
"signal_weight": weight,
|
||||
"user_id": user_id,
|
||||
"workspace_id": workspace_id,
|
||||
"product_id": product_id,
|
||||
"angle_label": angle_label,
|
||||
"reason": reason,
|
||||
"data_ownership": "client_data", # 原始行为信号归客户(PRD §3 data_ownership)
|
||||
}
|
||||
Reference in New Issue
Block a user