解构 Coze 工作流:可中断、可恢复的架构艺术

👋 大家好,我是十三!

在 AI Agent 与大模型应用蓬勃发展的今天,我们面临一个全新的工程挑战:如何构建能够与用户进行长周期、多轮深度交互的系统?

传统的、无状态的请求-响应模式在这种场景下显得力不从心。一个耗时的任务、一次需要用户中途确认的流程,都可能让后端架构陷入状态管理和异步通信的泥潭。

但 Coze Studio 的工作流引擎(Workflow)却优雅地解决了这个问题。它允许开发者通过简单的拖拽,构建出可以暂停、等待用户输入、然后从断点处恢复执行的复杂流程。这背后,是一种截然不同的架构设计哲学。

本文将深入 Coze 的后端源码,解构其工作流引擎的实现,共同探寻以下问题:

  • Coze 如何设计其 API,以支持长连接下的异步、流式通信?
  • 一个可中断的工作流任务,其请求如何在后端的整洁架构中优雅地流转?
  • 引擎内部是如何通过状态持久化与巧妙的异常处理,实现"冻结"与"解冻"执行现场这一核心能力的?

1. 通信的基石:基于 SSE 的可中断流式 API

要实现服务端与客户端之间的持续通信和中途交互,传统的"一问一答"式 HTTP 请求显然行不通。Coze 的架构选择是流式 API ,具体来说,是 SSE (Server-Sent Events)

这种模式彻底改变了通信模型:

  • 传统模式 : 客户端请求 → (长时间等待...可能超时) → 服务端响应最终结果
  • Coze 流式模式 :
    1. 客户端发起请求,建立长连接
    2. 服务端持续推送进度事件 (event: message)
    3. 服务端在需要时,抛出中断事件 (event: interrupt)
    4. 客户端响应中断,提交用户反馈
    5. 服务端接收反馈,恢复执行,继续推送事件
    6. 服务端推送结束事件 (event: done)

通过这种方式,服务端掌握了主动权,可以随时向客户端推送进度,并在需要时抛出"中断"事件,优雅地实现了"暂停-恢复"的逻辑。

1.1 核心 API 接口

这套交互模式背后,是两个核心的 API 接口:

  1. 启动工作流 : POST /v1/workflow/stream_run

    • 功能: 启动一个新的工作流实例,并建立一个 SSE 长连接。
    • 响应 : 服务端会通过这个连接持续推送事件,关键类型包括 message (进度), interrupt (中断), done (结束), error (错误)。
  2. 恢复工作流 : POST /v1/workflow/stream_resume

    • 功能 : 当收到 interrupt 事件后,客户端通过这个接口提交用户的反馈。
    • 关键参数 : 必须携带一个唯一的 executeID 来定位被暂停的工作流实例。

这套 API 设计,是整个可中断交互体验的架构基石。

1.2 技术选型:为何是 SSE 而非 WebSocket?

这是一个值得品味的工程决策,体现了"恰到好处"的设计哲学。

  • 简单性: SSE 基于标准 HTTP,协议开销小,对于服务端单向推送的场景,是量身定做的方案。
  • 可靠性: 自带断线重连机制,能有效应对网络抖动。
  • 兼容性: 作为标准 HTTP,对网络环境和防火墙非常友好。

相比之下,WebSocket 虽然功能更强,但在这个场景下显得"杀鸡用牛刀",增加了不必要的复杂性。Coze 的选择告诉我们:技术选型应服务于场景,够用且优雅是最佳原则。

2. 深入引擎:一次请求的生命周期

揭开 API 的面纱后,我们正式进入后端的"深水区"。一个 stream_run 请求进来后,Coze 的后端是如何一步步处理,最终驱动工作流运转起来的?

2.1 数据转换:从前端画布到后端可执行图

首先,系统需要将前端可视化画布的定义(JSON)转换为后端可执行的逻辑图。这个 Canvas → Schema → Workflow 的转换流程,是连接前端与后端的关键桥梁。

三个关键概念

1. Canvas(画布定义)

json 复制代码
{
  "nodes": [
    {"id": "start", "type": "start", "config": {...}},
    {"id": "llm1", "type": "llm", "config": {"model": "gpt-4", "prompt": "总结文档要点"}},
    {"id": "qa1", "type": "question_answer", "config": {"question": "总结是否满意?"}},
    {"id": "end", "type": "end", "config": {...}}
  ],
  "edges": [
    {"from": "start", "to": "llm1"},
    {"from": "llm1", "to": "qa1"},
    {"from": "qa1", "to": "end"}
  ]
}
  • 这是前端保存的原始数据,人类可读,但计算机难以直接执行

2. WorkflowSchema(内部图结构)

go 复制代码
type WorkflowSchema struct {
    Nodes map[string]*Node  // 节点定义
    Edges []*Edge           // 连接关系  
    Variables map[string]*Variable // 变量定义
}
  • 经过转换的标准化结构,图执行引擎可以理解和执行

3. Workflow(可执行实例)

go 复制代码
type Workflow struct {
    executor *GraphExecutor  // 图执行器
    context  *ExecuteContext // 执行上下文
    state    *WorkflowState  // 当前状态
}
  • 最终的可执行对象,包含完整的执行逻辑和状态管理

转换流程详解

css 复制代码
用户操作     → 前端保存      → 后端转换      → 引擎执行
拖拽节点    → Canvas JSON  → WorkflowSchema → Workflow实例

这个转换过程的关键在于适配器模式adaptor.CanvasToWorkflowSchema()

go 复制代码
func CanvasToWorkflowSchema(ctx context.Context, canvas *vo.Canvas) (*compose.WorkflowSchema, error) {
    // 格式转换:JSON → Go 结构体
    nodes := make(map[string]*compose.Node)
    for _, canvasNode := range canvas.Nodes {
        node, err := convertCanvasNode(canvasNode)
        if err != nil {
            return nil, err
        }
        nodes[canvasNode.ID] = node
    }
    
    // 语义理解:节点配置 → 执行逻辑  
    edges := make([]*compose.Edge, 0, len(canvas.Edges))
    for _, canvasEdge := range canvas.Edges {
        edge := &compose.Edge{
            From: canvasEdge.From,
            To:   canvasEdge.To,
        }
        edges = append(edges, edge)
    }
    
    // 🔗 依赖分析:连接关系 → 执行顺序
    return &compose.WorkflowSchema{
        Nodes: nodes,
        Edges: edges,
    }, nil
}

2.2 分层架构:请求的优雅流转

Coze 的后端遵循了经典的整洁架构 ,请求在 Handler -> Application -> Domain 三层之间清晰流转。

第一站:Handler 层 (api) - 请求的接待员

想象 Handler 层就像酒店的前台,职责单一且明确:

go 复制代码
// 路径: coze/coze-studio/backend/api/handler/workflow/openapi.go
func OpenAPIStreamRunFlow(c *app.RequestContext) {
    // 1. 接待客人:解析请求参数
    var req workflow.OpenAPIStreamRunFlowRequest
    if err := c.BindAndValidate(&req); err != nil {
        // 参数有问题,礼貌拒绝
        ResponseError(c, err)
        return
    }

    // 2. 准备房间:建立 SSE 长连接
    w := sse.NewWriter(c)
    c.SetContentType("text/event-stream; charset=utf-8")
    c.Response.Header.Set("Connection", "keep-alive")
    c.Response.Header.Set("Cache-Control", "no-cache")

    // 3. 联系服务部:调用业务逻辑
    ctx := context.Background()
    sr, err := appworkflow.SVC.OpenAPIStreamRun(ctx, &req)
    if err != nil {
        sendErrorEvent(w, err)
        return
    }
    
    // 4. 当服务员:持续转发消息
    sendStreamRunSSE(ctx, w, sr)
}

关键的数据转发过程

go 复制代码
func sendStreamRunSSE(ctx context.Context, w *sse.Writer, sr *StreamReader) {
    defer w.Close()
    
    for {
        // 从执行引擎读取一条消息
        message, err := sr.Recv()
        if err != nil {
            if err == io.EOF {
                // 正常结束
                return
            }
            // 发送错误事件
            sendErrorEvent(w, err)
            return
        }

        // 转换为 SSE 格式并发送给客户端
        event := sse.Event{
            ID:   message.ID,
            Type: message.Type,  // "message" | "interrupt" | "done" | "error"
            Data: message.Data,
        }
        
        if err := w.Write(event); err != nil {
            // 客户端断开连接
            return
        }
    }
}

核心原则 : Handler 层完全不关心业务逻辑,只负责协议转换和数据搬运,保持了极高的稳定性。

第二站:Application 层 (application) - 业务的指挥官

Application 层就像一个项目经理,负责业务编排:

go 复制代码
// 路径: coze/coze-studio/backend/application/workflow/service.go
func (svc *ApplicationService) OpenAPIStreamRun(ctx context.Context, req *Request) (*StreamReader, error) {
    // 1. 权限检查:这个用户能运行这个工作流吗?
    if err := svc.checkPermission(ctx, req.WorkflowID, req.UserID); err != nil {
        return nil, errors.Wrap(err, "permission denied")
    }

    // 2. 制定执行计划
    config := &vo.ExecuteConfig{
        From:        vo.FromSpecificVersion,  // 执行已发布版本
        Mode:        vo.ExecuteModeRelease,   // 线上模式
        SyncPattern: vo.SyncPatternStream,    // 流式执行(关键!)
        WorkflowID:  req.WorkflowID,
        Parameters:  req.Parameters,
        UserID:      req.UserID,
    }

    // 3. 委托给专业团队:领域层
    sr, err := svc.domainSVC.StreamExecute(ctx, config)
    if err != nil {
        return nil, errors.Wrap(err, "domain execute failed")
    }
    
    // 4. 事件转换:内部格式 → API 格式
    return svc.convertStreamEvents(sr), nil
}

为什么需要 Application 层?

一个精巧的设计是事件转换器

go 复制代码
// 领域层产生的内部消息
type entity.Message struct {
    ExecuteID string
    NodeID    string
    Content   string
    Type      string
    EventID   string
}

// 转换为面向 API 的格式
func (svc *ApplicationService) convertStreamEvents(sr *domain.StreamReader) *StreamReader {
    return &StreamReader{
        recv: func() (*APIMessage, error) {
            domainMsg, err := sr.Recv()
            if err != nil {
                return nil, err
            }
            
            // 格式适配:领域层关注业务,API 层关注接口规范
            return &APIMessage{
                ID:        domainMsg.EventID,
                Type:      domainMsg.Type,
                Data:      svc.formatMessageData(domainMsg),
                ExecuteID: domainMsg.ExecuteID,
            }, nil
        },
    }
}

这样做的好处:

  • 内外隔离:内部数据结构可以自由变化,不影响 API
  • 格式适配:领域层关注业务,API 层关注接口规范
  • 监控埋点:可以在这一层统一添加日志、监控等横切关注点

2.3 领域核心 (domain):中断与恢复的实现机制

终于,我们来到了引擎的心脏------Domain 层。所有真正的执行魔法都在这里发生。

巧妙的技术选型:站在巨人肩膀上

Coze 做了一个聪明的决定:不重新发明轮子

go 复制代码
import "github.com/cloudwego/eino/compose"

// 不是自己写一个图执行引擎,而是基于成熟的 eino
wf, err := compose.NewWorkflow(ctx, workflowSchema, options...)

为什么这样做?

  • 专注核心价值:把精力投入到 AI Agent 的业务逻辑上
  • 稳定可靠eino 是经过验证的图执行框架
  • 功能完整:支持并行执行、错误处理、状态管理等复杂特性

核心执行流程:从画布到运行

go 复制代码
func (svc *DomainService) StreamExecute(ctx context.Context, config *ExecuteConfig) (*StreamReader, error) {
    // 第一步:获取工作流定义
    wfEntity, err := svc.repo.GetWorkflow(ctx, config.WorkflowID)
    if err != nil {
        return nil, errors.Wrap(err, "get workflow failed")
    }
    
    // 第二步:Canvas JSON → WorkflowSchema
    canvas := &vo.Canvas{}
    if err := sonic.UnmarshalString(wfEntity.Canvas, canvas); err != nil {
        return nil, errors.Wrap(err, "unmarshal canvas failed")
    }
    
    workflowSchema, err := adaptor.CanvasToWorkflowSchema(ctx, canvas)
    if err != nil {
        return nil, errors.Wrap(err, "convert canvas to schema failed")
    }
    
    // 第三步:WorkflowSchema → 可执行的 Workflow
    workflow, err := svc.createExecutableWorkflow(ctx, workflowSchema, config)
    if err != nil {
        return nil, errors.Wrap(err, "create workflow failed")
    }
    
    // 第四步:开始执行!
    return svc.startStreamExecution(ctx, workflow, config.Parameters)
}

流式执行的秘密:生产者-消费者模式

这里有个非常巧妙的设计,解决了一个核心问题:如何让工作流在后台执行,同时实时向用户推送进度?

答案是:内存管道 (Pipe)

go 复制代码
func (svc *DomainService) startStreamExecution(ctx context.Context, workflow *Workflow, params map[string]any) (*StreamReader, error) {
    // 创建一个内存管道:sr 是读取端,sw 是写入端
    sr, sw := schema.Pipe[*entity.Message](10)  // 缓冲区大小为 10

    // 将写入端注入到工作流执行器中
    workflow, err := compose.NewWorkflow(ctx, schema, 
        compose.WithStreamWriter(sw))  // 🔑 关键!
    if err != nil {
        sw.Close()
        return nil, err
    }

    // 启动异步执行(生产者在后台运行)
    go func() {
        defer sw.Close()
        
        if err := workflow.AsyncRun(ctx, params); err != nil {
            // 错误也通过管道传递
            sw.Send(&entity.Message{
                Type: "error",
                Data: err.Error(),
            })
        }
    }()

    // 立即返回读取端给上层(消费者)
    return sr, nil
}

工作原理

  1. 🏭 生产者 :工作流在后台执行,每个节点的输出写入 sw
  2. 📦 缓冲区:消息暂存在大小为 10 的队列中
  3. 📤 消费者 :上层不断从 sr 读取消息,转发给前端

中断与恢复:状态快照的魔法

这是 Coze 工作流最强大的特性。想象一下:工作流执行到一半,突然需要用户输入,系统要能"冻结"当前状态,等用户输入后再"解冻"继续执行。

中断是如何发生的?

go 复制代码
// 当执行到"问答"节点时
func (node *QuestionAnswerNode) Execute(ctx context.Context, input map[string]any) (map[string]any, error) {
    // 构造问题并发送给用户
    question := node.Config.Question
    
    // 🔥 关键:抛出中断异常
    return nil, &InterruptException{
        EventID:   generateEventID(),
        Question:  question,
        // 当前执行状态快照
        State:     svc.captureCurrentState(ctx),
        ExecuteID: ctx.Value("execute_id").(string),
    }
}

// 状态快照的数据结构
type ExecutionState struct {
    WorkflowID    string                 `json:"workflow_id"`
    ExecuteID     string                 `json:"execute_id"`
    CurrentNodeID string                 `json:"current_node_id"`
    Variables     map[string]interface{} `json:"variables"`
    NodeStates    map[string]interface{} `json:"node_states"`
    CreatedAt     time.Time              `json:"created_at"`
}

执行引擎如何处理中断?

go 复制代码
// 在工作流执行器中
func (executor *WorkflowExecutor) executeNode(ctx context.Context, node Node, input map[string]any) error {
    result, err := node.Execute(ctx, input)
    
    if err != nil {
        if interruptErr, ok := err.(*InterruptException); ok {
            // 🛑 遇到中断:保存状态并通知上层
            if err := executor.saveExecutionState(ctx, interruptErr.ExecuteID, interruptErr.State); err != nil {
                return errors.Wrap(err, "save execution state failed")
            }
            
            // 发送中断事件给前端
            executor.streamWriter.Send(&entity.Message{
                Type:      "interrupt",
                EventID:   interruptErr.EventID,
                Data:      interruptErr.Question,
                ExecuteID: interruptErr.ExecuteID,
            })
            
            return interruptErr  // 暂停执行
        } else {
            // 其他错误:终止执行
            return errors.Wrap(err, "node execution failed")
        }
    }
    
    // 继续下一个节点...
    return executor.executeNextNode(ctx, result)
}

恢复是如何实现的?

go 复制代码
func (svc *DomainService) StreamResume(ctx context.Context, req *ResumeRequest) (*StreamReader, error) {
    // 1. 根据 executeID 找到被冻结的执行状态
    executionState, err := svc.repo.GetExecutionState(ctx, req.ExecuteID)
    if err != nil {
        return nil, errors.Wrap(err, "get execution state failed")
    }
    
    // 2. 重新构建执行环境
    workflow, err := svc.rebuildWorkflow(ctx, executionState)
    if err != nil {
        return nil, errors.Wrap(err, "rebuild workflow failed")
    }
    
    // 3. 注入用户的回答,从中断点继续执行
    resumeInput := map[string]any{
        "user_answer": req.UserAnswer,
        // 恢复之前的所有变量
        "_variables": executionState.Variables,
    }
    
    return workflow.ResumeFrom(executionState.CurrentNodeID, resumeInput)
}

func (svc *DomainService) rebuildWorkflow(ctx context.Context, state *ExecutionState) (*Workflow, error) {
    // 重新获取工作流定义
    wfEntity, err := svc.repo.GetWorkflow(ctx, state.WorkflowID)
    if err != nil {
        return nil, err
    }
    
    // 重新转换为 WorkflowSchema
    canvas := &vo.Canvas{}
    sonic.UnmarshalString(wfEntity.Canvas, canvas)
    schema, err := adaptor.CanvasToWorkflowSchema(ctx, canvas)
    if err != nil {
        return nil, err
    }
    
    // 创建新的工作流实例,但恢复之前的状态
    workflow, err := compose.NewWorkflow(ctx, schema)
    if err != nil {
        return nil, err
    }
    
    // 🔑 关键:恢复执行状态
    if err := workflow.RestoreState(state.NodeStates); err != nil {
        return nil, err
    }
    
    return workflow, nil
}

这个设计的精妙之处:

  • 状态外置:执行状态保存在数据库中,不依赖内存
  • 精确恢复:能从任意中断点继续,不需要重新执行
  • 多次交互:支持多轮问答,每次都能准确恢复

节点系统:插件化的设计艺术

Coze 的强大之处在于它的节点系统。每种节点都有特定的能力,但都遵循统一的接口:

go 复制代码
type Node interface {
    Execute(ctx context.Context, input map[string]any) (output map[string]any, err error)
    Stream(ctx context.Context, input map[string]any) (*StreamReader, error)
}

主要节点类型

1. LLM 节点:调用大模型的智能体

go 复制代码
type LLMNode struct {
    Config LLMConfig
    client ModelClient
}

func (n *LLMNode) Execute(ctx context.Context, input map[string]any) (map[string]any, error) {
    prompt := n.buildPrompt(input)
    
    response, err := n.client.ChatCompletion(ctx, &ChatRequest{
        Model:    n.Config.Model,
        Messages: prompt,
        Stream:   true,  // 支持流式输出
    })
    
    return map[string]any{
        "content": response.Content,
        "tokens": response.TokenUsage,
    }, err
}

2. 问答节点:触发中断的关键节点

go 复制代码
type QuestionAnswerNode struct {
    Config QAConfig
}

func (n *QuestionAnswerNode) Execute(ctx context.Context, input map[string]any) (map[string]any, error) {
    // 这个节点的唯一任务就是抛出中断
    return nil, &InterruptException{
        EventID:  generateEventID(),
        Question: n.Config.Question,
        State:    captureCurrentState(ctx),
    }
}

3. 代码执行节点:安全的沙箱环境

go 复制代码
type CodeNode struct {
    Config    CodeConfig
    sandbox   SandboxClient
}

func (n *CodeNode) Execute(ctx context.Context, input map[string]any) (map[string]any, error) {
    // 在隔离的沙箱中执行用户代码
    result, err := n.sandbox.RunCode(ctx, &SandboxRequest{
        Language: n.Config.Language,  // python, javascript, etc.
        Code:     n.Config.Code,
        Input:    input,
        Timeout:  30 * time.Second,
    })
    
    return map[string]any{
        "output": result.Output,
        "error":  result.Error,
    }, err
}

数据流转的秘密

go 复制代码
// 节点间的数据传递
previousOutput := map[string]any{
    "document_content": "这是一份重要文档...",
    "user_query": "请总结要点",
}

// LLM 节点接收上游数据,产生新的输出
llmOutput, err := llmNode.Execute(ctx, previousOutput)
// llmOutput = {"summary": "文档要点如下:1. ..."}

// 输出自动成为下一个节点的输入
nextNodeInput := llmOutput

这种设计让复杂的业务逻辑变得像"搭积木"一样简单。

3. 实践串联:一次完整交互的生命周期

让我们回到最初的"智能文档助手"案例,用一张图串起整个流程,看看这套架构在实践中是如何运转的。

sequenceDiagram participant Client as "用户浏览器" participant Handler as "Handler层" participant App as "Application层" participant Domain as "Domain层(Engine)" participant DB as "数据库" Client->>Handler: 1. POST /v1/workflow/stream_run Handler->>App: 2. 调用 OpenAPIStreamRun() App->>Domain: 3. 调用 StreamExecute() Domain->>Domain: 4. Canvas->Schema
创建 Workflow Domain->>Domain: 5. 启动异步执行
(go workflow.AsyncRun) Note right of Domain: LLM节点开始分析文档... Domain-->>Client: 6. (via Pipe & SSE)
event: message("分析中...") Note right of Domain: 执行到"问答节点" Domain->>Domain: 7. 抛出 InterruptException Domain->>DB: 8. 保存执行状态快照 Domain-->>Client: 9. (via Pipe & SSE)
event: interrupt("总结是否满意?") Note over Client, DB: 工作流暂停,等待用户输入 Client->>Handler: 10. POST /v1/workflow/stream_resume Handler->>App: 11. 调用 StreamResume() App->>Domain: 12. 调用 StreamResume() Domain->>DB: 13. 加载状态快照 Domain->>Domain: 14. 从断点恢复执行 Note right of Domain: 根据用户反馈,继续后续流程... Domain-->>Client: 15. (via Pipe & SSE)
event: message("已收到反馈...") Domain-->>Client: 16. (via Pipe & SSE)
event: done("PPT生成完毕")

3.1 场景深度解析:智能文档助手的技术实现

现在让我们深入分析这个完整的交互流程,看看每个步骤背后的技术原理:

技术实现流程

复制代码
用户上传文档 → AI总结要点 → 用户确认 → 生成PPT → 完成

第一阶段:启动与执行(Steps 1-6)

  1. 📤 用户点击运行POST /v1/workflow/stream_run

    json 复制代码
    {
      "workflow_id": "doc-assistant-v1",
      "parameters": {
        "document": "base64_encoded_content",
        "output_format": "ppt"
      }
    }
  2. Handler 层建立 SSE 长连接

    go 复制代码
    // 关键的 SSE 配置
    c.SetContentType("text/event-stream; charset=utf-8")
    c.Response.Header.Set("Connection", "keep-alive")
    c.Response.Header.Set("Cache-Control", "no-cache")
  3. Application 层构造执行配置

    go 复制代码
    config := &vo.ExecuteConfig{
        From:        vo.FromSpecificVersion,
        Mode:        vo.ExecuteModeRelease,
        SyncPattern: vo.SyncPatternStream,    // 流式执行
        WorkflowID:  "doc-assistant-v1",
        Parameters:  {"document": "...", "output_format": "ppt"},
    }
  4. ❤️ Domain 层解析 Canvas,创建工作流实例

    go 复制代码
    // Canvas 定义了完整的业务逻辑
    canvas := {
        "nodes": [
            {"id": "start", "type": "start"},
            {"id": "extract", "type": "code", "config": {"code": "extract_text()"}},
            {"id": "analyze", "type": "llm", "config": {"prompt": "分析文档要点"}},
            {"id": "confirm", "type": "question_answer", "config": {"question": "总结是否满意?"}},
            {"id": "generate", "type": "llm", "config": {"prompt": "生成PPT大纲"}},
            {"id": "end", "type": "end"}
        ]
    }
  5. 🤖 执行 LLM 节点:实时推送执行进度

    go 复制代码
    // LLM 节点开始执行
    func (node *LLMNode) Execute(ctx context.Context, input map[string]any) {
        // 实时推送思考过程
        node.streamWriter.Send(&Message{
            Type: "message",
            Data: "正在分析文档结构...",
        })
        
        // 调用大模型
        response := node.callLLM(input["document"])
        
        node.streamWriter.Send(&Message{
            Type: "message", 
            Data: "分析完成,生成总结中...",
        })
    }

第二阶段:中断与暂停(Steps 7-9)

  1. ❓ 遇到问答节点,触发中断机制

    go 复制代码
    func (node *QuestionAnswerNode) Execute(ctx context.Context, input map[string]any) {
        // 构造状态快照
        state := &ExecutionState{
            WorkflowID:    ctx.Value("workflow_id").(string),
            ExecuteID:     generateExecuteID(),
            CurrentNodeID: "confirm",
            Variables: map[string]any{
                "document_summary": input["summary"],
                "document_content": input["document"],
            },
            NodeStates: map[string]any{
                "extract": {"status": "completed"},
                "analyze": {"status": "completed", "output": input["summary"]},
            },
        }
        
        // 抛出中断异常
        return nil, &InterruptException{
            EventID:   generateEventID(),
            Question:  "这个文档总结是否满意?如不满意请说明需要调整的地方。",
            State:     state,
            ExecuteID: state.ExecuteID,
        }
    }
  2. 系统保存状态快照并推送中断事件

    go 复制代码
    // 执行引擎处理中断
    if interruptErr, ok := err.(*InterruptException); ok {
        // 保存到数据库
        if err := executor.repo.SaveExecutionState(ctx, interruptErr.State); err != nil {
            return err
        }
        
        // 推送中断事件
        executor.streamWriter.Send(&entity.Message{
            Type:      "interrupt",
            EventID:   interruptErr.EventID,
            Data:      interruptErr.Question,
            ExecuteID: interruptErr.ExecuteID,
        })
    }

第三阶段:用户反馈与恢复(Steps 10-16)

  1. 用户提供反馈POST /v1/workflow/stream_resume

    json 复制代码
    {
      "execute_id": "exec_123456",
      "event_id": "event_789",
      "user_answer": "总结太简略了,请增加技术细节部分的分析"
    }
  2. 系统加载状态并恢复执行

    go 复制代码
    func (svc *DomainService) StreamResume(ctx context.Context, req *ResumeRequest) {
        // 从数据库恢复状态
        state, err := svc.repo.GetExecutionState(ctx, req.ExecuteID)
        
        // 重建工作流环境
        workflow, err := svc.rebuildWorkflow(ctx, state)
        
        // 注入用户反馈,从中断点继续
        resumeInput := map[string]any{
            "user_feedback": req.UserAnswer,
            "previous_summary": state.Variables["document_summary"],
            // 恢复所有之前的变量和状态
            "_restored_variables": state.Variables,
        }
        
        return workflow.ResumeFrom("confirm", resumeInput)
    }
  3. 重新执行改进的总结

    go 复制代码
    // 根据用户反馈,重新生成总结
    improvedPrompt := fmt.Sprintf(`
    之前的总结:%s
    用户反馈:%s
    请根据反馈重新总结,特别注意:%s
    `, previousSummary, userFeedback, extractKeyPoints(userFeedback))

3.2 架构优势在实践中的体现

为什么传统方式很难实现这个场景?

传统 HTTP 接口:

go 复制代码
func ProcessDocument(doc Document) (PPT, error) {
    summary := callLLM(doc)
    // 问题:如何让用户确认?无法实现!
    if userSatisfied(summary) {  // 这行代码写不出来
        return generatePPT(summary), nil
    }
    return nil, errors.New("user not satisfied")
}

Coze 工作流方式

  • 流式交互:用户能实时看到AI的思考过程
  • 中断等待:系统主动暂停,等待用户反馈
  • 状态保持:即使中断也不会丢失之前的计算结果
  • 多轮优化:可以反复修改直到满意

3.3 技术实现的核心亮点

1. 状态的精确管理

go 复制代码
// 每次中断都会保存完整的执行现场
type ExecutionSnapshot struct {
    // 执行到哪个节点了
    CurrentNodeID string
    
    // 所有变量的当前值
    Variables map[string]any
    
    // 每个节点的执行状态
    NodeStates map[string]NodeState
    
    // 用户的历史交互
    InteractionHistory []UserInteraction
}

2. 无缝的恢复机制

go 复制代码
// 恢复时能完全重建执行环境
func rebuildExecutionContext(snapshot *ExecutionSnapshot) *ExecutionContext {
    ctx := NewExecutionContext()
    
    // 恢复所有变量
    for key, value := range snapshot.Variables {
        ctx.SetVariable(key, value)
    }
    
    // 恢复每个节点的状态
    for nodeID, state := range snapshot.NodeStates {
        ctx.SetNodeState(nodeID, state)
    }
    
    return ctx
}

3. 灵活的数据流转

go 复制代码
// 节点间的数据流动是动态的、类型安全的
type DataFlow map[string]any

// 上一个节点的输出自动成为下一个节点的输入
func (wf *Workflow) executeNextNode(currentOutput DataFlow) error {
    nextNode := wf.getNextNode()
    
    // 数据自动流转,无需手动映射
    result, err := nextNode.Execute(ctx, currentOutput)
    
    return wf.processNodeResult(result, err)
}

这张图和深度分析清晰地展示了 Coze 架构的优雅之处:各层职责分明,通过流式 API、异步管道和状态持久化,完美支撑了复杂的人机交互场景。

4. 结论:架构是体验的基石

通过对 Coze 工作流源码的深度探险,我们不仅理解了一个复杂系统的运作原理,更能从中提炼出宝贵的工程思想,为我们构建自己的 AI Agent 系统提供清晰的指引。

4.1 核心技术收获

架构设计层面

  • 分层架构的力量:Handler-Application-Domain 的清晰职责分离,让系统具备了极强的可维护性和可扩展性
  • 异步解耦的艺术:生产者-消费者模式配合内存管道,实现了执行与通信的完美解耦
  • 状态管理的精髓:外置状态快照机制,是实现中断与恢复功能的技术基石
  • 技术选型的智慧 :基于成熟的 eino/compose 框架,专注核心价值而非重复造轮子

实现细节层面

  • 流式通信模式:SSE 的选择体现了"够用且优雅"的工程哲学
  • 插件化节点系统:统一接口下的多样化能力,让系统具备无限扩展性
  • 精确的数据流转:节点间的数据传递既灵活又类型安全
  • 可靠的异常处理:将中断作为正常的业务流程,而非错误情况处理

4.2 工程思维的启发

在正确的地方创新

go 复制代码
// ❌ 错误的创新方向:重新发明图执行引擎
type CustomGraphEngine struct {
    // 大量重复实现已有功能的代码...
}

// ✅ 正确的创新方向:专注AI交互逻辑
type WorkflowInteractionEngine struct {
    base        compose.GraphEngine     // 复用成熟组件
    interruptor *InterruptManager       // 专注核心价值
    stateStore  *ExecutionStateStore    // 创新在这里
}

Coze 告诉我们:识别出项目的核心价值所在,把有限的资源投入到最重要的创新点上

🎨 用户体验驱动技术选择

makefile 复制代码
用户需求: "我希望能实时看到AI的思考过程,并在关键节点参与决策"
    ↓
技术方案: 流式API + 异步执行 + 状态快照 + 中断恢复
    ↓
架构设计: 分层解耦 + 事件驱动 + 插件化节点

这是从用户价值出发倒推技术实现的典范,告诉我们好的架构永远是为最终的体验服务的

4.3 对 AI Agent 开发的深层指导

🤝 交互式设计是刚需

go 复制代码
// 传统AI应用:一次性交互
func TraditionalAI(input string) string {
    return llm.Generate(input)  // 黑盒处理,用户只能等待
}

// 新一代AI应用:协作式交互
func InteractiveAI(input string) *WorkflowExecution {
    return workflow.StreamExecute(input)  // 可观察、可干预、可协作
}

复杂的AI应用不是一次性对话,而是需要多轮协作、实时反馈的伙伴关系。

实时反馈提升体验

  • 让用户看到AI的"思考过程",而不是黑盒等待
  • 在关键节点提供干预机会,让AI成为可控的工具
  • 通过流式交互建立信任感和参与感

状态管理是核心能力

go 复制代码
// 支持中断与恢复需要精心设计的状态机制
type AIAgentState struct {
    CurrentTask    string              // 当前执行的任务
    Context        map[string]any      // 执行上下文
    History        []InteractionStep   // 交互历史
    Checkpoint     *ExecutionSnapshot  // 执行快照
}

// 这样的设计让AI Agent具备了"记忆"和"恢复"能力

4.4 未来展望:下一代AI应用的特征

基于对Coze架构的深度理解,我们可以预见下一代AI应用将具备以下特征:

智能协作性

  • AI不再是被动的工具,而是主动的协作伙伴
  • 支持多轮对话、中途调整、迭代优化
  • 具备上下文记忆和学习能力

流程可视化

  • 复杂的AI逻辑通过可视化方式呈现
  • 非技术人员也能理解和调整AI的行为
  • 业务逻辑与技术实现彻底分离

实时交互性

  • 摒弃传统的"请求-响应"模式
  • 支持流式处理、实时反馈、动态调整
  • 用户能看到AI的"思考过程"

状态持久化

  • AI应用具备"记忆",能从任意断点恢复
  • 支持长时间、跨会话的复杂任务
  • 异常情况下不会丢失工作进度

4.5 实践建议:如何应用这些理念

如果你要构建自己的AI Agent系统,可以参考以下设计原则:

1. 架构设计原则

go 复制代码
// 分层设计,职责分明
Handler   -> 协议转换,数据搬运
Application -> 业务编排,权限控制  
Domain    -> 核心逻辑,AI交互

// 异步解耦,流式处理
Producer  -> AI推理引擎
Channel   -> 消息队列/管道
Consumer  -> 用户界面

2. 状态管理策略

go 复制代码
// 外置状态存储
type StateStore interface {
    Save(ctx context.Context, snapshot *Snapshot) error
    Load(ctx context.Context, id string) (*Snapshot, error)
    Delete(ctx context.Context, id string) error
}

// 可恢复的执行上下文
type ExecutionContext struct {
    Variables map[string]any
    History   []Step
    Metadata  map[string]any
}

3. 技术选型建议

  • 通信层:SSE用于服务端推送,WebSocket用于双向交互
  • 消息队列:内存管道用于进程内,Redis/RabbitMQ用于分布式
  • 状态存储:Redis用于临时状态,数据库用于持久化快照
  • 执行引擎:基于成熟框架(如eino)而非从零开始

现在,我们可以回答文章开头提出的那些核心问题了:面对AI时代长周期、多轮深度交互的系统构建挑战,我们该如何设计出既优雅又实用的工作流架构?

通过对 Coze 工作流引擎源码的这次深度探索,我找到了一个堪称现代分布式系统设计典范的答案。其核心设计思想如下:

  • 流式通信的架构革新 :用 SSE + 异步管道 + 生产者消费者模式 的组合,彻底打破传统请求-响应的束缚,构建起真正支持长周期交互的通信基础设施。
  • 状态外置的中断恢复机制 :通过 异常驱动 + 状态快照 + 精确恢复 的三位一体设计,将复杂的执行现场"冻结"与"解冻"变成了可靠的工程实践,而非魔法。
  • 分层解耦的系统架构 :采用 Handler-Application-Domain 的整洁架构,配合插件化节点系统,在保证职责分离的同时实现了极强的可扩展性和可维护性。

这套集通信、状态、架构于一体的完整方案,为构建下一代AI Agent系统提供了宝贵的工程指导!

Make Open Source Great Again!


关于十三Tech

资深服务端研发工程师,AI 编程实践者。

专注分享真实的技术实践经验,相信 AI 是程序员的最佳搭档。

希望能和大家一起写出更优雅的代码!

联系方式569893882@qq.com
GitHub@TriTechAI
微信:TriTechAI(备注:十三 Tech)

相关推荐
太凉16 小时前
Go 语言指针赋值详解
go
郭京京16 小时前
go语言字符串
go
DemonAvenger16 小时前
Go 语言网络故障诊断与调试技巧
网络协议·架构·go
吾鳴17 小时前
扣子(Coze)实战:一键混剪直击心灵的情感治愈系视频,终于有人讲清楚了
coze
刘晓倩17 小时前
扣子Coze中的触发器实现流程自动化-实现每日新闻卡片式推送
人工智能·触发器·coze
白应穷奇20 小时前
追踪go应用程序
性能优化·go
用户67570498850220 小时前
用 Go 写桌面应用?试试 Wails 吧!
go
vv安的浅唱20 小时前
Golang基础笔记十之goroutine和channel
后端·go
程序员爱钓鱼20 小时前
Go语言实战案例:使用sync.Map构建线程安全map
后端·go·trae