☰
论文AI助手实战:RAG结合代码执行实现科研数据分析
2026/10/13 11:55:33 网站建设 项目流程

论文阅读正在成为一项越来越“重”的体力活。过去我们读一篇论文,需要自己翻图表、找数据、核对实验设置;现在虽然有大模型可以做问答,但多数助手只停留在“聊天”层面——你问它“这篇论文的核心方法是什么”,它能答;你再问“把表2里不同模型的F1分数做个柱状图,并计算相对提升幅度”,它就只能泛泛而谈,给不出可复现的结果。

真正要被解决的不是“读懂论文”的问题,而是“把论文里的数据真正用起来”的问题。论文AI助手如果只带检索问答,本质上还只是一个带引用的搜索引擎;只有让它具备“跑分析”的能力,才能把论文从静态PDF变成一个可交互的科研数据工作台。这篇文章会带你从零搭建这样一个助手:先用向量库把论文内容变成可检索的知识,再让大模型通过函数调用执行Python代码,完成数据统计、可视化甚至简单复现分析。

读完你会理解传统RAG问答的边界在哪里,也会拿到一套可以直接扩展的代码骨架。整套方案不绑定特定科研领域,任何包含实验数据表的论文都可以套用。

1. 论文AI助手的真实需求:问答只是第一层

如果你平时只是泛读论文,那普通的摘要生成工具就够了。但科研和技术调研的真实场景往往是这样的:

  • 你想比较多篇论文的实验结果,需要从数据表中提取性能指标;
  • 你想快速验证一篇论文的结论,比如“作者声称方法A比基线高3个点”,需要自己算一遍;
  • 你想把某篇论文的数据重画成符合自己论文风格的图,需要访问底层数据;
  • 你需要追踪某篇论文不同版本或补充材料中的数据变化。

这些需求都指向同一个关键点:AI不仅要“读过”论文,还要“碰到”论文里的数据。

传统的RAG方案把PDF切块、向量化、检索,最后把文本块塞进Prompt让大模型生成回答。这种方法对“论文主要讲了什么”很有效,但对“帮我跑一个分析”基本无能为力。原因很简单:表格数据被PDF切块后,语义往往已经丢失;即使大模型能读出表格内容,它也无法直接动手算。

所以要做一个真正有用的论文AI助手,需要在RAG之上增加一层“工具调用层”。大模型负责理解用户意图、引用论文上下文,再通过代码执行工具完成实际的数据处理。用户看到的不再是一段文字,而是一个可以验证、可以修改、可以重新运行的分析结果。

2. 整体架构:检索增强 + 代码执行

这个论文AI助手的核心由三部分组成:

首先是把论文变成结构化知识。PDF里既有连续文本,又有非结构化的表格和图片。我们需要用工具抽取文本和表格,文本用于构建向量库,表格单独保存为CSV,供后续代码分析使用。

然后是检索问答。用户的每个问题先转换成向量,在论文文本索引中检索最相关的片段,连同问题一起交给大模型生成回答。这个过程负责处理“论文在讲什么”“方法是什么”“结论是什么”这类事实性问题。

最后是分析执行。当用户需要计算、统计、画图时,大模型会根据可用的数据文件生成Python代码,调用一个受控的代码执行函数。执行结果(文本输出、图片路径、新增数据文件)再反馈给大模型,由它组织成最终回答。

打个比方:传统RAG是一个“读过书的图书管理员”,能告诉你书里写了什么;加入代码执行后,这个管理员还变成了“实验室助理”,能帮你把书里的数据拿去做实验、画图表,并把实验报告交给你。

在实际实现中,我们不必把这三部分做成微服务。一个Python脚本就足以跑通最小验证。架构上只需要保证:数据文件与代码执行环境是隔离的,防止论文中的恶意指令污染宿主环境。

3. 环境准备与前置条件

实现这套方案需要准备以下基础环境:

  • Python 3.10 或更高版本;
  • 一个可用的 OpenAI API Key,或任何兼容OpenAI接口的大模型服务;
  • 如果使用本地模型,需要保证机器有足够内存,并选择支持函数调用和工具调用的模型;
  • 操作系统不限,Windows、macOS、Linux 均可。

需要安装的依赖库如下(版本请以实际安装为准,本篇文章不锁定具体版本):

pip install openai pandas matplotlib pdfplumber langchain langchain-openai langchain-community faiss-cpu python-dotenv

说明一点:langchain-openai用于向量嵌入接口,faiss-cpu用于向量检索。如果你更习惯使用Chroma或Elasticsearch,也可以替换,这里我们选择FAISS是因为它轻量、适合本地快速验证。

项目目录结构建议这样组织:

paper_ai/ ├── input/ │ └── paper.pdf ├── data_tables/ │ └── (代码自动生成 CSV) ├── paper_index/ │ └── (FAISS 向量索引) ├── outputs/ │ └── (图表和分析结果) ├── .env └── paper_ai.py

.env文件存放API密钥:

OPENAI_API_KEY=sk-xxxxxxxx

4. 论文解析:把PDF变成可计算的数据

解析论文是容易被低估的一步。很多人以为PDF解析就是把文本抽出来,真正做起来会发现两个难点:文本提取顺序错乱、表格提取困难。

对于文本部分,pypdf或pdfplumber都可以。既然我们要提取表格,直接使用pdfplumber更方便。下面的代码会遍历PDF每一页,分别提取文本和表格,表格数据保存为CSV文件,文本拼接后返回。

# 文件路径:parse_paper.py import pdfplumber import pandas as pd import os def parse_paper(pdf_path: str, output_dir: str = "data_tables") -> str: os.makedirs(output_dir, exist_ok=True) full_text = [] table_count = 0 with pdfplumber.open(pdf_path) as pdf: for page_index, page in enumerate(pdf.pages): # 提取文本 page_text = page.extract_text() or "" full_text.append(f"### Page {page_index + 1}\n{page_text}") # 提取表格 for table in page.extract_tables(): table_count += 1 # 第一行作为表头,其余作为数据行 header = [str(col) if col else "col" for col in table[0]] rows = table[1:] df = pd.DataFrame(rows, columns=header) # 清理空值和类型 df = df.apply(lambda col: pd.to_numeric(col, errors="ignore")) out_path = os.path.join(output_dir, f"table_{table_count}.csv") df.to_csv(out_path, index=False) full_text.append(f"\n### Table {table_count} located at Page {page_index + 1}\n") full_text_content = "\n".join(full_text) return full_text_content if __name__ == "__main__": text = parse_paper("input/paper.pdf") with open("paper_text.txt", "w", encoding="utf-8") as f: f.write(text) print("解析完成,表格和文本已保存")

这段代码会把PDF中的文本按页保存,同时把提取到的表格写入data_tables/table_*.csv。注意PDF表格的格式千差万别,extract_tables可能返回不完整的数据,后续需要人工抽查和清洗。如果某篇论文的表格特别复杂,更稳妥的办法是直接用Excel手动补充一份CSV,把论文中的原始数据录进去。AI助手不介意数据来自PDF还是手动录入,关键是数据要准确。

5. 构建向量索引与基础问答

论文文本准备好后,接下来的任务是把文本切块、向量化,存入FAISS索引。

切块大小一般选择500到1000字符,重叠100到200字符。这样既能保留上下文,又不会让检索粒度太粗。

# 文件路径:build_index.py from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain.text_splitter import RecursiveCharacterTextSplitter def build_index(text: str, index_path: str = "paper_index"): splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=150, separators=["\n\n", "\n", "。", ";", " ", ""] ) chunks = splitter.split_text(text) embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = FAISS.from_texts(chunks, embeddings) vectorstore.save_local(index_path) return len(chunks) if __name__ == "__main__": text = open("paper_text.txt", encoding="utf-8").read() count = build_index(text) print(f"已索引 {count} 个文本块")

基于这个索引,就可以写一个最基础的问答函数了。先检索相关文本片段,再把它们和问题一起传给大模型。

# 文件路径:rag_qa.py from openai import OpenAI from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS import os client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = FAISS.load_local("paper_index", embeddings) def ask_question(question: str) -> str: docs = vectorstore.similarity_search(question, k=4) context = "\n\n".join([doc.page_content for doc in docs]) response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是论文阅读助手,请基于提供的论文片段回答用户问题。如果片段不足以回答,请明确说明。"}, {"role": "user", "content": f"论文片段:\n{context}\n\n用户问题:{question}"} ] ) return response.choices[0].message.content

这个函数已经可以满足“论文在讲什么”“实验条件是什么”“结论是什么”这类问题。但距离“跑分析”还差关键一步。

6. 给AI加上“跑分析”能力:函数调用与代码执行

基础RAG只能输出文本,而我们的目标是在回答中输出真实的数据分析结果。实现方式是用大模型的函数调用能力,让模型在需要时生成Python代码,然后由我们的程序去执行。

先定义一个可安全执行的代码运行函数。这里使用的是Python内置的exec,但必须明确它的边界:它只能在受信任的本地环境中使用,不能用于处理不可信来源的论文。生产环境推荐用Docker容器或云沙箱隔离。

# 文件路径:executor.py import os import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt def execute_python_code(code: str, data_dir: str = "data_tables", output_dir: str = "outputs") -> dict: """ 执行模型生成的Python代码,并返回文本结果和生成的图片路径。 代码运行前会统一把数据目录和输出目录注入环境。 """ os.makedirs(output_dir, exist_ok=True) global_env = { "pd": pd, "plt": plt, "os": os, "data_dir": data_dir, "output_dir": output_dir, "__name__": "__main__", } captured_output = [] def print_wrapper(*args, **kwargs): captured_output.append(" ".join(str(arg) for arg in args)) global_env["print"] = print_wrapper try: exec(code, global_env) # 找出output目录下新增的图片 images = [f for f in os.listdir(output_dir) if f.endswith(".png")] return { "status": "success", "stdout": "\n".join(captured_output), "images": [os.path.join(output_dir, img) for img in images], } except Exception as e: return { "status": "error", "stdout": "\n".join(captured_output), "error": f"{type(e).__name__}: {e}", }

函数本身很简单,关键在于让大模型知道可以调用它。下面定义工具Schema:

# 文件路径:agent_tools.py tools = [ { "type": "function", "function": { "name": "execute_python_code", "description": "在本地Python环境中运行代码,可读取data_tables/下的CSV数据文件,进行统计分析,并生成图片到outputs/目录。", "parameters": { "type": "object", "properties": { "code": { "type": "string", "description": "要执行的Python代码。示例:df = pd.read_csv('data_tables/table_1.csv'); print(df.columns)" } }, "required": ["code"] } } } ]

接下来是Agent循环。流程是:把可用数据表的信息注入System Prompt,让模型知道有哪些数据文件;用户提问后,如果模型返回工具调用,我们就执行代码并把结果回传给模型,直到模型给出最终回答。

# 文件路径:run_agent.py from openai import OpenAI import os import glob import json from executor import execute_python_code client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) def list_available_tables(): tables = [] for path in glob.glob("data_tables/*.csv"): df = pd.read_csv(path) tables.append({ "path": path, "columns": list(df.columns), "shape": df.shape, }) return tables def run_agent(question: str, max_steps: int = 5) -> str: tables_info = list_available_tables() system_prompt = ( "你是一篇论文的AI分析助手。你可以阅读论文内容,也可以调用execute_python_code工具对论文中的实验数据执行分析。" "数据表位于data_tables目录下。当前可用数据表:\n" + json.dumps(tables_info, ensure_ascii=False, indent=2) + "\n当你需要统计数据、计算指标、画图时,请使用execute_python_code工具。" ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": question}, ] for step in range(max_steps): response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, tool_choice="auto", ) message = response.choices[0].message if not message.tool_calls: return message.content messages.append({ "role": "assistant", "content": message.content, "tool_calls": message.tool_calls, }) for tool_call in message.tool_calls: args = json.loads(tool_call.function.arguments) result = execute_python_code(args["code"]) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) return "达到最大步骤限制,未获得最终答案。" if __name__ == "__main__": question = "帮我读取data_tables/table_1.csv,统计不同方法在测试集上的F1分数,并绘制柱状图保存到outputs目录。" answer = run_agent(question) print(answer)

这个Agent流程看起来不复杂,但已经具备“跑分析”的核心能力:大模型会根据问题生成代码,代码在受控环境中执行,结果返回后再由大模型归纳总结。用户可以继续追问“那个图里方法A比方法B高多少”,系统会继续检索论文上下文并调用工具。

7. 运行与效果验证

按顺序运行脚本即可体验完整流程。

首先解析论文:

python parse_paper.py

解析完成后检查data_tables/目录下是否生成了CSV文件,同时打开paper_text.txt确认文本没有乱码。

然后构建向量索引:

python build_index.py

最后启动Agent:

python run_agent.py

正常情况下,你会在终端看到类似这样的过程:

  1. 模型先请求调用execute_python_code,参数里包含一行读取CSV和绘图的代码;
  2. 工具返回执行结果,例如{"status": "success", "stdout": "...", "images": ["outputs/f1_bar.png"]};
  3. 模型根据工具结果生成最终回答,说明图表已保存,并简单解读柱状图中的关键对比。

判断成功的标准有三个:问题能得到基于论文上下文的回答;数据分析结果有实际输出文件;回答中的数字能够和论文原始表格对应上。如果模型生成的结果和论文表格不一致,多半是CSV解析出错或代码读取了错误的列,需要检查data_tables目录下的数据是否准确。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
PDF解析后文本乱码PDF是扫描版,没有文本层用OCR工具识别,或从数据库获取原始论文文本接入OCR管线,人工校对关键段落
表格提取为空或内容错位pdfplumber对复杂合并单元格支持有限打开PDF对应页,检查表格结构提取后人工修正CSV,或手动录入数据
向量索引回答偏差大文本切块过大或过小,检索召回不准确打印检索到的chunk内容调整splitter参数,增加召回数量,或加入摘要索引
OpenAI API报错API Key无效、网络不通、余额不足检查.env配置和请求返回的错误码更换接口地址或重新配置密钥
工具执行报NameError代码中使用了未导入的库或变量查看execute_python_code返回的error信息在System Prompt中补充可用库列表,或扩充global_env
模型不调用工具,直接给泛泛回答模型版本不支持函数调用,或工具描述不清晰检查模型参数和tool schema使用支持函数调用的模型,完善工具描述
生成代码包含危险操作Prompt注入或恶意论文指令检查代码内容与执行环境隔离在容器中运行,限制文件系统访问,禁用网络

9. 最佳实践与工程建议

把这套原型放到真实生产环境之前,有几个问题值得提前思考。

第一是数据可信度。论文PDF解析出的表格往往有误差,尤其是跨页表格、合并单元格、小数点被截断的情况。一定要在构建索引前建立数据审计流程,至少核对每张表的行数和关键统计量。宁可让助手的数据来源是人工整理的CSV,也不能用来源不明的脏数据。

第二是代码执行安全。exec是一把双刃剑。如果论文文本中存在恶意指令(例如要求代码读取本地文件或联网),模型可能在生成代码时被误导。生产环境中应该把代码执行放在Docker容器、云函数或专门的沙箱服务中,并限制网络、文件系统访问权限。即使是本机调试,也要确保运行环境中没有敏感数据。

第三是函数调用设计。工具的名字和描述一定要具体,不要只写“执行代码”。要把可用的数据表路径、列名、输出目录规则都写进System Prompt,否则模型会反复生成错误的文件读取路径。

第四是评估与回归。论文AI助手和普通聊天机器人一样,需要一套评估集。准备10到20个固定问题,覆盖事实问答、数据提取、统计计算、可视化四类,每次改动解析逻辑或者Prompt后都跑一遍,防止“修好一个功能,搞坏另一个分析”。

第五是版本锁定。langchain、openai、pdfplumber这些库的接口更新很快,项目上线时建议把依赖版本锁定在requirements.txt中,否则半年后代码可能完全跑不起来。

10. 总结与后续学习方向

从“能答问题”到“能跑分析”,这套论文AI助手的核心改动其实只有两步:把论文中的表格独立成结构化数据,再为模型增加一个代码执行工具。如果你已经跑通过上面的流程,下一步可以尝试:

  • 把向量库从FAISS换成Chroma或Elasticsearch,支持更大规模的论文库;
  • 加入多篇论文对比功能,让助手可以横向比较不同论文的实验数据;
  • 将代码执行环境切换到Docker,增加安全边界;
  • 用LangSmith或自建日志来评估提示词和工具调用效果;
  • 把助手封装成FastAPI服务,供Web前端或Jupyter Notebook调用。

真正值得投入的是数据解析和数据清洗部分。大模型能力的下限被工具决定,但工具效果的上限被数据质量决定。一篇论文的PDF解析得好不好,直接决定了这个AI助手是能解决科研问题,还是只会在Token里消耗你的耐心。建议收藏这篇文章,自己找一篇有数据表的论文试一次。只有跑通从PDF到图表的完整链路,你才会真正体会到“论文AI助手”和“论文聊天机器人”之间的差别。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询