企业年报 AI 解读:让模型看财报,从表格到洞察的工程实践

AI2周前发布 beixibaobao
8 0 0

企业年报 AI 解读:让模型看财报,从表格到洞察的工程实践

一、个性化深度引言

"让大模型看财报"听起来不复杂——把 PDF 丢进去,"告诉我这家公司财务健康吗?"但这行不通。

第一个问题在表格理解上。一份标准年报有30+张表格,其中有合并报表、母公数据、分部业务数据。大模型擅长看文本,但看表格容易把合并口径的数据和母公司口径的数据混为一谈——而这两个口径差了一个量级。

第二个问题在数字串联上。营业收入的增长需要和应收账款、经营现金流、存货周转天数串联起来看。单看一个指标无法形成判断,需要多表联动分析。

第三个问题在幻觉上。让模型直接生成"这家公司财务状况良好"——证据在哪?如果模型编造成长率,财务报表分析就失去了可信度。

这篇文章复盘了一套"让模型看报表而不产生幻觉"的工程实践——核心策略是强制模型使用提取的表格数据而非训练数据,实现"引用即证据"。

二、个性化原理剖析

财报 AI 解读的核心架构:

核心设计思路是"计算归计算,推理归推理"。指标计算(同比增长率、毛利率、资产负债率)必须由代码精确计算,不允许 LLM 介入——因为 LLM 会对数字做近似处理。LLM 只在最后一步负责"从指标到洞察"的语义分析,且必须引用数据来源。

三、个性化代码实践

财报提取和分析的核心实现:

import re
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
from datetime import datetime
import numpy as np
class ReportPeriod(Enum):
    """报告期间——设计原因:统一口径防止年度/季度数据混淆"""
    ANNUAL = "annual"
    Q1 = "q1"
    H1 = "h1"
    Q3 = "q3"
@dataclass
class FinancialMetric:
    """财务指标——设计原因:统一数据结构,所有指标同一格式"""
    name: str
    value: float  # 指标值(需统一单位,如元)
    period: ReportPeriod
    year: int
    caliber: str  # 合并/母公司
    source_table: str  # 来源于哪个表
    source_cell: str   # 来源于哪个单元格
    def format_display(self) -> str:
        """格式化显示——设计原因:展示用,原始值保留精度"""
        if abs(self.value) >= 1e8:
            return f"{self.value/1e8:.2f}亿"
        elif abs(self.value) >= 1e4:
            return f"{self.value/1e4:.2f}万"
        else:
            return f"{self.value:.2f}"
@dataclass  
class FinancialAnalysis:
    """财务分析报告——设计原因:结构化输出,每条洞察有来源"""
    company_name: str
    report_period: str
    metrics: List[FinancialMetric]
    insights: List[Dict]  # 每条洞察: {text, confidence, source_metrics}
class FinancialTableExtractor:
    """财务报表表格提取器——设计原因:财报表格结构复杂,需要专门处理"""
    # 常见财务报表表头模式——设计原因:规则匹配快速且准确
    TABLE_PATTERNS = {
        "balance_sheet": [
            r"(合并|母公司)?.*资产负[债责]表",
            r"资产负债表",
        ],
        "income_statement": [
            r"(合并|母公司)?.*利润表",
            r"损益表",
        ],
        "cash_flow": [
            r"(合并|母公司)?.*现金流量表",
            r"现金流表",
        ],
    }
    # 金额单位映射——设计原因:必须统一单位,否则同比计算会差万倍
    UNIT_MAP = {
        "元": 1,
        "万元": 1e4,
        "百万元": 1e6,
        "亿元": 1e8,
    }
    def extract_table(self, pdf_page_text: str) -> List[Dict]:
        """提取表格——设计原因:识别表头后按行解析,保持行列关系"""
        tables = []
        # 识别表格类型——设计原因:不同类型有不同字段,不能混着用
        table_type = self._identify_table_type(pdf_page_text)
        # 识别金额单位——设计原因:单位不一会导致后续计算全错
        unit = self._identify_unit(pdf_page_text)
        # 识别合并/母公司口径——设计原因:这是财报分析最易错的点
        caliber = self._identify_caliber(pdf_page_text)
        if table_type and unit:
            tables.append({
                "type": table_type,
                "unit": unit,
                "caliber": caliber,
                "rows": []  # 实际需要解析表格行
            })
        return tables
    def _identify_table_type(self, text: str) -> Optional[str]:
        """识别表格类型——设计原因:关键词匹配,覆盖不同写法"""
        for table_type, patterns in self.TABLE_PATTERNS.items():
            for pattern in patterns:
                if re.search(pattern, text):
                    return table_type
        return None
    def _identify_unit(self, text: str) -> float:
        """识别金额单位——设计原因:在表头上方或下方通常会注明"""
        for unit_str, multiplier in self.UNIT_MAP.items():
            if f"单位:{unit_str}" in text or f"单位:{unit_str}" in text:
                return multiplier
        return 1.0  # 默认元
    def _identify_caliber(self, text: str) -> str:
        """识别报告口径——设计原因:合并和母公司数据差一个量级"""
        if "母公司" in text:
            return "parent"
        elif "合并" in text:
            return "consolidated"
        else:
            return "unknown"
class MetricCalculator:
    """指标计算器——设计原因:确定性计算,不依赖LLM"""
    @staticmethod
    def yoy_growth(current: float, previous: float) -> Optional[float]:
        """同比增长率——设计原因:处理除零和负数情况"""
        if previous == 0:
            return None
        return (current - previous) / abs(previous) * 100
    @staticmethod
    def gross_margin(revenue: float, cost: float) -> Optional[float]:
        """毛利率——设计原因:百分比表示,检查分母非零"""
        if revenue == 0:
            return None
        return (revenue - cost) / revenue * 100
    @staticmethod  
    def debt_ratio(total_liabilities: float, total_assets: float) -> Optional[float]:
        """资产负债率——设计原因:指标 > 100% 表示资不抵债"""
        if total_assets == 0:
            return None
        return total_liabilities / total_assets * 100
    @staticmethod
    def current_ratio(current_assets: float, 
                      current_liabilities: float) -> Optional[float]:
        """流动比率——设计原因:标准值2,低于1有短期偿债风险"""
        if current_liabilities == 0:
            return None
        return current_assets / current_liabilities
    def compute_all(self, 
                    current_metrics: Dict[str, FinancialMetric],
                    previous_metrics: Dict[str, FinancialMetric]) -> Dict[str, float]:
        """计算所有指标——设计原因:一次调用输出全部分析指标"""
        results = {}
        # 营业收入同比增长
        if "revenue" in current_metrics and "revenue" in previous_metrics:
            results["revenue_yoy"] = self.yoy_growth(
                current_metrics["revenue"].value,
                previous_metrics["revenue"].value
            )
        # 净利润同比增长
        if "net_profit" in current_metrics and "net_profit" in previous_metrics:
            results["profit_yoy"] = self.yoy_growth(
                current_metrics["net_profit"].value,
                previous_metrics["net_profit"].value
            )
        # 毛利率
        if "revenue" in current_metrics and "cost" in current_metrics:
            results["gross_margin"] = self.gross_margin(
                current_metrics["revenue"].value,
                current_metrics["cost"].value
            )
        # 资产负债率
        if "total_liabilities" in current_metrics and "total_assets" in current_metrics:
            results["debt_ratio"] = self.debt_ratio(
                current_metrics["total_liabilities"].value,
                current_metrics["total_assets"].value
            )
        # 流动比率
        if "current_assets" in current_metrics and "current_liabilities" in current_metrics:
            results["current_ratio"] = self.current_ratio(
                current_metrics["current_assets"].value,
                current_metrics["current_liabilities"].value
            )
        return results
class FinancialInsightGenerator:
    """财务洞察生成器——设计原因:LLM只负责语义分析,数字全部由计算引擎提供"""
    def __init__(self, llm_client=None):
        self.llm = llm_client
        self.calculator = MetricCalculator()
    def generate_analysis(self, 
                          company_name: str,
                          current: Dict[str, FinancialMetric],
                          previous: Dict[str, FinancialMetric]) -> FinancialAnalysis:
        """生成财务分析报告——设计原因:计算+推理的完整管线"""
        # 第一步:精确计算所有指标——设计原因:禁止LLM参与计算
        metrics = self.calculator.compute_all(current, previous)
        # 第二步:构建事实上下文——设计原因:作为LLM推理的唯一信息源
        context = self._build_context(company_name, current, previous, metrics)
        # 第三步:LLM推理生成洞察——设计原因:LLM做语义分析,不做数值计算
        insights = self._generate_insights(context, current)
        # 第四步:交叉校验——设计原因:检查LLM是否引用了计算数据
        verified_insights = self._verify_insights(insights, metrics)
        return FinancialAnalysis(
            company_name=company_name,
            report_period="2024年度",
            metrics=list(current.values()),
            insights=verified_insights
        )
    def _build_context(self, name: str,
                       current: Dict, previous: Dict, 
                       computed: Dict) -> str:
        """构建分析上下文——设计原因:结构化数据转自然语言传LLM"""
        parts = [f"公司:{name}"]
        # 收入相关
        if "revenue" in current:
            cur_rev = current["revenue"]
            parts.append(
                f"营业收入:{cur_rev.format_display()}元 "
                f"({cur_rev.caliber}口径)"
            )
            if "revenue_yoy" in computed and computed["revenue_yoy"] is not None:
                parts.append(
                    f"营业收入同比增长:{computed['revenue_yoy']:.2f}%"
                )
        # 利润相关
        if "net_profit" in current:
            parts.append(
                f"净利润:{current['net_profit'].format_display()}元"
            )
            if "profit_yoy" in computed and computed["profit_yoy"] is not None:
                parts.append(
                    f"净利润同比增长:{computed['profit_yoy']:.2f}%"
                )
        # 盈利能力
        if "gross_margin" in computed and computed["gross_margin"] is not None:
            parts.append(f"毛利率:{computed['gross_margin']:.2f}%")
        # 偿债能力
        if "debt_ratio" in computed and computed["debt_ratio"] is not None:
            parts.append(f"资产负债率:{computed['debt_ratio']:.2f}%")
        if "current_ratio" in computed and computed["current_ratio"] is not None:
            parts.append(f"流动比率:{computed['current_ratio']:.2f}")
        return "n".join(parts)
    def _generate_insights(self, context: str, 
                           metrics: Dict) -> List[Dict]:
        """LLM生成洞察——设计原因:基于计算数据做推理,不是基于训练数据"""
        prompt = f"""请基于以下财务数据分析生成3-5条洞察。
你必须基于提供的数字进行分析,不得编造数据。
每条洞察不超过80字。
{context}
要求:
1. 每条洞察必须引用具体的数字
2. 不能生成未提供的数据
3. 分析应覆盖盈利能力、成长性、偿债能力
4. 如果有异常数据(如负增长、比率过高/过低),需特别指出"""
        # 实际调用LLM
        response = "基于数据分析的洞察结果"  # 占位
        # 解析LLM输出为结构化洞察——设计原因:方便校验和展示
        return [
            {
                "text": insight.strip(),
                "confidence": 0.8,
                "source_metrics": ["revenue_yoy", "gross_margin"]
            }
            for insight in response.split("n") if insight.strip()
        ]
    def _verify_insights(self, insights: List[Dict], 
                         computed: Dict) -> List[Dict]:
        """交叉校验——设计原因:检查LLM的数字是否与计算结果一致"""
        verified = []
        for insight in insights:
            text = insight["text"]
            # 提取LLM输出中的数字——设计原因:检查是否和计算值一致
            numbers_in_text = re.findall(r"[-+]?d+.?d*%?", text)
            # 检查数字是否在计算指标中出现过
            has_verified_source = True
            for num_str in numbers_in_text:
                num_str = num_str.replace("%", "")
                try:
                    num = float(num_str)
                    # 检查该数字是否出现在计算指标中(容差0.1%)
                    found = any(
                        abs(v - num) / max(abs(v), 1) < 0.001
                        for v in computed.values()
                        if v is not None
                    )
                    if not found:
                        has_verified_source = False
                        insight["confidence"] *= 0.5
                except ValueError:
                    continue
            if has_verified_source:
                insight["confidence"] = min(1.0, insight["confidence"] * 1.1)
            verified.append(insight)
        return verified
# 使用示例
def analyze_financial_report():
    """完整财报分析流程——设计原因:端到端演示数据流"""
    extractor = FinancialTableExtractor()
    generator = FinancialInsightGenerator()
    # 模拟提取的财务数据
    current_metrics = {
        "revenue": FinancialMetric(
            "营业收入", 50.5e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并利润表", "B4"
        ),
        "net_profit": FinancialMetric(
            "净利润", 5.2e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并利润表", "B25"
        ),
        "cost": FinancialMetric(
            "营业成本", 32.0e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并利润表", "B6"
        ),
        "total_assets": FinancialMetric(
            "总资产", 120.0e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并资产负债表", "C40"
        ),
        "total_liabilities": FinancialMetric(
            "总负债", 65.0e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并资产负债表", "C60"
        ),
        "current_assets": FinancialMetric(
            "流动资产", 45.0e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并资产负债表", "C15"
        ),
        "current_liabilities": FinancialMetric(
            "流动负债", 30.0e8, ReportPeriod.ANNUAL, 2024,
            "consolidated", "合并资产负债表", "C75"
        ),
    }
    previous_metrics = {
        "revenue": FinancialMetric(
            "营业收入", 42.0e8, ReportPeriod.ANNUAL, 2023,
            "consolidated", "合并利润表", "B4"
        ),
        "net_profit": FinancialMetric(
            "净利润", 4.5e8, ReportPeriod.ANNUAL, 2023,
            "consolidated", "合并利润表", "B25"
        ),
    }
    # 生成分析
    analysis = generator.generate_analysis(
        "示例科技股份有限公司",
        current_metrics,
        previous_metrics
    )
    print(f"分析报告: {analysis.company_name}")
    for i, insight in enumerate(analysis.insights, 1):
        print(f"洞察{i} (置信度{insight['confidence']:.2f}): {insight['text']}")
analyze_financial_report()

核心设计是"计算与推理分离":指标计算由确定性算法完成,LLM 只负责语义分析。计算层保证数字准确,推理层保证分析有深度。两者之间的桥梁是"数据上下文"——LLM 看到的不是原始报表,而是计算引擎产出的结构化事实。这样从根本上杜绝了幻觉。

四、个性化边界权衡

计算精度 vs 分析灵活性

严格的计算层保证了精度,但牺牲了分析灵活性——比如 LLM 想分析"经营性现金流/净利润"这个比率,但计算引擎没有预设这个指标,LLM 就无法分析。解决方案是允许 LLM 提出需要的指标类型,计算引擎动态计算——相当于给 LLM 一个"计算请求"的 API 而非固定指标清单。

单品分析 vs 多品对比

单家公司年报分析已经比较复杂,行业内多公司横向对比复杂度乘3-5倍——不同公司会计科目名称不统一("营业收入"vs"主营业务收入"、"研发费用"可能归在管理费用里)。现阶段方案是:制作行业标准科目映射表,先统一科目再对比。

模型知识 vs 数据事实

LLM 的训练数据里可能包含该公司的历史分析文章,在没有当前数据时倾向于用训练知识"补全"。事实校验层必须严格检查——生成的数字不在提取的数据里,就标记为低置信度。宁可信息少,不能信息错。

五、总结

企业年报 AI 解读的核心工程策略是"计算归计算、推理归推理"。指标计算(同比增长率、毛利率、资产负债率、流动比率)全部由确定性算法完成,LLM 只负责语义分析。LLM 的输入不是原始报表文本,而是计算引擎产出的结构化事实数据。输出须经交叉校验——LLM 生成的数字与计算结果不一致时自动降低置信度。实施中需权衡计算精度与分析灵活性的关系、单品深度分析与多品横向对比的能力边界、LLM 训练知识与当前数据事实的冲突管理。核心原则是"引用即证据",每一条洞察都须有数据来源。

© 版权声明

相关文章