一、项目概览

1.1 概述

ChatFlow 是一个基于 Spring AI Alibaba Graph 框架构建的"智能待办事项助手"。

它能听懂你说的话,判断你是想"创建待办"还是"随便聊聊",然后分别走不同的处理流程。如果是创建待办,它会调用大模型帮你润色描述,最终生成规范的任务条目。

1.2 学习价值

在 Spring AI Alibaba Graph 的所有示例中,chatflow 是承上启下的关键节点

示例项目 难度 核心能力 学习价值
stream-node ⭐ 入门 简单的线性流程 了解 Graph 基础概念
chatflow ⭐⭐ 初级 多轮对话 + 意图识别 + 子图调用 掌握 Graph 核心用法
big-tool ⭐⭐⭐ 中级 工具调用(Function Calling) 扩展 AI 能力边界
parallel-stream-node ⭐⭐⭐ 中级 并行处理 提升执行效率
human-node ⭐⭐⭐⭐ 高级 人类介入决策 构建人机协作系统

💡 学习建议:如果你已经跑通了 stream-node,那么 chatflow 就是你必须攻克的下一个关卡。它涵盖了 Graph 框架 80% 的核心概念。

1.3 项目能做什么?

对话场景:

【第1轮】
用户:待办:学习TypeScript
助手:已创建任务:学习 TypeScript 编程语言,掌握其类型系统和高级特性。

【第2轮】  
用户:再帮我记一个,用Ts做一个小demo
助手:已创建任务:使用 TypeScript 开发一个实际的小型演示项目。

【第3轮】
用户:我有哪些待办?
助手:你当前待办有:
  1. 学习 TypeScript 编程语言,掌握其类型系统和高级特性。
  2. 使用 TypeScript 开发一个实际的小型演示项目。

【第4轮】
用户:今天天气怎么样?
助手:你当前待办有:[...]
闲聊回复:抱歉,我无法获取实时天气信息,建议您查看天气预报应用。

核心能力拆解:

能力 说明 技术实现
多轮记忆 同一个 sessionId 的对话,待办列表会跨轮累加 OverAllState + threadId 状态隔离
意图识别 自动判断用户想"创建待办"还是"闲聊" QuestionClassifierNode 分类节点
子图隔离 创建待办的逻辑封装在独立子图中 SubGraphNode 子图调用
LLM 润色 把口语化的输入变成规范的待办描述 LlmNode 大模型节点

二、核心原理:Graph 框架的灵魂

在深入代码之前,我们先花 5 分钟理解 Graph 框架的三个核心概念。这就像学开车前先理解"油门、刹车、方向盘"一样重要。

2.1 状态(State):整个系统的"记忆体"

Graph 框架中,State(状态)是整个图共享的数据存储,相当于一个全局变量池。

┌─────────────────────────────────────────┐
│           OverAllState(状态池)          │
│                                         │
│  session_id  →  "user-123"              │
│  user_input  →  "待办:学习Java"         │
│  intent_type →  "创建待办"               │
│  tasks       →  ["学习Python", ...]     │
│  chat_reply  →  ""                      │
│  ...                                     │
└─────────────────────────────────────────┘

关键特性:

  • 每个节点都可以读写状态:节点 A 把结果写入 todo_desc,节点 B 可以读取它
  • 状态有生命周期策略:通过 KeyStrategy 控制是"覆盖"还是"追加"
  • 跨轮持久化:配合 threadId,状态可以在多次调用间保持

2.2 节点(Node):流程中的"工作站"

节点是执行具体任务的单元,每个节点只做一件事,做好一件事。

Graph 框架提供了多种预制节点,像乐高积木一样可以直接使用:

节点类型 作用 类比
LlmNode 调用大语言模型,生成/处理文本 智能大脑
QuestionClassifierNode 对输入进行分类,判断意图 分拣员
AnswerNode 按模板组装最终回复 包装工
AssignerNode 在状态间复制/转移数据 搬运工
ToolNode 调用外部工具/函数 手艺人

节点的执行逻辑:

// 每个节点都遵循相同的"契约":接收状态,返回修改后的状态
Map<String, Object> apply(OverAllState state) {
    // 1. 从状态中读取输入
    String input = state.value("user_input").orElse("");

    // 2. 执行业务逻辑
    String result = doSomething(input);

    // 3. 把结果写回状态
    return Map.of("output_key", result);
}

2.3 边(Edge):连接节点的"道路"

边定义了节点之间的连接关系和流程走向。

有两种边:

普通边(Edge):固定连接,A → B

mainGraph.addEdge("nodeA", "nodeB");  // 执行完 A,一定去 B

条件边(Conditional Edge):动态路由,根据状态决定走向

mainGraph.addConditionalEdges("intent", 
    edge_async(state -> {
        // 根据 intent_type 的值决定下一步
        return state.value("intent_type").orElse("chat");
    }),
    Map.of("创建待办", "callSubGraph", "其它", "chat")
);

条件边是 Graph 框架的精髓——它让流程不再是死板的直线,而是能根据数据动态决策的智能网络。

2.4 子图(SubGraph):模块化设计的利器

子图是一个独立的 StateGraph,可以嵌入到主图中使用。

为什么要用子图?想象你在搭建一个复杂工厂:

  • 主图是总装车间,负责调度整体流程
  • 子图是专门的加工车间(如"待办创建车间"),有自己的设备和工人

子图的核心优势:

  1. 状态隔离:子图有自己的 threadId 和变量池,不会污染主图
  2. 逻辑复用:同一个子图可以在多个主图中使用
  3. 复杂度拆分:把大流程拆成小块,降低心智负担
┌─────────────────────────────────────────────────────────────┐
│                        主图(Main Graph)                     │
│                                                             │
│   START → [意图识别] ──┬── "创建待办" → [调用子图] ──▶ ...   │
│                      └── "其它" → [闲聊处理] ──▶ ...        │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              子图(Sub Graph)                        │   │
│   │                                                     │   │
│   │   START → [LLM润色] → [赋值] → [生成回复] → END      │   │
│   │                                                     │   │
│   │   (独立状态池:task_content, todo_desc, created_task)│   │
│   └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

三、技术架构:一张图看懂全貌

3.1 系统交互流程

大模型服务 子图 SubGraph 意图识别节点 主图 StateGraph TodoChatflowController 客户端 大模型服务 子图 SubGraph 意图识别节点 主图 StateGraph TodoChatflowController 客户端 POST /assistant/chat sessionId=123&userInput=待办:学习Java 1 compiledGraph.invoke(state) 2 执行意图识别节点 3 发送分类请求 4 返回 "创建待办" 5 更新 intent_type="创建待办" 6 调用子图(独立threadId) 7 发送润色请求 8 返回规范描述 9 AssignerNode 赋值 10 返回 created_task 11 AnswerNode 组装最终回复 12 返回完整状态 13 JSON 响应 14

3.2 主图结构详解

创建待办

其它

START

intent
意图识别节点

callSubGraph
调用子图节点

chat
闲聊节点

mainReply
主流程答复节点

END

3.3 子图结构详解

START

llm
LLM润色节点

assign
赋值节点

answer
回答节点

END

3.4 数据流转全景图

🔄 处理流程

⚙️ 子图状态池(隔离处理)

🧠 主图状态池

📥 客户端请求

1. 提交请求

2. 输入解析

3. 文本分析

4. 标记类型

5. 触发子图

6. 写入内容

7. 内容提取

8. 智能润色

9. 结构化处理

10. 创建任务

11. 合并结果

12. 读取现有任务

13. 读取历史回复

14. 生成回复

sessionId: '123'
userInput: '待办:学习Java'
timestamp: '2026-05-06'

session_id
(string)

user_input
(text)

intent_type
(enum)

tasks
(array)

chat_reply
(string)

task_content
(raw text)

todo_desc
(processed)

created_task
(structured)

🎯 意图识别引擎

🔧 待办创建服务

✨ LLM智能润色

📋 任务结构化

📤 AnswerNode

📤 最终响应


四、代码实战:主图与子图的编排艺术

4.1 主图配置:TodoChatFlowFactory

这是整个项目最核心的文件,主图就像乐高的底板,所有节点都安装在上面。

public static CompiledGraph build(ChatClient chatClient, CompiledGraph subGraph) throws Exception {

    // ═══════════════════════════════════════════════════════
    // 第1步:定义状态策略工厂
    // ═══════════════════════════════════════════════════════
    // 每个状态变量都需要指定"更新策略":是覆盖旧值,还是追加到列表?
    KeyStrategyFactory keyStrategyFactory = new KeyStrategyFactoryBuilder()
            .addPatternStrategy("session_id", new ReplaceStrategy())   // 会话ID:每次覆盖
            .addPatternStrategy("user_input", new ReplaceStrategy())   // 用户输入:每次覆盖
            .addPatternStrategy("intent_type", new ReplaceStrategy())  // 意图类型:每次覆盖
            .addPatternStrategy("chat_reply", new ReplaceStrategy())   // 闲聊回复:每次覆盖
            .addPatternStrategy("tasks", new ReplaceStrategy())        // 待办列表:每次覆盖
            .addPatternStrategy("created_task", new ReplaceStrategy()) // 创建的任务:每次覆盖
            .addPatternStrategy("answer", new ReplaceStrategy())       // 最终答案:每次覆盖
            .build();

    StateGraph mainGraph = new StateGraph("chatFlow-demo", keyStrategyFactory);

    // ═══════════════════════════════════════════════════════
    // 第2步:添加闲聊节点(Lambda 动态创建)
    // ═══════════════════════════════════════════════════════
    // ⚠️ 关键设计:为什么用 Lambda 而不是直接传入 LlmNode 实例?
    // 
    // 如果直接传入实例:mainGraph.addNode("chat", node_async(llmNodeInstance))
    // 问题:LlmNode 内部的参数在第一次执行后会被"固化",多轮对话时
    //       会重复使用旧的参数,导致结果错乱。
    // 
    // Lambda 方案:每次执行节点时都 new 一个新的 LlmNode,保证参数新鲜。
    mainGraph.addNode("chat", node_async(state -> {
        LlmNode node = LlmNode.builder()
                .userPromptTemplate("{user_input}")  // 模板:从状态中取 user_input
                .params(Map.of("user_input", "null")) // 初始参数占位
                .outputKey("chat_reply")              // 结果存入 chat_reply
                .chatClient(chatClient)
                .build();
        return node.apply(state);  // 执行节点,返回结果
    }));

    // ═══════════════════════════════════════════════════════
    // 第3步:添加意图分类节点
    // ═══════════════════════════════════════════════════════
    // QuestionClassifierNode 是框架预制组件,专门做"选择题"
    // 它会把用户输入发给大模型,让模型选择最匹配的类别
    QuestionClassifierNode intentClassifier = QuestionClassifierNode.builder()
            .chatClient(chatClient)
            .inputTextKey("user_input")     // 从状态中读取用户输入
            .categories(List.of("创建待办", "其它"))  // 两个选项
            .classificationInstructions(List.of(
                "判断用户是否想创建一个待办事项。"
                + "直接返回'创建待办'或'其它',不要返回 JSON,不要加任何标点符号"
            ))
            .outputKey("intent_type")       // 结果存入 intent_type
            .build();
    mainGraph.addNode("intent", node_async(intentClassifier));

    // ═══════════════════════════════════════════════════════
    // 第4步:添加子图调用节点
    // ═══════════════════════════════════════════════════════
    // 这是主图和子图的"桥梁"
    NodeAction callSubGraphNode = (OverAllState state) -> {
        // 从主图状态中获取 sessionId,作为子图的"根线程ID"
        String mainThreadId = (String) state.value("session_id")
                .orElseThrow(() -> new IllegalArgumentException("sessionId 不能为空"));

        // 生成唯一的子图线程ID,确保每次调用都是独立的
        // 格式:主线程ID + "-todo-" + UUID
        // 例如:user-123-todo-a1b2c3d4
        String subThreadId = mainThreadId + "-todo-" + UUID.randomUUID();

        // 创建子图的运行配置
        RunnableConfig subConfig = RunnableConfig.builder()
                .threadId(subThreadId)
                .build();

        // 从主图状态中提取子图需要的输入
        String taskContent = (String) state.value("user_input").orElse("");

        // 调用子图!这是关键的一行
        OverAllState subResult = subGraph.invoke(
            Map.of("task_content", taskContent), 
            subConfig
        );

        // 从子图结果中提取创建的任务,返回给主图
        String createdTask = (String) subResult.value("created_task").orElse("");
        return Map.of("created_task", createdTask);
    };
    mainGraph.addNode("callSubGraph", node_async(callSubGraphNode));

    // ═══════════════════════════════════════════════════════
    // 第5步:添加主流程答复节点
    // ═══════════════════════════════════════════════════════
    // AnswerNode 像模板引擎,把状态中的变量填充到模板里
    AnswerNode mainReply = AnswerNode.builder()
            .answer("你当前待办有:{tasks}\n闲聊回复:{chat_reply}")
            .build();
    mainGraph.addNode("mainReply", node_async(mainReply));

    // ═══════════════════════════════════════════════════════
    // 第6步:连接节点(构建流程图)
    // ═══════════════════════════════════════════════════════
    mainGraph.addEdge(StateGraph.START, "intent");  // 起点 → 意图识别

    // 条件边:根据 intent_type 的值分流
    mainGraph.addConditionalEdges("intent", 
        edge_async(state -> {
            return state.value("intent_type").orElse("其它");
        }), 
        Map.of(
            "创建待办", "callSubGraph",  // 如果意图是"创建待办",走子图
            "其它", "chat"              // 否则走闲聊节点
        )
    );

    mainGraph.addEdge("callSubGraph", "mainReply");  // 子图 → 答复
    mainGraph.addEdge("chat", "mainReply");          // 闲聊 → 答复
    mainGraph.addEdge("mainReply", StateGraph.END);  // 答复 → 终点

    return mainGraph.compile();
}
✅ 设计亮点解析
设计点 说明 好处
Lambda 动态创建 节点内部每次 new LlmNode() 避免多轮参数卡死,保证状态新鲜
条件分支 addConditionalEdges 实现动态路由 一个入口,多个出口,智能分流
子图隔离 独立 threadId + 独立状态池 主图子图互不干扰,逻辑清晰
策略配置 ReplaceStrategy 控制状态更新 显式声明数据行为,避免意外覆盖
⚠️ 常见问题与优化建议

问题1:意图识别返回 JSON 格式

原代码中有大段 JSON 解析逻辑(trim()replaceFirst("^```json") 等),这说明模型输出不稳定,需要"清洗"。

// ❌ 不推荐:依赖复杂的后处理
String intentRaw = ...;
if (intentRaw.startsWith("```json")) {
    intentRaw = intentRaw.replaceFirst("^```json", "").trim();
}
// 还要处理各种边界情况...

// ✅ 推荐:从源头解决问题——优化提示词
.classificationInstructions(List.of(
    "判断用户是否想创建一个待办事项。"
    + "直接返回'创建待办'或'其它',不要返回 JSON,不要加任何标点符号,"
    + "不要添加解释,只返回这两个词之一"
))

💡 原理:大模型是"提示词驱动"的。提示词越明确,输出越稳定。后处理只是补救措施,好的提示词才是根本解决之道。

问题2:sessionId 缺少校验

// ❌ 不推荐:静默使用默认值
String mainThreadId = (String) state.value("session_id").orElse("user-001");
// 问题:如果传了空字符串,也会用默认值,可能导致不同用户数据串扰

// ✅ 推荐:显式校验,快速失败
String mainThreadId = (String) state.value("session_id")
    .filter(s -> !s.toString().trim().isEmpty())
    .orElseThrow(() -> new IllegalArgumentException("sessionId 不能为空或空白"));

4.2 子图配置:TodoSubGraphFactory

子图负责处理"创建待办"这个具体任务,它完全独立于主图。

public static CompiledGraph build(ChatClient chatClient) throws Exception {
    // 子图有自己的状态策略,只关心待办相关的变量
    KeyStrategyFactory keyStrategyFactory = new KeyStrategyFactoryBuilder()
            .addPatternStrategy("task_content", new ReplaceStrategy())  // 原始输入
            .addPatternStrategy("todo_desc", new ReplaceStrategy())     // 润色后的描述
            .addPatternStrategy("created_task", new ReplaceStrategy())  // 最终创建的任务
            .build();

    StateGraph subGraph = new StateGraph("create-todo-subgraph", keyStrategyFactory);

    // ═══════════════════════════════════════════════════════
    // 节点1:LLM 润色
    // ═══════════════════════════════════════════════════════
    // 把用户的口语化输入转换成规范的待办描述
    subGraph.addNode("llm", node_async(state -> {
        LlmNode node = LlmNode.builder()
                .userPromptTemplate(
                    "请把以下待办事项润色成规范的一句话描述,"
                    + "保持原意但表达更专业、更具体:\n"
                    + "{task_content}"
                )
                .params(Map.of("task_content", "null"))
                .outputKey("todo_desc")
                .chatClient(chatClient)
                .build();
        return node.apply(state);
    }));

    // ═══════════════════════════════════════════════════════
    // 节点2:赋值
    // ═══════════════════════════════════════════════════════
    // AssignerNode 是"搬运工":把 todo_desc 的值复制到 created_task
    // 为什么要多这一步?为了语义清晰:todo_desc 是中间产物,created_task 是最终结果
    AssignerNode assignNode = AssignerNode.builder()
            .addItem("created_task", "todo_desc", AssignerNode.WriteMode.OVER_WRITE)
            .build();
    subGraph.addNode("assign", node_async(assignNode));

    // ═══════════════════════════════════════════════════════
    // 节点3:生成回答
    // ═══════════════════════════════════════════════════════
    AnswerNode answerNode = AnswerNode.builder()
            .answer("已创建任务:{todo_desc}")
            .build();
    subGraph.addNode("answer", node_async(answerNode));

    // ═══════════════════════════════════════════════════════
    // 连接子图节点(线性流程)
    // ═══════════════════════════════════════════════════════
    subGraph.addEdge(StateGraph.START, "llm");
    subGraph.addEdge("llm", "assign");
    subGraph.addEdge("assign", "answer");
    subGraph.addEdge("answer", StateGraph.END);

    return subGraph.compile();
}
子图设计的精妙之处

子图内部(对主图不可见)

主图视角

输入:task_content

输出:created_task

封装

封装

调用子图

黑盒子

继续主流程

task_content

LLM润色

todo_desc

赋值

created_task

主图完全不需要知道子图内部怎么处理,它只关心:

  • 输入:task_content
  • 输出:created_task

这就是模块化设计的魅力——高内聚、低耦合。

4.3 关键类速查表

类名 作用 使用场景
StateGraph 状态图定义器 构建流程图,添加节点和边
CompiledGraph 编译后的可执行图 调用 invoke() 执行流程
OverAllState 状态载体 在节点间传递数据
LlmNode 大模型调用节点 需要 AI 生成/处理文本时
QuestionClassifierNode 意图分类节点 做选择题(分类/路由)
AnswerNode 模板回答节点 组装固定格式的回复
AssignerNode 变量赋值节点 状态间复制数据
ReplaceStrategy 覆盖策略 新值覆盖旧值
AppendStrategy 追加策略 新值添加到列表

五、配置与接口:让项目跑起来

5.1 核心依赖(pom.xml)

<!-- 阿里云 DashScope 大模型服务 -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>

<!-- Spring AI 自动配置 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>

<!-- Graph 框架核心 -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-graph-core</artifactId>
</dependency>

5.2 配置文件(application.yml)

server:
  port: 8080

spring:
  application:
    name: spring-ai-alibaba-graph-chatflow-example

  ai:
    dashscope:
      # 从环境变量读取 API Key,安全最佳实践
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus  # 使用 qwen-plus,性价比高

💡 模型选择建议

  • qwen-max:能力最强,适合复杂推理,但价格较高
  • qwen-plus:均衡之选,适合对话场景(本项目使用)
  • qwen-turbo:速度最快,适合简单任务

5.3 接口定义

项目 说明
路径 POST /assistant/chat
Content-Type application/x-www-form-urlencoded
认证 无(示例项目简化处理)

请求参数:

参数 类型 必填 说明
sessionId String 会话ID,用于隔离不同用户/会话
userInput String 用户输入的文本

响应格式:

{
  "reply": "你当前待办有:[学习 TypeScript 相关知识。, 使用 TypeScript 创建一个小的演示项目。]\n闲聊回复:",
  "tasks": [
    "学习 TypeScript 相关知识。",
    "使用 TypeScript 创建一个小的演示项目。"
  ]
}

5.4 测试用例

# 第1轮:创建待办
# 预期:识别为"创建待办",调用子图润色,返回规范描述
curl -X POST "http://localhost:8080/assistant/chat?sessionId=123&userInput=待办:学习TypeScript"

# 第2轮:再创建一个(同一个 sessionId,测试多轮记忆)
# 预期:待办列表累加
curl -X POST "http://localhost:8080/assistant/chat?sessionId=123&userInput=待办:用Ts做一个小demo"

# 第3轮:查询待办(测试状态持久化)
# 预期:返回当前所有待办
curl -X POST "http://localhost:8080/assistant/chat?sessionId=123&userInput=我有哪些待办?"

# 第4轮:闲聊(测试意图识别分流)
# 预期:走 chat 节点,不创建待办
curl -X POST "http://localhost:8080/assistant/chat?sessionId=123&userInput=简单介绍下Spring Cloud"

# 第5轮:新用户(不同 sessionId,测试隔离性)
# 预期:待办列表为空
curl -X POST "http://localhost:8080/assistant/chat?sessionId=456&userInput=我有哪些待办?"

六、部署实操:从本地到生产

6.1 前置准备检查清单

# 检查 Java(需要 17 或 21)
java -version

# 检查 Maven
mvn -version

# 获取阿里云 API Key
# 1. 访问 https://dashscope.console.aliyun.com/
# 2. 登录后进入「API-KEY 管理」
# 3. 创建 Key 并复制(格式:sk-xxxxxxxx)

6.2 本地开发环境启动

# 进入项目目录
cd spring-ai-alibaba-graph-example/chatflow

# 设置环境变量(推荐方式,不写入代码)
export AI_DASHSCOPE_API_KEY=sk-你的Key

# 启动服务
mvn spring-boot:run

启动成功标志:

Started ChatFlowApplication in X.XXX seconds
Tomcat started on port: 8080

6.3 生产部署方案

方案A:Systemd 服务(推荐)
# 1. 打包
mvn clean package -DskipTests

# 2. 上传到服务器
scp target/chatflow-*.jar user@server:/opt/chatflow/

# 3. 创建 Systemd 服务文件
sudo tee /etc/systemd/system/chatflow.service > /dev/null <<'EOF'
[Unit]
Description=Spring AI Graph Chatflow
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/chatflow
ExecStart=/usr/bin/java -jar /opt/chatflow/chatflow-*.jar
Environment="AI_DASHSCOPE_API_KEY=sk-你的Key"
Restart=always
RestartSec=10
StandardOutput=append:/var/log/chatflow.log
StandardError=append:/var/log/chatflow.log

[Install]
WantedBy=multi-user.target
EOF

# 4. 启动并设置开机自启
sudo systemctl daemon-reload
sudo systemctl start chatflow
sudo systemctl enable chatflow
sudo systemctl status chatflow
方案B:Docker 部署
# Dockerfile
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache maven && mvn clean package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 构建并运行
docker build -t chatflow:latest .
docker run -d   --name chatflow   -p 8080:8080   -e AI_DASHSCOPE_API_KEY=sk-你的Key   chatflow:latest

6.4 Nginx 反向代理 + HTTPS

# /etc/nginx/sites-available/chatflow
server {
    listen 80;
    server_name your-domain.com;

    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 支持 WebSocket(如果后续需要)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

6.5 部署验证清单

检查项 命令 预期结果
服务运行状态 systemctl status chatflow Active (running)
端口监听 ss -tlnp | grep 8080 LISTEN
本地访问 curl -X POST "http://localhost:8080/assistant/chat?sessionId=test&userInput=待办:测试" JSON 响应
外网访问 curl -X POST "http://你的域名/assistant/chat?sessionId=test&userInput=待办:测试" JSON 响应
日志检查 tail -f /var/log/chatflow.log 无 ERROR

七、常见问题与扩展建议

7.1 常见问题排查

Q1:意图识别不准确

现象:用户说"帮我记个事",被识别为"其它"而不是"创建待办"。

原因:提示词不够全面,没有覆盖同义表达。

解决

.classificationInstructions(List.of(
    "判断用户意图。以下情况都属于'创建待办':"
    + "1. 明确说'待办'、'todo'、'任务'"
    + "2. 说'帮我记一下'、'提醒我'、'记住'"
    + "3. 说'我要做...'、'计划...'"
    + "4. 其他明确的任务创建意图"
    + "如果不符合以上任何一条,返回'其它'"
    + "直接返回'创建待办'或'其它',不要加任何解释"
))
Q2:待办列表没有累加

现象:每次创建待办,之前的待办都消失了。

原因tasks 使用了 ReplaceStrategy,每次都会覆盖。

解决

// 改用 AppendStrategy,新任务追加到列表
.addPatternStrategy("tasks", new AppendStrategy())

// 同时在子图返回时,确保返回的是单个任务(不是列表)
// 主图负责把单个任务追加到 tasks 列表
Q3:子图变量"泄漏"到主图

现象:子图内部的 todo_desc 出现在了主图状态中。

原因threadId 没有正确隔离,或者子图和主图使用了相同的状态键。

解决

// 确保子图使用独立的 threadId
String subThreadId = mainThreadId + "-todo-" + UUID.randomUUID();

// 子图的状态键名与主图区分开
// 主图:tasks, chat_reply
// 子图:task_content, todo_desc, created_task
Q4:API Key 泄露风险

现象:把 Key 写死在代码里,提交到 GitHub 了。

解决

# application.yml - 永远使用环境变量
api-key: ${AI_DASHSCOPE_API_KEY}
# 服务器上设置环境变量
export AI_DASHSCOPE_API_KEY=sk-xxx

# 或者使用 Docker Secret / K8s Secret
docker run -e AI_DASHSCOPE_API_KEY_FILE=/run/secrets/dashscope_key ...

7.2 扩展建议

扩展1:添加查询待办功能
// 修改分类器,增加"查询待办"类别
.categories(List.of("创建待办", "查询待办", "删除待办", "其它"))

// 添加查询节点
mainGraph.addNode("queryTodo", node_async(state -> {
    List<String> tasks = (List<String>) state.value("tasks").orElse(List.of());
    String reply = tasks.isEmpty() 
        ? "你当前没有待办事项" 
        : "你当前有 " + tasks.size() + " 个待办:\n" + String.join("\n", tasks);
    return Map.of("query_reply", reply);
}));

// 添加条件分支
.addConditionalEdges("intent", edge_async(...), 
    Map.of(
        "创建待办", "callSubGraph",
        "查询待办", "queryTodo",
        "其它", "chat"
    )
)
扩展2:添加数据库持久化
@Service
public class TodoService {
    @Autowired
    private TodoRepository todoRepository;

    public void saveTodo(String sessionId, String content) {
        Todo todo = new Todo();
        todo.setSessionId(sessionId);
        todo.setContent(content);
        todo.setCreatedAt(LocalDateTime.now());
        todoRepository.save(todo);
    }

    public List<String> getTodos(String sessionId) {
        return todoRepository.findBySessionId(sessionId)
            .stream()
            .map(Todo::getContent)
            .collect(Collectors.toList());
    }
}
扩展3:添加删除待办功能
// 在子图中添加删除逻辑
mainGraph.addNode("deleteSubGraph", node_async(state -> {
    String userInput = (String) state.value("user_input").orElse("");
    List<String> tasks = (List<String>) state.value("tasks").orElse(new ArrayList<>());

    // 使用 LLM 识别要删除哪个任务
    // 或者简单匹配:如果输入包含任务关键词,就删除
    String taskToDelete = extractTaskName(userInput);
    tasks.removeIf(task -> task.contains(taskToDelete));

    return Map.of("tasks", tasks, "deleted", taskToDelete);
}));

附录:一键部署脚本

#!/bin/bash
# deploy.sh - ChatFlow 一键部署脚本

set -e

# ========== 配置区 ==========
API_KEY="${AI_DASHSCOPE_API_KEY:-}"
JAR_FILE="chatflow-1.0.0.jar"
SERVICE_NAME="chatflow"
PORT=8080

if [ -z "$API_KEY" ]; then
    echo "❌ 错误:请设置环境变量 AI_DASHSCOPE_API_KEY"
    exit 1
fi

echo "🚀 开始部署 ChatFlow..."

# 安装 Java(如果需要)
if ! command -v java &> /dev/null; then
    echo "📦 安装 Java 21..."
    sudo apt update && sudo apt install -y openjdk-21-jdk
fi

# 创建目录
sudo mkdir -p /opt/$SERVICE_NAME
sudo cp $JAR_FILE /opt/$SERVICE_NAME/ 2>/dev/null || {
    echo "❌ 找不到 JAR 文件:$JAR_FILE"
    echo "请先运行:mvn clean package -DskipTests"
    exit 1
}

# 创建 Systemd 服务
sudo tee /etc/systemd/system/$SERVICE_NAME.service > /dev/null <<EOF
[Unit]
Description=Spring AI Graph Chatflow
After=network.target

[Service]
Type=simple
User=$(whoami)
WorkingDirectory=/opt/$SERVICE_NAME
ExecStart=/usr/bin/java -jar $JAR_FILE
Environment="AI_DASHSCOPE_API_KEY=$API_KEY"
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# 启动服务
sudo systemctl daemon-reload
sudo systemctl start $SERVICE_NAME
sudo systemctl enable $SERVICE_NAME

echo "✅ 部署完成!"
echo "📍 服务地址:http://localhost:$PORT"
echo "🧪 测试命令:"
echo "  curl -X POST "http://localhost:$PORT/assistant/chat?sessionId=test&userInput=待办:测试""
echo ""
echo "📊 查看状态:sudo systemctl status $SERVICE_NAME"
echo "📋 查看日志:sudo journalctl -u $SERVICE_NAME -f"

适用对象:Graph 框架学习者、Spring AI 开发者

如有疑问,建议结合官方示例源码对照阅读,实践出真知!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐