本地模型生成摘要时的踩坑点

前段时间在做一个项目,需要把用户的连续对话压缩成结构化记忆。直接调大模型API做摘要成本太高,就想着找个本地模型来跑。最后选了PaddleNLP的IDEA-CCNL/Randeng-Pegasus-523M-Summary-Chinese,用下来的确省了不少钱,但也踩了几个坑,记录一下。

为什么选这个模型

选型的时候主要看了几个点:中文支持要好、模型不能太大(机器资源有限)、推理速度要能接受、最好是开箱即用不用自己训练。

Randeng-Pegasus-523M-Summary-Chinese是IDEA研究院CCNL团队发布的,基于Pegasus架构,在中文摘要任务上做过专门训练。模型参数量5.23亿,对本地部署来说不算大,CPU也能跑。PaddleNLP的Taskflow封装得也比较友好,几行代码就能调用:

python 复制代码
from paddlenlp import Taskflow
summarizer = Taskflow("text_summarization", model="IDEA-CCNL/Randeng-Pegasus-523M-Summary-Chinese")
summarizer("你的文本")

PaddleNLP官方文档里明确支持文本摘要任务,覆盖自然语言理解与生成两大场景。综合来看,这个方案在当时是最合适的。

第一个坑:文本太长怎么办

对话积累多了以后,文本很容易超过模型的输入长度限制。

Pegasus这类Transformer架构的模型都有最大输入长度。Randeng-Pegasus-523M-Summary-Chinese的最大输入长度是1024个token 。中文里一个汉字大概对应1到2个token,所以实际能处理的汉字大约在500到1000字之间(取决于文本内容)。

对话动不动就几千字,肯定要分块处理。

我当时第一反应是滑动窗口切分------按固定字符数把长文本切成几段,每段单独生成摘要,最后合并。逻辑上没问题,但我实现的时候偷了个懒,直接在字符串上按字符位置硬截断。

结果出来的摘要让我懵了:

复制代码
???????????????????

全是问号。

排查了一下,问题出在切分方式上。硬截断把一句话从中间劈开了,模型收到的是语义不完整的片段。比如原始对话是这样的:

复制代码
用户:今天天气真好,我们出去走走吧。
AI:好啊,你想去哪里?
用户:去公园怎么样?

硬截断可能变成:

复制代码
用户:今天天气真好,我们出去走走吧。
AI:好啊,你想去

"你想去"三个字后面没了,模型根本不知道用户想去哪。语义断裂导致模型无法生成有效摘要,干脆返回了一堆问号。

解决方案:按消息边界切分

改了一下切分逻辑------不按字符数硬切,而是按消息的换行符边界切分

python 复制代码
async def _split_windows(self, text: str) -> List[str]:
    windows = []
    step = max(self.window_size - self.window_overlap, 1)
    start = 0
    while start < len(text):
        end = start + self.window_size
        if end < len(text):
            # 往回找最近的换行符,保证切分点在消息边界上
            newline_pos = text.rfind("\n", start, end)
            if newline_pos > start:
                end = newline_pos + 1
        windows.append(text[start:end])
        start += step
    return windows

每个窗口的结束位置都落在\n上,保证每条消息都是完整的。这样模型收到的每个片段都有完整的语义,摘要质量马上就正常了。

窗口大小我设的是512个字符,重叠100个字符。512字符大约对应300-400个token,远低于模型上限,比较安全。重叠部分是为了避免关键信息正好落在切分边界上被丢掉。

第二个坑:短文本直接输出原文

测试的时候还发现一个现象------输入文本少于100字左右的时候,模型经常直接返回原文,而不是生成摘要。

一开始以为是模型出了问题,后来看了一下,这其实是Pegasus的设计特点。对于已经足够短的文本,模型判断"不需要再压缩了",就直接返回输入。这个行为在摘要任务里其实挺合理的------原文本身就很精炼了,强行压缩反而可能丢失信息。

所以在代码里加了个保护逻辑:如果摘要生成失败或返回空,就返回原文,保证下游模块不会收到空内容。

python 复制代码
async def summarize(self, messages: str) -> str:
    if not messages or len(messages.strip()) < 10:
        return messages.strip() if messages else ""
    try:
        summary = await asyncio.to_thread(self._model, messages)
        if summary and len(summary) > 0:
            return summary[0].strip()
        return messages.strip()
    except Exception as e:
        logger.error(f"生成摘要失败:{e}")
        return messages.strip()

整体架构

最后的设计分两层:

**上层(MemoryConsolidator)**负责管理会话缓冲区、判断什么时候触发整合、调用格式化、窗口切分、摘要生成、关键词提取和存储。这一层关心文本有多长、怎么切、要不要合并。

**下层(PaddleNLPSummarizer)**只负责一件事------把传入的文本丢给模型,返回摘要或原文。不判断长度、不主动裁剪,只做纯粹的模型调用。

这样分层的好处是职责清晰。以后想换摘要模型(比如换成更大的Pegasus或者别的架构),只需要改下层;想调整切分策略,只需要改上层,互不影响。

一些数据

实际跑下来的效果:

  • 单次摘要推理耗时大约1-3秒(CPU环境,具体取决于文本长度)
  • 5.23亿参数的模型,CPU内存占用大概2-3GB
  • 100轮对话(约5000-8000字)压缩成一段150字左右的摘要,信息保留率目测在80%以上

成本方面,本地部署的好处是一次性投入,不像API按次收费。对于高频摘要场景,长期来看省了不少。

总结

本地小模型做摘要,效果上肯定比不过GPT这类大模型,但胜在成本可控、数据不出域。关键是要在工程上多做几层保护------语义完整的切分、异常兜底、合理的窗口设计。模型本身的能力是一回事,怎么用好它又是另一回事。

资料参考:

  • PaddleNLP Taskflow官方文档:文档地址
  • PaddleNLP模型库支持PEGASUS等社区模型:
  • IDEA-CCNL/Randeng-Pegasus-523M-Summary-Chinese模型权重:可在ModelScope或HuggingFace搜索获取