DeepResearch 的目标不是简单回答一个问题,而是让系统像研究助理一样完成一条完整链路:理解问题、拆解任务、搜索资料、检索知识库、分析数据、生成结构化报告,并且保留信息来源,方便后续验证。
基于 Spring AI Alibaba Graph 可以把这条链路拆成多个节点,每个节点只负责一类明确的工作。这样做的好处是:复杂任务不再由一个大提示词一次性完成,而是通过工作流把不同能力组合起来,例如搜索、代码执行、RAG(Retrieval-Augmented Generation,检索增强生成)、人类反馈、报告导出和链路观测。
适合这类系统的典型场景包括:
| 场景 | DeepResearch 能做什么 |
|---|---|
| 行业调研 | 搜索公开资料,整理市场、产品、趋势和参考来源 |
| 技术方案分析 | 拆解问题,检索资料,生成架构建议和风险点 |
| 企业知识库问答 | 结合用户上传文档、内部知识库和外部搜索生成答案 |
| 数据分析报告 | 搜集数据后调用 Python 工具进行处理,再生成结论 |
| 多轮研究任务 | 在同一会话里保留历史问题和报告,支持连续追问 |
整体架构:用 Graph 编排多 Agent 研究流程
DeepResearch 的核心是一个由多个节点组成的 StateGraph。每个节点接收当前状态,执行自己的任务,然后把结果写回状态,供后续节点继续使用。
系统主要由 11 个节点组成:
| 节点 | 职责 |
|---|---|
CoordinatorNode | 协调节点,判断用户输入是否需要进入研究流程;如果不是研究型任务,可以直接结束 |
RewriteAndMultiQueryNode | 重写和扩展用户问题,生成多个语义相近但关注点不同的查询 |
BackgroundInvestigationNode | 背景调查节点,根据主题类型调用搜索工具获取初始资料 |
PlannerNode | 规划节点,把研究任务拆成多个可执行步骤 |
InformationNode | 信息判断节点,判断当前资料是否足够支撑后续生成 |
HumanFeedbackNode | 人类反馈节点,允许用户补充限制条件、方向或额外材料 |
ResearchTeamNode | 研究组节点,并行调度研究节点和代码节点 |
ResearcherNode | 研究者节点,继续调用搜索工具和 MCP 工具查找资料 |
CoderNode | 代码节点,调用 Python 等工具进行数据处理 |
RagNode | RAG 节点,从用户文件或知识库中检索相关内容 |
ReporterNode | 报告节点,整合所有中间结果并生成最终报告 |
整体流程可以理解为一条“先判断、再规划、再并行研究、最后汇总”的流水线。
flowchart TD
A[用户问题] --> B[CoordinatorNode<br/>判断任务类型]
B -->|非研究任务| Z[结束或直接回答]
B -->|研究任务| C[RewriteAndMultiQueryNode<br/>问题重写与多查询扩展]
C --> D[BackgroundInvestigationNode<br/>背景调查]
D --> E[PlannerNode<br/>生成研究计划]
E --> F[InformationNode<br/>判断资料是否充足]
F -->|需要用户补充| G[HumanFeedbackNode<br/>用户反馈]
G --> E
F -->|资料可继续处理| H[RagNode<br/>知识库与文件检索]
H --> I[ResearchTeamNode<br/>并行研究组]
I --> J[ResearcherNode<br/>搜索与外部工具]
I --> K[CoderNode<br/>数据处理与代码执行]
J --> L[ReporterNode<br/>生成报告]
K --> L
L --> M[报告存储、导出与预览]
这种设计把大语言模型(Large Language Model,LLM)放在多个节点里使用,而不是让一个模型调用承担全部职责。每个节点有自己的提示词、工具集和状态输入,因此更容易调试,也更容易观察哪一步出了问题。
核心机制一:任务规划与多 Agent 协作
DeepResearch 不是拿到问题后直接生成长答案,而是先把问题变成可执行计划。以“分析某个行业的技术趋势”为例,系统通常会经历这些阶段:
sequenceDiagram
participant U as 用户
participant C as 协调节点
participant R as 重写扩展节点
participant B as 背景调查节点
participant P as 规划节点
participant T as 研究组节点
participant S as 搜索工具
participant Code as 代码工具
participant Rep as 报告节点
U->>C: 提交研究问题
C->>R: 确认进入研究流程
R->>B: 生成多个查询表达
B->>S: 获取背景资料
S-->>B: 返回搜索结果与来源
B->>P: 汇总初始资料
P->>T: 生成研究步骤
par 资料研究
T->>S: 深入搜索
S-->>T: 返回资料
and 数据处理
T->>Code: 执行数据分析
Code-->>T: 返回计算结果
end
T->>Rep: 提交研究结果
Rep-->>U: 返回结构化报告
这里有几个关键点:
-
问题会被重写和扩展
用户的问题可能很短,也可能描述不完整。RewriteAndMultiQueryNode会把原始问题改写成更适合检索的表达,并扩展出多个查询方向,提升搜索覆盖面。 -
规划节点负责拆任务
PlannerNode不直接生成答案,而是生成步骤。例如先查背景,再查竞品,再查技术路线,再查风险和限制。 -
研究组支持并行执行
ResearchTeamNode可以并行执行ResearcherNode和CoderNode。搜索类任务和数据处理类任务互不阻塞,适合耗时较长的研究流程。 -
报告节点只做汇总和表达
ReporterNode不重新发散搜索,而是把前面节点产生的证据、数据、引用来源和分析过程组织成报告。
核心机制二:混合 RAG 检索
RAG(检索增强生成)解决的是“模型不知道业务私有知识”以及“模型回答缺少事实依据”的问题。它的基本做法是:先从知识库或文档里检索相关内容,再把检索结果作为上下文交给大语言模型生成答案。
DeepResearch 中的 RAG 不只支持一种检索方式,而是通过策略模式接入多种数据源,并对多路召回结果进行融合排序。
RAG 的两个阶段
RAG 可以拆成两个阶段:数据摄取和查询检索。
flowchart LR
subgraph Ingestion[文档处理与索引]
A[原始文档<br/>PDF/DOCX/MD 等] --> B[TikaDocumentReader<br/>读取文档]
B --> C[TokenTextSplitter<br/>切分文本块]
C --> D[Embedding Model<br/>生成向量]
D --> E[(VectorStore<br/>向量数据库)]
end
subgraph Retrieval[查询检索与生成]
Q[用户问题] --> QE[查询扩展/翻译]
QE --> V[问题向量化]
V --> E
E --> R[相似性检索]
R --> P[组装 Prompt]
P --> LLM[大语言模型]
LLM --> Ans[增强回答]
end
数据摄取阶段负责把文档变成可检索的向量:
- 加载文档;
- 切分为更小的文本块;
- 使用嵌入模型把文本块转成向量;
- 写入向量数据库,同时保存元数据。
查询阶段负责把用户问题转成检索请求:
- 对问题做扩展或翻译;
- 把问题转换为向量;
- 在向量库中搜索相似文本块;
- 把检索结果和原始问题一起放进提示词;
- 让大语言模型基于上下文生成回答。
混合检索架构
DeepResearch 在 Spring AI 的 VectorStore 和 RetrievalAugmentationAdvisor 之上做了扩展,核心抽象是 HybridRagProcessor。
flowchart TD
A[RagDataController<br/>上传文件/触发摄取] --> B[HybridRagProcessor]
Q[用户查询] --> B
B --> C1[ProfessionalKbApiStrategy<br/>专业知识库 API 检索]
B --> C2[ProfessionalKbEsStrategy<br/>Elasticsearch 混合检索]
B --> C3[UserFileRetrievalStrategy<br/>用户文件检索]
C1 --> D1[外部专业知识库]
C2 --> D2[(Elasticsearch<br/>关键词 + 向量)]
C3 --> D3[(用户文件 VectorStore)]
C1 --> E[RrfFusionStrategy<br/>多路召回融合排序]
C2 --> E
C3 --> E
E --> F[最终文档列表]
F --> G[LLM Prompt 上下文]
G --> H[生成回答或报告]
三类检索策略各自解决不同问题:
| 策略 | 数据来源 | 适合场景 |
|---|---|---|
ProfessionalKbApiStrategy | 外部专业知识库 API | 已有成熟知识库服务,系统只需调用接口 |
ProfessionalKbEsStrategy | Elasticsearch | 需要关键词检索和向量检索结合 |
UserFileRetrievalStrategy | 用户上传文件向量库 | 临时文件、项目资料、用户私有文档 |
HybridRagProcessor 不关心具体数据源如何检索,只负责调用所有已注册的检索策略,再把结果交给融合策略排序。这种结构方便扩展:新增一种数据库、搜索引擎或业务知识库时,只需要实现新的 RetrievalStrategy。
RRF:多路召回结果如何融合
多种检索策略返回的结果可能不一致。关键词检索可能更擅长匹配精确术语,向量检索更擅长匹配语义相近内容,专业知识库可能返回业务上更可靠的资料。
RRF(Reciprocal Rank Fusion,倒数排序融合)用于把多个有序结果列表合并成一个最终排序。它不直接依赖每个检索器的原始分数,而是看文档在各个结果列表中的排名。
常见公式如下:
score(d) = Σ 1 / (k + rank_i(d))
含义是:
d表示某个文档;rank_i(d)表示文档在第i个检索结果列表中的排名;k是平滑参数,用于降低排名差异过大带来的影响;- 一个文档如果在多个检索器中都排得靠前,它的最终得分会更高。
RRF 的优势是简单、稳定,不要求不同检索器的分数具有相同量纲。对于同时使用 API 知识库、Elasticsearch 和用户文件检索的场景,这一点很重要。
RAG 相关配置
RAG 可以通过配置开关启用或禁用:
spring:
ai:
alibaba:
deepresearch:
rag:
enabled: true
向量存储支持本地简单存储和 Elasticsearch 两种方式。
本地向量存储适合开发和轻量部署:
spring:
ai:
alibaba:
deepresearch:
rag:
vector-store-type: simple
simple:
storage-path: ./data/vector-store.json
Elasticsearch 适合数据量更大、需要服务化检索的场景:
spring:
ai:
alibaba:
deepresearch:
rag:
vector-store-type: elasticsearch
elasticsearch:
uris: http://localhost:9200
username: elastic
password: your-password
index-name: deepresearch-rag
数据摄取可以来自三类入口:
| 摄取方式 | 说明 |
|---|---|
| 启动加载 | 应用启动时从 classpath:/data/ 加载文档 |
| 定时扫描 | 按 cron 表达式扫描指定目录,处理完成后归档 |
| 手动上传 | 通过 /api/rag/data/upload 上传文件 |
| API 接入 | 通过接口接入第三方知识库数据 |
定时扫描示例:
spring:
ai:
alibaba:
deepresearch:
rag:
ingestion:
cron: "0 */5 * * * *"
directory: ./rag-data/inbox
archive-directory: ./rag-data/archive
RAG 管道还可以在检索前后处理查询和文档:
spring:
ai:
alibaba:
deepresearch:
rag:
pipeline:
query-expansion-enabled: true
query-translation-enabled: false
query-translation-language: zh
post-processing-select-first-enabled: false
这些开关分别对应:
| 配置 | 作用 |
|---|---|
query-expansion-enabled | 为原始问题生成多个相关查询,提高召回覆盖 |
query-translation-enabled | 把查询翻译成指定语言后再检索 |
post-processing-select-first-enabled | 后处理时只选第一个文档,适合只相信最高排名结果的场景 |
核心机制三:搜索工具与结果过滤
DeepResearch 的背景调查和研究节点都需要调用外部搜索工具。系统支持 Tavily、Serp、百度搜索、阿里云 AI 搜索等服务,也可以启用 JinaCrawler 对搜索结果中的链接继续抓取和解析。
搜索能力的关键不只是“能搜到”,还要控制搜索结果质量。因为搜索引擎返回的页面来源不一定可靠,如果直接把低质量内容交给大语言模型,就可能污染最终回答。
搜索过滤由 SearchFilterService 负责,默认实现是 LocalConfigSearchFilterService。它可以从 JSON 文件读取站点黑白名单,并根据权重过滤或排序。
配置示例:
[
{
"host": "example.com",
"weight": 0.8
},
{
"host": "low-quality.example",
"weight": -1
}
]
weight 的取值范围是 -1 到 1:
| 权重 | 含义 |
|---|---|
1 | 高可信来源,优先保留 |
0 | 中性来源 |
-1 | 不可信来源,即使搜索返回也过滤掉 |
搜索链路可以表示为:
flowchart LR
A[LLM 生成搜索计划] --> B[SearchFilterService]
B --> C[选择 SearchService]
C --> D[调用 Tavily/Serp/百度/阿里云 AI 搜索]
D --> E[JinaCrawler 可选抓取正文]
E --> F[按黑白名单过滤排序]
F --> G[返回可信搜索结果]
G --> H[进入研究节点上下文]
生产环境里建议把搜索结果过滤当成必要环节,而不是可选功能。尤其在开放领域研究任务中,站点可信度会直接影响报告质量。
核心机制四:MCP 动态扩展工具能力
MCP(Model Context Protocol,模型上下文协议)用于把外部工具和服务接入大语言模型调用流程。DeepResearch 支持为 ResearcherNode 和 CoderNode 配置 MCP 服务,让研究节点可以调用地图、数据库、搜索、业务系统等外部能力。
MCP 有两种配置方式:
| 方式 | 适合场景 |
|---|---|
| 静态配置 | 服务固定,例如始终启用某个地图或数据库工具 |
| 动态请求配置 | 每次请求需要不同工具,例如用户临时指定 MCP 服务 |
启用 MCP 需要打开两个配置:
spring:
ai:
mcp:
client:
enabled: true
alibaba:
deepresearch:
mcp:
enabled: true
静态配置可以写在 mcp-config.json 中:
{
"researchAgent": {
"mcp-servers": [
{
"url": "https://mcp.amap.com?key=${AMAP_API_KEY}",
"sse-endpoint": "/sse",
"description": "高德地图位置服务",
"enabled": true
}
]
},
"coderAgent": {
"mcp-servers": []
}
}
动态配置则可以在调用 /chat/stream 时通过 mcp_settings 字段传入。后端会使用 McpProviderFactory 把配置组装成 AsyncMcpToolCallbackProvider,再交给 ChatClient 在当前会话里使用。
flowchart TD
A[/chat/stream 请求] --> B{是否携带 mcp_settings}
B -->|否| C[加载静态 mcp-config.json]
B -->|是| D[解析请求中的 MCP 配置]
C --> E[McpProviderFactory]
D --> E
E --> F[AsyncMcpToolCallbackProvider]
F --> G[ChatClient]
G --> H[ResearcherNode / CoderNode 调用外部工具]
这种设计让工具能力不必写死在系统里。固定能力可以通过配置文件常驻,临时能力可以在请求级别注入。
核心机制五:报告生成、存储与导出
DeepResearch 的最终产物通常是一份结构化报告,而不是一段短回答。报告模块负责把研究结果持久化,并提供 Markdown、PDF 和交互式 HTML 等输出方式。
报告模块主要由三个部分组成:
| 组件 | 职责 |
|---|---|
ReportService | 管理报告生命周期,包括保存、读取、判断存在、删除 |
ExportService | 把报告导出为 Markdown 或 PDF |
ReportController | 通过 REST API 暴露报告查询、导出、下载和 HTML 预览能力 |
报告存储有两种实现:
| 实现 | 存储介质 | 适合场景 |
|---|---|---|
ReportRedisService | Redis | 生产环境,读写快,便于集中存储 |
ReportMemoryService | 内存 ConcurrentHashMap | 开发测试,启动简单,但应用重启后数据丢失 |
每份报告通过 threadId 标识,Redis 中的 Key 可以采用这种形式:
report:{threadId}
报告导出流程如下:
flowchart TD
A[用户发起导出请求] --> B[ReportController]
B --> C[ExportService]
C --> D[ReportService 获取 Markdown 报告]
D --> E{导出格式}
E -->|Markdown| F[FileOperationUtil<br/>写入 .md 文件]
E -->|PDF| G[commonmark-java<br/>Markdown 转 HTML]
G --> H[commonmark-ext-gfm-tables<br/>支持 GFM 表格]
H --> I[openhtmltopdf<br/>HTML 转 PDF]
I --> J[加载 CJK 字体<br/>支持中文显示]
F --> K[生成下载文件]
J --> K
K --> L[返回下载链接]
PDF 导出有两个技术细节需要注意:
-
Markdown 要先转 HTML
commonmark-java负责解析 Markdown,commonmark-ext-gfm-tables用于支持 GitHub Flavored Markdown(GFM)表格。 -
中文字体必须显式处理
openhtmltopdf生成 PDF 时,如果没有加载兼容 CJK 的字体,中文可能显示为空白或乱码。
报告相关接口可以整理为:
| 接口 | 方法 | 作用 |
|---|---|---|
/api/reports/{threadId} | GET | 获取原始报告内容 |
/api/reports/export | POST | 发起异步导出任务,支持 pdf 和 md |
/api/reports/download/{threadId} | GET | 下载已生成的报告文件 |
/api/reports/interactive-html/{threadId} | GET | 基于报告内容流式生成交互式 HTML |
核心机制六:连续对话上下文
DeepResearch 支持同一会话内的连续提问。为了区分“长期会话”和“单次执行”,系统使用 GraphId 表示一次请求的身份信息。
GraphId 包含两个关键字段:
| 字段 | 含义 |
|---|---|
sessionId | 长期会话标识,同一个对话窗口共享同一个 sessionId |
threadId | 单次任务标识,每次工作流执行通常对应一个新的 threadId |
上下文由 SessionContextService 管理。它根据当前请求的 sessionId 获取最近几次历史请求和报告,再注入到特定节点的模型请求中。
flowchart TD
A[用户连续追问] --> B[生成 GraphId]
B --> C[提取 sessionId 和 threadId]
C --> D[SessionContextService]
D --> E[获取最近会话历史 SessionHistory]
E --> F[注入 CoordinatorNode]
E --> G[注入 BackgroundInvestigationNode]
F --> H[LLM 理解当前问题与历史上下文]
G --> H
H --> I[执行新一轮研究流程]
默认实现是 InMemorySessionContextService,它把历史记录保存在应用内存里。这种方式启动简单、访问速度快,但有两个限制:
| 限制 | 影响 |
|---|---|
| 应用重启后历史丢失 | 不适合需要长期保存上下文的场景 |
| 多实例之间无法共享内存 | 不适合横向扩展部署 |
生产环境可以自定义 SessionContextService,把会话历史存到 Redis、数据库或其他集中式存储中。
可观测性:用 Langfuse 追踪调用链路
DeepResearch 的工作流节点多,模型调用、搜索调用和工具调用也多。如果没有可观测能力,排查问题会比较困难。例如报告质量不稳定时,需要知道是搜索结果不够好、RAG 召回不准、规划节点拆错了任务,还是报告节点整合出了问题。
Spring AI Alibaba Graph 支持工作流观测,DeepResearch 可以接入 Langfuse 记录调用链路。接入后可以观察:
| 观测对象 | 排查价值 |
|---|---|
| 节点执行顺序 | 判断 Graph 是否按预期流转 |
| 每个节点的输入输出 | 定位错误上下文从哪里开始出现 |
| LLM 调用参数 | 检查模型、提示词、温度等配置 |
| 工具调用结果 | 判断搜索、MCP、代码执行是否成功 |
| Token 与耗时 | 分析成本和性能瓶颈 |
对复杂 Agent 工作流来说,可观测性不是锦上添花,而是调试和上线的基础设施。
部署方式一:Docker 构建完整项目
Docker 部署适合快速启动完整环境。项目使用多阶段构建:
| 阶段 | 镜像与任务 |
|---|---|
| 前端构建 | 使用 Node.js 21 Alpine,安装 pnpm,执行前端构建 |
| 后端构建 | 使用 Dragonwell JDK 17 Ubuntu,安装 Maven,构建后端 JAR |
| 运行时镜像 | 使用 Dragonwell JDK 17 Ubuntu,安装 Nginx 和 Supervisor,运行前后端产物 |
在项目根目录创建环境变量文件:
cd spring-ai-alibaba-deepresearch
mkdir -p dockerConfig
touch dockerConfig/.env
.env 示例:
# 百炼 API Key
AI_DASHSCOPE_API_KEY=<AI_DASHSCOPE_API_KEY>
# 报告导出目录,填写容器可访问的路径
AI_DEEPRESEARCH_EXPORT_PATH=<AI_DEEPRESEARCH_EXPORT_PATH>
# Tavily 搜索 API Key
TAVILY_API_KEY=<TAVILY_API_KEY>
# Langfuse 认证信息
YOUR_BASE64_ENCODED_CREDENTIALS=<YOUR_BASE64_ENCODED_CREDENTIALS>
构建并运行镜像:
docker build -t spring-ai-deepresearch .
docker run \
--env-file ./dockerConfig/.env \
-p 8080:80 \
--name deepresearch \
-d spring-ai-deepresearch
如果端口映射是 -p 8080:80,访问地址就是:
http://localhost:8080/
如果希望使用 8081 端口,可以改成:
docker run \
--env-file ./dockerConfig/.env \
-p 8081:80 \
--name deepresearch \
-d spring-ai-deepresearch
对应访问地址为:
http://localhost:8081/
部署方式二:本地开发启动
本地开发通常把中间件放在 Docker 里,后端用 IDE 启动,前端用 Vite 启动。
需要准备:
| 组件 | 版本要求 |
|---|---|
| Docker | 可运行 Compose |
| JDK | 17 或以上 |
| Node.js | 16 或以上 |
| pnpm | 与前端项目匹配 |
启动中间件
cd spring-ai-alibaba-deepresearch
docker compose -f docker-compose-middleware.yml up -d
该方式通常只启动 Redis 和 Elasticsearch。
编译后端
cd spring-ai-alibaba-deepresearch
mvn clean install -DskipTests
在 IDE 的运行配置中设置环境变量,例如:
AI_DASHSCOPE_API_KEY=<AI_DASHSCOPE_API_KEY>
TAVILY_API_KEY=<TAVILY_API_KEY>
AI_DEEPRESEARCH_EXPORT_PATH=./exports
然后启动后端应用。
启动前端
cd spring-ai-alibaba-deepresearch/ui-vue3
pnpm install
pnpm run dev
前端 .env 中的 VITE_BASE_URL 用来配置后端地址。可以直接写完整后端 URL:
VITE_BASE_URL=http://127.0.0.1:8080
也可以写相对路径:
VITE_BASE_URL=/deep-research
使用相对路径时,需要在 vite.config.ts 中配置代理:
export default {
server: {
proxy: {
'/deep-research': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/deep-research/, '')
}
}
}
}
前端启动后访问:
http://localhost:5173/ui
实践中的几个注意点
1. RAG 不是检索结果越多越好
召回太少会导致信息不足,召回太多会把噪声塞进上下文。更稳妥的做法是:
- 用多查询扩展提高覆盖面;
- 用站点权重过滤低质量来源;
- 用 RRF 融合多路召回;
- 控制最终进入 Prompt 的文档数量;
- 保留来源信息,方便用户验证。
2. 内存存储只适合开发测试
ReportMemoryService 和 InMemorySessionContextService 都很适合快速启动,但不适合生产环境。只要涉及多实例部署、应用重启恢复、会话长期保存,就应该换成 Redis 或数据库实现。
3. MCP 动态配置要做好安全控制
动态 MCP 能力很灵活,但也意味着用户可以在请求中引入外部服务。生产环境需要考虑:
- 服务白名单;
- 请求超时;
- 工具调用权限;
- 参数校验;
- 调用日志与审计。
4. 搜索来源要有可信度策略
开放搜索结果质量差异很大。对研究类系统来说,黑白名单和权重不是装饰配置,而是影响输出可信度的关键环节。
5. PDF 导出要提前验证中文字体
Markdown 转 PDF 经常在中文字体上出问题。部署镜像里应包含可用的 CJK 字体,并在 openhtmltopdf 转换时显式加载,否则导出的 PDF 可能出现乱码或缺字。
项目入口
项目代码可以从 GitHub 获取:
https://github.com/alibaba/spring-ai-alibaba
Spring AI Alibaba DeepResearch 的关键价值在于把 Agent 能力工程化:用 Graph 管理复杂流程,用 RAG 接入私有知识,用搜索和 MCP 扩展外部能力,用报告模块形成最终交付物,再通过可观测性把整条链路变得可调试、可追踪、可部署。