基于LLM的自动化报告生成——舆情分析报告

目录

0x00 项目架构

在线文档地址

  • 数据检索引擎

    给定查询词及其它查询参数,查询出最相关的数据(新闻)。

  • 报告生成引擎

    根据给定的报告提纲(含报告主题、各章节标题、也可包含用户对各章节的写作偏好)及其它参数(如偏好报告风格),调用写作器、润色器进行报告撰写

  • 大模型服务调度引擎

    对第三方大模型服务提供商的模型进行统一调度。

  • ……

0x01 核心数据结构

报告提纲树

提纲实际上是一棵树,根节点为以报告名称命名的节点,其下面的子节点为子章节,每个子章节又分别是独立的子树。

  • 报告章节节点

    class ArticleSectionNode:
        """
        ArticleSectionNode 用于处理文章的章节,包括存储章节正文、定义章节写作偏好.
        """
    
        def __init__(self, section_name: str, content=None):
            """
            section_name: 字符串形式的章节名称, e.g. 报告观点, 近期动态, etc.
            content: 章节正文内容. 可以是字符串或其它数据结构,取决于你的选择.
            """
            self.section_name = section_name
            self.content = content
            self.children = []
            self.preference = None
    
        def add_child(self, new_child_node, insert_to_front=False):
            if insert_to_front:
                self.children.insert(0, new_child_node)
            else:
                self.children.append(new_child_node)
    
        def remove_child(self, child):
            self.children.remove(child)
    
    class IndustryAnalysisArticleSection(ArticleSectionNode):
        def __init__(self,  section_name: str, content=None, description=None, parent_name=None, filter_words=None, query_words=None):
            super().__init__(section_name, content)
            if query_words is None:
                self.query_words = []
            else:
                self.query_words = query_words
            self.description = description
            self.parent_name = parent_name
            if filter_words is None:
                filter_words = []
            self.filter_words = filter_words
    
  • 报告提纲树

    class Article(ABC):
        def __init__(self, topic_name):
            self.root = ArticleSectionNode(topic_name)
    
    class IndustryAnalysisArticle(Article):
       def __init__(self, topic_name: str, description: str):
            super().__init__(topic_name=topic_name)
            self.description = description
            self.summary = ""
       def __repr__(self):
            return f"{__class__}({self.root.section_name}, {id(self)})"
    
       @timer
       def to_string(self) -> str:
            """
            将文章以字符串形式输出。
            """
    

信息表

  • 舆情信息

用于维护每条新闻的基本信息(title、abstract等)和便于检索的信息(特征向量、标签)

  • 舆情信息表

用于维护一篇报告所依赖的基础数据集,实现数据检索方法(离线、也可以有在线搜索)

class NewsInformation(Information):
  def __init__(self, url, description,  title, meta=None,abstract=None,tags: Optional[List[Tag]]=None):
      super().__init__(url, description, title, meta)
      self.abstract = abstract
      self.tags = tags if tags is not None else []
      self.encoded_title = None
      self.encoded_abstract = None

  def add_tag(self, name, tag_type=None):
      self.tags.append(Tag(name, tag_type))

  @property
  def content(self):
      return self.description

  @property
  def tag_set(self):
      return set(tag.name for tag in self.tags)

  def get_tags(self, type=None):
      if type is None:
          return set(tag.name for tag in self.tags)
      return set(tag.name for tag in self.tags if tag.type == type)

  def contains_keyword(self, keyword_list, title_only=False):
      if title_only:
          text = self.title
      else:
          text = f"{self.title}{self.content}"
      match_result = TextMatchUtil.contains_keywords(text,
                                                     keyword_list=keyword_list)
      if match_result.count > 0:
          self.tags = [Tag(word) for word in match_result.matched_keywords]
          return True
      return False

class NewsInformationTable(InformationTable):
  def __init__(self, news_collection: List[NewsInformation], filter_words=None, encoder=None):
      super().__init__()
      if filter_words is None:
          filter_words = []
      self.news_collection = news_collection
      self.title_to_info: Dict[str, NewsInformation] = NewsInformationTable.construct_title_to_info(self.news_collection)
      self.filter_words = filter_words
      self.encoder = encoder

  def __len__(self):
      return len(self.title_to_info)



  def retrieve_information_plus(self,  queries: Union[List[str], str], search_top_k: int=20,topic=None,
                                similarity_threshold: float=0.4, filter_words: List[str]=None,
                                remove_duplicated=False) -> List[NewsInformation]:
      pass

0x02 核心流程

0x03 效果示例及问题

检索效果

几种检索策略对比

  • 基础策略:向量语义检索

  • 向量检索结合标题关键词过滤

  • 向量检索结合标题+正文关键词过滤

报告结果效果

  • 基于事实(受限于参考数据源的权威性)写作,观点附带引用
  • 具有两种报告风格:全文型(篇幅较长)、事件提要型(较为简洁、事件列举)
  • 并行生成报告,效率高

全文类型报告示例

事件提要类型文章

问题

生成模型的问题

Q:大模型执行违背指令

如通义千问qwen-plus模型,如果prompt中显式提示不要输出markdown格式,模型反而会输出大量markdown格式文本。

- **格式要求:** 使用纯文本格式,不得包含任何Markdown或其他格式符号。

如果不额外提示不输出markdown,模型反而在大部份情况下不会输出markdown。输出示例如下图所示:

类似地,如果prompt中提示模型不要通过序号分点做法,模型反而会输出大量序号。示例如下:

直接提示模型分段输出,模型反而不输出序号:

说明qwen-plus系列模型(也包括qwen-max-0919)目前对于输入指令中“逻辑非”的理解也处理不到位。

可以通过规则的方式缓解这一问题,比如通过如下正则表达式:

    @classmethod
    def clean_markdown(cls, content):
        content = re.sub(r'^#{1,6}\s*[\u4e00-\u9fa5、\w]*', '', content, flags=re.MULTILINE)
        return content

检索模型的问题

Q:部分数据检索相关性较低

采用BAAI/bge-reranker-v2-m3,效果较差

采用bge-large和bge-m3效果较好,但难以通过阈值排除一些不太相关的信息。

bge-large-zh, 查询“政策法规”

bge-m3, 查询“政策法规”

bge-large-zh, 查询“DLC”

输入数据质量问题

Q:数据源信息量少

  • 部分新闻完全是图片,对于文本模型而言这类输入没有什么信息量。

  • 部分网络新闻在获取时包含较多噪音

Q:输入信息不全面

  • 输入给LLM的数据主要是新闻摘要,如果新闻摘要本身反映原文的信息较少,会影响报告效果(用户兴趣信息召回率)

    实践中,有一些新闻正文本身就是信息罗列型的,可能每一段都是不应被忽略的信息,大部份重点关注头部和尾部的摘要算法处理此类文本效果不理想。示例数据和结果如下:

A: 若模型可处理上下文长度足够长,可考虑输入正文或更长的摘要以提取更详细的信息

基于新闻正文生成的报告:

0x04 外部资源

特征抽取模型

CoolCats
CoolCats
理学学士

我的研究兴趣是时空数据分析、知识图谱、自然语言处理与服务端开发