返回文章列表
AI AgentGraphSpring AI AlibabaTool Calling

在智能座舱里用状态图编排 Agent 工作流

从为什么单次 Prompt 难以应对复杂业务讲起,结合智能座舱项目拆解基于 Graph 的状态图模型、工具循环、多专家协作,以及从客服工作流到 Coding Agent 与控制论的架构相通之处。

很多时候做技术分享或者聊到大模型应用,话题很容易一上来就变成罗列流行框架和名词:Spring AI、LangGraph、RAG、MCP……听起来很前沿,但听完后大家心里往往还是会有个疑问:“我们平时调个大模型 API 聊聊天挺顺畅的,为什么还要专门做一套复杂的工作流编排?”

刚开始做 Agent 时,我也经历过这个阶段。直到试着用 AI 去承接一个真实的业务诉求:

“帮我查一下订单 SO20261001,看我还能退多少钱。”

表面上看,这只是一句普通的自然语言咨询。但只要动手写过业务代码就会发现,单次 Prompt 在这里很难独立走完:

大模型刚收到这句话的瞬间,不可能知道这个订单签收了几天、买的是什么商品、当前处于什么物流状态。就算把提示词写得再长,让它在文字里列出“第一步查订单、第二步算金额”,自然语言的规划也不等于系统里的真实执行。程序必须先停下来,拿着提取出的订单号去订单中心发起请求;拿到“已签收 3 天、未拆封”这个事实后,再把数据送回给模型,模型才能依据退货政策算出准确金额。

这个最初为了内训分享、使用 Vue 3 + Spring AI Alibaba Graph + DeepSeek 编写的 agentDemo 原型,后来被我完整迁移合入了智能座舱项目(enterprise-ai-cockpit 仓库)。下面结合项目里的架构图、流程图和实际代码,记录为什么复杂业务智能体离不开工作流编排,以及这套系统是如何一步步落地的。

从单次 API 调用走向目标驱动的有状态系统

在最基础的 LLM 使用模式里,逻辑非常直观:写一段 System Prompt,拼上用户问题,调一次 Chat Completion 接口,再把返回的 Markdown 渲染出来。写周报、润色文案、提炼单篇长文时,这种“一次性请求”很好用。

但这也容易让人产生一种错觉,以为只要把 Prompt 调得足够细致,就等于做出了一个“智能体(Agent)”。一放到真实的业务系统里,单次调用很快就会触及三个绕不开的工程瓶颈:

  1. 步骤之间的强动态依赖:就像前面的退款场景,第二步的计算输入完全依赖第一步的系统返回值。没有拿到第一步的真实返回(Observation),后续推理只能靠猜。
  2. 确定性业务规则的强约束:政策解答必须对齐最新向量知识库;报表分析必须通过内部受控的 MCP 接口取数;退款试算必须走专用计费逻辑;用户情绪激动时必须转交人工。如果试图把整本业务手册、数据库表结构和各类接口定义一股脑塞进同一个长 Prompt,不仅上下文窗口吃紧,严重的注意力稀释和模型幻觉也会直接拉低业务可靠性。
  3. 环境波动与容错恢复:外部接口可能超时,首次向量检索可能漏掉关键切片,模型偶尔可能输出格式非法的 JSON。单次调用遇到局部抖动除了整体报错别无他法,缺乏保留当前状态并自动重试修复的能力。

因此,智能体并不是一段提示词,也不是单纯的模型 API,而是一个围绕目标推进任务的有状态工程系统。在架构设计上,需要把模型推理与宿主运行环境清晰拆开:

  • 大脑(LLM / Reasoner):负责非结构化的语义理解、意图抽取,以及基于当前已知事实推断下一步行动建议(Policy Inference);
  • 躯干与运行容器(Host Runtime & State Container):负责跨步骤状态的持有与持久化、真实工具的安全调用、业务权限校验、流转预算控制以及人工干预恢复。
对比维度单次 Prompt + API 模式状态图工作流编排
上下文供给运行前一次性静态塞入,容易信息过载或遗漏随执行逐步动态注入,前一步工具的实际结果作为下一步上下文
外部动作执行只能在文字里生成参数,无法自行完成外部调用宿主程序负责参数合法性校验、发起真实网络调用并捕获异常
逻辑分支跳转依赖模型在单次回答中覆盖不同可能分支通过状态机中的条件边(Conditional Edge)执行确定性路由
异常恢复能力单次调用抛异常即整体任务终止异常包装为 Observation 写入状态,支持自动重试、降级或转人工
任务终止依据模型输出结束符(EOS)即代表完结基于业务验收条件、证据完备性与最大执行预算综合判定

状态图的底层计算模型与技术选型

为什么线性流水线不够用

刚开始做 Agent 编排时,最容易想到的是类似早期链式管道的线性流程(Sequential Chain,A -> B -> C)。

线性链写固定流程很顺手,可一旦遇到真实业务里的**循环(Loop)与反馈修正(Feedback Revision)**就会非常别扭:检索结果不理想需要改写查询词重试,或者工具调用报错需要带着错误信息重新决策时,线性链要么陷入过深的递归调用,要么被迫在外部堆砌一堆带全局状态变量的 while 循环。

状态图(State Graph)正是为了解决这类带分支与回环的控制流而设计的。它并不是为了在页面上画流程图好看,而是一套明确的计算模型,把业务逻辑拆解为四个基本部分:

Graph 状态图的核心架构:节点、状态、边与循环控制

  1. 状态(State / 黑板机制):贯穿整张图生命周期的共享记忆容器。它记录了任务从启动到结束的关键数据:用户原始提问、意图分类与置信度、检索到的知识切片、工具调用的请求与真实返回(Observations)、当前迭代轮数以及生成的回答草稿。工程实现时,状态字段需要定义明确的合并策略,比如用覆盖策略替换单条记录,或者在节点内部合并列表后再写回。
  2. 节点(Node):图中的原子业务单元。每个节点遵循纯函数式思路:读取当前 State 的所需字段,执行单一职责的任务(例如调用模型、查向量库、调外部 API 或做格式转换),然后输出状态增量(State Delta)。节点自身不持有跨请求的私有状态。
  3. 边(Edge)与条件边(Conditional Edge):普通边表示固定的确定性跳转;条件边则绑定路由函数,根据当前 State 中的状态值动态决定下一个目标节点。比如意图是“政策咨询”就去检索节点,是“订单查询”就去工具节点,意图置信度不足时切去人工处理。
  4. 循环(Cycle)与安全预算(Budget):在 Graph 拓扑里,边允许指回前面的上游节点,自然表达出“决策 — 行动 — 观察 — 再决策”(ReAct)的反馈循环。为了防止死循环空转,宿主运行时同时设置递归深度上限(Recursion Limit)与业务轮数预算(Budget),一旦超出预算立即熔断转人工。

在 Java 生态里选用 Spring AI Alibaba Graph

在 Python 生态中,LangGraph 率先把图计算模型与有限状态机(FSM)思路引入了 Agent 运行时。Python 做原型验证很敏捷,但在企业微服务、中台与高并发后端场景里,Java 依然是主力技术栈。

如果为了一个 Agent 模块单独维护一套 Python 服务,在部署运维、并发控制以及与既有业务库打通上都会增加额外成本。智能座舱后端本身基于 Java 与 Spring Boot 构建,因此我们直接选用了 Spring AI Alibaba Graph,在同一套工程体系里复用强类型约束、依赖注入以及 Spring 生态组件。

智能座舱客服工作流的整体设计

放到智能座舱的企业客服场景里,真实用户的提问方式多种多样:

  • 咨询 A:“Aero 耳机的退货政策是什么?签收三天未使用可以退吗?”(纯政策咨询,需要向量知识库检索并溯源条文);
  • 咨询 B:上传耳机指示灯红闪截图问“这是什么问题?怎么处理?”(多模态视觉理解,需抽取 E02 错误码后检索排障手册);
  • 咨询 C:“查询订单 SO20261001,然后根据实际状态试算可退款金额。”(前后强依赖的动态工具调用);
  • 咨询 D:“查询 2026-09 月销售报表,告诉我营收和订单数。”(企业系统集成,需通过 MCP 协议动态发现并调用内部报表服务);
  • 咨询 E:“请分别说明耳机退货政策、SO20261001 可退款金额以及 9 月销售报表。”(跨领域组合任务,需拆解子任务并调度多个专家协作);
  • 咨询 F:“我对赔偿金额极不满意,必须给我一个说法!”(争议纠纷或低置信度意图,需保存完整上下文并平滑转交人工)。

针对这些诉求,我们设计了端到端的状态图工作流拓扑:

企业级智能客服端到端工作流拓扑结构

前后端运行时的数据流如下,各节点通过标准事件驱动,把状态变更与跟踪信息实时推送给前端工作台:

运行时前后端架构与事件流机制

合入座舱后,工作台提供了 8 个场景入口、运行历史追溯、客服主图可视化以及实时节点详情面板:

智能座舱完整编排工作台:八个场景入口、运行历史、客服主图和节点详情

主图的执行脉络分为五个阶段:

  1. 输入归一化与图片观察:若用户带图提问,先调用轻量视觉模型提取可见文字与故障码特征,转为规范文本写入状态;
  2. 意图分类与动态路由:输出目标分支(rag / report / tool / multi / human)与置信度;置信度低于 0.6 时,条件边直接路由至人工交接;
  3. 领域分支执行:进入 RAG 知识检索、MCP 报表查询、原生 Tool 循环或多 Agent 协作;
  4. 生成与评审(Draft & Review):汇聚 State 中的全部证据生成草稿,由 Review 节点核对证据引用的一致性,通过则结束,存疑则触发重写或改写查询重检;
  5. 人机协同(Human-in-the-Loop):在转人工节点保存上下文快照并挂起,等待人工填写处理意见后进入恢复流继续执行。

代码层面同样需要明确状态字段的合并规则。座舱主图使用 ReplaceStrategy 处理状态更新:节点返回新的证据或观察列表覆盖对应字段;需要追加工具结果时,由节点在返回增量前完成旧列表与新结果的合并,保证状态流转清晰可控。

另外,日常交流里常有人把 MCP 和 Tool Calling 混为一谈。在我们的架构划分里:Tool Calling 是模型层的协议标准,约定模型如何声明函数名和参数;MCP 是应用层连接外部系统的集成规范,约定客户端如何动态发现(tools/list)和调用(tools/call)外部服务。独立运行的报表 MCP Server 在工作流内部同样被包装为受控 Tool,调用时机与容错统一由状态机管理。

两大核心机制的代码实现

在工作台的八个场景里,最能体现状态图编排价值的是工具调用循环与多 Agent 协作。

工具调用循环(Tool Loop / ReAct Loop)

以“查订单 SO20261001 并依据实际状态试算退款金额”为例,模型在第一步拿不到试算所需的金额与商品状态,必须先查回订单事实并写回状态,才能继续规划第二步。

工具调用循环(ReAct 循环)的状态流转与反馈控制

在智能座舱界面中,可以看到两个实际执行轮次的流转详情:

智能座舱工具循环实际运行详情:两个实际执行轮次,分别查订单和试算退款

我们在 Graph 中把循环拆为 tool_plan(决策节点)与 tool_execute(实际执行节点):

// 1. 注册决策节点与实际执行节点
graph.addNode("tool_plan", this::toolPlanNode);
graph.addNode("tool_execute", this::toolExecuteNode);

// 2. 配置条件边:根据决策输出决定继续调工具、生成回答还是转人工
graph.addConditionalEdges(
    "tool_plan",
    edge_async(state -> (String) state.data().get("toolNext")),
    Map.of(
        "execute", "tool_execute",   // 提出新工具调用 -> 去执行
        "done",    "draft",          // 证据已齐全 -> 去生成回答
        "human",   "human_handoff"   // 超出预算或异常 -> 转交人工
    )
);

// 3. 回环连接:工具执行完毕后回到决策节点重新评估
graph.addEdge("tool_execute", "tool_plan");

CompiledGraph app = graph.compile(
    CompileConfig.builder().recursionLimit(80).build()
);

两个节点的职责边界划分得很清楚:

Map<String, Object> toolPlanNode(State state) {
    // A. 业务终止检查:若前序步骤已拿到退款试算结果,直接结束循环
    if (state.hasObservation("calculate_refund")) {
        return Map.of("toolNext", "done");
    }

    // B. 安全预算熔断:若当前工具轮次达到设定上限(如 3 轮),切断循环转人工
    if (state.getInt("toolRound", 0) >= state.getInt("maxIterations", 3)) {
        return Map.of("toolNext", "human", "handoffReason", "工具调用超出最大预算限制");
    }

    // C. 动态权限控制:按当前阶段只向模型开放合法的候选工具,防止越序乱调
    List<ToolDefinition> allowedTools = resolveAllowedTools(state);

    // D. 请求模型决策:基于已积累的 toolMessages 与 observations 选择下一步工具
    ToolCall call = modelGateway.planNextTool(state.getMessages(), allowedTools);

    return Map.of(
        "pendingTool", call,
        "toolNext", "execute"
    );
}

Map<String, Object> toolExecuteNode(State state) {
    ToolCall call = (ToolCall) state.get("pendingTool");

    // 实际发起外部调用(HTTP / MCP / RPC),捕获超时与异常
    ToolResult result = enterpriseTools.execute(call.name(), call.arguments());

    // 将 Observation 包装为 role=tool 协议消息写回状态,供下一轮决策读取
    return Map.of(
        "observations", state.appendObservation(result),
        "toolMessages", state.appendToolMessage(call.id(), result.toText()),
        "toolRound", state.getInt("toolRound", 0) + 1
    );
}

这里最关键的区分在于:模型输出的 tool_calls 只是调用提议,只有宿主发起请求拿到响应后,才算产生了真正的 Observation。

第一轮,tool_plan 提议调用 lookup_order,后端校验参数后请求订单服务,拿到“已签收三天、未使用、金额 599 元”;第二轮,订单数据已进入 observations,此时才向模型开放 calculate_refund,算出可退金额 599 元。两项证据齐全后跳出循环。如果中途遇到接口超时,超时信息同样作为失败 Observation 写入状态供下一轮重试;一旦超过轮数上限,条件边立刻切入人工兜底,保证运行边界可控。

多 Agent 协作(Multi-Agent Collaboration)

当用户提问:“请分别说明耳机退货政策、SO20261001 可退款金额以及 9 月销售报表”时,如果把知识库检索、订单接口和报表 Schema 全塞进同一个上下文,模型很容易因为上下文混杂和工具过多出现选错工具或串参数的问题。

对应的解法是引入“专家子图协作模型”(Specialist Sub-graphs):

主图调度专家子图的多 Agent 协作层次架构

在座舱工作台里,可以看到协调器依次调度知识、报表、订单三个专家子图的执行过程:

智能座舱多 Agent 工作台:知识、报表、订单专家的实际子图运行

// 多 Agent 协调器的状态图流转
graph.addNode("multi_plan", this::decomposeTasksNode);
graph.addNode("specialist", this::invokeSpecialistSubGraphNode);
graph.addNode("multi_review", this::reviewProgressNode);

graph.addEdge("multi_plan", "specialist");
graph.addEdge("specialist", "multi_review");

// 检查进度:未完成则继续调度下一位专家;全部完成则进入汇总生成
graph.addConditionalEdges(
    "multi_review",
    edge_async(state -> (String) state.data().get("multiNext")),
    Map.of(
        "next",  "specialist",
        "done",  "draft",
        "human", "human_handoff"
    )
);
  • 任务拆解(multi_plan 节点):主模型先将组合问题拆分为独立的子任务列表 tasks = [Task(RAG), Task(Tool), Task(Report)];
  • 专家子图执行(specialist 节点):主图按当前任务类型调用各自独立编译的 CompiledGraph(内部可包含自己的节点拓扑与工具循环)。子图使用独立的 Thread ID 隔离上下文状态,同时继承主图的 Trace ID 与父 Span ID,保证调用链完整可查;
  • 进度推进与汇总(multi_review -> draft 节点):维护任务游标逐一推进,全部专家完成后汇集各路证据,一次性生成结构清晰的综合答复。

目前我们采用顺序协作方式,既便于理清各子图的上下文边界,对服务器并发资源的消耗也更温和。

从业务客服延伸到 Coding Agent 与控制论

如果把视野从企业客服拉远,看看如今广泛使用的 Coding Agent(如 Claude Code、Cursor、OpenAI Codex、SWE-agent),会发现它们之所以能读项目、改代码、修 Bug,底层的运行结构与这套客服工作流在架构上是相通的。

Coding Agent 的闭环控制:以反馈消除误差

成熟的 Coding Agent 从来不是靠“给一段提示词一次性生成整套工程”做出来的,而是建立在高频的反馈循环之上:

  1. 需求解析与目标对齐(Goal Setting);
  2. 检索代码库与符号定位(Grep / AST 工具);
  3. 制定修改方案(Plan);
  4. 局部编辑代码文件(Action);
  5. 运行编译器、Linter 或单元测试(Observe);
  6. 检查报错堆栈:通过则结束,报错则把错误输出反哺回状态,重新规划下一轮修改(Self-Correction Loop)。

这也是 Anthropic 和 OpenAI 近年来反复强调 Harness Engineering(座架工程) 的原因——“Prompt is cheap, foundation model is commodity, harness is the differentiator”(提示词成本很低,基础模型正走向同质化,外围座架工程才是真正的分水岭)。

什么是 Harness?就是包裹在模型外部的安全沙箱、工具链、上下文裁剪管理、进度断言与评测系统。模型是大脑,Harness 则是赋予大脑真实生产力的工程座架。

从系统科学的视角看,这正是维纳在 20 世纪创立的“控制论(Cybernetics)”在现代软件工程中的具体体现:任何自主系统要想在不确定环境中稳定达成目标,都离不开负反馈调节机制——持续测量当前状态与目标状态之间的偏差(Error),将偏差反向输入给控制器以修正下一次行动,直到偏差消除、系统趋向收敛。

单次 Prompt 调用本质上是开环系统,输出质量高度依赖单次生成的运气;而基于状态图与工作流编排的智能体,则是具备负反馈调节能力的闭环系统。这正是架构编排带给 AI 应用的核心工程价值。


如果想直接体验这套工作流的实际运转,可以打开智能座舱工作流页面在线试跑八个场景;下一篇继续记录这套工作流背后的节点监控与时间线回放实现。

查看运行中的项目