
做科研的人最近应该都刷到过类似的讨论一边是「AI 已经能顶一个博士生」的判断另一边是实验室里真正跑过 Agent 的人说「它连我的实验记录都读不明白」。这两种说法其实都对只是说的不是同一件事。前者衡量的是模型在标准题目上的单点表现后者衡量的是能不能把一条完整的研究链路跑通、跑稳、跑得可复现。本文不打算停在观点层面而是从工程视角把「科研 Agent」拆开它的核心机制是什么、工具层怎么写、主循环怎么控、结果怎么评测、哪里最容易翻车。读者对象是已经会写 Python、想在自己的研究或业务里落地 Agent 的开发者也适合做实验管理、数据分析、文献调研的同学参考。跟着走完你能得到一个可运行的最小科研助手包含文献检索、数据统计、引用溯源和离线评测四个模块代码可以直接复制改造。1. 从「AI 能不能替代博士生」说到可落地的科研 Agent1.1 争论背后的真实分歧把这场讨论摊开看分歧点其实非常具体。支持方的证据通常是模型能在某些基准测试上拿到不错的分数比如代码题、数学推导、文献选择题反对方的证据则是模型会在参考文献上编造作者和期刊、会在多步实验里丢掉中间约束、会把相关性当成因果性。这两类证据并不矛盾因为它们测的根本不是同一个东西。基准测试测的是「给定完整上下文、单轮输出、有标准答案」的任务而科研的真实流程是「问题模糊、上下文散落在几十篇论文和几个 G 的实验数据里、没有标准答案、需要反复试错」。前者是一个信息检索加推理的问题后者是一个长程规划加状态管理的问题。用一个单轮任务的表现去推断长程任务的能力本质上和用一次函数调用能不能跑通来推断整个微服务能不能上线是一样的逻辑错误。所以工程上更靠谱的问法不是「它比博士生强吗」而是「在我的这条具体链路上它能稳定完成哪几步剩下的几步需要人补」。这个问法可以直接转成技术需求把链路拆成可枚举的步骤每步都定义输入输出和校验方式然后逐段压测。1.2 Agent 与「一次问答」的本质区别很多人第一次用大模型做科研辅助方式是打开对话框把问题一股脑丢进去。这种做法在信息量小的时候体验很好一旦涉及「先查文献、再筛选、再读全文、再抽数据、再算统计」这种多阶段任务就会迅速失控。原因很简单单次问答的上下文窗口是有限的而研究任务需要的中间状态会不断累积模型在上下文变长之后对早期约束的注意力会明显衰减。Agent 的价值就在于把这些中间状态外置。它不再要求模型一次性把所有东西都记住而是把「查到的文献」「解析出的段落」「算出来的统计量」都变成可以反复读取的外部结果模型每一步只需要关心「当前状态是什么、下一步该干什么」。这就把一个大而脆弱的推理拆成了若干个小的、可检查的推理。换句话说Agent 不是让模型变聪明了而是让模型的输出变成了可以被程序校验的对象。这个转变对科研场景特别关键因为科研最不能接受的就是「看起来对」。1.3 科研场景下 Agent 的能力边界把边界说清楚后面写代码才不会跑偏。根据实际落地经验可以把科研链路里的任务按可靠性分成三档。可靠性档位典型任务建议用法高文献关键词检索、摘要抽取、格式转换、数据清洗、指标统计、图表生成可以交给 Agent 自动执行人只看结果中文献综述初稿、实验方案草稿、代码骨架生成、异常结果归因Agent 出草稿人做实质修改和判断低新颖性判断、实验结论定性、审稿意见、跨领域因果推断Agent 只做信息整理结论必须人来下这个分档不是贬低模型能力而是提醒你把「生成」和「判断」分开。生成可以并行、可以回滚、可以重跑判断一旦错了后面所有工作都要推翻。工程上的做法是让 Agent 尽可能多产草稿和中间产物把最终判断留一个明确的人工确认点。2. Agent 做科研的核心概念拆解2.1 规划、执行、观察的循环结构几乎所有能跑通多步任务的 Agent骨架都是同一个循环模型先根据当前上下文决定下一步动作程序执行这个动作拿到结果把结果塞回上下文再进入下一轮。这个结构常被叫作 ReAct字面意思就是推理加行动交替进行。用 ASCII 图表示就是下面这样用户任务 | v [模型推理] -- 输出 JSON 指令 -- [程序解析] | | | v | [执行工具函数] | | | v --------- 观察结果回填 ---- 工具返回值 | v [模型判定信息足够] -- 输出最终答案这个循环里真正决定成败的往往不是模型本身而是两件事一是模型输出的指令能不能被程序稳定解析二是工具返回的内容够不够模型做下一步决策。前者靠约束输出格式解决后者靠工具设计解决。很多「Agent 效果不好」的案例追根到底都是工具返回了一坨模型看不懂的东西。2.2 工具签名的设计原则工具就是 Agent 的手。手好不好用直接决定它能干成什么事。设计工具签名时有几条经验值得记住。第一工具粒度要适中。太细模型需要很多步才能完成一件事步数一多出错概率就上去了太粗工具内部逻辑复杂出问题时模型无法定位。一般建议一个工具对应一个明确的动作参数控制在三个以内。第二工具描述要写成「什么时候用它」而不是「它是什么」。模型在选工具时依赖描述文本如果描述只写「检索文献」模型在需要统计数据时也可能误选。写成「根据关键词在本地文献库中检索标题和摘要返回匹配度最高的若干条用于查找相关工作」就会准确得多。第三工具必须自己处理异常并把错误变成可读文本。工具抛异常直接打崩 Agent 循环是最常见的翻车方式正确做法是捕获后返回一段模型能理解的话比如「文件不存在请检查路径」让模型有机会换个参数重试。2.3 检索增强与引用溯源科研场景对引用的要求远高于普通问答。模型自己「回忆」出来的文献标题、作者、年份可信度非常低这一点在大量实测中反复被验证。解决办法不是反复提示「不要编造」而是从架构上让模型没有机会编造所有文献信息都必须从检索工具返回模型只能引用工具给出的 doc_id。这就是检索增强生成的核心思路。落到代码上就是两步先检索把命中的片段连同标识符一起塞进上下文再要求模型在最终答案里用标识符标注出处。后一步是关键如果没有强制标注模型会倾向于把检索到的内容和自己的先验知识混在一起写你就再也分不清哪句有依据、哪句是幻觉。2.4 结果自检与不确定性表达一个成熟的科研 Agent 应该会「说自己不知道」。实现方式有两种一种是在提示词里明确要求「证据不足时直接说明不足」另一种是在程序层面做校验比如最终答案里如果没有出现任何 doc_id就把这次输出标记为「无引用」在展示层给出警示。后一种更可靠因为它不依赖模型自觉。工程上建议把这类校验做成硬性的后置检查只要不满足就降级展示或者打回重跑。这比在提示词里加十句「请务必严谨」有效得多。3. 环境准备与项目结构3.1 运行环境与依赖说明本文示例使用 Python 3.10 及以上版本主要考虑是类型标注和新语法用起来更顺手。依赖库尽量选常见且稳定的调用大模型用官方 SDK数据处理用 pandasPDF 解析用 pypdf命令行交互直接用标准库不引入额外框架。需要提前说明的是本文调用的是 OpenAI 兼容的 chat completions 接口。市面上多数模型服务都提供兼容层但具体模型名称、接口地址、参数支持情况各家不同请以你实际使用的服务文档为准。示例里的默认值都写成了占位符不要直接拿去跑。# 文件路径requirements.txt openai1.30.0 pandas2.0.0 pypdf4.0.0安装命令如下# 创建虚拟环境可选但推荐 python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate # 安装依赖 pip install -r requirements.txt如果你的环境里 pypdf 版本较低可能需要换用 PyPDF2两者的 API 有差异导入部分建议写成兼容形式后文示例会给出。3.2 目录结构设计一个容易维护的 Agent 项目目录划分要比普通脚本严格一些因为工具、配置、提示词、数据、评测这几块的修改节奏完全不同。示例结构如下research_agent/ ├── config.py # 配置与密钥读取 ├── agent/ │ ├── __init__.py │ └── core.py # Agent 主循环 ├── tools/ │ ├── __init__.py │ ├── base.py # 工具注册表 │ ├── literature.py # 文献检索工具 │ ├── analysis.py # 数据统计工具 │ └── pdf_parser.py # PDF 解析工具 ├── data/ │ ├── literature.json # 演示用文献库 │ └── experiments.csv # 演示用实验数据 ├── eval/ │ ├── cases.json # 评测用例 │ └── evaluate.py # 离线评测脚本 ├── workspace/ # Agent 产物输出目录 └── run_demo.py # 入口脚本把workspace单独拎出来是个小习惯但很有用。Agent 产生的中间文件、图表、日志都往这里写清理和备份都很方便也不会污染源码目录。3.3 配置管理与密钥安全密钥绝对不能写进代码。示例统一从环境变量读取同时在配置类里给出默认值和注释方便排查。# 文件路径research_agent/config.py import os from dataclasses import dataclass, field dataclass class AgentConfig: Agent 运行配置所有敏感信息从环境变量读取。 # 模型服务地址请替换为你实际使用的兼容接口地址 base_url: str field(default_factorylambda: os.getenv(LLM_BASE_URL, http://localhost:8000/v1)) # 模型服务密钥 api_key: str field(default_factorylambda: os.getenv(LLM_API_KEY, )) # 模型名称各家命名不同务必按文档填写 model: str field(default_factorylambda: os.getenv(LLM_MODEL, your-model-name)) # 单次任务最大循环步数防止死循环 max_steps: int 8 # 采样温度科研场景建议偏低 temperature: float 0.2 # 单次请求超时时间单位秒 timeout: int 60 # 产物输出目录 workdir: str field(default_factorylambda: os.getenv(AGENT_WORKDIR, ./workspace)) def validate(self) - None: if not self.api_key: raise ValueError(缺少 LLM_API_KEY请通过环境变量配置) if self.model your-model-name: raise ValueError(请通过 LLM_MODEL 指定实际模型名称)设置环境变量时Linux 和 macOS 用exportWindows 用set不要把这些命令提交到公开仓库里。export LLM_BASE_URLhttp://localhost:8000/v1 export LLM_API_KEYsk-你的密钥 export LLM_MODELyour-model-name这里validate()的作用不只是友好提示更重要的是让配置错误在启动阶段就暴露。Agent 类应用如果配置有问题往往表现为模型胡言乱语排查成本很高提前校验能省掉大量时间。4. 完整实战从零搭一个科研文献助手 Agent接下来把这套东西真正跑起来。我们要做的 Agent 接收一句自然语言任务能自己决定先查文献还是先算数据最后给出一份带出处的答复。4.1 工具注册表与统一错误处理先写工具注册表。它的职责是保存工具描述供模型选择并在调用时兜住异常。# 文件路径research_agent/tools/base.py from typing import Any, Callable, Dict class ToolRegistry: 工具注册表负责工具描述生成与安全调用。 def __init__(self) - None: self._tools: Dict[str, Dict[str, Any]] {} def register(self, name: str, description: str, func: Callable[..., Any]) - ToolRegistry: if not name or not callable(func): raise ValueError(工具名不能为空且必须可调用) self._tools[name] {description: description, func: func} return self def spec(self) - str: 生成给模型看的工具清单一段一行便于模型逐条对照。 return \n.join(f- {name}: {item[description]} for name, item in self._tools.items()) def call(self, name: str, **kwargs: Any) - str: 统一调用入口任何异常都转成文本返回避免打断主循环。 if name not in self._tools: return fERROR: 工具 {name} 不存在可用工具为{, .join(self._tools.keys())} try: result self._tools[name][func](**kwargs) return str(result) except TypeError as exc: return fERROR: 工具 {name} 参数不匹配请检查参数名详情{exc} except Exception as exc: # noqa: BLE001 return fERROR: 工具 {name} 执行失败详情{exc}这里对TypeError单独捕获是刻意的。模型最常见的错误之一就是参数名写错或者漏参数如果和其他异常混在一起返回的文本里就没有足够信息提示模型改参数。单独区分之后模型看到「参数不匹配」往往能自己修正。4.2 文献检索工具实现演示用的文献库放在本地 JSON 里每条记录包含doc_id、title、abstract、year。真实项目里可以换成搜索引擎接口或本地向量库但工具签名保持一致Agent 逻辑完全不用改。[ { doc_id: demo-001, title: 基于图神经网络的分子性质预测方法研究, abstract: 本文研究图神经网络在分子图表示学习中的应用提出一种面向分子性质预测的消息传递改进方案并在多个公开数据集上进行了对比实验。, year: 2022 }, { doc_id: demo-002, title: 小样本学习在材料发现中的应用综述, abstract: 本文系统梳理小样本学习在材料发现任务中的方法体系讨论数据稀缺条件下模型迁移与不确定性估计的若干问题。, year: 2023 }, { doc_id: demo-003, title: 科研工作流的可复现性度量框架, abstract: 本文提出一套面向计算类科研工作流的可复现性度量框架从环境、数据、代码三个维度定义可复现等级。, year: 2021 }, { doc_id: demo-004, title: 大语言模型辅助文献综述的方法与风险, abstract: 本文讨论大语言模型在文献综述环节的使用方法与潜在风险指出引用核验是当前最主要的薄弱点。, year: 2024 }, { doc_id: demo-005, title: 自动化实验平台的任务调度优化, abstract: 本文针对自动化实验平台的多任务并发场景提出一种基于优先级的调度策略降低实验排队时延。, year: 2023 } ]需要说明上面这些条目是为了演示程序流程构造的示例数据不是真实论文。如果你的项目要接入真实文献务必走正规的文献数据库接口并遵守对应的使用条款。检索本身用一个轻量的打分函数实现先做分词再算一个简化的相似度。真实项目可以换 BM25 或者向量检索接口不变。# 文件路径research_agent/tools/literature.py import json import re from pathlib import Path from typing import Any, Dict, List, Optional class LiteratureStore: 本地文献库提供关键词检索与按 ID 读取。 def __init__(self, path: str) - None: self.path Path(path) if not self.path.exists(): raise FileNotFoundError(f文献库文件不存在{self.path}) self.docs: List[Dict[str, Any]] json.loads(self.path.read_text(encodingutf-8)) staticmethod def _tokens(text: str) - List[str]: # 英文按词切中文按单字切够用即可 return re.findall(r[a-zA-Z]|[\u4e00-\u9fa5], text.lower()) def search(self, query: str, top_k: int 3) - List[Dict[str, Any]]: tokens_q self._tokens(query) if not tokens_q: return [] set_q set(tokens_q) scored: List[tuple] [] for doc in self.docs: body f{doc.get(title, )} {doc.get(abstract, )} set_d set(self._tokens(body)) if not set_d: continue # 余弦形式的重合度长度归一化避免长摘要压制短摘要 overlap len(set_q set_d) / ((len(set_q) * len(set_d)) ** 0.5) if overlap 0: scored.append((overlap, doc)) scored.sort(keylambda item: item[0], reverseTrue) return [ { doc_id: doc[doc_id], title: doc[title], year: doc.get(year), score: round(score, 4), abstract: doc.get(abstract, )[:120], } for score, doc in scored[:top_k] ] def get(self, doc_id: str) - Optional[Dict[str, Any]]: for doc in self.docs: if doc[doc_id] doc_id: return doc return Nonesearch返回的字段是刻意精简过的。摘要只截前 120 个字符因为完整摘要塞进上下文会快速吃掉预算而且模型在这一步通常只需要判断「这篇相不相关」不需要读全文。真正需要全文时再单独调用按 ID 读取的工具这种两步走的设计能显著降低整体成本。4.3 数据统计工具实现科研 Agent 另一半价值在数据处理。这里给一个基于 pandas 的聚合工具接收 CSV 路径、分组列和指标列返回 markdown 表格。让工具直接产出 markdown 有个好处模型不需要再把结构化数据转成自然语言直接把表格贴进答案里就行既准确又省 token。# 文件路径research_agent/tools/analysis.py from pathlib import Path from typing import Any, Dict import pandas as pd def aggregate_metrics(csv_path: str, group_by: str, metric: str) - str: 按分组列聚合指标返回 markdown 表格字符串。 path Path(csv_path) if not path.exists(): return fERROR: 数据文件不存在{csv_path} try: df pd.read_csv(path) except Exception as exc: # noqa: BLE001 return fERROR: 读取 CSV 失败可能是编码或分隔符问题详情{exc} for col in (group_by, metric): if col not in df.columns: return fERROR: 列 {col} 不存在现有列为{, .join(df.columns)} if not pd.api.types.is_numeric_dtype(df[metric]): return fERROR: 列 {metric} 不是数值类型无法计算均值 summary ( df.groupby(group_by)[metric] .agg([count, mean, std, min, max]) .round(4) .reset_index() ) header | | .join(summary.columns) | divider | | .join([---] * len(summary.columns)) | rows [ | | .join(str(v) for v in row) | for row in summary.itertuples(indexFalse) ] return \n.join([header, divider, *rows]) f\n\n数据来源{csv_path}这里几处校验都对应真实踩过的坑。列名写错是最高频的问题所以错误信息里直接把现有列名全部列出来模型下一轮基本就能改对。类型检查则是为了防止把字符串列当数值算pandas 对这种情况有时不报错而是给出奇怪结果主动拦下来更安全。配套的演示数据如下# 文件路径research_agent/data/experiments.csv method,dataset,accuracy,f1 GNN,dataset_a,0.812,0.798 GNN,dataset_b,0.785,0.771 MLP,dataset_a,0.744,0.731 MLP,dataset_b,0.729,0.715 RF,dataset_a,0.761,0.752 RF,dataset_b,0.748,0.736注意 CSV 文件里不要真的写第一行注释上面只是为了标明路径。实际文件第一行必须是表头。4.4 Agent 主循环实现核心循环来了。这里选择让模型输出 JSON 指令而不是依赖各家 SDK 的 function calling 结构原因是前者通用性更好换服务商只需要改地址和模型名解析逻辑不用动。# 文件路径research_agent/agent/core.py import json import re from typing import Any, Dict, List, Optional from openai import OpenAI from research_agent.config import AgentConfig from research_agent.tools.base import ToolRegistry SYSTEM_PROMPT 你是一个严谨的科研助手通过调用工具来完成用户的研究任务。 可用工具 {tool_spec} 输出要求每次只能输出一个 JSON 对象不要输出 JSON 之外的任何文字。 需要调用工具时输出 {thought: 你的推理过程, action: 工具名, action_input: {参数名: 参数值}} 信息足够时输出 {thought: 你的推理过程, final_answer: 最终回答} 必须遵守的规则 1. 工具名必须与清单完全一致参数名必须与工具说明中的一致。 2. 工具返回 ERROR 时分析原因后换一种参数或换一个工具不要用同样的参数重复调用。 3. 最终回答中的每一条事实性结论都要标注出处格式为 [doc_id: xxx] 或 [data: 文件名]。 4. 证据不足时直接说明不足禁止补充未经工具返回的信息。 class ResearchAgent: 最小可用的科研助手 Agent。 def __init__(self, config: AgentConfig, registry: ToolRegistry, verbose: bool True) - None: config.validate() self.config config self.registry registry self.verbose verbose self.client OpenAI( base_urlconfig.base_url, api_keyconfig.api_key, timeoutconfig.timeout, ) self.trace: List[Dict[str, Any]] [] staticmethod def _extract_json(text: str) - Dict[str, Any]: 从模型输出中提取第一个完整 JSON 对象。 match re.search(r\{.*\}, text, re.S) if not match: raise ValueError(未找到 JSON 对象) return json.loads(match.group(0)) def run(self, task: str, max_steps: Optional[int] None) - Dict[str, Any]: limit max_steps or self.config.max_steps system_prompt SYSTEM_PROMPT.replace({tool_spec}, self.registry.spec()) messages: List[Dict[str, str]] [ {role: system, content: system_prompt}, {role: user, content: task}, ] for step in range(1, limit 1): response self.client.chat.completions.create( modelself.config.model, messagesmessages, temperatureself.config.temperature, ) content response.choices[0].message.content or messages.append({role: assistant, content: content}) if self.verbose: print(f\n 第 {step} 步 ) print(content.strip()[:500]) try: payload self._extract_json(content) except (ValueError, json.JSONDecodeError) as exc: # 格式错误不直接失败回一句话让模型自己修 messages.append( {role: user, content: f输出格式错误{exc}请只输出一个合法 JSON 对象。} ) continue if final_answer in payload: answer str(payload[final_answer]) self.trace.append({step: step, type: final, content: answer}) return { success: True, answer: answer, steps: step, cited: bool(re.search(r\[(doc_id|data):, answer)), trace: self.trace, } action payload.get(action) action_input payload.get(action_input) or {} if not isinstance(action_input, dict): messages.append({role: user, content: action_input 必须是 JSON 对象。}) continue observation self.registry.call(action, **action_input) self.trace.append( {step: step, type: tool, action: action, input: action_input, output: observation[:800]} ) messages.append( {role: user, content: f工具 {action} 的执行结果\n{observation}} ) return { success: False, answer: 达到最大步数仍未得出结论请检查任务描述或工具能力。, steps: limit, cited: False, trace: self.trace, }几处设计值得展开说。第一格式错误不视为致命失败而是回灌一条提示让模型自己修这能把大部分偶发的格式问题就地消化掉。第二cited字段是硬性校验不管模型自述多么严谨只要最终答案里没有出现引用标记就记录为无引用展示层可以据此加警示。第三trace完整记录了每一步的工具调用和返回这是后续排查和评测的基础比事后再去翻日志可靠得多。一个需要提前说明的兼容性问题部分模型服务对消息角色的交替有严格要求连续两条 user 消息可能被拒绝。如果遇到这类报错可以在工具结果前包一层 assistant 的简短说明或者改用该服务提供的工具调用字段。这一点各家差异较大请按实际文档调整。4.5 组装入口与运行验证最后把工具注册和入口串起来。# 文件路径research_agent/run_demo.py import json from pathlib import Path from research_agent.agent.core import ResearchAgent from research_agent.config import AgentConfig from research_agent.tools.analysis import aggregate_metrics from research_agent.tools.base import ToolRegistry from research_agent.tools.literature import LiteratureStore def build_registry() - ToolRegistry: store LiteratureStore(./research_agent/data/literature.json) registry ToolRegistry() registry.register( search_literature, 根据关键词在本地文献库中检索标题和摘要返回匹配度最高的若干条。参数query检索词top_k返回条数默认 3。用于查找相关工作。, lambda query, top_k3: json.dumps(store.search(query, int(top_k)), ensure_asciiFalse), ) registry.register( read_literature, 按文献编号读取完整信息包括标题、年份和完整摘要。参数doc_id文献编号如 demo-001。用于需要细节时补充阅读。, lambda doc_id: json.dumps(store.get(doc_id) or {error: 未找到该文献}, ensure_asciiFalse), ) registry.register( aggregate_metrics, 对实验 CSV 按某列分组统计指标返回 markdown 表格。参数csv_path文件路径group_by分组列名metric数值指标列名。用于汇总实验数据。, aggregate_metrics, ) return registry def main() - None: config AgentConfig() Path(config.workdir).mkdir(parentsTrue, exist_okTrue) agent ResearchAgent(config, build_registry()) task ( 请查一下和图神经网络做分子性质预测相关的工作 并统计 experiments.csv 中不同方法在 accuracy 上的均值 最后给出一段简短结论每个结论都要标注来源。 ) result agent.run(task) print(\n 最终结果 ) print(result[answer]) print(f\n步数{result[steps]}是否包含引用标记{result[cited]}) log_path Path(config.workdir) / trace_last.json log_path.write_text(json.dumps(result[trace], ensure_asciiFalse, indent2), encodingutf-8) print(f调用链已保存至 {log_path}) if __name__ __main__: main()运行方式python -m research_agent.run_demo预期输出大致是两段内容一段是检索到的文献列表含demo-001这类编号一段是按方法分组的 accuracy 统计表最后是带[doc_id: demo-001]、[data: experiments.csv]标记的结论。如果你的模型第一次就给出了没有引用标记的答案cited会是False这时不要急着怀疑模型不行先检查提示词里的规则有没有被正确替换进 system 内容里。trace_last.json这个产物很值得养成习惯。每次出问题先看这个文件里第几步开始跑偏是工具返回不对还是模型选错了工具一眼就能看出来比对着终端输出翻要快得多。5. 科研 Agent 的评测怎么判断它行不行5.1 评测集怎么设计回到文章开头的争论。要回答「行不行」唯一的办法是自己建评测集。评测集不需要很大二十到五十条就足够暴露主要问题关键是覆盖典型链路。建议按三个维度设计用例检索类能不能找到正确文献、计算类能不能算对统计量、综合类多工具串联。每条用例包含问题、期望命中的 doc_id、期望调用的工具名、是否要求引用。[ { id: case-01, question: 有哪些工作把图神经网络用于分子性质预测, expect_doc_ids: [demo-001], expect_tools: [search_literature], require_citation: true }, { id: case-02, question: 统计 experiments.csv 中各方法 accuracy 的均值, expect_doc_ids: [], expect_tools: [aggregate_metrics], require_citation: true }, { id: case-03, question: 文献里提到的可复现性度量框架从哪几个维度定义, expect_doc_ids: [demo-003], expect_tools: [search_literature, read_literature], require_citation: true } ]5.2 离线评测脚本评测脚本的逻辑很直白逐条跑 Agent检查最终答案里有没有出现期望的 doc_id调用的工具里有没有覆盖期望工具以及是否正确带了引用。# 文件路径research_agent/eval/evaluate.py import json from pathlib import Path from research_agent.agent.core import ResearchAgent from research_agent.config import AgentConfig from research_agent.run_demo import build_registry def run_eval(cases_path: str) - None: cases json.loads(Path(cases_path).read_text(encodingutf-8)) agent ResearchAgent(AgentConfig(), build_registry(), verboseFalse) total len(cases) hit_doc hit_tool hit_cite 0 for case in cases: result agent.run(case[question]) answer result[answer] used_tools {item[action] for item in result[trace] if item[type] tool} doc_ok all(doc_id in answer for doc_id in case[expect_doc_ids]) if case[expect_doc_ids] else True tool_ok all(name in used_tools for name in case[expect_tools]) cite_ok bool(result[cited]) if case[require_citation] else True hit_doc int(doc_ok) hit_tool int(tool_ok) hit_cite int(cite_ok) print( f[{case[id]}] doc{doc_ok} tool{tool_ok} cite{cite_ok} fsteps{result[steps]} success{result[success]} ) print(\n 汇总 ) print(f用例总数{total}) print(f文献命中率{hit_doc / total:.2%}) print(f工具覆盖率{hit_tool / total:.2%}) print(f引用合规率{hit_cite / total:.2%}) if __name__ __main__: run_eval(./research_agent/eval/cases.json)运行python -m research_agent.eval.evaluate这套指标不追求绝对准确它的价值在于可比较。你换了模型、改了提示词、调了工具描述跑一遍就知道是变好还是变差。没有这个基线所有关于「效果提升」的判断都是感觉而感觉在工程上几乎不可靠。5.3 人工复核与盲评自动指标只能覆盖「有没有」覆盖不了「对不对」。比如 Agent 找到了正确的文献但把它得出的结论说反了自动评测是发现不了的。所以建议在自动评测之后对通过用例做一轮人工抽样复核重点看三件事结论是否被证据支持、有没有偷换概念、有没有隐藏的不确定性。条件允许的话可以做个简化盲评把 Agent 输出的综述初稿和人工写的初稿混在一起让同组同学判断哪份是机器写的、哪份更可用。这种测试对判断「能不能替代某个环节」比跑基准分数直观得多也更贴近真实需求。6. 常见问题与排查思路实际跑起来之后问题大多集中在几类。下面这个表格可以先快速定位后面几条是频率最高的几个展开讲。问题现象常见原因解决思路启动报缺少密钥环境变量未加载或拼写错误打印os.environ确认变量名完全一致提示模型名称非法未设置LLM_MODEL或使用了服务不支持的名称按服务商文档填入真实模型名模型输出不是 JSON提示词约束不足或温度过高降低 temperature强化输出格式要求反复调用同一个工具工具返回信息量不足模型无法判断进度在返回值里加上「已检索关键词」等状态信息最终答案没有引用规则未注入或被模型忽略检查{tool_spec}替换是否成功配合cited字段做后置校验达到最大步数未结束任务描述含糊或工具能力不足细化任务描述或增加专用工具减少步数统计结果明显错误列名写错但被自动容错关闭 pandas 的隐式类型推断显式做类型校验长任务超时单步工具耗时过长给工具加超时把大任务拆成子任务关于「反复调用同一个工具」这一条展开说一下。这是最典型的 Agent 卡死形态模型检索了一次没找到想要的换个说法再检索还是没找到然后一直循环。根本原因是检索工具返回的是空列表或者只有标题模型无法从结果里判断「是关键词不对」还是「库里确实没有」。解决办法是在工具返回里主动带上诊断信息比如「共匹配 0 条建议尝试更宽泛的关键词或同义词」。这一句话往往就能把模型从死循环里拉出来。再看「输出不是 JSON」。除了温度因素还有一种隐蔽情况是模型在 JSON 前后加了说明文字。主循环里用正则提取第一个花括号块能解决大部分情况但如果模型输出里先出现了一个不完整的花括号正则就会截错。更稳的写法是用json.JSONDecoder().raw_decode从每个{位置尝试解析解析成功即返回。这个改动不大但能把格式失败的比率明显压下来。最后是超时。科研类任务经常涉及解析几十页 PDF 或者跑一次较重的统计单个工具调用可能就要几十秒。如果没有超时控制一个卡住的工具会让整个任务悬停在那里。建议在工具内部做时间限制超时后返回「该操作超时建议缩小数据范围」让 Agent 有机会换策略。7. 最佳实践与工程建议7.1 可复现性从第一天就要抓科研场景里可复现性的重要性高于一切。落到 Agent 项目上至少要做到三点所有工具调用都以结构化形式落盘到 trace 文件每次运行记录模型名称、温度、提示词版本数据文件用相对路径并随项目一起版本管理。第三点容易被忽视。很多人习惯写绝对路径本地跑得好好的换台机器就全崩。统一用相对项目根目录的路径配合一个ROOT Path(__file__).resolve().parents[1]做基准能省掉大量环境问题。# 文件路径research_agent/paths.py from pathlib import Path ROOT Path(__file__).resolve().parent def data_path(*parts: str) - str: return str(ROOT / data / Path(*parts)) def workspace_path(*parts: str) - Path: target ROOT.parent / workspace target.mkdir(parentsTrue, exist_okTrue) return target.joinpath(*parts)7.2 幻觉控制的工程手段提示词里写「不要编造」是必要的但远远不够。真正有效的手段是架构级的约束可以归纳成三层。第一层是信息隔离。凡是涉及事实的字段例如文献标题、作者、年份、数值指标一律只允许来自工具返回模型在最终答案里只能引用标识符不能重新组织这些字段。这一层靠工具返回的结构化格式保证。第二层是输出校验。最终答案里必须出现[doc_id: xxx]或[data: 文件名]形式的标记没有标记就判定为不合规。这一层靠正则检查不依赖模型自觉。第三层是交叉核对。对于引用了具体数值的结论可以用程序把答案里的数字和工具返回的数字做比对不一致就报警。这一层实现成本稍高但对定量结论非常值得做。三层叠加之后幻觉不会消失但会从「悄悄混进结论」变成「触发告警被人看到」这个差别在科研场景里是决定性的。7.3 成本与延迟的权衡多步 Agent 的成本是单次问答的数倍因为它每一轮都要把完整的历史上下文重新送进模型。控制成本有几条常规手段工具返回内容做长度截断摘要类字段控制在几百字符以内中间结果用结构化数据而不是自然语言描述把可以并行的事情合并成一次工具调用例如一次检索返回多条而不是分多次查。延迟方面最大的瓶颈通常是模型推理本身。如果任务允许可以把「规划」和「执行」分离用能力更强的模型做规划、用更便宜的模型做格式化输出这样能在效果和成本之间取得不错的平衡。7.4 安全与合规边界这一点必须单独强调。Agent 具备执行能力就必须假设它会被误用。落到具体做法上有几条硬性要求所有文件操作限定在workspace目录内禁止工具接收绝对路径涉及删除、覆盖的操作必须二次确认或者在实现上直接禁止调用外部接口时做好频率限制和重试退避避免对第三方造成压力。数据方面实验数据、未发表结果、审稿材料都属于敏感内容接入任何外部模型服务前都要确认清楚数据流向和使用条款。如果条件不允许就老老实实在本地跑宁可效果差一点。另外涉及文献数据库、期刊平台的使用必须遵守对方的授权范围不要用自动化手段绕过访问限制。这一点不是技术问题是底线问题。8. 学习路线与后续扩展方向回到最初那个问题。跑完这套东西之后你大概会有自己的判断Agent 在检索、抽取、统计、格式化这些环节上确实比人快很多也确实能承担相当一部分重复劳动但在判断什么值得研究、结论能不能站住脚、哪个方向有前景这些事情上它目前给不出可靠的答案。所以更准确的表述不是「远超博士生」而是「在链路的某些环节上效率远超人工在另一些环节上还差得很远」。如果你想把这套东西继续做深有几个方向值得投入。一是把工具层做实接入真实文献检索接口、接入向量数据库做语义检索、接入实验管理系统的 API工具越扎实Agent 的上限越高。二是把评测做细用例覆盖到你自己领域的具体任务指标从「命中率」细化到「结论正确率」。三是把流程做稳加上重试、断点、并发和监控让它在无人值守的情况下也能跑完一批任务。代码层面还有两个小练习可以马上动手给 Agent 加一个write_report工具把检索结果和统计表格拼成一份 markdown 报告写到workspace给主循环加一个「相同工具相同参数不重复执行」的短路判断防止死循环。这两个改动都不大但能让整套流程从「能演示」变成「能日常用」。最后提醒一句无论 Agent 输出多顺眼凡是准备写进论文、写进报告、写进决策的内容都要回到原始数据和原始文献核对一遍。工具能帮你省掉搬运的时间但省不掉确认的责任。