项目介绍:AI 应用编排引擎
提取词
AI 应用编排 | 可视化工作流编辑 | DAG 调度、判环与条件分支 | LLM 节点与 Prompt 变量 | Agent 状态图编排 | 节点注册与公共基类 | Tiptap 变量引用 | SSE 流式执行反馈 | 发布与 API 服务化
项目介绍
这个项目是公司 AI 应用编排平台的核心引擎。用户在画布上拖拽节点、配置条件和 Prompt,引用上游输出,组合业务工作流或 AI 应用流程;编排结果可以发布成 API 服务,供其他系统调用。我负责工作流协议、TypeScript 执行引擎、节点体系与 LLM 节点、Agent 编排接入、变量编辑器,以及 SSE 服务端推送和前端接入,打通从配置到执行再到发布的反馈链路。
实现上,编辑器和引擎使用同一份流程定义,用 DAG 描述节点依赖。执行前通过深度遍历判环,再用 Kahn 算法安排执行顺序。条件分支要跳过未选路径,同时检查公共汇合节点是否还有有效前驱,避免把后面应该继续执行的节点一起排除。
我把节点业务和执行逻辑分开,用注册表按类型查找执行器,计时、变量解析和异常处理放到公共基类里。新增节点主要补充配置和执行器,不用改核心执行流程。
LLM 节点在这一层接入模型调用:节点配置描述模型、温度和输出上限,执行时把 system、user、assistant 三类消息组装成请求,先解析 Prompt 里的上游变量,再把返回文本和 token 消耗写回本次运行的上下文,供下游节点引用。模型经统一接入层调用,节点不关心模型部署在哪里。
需要循环、工具调用和人工中断的智能体流程,用 LangGraph 的状态图编排;确定性的业务流程仍然走自研 DAG。两者按节点能力分工,避免让一个引擎同时承担静态调度和动态状态两种模型。
变量编辑器用 Tiptap 实现,用户选择上游输出后,界面显示为一个不可拆分的标签,保存时转成表达式。执行时再从本次运行的独立上下文里取值,传给下游节点。让显示、保存和执行期取值各有分工。
为了让用户看到流程跑到了哪一步,我在 NestJS 中实现 SSE 推送,并完成前端接入。节点开始、结束、日志和错误会持续传到前端,再通过执行 ID 和节点 ID 对应到本次运行和具体节点。这样用户在发布前就能测试运行,查看每个节点的输入输出、耗时和错误,定位配置或路径问题。发布时把当前流程固化为一个版本快照,对外提供调用地址和调用凭据;发布管理界面、发布协议和流式执行接口的接入由我负责,版本存储与凭据鉴权与后端协同。
简历描述
AI 应用编排引擎全栈开发 / 架构设计
- 技术栈:TypeScript、React、Next.js、React Flow(@xyflow/react)、Tiptap、Tailwind CSS、Radix UI、React Hook Form、Zod、LangChain.js、LangGraph.js、Ollama、NestJS、SSE、Prisma、PostgreSQL、Nx + pnpm、Vitest
- 项目简介:设计并开发了一套企业级 AI 应用编排引擎,支持 DAG 工作流执行、LLM 与 Agent 节点编排和流式执行反馈。
- 工作内容与成果:
- 引擎架构设计:自主设计并实现了基于 DAG(有向无环图)的工作流执行引擎,通过 DFS 实现循环检测,利用 Kahn 算法实现节点拓扑排序,并以子树排除和汇合点入度保护处理条件分支。
- LLM 节点与插件体系:设计基于注册模式(Registry Pattern)的节点扩展系统,实现 LLM 节点的消息组装、Prompt 变量解析与 token 统计,新增节点无需改动调度内核。
- Agent 编排:引入 LangGraph 状态图承载循环、工具调用与中断恢复,与自研 DAG 按场景分工,确定性流程仍由静态图调度。
- 变量编辑器与执行反馈:集成 Tiptap 实现变量标签与
${node.var}协议序列化;在 NestJS 中实现 SSE 流式推送,实时回传节点状态、日志和错误。
可能追问
如何设计一个支持自定义流程的工作流引擎?核心难点是什么?
整体使用 DAG 描述流程,通过 WorkflowDefinition 统一编辑器和引擎的数据结构。节点保存 ID、类型和配置,边用 source、target 表示依赖,条件出口通过 sourceHandle 区分。
引擎拿到整个流程 JSON 后,构建邻接表、反向邻接表和入度表。调度采用 Kahn 算法:先把入度为零的节点入队,每执行一个节点,就减少后继节点的入度,降到零后再入队。比如 A、B 都是 C 的前驱,C 就会排在两者之后。
核心难点主要是环检测、条件分支和数据传递。
环检测使用深度遍历,分别维护全局访问集合和当前路径集合。前者避免重复遍历,后者判断是否回到了当前路径中的节点,回溯时再移出路径。这样可以区分真正的环和两条路径汇合到同一节点的情况。
条件节点执行后返回选中的出口,引擎再排除未选路径。剪枝时要保留公共汇合:假设 A、B 最后都连到 C,选 A 时应跳过 B,但仍然保留 C。我通过反向邻接表检查后继是否还有有效前驱,只有前驱路径全部失效,才继续排除。
数据传递由每次运行新建的执行上下文管理。上游按节点 ID 和变量名保存输出,下游通过 节点ID.变量名的固定表达式取值。
节点能力按需接入:常规业务节点遵循同一套执行器契约,LLM 节点负责模型调用,需要动态循环的流程交给 LangGraph。调度内核只负责顺序和上下文,不关心节点内部是查数据库还是调模型。
LLM 节点在 DAG 引擎里怎么执行?Prompt 变量、流式输出和 token 怎么处理?
LLM 节点和其他节点走同一套契约:实现 doExecute(),由基类的 execute() 包住变量解析、计时、异常捕获和输出写入。区别在于它的输入是一段 Prompt,而不是表单字段。
执行前先解析 Prompt 里的变量引用,把上游输出替换进去;再把 system、user、assistant 三类消息按顺序组装成消息数组,连同模型名、温度和输出上限一起交给模型接入层。节点配置只描述模型和参数,不关心模型部署在本地还是云端。
返回后把文本写进上下文,同时记录 token 消耗和耗时,供下游节点引用和日志面板展示。token 按字符数估算,中英文分开计算,作为成本参考而不是账单口径。
流式输出分两层:LLM 调用内部的增量回调是一层,工作流层按节点推送的 SSE 是另一层。工作流层以节点为单位,节点结束后把聚合文本、token 消耗和耗时一次性回传给前端;如果要在界面上做逐字输出,就要把增量片段透传成独立的 token 事件,并和节点事件共用 executionId 与 nodeId。两层混用会让事件协议和重连处理变复杂,所以先按节点粒度把执行状态讲清楚。
已经有 LangGraph,为什么还要自研 DAG 引擎?两者怎么分工?
自研引擎解决的是确定性编排:流程图是静态的,执行顺序由拓扑排序决定,每一步都能在画布上对应到具体节点,日志、断点和排查都很直接,适合审批、数据处理这类路径确定的流程。
LangGraph 解决的是状态驱动的流程:节点是状态转换函数,边是条件跳转,原生支持循环、检查点、中断恢复和子图嵌套,适合需要多轮推理、工具调用和人工介入的 Agent。
项目中两者按节点能力分工:常规链路走自研引擎,需要循环或工具调用的智能体流程交给 LangGraph,用节点封装接入。代价是两套模型并存,要在执行协议和上下文上做对齐;收益是确定性部分保持可控,探索性部分不必自己重造循环和状态管理。
编排好的流程怎么发布成 API 服务?
发布把当前流程固化成版本快照:提交时检查开始、结束节点是否齐全,通过后写入发布记录并生成新的版本号,应用切换到该版本;版本号与版本记录由服务端维护,此后再改草稿不影响线上调用的版本。
对外调用使用应用维度的调用凭据,请求进来先鉴权再解析输入,执行时复用同一套引擎和上下文,节点事件通过 SSE 推给调用方,最后返回整体结果、耗时和 token 统计。
我负责发布管理界面、发布协议和流式执行接口的接入;版本存储、凭据签发与鉴权与后端协同。
项目中的变量编辑器(支持 ${} 高亮)是如何实现的?
普通的 textarea 只能显示纯文本,不能把其中的变量单独渲染成高亮标签,所以项目使用 Tiptap,并通过插件机制实现自定义节点。
用户输入 / 后,Tiptap 自带的 Suggestion 插件展示上游节点的可用变量。选中后插入变量标签,标签显示节点和变量的可读名称,内部保存节点 ID 和变量名组合。这个组合作为整体被编辑,避免被误删其中一部分。
保存时,将普通文本和变量节点序列化为包含变量表达式的纯文本。回显时,用正则识别表达式,取出节点 ID 和变量名,再结合节点与输出定义中的名称,还原成自定义的节点标签。
如何设计支持插件化扩展的节点系统?
我用注册模式维护节点类型与执行器的对应关系,由不同执行器提供具体的执行策略。调度引擎通过统一契约调用节点,新增节点时主要补充配置和执行器。
我定义NodeExecutor 接口约定执行入口与结果格式,实现抽象基类BaseNodeExecutor 集中处理日志、变量解析和计时等公共步骤。具体执行器实现 doExecute() 抽象方法,由基类的 execute() 包装调用,公共能力和节点业务分开维护。
然后使用 NodeRegistry 在项目中以节点类型和执行器实例的 Key value 形式注册节点执行器。启动时完成注册,运行时引擎按照 type 查找并调用。
比如新增 LLM 节点,先定义模型、Prompt、温度和输出上限等配置 Schema,再编写继承基类的执行器,最后注册 llm 类型。执行过程中遇到模型节点,就会直接找到对应执行器运行,调度内核不改一行代码。
如何实现工作流执行过程的流式响应,并实时推送到前端?
工作流会执行多个节点,所以使用 SSE 持续推送过程信息,让前端能显示当前进度,而不只是等待最后的成功或失败。
我在 NestJS 中实现 SSE 服务端推送,通过 Controller 返回事件流,把执行事件编码成 data: ...\n\n 格式持续写入响应。前端可以通过 EventSource 接收,也可以使用 fetch 读取响应流,再通过 TextDecoder 逐块解码。事件协议与后端共同制定,SSE 服务端推送、前端接入和日志界面由我实现。
协议需要区分工作流开始、节点开始、节点结束、日志、整体结束和错误等事件。executionId 标识本次运行,节点事件再通过 nodeId 对应到画布中的节点。前端根据事件类型更新状态,展示输入、输出、耗时和错误,让用户可以查看整次执行,也可以定位到具体节点。