A8: 文案按A/B/C三套正交叙事生成,避免套路化重复

- constants: 新增 TEXT_NARRATIVE_BY_STRATEGY(A痛点/B场景/C成分),与图片侧同轴
- build_prompt: 加 strategy_narrative 参数并注入 prompt
- text_variants: 全链路透传(含优化轮)
- run_text_generation: 改循环三套,text_count均摊(divmod余前补),跨套去重,打_strategy标记
- TextCandidate: 加 strategy String(4) 字段 + 迁移021(已upgrade head)
- packaging: 打包按strategy精准配对文图(texts_by_strategy映射+三层兜底)
- SSE text_candidate 事件携带 strategy

独立agent交叉验证7改造点全过,边界(text_count<3/无别名/不截断)无must-fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-22 10:00:47 +08:00
parent 4bed7425a8
commit ff21116ff8
8 changed files with 123 additions and 26 deletions

View File

@@ -80,12 +80,27 @@ def build_delivery_package(self, package_id: int) -> dict:
if prev is None or ic.id > prev.id:
slot[ic.seq] = ic # 同 seq 留最新
# 文案按 strategy 建映射供图片组按套精准配对A8文图同套对齐不靠脆弱 idx
# 同套多条选中取第 1 条;老数据 strategy=None 的归入 fallback 列表。
texts_by_strategy: dict = {}
texts_no_strategy: list = []
for tc in selected_texts:
if tc.strategy:
texts_by_strategy.setdefault(tc.strategy, tc)
else:
texts_no_strategy.append(tc)
_text_fallback = iter(texts_no_strategy or selected_texts)
notes = []
for idx, (_strategy, slot) in enumerate(groups.items()):
images_data = [_read_image(slot[k]) for k in sorted(slot)]
# 文案配对:选中文案数≥套数则一套一条;否则各套共用第 1 条
# (图均以第 1 条文案为语境生成,共用合理;多选则尊重运营按套选的文案)
tc = selected_texts[idx] if idx < len(selected_texts) else selected_texts[0]
# 配对优先级:①同 strategy 文案精准对齐 ②无同套则按顺序取无套文案
# ③再兜底用第 idx 条/第 1 条,确保每组图都有文案不漏。
tc = texts_by_strategy.get(_strategy)
if tc is None:
tc = next(_text_fallback, None)
if tc is None:
tc = selected_texts[idx] if idx < len(selected_texts) else selected_texts[0]
text_data = json.loads(tc.content or "{}")
notes.append({
"title": text_data.get("title", ""),

View File

@@ -46,7 +46,7 @@ def run_text_generation(db, clients, task, product_dict: dict, flywheel_fragment
from app.models.flywheel import AiCallLog
from app.models.workspace import UserApiKey
from app.constants.enums import CandidateSource, BannedWordStatus
from app.services.ai_engine.constants import QUALITY_PASS_SCORE
from app.services.ai_engine.constants import QUALITY_PASS_SCORE, TEXT_NARRATIVE_BY_STRATEGY
banned_rows = db.query(BannedWord).filter(
BannedWord.workspace_id == workspace_id
@@ -61,20 +61,37 @@ def run_text_generation(db, clients, task, product_dict: dict, flywheel_fragment
).first()
key_id = key_row.id if key_row else None
# A8文案按 A/B/C 三套正交叙事分别生成,每套不同角度避免套路化重复,
# 与图片侧同轴(A痛点/B场景/C成分)。text_count 均摊三套(余数前补)
# 逐套把已生成的喂作 previous_copies 做跨套去重。
_strategies = ("A", "B", "C")
_base, _rem = divmod(task.text_count, 3)
_per = {s: _base + (1 if i < _rem else 0) for i, s in enumerate(_strategies)}
t0 = time.monotonic()
llm_success = True
try:
candidates_raw = asyncio.run(generate_text_variants(
llm_client=clients,
product=product_dict,
count=task.text_count,
banned_word_rows=banned_dicts,
flywheel_context=flywheel_fragment,
))
except Exception as exc:
llm_success = False
logger.error("generate_text_variants 失败: %s", exc)
candidates_raw = []
candidates_raw: list = []
for s in _strategies:
n = _per[s]
if n <= 0:
continue
try:
part = asyncio.run(generate_text_variants(
llm_client=clients,
product=product_dict,
count=n,
previous_copies=candidates_raw,
banned_word_rows=banned_dicts,
flywheel_context=flywheel_fragment,
strategy_narrative=TEXT_NARRATIVE_BY_STRATEGY.get(s, ""),
))
except Exception as exc:
llm_success = False
logger.error("generate_text_variants(套%s) 失败: %s", s, exc)
part = []
for c in part:
c["_strategy"] = s
candidates_raw.extend(part)
latency_ms = int((time.monotonic() - t0) * 1000)
# 写 ai_call_logs留痕不含明文key
@@ -113,6 +130,7 @@ def run_text_generation(db, clients, task, product_dict: dict, flywheel_fragment
workspace_id=workspace_id,
task_id=task.id,
source=CandidateSource.AI,
strategy=c.get("_strategy"),
angle_label=c.get("angle_label") or c.get("angle", ""),
content=json.dumps(c, ensure_ascii=False),
score_json=json.dumps(c.get("score_detail", []), ensure_ascii=False),
@@ -124,6 +142,7 @@ def run_text_generation(db, clients, task, product_dict: dict, flywheel_fragment
seq += 1
push_fn(task.id, workspace_id, "text_candidate", {
"candidate_id": tc.id, "angle_label": tc.angle_label,
"strategy": tc.strategy,
"content": c.get("content", ""), "score": score,
}, seq)
seq += 1