【第41篇】Graph - ChatFlow 智能待办助手
一、项目概览
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,可以嵌入到主图中使用。
为什么要用子图?想象你在搭建一个复杂工厂:
- 主图是总装车间,负责调度整体流程
- 子图是专门的加工车间(如"待办创建车间"),有自己的设备和工人
子图的核心优势:
- 状态隔离:子图有自己的
threadId和变量池,不会污染主图 - 逻辑复用:同一个子图可以在多个主图中使用
- 复杂度拆分:把大流程拆成小块,降低心智负担
┌─────────────────────────────────────────────────────────────┐
│ 主图(Main Graph) │
│ │
│ START → [意图识别] ──┬── "创建待办" → [调用子图] ──▶ ... │
│ └── "其它" → [闲聊处理] ──▶ ... │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 子图(Sub Graph) │ │
│ │ │ │
│ │ START → [LLM润色] → [赋值] → [生成回复] → END │ │
│ │ │ │
│ │ (独立状态池:task_content, todo_desc, created_task)│ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
三、技术架构:一张图看懂全貌
3.1 系统交互流程
3.2 主图结构详解
3.3 子图结构详解
3.4 数据流转全景图
四、代码实战:主图与子图的编排艺术
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
这就是模块化设计的魅力——高内聚、低耦合。
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 开发者
如有疑问,建议结合官方示例源码对照阅读,实践出真知!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)