解构 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)

相关推荐
lekami_兰3 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘6 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤7 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt1120 小时前
AI DDD重构实践
go
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
向上的车轮4 天前
开源版 Coze:创建知识库(RAG)
开源·coze
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
向上的车轮5 天前
开源版 Coze: 创建智能体-每日 ERP 系统巡检计划
开源·coze