liuzhiheng 2 месяцев назад
Родитель
Сommit
a9f57d0fdc

+ 8 - 2
examples_how/overall_derivation/input/家有大志/persona_data/persona_summary.json

@@ -1,4 +1,10 @@
 {
-  "主要品类": "脑洞视觉创意",
-  "核心总结": "以第一人称吐槽视角呈现离谱DIY与超现实视觉奇观,为追求趣味解压的年轻职场人提供极具冲击力的生活创意及场景化好物种草。"
+  "主要品类": "视觉创意种草",
+  "核心总结": "面向年轻情侣与职场打工人群体,通过夸张脑洞的借位摄影与搞笑日常剧情,提供幽默解压的情绪价值,并巧妙完成护肤与生活好物的场景化产品植入。",
+  "创作方向": [
+    "视觉错位与夸张构图创意拍摄:利用借位拍摄、DIY奇葩道具、前后对比及图文后期处理,制造具有强烈视觉反差感和超现实效果的创意影像。",
+    "情侣与职场搞笑互动小剧场:围绕“懒人妻子”、“怨种男友”及“打工人摸鱼”等固定人设标签,通过恶作剧、反转揭秘等叙事手法展现令人捧腹的日常互动。",
+    "脑洞场景下的产品痛点植入:将面部护肤、数码家电等商业产品无缝融入极寒、暴晒、奇葩姿势等极端或搞笑的生活场景中,以问题解决式的叙事逻辑展示产品卖点。"
+  ],
+  "分析过程": "1. 穿透现象看本质:由意图与实质树可知,虽然表现形式为生活日常(情侣/职场),但核心常量是“产品植入”与“护肤需求/商业产品”,可见本质是带货种草账号。2. 解构视觉与叙事:形式树中“借位摄影”、“夸张”、“视觉反差”等节点权重极高且为常量,说明账号核心壁垒在于奇特的视觉表现力;叙事上依赖“图文解说”、“补充说明式”及“反转揭秘”,风格幽默口语化。3. 提取人设与方向:聚类树高频出现“打工人”、“懒人妻子”、“摸鱼”、“恶作剧”等节点,推导出账号通过构建接地气的搞笑人设,结合视觉特效,最终服务于高客单价(数码)与高频(洗护/美妆)产品的创意营销。"
 }

+ 4 - 2
examples_how/overall_derivation/tools/search_and_eval.py

@@ -65,6 +65,9 @@ def _load_persona_text(account_name: str) -> str:
         return f"账号:{account_name}(暂无人设数据)"
     with open(persona_file, "r", encoding="utf-8") as f:
         data = json.load(f)
+    # 去掉不需要给 LLM 的中间推理字段,避免 prompt 过长或泄露分析细节
+    if isinstance(data, dict):
+        data.pop("分析过程", None)
     logger.debug("_load_persona_text: loaded persona for account=%s", account_name)
     return json.dumps(data, ensure_ascii=False, indent=2)
 
@@ -203,8 +206,7 @@ async def _eval_single_post(
         )
 
         # 仅对与人设匹配的帖子做选题点匹配
-        # if result["persona_match_result"] and result["post_keywords"]:
-        if result["post_keywords"]:
+        if result["persona_match_result"] and result["post_keywords"]:
             matched = await match_derivation_to_post_points(
                 result["post_keywords"], account_name, post_id
             )

+ 169 - 0
examples_how/overall_derivation/topic_summary.py

@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+"""
+账号人设总结:
+1. 从 input/{account_name}/tree 目录下读取人设树 JSON 文件并合并
+2. 将合并后的 JSON 填充到 topic_summary_prompt.md 中的 {topic_point_tree}
+3. 调用大模型生成账号人设总结,写入 input/{account_name}/persona_data/persona_summary.json
+"""
+
+import argparse
+import asyncio
+import json
+import logging
+import sys
+from pathlib import Path
+from typing import Any, Dict
+
+logger = logging.getLogger(__name__)
+
+
+# 确保可以导入 agent 内的 LLM 调用封装
+_project_root = Path(__file__).resolve().parent.parent.parent
+if str(_project_root) not in sys.path:
+    sys.path.insert(0, str(_project_root))
+
+try:
+    from agent.llm.openrouter import openrouter_llm_call
+except ImportError:  # pragma: no cover - 仅用于本地缺少依赖时的降级提示
+    openrouter_llm_call = None  # type: ignore[assignment]
+
+
+# 复用与 search_and_eval 相同的模型,保证行为一致
+EVAL_LLM_MODEL = "google/gemini-3.1-pro-preview"
+
+BASE_DIR = Path(__file__).resolve().parent
+INPUT_BASE = BASE_DIR / "input"
+
+
+def _extract_json_object(content: str) -> Dict[str, Any]:
+    """
+    从 LLM 回复中解析第一个 JSON 对象(允许被 ```json ... ``` 包裹)。
+    逻辑参考 tools/search_and_eval.py 中的实现。
+    """
+    content = content.strip()
+
+    # 处理 ```json ... ``` 包裹的情况
+    import re
+
+    m = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
+    if m:
+        content = m.group(1).strip()
+
+    # 截取最外层 { ... }
+    start = content.find("{")
+    end = content.rfind("}")
+    if start != -1 and end != -1:
+        content = content[start : end + 1]
+    return json.loads(content)
+
+
+def _load_topic_point_tree(account_name: str) -> Dict[str, Any]:
+    """
+    读取 input/{account_name}/tree 目录下的所有 JSON 文件,并合并成一个字典:
+    {
+      "<文件名去掉后缀>": <该文件对应的树 JSON>,
+      ...
+    }
+    """
+    tree_dir = INPUT_BASE / account_name / "tree"
+    if not tree_dir.is_dir():
+        raise FileNotFoundError(f"人设树目录不存在: {tree_dir}")
+
+    merged: Dict[str, Any] = {}
+    files = sorted(tree_dir.glob("*.json"))
+    if not files:
+        raise FileNotFoundError(f"人设树目录中未找到任何 JSON 文件: {tree_dir}")
+
+    for path in files:
+        with open(path, "r", encoding="utf-8") as f:
+            try:
+                data = json.load(f)
+            except json.JSONDecodeError as e:
+                raise ValueError(f"解析 JSON 文件失败: {path}") from e
+        merged[path.stem] = data
+        logger.info("已加载人设树文件: %s", path.name)
+
+    return merged
+
+
+def _load_prompt_template() -> str:
+    """读取 topic_summary_prompt.md 模板。"""
+    prompt_path = BASE_DIR / "topic_summary_prompt.md"
+    if not prompt_path.is_file():
+        raise FileNotFoundError(f"找不到 prompt 模板文件: {prompt_path}")
+    with open(prompt_path, "r", encoding="utf-8") as f:
+        return f.read()
+
+
+async def generate_topic_summary(account_name: str) -> Dict[str, Any]:
+    """
+    生成账号人设总结,并返回解析后的 JSON 结果。
+    同时将结果写入 persona_summary.json 文件。
+    """
+    if openrouter_llm_call is None:
+        raise RuntimeError("未找到 openrouter_llm_call,请检查 agent.llm 依赖是否可用。")
+
+    # 1. 加载并合并人设树
+    topic_tree = _load_topic_point_tree(account_name)
+    topic_tree_str = json.dumps(topic_tree, ensure_ascii=False, indent=2)
+    logger.info("已合并人设树,共包含 %d 个子树", len(topic_tree))
+
+    # 2. 读取并填充 prompt 模板
+    prompt_template = _load_prompt_template()
+    system_prompt = prompt_template.replace("{topic_point_tree}", topic_tree_str)
+
+    # 3. 调用 LLM 生成总结
+    messages = [
+        {"role": "system", "content": system_prompt},
+        {
+            "role": "user",
+            "content": "请根据以上说明,严格按照 JSON 模板输出账号人设总结,仅输出 JSON,不要包含其他解释性文字。",
+        },
+    ]
+
+    logger.info("开始调用 LLM 生成账号人设总结,account_name=%s", account_name)
+    llm_result = await openrouter_llm_call(messages, model=EVAL_LLM_MODEL)
+    content = llm_result.get("content", "") if isinstance(llm_result, dict) else ""
+    if not content:
+        raise RuntimeError("LLM 未返回任何内容")
+
+    try:
+        summary_data = _extract_json_object(content)
+    except Exception as e:  # noqa: BLE001
+        logger.exception("解析 LLM 返回的 JSON 失败")
+        raise RuntimeError(f"解析 LLM 返回内容失败: {e}") from e
+
+    # 4. 写入 persona_summary.json
+    persona_dir = INPUT_BASE / account_name / "persona_data"
+    persona_dir.mkdir(parents=True, exist_ok=True)
+    persona_file = persona_dir / "persona_summary.json"
+    with open(persona_file, "w", encoding="utf-8") as f:
+        json.dump(summary_data, f, ensure_ascii=False, indent=2)
+    logger.info("已写入账号人设总结到文件: %s", persona_file)
+
+    return summary_data
+
+
+def main(account_name) -> None:
+    # parser = argparse.ArgumentParser(description="根据人设树生成账号人设总结")
+    # parser.add_argument("account_name", help="账号名称(对应 input/{account_name} 目录)")
+    # args = parser.parse_args(argv)
+
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
+        datefmt="%H:%M:%S",
+    )
+
+    logger.info("生成账号人设总结,account_name=%s", account_name)
+
+    async def _run() -> None:
+        summary = await generate_topic_summary(account_name)
+        print(json.dumps(summary, ensure_ascii=False, indent=2))
+
+    asyncio.run(_run())
+
+
+if __name__ == "__main__":
+    main(account_name="家有大志")
+

+ 1 - 1
examples_how/overall_derivation/topic_summary_prompt.md

@@ -29,7 +29,7 @@
 一句话概括账号定位,需包含:**目标人群 + 内容形式/风格 + 核心价值**
 
 ## 3. 创作方向
-识别账号内容创作的个主要方向
+识别账号内容创作的个主要方向
 
 # 输出要求
 仅输出JSON,无任何额外文字: