之前探索了LLM长上下文和数值类有效输出的关系

https://blog.csdn.net/liliang199/article/details/159175752

这里选用 苹果公司 2023 财年 10-K 年报(约 90 页,约 70K tokens)作为测试文本。

任务包括:

1)直接数值提取:从文本中找出指定财务数据(如总营收、净利润)。

2)基于提取值的计算:如计算“研发费用占总营收的比例”。

3)结构化输出:要求模型以 JSON 格式返回结果,便于程序解析。

将通过两种方式处理长文本:

1)一次性传入,如果模型上下文窗口足够,如 128K context的模型。

2)分块 + 摘要/检索,模拟超长文本无法一次性处理的情况。

1 环境准备

假设openai、faiss等相关工具已按照,这里示例环境设置和数据获取过程。

1.1 设置环境

模拟分块检索需要向量模型,可能存在hf访问问题,所以这里先设置hf国内镜像。

同时设置api key、user_url、model_name等。

import os
os.environ['HF_ENDPOINT'] = "https://hf-mirror.com"

os.environ['OPENAI_API_KEY'] = "sk-xxxxxx"
os.environ['OPENAI_BASE_URL'] = "https://llm_provider.com/v1"

model_name = "qwen3-xxxx"

这里实际运行采用qwen3.5系列模型。

1.2 数据获取

从 SEC EDGAR 下载苹果 2023 年 10-K 文本,链接如下所示

https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm

由于是网页数据,这里采用选中所有内容后复制,然后在本地粘贴的方式,在本地构建aapl-20230930.txt文件。

1.3 预估token量

使用 tiktoken 预估 token 数,确保不超过模型限制。

import tiktoken

def num_tokens_from_string(string: str, encoding_name: str = "cl100k_base") -> int:
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens


with open("./aapl-20230930.txt") as f:
    full_text = f.read()

tokens = num_tokens_from_string(full_text)
print(f"文档 token 数: {tokens}")

if tokens < 128000:
    print("可直接使用128K上下文的模型")
else:
    print("需采用分块策略")

输出示例如下

文档 token 数: 45185
可直接使用128K上下文的模型

2  基于单轮对话的提取计算

这里构造提示并调用 API,示例单轮对话的提取计算过程。

2.1 设计示例

首先设计提示词,要求模型提取指定数据并以 JSON 返回,细节如下。

from openai import OpenAI

client = OpenAI()

def ask_model(document, questions):
    prompt = f"""
你是一个财务分析专家。以下是苹果公司2023财年10-K年报的部分文本。

请根据文本回答以下问题,并以JSON格式返回结果。JSON键为问题编号,值为对应的答案(数值或字符串)。

文本内容:
{document}

问题:
{questions}
请直接输出JSON,不要包含其他文字。
"""
    response = client.chat.completions.create(
        model=model_name,  # 支持128K上下文的模型
        messages=[{"role": "user", "content": prompt}],
        temperature=0,  # 降低随机性,提高数值准确性
        max_tokens=500
    )
    return response.choices[0].message.content

# 截取前60000 token(若文档超长可截断,此处假设一次性传入)
if tokens > 120000:
    # 简单截断(更优做法是按段落截取,保证完整性)
    encoding = tiktoken.get_encoding("cl100k_base")
    encoded = encoding.encode(full_text)
    truncated = encoding.decode(encoded[:120000])
    document = truncated
else:
    document = full_text


question_list = [
    "1. 2023财年总营收(Total net sales)是多少?请以百万美元为单位,只输出数字。",
    "2. 2023财年研发费用(Research and Development)是多少?请以百万美元为单位,只输出数字。",
    "3. 研发费用占总营收的比例是多少?请以百分比形式输出,保留两位小数,如\"15.23%\"。",
]
questions = "\n".join(question_list)

result = ask_model(document, questions)
print(result)

返回如下所示

{
  "1": 383285,
  "2": 29915,
  "3": "7.80%"
}

2.2 解析 JSON 并验证

在获取LLM返回后,解析json并进行验证,代码细节如下。

import json
import re

def extract_json(text):
    # 从模型输出中提取JSON部分(有时会夹杂额外文本)
    json_pattern = r'\{.*\}'  # 简单匹配,实际可用json.loads尝试
    match = re.search(json_pattern, text, re.DOTALL)
    if match:
        return json.loads(match.group())
    else:
        return json.loads(text)  # 如果整个输出就是JSON

try:
    data = extract_json(result)
    print("解析结果:", data)
    # 真实值(根据实际年报)
    ground_truth = {
        "1": 383285,  # 百万美元,约3833亿美元
        "2": 29915,   # 约299亿美元
        "3": 7.804     # 比例 = 29915/383285 ≈ 7.81%
    }
    print("真实值:", ground_truth)
    # 简单比较
    for q, pred in data.items():
        gt = ground_truth[q]
        if q == "3":
            # 百分比字符串转浮点比较
            pred_val = float(pred.strip('%'))
            print(f"Q{q}: 预测 {pred_val}% vs 真实 {gt}%,误差 {abs(pred_val-gt):.2f}%")
        else:
            print(f"Q{q}: 预测 {pred} vs 真实 {gt},误差 {abs(int(pred)-gt)}")
except Exception as e:
    print("解析失败:", e)
    print("原始输出:", result)

输出示例如下

解析结果: {'1': 383285, '2': 29915, '3': '7.80%'}
真实值: {'1': 383285, '2': 29915, '3': 7.804}
Q1: 预测 383285 vs 真实 383285,误差 0
Q2: 预测 29915 vs 真实 29915,误差 0
Q3: 预测 7.8% vs 真实 7.804%,误差 0.00%

可以在上下文长度允许的情况下,单次传入可以获得精确结果。

3 基于分块+检索的提取计算

这里假设文本远超模型上下文时,这时可采用检索增强生成RAG的思路。

将文档分块,用向量检索相关块,再将相关块送入模型。

3.1 分块与向量化

这里将文本划分为1000大小块,使用all-MiniLM-L6-v2将分块文本转向量,使用faiss管理向量。


from sentence_transformers import SentenceTransformer
import numpy as np
import faiss

# 分块函数(按段落或固定长度)
def chunk_text(text, chunk_size=500, overlap=50):
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = ' '.join(words[i:i+chunk_size])
        chunks.append(chunk)
    return chunks

chunks = chunk_text(full_text, chunk_size=1000)  # 每个块约1000词
print(f"分块数量: {len(chunks)}")

# 生成向量(使用轻量模型)
model = SentenceTransformer('all-MiniLM-L6-v2')
chunk_embeddings = model.encode(chunks, show_progress_bar=True)

# 构建FAISS索引
dimension = chunk_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(chunk_embeddings))

示例如下

分块数量: 32
Batches: 100%|██████████| 1/1 [00:00<00:00,  1.38it/s]

3.2 检索与问答
 

在将full_text文本向量化后,这里进行分块检索和LLM回答示例,代码如下所示。

def retrieve_relevant_chunks(query, k=3):
    query_emb = model.encode([query])
    distances, indices = index.search(query_emb, k)
    return [chunks[i] for i in indices[0]]

def ask_with_rag(questions):
    # 将多个问题合并成一个查询,检索相关块
    combined_query = " ".join(questions.values() if isinstance(questions, dict) else questions)
    relevant = retrieve_relevant_chunks(combined_query, k=5)
    context = "\n\n---\n\n".join(relevant)
    
    prompt = f"""
根据以下从苹果10-K年报中提取的相关段落,回答问题。以JSON格式返回。

相关段落:
{context}

问题:
1. 2023财年总营收(Total net sales)是多少?以百万美元为单位。
2. 2023财年研发费用(Research and Development)是多少?以百万美元为单位。
3. 研发费用占总营收的比例是多少?以百分比形式。

JSON输出示例:
{{"1": 383285, "2": 29915, "3": "7.81%"}}
"""
    response = client.chat.completions.create(
        model=model_name,  # 可使用较小模型
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=300
    )
    return response.choices[0].message.content

questions_dict = {
    "1": "2023财年总营收",
    "2": "2023财年研发费用",
    "3": "研发费用占总营收的比例"
}
rag_result = ask_with_rag(questions_dict)
print(rag_result)

输出如下

{
  "1": 383285,
  "2": 29915,
  "3": "7.80%"
}

4  数值可靠性增强技巧

以上示例了基于LLM以及RAG,对苹果财报的总营收、研发投入,以及研发占比进行提取和计算。

实现逻辑简单清洗,然而在实际案例中,可能需要提取更复杂的数据。

数据可能不是显式分布,可能更隐蔽,更难发现,这时可能要采用更可靠的增强技巧。

比如CoT、取均值、以及应用外部计算工具。

1)思维链多步推理

要求模型先解释推理过程,再给出数值,可减少计算错误。

cot_prompt = """
请逐步推理并计算:
1. 从文本中找到总营收数字。
2. 找到研发费用数字。
3. 计算比例。
最后以JSON输出结果。
"""

2)多次运行取均值

temperature能调节模型的灵敏度,多次采用不同的temperature采样,取多数答案或平均值。

另外,采用幂采样的方式优化模型输出,也能获得更好的结果,缺点是会拖慢处理速度。

参考链接如下

https://blog.csdn.net/liliang199/article/details/154833697

3)应用外部计算工具

LLM运行计算并不是总是可靠的,有可能出现幻觉,导致计算错误。

让模型输出表达式,用 Python eval() 执行计算,可以避免模型内部计算错误,示例如下。

calc_prompt = """
请输出一个Python表达式来计算研发费用占比,例如 "x / y * 100",其中x和y是提取的数字。
"""

该方法的前提是数据已就绪,否值还需要补充数据提取和清洗过程。

reference

---

LLM长上下文和数值类有效输出的关系探索

https://blog.csdn.net/liliang199/article/details/159175752

如何基于幂采样优化LLM推理-原理&代码&示例

https://blog.csdn.net/liliang199/article/details/154833697

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐