test:合并express提取和Jargon提取

pull/1421/head
SengokuCola 2025-12-07 16:24:13 +08:00
parent 2e31fa2055
commit b03e245817
5 changed files with 273 additions and 25 deletions

View File

@ -3,7 +3,7 @@ import json
import os import os
import re import re
import asyncio import asyncio
from typing import List, Optional, Tuple, Any from typing import List, Optional, Tuple, Any, Dict
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.database.database_model import Expression from src.common.database.database_model import Expression
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
@ -13,7 +13,8 @@ from src.chat.utils.chat_message_builder import (
) )
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.bw_learner.learner_utils import filter_message_content, is_bot_message from src.bw_learner.learner_utils import filter_message_content, is_bot_message, build_context_paragraph, contains_bot_self_name
from src.bw_learner.jargon_miner import miner_manager
from json_repair import repair_json from json_repair import repair_json
@ -24,7 +25,8 @@ logger = get_logger("expressor")
def init_prompt() -> None: def init_prompt() -> None:
learn_style_prompt = """{chat_str} learn_style_prompt = """{chat_str}
你的名字是{bot_name},现在请你请从上面这段群聊中用户的语言风格和说话方式 你的名字是{bot_name},现在请你完成两个提取任务
任务1请从上面这段群聊中用户的语言风格和说话方式
1. 只考虑文字不要考虑表情包和图片 1. 只考虑文字不要考虑表情包和图片
2. 不要总结SELF的发言 2. 不要总结SELF的发言
3. 不要涉及具体的人名也不要涉及具体名词 3. 不要涉及具体的人名也不要涉及具体名词
@ -33,19 +35,39 @@ def init_prompt() -> None:
注意总结成如下格式的规律总结的内容要详细但具有概括性 注意总结成如下格式的规律总结的内容要详细但具有概括性
例如"AAAAA"可以"BBBBB", AAAAA代表某个场景不超过20个字BBBBB代表对应的语言风格特定句式或表达方式不超过20个字 例如"AAAAA"可以"BBBBB", AAAAA代表某个场景不超过20个字BBBBB代表对应的语言风格特定句式或表达方式不超过20个字
请严格以 JSON 数组的形式输出结果每个元素为一个对象结构如下注意字段名 任务2请从上面这段聊天内容中提取"可能是黑话"的候选项黑话/俚语/网络缩写/口头禅
- 必须为对话中真实出现过的短词或短语
- 必须是你无法理解含义的词语没有明确含义的词语请不要选择有明确含义或者含义清晰的词语
- 排除人名@表情包/图片中的内容纯标点常规功能词如的啊等
- 每个词条长度建议 2-8 个字符不强制尽量短小
- 请你提取出可能的黑话最多30个黑话请尽量提取所有
黑话必须为以下几种类型
- 由字母构成的汉语拼音首字母的简写词例如nbyydsxswl
- 英文词语的缩写用英文字母概括一个词汇或含义例如CPUGPUAPI
- 中文词语的缩写用几个汉字概括一个词汇或含义例如社死内卷
输出要求
将表达方式语言风格和黑话以 JSON 数组输出每个元素为一个对象结构如下注意字段名
[ [
{{"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}}, {{"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}},
{{"situation": "CCCC", "style": "DDDD", "source_id": "7"}} {{"situation": "CCCC", "style": "DDDD", "source_id": "7"}}
{{"situation": "对某件事表示十分惊叹", "style": "使用 我嘞个xxxx", "source_id": "[消息编号]"}}, {{"situation": "对某件事表示十分惊叹", "style": "使用 我嘞个xxxx", "source_id": "[消息编号]"}},
{{"situation": "表示讽刺的赞同,不讲道理", "style": "对对对", "source_id": "[消息编号]"}}, {{"situation": "表示讽刺的赞同,不讲道理", "style": "对对对", "source_id": "[消息编号]"}},
{{"situation": "当涉及游戏相关时,夸赞,略带戏谑意味", "style": "使用 这么强!", "source_id": "[消息编号]"}}, {{"situation": "当涉及游戏相关时,夸赞,略带戏谑意味", "style": "使用 这么强!", "source_id": "[消息编号]"}},
{{"content": "词条", "source_id": "12"}},
{{"content": "词条2", "source_id": "5"}}
] ]
其中 其中
表达方式条目
- situation表示在什么情境下的简短概括不超过20个字 - situation表示在什么情境下的简短概括不超过20个字
- style表示对应的语言风格或常用表达不超过20个字 - style表示对应的语言风格或常用表达不超过20个字
- source_id该表达方式对应的来源行编号即上方聊天记录中方括号里的数字例如 [3]请只输出数字本身不要包含方括号 - source_id该表达方式对应的来源行编号即上方聊天记录中方括号里的数字例如 [3]请只输出数字本身不要包含方括号
黑话jargon条目
- content:表示黑话的内容
- source_id该黑话对应的来源行编号即上方聊天记录中方括号里的数字例如 [3]请只输出数字本身不要包含方括号
现在请你输出 JSON 现在请你输出 JSON
""" """
@ -104,13 +126,25 @@ class ExpressionLearner:
logger.error(f"学习表达方式失败,模型生成出错: {e}") logger.error(f"学习表达方式失败,模型生成出错: {e}")
return None return None
# 解析 LLM 返回的表达方式列表(包含来源行编号) # 解析 LLM 返回的表达方式列表和黑话列表(包含来源行编号)
expressions: List[Tuple[str, str, str]] = self.parse_expression_response(response) expressions: List[Tuple[str, str, str]]
jargon_entries: List[Tuple[str, str]] # (content, source_id)
expressions, jargon_entries = self.parse_expression_response(response)
expressions = self._filter_self_reference_styles(expressions) expressions = self._filter_self_reference_styles(expressions)
# 处理黑话条目,路由到 jargon_miner即使没有表达方式也要处理黑话
if jargon_entries:
await self._process_jargon_entries(jargon_entries, random_msg)
# 如果没有表达方式,直接返回
if not expressions: if not expressions:
logger.info("过滤后没有可用的表达方式style 与机器人名称重复)") logger.info("过滤后没有可用的表达方式style 与机器人名称重复)")
return None return []
# logger.debug(f"学习{type_str}的response: {response}")
logger.info(f"学习的prompt: {prompt}")
logger.info(f"学习的expressions: {expressions}")
logger.info(f"学习的jargon_entries: {jargon_entries}")
logger.info(f"学习的response: {response}")
# 直接根据 source_id 在 random_msg 中溯源,获取 context # 直接根据 source_id 在 random_msg 中溯源,获取 context
filtered_expressions: List[Tuple[str, str, str]] = [] # (situation, style, context) filtered_expressions: List[Tuple[str, str, str]] = [] # (situation, style, context)
@ -173,18 +207,24 @@ class ExpressionLearner:
return learnt_expressions return learnt_expressions
def parse_expression_response(self, response: str) -> List[Tuple[str, str, str]]: def parse_expression_response(self, response: str) -> Tuple[List[Tuple[str, str, str]], List[Tuple[str, str]]]:
""" """
解析 LLM 返回的表达风格总结 JSON提取 (situation, style, source_id) 元组列表 解析 LLM 返回的表达风格总结和黑话 JSON提取两个列表
期望的 JSON 结构 期望的 JSON 结构
[ [
{"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}, {"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}, // 表达方式
{"content": "词条", "source_id": "12"}, // 黑话
... ...
] ]
Returns:
Tuple[List[Tuple[str, str, str]], List[Tuple[str, str]]]:
第一个列表是表达方式 (situation, style, source_id)
第二个列表是黑话 (content, source_id)
""" """
if not response: if not response:
return [] return [], []
raw = response.strip() raw = response.strip()
@ -200,7 +240,8 @@ class ExpressionLearner:
raw = raw.strip() raw = raw.strip()
parsed = None parsed = None
expressions: List[Tuple[str, str, str]] = [] expressions: List[Tuple[str, str, str]] = [] # (situation, style, source_id)
jargon_entries: List[Tuple[str, str]] = [] # (content, source_id)
try: try:
# 优先尝试直接解析 # 优先尝试直接解析
@ -292,15 +333,23 @@ class ExpressionLearner:
for item in parsed_list: for item in parsed_list:
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
# 检查是否是表达方式条目(有 situation 和 style
situation = str(item.get("situation", "")).strip() situation = str(item.get("situation", "")).strip()
style = str(item.get("style", "")).strip() style = str(item.get("style", "")).strip()
source_id = str(item.get("source_id", "")).strip() source_id = str(item.get("source_id", "")).strip()
if not situation or not style or not source_id:
# 三个字段必须同时存在 if situation and style and source_id:
continue # 表达方式条目
expressions.append((situation, style, source_id)) expressions.append((situation, style, source_id))
elif item.get("content"):
# 黑话条目(有 content 字段)
content = str(item.get("content", "")).strip()
source_id = str(item.get("source_id", "")).strip()
if content and source_id:
jargon_entries.append((content, source_id))
return expressions return expressions, jargon_entries
def _filter_self_reference_styles(self, expressions: List[Tuple[str, str, str]]) -> List[Tuple[str, str, str]]: def _filter_self_reference_styles(self, expressions: List[Tuple[str, str, str]]) -> List[Tuple[str, str, str]]:
""" """
@ -438,6 +487,66 @@ class ExpressionLearner:
logger.error(f"概括表达情境失败: {e}") logger.error(f"概括表达情境失败: {e}")
return None return None
async def _process_jargon_entries(self, jargon_entries: List[Tuple[str, str]], messages: List[Any]) -> None:
"""
处理从 expression learner 提取的黑话条目路由到 jargon_miner
Args:
jargon_entries: 黑话条目列表每个元素是 (content, source_id)
messages: 消息列表用于构建上下文
"""
if not jargon_entries or not messages:
return
# 获取 jargon_miner 实例
jargon_miner = miner_manager.get_miner(self.chat_id)
# 构建黑话条目格式,与 jargon_miner.run_once 中的格式一致
entries: List[Dict[str, List[str]]] = []
for content, source_id in jargon_entries:
content = content.strip()
if not content:
continue
# 检查是否包含机器人名称
if contains_bot_self_name(content):
logger.info(f"跳过包含机器人昵称/别名的黑话: {content}")
continue
# 解析 source_id
source_id_str = (source_id or "").strip()
if not source_id_str.isdigit():
logger.warning(f"黑话条目 source_id 无效: content={content}, source_id={source_id_str}")
continue
# build_anonymous_messages 的编号从 1 开始
line_index = int(source_id_str) - 1
if line_index < 0 or line_index >= len(messages):
logger.warning(f"黑话条目 source_id 超出范围: content={content}, source_id={source_id_str}")
continue
# 检查是否是机器人自己的消息
target_msg = messages[line_index]
if is_bot_message(target_msg):
logger.info(f"跳过引用机器人自身消息的黑话: content={content}, source_id={source_id_str}")
continue
# 构建上下文段落
context_paragraph = build_context_paragraph(messages, line_index)
if not context_paragraph:
logger.warning(f"黑话条目上下文为空: content={content}, source_id={source_id_str}")
continue
entries.append({"content": content, "raw_content": [context_paragraph]})
if not entries:
return
# 调用 jargon_miner 处理这些条目
await jargon_miner.process_extracted_entries(entries)
init_prompt() init_prompt()

View File

@ -262,7 +262,8 @@ class ExpressionSelector:
# 4. 调用LLM # 4. 调用LLM
content, (reasoning_content, model_name, _) = await self.llm_model.generate_response_async(prompt=prompt) content, (reasoning_content, model_name, _) = await self.llm_model.generate_response_async(prompt=prompt)
# print(prompt) print(prompt)
print(content)
if not content: if not content:
logger.warning("LLM返回空结果") logger.warning("LLM返回空结果")

View File

@ -723,6 +723,145 @@ class JargonMiner:
logger.error(f"JargonMiner 运行失败: {e}") logger.error(f"JargonMiner 运行失败: {e}")
# 即使失败也保持时间戳更新,避免频繁重试 # 即使失败也保持时间戳更新,避免频繁重试
async def process_extracted_entries(self, entries: List[Dict[str, List[str]]]) -> None:
"""
处理已提取的黑话条目 expression_learner 路由过来的
Args:
entries: 黑话条目列表每个元素格式为 {"content": "...", "raw_content": [...]}
"""
if not entries:
return
try:
# 去重并合并raw_content按 content 聚合)
merged_entries: OrderedDict[str, Dict[str, List[str]]] = OrderedDict()
for entry in entries:
content_key = entry["content"]
raw_list = entry.get("raw_content", []) or []
if content_key in merged_entries:
merged_entries[content_key]["raw_content"].extend(raw_list)
else:
merged_entries[content_key] = {
"content": content_key,
"raw_content": list(raw_list),
}
uniq_entries = []
for merged_entry in merged_entries.values():
raw_content_list = merged_entry["raw_content"]
if raw_content_list:
merged_entry["raw_content"] = list(dict.fromkeys(raw_content_list))
uniq_entries.append(merged_entry)
saved = 0
updated = 0
for entry in uniq_entries:
content = entry["content"]
raw_content_list = entry["raw_content"] # 已经是列表
try:
# 查询所有content匹配的记录
query = Jargon.select().where(Jargon.content == content)
# 查找匹配的记录
matched_obj = None
for obj in query:
if global_config.expression.all_global_jargon:
# 开启all_global所有content匹配的记录都可以
matched_obj = obj
break
else:
# 关闭all_global需要检查chat_id列表是否包含目标chat_id
chat_id_list = parse_chat_id_list(obj.chat_id)
if chat_id_list_contains(chat_id_list, self.chat_id):
matched_obj = obj
break
if matched_obj:
obj = matched_obj
try:
obj.count = (obj.count or 0) + 1
except Exception:
obj.count = 1
# 合并raw_content列表读取现有列表追加新值去重
existing_raw_content = []
if obj.raw_content:
try:
existing_raw_content = (
json.loads(obj.raw_content) if isinstance(obj.raw_content, str) else obj.raw_content
)
if not isinstance(existing_raw_content, list):
existing_raw_content = [existing_raw_content] if existing_raw_content else []
except (json.JSONDecodeError, TypeError):
existing_raw_content = [obj.raw_content] if obj.raw_content else []
# 合并并去重
merged_list = list(dict.fromkeys(existing_raw_content + raw_content_list))
obj.raw_content = json.dumps(merged_list, ensure_ascii=False)
# 更新chat_id列表增加当前chat_id的计数
chat_id_list = parse_chat_id_list(obj.chat_id)
updated_chat_id_list = update_chat_id_list(chat_id_list, self.chat_id, increment=1)
obj.chat_id = json.dumps(updated_chat_id_list, ensure_ascii=False)
# 开启all_global时确保记录标记为is_global=True
if global_config.expression.all_global_jargon:
obj.is_global = True
# 关闭all_global时保持原有is_global不变不修改
obj.save()
# 检查是否需要推断(达到阈值且超过上次判定值)
if _should_infer_meaning(obj):
# 异步触发推断,不阻塞主流程
# 重新加载对象以确保数据最新
jargon_id = obj.id
asyncio.create_task(self._infer_meaning_by_id(jargon_id))
updated += 1
else:
# 没找到匹配记录,创建新记录
if global_config.expression.all_global_jargon:
# 开启all_global新记录默认为is_global=True
is_global_new = True
else:
# 关闭all_global新记录is_global=False
is_global_new = False
# 使用新格式创建chat_id列表[[chat_id, count]]
chat_id_list = [[self.chat_id, 1]]
chat_id_json = json.dumps(chat_id_list, ensure_ascii=False)
Jargon.create(
content=content,
raw_content=json.dumps(raw_content_list, ensure_ascii=False),
chat_id=chat_id_json,
is_global=is_global_new,
count=1,
)
saved += 1
except Exception as e:
logger.error(f"保存jargon失败: chat_id={self.chat_id}, content={content}, err={e}")
continue
finally:
self._add_to_cache(content)
# 固定输出提取的jargon结果格式化为可读形式只要有提取结果就输出
if uniq_entries:
# 收集所有提取的jargon内容
jargon_list = [entry["content"] for entry in uniq_entries]
jargon_str = ",".join(jargon_list)
# 输出格式化的结果使用logger.info会自动应用jargon模块的颜色
logger.info(f"[{self.stream_name}]疑似黑话: {jargon_str}")
if saved or updated:
logger.info(f"jargon写入: 新增 {saved} 条,更新 {updated}chat_id={self.chat_id}")
except Exception as e:
logger.error(f"处理已提取的黑话条目失败: {e}")
class JargonMinerManager: class JargonMinerManager:
def __init__(self) -> None: def __init__(self) -> None:

View File

@ -126,10 +126,10 @@ class MessageRecorder:
) )
# 触发 jargon 提取(如果启用),传递消息 # 触发 jargon 提取(如果启用),传递消息
if self.enable_jargon_learning: # if self.enable_jargon_learning:
asyncio.create_task( # asyncio.create_task(
self._trigger_jargon_extraction(extraction_start_time, extraction_end_time, messages) # self._trigger_jargon_extraction(extraction_start_time, extraction_end_time, messages)
) # )
except Exception as e: except Exception as e:
logger.error(f"为聊天流 {self.chat_name} 提取和分发消息失败: {e}") logger.error(f"为聊天流 {self.chat_name} 提取和分发消息失败: {e}")

View File

@ -21,8 +21,7 @@ def init_replyer_prompt():
{chat_prompt}你正在群里聊天,现在请你读读之前的聊天记录然后给出日常且口语化的回复平淡一些 {chat_prompt}你正在群里聊天,现在请你读读之前的聊天记录然后给出日常且口语化的回复平淡一些
尽量简短一些{keywords_reaction_prompt}请注意把握聊天内容不要回复的太有条理 尽量简短一些{keywords_reaction_prompt}请注意把握聊天内容不要回复的太有条理
{reply_style} {reply_style}
请注意不要输出多余内容(包括前后缀冒号和引号括号表情等)只输出一句回复内容就好 请注意不要输出多余内容(包括不必要的前后缀冒号括号表情包at或 @等 )只输出发言内容就好
不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )
现在你说""", 现在你说""",
"replyer_prompt", "replyer_prompt",
) )