简介:生成式人工智能研究报告及共识文件由世界互联网大会人工智能工作组于二零二三年十一月发布,面向人工智能研究者、产业从业者、政策制定者以及关注技术治理的读者,旨在系统回应生成式人工智能快速演进中的关键问题。资源为一份PDF文档,包体大小约1.69MB,结构完整、目录清晰,适合通读与后续查阅。目前已有四十一人学习浏览,报告内容既包括全球技术发展态势、大模型在文本、代码、图像、音视频等方面的能力跃升,也分析了由此带来的经济机遇与社会风险,并汇总各国、国际组织与产业界在负责任人工智能治理上的探索。特别是其中附有行业应用探索和缩略语对照,便于读者快速定位核心概念、掌握共识要点,是一份兼顾广度与深度的生成式人工智能入门及参考材料。
1. 生成式人工智能研究报告的 PDF 知识化,把“读完”变成“可检索”
投资机构、研究团队和负责内部知识管理的人拿到手的,通常不是干净的 Word,而是一份动辄一百多页、图表与正文混排的 PDF。生成式人工智能研究报告里,决定后续工作能否推进的往往是不太显眼的信息:口径是什么、数据截止到几月、谁在什么前提下提出了不同佐证。直接读 PDF 会丢失这些线索;把这 120 页转成一段段可定位、可引用的结构化文本,才是知识工作流的起点。下面这套路径就可以覆盖这个需求:用 PyMuPDF 抽取物理文本和书签,用 pdfplumber 处理跨页表格,再按章节语义切分、向量化,通过检索增强生成输出带页码的问答结果,最后用双层 PDF 和回链验证拿到可交付物。写给两类人:一类是刚接手 PDF 解析任务、需要快速上手的工程师,另一类是已经做过抽取、想弄清楚参数边界和验证方法的从业者。
2. 用 PyMuPDF + pdfplumber 把生成式人工智能研究报告拆成可检索内容
PDF 解析的第一原则是不贪多。cdx研究报告里的正文、表格、脚注、页眉页脚各有各的物理结构,一次转成纯文本会把表格数据和正文混在一起,后面做语义切分时反而要花更多力气清洗。常见做法是先抽文本块,再抽表格,最后按坐标和书签把两者合并成文档流。
2.1 为什么选 PyMuPDF:文本抽取精度与版式信息
PyMuPDF(导入名是 fitz)在速度和版式保留之间平衡得比较好。它把页面渲染成文本块,每个块带左上角坐标、右下角坐标、block 类型和字符级 span,这比 pdfminer.six 的树形结构更直观,也比直接把 PDF 转 Word 再解析更可靠。对生成式人工智能研究报告这种排版比较规范的文档,fitz 的page.get_text("blocks")基本能还原标题、正文、页脚的相对位置,为后续按语义合并文本块提供了坐标依据。
另一个必要能力是读取 PDF 自带书签。很多研究报告在发布前已经做过目录和书签,doc.get_toc()能直接拿到章节标题和对应页码,不用靠正则猜标题层级。书签可以用于两件事:一是切分章节边界,二是做后续问答结果的书签回链。
2.2 用 fitz 抽取研究报告的全文文本、书签和坐标块
下面这段代码是抽取研究报告文本的最小实现,可以放在项目根目录直接跑:
import fitz from pathlib import Path def extract_pdf(pdf_path: str): doc = fitz.open(pdf_path) toc = doc.get_toc(simple=True) # 返回 [(层级, 标题, 页码), ...] print("bookmarks:", len(toc)) pages = [] for page_no in range(doc.page_count): page = doc.load_page(page_no) blocks = page.get_text("blocks") # (x0, y0, x1, y1, text, block_no, block_type) text_blocks = [b for b in blocks if b[6] == 1] # 1 表示文本块 pages.append({ "page": page_no, "blocks": text_blocks, "height": page.rect.height, }) doc.close() return pages, toc if __name__ == "__main__": pages, toc = extract_pdf("generative_ai_report.pdf")逻辑说明:block_type == 1是文本块,block_type == 0是图片,先把图片块过滤掉,避免后面把扫描页的脏数据打进语料。page.rect.height用来做页眉页脚过滤的基准坐标:如果某个文本块的 y0 坐标小于 40 或 y1 大于 页面高度减 30,多半是页眉页脚,在生成语料时可以直接跳过。bookmarks的数量可以作为解析质量的早期信号,如果预期 20 章却只解析出 3 条书签,说明文档可能被扫描过,需要走 OCR 流程。
2.3 用 pdfplumber 还原跨页表格,处理模型对比数据
生成式人工智能报告里最常见的表格类型是模型参数对比和评测指标汇总。这类表的特点是列数多、单元格长、经常跨页,用普通文本抽取会把同一行的数据拆成多段。pdfplumber 的extract_tables基于坐标推断单元格边界,在带框线表格上表现稳定。
import pdfplumber def extract_tables(pdf_path: str, page_ids=None): results = [] with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): if page_ids and i not in page_ids: continue tables = page.extract_tables({ "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, }) if tables: results.append({"page": i, "tables": tables}) return results tables = extract_tables("generative_ai_report.pdf", page_ids=[10, 11]) for item in tables: for table in item["tables"]: for row in table: print(" | ".join(cell.replace("\\n", " ") if cell else "" for cell in row))参数说明:vertical_strategy和horizontal_strategy都设成lines,代表只相信 PDF 中真实的画线;如果报告里的表格是无线表,需要改成text模式,依靠文字纵向对齐推断边界,代价是误判率上升。snap_tolerance表示两条线之间小于 3 个像素时视为同一条线,对跨页续表和对齐不齐的表格有帮助。抽取出来的每个 row 是一个 list,元素是字符串或 None,把\\n替换成空格是为了后续向量化时不把单元格内换行拆成多余句子。
对于跨页表格,pdfplumber 默认把两页当作独立表格处理,合并时要以表头行作为锚点。常见做法是记录每个表格的表头,遇到下一页首个表格与上一页表头相同就进行拼接。
2.4 抽取成果的常见异常:乱码、页眉页脚、续表错位
PDF 解析失败通常发生在三个地方。第一,字体编码异常导致文本块全是乱码,这种文件多见于用特殊中文字体生成的研究报告,解法是切换page.get_text("rawdict")查看字体名,再决定是否用 OCR 补救。第二,页眉页脚混入正文,研究报告页码通常在页面底部居中,用 y 坐标过滤即可,但要注意正文最后一行的 y1 可能接近页脚,过滤阈值要留出余量。第三,表格跨页后列错位,pdfplumber 返回的表格可能出现某一列全为空,这时要回到extract_words手工按 x 坐标分组,而不是直接信任表格策略。遇到上述异常时,先抽一个页面样本来人工核对,再全量处理,比跑完 200 页再发现错误更高效。
3. 按语义切分研究报告并向量化,让 PDF 文本可语义检索
把 PDF 解析成文本块还不够,真正支撑问答和引用的是一段段语义自洽的 chunk。文本块太小,检索时缺少上下文;块太大,embedding 的语义被稀释。生成式人工智能研究报告通常以小节为一个语义单元,但 PDF 里的小节标题往往没有独立标记,需要结合坐标、字体和书签来判断。
3.1 为什么按语义切分,不按页切分
按页切分是最容易想到的做法,但报告里的章节经常从页面三分之一处开始,到下一页顶部结束,直接把整页作为检索单元,会导致一段完整论述被切到两个 chunk 中,检索时召回的往往是“半个章节”。更稳妥的做法是先按书签层级切出章节边界,再在章节内按文本块顺序做长度控制。PyMuPDF 的 blocks 天然带 y 坐标,按 y 坐标排序后就能得到符合阅读顺序的文本流,然后用字符数控制 chunk 大小。
这样的设计对齐了“研究报告及共识文件”这类文档的特点:共识条款往往分布在章节末尾,一个完整的共识段落可能横跨 6 到 8 行,按语义切分后,这些句子能出现在同一个 chunk 里,后续做向量化检索时不会被切断。
3.2 用坐标与标点做语义边界的 chunk 切分
下面这段代码在文本块序列上用窗口滑动切块,同时考虑语义边界:
import re def build_chunks(pages, max_chars=800, overlap=100): chunks = [] cur = [] cur_len = 0 for page in pages: blocks = sorted(page["blocks"], key=lambda b: (b[1], b[0])) for b in blocks: text = b[4].strip() if not text: continue cur.append((page["page"], text)) cur_len += len(text) if cur_len >= max_chars: cutoff = len(cur) accum = 0 # 从后往前找句号或分号,尽量在完整语义处断开 for i in range(len(cur) - 1, -1, -1): accum += len(cur[i][1]) if accum >= overlap or re.search(r"[。;]\\s*$", cur[i][1]): cutoff = i + 1 break chunks.append(cur[:cutoff]) cur = cur[cutoff:] cur_len = sum(len(t) for _, t in cur) if cur: chunks.append(cur) return chunks chunks = build_chunks(pages, max_chars=800, overlap=100)逻辑说明:max_chars=800是面向中文报告的起步值,对应 bge 系列向量模型大约 512 token 的输入上限。overlap=100让相邻 chunk 保留尾部若干句作为上下文重叠,避免一句话被硬切开导致两端都缺少主语。从后往前找句号和分号,是为了在截断前找到一个相对完整的语义点;如果整段都没有句号,就退化为按重叠窗口硬切。每个 chunk 保存为(page, text)列表,页号会跟着进入后续检索,这是最终答案能够标注页码的基础。
3.3 用 sentence-transformers 做向量化与相似度召回
chunk 构建好之后,下一步是把每段文字编码成向量:
from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer("BAAI/bge-small-zh-v1.5", device="cpu") chunk_texts = [" ".join(text for _, text in c) for c in chunks] embeddings = model.encode( chunk_texts, normalize_embeddings=True, batch_size=16, show_progress_bar=True, ) def retrieve(query: str, top_k: int = 5): q_vec = model.encode([query], normalize_embeddings=True)[0] scores = embeddings @ q_vec idx = np.argsort(scores)[::-1][:top_k] return [(int(i), float(scores[i])) for i in idx if float(scores[i]) > 0.3]参数说明:normalize_embeddings=True让向量归一化,之后用点积就能得到余弦相似度,节省一层计算。batch_size=16控制编码时的显存或内存占用,如果是纯 CPU 环境,调小到 8 更稳。相似度阈值 0.3 只是起步值,在真实语料上需要先跑一批 query,观察相似度分布再调整;中文语义检索对同义表述比较敏感,阈值设得过高会把“生成式人工智能”与“大语言模型”这类相关表达筛掉。
3.4 调整块大小、模型与召回验证的实践经验
参数调整有两条经验值得记下来。第一,chunk 大小与向量模型的 max_seq_length 强相关,bge-small 系列建议每个样本不超过 512 token,中文文本大体对应 800 到 1000 字;如果换成 bge-m3,上下文窗口更长,chunk 可以适当加大。第二,重叠量不是越大越好,overlap 超过 150 字容易让相邻 chunk 内容高度相似,检索结果出现重复片段,回答时反而不好拼接。验证召回率时,从报告里挑 20 个有明确答案的问题,人工标注期望页码,然后统计retrieve返回的 top 5 结果是否覆盖目标页,覆盖率达 80% 再继续往下做。
| 参数 | 推荐起始值 | 调整方向 |
|---|---|---|
| max_chars | 800 | 命中率低但相关性强时减小,上下文不足时增大 |
| overlap | 100 | 重复片段多时减小,句子被切断时增大 |
| top_k | 5 | 答案细节多时增大到 8 |
| 相似度阈值 | 0.3 | 误召回多时上调,漏召回多时下调 |
| embedding 模型 | bge-small-zh-v1.5 | 硬性准确率不足时换 bge-m3 |
4. 用 RAG 问答生成带引用答案,让模型区分事实与共识
向量检索把 PDF 内容变成了“可命题”的状态,但用户真正想要的是“这个问题在这份报告里是怎么说的”。直接把 top 5 chunk 拼进提示词让模型生成回答,就是标准的 RAG 流程。对生成式人工智能研究报告这种文档,最重要的不是让模型复述问题,而是让它区分“研究结论”和“共识要点”,并且每个观点都能回溯到页码。
4.1 研究报告问答的提示词设计:把检索片段当作证据
提示词设计决定了模型会不会编造页码。下面是一份针对该类文档的 system prompt:
SYSTEM_PROMPT = """你是生成式人工智能研究报告分析助手。 只能根据检索片段作答,禁止使用检索片段之外的信息。 回答必须分为两个部分: 1. 研究结论:报告中的事实性描述或分析判断。 2. 共识要点:两个及以上独立段落都支持的共同观点。 每个要点末尾添加引用标记 [p页码],页码来自检索片段。 如果检索片段不足以回答,明确写“无法从检索片段确认”。 如果不同片段存在矛盾,列出各方的页码和观点,不要强行调和。"""逻辑说明:分两部分输出,是为了在海量研究内容中把“事实”和“共识”分开,二者在后续评审中的使用方式不同。要求每句加[p页码],是为了强制模型引用检索片段中的页号,而不是自行编一个。最后一条“列出矛盾观点”对共识文件尤为重要,因为生成式人工智能领域的技术路线本来就有分歧,模型一旦自行调和就丢掉了原文的真实性。
4.2 接入 OpenAI 兼容接口的问答代码
实际调用时,优先选用本地推理服务或兼容 OpenAI API 的服务,代码不绑定具体厂商:
from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY", ) def answer(question: str, hits, chunks): context_parts = [] for rank, (idx, score) in enumerate(hits, 1): text = "".join(t for _, t in chunks[idx]) pages = ",".join(sorted(set(str(p) for p, _ in chunks[idx]))) context_parts.append(f"[{rank}] 页码: {pages}\\n{text}") context = "\\n\\n".join(context_parts) user_content = f"检索片段如下:\\n{context}\\n\\n问题:{question}" resp = client.chat.completions.create( model="qwen2.5:7b", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_content}, ], temperature=0.2, max_tokens=1024, top_p=0.8, ) return resp.choices[0].message.content, user_content参数说明:base_url指向本地推理服务时不需要真实密钥,填EMPTY即可;换成托管 API 时,去掉base_url并用环境变量注入密钥。temperature=0.2控制回答的确定性,做文档问答时建议固定在 0.1 到 0.3 之间,温度太高会让模型润色过度,把“报告没有明说的意思”也写出来。top_p=0.8配合低温度使用,避免采样过于发散。max_tokens=1024对多数问答足够,如果问题要求生成对比表,可以加到 2048。
4.3 引用页码回传:让检索到的 chunk 页码进入上下文
检索阶段拿到的idx只对应 chunk 索引,必须还原成页码数组,才能真正做到引用级溯源。上面的代码里,pages = ",".join(sorted(set(str(p) for p, _ in chunks[idx])))完成了这个转换:chunk 里可能有多个文本块来自不同页,比如一个 800 字的 chunk 跨了 2 页,这时页码就是“12,13”,模型会输出[p12-13]。这样做的目的在于,回答生成后审读人可以按引用翻到对应位置复核,解决了大模型问答“只给答案、不给依据”的信任问题。
4.4 控制幻觉的边界:检索缺失、上下文过载与页码伪造
RAG 问答最常见的失败是“检索不到但答案照给”。模型即使只拿到不相关内容,也会尽力组织出看起来合理的回答。缓解手段有三层。第一层是检索侧,相似度低于阈值时直接返回“无法确认”,而不是把低质量片段喂给模型。第二层是提示词侧,系统提示词中明确要求“无法从检索片段确认”时必须明说。第三层是生成参数侧,关闭或调低frequency_penalty和presence_penalty,防止模型为了语言多样性而偏离原文。页码伪造则要用规则校验解决:从回答中用正则抓出所有[p\\d+],去重后与检索上下文里的页码集合比对,发现页码不在集合中就在界面上标红提醒,这个校验逻辑比单纯依赖提示词更可靠。
5. 用版本对比与双层 PDF 验证研究报告,生成可回溯交付物
问答系统跑通后,真正缺少的是验收手段。没有验证步骤,解析和检索的质量就停留在“看起来还行”的状态。
5.1 用黄金样本做召回回归,量化解析质量
从生成式人工智能研究报告里人工挑出 20 个“问题-期望页码”对作为黄金样本,然后跑一次召回统计:
def evaluate_recall(gold_pairs, retrieve_func, top_k=5): hit_count = 0 for query, expected_pages in gold_pairs: got_pages = set() for idx, _ in retrieve_func(query, top_k): got_pages.update(p["page"] for p in chunks[idx]) if got_pages & set(expected_pages): hit_count += 1 return hit_count / len(gold_pairs) recall = evaluate_recall(gold_pairs, retrieve) print(f"top-{5} recall: {recall:.2f}")这段代码把检索质量变成了可量化的指标。每次调整 chunk 参数、模型或相似度阈值后都重跑一遍,避免改了表格抽取逻辑、忘了回归文本抽取效果。
5.2 把问答结果导出为带书签的双层 PDF
对需要分发给同事或归档的场景,把问答结果写回 PDF 是符合习惯的做法。用 fpdf2 生成带书签文本型 PDF:
from fpdf import FPDF pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=20) for section_title, lines in qa_sections: pdf.add_page() pdf.set_font("helvetica", "B", 16) pdf.cell(0, 10, section_title) pdf.ln() pdf.set_bookmark(section_title, level=0) pdf.set_font("helvetica", size=10) for line in lines: pdf.multi_cell(0, 6, line) pdf.ln() pdf.output("research_qa.pdf")逻辑说明:set_bookmark把每个问答段落注册为书签,生成的 PDF 在阅读器侧边栏可以直接定位。这里生成的是单层文本 PDF,如果原始报告是扫描件,想生成真正的双层 PDF,需要在底层插入原始扫描图片,再在相同坐标叠加透明文本层,fpdf2 不直接支持透明文本,通常用 PyMuPDF 的page.insert_text配合渲染层完成。
5.3 用 Markdown 回链替代 PDF,在评审阶段快速定位原文
在内部评审阶段,比 PDF 更适合的是带页码标记的 Markdown 输出,每一行共识要点后面紧跟页码标记,评审人用编辑器全局搜索页码即可跳到原始段落。这种验证方式最直接的收益是让“哪句话来自哪一页”一目了然,评审人不需要先下载 PDF 再手动翻页。整个流程最后得到的是一份可审计的问答材料,而不是一堆孤立的结果文本。
本文还有配套的精品资源,点击获取