芥末
发布于 2025-10-28 / 0 阅读
0
0

基于 Spring AI Alibaba 构建 Java 版 DeepResearch 的架构与实践

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 等工具进行数据处理
RagNodeRAG 节点,从用户文件或知识库中检索相关内容
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: 返回结构化报告

这里有几个关键点:

  1. 问题会被重写和扩展
    用户的问题可能很短,也可能描述不完整。RewriteAndMultiQueryNode 会把原始问题改写成更适合检索的表达,并扩展出多个查询方向,提升搜索覆盖面。

  2. 规划节点负责拆任务
    PlannerNode 不直接生成答案,而是生成步骤。例如先查背景,再查竞品,再查技术路线,再查风险和限制。

  3. 研究组支持并行执行
    ResearchTeamNode 可以并行执行 ResearcherNodeCoderNode。搜索类任务和数据处理类任务互不阻塞,适合耗时较长的研究流程。

  4. 报告节点只做汇总和表达
    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

数据摄取阶段负责把文档变成可检索的向量:

  1. 加载文档;
  2. 切分为更小的文本块;
  3. 使用嵌入模型把文本块转成向量;
  4. 写入向量数据库,同时保存元数据。

查询阶段负责把用户问题转成检索请求:

  1. 对问题做扩展或翻译;
  2. 把问题转换为向量;
  3. 在向量库中搜索相似文本块;
  4. 把检索结果和原始问题一起放进提示词;
  5. 让大语言模型基于上下文生成回答。

混合检索架构

DeepResearch 在 Spring AI 的 VectorStoreRetrievalAugmentationAdvisor 之上做了扩展,核心抽象是 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已有成熟知识库服务,系统只需调用接口
ProfessionalKbEsStrategyElasticsearch需要关键词检索和向量检索结合
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 的取值范围是 -11

权重含义
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 支持为 ResearcherNodeCoderNode 配置 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 预览能力

报告存储有两种实现:

实现存储介质适合场景
ReportRedisServiceRedis生产环境,读写快,便于集中存储
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 导出有两个技术细节需要注意:

  1. Markdown 要先转 HTML
    commonmark-java 负责解析 Markdown,commonmark-ext-gfm-tables 用于支持 GitHub Flavored Markdown(GFM)表格。

  2. 中文字体必须显式处理
    openhtmltopdf 生成 PDF 时,如果没有加载兼容 CJK 的字体,中文可能显示为空白或乱码。

报告相关接口可以整理为:

接口方法作用
/api/reports/{threadId}GET获取原始报告内容
/api/reports/exportPOST发起异步导出任务,支持 pdfmd
/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
JDK17 或以上
Node.js16 或以上
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. 内存存储只适合开发测试

ReportMemoryServiceInMemorySessionContextService 都很适合快速启动,但不适合生产环境。只要涉及多实例部署、应用重启恢复、会话长期保存,就应该换成 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 扩展外部能力,用报告模块形成最终交付物,再通过可观测性把整条链路变得可调试、可追踪、可部署。


评论