去除本地回复硬裁剪并改为分段多次发送
This commit is contained in:
@@ -8,39 +8,44 @@ def finalize_reply(response: str, reply_mode: str, limits: Dict | None = None) -
|
||||
text = str(response or "").strip()
|
||||
if not text:
|
||||
return []
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
text = text.replace("\n", " ").strip()
|
||||
# 这里不再把整段回复压成一行,也不再做“总字数硬截断”:
|
||||
# 1. 用户明确要求去掉本地裁剪,长文宁可拆开发多条,也不要直接砍掉后半段;
|
||||
# 2. 因此我们只做轻量清洗,保留原本的段落边界,后续按段落/句子拆分发送;
|
||||
# 3. 模型侧仍然会被提示尽量简短,本地这里只负责“安全分段”,不负责“删内容”。
|
||||
text = _normalize_reply_text(text)
|
||||
|
||||
options = _resolve_limits(reply_mode, limits or {})
|
||||
if reply_mode == "social_short":
|
||||
chunks = split_reply_chunks(
|
||||
return split_reply_chunks(
|
||||
text,
|
||||
sentence_limit=1,
|
||||
char_limit=options["char_limit"],
|
||||
chunk_limit=1,
|
||||
allow_clip_split=False,
|
||||
chunk_limit=0,
|
||||
allow_clip_split=True,
|
||||
)
|
||||
return _clip_total_chars(chunks, options["total_limit"])
|
||||
if reply_mode == "qa_fast":
|
||||
chunks = split_reply_chunks(
|
||||
return split_reply_chunks(
|
||||
text,
|
||||
sentence_limit=1,
|
||||
char_limit=options["char_limit"],
|
||||
chunk_limit=1,
|
||||
allow_clip_split=False,
|
||||
chunk_limit=0,
|
||||
allow_clip_split=True,
|
||||
)
|
||||
return _clip_total_chars(chunks, options["total_limit"])
|
||||
if reply_mode == "qa_with_context":
|
||||
chunks = split_reply_chunks(
|
||||
return split_reply_chunks(
|
||||
text,
|
||||
sentence_limit=options["sentence_limit"],
|
||||
char_limit=options["char_limit"],
|
||||
chunk_limit=options["chunk_limit"],
|
||||
allow_clip_split=False,
|
||||
chunk_limit=0,
|
||||
allow_clip_split=True,
|
||||
)
|
||||
return _clip_total_chars(chunks, options["total_limit"])
|
||||
chunks = [take_first_sentence(text, options["default_char_limit"]).strip()]
|
||||
return _clip_total_chars(chunks, options["total_limit"])
|
||||
return split_reply_chunks(
|
||||
text,
|
||||
sentence_limit=1,
|
||||
char_limit=options["default_char_limit"],
|
||||
chunk_limit=0,
|
||||
allow_clip_split=True,
|
||||
)
|
||||
|
||||
|
||||
def preview_text(text: str, limit: int = 80) -> str:
|
||||
@@ -63,12 +68,16 @@ def build_length_rule(reply_mode: str) -> str:
|
||||
return "尽量短,像群友临时接一句,不要长篇大论。"
|
||||
|
||||
|
||||
def take_first_sentence(text: str, limit: int) -> str:
|
||||
parts = re.split(r"(?<=[。!?!?;;])", text)
|
||||
first = parts[0].strip() if parts and parts[0].strip() else text.strip()
|
||||
if len(first) <= limit:
|
||||
return first
|
||||
return smart_clip(first, limit)
|
||||
def _normalize_reply_text(text: str) -> str:
|
||||
# 这里按“保内容、保段落”的思路做清洗:
|
||||
# 1. 每一行内部压缩多余空白,避免模型输出奇怪缩进;
|
||||
# 2. 行与行之间保留换行,这样后面可以按段落拆成多条消息发送;
|
||||
# 3. 不把整段文本合并成一行,避免后续丢失原有表达层次。
|
||||
lines = [re.sub(r"\s+", " ", line).strip() for line in str(text or "").splitlines()]
|
||||
cleaned = [line for line in lines if line]
|
||||
if not cleaned:
|
||||
return re.sub(r"\s+", " ", str(text or "")).strip()
|
||||
return "\n".join(cleaned)
|
||||
|
||||
|
||||
def split_reply_chunks(
|
||||
@@ -78,48 +87,97 @@ def split_reply_chunks(
|
||||
chunk_limit: int,
|
||||
allow_clip_split: bool = True,
|
||||
) -> List[str]:
|
||||
parts = [item.strip() for item in re.split(r"(?<=[。!?!?;;])", text) if item.strip()]
|
||||
if not parts:
|
||||
short = text.strip()
|
||||
clipped = smart_clip(short, char_limit)
|
||||
if not short:
|
||||
return []
|
||||
if not allow_clip_split:
|
||||
return [clipped] if clipped else []
|
||||
remainder = short[len(clipped):].strip(",,、;;:: ")
|
||||
return [item for item in [clipped, smart_clip(remainder, char_limit)] if item][:chunk_limit]
|
||||
normalized_sentence_limit = max(int(sentence_limit or 1), 1)
|
||||
normalized_char_limit = max(int(char_limit or 0), 8)
|
||||
# chunk_limit 历史上用于“最多保留几段”,但这会直接截掉后文。
|
||||
# 现在为了满足“长文可多次发送、不要硬截取”,这里不再把它当成截断开关。
|
||||
_ = chunk_limit
|
||||
_ = allow_clip_split
|
||||
|
||||
paragraphs = [item.strip() for item in str(text or "").splitlines() if item.strip()]
|
||||
if not paragraphs:
|
||||
return []
|
||||
|
||||
chunks: List[str] = []
|
||||
for part in parts[:sentence_limit]:
|
||||
current = part.strip()
|
||||
while current and len(chunks) < chunk_limit:
|
||||
if len(current) <= char_limit:
|
||||
chunks.append(current.strip())
|
||||
break
|
||||
clipped = smart_clip(current, char_limit)
|
||||
if not clipped:
|
||||
clipped = current[:char_limit].rstrip(",,、;;:: ").strip()
|
||||
if clipped:
|
||||
chunks.append(clipped)
|
||||
if not allow_clip_split:
|
||||
break
|
||||
current = current[len(clipped):].strip(",,、;;:: ")
|
||||
return chunks[:chunk_limit] or [smart_clip(text, char_limit)]
|
||||
for paragraph in paragraphs:
|
||||
chunks.extend(
|
||||
_split_paragraph_for_delivery(
|
||||
paragraph,
|
||||
sentence_limit=normalized_sentence_limit,
|
||||
char_limit=normalized_char_limit,
|
||||
)
|
||||
)
|
||||
return [chunk for chunk in chunks if chunk.strip()]
|
||||
|
||||
|
||||
def smart_clip(text: str, limit: int) -> str:
|
||||
def smart_take_prefix(text: str, limit: int) -> str:
|
||||
# 这里不做“裁掉尾巴”的 smart clip,而是返回一个“可以安全先发出去的前缀”:
|
||||
# 1. 如果窗口内有标点,优先在标点后断开,尽量保留语气完整;
|
||||
# 2. 如果没有合适标点,就按长度切一段,但后续剩余内容还会继续发送;
|
||||
# 3. 这样只是分段,不是删减。
|
||||
text = str(text or "").strip()
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
window = text[:limit]
|
||||
strong_punctuation = "。!?!?))】]」』 "
|
||||
strong_punctuation = "。!?!?))】]」』"
|
||||
weak_punctuation = ",,、;;::"
|
||||
split_at = _find_split_at(window, strong_punctuation)
|
||||
if split_at < 0:
|
||||
split_at = _find_split_at(window, weak_punctuation, lookback=12)
|
||||
if split_at >= 0:
|
||||
return window[:split_at].rstrip(",,、;;::。!?!? ").strip()
|
||||
return window.rstrip(",,、;;:: ").strip()
|
||||
return window[: split_at + 1].strip()
|
||||
return window.strip()
|
||||
|
||||
|
||||
def _split_paragraph_for_delivery(paragraph: str, sentence_limit: int, char_limit: int) -> List[str]:
|
||||
# 这里按“先按句聚合,句子过长再继续切”的顺序拆分:
|
||||
# 1. 优先把短句拼成一条,减少无意义的多条发送;
|
||||
# 2. 一旦单句本身超过阈值,再按更细的片段切开;
|
||||
# 3. 整个过程中不丢任何内容,所有字符最终都会进入某一条消息。
|
||||
parts = [item.strip() for item in re.split(r"(?<=[。!?!?;;])", paragraph) if item.strip()]
|
||||
if not parts:
|
||||
return _split_long_text_preserving_all(paragraph, char_limit)
|
||||
|
||||
chunks: List[str] = []
|
||||
current_parts: List[str] = []
|
||||
for part in parts:
|
||||
if len(part) > char_limit:
|
||||
if current_parts:
|
||||
chunks.append("".join(current_parts).strip())
|
||||
current_parts = []
|
||||
chunks.extend(_split_long_text_preserving_all(part, char_limit))
|
||||
continue
|
||||
|
||||
candidate_parts = current_parts + [part]
|
||||
candidate_text = "".join(candidate_parts).strip()
|
||||
if current_parts and (
|
||||
len(candidate_parts) > sentence_limit or len(candidate_text) > char_limit
|
||||
):
|
||||
chunks.append("".join(current_parts).strip())
|
||||
current_parts = [part]
|
||||
continue
|
||||
current_parts = candidate_parts
|
||||
|
||||
if current_parts:
|
||||
chunks.append("".join(current_parts).strip())
|
||||
return [chunk for chunk in chunks if chunk]
|
||||
|
||||
|
||||
def _split_long_text_preserving_all(text: str, char_limit: int) -> List[str]:
|
||||
remaining = str(text or "").strip()
|
||||
if not remaining:
|
||||
return []
|
||||
|
||||
chunks: List[str] = []
|
||||
while remaining:
|
||||
prefix = smart_take_prefix(remaining, char_limit)
|
||||
if not prefix:
|
||||
prefix = remaining[:char_limit].strip()
|
||||
if not prefix:
|
||||
break
|
||||
chunks.append(prefix)
|
||||
remaining = remaining[len(prefix):].lstrip()
|
||||
return chunks
|
||||
|
||||
|
||||
def _find_split_at(window: str, punctuation: str, lookback: int = 10) -> int:
|
||||
@@ -131,10 +189,10 @@ def _find_split_at(window: str, punctuation: str, lookback: int = 10) -> int:
|
||||
|
||||
def _resolve_limits(reply_mode: str, limits: Dict) -> Dict[str, int]:
|
||||
mode_defaults = {
|
||||
# 这里的默认值是“本地最终裁剪”的最后一道保险:
|
||||
# 1. 下限不做要求,模型可以回很短;
|
||||
# 2. 这里只管兜底上限,避免偶发输出过长;
|
||||
# 3. 所有模式统一往 30 字附近收,保持群聊接话感。
|
||||
# 这里的默认值不再承担“截断总长度”的职责,而是只作为“单条消息建议拆分长度”:
|
||||
# 1. 30 字以内通常比较像群里一条自然接话;
|
||||
# 2. 真超过了就继续拆成下一条,而不是把后半段吞掉;
|
||||
# 3. qa_with_context 允许单条略紧一点,方便多句分开发送。
|
||||
"social_short": {"sentence_limit": 1, "char_limit": 30, "chunk_limit": 1, "total_limit": 30},
|
||||
"qa_fast": {"sentence_limit": 1, "char_limit": 30, "chunk_limit": 1, "total_limit": 30},
|
||||
"qa_with_context": {"sentence_limit": 2, "char_limit": 18, "chunk_limit": 2, "total_limit": 30},
|
||||
@@ -171,27 +229,3 @@ def _resolve_limits(reply_mode: str, limits: Dict) -> Dict[str, int]:
|
||||
"total_limit": max(int(limits.get("default_total_limit", defaults["total_limit"]) or defaults["total_limit"]), 8),
|
||||
"default_char_limit": max(int(limits.get("default_char_limit", 30) or 30), 8),
|
||||
}
|
||||
|
||||
|
||||
def _clip_total_chars(chunks: List[str], total_limit: int) -> List[str]:
|
||||
if not chunks:
|
||||
return []
|
||||
normalized_limit = max(int(total_limit or 0), 8)
|
||||
result: List[str] = []
|
||||
used = 0
|
||||
for chunk in chunks:
|
||||
current = str(chunk or "").strip()
|
||||
if not current:
|
||||
continue
|
||||
remain = normalized_limit - used
|
||||
if remain <= 0:
|
||||
break
|
||||
if len(current) <= remain:
|
||||
result.append(current)
|
||||
used += len(current)
|
||||
continue
|
||||
clipped = smart_clip(current, remain)
|
||||
if clipped:
|
||||
result.append(clipped)
|
||||
break
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user