👋 大家好,我是十三!
在 AI Agent 与大模型应用蓬勃发展的今天,我们面临一个全新的工程挑战:如何构建能够与用户进行长周期、多轮深度交互的系统?
传统的、无状态的请求-响应模式在这种场景下显得力不从心。一个耗时的任务、一次需要用户中途确认的流程,都可能让后端架构陷入状态管理和异步通信的泥潭。
但 Coze Studio 的工作流引擎(Workflow)却优雅地解决了这个问题。它允许开发者通过简单的拖拽,构建出可以暂停、等待用户输入、然后从断点处恢复执行的复杂流程。这背后,是一种截然不同的架构设计哲学。
本文将深入 Coze 的后端源码,解构其工作流引擎的实现,共同探寻以下问题:
- Coze 如何设计其 API,以支持长连接下的异步、流式通信?
- 一个可中断的工作流任务,其请求如何在后端的整洁架构中优雅地流转?
- 引擎内部是如何通过状态持久化与巧妙的异常处理,实现"冻结"与"解冻"执行现场这一核心能力的?
1. 通信的基石:基于 SSE 的可中断流式 API
要实现服务端与客户端之间的持续通信和中途交互,传统的"一问一答"式 HTTP 请求显然行不通。Coze 的架构选择是流式 API ,具体来说,是 SSE (Server-Sent Events)。
这种模式彻底改变了通信模型:
- 传统模式 :
客户端请求 → (长时间等待...可能超时) → 服务端响应最终结果
- Coze 流式模式 :
客户端发起请求,建立长连接
服务端持续推送进度事件 (event: message)
服务端在需要时,抛出中断事件 (event: interrupt)
客户端响应中断,提交用户反馈
服务端接收反馈,恢复执行,继续推送事件
服务端推送结束事件 (event: done)
通过这种方式,服务端掌握了主动权,可以随时向客户端推送进度,并在需要时抛出"中断"事件,优雅地实现了"暂停-恢复"的逻辑。
1.1 核心 API 接口
这套交互模式背后,是两个核心的 API 接口:
-
启动工作流 :
POST /v1/workflow/stream_run
- 功能: 启动一个新的工作流实例,并建立一个 SSE 长连接。
- 响应 : 服务端会通过这个连接持续推送事件,关键类型包括
message
(进度),interrupt
(中断),done
(结束),error
(错误)。
-
恢复工作流 :
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
}
工作原理:
- 🏭 生产者 :工作流在后台执行,每个节点的输出写入
sw
- 📦 缓冲区:消息暂存在大小为 10 的队列中
- 📤 消费者 :上层不断从
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. 实践串联:一次完整交互的生命周期
让我们回到最初的"智能文档助手"案例,用一张图串起整个流程,看看这套架构在实践中是如何运转的。
创建 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)
-
📤 用户点击运行 →
POST /v1/workflow/stream_run
json{ "workflow_id": "doc-assistant-v1", "parameters": { "document": "base64_encoded_content", "output_format": "ppt" } }
-
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")
-
Application 层构造执行配置
goconfig := &vo.ExecuteConfig{ From: vo.FromSpecificVersion, Mode: vo.ExecuteModeRelease, SyncPattern: vo.SyncPatternStream, // 流式执行 WorkflowID: "doc-assistant-v1", Parameters: {"document": "...", "output_format": "ppt"}, }
-
❤️ 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"} ] }
-
🤖 执行 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)
-
❓ 遇到问答节点,触发中断机制
gofunc (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, } }
-
系统保存状态快照并推送中断事件
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)
-
用户提供反馈 →
POST /v1/workflow/stream_resume
json{ "execute_id": "exec_123456", "event_id": "event_789", "user_answer": "总结太简略了,请增加技术细节部分的分析" }
-
系统加载状态并恢复执行
gofunc (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) }
-
重新执行改进的总结
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)