Ling 工作流框架
Ling 是一个基于流式 JSON 数据的异步工作流框架,通过 Adapter(模型适配)、Bot(推理节点)、Tube(数据管道)三大模块,实现多 AI 节点间的高效数据流转和实时分发。
设计思想
核心问题
在复杂 AI 应用中,多个大模型节点需要协同工作:
- BotA 生成大纲 → BotB/C 并行展开各章节
- 某字段输出完成 → 立即触发语音合成(不等待整体完成)
- 所有节点的输出 → 通过单一 Stream 统一分发给客户端
架构概览
图表渲染中…
三大模块职责
| 模块 | 职责 | 核心能力 |
|---|---|---|
| Adapter | 对接不同大模型 API | 统一接口、流式处理、错误重试 |
| Bot | 工作流中的推理节点 | Prompt 管理、状态机、事件发射 |
| Tube | 节点间数据管道 | 数据路由、字段监听、并行分发 |
Adapter 模块
设计目标
屏蔽不同大模型 API 的差异,提供统一的调用接口:
typescript
// adapter/openai.ts
import type { ChatConfig, ChatOptions } from '../types';
export async function getChatCompletions(
config: ChatConfig,
options: ChatOptions = {}
): Promise<ReadableStream> {
const response = await fetch(config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
messages: config.messages,
stream: true,
...options,
}),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.body!;
}typescript
// adapter/coze.ts
export async function getChatCompletions(
config: ChatConfig,
options: ChatOptions = {}
): Promise<ReadableStream> {
const response = await fetch('https://api.coze.cn/v3/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
bot_id: config.botId,
user_id: config.userId,
stream: true,
additional_messages: config.messages,
}),
});
return response.body!;
}Bot 模块
状态机设计
每个 Bot 节点是一个状态机,管理推理生命周期:
typescript
enum WorkState {
INIT = 'init', // 初始化
WORKING = 'chatting', // 推理中
INFERENCE_DONE = 'inference-done', // 推理完成
FINISHED = 'finished', // 全部完成
ERROR = 'error', // 错误
}图表渲染中…
ChatBot 实现
typescript
import EventEmitter from 'node:events';
import { Tube } from '../tube';
import nunjucks from 'nunjucks';
import { getChatCompletions } from '../adapter/openai';
import type { ChatConfig, ChatOptions } from '../types';
export class ChatBot extends EventEmitter {
private prompts: Array<{ role: 'system'; content: string }> = [];
private history: Array<{ role: string; content: string }> = [];
private chatState = WorkState.INIT;
private config: ChatConfig;
private options: ChatOptions;
constructor(
private tube: Tube,
config: ChatConfig,
options: ChatOptions = {}
) {
super();
this.config = { ...config };
this.options = { ...options };
}
get state(): WorkState {
return this.chatState;
}
// 设置系统提示词(支持模板变量)
setPrompt(template: string, data?: Record<string, any>) {
const content = data
? nunjucks.renderString(template, data)
: template;
this.prompts = [{ role: 'system', content }];
return this;
}
// 添加用户消息
addUserMessage(content: string) {
this.history.push({ role: 'user', content });
return this;
}
// 启动推理
async start() {
this.chatState = WorkState.WORKING;
this.emit('state-change', this.chatState);
const messages = [...this.prompts, ...this.history];
try {
const stream = await getChatCompletions(
{ ...this.config, messages },
this.options
);
// 将流数据送入 Tube 进行解析和分发
await this.tube.process(stream);
this.chatState = WorkState.INFERENCE_DONE;
this.emit('state-change', this.chatState);
} catch (error) {
this.chatState = WorkState.ERROR;
this.emit('error', error);
}
}
}Tube 模块
数据管道职责
Tube 是连接 Bot 和输出流的管道,核心职责:
- 消费 Adapter 返回的原始流
- 通过 JSONParser 动态解析 JSON
- 在字段完成时触发
string-resolve事件 - 将增量数据路由到正确的输出位置
typescript
import { JSONParser } from '../parser';
import type { WritableStream } from 'stream/web';
export class Tube {
private parser: JSONParser;
private output: WritableStream;
constructor(output: WritableStream) {
this.parser = new JSONParser();
this.output = output;
this.setupEvents();
}
private setupEvents() {
// 增量数据 → 转发给客户端
this.parser.on('data', ({ uri, delta }) => {
this.writeToOutput({ type: 'data', uri, delta });
});
// 字段完成 → 通知订阅者
this.parser.on('string-resolve', ({ uri, value }) => {
this.emit('field-complete', { uri, value });
});
}
async process(stream: ReadableStream) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) this.parser.feed(content);
} catch { /* skip incomplete */ }
}
}
this.parser.end();
}
private writeToOutput(data: any) {
const writer = this.output.getWriter();
writer.write(`data: ${JSON.stringify(data)}\n\n`);
writer.releaseLock();
}
}工作流编排实战
两级工作流:大纲 + 展开
图表渲染中…
服务端编排代码
typescript
import { ChatBot } from '@bearbobo/ling';
import { Tube } from '@bearbobo/ling';
app.post('/api/generate', async (req, res) => {
const { question } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const tube = new Tube(res);
// 第一级:生成大纲
const outlineBot = new ChatBot(tube, {
endpoint: process.env.LLM_ENDPOINT,
apiKey: process.env.LLM_API_KEY,
model: 'deepseek-chat',
});
outlineBot.setPrompt(`
根据用户问题生成一篇儿童科普文章的大纲。
输出 JSON 格式:
{
"title": "文章标题",
"sections": [
{"title": "章节标题", "keywords": ["关键词"]}
]
}
`);
outlineBot.addUserMessage(question);
// 监听大纲字段完成,触发第二级
tube.on('field-complete', async ({ uri, value }) => {
if (uri.match(/\/sections\/\d+\/title/)) {
// 某个章节标题完成 → 启动展开 Bot
const expandBot = new ChatBot(tube, { /* config */ });
expandBot.setPrompt(`
为儿童撰写关于"${value}"的科普段落,200字左右,语言生动有趣。
`);
expandBot.addUserMessage(`主题:${value}\n原始问题:${question}`);
await expandBot.start();
}
});
await outlineBot.start();
res.end();
});客户端集成
typescript
import { set } from 'jsonuri';
// 安装: pnpm i @bearbobo/ling jsonuri
const eventSource = new EventSource('/api/generate');
const state = reactive({});
eventSource.onmessage = (event) => {
const { type, uri, delta } = JSON.parse(event.data);
if (type === 'data') {
const current = get(state, uri) || '';
set(state, uri, current + delta);
}
};常见问题与陷阱
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 并行 Bot 输出交错 | 多个 Bot 共用一个 Tube | 使用 uri 前缀区分不同 Bot 的输出 |
| 内存泄漏 | 事件监听器未清理 | 工作流结束后 removeAllListeners |
| 模板渲染错误 | nunjucks 变量未定义 | 提供默认值 {% if var %} |
| 流提前关闭 | 客户端断开连接 | 监听 close 事件,中止推理 |