简介:这份资料是清华大学团队出品的《DeepSeek+DeepResearch:让科研像聊天一样简单》PDF,面向高校研究生、科研人员及需借助大模型处理数据的研究者,核心是用多agent思路把数据采集、清洗、集成、变换、特征工程直至分析可视化串成一条低门槛流程。全文围绕“能做什么、怎么做、效果如何”展开,先讲爬虫、数据库读取、API调用等采集方式,再落到诊断、预测、关联与聚类分析,并给出客户细分、信用评分、股价预测等落地场景。资料还用统一提示词对比DeepSeek R1、OpenAI o3 mini、Claude 3.5 Sonnet与Kimi k1.5在网址筛选去重、爬虫脚本撰写、文件数据读取等任务上的实际表现,指出各自完整性与稳定性差异。压缩包为1个PDF文件,约5.43MB,已有211人学习,可帮助读者建立从数据到图表的完整认知并为模型选型提供参照。
1. 结论先行:让科研“像聊天一样简单”的,不是对话框,而是 agent 编排
很多人把 DeepSeek 这类推理模型接进科研流程,第一反应是怀疑自己提示词写得不够好。真实情况常常相反:单次回答的质量早就够用,卡住整条链路的是流程本身——采数据、读文件、清洗、分析、挖掘、出图,每一步都可能失败,失败原因还各不相同。这套多 agent 科研工具值得拆的地方就在这里:它把一条长链路切成五个阶段,每段由独立 agent 负责,产出可验证的中间文件;聊天只是最外层那层皮,底下是任务编排、上下文隔离和失败重入。搞懂这层机制,才能判断什么时候该换模型、什么时候该回头改提示词,而不是把所有锅都甩给“AI 抽卡”。
2. 多 agent 流水线:五个阶段、五份中间产物,谁交给谁什么
2.1 阶段职责:采集、处理、分析、挖掘、可视化各自交付什么
这套工具的骨架在正文里写得很直白:以多 agent 实现从数据采集到可视化的全流程。真正决定它能不能跑通的,不是“有五个 agent”这件事,而是五个 agent 之间那份数据契约——每个阶段吃什么、吐什么、吐出来的东西怎么判定合格。
| 阶段 | 典型输入 | 交付物 | 最先出问题的信号 |
|---|---|---|---|
| 数据采集 | 网址列表、数据库连接、本地文件、接口地址 | raw.jsonl 加采集日志 | 条数为 0、字段缺失、URL 列表不全 |
| 数据处理 | raw.jsonl | clean.csv 加变换规则表 | 类型不一致、日期格式混杂、重复行 |
| 数据分析 | clean.csv | 指标卡加结论 md | 只有描述没有指标 |
| 数据挖掘 | clean.csv 加分析结论 | 分类、聚类、时序明细表 | 只分类不关联、结论停在表层 |
| 数据可视化 | 指标加明细 | 图表 spec 或绘图代码 | 代码跑不通、图与数对不上 |
采集阶段的输入形态差异极大:编写爬虫代码抓社交媒体数据、连数据库取库表、直接读取上传的 txt 或 excel、调用 API 拿接口数据,这四条路径在实现上几乎不共享代码,但对下游只暴露一个契约——一份带来源字段和采集时间的记录集合。处理阶段负责数据清洗、数据集成、数据变换、特征工程,对应到动作就是纠错、整合、格式转换、特征提取,它输出的 clean.csv 必须自带一份变换规则表,否则后面分析出异常值时你根本不知道是数据本身有问题还是清洗把数据洗坏了。
分析、挖掘、可视化这三段的分工是最容易被混淆的。分析 agent 做的是诊断、预测、关联、聚类这类统计性工作,服务于问题定位、需求预测、异常检测;挖掘 agent 做的是分类、社交网络分析、时序模式挖掘,服务于客户细分、信用评分、社交媒体营销、股价预测这类带业务语义的结论;可视化 agent 只干一件事,把前两者产出的指标和明细翻译成统计图、热力图、网络关系图、词云、树形图。把“分析”和“挖掘”合成一个 agent 是很常见的偷懒做法,代价是这类 agent 会同时被要求“算清楚”和“讲明白”,于是常常两件事都做半截。
2.2 为什么不写一条超长提示词:上下文膨胀、错误不可归因、重试粒度
最直觉的做法是把五个阶段写进一条提示词:“请读取数据、清洗、分析、挖掘并画出图表”。这条提示词在数据量小的时候确实能跑,但它有三个结构性缺陷,而且都能被实测数据佐证。
第一是上下文互相污染。采集阶段塞进来的原始 HTML、未清洗的脏数据,会和分析阶段的推理过程挤在同一个上下文窗口里。工具在文本数据集成任务里的实测结果很能说明问题:一般长度文本(约 7000 token)时,DeepSeek R1 能详细全面地提取文本数据并集成为可视化表格,但受大样本或模型稳定性影响,输出表格末尾缺失,需要重复尝试生成;文本长度升到 15000 token,同一模型直接无法给出答复。这不是模型变笨了,而是任务粒度和上下文长度不匹配——把清洗用的中间态和推理用的证据链放进同一个窗口,窗口是被无价值内容占满的。
第二是错误不可归因。单条提示词跑出错误结果时,你不知道是 URL 没筛全、脚本写错了、字段解析错位,还是模型在长链推理里丢了指令。多 agent 拆开之后,每一段的输入输出都落盘,失败点是可以二分定位的:采集日志里的条数对不对,clean.csv 的行数对不对,指标卡里的分母对不对,一层层往上验就行。
第三是重试粒度太粗。采集失败重试成本很低,换个 UA、加个延时就能过;分析失败重试成本很高,因为要重新跑一遍推理。单条提示词的重试是全链路重跑,多 agent 的重试是单阶段重跑。
2.3 时序与落盘:一次任务从提问到出图的完整状态流
把契约写成代码之后,编排逻辑其实非常薄。下面这段定义了阶段表,关键在 validate 这一项——它是区分“跑完了”和“跑对了”的唯一闸门。
# 每个阶段显式声明依赖、产物、重试次数和校验函数 STAGES = { "collect": { "needs": ["sources"], # 网址 / 库表 / 文件路径 / 接口地址 "produces": ["raw.jsonl", "collect.log"], "retry": 2, # 采集失败多为网络抖动,重试收益高 "validate": lambda out: count_lines(out["raw.jsonl"]) > 0, }, "process": { "needs": ["raw.jsonl"], "produces": ["clean.csv", "transform_rules.json"], "retry": 1, # 清洗失败通常是规则问题,重试不如改规则 "validate": lambda out: has_columns(out["clean.csv"], REQUIRED_COLS), }, "analyze": { "needs": ["clean.csv"], "produces": ["metrics.json", "report.md"], "retry": 1, "validate": lambda out: "metric" in load(out["metrics.json"]), }, "mine": { "needs": ["clean.csv", "report.md"], "produces": ["segments.csv", "relations.json"], "retry": 1, "validate": lambda out: count_lines(out["segments.csv"]) > 0, }, "visualize": { "needs": ["metrics.json", "segments.csv"], "produces": ["charts.json", "plot.py"], # 出图代码与图表规格分离 "retry": 2, "validate": lambda out: syntax_ok(out["plot.py"]), }, }这段表结构本身就是文档:needs 决定时序,produces 决定落盘文件名,retry 反映各阶段的失败性质差异,validate 决定什么算成功。可视化阶段把 charts.json(图表规格)和 plot.py(绘图代码)分开产出,是因为实测里 R1 和 Kimi 都能给出多种可视化方案但需要本地跑 Python 代码,部分代码会出现错误——规格和代码分离后,代码跑挂了只要改代码,不必让模型重新想一遍画什么图。
调度循环只有十几行,重点在两点:传给下游 agent 的只有上一阶段的产物路径和摘要,不是全文;校验不通过就停在这一阶段,不带着脏数据往下游跑。
def run_pipeline(task, stages=STAGES): ctx = {"sources": task["sources"]} for name, spec in stages.items(): for attempt in range(spec["retry"] + 1): # 每个 agent 只拿到自己需要的上下文,避免长窗口被无关内容挤爆 out = call_agent(name, {k: ctx[k] for k in spec["needs"]}, spec) if spec["validate"](out): ctx.update(out) # 只传递产物句柄,不传正文 break ctx["last_error"] = f"{name} 第 {attempt + 1} 次校验未通过" else: raise RuntimeError(f"阶段 {name} 未产出可用结果:{ctx['last_error']}") return ctxfor-else 这个写法在这里刚好贴合语义:只有循环正常跑完没 break 才进 else,也就是重试次数耗尽仍未拿到合格产物时直接熔断。这个熔断很重要,它把一个黑匣子式的长任务拆成了若干个“要么产出合格文件、要么明确报错”的短任务,出了问题至少有后悔药可吃。
3. 爬虫 agent:提示词在这里是程序规格书,不是聊天
3.1 两段式拆解:先产出网址清单,再让模型写抓取脚本
采集任务最容易踩的坑是把“找数据”和“写代码”混成一个动作,让模型一边翻页面一边写脚本。这套工具在爬虫场景里用的提示词是明确的两段式:第一步阅读网页和对应源代码,把符合特定条件的网址筛选、去重、合并成列表;第二步基于这份列表撰写 Python 脚本,提取所有目标网址的正文内容,最后写入指定的 txt 文件。
为什么要强拆成两段?因为这两段的失败模式完全不同。第一段是检索与判定问题,失败表现是 URL 漏了或者重复了,用户可以肉眼核对;第二段是代码生成问题,失败表现是脚本跑出空文件或者字段解析错位,只能靠运行验证。混在一起的后果是:脚本跑出空文件时,你分不清是 URL 列表本来就错,还是解析逻辑写错了。四模型对比测试里反复出现“能够提取所有网址,代码运行后生成本地文件,但提取数据结果为空”这种结果,如果 URL 清单是独立产物,一眼就能看出问题出在第二段。
这一步还有一个隐性收益:网址清单是可核验的中间事实。用户对着列表确认一遍,等于给整条链路加了一次人工校验点。在多 agent 系统里,凡是人能低成本校验的中间产物,都应该单独落盘。
3.2 五个子任务的提示词骨架与脚本落地
原始任务被拆成五个子动作:阅读源代码提取特定内容、撰写脚本、提取并合并网址、提取网址内容、写入文件。这个拆分方式值得照抄,因为它把“模型负责判断”和“代码负责执行”分得很清楚——判定哪些网址命中条件交给模型,批量抓取交给脚本,两者职责不越界。
下面是对应的脚本骨架,两段函数分别对应上面的第一段和第二段任务:
import re, json, time, pathlib import requests from bs4 import BeautifulSoup URL_RE = re.compile(r"https?://[^\s\"'<>]+") def collect_links(index_html: str, must_contain: list[str]) -> list[str]: """第一段:只做筛选与去重,不抓正文,产出可人工核对的 URL 清单""" seen, links = set(), [] for url in URL_RE.findall(index_html): url = url.rstrip("/") # 命中关键词才算候选,避免把导航、版权、分享链接一起带进来 if url in seen or not all(k in index_html for k in must_contain): continue seen.add(url) links.append(url) return links def fetch_detail(url: str, field_patterns: dict) -> dict: """第二段:只抽字段,不做判断;字段抓不到就留空,交给下游清洗""" resp = requests.get(url, timeout=10, headers={"User-Agent": "research-agent/1.0"}) resp.encoding = resp.apparent_encoding text = BeautifulSoup(resp.text, "html.parser").get_text("\n") row = {"source_url": url} for field, pattern in field_patterns.items(): m = re.search(pattern, text) row[field] = m.group(1).strip() if m else None return row if __name__ == "__main__": links = collect_links(pathlib.Path("index.html").read_text("utf-8"), ["客运量"]) rows = [] for u in links: rows.append(fetch_detail(u, {"date": r"(\d{1,2}月\d{1,2}日)", "value": r"完?成([\d.]+)万人次"})) time.sleep(1) # 控速比并发更重要,硬冲只会拿到空页 pathlib.Path("data.txt").write_text( "\n".join(json.dumps(r, ensure_ascii=False) for r in rows), "utf-8")field_patterns 这个参数是整个脚本的可配置点:换任务只需要换正则,不用改流程。time.sleep 那行不是可选项,抓取类任务里最常见的“脚本能跑但结果为空”,一半以上是请求频率过高被限流后拿到了空页面或验证页。rows 里允许字段为 None 也是刻意的设计——抓不到就留空,让清洗阶段去统计缺失率,比在采集阶段悄悄丢掉整条记录要好,因为丢掉记录会让下游的分母无声地变小。
3.3 文件读取:把一段自由文本压成表格的约束写法
文件读取看起来比爬虫简单,实测反而是模型差异最大的一环。任务形态是:上传一份覆盖若干天的客运量文本,要求读取每一天的信息并整理成表格,表格必须包含当天日期、各类客运量、相比去年同期的百分比、环比百分比。这个任务里真正难的不是读,而是“约束”——指标维度多、每行要算两个比率、日期要排序。
四个模型在这一项上的分化很典型:R1 能详细全面提取并整理成逻辑性强、指标清晰的表格;Claude 3.5 Sonnet 完成度高且支持直接复制和表格文件下载;Kimi 能快速读取但填入数据有明显缺失;o3mini 效率高、输出准确,但在实测环境里暂不支持附件上传,只能靠粘贴文本,长文件就很别扭。同一份提示词、同一份数据,结果差这么多,说明这一环的瓶颈不在理解能力,而在“输出长度 + 字段约束”的组合压力。
把提示词写得更像表结构定义,是压低这类波动的有效手段。做法是显式列出表头、单位、缺失值写法和排序规则,让模型的输出空间被压缩到几乎只剩填值:
输出要求(严格遵守,不要解释): 1. 只输出一个 markdown 表格,表头固定为: 日期 | 铁路客运量(万人次) | 铁路同比(%) | 铁路环比(%) | 公路客运量(万人次) | ... 2. 日期格式统一为 YYYY-MM-DD,按日期升序排列 3. 原文没有的指标填 NA,不要估算、不要跨行借用数据 4. 同比、环比保留一位小数,正负号保留“不要估算、不要跨行借用数据”这一句看着多余,实际是最值钱的一句。长表格生成时模型很容易为了让表格好看而顺手补一个合理数字,这种补全在单次对话里几乎不可察觉,但它是数据类任务里最难排查的污染源——你拿到的表格每一行都填满了,没有一格是空的,于是没人会去核对。
4. 长文本与推理:token 预算、思维链和“维度缺失”的根因
4.1 7000 token 能过、15000 token 直接沉默,边界在哪
文本数据集成任务的实测结果给出了一条很清晰的曲线:约 7000 token 的一般文本,R1 能详细全面提取数据并集成为可视化表格,但输出表格末尾会缺失,需要重复尝试生成;文本加到 15000 token,同一模型直接无法给出答复。另外三个模型的表现方向相反:Kimi 在短文本上维度不够全面,到了长文本反而更突出,提取准确且维度更全;Claude 在长文本上集成的数据维度比短文本更全面;o3mini 全程响应最快,但集成维度始终偏窄。
这组结果指向一个常被忽略的机制点:长文本任务真正的约束不是“能不能读进去”,而是“读进去之后还剩多少输出预算”。一个 15000 token 的输入配上要求输出完整表格的指令,输入加输出很容易顶到窗口上限,模型的表现就不是“答得差一点”,而是“不答”。7000 token 时出现的“表格末尾缺失”是同一个问题的轻症版本——不是理解失败,是生成被截断。
工程上的解法不是换更大的模型,而是改任务结构。把长文本按记录切块、每块独立抽取、最后按主键合并,比指望模型一次吞下全文可靠得多。这也是自然语言处理里处理长文档的常规做法,只不过在大模型时代很多人把它忘了。
def chunk_by_record(text: str, max_chars: int = 6000, overlap: int = 200) -> list[str]: """按行切块而不是按字符硬切:保证一条记录不被从中间劈开""" lines, chunks, buf, size = text.splitlines(), [], [], 0 for line in lines: if size + len(line) > max_chars and buf: chunks.append("\n".join(buf)) # 保留尾部若干行做重叠,防止跨块的记录被丢掉 buf = buf[-3:] if overlap else [] size = sum(len(x) for x in buf) buf.append(line) size += len(line) if buf: chunks.append("\n".join(buf)) return chunks def extract_and_merge(chunks, schema_keys, key="date"): """每块单独抽取后按主键合并去重,冲突时保留字段更全的那条""" merged = {} for c in chunks: for row in call_agent("extract", {"text": c, "schema": schema_keys}): old = merged.get(row[key]) if old is None or filled(row) > filled(old): merged[row[key]] = row return [merged[k] for k in sorted(merged)]三个参数各有讲究。max_chars 定在 6000 字符量级,是给输出表格留足预算——输入占一半、输出占一半,比一次性塞满更稳。overlap 用“保留尾部三行”而不是字符重叠,是因为结构化文本的语义单位是行,字符级重叠会把一行切成两半,反而制造脏数据。key 选日期这类天然主键,合并时用 filled() 比较字段完整度来裁决冲突,比“后写覆盖先写”合理,因为后一次抽取不一定更准。
4.2 分析 agent 的异常检测:先统计后归因的两段式
数据分析这一环,四个模型的差距不在算得准不准,而在“会不会多看一眼”。以某乘客名单数据集为例,任务是读取文件并找出其中的规律。R1 的表现是详细展示长思维链、精准提取关键指标(幸存率)、分析多个因素特征对该指标的影响、结合历史背景做验证,并且敏锐察觉到数据异常,主动提出了异常处理建议;o3mini 输出快、语言自然、重点突出,也结合背景做了验证,但没有察觉数据异常;Claude 能给出分析思路却没形成明确结论;Kimi 只能识别较浅层的数据关联。
“察觉异常”这个能力不是玄学,它在机制上就是推理链长度带来的副产品。先统计后归因的两段式推理,会天然产生一次“数据体检”:算完各分组的幸存率之后,模型会注意到某些分组的样本量极小、某些字段的缺失率高得离谱、某些组合的比率明显偏离整体分布——这些观察只有在“先算完再解释”的顺序里才会浮现。反过来,如果提示词直接要求“分析影响因素”,模型会跳过统计直接编因果关系,输出读起来很顺,但每一句都无法回溯到具体数字。
这也解释了为什么这一环值得用推理型模型。深度学习那套特征重要性的做法在这里不适用——数据量往往只有几百上千行,特征也就十来个,模型靠的是在推理链里显式记账,而不是拟合。提示词层面能加的约束很朴素:要求先输出一张分组统计表,再基于这张表写结论,结论里的每个数字都必须能在表中找到。
4.3 挖掘深度不够时,用第二轮对话下钻
数据挖掘是四个模型分化最严重的一环。以某年度电影数据集为例,任务是读取数据并做深入分析和数据挖掘。结论是 Kimi 表现最出色,能对特征精准分类、从时间、语言、地区等多个维度深入挖掘关联性;R1 和 Claude 同样能准确完成分类任务,也能借助可视化图表给出可行建议,但数据之间的关联挖掘程度相对较浅;o3mini 受附件上传限制,数据集较大时无法完成。
注意 R1 那条评语的措辞:分类准确、建议可行,但“缺少对不同类型数据直接关联性的探究”。这是数据挖掘任务的典型半成品——它把数据切成了几堆,每堆都描述得很清楚,却没回答“这两堆之间是什么关系”。分类是单变量的,关联是双变量的,后者需要主动提问才能触发。
工具给出的解法是两轮对话:第一轮做整体分析与概括,第二轮基于初步结果选择其中一部分或某个方向深入挖掘。这个设计在机制上是把“探索”和“下钻”分开——第一轮负责铺开维度、暴露可疑关系,第二轮带着明确方向回去算交叉表。单轮提示词做不到这件事,因为模型不知道你对哪个方向感兴趣,只能平均分配注意力,结果每个方向都浅。
实操上第二轮提示词要写成“假设 + 验证”的形式,而不是“再深入分析一下”。前者是“我怀疑评分高的影片集中在某几个月,请按月份和评分的交叉表验证”,后者是“请深入挖掘数据价值”——后者的输出基本等于把第一轮换个说法重写一遍。
5. 排查手册:多 agent 科研流程里最常翻车的五处
5.1 采集与解析:空文件、少 URL、字段错位
现象一:脚本退出码是 0,生成的文件是空的。原因几乎总是页面结构和预期不一致——模型写脚本时假设了某个 class 名或某段固定文案,实际页面改版或返回了验证页,选择器全部落空,但脚本不会报错,只是抽出零条。解决办法是把“条数为 0”当作硬失败:在采集脚本末尾加一行断言,条数为零直接抛异常退出,并保留一份原始响应到 debug.html,方便对照。这类问题在对比测试里出现过不止一次,两个模型生成的代码都能跑、都生成了文件,只是里面没有数据。
现象二:URL 清单看起来对,但比预期少一大截。原因是筛选条件用了“全页面包含关键词”这种粗判,或者去重时把带不同查询参数的同一页面当成多条合并掉了。解决是让筛选条件作用在候选链接的上下文片段上,而不是整页;去重前先做一次 URL 归一化,去掉锚点和无意义的跟踪参数,保留真正区分内容的参数。
现象三:表格生成了,但某几列整体错位一行。原因是非结构化文本里存在跨行的记录,按行解析时把续行当成了新记录。解决是在切分前先做一次记录边界识别,把“以日期开头的行”作为新记录起点,其余行合并到上一条;合并完再统计行数与原文日期个数是否一致。
5.2 分析与出图:维度缺失、代码跑不通
现象四:分析结论读起来很专业,但没有一个具体数字。原因是指令里只写了“分析规律”,没要求输出指标。这个坑在对比测试里有明确体现:有的模型能给出分析思路却没有明确结论。解决办法是把输出结构写死——先给指标卡(核心指标加数值),再给分组统计表,最后才是文字结论,三段缺一段就算未完成。
现象五:可视化方案很漂亮,代码复制到本地跑不通。原因是模型倾向于使用不存在的列名或已弃用的 API,而它在对话里没法验证。工具层面的应对是让可视化 agent 同时产出图表规格和绘图代码,代码只允许使用基础库;用户侧的应对更简单——拿到代码先跑一遍,报错信息原样贴回对话里让它改,通常一两轮就能收敛,比重新描述需求快得多。同时在图表规格里明确写出 x 轴字段、y 轴字段、聚合方式,这样即使代码全错,你也能自己照着规格画出来,不至于整段白干。
6. 模型分工的取舍:把推理模型放在哪一环,决定整条链路的成本
五个环节、四个模型,最容易犯的错是“全线都上最强的那个”。实测数据不支持这种撒钱式用法:R1 在文件读取和数据分析上维度最全、逻辑最严谨,但长文本会直接沉默、可视化出图要靠本地跑代码;o3mini 响应最快、能直接出图,但长上下文里集成维度偏窄,且附件上传受限;Kimi 在长文本和数据挖掘上反而更强;Claude 在文本提取上稳、能一次性出表并支持下载,但爬虫环节容易卡在代码错误上。
| 环节 | 首选 | 备选 | 取舍理由 |
|---|---|---|---|
| 采集与筛选 | R1 | o3mini | R1 抓取结果更完整准确,o3mini 胜在速度 |
| 文件读取与表格化 | R1 或 Claude | o3mini | 前者维度全,后者格式工整但依赖粘贴 |
| 长文本集成 | Kimi 或 Claude | — | 长文本下维度反而更全面,R1 会拒答 |
| 数据分析与异常检测 | R1 | o3mini | 长思维链带来“多看一眼”的能力 |
| 数据挖掘 | Kimi | R1 | 多维关联挖掘更深,R1 分类准但关联浅 |
| 可视化出图 | o3mini | R1 + 本地 Python | 直出图省事,代码方案可控 |
真正可落地的编排是组合式的:采集环节用 R1 保完整、o3mini 补速度;处理环节用 Claude 做文本提取、R1 保数据完整性;分析环节 R1 负责深入分析和异常检测、Kimi 提供下钻思路;呈现环节 o3mini 直接调用出图能力、Kimi 供 Python 代码、Claude 负责图表逻辑优化。这套分工的本质是把“生成代码”和“判断结果”拆给不同的模型——让擅长推理的只管判断,让擅长生成的只管产出代码,再用校验函数把两者串起来。
一个提醒:所有对比数据都受数据样本、测试环境、提示词模板影响,不同轮次结果会有波动,别把某一次的名次当成定论。真正稳定的是机制层面的差异——上下文长度对输出的挤压、推理链长度对异常检测的影响、代码生成需要本地验证,这三条不随模型版本变化。
我自己的习惯是,任何一条跨三个环节以上的流程,都要先在中间插两个校验点:采集后核对条数,分析后核对分母。加这两个断言的代价是十分钟,省下的是半天。从那以后我每次搭这类多 agent 链路,都会强制先跑一遍最小样本、看着每一份中间产物落地,再放开数据量。希望帮到你。
本文还有配套的精品资源,点击获取