B站视频内容智能分析系统(五):LLM 内容精炼与多域分类

系列文章目录

B站视频内容智能分析系统(一):项目介绍与架构设计

B站视频内容智能分析系统(二):Docker Compose 一键部署

B站视频内容智能分析系统(三):B站视频自动采集

B站视频内容智能分析系统(四):语音转写三级回退

B站视频内容智能分析系统(五):LLM 内容精炼与多域分类

文章目录

  • 系列文章目录
  • 前言
  • 一、为什么需要精炼
  • 二、精炼流程总览
  • 三、双域精炼体系
    • [1. 情感域(emotional)](#1. 情感域(emotional))
    • [2. 求职域(career)](#2. 求职域(career))
    • [3. 域选择](#3. 域选择)
  • [四、精炼 Prompt 设计](#四、精炼 Prompt 设计)
    • [1. 三段式结构](#1. 三段式结构)
    • [2. Prompt 模板](#2. Prompt 模板)
    • [3. 输出校验](#3. 输出校验)
    • [4. 清理 LLM 思考痕迹](#4. 清理 LLM 思考痕迹)
  • 五、分类体系
    • [1. 31 个情感分类](#1. 31 个情感分类)
    • [2. 18 个求职分类](#2. 18 个求职分类)
    • [3. 分类 Prompt](#3. 分类 Prompt)
    • [4. 分类结果解析](#4. 分类结果解析)
  • [六、LLM 调用细节](#六、LLM 调用细节)
    • [1. API 调用封装](#1. API 调用封装)
    • [2. 重试机制](#2. 重试机制)
    • [3. 速率控制](#3. 速率控制)
  • 七、精炼结果入库
    • [1. 写入 DuckDB](#1. 写入 DuckDB)
    • [2. 写入 ChromaDB](#2. 写入 ChromaDB)
  • 八、完整精炼流程
  • 总结

前言

上一篇讲了怎么把音频转成文字。但转写出来的原始文本通常很长、很散------一个 20 分钟的视频,转写文本可能有 5000-8000 字,里面夹杂着口语化的表达、重复的内容、无关的闲聊。

如果直接把这些原文存进知识库,后面做 RAG 检索时效果会很差------搜索"吵架后怎么和好",可能会匹配到一大堆不相关的内容。

所以需要在入库前做一步LLM 精炼:用大模型把长篇大论浓缩成结构化的摘要,同时自动分类。这样后续检索时,既能精准命中,又能在搜索结果里直接看到核心观点。

一、为什么需要精炼

先看一个实际例子。一个 20 分钟的视频"女生不回消息怎么办",转写出来大概是这样的:

复制代码
大家好欢迎来到我的频道 今天我们来聊一个很多兄弟都会遇到的问题
就是女生突然不回你消息了怎么办 我有个学员他就遇到了这种情况
他跟我说 老师我跟一个女生聊了一个月了 聊得挺好的 但是
突然有一天她就不回我了 我发了好几条消息她都不回 然后
我就很着急 打了好几个电话她也没接 ...(省略5000字)...
所以总结一下 就是遇到这种情况 第一 不要焦虑 第二 给空间
第三 过几个小时再自然地开启新话题 好了今天的分享就到这里

精炼后的结果:

复制代码
**核心观点**
女生不回消息时不要焦虑追问,冷静给空间后用新话题自然重启对话。

**案例摘要**
学员与女生聊了一个月后对方突然不回消息,连发多条消息和电话均未获回应。
博主分析可能原因包括对方忙碌、话题无趣或情绪测试,建议避免追问式沟通。

**可行动建议**
- 收到不回消息后等待 2-3 小时再回复,不要连续发消息
- 用轻松有趣的新话题重新开启对话,不提"为什么不回我"
- 保持自己的生活节奏,不把注意力全放在一个人身上

精炼后的内容:信息密度高、结构清晰、方便检索。而且自动分到了"10_忽冷忽热"这个分类。

二、精炼流程总览

精炼发生在转写之后、入库之前:

复制代码
转写文本(5000+ 字)
    ↓
  ① refine_content():LLM 生成三段式摘要
    ↓
  ② classify_content():LLM 自动分类
    ↓
  ③ 写入 DuckDB(结构化元数据 + 摘要)
    ↓
  ④ 写入 ChromaDB(全文 + 摘要的向量)

精炼和分类是两步独立调用,用同一个模型(DeepSeek V4 Flash),但用不同的 Prompt。

三、双域精炼体系

1. 情感域(emotional)

这是主要的内容域,覆盖恋爱、两性关系相关的话题。精炼 Prompt 的角色设定是"情感/两性知识内容创作者",分类体系有 31 个类别。

2. 求职域(career)

后来扩展的域,覆盖求职面试、职业规划相关的话题。精炼 Prompt 的角色设定是"求职/职场/职业规划领域内容创作者",分类体系有 18 个类别。

两个域的精炼格式完全一样(三段式),只是 Prompt 的角色和分类体系不同。

3. 域选择

域信息存在每个 UP主 的 YAML 配置里:

yaml 复制代码
# config/恋爱教头桃姐.yaml
name: "恋爱教头桃姐"
uid: "3546912280021515"
domain: "emotional"    # ← 情感域

# config/职场老张.yaml
name: "职场老张"
uid: "123456789"
domain: "career"       # ← 求职域

精炼时根据 domain 字段自动选择对应的 Prompt 和分类体系:

python 复制代码
DOMAINS = {
    "emotional": {
        "name": "情感/两性",
        "refine_prompt": "你是一个情感/两性知识内容创作者...",
        "classify_prompt": "你是一个情感/两性知识内容分类专家...",
        "categories": {...},  # 31 个分类
    },
    "career": {
        "name": "求职/职场",
        "refine_prompt": "你是一个求职/职场/职业规划领域内容创作者...",
        "classify_prompt": "你是一个求职/职场知识内容分类专家...",
        "categories": {...},  # 18 个分类
    },
}

def get_domain_config(domain: str) -> dict:
    return DOMAINS.get(domain, DOMAINS["emotional"])

四、精炼 Prompt 设计

1. 三段式结构

精炼的输出格式是固定的三段式:

复制代码
**核心观点**
(一句话精准概括核心观点,不超过50字)

**案例摘要**
(浓缩案例核心,保留关键细节,100-200字)

**可行动建议**
(2-3条具体可执行的建议,每条不超过30字)

为什么是这三段?

  • 核心观点:一句话告诉你这个视频在说什么,用于搜索结果预览
  • 案例摘要:保留具体案例和关键细节,用于 RAG 检索时的上下文
  • 可行动建议:可以直接执行的行动步骤,这是用户最关心的部分

2. Prompt 模板

完整的精炼 Prompt:

python 复制代码
refine_prompt = """你是一个情感/两性知识内容创作者。请将下面的原始素材精炼成统一的三段式结构。

【格式要求】
**核心观点**
(一句话精准概括核心观点,不超过50字)

**案例摘要**
(浓缩案例核心,保留关键细节,100-200字)

**可行动建议**
(2-3条具体可执行的建议,每条不超过30字)

【原始素材】
"""

调用时把原始文本拼到 Prompt 后面:

python 复制代码
def refine_content(raw_text: str, domain: str = "emotional", max_length: int = 3000):
    cfg = get_domain_config(domain)
    prompt = cfg["refine_prompt"] + raw_text[:max_length]
    result = _call_llm(prompt, max_tokens=1500, temperature=0.3)
    return result

max_length=3000 限制了输入长度,避免超长文本浪费 token。实际上 3000 字的输入对于提炼核心观点来说足够了------再长的内容,核心信息通常也在前半部分。

temperature=0.3 用较低的温度,因为精炼是一个"提取+总结"任务,不需要太多创造性。

3. 输出校验

LLM 有时候会"偷懒",输出的格式不符合要求。所以我做了格式校验:

python 复制代码
def _validate_output(text: str) -> bool:
    return ('**核心观点**' in text
            and '**案例摘要**' in text
            and '**可行动建议**' in text)

三个标题都出现才算通过。不通过就重试,最多重试 2 次:

python 复制代码
for attempt in range(MAX_RETRIES):
    result = _call_llm(prompt, max_tokens=1500, temperature=0.3)
    if not result:
        continue

    result = _clean_response(result)

    if _validate_output(result):
        return result
    else:
        print(f"精炼格式不完整,第{attempt+1}次重试")

4. 清理 LLM 思考痕迹

DeepSeek V4 Flash 是一个推理模型,输出里可能包含 <think>...</think> 标签。这些思考过程对我们没用,需要清理掉:

python 复制代码
def _clean_response(text: str) -> str:
    text = re.sub(r"<thinking>[\s\S]*?</thinking>", "", text)
    text = re.sub(r"<Thought>[\s\S]*?</Thought>", "", text)
    text = re.sub(r"【思考】[\s\S]*?【/思考】", "", text)
    return text.strip()

用正则把各种思考标签和内容全部干掉,只保留最终输出。

五、分类体系

1. 31 个情感分类

情感域的分类是我根据实际内容手动整理的,覆盖了恋爱关系的各个阶段:

python 复制代码
"categories": {
    "01_喜欢":      "喜欢/心动/爱",
    "02_聊天":      "聊天技巧/话题/冷场/破冰",
    "03_撩妹":      "撩/暧昧/调情/升温",
    "04_筛选":      "筛选女生/识别渣女/捞女",
    "05_拒绝":      "表白/拒绝/好人卡/被发卡",
    "06_备胎":      "备胎/海王/鱼塘/养鱼",
    "07_修养":      "男生修养/特质/气场/框架",
    "08_婚姻":      "婚姻/相亲/彩礼/条件",
    "09_推进":      "关系推进/牵手/确认关系",
    "10_忽冷忽热":  "忽冷忽热/冷淡/不回消息",
    "11_话术":      "话术/公式/万能回复/幽默",
    # ... 共 31 个分类
    "32_两性健康":  "两性健康/生理知识",
}

分类编号用两位数字前缀(01-32),方便排序和过滤。分类名后面跟着关键词描述,帮助 LLM 理解每个分类的含义。

注意编号不是连续的------中间跳过了 20(原来是"出轨",后来合并到其他分类了),直接到 21。这种历史遗留问题在项目中很常见。

2. 18 个求职分类

python 复制代码
"categories": {
    "01_面试技巧":    "面试准备/自我介绍/常见问题",
    "02_简历优化":    "简历修改/项目包装/关键词优化",
    "03_薪资谈判":    "薪资议价/福利谈判/薪资结构",
    # ... 共 18 个分类
    "18_校招经验":    "秋招/春招/管培生/应届生策略",
}

3. 分类 Prompt

分类是在精炼之后做的,输入是精炼后的摘要:

python 复制代码
def classify_content(refined_text: str, domain: str = "emotional") -> str:
    cfg = get_domain_config(domain)
    categories = cfg["categories"]

    # 构建分类列表
    cat_list = "\n".join([f"{k} - {v}" for k, v in categories.items()])

    prompt = (
        f"{cfg['classify_prompt']}\n\n"
        f"【分类列表】\n{cat_list}\n\n"
        "【要求】\n只输出分类编号和分类名,格式:01 - 分类名\n"
        "不要输出任何解释性文字。\n\n"
        f"【精炼素材】\n{refined_text[:2000]}"
    )

    result = _call_llm(prompt, max_tokens=200, temperature=0.1)

几个关键点:

  • max_tokens=200:分类只需要输出一个编号,不需要长回答
  • temperature=0.1:极低温度,确保分类结果稳定一致
  • "不要输出任何解释性文字":明确要求只输出编号,避免 LLM 啰嗦

4. 分类结果解析

LLM 可能输出各种格式,所以我用正则提取编号:

python 复制代码
def classify_content(refined_text, domain):
    result = _call_llm(prompt, max_tokens=200, temperature=0.1)

    # 提取两位数字编号
    nums = re.findall(r"(?:^|[^0-9])([0-9]{2})(?:[^0-9]|$)", result)
    if not nums:
        return default_cat  # 解析失败,用默认分类

    # 取最后一个编号(LLM 有时候会先猜一个再修正)
    last_num = nums[-1]
    for cat in categories:
        if cat.startswith(last_num):
            return cat

    return default_cat

nums[-1] 取最后一个编号------因为推理模型有时候会先说"可能是 24_心态",然后修正为"最终判断:25_关系",取最后一个更接近最终结论。

如果解析失败(LLM 输出了完全不符合格式的内容),就用默认分类(情感域默认 22_追求,求职域默认 07_职业规划)。

六、LLM 调用细节

1. API 调用封装

LLM 调用用的是最原始的 urllib.request(没用 requests 库,减少依赖):

python 复制代码
def _call_llm(prompt: str, max_tokens: int = 1500, temperature: float = 0.3) -> Optional[str]:
    payload = {
        "model": REFINE_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "temperature": temperature,
    }
    headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}

    req = urllib.request.Request(
        API_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers=headers,
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        data = json.loads(resp.read().decode("utf-8"))
        return data["choices"][0]["message"]["content"].strip()

API 配置全部从 .env 读取,改 .env 就能切换端点和模型,不用改代码:

python 复制代码
API_URL = os.getenv('REFINE_API_URL', '')
API_KEY = os.getenv('REFINE_API_KEY', os.getenv('MINIMAX_API_KEY', ''))
REFINE_MODEL = os.getenv('REFINE_MODEL', '')

2. 重试机制

精炼函数有重试逻辑,最多 2 次:

python 复制代码
for attempt in range(MAX_RETRIES):
    result = _call_llm(prompt, max_tokens=1500, temperature=0.3)
    if not result:
        time.sleep(REFINE_SLEEP)
        continue

    result = _clean_response(result)

    if _validate_output(result):
        return result
    else:
        print(f"精炼格式不完整,第{attempt+1}次重试")
        time.sleep(REFINE_SLEEP)

重试的原因可能是:

  • API 调用失败(网络问题、限流)
  • LLM 输出格式不正确

3. 速率控制

每次 API 调用之间有一个 REFINE_SLEEP = 30 秒的间隔。这是因为 DeepSeek API 有速率限制,连续请求太快会被封。30 秒的间隔在实际使用中基本不会触发限流。

七、精炼结果入库

精炼和分类完成后,结果要写入两个地方。

1. 写入 DuckDB

精炼摘要和分类写入 video_meta 表:

python 复制代码
video_records.append({
    'bvid': bvid,
    'up_name': up_name,
    'up_uid': uid,
    'title': title,
    'publish_date': pub_date,
    'category': category,        # ← LLM 分类结果
    'duration': duration,
    'summary': refined_text,     # ← 三段式精炼摘要
    'tags': tags,
    'domain': domain,            # ← emotional / career
})
db.insert_videos(video_records)

summary 字段存的就是精炼后的三段式文本,category 存的是 LLM 选择的分类。

2. 写入 ChromaDB

精炼结果和原始全文都会写入 ChromaDB 做向量化:

python 复制代码
# 全文写入(用于 RAG 检索)
chroma_writer.add_document(
    text=raw_text,
    metadata={"bvid": bvid, "up_name": up_name, "category": category,
              "content_type": "full"}
)

# 精炼摘要也写入(用于精准匹配)
chroma_writer.add_document(
    text=refined_text,
    metadata={"bvid": bvid, "up_name": up_name, "category": category,
              "content_type": "summary"}
)

同一个视频在 ChromaDB 里至少有两条记录:一条全文、一条摘要。RAG 检索时可以根据 content_type 过滤,也可以混合搜索。

metadata 里的 categoryup_name 可以在检索时做过滤,比如"只看桃姐的忽冷忽热分类的内容"。

八、完整精炼流程

把所有步骤串起来,看完整的精炼+入库流程:

python 复制代码
def refine_and_classify(raw_text: str, domain: str = "emotional"):
    """精炼 + 分类一体化"""
    refined = refine_content(raw_text, domain)
    if not refined:
        return None, get_domain_config(domain)["default_category"]

    category = classify_content(refined, domain)
    return refined, category

monitor.py 的批次处理中调用:

python 复制代码
# 对每个转写文件做精炼
for txt_file in transcripts_dir.glob("*.txt"):
    raw_text = txt_file.read_text(encoding="utf-8")

    # 精炼 + 分类
    refined, category = refine_and_classify(raw_text, domain=domain)

    if refined:
        # 写入 DuckDB
        db.insert_video(bvid=bvid, summary=refined, category=category, ...)

        # 写入 ChromaDB
        chroma.add_document(text=raw_text, metadata={...})
        chroma.add_document(text=refined, metadata={"content_type": "summary", ...})
    else:
        print(f"  ⚠️ {txt_file.name} 精炼失败,使用原始文本")
        # 精炼失败也不丢数据,直接把原文入库

精炼失败了也不会丢数据------会把原始文本直接写入数据库,只是没有结构化的摘要和精确分类。

总结

精炼是连接"原始数据"和"知识检索"的桥梁。通过三段式 Prompt 把散乱的转写文本浓缩成核心观点+案例+建议,再通过分类 Prompt 自动归类到 31 个情感分类(或 18 个求职分类)之一。精炼和分类的结果同时写入 DuckDB(结构化查询)和 ChromaDB(语义检索),为后面的 Text-to-SQL 和 RAG 提供高质量的数据基础。下一篇讲 Text-to-SQL 的 4-Agent Pipeline。

相关推荐
范特西林1 小时前
Android 16 AppFunction 机制分析
android·ai编程
空杯_北冥有鱼1 小时前
第六篇:SpringAI 入门 06|官方核心概念全解析(Models/Prompt/Embedding/RAG/Tool Calling
ai编程
helloweilei1 小时前
手撸一个会“思考”的AI智能体
ai编程
会飞的蛛1 小时前
AI Coding 的终局,不是写更好的 Prompt,而是给 Agent 套上 Harness
ai编程
赛博三把手1 小时前
「2026 最新推荐」AI 大模型 API 中转站 | 国内直连 ChatGPT/Claude/Gemini 稳定优质的 API 接口服务
人工智能·github·ai编程
우리帅杰2 小时前
【AI测试】Python AI大模型介绍
开发语言·人工智能·python·ai编程
红信鸽3 小时前
Windsurf IDE实测:AI原生开发如何重构编程逻辑?
ai编程
Java知识技术分享3 小时前
node安装新版本,并解决opencode和claude code不能用问题
ai·个人开发·ai编程