拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多智能体工作流实战:构建AI深度研究助手

多智能体工作流实战:构建AI深度研究助手

1. 这个项目到底在解决什么问题

1.1 传统人工调研的三大痛点,以及AI的切入点

先把这个事情说透。Deep Research Assistant,我最初的想法其实非常朴素:工作里经常需要快速了解一个新行业、新竞品或者新技术领域,传统做法是人肉打开搜索引擎,一页一页翻搜索结果,点开网页,复制粘贴到笔记里,再对着这些零散资料慢慢整理成报告。这个过程有多耗时,做过咨询、行研、产品调研的朋友应该都有体会——单单是资料收集阶段就可能花掉半天,真正动笔写报告的时候又会发现有些关键数据没找到,还得回头再查。

我遇到的痛点可以归纳成三条:

  • 信息过载与筛选难。搜索一个关键词可能返回几百万条结果,真正有参考价值的可能就前面三五页,而且质量参差不齐。
  • 多轮检索的路径割裂。查完A资料去查B资料,中间的逻辑衔接全靠人脑硬记,经常查着查着忘了最初想问什么。
  • 输出整理的二次成本高。资料有了,还得重新组织语言、梳理逻辑、标注出处,本质上又是一轮脑力劳动。

所以这个项目的切入点很明确:让AI扮演一个“调研助理”的角色,把用户给的一个大方向拆解成若干子问题,逐个去检索、阅读、提炼,最后汇总成一份带引用来源的结构化报告。它不是一个简单的问答机器人,而是一个真正会“干活”的多智能体工作流。

1.2 深度研究助手和普通聊天问答的本质区别

很多人一开始会问:这和直接问ChatGPT有什么区别?区别很大。ChatGPT这类模型的知识截止日期是固定的,你问它“2025年最新的充电桩行业数据”,它往往只能给出一个训练数据范围内的通用回答,甚至可能会一本正经地编造不存在的数字。但Deep Research Assistant的核心逻辑不是“从模型参数里回忆答案”,而是“先检索真实世界的最新信息,再让模型基于检索结果做分析和写作”。

用大白话解释:传统问答是让一个天才靠记忆答题,而深度研究助手是让一个聪明的实习生先查资料、再动笔。前者的上限是训练数据的截止日期,后者能检索到的信息都是实时数据,只要网络上有相关内容,理论上都能拿回来分析。

另一个重要区别在于过程的拆解。一个合格的调研报告,通常会经历确认选题、拆分维度、收集数据、交叉验证、组织语言、排版校对这几个阶段。Deep Research Assistant把这件事流程化了:规划模块负责拆解任务,检索模块负责收集素材,综合模块负责写每个小节的正文,审校模块负责检查遗漏和矛盾。整个链路清晰,每个环节都可以单独调整和优化。

1.3 这套系统是谁需要、谁适合用

我在开发过程中顺手整理了一下目标用户画像,大致分三类:

  • 互联网从业者:产品经理做竞品分析、运营做行业动态扫描、投资岗做标的研究,这类人群最需要快速获取结构化信息。
  • 学术研究相关人员:毕业设计初期的文献调研、科研前的方向预研,虽然学术上有更专业的数据库,但Google Scholar之外的网络公开信息也是重要补充。
  • 内容创作者和技术博主:写深度文章之前需要大量背景素材,一条命令跑出来一份带引用的素材包,能节省大量前期准备时间。

下面这张表是我反复思考和实测后整理出的核心差异点,方便你判断自己是否真的需要这样一个工具:

对比维度普通问答式ChatDeep Research Assistant
信息时效受限训练数据截止日期实时检索网络信息
回答深度单轮单篇,浅尝辄止多轮迭代,按照提纲逐节产出
引用溯源通常没有或模糊带来源链接和参考段落
过程可控一个Prompt到底各阶段独立模块,可人工干预
适用场景日常咨询、临时问答深度调研、报告撰写、知识预研

2. 核心思路与方案选型:为什么要这样做

2.1 为什么采用多智能体工作流,而不是一个万能Prompt

一开始我确实尝试过用一个超长的Prompt把“搜索和分析”全塞给模型。比如写一个3000字的系统提示词,要求模型“思考用户需求,自行搜索,整理答案”。但实测效果很差,主要有三个原因。

第一是上下文窗口的限制。一次搜索可能要拿到大量网页原文,把这些内容全部塞进一次模型调用里,很快就超过上下文长度。即便强行塞进去,模型也会被无关信息干扰,导致重点抓取不准。

第二是过程不可控。如果一切都在一个对话里完成,用户很难干预“中间步骤”。比如模型检索方向跑偏了、使用的搜索关键词太泛,你只能干看着最终答案稀烂,无从纠正。而拆成独立模块后,每一步都可以设定规则,甚至可以在关键节点暂停,人工介入调整。

第三是成本不可控。一个长对话如果中途出错,整个上下文可能都要重来,浪费大量token。拆成模块后,每个模块可以单独重试,逻辑上清晰很多。

既然决定了多模块拆解,实现方式上又面临一个选择题:用现成的Agent框架(比如LangGraph、AutoGen),还是自己写一个调度循环?

我最终选择了自己写一个轻量级的调度核心,而不是直接套LangGraph。原因很实际:这种调研类任务本质上是一个有向执行流——规划、检索、综合、审校,每一步的输入输出都很明确,不需要复杂的状态机管理。自己写的话可控性最强,查错也容易。框架虽好,但很多时候杀鸡用了牛刀,调试成本反而更高。当然如果你后续要处理非常复杂的动态分支逻辑,引入LangGraph这类框架是有价值的,这是后话。

2.2 技术选型的三个关键决策

技术选型上,我做了三个比较关键的决策,直接把方案确定了下来。

第一个决策是大模型的选型。核心诉求有两个:一是中文和英文内容都要能处理得当,二是token成本不能太高,毕竟深度调研要跑很多轮检索和写作。我最后选了GLM-4-Flash为主力模型,这是因为目前它有一个其他家大模型没有的优势——提供了公开且免费的API调用额度,对于跑这种频繁调用的大任务非常友好。即便后续额度政策变化,我也预留了模型层抽象,可以随时无缝切换到其他家的接口。

第二个决策是搜索引擎API的选择。最开始我试图直接用爬虫去请求某度搜索页面,然后解析HTML里的结果。实测发现这种方式非常脆弱,搜索引擎的页面结构稍微调整一下脚本就废了,而且很容易触发反爬机制导致IP被临时限制。后来换成了Serper.dev提供的Google搜索API,它的免费版每月有2500次调用额度,对个人项目来说完全够用了。价格便宜、返回结构化JSON、稳定性好,确实省心很多。

第三个决策是网页正文提取使用Trafilatura库而不是我手写正则。爬虫容易,把爬下来的HTML清洗成干净的正文反而最麻烦。Trafilatura这个库我在多个项目里用过,它对新闻、博客、论坛页面的正文提取效果都相当不错,一行代码就能拿到干净文本。相比自己写正则慢慢抠,稳定性和效率都提升了一个量级。

2.3 系统整体工作流拆解

整个系统运行起来后的工作流,我用大白话描述一下:

  • 用户提交一个宽泛的问题,比如“2025年充电桩行业分析”。
  • 规划模块出场,编排出三到五个子调研方向,例如市场规模、政策环境、龙头玩家、技术趋势、用户痛点,每个方向还给出一组搜索关键词。
  • 每个子任务进入检索模块,拿着关键词去搜索引擎拿结果列表,然后逐条打开正文,截取关键段落,保存成“参考语料”。
  • 综合模块登场,根据该子任务的提问约束,把参考语料整理归纳成一段逻辑通顺、带引用标记的文字。
  • 若干子任务的产出拼在一起,审校模块检查引用是否真实、整体结构是否完整,如果有明显缺陷会触发一次重试。
  • 最终把所有子报告拼接成一个完整的Markdown文档,保存到本地。

这套流程跑一轮,通常能在大约两到四分钟内产出一份基础的行业报告,相比人工检索动辄半天的效率提升是非常明显的。

3. 手把手实现:从环境配置到核心代码

3.1 环境准备与依赖安装

在开始之前,先把项目的骨架建好。我用的是Python 3.10+,虚拟环境工具用的Anaconda,这是我现在所有Python项目标配了。

conda create -n deep_research python=3.10 conda activate deep_research pip install openai trafilatura httpx pydantic python-dotenv

你需要准备两个API Key:一个是模型供应商的API Key,另一个是Serper的API Key。建议都写进项目根目录的.env文件,不要在代码里硬编码。用python-dotenv加载,非常方便。

.env文件内容参考如下:

GLM_API_KEY=你的模型API密钥 SERPER_API_KEY=你的Serper密钥

3.2 核心数据结构设计

在写业务逻辑之前,我先把数据模型定好了。这一个步骤看起来不起眼,但对后续开发帮助极大——每个模块之间的数据流就靠这些结构衔接,类型清晰了,代码写起来不会乱。

from pydantic import BaseModel from typing import List class SubTask(BaseModel): """子任务定义:把大问题拆出来的一个独立调研方向""" name: str # 子任务名称,例如:市场规模 instruction: str # 对这个子任务产出内容的具体要求 search_queries: List[str] # 对应的一组搜索关键词 class SearchResult(BaseModel): """单条搜索结果""" title: str link: str snippet: str class SourceRecord(BaseModel): """一条已经从网页中提取出来的参考语料""" url: str title: str content: str # 清洗后的正文片段 class SubReport(BaseModel): """子报告的产出,后续会被拼进最终报告""" task_name: str content: str references: List[str]

这里我用Pydantic做数据模型,一方面是类型校验,另一方面是后续序列化保存中间结果非常方便。调试的时候能够直接dump成JSON看每一步产出的中间状态,对排查问题帮助很大。

3.3 规划模块:把模糊需求拆成可执行的调研提纲

规划模块要做的事,说复杂也复杂,说简单也简单——给大模型一个结构化输出要求,让它把用户的宽泛问题拆成子任务。

async def planner(client, research_question: str) -> List[SubTask]: system_prompt = """你是一个严谨的研究规划助手。你需要把用户提出的宽泛研究问题, 拆解成3到5个独立的子调研任务。每个子任务必须包含: 1. 明确的调研方向名称(例如:市场规模、竞争格局、技术趋势等) 2. 对最终产出的内容要求(需要使用中文描述) 3. 三到五个适合搜索引擎检索的关键词(尽量覆盖不同角度) 请以JSON数组输出,字段名为name、instruction、search_queries。""" response = await client.chat.completions.create( model="glm-4-flash", temperature=0.2, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": research_question} ] ) data = json.loads(response.choices[0].message.content) return [SubTask(**item) for item in data["tasks"]]

这里有几个实操细节值得你注意:

  • 要求模型返回JSON并指定response_format,会大幅降低解析出错的概率。如果不用这个参数,模型偶尔会在输出里夹杂解释性文字,解析必炸。
  • temperature设为0.2,保证规划结果相对稳定。规划任务不需要创造性,稳定比惊喜更重要。
  • 搜索关键词设计得要有层次感。比如调研“充电桩行业”,可以拆成行业规模类关键词、政策类关键词、技术类关键词、竞品财报关键词,不同维度能搜到不同类型的资料。

我见过一些人偷懒让规划模块只生成一个搜索词,然后检索模块把所有内容怼回来,这样信息密度太低,最终报告质量很难保证。所以这一步的产出质量几乎决定了整个漏斗的流量入口质量。

3.4 检索模块:搜索、抓取、清洗,三层递进

检索模块是重头戏,也是整个系统里最容易出问题的地方。它内部其实是三个子步骤:调用搜索API拿结果列表、逐个网页抓取正文、从正文中截取与子任务相关的片段。

先封装一个搜索函数:

async def web_search(serper_key: str, query: str, num_results: int = 5): url = "https://google.serper.dev/search" payload = {"q": query, "num": num_results, "gl": "cn", "hl": "zh-cn"} headers = {"X-API-KEY": serper_key, "Content-Type": "application/json"} async with httpx.AsyncClient() as client: resp = await client.post(url, json=payload, headers=headers) data = resp.json() results = [] for item in data.get("organic", []): results.append(SearchResult( title=item.get("title", ""), link=item.get("link", ""), snippet=item.get("snippet", "") )) return results

拿到搜索结果列表之后,下一步就是网页正文提取。这部分我强烈建议直接用Trafilatura库,而不是手动解析HTML。原因前面也说过了,手动解析面对不同网站的不同页面结构,写出来的规则非常脆弱。Trafilatura基于算法自动识别页面主内容区域,对绝大部分新闻、博客、企业官网都适用。

import trafilatura def extract_main_content(url: str) -> str: downloaded = trafilatura.fetch_url(url) if not downloaded: return "" result = trafilatura.extract(downloaded, include_comments=False, include_tables=True, output_format="txt") return result or ""

这里有两个小坑:一是include_tables要设为True,很多行业数据其实是在表格里的,不提取会丢失重要信息;二是有些网页会返回较长正文,需要做长度控制。

检索模块拿到的正文可能会非常长,几万字的文章直接塞给模型做综合,成本高且噪声大。我建议对每条网页正文做截断处理,优先保留开头部分和包含关键词的段落,这个逻辑也很简单,用关键词索引定位然后切片就行。

3.5 综合模块:让模型把语料写成结构化小节

检索完成之后,手里是一堆来源链接和正文片段。综合模块的任务,就是让模型扛着这些“素材”写出一段高质量的、带引用的小节文字。

async def synthesize(client, task: SubTask, sources: List[SourceRecord]) -> SubReport: source_text = "" for idx, src in enumerate(sources): source_text += f"\n\n【参考{idx+1}】来源: {src.url}\n{src.content[:1500]}" user_prompt = f"""请根据以下参考材料,完成一项子调研。 子任务:{task.name} 具体要求:{task.instruction} 可用参考材料: {source_text} 写作要求: 1. 输出应该像一篇行业分析报告的小章节,有观点、有论据,不使用列表式流水账 2. 在提到数据或关键事实的地方,用[ref:1]这样的标记标注对应参考来源 3. 如果参考材料相互矛盾,如实说明差异,不要强行调和 4. 控制在500到800字之间,使用中文""" response = await client.chat.completions.create( model="glm-4-flash", temperature=0.4, messages=[ {"role": "system", "content": "你是一个资深行业研究员。你善于从多个信息源中提炼关键事实,并形成逻辑清晰的中文分析报告。"}, {"role": "user", "content": user_prompt} ] ) content = response.choices[0].message.content return SubReport( task_name=task.name, content=content, references=[s.url for s in sources] )

这段代码里值得留意的一个设计,是我把每条参考材料都加了编号,并要求模型在写正文的时候用[ref:1]这种标记去对应。这一步非常关键——它让输出的报告天然具备引用溯源能力。即便模型偶尔写出来的数字和原网页稍有出入,读者也可以顺着标记回到原文去核实,这比没有引用的AI生成内容可信度高了一个档次。

3.6 审校模块和主控循环

审校模块的核心功能是检查综合模块产出的内容是否存在明显的“幻觉”和“不完整”。实际操作中,我主要让它做三件事:检查引用标记是否都对应到了真实参考源URL、检查内容是否满足子任务的指令要求、检查是否有明显前后矛盾。

主控循环的伪代码大致是这样:

async def run_research(question: str): subtasks = await planner(client, question) final_report = [] for task in subtasks: # 遍历每个子任务的所有搜索词,汇总检索结果 collected_sources = [] for query in task.search_queries: results = await web_search(SERPER_KEY, query, num_results=5) for r in results: content = extract_main_content(r.link) if len(content) > 200: # 过滤掉打不开或正文太短的页面 collected_sources.append(SourceRecord( url=r.link, title=r.title, content=content )) # 去重:同一域名下可能搜出来好几篇相似内容 seen_urls = set() unique_sources = [] for s in collected_sources: if s.url not in seen_urls: seen_urls.add(s.url) unique_sources.append(s) report = await synthesize(client, task, unique_sources[:5]) final_report.append(report) print(f"已完成子任务: {task.name}") await generate_final_markdown(final_report)

这段代码里我特别留了个去重逻辑。实践中发现,同一个站点的多篇文章有时候内容高度雷同,不去重的话会让综合模块参考到大量重复信息,影响产出多样性。限制每个子任务最多使用5条高质量语料,也是经过实测的成本与效果平衡点——少于3条素材不足,多于7条则模型容易抓到冗余信息。

3.7 最终报告的拼接与保存

最终报告我会拼成一个规范的Markdown文档,包括标题、导语、各小节正文、参考链接附录。因为每个子报告已经带了引用标记,整理起来不费劲,直接用Python字符串拼接即可。保存为带时间戳的文件名,便于回溯查看不同版本的产出差异。

4. 实测实录:跑通一个完整调研任务的复盘

4.1 测试用例与运行数据

为了验证系统效果,我选了一个真实场景做测试:分析2025年新能源汽车充电桩行业的主要趋势和市场格局。这是一个比较宽泛的问题,适合测试规划器的拆解能力。

设定参数:

  • 模型:glm-4-flash
  • 每个搜索词取前5条搜索结果
  • 每个子任务最多用5条参考语料
  • 最大抓取长度:单篇2000字

运行过程记录:

阶段实际耗时说明
规划拆解约12秒生成4个子任务
子任务1检索+综合约35秒搜索了3个关键词,抓取17条网页,提取并综合
子任务2检索+综合约30秒两个关键词打不开,程序自动跳过空结果
子任务3检索+综合约41秒有一个网站返回403,自动忽略
子任务4检索+综合约38秒正常完成
报告拼接约3秒生成最终Markdown文件

整体跑完耗时为2分39秒。API费用方面,由于使用免费额度的模型,几乎是零成本运行。如果换成GPT-4o这类付费模型,同样的运行大概消耗1到2美元,换算下来性价比也是可以接受的。

4.2 各阶段输出质量评估与分析

规划模块产出的四个子任务分别为:行业市场规模与发展现状、政策环境与补贴动向、技术路线演进趋势、竞争格局与主要玩家动态。这个拆解相对专业,覆盖面也很全,而且给出的搜索关键词组合也很有层次感,这说明规划模型确实理解了“分析一个行业”的基本维度。

综合模块产出的正文质量让人惊喜。其中一个子报告的片段是这样的:

国内充电桩市场规模在2024年继续保持高速增长态势。根据多份行业公开数据和券商研报显示,截至2024年底,全国充电基础设施累计数量已突破千万台级别,公共充电桩和私人充电桩的结构比例持续优化。与新能源汽车保有量增速相比,车桩比仍存在一定缺口,这既是行业痛点也意味着后续增长空间。部分地区充电桩利用率偏低的问题仍然突出,运营商盈利模式尚未完全成熟。

这段文字有数据、有分析、有结论,直接放进一份正式报告里也毫不违和。而我最终在测试报告中抽查了引用标记,确认标记确实对应到了真实的来源链接。

4.3 效果验收:哪些场景值得用它

实测后我的结论是:Deep Research Assistant最适合的场景是“快速建立对一个陌生话题的基础认知框架”。在信息真实性和时效性要求不高的探索阶段,它可以充当一个非常得力的信息预处理器。

但如果用在一线城市特定楼盘的投资分析、主板上市公司财务细节核查、小众技术专利检索这类对信息准确性有极高要求的场景,我仍然会建议人工逐条核实来源。毕竟任何检索型Agent都不可能完全避免模型在归纳环节引入的主观判断偏差。

5. 高频问题与正确避坑姿势

5.1 三大高频坑点与我的排查方案

踩坑一:搜索结果重复率过高,参考材料同质化严重。这是最开始跑测试时最常遇到的问题。同一个平台的内容经常被多个站转载,搜索结果里70%都指向同一个来源的缝缝补补版本。解决办法是我在检索阶段加入了去重逻辑,同时限定每个子任务最多用5条语料,强迫模型在多样性有限的材料中做取舍和提炼,反而让分析更有深度。

踩坑二:网页抓取失效,异常处理不到位导致任务中断。网站超时、403拒绝、返回内容过短,这些都是家常便饭。刚开始我的代码里没有完善的异常处理,任何一个站点出问题整体任务就会崩溃。后来我加了一个约定:抓取失败的标准是返回内容不足200字符,这样的URL直接跳过,不影响后续流程。实测下来稳定性从60%提升到95%以上。

踩坑三:模型输出JSON解析失败。调用glm-4-flash时,我强制指定了response_format为json_object,但偶尔仍然会遇到模型中英文混杂、引号转义的问题。我的处理方案是每次解析失败后自动重试一次,并把错误信息反馈给模型让它修正。加了这个自愈逻辑之后,解析失败率几乎归零。

5.2 提示词里的三个隐藏细节

提示词设计是这个项目里最值得反复打磨的部分。我总结下来有三个细节,直接影响最终产出质量。

第一,写作要求的颗粒度要足够细。只说了“写一篇分析报告”是远远不够的,必须明确是“像行业研究报告的小章节,有观点、有论据,不使用列表式流水账”,模型才会摆脱罗列要点的惯性。

第二,对矛盾信息的处理策略要提前约定。不同网页对同一事件的统计口径可能完全不同。如果不给模型指令,它往往会强行选一个数字,或者含糊其辞。我在提示词里明确要求“如果参考材料相互矛盾,如实说明差异,不要强行调和”,这个指令让最终报告的可信度提升了很多。

第三,引用标记必须写入写作指令。让模型逐条标注[ref:n]引用,原理其实利用的是模型的“溯源强迫症”——一旦要求它提供来源,它在编写内容时就会自觉减少自由发挥的比例,输出明显更保守也更可靠。

5.3 这套系统后续还能往哪些方向长

现在的版本已经能完成基础的调研报告生成,但距离一个成熟的产品还有不少空间。我梳理了几个投入产出比比较高的扩展方向:

一是引入浏览器自动化。目前只能依赖搜索API返回的摘要和网页正文,遇到信息藏在交互式页面里(比如需要点击展开的统计图表)就抓取不到。后续如果引入Playwright这类工具,可以模拟真实用户浏览行为,通过点击、滚动、翻页拿到更丰富的动态内容,信息覆盖范围会大很多。

二是增加情报记忆层。目前每次调研都是“一次性”任务,查过的东西下次再查又会重新搜索一遍。如果引入一个本地向量库,把历次调研中挖掘到的语料做嵌入存储,下一次检索时可以优先在历史资料里查重,不仅能节省API调用,还能形成持续的行业知识积累。

三是任务调度的并行化。当前的实现是串行处理子任务的,明显浪费了墙钟时间。改成asyncio.gather并行跑四个子任务后,单次调研耗时能压缩到一分钟以内,体验会再上一个台阶。

另外一个很有意思的方向是让AI“追问”用户。比如用户在调研提纲生成之后,AI可以反问一句“你更关注技术角度还是商业角度”,然后根据回答动态调整检索权重。这个功能我已经在设计原型了,目前在prompt层做了简单实现,后续会把它做成更灵活的交互反馈机制。

最后再分享一条我从这个项目里悟到的通用经验:真正好用的AI工具,不是把所有智能都丢给模型,而是把模型放到一个精心设计的流程里去发挥能力。就像管理一个优秀但需要明确分工的团队一样,你把每个人的职责边界和协作方式定义清楚,他们的产出才会稳定可靠。Deep Research Assistant这种“规划—检索—综合—审校”的流水线式设计,不局限于调研任务,换成竞品分析、文章素材收集、产品需求拆解,同样适用。你可以直接复制这套思路,然后替换具体模块的提示词和业务规则,就能快速搭出一个属于自己的垂直场景Agent。

返回列表