Files
beige/backend/app/services/ai_engine/preference_aggregator.py
yangqianqian 7f419f4c8b 存量积累:生图素人感约束+评图分+幂等防重跑+审核回路
- 015-017迁移:image_candidate 文案复审/AI视觉分/重生标记
- constants C7素人感约束(反电商摆拍对齐真实笔记)+C3叠字口子
- celery visibility_timeout=2h 防长任务被误判重投重复烧钱(task75教训)
- image_scorer 评图分(只筛选+展示,真实信号才进飞轮权重)
- storyboard/image_gen/pipeline_io 生图存量
- task_actions/tasks/task_service/flywheel 审核回路+飞轮存量

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:16:42 +08:00

147 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
偏好飞轮聚合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") or "").strip()
weight = int(e.get("signal_weight", 1))
# text_edit(改稿)是最强真实信号,角度按权重计入(倩倩姐2026-06-16拍板)
if sig_type in ("text_select", "approve", "text_edit") and angle:
angle_counts[angle] += weight
elif sig_type == "reject_with_reason":
reason = str(e.get("reason") or "").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
}