先说明一下背景。手上拿到一份上市公司年报,是 PDF,转出来之后大概有二十万字。放进对话窗口里,模型一开始还能回复,聊到中间就明显“失忆”了——前面的数据忘了,后面的表格没读进去,最后甚至直接编了一个营收增长比例出来,而我恰好知道那个数字是错的。
这几乎是所有做 LLM 长文本落地的人都会撞上的墙。后来我换了一种做法,不再追求“把整本财报塞进去”,而是把任务拆成“先定位关键信息、再用单样本做格式约束、最后让模型输出结构化结论”。这套流程在我处理的几个模拟项目里都很稳,今天就把它完整拆开讲一遍。
1. 为什么财报分析场景需要 oneshot 而不是堆长文本
1.1 财报文本和普通长文的本质区别
财报不是小说,也不是聊天记录,它在结构上有一个非常鲜明的特点:高密度数字 + 强格式模板 + 关键信息稀疏分布。一份二十万字的年报里,真正影响投资判断的内容可能只占两三成,其余都是公司沿革、行业介绍、风险提示模板、审计报告模板、股东信息等“填充物”。
这带来一个问题:如果你只是简单地把全文切块、向量化、随后做相似度检索,再让模型基于检索片段回答问题,很容易出现“答非所问”。因为相似度检索选中的段落,未必是财报里财务数字最密集的那一段,也可能是股东结构表格那一页,或者管理层讨论里那段空泛的“公司将继续深耕主业,提升竞争力”。
我做过的模拟项目 X 里,最开始就是走 RAG 路线,结果让模型去判断“公司当年毛利率变化的主要原因”,它抓回来的段落是董事会报告里那种放之四海而皆准的口号式表述,根本没法用。这让我意识到,财报场景需要的不是单纯的“长文本记忆”,而是先按财务分析框架做段落定位,然后由提示词强约束输出格式。
1.2 窗口扩展和流式输入为什么不够用
有人会说,现在很多模型支持 200K 甚至更长的上下文,直接把整本年报塞进去不就行了?我实际试过。先说结论:技术上能塞,但效果很不可靠。
第一,中间遗忘。即便上下文窗口够大,模型对长文本中前中后段落的注意力分配并不均匀,越靠中间的内容越容易被“忽略”。财报里最重要的三张报表往往集中在“财务会计报告”这一章,恰好处于全文的中间偏后位置,被忽略的风险很高。
第二,数值混淆。当多个相似数字出现在不同上下文位置时,比如营业收入、营业成本、应收账款,如果它们彼此离得很远,模型很容易把它们混在一起。我遇见过一次,模型把“经营活动现金流净额”直接说成了“净利润”,两个数字差了十几倍。
第三,成本与延迟。每次请求都带着二十万字进模型,token 费用是按输入算的,响应时间也会显著变长,这在批量分析多家公司财报时根本没法接受。
所以,我最终的方案是:按需取段 + oneshot 单样本约束。不是“让模型读完整个财报”,而是“让模型带着一个分析目标去财报里找答案,并且只针对定位好的局部文本输出固定结构的内容”。
1.3 oneshot 相比 few-shot 的本土化优势
关于 few-shot(多样本)和 oneshot(单样本),很多教程都建议“示例越多越好”,但财报分析是一个很特殊的场景。
Few-shot 意味着你要在提示里塞多个“用户问题-标准答案”对。问题来了:财报分析没有绝对标准答案,即便是同一年同一家公司的财务指标,不同分析师的表述口径也不一样。如果示例里出现两个口径不一致的答案,模型会无所适从,输出结果的波动性反而更大。
而 oneshot 只需要一个示例,它的核心作用不是给模型“学习知识”,而是给模型划定输出格式和表达粒度。比如示例里我们只展示了一条“收入分析”的写法,模型自然会在后续输出时保持同样的维度、同样的句式结构、甚至同样的字段名。这是我实测下来效果最稳定的做法。
另外,从控制角度看,oneshot 提示更容易做版本管理。你维护的是一个极小的提示模板和一条示例,而不是一整本带标注的“模拟答案集”。每次调整分析框架,只需要改这一条示例即可。
2. 从零构造一份可复用的财报分析单样本
2.1 单样本提示的标准结构
一份用于财报分析的 oneshot 提示,不管你怎么组织,里面都必须包含四个部分:系统指令、示例用户输入、示例理想输出、当前用户输入。
系统指令负责定义角色和分析框架,比如“你是资深财务分析师,请基于给定的财报片段输出结构化分析”。示例用户输入给出一段带财务数据的文本。示例理想输出给出一份格式完整的分析结果。当前用户输入则是你从财报里截取的实际片段。
这个结构看起来简单,真正容易做砸的地方在示例的选择与设计上。我踩过一次坑:一开始我随便找了一段“利润表”的内容当示例,结果模型在分析“资产负债表”时,也照葫芦画瓢输出了大量利润率相关的字段,完全跑偏了。
后来我总结出一条经验:示例内容必须与你要分析的目标保持同构。如果你希望模型分析三大报表,给三条独立请求,每个请求配一条对应报表的示例;如果你希望模型一次性输出综合分析报告,示例就得完整包含三大报表的关键指标来源和解读逻辑。
2.2 示例里应该有什么:指标选择的艺术
在构造示例时,我固定了一套“四维分析框架”,这套框架是从机构研报的常见写法里提炼出来的,我试过之后发现它兼容性很强。
第一个维度是增长性,看营业收入、净利润的同比变化,判断公司是扩张还是收缩。第二个维度是盈利性,看毛利率、净利率、ROE 的变化趋势,判断赚钱能力有没有变强。第三个维度是安全性,看资产负债率、流动比率、现金流状况,判断会不会有财务风险。第四个维度是质量,看应收账款、存货、经营现金流与净利润的匹配度,识别利润含金量。
在 oneshot 示例里,我一般只展示增长性和盈利性这两个维度的完整输出格式,安全性和质量则在系统指令里用列表方式规定。原因很简单:示例条数越少,模型越容易模仿;而维度越多、指令里说清楚优先级,效果比全塞进示例更好。
我还特别要求示例里必须有数据来源引用,即使只是“(利润表,第218页)”这样的粗粒度定位。实测下来,这个要求能显著降低模型编数字的概率。
2.3 示例里不能有什么:避免预设完美答案
这是我最想提醒新人的一点。在构造示例时,很多人会不自觉地把“理想答案”写得特别漂亮、特别完整,甚至带上了分析师的判断语气。这其实是错的。
比如你在示例里写“毛利率下降主要系原材料成本上涨所致”,模型在分析当前数据时就会倾向性地去寻找“成本上涨”的证据,哪怕当前财报里毛利率下降是因为产品降价。这不是模型故意犯错,而是单样本的强引导效应在起作用。
正确做法是:示例的结论部分尽量保留中性语气,比如“需结合营业成本明细进一步判断”,把真正的因果推断空间留给模型基于当前输入去做。这样既保留了输出结构,又不至于把模型的思维钳死。
3. 长文本的分段、筛选与定位实战
3.1 财报文本清洗:不干净的数据会直接带崩模型
财报 PDF 转出来的文本,质量参差不齐。页眉页脚混在正文里,表格被拆得七零八落,财务附注里那种“一列数字换行换得面目全非”的情况更是家常便饭。如果你不做清洗就直接截段喂给模型,最常见的后果就是数字对不上,模型拿着残缺数字一顿推理,得出来的结论自然没法看。
我的清洗流程大致三个步骤。第一步,去掉重复的页眉页脚,通常按“每页重复出现且含公司简称”的规则批量删。第二步,把表格转成线性文本,保留表头字段与数值的对应关系,这一步非常关键,千万不要把表格转成 Markdown 那样的大宽表,因为 token 浪费严重,模型也容易看串行。第三步,把英文、全角半角符号统一,把多余换行符合并,保证段落连贯。
这几步里面最容易反复返工的是表格转文本。我目前用的方案是:只提取表头所在行和数值所在行,按“项目:数值 单位:万元”的格式展平。这样固然会丢失表格的原始排版,但换来的是模型对字段与数值对应关系的清晰感知,值远远大于失。
3.2 分段策略:按“章节锚点”切而不是按字符数硬切
很多长文本处理的教程会教你把文本切成固定长度,比如每 512 个 token 一段。这个做法在通用问答里没问题,但在财报分析里会带来灾难:一个财务指标往往在一段里只写了一半,另一半被切进下一段,模型提取这个指标时必然丢数据。
我的做法是按章节标题切分。财报结构非常固定,一般都有“经营情况讨论与分析”“重要事项”“财务会计报告”“审计报告”这些大章节。先用正则或关键词匹配定位标题,把全文切成一棵章节树,再对每个大章节做内部子标题的二次切分。
这么做有一个额外好处:你可以对切分结果做人工锚点标记。比如“第三节 经营情况讨论与分析”里稳定的子章节是“主营业务分析”“资产及负债结构”“投资状况分析”,这些锚点可以直接用于提示词里的定位指令。
比如构造提示时我会写“请仅在以下片段中分析公司营业收入变化趋势,片段来自‘主营业务分析’章节”,模型就很少会跑到其他地方去“补脑”。
3.3 信息密度筛选:只喂模型最该看的部分
不是所有章节都值得进提示。我在一个实际项目里统计过,年报里“公司简介”“股本变动”“董监高情况”这些章节,与财务分析目标的关联度极低,喂进去纯属浪费 token 并稀释注意力。
最终我的筛选规则很简单:优先保留“经营情况讨论与分析”“财务报告附注”“审计报告”三个一级章节,再根据具体分析目标从中抽取对应子章节。
如果是分析盈利能力,重点截取“主营业务分析”和“利润表及附注”;如果是分析偿债风险,重点截取“资产及负债结构”和“资产负债表附注”;如果是分析现金流质量,重点截取“现金流量状况分析”对应的段落。
这个逻辑一旦固化为代码里的一个映射表,后面处理新财报时就非常省事。我只需要指定“我要分析什么”,脚本自己就能把对应片段从长文本里捞出来。
3.4 Token 预算分配:一条请求别超过这个量
有人问,oneshot 提示本身已经占了几个 token,剩下的窗口到底能塞多少财报正文?我实测下来,单次请求的输入总量控制在 8K token 以内效果最稳。
理由有两个:一是提示模板和示例本身会占掉 1K 到 1.5K token,余下空间要留给正文片段和模型输出;二是即便模型支持更长窗口,从财报中提取的原始片段一旦超过 8K,很可能多段之间的语义互相干扰,模型对数字的抓取精度会肉眼可见地下降。
所以我的做法是“一次一小块”。每一份财报至少发起五六次请求,每一次带不同的目标章节片段,最后把多次结构化输出汇总起来。这听着麻烦,但实际运行下来比一次塞一大堆文本要快,因为每次请求的返回延迟更低,输出解析也更省事。
4. 核心实操:把 oneshot 流程变成一段可复用的代码
4.1 定义清晰的输入输出结构
提示工程不能纯靠“聊”,要用契约化的方式定义输入输出。我在代码里用两个 JSON Schema 来约束模型行为,一个是输出的 JSON 结构,一个是输入正文的结构化包装。
输出结构大概长这样:
{ "target": "营业收入增长性分析", "period": "2023年度", "metric_value": 2358.6, "metric_yoy": 18.7, "conclusion": "营业收入保持较快增长,主要受益于核心产品线放量", "source": "财务会计报告-利润表", "risk_flag": 0 }字段全部小写下划线格式,conclusion 字段限制在五十字以内,source 字段强制填写页码或章节名。这些约束在系统指令里写得明明白白,模型即使偶尔“发挥”,也跑不出这个框。
4.2 提示模板实现细节
我用的提示模板大概长这样,你可以直接复制调整:
system_prompt = """ 你是一名持证的财务分析师。请基于给定的财报片段,完成指定的分析任务。 分析框架: - 增长性:关注营业收入、净利润、扣非净利润的同比变化 - 盈利性:关注毛利率、净利率、ROE的变化 - 安全性:关注资产负债率、流动比率、有息负债规模 - 质量:关注经营现金流与净利润之比,应收账款和存货的异常变动 输出规则: - 只输出一个JSON对象,不要输出任何额外解释 - 所有数字必须来源于财报片段,不允许推测 - 每个结论字段不得超过50字 - source字段必须说明数据来自哪张表或哪个章节 - 如果片段中缺少所需信息,对应字段填 null,并置 risk_flag = 1 """ example_user_input = """ 数据来源:2023年度利润表(第218页) 报告期:2023年度 营业收入:1,245,309万元,同比 +23.5% 营业成本:876,221万元,同比 +25.1% 销售费用:45,678万元,同比 +3.2% 研发费用:67,890万元,同比 +40.5% """ example_model_output = """ { "target": "盈利性分析", "metric_value": 29.6, "metric_yoy": -1.2, "conclusion": "毛利率小幅下滑,主因成本增幅略高于收入增幅", "source": "利润表(第218页)", "risk_flag": 0 } """真正发起请求时,把当前财报的目标片段替代到example_user_input的位置,同时替换target字段里的任务名即可。
这里有个细节:示例中的数字是带有明确“收入、成本”字段名的,这能提示模型去关注字段之间的计算关系。如果你的示例数据本身字段名混乱,模型输出时也会跟着乱。
4.3 多次调用与结构化汇总
单条请求返回的是一个 JSON,多次请求之后就得到了一组 JSON。串起来的逻辑很简单:先并行发请求,拿到结果后统一做字段校验、空值处理和冲突检测。
我强烈建议不要串行等待,因为每次请求耗时可能不一样,串行会让总耗时大幅拉长。用并行方式,五六个维度的分析可以在几十秒内全部返回。
冲突检测这一步容易忽略,但很重要。比如“增长性”结论说净利润大幅增长,“盈利性”结论却说 ROE 大幅下降,两者可能同时成立,但如果矛盾过于明显,就要回到原文本里人工复核。我会把这些冲突项单独列一个分组,不让它直接进入最终报告。
4.4 从结构化结果到自然语言分析报告
拿到结构化 JSON 之后,最后一步是把它翻译成一段可读性强的分析文本。这一步我不用再走大模型生成,直接用一段简单模板拼接就好。原因是:一份让分析师看起来专业的财报分析,语言要克制、准确、有层次感,直接让 LLM 自由发挥反而容易跑出浮夸的形容词。
我的模板大致是“本期营业收入 X 万元,同比增长 Y%,主要驱动力是……;但毛利率下降至 Z%,需关注……”。这些句子虽然机械,但作为工作总结、投决会材料,反而比模型生成的花团锦簇的文字更可信。
5. 实战中的高频问题与排查经验
5.1 数字幻觉怎么压
每次提到财报分析,数字幻觉都是绕不开的话题。我自己也交过学费:模型在一次分析里,把“应收账款周转天数 45 天”写成了“存货周转天数 45 天”,硬生生给公司加了一项完全错误的问题。
后来我定了一条硬规矩:所有关键数字在输出时,必须附带来源页码和表名。没有来源页码的输出,一律重新请求。有了这个约束之后,模型基本不敢编数字了,因为它知道来源可查,被抽查到编造就相当于分析报告作废。
5.2 截断和超时怎么处理
长文本请求最容易遇到两类错误:一类是输入超过限制被直接拒绝,另一类是模型输出到一半就停了。
第一类问题靠分段解决,前面已经讲得很细。第二类问题我一般用两个办法:一个是把输出 max_tokens 参数调高,保证输出空间足够;另一个是开启自动重试,遇到截断就把已输出的 JSON 返回给模型,让它“续写修正”,八成以上能补全。
另外,想提醒一句:别把温度参数设太高,财报分析要的是确定性,不是“创意”。我做这类任务时温度固定在 0.1 到 0.2 之间,几乎不波动。
5.3 输出格式不稳定怎么设兜底
模型偶尔会不按 JSON 输出,尤其是遇到长文本前后文纠缠的时候。我的兜底方案分三层:第一层用正则提取多余的 Markdown 标记,比如代码块注释;第二层用 JSON 解析器做容错解析,容忍尾逗号、单双引号混用;第三层如果还是解析失败,就把模型输出直接丢弃,重新调用一次,而不是硬着头皮去纠错。
实测下来,经过前三层兜底,格式问题导致的失败率可以压到 2% 以下。这个数值在我做的批量分析场景里是可以接受的。
5.4 分析结论怎么验证
最后一步是验证。我会把结构化输出里的关键数字和财报原文里的实际数据做一次自动比对。不需要太复杂,只要检查差值不超过 1% 就算通过。
如果发现有超偏差的数字,直接定位到对应的 source 字段,去原文对应段落实地核实。这一步虽然不费多少功夫,但能拦住绝大多数低级错误。
6. 我个人实操后的几点体会
这套 oneshot 流程跑通之后,我把同一份财报数据分别用“全文直塞”和“分段 oneshot”两种方式做过对比评测。直塞方案在回答第三问时就开始出现数据张冠李戴,而分段 oneshot 方案基本保持了全场零幻觉,唯一的缺陷是多了四五次请求,延迟从 10 秒涨到了 40 秒左右——这个代价换来的可靠性,我是认的。
另外,我越来越觉得,提示工程的重点不在于“把提示写得多花哨”,而在于把输入空间管得干净、把输出空间约束清楚。这跟带新人做分析报告是一个道理:你给的信息越准确,对方给你的结论就越靠谱。
如果后续要在这个方向上继续扩展,我建议可以从“自动抽取财报章节锚点”和“多期财报对比分析”两个方向入手。前者能让分段这一环节完全自动化,后者可以对公司连续几年数据做趋势判断——把单期 oneshot 的分析结果存下来,下一期再喂进去做对比,模型给出的连续性分析价值会又上一个台阶。